定时器与 PWM:把时间外包给硬件(以 Cortex-M4 为例)
你想给板子加个呼吸灯——LED 慢慢亮、慢慢灭,显得有生命感。PWM 还没学,就用最朴素的办法硬模拟:主循环里翻转引脚,亮多灭少就是”变亮”,亮少灭多就是”变暗”。灯真的呼吸起来了。然后你往主循环里加了一行 printf,呼吸开始抽搐;把它搬进 FreeRTOS 任务里,别的任务一忙,灯就一卡一卡。你反复检查,每一行都没写错。
错的是整个做法本身:你在用 CPU 的手指头充当电路。CPU 的每一拍都可能被中断抢走、被调度器拿走,而”亮 250 微秒、灭 750 微秒”这件事要求每 1 毫秒都分毫不差——一个连自己下一拍在哪都不敢保证的家伙,演不了节拍器。
这篇文章以应用最广的 32 位内核之一 Cortex-M4(以 STM32F407 为例)为背景,分三层把定时器拆开:先让一盏硬件 PWM 呼吸灯跑起来,再看那台”数数的小电路”内部发生了什么,最后想明白——为什么 MCU 里最不值钱的逻辑门,反而配得上接管最金贵的东西:时间。也是还账的一篇:《GPIO》 里说”消抖是定时器的活,这篇先不碰”,《UART》 结尾说”往定时器走”——两笔账今天一起清。
L1:先让它跑起来——还是那盏绿灯,这次自己呼吸
你在第一层。目标:用最小代码让硬件 PWM 呼吸灯跑起来,对定时器的配置长什么样有个地图感。
还是 GPIO 篇那盏 PD12 的灯,还是那块 STM32F407。这次它的身份变了:PD12 在芯片内部还连着 TIM4 的通道 1——定时器有个专门的输出引脚,就像串口有 TX/RX 一样。把引脚交接给定时器,从此灯的亮灭节拍就由它说了算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#define RCC_AHB1ENR (*(volatile unsigned int*)0x40023830)
#define RCC_APB1ENR (*(volatile unsigned int*)0x40023840)
#define GPIOD_MODER (*(volatile unsigned int*)0x40020C00)
#define GPIOD_AFRH (*(volatile unsigned int*)0x40020C24)
#define TIM4_CR1 (*(volatile unsigned int*)0x40000800)
#define TIM4_CCMR1 (*(volatile unsigned int*)0x40000818)
#define TIM4_CCER (*(volatile unsigned int*)0x40000820)
#define TIM4_EGR (*(volatile unsigned int*)0x40000814)
#define TIM4_PSC (*(volatile unsigned int*)0x40000828)
#define TIM4_ARR (*(volatile unsigned int*)0x4000082C)
#define TIM4_CCR1 (*(volatile unsigned int*)0x40000834)
void delay(volatile int n) { while (n--) ; } /* 粗糙的软件延时,量级对就行 */
int main(void)
{
RCC_AHB1ENR |= 1u << 3; /* 打开 GPIOD 的时钟 */
RCC_APB1ENR |= 1u << 2; /* 打开 TIM4 的时钟 */
GPIOD_MODER |= 2u << 24; /* PD12 -> 复用(10):老朋友 */
GPIOD_AFRH |= 2u << 16; /* PD12 -> AF2 = TIM4(AFRL/AFRH 的账 GPIO 篇算过) */
TIM4_PSC = 83; /* 84MHz / 84 = 1MHz:数一下就是 1µs */
TIM4_ARR = 999; /* 从 0 数到 999 再归零:一个周期 1ms,即 1kHz */
TIM4_CCR1 = 0; /* 阈值先放 0:灯全灭 */
TIM4_CCMR1 = (6u << 4) | (1u << 3);/* OC1M=110:PWM 模式 1 | OC1PE:阈值预装 */
TIM4_CCER = 1u << 0; /* CC1E:把通道 1 接到引脚上 */
TIM4_EGR = 1u << 0; /* UG:先强制装一次(为什么 L2 讲) */
TIM4_CR1 = 1u << 0; /* CEN:开始数 */
int step = 0, up = 1;
while (1) {
TIM4_CCR1 = step; /* 整个呼吸灯,CPU 只剩这一件事 */
step += up ? 5 : -5;
if (step >= 1000 || step <= 0) up = !up;
delay(50000);
}
}
烧进去,绿灯缓缓地呼吸。现在往主循环里随便加 printf、把它搬进 FreeRTOS 任务、开十个中断——灯的节拍纹丝不动。CPU 只剩”隔一会儿改一下阈值”这一件事,改完转头就走,节拍是硬件自己在数。
先挑几个”看着眼生”的地方钉一下:
TIM4_PSC = 83和TIM4_ARR = 999:这是定时器的两根”变速齿轮”,一个决定数得多快,一个决定数到几算一轮。84MHz 的时钟经过它们,变成 1kHz 的节奏——两个除法,就是文首那台”节拍器”的全部秘密,L2 专门拆。- 一个埋伏的坑:TIM4 挂在 APB1 总线上,APB1 是 42MHz——但定时器的时钟其实是 84MHz。APB1 分频系数不是 1 时,芯片会把定时器时钟自动翻倍(外设慢点无所谓,数数的不能慢),这是 STM32 出了名的”手册缝里的知识”,算频率前必须先知道。
TIM4_CCR1:比较值(Compare)。它就是那个”阈值”——CPU 唯一还要写的寄存器。占空比 = CCR / (ARR+1),写 250 就是 25% 亮度。TIM4_CCMR1里的6u << 4:模式编号 110,官方名字”PWM 模式 1”,翻译成人话是”数得比阈值小就亮“。旁边那个1u << 3是”预装”开关,L2 的影子里再讲。TIM4_EGR = 1u << 0:UG 位,”手动发一次更新事件”。看着突兀,删了这行,头一个周期的分频比不生效——为什么,正是 L2 的第一课。
到这里你已经有一盏不怕打扰的呼吸灯了。但你欠着一屁股解释:两个除法怎么就变成了 1kHz?CCR 凭什么控制亮度?那行神秘的 UG 又在倒腾什么?
L2:懂原理——一台只会数数的小电路
到这一层,问题变了:不再问”怎么写”,而是问”底下发生了什么”。
先看这台机器怎么数:PSC、ARR、CNT 三件套
TIM4 内部真正干活的,是一个 16 位计数器 CNT,外加两根你写死的数:PSC(预分频)和 ARR(自动重装值)。它的全部日常只有一句话:
拿着 84MHz 的钟拍,每 (PSC+1) 拍数一下;数到 ARR,”啪”地归零,顺便喊一声”到点了”(更新事件),再从 0 数起。
我们这组配置(PSC=83,ARR=999)画出来是一把锯齿:
1
2
3
4
5
6
7
8
9
999 ┤ /| /| /| /|
│ / | / | / | / | ← 顺着斜坡往上爬的是 CNT
│ / | / | / | / | 每微秒 +1,谁也不等
│ / | / | / | / |
0 ┤_/ |_/ |_/ |_/ |_
└──────┴──────┴──────┴──────┴────▶ 时间
←– 1ms –→←– 1ms –→
↑ 数到 999 归零的瞬间 = 更新事件
(想干活可以挂中断,L2 最后一节就干这个)
| 看这张图时注意那个竖直的” | ”“:计数器不是数到 999 就停,是瞬间归零重来。这个”归零重来”本身就是个事件——它可以触发中断,L3 会看到它是整套设计的灵魂。 |
现在把频率的账算给你看,一步除法都不含糊:84MHz ÷ (83+1) = 1MHz(PSC 负责),每个数 1µs;从 0 数到 999 共 1000 个数(ARR+1 个),周期 = 1000 × 1µs = 1ms,频率 1kHz。合起来一个公式:
\[f_{PWM} = \frac{f_{timer}}{(PSC+1) \times (ARR+1)} \qquad 占空比 = \frac{CCR}{ARR+1}\]顺便就能看懂那行神秘的 UG 了:PSC 是带”影子”的——你写进去的新值先存在影子里,要等下一次更新事件才真正生效。上电后 CNT 还没数到过顶,更新事件一次都没发生过,PSC 的影子还是出厂值 0。EGR 的 UG 位就是”手动补发一次更新事件”,把 PSC、ARR 一口气装进真身。别小看这个细节:漏写 UG,第一个周期的分频比是错的——频率对、周期差一拍的疑难杂症,病根常在这儿。(顺带一个坑:UG 会把状态寄存器 SR 里的 UIF 标志也置起来,后面若开更新中断,记得先清——定时器的标志是写 0 清除,跟 《UART》 里”读 SR 再读 DR”的清法不是一路的。)
PWM:在锯齿旁边放一条水平线
有了锯齿,PWM 就没有任何新东西了——一个比较器。PWM 模式 1 的规则翻译过来:CNT < CCR 时输出高,CNT ≥ CCR 时输出低。把你的阈值(CCR=250)画到锯齿图上,输出波形直接从图里”长”出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
250 ┤ ─ ─ ─╱┃─ ─ ─ ╱┃─ ─ ─ ╱┃─ ─ ← CCR:你画的那条水平线
999 ┤ ╱ ┃ ╱ ┃ ╱ ┃
│ ╱ ┃ ╱ ┃ ╱ ┃
│ ╱ ┃ ╱ ┃ ╱ ┃
0 ┤__╱ ┃__╱ ┃__╱ ┃__
└───────┴───────┴───────┴────▶ 时间
0µs 250µs 1000µs
↑ CNT 爬过 250 的瞬间,输出翻转
输出 ┌────┐ ┌────┐ ┌────┐
│ │ │ │ │ │
──┘ └───────┘ └───────┘ └──
←250µs→←–750µs–→
亮 25% 灭 75%
每个周期开始,CNT 从 0 起步,比 250 小,输出为高;爬过 250 的那一拍,比较器翻转输出;归零重来,又变高。PWM 的本质就是锯齿和水平线比大小,交点就是翻转沿——阈值抬高,交点右移,亮的时段变长;阈值降到 0,整轮都比它大,全程灭;阈值抬过 ARR,整轮都比它小,全程亮。
这就回答了 L1 埋的问题:CPU 为什么只写 CCR1 就够了?因为它写的是那条水平线,锯齿是硬件在爬。文首的软件呼吸灯错在哪儿也清楚了:它用 CPU 的循环去模拟这条锯齿,任何中断一来,斜坡就断——输出的沿由 CPU 的执行时间决定,而 CPU 的执行时间从不受它自己控制。硬件 PWM 的沿由计数器决定,计数器只认钟拍,不认任何人。
还有一个数字值得咂摸:1kHz,LED 每秒亮灭一千次。为什么看不出闪烁?人眼对上百赫兹的周期性明暗就基本无感了——你看到的一直是”平均值”。这个”平均值”是全文最反直觉的一件事,L3 收尾时专门翻出来。
频率不是随便选的:三个真实的档位
ARR 想写多大写多大?不行——TIM4 是 16 位的,ARR 最大 65535。这笔账一算,一个反直觉的事实就出来了:
- PSC=0(不减速)时,这块定时器能输出的最低 PWM 频率是 84MHz ÷ 65536 ≈ 1.28kHz。区区 50Hz?直接数不到,必须先用 PSC 把钟降下来。定时器明明数得飞快,却连”慢”都要费一番配置——快是它的天性,慢是它要努力的事。
- 反过来,PWM 频率选高也有代价:同一根钟,频率翻倍,一轮里能数的格子就减半。84MHz 出 20kHz,一轮 4200 个格子,占空比就只有 4200 档(约 12 位精度);出 1kHz 则有 84000 个格子——但 16 位装不下,又得请 PSC 出场。频率和分辨率,同一根钟掰不开的两头。
三个真实世界的档位,各有各的”为什么”:
| 应用 | 频率 | 为什么是这个数 |
|---|---|---|
| LED 调光 | ~1kHz | 高于人眼感知极限,低到占空比档位足够多 |
| 舵机 | 50Hz | 协议古老:20ms 一帧,脉宽 1–2ms 编码角度 |
| 电机调速 | ~20kHz | 高出人耳可听频段——低了线圈会发出哨声(”电机啸叫”) |
(舵机那一行,动手环节会让你亲手把 PSC 和 ARR 算出来。)
挂上中断:兑现 GPIO 篇欠的消抖账
锯齿每次”啪”地归零都是一次更新事件,可以挂中断——DIER 的 bit0 使能,NVIC 里给 TIM4 开闸(IRQ 号 30,ISER0 的第 30 位)。于是 GPIO 篇那句”按键抖几十毫秒,真实项目要消抖,那是定时器的活”可以还账了:每 1ms 醒一次,采样按键电平,连续 5 次读到同一个新状态才认——抖动是真假交替的毛刺,撑不过”连续 5 次”这道门槛:
1
2
3
4
5
6
7
8
9
10
11
12
13
void TIM4_IRQHandler(void)
{
static int stable = 1, count = 0; /* 1 = 松开(上拉的默认态) */
if (TIM4_SR & 1u) { /* UIF:又过了一个 1ms */
TIM4_SR &= ~1u; /* 写 0 清标志 */
int now = (GPIOD_IDR & (1u << 1)) != 0; /* PD1 现在的电平 */
if (now != stable) {
if (++count >= 5) { stable = now; count = 0; } /* 连续 5ms 异样才认 */
} else {
count = 0;
}
}
}
这套”定期醒来、多次确认”的打法,比 GPIO 篇的轮询体面在两处:CPU 平时可以干别的(甚至睡觉),且时间准——1ms 就是 1ms,不是”跑一圈大概是几毫秒”。你可能觉得眼熟:《FreeRTOS 核心原理》 里那个每 1ms 敲一次门的 SysTick,就是同一种机器——内核自带的 24 位定时器,专职给操作系统打拍子。从呼吸灯到操作系统心跳,底层是同一块数数电路。
到这里你已经懂原理了。但有个更根本的问题压着:数数这么简单的活,凭什么值得做成独立硬件?CPU 每秒上亿次运算,数个数不是顺手的事吗?
L3:想得透——为什么时间必须外包
到这一层,问题变成:这些设计是理所当然的吗?换掉行不行?
本质:定时器到底是什么
去掉所有术语,TIM4 是这样一件事:
一块独立于 CPU 的小电路,只干两件事:照着钟拍数数,数到头自动归零重来;再把”数到几”和”你写死的那个数”比个大小,结果直接变成引脚上的电平。CPU 从此和节拍脱钩——它只负责偶尔改一改那条水平线。
给同事的一句话:软件 PWM 和硬件 PWM 的差别,就是”乐手自己数拍子”和”跟着节拍器数拍子”的差别——乐手水平再高,一走神就抢拍;节拍器不会走神,因为它除了打拍子什么都不会。专用的笨,恰恰是专用的准。这是嵌入式反复出现的一条母题:《Cortex-M4 与 MCU 开发》 里 GPIO 替 CPU 看引脚、UART 替 CPU 收波形、DMA 替 CPU 搬数据,如今定时器替 CPU 管时间——外设的本质就是”把 CPU 不擅长或耗不起的事,外包给一块不会分心的小电路”。时间是最特殊的那个,因为时间不受任何人暂停:CPU 慢一拍就是真的少了一拍,没有补考。
三个”被否决的方案”
本质立住了,还得追问一层:为什么是这个方案,而不是显然的替代方案?
被否决的方案一:CPU 软件循环。(就是文首的你。) 看起来零成本——不用配寄存器,一个 while 就写了。失败场景文首已经演过:输出沿的位置 = 某几条指令的执行时间,而指令的执行时间会被中断、抢占、缓存全部拉长。占空比的精度上限就是中断延迟,毫秒级中断一来,25% 可能瞬间变成 40%。更深的代价是功耗:软件 PWM 意味着 CPU 永远不能睡——时间被 CPU 的清醒绑架了。硬件 PWM 下 CPU 关中断睡觉去,灯照常呼吸。
被否决的方案二:不要两根齿轮,用一根超宽计数器直接数。 疑问很正当:既然数快了,为什么要 PSC、ARR 两级分频这么麻烦?拿一个 32 位计数器直接从 0 数到目标周期不就完了?(芯片里真有 32 位定时器——TIM2 和 TIM5。)两级结构赢在两件事。一是经济的变速范围:两根 16 位齿轮咬合,用 32 位的代价换出了从微秒到分钟级的量程,多数应用里 16 位定时器就够,硅面积和功耗都是真金白银。二是那声”啪”:计数到 ARR 归零的更新事件,是所有参数的同步点——PSC、ARR、CCR 的新值都先存进影子寄存器,等下一次更新事件同一拍一起生效。改占空比、改频率永远不担心波形撕裂出一半新一半旧的畸形周期。一根宽计数器没有这个天然的”换挡时刻”。
被否决的方案三:CPU 查表 + DMA 定期喂引脚。 这条其实没被否决——它被收编了。用 DMA 按节拍把一张预先生成好的波形表搬到输出寄存器,CPU 出表格、DMA 出节拍,思路完全成立。代价是占用一条 DMA 通道、吃总线带宽,引脚一多就破产;但驱动 WS2812 灯带(每根数据线上跑极窄的时隙编码)时,PWM+DMA 查表正是标准打法。它是”CPU 出内容、硬件出时间”这条原则的另一种分工——本文的主角 PWM 比它更省:连 DMA 都不用,比较器自己就把波形”长”出来了。
一段顺手的历史
“定时器”这个名字的祖师爷是 555 芯片——1972 年 Signetics 出品,Hans Camenzind 设计,一片几毛钱的八脚小芯片,外围两三个电阻电容,就能当多谐振荡器或单稳态触发器。它卖了几十亿颗,至今还在电路板上活着,是很多工程师摸到的第一颗”会自己动”的芯片。MCU 上的 TIM4 可以看作几十颗 555 的职责被收编进片内、再统一挂上总线:“把时间做成独立小电路”这个思想 1972 年就定型了,MCU 只是让这块小电路可以直接写寄存器、还能反过来敲门喊中断。
(那”啪”地归零的更新事件,在 555 那里对应的是电容电压冲过阈值后猛然放电的那一下——同样的锯齿,同样的回零。你在示波器上看过 555 的话,现在你已经知道你在看什么了。)
一个反直觉收尾
回到 LED:你以为呼吸灯输出的是”变化的亮度”,但引脚上从来只有 0V 和 3.3V 两个电平,一分钟也没有过中间值。25% 占空比时,LED 每毫秒被点亮 250 微秒、掐灭 750 微秒——是眼睛的视觉暂留把这一串脉冲积分成了”25% 的亮度”。换到电机上做这件事的是线圈的电感:电流跟不上电压的快速翻转,于是”看见”的也是平均值;舵机的机械惯性同理。所以 PWM 的输出电压严格说是 3.3V × 占空比(25% → 0.83V)这个”平均”从来不出现在引脚上,它是负载自己积出来的。
PWM 不是”输出模拟电压”,而是”把一个模拟量打包成数字快递”——发件方只发 0 和 1,解码是收件人自带的物理惯性完成的。
这也解释了 PWM 为什么能大行其道:数字系统最擅长的是满血 0 和 1(晶体管开关状态最省电、最干净),最不擅长的是中间态。PWM 让数字系统永远只做最擅长的事,把”中间态”外包给物理世界——和本文把时间外包给定时器,是同一个思想的两张面孔。
动手环节:一张纸,把时间算出来
这次的实验还是不需要板子,一支笔就够——算的仍是本文的灵魂:那两级分频的账。
实验一:配一台舵机。 舵机要 50Hz、脉宽 1–2ms。TIM4 时钟 84MHz,PSC 取 83。先预测:ARR 该写几?脉宽 1.5ms(中位)对应 CCR 该写几?
答案:PSC=83 → 计数频率 1MHz;50Hz 一个周期 20ms = 20000 个数 → ARR = 19999。1.5ms = 1500 个数 → CCR = 1500;边界 1ms/2ms 就是 CCR 1000/2000。顺手验证上一节的断言:20000 < 65536,恰好塞进 16 位——但要是想省掉预分频硬数,84MHz ÷ 50Hz = 1680000 个数,16 位装不下,这就亲手证明了”50Hz 必须开 PSC”不是一句空话。
实验二:这台机器的下限。 PSC=0 时,TIM4 能输出的最低 PWM 频率是多少?再算:想用 84MHz 出 20kHz 的电机 PWM,占空比有几档?相当于几位精度?
答案:最低频率 = 84MHz ÷ 65536 ≈ 1.28kHz。20kHz 时一轮 = 84MHz ÷ 20kHz = 4200 个数,即 4200 档 ≈ 12 位(2¹² = 4096)。两个数字连起来看就是那条取舍曲线:频率每翻一倍,档位减半——”频率与分辨率是同一根钟掰不开的两头”,现在是你亲手掰过的了。
实验三:消抖的延迟账。 按键抖动最长 10ms,采样周期 1ms,要求连续 5 次读到”按下”才确认。先预测:从手指触底到系统确认”确实按下了”,最坏情况延迟多久?
答案:最坏情况,抖动的假”松开”恰好撑满前 10ms,之后需要连续 5 次按下采样——第 5 次出现在抖动结束后第 4ms,总延迟约 10 + 4 = 14ms。人手指对按键延迟的无感区在几十毫秒开外,14ms 完全无感——但要是你把”连续 5 次”加到”连续 50 次”图更稳,就是 60ms,开始硌手了。滤波强度和响应速度永远在互相拉扯,N 取多少是笔要算的账,不是拍脑袋的数。
三个自测问题,能答上说明这三层你真爬过了:
- 84MHz 的 TIM4 为什么直接输出不了 50Hz?两级齿轮里哪一级在救场?(答案在 L2:16 位 ARR 只数到 65535,最低 1.28kHz;PSC 先把钟降到 1MHz。)
- 软件呼吸灯为什么一加 printf 就抽搐,硬件 PWM 加十个中断都纹丝不动?(答案在 L2:软件的输出沿由 CPU 执行时间决定,中断一来就漂;硬件的沿由只认钟拍的计数器决定。)
- 改占空比就是改 CCR——为什么改的时候不用担心波形”撕”成一半旧一半新?(答案在 L2/L3:CCR 先进影子寄存器,更新事件那一拍才整拍生效。)
收尾地图
把整篇文章收进一张速查表:
| 你想干什么 | 配方 | 一句话理由 |
|---|---|---|
| LED 呼吸灯/调光 | ~1kHz,改 CCR 即改亮度 | 高于视觉暂留,低于费不了几格档位 |
| 舵机 | 50Hz:PSC=83、ARR=19999,CCR 1000–2000 | 20ms 一帧,脉宽编码角度 |
| 电机调速 | ~20kHz,高级定时器(TIM1/TIM8) | 出可听频段免啸叫;死区和互补输出下篇见 |
| 按键消抖 | 1ms 更新中断 + 连续 N 次一致 | 本文 L2 已兑现 GPIO 篇欠的账 |
| 周期性任务/OS 心跳 | SysTick 挂中断 | FreeRTOS 的敲门人,同一块电路 |
| 精密慢节奏 | 检查 16 位上限,不够先降 PSC | ARR≤65535,频率下限 ≈1.28kHz |
| 波形复杂但不挑脚 | PWM+DMA 查表(如 WS2812) | “CPU 出表、硬件出拍”的另一种分工 |
下一站,就是 GPIO 篇点灯、本文调速一路铺过来的那条路:电机控制。20kHz 只是入场券——H 桥上两个开关管绝不能同时导通(直通即短路),所以 TIM1/TIM8 这类高级定时器自带”互补输出 + 死区插入”:一对 PWM 波形天生互补、之间还留出几十纳秒的都关断的安全空隙,硬件替你看住上下管;再配上编码器和 PID,机器人底盘的轮子就从这里开始转。若想先歇歇脚,往总线走:I2C 和 SPI——它们各自的时钟线,正是一台降了速的定时器在门外数拍子。你已经认识它的内部构造了。