拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 DAC固定电压输出实战:从CubeMX配置到HAL库校准

我手头正好在做一块板子的模拟前端调试需要一个干净、可调的直流偏置电压给后级运放。当时手边没有可调电源也不想用电阻分压加运放跟随凑合于是直接用了STM32C552自带的DAC把PA4脚配置成固定电压输出一个万用表贴在输出端代码改一个数电压就跟着变整个过程比搭分立电路快得多。这篇就把这次DAC固定电压输出的完整思路写下来包括外设结构、CubeMX配置、HAL库代码、实测校准以及后续从固定电压扩展到正弦波输出时需要注意的东西。不管是刚接触STM32的新手还是想把DAC用在实际项目里的人都可以参考一下。1. 为什么偏偏要用DAC输出一个固定电压先说清楚应用场景。DAC固定电压输出并不是教科书里的抽象练习它在实际电路里出现的频率非常高。我这次的需求是给一个精密运放提供可调偏置用来把双极性小信号抬升到ADC可以采样的单极性范围内。类似的需求还包括传感器电桥的激励偏置或者恒压源替代方案。比较器阈值电压的动态调节尤其是需要程序在运行中反复修改阈值的时候。压控振荡器、压控增益放大器的调谐电压。显示器VCOM电压、触摸屏驱动电压等需要出厂校准的场景。模拟开关、模拟多路复用器的偏置点设定。这些场景的共同特点是电压值需要稳定、噪声低、可调整而且最好由MCU直接控制方便生产校准和在线调试。1.1 为什么不用电阻分压、LDO或者PWM滤波很多人第一反应是“固定电压而已用电阻分压不就行了”。电阻分压确实便宜简单但问题在于一旦电路板上要出多个不同电压就得配多个电阻组合BOM成本上去了而且分压值受电阻精度和温漂影响。更麻烦的是分压网络的输出阻抗偏高带负载能力差后面必须再接一级运放缓冲整个电路面积反而大了。PWM加RC滤波也是常见做法但PWM输出本质上是一个方波经过RC滤波后仍然会有残余纹波纹波大小取决于RC截止频率和PWM分辨率。如果需要干净的直流电压RC阶数要加高响应时间变慢调一次电压要等几十毫秒才能稳定。在一些对纹波敏感的场景里PWM方案并不合适。LDO或者基准芯片只能给固定值想通过程序改电压就得用数字电位器或DAC。数字电位器精度和温度稳定性一般不如DAC而且分辨率通常只有7位、8位左右做精细校准时往往不够用。DAC在这种场景下的优势是单芯片解决输出直接是模拟电压带内部缓冲器可以驱动一定负载分辨率能做到12位输出电压只和参考电压及写入的数字值有关程序控制起来非常直接。1.2 固定电压输出真正要关注的三个指标做固定电压输出表面上是“写一个数出一个电压”实际上要关注的不只是“电压对不对”还有三件事第一是分辨率也就是最小能改变的电压台阶。对于12位DAC电压台阶等于参考电压除以4096。如果参考电压是3.3V一个LSB大约是0.8mV。这个精度能否满足需求决定了项目需不需要外部高精度基准。第二是建立时间也就是写入数值后输出端电压稳定到目标值附近需要多长时间。固定电压场景对建立时间要求不高但如果是后面要讲的波形输出这个参数就很重要。第三是负载能力。STM32的DAC内部带输出缓冲器时可以直接驱动一些低功耗负载但规矩要看数据手册一般缓冲器输出能力有限带重负载时电压会跌落。不带缓冲器时输出阻抗高达几十千欧基本只能接高阻抗的运放输入端。2. STM32C552的DAC外设结构和关键参数在代码之前先花点时间把C552的DAC外设结构弄清楚。很多人配置DAC失败往往不是库函数调错了而是对外设结构理解不到位尤其是参考电压和缓冲器这两块。STM32C552属于STM32C5系列内核是Cortex-M33主频最高可以跑到250MHz。这个系列的DAC外设和以往STM32家族的基本框架一脉相承但细节上有一些差异需要留意。2.1 DAC整体框架双通道、12位、缓冲输出C552的DAC模块有两个独立输出通道DAC1_OUT1默认映射到PA4引脚DAC1_OUT2默认映射到PA5引脚。每个通道都支持12位分辨率也可以配置为8位模式用法上12位右对齐最常用。两个通道可以单独工作也可以同步更新这在需要同时输出两路成对电压的场景里很有用。本次固定电压只用了通道1也就是PA4引脚。DAC内部结构可以粗略理解成三段输入数据寄存器DHR、转换逻辑、输出级。用户往DHR寄存器写入数字值转换逻辑将该值按参考电压转换成模拟电压通过输出级送到引脚。输出级可以选择是否启用内部缓冲放大器。启用缓冲后输出阻抗很低能驱动一定负载不启用时输出引脚直接接在电阻网络上输出阻抗比较高通常需要外接运放跟随。另外C552的DAC支持采样保持模式Sample and Hold。这个模式在低功耗应用里很实用它通过周期性刷新保持电容上的电压来降低静态功耗。对固定电压输出场景来说一般不需要开启默认关闭即可开启反而会引入刷新带来的微小纹波。2.2 参考电压的来源问题DAC的输出电压是数字值相对于参考电压的比例结果所以参考电压几乎决定了DAC输出的绝对精度。STM32C552的DAC参考电压来自VREF引脚。这个引脚在不同封装里连接方式不完全一样部分封装VREF在芯片内部已经接到VDDA上部分封装是独立引脚需要外部接入基准电压源。使用C552时建议第一步就查数据手册的引脚定义确认VREF是不是独立引脚。如果是独立引脚外围电路需要加去耦电容并且最好和数字电源的走线分开。如果芯片封装内部直接把VREF连到VDDA那么VDDA的电源质量就直接影响DAC输出精度。C552还提供内部参考基准VREFINT但这个基准主要给ADC做校准用。DAC本身不能直接把VREFINT作为参考电压这一点容易搞混。DAC的参考电压仍然是VREF引脚上的电压所谓“内部基准”指的是一个固定的内部电压值可以用来校准ADC读数而不是给DAC当参考源。2.3 输出缓冲器带来的头部和底部死区这个点非常容易被忽视。很多人启用输出缓冲后发现DAC无法输出接近0V或接近参考电压的电压值以为芯片坏了。实际上这是输出缓冲器的固有特性轨到轨运算放大器在靠近电源轨的地方会进入饱和区线性度变差输出电压会有一段不可控的死区。具体表现是12位模式下设置值为0时输出可能还有几毫伏到几十毫伏的残留设置值接近4095时输出电压达不到参考电压会低一点。不同芯片的死区范围有差异一般在几十毫伏量级。这个特性在固定电压场景影响尚可但如果DAC输出需要覆盖近轨范围比如0到3.3V全程可调就需要通过外部电路扩展或者接受边缘的非线性。3. CubeMX图形化配置中容易踩的5个细节现在进入实际操作。用STM32CubeMX生成工程是STM32开发的主流方式STM32C552同样支持。图形化配置并不能保证整个工程的正确性尤其是在DAC这种和模拟外设相关的配置上有几个细节如果忽略代码写完也跑不出预期电压。3.1 时钟树和DAC外设时钟STM32C552的DAC挂在APB1总线上。CubeMX里默认生成的RCC配置一般不会漏掉DAC时钟但如果用标准外设库或者自己写寄存器很容易忘记开启DAC时钟导致所有DAC寄存器写入都无效。在使用CubeMX时建议在RCC配置里确认时钟源和系统时钟频率是否符合预期。C552最高主频250MHz但这意味着需要合理的时钟树配置。DAC本身对时钟频率不敏感因为固定电压输出走的是软件触发但系统时钟错了会影响串口调试和定时器触发排查问题时容易绕弯。3.2 DAC参数设置Trigger、Output Buffer、Sample and HoldCubeMX中DAC配置界面主要关注三个选项Trigger触发方式。固定电压输出选择None即可表示不需要外部事件触发写入数据后立刻更新输出。如果选择定时器触发DAC转换会等到定时器更新事件到达才执行适合后续波形输出场景但固定电压场景没必要。Output Buffer输出缓冲。一般选择Enable。启用后PA4引脚输出阻抗低可以直接驱动后级电路的大输入阻抗负载。如果后级接的是高阻抗运放输入端启用缓冲通常没有坏处。若需要输出极低的电压接近0V要考虑缓冲器底部死区的影响此时可以试试Disable并外接运放。Sample and Hold采样保持。固定电压场景推荐Disable。开启后DAC会周期性刷新输出电容虽然降低了功耗但输出端会有微小的电压波动对要求稳定直流的场景不友好。3.3 引脚冲突与重映射检查STM32引脚复用功能很多PA4和PA5不仅有DAC输出还可能是SPI片选、I2C地址线或者比较器输入。CubeMX里配置DAC时系统默认会在PA4/PA5上打勾但如果有其他外设也占用了同一引脚CubeMX会给出冲突提示。实际项目中更隐蔽的问题是开发板上DAC引脚可能被板载器件占用。比如某些评估板把PA4接到板载运算放大器或者ADC引脚上外部又可能连了RC滤波网络导致实测电压偏低或动态响应变慢。用最小系统板或者自己做板子时要确认PA4/PA5引脚挂载的外部器件不会影响DAC输出。3.4 模拟供电引脚的处理STM32这类MCU往往有独立的模拟电源引脚VDDA和模拟地VSSA还有参考电压引脚VREF。CubeMX不会自动帮你接好这些引脚它们必须在硬件上处理好。如果VDDA没有接好或者滤波电容缺失DAC输出会出现噪声变大、读数跳变的问题。通常VDDA和VDD之间需要接一个磁珠或几欧姆电阻配合1uF和100nF两级去耦电容。VREF引脚需要单独加去耦电容一般推荐1uF以上如果追求更高精度可以用外部基准芯片供VREF。3.5 生成代码时的工程组织CubeMX生成代码后DAC初始化函数会放在main.c或者dac.c中。默认生成的MX_DAC1_Init函数只完成基本配置代码生成结束之后需要自己添加启动DAC和写入电压的代码。在生成代码前建议在Project Manager里勾选“Generate peripheral initialization as a pair of .c/.h files per peripheral”这样DAC相关代码会独立生成到dac.c和dac.h里工程结构更清晰后续修改也方便。4. HAL库代码实现让PA4引脚吐出指定电压CubeMX生成工程之后剩下的代码量其实很少。核心就三步初始化DAC、启动通道、写入数值。4.1 初始化流程和HAL函数详解CubeMX生成的DAC初始化函数大概是这样的static void MX_DAC1_Init(void) { DAC_ChannelConfTypeDef sConfig {0}; hdac1.Instance DAC1; if (HAL_DAC_Init(hdac1) ! HAL_OK) { Error_Handler(); } sConfig.DAC_SampleAndHold DAC_SAMPLEANDHOLD_DISABLE; sConfig.DAC_Trigger DAC_TRIGGER_NONE; sConfig.DAC_OutputBuffer DAC_OUTPUTBUFFER_ENABLE; sConfig.DAC_ConnectOnChipPeripheral DAC_CHIPCONNECT_DISABLE; sConfig.DAC_UserTrimming DAC_TRIMMING_FACTORY; if (HAL_DAC_ConfigChannel(hdac1, sConfig, DAC_CHANNEL_1) ! HAL_OK) { Error_Handler(); } }逐个看这几个参数DAC_SampleAndHold设置为DISABLE前面说过固定电压场景不需要低功耗刷新。DAC_Trigger设置为NONE软件触发模式调用HAL_DAC_SetValue后数据立刻生效。DAC_OutputBuffer设置为ENABLE使能内部缓冲让PA4具备低阻抗输出能力。DAC_ConnectOnChipPeripheral设置为DISABLE这个选项控制DAC输出是否连接到片内外设比如比较器或运放固定电压输出场景不需要内部连接。DAC_UserTrimming设置为FACTORY使用出厂校准值。C552的DAC内部有校准寄存器出厂时写入了微调值一般情况下使用工厂校准即可。初始化完成后还需要开启DAC时钟对应的GPIO引脚CubeMX会自动生成引脚配置代码一般不需要手动改。4.2 启动DAC通道与写入电压初始化完成后在main函数中调用启动和写入函数HAL_DAC_Start(hdac1, DAC_CHANNEL_1); uint16_t voltage_code 2048; HAL_DAC_SetValue(hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, voltage_code);这个流程的关键点在于HAL_DAC_SetValue只是把数字值写入DHR寄存器如果DAC通道没有启动输出引脚仍然是高阻状态量不到电压。固定电压场景只需要设置一次DAC会一直保持这个输出值直到下次调用HAL_DAC_SetValue写入新值或者调用HAL_DAC_Stop停止通道。另外12位模式下数据范围是0到4095如果传入大于4095的数值HAL库会按16位数据截断处理输出异常所以建议在调用前加一层参数校验。4.3 输出电压的计算和公式陷阱已知参考电压和数字值时输出电压的理论值怎么算这里有一个容易混淆的点。STM32的DAC转换关系可以理解为数字值从0递增到最大值的4095时输出从0逐渐升到参考电压。按这个理解输出电压公式是Vout Vref × (DOR / 4095)但也有不少资料写成除以4096。差异在哪里如果按4096计算满量程输出是Vref × 4095 / 4096比参考电压小一个LSB。不同STM32系列的数据手册写法不完全一致有的直接给DOR/4095有的给DOR/4096实际使用中需要用万用表验证一下自己手上芯片的行为。无论公式按4095还是4096算12位DAC的最小电压台阶都是Vref/4096约等于0.244mVVref3.3V时严格说是0.001V左右算一下3.3/40960.000805664V约0.806mV。要输出精确的电压需要反向换算voltage_code (uint16_t)(target_voltage / Vref × 4095)比如想在Vref3.3V时输出1.65V则voltage_code (1.65 / 3.3) × 4095 2047.5四舍五入取2048。输出理论电压 2048 / 4095 × 3.3约1.6498V。实际项目和理论计算之间总会差一点因为Vref引脚上实际电压不是精确的3.3V万用表测出来可能是3.294V甚至更低所以严格计算时要使用实测的Vref值而不是标称值。4.4 一个可以直接跑的最小示例完整的最小测试代码结构如下#include main.h DAC_HandleTypeDef hdac1; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DAC1_Init(); HAL_DAC_Start(hdac1, DAC_CHANNEL_1); // 假设Vref实测3.3V输出1.65V uint16_t code (uint16_t)((1.65f / 3.3f) * 4095); HAL_DAC_SetValue(hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, code); while (1) { // 固定电压输出场景主循环可以什么都不做 } }编译下载后万用表红表笔接PA4黑表笔接GND应该能测到接近1.65V的电压。5. 实测结果、误差来源与软件校准代码跑通之后真正有价值的环节是实测和分析误差。我这次用的板子VDDA实测是3.294V万用表是普通四位半手持表下面把不同数字值下的实测结果列出来写入数字值理论电压按Vref3.294V计算实测电压误差00mV3.2mV3.2mV512411.75mV413.2mV1.45mV1024823.5mV825.7mV2.2mV20481647.0mV1649.5mV2.5mV30722470.5mV2473.8mV3.3mV40953294.0mV3286.4mV-7.6mV从数据可以看到三个明显特征数字值较小的时候输出偏大这是因为缓冲器有轻微失调数字值接近满量程时输出偏低这是缓冲器接近正电源轨时的输出能力下降全程存在一个比较一致的偏移约2mV左右。5.1 误差来源拆解固定电压输出的误差源主要有四类参考电压误差。VREF实际值和标称值之间的偏差是所有误差里占比最大的。如果外接基准芯片这一项可以做到很低如果直接使用VDDA那么VDDA的负载调整率、温漂都会影响DAC输出。DAC本身的失调和增益误差。DAC内部转换网络和缓冲放大器都存在失调电压典型值在几个毫伏量级。数据手册里一般会给出Voffset和Gain error指标12位DAC的线性度通常不差但输入端到输出端的绝对误差需要校准。缓冲器非理想特性。缓冲器在轨到轨范围的边缘会出现非线性输出越接近电源轨误差越大。实测数据里满量程偏低7.6mV就是这种表现。万用表测量误差和引线压降。万用表本身有精度等级普通三位半表测直流电压的误差是±0.5%量级引线接触电阻、表笔和排针之间的接触电阻也会引入微小电压降尤其在电流没有明显流动的情况下接触电阻影响较小但不能忽略。5.2 软件两点校准法如果项目要求DAC输出电压误差在2mV以内程序里可以做软件补偿。思路是测量两个点的实际电压构建一条线性校准曲线将目标电压反算成数字值时套用这条曲线。假设在数字值为code_low时实测电压为v_low数字值为code_high时实测电压为v_high那么实际输出电压V与数字值code之间的线性关系近似为code code_low (V - v_low) × (code_high - code_low) / (v_high - v_low)实际代码中可以这样实现#define CODE_LOW 512 #define CODE_HIGH 3072 float v_low 0.4132f; float v_high 2.4738f; uint16_t voltage_to_code(float target_voltage) { float code_f (float)CODE_LOW (target_voltage - v_low) * ((float)(CODE_HIGH - CODE_LOW) / (v_high - v_low)); if (code_f 0.0f) code_f 0.0f; if (code_f 4095.0f) code_f 4095.0f; return (uint16_t)(code_f 0.5f); }这种方法对固定电压场景非常实用因为校准只需要掉电前做一次把v_low和v_high存到Flash里每次上电直接用。对于批量生产可以在产线上自动测量这两个点并写入每台设备的Flash。5.3 为什么说不要迷信12位分辨率12位分辨率常常让人误以为DAC输出的电压精度也有12位。实际上分辨率只是“最小可改变量”不代表“绝对精度”。输出端的实际电压精度由VREF精度、DAC线性度、缓冲器失调和温漂共同决定通常综合精度在±10mV量级已经不错。如果项目真的需要万分之一以上的电压精度光靠STM32内部DAC是不够的需要外部基准芯片配合外加精细校准。反过来如果只是给比较器提供一个阈值电压3.3V下0.8mV的分辨率已经远高于需求实际误差水平完全可以接受。6. 从固定电压到任意波形正弦波输出扩展固定电压跑通之后稍微改一下就能输出任意波形。这里简单讲清楚原理和路径方便后续扩展。6.1 定时器触发加DMA加查表的基本结构固定电压是“设置一次保持不变”波形输出则是“按固定节奏不断更新DAC数值”。核心思路是在内存中准备一个正弦波查表数组用定时器周期性触发DAC转换同时用DMA把查表数据搬运到DHR寄存器。CubeMX里需要做三处修改定时器配置时基选择一种触发模式连到DAC。STM32的定时器触发DAC一般选择TIM6或TIM7配置好更新事件后在DAC配置中将Trigger改为对应定时器。DAC的Trigger不再选None而是选择对应的定时器触发源。这样每个定时器更新周期DAC会自动从DHR寄存器加载新数据。DMA配置为循环模式数据从内存数组搬运到DAC1的DHR12R1寄存器地址搬运方向是内存到外设数据宽度可以是半字。代码上需要增加一个正弦波表#define SIN_TABLE_SIZE 256 uint16_t sin_table[SIN_TABLE_SIZE]; void build_sin_table(void) { for (int i 0; i SIN_TABLE_SIZE; i) { float rad 2.0f * 3.1415926f * i / SIN_TABLE_SIZE; float value (sinf(rad) 1.0f) * 0.5f * 4095.0f; sin_table[i] (uint16_t)value; } }启动方式build_sin_table(); HAL_DAC_Start_DMA(hdac1, DAC_CHANNEL_1, (uint32_t*)sin_table, SIN_TABLE_SIZE, DAC_ALIGN_12B_R);这里一个容易忽略的细节是HAL_DMA配置的数据宽度要和DAC寄存器对齐HAL_DAC_Start_DMA函数的地址参数实际上接收的是uint32_t指针但数组元素是uint16_t需要注意指针类型转换和DMA数据宽度的匹配否则波形会出现奇偶错位。定时器频率和查表点数的关系决定了正弦波频率f_sin f_timer / SIN_TABLE_SIZE如果需要1kHz正弦波查表256点则定时器触发频率就是256kHz定时器分频和装载值按这个目标计算即可。6.2 波形输出时输出缓冲器的意义波形输出场景里输出缓冲器显得更加重要。DAC内部转换网络的输出阻抗很高如果直接驱动外部的滤波电路或负载波形幅度会变化甚至出现削顶。启用缓冲后输出阻抗低波形保真度好得多。同时波形输出时DAC输出电压的实际速度受限于缓冲器的压摆率和建立时间C552的DAC建立时间在us量级输出几十kHz以内的波形没有问题。如果要更高的输出频率就需要查表点数更少、触发频率更高或者考虑外部高速DAC芯片。6.3 从固定电压切换到波形的注意事项如果项目需要在固定电压和波形输出之间切换要注意几个问题HAL_DAC_Stop和HAL_DAC_Stop_DMA是直接停止通道停止后引脚进入高阻态。如果后级电路由DAC输出供电停止会导致电压跌落可能需要软件先把DAC设置成一个安全值再停止。从固定电压模式切到波形模式时需要重新调用HAL_DAC_Start_DMA同时确认原来的固定电压设置不会干扰DMA写入。两种模式本质上都在操作同一个DHR寄存器DMA模式下不能再调用HAL_DAC_SetValue否则下次定时器触发会把DMA搬运的值覆盖掉。7. 配套PCB布局影响DAC精度的3个关键要点如果只是在自己搭的板子上验证功能Layout问题不明显。一旦把DAC固定电压输出放到实际PCB上布局布线就会直接影响输出精度和稳定性。以下三个要点是我几次改板后总结出的教训。7.1 模拟电源和参考地去耦时钟线远离DAC输出第一件要做的事就是分清模拟域和数字域。C552的VDDA引脚、VREF引脚、VSSA引脚都算是模拟域它们需要独立的去耦路径。VREF旁边放一颗1uF陶瓷电容再就近并联一颗100nFVDDA和VSSA之间同样放一组去耦电容。时钟线是PCB上噪声最大的来源之一。HSE晶振引脚、时钟输出引脚、SPI时钟线如果从DAC输出走线附近穿过高频信号会通过寄生电容耦合到DAC输出走线上导致输出直流电压上叠加毫伏级的噪声。实测中这个问题非常隐蔽用万用表看不出问题但用示波器看时DAC输出上能看到和时钟同频的毛刺。避免方法就是让DAC输出走线远离所有时钟线和高频数字信号线必要时在DAC输出走线两侧铺地铜屏蔽。7.2 DAC输出走线的地回路DAC输出是单端信号信号电流从PA4引脚流经负载后回到系统地。如果DAC输出走线比较长而回流地平面被割裂回流电流就要绕很远的路形成一个较大的环路环路面积越大越容易拾取电磁干扰。建议DAC输出走线尽量短而直走线下方保证完整的地平面不要在输出走线中间打穿地平面的过孔。如果输出信号需要跨越分割区域建议在跨越处补接缝合电容或用0欧电阻连接两个地平面降低回流绕路。7.3 接地分割策略什么时候真正需要很多工程师一见到模拟电路就习惯性地把PCB地平面切割成模拟地和数字地但如果处理不当切割后的地平面反而会造成更大的环路面积让问题更严重。对于DAC固定电压输出这种场景真正的实践经验是小系统、单点参考地的情况下优先使用完整地平面而不是刻意分割。只要确保模拟信号不要跨越数字电路的强电流路径VDDA/VSSA去耦良好输出走线远离干扰源完整地平面的表现通常优于分割地平面。如果板子上有高精度ADC同时工作而且ADC模拟供电和DAC共用同一个VDDA域可以考虑把模拟部分和数字部分在地平面上做一个局部隔断但模拟地和数字地必须在某个单点相连通常是芯片下方的VSSA引脚处。盲目分隔只会带来回流问题。最后再分享一个调试技巧固定电压输出调试时,我习惯先在代码里写一个工件常量,比如2048,然后用示波器看PA4引脚的电平。如果电压正确但有波动,先排除万用笔接触问题,再用示波器看VDDA和VREF上的纹波,往往问题的根源就在这两个引脚上。给VDDA和VREF各加一颗较大的电解电容(比如10uF)并联原来的去耦电容,很多时候就能解决输出缓慢漂移的问题。如果手头有高精度万用表,校准时的v_low和v_high点不要选在最靠近0V和满量程的位置,因为缓冲器在轨边界线性度差,选10%~90%范围内的点校准效果更好。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门