
嵌入式开发里定时器可能是用得最多、但又最容易被“差不多就行”心态糊弄过去的外设。跑个流水灯、翻转个电平感觉怎么配都行一旦涉及PWM呼吸灯频率、输入捕获测脉宽、多定时器同步启动这类稍微精确点的需求就开始靠试错凑参数。我早年也被定时器配置折磨过后来花时间把计数器的底层流转过程彻底捋了一遍再回头看CubeMX自动生成的代码突然就通透了很多。这篇就用尽量直白的方式把“图说定时器原理、工作模式”这个主题拆开揉碎讲清楚。我自己整理了三条主线定时器到底在“数”什么工作模式分别对应硬件的哪些开关组合以及实际配寄存器或者用HAL库时每一步操作背后对应的硬件动作是什么。这三条线看明白了51单片机的定时器、STM32的通用定时器、甚至555这种纯硬件定时芯片对你来说都只是同一套逻辑的不同封装。1. 从最朴素的“数周期”说起定时器到底在数什么很多教程一上来就扔出预分频器PSC、自动重装载寄存器ARR、计数寄存器CNT这一堆名词初学者很容易被绕晕。我的经验是先忘掉所有寄存器名字只记住一句话——定时器的本质就是一个不停加一或减一的计数器而这个计数器数的是“时钟源的脉冲个数”。内部时钟源通常是芯片的主时钟经过总线分频之后送进来的比如STM32F103的APB1定时器时钟典型值72MHz。这个72MHz意味着每秒钟会有72000000个脉冲到达定时器如果一个脉冲计一次数那么计数器的变化频率就是72MHz。但如果直接这样数计数值到6553516位计数器上限只需要大约0.91毫秒做毫秒级以上的延时就得用软件循环补太折腾了。这时候预分频器就派上用场。它干的事情很简单把进来的脉冲按比例“丢弃”比如PSC设为71就是每72个输入脉冲才放行1个脉冲给计数器数。这样一来计数器收到的脉冲频率就变成了72MHz÷721MHz每两个脉冲之间的时间间隔就是1微秒。这几乎是所有定时器延时方案的统一套路先用预分频把计数频率压到合适的尺度再用计数值控制具体的时间长度。用公式表达就是定时时间 (PSC1) × (ARR1) ÷ 定时器时钟频率。这个公式里的PSC和ARR都要加一是因为这两个寄存器都是从0开始计数的。PSC71意味着72分频ARR999意味着计数值到达999后触发事件加上0这个起始值一共数了1000个脉冲。在这样的配置下72MHz ÷ 72 1MHz数1000下正好是1毫秒。这个换算关系我建议刻进脑子里因为之后不管是调PWM频率、算捕获的分辨率还是做多定时器级联最后都要落到这个公式上。理解了“数脉冲”之后还差最后一个关键角色自动重装载寄存器。它的作用是给计数器设定一个“天花板”或者“地板”——向上计数时CNT数到ARR的值就归零重新开始向下计数时CNT数到0就重新装载ARR的值。这个机制不只是为了延时更是PWM周期、输入捕获窗口、触发中断节拍的基础。如果你手边有逻辑分析仪建议做个实验把某个定时器的通道配置成PWM输出然后分别修改PSC和ARR的值观察波形频率的变化。你会发现改ARR对频率的影响是线性的改PSC的影响是按倍率跳变的。这个实验做一次比背诵十遍公式都管用。2. 工作模式的本质计数方向、时钟源、输出通道的开关组合“定时器工作模式”这个说法听起来很抽象但如果把它拆成三个维度来看就清晰多了计数方向、时钟源选谁、比较捕获通道干什么用。工作模式就是这三个维度的组合。2.1 计数方向决定波形骨架向上、向下、中心对齐向上计数模式是最常见的CNT从0开始每来一个脉冲加一到ARR后归零同时可以触发更新事件UEV。这个模式适合做周期固定的定时中断和普通PWM。向下计数模式几乎就是向上计数的镜像CNT从ARR开始往下减减到0后重新装载ARR。它和向上计数在大部分应用里没有本质区别但在某些特殊场景下有用比如要产生一个不对称的波形或者配合另一个定时器做精确的相对相位控制。中心对齐模式就有意思多了。CNT先向上数到ARR再向下减到0如此反复输出的波形是三角形的。如果把比较通道配成PWM输出中心对齐模式产生的是对称的PWM波形也就是高电平的中间点正好对准整个周期的中间点。这一点在电机控制领域特别重要因为对称PWM有助于减小电流纹波和转矩脉动。我早期做无刷电机驱动时就是靠中心对齐模式配合互补输出才把死区时间和换相噪声压下去的。2.2 时钟源怎么选内部时钟、外部时钟、编码器模式时钟源决定了计数器“数”的是谁发来的脉冲。内部时钟Internal Clock就是前面说的PSC分频后的芯片主时钟上电默认状态适合所有常规延时和PWM应用。外部时钟模式1External Clock Mode 1用定时器的外部引脚比如TIMx_ETR输入的脉冲边沿来驱动计数器。这时候定时器变成了一个硬件计数器数的是外部信号的脉冲数可以用来做频率计、流量计这类脉冲计数应用。注意这里的“边沿”可以是上升沿也可以是下降沿由寄存器里的极性位控制。外部时钟模式2External Clock Mode 2和模式1的区别在于信号路径模式2直接复用触发输入引脚但逻辑上本质一样。还有一个经常被忽略的应用场景编码器模式。这时候定时器同时采样两路正交编码信号A相和B相根据两路信号的相位关系判断旋转方向CNT的值就是编码器的位置计数。多路定时器配合编码器接口能在不占用CPU的情况下实时追踪电机转速和位置。这就是为什么很多电机控制板上的定时器资源那么紧张——每个编码器都要独享一个定时器。2.3 输出通道是“输出比较”的舞台PWM、单脉冲、翻转输出定时器真正发挥威力是在它的多个比较捕获通道上。以STM32的通用定时器为例通常有4个通道CH1~CH4每个通道都可以独立配置成输出比较模式。输出比较的基本原理是计数器CNT的值和预先设定的比较寄存器CCR的值不停做比较一旦两者相等就在输出引脚上产生一个动作——拉高、拉低、翻转或者触发一个中断/DMA请求。PWM模式就是输出比较的典型应用。ARR决定周期CCR决定占空比。比较结果决定当前时刻是输出高还是输出低。到了更新事件CNT归零又会按新的CCR值重新比较。所以调PWM的波形本质上就是调ARR和CCR。很多初学者以为改占空比是在“控制输出电压”其实数字电路里根本没有模拟电压PWM输出只是让引脚在一段时间内高电平时间比例变化经过外部RC滤波或者负载的机械惯性才表现为“等效电压”。单脉冲模式One-Pulse Mode则是只输出一个指定宽度的脉冲常用于触发信号、超声波测距的激励信号等场景。它的配置要注意要先确定哪个边沿作为触发点、比较值是多少、触发后计数器是立即停止还是继续跑。很多入门资料对这一块一笔带过但实际上时序很容易出错我当年做超声波测距时就是因为没弄明白单脉冲模式的触发边沿和比较值的时序关系波形怎么都对不上。翻转输出模式最简单比较匹配时直接把引脚电平翻转。它可以用来产生频率精确的方波但占空比固定是50%。如果只是需要一个节拍信号而不是PWM调光翻转模式其实最省事。2.4 输入捕获从“数脉冲”到“量时间”的逆操作输入捕获和输出比较正好反过来——输出是比较引脚的高电平和CNT的匹配输入捕获是外部信号边沿到达时“咔嚓”一下把当前CNT的值锁存到捕获寄存器里。这样一来就能测量两个事件之间的时间间隔。经典应用是测量输入信号的频率和脉宽。测量频率的方法把输入信号接到某个通道上配置上升沿捕获每次捕获都触发中断在中断里读取捕获寄存器的新值连续两次捕获值的差值乘以单个计数的时长就是输入信号的一个周期。测量脉宽的方法也类似同一个通道第一次配置成上升沿捕获第二次配置成下降沿捕获或者两个不同通道分别捕获上升沿和下降沿两次捕获的时间差就是高电平的持续时间。配合PWM输出就能做一个简单的“发送方-接收方”姿态测量应用一方输出不同脉宽的PWM另一方用输入捕获解析出脉宽数值。这里有一个常见的坑输入捕获的误差取决于捕获寄存器的更新时刻和中断响应延迟。如果中断里还有其他耗时的操作捕获值会被推迟读取导致测量结果偏大。标准做法是DMA方式搬运捕获值或者至少保证中断里只做“读寄存器记录”这两个动作把复杂的处理逻辑挪到主循环里。2.5 高级定时器的“隐藏技能”死区、刹车、重复计数以STM32的TIM1和TIM8为代表的高级定时器除了基础功能之外还多了几个面向工业控制的功能。其中死区发生器Dead-Time Generator是我用得最多的一个。电机驱动的H桥上下桥臂绝对不能同时导通否则就是直通短路所以要在PWM互补输出之间插入一段“死区时间”——下桥臂先关断延时一小段时间后上桥臂再开通。死区发生器就是硬件自动干这个事的你只需要在配置里填一个死区时间值任何情况下上下桥臂的PWM都不会短接。刹车输入Break Input相当于紧急停机信号。一旦刹车引脚收到有效电平PWM输出立刻进入预设的安全状态通常全部拉低并且由硬件自动执行不依赖软件响应时间。这个在伺服驱动、电动工具里属于必须功能。重复计数寄存器Repetition Counter则控制每多少次更新事件才产生一次中断或DMA请求。为什么需要它?因为默认每次CNT归零都触发更新事件如果PWM频率很高比如20kHzCPU会被中断淹没。有了重复计数寄存器可以让它跑满10次、100次才触发一次中断在高频PWM控制低速任务时非常有用。3. 三种主流定时器横向对比51定时器、STM32定时器、555定时器既然标题涉及“定时器”这个大的主题我觉得有必要把这三种最常见的定时器放在一起对比。它们的设计思路完全不同但核心逻辑都能统一到“计数脉冲”这一模型上。51单片机的定时器是最朴素的代表。以8051为例内部有定时器0和定时器1典型是16位结构可拆成两个8位使用。它的工作模式有13位定时、16位定时、8位自动重装还有波特率发生器模式。配置全靠寄存器TMOD和TCON用汇编或者C直接写位操作。它的运行逻辑简单直接计数器溢出时置位TF标志如果开了中断就跳转到中断服务程序。STM32的定时器是另一个极端。它把计数器、比较捕获、DMA、编码器接口、死区、刹车全部集成到一个外设里但底层还是那个“CNT数脉冲ARR比较CCR比较”的框架。学习曲线比51陡但换来的是极大的灵活度。555定时器则是纯硬件定时没有软件参与。它靠比较器、RS触发器、放电晶体管和外部RC电路实现延时或者振荡。NE555的经典应用是产生方波多谐振荡器、单脉冲延时再到单稳态触发。它的“定时原理”其实就是电容通过电阻充电电压到达阈值后比较器翻转原理上和MCU定时器完全不同但在“用时间控制动作”这个层面它们的目标又完全一致。为了更直观对比我列一个表格对比维度51单片机定时器STM32通用定时器555定时器核心结构16位加法计数器16位/32位计数预分频比较捕获比较器RS触发器RC电路定时基准机器周期内部时钟或外部时钟外部RC充电时间可编程性低寄存器少高多种模式中断DMA无靠外部R/C调参数特色功能串口波特率生成PWM、输入捕获、编码器、死区、刹车纯硬件方波产生适合场景入门教学、简单延时、波特率电机控制、电源、传感器采集硬件定时、信号产生、简易控制不管用哪一种设计时都要先问自己三个问题时钟源从哪来我要数多少个脉冲才能达到目的动作发生的时刻怎么表达这三个问题回答清楚了换任何一款芯片都不会慌。4. 配置实例CubeMXHAL库里的1毫秒定时中断理论的归理论最终还是要落到代码上。下面用一个最基础的实例把前面讲的原理完整串一遍用STM32的TIM3产生1毫秒定时中断让LED每500毫秒翻转一次。CubeMX里的配置步骤是在Pinout视图中找到TIM3勾选Internal Clock。将PrescalerPSC设为71因为定时器时钟是72MHz72分频后变成1MHz。将Counter PeriodARR设为999这样计数值从0到999正好1000个脉冲即1毫秒。在NVIC Settings中勾选TIM3 global interrupt启用定时器更新中断。生成代码。生成完代码后HAL库会自动调用HAL_TIM_Base_Init函数完成底层寄存器配置。在main函数里我们需要手动启动定时器并开启中断int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); // 启动TIM3并开启更新中断 HAL_TIM_Base_Start_IT(htim3); while (1) { // 主循环不做任何事LED翻转放在中断回调里 } }在定时器更新中断里HAL库会调用一个弱定义的回调函数我们需要在用户代码里重写void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t tick 0; if (htim-Instance TIM3) { tick; if (tick 500) // 500毫秒 { tick 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } }这里面最需要注意的一点是HAL_TIM_PeriodElapsedCallback是一个全局回调函数。你的工程里可能有多个定时器、多个外设都会触发这个回调所以函数内部必须用htim-Instance判断是哪个定时器实例。我曾经见过有人同时开着TIM2和TIM3却只判断了htim-Instance TIM2导致TIM3的回调事件被静默丢弃查了整整一个下午才发现问题。还有一个细节HAL_TIM_Base_Start_IT只管启动定时器并启用中断不会影响PWM输出功能。如果你的定时器既要产生PWM又要输出中断那你得同时调用HAL_TIM_PWM_Start不带_IT和HAL_TIM_Base_Start_IT两者的中断开关是分开管理的。类似地HAL_TIM_IC_Start_IT是启动输入捕获并开中断HAL_TIM_PWM_Start_IT则是在PWM模式下同时开中断。这种命名习惯很容易踩坑建议自己写一个启动函数把需要用到的通道全部集中管理。5. 定时时间的现场计算如何把延时和频率都换算到寄存器值配置定时器时最忌讳“凭感觉填数字”就是大概填一个值跑起来不对再改。正确做法是先把需求换算成PSC和ARR的值写进代码之前就知道结果必须是这个数。核心公式是定时器输出频率 定时器时钟频率 ÷ (PSC1) ÷ (ARR1)。PWM输出频率也是它。假设定时器时钟是84MHz比如STM32F407的APB1想要50Hz的PWM频率也就是20毫秒周期用来驱动舵机。如果PSC取83那计数频率是84MHz÷841MHz即每计数一次耗时1微秒。要想周期20毫秒ARR就必须是20000-119999。另外一个非常容易混淆的点ARR的值决定了PWM的“分辨率”。比如同样是20毫秒周期如果你的ARR只有999那么占空比的调节步进是20毫秒÷100020微秒也就是0.1%的精度。如果ARR提高到19999分辨率就是0.005%。占空比归一化到0~100相当于多了一位十进制精度。所以要求“细腻”的占空比调节时ARR应该尽量大而PSC尽量小这样计数频率更高、粒度更细。反过来如果是高频PWM比如20kHz那么周期是50微秒。如果时钟是72MHz不分频时PSC0ARR3599因为72000000÷200003600。注意此时占空比的调节步进是1÷72000000≈13.9纳秒精度非常高可以做出极平滑的LED调光效果。但如果时钟源不是稳定的晶振这个高频PWM的抖动也会更明显所以设计时还得考虑时钟源的质量。这些换算的要点是保持“先算后配”的习惯。我在做嵌入式调试时经常用一张随手写的速查表PSC71,ARR999产生的1毫秒中断PSC83,ARR19999产生的50Hz舵机控制PSC0,ARR3599产生的20kHz开关电源驱动。有了这些基准值再往旁边推演就很快了。6. 我把定时器配置玩明白之后才敢回答这三个坑网上关于定时器的热词里有好几条都是实践中的高频问题。我把它们集中在一起每个都给出我的结论和解法希望能帮你少走点弯路。6.1 占空比到不了100%的坑HAL库的PWM配置占空比 CCR ÷ (ARR1)。很多人把CCR配到ARR以为这就是100%占空比结果用示波器一看高电平宽度差了一个计数周期。原因在于PWM的极性设置。默认情况下PWM模式1是计数CNT小于CCR时输出高电平。如果CNT的范围是0到ARR那么CCRARR时几乎所有时刻CNT都比CCR小确实接近100%。但还有一些隐藏的边界比如中心对齐模式下PWM的判定会有细微差别再就是ARR和CCR的写操作有时序要求在更新事件之前直接写CCR可能被缓存在预装载寄存器里等到下一周期才生效。要可靠地把占空比打到100%标准的做法有两种一是直接把CCR设置成一个很大的值超过ARR让比较匹配永远不发生二是切换到PWM模式2调整极性和电平定义。我在写电机控制、LED调光时都会用到这个技巧。实测下来直接在更新事件中断里把CCR改成ARR1波形立即变成满高没有任何毛刺。6.2 一个定时器能不能同时收PWM和输出PWM答案是能但要注意通道分配和中断/捕获资源冲突。STM32通用定时器通常有4个通道每个通道都是独立的比较捕获单元。比如TIM4_CH1可以做输入捕获测外部信号脉宽而TIM4_CH2、CH3可以同时输出PWM。它们互不干扰因为它们各自使用自己的CCR寄存器和输入输出引脚。唯一共享的是CNT计数器和ARR周期也就是说同一时间内所有输出通道的PWM周期必须相同占空比可以不同输入通道的分辨率也受这个周期限制。实际项目中我就这么干过TIM4的CH1用来捕获一个红外遥控器传来的脉宽信号CH2输出一个和捕获信号同频率的PWM给外部驱动板。得益于定时器的共享周期机制捕获到的周期值几乎同步地反映在输出PWM上不需要软件同步延迟只有几个微秒。如果你需要两个通道输出不同周期的PWM那只能用两个定时器别硬挤一个外设。6.3 三个定时器如何同步启动这个问题的标准答案是用定时器的从模式(Slave Mode)和触发输出(Trigger Output)机制。在STM32的定时器矩阵里某些定时器可以产生触发信号其他定时器可以配置成“接收触发信号后启动”。例如TIM1的更新事件可以作为触发信号发给TIM2、TIM3、TIM4。配置要点是用TIM_SelectOutputTrigger把主定时器的更新事件映射到触发输出然后用TIM_SelectSlaveMode把从定时器配置为外部时钟模式1并选择触发源为对应的TRGI信号。这样主定时器一旦启动从定时器会在下一个时钟沿同步启动误差不超过一个时钟周期。这个功能在做多路电机同步驱动、多通道数据采集时特别有用。我在四路电机同步项目中就是这么做的TIM1做主控TIM2~TIM4各带一路电机的PWM四路PWM的起始相位完全一致启动时的电流冲击也小了不少。如果只用软件顺序启动四路之间会差几十微秒在高速运转时表现为明显的扭动和噪声。我对定时器的理解也是从“能跑就行”到“精确控制”一步步走过来的。每当想偷懒靠运气配寄存器时就会想起那个被抖动的PWM波形折磨的下午。定时器这玩意儿真值得把原理吃透后面所有外设的配置都顺手很多。