STM32无DAC实现PWM转DAC:二阶RC滤波器设计与实测
1. 从一个真实需求说起为什么MCU没有DAC还要硬造一个很多做嵌入式控制的朋友都遇到过这种尴尬选了一颗STM32F103C8T6价格便宜、资源够用、生态成熟结果项目里偏偏需要一个模拟量输出——比如给变频器一个0~3.3V的基准电压或者驱动一个需要模拟信号输入的传感器模块。翻遍数据手册才发现这颗芯片压根没有DAC外设。换芯片成本上去了PCB要重画软件要重写项目周期不允许。加一颗外置DAC芯片I2C或SPI接口的DAC虽然精度不错但多一颗物料、多一笔成本、多一道调试工序对于成本敏感的消费类或工业类产品来说能省则省。这时候PWM转DAC就成了一个非常实用的替代方案。原理说起来不复杂STM32的定时器可以输出固定频率、占空比可调的方波这个方波经过一个低通滤波器把高频分量滤掉剩下的直流分量就正比于占空比。占空比50%对应1.65V占空比100%对应3.3V占空比0%对应0V。理论上只要滤波器设计得当就能得到一个可调的模拟电压。但问题来了——很多人第一次做这个方案的时候随便焊了一个RC低通滤波器上去结果发现输出电压纹波大得离谱或者响应速度慢得像蜗牛又或者带载能力几乎为零接上负载电压就塌了。更麻烦的是他们不知道问题出在哪里只能反复试参数浪费大量时间。这篇文章就是来解决这个问题的。我会从PWM参数计算开始一步步推导二阶RC低通滤波器的设计过程用OrCAD做仿真验证最后给出实测数据和踩坑经验。整套流程适用于STM32F103系列、STM32F4系列以及其他任何带高级定时器的MCU。如果你正在做基于STM32的毕业设计、逆变器方案、电机控制或者智能台灯之类的项目需要模拟量输出但芯片没有DAC这篇内容可以直接抄作业。2. PWM转DAC的核心原理与关键参数计算2.1 从方波到模拟电压傅里叶视角下的本质要理解PWM转DAC得先从频域的角度看问题。一个占空比为D、频率为f_pwm、幅值为Vcc的方波信号用傅里叶级数展开后可以写成V(t) Vcc * D (2*Vcc/π) * Σ [sin(n*π*D)/n] * cos(2π*n*f_pwm*t)这个公式告诉我们两件事。第一项Vcc * D就是直流分量也就是我们想要的模拟电压。后面的求和项是各次谐波频率分别是f_pwm、2f_pwm、3f_pwm……这些高频分量就是需要被滤掉的“垃圾”。低通滤波器的任务就是让直流分量无损通过同时把f_pwm及其以上频率的谐波衰减到足够小。这里有一个关键认知PWM转DAC的精度上限取决于滤波器对基波频率的衰减能力而不是PWM分辨率本身。很多人以为用16位定时器就能得到16位精度的模拟输出这是不对的。如果滤波器对f_pwm的衰减只有20dB那么纹波会直接叠加在输出上有效精度可能连8位都不到。2.2 PWM频率与分辨率的权衡STM32的定时器PWM模式有一个基本约束PWM频率 定时器时钟 / (预分频系数 * 自动重装载值)。而分辨率取决于自动重装载值ARRARR越大占空比可调的档位越多分辨率越高。举个例子STM32F103的APB1定时器时钟是72MHz。如果预分频系数设为0ARR设为7199那么PWM频率 72MHz / 7200 10kHz分辨率是log2(7200) ≈ 12.8位。如果把ARR降到719PWM频率变成100kHz分辨率就只剩log2(720) ≈ 9.5位。这就形成了一个矛盾PWM频率越高滤波器越好设计但分辨率越低PWM频率越低分辨率越高但滤波器截止频率必须压得很低响应速度就慢。我的经验是对于大多数控制类应用PWM频率选在20kHz到100kHz之间比较合适。20kHz刚好在音频范围之上不会产生可听噪声100kHz则可以让滤波器截止频率设在1kHz左右响应速度勉强够用。如果对分辨率要求高可以用定时器的中心对齐模式或者提高系统时钟来缓解矛盾。2.3 二阶RC低通滤波器的传递函数推导一阶RC滤波器的衰减斜率是-20dB/十倍频程对于10kHz的PWM基波如果要衰减到1mV以下相对于3.3V幅值约-70dB截止频率需要设在10Hz左右响应时间常数达到16ms太慢了。所以实际项目中基本都用二阶RC。二阶RC低通滤波器的标准拓扑是两级RC级联传递函数为H(s) 1 / (1 s*(R1*C1 R1*C2 R2*C2) s^2*R1*R2*C1*C2)这个传递函数有两个极点衰减斜率是-40dB/十倍频程。截止频率的近似计算公式为fc 1 / (2π * sqrt(R1*R2*C1*C2))但要注意这个公式只在两级RC之间没有相互加载效应时成立。如果第二级的输入阻抗相对于第一级的输出阻抗不够大两级之间会互相影响实际截止频率会偏离计算值。这也是为什么很多人按公式算出来的参数仿真和实测对不上的原因。2.4 纹波抑制与响应速度的定量计算设计二阶RC滤波器时需要同时满足两个指标纹波抑制比和建立时间。纹波抑制比的计算方法是在PWM基波频率f_pwm处计算滤波器的幅频响应|H(j2πf_pwm)|然后纹波峰峰值约为V_ripple_pp ≈ 2 * Vcc * |H(j*2π*f_pwm)| / (π * n)其中n是谐波次数对于方波基波幅度最大所以主要看基波。建立时间则取决于滤波器的阻尼系数。二阶系统的阻尼比ζ (R1C1 R1C2 R2C2) / (2sqrt(R1R2C1*C2))。当ζ 1时系统是欠阻尼的会有过冲和振铃当ζ 0.707时是巴特沃斯响应最平坦当ζ 1时过阻尼响应慢但没有过冲。对于PWM转DAC应用我通常选择ζ在0.8到1.0之间牺牲一点响应速度换取更干净的输出。建立时间ts ≈ 4 / (ζ * ωn)其中ωn 2π*fc。3. 手把手设计从指标到元件参数的完整推导3.1 明确设计指标假设我们要做一个0~3.3V可调的模拟电压源用于给一个电机驱动器提供速度基准。指标要求如下PWM频率36kHzSTM32F10372MHz时钟ARR1999预分频0PWM幅值3.3V输出纹波小于5mV峰峰值建立时间小于5ms从0V到3.3V的90%输出电流最大1mA给高阻抗负载3.2 计算所需的衰减量纹波要求5mV峰峰值PWM幅值3.3V。基波纹波幅度约为2*3.3/π ≈ 2.1V。需要的衰减倍数为2.1V / 2.5mV ≈ 840倍即-58.5dB。二阶滤波器在截止频率之后的衰减斜率是-40dB/十倍频程。设截止频率为fcPWM频率为36kHz则40 * log10(36000/fc) ≥ 58.5 log10(36000/fc) ≥ 1.4625 36000/fc ≥ 29 fc ≤ 1241Hz所以截止频率需要低于1.24kHz。取fc 1kHz留一定余量。3.3 选择阻尼比和元件值取ζ 0.9fc 1kHz。对于二阶RC滤波器如果选择R1 R2 RC1 C2 C则fc 1 / (2π * R * C) ζ 1.5这个值偏大响应会偏慢为了得到ζ 0.9需要让C1和C2不相等。常用的方法是令C1 2*C2R1 R2 R。此时fc 1 / (2π * R * sqrt(2) * C2) ζ (2*C2 C2 C2) / (2*sqrt(2)*C2) 4 / (2*1.414) 1.414还是偏大。再试C1 10*C2R1 R2 Rfc 1 / (2π * R * sqrt(10) * C2) ζ (10*C2 10*C2 C2) / (2*sqrt(10)*C2) 21 / 6.32 3.32更大了。看来ζ和fc是耦合的不能随意取。实际上对于两级RC级联ζ的最小值是1.5当C1C2时也就是说无源二阶RC滤波器无法实现ζ 1.5。这是一个非常重要的认知很多教材和网上的文章都忽略了这一点。那怎么办两个办法一是接受ζ 1.5的过阻尼响应建立时间会慢一些二是在第二级后面加一个电压跟随器利用运放的输出阻抗极低来隔离负载同时可以在反馈回路中调整阻尼。但加运放就增加了成本和复杂度。对于大多数PWM转DAC应用ζ 1.5的过阻尼响应其实是可以接受的。建立时间ts ≈ 4 / (ζ * ωn) 4 / (1.5 * 2π * 1000) ≈ 0.42ms远小于5ms的要求。所以我们就按C1 C2 CR1 R2 R来设计。3.4 计算具体元件值取fc 1kHzC 100nF则R 1 / (2π * 1000 * 100e-9) 1591Ω取标准值1.6kΩ。验证截止频率fc 1 / (2π * 1600 * 100e-9) 995Hz在36kHz处的衰减|H(j*2π*36000)| 1 / sqrt((1 - (36000/995)^2)^2 (1.5*36000/995)^2) ≈ 1 / (36000/995)^2 ≈ 1 / 1309 ≈ -62.3dB纹波峰峰值 ≈ 2.1V / 1309 ≈ 1.6mV满足小于5mV的要求。3.5 输出阻抗与带载能力分析无源RC滤波器的输出阻抗在直流下等于R1 R2 3.2kΩ。如果负载电流为1mA输出电压会下降3.2V这完全不可接受。所以无源二阶RC滤波器只能用于高阻抗负载或者必须在后面加电压跟随器。如果负载电流很小比如运放输入端nA级别那没问题。但如果要驱动任何有实际电流需求的负载必须加缓冲。常用的方案是用一颗轨到轨运放做电压跟随器比如LM358便宜但非轨到轨、MCP6001轨到轨、低功耗或者OPA333高精度。加运放后输出阻抗降到毫欧级别带载能力大幅提升。注意加运放后运放的输入偏置电流会流过R2产生额外误差。对于1mA级别的负载这个误差可以忽略但对于高精度应用需要选择输入偏置电流在pA级别的运放。4. OrCAD仿真验证从原理图到波形实测4.1 搭建仿真原理图打开OrCAD Capture新建一个PSpice仿真工程。放置以下元件VPULSE模拟PWM信号源设置V10VV23.3VTR1nsTF1nsPW13.89usPER27.78us对应36kHz、50%占空比R1、R21.6kΩC1、C2100nF负载电阻RL10kΩ模拟高阻抗负载连接方式VPULSE正端接R1一端R1另一端接R2一端R2另一端接输出节点。C1从R1和R2的连接点接到地C2从输出节点接到地。RL从输出节点接到地。4.2 设置仿真参数新建Simulation Profile选择Time DomainTransient分析。运行时间设为10ms最大步长设为100ns。这样可以看到从启动到稳态的完整过程也能看清纹波细节。4.3 查看仿真波形与数据分析运行仿真后在Probe窗口中添加输出节点电压波形。你会看到启动阶段电压从0V开始上升大约在1.5ms左右达到稳态值1.65V50%占空比对应1.65V稳态阶段电压在1.65V附近有微小波动峰峰值约为1.5mV与理论计算吻合纹波频率主要成分是36kHz与PWM频率一致如果把占空比改为75%稳态电压应该变为2.475V。可以在VPULSE中修改PW为20.83us重新仿真验证。4.4 用AC扫描看幅频特性除了瞬态分析还应该做一次AC扫描看看滤波器的幅频响应。设置AC Sweep频率范围从1Hz到1MHz每十倍频程100个点。在Probe中查看输出节点的幅频曲线可以看到直流增益0dB无源滤波器直流衰减为1-3dB点约995Hz衰减斜率-40dB/十倍频程在36kHz处约-62dB这个结果和理论计算完全一致说明设计是可靠的。4.5 仿真中容易忽略的细节第一个坑是PSpice的初始条件。默认情况下电容的初始电压是0V这没问题。但如果你的电路中有多个电源或者复杂的初始化逻辑可能需要设置.IC语句来指定初始条件。第二个坑是步长设置。如果最大步长设得太大比如1us36kHz的PWM波形会被严重失真仿真结果不可信。对于36kHz信号步长至少要是周期的1/100即278ns。我通常设100ns兼顾精度和速度。第三个坑是负载效应。如果你在仿真中加了负载电阻要注意它和R2形成并联会改变输出阻抗和截止频率。对于10kΩ负载和1.6kΩ的R2并联后等效电阻约为1.38kΩ截止频率会略微升高。这个影响不大但如果负载电阻降到1kΩ影响就很明显了。5. 实测踩坑记录那些仿真不会告诉你的问题5.1 第一个坑PWM输出配置错误导致占空比不对我第一次做这个方案的时候用STM32CubeMX配置TIM3的PWM输出生成代码后烧进去用万用表测输出电压发现不管怎么改占空比输出电压都是0V。查了半天才发现CubeMX默认把PWM输出的GPIO配置成了开漏输出而我没有接上拉电阻。改成推挽输出后一切正常。这个坑的教训是STM32的PWM输出GPIO必须配置为推挽复用输出GPIO_Mode_AF_PP不能是开漏。开漏输出需要外部上拉否则无法输出高电平。5.2 第二个坑滤波器输出接负载后电压塌陷仿真的时候用的是10kΩ负载输出很正常。实际电路中我把输出接到了一个小型直流电机的调速输入端结果电压直接从1.65V掉到了0.3V。原因很简单电机调速输入端的等效阻抗只有几百欧姆而我的滤波器输出阻抗是3.2kΩ形成了严重的分压。解决方案有两个一是在滤波器后面加电压跟随器二是降低滤波器的电阻值。我选择了加一颗MCP6001做跟随器输出阻抗降到毫欧级别带载问题彻底解决。5.3 第三个坑电容选型不当导致纹波超标一开始我用的是普通的X7R陶瓷电容100nF。理论上没问题但实测纹波比仿真大了将近10倍。后来用示波器仔细看发现纹波里除了36kHz的基波还有很多高频毛刺。原因是X7R电容有压电效应和直流偏压特性在3.3V直流偏压下实际容值可能下降30%以上导致截止频率升高滤波效果变差。换成C0GNP0电容后纹波立刻降到了理论值附近。C0G电容的容值稳定不受直流偏压影响温度系数也小。虽然价格贵一点但对于这种精密模拟电路非常值得。提示如果容值需要较大比如1uF以上C0G电容可能不好买或者价格很高。这时候可以考虑用钽电容或者薄膜电容但要注意它们的等效串联电阻ESR和等效串联电感ESL会影响高频滤波效果。5.4 第四个坑PCB布局引入的噪声即使元件和参数都对了PCB布局不好也会导致输出噪声大。我第一次画板的时候把滤波器放在离MCU比较远的地方PWM走线很长结果输出上耦合了很多数字噪声。正确的做法是滤波器尽量靠近MCU的PWM输出引脚走线尽量短且远离高频数字信号线。电容的接地端要直接接到模拟地平面不要和数字地混在一起。如果板上有ADC模拟地和数字地要在单点连接避免数字噪声通过地平面耦合到模拟输出。5.5 第五个坑运放自激振荡加电压跟随器之后我发现输出有时候会振荡频率大概几百kHz。查了资料才知道运放驱动容性负载时容易自激。我的滤波器输出端有C2对运放来说就是容性负载。解决方案是在运放输出和C2之间串一个小电阻比如22Ω或者在反馈回路中加一个小电容做相位补偿。我用了22Ω的隔离电阻振荡立刻消失。这个电阻会引入一点输出阻抗但对于1mA级别的负载压降只有22mV完全可以接受。6. 进阶优化如何把精度和响应速度再提升一个档次6.1 用定时器DMA实现任意波形输出基本的PWM转DAC只能输出直流电压。如果想让输出电压按一定规律变化比如正弦波、三角波可以用定时器触发DMA动态修改CCR寄存器的值。STM32的TIMx_CCR寄存器可以在运行时更新配合DMA传输一个波形查找表就能输出任意低频波形。具体做法是配置一个定时器作为DMA触发源频率设为波形采样率比如10kHz。DMA从内存中的波形数组读取数据写入TIMx_CCR寄存器。PWM频率仍然保持36kHz但占空比按采样率更新。这样输出经过低通滤波器后就是一个低频模拟波形。这个方案的输出频率上限受限于PWM频率和滤波器截止频率。一般来说输出波形频率不要超过fc/10否则衰减和相移会很严重。对于fc1kHz输出波形频率最好在100Hz以下。6.2 用二阶有源滤波器替代无源RC如果对纹波和带载能力要求都很高可以用有源滤波器。常见的拓扑是Sallen-Key二阶低通滤波器用一颗运放加四个无源元件实现。Sallen-Key的优点是输入阻抗高、输出阻抗低、阻尼比可以自由调节通过电阻比值而且不需要电感。设计Sallen-Key时截止频率和阻尼比可以独立设定。对于巴特沃斯响应ζ0.707元件值的选择公式为R1 R2 R C1 2*C2 fc 1 / (2π * R * sqrt(C1*C2))这样可以在保证-40dB/十倍频程衰减的同时获得平坦的通带和较快的建立时间。代价是需要一颗运放成本增加几毛钱PCB面积增加一点。6.3 用STM32内置DAC的替代方案对比如果项目允许换芯片STM32F103RCT6、STM32F407等型号是带12位DAC的。内置DAC的输出阻抗较低可以直接驱动高阻抗负载精度和稳定性都比PWM转DAC好。但内置DAC也有缺点输出范围有限通常0~VREF建立时间较长几微秒到几十微秒而且DAC输出引脚通常和GPIO复用布局上需要注意。选择哪种方案取决于项目对成本、精度、响应速度和开发周期的综合要求。对于成本极度敏感、精度要求不高的场合PWM转DAC是首选对于精度要求高、有现成DAC资源的场合直接用内置DAC更省事。6.4 校准与软件补偿无论用哪种方案实际输出都会有一些误差来源包括PWM占空比的量化误差、滤波器元件的容差、运放的失调电压和偏置电流、电源电压的波动等。对于精度要求高的应用可以在软件中做校准。校准方法是在已知负载条件下测量几个不同占空比对应的实际输出电压拟合出一条直线然后在软件中做线性补偿。如果误差是非线性的可以用分段线性补偿或者查表法。STM32的Flash空间足够存储一张校准表运行时查表插值即可。我在一个项目中用这种方法把PWM转DAC的精度从±5%提升到了±0.5%代价只是增加了几十行校准代码和一次产线校准工序。7. 完整代码与工程配置要点7.1 STM32CubeMX配置步骤以STM32F103C8T6为例配置TIM3的通道1为PWM输出在Pinout视图中将PA6配置为TIM3_CH1在Timers配置中选择TIM3Clock Source设为Internal ClockChannel1设为PWM Generation CH1参数设置Prescaler 0Counter Period 1999Pulse 1000初始50%占空比GPIO设置中确认PA6的模式为Alternate Function Push-PullSpeed设为High生成代码7.2 关键代码片段// 启动PWM输出 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); // 设置占空比0~1999对应0~100% void SetDACOutput(uint16_t duty) { if (duty 1999) duty 1999; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, duty); } // 设置输出电压mV假设Vcc3300mV void SetOutputVoltage(uint16_t mV) { uint32_t duty (uint32_t)mV * 2000 / 3300; SetDACOutput((uint16_t)duty); }7.3 提高PWM分辨率的技巧如果觉得2000级的分辨率不够可以开启定时器的重复计数器RCR或者使用高分辨率定时器HRTIM部分STM32型号支持。另一个技巧是使用定时器的中心对齐模式在相同ARR下中心对齐模式的PWM频率是边沿对齐的一半但分辨率不变而且对称的波形对滤波更友好。对于STM32F103还可以把APB1的时钟从36MHz超频到72MHz如果芯片支持这样在相同ARR下PWM频率翻倍滤波器设计更容易。但超频有风险需要充分测试稳定性。8. 写在最后一些个人体会这套PWM转DAC的方案我从2018年第一次在电机控制项目中使用到现在已经迭代了五六个版本。最大的感受是仿真和实测之间的差距往往不在理论计算而在元件选型和PCB布局。理论计算告诉你需要1.6kΩ和100nF但实际用X7R电容和C0G电容效果天差地别仿真告诉你纹波1.5mV但PCB布局不好实测可能50mV。另一个体会是不要迷信高分辨率。很多人追求16位PWM分辨率但滤波器的衰减能力跟不上实际有效精度可能只有8位。与其在分辨率上堆资源不如把滤波器设计好、把布局做好、把校准做扎实。最后分享一个小技巧如果你手头没有精密电阻电容可以用两个普通电阻串联或并联来凑出接近的值。比如需要1.6kΩ可以用1.5kΩ和100Ω串联误差从5%降到1%以内。电容也可以用两个100nF并联得到200nF或者用两个47nF并联得到94nF。这种方法在打样阶段特别实用不用专门去买精密元件。