
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.UGUpdate 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}{(PSC1) \times (ARR1)}fPWM(PSC1)×(ARR1)TIM_CLK② 占空比决定输出有效电平的比例D C C R A R R 1 × 100 % D \frac{CCR}{ARR1} \times 100\%DARR1CCR×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/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)。从模式控制器触发。例如主从定时器同步、外部触发复位等场景。还要注意两个控制位位含义工程影响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 999T11 到 799正常输出 PWM999T800800写TIMx_ARR 499仍然是 999T801 到 T999801 到 999无仍然是 999周期边界999 后回到 0自动产生UEVpreload 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_Prescalerpsc;tb.TIM_CounterModeTIM_CounterMode_Up;tb.TIM_Periodarr;tb.TIM_ClockDivisionTIM_CKD_DIV1;TIM_TimeBaseInit(TIM3,tb);TIM_ARRPreloadConfig(TIM3,ENABLE);oc.TIM_OCModeTIM_OCMode_PWM1;oc.TIM_OutputStateTIM_OutputState_Enable;oc.TIM_Pulseccr;oc.TIM_OCPolarityTIM_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 表示设置 UDIS1也就是暂时禁止更新事件TIM_UpdateDisableConfig(TIM3,ENABLE);TIM_SetAutoreload(TIM3,new_arr);TIM_SetCompare1(TIM3,new_ccr);// 恢复更新事件。新的 ARR/CCR 会在下一次自然 UEV 同步进入 active/shadowTIM_UpdateDisableConfig(TIM3,DISABLE);if(primask0U){__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_treadbackTIM_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写进去的只是 preloadactive 仍然是 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 MCUsSTMicroelectronics, STM32F4 series documentation pageSTMicroelectronics, RM0090 Reference manual for STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439