news 2026/9/8 15:21:06

简单谈谈STM32F4 定时器影子寄存器:ARR、CCR、PSC 为什么不会总是立即生效

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
简单谈谈STM32F4 定时器影子寄存器:ARR、CCR、PSC 为什么不会总是立即生效

STM32F4 定时器影子寄存器详解:ARR、CCR、PSC 为什么不会总是立即生效

很多人在调 STM32F4 的 PWM 时都会遇到一个很反直觉的现象:代码里明明已经改了ARRCCRPSC,示波器上的波形却没有立刻变化,往往要等到下一个周期才生效。

这通常不是代码没执行,也不是示波器骗人,而是 STM32 定时器内部有一套很重要的机制:预装载寄存器和影子寄存器。

一句话先说结论:

STM32F4 定时器用“预装载寄存器”接收软件写入,用“影子寄存器/活动寄存器”参与硬件比较,并在更新事件UEV到来时把预装载值同步到真正生效的影子值,从而避免 PWM 在周期中间被改坏。

本文以 STM32F4 系列为对象,不同 F4 子系列的定时器数量和高级功能会有差别,但ARRCCRPSC的预装载逻辑大体一致。


先简述PWM

对于初学者,可能会把PWM(脉宽调制)理解成硬件外设等等,但本质上是一个物理电气过程,是电压在“高电平”和“低电平”之间高速切换的物理现象。固件(代码)是用来操控物理过程的“指令”,真正产生电信号的定时器、比较器等是硬件外设。

简单三句话说明

  • 核心动作:在一个固定周期(频率),快速的把电压“接通”,”断开“。
  • 关键指标:占空比(有效电平时间占整个周期的百分比)。
  • 最终效果:利用人眼/电机的“惯性”平滑效应,等效出一个平均电压。占空比越高,等效输出电压越高。

1. 先把 STM32F4 上的几个事实讲准

原文里有几个常见说法需要按 STM32F4 的真实行为修正:

  1. TIM_ARRPreloadConfig(TIMx, ENABLE)只是打开ARR的预装载功能,也就是设置ARPE(自动重装载预装载使能)位;它不是写入新的ARR值。标准外设库里修改周期应使用TIM_SetAutoreload(TIMx, new_arr)
    • ARPE = 0(立即生效):修改 ARR 值后,新周期立刻生效。这可能导致当前正在进行的PWM周期被强行截断,输出一个不完整的畸形脉冲。
    • ARPE = 1(缓冲使能,强烈推荐):修改的是预装载寄存器,真正的影子寄存器只在定时器溢出更新事件时才加载新值。这样能保证当前周期完整跑完,下一个周期再按新频率执行,波形过渡极其平滑。
  2. PSC有预装载机制,但在 STM32 定时器里它的预装载不能关闭。也就是说,运行中修改PSC后,新分频值通常要等下一次更新事件才会真正进入分频器。
  3. CCR不是“立即改也无所谓”。如果没有打开OCxPE(输出比较通道预装载使能),运行中改CCR会立即影响比较点,可能导致当前 PWM 周期出现异常边沿。对 LED 呼吸灯这可能只是轻微抖动;对电机、半桥、全桥、电源控制,这就可能是很难看的尖峰。
    • OCxPE = 0(预装载禁用):立即生效。你修改CCR,输出波形会立刻改变。适合需要即时快速响应的场景(如软件实时调节)。
    • OCxPE = 1(预装载使能,推荐):平滑更新。你修改的是“预装载寄存器”,真正的“影子寄存器”只在更新事件(如定时器溢出)时才更新。这能保证一个完整的PWM周期内参数恒定,防止产生不完整的畸形波形。
  4. 运行中手动写EGR.UG(Update Generation,更新生成,写入1强制生成更新事件EUV)不是一个完全无副作用的“同步按钮”。它会立即产生更新动作,还会重新初始化计数器/预分频器相关状态。启动前用UG刷影子很常见;运行中强行UG要确认你能接受相位被打断。
    • 作用:立即“刷”新影子寄存器。当你修改了 ARR(周期)或 CCR(占空比)的预装载值后,正常要等定时器溢出才能生效。而置位 UG 会立即把这些新值从预装载寄存器拷贝到影子寄存器,并同时将计数器 CNT 清零。
    • 最大价值(初始化必做):在启动定时器之前,配置好 ARR/CCR 后务必置位一次 UG。这能确保定时器从 0 开始第一个周期就使用你设定的正确值,否则第一个脉冲可能长度异常或错误。
    • 附带效应:置位 UG 会触发更新中断标志(UIF),如果开启了中断,会立刻进中断服务函数。
  5. 如果关闭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 = 999CNT = 800,这时软件想把周期改短,写入ARR = 499

