H43 用一片 51 单片机做两位数码管秒表:0.1 秒精度靠定时器 10ms 中断累计十次,K1 一键启动/暂停/继续,K2 清零,暂停时定时器照跑只是不采纳结果,显示函数自带 10ms 节拍在主循环里动态扫描,小数点靠段码最高位清零点亮,附 Keil 程序、Proteus 仿真与设计报告。

01 设计任务
功能介绍:
使用51单片机做一个两位数码管秒表,两个按键操作,要求如下:
1、0.1 秒计时:定时器 10ms 中断,累计 10 次得到 0.1 秒,两位数码管显示"秒.十分位"(如 3.5 表示 3.5 秒);
2、一键启动/暂停:K1 键启动计时,再按一次暂停(数值保持不动),再按继续——一个键管两件事;
3、K2 清零:回到 00.0 并停住,之后按 K1 从零重新开始;
4、满 99 回绕:计时值到 99(9.9 秒)后归零,继续循环。
特别注意:这是本系列第一个"正向计时"的设计。前面的 H27 抢答器、H29 洗衣机都是倒计时,H30/H31 是外部事件计数——它们的共同点是"从设定值往零走"或"别人给我脉冲",而秒表是"自己的时基在走,我只决定走不走"。
差别最直接体现在代码里:秒表的核心根本不是显示,是一个三态的 mode 变量(停止/计时/暂停)。全篇 106 行,显示占了 20 行,状态逻辑加起来不到 10 行——秒表 = 时基 + 状态机 + 显示,三块拼起来就这么简单。
02 硬件:两位数码管 + 两个按键
这是全系列最省的一个设计,一共就五样东西:


