
先问个问题你上一次调STM32定时器是几分钟就搞定还是对着示波器怀疑人生我这几年帮人排查过不少定时器相关的问题发现十次里有七八次最后都归结到PSC、ARR和时钟源这三个点上。不是逻辑复杂而是太容易想当然觉得APB1是36MHz定时器就是36MHz觉得PSC写上71就是71分频觉得ARR就是周期值设置成1000就完事了。结果波形一出来频率要么翻倍要么多出一个“1”的偏差数据手册翻烂了才发现是基础概念没扣细。这篇文章就把这三个最容易算错的地方逐个拆开讲清楚附带实际项目的排查过程和速查表。无论你是刚接触STM32还是已经写了几年固件但偶尔还会被定时器摆一道下面这些内容都能帮你少走弯路。1. 定时器时钟源很多人从第一步就算错了1.1 为什么同样是72MHz主频定时器时钟会有72和36之分STM32的定时器时钟不是简单等于系统主频。它挂在AHB下面的APB1和APB2总线上而STM32参考手册里有一条容易被忽略的规则当APBx预分频系数不等于1时定时器时钟频率是APBx时钟的2倍当APBx预分频系数等于1时定时器时钟频率等于APBx时钟。这条规则直接导致了一个经典错误很多人看CubeMX生成的代码发现PCLK1是36MHz就想当然地把TIM2、TIM3这些挂在APB1上的定时器按36MHz去算PSC和ARR。但实际上只要APB1的分频系数是2定时器时钟就是72MHz而不是36MHz。如果你按36MHz去算PWM频率会翻倍中断周期会减半示波器一看完全对不上。拿STM32F103来举例。系统复位后默认使用HSI但实际项目里一般会配置成HSE加PLL把SYSCLK拉到72MHz。此时AHB预分频通常设成1HCLK就是72MHzAPB1预分频设成2PCLK1变成36MHz但是TIM2、TIM3、TIM4、TIM5这些挂在APB1上的定时器时钟会被自动倍频成72MHz。APB2预分频设成1PCLK2保持72MHzTIM1和TIM8直接就是72MHz。所以结论是在F103最常用的配置下所有定时器时钟基本都是72MHz。好多人在这没犯错但到了F407这类芯片上就彻底乱了。1.2 F103和F407两套时钟树最容易弄混的地方F407的主频通常是168MHz这就带来一个更隐蔽的差异。168MHz主频下AHB预分频一般是1HCLK为168MHzAPB1预分频设成4PCLK1为42MHz那么挂在APB1上的TIM2、TIM3、TIM4、TIM5、TIM12、TIM13、TIM14的时钟就是42MHz的两倍也就是84MHzAPB2预分频设成2PCLK2为84MHz那么挂在APB2上的TIM1、TIM8、TIM9、TIM10、TIM11的时钟就是84MHz的两倍也就是168MHz。一样的规则两个不同的结果。如果把F407上TIM1的时钟错当成84MHz配置20kHz的PWM时ARR算出来只有4199实际输出却是40kHz。反过来如果把TIM2的时钟错当成168MHz去配置本来想要1ms的中断实际会变成2ms。在F4系列里还有一部分定时器支持选择PLLI2S作为时钟源比如TIM2、TIM3、TIM4、TIM5在某些型号上可以通过TIMx_CR2和RCC的配置切换到PLLI2S。这个功能平时用的人少但如果你在某天调试时发现定时器频率怎么算都不对看一眼时钟源选择寄存器别一直在PSC和ARR里面死磕。1.3 实际核对定时器时钟的三种方法第一种方法最简单看CubeMX的Clock Configuration页面。它会直接把你配置的APB1、APB2分频和最终的定时器时钟画出来前提是你会看。在这个页面里注意APB1和APB2的定时器时钟那一栏很多时候CubeMX显示的是APB1外设时钟36MHz但定时器时钟那一栏会单独标一个72MHz如果没注意照样会看错。第二种方法是读寄存器。RCC-CFGR里存放着PPRE1和PPRE2位段把它们读出来对照数据手册就能算出当前的实际分频系数。代码可以这样写uint32_t cfgr RCC-CFGR; uint8_t ppre1 (cfgr 8) 0x07; uint8_t ppre2 (cfgr 11) 0x07; uint8_t sw cfgr 0x03; // 0: HSI, 1: HSE, 2: PLL // 根据PPRE位段查表可以得到PCLK1和PCLK2 // 定时器时钟规则 // PPREx 0b0xx - 定时器时钟 PCLKx // PPREx ! 0b0xx - 定时器时钟 PCLKx * 2这种方法不需要示波器只需要一个调试器在IDE的Watch窗口里看寄存器值就行。我排查问题的时候经常先看这一步因为它能快速排除掉“我以为我在跑168MHz其实PLL没起来”这种尴尬情况。第三种方法是实测。配置一个定时器输出固定频率的方波用示波器或逻辑分析仪量一下实际频率反推定时器时钟。比如配置PSC71、ARR999如果输出的是1kHz方波说明定时器时钟是72MHz如果输出的是2kHz说明时钟是144MHz这就不太正常得回头检查时钟树。提示不管用哪种方法第一优先级是把“定时器时钟到底是多少”这个问题钉死。PSC和ARR算得再准时钟源错了一切白算。2. PSC这个坑写之前先问自己一句“我的分频系数是多少”2.1 PSC寄存器和实际分频系数的关系PSC预分频器英文全称Prescaler。很多人看名字觉得写71就是71分频写255就是255分频大错特错。STM32的PSC寄存器设计是写0表示1分频写1表示2分频写71表示72分频。也就是说真实分频系数是PSC寄存器的值加1。为什么这么设计从硬件角度解释最直接。计数器时钟CK_CNT和定时器输入时钟CK_PSC之间的关系是CK_CNT CK_PSC / (PSC[15:0] 1)当PSC写0时CK_CNT等于CK_PSC也就是不分频。为什么要用“寄存器值加1”而不是直接用寄存器值当分频系数因为PSC是16位寄存器理论上需要表示0到65535如果0表示1分频那最大值65535就能表示65536分频把范围利用满了。这个设计在单片机的外设里非常常见不只是STM32AVR、NXP的一些定时器也都是这么干的。我在实际项目里见过有人把PSC直接写成72本意是72分频结果实际是73分频。在有些场合这个误差无所谓但如果你要生成精密时钟、做频率计或者和别的设备做同步这就直接导致频率偏了1.4%时间一长累计误差就会非常明显。还有一点需要注意PSC的值不能随便填。它最大是65535也就是最多只能分频65536。如果你需要把72MHz分成1Hz也就是分频72000000一个PSC搞不定那就需要级联或者换用32位定时器或者把分频任务分摊到PSC和ARR两级。很多人卡在这以为是芯片坏了其实就是没有理解PSC的位数限制。2.2 一套不会错的手算模板我在调定时器的时候习惯按下面这个顺序来算基本不会出错。先说定时中断场景目标定一个1ms的定时周期定时器时钟为72MHz。第一步确定计数器时钟。先给PSC赋值让计数器时钟变成一个好算的整数。比如我希望计数器以1MHz递增那么分频系数就是72MHz除以1MHz等于72。PSC 72 - 1 71。第二步确定ARR。计数器以1MHz递增也就是每1微秒加1。要让定时器1ms溢出一次计数器需要数1000个数从0数到999。ARR 1000 - 1 999。写成代码就是这样RCC-APB1ENR | RCC_APB1ENR_TIM3EN; TIM3-PSC 72 - 1; // 计数时钟 72MHz / 72 1MHz TIM3-ARR 1000 - 1; // 溢出周期 1000 / 1MHz 1ms TIM3-EGR TIM_EGR_UG; // 立即产生更新事件让PSC和ARR生效 TIM3-CR1 | TIM_CR1_CEN; // 启动计数很多教程里写PSC71、ARR999但不解释为什么。你只要记住PSC先加1再参与计算ARR先加1才是完整周期。这个模板用在PWM场景的时候稍微有一点变化但核心逻辑一样先确定计数器时钟再确定完整周期需要多少个计数脉冲。再说PWM场景。目标输出一个20kHz、占空比50%的方波定时器时钟还是72MHz。如果希望分辨率高一点PSC可以设成0也就是计数器时钟就是72MHz。那么ARR 72MHz / 20kHz - 1 3599。占空比50%的意思是一个周期内高电平时间占一半。由于计数器从0计数到3599周期内一共3600个计数点所以比较值CCR应该等于(3600 / 2) 1800。也就是CCR (ARR 1) / 2 1800不是ARR / 2。注意如果你这里图省事写CCR 3599 / 2 1799那么占空比就不是精确的50%而是1799/3600约等于49.97%。对大部分应用来说无所谓但你心里要清楚这个“整除舍入”带来的差异。2.3 动态修改PSC什么时候生效PSC寄存器在STM32里默认是带缓冲的。什么意思就是程序往TIMx-PSC写入新值以后这个值不会立刻生效要等下一个更新事件到来时才会被真正的影子寄存器加载。如果你在运行时改了PSC然后立刻读计数器频率会发现频率还是老样子于是开始怀疑是不是寄存器没写进去其实不是。这个问题在初始化阶段不会暴露因为初始化之后通常会主动产生一次更新事件。CubeMX生成的代码里在配置完定时器后会有这么一行__HAL_TIM_SET_PRESCALER(htim3, 71);如果你手动写寄存器记得在配置完PSC和ARR之后加上TIM3-EGR TIM_EGR_UG;这条语句的作用就是软件产生更新事件把PSC和ARR的影子寄存器更新掉。如果不加这一句在你第一次启动计数器之后PSC可能在下一次溢出时才生效导致前几个周期的频率不是你预期的值。这在做电机控制或者PWM输出时非常危险可能启动瞬间输出一个错误的脉宽。还有一个细节APRE位也就是TIMx_CR1里的Auto-Reload Preload Enable控制的是ARR的预装载不是PSC的。PSC的缓冲是始终存在的而ARR是否缓冲要看APRE位的设置。如果APRE置1ARR写入后也要等更新事件才生效如果APRE清零ARR写入后立即生效。这个细节很多人搞混以为PSC和ARR的预装载是同一个开关控制实际上不是。提示动态修改PSC后如果不想等自然溢出就手动置一下UG位。但要注意置UG会触发更新事件也可能会触发更新中断如果你的中断服务函数和更新事件挂钩要确保这一下不会造成多余的动作。3. ARR算错的方式更多周期、PWM、占空比、中心对齐全都有份3.1 ARR与定时器溢出频率的关系ARR自动重装载寄存器英文Auto-Reload Register。它的作用是在向上计数模式下规定计数器从0数到哪个值就溢出然后重新从0开始。很多入门资料只说“ARR决定定时器周期”这个说法没错但省略了最关键的一个“1”。在向上计数模式下定时器从0开始数到ARR然后归零并产生更新事件。所以一个完整周期内计数器经过的数值个数是ARR 1从0到ARR一共是ARR加1个数。定时器溢出频率的计算公式是溢出频率 定时器时钟 / ((PSC 1) * (ARR 1))定时器时钟84MHzPSC83ARR999那么分频后计数时钟是1MHz溢出频率是1MHz / 1000 1kHz。也就是1ms一个周期。如果你把ARR写成1000那溢出频率就是1MHz / 1001约等于999Hz周期大约是1.001ms。单独看一个周期还好如果你用它做RTC、做秒表误差会累积起来一天下来差了几十秒。在PWM模式里这个公式同样成立。PWM频率 定时器时钟 / ((PSC 1) * (ARR 1))。很多人在计算PWM频率的时候会顺手把ARR当成完整周期数写成ARR 定时器时钟 / 频率然后再减1。如果忘了减1输出频率就会比预期略低。我见过很多人纠结“为什么我按计算器算的20.000kHz示波器量出来是19.98kHz”多半就是这个加一减一的问题。3.2 从ARR推导PWM频率和占空比在PWM模式1、向上计数、输出高电平有效的配置下计数器值小于比较值CCR时输出高电平计数器值大于等于CCR时输出低电平。因此占空比的计算公式是占空比 CCR / (ARR 1)很多教程里简化成CCR / ARR那是在ARR远大于CCR时误差才不明显。如果你做的是精密调光或者电源控制这个误差就会体现出来。举个例子ARR99想输出50%占空比CCR应该等于(100 / 2) 50也就是(ARR 1) / 2。如果照着CCR ARR / 2去写得到的是99/249整数除法实际占空比是49/10049%低了1%。在LED调光的时候可能看不出什么但在模拟信号输出、DAC替代方案里1%的误差就会影响精度。再说一个容易踩的误区有人想让PWM输出100%占空比直接把CCR设置成ARR结果发现输出不是一直高电平而是有一小段低电平。原因就是要输出100%占空比CCR必须大于等于ARR 1或者参考手册中提到的“把CCR设置成大于ARR的值”而不是等于ARR。同理0%占空比也不仅仅是把CCR设成0有些芯片在CCR0时会有毛刺理解了这个原理排查起来就有方向。PWM还有一个特殊性修改ARR会同时改变频率和占空比。因为占空比是CCR除以(ARR 1)如果你的应用要求频率不变只调占空比那是改CCR如果频率和占空比都要动态调整那就得考虑修改时序避免计数器在运行过程中出现超出新ARR的计数值导致输出波形异常。我之前做过一个云台电机驱动动态调速时如果没有在溢出中断里同步修改ARR和CCR电机就会出现“咔哒”一声后来改成在更新事件里统一修改问题才消失。3.3 中心对齐、重复计数器和32位定时器带来的额外变数ARR的坑不止在于“加一”还在于工作模式变了计算方式也要跟着变。中心对齐模式就是典型的例子。在中心对齐模式下计数器先从0向上计数到ARR产生一次更新事件然后向下计数到0又产生一次更新事件。也就是说计数器走一个完整来回ARR本身只有一个但更新事件发生了两次。如果你直接在中心对齐模式下开更新中断并且不做处理你的中断频率会是普通向上计数模式的两倍。这不是什么玄学而是计数器的物理行为决定的。所以当你在中心对齐模式下做PWM想要一个固定频率的更新中断来同步ADC采样时一定要先想清楚你要的是计数器溢出事件还是更新事件两者频率是不同的。高级定时器还有一个RCR位域叫重复计数寄存器。默认值是0意思是一次溢出产生一次更新事件。如果你给它赋了非零值比如3那么要经过RCR 1次溢出才会产生一次更新事件。这个寄存器本意是做多脉冲输出但如果你写初始化代码时不小心碰了它定时器中断频率就会变成预期的1/(RCR1)。我遇到过这种情况查了半天中断函数没发现问题最后读了一下TIMx_RCR发现值是2顿时无语。32位定时器比如F103的TIM2和TIM5ARR可以达到0xFFFFFFFF做长时间定时非常方便。但要注意32位定时器的ARR寄存器是32位操作时一般要一次性写入。代码写法上没问题但在思维上有个隐藏的坑32位定时器能表示的大数让很多人放松了警惕直接填入一个巨大的ARR却忘了PSC还有限制结果定时精度反而因为PSC分频倍数太大而变差。正确的做法是把时间基准放在计数器时钟上通过PSC把计数时钟调整到一个合理的频率再用ARR控制周期。提示在中心对齐模式下PWM频率的计算公式仍然包含(ARR 1)但因为计数是先上后下计数器时钟相当于被“折返”了所以更新事件频率翻倍。如果看到这里有点绕建议动手用示波器看一次比看十遍资料都管用。4. 实战排查三个坑叠在一起怎么快速定位4.1 一个呼吸灯频率不对的真实排查过程有一次帮朋友调一个呼吸灯项目MCU是多品种使用从F103C8T6换成了F407VET6。代码是从旧工程直接搬过来的只改了芯片型号和引脚定义结果呼吸灯亮灭的周期变得非常快原来2秒一个循环变成了大约1秒一个循环。朋友说这是不是定时器坏了我看了一眼代码PSC8399ARR999在F103的72MHz时钟下计数时钟为72MHz除以8400约等于8.571kHzARR1是1000溢出频率约8.57Hz周期约116msLED亮度刷新大概是这么个节奏。但换成F407后如果TIM2挂在APB1上时钟是84MHz而PSC和ARR没改则84MHz除以8400等于10kHz溢出周期变成100ms确实快了一些。但朋友描述的是“快了将近一倍”那原因就不是PSC和ARR的问题根子一定在时钟源。我让他把SystemClock_Config发过来发现CubeMX重新生成时把F407的APB1分频配成了4APB2配成了2。如果这段代码是在F103项目里写的原工程PCLK136MHz、定时器时钟72MHz换到F407后PCLK142MHz、定时器时钟84MHz。单纯看差值不应该快到一倍。但问题在于他为了兼容代码把APB1分频手动改成了2导致F407的APB184MHz定时器时钟变成168MHz。旧代码里的PSC和ARR是按72MHz算的到了168MHz频率自然就不对了。后来把APB1恢复成4、APB2恢复成2重新按168MHz计算PSC和ARR呼吸灯就正常了。这个案例说明排查定时器问题时最优先做的不是看PSC、ARR而是确认当前芯片的时钟树配置。因为代码可以换芯片但时钟树和中断频率不可能自动适配。4.2 推荐一套排查顺序和验证手段我自己在排查定时器相关问题时有一套固定顺序。先看时钟源再看分频最后看模式。第一步肉眼检查。打开CubeMX的Clock Configuration确认SYSCLK、HCLK、PCLK1、PCLK2和定时器时钟的实际数值。如果工程不用CubeMX就看SystemClock_Config函数里的分频设置。第二步调试器读取寄存器。挂上ST-Link或J-Link在调试模式里查看RCC-CFGR的值软件算出当前AHB、APB1、APB2分频再算定时器时钟。同时看一眼当前使用的定时器是挂在APB1还是APB2别把TIM1当成APB1设备来算。第三步用一个最简单的配置验证。把PSC设成一个整数值比如71ARR设成999输出方波用示波器量频率。如果出来的是1kHz说明时钟源和PSC都没问题如果差得远说明时钟源不对继续往上查。如果输出的是999Hz说明ARR的“加一”没理解对。第四步问题锁定后再检查PWM模式下CCR和ARR的关系、中心对齐模式下更新事件的频率、高级定时器里RCR的值。这些寄存器平时不看但一旦出问题它们就是潜伏的炸弹。这四步走下来不需要反复烧写程序大部分问题都能在一两分钟内定位。我在公司培训新人的时候会直接把这个顺序贴在工位上因为定时器问题排查真的没那么玄多数时候是“基础假设错了”。4.3 易错点速查表我把这些年踩过和帮别人排查过的定时器问题汇总成了一个速查表基本覆盖了PSC、ARR和时钟源三个方面的典型症状。建议大家收藏或者存成笔记调试的时候对照着看。症状可能原因验证/处理方式PWM频率是预期的一半把APB预分频后的PCLK当成定时器时钟忽略了APB分频不等于1时定时器倍频的规则读RCC-CFGR确认PPRE对照参考手册时钟树计算PWM频率是预期的两倍F407上APB2定时器按PCLK2计算白扔了一个“2倍”确认定时器挂在APB1还是APB2按倍频规则计算定时中断周期略大于预期PSC或ARR的值没减1周期变成了(PSC1)*(ARR1)用(PSC1)*(ARR1)/定时器时钟重新计算周期完全不对差得很离谱时钟源不是预期的HSEPLL可能跑在HSI上读RCC-CFGR确认SW位段检查HSE是否有起振修改PSC以后频率没变PSC有影子寄存器更新事件还没发生写PSC后手动置EGR-UG或用定时器溢出重新装载ARR改了PWM频率没变APRE位为1ARR预装载只在更新事件生效确认APRE值在更新事件回调里重新赋值中心对齐模式下中断频率翻倍上溢和下溢都产生更新事件看CMS位按应用场景改用单边沿或处理更新标志高级定时器中断频率变成预期的1/NTIMx_RCR重复计数寄存器不为0读TIMx_RCR按需要清零或补偿PSC超过65535导致分频不够PSC只有16位最大65536分频级联定时器或换32位定时器或分两级分频20kHz PWM输出变成约19.98kHzARR忘了减1实际周期是ARR1先算完整周期需要的计数个数再减1写入ARR这张表看着简单每一条背后都是一个真实的调试故事。尤其是最后一条我见过太多人拿着计算器按完直接填ARR填完又对着示波器发愣。频率差一点点单独看波形根本看不出问题但累计误差或者和其他模块同步时就会凸现出来。结尾一个让我少踩很多坑的小习惯最后分享一个我从做电机驱动之后养成的习惯每次写定时器初始化代码之前先在上方注释里写清楚三件事定时器时钟是多少、PSC要填多少、ARR要填多少以及它们是怎么算出来的。代码长这样/* * TIM1, 20kHz PWM, F407 * TIM1CLK 168MHz * PSC 0 - CK_CNT 168MHz * ARR 8399 - PWM Freq 168MHz / 8400 20kHz * CCR1 4200 - Duty 4200 / 8400 50% */刚开始觉得多此一举后来发现这个习惯帮我省了无数时间。三个月后回看代码不需要重新翻数据手册不需要拿计算器按半天注释里的推导过程清清楚楚。团队协作的时候同事拿到你的代码也一眼就能看懂定时器配置的意图。定时器这东西越是基础越值得较真。PSC、ARR、时钟源这“三件套”掰扯明白了STM32开发里的定时器这块你就再也不会心里发虚了。