news 2026/9/12 21:22:32

51单片机进阶总结(二):数码管、中断与定时器(附完整代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51单片机进阶总结(二):数码管、中断与定时器(附完整代码)

文章目录

    • 前言
    • 一、数码管
      • 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 为最高位)

数字段码数字段码
00x3F50x6D
10x0660x7D
20x5B70x07
30x4F80x7F
40x6690x6F

共阳段码 = 共阴段码按位取反:

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. 关闭第 1 位,位选2拉低,段选送第 2 位数字 → 第 2 位亮
  3. ……依次快速循环

只要扫描速度够快(每位 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)00x0003P3.2 引脚电平/边沿
定时器0 (T0)10x000BT0 溢出
外部中断1 (INT1)20x0013P3.3 引脚电平/边沿
定时器1 (T1)30x001BT1 溢出
串口 (UART)40x0023接收/发送完成

2.2 中断相关寄存器

IE —— 中断允许寄存器
名称作用
EA总中断允许1=开总中断
ES串口中断1=允许
ET1定时器1中断1=允许
EX1外部中断11=允许
ET0定时器0中断1=允许
EX0外部中断01=允许

记住:任何中断要生效,必须 EA=1 且对应位=1。

TCON —— 中断触发与标志
名称作用
IT0INT0 触发方式1=下降沿,0=低电平
IE0INT0 请求标志硬件自动置位
IT1INT1 触发方式1=下降沿,0=低电平
IE1INT1 请求标志硬件自动置位
TR0 / TR1定时器启停1=启动
TF0 / TF1定时器溢出标志溢出时硬件置 1

实际开发中通常只操作IT0IT1

IP —— 中断优先级

默认同级中断不能嵌套。设置PX0=1PT0=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方式013 位定时器
0 1方式116 位定时器(最常用)
1 0方式28 位自动重装(串口波特率常用)
1 1方式3拆分使用

常用配置:

TMOD=0x01;// T0,方式1,定时器TMOD=0x10;// T1,方式1,定时器TMOD=0x21;// T1 方式2 + T0 方式1(串口场景)

保险的写法是先清零再置位:

TMOD&=0xF0;// 清 T0 的低 4 位TMOD|=0x01;// T0 方式1

3.3 定时初值计算

方式1是 16 位,最大值 65536。从初值计到 65536 溢出:

定时时间 = (65536 - 初值) × 机器周期 机器周期 = 12 / 晶振频率

12MHz 晶振:机器周期 = 1μs,一个计数 = 1μs。

定时时长计数次数初值THTL
1 ms1000645360xFC0x18
10 ms10000555360xD80xF0
20 ms20000455360xB10xE0
50 ms50000155360x3C0xB0

最大定时:方式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 bitvolatile 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 和主程序之间"不更新"

  • volatilevolatile u16 seconds;
  • 多字节变量(如u16)在主程序读取时可能被中断打断,造成读到半新半旧的值。临界区可以这样处理:
EA=0;tmp=seconds;EA=1;

七、总结

这一篇的核心就三句话:

  1. 数码管:多位显示靠动态扫描,扫描关键是消影
  2. 中断:让 CPU 从"死等"变成"事件驱动",EA=1+ 对应允许位 + 中断服务函数。
  3. 定时器:内部硬件计数,精准定时,是数码管扫描、按键消抖、串口波特率、PWM的通用基础设施。

把这三块吃透,就可以做很多综合项目了:

  • 数码管电子钟(时/分/秒 + 按键调时)
  • 秒表 / 计数器
  • 红外遥控解码 + 数码管显示
  • 温度采集 + 数码管显示
  • 串口通信 + 上位机显示

下一篇可以聊串口通信(UART)PWM 调光,那也是 51单片机非常实用的两块内容。


本文为学习笔记,代码已尽量精简可读,欢迎交流指正。如果对你有帮助,可以点赞收藏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 21:20:56

ZLUDA完整配置指南:AMD显卡跑通CUDA应用

ZLUDA完整配置指南&#xff1a;AMD显卡跑通CUDA应用 【免费下载链接】ZLUDA CUDA on non-NVIDIA GPUs 项目地址: https://gitcode.com/GitHub_Trending/zl/ZLUDA 如果你的应用只提供 CUDA 版本&#xff0c;你不必非得买 NVIDIA 显卡。ZLUDA 是面向非 NVIDIA GPU 的开源 …

作者头像 李华
网站建设 2026/9/12 21:20:01

Python还能这么跑?不用改代码,性能飙升50倍

还能这么跑&#xff1f;不用改代码&#xff0c;性能飙升50倍有段 脚本&#xff0c;平时跑一批对账文件要二十多分钟。代码没改&#xff0c;SQL 没改&#xff0c;机器也没换&#xff0c;只把启动命令从&#xff1a;python check_bill.py换成&#xff1a;pypy3 check_bill.py第二…

作者头像 李华
网站建设 2026/9/12 21:19:52

jQuery

关键知识点速览DOM 操作与选择器&#xff1a;利用 $(#id) 或 $(.class) 快速抓取页面元素&#xff0c;比原生 JavaScript&#xff08;document.getElementById&#xff09;简洁得多。事件绑定&#xff1a;通过 $(#btn).click(function() { ... }) 轻松处理点击、提交等交互事件…

作者头像 李华
网站建设 2026/9/12 21:16:20

Clipcat 评测:适合 TikTok Shop 卖家的 AI 提示词资源库吗?

如果你在做 TikTok Shop&#xff0c;大概率见过这样的工作状态&#xff1a;选品表、素材盘、剪辑软件、AI 对话工具一个不少&#xff0c;但每天要做新视频时&#xff0c;团队还是会卡在第一步。 “这条该从什么角度讲&#xff1f;” “同一个保温杯&#xff0c;除了开箱还能拍什…

作者头像 李华