| 部件 | 引脚 | 说明 |
| 两位共阳数码管 | 段码 P0(经 RP1 排阻上拉)、位选 P2.0/P2.1 | 左边位显示秒的整数、右边位显示十分位 |
| K1 启动/暂停 | P3.6,另一端接地 | 靠 P3 内部上拉,按下为低 |
| K2 清零 | P3.7,另一端接地 | 同上 |
| 晶振 | XTAL1/XTAL2,C1/C2 22pF | 12MHz(定时器 10ms 用) |
| 复位 | C3 10uF + R1 100Ω | 上电复位 |
两个细节看图就能确认:
一是 P0 必须上拉。 段码从 P0 出来,先过 RP1 排阻再到数码管——P0 是开漏结构,不接上拉就读不到高电平、屏幕不亮。这几乎是每篇用 P0 驱动外设时的固定动作了。
二是按键直接接地。 K1、K2 一端接 P3.6/P3.7,另一端直接到地,没有外接上拉电阻——因为 P1/P2/P3 内部有弱上拉(约几十 kΩ),按键不按时读到高电平、按下时被拉到地。P0 就没有这个待遇,所以 P0 必须外接上拉。记住这条:P0 要上拉,P1~P3 可以省。
位选用的是"两脚分别选通":S1 = P2^0、S2 = P2^1,共阳数码管的公共端接 P2(高电平选通)。两位一体、段码共用,这就是动态扫描的最小形态——比 H23 的 74HC138 位选、H37 的三极管位选都简单,因为它只有两位、电流也小。
03 0.1 秒是怎么数出来的
秒表的时基在 T0 中断里,逻辑非常直白:
定时器初始化只干四件事:把 T0 设成方式 1(16 位),把 65536 减 10000 拆进 TH0 和 TL0 当 10ms 初值,打开 T0 中断允许位,然后启动 T0 并开总中断。中断服务函数一进来先按同样的算式重装一次初值,之后按 mode 分三支:为 0 时把 Time 和 temp 一起清零;为 1 时走两级累加,Time 超过 99 就回绕成 0;为 2 时整个函数什么都不做,Time 保持原值。
两层计数是秒表的核心结构:
· 第一层 temp:数"10ms 中断了几次",满 10 次(100ms)进位;
· 第二层 Time:0.1 秒的计数,直接就是显示值(0~99)。
显示时 Time = 35 就显示 3.5——把"显示格式"和"存储格式"绑在一起,不做"秒 + 毫秒"的分离存储。这是这份程序最省事的取舍:只需要 0.1 秒精度,就没必要拆成"秒""十分位"两个变量。
精度问题值得说一句。 12MHz 晶振下,一个机器周期是 1μs,10ms 需要 10000 个机器周期,初值 65536-10000 完全正确。但中断响应本身有几个机器周期的延迟,每次重装又有一点误差——累积几十次之后,走时会比真实时间慢一点点。做秒表、时钟这类要求长时准确的设备,正规做法是:要么改用自动重装的方式 2 减小误差、要么用时钟芯片(DS1302/PCF8563)做基准。教学用的秒表,这点误差看不出来,可以不管。
04 一个按键管启动和暂停
K1 分支里用 mode 是否等于 1 当条件,两种结果分别是把 mode 赋 1、把 mode 赋 2;K2 分支不用判断,直接赋 0。
看第一行 if(mode != 1) mode = 1;——判断的是"不等于计时中",所以从"停止(0)"和"暂停(2)"两个状态按下去都会进入计时。一行代码同时实现了"启动"和"继续"两个动作。
这就是用状态而不是用动作来组织逻辑的好处。如果按"动作"思路写,就得记"上一次是什么状态、这次该干什么",还得防重复触发;而按"状态"思路写,只需要回答一个问题:按下去之后我该处于哪个状态?
三个状态各自的行为:
| mode | 含义 | 中断里的行为 | 显示 |
| 0 | 停止/已清零 | 每一拍都把 Time、temp 清零 | 一直 00.0 |
| 1 | 计时中 | 每 10 拍 Time++ | 数值跳动 |
| 2 | 暂停 | 什么都不做,Time 保持不变 | 定格 |
暂停的实现特别值得学:它不是"停掉定时器"(TR0=0),而是定时器照常跑、只是不加。好处是恢复时不需要重新初始化定时器、也不会有重装初值的误差;坏处是定时器白跑——对一个秒表来说完全无所谓。
如果写成"暂停就 TR0=0",恢复时得记得 TR0=1、还要重装初值,多两个易错点。"让外设一直跑,只控制要不要采纳它的结果",是比"开关外设"更稳的思路。
另外注意 mode=0 时的清零写在中断里,不是按键处理里——这意味着只要处于停止态,每一拍都在清零。这样处理有个好处:按 K2 之后哪怕 Time 因为某种原因是脏的,下一拍立刻归零,界面永远是干净的 00.0。这就是"清零是一种持续状态,不是一次性动作"。
05 显示:自带节拍的动态扫描
Display() 每调用一次就把两位各点亮 5ms,一轮 10ms:

