十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

定时器原理图解:从计数器骨架到PWM、捕获,一次搞懂

定时器原理图解:从计数器骨架到PWM、捕获,一次搞懂 先看一张图这是我在调试一块STM32板子时的真实经历LED闪灯程序定时器中断方式配置一步一步照着参考写中断就是不进来。折腾了两个小时最后发现是定时器时钟树算错了预分频值少除了一个倍数。后来我把定时器的内部结构画到白板上一眼就看穿了问题所在。从那以后我养成了一个习惯不管用什么芯片先把定时器的“骨架图”在脑子里搭起来。定时器这东西说复杂可以很复杂说简单其实就一句话——它是一个会按固定节拍数数、数到头就会触发动作的硬件计数器。理解了这句话51单片机的定时器、STM32的TIM、GD32的定时器、甚至555定时器全部一通百通。这篇内容适合正在学单片机但被各种“工作模式”绕晕的初学者也适合用HAL库开发但遇到定时器相关问题排查不出来的嵌入式工程师还适合准备笔试面试时想把定时器原理说清楚的同学。1. 定时器的底层骨架一个带闹钟的计数器很多人学定时器喜欢直接背寄存器名字CNT、PSC、ARR、CCR背完就忘。我建议反过来先看结构再看寄存器因为寄存器只是结构里某个模块的“控制旋钮”。定时器内部其实只有四件事给时钟、数数、比较、触发动作。1.1 四个关键模块时钟源、计数器、自动重装载、比较器我把一张典型的定时器内部结构图画在下面虽然简化了但核心链路都在时钟源 ── 预分频器(PSC) ── 计数器(CNT) ── 比较逻辑 │ │ │ │ ▼ ▼ │ 自动重装载(ARR) 输出引脚/中断/事件 │ │ └────────────────┘逐个说。时钟源定时器不能自己凭空数数它需要一个节拍信号。这个节拍可以来自芯片内部的时钟树比如STM32的APB1/APB2外设时钟也可以来自外部引脚输入比如用外部信号触发计数。绝大多数场景用的是内部时钟。预分频器(PSC)如果内部的时钟节拍是72MHz意味着每秒钟数7200万个数多数场景用不了这么快。PSC的作用就是“降频”把节拍调慢。这里有个极其容易出错的点PSC的寄存器值等于分频系数减1。想要4分频写入的是3不是4。我在接手别人代码时经常看到定时器频率完全对不上的情况一半以上是这里栽的。计数器(CNT)这是定时器的心脏一个向上的计数器。每来一个节拍CNT加1。它是实实在在的硬件寄存器你能读到当前计到哪儿了也能直接写入一个初值让它“跳到”你指定的位置。自动重装载寄存器(ARR)这是定时器的“长度标尺”。CNT数到ARR的值以后会发生什么看配置。最常见的是CNT清零重新计数同时产生一个更新事件这就是“定时中断”的来源。ARR的值不是随便设的它直接决定定时周期。比较寄存器(CCR)这是定时器“输出波形”的关键。CNT会一直和CCR里的值做比较当CNT小于CCR时输出一个电平等于或超过之后输出另一个电平。一高一低组合起来就是PWM波。这么说可能还有点抽象打个比方PSC是节拍器的速度档位CNT是节拍器跳动的次数ARR是“跳到第N下就清零重来”的闹钟CCR是“跳到第M下我就切换电灯开关”的阈值。整个定时器的所有工作模式都是这四个模块不同玩法排列组合的结果。1.2 时序图画法计数值随时间怎么变“图说定时器”最重要的工具就是画时序图。拿到一个定时器问题只要能把CNT随时间的变化趋势画出来问题的答案基本就浮出水面了。以最常见的向上计数模式为例计数值随时间变化的关系是这样一张图计数值 ↑ ARR ─────────────────┐ │ │ │ │ ┌──── │ ┌────┐ │ │ │ │ │ │ │ 0 ────┴────┴───────┴─────┴──────────→ 时间 周期1 周期2CNT从0开始往上加加到ARR就瞬间归零如此循环。周期T等于(ARR1)个计数节拍的时间换算成秒就是T (PSC1) / 定时器时钟频率 × (ARR1)这里有一个很多教程都不强调的细节无论PSC还是ARR算周期时都要1。因为寄存器从0开始数写入的99代表100个节拍。我见过有人问“为什么我设置1ms中断实际却是1.01ms”多半是这里少加了一个1。如果是低频场景这1%的误差无所谓如果做高精度时钟就麻烦了。2. 定时器工作模式逐个看图拆解“工作模式”这个词听起来像有很多种其实都是在回答一个问题CNT数到特定值时硬件的反应是什么根据反应方式的不同模式分成了定时中断、PWM输出、输入捕获、编码器模式等几种。下面逐个画图拆解。2.1 定时中断数满即触发最简单的模式定时中断是单片机入门的第一个定时器场景比如让LED每500ms翻转一次。它的工作逻辑就是前面那张“向上计数”的图CNT从0涨到ARR硬件自动把CNT清零同时拉起一个更新中断标志如果中断使能了CPU就跳进中断服务函数。需要特别注意的是这里“定时”不是CPU在计时间而是硬件定时器在后台自己数数。CPU只管在该响应的时候响应一下其余时候该干嘛干嘛。这就是定时器存在的根本意义——把CPU从无聊的延时循环里解放出来。配置上有一个坑很多芯片的中断标志位在进入中断函数后需要软件主动清零。我见过不少人刚换到某个新平台时忘了清标志导致中断不停地进看起来像死机了一样。TIM的中断标志位不清理硬件会一直认为事件没被处理中断请求持续有效。2.2 PWM输出用CCR画出高低电平的占空比PWM输出模式是“比较”这个动作的直接体现。定时器被配置成PWM模式后每来一个计数节拍都要做一次比较CNT和CCR比。输出引脚的电平状态由比较结果决定。CNT值 0→ARR逐周期增长 CCR点比较翻转点 ↓ 输出高 ┌──────────────────┐ 电平 │ │ 输出低 │ │ └──────────────────┘ ├── 高电平时间 ──┤ ├──── PWM周期 ────────┤ARR决定了PWM的周期也就是频率CCR决定了高电平维持的时间也就是占空比。占空比 CCR / (ARR1)。这个公式简单到不能再简单但我见过无数人在“输出占空比到不了100%”的问题上卡住原因后面专门讲。PWM模式还有两种细分PWM1和PWM2以STM32为例。PWM1模式下CNT小于CCR时输出有效电平PWM2模式则相反。所谓“有效电平”又和输出极性配置有关可以是高电平也可以是低电平。这一层套一层的逻辑关系光看手册很容易绕晕我的建议是配置好之后一定要用示波器或者逻辑分析仪实测一下波形眼见为实。还有中心对齐模式CNT从0加到ARR再减回0像一个三角形。这种模式产生的PWM是对称的在电机控制里可以减少谐波。2.3 输入捕获反向看图测外部信号的宽度和周期输入捕获模式可以理解成把图纸反过来看PWM模式是我们自己画波形输出输入捕获是我们用定时器去量一个外部波形。怎么量还是在外部信号的边沿上升沿或下降沿到来那一刻把当前CNT的值“快照”下来。外部信号 __┌──────┐______┌──────┐______ 捕获时刻 ↑捕获1 ↑捕获2 CNT值 N1 N2N2减去N1再除以定时器时钟频率就是两次边沿之间的时间长度。如果捕获的是同一个PWM信号的两个连续上升沿算出来的就是该信号的周期如果捕获的是一个脉冲的上升沿和下降沿算出来的就是脉宽。这就是“频率测量”和“脉宽测量”的基本原理。实际写代码的时候有一个常见的精度问题定时器可能在中途溢出多次。比如被测信号很慢等它下一个上升沿到来时CNT已经溢出了好几轮。这种情况必须用溢出中断来记录溢出的次数再用溢出次数 × 溢出周期 当前CNT差值才能算准。很多人在低频测频时数据跳来跳去就是这个细节没处理好。2.4 编码器模式把轴旋转角度变成计数方向和数据编码器模式是最能体现定时器“计数器”本质的模式。正交编码器输出两路相位相差90°的方波信号A和B。定时器的两个输入引脚分别接收A和B硬件通过判断A、B的相位关系来自动决定CNT是往上加还是往下减。A相 ┌──┐ ┌──┐ ┌──┐ │ │ │ │ │ │ └──┘ └──┘ └──┘ B相 ┌──┐ ┌──┐ │ │ │ │ └──┘ └──┘如果A相超前B相90°CNT加1B相超前A相90°CNT减1。电机正转、反转都能从同一个计数器里读出方向来。做小车、机械臂、旋钮编码器模式都是标配。我没有把编码器模式当“复杂模式”对待因为它背后的原理仍然只有一个计数。只是计数方向和外部输入状态绑定而已。3. 进阶玩法单脉冲、同步启动、同定时器收发PWM掌握了上面四种基本模式大多数定时器应用场景都能覆盖了。但实际项目中还有几个进阶需求经常出现比如“只输出一个脉冲”“多个定时器同时启动”“一个定时器又收又发”等。这些需求看着难拆开看还是那四个模块在配合。3.1 单脉冲模式只发一个脉冲不多不少单脉冲模式One-Pulse Mode名字很直观CNT被触发后从0数到ARR输出一个完整的可设置宽度的脉冲然后停下来等着下一次触发。这种模式在哪些地方用我做过的项目里有几个典型场景给ADC提供一个外部触发采样脉冲给可控硅或MOSFET一个精确宽度的驱动信号给对讲机模块一个PTT控制脉冲。关键是它不需要CPU持续干预触发一次就自动完成完成之后又不自动重装载通常关闭自动重装载非常省心。配置上和普通PWM的差异只有一个核心点关闭自动重装载让定时器数到ARR就停下。触发源可以是软件触发也可以是外部引脚或其它定时器的触发事件。3.2 多个定时器同步启动主从模式内部是怎么约定的做电机控制或者多路同步采样时经常需要几个定时器在同一时刻开始工作。如果代码里分别调用启动函数因为每一条指令都有执行时间必然存在微小的时间差。要求不高时无所谓但做高频PWM或者精确相位控制时这点时间差就会体现在波形错位上。硬件层面的标准做法是主从模式选一个定时器当“主”其余定时器当“从”。主定时器产生一个触发事件比如更新事件通过芯片内部特定的触发线路STM32里叫ITRx内部触发去同时启动从定时器。这个过程完全由硬件完成没有任何软件延迟。从软件角度来说就两步把从定时器配置成“外部时钟模式1”输入源选对应的ITR然后只启动主定时器。主定时器一运行从定时器通过硬件线路就被拉起来了。用逻辑分析仪看多个PWM通道的第一个上升沿严格对齐这就是硬同步的优势。3.3 一个定时器能不能同时接收PWM和输出PWM这个问题我经常在论坛里看到答案是可以但有条件。以STM32的通用定时器为例一个TIM有4个独立的捕获/比较通道CH1~CH4每个通道可以独立配置成输入或者输出。也就是说可以让CH1做输入捕获去读外部PWM的频率同时让CH2、CH3输出你计算好的PWM波形。但这里有一个前提同一个定时器的所有通道共享同一个CNT、ARR和PSC。这意味着它们的时间基准是一致的。如果你需要输入捕获测的波形频率范围很宽而你输出的PWM频率又要求很精确两者可能互相牵制。这时候要么换用不同定时器要么接受一定的限制。说到底定时器资源也是一种“多路复用”的关系通道独立但底座是共享的。同理GD32的定时器也遵循这个规律只是寄存器命名上和STM32稍有差别库函数接口更多带RCU前缀。换芯片时最怕一个函数一个函数地对照先把这张资源关系图画对能省一半的时间。4. 从波形图出发的排错链路定时器不工作怎么查定时器出问题有一个特点现象往往一样原因五花八门。中断不进来可能是时钟没开、可能是ARR算错、可能是NVIC没配、可能是标志位没清但表现出来的现象都是“程序好像卡死了”。这种问题靠猜效率极低靠波形图排查才靠谱。4.1 定时器溢出中断不触发的三个高频原因先说排查思路中断不触发本质是“更新事件没有产生”或者“更新事件产生了但CPU不知道”这两个方向把问题一刀切成两半。第一个是时钟树没配好。STM32的TIM2~TIM7挂在APB1上TIM1和TIM8挂在APB2上。很多人知道APB1的频率是系统时钟的一半比如72MHz主频对应36MHz但忽略了一个关键规则当APB1的预分频系数大于1时定时器时钟是APB1频率的2倍。也就是说在主频72MHz、APB1分频2的情况下挂在APB1上的定时器时钟反而是72MHz。这个“反直觉”的规则让无数人在计算定时周期时少乘了2。我调试时第一件事就是打开时钟树配置界面把每个分支的频率数值亲眼确认一遍不要靠背。第二个是中断线没使能。定时器产生更新事件后需要把“更新中断”在定时器内部使能然后在NVIC嵌套向量中断控制器里使能对应外设的中断通道同时设好优先级。这两个使能缺一不可。用HAL库写代码时很多人只调了HAL_TIM_Base_Start_IT却忘了在初始化里把NVIC的使能打开中断自然进不来。第三个是中断标志没清干净。更新中断进了中断函数后要用函数主动清除更新标志位。有些芯片需要先读状态寄存器再清标志顺序反了也可能导致清不掉。如果标志一直置位中断会不停地触发看起来像是“中断风暴”程序其它逻辑全被打乱。这三个原因内部没有先后顺序完全是并列关系。所以排查的时候不要只盯一个就用一条链路逐个确认时钟树频率读数 → 定时器初始化返回值 → 中断使能寄存器→ 标志位状态。4.2 PWM占空比到不了100%边界值处理的学问我搜到“stm32f103芯片定时器输出占空比到不了100%”这个热词时一点都不意外因为这是几乎所有做PWM的人都会撞上的问题包括我。先说现象CCR配置到和ARR相等想着输出100%占空比也就是一直保持高电平有效结果示波器上一看波形还是有时钟跳变或者说输出的完全是低电平跟预期完全相反。问题出在PWM比较的边界处理上。以PWM1模式为例有效电平为高CNT小于CCR时输出高电平。当CCR ARR 1时CNT永远小于CCR所以输出恒定高电平这才是100%占空比。而CCR ARR时CNT从0数到ARR的过程中在最后一拍CNT等于ARR此时CNT不小于CCR输出会翻转为低电平然后立刻更新事件让CNT归零于是每一周期末尾都出现一个极窄的低电平毛刺。示波器上看起来像是100%实际不是如果接了功率器件这个毛刺可能带来噪声甚至误动作。解决办法很简单想要100%输出就把CCR设成ARR1。反过来想要0%占空比把CCR设成0。这也解释了为什么我说“占空比CCR/(ARR1)”而不是“CCR/ARR”因为边界值就是这么定义的。4.3 捕获测频率不准从波形图上的毛刺说起输入捕获测频率时数据不稳定有几个常见原因。最容易被忽视的是引脚上的毛刺。外部信号在上升沿或者下降沿附近可能因为反射、串扰产生多次跳变定时器可能把一次跳变当成多次捕获测出来的周期就会异常偏小。解决办法有两个层面的硬件层面在被测信号引脚对地加一个小电容如100pF~1nF或者在输入线上串联一个几百欧的电阻软件层面很多芯片的输入捕获自带数字滤波器配置一个采样频率和采样次数让硬件对输入的跳变做几次确认后再触发捕获。STM32的捕获滤波器配置和采样频率相关配得太严会滤掉合法信号配得太松等于没有需要根据信号的实际频率去算。另外还有一个更容易忽略的问题定时器的时钟频率必须远高于被测信号频率。根据采样定理的工程经验被测信号频率至少要比定时器时钟频率低几十倍以上测量结果才有意义。如果被测信号是10MHz定时器时钟也是10MHz每个计数节拍对应一个信号周期一次捕获能差出好几个周期来。4.4 CubeMX里Timebase Source选错引发的“隐形故障”用STM32CubeMX配置工程时有一个很多人默认忽略的选项Timebase Source。默认值是SysTick滴答定时器。SysTick是Cortex-M内核自带的定时器HAL库的延时函数HAL_Delay就靠它实现。问题来了如果你在工程里把SysTick用于其它用途比如操作系统的系统节拍、高精度延时等再去调用HAL_Delay整个系统就乱套了最典型的现象是程序莫名其妙卡在延时函数里出不来。这个时候排查方向可能完全跑偏追半天电源、晶振最后发现是定时器源选错了。CubeMX允许把Timebase Source改成TIM6或TIM7基本定时器资源独立。此时HAL_Delay不再占用SysTick可以把SysTick省出来干别的事。但这个选择的代价是HAL_Delay的精度会依赖你配置的定时器中断优先级如果被更高优先级的中断频繁打断延时就不准了。我在项目里的做法是需要操作系统的场景Timebase Source直接用TIM6简单裸机程序保持SysTick不动如果涉及高精度延时的场合干脆不用HAL_Delay自己写基于定时器CNT的查询延时函数不依赖中断。5. 主流定时器家族的差异与移植要点不同芯片的定时器寄存器名字可能完全不同但其内部骨架是高度同源的。把某个平台的定时器彻底弄懂之后换平台只是“翻译”的问题而不是重新学一遍的问题。5.1 51单片机定时器8位/16位计数器的原始形态51单片机的定时器是学习定时器原理的最佳起点因为它足够简单TL0和TH0两个寄存器拼成一个16位计数器有一个预分频选项12T或6T模式溢出时置位TF0标志。没有那么多通道没有比较输出就是一个纯粹的计数器加一个溢出标志。51的定时器工作模式有四种方式0~方式3方式1是16位定时方式2是8位自动重装。方式2在实际中用得很多因为它省掉了在中断里重新赋初值的麻烦定时器溢出后自动从TH0重新开始计数。这个“自动重装”思想在STM32的ARR里得到延续只不过STM32把ARR做成16位的完整寄存器而51的方式2只有8位。设置初值的方式也体现出了“计数到溢出”的本质如果要定时1ms定时器从初值N开始往上计计到65535溢出所以N 65536 - 需要计数的个数。这跟STM32“从0计到ARR”的方向正好相反但本质完全一样——都是计数器数到固定次数产生事件。这个对比恰好说明理解原理比背寄存器更耐用寄存器只是不同芯片对“计数到固定次数”的不同表达方式。5.2 STM32三个等级的定时器基本、通用、高级STM32把定时器分成三个等级这个划分本身就是一个“按功能由简到繁”的学习路径。基本定时器TIM6/TIM7功能最少只有定时功能没有输入输出引脚不能捕获不能输出PWM。它就像一个纯粹的闹钟适合做延时基准、任务调度时基、DMA触发的时钟源。通用定时器TIM2~TIM5在基本定时器上加了输入捕获、输出比较、PWM生成、编码器接口等功能。绝大多数PWM调速、频率测量都用这一档。高级定时器TIM1/TIM8在通用定时器基础上又加了互补输出、死区插入、刹车输入等功能这些东西是专门为电机控制设计的目的是防止上下桥臂同时导通烧毁功率管。所以拿到一个项目先想清楚“需要定时器做什么”只是定时TIM6够了输出PWM调速TIM2够用做三相电机FOC控制TIM1或TIM8是必须的。功能越强配置越复杂不要大材小用。5.3 GD32的定时器与STM32的兼容性差异GD32在寄存器和库函数层面上跟STM32神似但有一个高频出现的关键差异GD32的定时器时钟配置带RCU前缀且分频系数初始化和STM32不完全一样。比如热词里提到的rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2)就是把定时器时钟设为2倍。这个配置如果漏了定时频率就会差一倍和STM32的APB1预分频陷阱如出一辙。另外GD32的定时器在某些型号上工作频率上限比STM32高能跑更高的定时器时钟。这意味着同样的PSC和ARR设置GD32实际产生的PWM频率可能跟STM32不同移植代码时最好用示波器实测确认频率不要只凭“兼容”两个字就认为参数可以原封不动。5.4 555定时器硬件定时器的三种模式与数字定时器的对照555定时器是纯模拟电路实现的“定时器”它不需要写代码用电阻电容的组合就能产生方波或者单脉冲。它的三种工作模式——无稳态、单稳态、双稳态恰好可以和数字定时器的模式做一个有趣的对照。无稳态模式555不断输出方波周期由两个电阻和一个电容决定相当于数字定时器里的PWM输出。单稳态模式外部触发一下输出一个宽度由RC决定的高电平脉冲相当于单脉冲模式。双稳态模式两个触发脚分别置位和复位输出相当于用定时器做一个RS触发器。虽然现在做产品很少用555但调硬件电路时偶尔会遇到用555做振荡源的供电方案能看懂它的RC计算还是有用的。555输出的频率公式里同样有类似“1”的边界细节充放电时间常数不一样输出占空比通常不等于50%除非特意的二极管路径设计。这种硬件定时器的不对称性也是数字定时器普及的原因之一——数字定时器的PWM占空比可以做到任意精度而且不随温度漂移。6. 最后分享几个我自己图说定时器的调试习惯文章写到这里主体内容已经展开得比较充分了。最后分享几个我在实际调试中沉淀下来的个人习惯算不上教程但确实帮我省了不少时间。第一调定时器前先画图再写代码。哪怕是再简单的PWM输出我也先在纸上画出目标波形标出周期和高电平时间然后反推ARR和CCR。这个习惯让我几乎没有因为“配置值与期望不符”而反复烧录调试的经历。如果连波形都画不清楚写出来的代码大概率也是碰运气。第二示波器和逻辑分析仪是你最好的老师。程序跑不通的时候别急着怀疑编译器先把定时器引脚的波形抓出来看看。如果波形不对对照波形图去查配置如果波形根本没有就从时钟树开始查。用波形说话比用猜的可靠十倍。第三遇到定时器频率翻倍或者减半的问题优先查时钟树。这类问题八成是APB分频导致的定时器时钟倍频规则没算对。具体数值以芯片参考手册的时钟树章节为准不要拿网上别人的示例参数直接套因为主频和分频配置可能完全不一样。第四调试定时器中断时打一个IO口翻转的探针。在中断服务函数进出时翻转一个空闲GPIO用示波器量这个GPIO的频率就能知道中断的实际触发频率比在中断里计数再打印靠谱得多。这个做法本质上还是在“画图”——把定时器的行为变成波形图来观察。第五从简单模式起步。先让定时器以最基础的定时中断模式跑通再逐步叠加PWM、捕获、编码器等功能。每叠加一步用波形验证一步。所有高级模式都是基础模式的排列组合基础不稳越往上越容易崩。定时器的学习曲线其实不算陡只要脑子里始终挂着那张“时钟源-预分频器-计数器-比较器”的结构图再复杂的模式都能拆成简单的几个动作。也希望这篇文章能帮你把这张图刻在脑子里。
返回列表