
先把结论放在前面秒表程序看上去只是按两下按键、看几个数字跳变但它是51单片机学习路上一个绕不过去的整合型实验。天祥TX-1C开发板这块板子在学生手里流转了小二十年主控是STC89C52RC板载数码管、独立按键、蜂鸣器这些外设做秒表刚好把定时器、中断、动态扫描、按键消抖这几块硬骨头一锅端了。这篇文章我会从定时器初值怎么算、中断函数怎么写、数码管扫描为什么放主循环不放中断到最常见的计时不准、按键失灵、数码管重影逐层拆给你看。这套程序适合刚把跑马灯玩明白、准备进入定时器中断阶段的初学者也适合做课程设计想找一个说得清原理的参考实现。秒表这题目太经典了网上抄代码的一大把但真正能讲清楚为什么这么写的很少我这篇尽量把它讲透。1. 做秒表之前先把天祥TX-1C的硬件资源摸清楚1.1 这块开发板用到了哪些板载外设天祥TX-1C是郭天祥《十天学会单片机》配套的那块经典学习板整套设计就是为教学服务的。主控芯片是STC89C52RC带8K Flash、512字节RAM传统8051内核12T模式。板上东西很全8个LED接在P1口、8位数码管通过74HC138译码器选位、P0口做段选信号、4个独立按键、4x4矩阵键盘、有源蜂鸣器、DS1302时钟芯片、DS18B20温度传感器、LCD1602/LCD12864接口、串口和USB下载电路几乎把所有单片机入门外设集齐了。做秒表这个项目真正用到的板载资源其实只有三样8位数码管负责显示时间、2个独立按键负责启动暂停和清零、定时器中断负责计时。其他东西属于顺带能用比如蜂鸣器可以在计满或者按键按下时响一声做反馈但核心功能不依赖它。这里要特别记住一个信息这块板子的晶振是11.0592MHz不是12MHz。这个数值在后面算定时器初值时非常关键很多新手把初值抄错了根本原因就是没注意到晶振是多少。11.0592MHz这个频率在51世界里很常见因为它分频之后能精确产生9600波特率串口通信方便。但在定时器计时这件事上11.0592MHz反而让整毫秒变得稍微有点别扭后面我会详细算。1.2 秒表的功能拆解启动、暂停、清零背后是状态机秒表看着简单三个按键动作或两个按键组合但代码层面要解决的其实是三件事计时要准、显示要稳、按键要按一下是一下。稍微把功能想清楚一点就能写出结构清晰、不容易出 bug 的程序。我把秒表的状态拆成两个运行RUN和停止STOP。按键A负责切换状态运行的时候按一下变停止停止的时候按一下变运行这就是一个翻转开关按键B负责清零不管当前是运行还是停止只要按下清零时间变量全部归零显示回到0。如果是课程设计想要更花哨一点还可以把清零键设计成只在停止状态下有效或者在清零的同时自动回到停止状态这些都属于逻辑层面的小变化不影响底层结构。这个运行/停止的设计本质就是一个状态机。单片机程序最怕的就是逻辑打结用状态变量把所有可能的情况枚举清楚写出来就是几行if判断的事。我见过很多新手在这个地方翻车不用状态变量直接用两个按键分别控制启动和停止就会出现启动之后没停又按了启动时间会不会乱这种问题。状态机思想要从这个秒表程序开始建立。时间变量这边我习惯用三个变量minute分钟、second秒、centisecond百分秒再加上一个毫秒计数器cnt_ms。因为定时器中断设成1ms触发一次所以每进一次中断cnt_ms加1加到1000就是1秒second加1second加到60minute加1。百分秒其实不需要额外的中断源每10ms算一个百分秒这样显示最直观分:秒.百分秒。2. 定时器初值计算秒表走时准不准全看这一步2.1 为什么选定时器T0工作方式1是多少位的计数器STC89C52RC内部有两个16位定时器/计数器T0和T1。做秒表计时用T0还是T1完全看个人习惯我习惯用T0因为它是教材里第一个讲的定时器后面学串口的时候T1通常要留作波特率发生器从小养成T0做定时、T1留给串口的习惯能少走弯路。定时器有四种工作方式方式0是13位计数器方式1是16位计数器方式2是8位自动重载方式3是拆成两个8位。秒表程序我选方式1也就是16位计数器。原因很简单定时器的计数范围在11.0592MHz晶振、12分频下16位最大能定约71ms而8位最大只能定约0.28ms。秒表需要一个相对长的中断间隔1ms、10ms都可以8位计数器每次中断要自己重新装初值还要在中断里处理重装时机稍微不注意就会产生累积误差。16位方式1在中断里只要重新赋一遍初值就行简单可靠。定时器的工作方式由TMOD寄存器控制。T0用方式1的话TMOD的低四位配置成0001用十六进制写就是0x01。这条代码基本是固定的但你要明白它为什么是0x01而不是背下来就完事。TMOD高四位管T1低四位管T0方式1对应的二进制是0001合在一起TMOD 0x01就这么来的。2.2 1ms中断的初值计算全过程11.0592MHz晶振秒表计时需要一个时间基准这个基准就是定时器中断。中断触发间隔越短计时粒度越细但我选了1ms而不是更细因为秒表显示的最小单位是百分秒1ms中断足够支撑百分秒的精度而且1ms的初值计算和中断消耗时间都比较好控制。下面进入全篇最核心的计算环节我一步一步写晶振频率 f 11.0592MHz。51单片机的机器周期是12个晶振周期所以机器周期 12 / 11.0592MHz ≈ 1.085微秒这句话意思是定时器每计数一次实际经过的时间大约是1.085微秒。注意这里不是1微秒很多人默认12MHz晶振的机器周期是1微秒然后直接套公式初值就偏了。如果要让定时器每1ms也就是1000微秒中断一次需要计数多少次计数次数 1000微秒 / 1.085微秒 ≈ 922次定时器从初值开始往上计数计到65536溢出触发中断。所以初值 65536 - 922 64614转成十六进制64614 0xFC66所以 TH0 0xFCTL0 0x66。这里有个细节必须说清楚因为11.0592MHz不是12的整倍数922次计数实际经过的时间是922 x 1.085微秒 1000.37微秒比1ms多了0.37微秒。也就是说一个1ms中断实际是1.00037ms累积1秒会偏差约0.37ms一分钟偏差约22ms。对秒表这种显示到百分秒的场景来说这个误差大概率看不出来因为人眼对秒表准不准的感知主要看分钟级累积22毫秒的偏差完全在可接受范围。但如果你较真想用这块板子的晶振做高精度时钟那就得用软件校准。省事的做法是不把每1000次中断当成1秒而是通过实测1分钟走时误差调整中断里够多少ms算1秒的阈值。比如实测1分钟慢了30ms那就在秒计数那里每1000次中断悄悄加一点补偿这在后面扩展部分我会再提到。2.3 中断服务函数里的计数逻辑与变量设计定时器溢出会触发中断程序跳进中断服务函数。这个函数里做的事情越少越好我的原则是中断里只计数不做显示不做按键扫描。为什么因为中断服务函数执行时间过长会占用主循环的时间片如果显示扫描刚好需要中断配合两者互相抢占就会出现数码管亮度不均、闪烁之类的怪问题。中断服务函数大致长这样void timer0_isr() interrupt 1 { TH0 0xFC; TL0 0x66; // 重新赋初值保证每次中断间隔都是1ms cnt_ms; if(cnt_ms 1000) // 1秒到了 { cnt_ms 0; centisecond; if(centisecond 100) { centisecond 0; second; if(second 60) { second 0; minute; if(minute 99) // 最大显示99分钟防止溢出 { minute 0; } } } } }这里有两个常见坑。第一个坑是忘记重新赋初值定时器就会从0开始重新计数中断间隔变成固定的65536个机器周期约71ms秒表走时直接乱套第二个坑是把显示更新放在中断里破坏主循环的节奏。我的做法是中断里只更新那三个时间变量主循环里每次查询一次这三个变量把它们的值刷新到显示缓冲区。变量对这些操作是原子性的吗严格来说在8051上对int变量非原子操作可能存在读到一半被中断改掉的情况但因为主循环里读变量的操作很快而且就算偶尔读到新旧混合值下一轮扫描立刻会修正视觉上完全无感所以实战中没人较真这个问题。3. 数码管动态扫描和按键消抖看着简单坑不少3.1 动态扫描原理人眼视觉暂留如何被利用天祥TX-1C板上有8位数码管位选由74HC138译码器控制。数码管没办法同时亮起8位因为硬件上所有位共用同一组段选线所以只能一位一位地点亮先选中第1位把这一位要显示的数字段码送到P0口延时一小会儿然后关掉第1位再选中第2位送段码延时……以此类推。只要整个循环跑得足够快人眼因为视觉暂留效应看到的就是所有位同时在亮。这个原理说起来简单实际操作中有两个关键参数单次点亮时间不能太短否则亮度不够整个扫描周期不能太长否则会闪烁。我实测的经验值是8位扫一圈的时间控制在8ms到16ms之间也就是每位点亮1到2ms。如果主循环里没有其他耗时操作这个目标很容易达到。扫描函数我建议写成每次调用只送一位的结构而不是一次调用循环送完8位void display_scan(void) { static unsigned char i 0; // 记录当前扫描到第几位 P0 0xff; // 消隐先熄灭所有段防止切换位选时出现拖影 switch(i) { case 0: LSA 0; LSB 0; LSC 0; break; case 1: LSA 1; LSB 0; LSC 0; break; case 2: LSA 0; LSB 1; LSC 0; break; case 3: LSA 1; LSB 1; LSC 0; break; case 4: LSA 0; LSB 0; LSC 1; break; case 5: LSA 1; LSB 0; LSC 1; break; case 6: LSA 0; LSB 1; LSC 1; break; case 7: LSA 1; LSB 1; LSC 1; break; } P0 seg_code[display_buf[i]]; // 送段码 i; if(i 8) i 0; }主循环每次转一圈都会调用一次这个函数相当于每次只刷新一位8次循环刷新完8位。这样做的最大好处是主循环的其他任务按键检测、刷新缓冲区不会长时间阻塞显示亮度均匀也不会出现按一下键显示就闪一下的问题。天祥TX-1C的位选控制脚是P2.4、P2.5、P2.6也就是上面代码里的LSA、LSB、LSC段码走P0口这是板子原理图设计好的。3.2 显示缓冲区设计先拆数再查表再扫描秒表需要显示分、秒、百分秒各两位一共6位。8位数码管还空2位我的处理是前两位显示分钟中间两位显示秒后两位显示百分秒空出来的两位熄灭。显示格式大致是00:00.00但因为有2位空着实际上看起来是00 0000或者我干脆只用6位第7、8位不点亮。在代码层面不能直接把minute这个变量送去显示要把它拆成十位和个位两个数字。这就要引入一个显示缓冲区数组display_buf[8]每个元素存一位要显示的数字0到9。主循环里的刷新函数负责把minute、second、centisecond三个变量拆成6个数字填进缓冲区。void refresh_display_buf(void) { display_buf[0] minute / 10; display_buf[1] minute % 10; display_buf[2] second / 10; display_buf[3] second % 10; display_buf[4] centisecond / 10; display_buf[5] centisecond % 10; display_buf[6] 10; // 不显示 display_buf[7] 10; // 不显示 }为什么要把拆数和扫描分开因为拆数只在时间变量变化时才需要更新而扫描是每一轮都要跑的。如果每次扫描都现拆现算代码逻辑会很乱。把这两件事拆开各自干各自的活逻辑清晰调试也方便。段码表这里用共阳数码管的编码比如数字0对应0xC0数字1对应0xF9。天祥TX-1C板上用的是共阳数码管如果你手头的板子不是把段码表全部取反就行这个判断方法很简单写个测试程序给P0口送0xC0如果显示0就是共阳显示8或乱码换0x3F试试0x3F是共阴的0。3.3 按键消抖和防重复触发的处理机械按键按下和松开时触点会抖动几百微秒到几毫秒期间电平会跳变多次。如果不处理一次按键可能被单片机判断成按了好几次秒表就会出现按一下启动结果立刻又暂停的抽风现象。消抖有两种方案一种是检测到按下后延时10到20ms再确认电平另一种是每隔几毫秒采样一次连续多次采样都是低电平才认为确实按下。第一种在教材里最常见代码直观适合秒表这种按键频率不高的场景。但延时消抖有个问题delay(20)期间CPU卡在那里什么都不干如果数码管正在扫描就会闪一下。解决办法也简单消抖延时用短一点的10ms而且只在检测到电平变化时才延时平时主循环跑得飞快显示基本不受影响。防重复触发比消抖更容易被忽略。按键在消抖之后如果只判断电平而不处理松开事件按下不松手的状态下主循环每轮都会检测到低电平状态变量会被反复翻转表现出来就是秒表启动-停止-启动-停止乱跳。我的处理方法是确认按键按下并执行完动作之后用while等待按键释放没释放就卡在那里。这样一次物理按键只触发一个动作。天祥TX-1C板上的独立按键是按下为低电平、平时高电平我把启动/暂停键定义在P3.4清零键定义在P3.5。按键扫描放主循环里写法大概是这样void key_scan(void) { if(KEY_START 0) { delay_ms(10); // 消抖 if(KEY_START 0) { run_flag !run_flag; // 翻转运行状态 while(KEY_START 0); // 等待松开 } } if(KEY_CLEAR 0) { delay_ms(10); if(KEY_CLEAR 0) { minute 0; second 0; centisecond 0; cnt_ms 0; while(KEY_CLEAR 0); } } }这个写法有个细节判断完启动键之后清零键的判断还是会执行。如果启动键按下的同时清零键也是低电平理论上两个动作都会触发。实际体验中两个键同时按下的概率很低但如果你想做得更严谨可以在结构上让两个按键的判断互斥。另外delay_ms(10)这个延时函数在秒表里其实会影响主循环节奏但因为单次只有10ms在可接受范围内。高级一点的写法是用定时器做一个非阻塞的按键扫描状态机但那是后话了秒表这个级别用延时消抖完全够用。4. 完整秒表程序代码走读从初始化到主循环4.1 程序整体框架一个主循环加一个中断整个秒表程序的核心结构就是两部分初始化配置、无限主循环。定时器T0的初始化包括TMOD赋值、TH0和TL0赋初值、ET0开启T0中断、EA开总中断、TR0启动定时器。初始化完成后定时器就开始走中断开始触发主循环只需要反复做三件事扫描按键、刷新显示缓冲区、驱动数码管扫描。我把完整代码整理如下直接在Keil里建工程新建main.c粘贴进去就能编译下载。#include reg52.h #define uchar unsigned char sbit KEY_START P3^4; // 启动/暂停键 sbit KEY_CLEAR P3^5; // 清零键 sbit LSA P2^4; // 74HC138译码器位选线A sbit LSB P2^5; // 位选线B sbit LSC P2^6; // 位选线C uchar code seg_code[] { 0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e }; // 共阳数码管段码表 0~F uchar display_buf[8]; // 显示缓冲区 uchar minute 0; // 分钟 uchar second 0; // 秒 uchar centisecond 0; // 百分秒 uint cnt_ms 0; // 毫秒计数 bit run_flag 0; // 运行标志0停止1运行 void delay_ms(uint ms) { uint i, j; for(i 0; i ms; i) for(j 0; j 120; j); } void timer0_init(void) { TMOD 0x01; // T0方式116位定时 TH0 0xFC; TL0 0x66; // 11.0592MHz晶振1ms中断 ET0 1; EA 1; TR0 1; } void timer0_isr() interrupt 1 { TH0 0xFC; TL0 0x66; if(run_flag) { cnt_ms; if(cnt_ms 1000) { cnt_ms 0; centisecond; if(centisecond 100) { centisecond 0; second; if(second 60) { second 0; minute; if(minute 99) { minute 0; } } } } } } void refresh_display_buf(void) { display_buf[0] minute / 10; display_buf[1] minute % 10; display_buf[2] second / 10; display_buf[3] second % 10; display_buf[4] centisecond / 10; display_buf[5] centisecond % 10; display_buf[6] 10; // 10代表无内容 display_buf[7] 10; } void display_scan(void) { static uchar i 0; P0 0xff; switch(i) { case 0: LSA 0; LSB 0; LSC 0; break; case 1: LSA 1; LSB 0; LSC 0; break; case 2: LSA 0; LSB 1; LSC 0; break; case 3: LSA 1; LSB 1; LSC 0; break; case 4: LSA 0; LSB 0; LSC 1; break; case 5: LSA 1; LSB 0; LSC 1; break; case 6: LSA 0; LSB 1; LSC 1; break; case 7: LSA 1; LSB 1; LSC 1; break; } if(display_buf[i] 10) P0 0xff; // 空位不显示 else P0 seg_code[display_buf[i]]; i; if(i 8) i 0; } void key_scan(void) { if(KEY_START 0) { delay_ms(10); if(KEY_START 0) { run_flag !run_flag; while(KEY_START 0); } } if(KEY_CLEAR 0) { delay_ms(10); if(KEY_CLEAR 0) { minute 0; second 0; centisecond 0; cnt_ms 0; run_flag 0; while(KEY_CLEAR 0); } } } void main(void) { timer0_init(); refresh_display_buf(); while(1) { key_scan(); refresh_display_buf(); display_scan(); } }4.2 秒表核心代码逐段说明初始化这部分有一个很多人都忽略的细节清零按键那里我把run_flag也清成了0。意思是什么清零之后秒表自动回到停止状态而不是保持原来的运行状态。这个设计是故意的因为用户清零之后大概率想从0开始重新计时如果清零之后还保持运行数字会立刻翻动体验比较怪。当然如果你觉得清零但保持运行更顺手把run_flag 0去掉就行这种小逻辑根据自己的使用习惯调。中断服务函数里我用run_flag作为门控只有运行状态下才累加时间。这个做法省掉了在外面控制TR0开关定时器的步骤。很多人会纠结要不要在按键里直接TR0 0停表、TR0 1启表这两种思路都能实现。我用run_flag门控的好处是定时器始终在跑中断始终在触发main函数不需要关心定时器状态逻辑统一。坏处是中断每次都在进即使停止状态也白跑一趟但对51这种单片机来说这点开销可以忽略。刷新显示缓冲区这里有一个性能上的小优化点其实display_buf只在时间变量变化时才需要刷新而时间变量只在每次秒进位时才变化。理论上可以定义一个时间更新标志在中断里置位主循环检测到标志才调用refresh_display_buf。但我实测发现即使每轮主循环都刷新一次缓冲区代码执行时间也只有几微秒对显示扫描没有可感知的影响就保留了这个更直观的写法。写程序有时候不用过度优化先让逻辑直白好懂。4.3 几个容易忽略的代码细节第一个细节display_buf里我用了数字10来标记不显示。为什么不用-1或者其他值因为数组类型我定义成uchar是无符号字符型只能存0到255-1会被转成255判断反而不方便。用10很自然因为段码表只有0到15共16项10比9大不可能和正常数字冲突。当然你可以在段码表里给10单独定义一个全灭的编码但我更倾向于在送段码前判断这样段码表保持干净。第二个细节delay_ms延时函数里的循环次数120是我在11.0592MHz下直接试出来的近似值让这个函数大约延时1ms。严格来说这个循环延时和编译器优化级别有关如果开了高优化循环可能被优化掉延时就不准了。但这个函数在这里只用于按键消抖就算时间偏一点问题也不大。你要是追求严谨可以用逻辑分析仪实测调整或者干脆用定时器做一个非阻塞延时。第三个细节也是我踩过最深的坑display_scan函数里那个static局部变量i。static的意思是这个变量在函数退出后不会销毁下次进入函数时还保留上次的值。这个往小了说是写扫描函数的惯用法往大了说是在教你保存运行到哪一步了的现场信息。如果你把static忘了每次进函数i都从0开始数码管就永远只显示第1位其他7位全灭。5. 调试实录秒表跑起来之后这些问题最常见5.1 问题速查表先对号入座我把秒表调试中遇到过的典型问题整理成一张表方便你对号入座现象大概率原因排查方向走时明显偏快或偏慢定时器初值算错或晶振频率不是11.0592MHz重新计算初值确认板子晶振数码管亮度很不均匀某一位点亮时间过短或有一段位选逻辑写错检查display_scan里每位是否都执行到位数码管有拖影、重影位选切换前没有消隐或P0口输出没先清理扫描函数开头P0 0xff按键按一下触发两次消抖不够或没有等待松开加强消抖、加while等待释放按下清零数字不变按键端口引脚定义和原理图不一致查原理图确认KEY_CLEAR的引脚编译报错redefinition头文件或变量重复定义检查是否重复包含reg52.h下载程序后数码管全乱码上电时P0口未初始化显示缓冲区没刷新main开头先refresh_display_buf5.2 计时不准先怀疑初值再怀疑晶振计时不准是秒表程序里最让人头大的问题。我的排查思路是分层排除第一步用秒表或手机计时器做对拍。让单片机跑1分钟同时手机秒表计时。如果1分钟误差在几十毫秒级别初值计算基本没问题误差来自晶振频率和初值取整可以接受。如果误差达到几秒甚至十几秒初值大概率错了。第二步验证初值。把代码里的TH0和TL0注释掉在中断服务函数里随便置一个固定值比如TH0 0x00; TL0 0x00看看数码管是不是在快速乱跳。如果乱跳说明中断本身在触发问题在初值如果没反应检查ET0、EA、TR0有没有都开着中断向量号interrupt 1写没写对。第三步确认晶振。有示波器的直接测晶振引脚没有的话看板子上的晶振外壳11.0592MHz通常印着11.059212MHz印着12.000。万一真是初值误差累积导致的走时偏差偏小还好说偏大就要做软件校准。校准的思路是这样的实测1分钟走时假设快了0.36秒也就是360ms。这意味着每1分钟要多出360个1ms中断。可以把秒进位的判断从cnt_ms 1000改成cnt_ms 994也就是每1000次中断少计6次一分钟恰好少360次走时就准了。这个粗略校准在课程设计答辩的时候很好用实测从肉眼可见偏快调整到和手机秒表几乎同步。5.3 数码管闪烁、重影八成是扫描节奏的问题数码管显示问题可以从现象反推原因。闪烁是扫描周期太长人眼能察觉出亮灭变化通常意味着主循环里有大延时或者display_scan没有被频繁调用。重影或者叫拖影是切换位选时上一位的余晖串到了下一位因为P0口段码还没换位选就已经切过去了。解决办法就是扫描函数开头那句P0 0xff先把段码全部置高共阳管全灭再切位选再送新段码。还有一个隐蔽的坑STC89C52RC的P0口是开漏输出不能输出高电平驱动数码管段选所以TX-1C板子在P0口到数码管之间加了上拉电阻。如果你是在面包板上自己搭电路忘了加上拉数码管会非常暗。板载电路不存在这个问题但用洞洞板复刻的同学注意了。如果数码管在按下按键的瞬间闪烁一下那基本就是按键消抖的delay在作怪。10ms的delay卡住了主循环数码管扫描暂停了10ms亮度瞬间降一截。高情商的做法是把消抖改成非阻塞式用时间戳判断低情商但有效的做法是保持现状因为10ms的闪烁肉眼其实不容易捕捉到。我自己写秒表这类简单程序时通常会选择后者把精力花在更重要的逻辑上。6. 后续还能怎么扩展这个秒表程序秒表程序做完只是开始它像一个毛坯房后续想怎么装修都行。我个人强烈建议在现有基础上做以下几个方向的扩展每一个都能把知识点往深挖一截。第一个方向加蜂鸣器反馈。天祥TX-1C板载有源蜂鸣器按下按键响一声、计满99分钟报警这些功能本质上都是在一个事件发生时让蜂鸣器引脚翻转产生声音。有源蜂鸣器只要给高低电平就能响程序实现非常简单但可以让作品的完成度提升一个档次课程设计拿出去展示观感完全不一样。第二个方向引用状态机思维重构按键逻辑。现在按键扫描是简单地判断电平、翻转标志但如果你把空闲-按下-确认-释放这几个状态用变量枚举出来写成一个专门的按键状态机后面做矩阵键盘、做复杂菜单时就能直接复用。天祥TX-1C板上还有矩阵键盘很多课设题目会要求用矩阵键盘输入时间或者切换模式那时候按键状态机这套东西就派上大用场了。第三个方向掉电保存计时数据。STC89C52RC内部有EEPROM可以在断电前把分钟、秒、百分秒写进EEPROM下次上电恢复。这对秒表计时到一半突然断电的场景很实用。代码上就是调STC的IAP读写函数难度不大但能让你把数据存储的概念摸清楚。第四个方向也是我自己做秒表时觉得最有意思的用串口把计时数据实时发到上位机。1ms中断每秒钟产生一次秒数据用串口发送一个字符串00:01:23到电脑的串口助手电脑端就能实时看到秒表状态相当于给秒表加了一个远程显示。这背后就是51单片机串口通信、波特率发生器、中断发送这些知识正好把T1定时器用起来。如果后续要接蓝牙模块、ESP8266之类的无线器件这个串口发送的函数就是现成的底子。第五个方向显示增强。把数码管显示格式做成00-00-00带分隔符或者利用TX-1C板上的LED灯做秒表心跳指示每秒闪一下LED。这些改动都能加深对板载资源灵活运用的理解。我个人在做完秒表之后的最大收获不是学会了怎么写一个具体的秒表而是弄懂了时间片这个概念。51单片机是单核的同一时刻只能干一件事但通过中断和主循环的配合它可以一边计时一边扫描数码管一边检测按键这三件事看起来是同时发生的。秒表程序把这种并发的雏形摆到你面前理解透了这个模型后面学更复杂的单片机、上操作系统思路都会顺很多。建议你烧录成功之后别急着做下一个项目先把定时器初值重新算三遍把每个寄存器的位含义默写一遍再把整个程序的执行流程画成时间线画完你就能体会到这玩意儿的价值了。