STM32定时器时基详解:PSC/ARR配置与时钟树实战
定时器一直是个“会用但说不清”的模块。很多人知道配置PSC和ARR知道定时器能产生中断、能输出PWM但你要问一句“定时器到底在数什么”不少朋友会愣一下。说白了定时器数的是时钟周期——它本身不认识“微秒”“毫秒”这些时间单位它只认识一个又一个脉冲。所谓“时间基准”就是这些脉冲从哪里来、以什么频率来。把这条链路搞明白STM32定时器的一切玩法延时、测频、PWM、输入捕获、编码器接口都能串起来。这篇东西适合刚入门STM32、对着CubeMX一脸懵的新手也适合那种“代码能跑但心里没底”的朋友。我把时基的来龙去脉、配置背后的计算逻辑、以及实际调试中容易踩的坑一次讲清楚看完你再去配定时器心里就有谱了。1. 定时器到底在数什么先搞清楚“数数”这件事1.1 时基三兄弟CNT、PSC、ARRSTM32的通用定时器TIM2~TIM5、TIM9~TIM14这些核心动作就一个计数器CNT在每个时钟脉冲到来时加1。你可以把计数器想象成一个秒表但它的“秒”不是自然秒而是脉冲个数。为了让脉冲个数变成我们熟悉的时间单位硬件里给了两个可编程的分频/限位寄存器PSC预分频器把进来的时钟先分频。比如进来的是72MHzPSC设成71那么真正送给计数器的就是1MHz一个脉冲代表1微秒。ARR自动重装载寄存器规定计数器数到多少就“归零”并产生事件。ARR设成999计数器就从0数到999数满1000个脉冲产生一次更新事件。所以溢出周期也就是定时器中断周期有个非常经典的公式[ T_{溢出} \frac{(ARR 1) \times (PSC 1)}{TIMxCLK} ]这个公式值得抄在笔记本上。代码里所有关于“定时X毫秒”“输出X赫兹PWM”的配置本质都是在解这个方程里的未知数。1.2 一个周期内发生什么以向上计数模式为例完整流程是计数器CNT从0开始每个时钟脉冲加1当CNT等于ARR的值时硬件自动做两件事——把CNT清零然后置位更新标志位UIF。如果开了中断这时候就进中断服务函数如果开了DMA这时候就触发一次DMA传输。这里有个细节很多人第一次会忽略CNT和ARR相等的那个时刻CNT并不会停在ARR上而是下一个脉冲直接回到0。所以ARR999代表的是“1000个脉冲一个周期”不是“999个”。公式里写AR1就是这个原因跟“从0数到999一共有1000个数”是一个道理。1.3 为什么说PSC和ARR决定了“分辨率”和“范围”PSC决定的是定时器能分辨的最小时间单位ARR决定的是单次能数到的最大范围。两者配合本质是在“精度”和“量程”之间做权衡PSC越小分辨越细但ARR要相应设大才能覆盖同样的时间寄存器只有16位ARR最大65535。PSC越大量程自然变大但每一步的时间粒度变粗PWM或捕获的精度就会下降。举个例子STM32F103内部时钟72MHz想实现1秒中断。可以PSC71计数频率1MHz则ARR999999但ARR超出16位范围了必须用32位定时器TIM2/TIM5才行。也可以PSC7199计数频率10kHz用16位定时器就能装下ARR9999。更巧的做法是PSC719100kHzARR9999同样是1秒。所以“同一个定时目标有好几种参数组合”关键在于你想留多少余量给后续的PWM频率或捕获分辨率。2. 时间基准从哪里来时钟源的几条路径2.1 最常用内部时钟 TIMxCLK这是最简单、也是初学者接触最多的一条路。STM32内部有一个RCC复位和时钟控制模块它像一棵时钟树外部晶振HSE或者内部RC振荡器HSI作为根经过PLL倍频得到系统时钟SYSCLK再通过AHB预分频器得到HCLK接着APB1/APB2预分频器给外设喂时钟。很多人栽过跟头注意看这里定时器时钟不是直接等于APB总线时钟。STM32给定时器留了一个“倍频器”——当APB预分频系数为1时定时器时钟等于APB时钟当APB预分频系数大于1时比如2、4、8定时器时钟是APB时钟的2倍。拿F103举例系统时钟72MHzAPB1预分频器设成2那么APB1上的外设USART2、I2C1这些是36MHz但挂在APB1上的TIM2~TIM5时钟是36MHz的2倍也就是72MHz。用CubeMX自动配置时它已经帮你算好了但用标准库或寄存器开发时很多人会在这里按36MHz去算时基结果定时时间翻倍误差很大。一句话总结查数据手册时钟树别猜。2.2 外部时钟模式1TINx引脚输入除了内部时钟定时器还能从外部引脚接收脉冲。外部时钟模式1的意思是把TIMx_CH1/CH2这些通道引脚直接当作一个“外部脉冲输入端”引脚上每来一个上升沿或下降沿计数器就加1。这种模式特别适合“数外部事件”。比如你要统计流水线上一个传感器产生了多少个脉冲直接把传感器信号接到定时器通道引脚配置成外部时钟模式1读CNT寄存器就知道数量了。它跟输入捕获的差别在于输入捕获是“测量信号频率/脉宽”而外部时钟模式是“把信号本身当作时钟来数”。2.3 外部时钟模式2ETR引脚输入TIMx_ETR外部触发输入引脚提供另一种外部时钟输入。与模式1的区别主要是触发方式更灵活可以配置极性、预分频和滤波。实际项目中我一般用它做“低速高精度计数”比如编码器信号在极端情况下的备用计数通道或者一些需要硬件直接计数的传感器脉冲。2.4 内部触发级联一个定时器给另一个定时器“供脉冲”这是STM32定时器一个比较少人用、但极其强大的特性。比如用TIM2做基础时基当TIM2更新事件发生时通过内部触发连接ITR0~ITR3把信号送给TIM3让TIM3“等TIM2数满一次再数一次”。这就实现了定时器的“级联”。这样做有什么用两个典型场景一个定时器的ARR不够大16位寄存器装不下大计数值级联两个定时器实现“超长定时”。想让PWM波形跟某个特定时刻严格同步用主定时器控制从定时器的启动时刻避免软件启动带来的不确定延迟。级联的配置需要查对应芯片的参考手册“定时器同步”章节因为不同定时器之间的ITR映射关系不完全一样。2.5 特殊定时器SysTick、LPTIM、看门狗除了TIM外设还有几个“特殊时间基准”值得一提SysTick滴答定时器内核自带24位递减计数器专门用来给操作系统提供时基裸机里最常被当作精确定时器用。它的问题是——如果代码里同时有多个地方用SysTick比如延时函数和RTOS很容易互相干扰导致delay卡死。LPTIM低功耗定时器可以在STOP模式下继续运行适合做低功耗产品的周期性唤醒源。但注意它的时钟源选择和分频能力都有限时基精度要求高的场景不如TIM外设。IWDG独立看门狗严格来说它也是一个“定时器”不过它的作用不是产生时基而是“超时复位”。它的时钟来自LSI内部低速RC精度不高用来做时间计算不靠谱。3. 实操走一遍从LED闪烁到超声波测距的时基换算3.1 例一500ms LED闪烁标准库和HAL各自的写法目标F103内部72MHz定时器TIM3想让LED每500ms翻转一次。先算参数。TIM3挂在APB1上时钟72MHz。选用计数频率1MHz即PSC71这样每个脉冲1微秒想要500ms 500000微秒ARR需要499999。但TIM3的ARR是16位最大65535装不下。所以调整策略PSC改成3599计数频率20kHz这样每个脉冲50微秒ARR 9999一次溢出正好500ms。HAL库写法配置其实很短TIM_HandleTypeDef htim3; htim3.Instance TIM3; htim3.Init.Prescaler 3599; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 9999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim3); HAL_TIM_Base_Start_IT(htim3);中断回调里翻转LEDvoid HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }这里有个关键点ARR9999对应的是10000个脉冲不是9999个。PSC3599对应的是3600个输入时钟周期产生一个计数脉冲也不是3599个。如果你在调试器里观察定时周期发现比预期多了那么一点点多半就是在这里少加了1。3.2 例二用定时器实现US级延时很多人习惯用HAL_Delay()但它只能毫秒级想做微秒级延时怎么办用定时器做一个us延时函数是最常见的做法。思路很简单定时器跑起来计数频率设为1MHzPSC71要延时N微秒时清零CNT然后等CNT数到N即可。void TIM_US_Delay(TIM_TypeDef *TIMx, uint32_t us) { __HAL_TIM_SET_COUNTER(TIMx, 0); while (__HAL_TIM_GET_COUNTER(TIMx) us); }前提是这个定时器已经初始化成1MHz的计数频率。实测在72MHz主频下这种延时的抖动在1~2微秒以内对绝大多数传感器时序比如DHT11、超声波模块都够用。注意别把这个函数用在中断里耗时太长的地方也不要在多个任务里同时抢同一个定时器做us延时否则逻辑会乱。3.3 例三超声波测距的时基选型和范围估算超声波模块HC-SR04的测距流程是给Trig引脚一个10us以上的高电平触发模块回一个与距离成正比的高电平脉宽脉宽时间乘以声速340m/s再除以2就是距离。用定时器输入捕获来测这个脉宽时基怎么选假设测量距离最大4米来回时间 2×4/340 ≈ 23.5ms。计数值不能太粗否则近距离误差大。0.1mm距离对应的时间约0.588us所以计数频率最好在1~2MHz级别。我习惯把定时器计数频率设为1MHzPSC71捕获到上升沿时清零CNT捕获到下降沿时读CNT得到的就是以微秒为单位的脉宽。距离毫米值直接除以5.8因为在1MHz计数频率下1mm对应5.8个脉冲再除以2距离(mm) CNT值 × 340 / 2000因为CNT单位是us340m/s 0.34mm/us往返要除以2所以单程就是CNT值 × 0.17mm/us。测出1000us的脉宽对应170mm跟理论值吻合。4. 定时器不只有“数数”那些建立在时基上的模式4.1 输入捕获用定时器“测”外部信号输入捕获本质上是“借时基量时间”。当引脚出现指定的边沿时硬件会把当前的CNT值锁存到捕获寄存器里。连续捕获两个边沿用两次CNT的差值乘以脉冲周期就能算出信号的周期或脉宽。测频率两个相邻上升沿之间的CNT差值就是信号周期。测脉宽上升沿和下降沿之间的CNT差值就是高电平宽度。做这种功能时时基选择特别讲究。比如测一个1kHz方波周期1ms如果你的计数频率只有10kHz即一个脉冲100us那么整个周期只有10个计数值分辨率低得可怜。把计数频率提到1MHz就有1000个计数值分辨率足够。所以输入捕获的分辨率直接取决于PSC选择PSC越小精度越高但要注意计数值别溢出。4.2 输出比较/PWM用定时器“产生”波形输出比较是另一个方向定时器内部拿CNT和比较寄存器CCR做比较一旦相等就翻转输出引脚或拉高/拉低。PWM频率就是溢出频率占空比就是CCR跟ARR的比值。所以配置PWM本质上还是那一套时基公式[ f_{PWM} \frac{TIMxCLK}{(ARR1) \times (PSC1)}, \quad 占空比 \frac{CCR}{ARR1} ]FOC、舵机控制、电机驱动里那些“PWM波形和定时器”的问题基本都落在这两个式子里。做电机控制时我建议把PWM频率定在16~20kHz避开音频同时满足MOSFET开关频率要求然后反推PSC和ARR再根据需要的控制分辨率选CCR位宽。高级定时器TIM1/TIM8还额外提供互补输出和死区插入那是专门为H桥、三相逆变器准备的配置时多了一组死区参数但时基逻辑不变。4.3 编码器接口模式硬件帮你“判方向、数脉冲”编码器接口模式把定时器CH1和CH2当作两路正交信号的输入硬件自动根据两路信号的相位关系判断旋转方向并递增或递减CNT。这个模式不需要你写“测频率”的代码CNT本身就是一个带方向的位置计数器。这种模式下时基的意义变成了“分辨率系数”如果你用了4倍频1x、2x、4x模式电机转一圈产生的CNT变化量会翻倍。配置时要把提高倍频带来的计数溢出问题考虑进去否则CNT回绕后位置值会跳变。4.4 低功耗场景LPTIM才是STOP模式下的定时器标准TIM在停机STOP模式下是停止工作的因为它的时钟源在STOP时会被关掉。真正能在STOP模式下唤醒MCU的定时器是LPTIM。LPTIM的时钟可以来自LSI约32kHz或LSE32.768kHz晶振。在低功耗产品里LPTIM通常被配置成“每隔N秒产生一次中断”的周期性唤醒源MCU大部分时间睡在STOP模式醒来干活几毫秒再睡回去整机功耗能做到很低。用LPTIM时有个常见问题LSI在不同温度下的频率偏差比晶振大如果产品对时间精度有要求比如RTC要准优先用LSE别用LSI。我调试低功耗板子时专门量过LSI从冷启动到稳定要几十毫秒频率也可能漂几个百分点对“定时唤醒”没问题对“计时累积”就有隐患。5. 常见问题与排查技巧实录5.1 delay卡死先查SysTick有没有被“抢”很多人的HAL_Delay()在某天突然卡死多半是因为别的地方也动了SysTick。比如中断服务函数里调了HAL_Delay()而SysTick中断优先级比这个外设中断低或相同导致SysTick在中断里永远等不到执行。某段代码关掉了全局中断__disable_irq()太久SysTick中断没机会触发而HAL_Delay()一直傻等。排查思路最简单粗暴把sysTick中断优先级调到最高尽量避免在中断里做延时。如果需要“中断里的非阻塞等待”用状态机或者定时器查询方式替代。5.2 定时时间总是偏大一点“1”的锅PSC和ARR设好之后理论上1秒就是1秒但如果你在调试时发现时间偏大先核对两点是否忘了“PSC1”和“ARR1”。是否搞错了定时器时钟APB1分频系数影响TIMxCLK。F103上TIM2~TIM5的时钟是72MHz但很多人以为它是36MHz于是PSC设成35计数频率1MHzARR设成999结果定时周期2ms而不是1ms。这种问题用标准库开发时很难发现因为代码配置看起来“都合理”。5.3 CAN通信突然连不上波特率只是表面现象CAN外设的波特率也是从APB1时钟分频来的。如果有人动过系统时钟或者定时器相关配置导致APB1分频系数变化CAN的位时序就会跟着漂。CAN总线对位时间误差容忍度有限一旦偏差大表现就是“能进初始化但收不到报文”。排查时别只盯着CAN寄存器先用逻辑分析仪看CAN_TX引脚的实际波特率再回头看时钟树配置。顺带说一句很多“串口波特率不对”“I2C时序怪”的问题根源也在时钟配置。5.4 低功耗唤醒异常LPTIM没配好或者时钟选错LPTIM在STOP模式下不工作的场景很常见原因一般是没有把LPTIM的中断优先级和NVIC正确使能。LPTIM的时钟源在STOP下也被关了比如你选了某个依赖PLL的时钟源。唤醒后没有清理标志位导致主程序逻辑异常。排查低功耗问题时先查供电电流再查唤醒标志配合示波器看唤醒引脚动作通常能快速定位。5.5 踩坑速查表现象大概率原因处理建议定时周期偏大APB1分频导致TIMxCLK被算错查数据手册时钟树定时周期有微小偏差忘了PSC/ARR需要1用调试器看CNT实测间隔多个定时任务互相干扰共用了一个定时器做延时拆成独立定时器或改用“软件定时器”轮询PWM频率严重不对PSC、ARR计算时带入错误时基反过来用示波器校准delay卡死SysTick被中断阻塞调优先级、避免中断内延时LPTIM无法唤醒STOP时钟源被关闭或NVIC配置遗漏选LSE/LSI审查唤醒流程6. 时基选择的一些个人体会说实话玩定时器玩了这么多年踩坑最多的反而不是寄存器配置本身而是“脑子里对时基的建模不清晰”。很多人把“定时器”当成一个黑盒子填了几个参数能跑就完事一旦波形不对、时间不准整个就懵了。我自己的做法是拿到一块新板子先把时钟树默写一遍——系统时钟多少、APB1分频多少、APB2分频多少、各总线下挂的定时器分别时钟是多少。然后心里默念那三句话定时器数的是脉冲PSC决定脉冲的“密度”ARR决定数多久。所有模式、所有应用都绕不开这三句话。调试时多利用调试器的实时寄存器窗口把CNT、PSC、ARR、CCR同时拉出来看波形异常时盯着这些值的变化比盲猜配置高效得多。还有一个小建议做工程时把“时基配置”集中放在一个模块里用宏定义标明定时器时钟源频率所有依赖时间的计算都从这个宏推导别在多个文件里各写各的数字。否则改一次系统时钟满世界的定时器参数全得跟着遭殃。如果哪天你发现一个定时器不够用了不要急着加定时器外设先回头看看是“分辨率不够”还是“量程不够”。前者降PSC后者升ARR搞不定再考虑级联或者换32位定时器。这套思路比单纯记住几个例程有用得多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →