ARTICLE DETAIL

资讯详情

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

STM32 PWM DAC设计指南:原理、滤波与校准方法

STM32 PWM DAC设计指南:原理、滤波与校准方法 简介实验22 PWM DAC实验是一套面向STM32嵌入式开发者和硬件初学者的完整工程资源聚焦PWM与DAC的协同使用解决如何利用定时器输出占空比可调方波、并借助DAC生成连续模拟电压的问题。PWM通过占空比变化等效输出电压DAC直接输出模拟量两者配合可产生更平滑的波形适用于波形生成、电压控制等模拟输出场景。压缩包采用RAR格式共63个文件核心由30个h头文件和22个c源文件构成另含启动文件、hex固件、Keil工程与调试配置、串口调试组件和说明文档整体仅238KB目录按USMART、SYSTEM、HARDWARE等功能模块划分便于按需查阅和二次开发。已有411人学习/下载实验覆盖GPIO复用配置、定时器预分频与计数模式、PWM通道占空比设定、DAC触发与数据写入等关键环节全部代码可导入Keil直接编译学习。通过示波器观察波形并调整参数可直观理解模拟信号生成过程工程中保留的外设驱动与调试信息也为后续传感器采集、波形合成或闭环控制项目提供了可直接迁移的代码基础和排错思路适合自学与实验教学两用。1. 为什么用 STM32 的 PWM 当 DAC先把概念立住很多 STM32 开发板实验里标题写着“实验22 PWM DAC实验”或者“STM32PWMDAC实验”第一次看到的人多少会疑惑PWM 不是数字方波吗DAC 是模拟输出两个词怎么能并列。这里的关键是方波经过低通滤波后直流分量等于高电平乘以占空比占空比改变等效输出电压就改变。只要对更新速率要求不高完全可以用一路 PWM 加一两个阻容元件当作一路便宜的单通道 DAC 用。常见应用包括 LED 亮度调节、风扇恒速控制、简单波形发生器甚至让单片机去控制外部的 0~3.3V 可调电平接口。这篇文章会从 PWM 转 DAC 的数学关系入手再讲 STM32 定时器配置、RC 滤波与 PCB 布局最后给出一套能直接用的 ADC 校准与故障保护做法适合还没到上专用 DAC 芯片阶段的嵌入式项目。2. PWM DAC 的数学原理占空比计算公式与分辨率边界2.1 平均电压公式为什么占空比能当模拟输出PWM 信号只有高电平 V_H 和低电平 V_L 两个状态若 V_L 0经过低通滤波器后输出稳定在长时间平均值V_out V_H × D V_H × CCR / (ARR 1)在 STM32 定时器里ARR是自动重装载值CCR是比较寄存器值。只改CCR就能让占空比在线变化输出电压随之线性变化。这个线性关系成立的前提是 RC 滤波的时间常数远大于 PWM 周期并且后级负载阻抗远大于 RC 电阻否则负载会分压输出会偏离计算值。实际写代码时不要在代码里用百分比去算而是直接算CCR。比如目标电压是 1.65V参考电压是 3.3VARR 设为 7199那么CCR 1.65 / 3.3 × 7200 3600。这样做的好处是占空比永远不会因为浮点舍入误差出现跳变而且每一步都是整数示波器检查波形时更容易看出阶梯是否均匀。这里有一个容易踩的坑占空比公式是CCR / (ARR 1)不是CCR / ARR。ARR 等于 7199 时如果用后者计算CCR 最大只能到 7199输出永远达不到 100% 占空比虽然误差很小但在做满量程校准时会发现高段非线性。2.2 滤波截止频率和纹波RC 时间常数怎样权衡一阶 RC 低通的截止频率为f_c 1 / (2 × π × R × C)PWM 基频是 f_pwm经过 RC 后基频和谐波被衰减输出纹波近似为V_ripple ≈ V_H × D × (1 - D) / (f_pwm × R × C)从公式可以看出纹波与 R×C 成反比与 PWM 频率成反比。但 R×C 越大建立时间越长R 太大又会被后级负载分压所以不能盲目加大 RC。常见做法是把截止频率设为 PWM 频率的 1/10 到 1/50。下面这组参数按 3.3V、占空比 50% 估算可直接用于 STM32 实验板PWM频率目标截止频率电阻 R电容 C理论纹波D0.51kHz100Hz1.6kΩ1.0μF约70mV后级需要再滤波10kHz1kHz1.6kΩ0.1μF约7mV建立时间约0.8ms50kHz2kHz1kΩ0.1μF约1.9mV建立时间约0.3ms100kHz3kHz510Ω0.1μF约1.1mV建立时间约0.17ms纹波估算没有把电容 ESR 和输出负载算进去实际数值会略大。只要能控制在 ADC 的 1~2 LSB 以内一般不影响使用。如果你要驱动 LED这点纹波根本看不出来如果要给传感器供电作偏置电压则要尽量选 PWM 频率高的一档。2.3 分辨率边界STM32 定时器时钟频率是上限STM32 的 PWM 计数器由定时器时钟驱动F103 系列内部是 72MHz。占空比每一步对应计数器一个时钟周期分辨率位数和 PWM 频率的关系是bits log2(ARR 1) f_pwm f_clk / ((PSC 1) × (ARR 1))PSC是预分频系数。ARR 越大分辨率越高但 PWM 频率越低。我们可以用一小段 Python 直接算避免手算误差import math clk 72_000_000 # STM32F103 定时器时钟 for arr in (7199, 3599, 719, 99): f_pwm clk / ((0 1) * (arr 1)) bits math.log2(arr 1) print(fARR{arr:5d} - {f_pwm:8.1f}Hz, {bits:5.2f}bit)这段代码把 PSC 固定为 0打印出的结果很直观ARR7199 时 PWM 频率是 10kHz分辨率约 12.8 位ARR99 时频率到 720kHz但分辨率只有 6.7 位。所以不要凭空追求“高分辨率 PWM DAC”先看目标的更新率和纹波要求。如果只做直流电压输出PWM 频率 1kHz 都够分辨率可以做到 16 位如果要做音频波形20kHz 以上才够分辨率通常要降到 8~10 位。另外定时器时钟若是内部 RC 振荡器频率会随温度漂移直接影响 RC 滤波后的直流电压精度。位置允许时尽量用外部晶振作为定时器时钟源这样 PWM 的“平均电压”才是真正稳定的。3. STM32 上配 PWM DAC定时器配置、代码和 DMA 更新3.1 选定时器和引脚先定 PWM 频率再选通道做 PWM DAC 不需要高级定时器的死区功能普通通用定时器就够用。常见引脚映射如下定时器通道1引脚F103通道2引脚备注TIM2PA0PA1通用定时器常用TIM3PA6PA7可与其他输出复用TIM4PB6PB7通用定时器TIM1PE9PE11高级定时器带故障保护功能选择通道前先看开发板引出的引脚再查数据手册的 AF 映射表。PA0 和 PA1 是很多 STM32 板卡默认引出的优先用 TIM2_CH1 会方便接线。选好通道后下一步才是定 ARR 和 PSC。比如要输出 50kHz 的 PWM定时器时钟 72MHz把 PSC 设为 0ARR 设为 1439得到分辨率约 10.5 位。这个分辨率对 LED 调光够用但如果要输出 12 位精度则 PWM 频率就要降到 18kHz 以下。实际项目里我一般先定分辨率再用余下的 ARR 范围反推最高 PWM 频率。3.2 用 HAL 库输出一路可调 PWMCubeMX 生成工程后最小配置看起来像下面这样。下面代码以 TIM2_CH1 为例输出 10kHz、50% 占空比的 PWMTIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; GPIO_InitTypeDef GPIO_InitStruct; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 7199; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 3600; // 占空比 3600/7200 50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);代码里Prescaler 0Period 7199所以 PWM 频率是72MHz / 7200 10kHz。Pulse就是 CCR初始值 3600 对应 50% 占空比。如果之后要改变电压直接用__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 5000);这个宏会把 CCR 改为 5000占空比变成 5000/7200≈69.4%。要注意CCR必须小于等于ARR否则定时器会按照满周期输出你在示波器上看到的是稳定的高电平丢失了所有 PWM 信息。还有一个细节GPIO 速度不要随便设成VERY_HIGH否则高速翻转会造成地弹反而增加纹波。3.3 用 PWMDMA 把 CCR 值批量刷新提高更新率如果要用 PWM DAC 输出正弦波、三角波或者每个 PWM 周期都需要更新占空比再用__HAL_TIM_SET_COMPARE一个个写就会占用 CPU。常见做法是让定时器更新事件触发 DMA把内存里的数组批量搬运到 CCR 寄存器。CubeMX 里配置 TIM2 通道 1 的 DMA 请求方向设为“内存到外设”模式设为“循环”数据宽度用半字。HAL 启动调用是uint16_t ccr_table[256]; // 波形数据预先填入 ccr_table HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t*)ccr_table, 256);这里的关键点是数据宽度必须用 16 位半字。如果你在 CubeMX 里选了 WordDMA 会把高 16 位当作另一个数据CCR 会出现跳变波形上出现随机毛刺。启动后DMA 每到一个 PWM 更新事件就搬一个数到 CCR表走完一圈硬件自动循环不需要中断参与。用这个方式输出正弦波时表长 256 点PWM 频率 50kHz输出基频就是50kHz / 256 ≈ 195Hz。要改变输出频率可以修改 PWM 频率也可以改表长度。修改表长度需要重启 DMA否则会进入不确定状态修改 PWM 频率可以直接改 ARR但要重新计算占空比数值否则电压范围会跟着变。4. RC 滤波器选型与布局PWM DAC 精度从电路上决定4.1 先定 R 再定 C负载阻抗决定滤波器能不能用第 2 章算出了 RC 的理论值但实际选型还要看后级负载。RC 滤波器的输出阻抗等于 R如果后级输入阻抗只有 10kΩ而 R 用了 100kΩ分压就会让输出比预期低很多。下面的表是一般选择顺序应用场景建议做法原因后级是高阻输入R 用 1k~10kΩ分压误差小于 1%后级有 10kΩ 负载R 降到 1kΩ 以下或者加运放跟随器避免负载拉低输出需要驱动长线RC 直接输出再加一级缓冲放大器提高带载能力需要更低纹波两级 RC后级电阻取前级 3~5 倍第二级减轻第一级负载我先说明一个常见误用一些人会把两个一样的 RC 串起来想让纹波变成原来的平方。但两级 RC 之间如果没有缓冲第二级的输入阻抗会并联到第一级的电容上实际截止频率不是简单相乘反而出现台阶。要这样设计第一级电阻要小第二级电阻要大才能获得接近理论的滤波效果。如果输出要接 ADC 采样脚还要注意 STM32 的 ADC 采样电容。ADC 采样瞬间会从 RC 输出抽走电荷造成电压跌落。解决办法是增大 ADC 采样时间或者把 RC 电阻减小到几百欧姆让电容在采样间隙恢复回来。对 10kΩ 采样频率不高的场合R1kΩC0.1μFADC 采样时间设为 239.5 周期基本够用。4.2 PCB 布局规避时钟抖动与电源噪声的 3 个要点PWM DAC 的电路结构很简单但布局随意的人经常得到比理论计算高得多的纹波。这里的核心原因不是 RC 选得不对而是地回路和数字开关噪声被采样到输出点。第一个要点PWM 输出线要短并且不要和 RC 之后的模拟线平行走线。PWM 信号在每次翻转时会对旁边的模拟线形成容性耦合。如果模拟线必须穿过 PWM 线走线时加一根地线隔开不要只依赖铺铜。第二个要点RC 的接地端要单独回到单片机模拟地。很多实验板把模拟地和数字地直接连成一片这本身问题不大但关键是 RC 地不能接到电机 MOS 管、LED 驱动这类大电流开关旁边。大电流回流路径经过的地铜箔会产生毫伏级压差这些压差会串联进输出电压表现为离散的毛刺。第三个要点ADC 采样点放在 RC 电容引脚处采样线两侧不要走高频数字线。采样点离电容引脚越远寄生电感越大采样到的噪声越多。如果空间允许在采样点放一个 10nF 的陶瓷电容到模拟地可以吸收高频噪声。不过这个电容会和 RC 电容并联改变截止频率所以加之前要把 RC 电阻适当调大。另外时钟抖动对 PWM DAC 的影响往往被人忽略。定时器时钟来自内部 RC 时频率本身会呈周期性抖动表现在输出电压上是几十微伏到几百微伏的低频波动。用示波器看 PWM 频率稳定不代表 RC 后的直流没有抖动。精调电压时把定时器时钟源切到外部晶振。4.3 用 ADC 校准吃掉零点偏移和非线性STM32 片内 DAC 有时会输出带死区的占空比RC 元件也有 1%~5% 的误差所以实测电压和计算值之间会有偏差。对一个 10kHz、12 位的 PWM DAC理论阶梯是 0.8mV而电阻电容误差可能带来几十毫伏的偏差远超一个 LSB。这时用 ADC 回采校准是最快的办法。下面的例子用两点线性校准。先设置两个不同的 CCR分别读 ADC 的平均值再根据目标 ADC 码值反推需要写入的 CCRstatic uint16_t dac_reg_lo, dac_reg_hi; static uint16_t adc_val_lo, adc_val_hi; void pwm_dac_calibrate(TIM_HandleTypeDef *htim, uint32_t channel) { __HAL_TIM_SET_COMPARE(htim, channel, 512); // 低点 HAL_Delay(20); adc_val_lo read_adc_average(16); __HAL_TIM_SET_COMPARE(htim, channel, 4095); // 高点 HAL_Delay(20); adc_val_hi read_adc_average(16); dac_reg_lo 512; dac_reg_hi 4095; } uint16_t pwm_dac_set_voltage(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t adc_target) { float slope (float)(dac_reg_hi - dac_reg_lo) / (adc_val_hi - adc_val_lo); float bias dac_reg_lo - slope * adc_val_lo; uint32_t reg (uint32_t)(slope * adc_target bias 0.5f); if (reg 4095) reg 4095; __HAL_TIM_SET_COMPARE(htim, channel, reg); return (uint16_t)reg; }这段代码假设 ARR 设为 4095CCR 范围是 0~4095。read_adc_average是自己写的函数连续采样 16 次求平均目的是去掉 RC 纹波和 ADC 本身的量化噪声。校准过程需要等 RC 到达稳态HAL_Delay 的 20ms 足够一阶 RC 完成 25 个时间常数实际可以缩短到 5ms。两点校准能纠正增益误差和零点偏移但纠正不了严重非线性。如果校准后发现中间段偏差超过 2 个 LSB先检查 RC 电容是否使用了 X5R 类型的大容量电容X5R 的直流偏压特性会让有效电容值随电压下降导致截止频率漂移。这个问题在 4.7μF 以上的陶瓷电容上特别明显换成 C0G 电容或薄膜电容会好很多。5. 把 PWM DAC 用到执行器上故障保护与波形输出技巧5.1 给 PWM DAC 加故障保护Break 事件和死区普通 PWM 输出在程序死机时会保持最后一次 CCR 值如果这个值对应的电压恰好是执行器的危险电压就可能烧毁设备。所以凡是 PWM DAC 输出到电机、发热丝、大功率 LED 的地方都建议加硬件保护不依赖软件关闭。STM32 的高级定时器 TIM1/TIM8 提供 Break 输入可以在一两个时钟周期内把输出拉回空闲电平。CubeMX 里启用 Break 功能后需要配置极性、空闲状态和死区TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; sBreakDeadTimeConfig.OffStateRunMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_1; sBreakDeadTimeConfig.DeadTime 10; sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_LOW; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig);BreakPolarity设为低电平有效表示 BKIN 引脚被拉低时 PWM 立即关闭。DeadTime在 H 桥驱动里用来防止上下管直通在 PWM DAC 场景下可以设小一些避免每次换向时输出出现短暂毛刺。要注意 Break 功能只在高级定时器上有普通 TIM2/TIM3 没有这个寄存器。如果你的主控只有普通定时器就得用外部比较器或者看门狗额外实现保护。5.2 用 DMA 查表生成正弦波并检查有效位数PWM DAC 最常用的进阶场景是生成正弦波。先建一个 256 点的查找表再通过 DMA 循环送数#define SIN_TABLE_SIZE 256 uint16_t sin_lut[SIN_TABLE_SIZE]; void generate_sin_lut(float vref) { for (int i 0; i SIN_TABLE_SIZE; i) { float v vref / 2.0f (vref / 2.0f) * sinf(2.0f * 3.1415926f * i / SIN_TABLE_SIZE); sin_lut[i] (uint16_t)(v * (4095.0f / 3.3f)); } }这里假设 ARR4095相当于 12 位 PWM。输出正弦波的中心电平是 Vref/2峰峰值接近 Vref。之后调用HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t*)sin_lut, 256)硬件就会持续输出该波形。验证波形时用示波器看两个相邻台阶的间隔必须完全一致如果出现间隔错位先查 DMA 配置的外设地址是否误设成了增量模式这个问题比电路噪声更难排查。PWM DAC 的有效位数不一定等于 ARR 的位数。用示波器看占空比高段时如果每两个相邻 CCR 值对应的电压间隔忽大忽小说明存在死区或者时钟抖动这时应当把有效位数往下调 1~2 位或者在固件里去掉最后几位不用的数据。比如 ARR4095实际只取高 10 位低两位强制清零输出会稳定不少代价是电压分辨率从 12 位降到 10 位但对大多数反馈控制来说已经够用。本文还有配套的精品资源点击获取
返回列表