news 2026/9/9 18:49:51

多路PWM模拟DAC实战:从RC滤波到STM32实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多路PWM模拟DAC实战:从RC滤波到STM32实现指南

简介:基于STM32的多路PWM模拟DAC输出电压工程包,聚焦PWM脉宽调制技术在高精度多通道电压输出场景的应用。工程基于STM32与Keil5环境开发,利用12路PWM通道通过调节占空比生成最高3.2V的可变电压,可用于LED调光、电机调速、模拟量输出等嵌入式开发实践。资源共530个文件,压缩包约7.95MB,包含134个h头文件与115个c源文件等完整工程代码,以及uvprojx工程文件、hex/axf编译下载文件、s启动汇编与lst列表文件,可直接在Keil5下打开、编译并烧录验证;readme与pdf文档补充了使用说明和原理介绍。目前已有612人学习浏览。配套源码结构清晰,可快速理解多路PWM通道的初始化配置、占空比调整与模拟电压输出逻辑,对正在学习STM32定时器/PWM外设、想掌握软件模拟DAC方法的开发者具有直接参考价值。 多路PWM模拟DAC输出电压这事儿,我在不少项目里都遇到过。MCU自带的DAC通道通常就那么一两个,碰上需要输出好几路可调电压的场景,比如程控增益、偏置调节、多路基准源,要么外挂DAC芯片,要么就得想办法用现有的PWM资源硬凑。这篇文章我就把这套“多路PWM模拟DAC”的方案完整拆开讲:从原理、滤波计算,到STM32上的配置代码、标定流程,再到我实际踩过的坑,一次性说清楚。适合手头有MCU开发板、想在不出钱买DAC芯片的情况下解决多路模拟输出的朋友参考。

1. 为什么用多路PWM去模拟一个DAC

1.1 什么场景适合这种方案

先捋一下需求。很多嵌入式项目里,“输出电压”不一定要求高精度、高带宽,只要是能调的直流电平就行。比如:

  • 给运放提供偏置电压,实现零点调节
  • 控制VCO的调谐电压,改变输出频率
  • 做可调比较器阈值
  • 多路LED亮度控制,虽然这个一般用PWM直驱,但某些需要模拟调光信号给驱动芯片的场景就得转成直流

这类需求电压范围可能就是0~3.3V或者0~5V,精度要求1%左右,更新速度几毫秒一次就够。这时候用PWM加RC低通滤波变成直流电平,成本几乎为零,纯粹靠MCU的定时器资源就能实现。

我见过有人一上来就纠结“PWM模拟DAC不干净,不如买DAC芯片”,但在实际产品里,能用一颗几毛钱的MCU解决的事情,硬要加一颗几块钱的DAC芯片,成本和PCB面积都被浪费了。方案选型永远是看需求边界,而不是看参数表谁好看。

1.2 和真DAC对比能省什么、要付出什么代价

直接列个对比,大家心里有数:

维度多路PWM模拟DAC外置真DAC芯片
硬件成本几乎为零(几个电阻电容)芯片本身几块到几十块不等
通道数扩展取决于定时器/引脚资源,容易扩展每路都要一颗芯片或选多通道型号
输出建立时间慢,RC时间常数决定,ms级常见快,us级甚至更快
纹波/噪声RC滤波后仍有残余纹波,需权衡由芯片指标决定,通常很干净
精度受限于PWM分辨率、RC稳定性和温漂由DAC位数和基准决定
软件复杂度需要维护占空比映射和补偿表写寄存器就行

一句话总结:PWM模拟DAC是用时间换硬件。如果系统本身对输出精度要求不是特别苛刻,更新速度也不快,这招非常香。反过来,如果要做音频波形输出或者高精度可调电源,老老实实上真DAC,别折腾PWM方案。

2. PWM转DAC的核心原理:占空比、滤波和纹波

2.1 PWM信号怎么看

PWM输出本质上是数字方波,只有高电平和低电平两个状态。它的平均值等于:

V_avg = V_H × D

