尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多路PWM模拟DAC输出电压的工程实战:RC滤波参数计算与STM32实现

多路PWM模拟DAC输出电压的工程实战:RC滤波参数计算与STM32实现 简介多路PWM模拟DAC输出电压的STM32工程压缩包适合嵌入式入门和进阶开发者用于解决多路独立模拟电压输出的实际需求。工程基于STM32F03VC微控制器与Keil uVision5开发环境通过12路PWM独立调节占空比实现近似DAC功能实测最高电压约3.2V已在作者所需场景中验证满足要求且未改动频率可沿用其设计思路。资源共530个文件以134个h头文件、115个c源文件为核心并包含o、d、crf、s、hex、axf等编译中间文件和固件输出另有PDF文档与readme说明整体约7.95MB目录结构清晰可直接用Keil打开工程并二次开发。已有612人浏览学习。通过该实例可掌握PWM脉宽调制原理、占空比与平均电压的换算、STM32定时器PWM通道配置以及在不改变频率情况下仅调节占空比实现模拟DAC输出的调试方法对需要多路可调电压输出或低成本模拟量控制的实践者而言这份工程具有直观的参考价值。 搞嵌入式这些年遇到“模拟量输出”需求的次数多得数不过来。最直接的方案是上DAC芯片但有时候手头没有多余串行接口或者BOM成本卡得紧又或者就缺那么一两路额外输出专门加一颗DAC总觉得不划算。这时候用PWM加RC低通滤波去模拟DAC输出是我在项目里用过很多次的土办法——成本几乎为零代码也不复杂。今天这篇就围绕我在实际工程里整理出来的“多路PWM模拟DAC输出电压”方案把我自己踩过的坑、调过的参、量过的波形一次说清楚。这方案能解决什么问题简单说就是让一颗没有DAC外设或者DAC通道不够用的MCU通过多个定时器输出的PWM信号经滤波后变成多路可调直流电压。适合什么场景控制模拟电路偏置电压、调速、调光、给传感器输出参考电平或者做个简易信号源都没问题。当然它也有局限比如输出带宽低、纹波没办法彻底消除但这些限制咱们后面会一条条拆开讲。网上关于PWM做“伪DAC”的资料其实不少但大部分只讲原理不告诉你怎么算参数更不会说多路同时用的时候有哪些坑。所以我这篇会把硬件计算、软件配置、实测波形、故障保护全部串起来照着做基本能直接复现。1. 方案选型为什么不用DAC芯片非要用PWM硬凑1.1 项目需求的真实起点咱们先还原一个典型的开发场景。我用的是STM32F103ZET6这颗料板子上已经接了串行Flash、温湿度传感器、一块TFT屏外设接口已经占得差不多了。但测试时突然需要三路可调的模拟电压一路给压控振荡器的调谐端一路给比较器的阈值电平还有一路给后级运放做偏置。这三路电压的要求其实都不高电压范围0到3.3V分辨率12位左右足够更新速率每秒几十次就够用根本不追求什么高速。但三路如果都加DAC芯片先不说接口够不够光成本就从几毛钱跳到十几块。而且F103这颗片子本身没有DAC换带DAC的型号又不现实。这时候用PWM去模拟既不动硬件架构也不需要外扩芯片就是最务实的解法。1.2 PWM模拟DAC的核心原理PWM本身是一个频率固定、占空比可变的方波信号。它的平均值其实就是高电平时间占比乘上高电平电压再考虑低电平接近0V那么平均值可以近似写成V_avg D × V_HD是占空比0到1之间的数V_H是PWM高电平电压对3.3V系统来说通常接近3.3V。这个平均值恰好就是DAC要输出的直流电压。咱们要做的就是把这个“平均值”从方波里提取出来。RC低通滤波器就是最常用的提取工具它把高频方波分量滤掉剩下平滑的直流分量。明白了这个原理就能理解为什么占空比和电压是线性关系想要多少伏电压就算出对应占空比然后让定时器产生这个占空比的PWM再经过滤波输出端就是直流电压。整个过程本质上是在用“时间平均”代替“幅度转换”。1.3 方案的边界条件什么时候别硬上这里我得泼点冷水。PWM模拟DAC不是万能的有几个硬性限制必须提前认清。输出带宽低。RC滤波要滤掉PWM基频响应速度必然慢。想输出变化的音频信号基本没戏输出缓慢变化的直流完全没问题。纹波无法归零。滤波后残余纹波会随负载和滤波阶数变化要求极高精度的场合不适合。带载能力弱。RC滤波输出阻抗高直接接重负载电压会被拉垮必须加运放缓冲。我一般会用一句话做判断如果输出信号变化速度远低于PWM频率的十分之一并且纹波要求不是很苛刻这方案就能用反之老老实实上DAC芯片。2. 硬件设计RC滤波电路与输出调理的工程计算2.1 一阶RC滤波参数的计算思路RC低通滤波器的截止频率计算公式是f_c 1 / (2 × π × R × C)很多新手拿到这个公式就开始套数但实际选参数时要同时满足两个约束截止频率要远低于PWM频率同时又要比所需信号最高频率高。我一般先定PWM频率再倒推RC。举个例子PWM频率选20kHz那就把RC截止频率放在十分之一处也就是2kHz左右。此时若R取1kΩ则C 1 / (2 × π × 1000 × 2000) ≈ 0.08μF我实测发现0.1μF的CBB电容配合1kΩ电阻对于一个PWM频率20kHz的系统来说效果不错。但如果输出要求纹波更小可以把截止频率再往下降同时接受输出建立时间变长。需要说明的是R不能取太大否则输出阻抗太高C也不建议用太廉价的电解电容漏电会影响精度用CBB或者X7R贴片更稳。2.2 多路输出的降阶与隔离策略做多路输出时有个容易被忽视的坑各路RC滤波的地回路和电源退耦要是没处理好通道之间会串扰。我在工程里常用的做法是每一路独立一个RC滤波然后在各路输出端共用一个去耦电容比如10μF钽电容到模拟地。如果板子上模拟地和数字地分开PWM部分的数字地和模拟地要在单点汇合避免地环流影响输出电压。我再补充一个实操细节多路PWM的RC滤波参数别贪图省事全部统一如果某一路后续接的是高阻抗输入那R取大一点没问题但要是某路要驱动阻性负载R就得取小不然带载后电压跌落很明显。最好在R和C之间留个空焊位方便调试时换参数。2.3 运放跟随器解决带载能力弱的必备环节RC滤波后信号源的输出阻抗就是R的值1kΩ或者10kΩ的输出阻抗直接带负载是不行的。我在实际项目里在RC滤波之后都会跟一级运放接成电压跟随器。这样负载的影响落在运放身上RC输出只面对运放的高输入阻抗电压几乎不衰减。选运放时要注意三点输入偏置电流别太大否则在RC输出电阻上产生额外压降压摆率不用高因为信号是缓变的电源轨要能到0到3.3V或者至少接近轨到轨。我曾经用LM358做过这活但LM358输出没法到0V低端会有一截电压后来换成轨到轨运放后问题才解决。这一条经验直接导致我在后续所有类似电路里都优先选轨到轨运放。3. 软件实现STM32定时器多路PWM的配置与占空比换算3.1 PWM频率的选择逻辑PWM频率决定了滤波的难易程度和电压分辨率选型时主要看三点。频率越高RC滤波后纹波越小因为RC对高频衰减更明显。频率越高同一定时器时钟下的分辨率越低。比如72MHz系统时钟若PWM频率选72kHz那么一个周期只有1000个时钟计数占空比分辨率只有千分之一约等于10位分辨率如果频率降到20kHz一个周期有3600个计数分辨率高得多。频率不能低到可闻范围否则电路会发出丝丝声容易被人投诉。我最终通常选20kHz到40kHz这个区间既能保证12位左右的分辨率需要定时器重载值约4096甚至更高又不会产生音频噪音。这次工程用的STM32F103ZET6主频72MHzTIM3想要得到20kHz的PWM预分频PSC可以设0ARR则可以按下面方式推算PWM_Freq 72MHz / (PSC 1) / (ARR 1)若PSC 0则 ARR 1 72000000 / 20000 3600因此ARR 3599。这个重载值对应占空比分辨率约为1/3600差不多12位水平够日常用。3.2 TIM3通道配置与引脚重映射细节STM32F103ZET6的TIM3有四个通道可以输出四路PWM。有的板子引出的TIM3引脚默认不是普通IO比如TIM3_CH1在默认引脚是PA6重映射后可以到PB4。我在硬件设计时就确认了引脚冲突所以直接在代码里开了部分重映射把TIM3_CH1映射到PB4。用标准外设库配置时核心步骤是先把对应引脚配置为复用推挽输出然后初始化定时器时基再初始化输出比较通道最后使能。以下是当时初始化的关键代码片段GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 引脚重映射将TIM3_CH1部分重映射到PB4 GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); // PB4、PB5、PB6、PB7等分别作为TIM3_CH1~CH3等输出引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 定时器时基72MHz / (01) / (35991) 20kHz TIM_TimeBaseStructure.TIM_Period 3599; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 配置PWM模式1输出比较极性为高 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse 0; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC3Init(TIM3, TIM_OCInitStructure); TIM_OC4Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE);这里有个特别容易被忽略的坑部分引脚重映射后如果对应引脚一直复用在JTAG功能上比如PB3、PB4默认是JTAG复用要先把JTAG关掉否则引脚配置不生效。我调试时遇到过PWM波形死活不出来查了一圈才发现是JTAG复用占着引脚不放后来加了一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)波形立刻就有了。3.3 占空比换算与多路同步刷新占空比和期望电压的换算公式很直接Duty (V_target / V_ref) × ARRV_ref是PWM高电平实际电压。这里要特别提一个坑PWM高电平并不严格等于3.3VMCU的IO在重负载下会有压降而且不同引脚的驱动能力也有差异。所以我建议量产或校准场景下用万用表实测一下高电平电压修正一下V_ref不然理论算出来的1.65V实际输出可能是1.58V。多路PWM同时更新时有个优先级问题如果一路一路改CCR寄存器那么一个周期内各路输出的调整时刻不一致后端滤波出来的电压会出现短暂的阶梯跳变。要避免这个问题可以利用定时器的预装载功能让CCR寄存器的更新发生在更新事件这样所有通道在同一个时刻同步刷新。我在代码里用的是同时修改TIM3的四个CCR然后靠定时器更新事件装载保证多路动作一致。void dac_set_channel(uint8_t ch, uint16_t duty) { switch(ch) { case 0: TIM_SetCompare1(TIM3, duty); break; case 1: TIM_SetCompare2(TIM3, duty); break; case 2: TIM_SetCompare3(TIM3, duty); break; case 3: TIM_SetCompare4(TIM3, duty); break; default: break; } }这段代码本身没什么花样但底层的关键点是确保TIM3的CCR预装载是使能的否则写入的占空比会立即生效和预想中的同步机制不一致。4. 实测波形、纹波分析与PWM故障保护实操4.1 滤波前后的波形对比拿示波器直接量TIM3输出引脚看到的是标准的方波量RC滤波之后方波就变成了一条带细小纹波的直流电平。我分别抓了PWM引脚和滤波输出的波形做对比PWM引脚波形频率20kHz高电平约3.28V实测略低于理论3.3V占空比稳定。一阶RC滤波后平均电压与理论值偏差在几十毫伏以内纹波约20mV左右对于0到3.3V量程来说这个精度已经能接受。加一级运放跟随器后输出电压基本等于滤波后的平均值负载从10kΩ换到1kΩ电压变化小于10mV。这个实测对比说明两个问题一是方案可行精度够用二是V_ref不能用理论值最好实测修正否则初始误差可能达到0.5%。4.2 输出建立时间与纹波的取舍我特意测试了输出从0V跳到2.5V时滤波输出的建立时间。RC截止频率约1.6kHz时建立到99%大概需要几个毫秒。这个时间在不同应用里影响不一样用于灯光调光完全没感觉但如果是给伺服驱动器的速度环做模拟偏置几个毫秒的滞后在某些极端情况下会让人很不舒服。所以做工程要接受一个事实PWM模拟DAC本质上是拿响应速度换实现成本。追求更小纹波就得加大RC常数建立时间随之变长追求快速响应就得提高PWM频率但同时分辨率会下降不然就得用更高主频的MCU。我在工程里最后的折中是PWM频率定在20kHz一阶RC截止频率约1.6kHz纹波控制在20mV以下建立时间在几毫秒到十几毫秒之间符合项目需求。值得注意的是有时候RC截止频率设得太低会导致输出存在明显的过冲或缓慢爬升现象。我在调另一路输出时把C从0.1μF换成1μF结果建立时间从几毫秒变成几十毫秒肉眼可见电压还在慢慢趋近目标值。后来根据负载需求把R调小到220ΩC保持1μF截止频率又回到约723Hz建立时间缩短到可接受范围。调参数时必须用示波器看实际建立特性光靠纸上计算远远不够。4.3 故障保护PWM失效时的后手热词里有人提到“PWM故障保护”这个我在实际项目里真的吃过亏。多路PWM输出如果某一通道的信号因为程序跑飞、定时器配置错误而停止翻转输出端电压可能直接掉到0V或者跳到满量程如果这个电压控制的是功率级的偏置轻则输出异常重则烧后级。所以我在设计了完善代码后加了几层保护措施。软件看门狗主循环里定期喂狗一旦程序跑飞看门狗复位MCUPWM恢复默认安全占空比。默认占空比策略上电时所有通道先输出安全电平比如0V对应占空比0等产品初始化完成后再按指令调整避免上电瞬间电压乱跳。短路保护预留RC输出端串一个阻值很小的采样电阻配合比较器做输出过流检测这个成本极低但对恶劣负载场景帮助很大。如果不加这些前面调得再完美一旦系统跑飞多路PWM输出的电压就可能失控。从工程可靠性角度讲PWM故障保护不是锦上添花而是必须优先考虑的后手。5. 常见问题与排查技巧实录5.1 一个表格说清高频故障我把这几年用这个方案时遇到的问题整理成了一张表基本覆盖了大多数调试场景。现象可能原因排查与解决输出电压偏低且带载跌落RC输出电阻过大后级负载吃电流减小R或在后级加运放跟随器输出纹波过大PWM频率太低或RC截止频率偏高提高PWM频率或加大RC常数多路调压时通道互相干扰各路共用地线阻抗过高去耦不足各路单独RC模拟地单点汇合增加去耦电容某通道PWM波形始终为高或低引脚被JTAG/SWJ复用占用或重映射没生效关闭JTAG复用检查重映射配置占空比调大后输出电压非线性V_ref受负载影响波动实测并修正V_ref必要时加缓冲运放上电瞬间输出非预期电压初始化完成前占空比未设安全值上电先设默认安全占空比再执行控制逻辑这张表里的每一条都是我实际碰到过的不是理论推测。尤其是第一条当时我把R取到10kΩ结果输出端带个100kΩ负载就直接掉了一截电压这种问题用示波器冷眼看着很难发现只有装上后级负载才暴露。5.2 调整PWM频率时要注意分辨率突变有不少人从网上拉一份例程把PWM频率一改占空比计算还是按老参数来结果输出电压对不上。原因在于ARR变了之后占空比计数值也要等比换算。举个例子原来ARR3599想输出50%占空比对应CCR1800如果为了降纹波把PWM频率改成10kHzARR变成7199这时候还写1800输出就只有25%。我的习惯是把“目标电压→占空比计数”这个换算封装成一个函数所有代码都走这个入口修改PWM频率时只要同步修改函数里的比例系数就不会出现这种低级错误。uint16_t voltage_to_duty(float volt, float v_ref, uint16_t arr) { if (volt 0.0f) return 0; if (volt v_ref) return arr; return (uint16_t)((volt / v_ref) * (float)arr); }这个函数看着简单但能把“电压→占空比”的换算逻辑统一起来避免多处手写计算、比例不一致的问题。我在这套工程里所有通道的电压配置都通过它换算后续维护时省了不少心。5.3 提升精度的独家技巧如果项目对输出电压精度要求比较高还有几个技巧可以叠加使用。软件校准输出电压用ADC回采一遍和设定值比对误差存到Flash下次输出时补偿。这是我用的最多的做法能把误差从0.5%压到0.1%以内。用更高分辨率的PWM如果MCU支持可以把多个定时器级联组成更高分辨率的PWM输出。F103的级联能力弱一些新出的M4、M7片子做起来更方便。二阶RC滤波在空间允许的情况下多一级RC能把纹波再压下去一大截代价是输出建立时间又翻倍。结束语与个人经验总结最后再分享一点我个人的体会。用PWM模拟DAC这事儿说白了是在成本和性能之间找平衡很多做硬件的朋友一听“模拟DAC”就觉得不稳但真按工程方法做把RC参数、PWM频率、输出缓冲都算清楚实际用起来是完全能上量的。最怕的是拿个原理图就抄连截止频率都不算量到纹波超标就说方案不行。我在多个项目里用这招应急最久的一版产品用了两年多输出电压稳定度一直在线。这套“多路PWM模拟DAC”源码和文档我在打包的时候整理成了“多路pwm.zip”工程里除了标准库工程还包含了硬件参数计算表格老规矩有需要研究的直接下载看有问题欢迎在评论区交流我看到都会回。本文还有配套的精品资源点击获取
返回列表