STM32F4 定时器影子寄存器详解:ARR、CCR、PSC 为什么不会总是立即生效
很多人在调 STM32F4 的 PWM 时都会遇到一个很反直觉的现象:代码里明明已经改了ARR、CCR或PSC,示波器上的波形却没有立刻变化,往往要等到下一个周期才生效。
这通常不是代码没执行,也不是示波器骗人,而是 STM32 定时器内部有一套很重要的机制:预装载寄存器和影子寄存器。
一句话先说结论:
STM32F4 定时器用“预装载寄存器”接收软件写入,用“影子寄存器/活动寄存器”参与硬件比较,并在更新事件
UEV到来时把预装载值同步到真正生效的影子值,从而避免 PWM 在周期中间被改坏。
本文以 STM32F4 系列为对象,不同 F4 子系列的定时器数量和高级功能会有差别,但ARR、CCR、PSC的预装载逻辑大体一致。
先简述PWM
对于初学者,可能会把PWM(脉宽调制)理解成硬件外设等等,但本质上是一个物理电气过程,是电压在“高电平”和“低电平”之间高速切换的物理现象。固件(代码)是用来操控物理过程的“指令”,真正产生电信号的定时器、比较器等是硬件外设。
简单三句话说明
- 核心动作:在一个固定周期(频率),快速的把电压“接通”,”断开“。
- 关键指标:占空比(有效电平时间占整个周期的百分比)。
- 最终效果:利用人眼/电机的“惯性”平滑效应,等效出一个平均电压。占空比越高,等效输出电压越高。
1. 先把 STM32F4 上的几个事实讲准
原文里有几个常见说法需要按 STM32F4 的真实行为修正:
TIM_ARRPreloadConfig(TIMx, ENABLE)只是打开ARR的预装载功能,也就是设置ARPE(自动重装载预装载使能)位;它不是写入新的ARR值。标准外设库里修改周期应使用TIM_SetAutoreload(TIMx, new_arr)。- ARPE = 0(立即生效):修改 ARR 值后,新周期立刻生效。这可能导致当前正在进行的PWM周期被强行截断,输出一个不完整的畸形脉冲。
- ARPE = 1(缓冲使能,强烈推荐):修改的是预装载寄存器,真正的影子寄存器只在定时器溢出更新事件时才加载新值。这样能保证当前周期完整跑完,下一个周期再按新频率执行,波形过渡极其平滑。
PSC有预装载机制,但在 STM32 定时器里它的预装载不能关闭。也就是说,运行中修改PSC后,新分频值通常要等下一次更新事件才会真正进入分频器。CCR不是“立即改也无所谓”。如果没有打开OCxPE(输出比较通道预装载使能),运行中改CCR会立即影响比较点,可能导致当前 PWM 周期出现异常边沿。对 LED 呼吸灯这可能只是轻微抖动;对电机、半桥、全桥、电源控制,这就可能是很难看的尖峰。- OCxPE = 0(预装载禁用):立即生效。你修改CCR,输出波形会立刻改变。适合需要即时快速响应的场景(如软件实时调节)。
- OCxPE = 1(预装载使能,推荐):平滑更新。你修改的是“预装载寄存器”,真正的“影子寄存器”只在更新事件(如定时器溢出)时才更新。这能保证一个完整的PWM周期内参数恒定,防止产生不完整的畸形波形。
- 运行中手动写
EGR.UG(Update Generation,更新生成,写入1强制生成更新事件EUV)不是一个完全无副作用的“同步按钮”。它会立即产生更新动作,还会重新初始化计数器/预分频器相关状态。启动前用UG刷影子很常见;运行中强行UG要确认你能接受相位被打断。- 作用:立即“刷”新影子寄存器。当你修改了 ARR(周期)或 CCR(占空比)的预装载值后,正常要等定时器溢出才能生效。而置位 UG 会立即把这些新值从预装载寄存器拷贝到影子寄存器,并同时将计数器 CNT 清零。
- 最大价值(初始化必做):在启动定时器之前,配置好 ARR/CCR 后务必置位一次 UG。这能确保定时器从 0 开始第一个周期就使用你设定的正确值,否则第一个脉冲可能长度异常或错误。
- 附带效应:置位 UG 会触发更新中断标志(UIF),如果开启了中断,会立刻进中断服务函数。
- 如果关闭
ARPE,又在CNT已经大于新ARR时把ARR改小,不能简单写成“下一个计数脉冲马上归零”。在不少 STM32 实测中,计数器可能继续数到本计数器满量程再绕回,导致一个异常长周期。更稳妥的表述是:当前周期会被破坏,具体表现不应作为设计依赖。
这些修正并不会削弱“影子寄存器避免毛刺”的结论,反而让这个结论更工程化:预装载的价值不是让波形必然“延迟”,而是让多个影响波形的量在可控的边界同时生效。
2. 为什么需要影子寄存器
以边沿对齐、向上计数 PWM 为例:
① PWM 频率(决定周期):
f P W M = T I M _ C L K ( P S C + 1 ) × ( A R R + 1 ) f_{PWM} = \frac{TIM\_CLK}{(PSC+1) \times (ARR+1)}fPWM=(PSC+1)×(ARR+1)TIM_CLK
② 占空比(决定输出有效电平的比例):
D = C C R A R R + 1 × 100 % D = \frac{CCR}{ARR+1} \times 100\%D=ARR+1CCR×100%
例如 STM32F407 上常见配置为:
APB1 timer clock = 84 MHz PSC = 83 -> 计数频率 1 MHz ARR = 999 -> PWM 频率 1 kHz CCR = 400 -> 约 40% 占空比假设当前ARR = 999,CNT = 800,这时软件想把周期改短,写入ARR = 499。
如果没有预装载保护,定时器正在跑的这个周期就会遇到麻烦:当前计数值已经超过新的终点。硬件接下来到底表现为异常长周期、异常短周期,还是某些边沿错乱,取决于具体定时器实现和计数模式。无论哪种结果,它都不是一个完整、可预测、干净的 PWM 周期。
影子寄存器解决的正是这个问题:软件可以先写新参数,但硬件仍然用旧参数完成当前周期,等到更新事件UEV到来,再把新参数一次性搬到活动寄存器里。
3. 预装载和影子寄存器的双层模型
当某个定时器寄存器开启预装载后,它在硬件里有两个实例。
| 层级 | 软件能否直接访问 | 硬件是否用它产生 PWM | 作用 |
|---|---|---|---|
| 预装载寄存器 preload | 能 | 暂时不用 | 接收 CPU 写入的新值 |
| 活动寄存器 active,也常叫 shadow | 通常不能直接读写 | 用 | 真正参与计数、分频、比较和输出 |
开启预装载后,CPU 写入的是 preload。只有当更新事件UEV到来时,preload 才会被搬运到 active/shadow。
这个“等到边界再切换”的动作,就是 PWM 不容易被写寄存器动作打出毛刺的核心。
4. STM32F4 里哪些东西有预装载
下面这张表建议直接记住。
| 对象 | STM32F4 上的预装载行为 | 控制位或接口 | 典型用途 |
|---|---|---|---|
TIMx_PSC | 有预装载,不能关闭 | 无独立关闭位 | 改计数频率,下一次UEV生效 |
TIMx_ARR | 可选 | TIMx_CR1.ARPE | 改 PWM 周期,建议动态 PWM 中开启 |
TIMx_CCRy | 可选 | TIMx_CCMR1/2.OCyPE | 改占空比,功率控制中建议开启 |
CCxE/CCxNE、OCxM | 部分高级定时器支持预装载 | 常与 COM 事件相关 | 电机换相、互补输出、刹车相关控制 |
TIMx_RCR | 高级定时器相关 | TIM1/TIM8 常见 | 控制多少个周期才产生一次真正更新 |
几个工程提醒:
ARR的ARPE复位后通常是关闭的,想要周期在边界更新,需要显式打开。CCR的OCxPE复位后也通常是关闭的。使用 Cube HAL 配 PWM 时,HAL 的 PWM 配置流程通常会帮你设置通道预装载,但不要凭感觉判断,调试时直接看CCMR1/CCMR2的OCxPE位。PSC修改后不一定立刻改变计数频率,因为它的预装载不能关闭。初始化阶段通常要手动产生一次UG,让PSC立即装入。使用 TIM1/TIM8 时,如果配置了重复计数器
RCR,预装载值不是每次溢出都必然生效,而是在真正产生UEV时生效。RCR = N时,常见理解是每N + 1个计数周期才产生一次更新事件。
5. UEV 是真正的同步闸门
更新事件UEV是影子寄存器同步的关键节点。STM32F4 定时器里常见的UEV来源有:
计数器溢出或下溢。向上计数时,通常是从
ARR对应的周期边界回到 0 附近。(这个很重要!)软件写
TIMx_EGR.UG。标准外设库对应TIM_GenerateEvent(TIMx, TIM_EventSource_Update)。从模式控制器触发。例如主从定时器同步、外部触发复位等场景。
还要注意两个控制位:
| 位 | 含义 | 工程影响 |
|---|---|---|
UDIS | Update disable | 置 1 后禁止产生更新事件,预装载不会搬到 shadow。可以短时间用于批量写参数,但不要长时间忘记恢复 |
URS | Update request source | 限制哪些更新源产生更新中断或 DMA 请求。它常用于避免软件UG引起不想要的中断请求 |
重点是:UEV不只是“中断”。即使你不开更新中断,UEV仍然可能发生,并负责把 preload 传到 active/shadow。很多新手把“没有进中断”等同于“没有更新事件”,这会把问题想偏。
6. 一个完整时序:ARR 在周期中间被修改
假设:
ARR_active = 999 ARR_preload = 999 CNT = 800 ARPE = 1 软件写入新的 ARR = 499时序如下:
| 时间点 | CNT | 软件动作 | 硬件真正使用的周期值 |
|---|---|---|---|
| T0 | 0 | 初始同步完成 | ARR_active = 999 |
| T1 | 1 到 799 | 正常输出 PWM | 999 |
| T800 | 800 | 写TIMx_ARR = 499 | 仍然是 999 |
| T801 到 T999 | 801 到 999 | 无 | 仍然是 999 |
| 周期边界 | 999 后回到 0 | 自动产生UEV | preload 499 搬到 active |
| 新周期 | 0 开始 | 无 | ARR_active = 499 |
所以你在 T800 写入的新周期没有立刻改变当前 PWM,而是从下一个完整周期开始生效。
这不是延迟的 bug,而是硬件在保护你的波形。
7. 坑位一:上电后第一个 PWM 周期不对
典型现象:
配置好ARR和CCR,启动 PWM 后,示波器看到第一个周期不是预期占空比。它可能是 0%,可能是 100%,也可能只是第一个高电平比后面宽。
原因通常有两个:
你打开了
ARR/CCR预装载,但启动计数器前没有产生一次UG,导致 active/shadow 里还是复位值或旧值。通道输出先于计数器启动被使能,
CNT停在 0,而 PWM 模式和极性已经让输出进入某个电平状态。等计数器真正启动时,这段启动前的电平会和第一个脉冲连在一起,看起来就像第一个脉冲变宽。
对 STM32F4,初始化阶段推荐这样做:
// 标准外设库示例,GPIO 复用配置省略voidTIM3_PWM_CH1_Init(uint16_tpsc,uint32_tarr,uint32_tccr){TIM_TimeBaseInitTypeDef tb;TIM_OCInitTypeDef oc;TIM_TimeBaseStructInit(&tb);TIM_OCStructInit(&oc);RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);tb.TIM_Prescaler=psc;tb.TIM_CounterMode=TIM_CounterMode_Up;tb.TIM_Period=arr;tb.TIM_ClockDivision=TIM_CKD_DIV1;TIM_TimeBaseInit(TIM3,&tb);TIM_ARRPreloadConfig(TIM3,ENABLE);oc.TIM_OCMode=TIM_OCMode_PWM1;oc.TIM_OutputState=TIM_OutputState_Enable;oc.TIM_Pulse=ccr;oc.TIM_OCPolarity=TIM_OCPolarity_High;TIM_OC1Init(TIM3,&oc);TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);// 关键:启动前手动产生一次更新事件,把 PSC/ARR/CCR 的预装载值刷入 active/shadowTIM_GenerateEvent(TIM3,TIM_EventSource_Update);// 如果后面要开更新中断,建议先清掉这次 UG 可能留下的 UIFTIM_ClearFlag(TIM3,TIM_FLAG_Update);TIM_Cmd(TIM3,ENABLE);}如果是 TIM1/TIM8 这类高级定时器,还要注意主输出使能MOE的时机。通常把 GPIO、模式、预装载、UG、清标志都准备好之后,再启动计数器和输出。
8. 坑位二:运行中同时改频率和占空比,偶发尖峰
典型现象:
你在电机调速或功率控制中同时修改ARR和CCR。大多数时候没事,但偶尔会出现一个异常脉冲,电流突然尖一下。
根因通常不是“ARR 有影子寄存器”,而是多个相关参数没有在同一个边界一起生效。
比如:
TIM_SetAutoreload(TIM3,new_arr);TIM_SetCompare1(TIM3,new_ccr);如果ARR开了预装载而CCR没开,或者这两次写入刚好跨过一次UEV,就可能出现一个周期里“新频率配旧占空比”或“旧频率配新占空比”的状态。
功率控制中更推荐这几条:
ARR开启预装载:ARPE = 1。所有相关 PWM 通道的
CCR都开启预装载:OCxPE = 1。尽量在更新中断刚发生后写入下一周期参数,这样距离下一次
UEV有完整一个周期,多个寄存器更不容易跨边界。如果必须在主循环或控制线程里批量写,短时间设置
UDIS = 1冻结更新,写完后恢复,让下一次自然UEV同步生效。
示例:
voidTIM3_PWM_CH1_UpdateAtNextUEV(uint32_tnew_arr,uint32_tnew_ccr){uint32_tprimask=__get_PRIMASK();__disable_irq();// ENABLE 表示设置 UDIS=1,也就是暂时禁止更新事件TIM_UpdateDisableConfig(TIM3,ENABLE);TIM_SetAutoreload(TIM3,new_arr);TIM_SetCompare1(TIM3,new_ccr);// 恢复更新事件。新的 ARR/CCR 会在下一次自然 UEV 同步进入 active/shadowTIM_UpdateDisableConfig(TIM3,DISABLE);if(primask==0U){__enable_irq();}}不要把下面这句当作运行中无条件推荐的做法:
TIM_GenerateEvent(TIM3,TIM_EventSource_Update);它确实能强制同步,但它也会立刻打断当前计数相位。对初始化很有用;对正在输出的电机 PWM、同步采样 PWM、互补 PWM,则要谨慎。很多场景里,更好的做法是等下一次自然UEV,或把更新放进定时器更新中断。
更新中断里的写法大致是:
voidTIM3_IRQHandler(void){if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET){TIM_ClearITPendingBit(TIM3,TIM_IT_Update);TIM_SetAutoreload(TIM3,next_arr);TIM_SetCompare1(TIM3,next_ccr);}}因为这段代码在一个UEV刚发生后执行,写进去的新 preload 值会等到下一次UEV再生效。它牺牲了一个周期的延迟,换来的是非常干净的同步边界。
9. 坑位三:调试时读 CCR,为什么和波形对不上
当OCxPE = 1时,软件访问TIMx_CCRy看到的是预装载侧的值,不是正在参与比较的 active/shadow 值。
这会导致一个很迷惑的现象:
TIM_SetCompare1(TIM3,700);uint16_treadback=TIM_GetCapture1(TIM3);你读回来的可能已经是700,但当前 PWM 周期仍然可能在用上一个 active 值。只有下一次UEV后,700才真正参与比较。
所以调试时要记住:
软件读回寄存器,只能证明 preload 侧写进去了。
它不能证明 shadow/active 已经生效。
要判断真实波形,只相信示波器、逻辑分析仪,或者用另一个定时器输入捕获去测量输出脚。
如果你正在 Debug 窗口里盯着CCR1看,却发现它和引脚波形不同步,这正是预装载机制在工作。
10. 坑位四:ARR 从 0 改成非 0,定时器还是不跑
这个坑在 STM32 里很经典。
如果ARPE = 1,而 active/shadow 里的ARR当前为 0,计数器可能没有机会通过自然溢出产生下一次UEV。这时你再写TIMx_ARR = 999,写进去的只是 preload;active 仍然是 0,定时器看起来就像“卡死”。
解决办法:
TIM_SetAutoreload(TIM3,999);TIM_GenerateEvent(TIM3,TIM_EventSource_Update);TIM_ClearFlag(TIM3,TIM_FLAG_Update);TIM_Cmd(TIM3,ENABLE);初始化阶段产生一次UG,不只是为了首周期占空比正确,也是为了把PSC和ARR的初值真正装入硬件。
11. 用逻辑分析仪验证这个机制
可以做一个非常直观的实验。
实验条件:
TIM3_CH1 输出 PWM PSC = 83 ARR 初始为 999 CCR 初始为 400 在 CNT 大约等于 800 时,把 ARR 改成 499对比两组:
ARPE = 1:当前周期完整跑完,到下一个周期才变成新频率。ARPE = 0:当前周期被破坏。尤其当新ARR小于当前CNT时,可能出现异常长周期或其他不可接受的边界行为。
再加一组:
OC1PE = 0:周期中间修改CCR1,边沿可能立刻移动。OC1PE = 1:周期中间修改CCR1,当前周期不被打扰,下一个UEV后占空比改变。
示波器上看到这几组差异后,影子寄存器这件事基本就从“概念”变成“直觉”了。
12. 最推荐的 STM32F4 PWM 配置模板
如果只是普通 PWM 输出,可以按下面的顺序记:
配 GPIO 复用和时钟。
配
PSC、ARR、计数模式。打开
ARR预装载:ARPE = 1。配 PWM 模式、极性、
CCR。打开相关通道
CCR预装载:OCxPE = 1。启动前写
EGR.UG,把 preload 初值刷入 active/shadow。清
UIF,避免后续一开中断就进一次假更新。启动计数器和输出。TIM1/TIM8 还要处理
BDTR.MOE。
如果是电机或电源控制,再加三条:
运行中更新
ARR/CCR/PSC时,让相关量在同一个UEV生效。不要随手在运行中
UG,除非你明确希望立即重装并接受相位变化。多通道、多参数更新可以考虑更新中断、DMA burst、或者高级定时器的 COM 同步机制。
13. 总结
STM32F4 定时器的影子寄存器不是玄学,它就是一个硬件级的双缓冲:
CPU 写入 -> preload UEV 同步 -> active/shadow 硬件比较 -> PWM 输出理解它以后,很多 PWM 问题都会变得清楚:
为什么改了
ARR,频率不是马上变。为什么改了
PSC,计数频率要等更新事件。为什么
CCR读回来了,但波形还没变。为什么启动前要手动
UG。为什么运行中乱用
UG会破坏相位。
真正可靠的 PWM 更新策略,不是“写寄存器越快越好”,而是让影响波形的寄存器在正确的周期边界一起生效。
这就是 STM32F4 定时器预装载和影子寄存器最值得掌握的地方。
参考资料
STMicroelectronics, AN4776: How to use general-purpose timer peripheral on STM32 MCUs
STMicroelectronics, STM32F4 series documentation page
STMicroelectronics, RM0090 Reference manual for STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439