其中D是占空比,取0到1。如果高电平是3.3V,占空比50%,平均值就是1.65V。这个公式是整个方案的基础。

但平均值不等于物理输出。直接把PWM引脚接负载,你测到的是方波,不是直流。要拿到接近直流的电压,必须经过低通滤波器,把方波里高频分量滤掉。滤波器截止频率要远低于PWM频率,同时要保证负载不影响滤波特性,后面会详细算。

2.2 低通滤波器和RC参数计算

最常用的一阶RC低通,便宜、简单。关键参数就俩:R和C。截止频率:

f_c = 1 / (2π × R × C)

设计原则有两个:

  1. 截止频率要远低于PWM频率。一般取PWM频率的1/10到1/100。低于这个范围纹波更小,但响应变慢。
  2. 输出端负载阻抗要高。RC滤波后的输出节点如果有负载电阻,会跟C形成分压,导致电压误差。

举个例子。PWM频率20kHz,想做到纹波尽可能小,同时建立时间接受,取f_c = 200Hz。若R = 10kΩ,则:

C = 1 / (2π × 10kΩ × 200Hz) ≈ 79.6nF

取标称值100nF的话,实际f_c ≈ 159Hz。此时10kΩ电阻和100nF电容的时间常数τ = 1ms。从占空比变化到输出稳定到63.2%需要1ms,稳定到99%需要约4.6ms,也就是约4.6ms。这个响应速度对前面说的偏置调节场景完全够用。

如果想要响应更快,同时保持低纹波,可以加一级有源二阶滤波,或者用多个RC级联。不过对于大多数应用,一阶滤波就够用了。

2.3 关于“多路”的取舍:独立通道还是加权合并

标题里写的是“多路PWM模拟DAC”,这个词有两种理解:

  • 多个PWM通道,每一路都各自滤波,输出一个独立可调的模拟电压
  • 多路PWM信号通过电阻网络加权合并成一路更高分辨率的DAC

第二种方案业内也叫PWM加权DAC,用多位PWM叠加出高分辨率输出。比如3个PWM通道分别对应1/2、1/4、1/8的权重,电阻比例做到2:1就能叠加出三位的效果。但实际操作中非常痛苦,因为电阻精度、温漂、PWM相位不一致都会引入误差,精调起来相当费劲。

我自己的建议是:老老实实走第一种,一路PWM对应一路独立模拟输出。多路的意义在于扩展通道数量,而不是强行提高单路分辨率。单路分辨率想要提高,用MCU定时器的更高位计数器配合低频PWM就能解决,没必要把多路的精度搞到一起。

3. 实操设计:以STM32为例搭建多路PWM模拟DAC

3.1 硬件连接与滤波电路

我以STM32F103ZET6为例,这个芯片定时器资源多,TIM1到TIM8加基本定时器,随便挤出来4路PWM很轻松。硬件连接很简单:

PWM引脚 —— 10kΩ电阻 —— 节点A —— 100nF电容 —— GND

节点A就是模拟输出。如果后面接的负载阻抗不够高(比如小于100kΩ),建议在节点A后面加一个电压跟随器,用运放做阻抗变换。

这里踩过的一个坑是:RC滤波电阻不敢选太大。曾经我为了减小电流,把R选到100kΩ,结果输出阻抗特别高,后续接了一个100kΩ对地的测量电阻,电压直接掉了将近一半。后来才反应过来,输出阻抗必须远小于负载阻抗,否则你看到的电压就是分压结果而不是PWM平均值。

3.2 TIM配置与占空比计算

以TIM3为例,配置四路PWM输出。PWM频率选择要综合分辨率。

STM32F103的TIM3挂在APB1上,时钟72MHz。设置预分频PSC=71,则计数器时钟为1MHz。自动重载值ARR设成999,则PWM频率为:

f_PWM = 1MHz / (999 + 1) = 1kHz

