ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32定时器到底在数什么?一文讲透PSC、ARR与时钟链路

STM32定时器到底在数什么?一文讲透PSC、ARR与时钟链路 定时器是STM32里存在感最高、但常被忽略的一类外设。很多人用定时器做延时、PWM、输入捕获、超声波测距代码抄得飞快一问“定时器到底在数什么时间基准从哪里来”却说不清楚。我也经历过这个阶段PSC填了个数、ARR填了个数中断倒是会进可换块芯片或改了个时钟源时间就全乱了。这篇文章就把这根链路彻底讲透——定时器数的是脉冲脉冲来自时钟时钟与配置有关。读完你就能自己算出“1毫秒”是怎么来的也排查得了典型的定时器故障。适合刚入门STM32的人、做毕业设计时被定时器折磨的人以及玩过51但刚切换过来的同学。1. 定时器到底在“数”什么——先认识时基单元1.1 定时器的本质是“脉冲计数器”不是“秒表”STM32的定时器英文名叫Timer中文翻译成“定时器”这个翻译容易误导人。很多人以为它内部有个“秒表”是直接数时间的。真相不是这样——它本质上是一个计数器数的是脉冲个数。如果它数的是固定频率的脉冲那每个脉冲就代表固定的时间长度数够多少个脉冲就等于过了多少时间这才有“定时”的效果。打个比方你站在厂门口数工人进厂的人数你本人就是一台计数器。如果工人每分钟进10个你数到100个就知道过了10分钟。定时器也一样它内部有个寄存器叫CNT它会根据某个时钟信号“1”。这个时钟信号每跳一次边沿CNT就加一。关键点在于这个时钟信号的频率决定了CNT每加一次需要多久。很多人卡在这里是因为只记住了“PSC分频”“ARR自动重装”这些词却没理解它们是在“改造脉冲”而不是“直接改时间”。理解成“先决定每个脉冲多长时间再决定数多少个脉冲”整个定时器就顺了。1.2 计数脉冲从哪来内部时钟才是时间基准的主力定时器的计数脉冲来源其实有好几个典型的有四种内部时钟CK_INT来自定时器所在总线APB1/APB2的时钟这是最常用的来源。几乎所有的定时中断、PWM、延时都是靠它。外部时钟模式1来自定时器的外部引脚TIx例如TIM2的CH1引脚PA0接一个外部方波信号定时器直接对这个信号的边沿计数。外部时钟模式2来自ETR引脚需要配置触发检测。内部触发输入ITRx用一个定时器的输出触发另一个定时器实现定时器级联。在绝大多数应用里我们用的是第一种。内部时钟CK_INT就是定时器的时间基准。而CK_INT是从哪条链路来的就是下文要重点讲的时钟树问题。这也是标题里“时间基准从哪里来”的核心答案定时器本身不产生时间它的时间基准完全来自系统时钟树的分频、倍频结果。1.3 关键认知定时器数的是“边沿”不是“秒”再说一个常见误区。很多人看寄存器手册里面写着“Timer clock 72 MHz”就认为定时器每秒跑72M次。实际上计数器的时钟频率就是它“1”的频率吗不完全对。有些定时器内部还有额外的分频有些计数器并非每来一个时钟脉冲就加一比如编码器模式、外部时钟的滤波与分频。但即便在最基础的内部时钟模式下定时器接收到的时钟通常是经过时钟树预分频之后的信号。这里要强调一个概念一个周期为T的时钟信号每经过一个上升沿或下降沿CNT就加1。如果这个时钟信号每秒变化72M次那两次计数之间的间隔就是 1/72MHz ≈ 13.89纳秒。这个间隔就是定时器的最小计时单位。如果你把计数器配成每1微秒加一次那它每秒加100万次数到1000就是1毫秒。所以定期器“数”的从来不是秒数而是若干个边沿。只要把“每个边沿代表多长时间”搞清楚所有定时计算都只是乘法。2. 时间基准从哪里来时钟树与定时器时钟链路2.1 从晶振到定时器总线一条完整的时钟链路我们以最经典的STM32F103系列为例走一遍时钟链路。一颗8 MHz的外部晶振接到OSC_IN/OSC_OUT引脚经过锁相环PLL倍频F103典型配置为×9得到72 MHz的SYSCLK。SYSCLK再经过AHB预分频器通常为1/1得到72 MHz的HCLK。之后HCLK经过APB1预分频器通常为2分频得到36 MHz的APB1外设时钟。按照STM32的惯例APB1上的定时器时钟会自动翻倍也就是说TIM2、TIM3、TIM4、TIM5、TIM6、TIM7的时钟是72 MHz。同理APB2预分频器一般配置为1/1所以APB2上的外设时钟就是72 MHz。挂在APB2上的TIM1、TIM8它们的定时器时钟也是72 MHz。这就是为什么很多人用CubeMX看到TIM3的时钟显示是72 MHz而APB1总线时钟只有36 MHz——翻倍机制在起作用。这条链路可以用一个简单的式子表示 HSE(8 MHz) → PLL×9 → SYSCLK(72 MHz) → AHB:1 → APB1:2 → TIM3时钟 APB1外设时钟×2 72 MHz。如果换成HSE为25 MHz的板子PLL配置成其他倍数定时器时钟也会跟着变。很多初学者把“代码在正点原子板上能用到手头的板子就不对”的原因正是两块板的晶振频率不同导致整条时间基准都变了。2.2 为什么APB分频为1时定时器时钟不变分频不为1时反而翻倍这是一个非常容易混淆的细节。STM32的参考手册里写着如果APB预分频器为1定时器时钟频率等于APB外设时钟频率如果APB预分频器不为1定时器时钟频率是APB外设时钟频率的两倍。我把这句话用人话翻译一下APB1平时是2分频此时APB1外设只能跑36 MHz但定时器需要更快的时钟所以硬件自动给你×2给定时器补回72 MHz。如果APB1设置为1分频说明APB1外设本身就是72 MHz那定时器也不需要翻倍了直接用72 MHz即可。这个“硬件自动补”的设计是给开发者节省功耗的外设部分以低频运行只有定时器这种需要精准计时的模块才用高频时钟。但也因为有了这个机制很多人看数据手册时绕不过弯我最初也在这里踩过坑——以为TIM3的时钟就是36 MHz结果定时时间全部多了一倍。判断定时器时钟究竟是多少一定不要光看APB总线频率要去看定时器时钟那一栏CubeMX的时钟配置页面里会写得清清楚楚。2.3 实际操作用CubeMX确认你的定时器时钟在CubeMX里打开STM32F103C8T6工程进入Clock Configuration页面。你可以看到左侧“APB1 peripheral clocks”显示36 MHz而下面“APB1 timer clocks”显示72 MHz。如果你把APB1 Prescaler改成1/1两栏都会变成72 MHz。这个页面就是最直观的时间基准入口。配置TIM3时我建议先把这个页面拍下来把“APB1 timer clocks”这个数记为TCLK。后续PSC和ARR的计算全靠它如果这个数错了后面全部白算。用CubeMX的好处是它生成代码时会在HAL_TIM_Base_Init里自动把时钟配置好你只需要在__HAL_RCC_TIM3_CLK_ENABLE()后使用即可不需要手动访问RCC寄存器。2.4 顺带一提TIM3为何如此常用热词里频繁出现“stm32f103 cubemx 定时器3”不是巧合。TIM3是一个通用定时器向上/向下计数都支持有4个捕获/比较通道可以输出PWM、做输入捕获、编码器模式还能触发ADC。它挂在APB1上时钟72 MHz位置和功能都比较均衡。很多毕业设计的“超声波测距”“PWM调速”“舵机控制”底层都是TIM3在跑。如果你是从C8T6入手TIM3基本就是你的“瑞士军刀”。3. 把“数数”变成“时间”PSC、CNT与ARR的配合3.1 预分频器PSC先给脉冲降频再喂给计数器预分频器PSCPrescaler是定时器里的第一道关卡。它的作用很直白把输入的定时器时钟频率分成更低频率的计数脉冲。为什么需要它因为72 MHz太高了如果直接用CNT去数72 MHz的脉冲计数器从0加到65535只要不到1毫秒定时太长就没法覆盖。通过预分频器把计数脉冲降到合适的频率然后再数。PSC是一个16位的寄存器写入的值会加1后作为分频系数。比如PSC71实际分频系数是72计数频率从72 MHz变成1 MHz每个计数脉冲周期1微秒。这里要注意PSC寄存器的值和实际分频系数的关系是“差1”的很多人写PSC1000以为分了1000倍实际分了1001倍在经常计数的场景下会积累出明显误差。3.2 计数器CNT与自动重装载寄存器ARR数到什么时候“归零”计数器CNT是一个随每个计数脉冲递增的寄存器。它从0开始每来一个计数脉冲加1。自动重装载寄存器ARR里存着一个上限值当CNT数到ARR也就是CNT将要溢出时硬件会自动把CNT清零同时产生一个更新事件Update Event并置位UIF标志。如果开启了更新中断此时就会进入中断回调。把PSC和ARR结合起来看PSC决定“每个计数脉冲间隔多久”ARR决定“总共数多少个脉冲”。两者相乘才是定时周期。公式非常简洁定时周期T (PSC1) × (ARR1) / TCLK这个公式是定时器时基计算的核心。所有“1毫秒”“1秒”都是拿这个公式倒推出来的。3.3 一个完整的计算实例算出“1毫秒”定时中断假定TCLK 72 MHz需求是产生1 ms的定时中断。先设PSC71得到计数频率1 MHz。再算ARRT (711) × (ARR1) / 72 MHz 1 ms。即 (ARR1) / 1 MHz 0.001 s所以ARR11000ARR999。也就是说CNT从0数到999刚好数了1000次每次数1微秒总耗时1毫秒。此时CNT从999变到1000时发生溢出自动归零并触发更新中断。这个计算里我不推荐“心算”ARR而是每次都拿计算器推一下尤其是改过TCLK之后。再举一个反例如果PSC配错了比如PSC70那计数频率就是72 MHz/71≈1.014 MHz每周期约0.985微秒定时1秒大概偏0.015秒。如果PSC72计数频率1 MHz不是正好71对应1.000 MHz吗错是PSC71对应72分频PSC72对应73分频。这个“差1”的坑我在实际项目中踩过不止一次。3.4 别忘了计数器位宽16位和32位的区别STM32的定时器分为16位和32位两类。TIM3是16位定时器PSC和ARR都是16位最大只能写到65535。如果你要产生很长的定时周期例如PSC65535、ARR65535、TCLK72 MHz算出来周期大约为(65536×65536)/72MHz ≈ 59.65秒。如果超过了这个范围16位定时器就无能为力了。TIM2和TIM5在F103里是32位计数器ARR最大可以到4294967295能定几个小时甚至更长。但32位定时器占用的寄存器更多中断回调里处理的数据类型也要用32位否则容易溢出。这个在选择定时器时就要想清楚如果是秒级以下的短周期任务16位完全够用如果要做长时间计时、低频计数优先用32位定时器。另外更新事件并不只是用来进中断。它还会触发DAC、ADC采样可以作为DMA请求也能触发其他定时器级联。这就是为什么很多高级用法总要先配一个“时基定时器”——你要先用PSC和ARR把节奏定好后面的事才有时间轴可依。4. 时基应用的三个典型场景拆解4.1 场景一用定时器做1ms系统心跳嵌入式系统经常需要一个周期固定的“心跳”比如1 ms中断一次来扫描按键、刷新显示、做软件定时器累加。用TIM3实现很直接。在CubeMX里选择TIM3设置Clock Source为Internal ClockPSC填71ARR填999使能Update Interrupt。生成代码后在main()里调用HAL_TIM_Base_Start_IT(htim3)然后在回调函数里处理。回调函数名字是固定规律void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)。它不属于任何特定定时器所以进去之后要先判断if (htim-Instance TIM3)再执行自己的逻辑。这一点新手很容易忽略以为TIM3和TIM4的回调是分开的结果两个定时器共用同一个回调里面没做区分逻辑就乱了。我通常在小工程里维护一个全局的volatile uint32_t sys_tick每次TIM3中断加一。所有需要延时的模块都去读这个值而不是直接HAL_Delay()阻塞这样整个系统的实时性会好很多。4.2 场景二输入捕获测频率——定时器“反向”用定时器不仅能自己产生脉冲还能拿外部信号“喂”给它。输入捕获的经典做法是把被测量的方波信号接到定时器的某个捕获通道引脚上配置为上升沿捕获。信号每来一个上升沿硬件就会把此时CNT的值锁存到捕获寄存器CCRx里。两次连续捕获之间的CNT差值就是信号一个周期内计数器增加的次数。把这个差值乘以计数脉冲的周期就是信号的周期。再取倒数就得到频率。举个例子TIM3的计数频率为1 MHz输入信号第一次捕获时CCR1000第二次捕获时CCR4000差值3000那么信号周期3000微秒频率约333.33 Hz。热词“stm32定时器捕获测频率”指的就是这个玩法。STM32还支持PWM输入模式本质是用两个通道同时捕获上一个上升沿和下一个上升沿直接算出高电平时间。测超声波模块的Echo脉宽就是靠这种捕获实现的。这里有个技巧捕获模式下的CNT溢出要处理好如果两次捕获间隔太长中间CNT溢出过多次差值就会算错这时要在更新中断里统计溢出次数。4.3 场景三从时基角度理解PWM波形PWM和定时器的关系其实就是“计数比较”的关系。CNT从0数到ARR周而复始CCR里存着一个阈值硬件每个计数周期都把CNT和CCR比较CNT小于CCR时输出一个电平CNT大于等于CCR时输出另一个电平。这样CNT的周期决定了PWM的周期CCR的值决定了高电平的占比也就是占空比。PWM频率的计算公式为f_PWM TCLK / ((PSC1) × (ARR1))。这个式子展开看其实就是时基公式的倒数。改频率用PSC和ARR改占空比用CCR二者互不影响。这就是为什么电机驱动里经常用TIM3输出20 kHz左右的PWMTIM4改占空比调速度分工非常明确。热词里还有“foc pwm波形和定时器”FOC控制对PWM的频率和占空比精度要求都很高特别是中心对齐模式需要定时器支持中央对齐计数。正规地说STM32的TIM1和TIM8是高级控制定时器带互补输出、死区控制专门给电机FOC这类场景用。如果你只是要一个呼吸灯TIM3就够了如果要驱动三相逆变桥那就得上TIM1/TIM8。二者定位不同根源还是各自的时基单元能力和带外设资源不同。5. 常见问题与排查技巧实录5.1 定时时间到了却进不了中断这个问题出现的频率极高。配置了定时器中断也开了但函数就是进不去。排查顺序建议这样第一检查RCC里是否使能了TIM3的时钟。CubeMX生成的代码一般不会有问题但手写工程很容易漏掉__HAL_RCC_TIM3_CLK_ENABLE()。第二检查NVIC里是否勾选了TIM3全局中断。第三检查代码里是否调用了HAL_TIM_Base_Start_IT()这个函数不仅要启动定时器还会把更新中断打开漏了就白配。第四检查回调函数名有没有拼错HAL库要求完全匹配少一个下划线或多一个字母都会导致中断无效。实战中我还遇到过一种隐蔽情况中断回调里执行时间太长比如在里面跑了一个HAL_Delay(5)导致下个中断还没处理完又来了UIF标志一直置位看起来像“卡死”。定时中断里面尽量不要做耗时操作哪怕打印串口都要控制最好只置标志位让主循环去处理。5.2 定时时间总是两倍或一半定时时间翻倍基本可以锁定是时钟配置问题。最常见的就是我之前提到的APB1分频理解错误。比如你以为TIM3时钟是36 MHz按TCLK36 MHz去配PSC但实际定时器时钟是72 MHz那么实际周期就是你算的一半也就是定时器节奏变快了一倍。反之如果你按72 MHz算但某个工程下定时器时钟只有36 MHz周期就是两倍。怎么定位在CubeMX的Clock Configuration页面里看实际值或者读RCC_CFGR寄存器里的PPRE1位段。还有一个土办法写一个翻转LED的程序把周期设为1秒拿手机秒表或者逻辑分析仪测一下。实测周期是2秒说明定时器时钟比预期慢一半实测周期0.5秒说明快一倍。这比纯看寄存器直观多了。5.3 换了个外部晶振定时全乱很多开发板上HSE是8 MHz但也有12 MHz、25 MHz的外部晶振。程序里如果默认按8 MHz配置倍频系数在12 MHz晶振板上会导致SYSCLK跑飞定时器时钟当然也跟着乱。更隐蔽的是外部晶振起振失败芯片会默默从HSE切回HSI。HSI是内部RC振荡器频率一般为8 MHz左右但不同芯片差异较大。如果从72 MHz降到8 MHz定时器节奏会慢接近9倍表现就是LED闪得非常慢。遇到这种“定时突然全乱”的板子我建议第一步就怀疑时钟源。看程序里是否读取过RCC_GetFlagStatus(RCC_FLAG_HSERDY)或者用示波器量一下MCO引脚输出的SYSCLK信号可以快速判断到底跑在什么频率上。这也是为什么我总说定时器排查很大程度是时钟排查。5.4 SysTick滴答定时器和通用定时器怎么分工热词里也有“滴答定时器”。SysTick是一个集成在Cortex-M内核里的简单定时器它的时钟一般来自内核时钟HCLK。很多人分不清它和TIM3的关系SysTick主要负责操作系统比如RTOS的周期性时基HAL_Delay也是用它在实现的。它只有24位不占外设资源但不能做PWM、输入捕获这些功能。通用定时器则功能完整资源有限且多功能适合真正需要计时的场景。实际工程里我习惯让TIM3专门做“软实时”的调度时基SysTick留给HAL库自己用。如果你在裸机上需要µs级延时用SysTick配合DWT会更准但那是另一个话题了。5.5 从51定时器迁移过来的思维转变51单片机的定时器很多人是照着“机器周期”理解的12 MHz晶振机器周期1微秒TH0/TL0计数。到了STM32这种固定对应关系不存在了因为定时器时钟是可以通过PLL倍频出来的PSC又给你一道自定义分频配置一下可以精确到纳秒级。最大的思维转变是51里通常“定时器溢出时间是固定的”而STM32里你必须自己定义“每个计数脉冲的含义”。51的TH0/TL0只有16位很多人养成了“溢出一次重新塞初值”的习惯STM32有自动重装载寄存器不需要在中断里手动给CNT赋值溢出后自动归零省了一条赋值语句也少了一个容易出错的环节。如果你是从51过来的我的建议是忘掉“每次重装初值”的套路CLR也好、赋值也好都让硬件来。你只需要维护好PSC、ARR两个数程序会干净很多。6. 个人调试心得先问“时钟对不对”再问“代码对不对”写了这么多最想说的一句经验就是定时器出问题九成出在时钟链路上不是出在定时器本身。我调试定时器相关功能时第一件事永远是打开CubeMX的时钟配置页面把TCLK确认一遍第二件事才是看PSC和ARR的计算第三件事才是看中断配置。这个顺序帮我省了很多无用的调试时间。再分享一个小工具手头没有逻辑分析仪时我会把GPIO翻转和定时器中断绑在一起在TIM3中断里翻转PA5再把PA5接到另一个定时器的输入捕获通道上自测自量。这样能确认系统实际跑的频率也可以验证整个时基链路是否正确。这个方法不用额外设备只用STM32自带的定时器互测很实用。最后再提醒一个细节也是最容易踩的坑如果修改了PSC或ARR的值最好在修改前关闭定时器修改后再重新启动。有些库函数在运行时改ARR会出现“缓冲前值”的问题导致第一个周期异常。配置好参数再启动能够避免很多莫名其妙的现象。定时器的世界并不复杂把“时钟→分频→计数→重装载”这条链记在心里剩下的都是简单的算术题。
返回列表