
1. 定时器的本质一套能自己数数的硬件计时器很多朋友搜定时器原理的时候其实已经被各种芯片文档绕晕了。我刚开始接触这个领域时也踩过同样的坑看数据手册里一堆英文缩写什么PSC、ARR、CNT每个字母都认识合在一起完全不知道在说什么。后来想明白一件事就通了定时器本质上就是一个会数数的硬件模块你给它一个节奏它自己在那儿数数到一定数量就通知你。这里面最关键的三个寄存器我建议你当成三兄弟来记CNT计数器只管闷头数数每次来一个时钟脉冲就加1。它就像你跑步时手里的计圈器每跑一圈就按一下。PSC预分频器它负责把原始的时钟频率降速。比如芯片主频是72MHz如果不降速计数器每秒钟要数7200万次数得特别快定时的时间粒度就很细但想定个1秒的时长计数器得数到7200万这个数字太大了寄存器装不下。加了预分频器相当于把时钟分频一下比如8分频那计数器每秒只需要数900万次了。ARR自动重装载寄存器设定数到多少就停下并通知你。这就像闹钟定在6点响ARR就是那个6。所以一套最简单的定时流程是这样的时钟脉冲先经过PSC降速变成计数器能接受的速度然后CNT从0开始往上加每加一次就和ARR比较一次。当CNT数到ARR设定的值时定时器就产生一个更新事件同时CNT自动归零重新开始下一轮计数。然后这个归零通知的动作就产生了我们常说的一包定时器中断。实际计算定时时间的公式也特别简单定时时间 PSC 1×ARR 1÷ 时钟频率。注意这里为什么要加1因为PSC和ARR都是从0开始计数的如果预分频值设为7实际是8分频自动重装载值设为99实际是数到100才触发。这个差1的问题是我见过新手最容易出错的地方几乎每周都有朋友在社区里问为什么我定时器中断时间比预期快了一倍一查全是忘加1。再打个比方你就能彻底理解了假设你在停车场出口收费有个摄像头每隔1秒拍一张照片记录车辆进出的时间你想在每辆车停满10分钟时自动抬杆。那1秒拍照就相当于时钟节拍10分钟就是ARR设定的值。处理器不需要自己盯着时间摄像头每拍一张照计数器就加1数到600拍也就是10分钟杆子自动抬起。定时器帮处理器把时间这种软事情变成了数数这种硬事情。2. 从GPIO和系统时钟出发理解定时器的三个时钟来源定时器要跑起来首先得有节拍来源。这部分大家搜定时器原理时容易忽略但恰恰是配置错误的根源。定时器的时钟不只有内部时钟一种不同场景下选错时钟源整个定时就全乱了。第一种内部时钟。这种最常用芯片上电后内部时钟电路自动产生稳定的方波定时器直接拿来用。比如STM32系列中APB1和APB2总线分别给不同的定时器组提供时钟配置时需要先确认挂在哪条总线上不然预分频算出来的时间会差一倍。很多人配置GD32或STM32时发现定时时间偏大或偏小十有八九是总线时钟树没看清。第二种外部时钟模式1。这种模式下定时器的时钟不是内部产生的而是从外部引脚输入的脉冲信号。每当外部引脚来一个跳变计数器就加1。这个模式适合做什么测外部脉冲个数或者做简易计数器。比如流水线上检测到10个零件通过就产生一个信号这种需求用外部时钟模式太合适了。第三种外部时钟模式2。和模式1类似但触发方式有差异通常用于需要外部信号控制定时器启动和停止的场景。还有一种比较特殊的内部触发时钟一般用于高级定时器的级联。比如把定时器1的更新事件作为定时器2的时钟两个定时器串联起来就可以做出一个超长时间的延时功能这个在低频方波输出或者长时间定时任务中很实用。时钟源定下来之后还有一个隐藏的知识点容易被忽略时钟极性。有些外部信号是上升沿触发有些是下降沿触发配置反了计数器逻辑就完全对不上。做实际项目时我记得有一次调试一个编码器计数不准的问题一开始怀疑是硬件干扰后来一查是外部输入信号的极性配置不对半个小时的排查时间全花在这里了。3. 定时器六大工作模式的拆解与选型对照定时器能干什么很大程度上取决于你把它配置成哪种工作模式。这部分是全网搜索定时器工作模式的核心需求我按自己在项目中的使用频率排个序每个模式都给出典型的应用场景。3.1 定时中断模式基础中的基础。配置好PSC和ARR打开更新中断到了时间就进中断服务函数。这种模式最常见的应用是做一个操作系统的心跳节拍或者LED闪烁的底层驱动。需要提醒的是进中断之后要立刻处理业务逻辑越短越好千万别在里面做延时或者跑那种几百毫秒的任务否则主循环直接被拖死整个系统看起来就像卡顿了。3.2 PWM输出模式热度词里有人搜51定时器PWM也有人搜Mega2560定时器PWM足见这个模式的使用率有多高。PWM的本质是让引脚在高低电平之间快速切换并通过调整高低电平的时间比例来改变等效输出电压。这里有几个概念必须搞清楚频率每秒钟高低电平切换的次数。舵机的控制频率一般50Hz而LED呼吸灯几百Hz到几kHz都可以。占空比高电平占整个周期时间的百分比。从0%到100%连续调整输出平均电压就连续变化。死区高级定时器在互补PWM输出中用到的概念指的是上下桥臂切换时插入一个短暂的空闲时间防止上下同时导通烧毁器件。PWM模式配置时的核心寄存器是CCR捕获/比较寄存器。当计数器CNT的数值小于CCR时输出高电平大于CCR时输出低电平。通过循环改变CCR的值就能实现呼吸灯效果。3.3 输入捕获模式这个模式是定时器的反向用法。之前是定时器自己产生节拍现在是外部信号给节拍定时器只负责记录时间点。说白了就是外部信号跳变时定时器把当前CNT的值拍个快照存到CCR寄存器里然后你可以通过连续两次捕获值的差值推算出信号的特征。搜STM32定时器捕获测频率的朋友就是用这个模式的经典场景。测量频率时思路是这样的配置上升沿捕获第一次捕获得到计数器的值T1第二次捕获得到T2中间经过一次完整的信号周期。那这个信号频率就等于定时器时钟频率除以T2 - T1。这里有个容易踩的坑如果被测信号频率很低两个上升沿之间跨越的时间超过了计数器最大计数值中间会发生溢出算出来的数值就不对了。解决办法是打开溢出中断在中断里记录溢出了多少次把溢出次数也参与到计算中。3.4 编码器模式编码器模式是我个人觉得定时器最聪明的工作模式。它不需要软件介入硬件会自动根据编码器A、B两相的相位关系判断旋转方向和转速。A相和B相之间存在90度的相位差正转时A相超前B相90度反转时B相超前A相90度定时器硬件检测到这个相位差异后计数器自动向上计数或者向下计数。这种模式的最大价值是MCU的主循环和中断完全不需要理睬编码器的脉冲只管定时去读计数器的值就行了。在直流电机调速、舵机角度反馈、丝杆位置检测等场景中非常实用。我用GD32F450配置编码器模式时发现一个细节编码器接口模式下计数器的溢出行为跟普通计数模式不太一样需要额外注意溢出的方向不然算出来的绝对位置会跳变。3.5 单脉冲模式单脉冲模式也叫One-Pulse Mode它的使用场景很专一某个外部事件触发后定时器在可编程的延时之后输出一个宽度可编程的脉冲。比如在超声波测距中需要MCU发一个10μs的TRIG脉冲配合单脉冲模式可以让硬件自动完成这个动作不占用软件时间。这个模式用的人相对少但真正用到时代码简洁程度和时序精度都比软件模拟好一个档次。3.6 门控模式与从模式门控模式本质是把外部信号当作定时器的开关。外部信号为高电平时定时器正常计数信号为低电平或跳变沿到来时定时器停止计数。这种模式适合做需要精确测量外部信号脉宽的任务信号高电平持续期间定时器一直在计数信号变低时停止计数这时计数器的值就是脉宽。相比输入捕获模式门控模式测脉宽的代码更少精度也更高。我把这几种模式整理成了一张表格方便你做选型工作模式核心用途典型应用关键配置点定时中断产生定时间隔系统节拍、LED闪烁PSC和ARR、更新中断PWM输出调整电压/控制功率电机调速、呼吸灯、舵机CCR、频率、占空比输入捕获测量外部信号频率计、脉宽测量捕获边沿、溢出处理编码器模式检测转速和方向旋转编码器、电机测速A/B相极性、计数方向单脉冲模式精确延时单脉冲超声波TRIG、时序控制触发源、脉冲宽度门控模式外部信号控制计数脉宽精测、事件计数门控信号极性、启停逻辑4. 从硬件到软件C# UI定时器刷新卡顿与锁屏失效的解救方案热搜词里出现了C# UI定时器刷新卡顿和Windows锁屏定时器失效这两类问题虽然不是单片机领域的但本质上是定时器体系的软件化延伸。很多做上位机的朋友遇到这两个问题时很抓狂我来把它们的根因和解决办法说透。UI定时器刷新卡顿你在C# WinForm或WPF里用的Timer其实是基于Windows消息循环的异步定时器。它的运行逻辑是每隔设定的时间向UI线程的消息队列里塞一条WM_TIMER消息UI线程处理完当前消息后再处理这条定时器消息。如果UI线程正忙着执行一个耗时任务比如大量数据计算或者文件读写消息队列里的定时器消息只能排队等待表现出来就是界面刷新突然掉到几帧甚至完全卡住。这个问题的解法有两层把耗时任务移出UI线程用BackgroundWorker、Task.Run或者ThreadPool处理计算逻辑UI线程只负责轻量的界面刷新。这是治本的办法。如果定时器间隔极短比如16ms追求60帧刷新率直接把任务挪到单独的绘制线程里用双缓冲技术把画面先绘制到内存然后一次性拷贝到屏幕避免逐像素绘制造成的闪烁。锁屏定时器失效Windows检测到系统进入锁定状态或者电源计划切换时会挂起部分应用程序的定时器以节省电量。如果你的程序依赖定时器在锁屏状态下继续执行任务比如定时检测某个设备状态直接使用WINForm自带的Timer就不行了。解决办法是改用System.Threading.Timer或者自己封装一个线程定时器。在系统锁屏后部分后台任务虽然被限制了UI刷新频率但线程委托执行的任务本身通常仍运行尤其是你把这些任务设计成不依赖UI上下文的工作线程时。另一种思路是注册系统的电源状态通知在检测到锁屏时暂停自己的定时任务等用户重新登录后再恢复这样既省电也避免程序在锁屏状态下出现不可预知的异常。这个迁移思路和我做嵌入式开发时的心态很像先分清是什么原因导致定时不准确再决定软件解决还是硬件解决。软件定时器再方便它依赖CPU运行CPU被占用或者系统进入低功耗模式时间就不靠谱了。硬件定时器虽然配置稍微麻烦但一旦启动它自己独立工作CPU跑飞了它照常计时所以对时间精度有严格要求的场景我无条件选择硬件定时器。5. 定时器资源盘点从51、STM32到GD32、Mega2560的选型心得搜索热词里有人点名STM32ZET6定时器资源和GD32F450定时器配置说明不少朋友在芯片选型阶段就卡住了。我结合自己用过的芯片把定时器资源的选型逻辑分享一下。51单片机经典的51核心通常只有Timer0和Timer1两个16位定时器定时能力很基础。如果你只需要做个简单的延时或者波特率发生器那51是够用的。但如果你想同时做PWM输出和输入捕获那资源就吃紧了通常需要软件模拟或者外扩定时器芯片。51的优势是文档生态极其丰富任何一个定时器配置问题都能在网上搜到现成案例非常适合学习阶段理解定时器原理。STM32F103系列以ZET6为例它有TIM1、TIM2、TIM3、TIM4、TIM5、TIM6、TIM7、TIM8一共8个定时器。其中TIM1和TIM8是高级定时器支持互补PWM输出和死区插入适合电机控制领域TIM2到TIM5是通用定时器支持PWM、输入捕获、编码器模式TIM6和TIM7是基本定时器只有定时中断的功能。这种高中低搭配的资源结构基本上覆盖了入门到中高级的所有玩法。GD32F450作为STM32的国产替代方案定时器资源更丰富同时引入了RUCU相当于STM32的RCC时钟控制的额外时钟分频配置项。搜热词的朋友提到的rcu_timer_clock_prescaler_config(rcu_timer_psc_mul2)这个配置就是GD32独有的一层定时器时钟源预分频。很多从STM32转GD32的人容易忽略这层配置导致定时器时钟频率比预期高了一倍。我实测下来GD32的定时器功能在兼容性基础上做了一些增强但前提是你必须仔细阅读对应的用户手册不能完全照搬STM32的配置流程。Arduino Mega2560它用的ATmega2560芯片有6个定时器其中Timer0是系统时钟用的改它会直接影响millis()函数的时间基准建议不要乱动Timer1是16位定时器常用于舵机库和精确PWMTimer2和Timer3分别支持不同的引脚输出。Arduino生态的定时器库非常方便但代价是你很难精确控制底层细节适合快速原型验证。选型方面的个人经验是先画一张功能需求表列出你的项目要几个PWM通道、几路输入捕获、要不要编码器接口然后再去看芯片的定时器资源列表。不要一开始就选个资源特别多的芯片定时器多了配置复杂度也上来了很多新手就是被TIM1到TIM8这么多外设吓退的。6. 定时时间与PWM频率计算参数之间的隐藏关系配置定时器的本质无非是算参数。算法我都帮你整理好了照着套就行。定时中断模式的计算公式定时时间T (PSC 1) × (ARR 1) / 定时器时钟频率举个例子STM32F103的TIM2挂在APB1总线上APB1时钟为36MHz但定时器时钟倍频后为72MHz。我想产生一个10ms的定时中断可以选择PSC 71ARR 999定时时间 (711) × (9991) / 72000000 72 × 1000 / 72000000 0.001秒 1ms如果要10ms把ARR调成9999即可。 72 × 10000 / 72000000 0.01秒这里有一个很重要的设计取舍同样的10ms你可以用PSC大、ARR小的组合也可以用PSC小、ARR大的组合。前者定时器溢出得更频繁但计数分辨率低后者分辨率高但ARR值变大占用寄存器位数更多。一般来说PSC优先选到让ARR落在合适的范围内既能保证精度又不溢出。PWM频率计算公式PWM频率 定时器时钟频率 / (PSC 1) / (ARR 1)比如你想在TIM2上输出一个1kHz、占空比50%的方波时钟72MHz选PSC 71ARR 999频率就是72MHz / 72 / 1000 1kHz占空比由CCR决定CCR 499或者500差别只在极短的高电平时间长度上对大多数负载来说没有本质区别。输入捕获测频率时的分频选择这种场景下PSC直接决定测量精度。例如被测信号是1kHz定时器时钟72MHz如果你不预分频每个时钟周期约13.8ns那测量的时间分辨率极高。但如果被测信号是100MHz的高频信号计数器会被触发得措手不及这时需要适当分频让计数器计数值的变化不要超过寄存器范围再用捕获值的差值乘以分频比得到实际周期。核心思路概括起来就是三十个字**先定频率或定时间再选PSC让计数范围合适最后用ARR精调CCR控制占空比或比较值。**别一上来就抄网上现成的参数不同主频芯片算出来的值完全不同。7. 定时器实际调试中的十大坑每一个都是真金白银换来的这部分是翻阅了无数种提问帖和论坛讨论后结合我自己的排错经历总结出来的。新手做定时器项目最容易出问题的点几乎都集中在下面这些地方。7.1 时钟树没弄对时间全乱套STM32的定时器时钟不一定等于主频。比如F103中APB1预分频系数为2时挂在APB1上的定时器时钟是系统时钟的2倍挂在APB2上的定时器时钟等于系统时钟。很多人直接拿72MHz去算但APB1上的定时器实际是72MHz算出来反而碰巧是对的但如果APB1预分频不为2那定时器时钟就变成36MHz算出来的时间直接翻倍。配置之前先去数据手册把时钟树看图看明白。7.2 中断标志忘了清定时器更新中断触发后如果不手动清除更新标志位中断服务函数会立即再次进入看起来就像中断频率加倍了。虽然部分库函数在进入回调函数时会自动清标志但如果你直接操作寄存器必须养成一进中断先清标志再处理业务的习惯。7.3 ARR寄存器别设成0寄存器是从0开始计数的。如果你把ARR设置成0那计数器根本不会发生溢出定时器就罢工了。同理把PSC设置成0表示的预分频值是1没有降速效果。差别很大对程序员来说尤其容易放松警惕。7.4 预分频器改变时机不对很多定时器在运行时修改PSC新的值不会立即生效需要产生一次更新事件后才会被加载。如果你在运行中动态调整定时频率必须注意加一条强制加载更新事件的指令或者让计数器走完一个完整周期后再修改不然改了半天完全没反应。7.5 输入捕获中的输入滤波器参数外部信号如果带有毛刺噪声会干扰捕获精度。定时器的输入捕获通道自带一个数字滤波器原理是对输入信号进行多次采样只有连续几次采样结果一致才认为是有效的跳变。这个滤波器的采样频率和采样次数都要根据实际信号的噪声情况配置配得太短滤不掉噪声配得太长会把真实信号也滤没了。7.6 PWM通道的极性搞反配置PWM输出时经常有有效电平这个概念。有的库默认高电平有效你设置CCR 500输出高电平的时间占一半如果配置成低电平有效同样CCR 500输出的波形正好反相但你要的控制效果完全不对。舵机尤其敏感极性反了轻则无法控制重则烧舵机。7.7 互补输出的死区时间忌过短控制H桥电路时上下桥臂不能同时导通否则电源直接短路。死区时间必须大于MOS管或者IGBT的开关延时常用值在几百纳秒到几微秒之间。有一些低成本电机驱动板烧管的案例排除硬件问题以后九成是死区时间配置过短。7.8 编码器模式下的上拉电阻编码器输出一般是开漏结构如果你忘了使能内部上拉信号会悬浮不定定时器就会把噪声当脉冲计数。每次位置数据莫名其妙地漂移首先怀疑的就是外部电路上下拉的问题。7.9 定时器中断服务函数执行时间过长如果中断服务函数本身执行时间超过了定时周期那下一次中断到来时上一次还没处理完轻则丢失事件重则系统卡死在中断里。比较稳妥的做法是中断服务函数只做标志位置位和简单的数据读取复杂的业务逻辑放到主循环中处理。7.10 低功耗模式下定时器状态没考虑进入低功耗模式后内部时钟可能被关闭定时器也停止计数。如果你的系统需要在睡眠期间定时唤醒用定时器的唤醒功能而不是普通的中断功能。这个问题在电池供电类设备中尤其常见我曾经在低功耗项目里就是因为忽略了这一点设备每次睡眠后定时全部失效排查了很久才发现是时钟源被关了。8. 基于定时器的三个实用项目思路帮你把原理焊在脑子里光看原理不动手永远是纸上谈兵。这里分享三个我亲手做过的小项目难度从低到高每个都能强制你用上上文的所有知识点。8.1 旋钮编码器调速的RGB灯这是入门级的经典项目。用定时器的编码器模式读取旋转编码器的位置变化再用PWM接口控制RGB灯的亮度或色调。整个过程你会同时用到编码器模式和PWM模式还要留意编码器计数溢出导致的位置回跳问题。实测下来这个项目花一个晚上就能完成但对定时器从会配置到会灵活用的转变特别有帮助。8.2 高精度超声波测距报警器用单脉冲模式触发超声波模块的TRIG信号再用定时器的输入捕获或者门控模式测量ECHO高电平的持续时间根据声速换算距离。超声波传播速度大约是340m/s所以测得的脉宽时间除以58就能粗略得到厘米数。这个项目里你会真正理解发出一个窄脉冲和精确测量一个脉冲宽度这两件事之间的关系。8.3 双电机差速循迹小车这种项目我强烈推荐给有一定基础的人。两个电机每个都需要一路PWM控制速度还需要至少一个编码器接口用于测速闭环。底盘上再装三个循迹传感器MCU根据传感器数据动态调整左右轮的PWM占空比。实际做的时候你会发现编码器测速数据有噪声直接拿来用会让小车抖得厉害你需要自己写个简单的低通滤波或者平均值滤波。这一步完全不是定时器本身的问题但对整个系统的能力提升是几何级的。三个项目做完定时器几乎就成了你身体的一部分看到芯片上有定时器资源本能就知道这几个定时器分别适合干什么活。别小看这种本能它才是从会配置走向会设计的关键分水岭。定时器的学习路径从来不需要特别高级的开发板一块入门级的开发板加一个旋钮编码器、一个超声波模块、两个直流电机也就几十块钱的成本。但把这些东西做完之后你再去看数据手册里定时器相关的章节以前觉得晦涩的每一个寄存器配置项都会变得特别顺眼。那些功能各异的模式也不是表格里冷冰冰的英文而是一个个调试场景在你脑子里留下的画面。