占空比分辨率是1/1000,也就是大约相当于10位DAC。如果觉得1000分之一不够用,可以把ARR调到3999(PWM频率250Hz),分辨率提升到4000分之一,约12位。代价是滤波截止频率要相应降低,否则纹波明显。

代码示例(基于标准外设库的写法,用寄存器也能达到同样效果):

// 初始化GPIO,将PA6、PA7、PB0、PB1配置为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_TIM3, DISABLE); // 默认映射即可,不重映射 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // PA6对应TIM3_CH1, PA7对应TIM3_CH2, PB0对应TIM3_CH3, PB1对应TIM3_CH4 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_Init(GPIOB, &GPIO_InitStructure); // 配置TIM3 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 999; // PWM频率1kHz,分辨率1000 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 配置四个通道为PWM1模式 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初值占空比0% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_OC3Init(TIM3, &TIM_OCInitStructure); TIM_OC4Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE);

然后改变输出电压只需要设置比较寄存器:

TIM_SetCompare1(TIM3, 500); // 占空比50%,理论输出1.65V TIM_SetCompare2(TIM3, 250); // 占空比25%,理论输出0.825V

值得注意的是,TIM_SetCompare函数的参数是0到ARR之间的值。如果你用的是自动重载值和PSC的配置,占空比就是CCR除以ARR。算好输出目标电压对应的CCR值,往下写就行。

3.3 输出标定和精度提升

理论上公式很简单:V_out = (CCR / ARR) × V_ref。但实际测量总会偏离一点,原因是:

  • 引脚输出高电平并不是精确的3.3V,而是略低一点,大概3.25V到3.29V
  • RC电阻有误差,电容漏电流也会影响最终电压
  • 负载阻抗带来的分压效应

所以实际项目里我一般会做两点标定。方法:设置占空比0%和99%两个点,用万用表读取实际输出,然后做线性映射。代码里把目标电压换算成实际需要写入的CCR值。

比如实测0%输出是2mV(不完全到0,因为引脚内部有上拉/下拉干扰),99%输出是3.26V,那实际斜率就是(3.26V - 0.002V) / 0.99 = 3.29V左右。你可以把这些校准数据存到Flash里,上电自动加载。

如果对精度要求更高,还可以做多点标定(比如10%、50%、90%三个点),用分段线性插值修正非线性。对于RC滤波+PWM的方案,非线性主要来自RC时间常数变化,做三点标定基本就够了。

4. 多路扩展中的常见坑

4.1 100%占空比异常

这一点我必须单独列出来。STM32定时器输出PWM时,当CCR设置等于ARR+1或者某些特殊值时,输出不一定保持高电平,而可能表现为窄脉冲甚至反相,这取决于定时器的计数器模式和输出极性配置。某些芯片在计数器溢出瞬间,如果CCR刚好等于ARR,占空比输出会异常。这个在STM32F103上反而是小概率,但在其他系列上常见。

解决方法是:设置占空比时不要直接让CCR等于ARR或0,而是用ARR-1表示最高占空比,用1表示最低占空比。或者使用比较模式时,确保CCR的范围在1到ARR-1之间。极端情况下如果确实需要0%或100%输出,直接控制GPIO电平切换,不要依赖PWM外设。

这个逻辑我在多个项目中反复检验,90%的“占空比不对”问题都可以用这个办法绕开。

4.2 PWM故障保护与停振

热词里反复出现“PWM故障保护”。其实这是定时器刹车(Break)功能在作怪。STM32高级定时器TIM1和TIM8有刹车输入功能,一旦刹车信号触发,PWM输出会被强制拉到安全电平。如果你初始化了高级定时器但没有正确配置刹车功能,哪怕刹车引脚悬空,也可能因为噪声误触发导致输出异常。

对于多路模拟DAC,建议直接用通用定时器TIM2、TIM3、TIM4这些没有刹车功能的定时器,能省掉很多麻烦。如果非要用TIM1/TIM8,必须显式配置:

TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = 0; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE);

