51单片机从零开始入门教程 第七章(定时器实战:精准延时与多任务调度)

1. 定时器基础回顾:你的单片机"闹钟"

刚接触51单片机的定时器时,我总喜欢把它比作厨房里的多功能计时器。想象一下,你正在煮一锅汤,同时还要烤面包。如果一直盯着时钟看时间,其他事情就做不了了。这时候定时器就像你的厨房助手,到点就会提醒你该做什么。

51单片机通常配备2-3个定时器(T0/T1/T2),它们本质上都是16位的计数器。以最常见的12MHz晶振为例,经过12分频后,定时器每1微秒会计数一次。就像沙漏里的沙子,当沙子漏完(计数溢出)就会触发中断,告诉CPU:"时间到了!"

这里有个关键公式要记住:

定时时间 = (65536 - 初值) × 机器周期

比如要实现50ms定时,计算过程是这样的:

// 12MHz晶振下 初值 = 65536 - 50000 = 15536 // 因为50ms=50000us TH0 = 15536 / 256; // 高8位 TL0 = 15536 % 256; // 低8位

2. 精准延时:告别低效的Delay函数

很多新手最常犯的错误就是滥用Delay函数。我曾经调试一个项目,发现LED闪烁总是不准,最后发现是Delay函数被中断打断了。来看个典型对比:

传统Delay方式

void delay_ms(unsigned int ms) { while(ms--) { for(i=0; i<114; i++); // 空循环 } }

这种方式的三大致命伤:

  1. CPU完全被占用,不能做其他事
  2. 时间精度受中断影响
  3. 不同编译器优化效果不同

定时器中断方案

volatile unsigned int timer_count = 0; void Timer0_Init() { TMOD = 0x01; // 模式1 TH0 = 0x3C; // 50ms初值 TL0 = 0xB0; ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR() interrupt 1 { TH0 = 0x3C; // 重装初值 TL0 = 0xB0; timer_count++; } // 使用时 while(timer_count < 20); // 等待1秒 timer_count = 0;

实测下来,定时器方案的精度可以控制在±0.1%以内,而Delay函数可能会有5%以上的误差。

3. 多任务调度:时间片轮转实战

我曾用51单片机同时控制LED流水灯、数码管显示和按键扫描,秘诀就是时间片轮转。原理就像幼儿园老师分糖果:给每个任务分配固定时间,轮流执行。

任务调度器框架

#define MAX_TASKS 3 unsigned char task_delay[MAX_TASKS]; // 任务延时计数器 void Timer0_ISR() interrupt 1 { static unsigned char task_idx = 0; TH0 = 0xFC; // 1ms中断 TL0 = 0x66; // 任务调度 if(--task_delay[task_idx] == 0) { switch(task_idx) { case 0: task_led(); break; case 1: task_display(); break; case 2: task_key(); break; } task_delay[task_idx] = task_interval[task_idx]; } task_idx = (task_idx + 1) % MAX_TASKS; }

典型任务配置

任务执行间隔执行时间优先级
LED控制10ms<0.5ms
数码管扫描2ms<1ms
按键检测50ms<0.2ms

实际项目中我踩过的一个坑:某个任务执行时间超过了分配的时间片,导致其他任务"饿死"。解决方法要么优化代码,要么增加超时检测:

if(P3_4 == 0) { // 按键检测 static unsigned char hold_time = 0; if(++hold_time > 100) { // 超时保护 hold_time = 0; return; } }

4. 进阶技巧:定时器的创造性应用

除了基础功能,定时器还能玩出很多花样。去年我做的一个智能花盆项目,就用T0和T1实现了这些功能:

PWM调光(用定时器模拟):

void Timer1_ISR() interrupt 3 { static unsigned char pwm_cnt = 0; TH1 = 0xFF; // 高频中断 TL1 = 0x00; if(pwm_cnt++ >= 100) pwm_cnt = 0; LED = (pwm_cnt < duty_cycle) ? 1 : 0; }

外部事件计数(将定时器设为计数器模式):

TMOD = 0x05; // T0作为计数器 TH0 = 0x00; // 从0开始计数 TL0 = 0x00; TR0 = 1; // 启动计数 // 通过P3.4(T0)引脚输入脉冲

硬件消抖(比软件延时更可靠):

if(key_pressed) { TR0 = 1; // 启动20ms定时器 } void Timer0_ISR() interrupt 1 { TR0 = 0; // 停止定时器 if(key_stable) { process_key(); } }

定时器就像单片机的瑞士军刀,用的好能让你的代码既高效又优雅。刚开始可能会觉得寄存器配置很复杂,但掌握后会发现它比软件延时可靠得多。建议从简单的1ms定时开始,逐步尝试更复杂的应用,你会爱上这种"设置好就忘掉"的编程方式。