如果没有预装载保护,定时器正在跑的这个周期就会遇到麻烦:当前计数值已经超过新的终点。硬件接下来到底表现为异常长周期、异常短周期,还是某些边沿错乱,取决于具体定时器实现和计数模式。无论哪种结果,它都不是一个完整、可预测、干净的 PWM 周期。

影子寄存器解决的正是这个问题:软件可以先写新参数,但硬件仍然用旧参数完成当前周期,等到更新事件UEV到来,再把新参数一次性搬到活动寄存器里。

3. 预装载和影子寄存器的双层模型

当某个定时器寄存器开启预装载后,它在硬件里有两个实例。

层级软件能否直接访问硬件是否用它产生 PWM作用
预装载寄存器 preload暂时不用接收 CPU 写入的新值
活动寄存器 active,也常叫 shadow通常不能直接读写真正参与计数、分频、比较和输出

开启预装载后,CPU 写入的是 preload。只有当更新事件UEV到来时,preload 才会被搬运到 active/shadow。

PWM 输出active/shadowpreloadCPU 写寄存器PWM 输出active/shadowpreloadCPU 写寄存器周期中写入新 ARR/CCR/PSC当前周期继续使用旧 active 值UEV 到来时同步新周期使用新 active 值

这个“等到边界再切换”的动作,就是 PWM 不容易被写寄存器动作打出毛刺的核心。

4. STM32F4 里哪些东西有预装载

下面这张表建议直接记住。

对象STM32F4 上的预装载行为控制位或接口典型用途
TIMx_PSC有预装载,不能关闭无独立关闭位改计数频率,下一次UEV生效
TIMx_ARR可选TIMx_CR1.ARPE改 PWM 周期,建议动态 PWM 中开启
TIMx_CCRy可选TIMx_CCMR1/2.OCyPE改占空比,功率控制中建议开启
CCxE/CCxNEOCxM部分高级定时器支持预装载常与 COM 事件相关电机换相、互补输出、刹车相关控制
TIMx_RCR高级定时器相关TIM1/TIM8 常见控制多少个周期才产生一次真正更新

几个工程提醒:

  1. ARRARPE复位后通常是关闭的,想要周期在边界更新,需要显式打开。

  2. CCROCxPE复位后也通常是关闭的。使用 Cube HAL 配 PWM 时,HAL 的 PWM 配置流程通常会帮你设置通道预装载,但不要凭感觉判断,调试时直接看CCMR1/CCMR2OCxPE位。

  3. PSC修改后不一定立刻改变计数频率,因为它的预装载不能关闭。初始化阶段通常要手动产生一次UG,让PSC立即装入。

  4. 使用 TIM1/TIM8 时,如果配置了重复计数器RCR,预装载值不是每次溢出都必然生效,而是在真正产生UEV时生效。RCR = N时,常见理解是每N + 1个计数周期才产生一次更新事件。

5. UEV 是真正的同步闸门

更新事件UEV是影子寄存器同步的关键节点。STM32F4 定时器里常见的UEV来源有:

  1. 计数器溢出或下溢。向上计数时,通常是从ARR对应的周期边界回到 0 附近。(这个很重要!)

  2. 软件写TIMx_EGR.UG。标准外设库对应TIM_GenerateEvent(TIMx, TIM_EventSource_Update)

  3. 从模式控制器触发。例如主从定时器同步、外部触发复位等场景。

还要注意两个控制位:

含义工程影响
UDISUpdate disable置 1 后禁止产生更新事件,预装载不会搬到 shadow。可以短时间用于批量写参数,但不要长时间忘记恢复
URSUpdate request source限制哪些更新源产生更新中断或 DMA 请求。它常用于避免软件UG引起不想要的中断请求