很多新人卡在高级定时器上输出PWM没有波形,就是因为漏了最后那一行TIM_CtrlPWMOutputs(TIM1, ENABLE),这是高级定时器特有的主输出使能,不打开的话引脚一直是高阻或无效状态。

4.3 死区配置的注意点

热词里提到28335配置PWM死区DBFED和DBRED,这是TI DSP里C2000系列的死区配置。虽然DSP和STM32是不同平台,但死区的原理是相通的:死区是为了防止上下桥臂同时导通而插入的一段延迟。我在做电机驱动时配置过F28335的PWM死区,这里提醒一点——如果你只是想模拟DAC输出,千万不要开死区功能。死区会让PWM的有效占空比变小,而且不是简单的线性偏移,会导致输出电压与理论值不符。

就算你是做电机控制,也要先搞清楚DBFED(死区上升沿延迟)和DBRED(死区下降沿延迟)各自的作用。如果设置不当,可能导致输出波形明显畸变,甚至引起桥臂直通。我当时调28335死区,本来以为只是延迟几微秒的事,实际上从配置寄存器到示波器验证花了整整一个下午。核心教训是先确认极性选择,再配时间值,最后看波形确认。

4.4 波形查看与滤波前后对比

调试PWM模拟DAC,示波器是必备工具。我常用的检查流程是:

  1. 直接看PWM引脚波形,确认频率和占空比正确
  2. 在RC滤波节点看直流电平,同时观察纹波幅度
  3. 切换占空比,观察输出建立时间和是否有过冲

如果你用Keil5调试STM32,可以用逻辑分析仪窗口或者MDK的波形仿真功能看PWM输出,但实际效果不如真示波器。我早期偷懒用仿真看波形,结果相位和时序全对,一上实物就出问题,追问原因才发现仿真里的寄存器初值跟实际硬件有差异。

滤波前后波形对比很重要。单纯PWM引脚上的方波再漂亮也不代表模拟输出就干净。我用示波器在RC节点看到的纹波一般在10mV到50mV之间(视滤波截止频率而定),如果纹波超出预期,优先检查滤波电容是否靠近输出节点、地线是否走好。

4.5 开关量PWM恒温控制的思路

热词里有“开关量PWM恒温控制”。这是个有意思的应用:用PWM控制加热器,通过调节占空比来改变平均加热功率,本质上和模拟DAC是一个思路——用时间换模拟量。区别在于被控对象是功率而不是电压。

这种场景下,PWM频率不能太高也不能太低。太高了,加热器开关损耗增大;太低了,温度波动明显。我一般用0.5Hz到10Hz之间的频率,配合温度传感器做PID闭环控制,输出的就是不断调整的PWM占空比,相当于一个动态变化的“模拟DAC”。如果你已经理解了前面讲的PWM平均值原理,这个应用就很自然了。

5. 变通应用与心得

5.1 从呼吸灯、舵机到RC电路

搜索热词里提到“用STM32F103ZET6的TIM3制作呼吸灯:从引脚重映射到PWM占空比调节”,这个我实测过。

呼吸灯无非就是让LED亮度呈正弦变化,也就是PWM占空比按正弦曲线变化。用一个定时器更新占空比,每次更新后延迟一小段时间,1000个等级从0变到999再反向变回来,就形成了渐变效果。引脚重映射这个点要注意,如果TIM3的默认引脚PA6、PA7被其他功能占用,可以用GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE)把引脚映射到PB4、PB5之类的位置。

舵机控制也是PWM占空比的经典应用。舵机需要的是周期20ms(50Hz)、脉冲宽度1ms到2ms的PWM信号。注意这里我们关心的是脉冲宽度而不是占空比百分比,虽然本质一样,但算起来更直观。拿STM32来说,设置PSC和ARR让PWM周期精确为20ms,然后CCR的值在1000到2000之间变化,对应0度到180度。

RC电路就更常用了。PWM信号直接驱动RC积分电路,本质就是低通滤波器,跟我们的模拟DAC原理完全一致。在做一些简单的RC延时或波形整形时,PWM频率、电阻电容三者要匹配,否则RC充放电还没完成,PWM就已经切到下一个周期,输出会一直在某个中间值附近波动。

