ARTICLE DETAIL

资讯详情

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

51单片机定时器原理与应用:从精准延时到多任务调度

51单片机定时器原理与应用:从精准延时到多任务调度 1. 从“跑马灯”到“精准延时”为什么51单片机的定时器是灵魂很多刚接触51单片机的朋友最开始都是从点亮一个LED灯开始的。接着我们会让这个灯闪烁起来也就是所谓的“跑马灯”。在最初的例程里我们常常会看到这样的代码一个简单的for循环或者while循环里面嵌套着另一个循环通过空转来消耗时间实现延时。这种方法我们称之为“软件延时”。void Delay_ms(unsigned int ms) { unsigned int i, j; for(i0; ims; i) for(j0; j114; j); // 这个114是根据晶振频率估算的 }这段代码看起来简单直接但它有一个致命的问题CPU被完全绑架了。在执行这段延时函数时单片机不能做任何其他事情比如检测按键、刷新显示、处理数据。整个系统就像被“定住”了一样效率极低且延时时间严重依赖于晶振频率和编译器的优化精度很差。而定时器/计数器就是用来把CPU从这种无意义的等待中解放出来的核心硬件。它就像单片机内部的一个独立、精准的“闹钟”或**“里程表”**。你只需要告诉这个“闹钟”“请在10毫秒后响铃”然后你就可以放心地去执行其他任务主程序。10毫秒一到“闹钟”定时器就会通过中断的方式“通知”CPU“时间到了该来处理一下了” CPU这时会暂时放下手头的工作去执行你预先写好的中断服务函数处理完后再回来继续原来的工作。这个机制是单片机从“玩具”迈向“工具”实现多任务、实时性系统的基石。无论是测量一个脉冲的宽度输入捕获、产生特定频率的方波PWM输出、还是累计外部事件的次数计数器模式都离不开它。可以说玩不转定时器就谈不上真正掌握了单片机。2. 庖丁解牛51单片机定时器/计数器的内部结构与工作原理51单片机通常指Intel MCS-51内核及其兼容产品如STC89C52内部通常集成了两个16位的定时器/计数器称为Timer0和Timer1。在一些增强型51内核中还会有Timer2。它们的基本结构是相似的我们可以将其拆解为几个核心部件来理解。2.1 核心寄存器控制一切的“开关面板”对定时器/计数器的所有操作本质上就是对几个特殊功能寄存器SFR的读写。这几个寄存器构成了程序员与定时器硬件之间的接口。TCON (Timer Control Register定时器控制寄存器)这是一个8位寄存器负责控制定时器的启动、停止以及标志中断请求。其关键位如下TF1/TF0定时器1/0的溢出标志位。当定时器从最大值65535再加1变为0时硬件会自动将此位置1表示“时间到了”或“数满了”。如果中断被允许CPU就会跳转到中断服务程序。注意进入中断后需要软件清零此位TFx 0;否则会反复触发中断。TR1/TR0定时器1/0的运行控制位。TRx 1启动定时器TRx 0停止定时器。它相当于定时器的“启动/停止”按钮。其余位如IE1/IE0,IT1/IT0主要用于外部中断与定时器关系不大。TMOD (Timer Mode Register定时器模式寄存器)这是一个8位寄存器但不可位寻址必须整体赋值。它用于设置定时器的工作模式定时 or 计数和具体的工作方式。其高4位控制T1低4位控制T0。每4位的结构如下GATE门控位。通常设为0。若设为1则定时器的启动不仅受TRx控制还受对应外部中断引脚INTx电平的控制用于做脉冲宽度测量等高级功能。C/T定时器or计数器选择位。这是理解定时和计数区别的关键C/T 0定时器模式。此时定时器的计数脉冲来源于单片机内部的系统时钟经过分频。每过一个机器周期计数值加1。因此定时器模式是对内部固定频率的脉冲进行计数用来计时。C/T 1计数器模式。此时定时器的计数脉冲来源于外部引脚T0对应P3.4T1对应P3.5。每来一个外部脉冲的下降沿1-0的跳变计数值加1。因此计数器模式是对外部不规律的事件进行计数。M1 M0工作方式选择位。这两位组合决定了定时器的工作方式主要有四种M1M0工作方式描述00方式013位定时器/计数器高8位THx 低5位TLx兼容早期8048现已少用。01方式116位定时器/计数器THxTLx全用。这是最常用、最直观的方式。计数值从0到65535溢出后归零。10方式28位自动重装定时器。TLx用于计数THx用于保存重装初值。TLx溢出后硬件自动将THx的值装入TLx同时置位TFx。适用于需要精确定时且不占用CPU时间重装初值的场景如串口通信波特率发生器。11方式3仅T0有此方式。将T0拆成两个独立的8位定时器TL0和TH0此时T1通常用作串口波特率发生器。2.2 计数单元THx 和 TLx这是两个8位的寄存器共同组成一个16位的计数器。在方式1下THx是高8位TLx是低8位。我们设定的初值就存放在这里。定时器启动后每来一个脉冲内部时钟或外部引脚这个16位寄存器的值就加1直到从655350xFFFF加到655360x10000时发生溢出TFx置1计数值归零。2.3 初值计算如何让“闹钟”在准确的时间响铃我们很少需要定时器从0一直数到65535。通常我们需要一个特定的时间间隔比如10ms。这就需要我们给定时器设定一个初始值让它从这个值开始加加到65536溢出所经过的计数次数正好对应我们所需的时间。核心公式定时器模式方式1所需计数值 65536 - (定时时间 / 机器周期)其中机器周期51单片机中1个机器周期 12个时钟周期对于标准12T模式。如果你的晶振是12MHz那么时钟周期 1/12 us机器周期 1us。定时时间你希望定时器溢出中断的时间间隔比如0.01秒10ms。举例使用12MHz晶振12T模式实现50ms定时。计算机器周期T_machine 12 * (1 / 12MHz) 1us。计算50ms需要的机器周期数N 50,000us / 1us 50000。计算定时器初值X 65536 - 50000 15536。将15536转换为十六进制0x3CB0。分别赋值TH0 0x3C;TL0 0xB0;这样定时器从15536开始计数数50000次后达到65536溢出正好耗时50ms。注意上述计算基于标准12T模式。现在很多增强型51单片机如STC系列支持1T模式1个时钟周期1个机器周期速度更快。此时计算公式中的“12”需要替换为相应的分频系数可能是1, 12, 可选等具体需查阅芯片手册。3. 实战演练四种工作方式的代码实现与典型应用场景理解了原理我们通过代码来具体看看不同工作方式如何使用。这里以Timer0为例。3.1 方式1最通用的16位定时器这是最常用的模式可以实现从微秒到几十毫秒量级的定时取决于晶振频率。应用场景精准延时、周期性任务调度如每10ms扫描一次按键、每1s刷新一次显示屏。#include reg52.h // 包含51单片机寄存器定义头文件 #define FOSC 11059200L // 定义晶振频率11.0592MHz #define T1MS (65536 - FOSC/12/1000) // 计算1ms所需的定时器初值 unsigned int count_1ms 0; // 1ms计数器 unsigned int seconds 0; // 秒计数器 void Timer0_Init() { TMOD 0xF0; // 清零T0的控制位低4位保持T1设置不变 TMOD | 0x01; // 设置T0为工作方式116位定时器 TH0 T1MS / 256; // 计算初值高8位 TL0 T1MS % 256; // 计算初值低8位 ET0 1; // 允许T0中断 EA 1; // 开启总中断 TR0 1; // 启动T0 } void main() { Timer0_Init(); while(1) { // 主循环可以执行其他任务如显示、逻辑处理 // 定时任务在中断中处理 if(seconds 5) { // 例如每5秒执行一次的任务 P1 ~P1; // 取反P1口LED闪烁 seconds 0; } } } // Timer0 中断服务函数 void Timer0_ISR() interrupt 1 { // 进入中断后需要重装初值方式1不是自动重装 TH0 T1MS / 256; TL0 T1MS % 256; count_1ms; if(count_1ms 1000) { // 累计1000个1ms即1秒 count_1ms 0; seconds; } }关键点方式1中断服务程序中必须手动重装初值否则下次定时将从0开始导致定时时间错误。通过一个全局变量count_1ms在中断中累加实现更长周期的定时1秒这是一种常用的“软件分频”方法。3.2 方式28位自动重装定时器TLx用于计数THx保存重装值。TLx溢出后硬件自动将THx的值装入TLx同时触发中断。应用场景产生固定频率的脉冲信号、作为串口通信的波特率发生器这是T1在方式2下的经典应用。void Timer1_Init() { TMOD 0x0F; // 清零T1的控制位高4位 TMOD | 0x20; // 设置T1为工作方式28位自动重装 // 假设需要产生约100us的定时 TH1 256 - (FOSC/12/10000); // 计算重装值100us 0.1ms 10000Hz TL1 TH1; // 初始计数值等于重装值 ET1 1; EA 1; TR1 1; } void Timer1_ISR() interrupt 3 { // 方式2中断无需重装初值硬件自动完成 P1_0 ~P1_0; // 在P1.0引脚产生方波 }优势定时精度高因为重装过程由硬件完成没有软件重装带来的时间误差。特别适合对定时精度要求高、且定时周期固定的场合。3.3 方式0与方式3特定场景下的选择方式013位计数器为了兼容早期产品现在基本不用可忽略。方式3仅T0特有。将T0拆成两个8位定时器TL0和TH0。TL0使用T0原有的控制位TR0, TF0, 中断号1TH0则固定为定时器模式对机器周期计数并使用T1的控制位TR1和中断标志TF1中断号3但此时T1的溢出位TF1不会被置位。应用场景当你需要两个独立的8位定时器且其中一个TH0不需要中断功能或者系统只需要一个定时器T0但串口需要T1作波特率发生器时可以将T0设为方式3T1设为方式2作波特率发生器。3.4 计数器模式统计外部事件将TMOD寄存器中的C/T位设为1定时器就变成了计数器对来自T0P3.4或T1P3.5引脚的外部脉冲下降沿进行计数。void Counter0_Init() { TMOD 0xF0; // 清零T0位 TMOD | 0x05; // 设置T0为计数器模式方式1 TH0 0; // 初值设为0 TL0 0; ET0 1; EA 1; TR0 1; // 启动计数器 } void Timer0_ISR() interrupt 1 { // 每计数65536个脉冲进入此中断 // 可以在这里处理累计值 }应用场景测量转速通过光电传感器、统计产品数量通过光电门、测量频率在已知时间内统计脉冲数等。重要提醒计数器模式对外部脉冲的采样频率有限制。为了保证不丢失脉冲外部脉冲的高电平和低电平持续时间至少需要保持1个机器周期。对于12MHz晶振最高计数频率理论上不超过500KHz1/2机器周期。实际应用需留有余量。4. 避坑指南与高阶应用技巧在实际项目中仅仅让定时器跑起来是不够的。如何用得稳定、精准、高效才是体现功力的地方。4.1 中断服务函数编写“三要三不要”三要要快进快出中断服务函数ISR执行时间必须尽可能短。长时间占用中断会导致其他中断无法响应或影响主程序运行。复杂计算、延时等操作应放到主循环中通过中断设置标志位来触发。要保护现场如果ISR中使用了与主程序共享的变量尤其是多字节变量且主程序可能被打断要考虑使用“原子操作”或暂时关闭中断进行保护防止数据被破坏。要清除标志对于需要软件清除的中断标志如TFx必须在ISR开始或结束时及时清除。三不要不要调用耗时函数避免在ISR中调用printf、scanf等标准库函数它们通常很慢且不可重入。不要进行动态内存分配如malloc,free极易导致堆栈混乱。不要过度嵌套避免在ISR中调用可能触发其他中断的函数防止中断嵌套过深导致堆栈溢出。4.2 精准定时与误差补偿软件重装初值方式1必然引入误差因为从溢出发生到CPU响应中断、再到执行重装指令需要一定时间几个机器周期。对于高精度应用需要进行补偿。补偿方法在计算初值时减去中断响应和重装指令所消耗的机器周期数。例如假设从TFx置1到执行完TH0...; TL0...;这两条指令共用了7个机器周期那么计算初值时N就应该增加7。X 65536 - (N 补偿周期数)更高级的做法是使用自动重装模式方式2或者使用增强型51单片机中带重装预装载寄存器的定时器可以在本次定时结束时自动将预装好的值载入实现“零延迟”重装。4.3 使用定时器实现多任务调度简易OS思想这是定时器最强大的应用之一。通过一个高精度定时器如1ms中断一次我们可以构建一个简单的协作式调度器。typedef struct { void (*task)(void); // 任务函数指针 unsigned int interval; // 执行间隔ms unsigned int counter; // 倒计时计数器 } Task_t; Task_t task_list[] { {Task_LED_Blink, 500, 0}, // 每500ms执行一次LED闪烁 {Task_Key_Scan, 20, 0}, // 每20ms执行一次按键扫描 {Task_Sensor_Read, 100, 0},// 每100ms执行一次传感器读取 // ... 可以添加更多任务 }; #define TASK_NUM (sizeof(task_list)/sizeof(Task_t)) void Timer0_ISR() interrupt 1 { // 1ms定时中断 TH0 ...; TL0 ...; // 重装初值 for(unsigned char i0; iTASK_NUM; i) { if(task_list[i].counter 0) { task_list[i].counter task_list[i].interval; // 重置计数器 task_list[i].task(); // 执行任务 } else { task_list[i].counter--; } } }在主循环中CPU可以处理一些非实时性的任务或者直接进入低功耗模式。所有周期性任务都由定时器中断驱动的调度器来管理结构清晰实时性好。4.4 测量脉冲宽度与频率输入捕获思想标准51单片机没有专门的输入捕获单元但我们可以利用“门控”模式GATE1配合定时器来测量高电平宽度。原理设置GATE1,C/T0定时模式。此时定时器启动条件变为TRx1且INTx引脚为高电平。将待测脉冲接到INT0P3.2引脚。脉冲上升沿到来INT01定时器开始计数。脉冲下降沿到来INT00定时器停止计数。读取此时定时器的值TH0, TL0这个值就对应了高电平期间经过的机器周期数从而计算出高电平宽度。对于频率测量则可以在计数器模式下在固定的“闸门时间”比如1秒内统计外部引脚T0/T1的脉冲个数个数即为频率。4.5 与STC单片机等增强型内核的差异现在流行的STC89C52/STC12C5A60S2等单片机在标准51基础上做了大量增强其定时器也更为强大更多定时器可能有T0, T1, T2甚至T3, T4。可编程时钟分频不再是固定的12分频可以选择1T, 12T等模式灵活平衡速度与功耗。更多工作模式除了传统的4种可能增加可重装16位模式兼具方式1的长度和方式2的自动重装、自动重装模式等。附加功能定时器可能直接与PCA可编程计数器阵列、PWM、ADC触发等模块关联。因此在使用具体型号时第一要务是仔细阅读其数据手册了解其定时器模块的特殊寄存器和功能不要想当然地套用标准51的代码。5. 从51到STM32定时器思想的演进与拓展学习51的定时器不仅是掌握一款芯片更是理解“定时/计数”这一核心硬件概念。当你过渡到STM32等更强大的ARM Cortex-M内核单片机时会发现其定时器TIM功能呈指数级增长但核心思想一脉相承。STM32的通用定时器如TIM2-TIM5可以看作是51定时器的“超级增强版”基本功能同样有向上/向下计数、自动重装、溢出中断。强大扩展多通道一个定时器有4个独立通道CH1-CH4。丰富模式输入捕获精准测量脉冲宽度/频率、输出比较产生PWM、单脉冲、PWM生成电机控制、调光、编码器接口直接读取正交编码器。高级特性主从模式定时器联动、刹车功能、互补输出带死区控制用于驱动三相电机。时钟源多样不仅来自内部系统时钟还可以来自外部时钟、其他定时器。例如STM32CubeMX配置定时器生成PWM其本质仍然是设置“自动重装值”ARR和“比较值”CCRx当计数器计数到比较值时输出电平翻转。这与51单片机中通过定时器中断软件翻转IO口实现PWM的思路一致但STM32通过硬件实现精度和实时性是天壤之别。理解51单片机定时器“计数-比较-溢出-中断/动作”这一核心流程将为学习任何更复杂的定时器外设打下坚实的基础。它就像内功心法招式各种高级模式可以千变万化但内力对时序和事件的理解是相通的。我个人在从51转向STM32时最大的体会是不要被STM32 HAL库复杂的结构体初始化吓倒。静下心来找到那个设置“计数值”和“分频”的寄存器ARR和PSC找到那个“使能计数”的开关CR1寄存器的CEN位你会发现一切又回到了熟悉的起点。只是这个“闹钟”变得更智能、更精准能同时设定多个“响铃”时间比较匹配还能在“响铃”时自动帮你把灯打开或关上输出动作。
返回列表