左位和右位各点亮 5 毫秒,两次之间夹一次全灭;左位送出的是带小数点的十位段码,右位送个位段码。
三步骤的顺序不能乱:先全灭(P0=0xFF)、再换位选、最后送段码。如果先送段码再换位选,切换的那一瞬间新一位会"继承"上一位的段码,出现鬼影(H23、H26、H30、H38 都踩过这个坑,这里是最小复现版)。
小数点怎么来的?table[num/10] & 0x7f——共阳数码管的段码最高位(bit7)是小数点 DP,& 0x7f 把它清 0,共阳是低电平点亮,小数点就亮了。所以"个位带小数点"不是加了什么器件,只是把这一位送出去的字节改了一下。同理 H38 讲过的"仅 g 段亮 = 负号(0xbf)",也是靠改字节。
这份程序最特别的地方是:显示不用定时器刷。 前面大部分篇目(H26、H27、H30、H37、H38)都是把显示扫描挂在定时器中断上,保证刷新节拍绝对均匀。这里放在了主循环里:
主循环的循环体只有三件事:取一次键号,按键处理里更新 mode,最后把 Time 交给显示函数画一遍;画完立刻回到循环开头。
因为 Display() 里面自带两个 Delay_1ms(5),一轮正好 10ms,主循环的节奏就被显示函数"带"起来了——刷新率 100Hz,人眼完全看不出闪。
这是一种"偷懒但有效"的写法,代价是:主循环被显示函数占满,按键扫描也只能每 10ms 做一次。对秒表够用(人按键不可能快过 10ms),但如果哪天要加"同时测温 + 通讯",这种写法就会成为瓶颈——那时候必须把显示挪到定时器中断里。能用主循环刷显示的前提是:主循环没有别的实时任务。
06 只有 9.9 秒?量程怎么扩
有个细节必须指出来:Time 是 uchar,if(++Time > 99) Time = 0;——最大只能显示 9.9 秒,超过就回绕到 00.0。
对课堂演示(跑个几秒)完全够,但真要做"60 秒秒表",得动三处:
1. 显示位数:两位不够了,要么加位(三位/四位数码管 + 位选扫描),要么改显示格式(MM:SS 用两位反而不够,得四位);
2. 计数变量:改成"秒 + 十分位"两个变量,或者用 uint 存"多少个 0.1 秒",显示时再拆;
3. 回绕条件:> 99 改成对上层的进位(十分位满 10 进 1 秒、秒满 60 进 1 分)。
所以看别人代码里的 > 99 这种魔法数字,要意识到它其实是量程的天花板——不是随便写的一个数,而是"两位显示能表达的最大值"。
07 排错清单
1、数码管不亮/全黑:先查 P0 有没有接 RP1 排阻上拉(P0 开漏),再查共阳数码管的公共端(位选)有没有接到 P2.0/P2.1 且为高电平;
2、显示一直是 00.0,按 K1 不动:mode 没进 1,或者进了 1 但定时器没开(TR0)。检查 Time_Init() 有没有被调用、ET0/EA 有没有置位;
3、按 K1 反而清零了:K1、K2 的引脚搞反了(K1=P3.6、K2=P3.7),或者按键一端没接地;
4、计时速度不对(偏快/偏慢):晶振频率与初值不匹配。65536-10000 是按 12MHz(1μs/机器周期)算的 10ms;用 11.0592MHz 晶振会慢约 8%;
5、数码管有鬼影/重影:消隐顺序错了,必须"先 P0=0xFF、再换位选、最后送段码"(见 05 节);
6、小数点不亮或常亮:& 0x7f 写错(写成 & 0xf7 会点到别的段),或者段码表不是共阳表(这份是 0xC0 起,共阴要换 0x3F 起那套);
7、跑到 9.9 秒后突然归零:不是 bug,Time>99 的设计上限(见 06 节),要更长就扩量程。
08 总结
秒表虽小,把三件基础功都占了:
· 时基:定时器 10ms + 软件分频到 100ms,这是所有计时类设计的地基(H27 的 30 秒倒计时、H29 的分钟倒计时都是这个套路);
· 状态机:mode 三态,一个按键管"启动/继续",判断的是状态而不是动作;
· 显示节拍:动态扫描 + 消隐顺序 + 小数点靠段码位,这套动作在这里是最小清晰版。
如果只记一句话:暂停不是"停定时器",而是"定时器照跑、不采纳结果";清零不是"按一下清一次",而是"停止态下一直清"。
下一篇继续,往更复杂的显示和协议上走。
欢迎关注,持续更新!
09 文件日期修改
如果你想把文件修改为最新的日期,又不想新建工程,那么你可以参考下面的文章。
"如何不打开程序的情况下更改proteus和keil程序的日期?"
工具和仿真文件在一起,见下面的下载链接。
10 资料下载
全套资料包含 Keil 工程(C 语言源码 + hex 文件)、Proteus 仿真工程、设计报告、程序流程图、讲解视频,以及编译截图和仿真截图。下载后可直接打开运行,也可用于课程设计、毕业设计参考。
也可以关注公众号「单片机仿真」,回复 H43 获取下载入口。公众号内还有 A / T / H 全系列 400 多个单片机仿真实例,回复「目录」可查看完整清单。