5.2 我踩过的几个坑和心得

最后分享几个实操中容易翻车的点。

第一,多路PWM的相位同步问题。如果你把两路PWM引脚的输出同时滤波,并且要求两者之间电压差恒定,那么两路PWM最好由同一个定时器产生(如TIM3的四个通道),因为它们共用同一个计数器,相位严格同步。如果用两个定时器,相位差可能会导致毛刺和误差。

第二,电源去耦不能省。PWM输出瞬间会有电流变化,如果MCU电源引脚上的去耦电容不足,PWM切换瞬间会在模拟输出上产生毛刺,严重影响纹波指标。我一般在每个PWM输出引脚附近放一个100nF电容,模拟输出节点再接一个10uF电解电容。

第三,批量一致性。RC元件的精度直接决定输出电压的一致性。如果电阻电容精度只有5%,输出会偏差比较大。要想一致性更好,电阻选0.1%精度,电容选C0G材质的,成本也就多几毛钱,值得。

这个方案后期可以扩展的方向也很多。比如,把RC滤波换成运放构成的二阶低通滤波器,纹波可以压到很低;或者把多路PWM输出配合模拟开关,实现可切换的DAC通道矩阵。我自己最近的板子上,就是把6路PWM模拟DAC用在了电源模块的多个偏置引脚上,配合一个简单校准程序,精度做到了千分之五以内,已经完全替代了原来的DAC芯片方案。

本文还有配套的精品资源,点击获取

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

高通GPU内置AI核心:端侧AI算力融合的关键一跃

看到“高通给GPU装上专用AI核心”这条消息,我第一反应不是“又升级了”,而是“终于走到这一步了”。从早年的Hexagon DSP到后来的独立NPU,再到今天把AI核心直接设计进Adreno GPU内部,高通的算力架构终于和苹果的Neural Engine、英…

作者头像 李华
网站建设 2026/9/9 18:47:53

10 分钟上手 ShareX:免费开源截图、打码、录屏一次搞定

10 分钟上手 ShareX:免费开源截图、打码、录屏一次搞定 【免费下载链接】ShareX ShareX is a free and open-source application that enables users to capture or record any area of their screen with a single keystroke. It also supports uploading images, …

作者头像 李华
网站建设 2026/9/9 18:47:46

AI编程搭子实战:用OpenClaw开发Android五子棋全流程

最近在整理手头的AI辅助开发流程,正好朋友让我用OpenClaw做一个五子棋练手项目。花了一个周末把简易版跑通,整体体验还挺顺。这篇就记录一下整个过程中我是怎么拆需求、怎么写代码、怎么让OpenClaw给我当编程搭子的,给同样想用AI工具做Androi…

作者头像 李华
网站建设 2026/9/9 18:47:32

Ruffle 桌面版快速上手指南:2 分钟跑通 SWF 播放与 5 类常用入口

Ruffle 桌面版快速上手指南:2 分钟跑通 SWF 播放与 5 类常用入口 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 硬盘角落囤着老游戏和课件,双击却没人接得住——Ru…

作者头像 李华
网站建设 2026/9/9 18:46:43

Obsidian多端同步方案横评:官方Sync、WebDAV与Git组合拳

先说结论:折腾了两年,在Windows、macOS、Android、iOS四端之间反复横跳之后,我最终把主力同步方案固定成了“Obsidian官方Sync打底 Obsidian Git定期做版本备份 坚果云WebDAV当应急通道”的三保险组合。如果你不想花钱,那么简单…

作者头像 李华
网站建设 2026/9/9 18:45:21

行业消息热度排行和主要指数行情哪里一起看

行业消息热度排行和主要指数行情哪里一起看? 想同时看到行业消息热度排行和主要指数行情,可以把每日财经(https://findailys.com/)的行业关系页和盘面页一起用:行业关系页展示近 30 天行业消息量、归一化价格走势和相…

作者头像 李华