搞嵌入式这几年,我越来越觉得“手里有什么就用好什么”才是真本事。项目中经常遇到这种尴尬:芯片选型时没注意,做到一半发现需要输出模拟量,结果手头这颗STM32根本没有DAC外设;或者DAC通道就一两路,项目却要求同时输出两路甚至三路独立电压。这个时候,STM32 PWM DAC这个组合就派上用场了——用定时器输出PWM,再接一级RC滤波电路,把方波的平均值变成稳定模拟电压。成本几乎为零,代码也不复杂,只要参数算对,精度可以做到0.1%以内,非常适合做传感器偏置、音频输出、调速控制参考电压这类场景。
这篇文章我按自己实际做过的项目流程来讲:先从原理上搞清楚PWM为什么能当DAC用,再手把手算RC滤波参数,然后给一份靠谱的元件选型指南,最后是STM32的软件配置和踩坑记录。无论你是刚学STM32的新手,还是被“没有DAC”坑过的老手,照着这套思路走,都能做出能用的模拟输出。
1. 方案评估:为什么拿PWM去顶替DAC
1.1 什么时候需要这种“自制DAC”
先说结论:PWM+RC这套方案的定位,是“够用就好”的模拟输出,而不是跟高性能DAC芯片硬刚。
实际项目里最常见的三种触发场景是这样的。第一,芯片没有DAC外设。很多入门级STM32型号确实不带DAC,或者带DAC但通道数很少,你可能在选型时没注意,等到要输出模拟量才发现硬件不支持。第二,通道数不够用。哪怕芯片带两路DAC,项目里要出三路、四路可调电压时,DAC就不够了,而PWM通道几乎每个定时器都有好几个,多路复用的成本低得多。第三,成本敏感。外置一颗12位SPI接口的DAC芯片,便宜的也要两三块钱,加上PCB面积和采购周期,对量产项目来说都是负担。
当然,PWM DAC也不是万能的。它的输出带宽很低,动态响应慢,纹波没办法完全消除。但在几百赫兹以内的缓慢变化信号里,它可以顶替大多数DAC应用场景。
1.2 PWM当DAC的原理
PWM信号本身是数字方波,电压只有高和低两个状态,为什么会变成模拟量?答案在平均值里。
一个周期T内,PWM高电平持续时间为D×T(D是占空比,0~1之间),低电平持续时间为(1-D)×T。如果不做任何处理,信号在VCC和0V之间来回跳,平均值等于D×VCC。问题在于,这个平均值前面叠加了一堆高频分量。RC低通滤波电路做的事情,就是把这些高频分量滤掉,留下直流分量。滤波后的输出电压就近似等于D×VCC。
举个例子,VCC=3.3V,占空比设为50%,滤波后输出约1.65V;占空比设为25%,输出约0.825V。这就实现了数字到模拟的转换。
这里有一个关键约束:分辨率和PWM频率是互相拉扯的。PWM的占空比调节级数由定时器的自动重载值ARR决定,ARR越大,分辨率越高,但PWM频率越低,因为:
F_PWM = F_Timer / (ARR+1)
假设定时器计数频率是72MHz,如果配置成8位分辨率(ARR=255),PWM频率最高约281kHz;如果配置成10位分辨率(ARR=1023),PWM频率只有约70kHz;如果贪心要12位分辨率(ARR=4095),PWM频率就掉到约17.6kHz。所以不是说想要多少位精度都行,得看你的应用能容忍多低的PWM频率,或者你的定时器时钟能拉多高。这就是PWM DAC最核心的“分辨率-频率权衡”。
1.3 三种方案横向对比
我整理了一个对比表,方便你根据项目情况快速判断:
| 对比项 | 内置DAC | PWM+RC | 外置SPI/I2C DAC |
|---|---|---|---|
| 成本 | 免费 | 几毛钱(电阻+电容) | 几块到几十块 |
| 分辨率 | 12位左右 | 8~12位(受时钟限制) | 12~16位 |
| 输出带宽 | 较高(可输出音频级信号) | 低(受RC截止频率限制) | 中高 |
| 通道扩展 | 1~2路 | 看定时器通道数量,可轻松扩展 | 按芯片规格 |
| CPU占用 | 低 | 极低(设置好占空比后不用管) | 需要通信开销 |
| 纹波 | 小 | 有,需要RC参数压制 | 极小 |
从表里能看出,PWM+RC最大的卖点就是成本和通道数。它适合的信号类型是缓慢变化的直流电压,比如可调基准、电机堵转电流保护阈值、传感器零点偏置。如果你要做高保真音频输出,或者对建立时间有严格要求的快速信号,还是老老实实上真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)
这个公式在D=0.5时纹波最大。举个实际例子:VCC=3.3V,D=50%,PWM频率f=20kHz,R=1kΩ,C=10μF,那么:
Vripple = 3.3 × 0.25 / (20000 × 1000 × 10e-6) ≈ 41mV
41mV的纹波对应8位ADC大约是LSE的一半多,对于很多应用来说太大了。把PWM频率提高到100kHz,同样参数下纹波可以降到8.25mV左右。或者把RC乘积加大到R=10kΩ、C=10μ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。以VCC=3.3V、R=220Ω来计算,瞬时功耗约50mW,选0603封装的电阻(额定功率100mW)已经有点紧张,最好用0805或更大。如果R取1kΩ以上,功耗就很小了,不必过度担心。另外,电阻阻值也不要取得太大,否则输出阻抗高,稍微带一点负载电压就塌了。
3.2 电容:介质类型和容值选择
电容这块坑最多,我踩得也最深。第一坑是电容材质。MLCC陶瓷电容常见的有X7R、X5R、C0G(NP0)等。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μF+100nF滤波电容,效果比直接接3.3V干净得多。
4. STM32软件配置:定时器PWM生成与输出控制
4.1 CubeMX配置定时器PWM
软件部分首先要在STM32CubeMX里把定时器配成PWM模式。我用STM32F103系列做例子,时钟树默认配置为72MHz主频,选择TIM1的CH1通道作为PWM输出。
配置时重点填三个参数:Prescaler(分频器)、Counter Period(自动重载值ARR)、Pulse(初始占空比比较值)。
假设我们要输出频率约24kHz、分辨率接近10位的PWM,可以这样算:
先确定ARR=999,这样占空比可调范围是0~999,共1000级,接近10位精度。然后目标PWM频率f=24kHz,定时器计数频率F_T = f × (ARR+1) = 24kHz × 1000 = 24MHz。72MHz主频需要分频得到24MHz,所以Prescaler = 72/24 - 1 = 2。
对应CubeMX参数就是:
- Prescaler = 2(实际分频系数为3)
- Counter Period = 999
- PWM频率 = 72MHz / 3 / (999+1) = 24kHz
有两点要注意。PWM频率越高,纹波越小,但分辨率会下降,因为ARR变小了;反之分辨率提高,PWM频率又降下来。选择原则是先把PWM频率定在应用可接受的范围(常用20kHz到100kHz),再在这个前提下尽量提高ARR。如果你需要12位分辨率且PWM频率不低于20kHz,72MHz时钟根本不够用,就要考虑提高定时器时钟或者选择更高主频的芯片。
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.5V,VCC=3.3V,占空比D=2.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 提升精度的软件技巧:抖动、过采样与校准
如果你已经做到ARR=1023(约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节点稳定后短接一个采样保持电容再采。我实际项目中走的都是运放缓冲路线,稳定性和精度都高很多。