尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32定时器到底在数什么?一文讲透时基与时钟树

STM32定时器到底在数什么?一文讲透时基与时钟树 搞嵌入式这么久我发现一个很有意思的现象很多人对STM32的定时器又熟悉又陌生。说熟悉是因为几乎每个项目都会用到——做个LED闪烁、输出个PWM、抓个脉宽随手一调就能跑起来。说陌生是因为一旦遇到定时不准、周期对不上、休眠后再唤醒定时器罢工这类问题很多人的排查思路就会卡住最后只能靠试参数碰运气。你问他定时器是怎么工作的他能背出来PSC分频、ARR重装载、CNT计数。但你再问他一句这个定时器到底在数什么他往往就愣住了。这篇文章我就把这个底层问题彻底讲透。围绕时间基准从时钟树一路拆到PSC、CNT、ARR三个寄存器怎么配合再到实际项目中几个高频应用场景的配置思路。标题叫定时器到底在数什么其实就是想搞清楚一件事STM32定时器的那个心跳究竟是从哪里来的又是怎么被一步步加工成我们想要的定时时间的。适合谁看准备入门STM32的初学者、被定时器问题折磨过的进阶玩家以及想系统梳理一遍时间基准来龙去脉、把知识补完整的工程师。读完不敢说让你成为定时器专家但至少以后再遇到定时类问题你能自己推算出问题出在哪个环节而不是盲目改参数。1. 时间基准从哪来先从时钟树说起1.1 先搞清楚频率和周期的关系在聊定时器之前必须先把一个最基础的概念掰开揉碎频率和周期互为倒数。定时器的计数本质是在数一个固定频率的脉冲。假设给定时器输入的时钟是72MHz那就意味着每秒钟有72,000,000个脉冲送进来。每个脉冲持续的时间就是1/72,000,000秒约等于13.89纳秒。定时器每收到一个脉冲计数值CNT就加1。想定时1秒就需要CNT数够72,000,000个脉冲想定时1毫秒就需要数够72,000个脉冲。道理就这么简单——但是有一个问题如果仅靠数72MHz的脉冲来实现1秒的定时CNT寄存器得能装下72000000这个数字。STM32的定时器计数器大多是16位的最大值只能到65535。所以必须想办法把输入频率先降下来降到CNT能数得过来的程度。这个降频动作就是预分频器PSC的活。1.2 整棵时钟树定时器的心跳源头那72MHz又是从哪来的答案藏在RCCReset and Clock Control复位和时钟控制模块管理的时钟树里。我见过不少开发者配置定时器的时候只看定时器本身根本不关心时钟树。这是个大坑。时钟树不配置对定时器的时钟源就是错的后面所有计算结果都会跟着错。以最常见的STM32F103为例整个时钟路径是这样的外部高速晶振HSE频率通常是8MHz。HSE经过PLL锁相环倍频得到系统时钟SYSCLKF103最高能到72MHz。SYSCLK经过AHB预分频器得到APBx总线时钟。APB1和APB2总线时钟再喂给各自的定时器。关键点来了挂在APB1上的定时器TIM2~TIM7、TIM12~TIM14等和挂在APB2上的定时器TIM1、TIM8等它们的时钟频率并不是直接等于总线时钟而是存在一个倍频器。在F103上当APB1预分频系数不等于1时APB1上的定时器时钟频率会自动翻倍。举个例子如果APB1总线时钟被系统初始化代码配置为36MHz那挂在APB1上的定时器实际拿到的时钟是36MHz × 2 72MHz。很多人在用CubeMX配置工程时没注意这个细节以为定时器时钟就是总线时钟结果算出来的定时时间差了整整一倍。1.3 不止有内部时钟外部时钟和外部触发模式除了内部时钟定时器还能从外部引脚输入时钟。TIMx_CH1、TIMx_CH2以及ETR引脚都可以作为外部时钟源。这个功能用在对精度要求比较高的场合。比如你想用一个外部传感器输出脉冲来精确计数每个脉冲代表一个事件这时候外部时钟模式就有用武之地了。外部时钟模式又分两种外部时钟模式1External Clock Mode 1把某个输入引脚TI1、TI2的边沿作为计数时钟。外部时钟模式2External Clock Mode 2使用ETR引脚的信号作为计数时钟。这两种模式的本质区别在于信号预处理路径不同。ETR引脚通常带有极性选择、预分频和滤波功能适合直接对高速脉冲计数而TI1/TI2引脚的信号要先通过捕获通道的滤波器和边沿检测器再作为时钟使用。还有一种特殊的外部触发模式它不改变计数时钟来源而是通过外部信号触发定时器启动计数。就是门控或触发的概念常用于实现脉冲计数和信号同步测量。不过在日常开发中绝大多数场景用到的还是内部时钟。真正的定时时间基准来自内部时钟源经PSC分频后的脉冲流。2. PSC、CNT、ARR时基单元三兄弟怎么配合2.1 预分频器PSC第一个降频环节PSCPrescaler预分频器是定时器内部第一个对时钟信号做处理的环节。它把输入时钟频率除以PSC1然后输出给计数器CNT作为计数脉冲。为什么是PSC1而不是PSC这是寄存器设计的老规矩PSC寄存器的值从0开始所以实际分频系数要比寄存器值大1。这个加1的细节坑过无数人。举个例子想把72MHz降到1MHz分频系数应该是72。那PSC寄存器应该写多少是72还是71正确写法是写71。因为实际分频系数 72MHz / (PSC 1) 72MHz / (71 1) 1MHz。很多人在这里算错直接把PSC写成了72结果实际分频是73输出频率是72MHz / 73 ≈ 0.986MHz定时时间偏长了约1.4%。看似误差不大但在高频应用或多级级联的场景中会被放大得很明显。2.2 计数器CNT和重装载值ARR分频之后定时器就拿到了一个干净的计数时钟。假设经过PSC分频后是1MHz那每个计数脉冲的时间就是1微秒。接下来就轮到CNT上岗了。CNT是个计数器每来一个脉冲就加1或减1具体取决于计数模式。对于最常见的向上计数模式CNT从0开始一直加到ARRAuto-Reload Register自动重装载寄存器的值。还是一微秒一个脉冲的例子。如果你想让定时器每1毫秒产生一次更新事件也就是溢出那ARR就应该设为999。因为CNT从0数到999一共经历了1000个脉冲耗时1000微秒也就是1毫秒。把定时时间计算公式写全定时时间 (PSC 1) × (ARR 1) / 定时器输入时钟频率这个公式建议刻在脑子里。任何一个定时器时基配置本质都是在解这个等式。2.3 三个人协同起来的完整流程把整个流程串起来看定时器的时基单元由PSC、CNT、ARR三个寄存器组成它们各有分工。PSC负责把高频时钟降压到适合计数的程度CNT负责逐个数脉冲是真正干活的角色ARR则规定了数到多少算一轮。一个完整的计数周期是这样走的定时器启动后CNT从0开始等第一个计数脉冲到来。每来一个脉冲CNT加1。CNT的值和ARR进行比较当CNT等于ARR时产生更新事件Update Event。更新事件会触发更新中断如果使能了同时CNT清零开始下一轮计数。在某些高级定时器上CNT清零后还可以自动把影子寄存器里的ARR值重新装载进来这就是自动重装载名字的来历。更新事件还分为周期性更新和软件触发更新。正常情况下每个计数周期结束时都会自动产生更新事件。但有些场景需要手动触发更新比如在初始化时配置完PSC和ARR后软件向UG位写1立即产生一次更新事件让影子寄存器预装载生效。提一个细节ARR在STM32的很多定时器上是有影子寄存器的。所谓影子寄存器就是有一个你写入的值和一个实际生效的值两套。默认情况下你写ARR实时生效但如果你开启了预装载功能新写的ARR值要等到下一个更新事件到来时才会真正生效。这个机制有什么用想象你在PWM输出过程中想动态改变频率——如果ARR直接改可能在新旧值交界处产生一个毛刺脉冲。开了预装载新值在最干净的时机切换PWM波形就不会出乱子。3. 定时器家族怎么选从高级到低功耗一网打尽3.1 STM32定时器分类和各自定位搞清楚了时基原理再来看STM32的定时器家族就会清晰很多。STM32的定时器大致可以分成四类第一类是高级定时器代表型号是TIM1、TIM8。它们功能最全除了基本的定时功能还具备死区插入、互补输出、刹车输入等功能专门为电机控制这类场景设计。第二类是通用定时器代表型号是TIM2、TIM3、TIM4、TIM5。它们是开发中最常用的定时器支持定时、PWM、输入捕获、输出比较等几乎全部常用功能。最典型的特点是都能产生更新中断和多种PWM模式。第三类是基本定时器代表型号是TIM6、TIM7。它们没有外部引脚不能做输入捕获和PWM输出只能做纯粹的定时。适合用作时基比如给系统提供周期性的调度时钟或者喂IWDG独立看门狗。第四类是低功耗定时器LPTIM代表型号是LPTIM1、LPTIM2。它最大的特点是可以在低功耗模式下继续运行。普通定时器在STOP模式下会因时钟停止而停工但LPTIM可以由低速时钟如LSI或LSE驱动在低功耗模式下保持计时常用于唤醒定时器。选型的时候先问自己三个问题需要输出波形吗需要测量外部信号吗需要在低功耗模式下工作吗答案组合起来定时器类型就定下来了。3.2 和中断配合定时器时基的延伸应用定时器最重要的能力之一就是把计数转化成中断。通过更新中断定时器能在每个周期结束时调用中断服务函数这是嵌入式系统分时处理的基石。在F103上定时器中断的响应路径是这样的更新事件 → 触发更新中断 → 进入NVIC中断控制器 → CPU执行中断服务函数。整个链路上任何一环断了定时器中断都不会按预期工作。常见的坑是NVIC没配置好。很多入门者只写了定时器的寄存器配置忘记使能NVIC对应中断通道结果定时器在跑但中断从不触发。这个问题在后面我会专门展开讲。另外一个和中断密切相关的概念是DMA。在定时器场景中定时器可以产生事件触发DMA传输。最经典的例子就是定时器 DAC DMA产生任意波形定时器通过更新事件触发DMA搬运数据。这种做法不用CPU参与波形就能持续输出是信号发生器的经典实现方法。3.3 选型时容易被忽略的细节结合热门搜索词我梳理出几个容易踩的选型坑第一个坑是以为所有定时器都有输入捕获功能。基本定时器TIM6/TIM7就没有外部引脚做不了输入捕获。如果你要做频率测量选TIM6必然失败。第二个坑是高级定时器杀鸡用牛刀。做简单PWM时很多人习惯性用TIM1。但TIM1的PWM输出默认输出极性、死区配置比较复杂调试反而费时间。用一个通用定时器反而省事。第三个坑和LPTIM有关。LPTIM的时钟源选择比普通定时器多可以在多种低功耗时钟之间切换。但它的分频逻辑和配置方式与通用定时器不同不少工程师照着TIM2的配置方式写LPTIM代码毫无反应。遇到这类问题查一下LPTIM的参考手册章节重点看时钟源选择和预分频寄存器基本上能找到原因。4. 实操三种最典型的时基应用从头跟到尾4.1 需求一定时中断驱动LED闪烁这是定时器最基础的用法但也很适合用来验证对时基的理解。假设系统时钟72MHzAPB1预分频为2即APB1总线时钟36MHz定时器时钟翻倍为72MHz想实现500ms翻转一次LED。根据公式定时时间 (PSC 1) × (ARR 1) / 定时器时钟如果选择PSC 7199分频系数7200则计数时钟是72MHz / 7200 10kHz一个计数脉冲100微秒。ARR设为4999则一个计数周期是100微秒 × 5000 500000微秒 500毫秒。代码用HAL库写核心就三步。第一步定义定时器句柄并配置时基TIM_HandleTypeDef htim2; void MX_TIM2_Init(void) { htim2.Instance TIM2; htim2.Init.Prescaler 7199; // 分频系数7200计数时钟10kHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; // 向上计数 htim2.Init.Period 4999; // 5000个计数脉冲500ms htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); }第二步在main函数里启动定时器的更新中断HAL_TIM_Base_Start_IT(htim2);第三步在中断回调函数里翻转LEDvoid HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); } }这里有个很实用的经验HAL_TIM_PeriodElapsedCallback是所有定时器共用的回调函数所以在回调里一定要先判断是哪个定时器触发的。不判断或判断错误会导致多个定时器共用回调时逻辑混乱。4.2 需求二输入捕获测量频率和脉宽输入捕获是时基单元的逆向应用——不靠定时器定时而是利用时基来测量外部信号。原理是定时器内部有一个捕捉寄存器当外部信号发生边沿跳变时CNT的当前值会被瞬间锁存到捕捉寄存器里。两次捕获值的差值就是两个边沿之间经过的计数脉冲数。如果计数时钟已知比如PSC后为1MHz每个脉冲1微秒那么两次捕获差值就是边沿间隔的微秒数。连续两个上升沿之间的间隔就是信号周期周期的倒数就是频率。用HAL库配置输入捕获也很简单。这里以捕获一个信号的频率为例void MX_TIM3_Init(void) { htim3.Instance TIM3; htim3.Init.Prescaler 71; // 72MHz / 72 1MHz每计数一次1us htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 0xFFFF; // 最大值保证计数范围最大化 HAL_TIM_IC_Init(htim3); }然后配置捕获通道TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿捕获 sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; // 直接映射到TI1 sConfigIC.ICPrescaler TIM_ICPSC_DIV1; // 不分频 sConfigIC.ICFilter 0; // 不滤波 HAL_TIM_IC_ConfigChannel(htim3, sConfigIC, TIM_CHANNEL_1);启动捕获HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1);在回调函数中读取捕获值uint32_t capture_val 0; uint32_t last_capture_val 0; uint32_t frequency 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { capture_val HAL_TIM_ReadCapturedValue(htim3, TIM_CHANNEL_1); if (capture_val last_capture_val) { frequency 1000000 / (capture_val - last_capture_val); } else { frequency 1000000 / (0xFFFF - last_capture_val capture_val); } last_capture_val capture_val; } }注意代码里处理了计数器回绕的情况。当计数溢出时CNT会从0重新开始如果直接用两次捕获值相减会得到负数。这种回绕处理对于精确测量至关重要尤其是被测信号周期接近计数周期时。4.3 需求三用定时器实现软件模拟串口这个方法比较进阶但非常能验证你对时基颗粒度的把控能力。为什么需要软件串口因为有些项目的硬件UART数量不够用或者某些引脚被占用只能通过GPIO配合定时器来模拟一个串口。以最典型的8N1格式8位数据、无校验、1位停止位为例传输一个字节需要10个位时间1个起始位 8个数据位 1个停止位。如果波特率是9600那一个位时间是104.17微秒。定时器可以用来确定每个位的采样点。思路是这样的用一个定时器产生固定周期的中断中断频率等于波特率。每个中断到来时把一个数据位送到GPIO输出同时读取GPIO输入收集接收位。在F103上实现时我习惯用TIM4做发送位定时用TIM2做接收采样// 波特率9600系统时钟72MHz // 定时器时钟 72MHz / (7191) 100kHz一个计数脉冲 10us // 需要位中断周期 104.17us即大约 10.417 个计数脉冲 // 所以取整为10个计数脉冲实际波特率 72MHz / (720 * 10) 10000 // 实际波特率偏差约4%对于9600的目标来说偏高这里有个现实矛盾定时器分频只能取整数无法精确产生104.17微秒的周期。误差在低速传输时通常可以容忍但在115200这类高速率下就会出问题。这也是软件串口的一个重要限制。所以实际操作中做软件串口时建议把定时器先分频到一个比较细的粒度再用重装载值精细调整。比如输入时钟72MHzPSC设为71得到1MHz计数时钟每计数1微秒。波特率9600时位周期约104.17微秒ARR设为104每104微秒中断1次。实际位周期104微秒对应波特率9615误差约0.16%完全在可接受范围内。这就是时基精度对功能成败的影响的典型例子。很多人做软件串口失败不是逻辑写错而是时基算得不够细、误差太大。5. 常见问题排查时基相关的那些坑5.1 定时器时间完全不对这是最多人遇到的问题。时间不对通常不是定时器本身坏了而是时钟源头错了。第一步用debugger读取RCC配置寄存器确认定时器实际输入时钟。在F103上重点检查APB1和APB2预分频系数。前面讲过APB1预分频不为1时定时器时钟翻倍很多人漏了这一点。第二步检查PSC和ARR是否算对。记住公式实际分频是PSC1周期是ARR1。很多入门者把这两个1忘了时间偏差一个数量级。第三步检查HAL_TIM_Base_Init中Prescaler和Period的赋值类型。Prescaler是uint32_tPeriod也是uint32_t但如果你的配置值是负数或超出了寄存器位数16位高位会被截断导致实际生效的值和你以为的值完全不同。5.2 定时器在跑但中断不触发这个问题的排查顺序建议按由内到外来进行。先看定时器本身是不是没调用HAL_TIM_Base_Start_IT很多人只执行了初始化函数就等中断但HAL库在初始化后默认是不启动中断的。再看NVIC定时器对应的中断优先级有没有设置中断使能有没有打开在HAL库中HAL_TIM_Base_Start_IT内部会调用HAL_NVIC_EnableIRQ和HAL_NVIC_SetPriority来配置NVIC。但有些老版本的库或手动配置时容易遗漏。最后看回调函数HAL_TIM_PeriodElapsedCallback有没有被实现HAL库的中断处理流程是这样的中断发生 → 进入中断服务函数 → 调用HAL_TIM_IRQHandler → 处理标志位 → 调用弱回调HAL_TIM_PeriodElapsedCallback。如果你没有实现这个回调函数中断发生了也没人去翻转LED。排查时建议在回调函数里先放一个断点看能不能停到。停不到就往中断服务函数和NVIC方向查。5.3 掉进底层的几个特殊问题第一个是LPTIM在低功耗模式下的表现。LPTIM由LSI或LSE驱动在STOP模式下依然能保持计数。但要注意LPTIM的时钟源和分频配置与普通定时器不同它的预分频寄存器是7位的最大分频系数是128。如果配置不当LPTIM在正常工作模式下能跑但一进入STOP模式就停止工作大概率是时钟源选择了依赖主时钟的选项。第二个是调试器停在断点时定时器仍在计数。默认情况下定时器不受调试暂停影响。如果你在调试模式下想让定时器跟着CPU一起停下来需要设置DBGMCU寄存器中对应定时器的DBG_XXX位。很多初学者在断点调试时发现定时器状态乱了就是这个原因。第三个和系统嘀嗒定时器SysTick有关。SysTick是Cortex-M内核自带的定时器常用于操作系统时基和HAL_Delay。很多人把SysTick和STM32外设定时器搞混。SysTick确确实实就是一个内嵌的定时器它也在数时钟——数的是内核时钟。区别在于SysTick不参与外设功能不做PWM、不捕获纯粹用于系统级的延时和调度。如果你用HAL_Delay卡死第一反应不要怀疑SysTick本身先检查是否有更高优先级的中断长时间霸占CPU导致SysTick的更新中断一直得不到执行。5.4 排查表格速查问题现象优先排查点解决办法定时时间偏大或偏小时钟树配置、PSC/ARR的1是否遗漏用调试器读寄存器确认实际时钟频率定时中断完全不触发NVIC是否配置、Start_IT是否调用检查HAL_TIM_Base_Start_IT和NVIC使能回调函数没被执行是否实现了HAL_TIM_PeriodElapsedCallback在回调中加断点确认定时器动态修改周期无效是否开启了预装载功能配置ARR预装载修改在更新事件时生效STOP模式唤醒后定时器不工作时钟源是否选择了主时钟改用LSI/LSE确认LPTIM配置调试暂停时定时器还在计数DBGMCU没设置使能对应定时器的调试暂停功能软件延时卡死SysTick被阻塞或优先级问题检查是否有中断长时间运行6. 一次内核时钟细节的补充这里再补充一点关于定时器的时基到底能不能完全精准的思考。定时器的时基精度取决于两件事一是输入时钟源的精度二是分频和重装载的整数取整误差。关于第一点内部RC振荡器HSI的精度通常在1%~3%左右温度变化还会漂移。如果项目需要用定时器做精确计时或通信波特率生成强烈建议使用外部晶振HSE作为系统时钟源。在F103上这意味着要正确配置PLL使能让系统时钟从HSE倍频而来而不是默认的HSI。关于第二点任何整数分频都无法精确复现一个带小数的分频比。比如72MHz分频到1MHz72MMHz除以7200等于10kHz这些都是整数除法可以精确做到。但如果想分频到1.5MHz72/1.5 48也是整数那也行。真正麻烦的是像9600波特率这种带小数的目标频率它无法由72MHz整数分频得到只能取近似值。所以做高精度定时应用时要反过来设计先看定时器时钟是多少算出差一个计数脉冲会造成多大误差再决定是否满足需求。这个思维方式和直接用库函数填参数是完全不同的层次。我个人在实际调试中的体会是定时器的时基问题90%的根源都不在定时器本身而在时钟树和分频计算的细节上。只要你把HSE怎么变成SYSCLK、SYSCLK怎么变成APB时钟、APB时钟怎么喂给定时器这条链路完全打通PSC和ARR就只是两个乘除法问题再也难不住你。最后再分享一个小技巧在你刚开始接触一个新板子、不确定定时器配置对不对的时候别急着写复杂的应用逻辑。先配置一个最简单的定时器中断翻转LED观察LED的实际闪烁周期再和你的理论计算值对比。这一步能最快帮你验证整条时钟链路是否正确。基准确认了后面做什么都有底气。
返回列表