
搞嵌入式这几年要说被问得最多的问题定时器绝对排第一。而这其中PSC、ARR、时钟源这三个词简直是新手翻车重灾区。我见过太多人拿着一个看起来很合理的公式算出个“应该没错”的数下载到板子里却发现——定时时间整整快了一倍或者PWM占空比死活到不了100%。最气人的是这东西看代码根本看不出毛病寄存器配置看起来都对就是用起来不对。这篇就想把这几个“最容易算错”的点彻底掰开揉碎。我会结合STM32常用的F1和F4系列从时钟树怎么走、PSC和ARR的底层逻辑到具体怎么算、算完怎么验证把每一步背后的原理讲清楚。这篇文章适合刚用标准库或HAL库写定时器的人也适合那些定时时间、PWM频率总差那么一点却不知道去哪查的老手。看完你就能搞清楚为什么算出来是10ms实际却是5ms为什么占空比设了100%却一直没输出以及CubeMX里那些时钟配置到底在背后干了什么。1. 时钟源定时器背后的“隐藏乘法器”很多人第一步就栽在这。不是说不会配时钟而是根本不知道STM32定时器时钟有个特殊的“加倍”规则——APB预分频系数不等于1的时候定时器时钟会自动变成APB的2倍。这个规则隐藏在参考手册的时钟树图里但绝大多数人只会看CubeMX生成的SystemClock_Config从来不会去翻那根线怎么走的。1.1 APB1和APB2的分频系数决定了定时器能否“偷跑”拿最经典的STM32F103来说系统时钟最高72MHz。这里有两条总线APB1的最高频率是36MHzAPB2是72MHz。低速外设挂在APB1上比如USART2、I2C、TIM2~TIM7高速外设挂在APB2上像GPIO、ADC、TIM1和TIM8。算定时器时钟的时候关键就在APB分频器上系统时钟72MHzAPB1要压在36MHz以内就得2分频此时APB1 36MHz。但APB1定时器时钟不是36MHz而是36MHz × 2 72MHz。APB2如果1分频也就是保持72MHz这种情况下定时器时钟是72MHz不翻倍。如果APB2也2分频成36MHz那TIM1和TIM8照样翻倍成72MHz。这个规则在STM32参考手册里写得清清楚楚当APB预分频系数为1时定时器时钟 APB时钟当APB预分频系数不等于1时定时器时钟 APB时钟 × 2。好多人直接把“APB1 36MHz”当成“定时器时钟 36MHz”来算结果就是预分频后计数频率全部少了一半算出来的定时时间翻倍PWM频率砍半。1.2 F4系列更要注意不同定时器时钟源不一样如果你用的是STM32F407这种情况稍微复杂一点。F407系统时钟最高168MHzAPB1最高42MHzAPB2最高84MHz。假设你的时钟配置是AHB 168MHzAPB1 42MHz4分频APB2 84MHz2分频。此时TIM2、TIM3、TIM4、TIM5、TIM6、TIM7这些挂在APB1上的定时器时钟 42MHz × 2 84MHz。TIM1、TIM8、TIM9~TIM11这些挂在APB2上的84MHz × 2 168MHz。你没看错同一颗芯片上不同定时器的时钟源差一倍TIM1桶里跑到168MHzTIM3却只有84MHz。有的项目里用TIM1做高频PWM用TIM3做低速延时如果你都用“总线频率”去算必然一个偏快一个偏慢。我自己的习惯是每次新建工程后先在纸上把时钟树画一遍标清AHB、APB1、APB2和定时器倍增器再挂上对应定时器最后才去写PSC和ARR。虽然多花三分钟但能省掉后面排查好几小时的频偏问题。1.3 CubeMX配置里怎么确认定时器时钟用CubeMX的朋友可以打开“Clock Configuration”页面里面会显示APB1和APB2的分频系数。比如STM32F103设系统时钟72MHz后APB1分频器会显示“/2”APB2显示“/1”。这时候你点开某个定时器的配置比如TIM2CubeMX会在底下显示“Timer Clock: 72MHz”之类的一行看到是72MHz就对了。如果你用的是标准库或者直接撸寄存器那就只能自己算。查一下RCC_CFGR寄存器的PPRE1和PPRE2位就能知道分频系数然后根据规则算出定时器时钟。这一步别偷懒后面所有计算都建立在这个数上。我见过一个比较典型的坑有人用CubeMX生成工程后把timebase source从SysTick改成了TIM7原因是想自己用SysTick做微秒延时。结果发现HAL_Delay完全错乱因为CubeMX生成的HAL库底层是靠timebase source做延时基础的你把它挪到TIM7如果TIM7的PSC、ARR和原本SysTick的1ms节奏不一样整个HAL_Delay就废了。这个设置本身是个合法用法但你必须清楚它改变了什么——后面章节我会专门讲。2. PSC计算预分频器里的“减一”陷阱PSC全称Prescaler预分频器。它的作用是把定时器时钟降低到我们能用的计数频率。公式大家都知道计数频率 定时器时钟 / (PSC 1)。问题就出在那个“1”上。如果定时器时钟是72MHz你想得到1MHz的计数频率PSC应该写71不是72。2.1 为什么PSC要减一因为计数器从0开始我们看硬件结构预分频器本质上是一个计数器定时器时钟每来一个脉冲预分频计数器就加一次从0数到PSC的值然后归零并输出一个计数脉冲给主计数器。所以它实际分频的倍数是“PSC计数值1”。举个例子就明白了PSC 0预分频计数器数0就归零不是PSC0时计数器从0开始第1个时钟脉冲到达后就匹配并归零相当于每个时钟脉冲都输出一次这就是1分频PSC11。PSC 1呢数0、1两个状态每两个输入脉冲输出一个脉冲2分频。所以当你想要10分频的时候寄存器里应该写9。这不是什么隐藏逻辑这是硬件计数从0开始的必然结果。但架不住忙起来手一快就写错。2.2 小数分频怎么处理取整 误差评估另一个PSC相关的问题是小数分频。比如定时器时钟72MHz你非要弄一个5MHz的计数时钟72 / 5 14.4PSC就没法取整了。有人说可以选PSC13这样计数时钟是72/14 ≈ 5.142857MHz误差2.86%。如果只是LED闪烁或者按键扫描这个误差完全无所谓。但如果你是要做高精度测量或者串口波特率这种误差就得认真算。处理小数分频的标准思路是先从系统时钟出发把PSC和ARR作为两个变量列出方程组寻找整数解。比如你要产生一个精确的1kHz PWM计数时钟与周期的关系是定时器时钟 / ((PSC 1) * (ARR 1)) 目标频率72MHz的定时器时钟如果直接选PSC 71得1MHz计数时钟那ARR需要999完美匹配。但如果要3kHz呢1MHz / 3000 ≈ 333.33ARR没法取整。试试其他PSC组合72000000 / (48 * 500) 3000PSC47、ARR499正好。所以别死盯着一个PSC不放多试几组整数解往往能凑出精确频率。2.3 修改PSC要注意影子寄存器几乎所有STM32定时器的PSC都有影子寄存器意思是你运行时修改PSC不会立刻生效要等更新事件发生后才会把影子值传到实际工作的寄存器里。有些芯片还支持预装载功能通过TIMx_CR1的ARPE位控制ARR是否也能缓冲更新。这个细节对生产调试影响很大。很多人做动态变频比如电机启动时降低PWM频率于是直接改PSC寄存器却发现频率变了一下又跳回去或者完全不变排查半天发现是忘了触发更新事件。正确做法是改完PSC后设置TIMx_EGR的UG位产生一次更新事件强制把影子寄存器内容装载进去然后注意清除更新标志位。如果你用HAL库__HAL_TIM_SET_PRESCALER宏也会帮你处理一部分工作但底层触发UG的逻辑还是要稍微留意。我在实际项目中一般会在PSC初始化后先手动产生一次更新事件再开启中断这样能确保第一个周期的定时值就是精确的不会因影子寄存器里残留旧值导致首个中断提前或延后。3. ARR计算边界条件和中断标志比你想象的坑ARRAuto-Reload Register自动重载寄存器。它决定了计数器从0数到多少算一轮。多数人都能背出公式定时周期 (PSC 1) × (ARR 1) / 定时器时钟。可一旦涉及到具体场景ARR的边界问题就开始暴露。3.1 计数范围是0到ARR这一圈其实是ARR1个脉冲这是ARR最容易忽略的点。假设定时器时钟1MHz每个计数脉冲1微秒你要定时1毫秒应该设置ARR为999而不是1000。为什么计数器从0开始依次经过0、1、2……直到999这中间已经过了1000个计数脉冲正好1毫秒第1000次溢出发生在CNT从999回0的瞬间。如果把ARR设成1000那就是1001个脉冲1.001毫秒。表面看误差0.1%很多场合无所谓但在高频场景下就完全不一样了。比如你要做1kHz PWMARR 999时周期是1000个计数时钟PWM频率 计数时钟 / 1000正好1kHz。如果ARR取1000频率变成999Hz你拿频率计一测怎么都差那么一点还不容易想到是这个1在作怪。3.2 更新中断到底什么时候产生CNT和ARR相等不算这个问题对应一个很典型的现象我设置了ARRCNT明明已经到了ARR的值为什么中断不触发很多人被“自动重载”这个名字误导以为计数器走到ARR就触发中断。实际上向上计数模式下更新事件发生在CNT溢出那一瞬间也就是CNT从ARR加1回到0的跳变沿。你用调试器看寄存器时CNT确实会等于ARR但这时候中断还没产生你再往下单步CNT跳回0的那一下更新中断标志UIF才会置位。这对写代码有什么实际影响如果你在中断里读取CNT想判断“当前是不是正好一个周期”当你读到CNTARR时其实已经接近周期末尾了真正的周期结束要等CNT翻转到0。某些对时序敏感的控制逻辑比如电机PWM换相或者ADC同步采样如果以CNTARR为触发点和以溢出中断为触发点时序上会有细微差别可能导致采样点偏差。3.3 占空比100%的老大难问题CCR和ARR的关系标题里的热搜词里有一条“stm32f103芯片 定时器输出占空比到不了100”这个我太有体会了。先说结论在很多情况下你把CCR设置成和ARR一样的值期望输出全高但实际得到的并不是100%占空比而可能是99.9%。原因在PWM模式的比较逻辑上。以向上计数、PWM模式1为例输出电平在CNT CCR时有效CNT CCR时无效。如果你设CCR ARR那么理论上CNT在0到ARR-1时输出有效最后CNT ARR时无效——虽然这个无效窗口只有一个计数时钟那么短但用示波器看占空比确实是99.9%不是100%。想要真正的100%占空比几种办法把CCR设成大于ARR的值比如ARR999CCR1000这样CNT永远不可能大于等于CCR输出全程有效。使用强制输出功能直接让引脚输出高电平。有的定时器在CCR ARR 0这种极端情况下表现不同但常规配置下建议用第一种。反过来0%占空比也有坑。CCR0时PWM模式1下CNT从0开始第一拍就满足CNT CCR输出无效看起来整个周期都是低但如果你设置了反向极性那第一拍可能瞬时拉高一下实际波形会有毛刺。调试占空比边界时最好用示波器看启动瞬间别只看稳态。3.4 ARR的动态更新用PWM做呼吸灯时最典型的场景呼吸灯是练习ARR和CCR动态修改的好例子。你要让LED亮度从暗到亮再从亮到暗通常做法是固定的PSC得到1MHz计数时钟固定ARR 1000得到1kHz PWM频率然后在一个定时器中断里周期性地把CCR从0逐步加到1000再逐步减回0。这里有个隐藏问题只用CCR做渐变1000步的呼吸效果可能有点跳跃。更好的做法是引入二级插值比如每2毫秒改变一次CCR每次变1这样整个呼吸周期2秒比较自然。但重点不在这重点在于你修改CCR时用了预装载功能没有。如果没有开启ARPE位CCR的新值是立即生效的这意味着在一个PWM周期中间改变了占空比波形会出现一个不完整的脉宽人眼虽然看不出来但如果后面接的是可控硅或者电机驱动这种瞬时占空比跳变会引起噪声。所以做PWM动态调节时最好打开ARPE让CCR的新值在下一个更新事件时才加载。这样每个PWM周期都是完整的波形平滑没有毛刺。4. 实操案例把PSC、ARR和时钟源串起来算光说原理还不够扎实我拿一个实际项目片段出来算一遍。这个案例里既有输入捕获测频率又有PWM生成基本把前面三个“计算坑”全过一遍。4.1 案例一用TIM3的输入捕获测一个未知频率方波硬件环境STM32F103系统时钟72MHzCubeMX默认时钟配置APB1 2分频成36MHz所以TIM3时钟是72MHz。首先确认TIM3的时钟是72MHz这在CubeMX里看Timer Clock列是72.0 MHz。然后分两步定PSC和ARR。我要测的目标频率大约在几百赫兹到几十千赫兹。为了兼顾分辨率和量程我选择计数时钟1MHz也就是每微秒计数一次。PSC 72MHz / 1MHz - 1 71。这样计数器每加1代表1微秒测得的周期直接就是微秒数。ARR方面用输入捕获时我不依赖溢出中断ARR可以设成最大65535这样计数范围0到65535微秒即能测的最低频率约为15Hz。对几百赫兹的信号来说周期几千微秒完全够用。如果被测信号频率特别低比如10Hz一个周期100000微秒16位计数器就不够了要么把计数时钟降下来要么加一个溢出计数变量。关键校准步骤来了。算完PSC和ARR后先确认计数时钟。我常用一个土办法把定时器的计数时钟引到某个引脚用频率计测一下它或者用另一个定时器测量这个引脚的频率。如果设了1MHz计数时钟实测引脚上出来的是1.000MHz那就证明PSC没算错。有些前辈会在调试接口里把MCO引脚配置成输出系统时钟用示波器先校准时基再反推定时器时钟这个思路也可行。输入捕获过程不必开中断也可以用DMA但应用层如果只是低频周期测量轮询捕获标志就够了。捕获到两次上升沿的CCR差值就是信号的周期微秒取倒数就是频率。如果信号一直接不上还要加一个超时判断防止计数器溢出导致差值错误。4.2 案例二TIM2输出1kHz、占空比可到100%的PWM同一块板子系统时钟72MHzTIM2挂在APB1上时钟同样是72MHz。我要生成1kHz PWM所以递推计数时钟 72MHz / (PSC 1)。为了方便算取PSC 71得到1MHz。PWM频率 计数时钟 / (ARR 1)。1MHz / 1000 1kHz所以ARR 999。此时CCR的可设置范围是0到999。占空比 CCR / 1000。如果你要一个20%占空比CCR 200。但注意真正要输出100%占空比时前面说的坑就来了。如果你写CCR 999示波器测出来的占空比实际上只有99.9%。你需要让CCR 1000大于ARR或者使用强制输出高。有的HAL库例程里生成PWM时会判断pulse参数不能超过ARR但如果你把它设成ARR1虽然库函数不反对却未必符合“占空比有效范围”的常识。我一般在应用层统一处理——当外部下发占空比等于100%时直接把PWM引脚设为推挽输出高电平不走PWM外设避免CCR和ARR边界上的歧义。4.3 案例三多个定时器如何同步启动热搜里有一条“三个定时器如何同步启动”这也是把定时器当天花板后常遇到的问题。比如三相逆变桥需要三路PWM同时开始输出如果分别启动先启动的定时器会先跑几个周期后启动的就会滞后轻则波形错位重则炸管。STM32提供了主从同步机制。一个定时器做主它的更新事件或者触发输出TRGO可以连到其他定时器的从模式触发输入从而实现同步启动。常见做法是配置主定时器产生一个更新事件从定时器设置为门控模式或触发模式主定时器一旦开始计数从定时器也即刻跟着启动。CubeMX里有专门的主从模式配置界面设置为主模式输出更新事件、从模式选择Trigger模式、触发源选择ITR0之类的内部触发。如果你不想动主从模式还有一个更简单的“伪同步”法用一个GPIO控制多个定时器的CEN位或者让一个定时器中断里同时去置位几个定时器的CEN。不过这种软件同步有一定的延迟抖动精度要求高时不如硬件主从模式可靠。5. 常见问题速查表和排查流程这部分我整理了一份实战中遇到的典型问题对照表基本上每条都是我自己或同事踩过甚至踩过多次的坑。遇到症状先查表能省不少时间。症状可能原因排查方向定时时间约等于预期值一半定时器时钟其实比你以为的时钟高了一倍检查APB分频确认是否触发了定时器×2规则定时时间约为预期值一倍PSC写大了1或ARR写大了1重新用 (PSC1) × (ARR1) 反推PWM频率与计算值差0.1%每次修改ARR或PSC时临界值没减一频率正好是整数倍但差一点时检查是否有1误差占空比到不了100%CCR等于ARR时有效窗口少一个计数时钟设置CCR ARR或强制输出高更新中断不触发CNT到ARR不代表溢出需回0才触发检查是否忘了开更新中断或是否在等待到ARR就return改了PSC/ARR没反应影子寄存器未更新执行UG位产生更新事件或开预装载功能定时器停止时用调试器看CNT一直跳没启用调试冻结功能设置DBGMCU寄存器冻结定时器时钟两个定时器启动时间不一致软件逐个开启CEN位使用主从模式或同时置位多个CEN5.1 调试中验证定时器时钟的土办法有时候配置看起来全对但实际现象就是不对。为了验证定时器时钟到底是不是你以为的72MHz我习惯用两步走第一步在初始化定时器后配置一个最简单的1Hz闪烁或者1ms翻转IO。如果翻转时间和实际差得离谱就说明时钟配置有问题而不是你PSC/ARR的加减法算错了。第二步用MCO引脚把系统时钟输出到示波器。STM32的PA8可以复用为MCO能输出SYSCLK、HSI、HSE、PLL等时钟。你把MCO配成输出PLLCLK用示波器确认系统时钟是不是72MHz再把PA8配成MCO输出SYSCLK逐级确认。如果系统时钟本身就错了后面定时器全错也就正常了。我的经验是调试定时器问题先确定时钟源其次确定PSC和ARR最后看中断。千万不要一上来就怀疑中断优先级配错了。顺序搞反了排查一个通宵都找不到问题。5.2 计算心法拿到项目先列公式再写代码最后分享一个我自己的习惯。我在建定时器工程的时候一定会在代码头部注释里先写上这么一段伪公式TIM_CLK 72000000 // 72MHzAPB1预分频2定时器倍频后 PSC TIM_CLK / f_cnt - 1 ARR f_cnt / f_pwm - 1每次改频率时只要更新目标频率值后面加减一的计算就不会漏。针对不同系列可能还要标记出TIM_CLK的来源比如F4的TIM1是168MHzTIM3是84MHz不标清楚时间一长自己都容易看混。5.3 我踩过的那个“最蠢的坑”和现在是怎么防的早年做一个小东西定时器产生一个10ms中断做按键消抖。我用的F103系统72MHzAPB1 2分频到36MHz。当时的我心算定时器时钟36MHz要10msPSC 3599ARR 99。算完直接焊板子跑结果按键反应奇快消抖失效。示波器一挂发现中断周期只有5ms整整快了一倍。后来翻参考手册看到时钟树里那根“×2”的线恍然大悟。充其量就是我前面1.1节写的那样——APB1分频系数不为1定时器时钟直接翻倍。这件事之后我给自己定了一条规矩任何定时器相关代码上板前先花一分钟把时钟树手动画一遍标注出所用定时器的实际时钟频率再写PSC和ARR。这个习惯之后至少帮我拦下了三次同类型的错误其中包括一次在F407上TIM1和TIM3时钟不一致的坑。5.4 顺带聊聊SysTick、HAL_Delay和timebase source的关系热搜里那几条“滴答定时器”、“cubemx设置timebase source为定时器”其实跟PSC和ARR也有关系。SysTick是Cortex-M内核自带的24位倒计数定时器HAL库默认用它来做HAL_Delay和HAL_GetTick的时基。SysTick没有PSC的概念只有LOAD寄存器你配置好重载值后它从LOAD倒数到0再自动重载。如果你用CubeMX把timebase source改成TIM6或者TIM7原因是想把SysTick留给自己做微秒延时那你要清楚HAL_Delay的精度就完全取决于你给TIM6/TIM7配置的PSC和ARR了。如果配的是1ms中断还好如果配错了HAL_Delay要么飞快要么极慢甚至永远不返回。我见过有人把TIM6配置成100Hz还指望HAL_Delay(1000)能延时1秒结果系统卡了10秒才反应。原因就是PSC/ARR没有按1ms中断来计算。这个时候一定要回头用第2节、第3节的方法先确认定时器时钟再反推PSC、ARR确保更新中断频率 1000Hz这样HAL_Delay才会行为正常。5.5 调试触发中断错位的一个冷门技巧做PWM和ADC采样配合时有时候需要让ADC在PWM周期的特定时刻触发采样。用定时器的TRGO事件去触发ADC是比较常规的做法。但是TRGO的触发点和更新中断之间是有固定相位关系的你如果在更新中断里再去启动ADC那会比TRGO直接触发晚几个时钟周期。解决这个问题我用的是TIM的OCxREF信号做触发源而不是更新事件。通过配置定时器的输出比较通道在指定计数值上产生触发信号可以精确到单个计数时钟的相位控制。这已经超出了PSC和ARR的基本计算但设计思路上要明确TRGO和更新中断是两个不同的硬件信号前者由硬件直接触发外设后者走中断流程两者天然存在微秒级以下的延迟差。对高精度采样而言这个差别可能是致命的。6. 写在最后的心得定时器这块东西入门容易精通难。PSC和ARR的公式就那么一行可稍有疏忽就会差出几倍去。我最深的体会是不要靠记忆去写这两个数每次都要从“定时器时钟是多少”开始推一遍。很多人觉得这一步多余但正是这一步能拦住80%的低级错误。调试上也有个实用心得先用固定的GPIO翻转来验证定时周期再接实际业务逻辑。比如你先让定时器中断里翻转一个LED频率对了再去挂PWM或捕获功能。这样能把“定时器本身没配好”和“业务逻辑有bug”两个问题隔离开排查效率高很多。最后再分享一个冷门但很好用的小技巧如果你的定时器在调试时想暂停下来观察CNT和CCR记得开启DBGMCU的定时器冻结功能否则你程序停在断点处定时器硬件还在跑CNT会一直跳你看不到任何快照值。这个功能在STM32F1里叫DBGMCU_APB1PeriphConfig在F4里是DBGMCU_APB1/APB2PeriphConfig用HAL库则对应__HAL_DBGMCU_FREEZE_TIMx。调试定时器相关代码时我必开这个省了不知道多少烦心事。