重点是:UEV不只是“中断”。即使你不开更新中断,UEV仍然可能发生,并负责把 preload 传到 active/shadow。很多新手把“没有进中断”等同于“没有更新事件”,这会把问题想偏。

6. 一个完整时序:ARR 在周期中间被修改

假设:

ARR_active = 999 ARR_preload = 999 CNT = 800 ARPE = 1 软件写入新的 ARR = 499

时序如下:

时间点CNT软件动作硬件真正使用的周期值
T00初始同步完成ARR_active = 999
T11 到 799正常输出 PWM999
T800800TIMx_ARR = 499仍然是 999
T801 到 T999801 到 999仍然是 999
周期边界999 后回到 0自动产生UEVpreload 499 搬到 active
新周期0 开始ARR_active = 499

所以你在 T800 写入的新周期没有立刻改变当前 PWM,而是从下一个完整周期开始生效。

这不是延迟的 bug,而是硬件在保护你的波形。

7. 坑位一:上电后第一个 PWM 周期不对

典型现象:

配置好ARRCCR,启动 PWM 后,示波器看到第一个周期不是预期占空比。它可能是 0%,可能是 100%,也可能只是第一个高电平比后面宽。

原因通常有两个:

  1. 你打开了ARR/CCR预装载,但启动计数器前没有产生一次UG,导致 active/shadow 里还是复位值或旧值。

  2. 通道输出先于计数器启动被使能,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. 坑位二:运行中同时改频率和占空比,偶发尖峰

典型现象:

你在电机调速或功率控制中同时修改ARRCCR。大多数时候没事,但偶尔会出现一个异常脉冲,电流突然尖一下。

根因通常不是“ARR 有影子寄存器”,而是多个相关参数没有在同一个边界一起生效。

比如:

TIM_SetAutoreload(TIM3,new_arr);TIM_SetCompare1(TIM3,new_ccr);

如果ARR开了预装载而CCR没开,或者这两次写入刚好跨过一次UEV,就可能出现一个周期里“新频率配旧占空比”或“旧频率配新占空比”的状态。

功率控制中更推荐这几条:

  1. ARR开启预装载:ARPE = 1

  2. 所有相关 PWM 通道的CCR都开启预装载:OCxPE = 1

  3. 尽量在更新中断刚发生后写入下一周期参数,这样距离下一次UEV有完整一个周期,多个寄存器更不容易跨边界。

  4. 如果必须在主循环或控制线程里批量写,短时间设置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才真正参与比较。

所以调试时要记住:

  1. 软件读回寄存器,只能证明 preload 侧写进去了。

  2. 它不能证明 shadow/active 已经生效。

  3. 要判断真实波形,只相信示波器、逻辑分析仪,或者用另一个定时器输入捕获去测量输出脚。

如果你正在 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,不只是为了首周期占空比正确,也是为了把PSCARR的初值真正装入硬件。

11. 用逻辑分析仪验证这个机制

可以做一个非常直观的实验。

实验条件:

TIM3_CH1 输出 PWM PSC = 83 ARR 初始为 999 CCR 初始为 400 在 CNT 大约等于 800 时,把 ARR 改成 499

对比两组:

  1. ARPE = 1:当前周期完整跑完,到下一个周期才变成新频率。

  2. ARPE = 0:当前周期被破坏。尤其当新ARR小于当前CNT时,可能出现异常长周期或其他不可接受的边界行为。

再加一组:

  1. OC1PE = 0:周期中间修改CCR1,边沿可能立刻移动。

  2. OC1PE = 1:周期中间修改CCR1,当前周期不被打扰,下一个UEV后占空比改变。

示波器上看到这几组差异后,影子寄存器这件事基本就从“概念”变成“直觉”了。

12. 最推荐的 STM32F4 PWM 配置模板

