
1. 定时器是51单片机的“心脏节拍器”很多人学51单片机到定时器这一章就开始犯怵觉得又是寄存器又是中断向量抽象得很。我当年第一次接触定时器的时候也是满脑子问号计数器到底是怎么“自己走”的为什么装上初值就能精准定时中断又是怎么“突然跳出来”打断主程序的把这些概念打散之后你会发现定时器的核心逻辑其实简单得惊人——它就是单片机内部一个不断往上累加的计数器加满之后产生一个标志信号你只需要在它加满之前给它预设好起点剩下的全部交给硬件自动完成。这次要分享的是51单片机定时器的完整玩法从寄存器配置到代码实现从初值计算到常见坑点配合实际可跑的示例程序帮你一口气把这块硬骨头啃下来。适合正在学51单片机、被定时器搞得头大的同学也适合想复习定时器细节的老朋友。说实话定时器这一章学透了后面做电子时钟、PWM调速、按键消抖、超声波测距全都是顺着这条路往下走的重要性不比中断低。2. 定时器到底“定时”在哪儿2.1 定时器的工作本质是数脉冲51单片机里的定时器本质就是一个二进制加法计数器。给它一个脉冲它就加一加到满溢出之后清零并往标志位上写1。这个流程完全由硬件完成不占用CPU时间去一条一条数数。所以“定时”这件事的本质就是“数固定频率的脉冲”。脉冲来源有两种一种是单片机内部的机器周期时钟这时候它叫定时器另一种是外部引脚输入的脉冲这时候它叫计数器。同一个硬件两种角色靠配置寄存器来切换。常用的51单片机型号里定时器数量不太一样。标准8051是T0和T1两个16位定时器增强型型号会多出T2甚至更多。最常用、最经典的还是T0和T1这一篇就围绕这两个展开。2.2 定时器相关的四个关键寄存器定时器操作就围绕着四个寄存器转TMOD、TCON、THx、TLx。逐个拆开看一点都不吓人。TMOD是工作模式寄存器高四位管T1低四位管T0。每一位的含义是GATE门控位、C/T定时计数选择位、M1M0模式选择位。GATE位比较特殊它决定定时器是不是受外部引脚INT0/INT1控制。正常情况下我们让GATE 0表示不受外部引脚控制写软件启动指令就开跑GATE 1的时候还得INT引脚同时为高电平才开始计数这个用途在测量外部脉冲宽度时会用到平时用不到。C/T位是好理解的C/T 0选内部机器周期定时模式C/T 1选外部引脚脉冲计数模式。M1M0组合对应四种工作方式见下表M1M0工作方式说明00方式013位定时器兼容老设备用的01方式116位定时器最大计数值6553610方式28位自动重装用完自动补初值11方式3两个8位独立定时器特殊用法TCON是控制寄存器里面有两个最核心的位TRx是启动位置1开始计数清零停止TFx是溢出标志位计数溢出时硬件自动置1如果开了中断就会进入中断服务函数如果用查询方式就得手动清零。THx和TLx是计数初值寄存器。THx存高8位TLx存低8位。16位定时器模式下这两个拼起来就是完整的16位初值。3. 初值计算是定时器最需要动脑的一步3.1 机器周期和晶振的关系要算定时时间第一步是搞清机器周期。51单片机一个机器周期等于12个时钟周期。如果晶振频率是12MHz那机器周期就是12 ÷ 12MHz 1微秒。如果晶振是11.0592MHz机器周期就是12 ÷ 11.0592MHz ≈ 1.085微秒。为什么要区分这两种晶振呢因为很多开发板上用的是12MHz方便计算而用到串口通信的时候常用11.0592MHz来保证波特率整除精准。这两种晶振下的定时器初值计算方式一模一样就是具体数值有区别。定时器在16位模式下从初值开始加加到65536溢出经历65536 - 初值也就是65536 - 初值个机器周期。所以定时时间 (65536 - 初值) × 机器周期。3.2 由定时时间反推初值公式反过来用就是初值 65536 - 定时时间 ÷ 机器周期。拿12MHz晶振、机器周期1微秒来说如果想让定时器溢出周期是50毫秒那么定时时间除以机器周期等于50000初值就是65536 - 50000 15536换算成十六进制是0x3CB0。于是TH0 0x3CTL0 0xB0。这里有一个非常关键的点定时器溢出周期不等于你想要的最终定时周期。因为定时器是溢出一次就中断一次你需要靠软件在中断服务程序里做分频。50ms中断一次想要1秒就要在中断里数20次想要10ms中断一次数100次才是1秒。3.3 12MHz和11.0592MHz下的常用初值对照我大概整理过几个常用定时时长的初值直接抄作业晶振频率机器周期目标定时重装初值(16位)THxTLx12MHz1μs1ms64536 0xFC180xFC0x1812MHz1μs10ms55536 0xD8F00xD80xF012MHz1μs50ms15536 0x3CB00x3C0xB011.0592MHz约1.085μs10ms56320 ≈ 0xDC000xDC0x0011.0592MHz那个10ms的初值我用的是近似数实际会有微小误差。如果要求非常精确需要把小数部分也折算进去调整初值。日常做实验、控制继电器这些完全够用。注意任何定时器都存在不可避免的误差来源——一个是初值取整带来的量化误差另一个是中段响应本身需要时间中断函数被执行时CPU还要完成现场保护和恢复这部分时间也会叠加进实际定时周期。对精度要求不高的场合不用管做时钟之类的还是要考虑。4. 定时器完整代码实现与寄存器配置4.1 最基础的定时器初始化模板定时器的初始化其实就四步配TMOD选模式、装THx/TLx初值、开中断、启动定时器。下面是一段完整的定时器0、方式1、50ms中断的初始化代码#include reg51.h void Timer0_Init(void) { TMOD 0xF0; // 清除T0的控制位不影响T1的配置 TMOD | 0x01; // 设置T0为方式116位定时器GATE0 TH0 0x3C; // 装入初值高8位50ms 12MHz TL0 0xB0; // 装入初值低8位 ET0 1; // 打开T0中断开关 EA 1; // 打开总中断开关 TR0 1; // 启动T0 }注意TMOD那一行的写法。很多新手直接写TMOD 0x01这在单独使用T0、不影响T1的时候没问题但如果你TMOD的高四位还配置了T1的工作方式直接赋值就会把T1的配置冲掉。写TMOD 0xF0再TMOD | 0x01是“只改自己、不动别人”的做法养成这个习惯后面同时用两个定时器的时候就不会莫名其妙出bug。4.2 中断服务函数怎么写得对51单片机的中断服务函数有固定写法C51编译器用interrupt关键字标记后面跟的编号是中断号不能随便填。定时器0的中断号是1定时器1的中断号是3。void Timer0_ISR(void) interrupt 1 { static unsigned int count 0; TH0 0x3C; TL0 0xB0; count; if(count 20) { count 0; P1 ~P1; // 每1秒翻转一次P1口 } }中断函数里必须重装初值这是方式1和方式0跟方式2最大的区别。方式2是8位自动重装硬件会自动把初值从初值寄存器装回去不需要软件干预精度更高、响应更快。但方式2只能计255个脉冲定时短适合PWM等需要高频率稳定中断的场景。方式1虽然要手动重装但16位范围大做长定时方便。4.3 一段完整的LED闪烁Demo把前面的初始化函数和中断函数组合起来就是完整的1秒翻转LED的工程。主函数只需要调用初始化然后进入死循环等待中断就好。void main() { Timer0_Init(); while(1); }这段程序跑起来的效果就是LED以1秒为周期闪烁如果你手边有开发板直接烧进去就能看到效果。这个Demo虽然简单但把“定时器中断 软件分频”的经典套路完整走了一遍后面几乎所有定时器应用都是这个框架的扩展。5. 实战案例组合按键扫描和PWM呼吸灯5.1 用定时器驱动按键消抖和长按检测按键消抖常见做法是在主循环里检测到低电平后延时10ms再确认这个方法的问题在于阻塞了CPU延时期间什么都干不了。如果用定时器做10ms周期的节拍在主循环里检查节拍标志就能不阻塞地完成消抖。我在一个具体项目里这样写过定时器0做1ms中断软件里维护一个计数器每10ms置一次KeyScanFlag。主循环检测到KeyScanFlag为1时就执行一次按键扫描函数扫描函数里做两次采样确认两次之间刚好间隔一个周期消抖效果非常好。长按检测也可以做维护几个计数值按键连续按下超过500ms就判为长按这些都靠定时器节拍实现主循环依旧轻巧。这个思路的核心是“用一个周期性中断作为全局心跳”所有需要时间的逻辑都可以挂在这个心跳上。做电子钟、做状态机控制、做按键多层功能全部适用。我后面做矩阵键盘复杂按键逻辑的时候还是用这套心跳方案一个都没改。5.2 用定时器输出PWM做呼吸灯PWM本质上也是定时器的活儿。一个周期内高电平占的比例叫占空比通过定时器精确控制占空比的变化就能实现LED从暗到亮再变暗的呼吸效果。另一种更常用的方式是用定时器中断维护一个软PWM把周期切成100份每1ms中断一次用变量控制前N次输出高电平、后100 - N次输出低电平N从0渐变到100再回到0就是呼吸灯了。void Timer0_ISR(void) interrupt 1 { static unsigned int pwm_count 0; static unsigned char duty 0; static bit increasing 1; TH0 0xFC; TL0 0x18; // 1ms 12MHz pwm_count; if(pwm_count 100) { pwm_count 0; if(increasing) { duty; if(duty 100) increasing 0; } else { duty--; if(duty 1) increasing 1; } } if(pwm_count duty) LED_PIN 1; // 前duty个节拍输出高 else LED_PIN 0; // 后续节拍输出低 }这个40行不到的代码就能实现呼吸灯而且不阻塞主循环。呼吸效果好不好看关键在占空比的步进值和时间片长度步进值决定了亮度变化的细腻程度时间片太粗或者步进太大就会出现明显的“跳变感”。我个人习惯用100级、每级1ms一整个周期刚好100ms呼吸循环频率合适。6. 定时器常见翻车现场与排查思路6.1 进不了中断先从头顺一遍这是定时器应用里面出现频率最高的问题。排查顺序其实有固定套路EA总开关有没有开这个最基础也最容易忘。ETx分中断开关有没有开开了总开关忘了分支开关也是白搭。TRx有没有置1没启动定时器它压根不会数数。初值有没有怼进去初值是0定时器一启动就会溢出看起来像是不停进中断实际上是被初值坑了。中断服务函数名和interrupt编号对不对interrupt 1是T0interrupt 3是T1写反了就不执行。我见过有个同学查了半天中断进不去最后发现是中断服务函数里加了多余的形参编译器连报错都不报但就是不进中断。这种坑属于编译器层面的排查起来最恼火。6.2 初始化代码被反复执行定时器初始化放进主循环里是新手常见问题。理论上循环每次执行都会重装初值但关键是TR0每次都被置1定时器永远从头开始数最后表现出来的就是定时时间异常长或者干脆不动作。初始化代码必须只执行一次放在主函数最开始或者用某种初始化标志来保护。6.3 方式2自动重装与中断标志的细节方式2模式下重装是自动的所以中断函数里面不需要再写TH0、TL0赋值。如果还像方式1那样手动重装一遍不会报错但属于多此一举。另一个容易搞混的是TFx溢出标志位在中断模式下硬件会自动清零查询模式下必须软件清零。这个区别在中断和查询两种模式下特别容易混淆。6.4 定时器初值更新的时机问题有时候我们需要在程序运行中改变定时器溢出周期比如一会儿10ms一会儿50ms。这里有个坑重装初值如果发生在中断函数里那下一次溢出才生效如果发生在主循环里有可能正好撞上定时器计数到一半导致本次周期极短或极长。稳妥做法是修改定时器配置时先TRx 0停表改完初值再TRx 1启动保证配置生效干净利落。6.5 寄存器位操作技巧配置TMOD的时候建议只动需要的位。举一个实际例子TMOD 0x0F; // 把高四位清零低四位保留T0不受影响 TMOD | 0x10; // T1设为方式1这个写法保证一次只配置T1完全不碰T0。如果你直接写TMOD 0x10看起来没问题但你忘了T0之前是什么配置直接就把T0覆盖了。这种“动谁就只动谁”的位操作思路不光适用于定时器寄存器IO口配置、中断使能标志全部通用。7. 定时器深水区周期测量和频率测量定时器还能直接测外部信号的频率或者周期这个思路很实用也很有趣。周期测量的思路是用T0定时比如配置成16位模式去数内部机器周期同时用外部INT引脚作为门控信号在信号的一个周期内启动和停止定时器。GATE位置1之后外部引脚为高时计数、为低时停止天然就实现了“只在高电平期间计数”。测出高电平持续了多少个机器周期再乘以机器周期就是高电平的持续时间。频率测量的思路反过来让定时器工作在计数模式C/T位置1外部脉冲接到T0引脚P3.4上然后配合另一个定时器作为标准时间闸门比如T1定时100ms在100ms内数T0计了多少个外部脉冲乘以10就是每秒的脉冲数也就是频率值。这些玩法已经非常接近“数字频率计”的原理了。我自己做过一个小项目就是用这种方式测风扇的转速传感器输出脉冲单片机数脉冲算转速整套系统逻辑非常清晰。定时器从基础到进阶全部串起来之后你会发现在51单片机这个平台上定时器是真的强大。8. 定时器中断代码的风格建议最后分享几个我写定时器相关的代码时长期坚持的小习惯。中断函数里最忌讳做耗时的事比如串口打印、大数组复制、复杂浮点运算。中断函数要求快速进出原则是“能放主循环的绝不放中断里”中断里最多做置标志、累加计数值、刷新PWM占空比之类的轻量操作。这个原则我踩过坑——早期做音乐播放器的时候在中断里放了个延时结果整个系统卡死排查了半天才反应过来是中断函数耗时太长抢占了主循环的执行时间。中断函数和主循环共享的变量要用volatile修饰防止编译器优化掉。比如在主循环里检查的中断标志变量不加volatile有时候编译出来完全不更新这个坑是编译器优化导致的特别隐蔽。命名规范方面中断服务函数名虽然有编译器默认名字可以用但建议自己按功能命名并且用注释标明中断源。我自己的习惯是Timer0_ISR、Timer1_ISR这种。另外初始化函数、变量名、注释要保持清晰这种代码过两个月自己回来看也不会陌生。定时器用熟了以后你会发现很多看起来花哨的功能其实都是定时器在各种应用场景下的组合。学完这一章后面的PWM、串口定时配合、多任务时间片调度都会顺很多。做几个小实验感受一下比看十篇文章都管用。