资讯动态

51单片机秒表程序设计:定时器中断与数码管动态扫描实战

发布时间:2026/10/5 1:20:24 来源:尧图企业网站定制
用天祥TX-1C单片机开发板写一个秒表程序几乎是每个学51单片机的人都会碰到的“标准动作”。这块板子上有STC89C52主控、8位数码管、独立按键、LED和蜂鸣器资源不算豪华但定时器、中断、数码管动态扫描、按键消抖这些单片机基础模块恰好都能在一个秒表项目里串起来。很多人拿到开发板后跟着教程点亮几个LED、跑个流水灯就觉得自己会了可真到要自己写一个能开始、暂停、复位的秒表时才会发现中断和动态扫描之间有那么多的坑要填。这篇文章我就以自己的实际调试经历为线索把TX-1C开发板上做秒表程序的完整思路、原理解析、代码实现、烧录验证和常见问题一次讲透。不管你是刚学完C语言和GPIO操作、正在做课程设计还是想把手头开发板真正玩明白的入门爱好者都可以照着我这套方案直接上手。代码用Keil C51编写下载到板子上就能跑烧录部分也会说明STC-ISP的操作细节。1. 项目概述与需求拆解1.1 TX-1C开发板的硬件家底天祥TX-1C是一块很经典的51学习板核心芯片是STC89C52RC属于8051内核片上有8KB Flash程序存储、512字节RAM、3个定时器/计数器T0、T1、T2、6个中断源典型工作频率12MHz。对做秒表这个项目来说板上真正用到的资源就三类定时器、数码管、按键。具体硬件连接方面TX-1C的8位数码管是共阴结构段选通过74HC573锁存驱动位选也通过74HC573锁存驱动两个锁存器的LE控制引脚分别是P2.6和P2.7数据输入都挂在P0口上。独立按键有4个常见接法是K1接P3.0、K2接P3.1、K3接P3.2、K4接P3.3按下时引脚被拉低。如果你手里的板子接线不同稍微改一下引脚定义就行逻辑不用变。这块板子的设计思路很清楚P0口加上拉电阻后作为数据总线通过锁存器分时复用所以它的LED、数码管、液晶背光都共用P0口。好处是省引脚坏处是操作前必须搞清楚当前操作的锁存器是谁。写代码时最忌讳的就是忘记切换锁存器疯狂改P0但数码管死活没反应十有八九就是段选和位选锁存顺序搞错了。1.2 秒表需要哪些核心功能一个能拿得出手的秒表程序至少要满足三个基本需求可以随时启动和暂停计时、可以一键归零重新开始、显示界面要稳定不闪烁。往细了看还隐含着几个隐藏需求精确到十分之一秒或秒级的时间基准、四位数码管的动态刷新、按键防抖与防误触。我设计的版本用了四位数码管显示“分钟:秒钟”的格式比如“00:00”、“01:35”。中间用第二位数码管的小数点当作分隔符视觉上类似电子表上的冒号。考虑到秒表的实际使用场景复位键按下后无论当前是否在计时都立即清零并且回到暂停状态这个逻辑更符合“重新开始”的心理预期。K1负责启动/暂停切换K2负责归零按键逻辑清晰用户上手零学习成本。1.3 方案选型定时器、中断、动态扫描怎么搭实现计时的方案无非三条路软件延时、专用于延时的定时器查询、定时器中断。软件延时在按键扫描期间CPU会被卡住秒表走时必然受影响定时器查询虽然不用中断但主循环必须不停读定时器标志显示刷新和按键检测会被挤占。所以最稳妥的方案是定时器0工作在方式116位计数模式产生50ms固定间隔的中断中断里对50ms次数累加每20次进1秒。主循环专心做两件事扫描按键、刷新数码管。数码管显示只有4位动态扫描非常简单。所谓动态扫描就是同一时刻只点亮一位数码管快速轮流点亮第0位、第1位、第2位、第3位利用人眼视觉暂留效应让人感觉四位数码管同时亮着。扫描频率要大于50Hz我的写法里一次主循环刷新一位4次循环完整轮一圈速度远高于肉眼极限显示稳定不闪。2. 三个关键技术点的原理解释2.1 定时器为什么能“计时”STC89C52的定时器本质上就是一个加法计数器。T0工作在方式1时是16位的计数器计数脉冲来自系统时钟经过12分频后的机器周期。板载12MHz晶振时机器周期就是1微秒计数器每微秒加1。从0加到65535溢出需要65536微秒约65.536毫秒。我们想让定时器每50ms产生一次中断就需要提前给TH0和TL0装入一个初值让计数器从初值开始跑等它加到65536溢出时正好过去50ms。初值计算是65536 - 50000 15536换算成十六进制是0x3CB0所以TH0 0x3CTL0 0xB0。代码里用(65536 - 50000) / 256和(65536 - 50000) % 256分开写这样一眼就能看出逻辑修改计时周期时也只需要改50000这个数。中断产生后我在中断服务程序里对cnt_50ms做累加累加到20就说明已经过了1秒然后把秒计数器加1。这种“先设初值、再溢出中断、中断里累计”的做法是51单片机所有精准延时项目的通用模板只要晶振频率稳定秒表走时就会非常准确。2.2 数码管动态扫描是怎么回事共阴数码管内部是多个LED段共用一个阴极段选控制哪些段亮位选控制哪一位通电。TX-1C用了两块74HC573做锁存锁存器的LE引脚为高电平时输出跟随P0数据变化LE拉低后输出保持之前的数据这样P0就可以腾出来去操作另一个锁存器。动态扫描的关键在于“先选位、再送段”。我的刷新函数先把段选锁存器打开P0输出0x00把当前段的显示全部灭掉这样可以防止切换位选时残留上一轮段码造成拖影接着打开位选锁存器P0输出位选数据选中当前要显示的数码管最后再打开段选锁存器P0输出当前位的段码。这个过程用生活中的例子来比喻就像打地鼠游戏里快速指挥四个人轮流举手谁举手谁亮牌速度快了大家看到的就是四个人同时举着牌子。2.3 按键消抖怎么消才不闪屏机械按键在按下和松开的瞬间金属触点会因弹性产生几毫秒到十几毫秒的抖动波形就是连续的高低电平跳变。如果不去抖一次按下可能被误判成多次触发秒表就会出现按一下跳好几秒的情况。最常见的消抖方法是软件延时消抖检测到按键为低后延时10ms再检测一次如果还是低就确认按下有效。10ms这个时间足够避开机械抖动又不会让用户觉得按键迟钝。不过这里有个坑很多新手在按键消抖时直接调用一个大延时函数比如DelayMs(100)导致主循环里的数码管刷新被中断整个显示明显闪一下。我的做法是消抖延时控制在10ms左右并且只在待机状态下扫描按键运行状态下每次主循环也会快速刷新显示这样同时满足了防抖和显示稳定两个要求。3. 代码实现与核心环节拆分3.1 工程结构、引脚定义与全局变量用Keil C51新建工程时芯片型号选STC89C52RC或者Atmel 89C52都可以反正内核一样。工程里只需要一个源文件我习惯命名为main.c把所有代码集中在一个文件里因为这个项目还不到拆成多文件的体量。代码开头是头文件和引脚宏定义。这里要特别强调TX-1C的段选和位选锁存引脚是固定的P2.6和P2.7如果换其他开发板必须先看原理图确认。全局变量一共五个显示缓冲区disp_buf[4]、秒计数器sec、分钟计数器min、50ms中断累加器cnt_50ms、运行标志位running。running用bit类型定义节省RAM也方便直接赋值这是51单片机C语言里的经典小技巧。#include reg52.h sbit DU P2^6; // 段选锁存TX-1C上接74HC573的LE sbit WE P2^7; // 位选锁存TX-1C上接74HC573的LE sbit K1 P3^0; // 启动/暂停键 sbit K2 P3^1; // 复位键 unsigned char code seg_code[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; unsigned char disp_buf[4] {0, 0, 0, 0}; unsigned char sec 0; unsigned char min 0; unsigned char cnt_50ms 0; bit running 0;段码表对应共阴数码管的0到9比如显示数字0时需要点亮a、b、c、d、e、f六段二进制是0b00111111也就是0x3F。如果发现显示的数字缺笔画或者多笔画优先检查段码表而不是怀疑硬件。3.2 定时器初始化与中断服务程序定时器0初始化四步走设置TMOD、装初值、开中断、启动定时。TMOD的低四位控制T0TMOD 0xF0是为了只改低四位、不动高四位高四位是T1的配置。方式1就是两个8位组成16位计数器模块代码如下。void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // T0方式116位定时 TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; ET0 1; // 允许T0中断 EA 1; // 开总中断 TR0 1; // 启动T0 } void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; cnt_50ms; if (cnt_50ms 20) { cnt_50ms 0; if (running) { sec; if (sec 60) { sec 0; min; if (min 100) min 0; } } } }这里要特意说一下方式是方式1不是自动重装模式所以进入中断后必须重新给TH0和TL0赋值。错过这一步定时器会从0x0000接着跑计时周期变成65.536ms秒表会明显越走越慢。另外我在复位键逻辑里会把cnt_50ms也清零这样归零后下一次秒数更新是从完整的50ms开始计算不会出现复位后第1秒只走了0.9秒就跳变的情况。3.3 数码管显示驱动显示驱动是整个程序的“门面”写得不好会直接看到闪烁、拖影、亮度不均。我的方案是按“灭段、选位、送段”的顺序封装成一个原子操作每次调用只操作一位。显示缓冲区更新和硬件刷新分开主循环里先更新数据再刷新当前位逻辑清晰。void Display_OneBit(unsigned char pos, unsigned char num, bit point) { DU 1; P0 0x00; // 段选全灭消除残影 DU 0; WE 1; P0 0x01 pos; // 选中第pos位数码管 WE 0; DU 1; P0 seg_code[num]; if (point) P0 | 0x80; // 点亮小数点 DU 0; } void RefreshDisplay(void) { static unsigned char pos 0; Display_OneBit(pos, disp_buf[pos], (pos 1)); pos (pos 1) 0x03; } void UpdateDispBuf(void) { disp_buf[0] min / 10; disp_buf[1] min % 10; disp_buf[2] sec / 10; disp_buf[3] sec % 10; }位选数据使用的是0x01 pos也就是第0位选中左边第一位数码管第3位选中右边第四位。由于TX-1C的78HC573输出端在LE高电平时跟随输入变化所以在P0赋值前要先保证锁存器处于打开状态赋值后再立刻关闭锁存保证P0在切换第二个锁存器时数码管显示内容不变。3.4 按键扫描与主循环按键扫描放在主循环里用“检测低电平、延时消抖、再检测低电平、等待释放”四步流程。等待释放的while (K1 0);看起来有点粗暴但实际上能避免按住按键时反复触发切换。缺点是长按期间主循环卡住数码管会短暂停止扫描但人眼对10毫秒级停顿感知不强实际使用中影响很小。void ScanKey(void) { if (K1 0) { DelayMs(10); if (K1 0) { running !running; while (K1 0); } } if (K2 0) { DelayMs(10); if (K2 0) { running 0; sec 0; min 0; cnt_50ms 0; while (K2 0); } } } void main(void) { Timer0_Init(); while (1) { ScanKey(); UpdateDispBuf(); RefreshDisplay(); } }主循环四个环节扫描按键、更新显示数据、刷新一位数码管然后回到起点继续。这里有一个容易忽略的小细节RefreshDisplay每循环一次只刷新一位四位数码管合起来需要四次循环才完整刷新一轮但每次循环还穿插了按键扫描和显示数据更新整体循环周期仍然很短人眼看过去四个数字就是同时亮着的。3.5 完整可直接烧录的源码清单考虑到方便你直接对照验证我把完整代码放在这里。这个版本就是我在TX-1C上实测跑的功能为K1启动/暂停、K2复位、四位共阴数码管显示“分钟:秒钟”。换到其他开发板请先确认锁存引脚和按键引脚再改动定义部分。#include reg52.h sbit DU P2^6; // 段选锁存TX-1C上接74HC573的LE sbit WE P2^7; // 位选锁存TX-1C上接74HC573的LE sbit K1 P3^0; // 启动/暂停键 sbit K2 P3^1; // 复位键 unsigned char code seg_code[10] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; unsigned char disp_buf[4] {0, 0, 0, 0}; unsigned char sec 0; unsigned char min 0; unsigned char cnt_50ms 0; bit running 0; void DelayMs(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 125; j); } void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; ET0 1; EA 1; TR0 1; } void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 50000) / 256; TL0 (65536 - 50000) % 256; cnt_50ms; if (cnt_50ms 20) { cnt_50ms 0; if (running) { sec; if (sec 60) { sec 0; min; if (min 100) min 0; } } } } void Display_OneBit(unsigned char pos, unsigned char num, bit point) { DU 1; P0 0x00; DU 0; WE 1; P0 0x01 pos; WE 0; DU 1; P0 seg_code[num]; if (point) P0 | 0x80; DU 0; } void RefreshDisplay(void) { static unsigned char pos 0; Display_OneBit(pos, disp_buf[pos], (pos 1)); pos (pos 1) 0x03; } void UpdateDispBuf(void) { disp_buf[0] min / 10; disp_buf[1] min % 10; disp_buf[2] sec / 10; disp_buf[3] sec % 10; } void ScanKey(void) { if (K1 0) { DelayMs(10); if (K1 0) { running !running; while (K1 0); } } if (K2 0) { DelayMs(10); if (K2 0) { running 0; sec 0; min 0; cnt_50ms 0; while (K2 0); } } } void main(void) { Timer0_Init(); while (1) { ScanKey(); UpdateDispBuf(); RefreshDisplay(); } }4. 烧录、调试与常见问题排查4.1 烧录流程与必须注意的细节STC系列单片机用的是串口下载烧录前需要接一根USB转TTL线RX接板子的TX、TX接板子的RX还要共地。TX-1C板上有现成的串口电路直接用USB线通过板载CH340或者外接USB转TTL模块都可以。打开STC-ISP烧录软件选择单片机型号STC89C52RC打开编译生成的hex文件波特率默认就好然后在软件里点“下载/编程”。关键一步来了STC芯片是“冷启动”下载要先让开发板断电点击下载按钮后再给开发板上电软件才能给芯片写入程序。用一句话概括就是“先点下载再上电”。顺序反了或者板子一直上着电软件就会一直卡在“正在检测目标单片机”的提示上。烧录成功后开发板会立刻运行新程序。正常现象是数码管显示00:00按K1开始计时K2归零。如果程序没反应先别急着改代码检查三件事电源指示灯亮了没有、串口接线有没有接反、烧录时是否执行了“断电再上电”的操作。这三个问题占了新手烧录失败原因的九成。4.2 常见故障速查表我在调试过程中把最容易踩的坑整理成了一张速查表你可以对照排查。症状可能原因解决办法上电后数码管乱码或者完全不亮段选和位选锁存引脚搞反确认原理图交换P2.6和P2.7定义显示数字缺笔画或多笔画共阴/共阳段码表用错换用正确段码表共阳则取反按按键没反应按键引脚定义错误或P3口被其他功能占用检查K1/K2接的是不是P3.0/P3.1秒表走时明显偏慢定时器没有重新赋初值确认中断里重写TH0和TL0秒表走时偏快定时器初值计算有误或晶振不是12MHz重新计算初值适配实际晶振显示有拖影残影切换位选前没有灭段在选位前先让段选输出0x00按一下K1连续切换多次缺消抖或消抖延时太短延时消抖加到10ms左右复位后秒表第一次跳秒不均匀复位时没清零cnt_50ms复位逻辑里把cnt_50ms赋为0按住按键时数码管闪烁等待释放的while卡住主循环接受少量闪烁或改为边沿触发方式这里重点讲两个容易忽略的问题。第一个是P0口是开漏输出如果没有外部上拉电阻驱动74HC573会不稳定好在TX-1C板上已经给P0加了上拉排阻所以你不用额外处理。第二个是中断里的操作要尽量精简不要在中断里调用耗时的显示函数或延时代码否则会破坏定时精度即使显示速度还行秒表也会随着显示内容变化出现细微走时偏差。4.3 调一套秒表程序的调试思路很多初学者遇到问题喜欢从头到尾重写代码这是很低效的做法。我建议用“先分后合”的思路排查先写一个只点亮一位数码管显示固定数字的程序确认段选和位选锁存都能正常控制再写一个四位数码管轮流刷新显示固定数字的程序确认动态扫描没问题然后加入定时器中断用秒数变量驱动LED翻转来验证中断周期是否正确最后再把按键加进去。每加一个模块就烧录一次验证出问题的时候范围就很明确。比如显示固定数字正常、按键单独测试也正常但合在一起后按键反应变慢那基本可以断定是消抖延时太长或者主循环被某个操作卡住。这个“增量验证”的方法比一次写完再从头查编译错误要可靠得多也符合真实项目开发的节奏。5. 个人心得与扩展方向5.1 踩过几次坑后我总结的经验做这个秒表项目时我印象最深的坑有两个。第一个是第一次上手时把段选和位选锁存顺序写反了结果数码管永远是乱码那会儿不懂看原理图折腾了一整个晚上最后发现只是P2.6和P2.7写反。所以拿到开发板第一件事一定是看原理图哪怕这块板子你已经用得很熟也要把关键引脚抄在代码注释里人的记忆真的不可靠。第二个坑是复位键逻辑。最开始我把复位设计成“清零所有计数并自动开始计时”结果实际测试时想记一个新时刻按下复位又马上走起来了根本来不及准备。后来改成复位后回到暂停状态使用起来才顺手。这个细节告诉我功能设计不能只看代码能不能跑更要考虑用户怎么操作程序里的状态切换要符合直觉。还有一个值得说的经验动态扫描的刷新顺序其实会影响亮度均匀性。如果某一位置于刷新顺序的更后面它每次点亮的时间间隔就会略长肉眼可能感觉那一位比其他位置暗。我这个版本刷新一轮只要四次间隔差异微乎其微但如果把程序扩展到8位数码管就要注意轮流顺序和扫描周期了必要时可以把主循环里的刷新函数放在中断里定时触发确保刷新周期恒定。5.2 这个秒表还能怎么升级秒表程序虽然基础但它是很多复杂功能的地基。想继续进阶的话可以从三个方向入手。第一方向是改用定时器1做系统时钟把T0留给PWM输出或者外部信号计数。比如配一个霍尔传感器或者光电码盘就能把秒表改造成转速表中断里同时计数和计时这套思路在电机测速项目里非常常见。第二方向是增强交互和显示。TX-1C有蜂鸣器可以在秒表计时到整分钟时响一声也可以把显示格式改成“秒 十分位”这样需要一个0.1秒级的时间基准实现起来就是在中断里每5次50ms事件给十分位加1显示改成小数点后一位或两位。如果你想把秒表做成真正的体育计时器还需要加入记录分段时间的功能也就是按一次键记录当前时间再按一次显示下一条记录。第三方向是升级主控或接口。如果以后换到STC15系列或者STM32定时器初值、中断向量这些会变但“定时器中断产生时间基准 主循环动态扫描 按键状态机”的整体架构完全可以直接迁移。这也是我建议初学者认真吃透这个小项目的原因它背后的设计思想比代码本身值钱得多。我个人在实际调试中的体会是单片机项目没有真正意义上的“小项目”。一个秒表看着简单但把定时、显示、输入、状态切换全部组合到一起之后系统复杂度立刻上来了。把这个项目做通、做透后面不管是做电子钟、计数器还是更复杂的仪表都会顺畅很多。如果你在照着抄代码的过程中遇到和我当时一样的问题不用怀疑自己先把原理图放大了看把每一步的执行顺序在纸上画出来问题很快就能定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