1. 项目缘起与整体设计思路
1.1 为什么选择51单片机做计时器
在嵌入式入门领域,51单片机始终是一个绕不开的经典平台。它的架构简单、资料丰富、成本极低,一片STC89C52RC零售价不过几块钱,配合最小系统和一块LCD1602或者数码管模块,整体物料成本能控制在二十元以内。拿它来做计时器,既能把定时器中断、数码管动态扫描、按键消抖这些基本功全部练一遍,又不会因为芯片本身太复杂而让人把精力耗在啃手册上。
我见过不少初学者一上来就选STM32或者ESP32做小项目,结果光是配时钟树和HAL库就耗掉大半热情,最后项目烂尾。51单片机的好处在于,它的寄存器数量少、结构直观,你写的每一行代码几乎都能对应到硬件行为上。对于计时器这种对精度要求不算极端、但对逻辑清晰度要求较高的项目来说,51单片机是非常合适的练手平台。
这个计时器的核心功能需求可以拆成三块:第一,能够以秒为单位进行计时,并且支持分钟和小时的进位;第二,能够通过按键控制启动、暂停和复位;第三,能够把当前时间实时显示出来。如果还想再进一步,可以加入倒计时模式、蜂鸣器整点提醒、掉电存储等功能。但作为基础版本,先把前三块做扎实,后面扩展就是水到渠成的事。
1.2 系统方案选型与模块划分
整个系统我把它分成四个模块来设计:主控模块、显示模块、按键输入模块和时钟基准模块。主控模块就是51单片机最小系统,负责所有逻辑运算和中断处理;显示模块我选的是四位共阳数码管,用动态扫描的方式驱动;按键输入用三个独立按键,分别对应启动/暂停、复位和模式切换;时钟基准则完全依赖单片机内部的定时器中断,不需要外挂DS1302之类的时钟芯片。
这里重点说一下为什么不用DS1302。DS1302是一块专用的实时时钟芯片,自带32.768kHz晶振,走时精度确实比单片机定时器高不少,但它需要额外的IO口、需要处理BCD码转换、还需要备用电池来维持掉电走时。对于一个以学习为目的的计时器项目来说,这些额外复杂度反而会分散注意力。用定时器中断做时钟基准,虽然精度受晶振频率影响,但通过合理的初值计算和中断补偿,做到日误差几秒以内是完全可行的,对于日常计时场景足够用了。
显示模块选数码管而不是LCD1602,理由也很直接:数码管更便宜、驱动逻辑更锻炼人、动态扫描的过程能让你真正理解“视觉暂留”是怎么回事。LCD1602虽然显示内容更丰富,但它是靠内置控制器驱动的,你发送的是字符而不是段码,反而少了一层对硬件时序的感知。当然,如果你后续想显示年月日或者多组数据,LCD1602会是更好的选择,但基础计时器阶段,数码管足够了。
1.3 定时器中断的核心地位
整个计时器项目里,最核心的技术点就是定时器中断。51单片机通常有两个定时器:Timer0和Timer1。我用Timer0来产生1毫秒的基准中断,然后在中断服务函数里对一个计数器进行累加,每累加到1000次就代表过了1秒,秒计数器加一,再往上进位到分和时。
为什么选1毫秒而不是直接选1秒?因为1秒的定时周期太长,定时器初值计算出来会超出16位寄存器的范围,需要额外的软件计数来补偿,反而麻烦。而且1毫秒的中断频率足够高,可以用来做按键消抖和数码管扫描的时基,一举多得。实际上,很多初学者会把数码管扫描放在主循环里用延时函数控制,这样做的问题是按键响应会变迟钝,而且一旦主循环里有其他耗时操作,显示就会闪烁。把扫描也挂到定时器中断里,整个系统的实时性会好很多。
定时器初值的计算是这样的:假设晶振频率是12MHz,51单片机的一个机器周期等于12个时钟周期,所以机器周期频率是1MHz,也就是每个机器周期1微秒。定时器工作在模式1(16位定时器)时,最大计数值是65536。要产生1毫秒的定时,需要计数1000个机器周期,所以初值应该是65536减去1000,等于64536,转换成十六进制就是0xFC18。高八位是0xFC,低八位是0x18。这个计算过程看起来简单,但实际写代码时很容易把高低位写反,导致定时周期变成原来的256倍或者完全乱掉,后面我会在实操部分详细说明怎么排查这类问题。
2. 硬件选型与电路连接细节
2.1 单片机型号与最小系统
我用的单片机是STC89C52RC,这是51系列里最经典的型号之一,Flash程序存储器8KB,RAM 512字节,对于计时器项目来说绰绰有余。它的工作电压是5V,晶振频率我选的是12MHz,因为12MHz能让机器周期正好是1微秒,计算定时初值的时候心算就能搞定,不需要拿计算器按半天。如果你手头只有11.0592MHz的晶振,也能用,但初值计算会稍微麻烦一点,后面我会给出对应的数值。
最小系统包括单片机本体、晶振电路、复位电路和电源滤波。晶振电路就是在XTAL1和XTAL2两个引脚之间接一个12MHz的晶体,两端各接一个30pF的瓷片电容到地。复位电路我用的是一颗10微法电解电容并联一个10K电阻,上电时电容充电产生高电平复位信号,稳定后变为低电平。电源部分在VCC和GND之间并一个0.1微法的去耦电容,位置尽量靠近单片机电源引脚,这个电容虽然小,但没有它的时候程序跑飞的概率会明显增加。
这里有一个容易被忽略的细节:51单片机的P0口是开漏输出,驱动数码管的时候必须外接上拉电阻,通常用10K的排阻。P1、P2、P3口内部有弱上拉,可以直接驱动LED或者数码管的段选,但如果你用的数码管亮度要求高,还是建议加驱动电路。我在这个项目里用的是共阳数码管,段选信号低电平有效,位选信号通过PNP三极管来驱动,这样电流能力足够,亮度也均匀。
2.2 数码管驱动方式与限流电阻计算
四位共阳数码管的内部结构是四个独立的阳极(位选),八个段选引脚是并联在一起的。也就是说,同一时刻你只能点亮一位数码管,但通过快速轮流点亮四位,利用人眼的视觉暂留效应,看起来就是四位同时亮着。这个轮流点亮的频率不能太低,一般控制在50Hz以上,也就是说每位数码管的刷新间隔要小于5毫秒。我在定时器中断里每2毫秒切换一位,四位轮一遍是8毫秒,刷新率125Hz,肉眼完全看不到闪烁。
限流电阻的计算需要根据数码管的额定电流来定。假设每段LED的正向压降是2V,单片机输出低电平时的电压是0.3V,那么电阻上的压降就是5V减去2V再减去0.3V,等于2.7V。如果我希望每段电流在8毫安左右,电阻值就是2.7V除以8mA,约等于337欧姆,取标准值330欧姆。实际使用时,如果发现亮度不够,可以适当减小到220欧姆,但不要低于150欧姆,否则总电流可能超过单片机端口的承受能力。
这里要特别注意:数码管的段选引脚是并联的,所以当四位同时显示不同的数字时,每一段的电流是动态变化的。如果你用万用表测总电流,会发现它随着显示内容不同而波动。计算限流电阻时,要按最坏情况来考虑,也就是所有段都亮的情况。不过实际使用中,330欧姆的电阻已经足够安全,我实测下来单片机端口温度正常,长时间运行也没有问题。
2.3 按键电路与消抖方案
三个独立按键分别接在P3.2、P3.3和P3.4上,另一端接地。按键按下时,端口被拉低,松开时通过内部上拉电阻回到高电平。这种接法简单可靠,但有一个问题:机械按键在按下和松开的瞬间会产生抖动,抖动时间通常在5到20毫秒之间。如果不处理,一次按键会被识别成多次,导致计时器状态乱跳。
消抖方案有两种:硬件消抖和软件消抖。硬件消抖是在按键两端并联一个0.1微法的电容,利用电容的充放电来吸收抖动。但电容消抖的效果受电容质量和按键特性影响,有时候并不能完全消除抖动。所以我更推荐软件消抖:在定时器中断里每2毫秒检测一次按键状态,连续检测到多次相同状态才确认按键有效。具体来说,我设置了一个按键状态变量,每次中断检测到低电平时计数器加一,计数器达到5次(也就是10毫秒)才认为按键真正按下。这样既能有效消抖,又不会影响按键响应速度。
注意:按键消抖的检测频率不能太高,否则会占用过多中断时间;也不能太低,否则按键响应会迟钝。2毫秒一次是比较合适的值,既能在10毫秒内完成消抖,又不会给中断服务函数增加太多负担。
3. 软件架构与核心代码实现
3.1 定时器中断服务函数的完整实现
定时器中断是整个计时器的心脏,它负责三件事:计时基准累加、数码管动态扫描、按键状态检测。这三件事都放在同一个中断服务函数里,按顺序执行,确保时序的确定性。下面是我实际使用的代码框架,基于Keil C51环境编写。
#include <reg52.h> #define uchar unsigned char #define uint unsigned int // 数码管段码表,共阳数码管,0-9 uchar code seg_code[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // 位选控制引脚 sbit DIG1 = P2^0; sbit DIG2 = P2^1; sbit DIG3 = P2^2; sbit DIG4 = P2^3; // 按键引脚 sbit KEY_START = P3^2; sbit KEY_RESET = P3^3; sbit KEY_MODE = P3^4; // 全局变量 uchar hour = 0, minute = 0, second = 0; uchar ms_count = 0; uchar display_buf[4] = {0, 0, 0, 0}; uchar scan_index = 0; bit running = 0; // 按键消抖计数器 uchar key_start_cnt = 0, key_reset_cnt = 0, key_mode_cnt = 0; void Timer0_Init() { TMOD &= 0xF0; // 清除Timer0模式位 TMOD |= 0x01; // Timer0工作在模式1,16位定时器 TH0 = 0xFC; // 初值高八位,1ms定时 TL0 = 0x18; // 初值低八位 ET0 = 1; // 使能Timer0中断 EA = 1; // 使能总中断 TR0 = 1; // 启动Timer0 } void Timer0_ISR() interrupt 1 { // 重装初值 TH0 = 0xFC; TL0 = 0x18; // 计时基准累加 if (running) { ms_count++; if (ms_count >= 1000) { ms_count = 0; second++; if (second >= 60) { second = 0; minute++; if (minute >= 60) { minute = 0; hour++; if (hour >= 24) { hour = 0; } } } } } // 更新显示缓冲区 display_buf[0] = hour / 10; display_buf[1] = hour % 10; display_buf[2] = minute / 10; display_buf[3] = minute % 10; // 数码管动态扫描 // 先关闭所有位选 DIG1 = 1; DIG2 = 1; DIG3 = 1; DIG4 = 1; // 输出段码 P0 = seg_code[display_buf[scan_index]]; // 打开当前位选 switch (scan_index) { case 0: DIG1 = 0; break; case 1: DIG2 = 0; break; case 2: DIG3 = 0; break; case 3: DIG4 = 0; break; } scan_index++; if (scan_index >= 4) scan_index = 0; // 按键消抖检测 if (KEY_START == 0) { key_start_cnt++; if (key_start_cnt == 5) { running = !running; } } else { key_start_cnt = 0; } if (KEY_RESET == 0) { key_reset_cnt++; if (key_reset_cnt == 5) { hour = 0; minute = 0; second = 0; ms_count = 0; running = 0; } } else { key_reset_cnt = 0; } } void main() { Timer0_Init(); while (1) { // 主循环空闲,所有逻辑都在中断中处理 } }这段代码的核心逻辑是:定时器每1毫秒产生一次中断,中断服务函数里先重装初值,然后根据running标志决定是否累加计时。计时部分采用三级进位结构,秒到分、分到时、时到24小时循环。显示部分把小时和分钟拆成四位数字存入缓冲区,然后每次中断切换一位数码管。按键部分用计数器实现消抖,连续5次检测到低电平才确认按键有效。
3.2 定时初值的精确计算与误差补偿
前面提到12MHz晶振下1毫秒定时的初值是0xFC18,这个值是怎么来的,我再详细推导一遍。51单片机的定时器在模式1下是一个16位加法计数器,每来一个机器周期计数值加一。当计数值从初值加到65536时,产生溢出中断。所以定时时间等于(65536减去初值)乘以机器周期。
12MHz晶振下,机器周期等于12除以12MHz,也就是1微秒。要定时1毫秒,需要计数1000次,所以初值等于65536减去1000,等于64536。把64536转换成十六进制:64536除以16等于4033余8,4033除以16等于252余1,252除以16等于15余12(也就是C),15除以16等于0余15(也就是F)。所以从低位到高位是8、1、C、F,反过来就是0xFC18。高八位是0xFC,低八位是0x18。
但这里有一个实际问题:中断服务函数本身需要执行时间,从溢出到重装初值之间会有几个机器周期的延迟。如果每次中断都重新装载初值,这个延迟会累积,导致定时越来越慢。解决的办法有两种:一种是在中断里手动补偿,把初值加上中断延迟对应的计数值;另一种是使用定时器的自动重装模式,也就是模式2。但模式2是8位定时器,最大只能定时256个机器周期,对于1毫秒来说不够用。
所以我采用手动补偿的方式:在中断服务函数的第一条语句就重装初值,把延迟降到最低。实测下来,12MHz晶振下,24小时累积误差大约在3到5秒之间。如果你对精度要求更高,可以换用11.0592MHz晶振,虽然初值计算麻烦一点,但它的频率更接近标准值,长期稳定性更好。11.0592MHz下1毫秒定时的初值计算:机器周期等于12除以11.0592MHz,约等于1.085微秒,1000个机器周期就是1085微秒,所以需要计数1085次,初值等于65536减去1085,等于64451,十六进制是0xFBC3。
提示:如果你发现计时器走得明显偏快或偏慢,首先检查晶振频率是否和代码中的假设一致。我遇到过有人用12MHz的代码烧到11.0592MHz的板子上,结果计时速度差了将近10%,一天下来能差两个多小时。
3.3 数码管动态扫描的时序优化
数码管动态扫描看起来简单,但实际做的时候有几个坑。第一个坑是“鬼影”:当你在切换位选的时候,如果段码没有先清零,上一位的数字会短暂地出现在下一位上,形成一道淡淡的残影。解决的办法是在切换位选之前,先把段码输出清零,或者先把所有位选关闭,再输出新的段码,最后打开新的位选。我在代码里用的是后者:先关闭所有位选,再输出段码,再打开当前位选。这样虽然多执行了两条语句,但彻底消除了鬼影。
第二个坑是亮度不均匀。如果你在中断里先做计时累加,再做按键检测,最后才做数码管扫描,那么当计时累加或者按键检测耗时较长时,数码管扫描的间隔就会变长,导致亮度下降。解决办法是把数码管扫描放在中断服务函数的最前面,确保每次中断都能准时刷新。我实际测试过,把扫描放在最后,当按键消抖计数器达到阈值时,那一次中断的执行时间会多出几十微秒,肉眼就能看到数码管轻微闪一下。把扫描提前之后,这个问题就消失了。
第三个坑是扫描频率和中断频率的匹配。我的中断是1毫秒一次,四位数码管如果每次中断切换一位,那么每位刷新间隔是4毫秒,刷新率250Hz,远高于人眼感知的闪烁阈值。但如果你把中断改成2毫秒一次,刷新率就降到125Hz,虽然也看不到闪烁,但余晖效应会弱一些,亮度会稍微降低。所以中断频率和扫描位数的关系是:刷新率等于中断频率除以位数。对于四位数码管,中断频率在500Hz到2kHz之间都是合适的。
4. 常见问题排查与实操经验
4.1 数码管显示异常问题速查
在实际调试过程中,数码管显示异常是最常见的问题。我把遇到过的情况整理成了一张速查表,方便你对照排查。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 全部不亮 | 电源未接通或位选三极管损坏 | 用万用表测数码管阳极电压 | 检查电源和驱动电路 |
| 某一位不亮 | 该位选引脚虚焊或三极管损坏 | 交换位选信号测试 | 重新焊接或更换三极管 |
| 显示数字缺段 | 段选引脚虚焊或限流电阻开路 | 逐个测试段选引脚 | 补焊或更换电阻 |
| 显示数字错乱 | 段码表错误或高低位接反 | 对照段码表检查接线 | 修正段码表或接线 |
| 亮度不均匀 | 扫描时序不一致或限流电阻偏差 | 测量各段电流 | 统一限流电阻值 |
| 有鬼影 | 位选切换时未关闭段码 | 观察切换瞬间 | 先关位选再换段码 |
这张表里的每一行都是我实际踩过的坑。比如“显示数字缺段”这个问题,我一开始以为是代码写错了,反复检查段码表都没发现问题,后来用万用表逐个测量段选引脚,才发现是其中一根杜邦线内部断了,外表看不出来。所以遇到显示问题,先查硬件再查软件,能省很多时间。
4.2 定时器不中断或中断频率不对
定时器不中断或者中断频率不对,通常有以下几个原因。第一,忘记使能总中断EA或者定时器中断ET0。这两个标志位必须都置1,中断才能正常触发。第二,定时器初值计算错误,导致溢出周期远大于或远小于预期。第三,中断服务函数的函数名或者中断号写错。51单片机的中断号是固定的,Timer0的中断号是1,所以函数声明必须是void Timer0_ISR() interrupt 1,写成其他数字就不会进入这个中断。
我遇到过一次特别隐蔽的情况:中断服务函数里有一个死循环,导致程序卡在中断里出不来,主循环完全没执行。当时的现象是数码管显示正常但按键完全没反应,因为按键检测也在中断里,中断卡住了自然就不检测了。排查的时候我在中断入口和出口各翻转一个IO口,用示波器看波形,发现只有入口有跳变,出口没有,这才定位到死循环的位置。
还有一个常见问题是中断频率不对。如果你发现计时速度是预期的两倍,很可能是定时器初值重装时写错了,导致实际定时周期变成了500微秒。这时候用示波器测量一个IO口的翻转频率就能快速判断:在中断里翻转IO口,如果频率是1kHz,说明中断周期是1毫秒,正确;如果频率是2kHz,说明中断周期是500微秒,初值需要调整。
4.3 按键响应不灵敏或连击
按键问题主要有三种表现:按了没反应、按一次跳多次、长按连续触发。按了没反应通常是消抖计数器阈值设得太高,比如设成了20次,那就是40毫秒才确认,手感会很迟钝。我建议阈值设在5到10次之间,对应10到20毫秒,这个范围既能有消抖效果,又不会影响手感。
按一次跳多次是因为消抖不彻底,抖动被识别成了多次按键。解决办法是增加消抖计数器的阈值,或者在确认按键之后加一个“等待松开”的逻辑:只有检测到按键松开后,才允许下一次按键检测。我在代码里用的是后者,在按键确认后把计数器清零,并且只有在按键松开时才重新开始计数。这样即使抖动持续了较长时间,也只会被识别成一次按键。
长按连续触发是因为没有区分“按下”和“长按”。如果你希望长按有连续触发的效果,可以在确认按下后启动一个长按计数器,每隔一定时间触发一次。如果你不希望长按连续触发,那就需要在按键确认后忽略后续的按下状态,直到检测到松开。这个逻辑根据你的实际需求来定,没有绝对的对错。
4.4 程序跑飞与看门狗的使用
51单片机没有内置看门狗,但STC89C52RC有专门的看门狗定时器,需要通过寄存器配置来启用。程序跑飞通常是因为中断嵌套太深、堆栈溢出或者电源波动。我在调试阶段遇到过几次程序跑飞,现象是数码管显示乱码或者完全熄灭,按键也没反应。后来发现是电源的问题:我用的是USB供电,电脑同时插了好几个USB设备,电压波动比较大,导致单片机复位。
解决电源问题之后,我又加上了看门狗。看门狗的配置很简单,先向WDTRST寄存器写入0x1E,再写入0xE1,就启用了。然后在主循环里定期喂狗,也就是再次写入这两个值。如果程序跑飞,看门狗溢出会产生复位,让系统重新开始。但要注意,喂狗的时间间隔不能太长,否则看门狗会误复位。STC89C52RC的看门狗溢出时间大约是16毫秒到2秒之间,具体取决于配置。我一般设置在500毫秒左右,主循环里每100毫秒喂一次,这样既不会误复位,又能在跑飞时快速恢复。
注意:启用看门狗之后,调试会变得麻烦,因为程序一旦停下来(比如设断点),看门狗就会复位。所以建议在调试阶段先不启用看门狗,等程序稳定后再加上。
5. 功能扩展与进阶玩法
5.1 倒计时模式与蜂鸣器提醒
基础计时器做完之后,最自然的扩展就是倒计时模式。倒计时的逻辑和正计时正好相反:从设定的时间开始,每秒减一,减到零的时候触发蜂鸣器提醒。实现上只需要增加一个模式标志位,在中断服务函数里根据模式决定是加还是减。设定时间可以通过按键来调整,比如按模式键进入设置状态,用启动键来增加数值,用复位键来切换设置位。
蜂鸣器我用的是有源蜂鸣器,接在P1.5引脚上,通过一个NPN三极管驱动。有源蜂鸣器内部有振荡电路,只要给高电平就会响,不需要单片机输出PWM。提醒逻辑很简单:倒计时减到零时,把蜂鸣器引脚置高,持续500毫秒后置低。如果你想要更悦耳的声音,可以换成无源蜂鸣器,用定时器产生不同频率的方波,就能发出不同音调的声音。
这里有一个细节:蜂鸣器开启时会消耗较大电流,可能导致电源电压瞬间下降,影响单片机工作。所以蜂鸣器的电源最好和单片机分开走线,或者在蜂鸣器电源引脚附近加一个100微法的电解电容来储能。我实测过,不加电容的时候,蜂鸣器一响数码管就会变暗,加了电容之后就稳定了。
5.2 掉电存储与数据保存
掉电存储需要用到EEPROM,STC89C52RC内部有2KB的EEPROM,可以通过IAP方式读写。IAP就是“在应用编程”,简单说就是程序运行时可以擦写自己的Flash空间。STC的EEPROM操作有专门的寄存器:ISP_DATA、ISP_ADDRH、ISP_ADDRL、ISP_CMD、ISP_TRIG。操作流程是:先设置地址,再设置命令(读、写、擦除),然后触发命令,最后等待完成。
我一般把计时数据保存在EEPROM的最后一个扇区,避免和程序代码冲突。保存的时机是每次秒数变化时写入,但这样写入太频繁,EEPROM的擦写寿命有限(通常10万次左右),每秒写一次的话不到两天就写坏了。所以更好的策略是每隔一分钟保存一次,或者只在检测到电源电压下降时保存。检测电源下降可以用ADC采样电源电压,但51单片机没有内置ADC,需要外接比较器或者用IO口配合RC电路来检测。对于基础项目来说,每分钟保存一次已经足够实用了。
5.3 多组计时与串口输出
如果你需要同时计多个时间,比如体育比赛中的分段计时,可以扩展成多组计时器。每组计时器有独立的时、分、秒变量,通过按键切换当前显示的是哪一组。这个扩展主要考验的是数据结构的设计:你可以用数组来存储多组时间,用索引来指示当前组。中断服务函数里遍历所有正在运行的组,分别累加时间。
串口输出是另一个实用的扩展。51单片机有一个UART,可以通过串口把计时数据发送到电脑上显示。配置串口需要设置波特率、数据位、停止位等参数。以12MHz晶振为例,波特率9600对应的定时器初值是0xFD,波特率加倍位SMOD设为0。发送数据时,把数据写入SBUF寄存器,等待TI标志置位,然后清除TI,就可以发送下一个字节了。串口输出可以用来做数据记录,比如记录每次按键的时间点,方便后续分析。
6. 项目收尾与个人实操体会
这个计时器项目我从画电路图到写代码再到调试完成,前后花了大约三个晚上。第一个晚上搭硬件,第二个晚上写代码,第三个晚上调试和优化。硬件部分最耗时间的是焊接数码管和按键,因为引脚比较多,容易焊错。我的建议是先用面包板搭电路,确认功能正常之后再焊接到洞洞板上,这样即使接错了也容易改。
软件部分最让我头疼的是数码管动态扫描和按键消抖的时序配合。一开始我把扫描放在主循环里,用延时函数控制,结果按键响应很慢,而且数码管偶尔会闪。后来把扫描和按键检测都移到定时器中断里,问题就解决了。这让我深刻体会到:在嵌入式系统里,中断才是处理实时任务的正道,主循环只适合处理那些对时间不敏感的逻辑。
还有一个体会是关于代码的可读性。我一开始为了省事,把所有变量都定义成全局的,中断服务函数里直接操作这些变量。后来想增加倒计时功能时,发现代码已经乱成一团,改一处就影响另一处。于是我把计时逻辑、显示逻辑、按键逻辑拆成独立的函数,用结构体来组织数据,代码一下子就清晰了。所以即使是一个小项目,也要注意模块化,不然后期扩展会很痛苦。
最后分享一个调试小技巧:如果你怀疑定时器中断没有正常执行,可以在中断里翻转一个空闲的IO口,然后用LED或者示波器观察。如果LED在闪烁,说明中断在跑;如果LED常亮或常灭,说明中断没进或者卡住了。这个方法比用调试器设断点更直观,而且不会影响中断的实时性。我在调试定时器初值的时候,就是靠这个方法快速确认了中断频率是否正确。