news 2026/9/24 13:08:37

STM32无DAC怎么办?用PWM+RC低成本实现高精度模拟输出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32无DAC怎么办?用PWM+RC低成本实现高精度模拟输出

搞嵌入式这几年,我越来越觉得“手里有什么就用好什么”才是真本事。项目中经常遇到这种尴尬:芯片选型时没注意,做到一半发现需要输出模拟量,结果手头这颗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 三种方案横向对比

我整理了一个对比表,方便你根据项目情况快速判断:

对比项内置DACPWM+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节点稳定后短接一个采样保持电容再采。我实际项目中走的都是运放缓冲路线,稳定性和精度都高很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 13:07:54

晶振选型实战指南:频率匹配、精度等级与负载电容计算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 13:06:20

中兴B862AV3.2M救砖实战:免拆机免ADB,U盘+双公头USB线全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 13:06:04

高通9008模式与QPST救砖原理及红魔6实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 13:03:16

计算机单片机毕设实战-基于 STM32 的 PM2.5 与气象环境监测预警装置设计 基于 STM32 的多传感器环境数据采集与阈值报警系统设计(010609)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/24 13:01:47

PolyWorks MS 2020加密狗安装激活全攻略:Win10/11驱动避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 13:01:23

ESP32-CAM烧录教程:Arduino IDE配置、接线与常见错误排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华