简介:围绕AT89C51单片机与MAX7219数码管驱动展开的课程设计报告,面向电子信息、物联网、自动化等专业学生及单片机初学者,服务于00~99秒秒表类课程设计与数码管显示练习。文档以单份doc形式提供,压缩包约915KB,内容从设计要求、系统总体方案与方框图讲起,延伸到复位电路、晶体振荡电路、显示电路、键盘电路等单元硬件设计,并涉及MAX7219器件说明、定时器/计数器计时原理以及暂停、清零等功能的软硬件配合。读者可据此梳理秒表项目的完整设计链路,理解数码管动态显示与按键输入的处理方式,也能参考报告中的模块划分和调试思路完成自己的课程作业或万用板制作。目前已有3606人学习下载,适合需要一份可直接对照的经典单片机课程设计范例的读者。
1. 一块 4 位数码管和 6 个按键:这套 00~99 秒秒表方案先把边界划清楚
不少同学拿到「51 单片机 00~99 秒数码管显示」这道题,第一反应是把两位数码管直接挂在 P0 口上做动态扫描,主循环里一个死循环反复刷新。功能表只写「开始 + 复位」时,这套写法确实能跑;可一旦题目要求扩展成四位显示(秒和百分秒)、再加上暂停、保存、上翻、下翻,主循环立刻被扫描吃掉,按键响应变迟钝,数码管开始发抖。这份基于 AT89C51 与 MAX7219 的秒表方案,从第一步就把职责切开:单片机只负责 10ms 时基、按键判定和数值拆分,四位数码管的译码、扫描、亮度全部交给 MAX7219 专用驱动芯片。它适合正在做《单片机原理及应用》课程设计、需要一份能照着画图、照着烧片、照着答辩的完整方案的人。
2. 总体方案与选型:AT89C51 管计时、MAX7219 管点亮的二八分工
2.1 主控只留三件事:定时、读键、写显示
这套系统里,AT89C51 承担的工作被刻意压到三件:用定时器 T0 产生 10ms 硬件节拍、用 P1 口读六个按键并做消抖、把当前时间和记录值拆成四位十进制数写进 MAX7219。除此之外的活一概不接。之所以敢这么切,是因为 AT89C51 的资源实在不富裕——4KB Flash、128 字节 RAM、四个 8 位 I/O 口,若再让它自己扫四位共阴数码管,主循环里就要塞进「取段码 → 送段 → 开位 → 延时 1~2ms → 关位」的循环,四位数一轮下来接近 8ms,按键扫描的间隔被拉长,手感一定发飘。
交给 MAX7219 之后,单片机与显示之间只剩三根线:DIN(P3.0,串行数据)、LOAD(P3.1,装载/锁存)、CLK(P3.2,移位时钟)。一帧数据是 16 位,高 8 位是寄存器地址,低 8 位是数据,CLK 上升沿把数据推入内部移位寄存器,LOAD 上升沿锁存生效。刷新一位要 16 个时钟,四位全刷 64 个时钟,看似不少,但关键在于——只有数值变化时才写,秒表每秒才变一次,主循环里几乎不花时间在显示上。
提示:P3.0~P3.2 同时是串口 RXD/TXD 和外部中断 INT0 的复用引脚。这个设计里没用串口和外中断,所以可以放心占用;如果后续要加串口上传计时数据,就得把三根线挪到 P2 口,改动只在位定义那三行。
2.2 关键参数怎么来的:11.0592MHz、33pF、0xDC00
定时精度的根子在晶振和初值。AT89C51 在标准 12T 模式下,一个机器周期等于 12 个振荡周期。选 11.0592MHz 晶振时,机器周期 = 12 ÷ 11.0592MHz ≈ 1.085μs。要凑 10ms,需要 10000μs ÷ 1.085μs ≈ 9216 个机器周期。定时器 T0 工作在方式 1(16 位定时),初值 = 65536 − 9216 = 56320 = 0xDC00,于是重装值就是 TH0 = 0xDC、TL0 = 0x00。这个初值低字节刚好是 00,写代码时不容易记错,是选它的一层便利。
为什么不用更常见的 12MHz?12MHz 下 10ms 对应 10000 个机器周期,初值 55536 = 0xD8F0,一样能算,而且整除得更干净。我一般还是推荐 11.0592MHz,理由不在这个秒表本身,而在这门课后面往往要接串口通信或做串口下载,11.0592MHz 是能被 9600、19200 这些波特率整除的标准频率,一次焊好不用改板。
起振电容取 33pF,接在 XTAL1、XTAL2 与地之间,作用是微调振荡频率并帮助起振,取值在 20~47pF 都常见,33pF 是配套 11.0592MHz 晶振的常规选择。复位走按键电平复位:10μF 电解电容正极接 VCC、负极接 RST,复位按钮并联在电容两端,RST 再经一只 10kΩ 电阻下拉到地。按下按钮时 RST 直接被拉到高电平,松开后电容充电维持一小段高电平,只要高电平持续超过 2 个机器周期就能可靠复位。这种接法的好处很直接——程序跑飞了,手一按就回来,不用反复断电。
2.3 三种显示方案的取舍:静态、动态扫描、专用驱动
把三条路线摆在一起看,选型的理由会清楚得多:
| 对比项 | 静态显示 | 动态扫描 | MAX7219 专用驱动 |
|---|---|---|---|
| 占用 I/O | 4 位 × 8 段 = 32 根,必须扩 74HC595 或 573 | 段码 8 根 + 位选 4 根 = 12 根 | 3 根(DIN/LOAD/CLK) |
| 程序负担 | 最轻,写一次就不管 | 最重,必须持续刷新,主循环不敢停 | 轻,数值变化时才写 |
| 亮度稳定性 | 高,无闪烁 | 受扫描节拍影响,中断一抢占就抖 | 高,芯片内部恒流扫描 |
| 扩展性 | 差,加位就要加线 | 一般,位选线随位数线性增加 | 好,DOUT 可级联多片 |
| 适合场景 | 位数少、端口富余 | 成本敏感、位数固定 | 位数多、要腾出主循环 |
课程设计里最常见的翻车是把动态扫描和长延时业务塞进同一个主循环,比如按键消抖用软件延时 10ms,这 10ms 里显示完全不刷新,肉眼就是一片闪烁。用 MAX7219 之后这类问题从根上消失,代价只是多一颗芯片和一次初始化。
3. 硬件电路设计:四单元连接与 P1、P3 口的端口账本
3.1 系统方框图与信号走向
整块板子的信号流很线性:时钟电路和复位电路为 AT89C51 提供工作基准,AT89C51 通过 P3.0~P3.2 三根线向 MAX7219 下发显示数据,MAX7219 用自己的 DIG0~DIG3 位选线和 SEG A~G、DP 段线直接驱动 7SEG-MPX4-CC 四位共阴数码管,六个独立按键则接在 P1.0~P1.5 上,另一端统一接地。
注意这里没有「单片机 → 限流电阻 → 数码管」这一段。段电流由 MAX7219 的 ISET 引脚外接电阻设定,芯片内部八路段驱动是恒流源,不需要每段串电阻。这一步省掉的不只是 32 只电阻,还省掉了一堆焊点——万用板制作时,焊点数量直接决定你调试时要排查的故障点数量,血泪经验。
3.2 显示电路:7SEG-MPX4-CC 与 MAX7219 的引脚对位
7SEG-MPX4-CC 这个名字拆开看:7SEG 表示七段,MPX4 表示四位一体,CC 表示共阴(Common Cathode)。共阴意味着每一位的公共端是阴极,要点亮某一段,段线给高电平、该位公共端给低电平——正好对上 MAX7219 的输出特性:SEG 段驱动输出电流,DIG 位选吸入电流。
接线对位如下表:
| MAX7219 引脚 | 接到哪里 | 说明 |
|---|---|---|
| DIG0 ~ DIG3 | 数码管第 1~4 位公共阴极 | 位选,对应寄存器地址 0x01~0x04 |
| SEG A ~ SEG G、DP | 数码管对应段引脚 | 段驱动,DP 为小数点 |
| DIN | P3.0 | 串行数据输入,高位先入 |
| LOAD | P3.1 | 装载信号,上升沿锁存一帧 |
| CLK | P3.2 | 移位时钟,上升沿采样 |
| ISET | 经 10kΩ 电阻接 VCC | 设定段峰值电流 |
| V+ | +5V | 电源正 |
| GND | 地 | 电源地,与单片机共地 |
ISET 那只电阻是亮度旋钮的硬件版。数据手册给出的对应关系里,9.53kΩ 约对应 40mA 段峰值电流,取 10kΩ 与之接近,配合芯片内部 1/8 占空比扫描,平均每段电流落在几毫安量级,普通四位共阴数码管亮度足够而不至于发烫。如果装好后发现偏暗,把这只电阻减到 8.2kΩ 或 6.8kΩ;如果整块板子发热、5V 电源被拉低,就往大改到 15kΩ 以上。
3.3 键盘电路:六个功能键与 P1 口的分配账本
六个按键一端接 P1 口对应引脚,一端接地。AT89C51 的 P1 口内部自带提升电阻,不需要外接上拉,按下时读到 0,松开时读到 1。端口分配必须一开始就定死并写进注释,否则软件调到最后很容易忘记哪个口是哪个功能:
| 按键 | 端口 | 功能 | 按下后的动作 |
|---|---|---|---|
| KEY0 | P1.0 | 开始 | 置位 TR0,启动 T0 计数 |
| KEY1 | P1.1 | 暂停 | 清零 TR0,停表并保留当前值 |
| KEY2 | P1.2 | 保存 | 把当前秒/百分秒写入记录区 |
| KEY3 | P1.3 | 下翻 | 查看指针向低地址方向移动一组 |
| KEY4 | P1.4 | 上翻 | 查看指针向高地址方向移动一组 |
| KEY5 | P1.5 | 复位 | 停表并把秒、百分秒、记录全部清零 |
关于按键数量,原始设计要求里写的是「七个开关按键」,但实际端口分配只到 P1.5,共六个功能。做法上以端口表为准,六个功能键已经覆盖开始、暂停、保存、上下翻、复位全部需求,多出来的口留空当扩展位即可,答辩时也不必硬凑数。
3.4 复位与晶振电路:两个最容易被抄错的地方
晶振电路的抄错点在于电容接法。正确接法是晶振两端各接一只 33pF 电容到地,而不是两只电容串联后跨接在晶振两端。串联接法在仿真软件里可能照样起振,到了实物板上就是不起振或者起振极慢,表现为上电后毫无反应——这类问题在仿真里永远复现不出来,属于典型的仿真与实物脱节。
复位电路的抄错点在按钮的并联位置。按钮要和 10μF 电容并联,而不是串在 RST 与 VCC 之间。串联接法的后果是按下按钮时电容先充电,RST 上是个缓慢上升的电压,可能刚好卡在复位门限附近,表现成「有时能复位、有时不能」这种玄学故障。另外,如果板上还有别的芯片需要同步复位,直接把它们的 RST 并到同一个节点即可,不必各做一套。
4. 软件设计:从 10ms 节拍到四位显示的五段程序链
4.1 程序模块划分与内存单元的账本
软件按模块化思路拆成五段:初始化(含 MAX7219 初始化)、定时中断、显示拆分与写入、键盘扫描与消抖、记录区管理。每段都是一个独立子程序,主程序在初始化之后进入键盘查询的循环,其余全部由中断和按键触发。
内存分配要提前画成一张表,否则写到一半必然互相踩踏。这套方案的账本如下:
| 位置 | 用途 | 取值范围 |
|---|---|---|
| R4 | 秒计数 | 0~99 |
| R5 | 百分秒计数(每 10ms +1) | 0~99 |
| R0 / R1 | 记录区写指针(分别指向一组的两字节) | 50H 起 |
| 30H | 消抖用的键值暂存 | 0~0FFH |
| 40H | 待写 MAX7219 的数据 | 0~0FFH |
| 41H | 待写 MAX7219 的寄存器地址 | 01H~0CH |
| 42H / 43H | 显示缓冲(秒、百分秒的快照) | 0~99 |
| 50H~59H | 五组记录,每组 2 字节(秒、百分秒) | 共 10 字节 |
| 5AH | 已记录组数 | 0~5 |
| 5BH | 当前查看指针(组号) | 0~4 |
堆栈指针设为 60H,向上增长到 7FH,正好和 50H~5BH 的数据区错开。这里有个容易被忽略的坑:如果把变量放到 60H 以后,堆栈一压就把它覆盖了,症状是「调用子程序之后某个变量莫名其妙变了」,查起来极其费劲。
4.2 定时器 T0 的 10ms 节拍与秒进位
时基靠 T0 方式 1 中断产生,每 10ms 进一次,中断里做两件事:重装初值、把百分秒加一并处理进位。
ORG 000BH ; T0 中断入口 LJMP T0_ISR ORG 0100H T0_ISR: PUSH PSW PUSH ACC MOV TH0, #0DCH ; 先重装初值,再处理计数 MOV TL0, #00H ; 0xDC00 对应 10ms INC R5 ; 百分秒 +1 CJNE R5, #100, ISR_OUT ; 满 100 次(1 秒)才进位 MOV R5, #0 INC R4 ; 秒 +1 CJNE R4, #100, ISR_OUT MOV R4, #0 ; 99 秒之后回绕为 00 ISR_OUT: POP ACC POP PSW RETI重装初值放在中断服务程序最前面,是为了把中断响应延迟的影响压到最小。方式 1 不是自动重装,从进入中断到写 TH0/TL0 之间已经白跑了若干机器周期,这段时间每次中断都差不多,属于固定偏移,100 次累计下来约几十微秒,肉眼和秒表对比看不出来。R4、R5 在中断里故意不压栈,因为它们本身就是主程序和中断共享的状态量,压了反而会导致主程序读不到最新值。
初始化部分对应地写:
MAIN: MOV SP, #60H ; 堆栈从 60H 起,避开 50H~5BH 数据区 MOV TMOD, #01H ; T0 方式 1,16 位定时 MOV TH0, #0DCH MOV TL0, #00H SETB EA ; 开总中断 SETB ET0 ; 开 T0 中断 CLR TR0 ; 上电先不跑,等按开始键 LCALL INIT_7219 ; 显示驱动初始化 LCALL CLEAR_ALL ; 秒、百分秒、记录区清零注意 TR0 上电是清零的,秒表不会一通电就自己跑,必须等 KEY0 按下才置位。如果上电就跳数,八成是初始化里漏了 CLR TR0 或者复位后 RAM 里的残留值没清。
4.3 MAX7219 初始化序列与写时序
MAX7219 上电后默认处于关断模式,此时所有数码管熄灭。所以初始化必须按固定顺序走:先退出测试模式、设定扫描位数、设定译码方式、设定亮度,最后一步再切换到正常工作模式。顺序错一步,最常见的表现就是一上电全亮或者全灭。
; 写一帧: 41H 放寄存器地址, 40H 放数据 INIT_7219: MOV 40H, #00H MOV 41H, #0FH ; 显示测试寄存器,写 00 退出测试 LCALL WRIT_7219 MOV 40H, #03H MOV 41H, #0BH ; 扫描限制: 只扫 DIG0~DIG3 四位 LCALL WRIT_7219 MOV 40H, #00H MOV 41H, #09H ; 译码模式: 不译码,段码自己送 LCALL WRIT_7219 MOV 40H, #07H MOV 41H, #0AH ; 亮度: 0~15 取中间值 7 LCALL WRIT_7219 MOV 40H, #01H MOV 41H, #0CH ; 最后才退出关断,进入正常工作 LCALL WRIT_7219 RET ; 三线串行写一帧 16 位 WRIT_7219: CLR LOAD ; LOAD 拉低,开始一帧 MOV A, 41H ; 先送寄存器地址 LCALL SEND_BYTE MOV A, 40H ; 再送数据 LCALL SEND_BYTE SETB LOAD ; 上升沿锁存,本帧生效 RET SEND_BYTE: MOV R7, #08H ; 一字节 8 位 SB_LOOP: RLC A ; 最高位先出 MOV DIN, C CLR CLK SETB CLK ; 上升沿把数据推入内部移位寄存器 DJNZ R7, SB_LOOP RET几点说明。位定义在文件头用DIN BIT P3.0、LOAD BIT P3.1、CLK BIT P3.2声明,改端口时只改这三行。RLC 是带进位循环左移,移位前 A 的最高位先进 C,再由 C 送到 DIN,这正好符合 MAX7219 「高位先入」的要求。CLK 先拉低再拉高,保证每次都是一个干净的上升沿;如果写成先拉高再拉低,数据会在下降沿被采样,帧就整体错位,症状是数码管显示乱码或全亮。LOAD 必须在整整 16 位送完之后才拉高,中途抖动一下就会把半帧数据锁存进去。
译码模式选 0x00 而不是 0xFF,是因为这套程序要用段码表自己控制小数点。如果选全 BCD 译码,芯片只认 0~9 的数值,DP 段就不受数据位控制,秒和百分秒之间的分隔点会做不出来。
4.4 求商取余的显示拆分:一次 DIV 拆两位
四位显示要表达「秒.百分秒」,即 SS.CC 的形式。R4 存 0~99 的秒,拆成十位和个位;R5 存 0~99 的百分秒,同样拆成两位。拆分用一个除法指令就够:把待拆数放 A、除数 10 放 B,执行 DIV AB 后 A 是商(十位)、B 是余数(个位)。
; 把 42H(秒)拆到 DIG1/DIG2 XS0: MOV A, 42H MOV B, #10 DIV AB ; A = 十位, B = 个位 MOVC A, @A+DPTR ; 查段码表 MOV 41H, #01H ; DIG1 地址 MOV 40H, A LCALL WRIT_7219 MOV A, B MOVC A, @A+DPTR ADD A, #80H ; 加 0x80 点亮小数点 MOV 41H, #02H ; DIG2 地址 MOV 40H, A LCALL WRIT_7219 RET ; 把 43H(百分秒)拆到 DIG3/DIG4,小数点不再点亮 XS1: MOV A, 43H MOV B, #10 DIV AB MOVC A, @A+DPTR MOV 41H, #03H MOV 40H, A LCALL WRIT_7219 MOV A, B MOVC A, @A+DPTR MOV 41H, #04H MOV 40H, A LCALL WRIT_7219 RET段码表按共阴接法给出,DP 位单独占最高位:
| 数字 | 段码 | 加 0x80 后(带小数点) |
|---|---|---|
| 0 | 0x3F | 0xBF |
| 1 | 0x06 | 0x86 |
| 2 | 0x5B | 0xDB |
| 3 | 0x4F | 0xCF |
| 4 | 0x66 | 0xE6 |
| 5 | 0x6D | 0xED |
| 6 | 0x7D | 0xFD |
| 7 | 0x07 | 0x87 |
| 8 | 0x7F | 0xFF |
| 9 | 0x6F | 0xEF |
小数点只加在 DIG2 上,视觉上就是「12.34」读作 12 秒 34 百分秒,位置固定不会跑。这里有个细节必须处理:R4、R5 会被 T0 中断随时修改,如果主程序读到一半被打断,可能读到 R4 已进位而 R5 刚清零的中间态,显示上瞬间闪出一个错误值。稳的做法是先关 ET0 把两个值快照到 42H、43H,再开 ET0 慢慢显示:
DISPLAY: CLR ET0 ; 关 T0 中断,取一致快照 MOV 42H, R4 MOV 43H, R5 SETB ET0 MOV DPTR, #TAB LCALL XS0 LCALL XS1 RET4.5 键盘扫描与消抖:等待释放这一步不能省
按键扫描先整体读一次 P1,若读到 0xFF 说明无键按下,继续循环;否则延时约 10ms 后重读,若与第一次相同才确认有效,否则判定为抖动返回。确认之后按位判断是哪个键,分别跳到处理程序。
KEYIN: MOV P1, #0FFH ; 读之前先写全 1,准输入状态 NOP MOV A, P1 CJNE A, #0FFH, QUDOU ; 有键按下,去消抖 LJMP KEYIN QUDOU: MOV 30H, A ; 暂存第一次读数 LCALL DELAY10 ; 延时约 10ms MOV A, P1 CJNE A, 30H, KEYIN ; 两次不一致,判为抖动 ; 逐位判断是哪个键 KEY0: JB P1.0, KEY1 LJMP PKEY0 KEY1: JB P1.1, KEY2 LJMP PKEY1 ; …… KEY2~KEY5 同理 PKEY0: ; 开始键处理 MOV A, P1 NOP CJNE A, #0FFH, PKEY0 ; 等松手,防止一次按下算多次 SETB TR0 LJMP KEYIN等松手那三行是整套键盘程序里最容易漏、也最容易出洋相的地方。少了它,手指按下去的几十毫秒里主循环已经绕了好几圈,一次按下会被识别成十几次,表现就是按一下开始键,秒表直接从 00 跳到 05,或者按一下上翻直接翻到底。这个坑我见过不止一次,根源就在这。
4.6 保存、上翻、下翻:五组记录区的指针管理
记录区 50H~59H 共 10 字节,按每组 2 字节划分,正好五组。R0 指向当前可写组的低字节(秒),R1 = R0 + 1 指向高字节(百分秒),每写入一组后 R0、R1 各加 2 而不是加 1。5AH 记录已存组数,5BH 记录当前查看的组号。
保存键(KEY2)的处理是:读 5AH,若已达 5 组就忽略本次(或做覆盖最旧一组的处理),否则把当前 R4、R5 分别写入 @R0、@R1,R0、R1 各加 2,5AH 加 1。暂停键(KEY1)除了清零 TR0 停表,也可以顺手存一组,这就是原始需求里说的"双计时"用途——先记下第一个时间点,继续跑,再记第二个,两个值都能翻出来对比。
上翻下翻的边界处理要写成显式判断,不能只做加减。查看指针加到 5AH 就回 0,减到 0 以下就回到 5AH−1,这样循环翻阅不会越界读到未使用的 5AH、5BH 之外的空单元。翻阅时显示的是记录值而非实时值:把 5BH 对应的两个字节读到 42H、43H,再走一遍 XS0/XS1 就行。要让用户能分清当前看的是记录还是实时值,一个简单办法是翻页时把小数点熄灭,退出翻页时恢复——这个细节答辩时讲出来,比多写十个功能都加分。
5. 避坑与排查:秒表项目里最容易翻车的五个现场
5.1 上电后数码管全亮或全灭
现象:接通 5V 电源,四位数码管要么八段全亮成「8.8.8.8.」,要么一点反应都没有,按键也不起作用。
原因:MAX7219 上电默认进入关断模式且显示测试寄存器状态不确定,如果初始化序列里漏写 0x0C 寄存器,芯片永远停在关断模式;反过来,如果先写了数据再切正常模式,或者误把 0x0F 写成 0x01,就会一直卡在显示测试状态,所有段全亮。
解决:把初始化顺序固定成「0x0F=0x00 → 0x0B=0x03 → 0x09=0x00 → 0x0A=0x07 → 0x0C=0x01」,最后一条必须是退出关断。调试时可以先只写 0x0C=0x01 和 0x0F=0x00 两条,用万用表量 SEG 引脚有没有输出,确认时序通了再往下加。
5.2 显示闪烁或者只亮其中一位
现象:四位里只有一位稳定亮着,其余忽明忽暗,或者整块屏幕有轻微的高频抖动。
原因:扫描限制寄存器写错了。0x0B 写 0x00 时芯片只扫 DIG0 一位,写 0x03 才扫四位。另一种可能是段电流设得太大,四位同时点亮时 5V 电源被拉低,芯片欠压复位,表现为周期性闪断。
解决:先确认 0x0B 的值是 0x03;再把 ISET 电阻从 10kΩ 临时换成 20kΩ 试试,如果闪烁消失,说明是电源带不动,需要在 V+ 与 GND 之间就近加一只 10μF 电解加 0.1μF 瓷片做去耦,而不是继续加大电阻牺牲亮度。
5.3 计时越走越慢,实测比分秒表差一截
现象:跟手机秒表同时启动,跑满 60 秒时板子上显示 59 秒多,跑得越久差得越多。
原因:T0 是方式 1 非自动重装,每次进中断到写 TH0/TL0 之间有一段固定延迟,100 次累计起来就是可见误差。更常见的是主程序里有关中断的操作——比如显示快照那段 CLR ET0 之后做了太多事才 SETB ET0,中间丢掉了一次中断,节拍就少了一拍。
解决:重装初值紧贴在中断入口之后;关中断的临界区里只做两条 MOV,别夹带任何子程序调用;如果对精度要求更高,把误差值反过来预装进初值,比如算出每次中断固定偏移 8 个机器周期,就把初值从 0xDC00 改成 0xDC08。课程设计做到这个程度,答辩时讲出来已经够用了。
5.4 按一次键跳好几个数
现象:按一下开始键,秒表不是从 00 开始,而是直接跳到 03、05 甚至更大;按一下上翻,指针一下翻到最后一组。
原因:按键处理里没做「等待释放」,按键机械触点在按下和松开的瞬间都会产生几毫秒到几十毫秒的抖动,主循环绕得快,把这些抖动全当成了独立按键。
解决:在每个 PKEY 处理程序的最后,加一段「读到 P1 恢复全 1 才继续」的循环,例如PKEY0: MOV A,P1 / CJNE A,#0FFH,PKEY0。等待释放要加超时保护,否则按键卡住会把整个程序挂死,简单做法是循环里计数,超过一定次数强制跳出。
5.5 保存几组之后再翻页,数据整体串位
现象:存第一组时是对的,存到第三组之后,上翻下翻显示的值和实际记录的对不上,差半组或者错开一位。
原因:记录区指针的步长和记录单元宽度不一致。每组占 2 字节,指针必须每次加 2;如果照抄了「每按一次加 1」的写法,第二次写入就会覆盖掉第一组的百分秒字节,数据整体错位。
解决:把 R0、R1 的增量统一写成INC R0 / INC R0,或者一开始就让 R1 恒等于 R0+1,只在写完后统一调整 R0。调试阶段可以在保存后立刻上翻读回来显示,存一组验一组,比存满五组再查容易定位得多。
6. 验收与答辩:一张测试表把功能讲成证据
这套设计的验收不靠「我看着差不多」,靠一张能逐项打勾的测试表。流程是先烧片、再上电、按顺序跑测试项,每一项都记录现象,答辩时直接把表摆出来讲。通常我会准备下面这几项:
| 序号 | 测试项 | 操作步骤 | 预期现象 | 判定依据 |
|---|---|---|---|---|
| 1 | 上电自检 | 接通 5V,不按任何键 | 显示「00.00」,小数点固定在第 2 位 | 无全亮、无全灭、无乱码 |
| 2 | 计时精度 | 与标准计时器同时启动,跑 100 秒 | 两者差值 ≤ 1 秒 | 连续测三次取最大偏差 |
| 3 | 开始/暂停 | 按 KEY0 跑 5 秒,按 KEY1 停 3 秒再按 KEY0 | 暂停期间数值冻结,恢复后从冻结值继续 | 数值不跳变、不回零 |
| 4 | 复位 | 计时中按 KEY5 | 显示立即回到「00.00」并停表 | 记录区同时清空 |
| 5 | 保存与翻页 | 依次在不同时刻按 KEY2 存 3 组,再按 KEY3/KEY4 翻阅 | 三组数据与保存时读数一致,可循环翻阅 | 无串位、无越界乱码 |
| 6 | 连按测试 | 快速连按开始键 10 次 | 状态只在开始/暂停之间切换,不出现跳数 | 每次按下只响应一次 |
| 7 | 掉电重启 | 断电 5 秒后重新上电 | 恢复为「00.00」,正常工作 | 无需手动复位即可启动 |
第 2 项最容易出问题,也最有说服力。测的时候不要只跑 10 秒,10 秒内的误差肉眼看不出来;跑到 100 秒,累计误差才现形。如果超差,回到 5.3 逐条排。第 6 项是很多人不好意思测的——它专治「按一下跳三个数」,能过这一项,键盘消抖和等待释放才算真做对了。
答辩环节的讲法,我的建议是别从「我用了什么芯片」开场,而是从「这个秒表要解决什么问题、为什么不能只挂两个数码管」开场。先讲清主循环被动态扫描吃掉的矛盾,再引出 MAX7219 的分工;接着把内存账本和定时初值这两个数字讲透——0xDC00 是怎么算出来的、50H~59H 为什么刚好放五组。评委问得最多的一定是「定时精度怎么保证」和「按键怎么消抖」,这两块答得出来,其余都是细节。最后把测试表一亮,配一句「按这个表跑过三遍」,比任何形容词都管用。
从那以后我每次做带显示和按键的单片机项目,都会在画原理图之前先把端口分配表和内存账本写出来,写不满一页纸就说明方案还没想清楚。这套秒表的报告文档、汇编源码、电路连接图整理在一起存放,需要照着改课程设计的同学可以拿去对照。希望帮到你。
本文还有配套的精品资源,点击获取