4. 定时器基础与PWM:通用定时器配置、HAL的PWM接口、PWM频率与占空比精度、测量PWM输出的抖动(Jitter)
定时器和PWM,可以说是嵌入式世界里最常用的外设之一了。我做了这么多年驱动,几乎每个项目都离不开它——从控制电机转速,到调节LED亮度,再到生成音频信号,PWM的身影无处不在。
但说实话,很多人用PWM只是停留在「能亮就行」的阶段。一旦涉及到频率精度、占空比分辨率、输出抖动这些指标,就容易翻车。我自己就踩过不少坑,今天咱们把这些硬骨头啃下来。
4.1 通用定时器配置:从时钟树到计数器
先聊聊定时器的底层逻辑。你想想看,一个定时器本质上就是个计数器,它按照某个时钟源不断累加,到了设定值就触发事件。但真正用起来,有几个关键参数必须搞清楚。
| 参数 | 说明 | 典型范围 |
|---|---|---|
| 时钟源 | 定时器计数的基础时钟频率 | 1kHz ~ 100MHz+ |
| 预分频器 | 对时钟源进行分频,降低计数频率 | 1 ~ 65535 |
| 自动重装载值 | 计数器计到该值后溢出或更新 | 0 ~ 65535 (16位) 或 0 ~ 4294967295 (32位) |
| 计数模式 | 向上、向下、中心对齐 | 3种模式 |
在Rust的嵌入式HAL里,配置定时器通常用Timer::new()配合start()方法。但这里有个细节——不同芯片的时钟树结构差异很大。我曾在STM32F4上遇到过一个坑:默认的定时器时钟是84MHz,但APB1总线上的定时器时钟是42MHz,如果不看参考手册直接配,频率直接翻倍错。
核心公式:
PWM频率 = 定时器时钟 / (预分频器 + 1) / (自动重装载值 + 1)
占空比分辨率 = 1 / (自动重装载值 + 1) × 100%
举个例子,假设定时器时钟84MHz,想要生成1kHz的PWM:
- 预分频器设为83(实际分频84倍)→ 计数频率1MHz
- 自动重装载值设为999 → 溢出频率1kHz
- 占空比分辨率 = 1/1000 = 0.1%
4.2 HAL的PWM接口:Rust风格的抽象
Rust嵌入式HAL对PWM的抽象非常优雅。它定义了一个PwmPin trait,提供get_duty()、set_duty()、get_max_duty()这些方法。说白了,你不需要关心底层寄存器怎么操作,只需要告诉HAL「我要多大的占空比」就行。
use embedded_hal::pwm::PwmPin;
use stm32f4xx_hal::{
prelude::*,
timer::{Timer, PwmChannel},
};
// 初始化定时器,配置为PWM模式
let timer = Timer::new(device.TIM2, &clocks).pwm(
&mut device.PA0, // 通道1输出引脚
1.kHz(), // PWM频率
&mut afio, // 复用功能配置
);
// 获取PWM通道
let mut pwm = timer.bind_pin(device.PA0, PwmChannel::C1);
// 设置占空比为50%
let max_duty = pwm.get_max_duty();
pwm.set_duty(max_duty / 2);
// 使能输出
pwm.enable();
这段代码看起来简单,但背后做了不少事。HAL帮你处理了:
- 时钟使能和预分频器计算
- 自动重装载值的设定
- GPIO复用功能配置
- PWM模式选择(边沿对齐还是中心对齐)
我的习惯:在初始化PWM时,我会先调用get_max_duty()看看HAL算出来的最大值是多少。有些HAL实现里,这个值就是自动重装载值,有些则是重装载值+1。搞清楚这个,后面调占空比才不会懵。
4.3 PWM频率与占空比精度:鱼和熊掌
这里有个很现实的问题:频率和精度是矛盾的。你想想看,定时器的计数频率是固定的,想要更高的PWM频率,就得减小自动重装载值,但这样一来占空比的分辨率就降低了。
| 场景 | PWM频率 | 自动重装载值 | 占空比分辨率 |
|---|---|---|---|
| LED调光 | 1kHz | 999 | 0.1% |
| 电机控制 | 20kHz | 49 | 2.0% |
| 音频DAC | 44.1kHz | 22 | 4.5% |
我在做无人机电调的时候,就吃过这个亏。电机需要20kHz的PWM频率来避免啸叫,但占空比分辨率只有2%,导致电机低速时转速抖动特别明显。后来我换用了32位定时器,把计数频率提到168MHz,才在20kHz下实现了0.1%的分辨率。
避坑指南:不要盲目追求高频率。我曾经为了降低电机噪音,把PWM频率设到100kHz,结果占空比分辨率只有0.5%,电机反而因为步进太大而振动更厉害。找到频率和精度的平衡点才是关键。
4.4 测量PWM输出的抖动(Jitter)
抖动,说白了就是PWM波形的边沿位置在时间上的不稳定。理想情况下,每个周期的上升沿应该精确地间隔相同的时间。但现实是,由于中断延迟、总线竞争、时钟漂移等因素,边沿总会有些偏差。
测量抖动的方法有很多,我推荐用逻辑分析仪或者示波器做长时间采集。在Rust里,我们也可以用定时器输入捕获来测量:
use embedded_hal::timer::CountDown;
use nb::block;
// 配置定时器为输入捕获模式
let mut capture_timer = Timer::new(device.TIM3, &clocks).input_capture(
&mut device.PA1, // 捕获输入引脚
&mut afio,
);
// 连续捕获上升沿的时间戳
let mut prev = 0u32;
for _ in 0..1000 {
let curr = block!(capture_timer.wait()).unwrap();
let period = curr.wrapping_sub(prev);
// 记录period的统计信息
prev = curr;
}
抖动通常用两个指标衡量:
- 周期抖动(Period Jitter):相邻周期之间的时间差
- 累积抖动(Accumulated Jitter):多个周期后的总时间偏差
我见过最夸张的一次,某款国产MCU在开启WiFi后,PWM抖动从±5ns飙到了±200ns。查了半天才发现是DMA和定时器抢总线带宽导致的。解决办法是把PWM定时器放到高优先级的总线上,或者用双缓冲技术来隔离干扰。
抖动容忍度参考:
- LED调光:±50μs以内人眼基本无感
- 电机控制:±10μs以内影响不大
- 音频应用:±1μs以内才能保证音质
- 精密测量:±10ns以内才合格
4.5 知识结构总览
下面这张图把本章的核心知识点串起来了。从时钟树开始,经过预分频器和自动重装载值,最终生成PWM波形。抖动测量则是对输出质量的检验环节。
4.6 实战建议
最后给几个实在的建议:
- 先算后配:别急着写代码,拿计算器把频率和分辨率算清楚再动手
- 用示波器验证:我每次调完PWM都会用示波器看波形,频率对不对、占空比准不准、抖动大不大,一眼就能看出来
- 注意中断优先级:如果PWM用了中断更新,别让其他中断抢了它的优先级,否则抖动会明显增加
- 考虑温度漂移:晶振和RC振荡器都会随温度变化,对精度要求高的场景,建议用外部晶振或者做温度补偿