ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 PWM DAC实验:从RC滤波到模拟输出

STM32 PWM DAC实验:从RC滤波到模拟输出 简介面向STM32进阶开发者的PWM DAC实验资料包涵盖定时器PWM输出与DAC数模转换的完整工程实现。资源以HAL库工程源码为主体包含30个头文件与22个C源文件涉及USMART调试组件、OLED、LCD、DAC、定时器及串口等外设驱动另有工程配置文件、链接脚本和烧录辅助脚本方便直接编译调试。压缩包仅238KB共63个文件结构按HARDWARE、SYSTEM、USER等模块划分便于对照学习。目前已获411人学习浏览适合正在练习STM32信号生成、希望通过实际工程理解PWM与DAC协同工作的开发者。通过该资源可快速搭建实验环境查看外设初始化与波形输出的完整代码逻辑缩短从原理到实现的摸索时间。1. 这个PWM DAC实验解决的工程问题很多工程师第一眼看到这个实验会觉得奇怪STM32F429内部明明有独立的12位DAC硬件为什么还要折腾PWM去拼一路DAC但真实项目里这种方案反而常见。低成本的STM32G0、F0系列经常没有DAC或者DAC通道只有一路而PWM输出几乎每个定时器都有一颗电阻一颗电容就能换一路模拟输出。这个实验把PWM生成、RC滤波、ADC回读串成一条完整链路HARDWARE目录里同时保留了TIMER、DAC和ADC模块意思很明确先用PWM产生模拟电压再用ADC验证最后拿内置DAC做对比。我认为适合两类人一类刚学完定时器PWM、想搞清楚占空比如何变成电压的开发者另一类是选型阶段正在评估“PWM代替DAC”是否能满足精度指标的应用工程师。2. PWM转DAC的理论与RC滤波参数计算2.1 PWM输出平均电压与等效位数PWM输出的本质是固定频率的方波高电平时间由比较寄存器CCR决定。对于向上计数模式的定时器一个PWM周期内计数器从0增长到ARR输出电平在CCR处翻转因此占空比D CCR / (ARR 1)。如果引脚高电平近似为VDD、低电平近似为0V那么经过低通滤波器后的平均电压为V_out ≈ VDD × (CCR / (ARR 1))当CCR从0连续增大到ARR时平均电压从0线性变化到VDD。这意味着PWM引脚的“可控制电平级数”是ARR1等效DAC位数是log2(ARR1)。这是PWM DAC最核心的概念。这里有一个常见误解以为PWM频率越高越好。从平均电压角度看ARR1决定了电压分辨率而PWM频率由定时器时钟和ARR共同决定。STM32的16位计数器限制ARR最大为65535所以如果PWM频率太高ARR会被压缩占空比调节级数反而下降。比如PWM达到1MHz时ARR只有89满量程只有90个电压台阶等效不到7位精度这样的PWM DAC只能用于对精度要求很低的应用。2.2 一阶RC滤波的截止频率、纹波与负载效应PWM平均电压需要经过低通滤波才能变成平滑的模拟信号。最简单也最常用的是单级RC低通滤波器其截止频率为fc 1 / (2 × π × R × C)滤波后的残余纹波主要来自PWM基波分量。对于一阶RC近似估算纹波峰峰值为V_ripple ≈ VDD × fc / f_pwm这个近似在占空比50%附近最坏且要求fc远小于f_pwm。举个例子VDD3.3Vf_pwm100kHzfc取1kHz那么理论纹波约33mV如果把fc降到500Hz纹波降到16.5mV但信号的带宽也同步收窄。输出正弦波时如果信号频率接近fc会发生明显衰减和相位滞后。实际设计里R和C还带来输出阻抗问题。RC滤波器的输出阻抗在低频时约等于R如果后面直接接低阻抗负载负载分压会让输出电压明显下跌。典型做法是在RC输出后面接一个运放跟随器或者让R小于负载的1/10。对于ADC采集来说ADC的采样电容在每个采样周期会从RC网络抽取电荷如果R太大采样结果会偏低所以必须把串联电阻控制在几千欧以内或者像很多开发板那样接一个缓冲器。2.3 从F429时钟树反推PWM参数STM32F429在典型配置下SYSCLK为180MHzAHB为180MHzAPB1时钟45MHzAPB2时钟90MHz。关键点在于APB1和APB2预分频系数不为1时定时器时钟是APB时钟的两倍。因此APB1上的定时器TIM2~TIM7、TIM12~TIM14时钟是90MHzAPB2上的定时器TIM1、TIM8~TIM11时钟是180MHz。这个差别直接影响PWM频率计算。PWM频率计算公式为f_pwm TimerClock / ((PSC 1) × (ARR 1))为了在固定PWM频率下让占空比分辨率尽量高我们应该首先尝试PSC0因为此时ARR接近TimerClock/f_pwm尽可能大。如果计算出的ARR超过65535再逐步增大PSC。下面列出TimerClock90MHz时不同PWM频率对应的参数PWM频率PSCARR1电压级数等效位数建议fc约f_pwm/100估算纹波VDD3.3V1kHz1450004500015.5bit10Hz0.33mV10kHz09000900013.1bit100Hz3.3mV50kHz01800180010.8bit500Hz16.5mV100kHz09009009.8bit1kHz33mV1MHz090906.5bit10kHz330mV从表格能明显看出PWM频率越高滤波器截止频率也能设计得越高输出信号带宽越大但电压分辨率越差。实验里如果目标是输出1kHz正弦波我会把PWM设为100kHzfc放在1~3kHz之间既保证波形幅度损失不大也把纹波控制在几十毫伏内。3. 基于HAL的PWM DAC实现从GPIO到正弦波3.1 外设规划与GPIO复用配置这个实验里我选择TIM3作为PWM产生器通道1输出到PA6。为什么不用TIM1或TIM8因为高级定时器有额外的刹车和死区逻辑初学者容易配置出互相矛盾的状态普通定时器更干净。TIM3挂在APB1总线上时钟90MHz够用。GPIO部分需要把PA6复用为TIM3_CH1。用STM32CubeMX生成代码时PA6的Alternate功能选AF2即TIM3。同时我习惯把RC滤波器的RC节点引到PA1用于ADC采集但注意PA1要配置为模拟输入否则GPIO内部施密特触发器会增加额外漏电流和噪声。需要注意的是PA6输出的是PWM方波经过RC滤波后才成为模拟量如果OLED显示电压OLED只接到滤波节点不能直接接PA6。3.2 TIM3 PWM初始化代码下面的初始化函数不依赖CubeMX生成的固定配置而是根据目标PWM频率自动计算PSC和ARRstatic TIM_HandleTypeDef htim3; static TIM_OC_InitTypeDef sConfigOC; void MX_TIM3_PWM_Init(uint32_t pwm_freq) { uint32_t timer_clock 90000000; // TIM3时钟源90MHz uint16_t psc 0; uint16_t arr 0; // 从PSC0开始找第一个ARR不超16位计数器的组合 for (psc 0; psc 65535; psc) { uint32_t count_freq timer_clock / (psc 1); uint32_t tmp_cycle count_freq / pwm_freq; if (tmp_cycle 0 tmp_cycle 65536) { arr (uint16_t)(tmp_cycle - 1); break; } } __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance TIM3; htim3.Init.Prescaler psc; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period arr; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse arr / 2; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); }这段代码里最关键的是PSC和ARR的匹配逻辑。计数器时钟是timer_clock/(PSC1)PWM周期需要的计数次数是count_freq/pwm_freq也就是ARR1。循环从PSC0开始保证ARR在当前PWM频率下尽可能大占空比分辨率最高。以100kHz为例计数值count_freq90MHzcycle900ARR899若误把PSC设成89ARR会降到9PWM仍能输出但只剩10个占空比台阶波形很容易出量化毛刺。Pulse arr / 2只是初始占空比50%。后面更新CCR时只需要调用__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, new_pulse)即可。3.3 查表法生成正弦波数据要输出正弦波不能实时调用sin函数因为浮点运算会占用过多CPU而且PWM更新间隔很短。常见做法是预处理一张正弦波形查表表里每个元素是当前相位对应的CCR值。生成表可以用上位机脚本也可以在开发板上电时用一次浮点运算生成但更推荐固定数组。用Python生成C数组是很顺手的做法import math N 128 # 每周期采样点数 ARR 899 # 与PWM初始化保持一致 arr [] for i in range(N): duty int((1.0 math.sin(2 * math.pi * i / N)) * ARR / 2) arr.append(duty) print(, .join(str(x) for x in arr))逻辑说明(1sin)把正弦波从[-1,1]映射到[0,2]再乘以ARR/2使CCR在0到ARR之间变化。这样正弦波的平均电压是VDD/2正负半周对称。选择N128时正弦波频率等于PWM频率除以128如果PWM是128kHz正弦是1kHz。N越大波形越平滑但最高可输出频率会降低实验中我通常选128或256。3.4 在PWM更新中断里动态修改CCR生成正弦波还需要按固定周期把查表值写到CCR。最简单可靠的方法是用同一个定时器的更新中断也就是每个PWM周期更新一次CCR。中断服务函数可以写成static volatile uint16_t phase 0; void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim3, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, pwm_table[phase]); phase (phase 1) % TABLE_SIZE; __HAL_TIM_CLEAR_FLAG(htim3, TIM_FLAG_UPDATE); } }这样每个PWM周期占空比都会跳变到查找表里的下一项。如果PWM频率是128kHz表长128输出正弦波就是1kHz。整个过程中CPU只做了一次数组取值和寄存器赋值耗时极短不会影响PWM输出。唯一要注意的是如果还在中断里做浮点运算或打印调试信息中断周期会拉长导致CCR更新滞后波形出现抖动。USMART组件里的delay和printf都不要在更新中断里调用。更极端一点的做法是使用DMA循环模式把整张查找表通过DMA自动搬运到CCR完全不需要中断。DMA方式的优点是更新步骤由硬件完成适合连续输出高频波形。代价是DMA流的映射关系要查参考手册而且TIM3_CH1的DMA请求可能和别的外设冲突。对实验来说上一段的中断实现已经足够验证PWM DAC原理DMA留到做工程化时再换也不迟。4. 用ADC回读和内置DAC验证PWM DAC4.1 ADC采样与移动平均PWM DAC输出到底准不准不能只靠示波器目测最好用STM32自己的ADC回读。这个实验里RC滤波节点连接到PA1我将ADC1的通道1配置为软件触发连续采样。读取时注意不要只采一次因为纹波和开关噪声仍然存在单次采样结果会跳动。下面是一段简单的多次采样平均函数uint32_t read_adc_average(ADC_HandleTypeDef *hadc, uint32_t times) { uint32_t sum 0; HAL_ADC_Start(hadc); for (uint32_t i 0; i times; i) { if (HAL_ADC_PollForConversion(hadc, 10) HAL_OK) { sum HAL_ADC_GetValue(hadc); } } HAL_ADC_Stop(hadc); return sum / times; }逻辑说明连续启动多次转换累加后取平均等效于对输出数字量做低通滤波。times取多少要权衡我一般取32或64次既抑制纹波又不牺牲实时性。转换结束后实际电压等于ADC值 × VREF / 4096。如果3.3V基准则每个LSB约0.8mV。特别提醒ADC的采样时间配置不能太短。STM32 ADC采样电容大约是几个pF如果前级RC网络电阻较大采样保持时间不足会导致采样电压建立不充分通俗讲就是“采到的值总是比实际小”。我把采样周期配置为480个时钟周期同时将RC滤波的R控制在1kΩ以内C取100nF这样建立时间足够误差会小很多。4.2 PWM DAC与内置DAC的关键差异工程里既然有DAC驱动就不要浪费。把同一张正弦查找表写到DAC数据寄存器再用同样ADC采样能得到一组直观的对比数据。下表是我在F429上实测时会关注的差异对比项PWM RC滤滤滤波内置DAC输出延迟受RC时间常数影响变化滞后寄存器更新后立即输出分辨率由ARR1决定位数可变固定12位纹波几十mV到更大取决于fc很小约1~2个LSB驱动能力输出阻抗近似R不适合直连负载运放输出相对更强最高信号频率受fc限制PWM频率很难兼顾分辨率可达数MHz级别更新速率有限硬件成本至少一颗电阻一颗电容无需额外器件PWM DAC在信号路径上多一个RC滤波器这个滤波器同时带来了好处和坏处。好处是天然平滑了输出信号坏处是信号带宽受限且因为PWM周期是离散的输出本质上是阶梯波。内置DAC虽然也有建立时间但它连续输出波形质量明显更好。实验如果要求失真度低我还是会用真正的DAC。4.3 实验中最容易翻车的三个点第一个坑是RC滤波器的R选得过大。比如R47kΩ、C10nF时fc约338Hz看起来挺好但ADC输入阻抗通常在几千欧到几十千欧之间接上去等于在47kΩ上并了一个负载输出电压直接跌破预期。我一般会让滤波R不超过10kΩ且ADC采用率足够低或者干脆加运放跟随。第二个坑是PWM极性配置错误。TIM_OCMODE_PWM1模式下向上计数时CCR以上的部分输出有效电平如果误配成PWM2占空比会变成反比。更隐蔽的是高级定时器的OCREF清0逻辑普通TIM3没有这个问题所以实验里我始终建议先不用TIM1。第三个坑是PCB布局。PWM引脚到滤波电阻的走线如果与ADC采样线贴近开关噪声会通过寄生电容耦合到模拟采样回路。我见过不少实验板飞线接出来的波形毛刺很大而不是RC正常滤波后的平滑信号。实际焊接时尽量让PWM走线和ADC采样线分开地线用同一个模拟地不要形成大环路。5. 从实验到实用分辨率提升与故障保护设计5.1 用软件抖动提高有效分辨率PWM DAC的物理分辨率受ARR限制但可以通过占空比跳变来“赚”额外有效位数。思路是如果目标电压落在两个相邻PWM占空比之间就在多个PWM周期里交替输出这两个占空比让平均电压落在中间。比如ARR899时相邻台阶约3.67mV如果一组周期里70%时间输出CCR45030%时间输出CCR451平均等效0.7×4500.3×451450.3相当于把步进细分到了0.3个LSB。这种抖动方式虽然提高了有效分辨率但会引入额外噪声输出带宽也下降实际使用时需要评估。5.2 高级定时器的故障保护与刹车输入如果PWM DAC以后要驱动外部功率级比如PWM控制电机或LED电源必须考虑故障状态下的输出安全。STM32高级定时器TIM1/TIM8带有刹车Break输入检测到刹车信号后PWM输出可以强制变为低电平或高阻态。配置一段示例TIM_BreakDeadTimeConfigTypeDef brk {0}; brk.OffStateRunMode TIM_OSSR_DISABLE; brk.OffStateIDLEMode TIM_OSSI_DISABLE; brk.LockLevel TIM_LOCKLEVEL_OFF; brk.DeadTime 0; brk.BreakState TIM_BREAK_ENABLE; brk.BreakPolarity TIM_BREAKPOLARITY_HIGH; brk.BreakFilter 0; HAL_TIMEx_ConfigBreakDeadTime(htim1, brk);逻辑说明把TIM1的刹车功能使能并设置极性为高电平有效。当刹车输入引脚变高时PWM通道立即封锁输出防止过流损坏。这个机制和PWM DAC本身没有直接关联但凡是把PWM输出接到功率电路的实验都应该在开发阶段就把故障保护加上。5.3 向其他MCU移植时重点看时钟和DMA实验代码若想换到APM32、PY32这类兼容方案上不能直接复制就完事。先改定时器时钟APM32F103的TIM1挂在APB2上最大72MHzPY32F003常用48MHz内部RCPWM频率和ARR要重新计算。其次是GPIO复用表不同MCU的AF编号不同。再次是DMA请求映射TIM3_CH1在F429上挂的DMA流和F103不一样如果强行用DMA循环查参考手册“DMA request mapping”表最稳。最后别忘了看ADC参考电压PY32内部基准可能不是3.3V直接影响电压换算系数。PWM DAC的精髓并不在某个具体系列而是那套“占空比—平均值—滤波”的链路。理解了这个换芯片只是改几个时钟参数和引脚编号的事。本文还有配套的精品资源点击获取
返回列表