如果只是普通 PWM 输出,可以按下面的顺序记:

  1. 配 GPIO 复用和时钟。

  2. PSCARR、计数模式。

  3. 打开ARR预装载:ARPE = 1

  4. 配 PWM 模式、极性、CCR

  5. 打开相关通道CCR预装载:OCxPE = 1

  6. 启动前写EGR.UG,把 preload 初值刷入 active/shadow。

  7. UIF,避免后续一开中断就进一次假更新。

  8. 启动计数器和输出。TIM1/TIM8 还要处理BDTR.MOE

如果是电机或电源控制,再加三条:

  1. 运行中更新ARR/CCR/PSC时,让相关量在同一个UEV生效。

  2. 不要随手在运行中UG,除非你明确希望立即重装并接受相位变化。

  3. 多通道、多参数更新可以考虑更新中断、DMA burst、或者高级定时器的 COM 同步机制。

13. 总结

STM32F4 定时器的影子寄存器不是玄学,它就是一个硬件级的双缓冲:

CPU 写入 -> preload UEV 同步 -> active/shadow 硬件比较 -> PWM 输出

理解它以后,很多 PWM 问题都会变得清楚:

  1. 为什么改了ARR,频率不是马上变。

  2. 为什么改了PSC,计数频率要等更新事件。

  3. 为什么CCR读回来了,但波形还没变。

  4. 为什么启动前要手动UG

  5. 为什么运行中乱用UG会破坏相位。

真正可靠的 PWM 更新策略,不是“写寄存器越快越好”,而是让影响波形的寄存器在正确的周期边界一起生效。

这就是 STM32F4 定时器预装载和影子寄存器最值得掌握的地方。

参考资料

  1. STMicroelectronics, AN4776: How to use general-purpose timer peripheral on STM32 MCUs

  2. STMicroelectronics, STM32F4 series documentation page

  3. STMicroelectronics, RM0090 Reference manual for STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 15:18:42

从npx skill add到ponytail:理解skill包与命令行任务编排

最近热搜榜上出现了一个挺有意思的词组: ponytail 。按常理,这词儿该出现在美妆区或者穿搭区,结果点进去一看,满屏都是 npx skill add dietrichgebert/ponytail 这条安装命令。常年混 DevTools 圈子的朋友看到这行字应该秒懂—…

作者头像 李华
网站建设 2026/9/8 15:16:27

多分类问题核心机制与PyTorch实践:Softmax与交叉熵

1. 多分类的定位:从单一答案到概率分布1.1 为什么多分类不是二分类的简单扩展Day18这节课的标题看起来很简单,就叫“多分类问题”,但我跟完整个课程后发现,它其实是很多入门选手第一次真正触碰到"模型如何表达不确定"的…

作者头像 李华
网站建设 2026/9/8 15:16:08

从单片机控制到系统工程,嵌入式还值得学吗?

上周末一个做前端的朋友突然问我:“现在学嵌入式怎么样?”他说自己写了几年业务代码,感觉有点飘,想找点有“硬货”的方向沉淀一下。这个问题如果放五年前,答案会简单很多——学单片机、C语言、电路基础,会点…

作者头像 李华
网站建设 2026/9/8 15:16:02

AI Agent Skills 完全指南:从 Prompt 到可复用工作流

最近一段时间,GitHub 上被一个词刷屏了,就是skills。点进去一看,有叫superpower skills的,有叫baoyu skills的,还有各种claude code skills、codex skills、opencode skills。如果你跟我一样,第一反应是“这…

作者头像 李华
网站建设 2026/9/8 15:14:38

Spring Boot自动配置深度拆解:从条件注解到源码实战

第一次真正自己动手去翻 Spring Boot 自动配置源码的时候,我印象很深。当时项目出了个诡异的问题:本地 Redis 连得好好的,一上测试环境就抛 bean 不存在,报错信息里说的是 StringRedisTemplate 没注入进去。我第一反应是代码写错了…

作者头像 李华
网站建设 2026/9/8 15:13:55

GitNexus:用工程纪律驯服AI代码生成,防止改崩项目

周五晚上十点多,我正准备把分支合进主干,Git 弹出一行提示:一共改了 43 个文件。我只让 AI 把工具函数 formatUser 的入参从两个改成三个,它倒好,顺着调用链把项目里所有用到这个函数的地方全改了一遍,连…

作者头像 李华