文章目录
- 前言
- 一、数码管
- 1.1 数码管的结构
- 1.2 静态显示
- 1.3 动态显示原理
- 1.4 关键:消影
- 1.5 静态扫描代码(软件延时版)
- 二、中断系统
- 2.1 什么是中断
- 2.2 中断相关寄存器
- IE —— 中断允许寄存器
- TCON —— 中断触发与标志
- IP —— 中断优先级
- 2.3 中断服务函数的写法
- 2.4 外部中断实验:按键翻转 LED
- 2.5 外部中断 vs 按键轮询
- 三、定时器 / 计数器
- 3.1 定时器原理
- 3.2 TMOD —— 工作方式寄存器
- 3.3 定时初值计算
- 3.4 定时器初始化模板
- 3.5 定时器中断实验:1 秒 LED 闪烁
- 3.6 定时器做计数器
- 四、综合实验:数码管秒表(定时器扫描 + 计时)
- 五、把按键也加进来(秒表启停)
- 六、常见问题
- 6.1 数码管显示重影 / 花屏
- 6.2 数码管亮度不均匀
- 6.3 定时器时间不准
- 6.4 中断不触发
- 6.5 变量在 ISR 和主程序之间"不更新"
- 七、总结
前言
上一篇我们总结了 51单片机的基本概念、最小系统、流水灯和独立按键。这一篇继续往下走,聊三个非常核心的内容:
- 数码管:从静态显示到动态扫描
- 中断系统:外部中断的用法
- 定时器/计数器:精准定时、定时器中断
这三者是 51单片机从"会点灯"到"能做事"的分水岭。掌握之后,就可以做数码管时钟、计数器、按键计时器等综合项目了。
代码基于 Keil C51 + STC89C52 / AT89C52,12MHz 晶振。不同开发板引脚可能不同,请以实际原理图为准。
一、数码管
1.1 数码管的结构
数码管本质上是 8 个 LED(7 段 + 1 个小数点)封装在一起:
a ┌─────┐ f │ │ b ├──g──┤ e │ │ c └─────┘ · dp d按公共端接法分为两类:
| 类型 | 公共端 | 点亮方式 | 段码 |
|---|---|---|---|
| 共阴 | 接 GND | 段线给高电平 | 正逻辑 |
| 共阳 | 接 VCC | 段线给低电平 | 反逻辑 |
共阴段码表(a 为最低位,dp 为最高位):
| 数字 | 段码 | 数字 | 段码 |
|---|---|---|---|
| 0 | 0x3F | 5 | 0x6D |
| 1 | 0x06 | 6 | 0x7D |
| 2 | 0x5B | 7 | 0x07 |
| 3 | 0x4F | 8 | 0x7F |
| 4 | 0x66 | 9 | 0x6F |
共阳段码 = 共阴段码按位取反:
0 -> 0xC0 1 -> 0xF9 2 -> 0xA4 ...带小数点的数字:共阴段码| 0x80,例如 0. 是0xBF。
1.2 静态显示
一位数码管独占一组 IO 口,直接输出段码即可:
#include<reg52.h>typedefunsignedcharu8;u8 code seg_code[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};voidmain(){while(1){P0=seg_code[8];// 显示数字 8}}优点:程序简单,显示稳定,无闪烁。
缺点:一位占用 8 个 IO。4 位数码管就要 32 个 IO,51单片机根本不够用。
所以多位数码管几乎都用动态扫描。
1.3 动态显示原理
多位数码管内部段线并联在一起,只有位选端(公共端)独立。
以 4 位共阴数码管为例:
段选 P0 (a~dp) 共用 │ ┌─────────┼─────────┼─────────┼─────────┐ │ │ │ │ │ 位选1 位选2 位选3 位选4 P2.0 P2.1 P2.2 P2.3扫描过程:
- 位选1拉低,段选送第 1 位数字 → 第 1 位亮
- 关闭第 1 位,位选2拉低,段选送第 2 位数字 → 第 2 位亮
- ……依次快速循环
只要扫描速度够快(每位 1~2ms,整屏刷新率 > 50Hz),人眼就会因为视觉暂留看到 4 位同时显示。
1.4 关键:消影
扫描时如果不消影,上一位的段码会残留在下一位上,导致显示重影。
正确顺序:
P2=0xFF;// 1. 先关闭所有位选(消影)P0=seg_code[digit];// 2. 再送新的段码P2=~(1<<pos);// 3. 最后打开当前位如果先送段码再切位选,就会看到上一位的"鬼影"。
1.5 静态扫描代码(软件延时版)
#include<reg52.h>typedefunsignedcharu8;typedefunsignedintu16;u8 code seg_code[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};u8 disp_buf[4]={1,2,3,4};// 4 位显示缓冲区voiddelay_ms(u16 ms){u16 i,j;for(i=ms;i>0;i--)for(j=110;j>0;j--);}voiddisplay(){u8 i;for(i=0;i<4;i++){P2=0xFF;// 消影P0=seg_code[disp_buf[i]];P2=~(1<<i);// 打开第 i 位delay_ms(2);// 每位显示 2ms}}voidmain(){while(1){display();}}这段代码能显示,但问题在于display()会一直占用 CPU。如果还要做按键检测、计时,就会互相干扰。真正工程化的做法是用定时器中断来扫描,后面会讲。
二、中断系统
2.1 什么是中断
中断就是:CPU 正在执行主程序时,突然有个"紧急事件",CPU 暂停主程序,转去执行中断服务函数(ISR),执行完再回到原处继续。
51单片机有 5 个中断源:
| 中断源 | 中断号 | 入口地址 | 触发条件 |
|---|---|---|---|
| 外部中断0 (INT0) | 0 | 0x0003 | P3.2 引脚电平/边沿 |
| 定时器0 (T0) | 1 | 0x000B | T0 溢出 |
| 外部中断1 (INT1) | 2 | 0x0013 | P3.3 引脚电平/边沿 |
| 定时器1 (T1) | 3 | 0x001B | T1 溢出 |
| 串口 (UART) | 4 | 0x0023 | 接收/发送完成 |
2.2 中断相关寄存器
IE —— 中断允许寄存器
| 位 | 名称 | 作用 |
|---|---|---|
| EA | 总中断允许 | 1=开总中断 |
| ES | 串口中断 | 1=允许 |
| ET1 | 定时器1中断 | 1=允许 |
| EX1 | 外部中断1 | 1=允许 |
| ET0 | 定时器0中断 | 1=允许 |
| EX0 | 外部中断0 | 1=允许 |
记住:任何中断要生效,必须 EA=1 且对应位=1。
TCON —— 中断触发与标志
| 位 | 名称 | 作用 |
|---|---|---|
| IT0 | INT0 触发方式 | 1=下降沿,0=低电平 |
| IE0 | INT0 请求标志 | 硬件自动置位 |
| IT1 | INT1 触发方式 | 1=下降沿,0=低电平 |
| IE1 | INT1 请求标志 | 硬件自动置位 |
| TR0 / TR1 | 定时器启停 | 1=启动 |
| TF0 / TF1 | 定时器溢出标志 | 溢出时硬件置 1 |
实际开发中通常只操作IT0、IT1。
IP —— 中断优先级
默认同级中断不能嵌套。设置PX0=1、PT0=1等可提高优先级,实现中断嵌套。初学阶段一般不用动。
2.3 中断服务函数的写法
void函数名()interrupt 中断号{// 中断处理代码}interrupt是关键字,中断号必须是 0~4- ISR 应尽量短小,不要在里面放长延时
- 全局变量在 ISR 和主程序之间共享时,建议加
volatile
2.4 外部中断实验:按键翻转 LED
硬件:按键接 P3.2(INT0),LED 接 P2.0,低电平点亮。
#include<reg52.h>sbit LED=P2^0;voiddelay_ms(unsignedintms){unsignedinti,j;for(i=ms;i>0;i--)for(j=110;j>0;j--);}voidmain(){IT0=1;// INT0 下降沿触发EX0=1;// 允许外部中断0EA=1;// 开总中断LED=1;// 初始熄灭while(1){// 主循环可以去做别的事}}voidEX0_ISR()interrupt0{delay_ms(20);// 简单消抖if(P3^2==0){LED=!LED;// 翻转 LED}}注意:
- 按键未按下时 P3.2 应为高电平(需要上拉)
- 下降沿触发时,按键按下的瞬间会触发中断
- ISR 里加延时是权宜之计,更好的做法是用定时器消抖
2.5 外部中断 vs 按键轮询
| 方式 | 优点 | 缺点 |
|---|---|---|
| 轮询(while里判断) | 简单直观 | 占用 CPU,主循环被拖慢 |
| 外部中断 | 响应快,不占用主循环 | 需要消抖,ISR 要短 |
实际项目中,按键通常还是用"定时器扫描 + 状态机"最稳,外部中断更适合处理脉冲计数、编码器、红外接收这类事件。
三、定时器 / 计数器
3.1 定时器原理
51单片机内部有两个定时器/计数器:T0 和 T1(52 系列还有 T2)。
它的本质是一个加法计数器:
- 每个机器周期计一次数
- 计满溢出时,硬件置
TF0 = 1 - 如果开了中断,就会触发定时器中断
计数脉冲来源:
C/T = 0:内部机器周期 →定时器模式C/T = 1:外部引脚(T0 是 P3.4,T1 是 P3.5)→计数器模式
3.2 TMOD —— 工作方式寄存器
高 4 位管 T1,低 4 位管 T0:
| 位 | 名称 | 说明 |
|---|---|---|
| GATE | 门控 | 0=TRx 启动,1=INTx 引脚+TRx 共同启动 |
| C/T | 定时/计数选择 | 0=定时,1=计数 |
| M1 M0 | 工作方式 | 见下表 |
| M1 M0 | 方式 | 说明 |
|---|---|---|
| 0 0 | 方式0 | 13 位定时器 |
| 0 1 | 方式1 | 16 位定时器(最常用) |
| 1 0 | 方式2 | 8 位自动重装(串口波特率常用) |
| 1 1 | 方式3 | 拆分使用 |
常用配置:
TMOD=0x01;// T0,方式1,定时器TMOD=0x10;// T1,方式1,定时器TMOD=0x21;// T1 方式2 + T0 方式1(串口场景)保险的写法是先清零再置位:
TMOD&=0xF0;// 清 T0 的低 4 位TMOD|=0x01;// T0 方式13.3 定时初值计算
方式1是 16 位,最大值 65536。从初值计到 65536 溢出:
定时时间 = (65536 - 初值) × 机器周期 机器周期 = 12 / 晶振频率12MHz 晶振:机器周期 = 1μs,一个计数 = 1μs。
| 定时时长 | 计数次数 | 初值 | TH | TL |
|---|---|---|---|---|
| 1 ms | 1000 | 64536 | 0xFC | 0x18 |
| 10 ms | 10000 | 55536 | 0xD8 | 0xF0 |
| 20 ms | 20000 | 45536 | 0xB1 | 0xE0 |
| 50 ms | 50000 | 15536 | 0x3C | 0xB0 |
最大定时:方式1 单次最长 65536 μs ≈ 65.5 ms(12MHz)。
如果要定时 1 秒,就让定时器每 50ms 中断一次,累计 20 次。
3.4 定时器初始化模板
voidTimer0_Init(){TMOD&=0xF0;TMOD|=0x01;// T0 方式1TH0=0xFC;// 1ms @ 12MHzTL0=0x18;TF0=0;// 清溢出标志ET0=1;// 允许 T0 中断EA=1;// 开总中断TR0=1;// 启动 T0}注意:方式1不是自动重装的,ISR 里必须重新赋初值,否则下一次定时时间会乱。
3.5 定时器中断实验:1 秒 LED 闪烁
#include<reg52.h>typedefunsignedcharu8;typedefunsignedintu16;sbit LED=P2^0;u16 cnt=0;// 50ms 中断计数voidTimer0_Init(){TMOD&=0xF0;TMOD|=0x01;TH0=0x3C;// 50ms @ 12MHzTL0=0xB0;ET0=1;EA=1;TR0=1;}voidmain(){Timer0_Init();while(1){// 主循环空闲,可以做别的}}voidTimer0_ISR()interrupt1{TH0=0x3C;// 重装初值TL0=0xB0;cnt++;if(cnt>=20)// 20 × 50ms = 1s{cnt=0;LED=!LED;}}主循环完全空闲,LED 依然精准闪烁。这就是定时器的价值。
3.6 定时器做计数器
将 T0 设为计数器模式,接 P3.4 输入脉冲:
voidCounter0_Init(){TMOD&=0xF0;TMOD|=0x05;// T0,方式1,计数器模式TH0=0;TL0=0;ET0=1;EA=1;TR0=1;}每来一个下降沿脉冲,T0 加 1,计满 65536 溢出触发中断。常用于测速、计件。
四、综合实验:数码管秒表(定时器扫描 + 计时)
把前面三块拼起来:用一个定时器同时完成数码管动态扫描和秒表计时。
硬件:
- 4 位共阴数码管
- 段选 → P0
- 位选 → P2.0 ~ P2.3(低电平选通)
思路:
- 定时器 0 每 1ms 中断一次
- 每次中断扫描一位数码管,4ms 扫完一轮
- 累计 1000 次中断 = 1 秒,秒数 +1
- 主循环只负责把秒数拆成 4 位数字
#include<reg52.h>typedefunsignedcharu8;typedefunsignedintu16;u8 code seg_code[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};u8 disp_buf[4]={0,0,0,0};// 显示缓冲区u16 ms_cnt=0;// 毫秒计数u16 seconds=0;// 秒数/* ---------------- 定时器0 初始化:1ms ---------------- */voidTimer0_Init(){TMOD&=0xF0;TMOD|=0x01;// T0,方式1TH0=0xFC;// 1ms @ 12MHzTL0=0x18;ET0=1;EA=1;TR0=1;}/* ---------------- 主函数 ---------------- */voidmain(){Timer0_Init();while(1){// 把秒数拆成 4 位数字disp_buf[0]=seconds/1000%10;disp_buf[1]=seconds/100%10;disp_buf[2]=seconds/10%10;disp_buf[3]=seconds%10;if(seconds>9999)seconds=0;}}/* ---------------- 定时器0 中断服务函数 ---------------- */voidTimer0_ISR()interrupt1{staticu8 digit=0;/* --- 重装初值 --- */TH0=0xFC;TL0=0x18;/* --- 数码管扫描(含消影) --- */P2=0xFF;// 关闭所有位选P0=seg_code[disp_buf[digit]];// 送段码P2=~(1<<digit);// 打开当前位(低电平选通)digit++;if(digit>=4)digit=0;/* --- 计时 --- */ms_cnt++;if(ms_cnt>=1000)// 1000 × 1ms = 1s{ms_cnt=0;seconds++;}}现象:数码管从 0000 开始,每秒 +1,显示稳定无闪烁。
关键点:
- 扫描周期 4ms,刷新率 250Hz,远高于人眼临界频率
- 每次中断只做一件事(扫一位),ISR 很短
- 主循环空闲,可以继续加按键处理、串口通信
五、把按键也加进来(秒表启停)
在中断里扫描数码管,主循环里读按键,两者互不干扰:
sbit KEY=P3^1;voidmain(){bit running=1;// 秒表运行标志Timer0_Init();while(1){/* 按键检测:启停切换 */if(KEY==0){delay_ms(20);if(KEY==0){running=!running;while(KEY==0);// 等待松手}}/* 显示缓冲区更新 */disp_buf[0]=seconds/1000%10;disp_buf[1]=seconds/100%10;disp_buf[2]=seconds/10%10;disp_buf[3]=seconds%10;/* 用一个全局标志控制计时是否累加 */g_running=running;}}在 ISR 中:
if(g_running){ms_cnt++;if(ms_cnt>=1000){ms_cnt=0;seconds++;}}g_running需要定义为volatile bit或volatile u8,避免编译器优化导致的读取异常。
六、常见问题
6.1 数码管显示重影 / 花屏
- 没消影:先关位选,再送段码,最后开位选
- 扫描太慢:每位控制在 1~2ms
- 位选引脚冲突:检查位选和段选是否共用端口
6.2 数码管亮度不均匀
- 各位显示时间不同 → 确保每位延时一致
- 限流电阻过大 → 适当减小(注意别超过 IO 灌电流上限)
- 位数太多 → 每位的有效点亮时间变短
6.3 定时器时间不准
- 忘记重装初值:方式1 必须在 ISR 里重新赋值
- ISR 执行时间过长:会挤占计时,导致累计误差
- 晶振频率不对:11.0592MHz 与 12MHz 初值完全不同
- 想更准 → 用方式2 自动重装,或改用 11.0592MHz 晶振
6.4 中断不触发
EA = 1忘了吗?- 对应中断允许位(
EX0/ET0)开了吗? - 外部中断触发方式(
IT0)设对了吗? - 按键没上拉,P3.2 一直是低电平(低电平触发时会导致反复进中断)
6.5 变量在 ISR 和主程序之间"不更新"
- 加
volatile:volatile u16 seconds; - 多字节变量(如
u16)在主程序读取时可能被中断打断,造成读到半新半旧的值。临界区可以这样处理:
EA=0;tmp=seconds;EA=1;七、总结
这一篇的核心就三句话:
- 数码管:多位显示靠动态扫描,扫描关键是消影。
- 中断:让 CPU 从"死等"变成"事件驱动",
EA=1+ 对应允许位 + 中断服务函数。 - 定时器:内部硬件计数,精准定时,是数码管扫描、按键消抖、串口波特率、PWM的通用基础设施。
把这三块吃透,就可以做很多综合项目了:
- 数码管电子钟(时/分/秒 + 按键调时)
- 秒表 / 计数器
- 红外遥控解码 + 数码管显示
- 温度采集 + 数码管显示
- 串口通信 + 上位机显示
下一篇可以聊串口通信(UART)和PWM 调光,那也是 51单片机非常实用的两块内容。
本文为学习笔记,代码已尽量精简可读,欢迎交流指正。如果对你有帮助,可以点赞收藏。