1. 引言
在单片机入门与进阶过程中,数码管显示、LED灯控制、独立按键检测、外部中断和定时器是五个最基础也最常用的功能模块。本文以51单片机(STC89C52)为例,把这五个模块整合到一个综合实战项目中,帮助读者理解它们之间的协作关系,并掌握中断与定时器在真实场景中的典型用法。
2. 硬件基础与原理
在动手写代码之前,先梳理一下各模块的硬件连接和基本原理。
2.1 数码管显示原理
数码管按段数分为七段和八段(含小数点),按结构分为共阴极和共阳极两种。共阴极数码管公共端接地,某段引脚给高电平时对应段点亮;共阳极则相反,公共端接电源,某段引脚给低电平时点亮。
多位数码管通常采用动态扫描方式驱动:同一时刻只点亮一位,通过快速轮流点亮各位,利用人眼视觉暂留效应形成"同时显示"的效果。扫描频率一般不低于50Hz,否则会出现明显闪烁。
2.2 LED灯控制
LED灯通过单片机I/O口驱动,通常串联一个限流电阻(约220Ω到1kΩ)防止电流过大损坏器件。输出高电平点亮还是低电平点亮,取决于LED的接法(灌电流或拉电流)。
2.3 独立按键与消抖
独立按键一端接I/O口,另一端接电源或地。按键按下和释放时,由于机械触点的弹性,会产生约5ms到10ms的电平抖动,需要软件消抖(延时检测)或硬件消抖(RC滤波)来避免误触发。
2.4 外部中断
51单片机提供两个外部中断INT0(P3.2)和INT1(P3.3),可配置为下降沿触发或低电平触发。外部中断适合处理需要即时响应的外部事件,例如紧急停止、计数脉冲等。
2.5 定时器
51单片机通常内置两个16位定时器/计数器T0和T1。定时器工作在定时模式时,对机器周期计数,可用于精确延时、定时扫描、产生PWM等。定时器溢出会触发中断,在中断服务函数中处理周期性任务。
3. 系统整体设计
本实战项目实现以下功能:
- 数码管动态显示一个从0到99循环计数的数值。
- LED灯以1Hz频率闪烁,指示系统运行状态。
- 独立按键K1用于暂停/恢复计数,K2用于清零。
- 外部中断INT0用于紧急暂停,INT1用于切换LED闪烁模式。
- 定时器T0产生1ms基准中断,用于数码管扫描和计时。
整体架构采用"定时器中断驱动 + 主循环查询"的方式:定时器中断负责精确计时和数码管扫描,主循环负责按键检测和逻辑处理。
4. 硬件连接
| 模块 | 引脚 | 说明 |
|---|---|---|
| 数码管段选 | P0 | 共阴极数码管段码输出 |
| 数码管位选 | P2.0-P2.3 | 四位数码管位选控制 |
| LED灯 | P1.0 | 状态指示灯 |
| 按键K1 | P3.0 | 暂停/恢复计数 |
| 按键K2 | P3.1 | 计数清零 |
| 外部中断INT0 | P3.2 | 紧急暂停 |
| 外部中断INT1 | P3.3 | 切换LED闪烁模式 |
5. 软件设计
5.1 定时器初始化
定时器T0工作在方式1(16位定时),定时1ms。晶振12MHz时,机器周期为1us,计数1000次即1ms,初值为65536-1000=64536,即0xFC18。
c
void Timer0_Init(void)
{
TMOD &= 0xF0; // 清除T0模式位
TMOD |= 0x01; // T0方式1,16位定时
TH0 = 0xFC; // 初值高8位
TL0 = 0x18; // 初值低8位
ET0 = 1; // 开启T0中断
EA = 1; // 开启总中断
TR0 = 1; // 启动定时器
}
5.2 外部中断初始化
INT0和INT1均配置为下降沿触发,优先级设为最高,确保紧急操作能及时响应。
c
void External_Interrupt_Init(void)
{
IT0 = 1; // INT0下降沿触发
IT1 = 1; // INT1下降沿触发
EX0 = 1; // 开启INT0中断
EX1 = 1; // 开启INT1中断
PX0 = 1; // INT0高优先级
PX1 = 1; // INT1高优先级
EA = 1; // 开启总中断
}
5.3 数码管段码表
共阴极数码管0到9的段码定义如下,按a、b、c、d、e、f、g、dp顺序排列。
c
unsigned char code seg_code[10] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F // 9
};
5.4 数码管动态扫描
在定时器中断中,每1ms切换一位数码管,实现四位数码管的动态扫描。
c
void Display_Scan(void)
{
static unsigned char pos = 0;
P2 = 0x00; // 关闭所有位选,消隐
switch(pos)
{
case 0: P0 = seg_code[count / 10]; P2 = 0x01; break;
case 1: P0 = seg_code[count % 10]; P2 = 0x02; break;
case 2: P0 = seg_code[led_mode]; P2 = 0x04; break;
case 3: P0 = seg_code[state]; P2 = 0x08; break;
}
pos++;
if(pos >= 4) pos = 0;
}
5.5 定时器中断服务函数
定时器中断每1ms进入一次,负责数码管扫描和计时累加。每1000次(即1秒)更新一次计数。
c
void Timer0_ISR(void) interrupt 1
{
static unsigned int ms_count = 0;
TH0 = 0xFC; // 重装初值
TL0 = 0x18;
Display_Scan(); // 数码管扫描
ms_count++;
if(ms_count >= 1000)
{
ms_count = 0;
if(running)
{
count++;
if(count > 99) count = 0;
}
led_toggle = !led_toggle;
P1_0 = led_toggle;
}
}
5.6 外部中断服务函数
INT0用于紧急暂停,INT1用于切换LED闪烁模式。
c
void INT0_ISR(void) interrupt 0
{
running = 0; // 紧急暂停
state = 0;
}
void INT1_ISR(void) interrupt 2
{
led_mode = !led_mode; // 切换LED模式
if(led_mode == 0)
P1_0 = 0; // 模式0:LED熄灭
else
P1_0 = 1; // 模式1:LED常亮
}
5.7 按键检测与消抖
主循环中轮询按键,采用延时消抖方式,避免机械抖动造成误触发。
c
void Key_Scan(void)
{
if(K1 == 0)
{
delay_ms(10);
if(K1 == 0)
{
running = !running;
state = running ? 1 : 0;
while(K1 == 0); // 等待释放
}
}
if(K2 == 0)
{
delay_ms(10);
if(K2 == 0)
{
count = 0;
while(K2 == 0); // 等待释放
}
}
}
5.8 主函数
主函数完成初始化后进入主循环,主循环只负责按键检测,其余任务由中断驱动。
c
void main(void)
{
Timer0_Init();
External_Interrupt_Init();
running = 1;
state = 1;
led_mode = 1;
while(1)
{
Key_Scan();
}
}
6. 完整代码
将上述模块整合为完整工程,可直接在Keil中编译下载运行。
c
#include <reg52.h>
sbit K1 = P3^0;
sbit K2 = P3^1;
sbit P1_0 = P1^0;
unsigned char code seg_code[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
unsigned char count = 0;
unsigned char running = 1;
unsigned char state = 1;
unsigned char led_mode = 1;
bit led_toggle = 0;
void delay_ms(unsigned int ms)
{
unsigned int i, j;
for(i = 0; i < ms; i++)
for(j = 0; j < 120; j++);
}
void Timer0_Init(void)
{
TMOD &= 0xF0;
TMOD |= 0x01;
TH0 = 0xFC;
TL0 = 0x18;
ET0 = 1;
EA = 1;
TR0 = 1;
}
void External_Interrupt_Init(void)
{
IT0 = 1;
IT1 = 1;
EX0 = 1;
EX1 = 1;
PX0 = 1;
PX1 = 1;
EA = 1;
}
void Display_Scan(void)
{
static unsigned char pos = 0;
P2 = 0x00;
switch(pos)
{
case 0: P0 = seg_code[count / 10]; P2 = 0x01; break;
case 1: P0 = seg_code[count % 10]; P2 = 0x02; break;
case 2: P0 = seg_code[led_mode]; P2 = 0x04; break;
case 3: P0 = seg_code[state]; P2 = 0x08; break;
}
pos++;
if(pos >= 4) pos = 0;
}
void Timer0_ISR(void) interrupt 1
{
static unsigned int ms_count = 0;
TH0 = 0xFC;
TL0 = 0x18;
Display_Scan();
ms_count++;
if(ms_count >= 1000)
{
ms_count = 0;
if(running)
{
count++;
if(count > 99) count = 0;
}
led_toggle = !led_toggle;
P1_0 = led_toggle;
}
}
void INT0_ISR(void) interrupt 0
{
running = 0;
state = 0;
}
void INT1_ISR(void) interrupt 2
{
led_mode = !led_mode;
if(led_mode == 0)
P1_0 = 0;
else
P1_0 = 1;
}
void Key_Scan(void)
{
if(K1 == 0)
{
delay_ms(10);
if(K1 == 0)
{
running = !running;
state = running ? 1 : 0;
while(K1 == 0);
}
}
if(K2 == 0)
{
delay_ms(10);
if(K2 == 0)
{
count = 0;
while(K2 == 0);
}
}
}
void main(void)
{
Timer0_Init();
External_Interrupt_Init();
running = 1;
state = 1;
led_mode = 1;
while(1)
{
Key_Scan();
}
}
7. 调试要点与常见问题
- 数码管闪烁:检查扫描频率是否足够,位选切换前务必先消隐,避免拖影。
- 按键误触发:确认消抖延时是否足够,建议10ms到20ms。
- 中断不响应:检查EA总中断是否开启,外部中断触发方式配置是否正确。
- 定时不准:确认晶振频率与初值计算是否匹配,中断服务函数中要及时重装初值。
- 优先级冲突:外部中断和定时器中断同时触发时,注意优先级设置,避免关键操作被延迟。
8. 总结
本文通过一个综合实战项目,把数码管动态扫描、LED控制、独立按键消抖、外部中断和定时器中断整合在一起。核心思路是:定时器中断提供稳定的时间基准,负责周期性任务;外部中断处理紧急事件;主循环处理按键等非实时任务。理解这种"中断驱动 + 主循环查询"的架构,对后续学习更复杂的单片机系统非常有帮助。