
搞嵌入式这几年我越来越觉得“手里有什么就用好什么”才是真本事。项目中经常遇到这种尴尬芯片选型时没注意做到一半发现需要输出模拟量结果手头这颗STM32根本没有DAC外设或者DAC通道就一两路项目却要求同时输出两路甚至三路独立电压。这个时候STM32 PWM DAC这个组合就派上用场了——用定时器输出PWM再接一级RC滤波电路把方波的平均值变成稳定模拟电压。成本几乎为零代码也不复杂只要参数算对精度可以做到0.1%以内非常适合做传感器偏置、音频输出、调速控制参考电压这类场景。这篇文章我按自己实际做过的项目流程来讲先从原理上搞清楚PWM为什么能当DAC用再手把手算RC滤波参数然后给一份靠谱的元件选型指南最后是STM32的软件配置和踩坑记录。无论你是刚学STM32的新手还是被“没有DAC”坑过的老手照着这套思路走都能做出能用的模拟输出。1. 方案评估为什么拿PWM去顶替DAC1.1 什么时候需要这种“自制DAC”先说结论PWMRC这套方案的定位是“够用就好”的模拟输出而不是跟高性能DAC芯片硬刚。实际项目里最常见的三种触发场景是这样的。第一芯片没有DAC外设。很多入门级STM32型号确实不带DAC或者带DAC但通道数很少你可能在选型时没注意等到要输出模拟量才发现硬件不支持。第二通道数不够用。哪怕芯片带两路DAC项目里要出三路、四路可调电压时DAC就不够了而PWM通道几乎每个定时器都有好几个多路复用的成本低得多。第三成本敏感。外置一颗12位SPI接口的DAC芯片便宜的也要两三块钱加上PCB面积和采购周期对量产项目来说都是负担。当然PWM DAC也不是万能的。它的输出带宽很低动态响应慢纹波没办法完全消除。但在几百赫兹以内的缓慢变化信号里它可以顶替大多数DAC应用场景。1.2 PWM当DAC的原理PWM信号本身是数字方波电压只有高和低两个状态为什么会变成模拟量答案在平均值里。一个周期T内PWM高电平持续时间为D×TD是占空比0~1之间低电平持续时间为(1-D)×T。如果不做任何处理信号在VCC和0V之间来回跳平均值等于D×VCC。问题在于这个平均值前面叠加了一堆高频分量。RC低通滤波电路做的事情就是把这些高频分量滤掉留下直流分量。滤波后的输出电压就近似等于D×VCC。举个例子VCC3.3V占空比设为50%滤波后输出约1.65V占空比设为25%输出约0.825V。这就实现了数字到模拟的转换。这里有一个关键约束分辨率和PWM频率是互相拉扯的。PWM的占空比调节级数由定时器的自动重载值ARR决定ARR越大分辨率越高但PWM频率越低因为F_PWM F_Timer / (ARR1)假设定时器计数频率是72MHz如果配置成8位分辨率ARR255PWM频率最高约281kHz如果配置成10位分辨率ARR1023PWM频率只有约70kHz如果贪心要12位分辨率ARR4095PWM频率就掉到约17.6kHz。所以不是说想要多少位精度都行得看你的应用能容忍多低的PWM频率或者你的定时器时钟能拉多高。这就是PWM DAC最核心的“分辨率-频率权衡”。1.3 三种方案横向对比我整理了一个对比表方便你根据项目情况快速判断对比项内置DACPWMRC外置SPI/I2C DAC成本免费几毛钱电阻电容几块到几十块分辨率12位左右8~12位受时钟限制12~16位输出带宽较高可输出音频级信号低受RC截止频率限制中高通道扩展1~2路看定时器通道数量可轻松扩展按芯片规格CPU占用低极低设置好占空比后不用管需要通信开销纹波小有需要RC参数压制极小从表里能看出PWMRC最大的卖点就是成本和通道数。它适合的信号类型是缓慢变化的直流电压比如可调基准、电机堵转电流保护阈值、传感器零点偏置。如果你要做高保真音频输出或者对建立时间有严格要求的快速信号还是老老实实上真DAC或者DDS芯片别在PWM上死磕。2. RC低通滤波器参数计算与速度权衡2.1 看频谱PWM不是“方波平均”那么理想很多人以为给PWM后面挂个电容就完事了实际输出跟想要的差很远原因在于没有从频域去理解滤波。PWM方波可以用傅里叶级数展开包含一个直流分量就是D×VCC以及基波和谐波分量。基波频率就是PWM频率幅度跟占空比有关2次、3次谐波幅度递减但依然存在。RC低通滤波器的作用是让直流分量顺利通过把频率在截止频率以上的分量衰减掉。所以RC滤波器的设计目标很明确截止频率要远低于PWM基波频率同时又要足够高否则有用信号的变化也会被滤掉。这个矛盾是PWM DAC精度的根本约束。2.2 截止频率、纹波、响应时间的三方权衡RC一阶低通滤波器的截止频率公式fc 1 / (2πRC)截止频率决定了三个关键指标。第一是纹波截止频率越低对PWM高频分量的衰减越大输出纹波越小。第二是响应速度RC充放电时间常数τRC输出电压从当前值变化到新目标值大约需要3τ到5τ这个时间决定了你能输出的信号最高频率。第三是输出阻抗RC电路从R端看进去输出阻抗在高频下接近R如果后级负载阻抗不够大会分压导致输出不准。纹波可以用一个近似公式估算Vripple ≈ VCC × D × (1-D) / (f × R × C)这个公式在D0.5时纹波最大。举个实际例子VCC3.3VD50%PWM频率f20kHzR1kΩC10μF那么Vripple 3.3 × 0.25 / (20000 × 1000 × 10e-6) ≈ 41mV41mV的纹波对应8位ADC大约是LSE的一半多对于很多应用来说太大了。把PWM频率提高到100kHz同样参数下纹波可以降到8.25mV左右。或者把RC乘积加大到R10kΩ、C10μF纹波也能压下来但响应时间会变长5τ需要0.5秒太慢了。这就是设计上真正的难点阻尼纹波又要保持足够的响应速度还要保证后级电路能正常驱动。实际项目里通常的做法是选择适当的PWM频率然后让RC截止频率大概落在PWM频率的1/10到1/100之间再把纹波代入公式核算不满意就微调参数。2.3 一阶、二阶滤波器怎么选一阶RC是最基本的方案优点是好算、好调试、元件少。缺点是高频衰减斜率只有20dB/dec意味着你要想在PWM频率上压掉纹波截止频率就得取得很低于是响应变得很慢。二阶RC是在一阶后面再串一级RC衰减斜率变成40dB/dec同样的截止频率下对高频分量衰减强得多。比如一阶到PWM频率处可能只剩20dB衰减二阶能到40dB以上。代价是电路多一组元件输出阻抗更高计算稍微复杂一点而且要注意两级之间可能产生的谐振峰。我的经验是如果应用信号变化很慢比如几赫兹以下的直流电压调节一阶完全够用如果需要稍微有些响应速度或者纹波要求比较苛刻就用二阶。还有一种做法是在RC后面加运放跟着既隔离负载对滤波电路的影响又能获得更低的输出阻抗这是后面要说的完整形态。3. 元件选型指南与硬件设计细节3.1 电阻精度、温漂、额定功率电阻选型很多人不重视随便从抽屉里抓一个。但在PWM DAC里R的精度直接影响截止频率和分压精度建议至少用1%精度金属膜电阻。温漂方面普通贴片厚膜电阻温度系数大约±100ppm/°C到±200ppm/°C意味着温度变化50°C电阻值可能漂1%对控制类应用来说这个漂移太大了。要求高时选薄膜电阻TCR可以做到±25ppm/°C甚至更好。成本当然会高一些但比起外置DAC芯片还是便宜得多。额定功率也不能忽略。RC电路中电阻两端承受的是PWM方波峰值电压几乎等于VCC。以VCC3.3V、R220Ω来计算瞬时功耗约50mW选0603封装的电阻额定功率100mW已经有点紧张最好用0805或更大。如果R取1kΩ以上功耗就很小了不必过度担心。另外电阻阻值也不要取得太大否则输出阻抗高稍微带一点负载电压就塌了。3.2 电容介质类型和容值选择电容这块坑最多我踩得也最深。第一坑是电容材质。MLCC陶瓷电容常见的有X7R、X5R、C0GNP0等。C0G温度特性最好容量稳定性高但大容值贵X7R/X5R温度系数较大更重要的是在直流偏压下实际容值会明显下降标称10μF的X5R电容在加3.3V电压后实际可能只剩6μF。这就导致你用公式算好的截止频率实测跟你算的对不上。所以RC滤波里面的C我的建议是滤波电容容量在1μF以下时优先选C0G/NP0容量需求更大时要选直流偏压特性好的X7R薄膜电容或者专门的稳压滤波电容。如果完全不能接受容值变化就选薄膜电容比如聚丙烯薄膜电容虽然体积大稳定性是真的好。第二坑是ESR。电容等效串联电阻ESR会影响高频衰减效果。一般陶瓷电容ESR比较低铝电解电容ESR偏高所以RC滤波不建议用电解电容当主角尤其是小信号滤波。如果一定要大容量可以在陶瓷电容旁边并联一个小容量C0G电容用来吸收高频分量。3.3 缓冲运放让输出扛得住负载RC滤波后的输出阻抗约等于R如果R取10kΩ后级电路输入阻抗也是10kΩ分压就直接把电压拉低一半。这个问题最常见的解决办法是RC后面接一个运放做电压跟随器。运放选型有几个注意点输入偏置电流要小否则会在高阻抗节点上产生额外的电压偏置输入共模范围要覆盖你的输出范围输出要能驱动你的负载电流。LM358这类老运放输入偏置电流在几十纳安级别接10kΩ输出阻抗时引入的误差可达数百微伏对高精度应用来说偏大选JFET输入的运放如TL072或者轨到轨运放更稳妥。如果你的信号是单极性且输出范围在0到VCC之间直接用一个单电源轨到轨运放比如MCP6001非常合适。运放后面如果需要驱动更重的负载还可以再加一级缓冲或功率级不过这就属于后话了。常规的PWM DAC到运放跟随即结束输出阻抗可以降到几欧姆甚至更低基本不再受负载影响。3.4 PCB布局地线和铺地处理PWM DAC本身不是高频电路但PWM信号是高频数字方波所以布局上还是要注意几点。第一RC滤波部分要靠近STM32的PWM引脚放置让PWM走线尽量短减少空间辐射耦合。第二电容一端连接RC节点另一端直接就近打过孔到完整地平面减小地环路。第三运放反馈路径要短输入输出不要平行走线防止自激。第四如果板上还有其他数字电路RC滤波网络和模拟输出区域要跟数字电路保持间距敏感信号线可以加一点地包围。另外有一点容易忽略STM32的模拟参考电压。如果你用的是内置ADC去回读自己的PWM输出做闭环这是很常见的做法那么VDDA和VREF的电源质量直接影响整个环路精度。我给VDDA串联一个小磁珠再加10μF100nF滤波电容效果比直接接3.3V干净得多。4. STM32软件配置定时器PWM生成与输出控制4.1 CubeMX配置定时器PWM软件部分首先要在STM32CubeMX里把定时器配成PWM模式。我用STM32F103系列做例子时钟树默认配置为72MHz主频选择TIM1的CH1通道作为PWM输出。配置时重点填三个参数Prescaler分频器、Counter Period自动重载值ARR、Pulse初始占空比比较值。假设我们要输出频率约24kHz、分辨率接近10位的PWM可以这样算先确定ARR999这样占空比可调范围是0~999共1000级接近10位精度。然后目标PWM频率f24kHz定时器计数频率F_T f × (ARR1) 24kHz × 1000 24MHz。72MHz主频需要分频得到24MHz所以Prescaler 72/24 - 1 2。对应CubeMX参数就是Prescaler 2实际分频系数为3Counter Period 999PWM频率 72MHz / 3 / (9991) 24kHz有两点要注意。PWM频率越高纹波越小但分辨率会下降因为ARR变小了反之分辨率提高PWM频率又降下来。选择原则是先把PWM频率定在应用可接受的范围常用20kHz到100kHz再在这个前提下尽量提高ARR。如果你需要12位分辨率且PWM频率不低于20kHz72MHz时钟根本不够用就要考虑提高定时器时钟或者选择更高主频的芯片。4.2 HAL库代码实现与占空比更新CubeMX生成初始化代码之后只需要三行关键代码就能把PWM跑起来HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 500);第一行启动PWM输出第二行设置比较值。把Pulse设为500对应占空比50%滤波后输出大约1.65V。这个函数在HAL库里的实现本质就是更新定时器比较寄存器CCR定时器硬件会在下一个PWM周期自动生效不需要CPU干预。动态调整占空比输出任意波形时直接把要输出的电压换算成比较值即可。例如目标电压是2.5VVCC3.3V占空比D2.5/3.3≈0.7576那么比较值round(0.7576×999)757。代码就是uint32_t val (uint32_t)(voltage / 3.3f * 999.0f); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, val);这里要注意每次更新之后不要频繁用printf打印调试信息否则查打印会占用大量时间输出波形会出现台阶状失真。调试时可以用DMA或中断批量更新业务代码里直接改寄存器更推荐。4.3 提升精度的软件技巧抖动、过采样与校准如果你已经做到ARR1023约10位却需要在某段输出上达到12位甚至更高的表现有两个软件技巧可以帮你“挤”出精度。第一个技巧是抖动。原理很简单比如你要输出1000.25级的占空比硬件做不到0.25级那么可以在N个PWM周期里交替输出1000级和1001级比例接近3:1滤波后的平均电压就等效于1000.25级。这跟数字音频里的Delta-Sigma调制思路类似。实际实现可以用一个累加器做误差积累把小数部分的误差滚动分配到后续周期里。这样做的代价是纹波会稍微变大但对精度提升非常可观我测试过可以把等效分辨率从10位提高到12位以上信号频率低时效果尤其明显。第二个技巧是校准。STM32的GPIO高电平输出并不是理想的VCC内部PMOS管有导通电阻会造成几十毫伏的压降低电平也不是绝对0V。可以通过外接精密电压表实测几点电压在软件里做线性校准把实际电压和理论电压的偏差拟合掉。实测下来校准后的输出误差可以从几十毫伏降到几毫伏以内。另外提一下如果你需要输出的是周期性波形比如正弦波、三角波可以预先把波形表算好放在Flash然后用定时器更新事件触发DMA把波形数据自动搬运到比较寄存器。这样可以做到无CPU干预的连续波形输出波形更新率由PWM频率决定非常流畅。核心代码思路如下// 波形表用DMA半字循环传输到CCR寄存器 HAL_TIM_PWM_Start_DMA(htim1, TIM_CHANNEL_1, (uint32_t*)wave_table, TABLE_SIZE);5. 实测中的问题与排查技巧实录5.1 纹波超标第一反应不该是加大电容很多同学一看示波器上纹波大就拼命加大电容结果纹波没压多少输出反而变得“软绵绵”——负载一变化电压就慢慢爬半天。正确的排查顺序应该是先用示波器确认PWM频率和占空比是否正确然后用公式反算当前纹波是否符合预期。如果实际纹波比公式算出来的大很多多半是电容ESR太高或者测量探头地线夹子太长引入noise。把探头地线尽量缩短甚至用弹簧接地针测量纹波数值会有很大变化。如果算出来纹波本身就不达标优先提高PWM频率而不是盲目加大电容。比如把PWM频率从20kHz提升到50kHz纹波直接降到四成而响应速度几乎不受影响。5.2 输出电压系统性偏低PWM占空比50%时输出理论值应该是1.65V附近实测只有1.55V到1.60V而且偏差跟占空比呈线性关系。这是GPIO高电平压降和低电平残留电压造成的。STM32的GPIO输出高电平在负载电流较大时会有压降低电平也会有一定剩余电压。解决方法是校准或者把VCC作为ADC参考源用内部ADC回读后闭环修正。如果偏差太大要检查GPIO配置成推挽输出没有——配成开漏输出的常见错误之一就是输出电压上不去。5.3 接了负载电压就跌这通常是RC输出阻抗过高而后级电路输入阻抗又不够大。最简单的方法是运放缓冲级就是把RC输出接运放同相端运放输出接到后级负载。如果确实不想加运放那就减小R的阻值比如从10kΩ降到1kΩ但代价是功耗增加、驱动能力需要重新核算。5.4 示波器测出来的波形自带毛刺有时候示波器上看到的毛刺根本不是RC输出本身的纹波而是测量环路引入的。探头地线过长、探头带宽过高、甚至探头电容约10pF到100pF都会影响测量结果。我用过100MHz探头去测PWM输出结果在上升沿看到非常大的振铃换成10x档并缩短地线后毛刺就消失了。提醒一句测量这类信号使用10x探头带宽不用太高反而要关注探头电容对RC节点的影响。5.5 常见问题速查表现象可能原因解决方案输出纹波偏大PWM频率过低、电容ESR偏高、测量地线过长提高PWM频率、选用C0G电容、缩短探头地线输出电平系统性偏低GPIO推挽问题、参考电压偏差确认推挽模式、软件校准、检查VREF接负载电压下跌RC输出阻抗过高增加运放跟随、减小R阻值波形建立太慢RC时间常数过大提高截止频率、换成二阶滤波再调参改变占空比后瞬间过冲RC谐振或运放自激检查运放相位裕度、加补偿电容输出和计算值非线性偏差电容直流偏压导致容值变化换C0G电容、用校准表修正最后再分享一个小技巧如果你在做闭环控制需要实时读取PWM DAC输出电压来验证是否准确不要把ADC直接接到RC滤波输出引脚上。ADC采样瞬间的开关电容会瞬间从RC节点抽取电荷导致输出电压被拉低从而干扰模拟输出。要么经过运放缓冲后再接ADC要么等RC节点稳定后短接一个采样保持电容再采。我实际项目中走的都是运放缓冲路线稳定性和精度都高很多。