
手头正好翻出一块吃灰很久的天祥TX-1C单片机开发板接上电源试了试还能亮索性就把它拿来重新跑了一遍经典的秒表程序。这段时间正好有朋友问这块老开发板还值不值得学、秒表程序到底怎么下手我干脆把这几天重新折腾的过程整理成一篇实操笔记。天祥TX-1C这块板子用的是STC89C52RC单片机虽然发布已经有些年头但51内核的学习价值一点儿没过时尤其是秒表程序这个项目几乎能把定时器、中断、数码管动态扫描、按键消抖这些基本功一网打尽。这篇东西适合刚学完流水灯、想往中断和定时器进阶的人也适合手里有这块板子但一直没跑通完整项目的同学参考。先说结论秒表程序看着简单但真正上手之后你会发现它把单片机开发里最常用也最容易出问题的几个点全串起来了。如果把流水灯比作编程里的“Hello World”那秒表就是第一次让自己写的程序“同时处理多件事”的入门项目。我这两天重新写代码、烧录、调试下来确实踩了几个典型的坑后面我会把排查思路和解决办法一起写清楚。1. 天祥TX-1C开发板到底适合学什么1.1 老开发板的定位没变廉价、直观、资料多TX-1C这块板子在很多高校的单片机课程和电子竞赛培训里出现过原因很简单板载资源刚好覆盖51单片机入门的全部核心外设。STC89C52RC这颗芯片是增强型51内核片上有8KB Flash、512字节RAM、3个定时器、1个串口对于课程设计级别的小项目完全够用。板子上的8位数码管、4x4矩阵键盘、独立按键、蜂鸣器、DS1302时钟芯片、DS18B20温度传感器接口、LCD1602接口基本把入门阶段要玩的外设都集齐了。很多初学者一上来就想买STM32或者更高级的开发板觉得51已经过时了。我的看法是如果目标是搞懂单片机底层运行逻辑51仍然是极好的教学平台因为它足够简单寄存器就那么几个看手册就能明白CPU是怎么一步步干活的。STM32虽然性能强很多但复杂度和抽象层也高很多直接上手容易卡在初始化配置上反而不利于建立“程序是如何操控硬件”的直觉。1.2 秒表程序为什么是恰到好处的进阶项目选秒表作为项目是因为它能逼着你把以下几个知识点同时用起来定时器/计数器怎么配置中断服务函数怎么写数码管动态扫描的显示原理独立按键的消抖处理程序状态机的划分与状态切换这几个知识点单独拿出来都不难但组合在一起就需要一定的编程组织能力。写秒表不是简单地把每一部分代码拼起来就完事而是要考虑CPU怎么在计时和显示之间“一心二用”。这种“并发”的思维恰恰是单片机开发与普通上位机开发最大的不同点。1.3 上手前的环境准备与Keil工程搭建在写代码之前先把Keil工程的坑说清楚。TX-1C板载单片机的型号比较老不同批次可能有细微区别一般下载程序用的是STC-ISP软件。常见的一个坑是程序烧录时提示“连接超时”或“握手失败”十有八九是下载前没有给板子重新上电。STC单片机是冷启动下载你先点“下载”按钮然后把板子电源断掉再接通才能进入下载状态。Keil工程建立的时候选择芯片型号要注意如果是STC89C52RC在Keil里选Atmel的AT89C52也能编译但推荐去STC官网下载STC-ISP安装包里面自带了STC单片机型号库直接选“STC89C52RC”或“STC89C54RD”这类具体型号。选对型号的意义在于启动文件和内存模型更准确避免有些编译器按老51默认配置导致RAM溢出。另外提醒一点老开发板自带的晶振一般标注11.0592MHz或12MHz秒表计时对时间精度有要求这两个晶振频率会直接影响定时器初值的计算。后面写定时器代码的时候我会专门讲一下不同晶振下初值怎么算这一步如果偷懒直接抄别人的数值几乎必出时间偏差。2. 秒表程序的硬件基础与原理拆解2.1 板载数码管和按键的接线方式TX-1C这块板子的数码管是8位共阴或共阳具体要对照原理图确认不同批次有差异。我第一次用的时候没看原理图直接写代码结果显示全乱查了半天发现是位选电平刚好反了。这一点提醒所有初学者开发板到手第一件事就是把原理图从头到尾翻一遍确认每个外设的IO口对应关系。宁可多花半小时看图纸也不要在调试上搭进去半天。按键部分TX-1C一般提供独立按键和矩阵键盘两种。秒表控制只需要三个独立按键就够了比如K1控制开始/暂停K2控制清零K3可以作为模式切换键。如果我记忆没出错的话独立按键一般接到P3口的低四位按下为低电平。这里也需要对照原理图确认因为有些板子的按键接法带上了上拉电阻。2.2 定时器中断的原理为什么系统能“同时”干很多事51单片机有定时器/计数器工作方式有几种秒表程序最常用的是方式1——16位定时器。定时器的本质是一个计数器每个机器周期计数寄存器的值自动加1当计数溢出的时候会触发中断。比如12MHz晶振的普通51单片机机器周期是1微秒12个时钟周期为一个机器周期那么定时器每微秒加116位计数器从0到65535溢出一次需要65.536毫秒。这就带来一个问题如果想让定时器每10毫秒产生一次中断不能直接从0开始计而是要先给寄存器赋一个合适的初值。初值的计算公式是初值 65536 - 需要的定时时长 / 机器周期假设晶振12MHz机器周期1微秒需要10毫秒定时初值 65536 - 10000/1 55536转成十六进制就是0xD8F0所以把TH0设为0xD8TL0设为0xF0就可以让定时器从0xD8F0开始加1加到65536溢出时刚好10毫秒。这个计算逻辑后面还会再展开因为秒表精度、上位机通信、PWM生成都要靠它。有了定时器中断主程序就可以专心处理按键扫描和数码管刷新定时器到点自动打断主程序去执行中断服务函数执行完再回来接着干活。你完全可以把它理解成一个闹钟你一边写作业闹钟每隔10秒响一次催你喝水你不必一直盯着时钟闹钟到点自然就会提醒你。2.3 数码管动态扫描用“骗眼睛”的方式节省IO口数码管的驱动有两种方式静态显示和动态扫描。静态显示是每个数码管单独占用一组IO口显示内容稳定但占用的引脚太多。动态扫描则利用了人眼的视觉暂留效应——8个数码管轮流显示每个数码管点亮一小段时间然后熄灭下一个数码管接着亮只要刷新频率够快通常在50Hz以上人眼就看不出闪烁以为8个数码管是同时亮的。TX-1C板载数码管通常用两个74HC573锁存器来控制。一个锁存器控制段选显示什么数字一个控制位选哪一位亮。操作顺序一般是先送段码到锁存器的数据口然后给锁存器一个脉冲让数据锁存再送位选码同样锁存延时一小段时间让当前位保持点亮清除显示切换到下一位重复上面步骤这里有个细节位选切换的时候必须“消隐”也就是在切换位选之前先把段码清掉。如果不消隐下一位数码管会在切换瞬间显示上一位的段码残影肉眼看就是某个数字后面拖着虚影这个问题后面我会在调试部分再讲。3. 秒表程序的设计思路与代码实现3.1 程序框架前后台系统与状态机秒表程序虽然小但我建议从一开始就养成前后台系统的编程习惯。所谓“前台”就是主循环里不断扫描按键、刷新显示“后台”就是定时器中断服务函数负责精确计时。这种结构以后做任何复杂一点的嵌入式项目都用得上包括很多RTOS任务切换的雏形。按键控制这块我强烈建议用一个简单的状态机来组织代码。状态可以这样定义状态0停止状态秒表不走显示当前数值状态1运行状态秒表计时增加状态2暂停状态秒表不走但不清零保留当前数值K1键负责在这几个状态之间切换停止状态下按K1进入运行状态运行状态下按K1暂停暂停状态下按K1恢复运行。K2键在停止状态下清零但如果处在运行或暂停状态按K2就不响应。这样设计符合一般人对电子秒表的认知也不会出现误操作。用状态机的好处是逻辑清晰每个状态下程序该干什么一目了然改功能的时候只需要在对应状态下修改动作不会把一堆if-else搅在一起。3.2 定时器初始化代码与初值计算细节先上定时器0的初始化代码void Timer0_Init(void) { TMOD 0xF0; // 清除定时器0的模式位 TMOD | 0x01; // 定时器0工作在方式116位定时器 TH0 0xD8; // 高8位初值 TL0 0xF0; // 低8位初值 ET0 1; // 使能定时器0中断 EA 1; // 打开总中断 TR0 1; // 启动定时器0 }这里初值0xD8F0对应12MHz晶振下的10毫秒。如果板子用的晶振是11.0592MHz机器周期是12/11.0592MHz约1.085微秒同样的初值定时时长会变成10.85毫秒时间也就不准了。所以拿到板子之后第一件事就是确认晶振频率不要默认是12MHz。定时器中断服务函数里做的是累加计时unsigned char timer_count 0; unsigned int second_100 0; // 0.01秒计数 void Timer0_ISR(void) interrupt 1 { TH0 0xD8; // 重新装载初值 TL0 0xF0; timer_count; if (timer_count 10) // 累加到100毫秒 { timer_count 0; // 这里根据状态决定是否递增秒表 if (run_flag 1) { second_100; if (second_100 6000) // 60秒后清零 second_100 0; } } }重装初值这个操作特别关键。定时器溢出之后寄存器自动归零如果不在中断里重新把TH0和TL0赋回初值下一次溢出周期就会变成65.536毫秒计时完全乱套。千万不能漏掉这两行。3.3 数码管显示与按键扫描的实现显示函数用了一个数码管码表把0到9的段码预先存好unsigned char code seg_table[] {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90};这个表格是共阳极数码管的编码如果是共阴极数码管需要取反。显示函数每次点亮一位通过一个全局变量记录当前扫描到第几位void Display_Scan(void) { static unsigned char pos 0; P0 0xFF; // 消隐防止拖影 P2 0x01 pos; // 位选 P0 seg_table[display_buf[pos]]; // 段选 pos; if (pos 8) pos 0; }注意P0和P2在不同批次TX-1C板子上的定义可能不同一定要按自己手里的原理图来改。display_buf数组存的是每一位要显示的数字主循环里一开始先把这个数组初始化成0计时过程中再把second_100的各位数字拆开填进去。按键扫描的核心是防抖。机械按键按下的时候会有大约10-20毫秒的电平抖动如果不处理一次按键可能会被识别成好几次。我常用的消抖方法是延时消抖检测到按键按下后延时20毫秒再检测一次如果还是按下状态就确认按键有效。void Key_Scan(void) { if (K1_PIN 0) // 检测到K1按下 { Delay_20ms(); // 延时消抖 if (K1_PIN 0) { // 按键确认有效切换状态 } while (K1_PIN 0); // 等待按键松开 } }3.4 主循环的完整组织主循环里不要做延时太长的操作因为如果某个函数卡了100毫秒数码管的刷新就会中断导致显示亮度不均匀甚至闪烁。我的主循环逻辑大致是void main(void) { Timer0_Init(); while (1) { Key_Scan(); Display_Scan(); } }这个结构看起来简单却包含了一个很重要的设计思想数码管的动态扫描函数每次只点亮一位然后立刻返回主循环继续执行下一次循环再点亮下一位。这样即使按键扫描函数里有20毫秒的延时数码管的刷新也不会完全停摆最多是某一位亮的时间长了一点肉眼几乎察觉不到。4. 实际调试中遇到的坑与排查技巧4.1 数码管亮度不均、闪烁严重这个问题我第一次调动态扫描的时候踩过后来检查发现原因有两个方向。一是位选和段选的顺序反了先送段码再送位选导致瞬间有杂乱的段码出现在不该亮的位上。二是刷新率太低8位数码管每一圈扫描周期太长人眼明显看出闪烁。排查方法很简单把刷新周期控制在20毫秒以内也就是说每8位扫一圈不超过20毫秒每位点亮时间大约2毫秒左右。如果主循环里有太长的延时函数必须先优化延时或者把延时操作挪到不影响显示的位置去。另外提一个硬件相关的点TX-1C的数码管如果长时间不扫描亮度会明显发暗。如果你发现某个数码管总是比其他几个暗大概率不是代码问题而是锁存器驱动能力不足或者该位对应的IO口接触不良。4.2 秒表计时不准走快了或走慢了计时不准的原因归根结底就两个定时器初值算错或者晶振频率和代码预设的不一致。先确认晶振频率。你可以看板子上的晶振外壳印的字上面一般标了11.0592或12.000。如果实在看不清最笨但最可靠的办法是用另外一个带秒表功能的设备做对比跑个一分钟看误差有多大然后用误差比例反推实际晶振频率。还有一种隐蔽的情况中断服务函数里处理了太多事情导致来不及执行完就进入下一次中断丢掉了计时更新。这种情况在51单片机上虽然少见但如果这间服务函数里放了数码管扫描这种耗时的操作确实会出现问题。所以一个原则中断服务函数越短越好只做必要的计时和标志位更新显示和按键一概放到主循环里。4.3 按键按下去没反应或者一次触发多次按键没反应最常见的是消抖延时期间程序卡在等待而按键松开的检测不当导致程序误以为没有按下。如果确认接线没问题、程序逻辑也没问题可以试着换一个IO口接按键排除硬件上个别引脚接触不良的情况。一次触发多次则是消抖间隔不够或松开检测不严。我自己的经验是消抖延时设在10-20毫秒比较稳同时在确认按下之后要“等待按键松开”再返回否则按住按键的时候程序会反复进入按下处理逻辑。这段“等松开”的while循环虽然简单粗暴但在这种小项目里非常可靠。4.4 烧录失败和程序不跑的排查顺序很多初学者遇到程序写进去但板子没反应第一反应是改代码其实大概率是烧录或硬件链路的问题。我一般按照这个顺序排查板子上电后电源指示灯是否亮晶振是否起振有条件就用示波器测烧录软件是否能识别到设备注意STC的冷启动下载方式Keil编译是否报错有没有选对芯片型号程序里有没有在while(1)之前就意外死循环比如中断没开导致程序卡在某处最小排查法先下载一个开发板自带的测试程序确认硬件没问题再换成自己的程序这个排查思路非常有用尤其是“先用官方测试程序验证硬件”这一步能把软件和硬件问题快速分开避免对着自己的代码瞎猜。5. 秒表程序的扩展方向与实际心得5.1 从秒表到更多实用功能的升级路径秒表程序跑通之后可以往几个方向扩展难度递增但成就感也很足。加一个LDC1602显示把数码管显示改成液晶显示给秒表加蜂鸣器提示音按下按键时响一声计时到一定时间告警用DS1302时钟芯片做实时时钟顺便了解I2C或SPI类总线的通信思路把时间数据通过串口发给上位机这就涉及串口通信协议的设计我在这次调试的时候顺手把秒表的时间值通过串口发出去了用的就是STC89C52的UART模块。接收端先用PC上的串口助手看数据格式后面再接上位机软件实时显示。整个过程对理解“单片机如何与外部系统通信”帮助很大比单独跑一个串口发送示例要有意思得多。5.2 一块老开发板能教给你的远不止代码说到最后我想聊聊这一星期重新折腾TX-1C的体会。它毕竟是一块老开发板没有板载调试器没有图形化配置工具连烧录都要靠冷启动这种略显原始的操作。但正是这种“简陋”逼着你去搞懂底层原理你得自己算定时器初值自己调数码管时序自己画程序流程图。这些基础打牢之后再切到STM32、ESP32这类高性能平台你会发现自己能快速定位问题而不是一上来就被各种库函数和HAL封装搞得晕头转向。秒表项目虽然小但它完整走了一遍“需求分析、硬件梳理、代码编写、联调排错”的流程。如果你能把这个项目做得干净、稳定、逻辑清晰再回头看那些只会跑流水灯的同学差距就已经拉开了。老话说磨刀不误砍柴工51这块老磨刀石现在用它的人少了但它磨出来的刀确实锋利。