简介:这份文档是华南理工大学2020年微机课程设计报告,面向学习微机原理、51单片机与汇编语言的学生及自学者,围绕电子日历时钟项目提供完整的设计思路与实现记录。报告从基本要求出发,涵盖北京时间显示、时间校准、汇编语言编程及小数点分隔时间格式,并扩展到公历日期显示与校准、秒表、闹钟、PC联机校准等功能;方案部分讨论了4位LED数码管静态与动态扫描驱动、矩阵键盘一键多用、定时中断基准时间选取、余辉处理及闰年平年判断等关键问题,还给出六种显示模式与按键操作逻辑。资源包共1个doc文件,约3.89MB,内容为课程设计报告书正文,包含功能要求、软硬件方案、程序流程与结果分析,适合作为课设参考与答辩准备材料。已有1145人学习,可帮助读者理解51单片机项目从需求到实现的完整链路。
1. 电子日历时钟:为什么一块 4 位数码管能同时装下时间和状态
一块 4 位 LED 数码管,只有 4 个数字位,却要显示年、月日、时分、秒毫秒、闹钟、秒表六种信息,还要在每种模式下支持校准。第一次看到这个需求,直觉反应是“位不够用”。但真正跑起来之后会发现,限制从来不是位数,而是人怎么在有限显示面积里建立稳定的心智模型。这份 51 单片机汇编课设报告给出的答案很直接:用外部中断做模式切换,用矩阵键盘做“一键多用”,用小数点做状态分隔符,把 4 位数字的语义交给当前 MODE 决定。换句话说,显示内容不是固定的,而是被一个状态机动态解释的。搞懂这套机制,比背会任何一条 51 指令都值——因为它解释了嵌入式系统里最典型的一类设计取舍:资源极度受限时,如何用软件状态去换硬件面积。下面从定时基准、显示扫描、按键复用、日历边界四个层面,把这份汇编实现拆开看。
2. 定时基准:为什么 50ms 中断能撑起一个“秒”
2.1 定时器初值 0x3CB0 是怎么算出来的
报告里定时器 0 工作在方式 1,16 位定时模式,初值TH0=0x3C、TL0=0xB0。这个值不是随手写的。51 单片机常见晶振为 12MHz 时,机器周期是 1μs,方式 1 最大计数值 65536,从0x3CB0计到溢出需要65536 - 0x3CB0 = 50000个机器周期,也就是 50ms。50ms 是秒的整除数,20 次中断正好凑成 1 秒,这就是报告里“定时时间应是秒的整除数”那句话的落地方式。
MOV TMOD, #11H ; 定时器0和1都工作在方式1,16位定时 MOV TL0, #0B0H ; 低8位初值 MOV TH0, #03CH ; 高8位初值,合计0x3CB0,50ms@12MHz MOV TL1, #0B0H ; 定时器1同样配置,用于秒表毫秒计时 MOV TH1, #03CH这里有个容易被忽略的细节:定时器 1 用了同样的初值,但它的用途不是走时钟,而是给秒表的毫秒位提供时基。时钟和秒表共用同一套 50ms 节拍,只是各自维护独立的计数器。这种“一个时基、多个消费者”的做法在资源紧张的 51 系统里很常见,代价是秒表精度被锁死在 50ms 粒度上,报告里毫秒位显示的是“50ms 的倍数”而不是真正的毫秒。
2.2 计数器链:从 50ms 到年,靠的是级联判断
中断服务程序里并没有把“秒、分、时、日、月、年”写成一条链,而是用计数器 R6 对中断次数累加,每到 20 次触发一次秒进位,秒满 60 进分,分满 60 进时,时满 24 进日。这种级联判断的写法在汇编里比查表更省空间,但代价是每次进位都要做一串比较跳转。
; 定时器0中断服务程序(结构示意) EINT0: PUSH ACC INC R6 ; 中断次数计数 CJNE R6, #20, EINT0_EXIT MOV R6, #0 INC SECOND ; 满20次,秒加1 CJNE SECOND, #60, EINT0_EXIT MOV SECOND, #0 INC MINUTE ; 后续分、时、日、月、年同理级联 EINT0_EXIT: POP ACC RETI参数含义上,R6是 50ms 计数寄存器,SECOND、MINUTE、HOUR分别对应 2CH、2BH、2AH 单元。这里的设计动机很明确:51 的定时器最长只能定 65ms 左右(12MHz 下方式 1),不可能直接产生 1 秒中断,所以必须用“短基准 + 软件计数”的方式合成秒。基准越短,秒的长期误差越小,但中断开销越大。50ms 是一个折中值,既不会让 CPU 频繁进出中断,又能把秒误差控制在可接受范围。
2.3 中断优先级:为什么定时器 0 排在外部中断 0 前面
初始化段里有一组优先级设置:SETB PT0、SETB PX0、SETB PT1,报告注释写明优先级从高到低是定时器 0、外部中断 0、定时器 1、外部中断 1。这个顺序不是随便排的。定时器 0 负责走时,一旦被按键中断长时间阻塞,秒计数就会丢拍;外部中断 0 负责模式切换,响应要快但不能压过走时;定时器 1 只服务秒表,优先级最低合理。血泪经验是:如果让按键中断优先级高于定时器 0,长按按键时时钟会明显变慢,这种“玄学走时不准”的根因往往就在优先级配置上。
3. 动态扫描与 74HC595:4 位数码管怎么被“骗”出稳定显示
3.1 消影为什么必须写在送段码之前
报告里SEG_DISPLAY的流程是:先算字型码,再把 P0 低四位清零(消影),然后送段码、点亮对应位、延时、再消影下一位。这个顺序不能反。LED 数码管有余辉,如果新段码写入时上一位还没完全熄灭,视觉上会出现“鬼影”,表现为相邻位数字重叠模糊。报告原文专门强调“新内容写入显示器之前须将所有的 LED 数码管熄灭”,说的就是这件事。
DISPLAY1: MOV A, P0 ANL A, #0F0H ; 保留高四位,低四位清零,消影 MOV P0, A MOV A, GEWEI ; 取个位字型码 ACALL SEND_BYTE ; 通过595串行送出 ORL P0, #08H ; P0.3置1,选中最右位数码管 MOV R7, #10H LCALL DELAY ; 保持点亮,保证亮度ANL A,#0F0H这一步是消影的核心,它把 P0 低四位(位选线)全部拉低,让当前所有数码管瞬间熄灭,再送新段码。延时R7=10H控制的是单位点亮时间,太短亮度不够,太长扫描周期变长会闪。常见的实现思路是让消影延时略短于点亮延时,既压住鬼影又不至于明显变暗。
3.2 74HC595 串行送字节:为什么要移 9 次而不是 8 次
SEND_BYTE里有个反直觉的细节:MOV R7,#9,循环 9 次。标准 595 移位是 8 位,这里多移一次是为了配合RCK锁存时序。代码先CLR RCK再SETB RCK再CLR RCK,制造一个锁存脉冲,把移位寄存器内容推到输出锁存器。多出的那一次循环让最后一位数据有足够时间稳定在SER线上,避免末位抖动。这种写法在汇编里属于“时序补丁”,C 语言里编译器可能帮你插了 NOP,手写汇编就得自己数周期。
SEND_BYTE: MOV BYTE, A MOV A, #80H ; 从最高位开始判断 MOV R7, #9 ; 9次循环,末次用于时序稳定 JUDGE_R7: DJNZ R7, SEND_LOOP CLR RCK SETB RCK ; 锁存脉冲 CLR RCK RET SEND_LOOP: RL A MOV B, A ANL A, BYTE JNZ SET_SER CLR SER ; ... 移位时钟翻转参数上,SER是 P0.7 数据线,SRCK是 P0.4 移位时钟,RCK是 P0.5 锁存时钟,RST是 P0.6 复位端,初始化时CLR RST保证 595 不被复位。这套引脚分配和 STC-10F08XE 开发板的走线绑定,换板子就得改。
3.3 模式 4 和模式 5 的小数点点亮逻辑
报告在扫描每一位时都插了一段CJNE A,#4或CJNE A,#5的判断,用来在特定模式下点亮小数点。模式 4 是闹钟,用小数点区分“这是闹钟时间不是当前时间”;模式 5 是秒表,用小数点分隔分、秒、毫秒。这个设计很省——不需要额外指示灯,复用数码管自带的小数点段就能表达状态。踩坑点在于:小数点点亮是在段码送出之后通过MOVC A,@A+DPTR取第 10 号字型码实现的,如果字型表里第 10 项不是“小数点单独点亮”的编码,就会出现数字和小数点一起亮的翻车现场。
4. 按键复用与状态机:两个外部中断怎么管住六个模式
4.1 INT0 切模式、INT1 切校准,TD 变量是开关
整个交互模型只有两个物理中断键加四个矩阵键。INT0 每按一次MODE加一,到 6 归零;INT1 翻转TD变量,TD=1表示进入校准态,TD=2表示退出。校准态下,矩阵键盘 S11、S12、S15、S16 的语义由当前 MODE 决定:模式 0 下分别控制年份的千、百、十、个位,模式 1 下控制月加、月减、日加、日减,模式 2 和模式 4 下控制时加、时减、分加、分减,模式 3 下控制秒的十位和个位,模式 5 下控制秒表启停清零。
PRO_INT0: PUSH ACC MOV R7, #10 ACALL DELAY ; 按键消抖 JB P3.2, INT0R ; 确认仍按下 MOV A, TD CJNE A, #1, INT0_NEXT MOV TD, #2 ; 校准态下按INT0,退出校准 AJMP INT0R INT0_NEXT: INC MODE ; 正常态下切换模式 ACALL BEEP_SHORT MOV A, MODE CJNE A, #6, INT0R MOV MODE, #0 ; 模式溢出归零 INT0R: POP ACC RETI这段逻辑里TD的取值设计值得注意:0 是正常态,1 是校准态,2 是“刚退出校准”的过渡态。为什么要多一个 2?因为 INT1 中断里判断TD等于 1 才启动键盘扫描,等于 2 时把TD清回 0。这样一次按键产生“进入—退出”两个明确状态,避免长按导致反复进出校准。这种用状态值而非布尔量表达瞬态的做法,在按键交互里是减少玄学 bug 的有效手段。
4.2 矩阵键盘扫描与“一键多用”的边界
报告没有贴出完整的KEYSCAN1代码,但从调用关系能推断:校准态下主循环反复调用KEYSCAN1,扫描 S11、S12、S15、S16 四键,根据MODE分派到不同的加减逻辑。一键多用的代价是代码里出现大量CJNE A,#0、CJNE A,#1的分支,可读性下降,但换来了物理按键数量从十几个压到四个。对于时钟类产品,按键少本身就是需求——报告原文写“时钟的按键不宜多”,这是从用户操作成本出发的约束,不是单纯省 IO。
4.3 闹钟检测为什么放在主循环入口而不是中断里
START段一进来就先比较CLOCK_H和HOUR、CLOCK_M和MINUTE,相等就调BEEP_CLOCK响三短一长。这个检测放在主循环而不是定时中断里,原因是蜂鸣器响需要毫秒级延时,放在中断里会阻塞走时。主循环轮询的代价是闹钟触发有一轮循环的延迟,但人耳根本感知不到。设计取舍很清楚:走时精度优先,闹钟响应可以容忍几十毫秒的滞后。
5. 日历边界:月大月小和闰年为什么不能靠查表糊弄
5.1 2 月天数的两级判断
报告明确写了规则:普通年能整除 4 且不能整除 100 为闰年,能整除 400 也是闰年;平年 2 月 28 天,闰年 29 天。4、6、9、11 月固定 30 天,其余 31 天,2 月单独算。这段逻辑在汇编里通常写成:先判断月份是否等于 2,是则进闰年判断,否则查月大月小表或做位测试。
; 日期进位时的2月判断(结构示意) CHECK_FEB: CJNE MONTH, #2, CHECK_OTHER ; 闰年判断:年%4==0 且 (年%100!=0 或 年%400==0) MOV A, YEAR_L ANL A, #03H ; 低两位为0说明能被4整除 JNZ FEB_28 ; 进一步判断整百年 ; ... FEB_28: MOV A, #28 SJMP SET_DAY_MAX这里有个容易翻车的点:年份存成YEAR_H和YEAR_L两个字节,判断整除 4 只需要看YEAR_L低两位,但判断整除 100 和 400 要跨字节运算。如果只判断了低字节,2100 年会被误判为闰年。报告默认年份 2020,短期内碰不到整百年,但代码里如果省了 100 和 400 的判断,就是埋雷。
5.2 校准时的边界钳制
模式 1 下按 S11 月份加一,如果加到 13 必须回 1;日期加到超过当月最大天数必须回 1。报告的结果分析里展示了从 0826 按 S12 和 S16 后的变化,但没有展开越界处理。常见的实现思路是在加减之后立刻做一次范围比较,超上限置最小值,低于最小值置最大值。这一步如果漏了,用户能把日期调到 2 月 30 日,后续走时进位就会错乱。
5.3 秒表与主时钟的隔离
模式 5 的秒表用WATCH_MIN、WATCH_SEC、WATCH_MSEC三个独立单元,和主时钟的MINUTE、SECOND完全分开。进入模式 0 时主循环会把秒表三个单元清零并CLR TR1,保证切走时秒表停住。这种隔离设计避免了秒表计时污染当前时间,代价是定时器 1 的中断服务程序里要同时维护两套计数。如果两套计数共用一个变量,切模式时就会出现时间跳变,这是这类课设里高频出现的 bug。
6. 避坑与排查:这份汇编实现里最容易翻车的五处
6.1 现象:数码管显示闪烁且伴随鬼影
原因通常是消影顺序写反,或者点亮延时R7设得太小。如果先送段码再清位选,上一位的余辉会和新段码叠加。解决方法是严格按“清位选→送段码→开位选→延时”的顺序,并把消影延时设为点亮延时的三分之一左右。
6.2 现象:时钟走时偏慢,按一次键慢好几秒
根因多半是中断优先级配置错误,按键中断阻塞了定时器 0。检查PT0、PX0是否置位,确保定时器 0 优先级最高。另一个可能是中断服务程序里做了耗时操作,比如在定时中断里直接调蜂鸣器延时。
6.3 现象:闹钟到点不响,或者响个不停
前者检查CLOCK_H、CLOCK_M的初值和比较逻辑,注意比较的是“时”和“分”同时相等,只比一个不会触发。后者是因为主循环每轮都检测,相等期间会反复调用BEEP_CLOCK,需要在响过一次后加标志位或把闹钟时间清掉,否则一分钟内会连续触发。
6.4 现象:校准模式下按键无反应
先确认TD是否真的进入了 1。INT1 中断里INC TD之后有CJNE A,#1的判断,如果TD初值不是 0 或者被别处改写,就会跳过KEYSCAN1。另外矩阵键盘扫描需要消抖,KEYSCAN1内部如果没有延时确认,快速按键会被漏掉。
6.5 现象:切换模式后秒表数值跳变
检查模式 0 分支里是否执行了CLR TR1和三个秒表单元清零。如果只在进入模式 5 时初始化,从其他模式切回来会带着上次的残留值。隔离原则是:秒表状态只在模式 5 内有效,离开即复位。
7. 从这份课设看资源受限系统的设计取舍
这份实现最值得琢磨的地方,不是它用了多少条指令,而是它在“显示位数、按键数量、定时器精度”三重约束下做出的取舍。4 位数码管装六种信息,靠的是 MODE 状态机把语义动态化;两个中断键管住全部交互,靠的是 TD 变量区分正常态和校准态;50ms 基准合成秒,靠的是软件计数换硬件定时器的上限。每一条都不是最优解,但组合起来是一个能跑、能演示、能扩展的完整系统。
如果要把这套东西往产品方向推,我会优先改三处:把 50ms 基准换成更短的时基并做误差补偿,把闹钟检测从主循环挪到独立比较逻辑避免重复触发,把闰年判断补全 100 和 400 的跨字节运算。这三处改动都不大,但决定了它是“课设能跑”还是“长期走时可信”。
我自己的习惯是,每次写完定时中断和显示扫描,都强制用示波器或逻辑分析仪看一眼位选波形和中断周期,确认消影时间和 50ms 节拍没有被人为拉长。汇编里一条多余的LCALL DELAY就能让走时慢出肉眼可见的偏差,这种坑光看代码是看不出来的。希望这份拆解帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取