ARTICLE DETAIL

资讯详情

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

STM32定时器时间基准全解析:时钟树、PSC与ARR的底层逻辑

STM32定时器时间基准全解析:时钟树、PSC与ARR的底层逻辑 做嵌入式这些年经常碰到有人拿着定时器的问题来问我开场白往往是“我的定时器明明配好了怎么时间不对”或者“这个PWM频率怎么算的怎么跟我想的不一样”。聊到最后你会发现大部分人对ST芯片里那个整天在“数数”的硬件单元也就是定时器其实一直停留在“会用CubeMX点两下”的阶段。定时器到底在数什么它的时间基准又是从哪里来的这两个问题如果说不清楚后面什么PWM、输入捕获、编码器模式全都像在走钢丝。这篇就专门把定时器的时间基准这件事掰开揉碎从时钟树一路顺到预分频器和自动重装载寄存器顺便把实际工程里容易踩的坑也一并收拾了。1. 时间基准从哪里来时钟树里的“源头之水”1.1 内核时钟不等于定时器时钟刚接触STM32的时候很多人会有一个误区以为定时器的频率跟主频一样。比如F103跑72MHz那么定时器精度就是72MHz。这个概念用起来会出问题因为定时器挂在不同的总线桥上而总线桥本身还有分频逻辑。拿最常见的STM32F103来说内核最高跑72MHz这个叫SYSCLK。SYSCLK经过AHB预分频器之后喂给APB1和APB2总线。APB1的极限频率是36MHzAPB2是72MHz。定时器TIM2、TIM3、TIM4挂在APB1上TIM1挂在APB2上。关键点来了如果APB1的分频系数不是1那么定时器时钟会被强制翻倍。APB1分频为2时定时器时钟 APB1时钟 × 2 36 × 2 72MHz。所以F103的定时器无论挂在APB1还是APB2默认都能拿到72MHz。曾经有人问“我的APB1才36MHz为什么TIM2最大还能跑到72MHz”答案就是这个2倍补偿机制。这是芯片设计时为了弥补总线频率降低而做的补偿也是手册里容易忽略的注释。1.2 APB分频带来的“反直觉”这个2倍补偿是新手最容易踩的坑。如果你用STM32CubeMX建工程外设时钟面板上会直接显示定时器时钟频率很多人看了一眼没在意直接去配预分频和重装载值结果算出的时间全偏了。实际工程里有这么一种情况你用STM32F407跑168MHzAPB1设为4分频那么APB142MHz定时器时钟 42 × 2 84MHz。如果你以为TIM挂在APB1上就按42MHz计算那定时延和PWM频率全错。判断规律一句话总线分频系数为1定时器时钟就等于总线时钟总线分频系数大于1定时器时钟等于总线时钟的2倍。自己在代码里也可以验证RCC挂钟配置完成后用HAL_RCC_GetPCLK1Freq()读出APB1频率再检查CUBeMX生成的__HAL_RCC_TIM2_CLK_ENABLE()之后读一下TIM2的输入时钟或者直接用调试器看初始化后的寄存器值。1.3 在CubeMX里看懂时钟树STM32CubeMX的价值之一就是把复杂的时钟树可视化但这个可视化反而让很多人失去了“手算”的能力。配时钟的第一步是先设好系统时钟源和PLL倍频让SYSCLK达到目标频率。第二步看左侧的APB1/APB2分频系数。如果APB1那一栏显示“/4”APB1总线频率就显示为具体数值例如F407的42MHz。接下来要找的是定时器时钟那一栏CubeMX通常不直接显示定时器时钟除非你选中某个定时器在它的配置页底部能看到Timer Input Frequency。曾经配过一个F411项目目标是用TIM3产生20kHz的PWM。TIM3挂在APB1上APB1分频设为1此时定时器时钟就是APB1 100MHz。如果分频设为2APB1 50MHz定时器时钟反而变成100MHz。看起来数字没变但APB1上的其他外设频率已经变了这部分要心里有数。时间基准的源头清楚了接下来就是定时器内部怎么拿这些时钟“数数”的问题。2. 定时器到底在数什么预分频器和自动重装载寄存器2.1 从节拍到周期PSC和ARR的分工定时器内部其实就是一个不断累加的计数器它数的是脉冲。一个时钟周期到来计数器的值加一。这个计数器的上限由自动重装载寄存器ARR决定。但问题来了72MHz的时钟意味着1秒有7200万个脉冲全部数一遍显然太“细”了我们需要的可能是1毫秒、1微秒或者一个合适的PWM周期。所以定时器前面还有一道关卡预分频器PSC。预分频器的作用是“数到一定次数才给计数器派发一个脉冲”本质上是一种降频处理。计数器最终的工作频率是定时器计数频率 定时器时钟 / (PSC 1)计数一个周期的时间 (PSC 1) / 定时器时钟再搭配ARR可以得到完整周期定时器溢出周期 (PSC 1) × (ARR 1) / 定时器时钟这个公式是定时器所有应用的地基。用日历打个比方系统时钟是一秒一秒走的秒针PSC相当于在秒针下再设一个“每60秒算一分钟”的逻辑ARR则像是闹钟定在60的位置每到60就响一次铃触发中断或更新事件。你调整PSC和ARR本质上是决定“该数多少个最小时间单位才做一次事”。2.2 动手算一个LED闪烁的例子假设F103跑72MHzAPB1分频为2定时器时钟 72MHz。要让LED每秒钟闪一次周期1秒高低电平各500ms怎么定PSC和ARR思路是先把计数频率降到一个好算的整数再让溢出周期等于目标。习惯上先把PSC设为7199这样计数频率就是72000000/7200 10kHz也就是每0.1ms计数器加一。再设ARR为9999溢出周期 (99991) × 0.0001s 1s。代码里是这样的TIM_HandleTypeDef htim2; htim2.Instance TIM2; htim2.Init.Prescaler 7199; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 9999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2);有个细节值得注意Prescaler虽然叫“预分频值”但在硬件里实际生效的是寄存器值所以要给目标分频系数减1。很多人设PSC7200算出来频率就不是整数调了半天没找到原因。2.3 为什么要先选PSC再选ARR实际调参有个经验顺序先把PSC设成一个让计数频率尽量高的值再调整ARR来定制目标周期。这里有个权衡。计数频率越高PWM的分辨率越好占空比可以调得更细但ARR的值也会变大意味着中断频率变低实时响应的粒度变粗。举个实际例子你需要1kHz的PWM系统时钟84MHz。方案APSC83计数频率1MHzARR999占空比调节步长0.1%。方案BPSC839计数频率100kHzARR99占空比调节步长只有1%。两种方案都能输出1kHz但方案B的输出电压调节会很粗糙用在LED调光上肉眼看不出用在电机调速上会感觉到噪声和扭矩波动。所以我的习惯是先看负载对频率的要求频率定了之后尽量让计数频率高同时不要超过32位ARRF1的16位定时器上限65535F4/H7等32位定时器上限可以到4294967295或16位定时器的上限。对于F103这种16位定时器如果PSC太大ARR会超出65535这时候就反过来牺牲计数分辨率保证ARR在16位范围内。2.4 PWM的频率和占空比从哪里来PWM的本质是“定时器在数数时输出引脚的电平受比较值CCR控制”。计数器从0增长到ARR当计数值小于CCR时输出高电平大于等于CCR时输出低电平。频率由PSC和ARR决定占空比由CCR和ARR共同决定。FOC电机控制里用的PWM波形要求高精度、低抖动。电机SVPWM的载波频率通常为10kHz到20kHz这时候PSC选得小一点ARR能算到几千相位分辨率就够了。但要注意互补PWM和死区插入依赖TIM1、TIM8这种高级定时器F103上用普通定时器做不到硬件死区得靠外部电路或者软件模拟。3. 三种计数模式向上、向下、中央对齐3.1 向上计数与向下计数的选择默认的向上计数是最常用的计数器从0数到ARR触发事件后清零重新开始。向下计数则从ARR数到0。两者在对称性输出上没有本质区别但在与DMA配合时会影响数据的到达时刻。向下计数在少数场景下有用比如需要某些外设“在计数值最低时触发DMA”来避免总线占用高峰。但大部分工程里向上计数已经够用。3.2 中央对齐模式的真实用途中央对齐模式是“先向上数到ARR再向下数到0”输出波形天然关于中点对称因此生成PWM时脉宽在两边缘同时变化谐波更小。这用在电机控制的正弦波PWM上能有效降低电流谐波和噪声。不过中央对齐模式的更新事件发生在两个点上上溢和下溢因此中断或DMA触发频率是计数频率的两倍。同样的ARR和PSC中断频率翻倍这个要注意不然实时系统里一个50kHz的中断会在你意想不到的地方耗费CPU。3.3 编码器模式的“特殊数法”定时器还可以直接读取正交编码器信号这时候它数的就不再是内部脉冲而是外部输入的A、B两路相位差90°的方波。通过判断A、B信号的先后顺序定时器内部硬件能自动分辨正反转同时每来一个边沿都可以计数。这意味着只接一个定时器既能得到位置信息也能得到方向信息。编码器模式下CNT寄存器就是位置计数器方向翻转由硬件自动处理不需要软件轮询判断。电机转速计算要用到定时频率简单做法是固定一个时间窗口比如每10ms读一次CNT差值除以时间就是速度。这个算法我实际调过位置精度取决于编码器线数和定时器配置的计数边沿。4倍频模式A和B的上下边沿都计数是常用的选择但CNT溢出时要处理好方向判断否则速度会跳变。4. 时间基准的扩展玩法外部时钟、输入捕获与SysTick4.1 让定时器去数外部脉冲定时器的时基不一定要来自内部的RCC它也可以接收外部引脚上的脉冲。STM32的定时器外部时钟有两种外部时钟模式1TS引脚上的信号触发和模式2ETR引脚直接作为时钟源。模式2最典型的应用是“数脉冲数”。比如流量计输出一个频率和流速成比例的方波直接把信号接到定时器ETR引脚配置外部时钟模式计数器每个上升沿加一。过一段时间读CNT除以时间就是频率。这种做法的好处是不占CPU资源也不会丢脉冲。用CubeMX配置时在定时器的“Clock Source”里选“External Clock Mode2”再指定ETR引脚和极性。有一个隐藏的坑ETR输入引脚通常会经过输入滤波器滤波参数太大会把高频脉冲滤掉。测量几kHz无所谓测量几十kHz的流量脉冲时要适当缩小滤波采样频率。4.2 输入捕获同一个定时器不同的“数完了没”输入捕获也是基于定时器内部的时基但新增了一组捕获寄存器。信号输入引脚检测到边沿时计数器的当前值会被硬件锁存到捕获寄存器里。两次捕获值的差就是两次边沿之间计数器走了多少步乘以单步时间就能得到脉宽或周期。超声波测距就是标准例子发送超声波的同时启动定时器回波到达时读取捕获值。用上升沿捕获得到起点下一次上升沿或下降沿捕获得到终点时间差换算成距离。我用过的方案是TIM2的通道1做输入捕获配置上升沿和下降沿都捕获HAL_TIM_IC_CaptureCallback里分别记录两个时间戳。距离 (结束时间 - 开始时间) × 声速 / 2。要注意捕获溢出也就是两次事件之间计数发生了重载翻转需要在中断里处理CNT回绕不然时间全部算错。PWM输入模式是输入捕获的进阶一个通道测频率另一个通道测占空比。信号接入后结合主从模式硬件上能自动把周期值和脉宽值锁存。这里对时钟源的稳定性要求高采样用的定时常数不稳定捕获结果就会抖动。4.3 滴答定时器SysTick最容易“数不准”的时间基准SysTick是内核里的一个24位递减计数器用来做系统节拍和软件延时。很多人写HAL_Delay()、delay_us()都是基于SysTick的但实际工程中SysTick的时间基准如果被其他中断抢占或者被FreeRTOS接管延时就会不准确。SysTick的时钟源选择在STM32上有个细节可以用HCLKAHB时钟或HCLK/8两种。CubeMX默认选择HCLK意味着72MHz下每个tick约13.9ns。如果你用HAL_Delay(1)它内部的实现方式是等待SysTick计数值归零的标志但中断优先级不够高时延时会被其他高优先级中断拉长。在串口打印、I2C时序要求严格的场合这种误差会累积成可见的问题。更稳的微秒级延时我试过几种做法最实用的是用定时器做基准比如TIM2配置成1MHz计数频率PSC 系统时钟 - 1然后在要延时的地方读取CNT直到CNT增量达到目标。这样不受SysTick中断优先级影响精度可以达到微秒级别代价是占用了定时器资源和一点点功耗。4.4 低功耗定时器的特别之处有低功耗需求的设备主时钟关掉后普通定时器全部停摆只有LPTIM这类低功耗定时器能靠外部低速时钟如LSI、LSE继续运行。它在停机模式下仍可工作可以用来做RTC唤醒、外部脉冲计数等。但它毕竟是低功耗定位时钟源频率很低做不了PWM和输入捕获的精细工作。如果你的代码里有“待机模式下还能定时唤醒”的需求选LPTIM而不是普通TIM这是硬件设计层面的选择不是软件调参能解决的。5. 实操过程中的高频问题与排查经验5.1 溢出的频率怎么跟理论对不上这是最常见的基本上都是PSC和ARR的“1”没处理好。寄存器存的数是从0开始的PSC7199表示7200分频ARR9999表示10000个计数周期。手册里写得很清楚寄存器值等于期望分频系数减1。用调试器看TIMx-PSC和TIMx-ARR的实际值如果和理论上要写入的值差1就说明你理解了问题所在。另一种不太容易想到的概率是定时器用了外部时钟或从模式。主从模式下例如触发另一个定时器启动计数此时定时器的时钟不再是简单的PCLK而是触发的源。排查时先缩小范围检查SMCR寄存器里的触发选择位再用逻辑分析仪看输入波形。5.2 定时器中断从来没触发中断没反应第一件事不是改代码而是查NVIC使能位。HAL库初始化之后要调用HAL_TIM_Base_Start_IT(htim2)这个函数负责局部使能更新中断并启动计数。很多人只调用了HAL_TIM_Base_Init定时器没有进入中断模式当然毫无反应。第二个隐藏点在TIM_IT_UPDATE的使能位。HAL库的初始化流程在状态机里会把更新中断关闭必须在HAL_TIM_Base_Start_IT后才打开。如果调试时发现中断标志置位但程序不跳转先确认中断处理函数是否进到了HAL_TIM_IRQHandler再检查优先级分组是否配好。如果你的系统用了FreeRTOS还要确认中断优先级不高于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY否则中断回调里调用HAL库函数可能触发断言。5.3 PWM就是没有波形输出PWM最常见的问题是忘了调用HAL_TIM_PWM_Start。这个函数与HAL_TIM_Base_Start_IT的通道参数格式不同第二个参数是TIM_CHANNEL_x不是周期或占空比。第二类问题出在引脚复用。CubeMX生成的代码会自动配置GPIO复用但如果是手动移植工程很容易漏掉__HAL_AFIO_REMAP_PIN或者GPIO的复用功能配置。F1系列尤其要注意复用重映射比如TIM2的通道1默认在PA0如果用了TIM2_REMAP部分重映射到PA15GPIO配置就要跟着变。第三类问题是极性。PWM输出极性和初始电平不匹配测量的波形看起来“反了”。CubeMX里可以配Pulse和Polarity手动移植时这两个配置分散在TIM_OC_InitTypeDef和GPIO_InitTypeDef里检查顺序颠倒就很隐蔽。5.4 输入捕获的值总是偏差输入捕获得到的CNT值和实际时间对不上先检查两件事本地时基有没有和捕获共用PSC滤波参数是不是过大。输入引脚的高频噪声会导致捕获触发过早或过晚滤波器能滤掉尖刺但如果采样窗口太宽也会把有效边沿丢失。更麻烦的是溢出处理。捕获中断里读了CCR但计数器可能已经经历了一到多次回绕。此时记录的“差值”其实是两段余数的差不是真正的差值。必须在捕获中断里同时读取SR寄存器中的更新标志若为1则记录一次溢出最后用溢出次数乘以ARR1再补上余数差。5.5 实测小技巧用DMA搬运捕获值不需要边沿时实时响应的场景我习惯把捕获和DMA结合。配置定时器捕获为DMA请求DMA在多个通道事件发生时把CCR搬运到内存数组。这种模式的优点是CPU不需要在CaptureCallback里频繁进出大数据量分析时特别省心。但要注意DMA循环模式的长度和数组缓冲区的边界。DMA循环传输时内存写指针会自动回到数组起点如果主循环来不及消费数据新数据会覆盖旧数据。设计上建议使用双缓冲或者环形缓冲手动维护读写索引。写在最后的个人总结定时器这套东西理解清楚“数什么”和“基准从哪来”这两个问题后面所有模式都可以顺着推出来。我的建议是拿到一块开发板不要急着用CubeMX生成工程先手写一个TIM的寄存器版本把PSC和ARR写进代码配合调试器看计数器的实际波形再看它产生的中断频率和理论是否一致。这个流程走一遍比背十遍手册都管用。还有个小技巧调试定时器相关的代码时开一个逻辑分析仪或者示波器直接测引脚波形比盯寄存器效率高得多。示波器不是万能但至少能帮你排除一大半“寄存器看起来对实际不对”的玄学问题。祝你少踩几个坑。
返回列表