微机课程设计:用汇编写一个电子表

微机课程设计:用汇编写一个电子表

August 25, 2020·
ToddZ

享受操纵所有寄存器的快感~

声明

本项目由 ZZF(aka:Todd Zhou)设计,由ZZF、LDF、ZJC编程实现,作为大二下微机课程设计的作业。未经本人允许,任何人不准使用、转载里面的文字、表格、图片等。你可以参考,但不允许剽窃。

另外,这里所有的汇编代码均由我们(ZZF、LDF、ZJC)手动输入,仅参考了开发板附带的C语言范例(用于确定硬件连线及时序),未参考互联网、往届同学的设计。因此我可以保证本项目的原创性。

题目

通过按键切换完成以下功能:

  • 基本要求
    • 显示北京时间;
    • 能够校准时间;
    • 使用汇编语言;
    • 时、分、秒之间或年、月、日间以小数点分隔;
  • 发挥性要求
    • 显示公历日期
    • 能够校准日期
    • 运动秒表
    • 闹钟功能
    • 倒计时
    • 亮度调节
    • PC联机校准时间等

完成时间:

  • 2020/08/25:计时
  • 2020/08/26:显示
  • 2020/08/27:按键
  • 2020/08/28:验收
  • 2020/08/30:成绩94分(ノ´▽`)ノ♪

分析

程序

  从题目中可以看出,主要有三个部分:

  1. 显示
  2. 计时
  3. 按键

  这三个部分需要并行运行,其中,显示和计时都是间隔一段时间再运行,只有按键需要一直扫描,所以显示和计时分别用定时器0和定时器1来定期运行,按键则在主程序中循环扫描。

  那么可不可以都用中断来响应按键呢?很遗憾,受硬件限制,不能。开发板上的中断0和1分别连接了一个按键,但是要实现所有功能,至少需要 4 个按键,显然中断上连接的按键是不够用的。当然,也可以两个按键用中断,两个按键用扫描,但这样的话太麻烦了,代码写起来不够美观,不过出于炫技可以试一下( ̄▽ ̄)~*。

数据

  那么如何存储数据呢?综合来看,我们主要需要存储以下数据:

  • 时间
    • 当前时间:毫秒、秒、分、时、日、月、年
    • 闹钟:分、时
    • 秒表:毫秒、秒、分
    • 倒计时:毫秒、秒、分
  • 状态
    • 当前模式:时间、日期、年份、闹钟、秒表、倒计时
    • 开关:闹钟、秒表、倒计时
    • 选中LED闪烁:前两位、后两位
    • LED闪烁计时

三个程序部分通过数据连接起来,即:计时改变时间数据,按键改变时间和状态数据,显示则读取时间和状态数据。

除此之外,还有一些数据要存放在 ROM 中,比如每个月的天数、LED显示编码等

设计

数据格式

  总的来说,时间存放字节寻址区(30H~7FH),状态存放在位寻址区(20H~2FH)。

程序中的变量名应该与括号中的英文一致。

字节寻址区

当前时间(Time)存放在 50H~56H,具体来说:

  • 56H:年(只保存后两位,即 20XX 的 XX)(Time_year
  • 55H:月(Time_month
  • 54H:日(Time_day
  • 53H:时(Time_hour
  • 52H:分(Time_minute
  • 51H:秒(Time_second
  • 50H:毫秒×10(Time_ms

闹钟(Alarm)存放在 57H~58H,具体来说:

  • 57H:分(Alarm_minute
  • 58H:时(Alarm_hour

秒表(Stopwatch)存放在 59H~5BH,具体来说:

  • 59H:毫秒×10(Stopwatch_ms
  • 5AH:秒(Stopwatch_second
  • 5BH:分(Stopwatch_minute

倒计时(Countdown)存放在 5CH~5EH,具体来说:

  • 5CH:毫秒×10(Countdown_ms
  • 5DH:秒(Countdown_second
  • 5EH:分(Countdown_minute

待显示(Show)的二进制数

  • 5FH:低两位,即右边两位(Show_low
  • 60H 高两位,即左边两位(Show_high

数码管亮度(Brightness):

  • 65H:亮度(Brightness

位寻址区

当前模式(Mode)存放在 20H,具体来说:(从低位到高位)

  1. 时间(Mode_time_bit
  2. 日期(Mode_date_bit
  3. 年份(Mode_year_bit
  4. 闹钟(Mode_alarm_bit
  5. 秒表(Mode_stopwatch_bit
  6. 倒计时(Mode_countdown_bit

开关(Flag)存放在 21H,具体来说:(从低位到高位)

  1. 闹钟开关(Flag_alarm_bit
  2. 秒表开关(Flag_stopwatch_bit
  3. 倒计时开关(Flag_countdown_bit
  4. 静音开关(Flag_mute_bit)(判断蜂鸣器响后是否有按下按钮)

LED闪烁(Blink)存放在 22H,具体来说:(从低位到高位)

  1. 低两位LED(Blink_low_bit
  2. 高两位LED(Blink_high_bit

闪烁计数0、1、2(Blink_count)存放在 23H~25H,具体来说:

  • 23H:闪烁计数0(Blink_count0
    • 闪烁计数0最高位(Blink_count0_bit
  • 24H:闪烁计数1(Blink_count1
    • 闪烁计数1最高位(Blink_count1_bit
  • 25H:闪烁计数2(Blink_count2
    • 闪烁计数2最高位(Blink_count2_bit

另外规定一下,R0R1 是按键循环用,R2R3 是计时用,R4~7 是LED用

引脚

数码管(LED)选中端:

  • P0.3:右起第一个LED(LED1
  • P0.2:第二个LED(LED2
  • P0.1:第三个LED(LED3
  • P0.0:第四个LED(LED4

HC595 串行转并行芯片相关引脚:

  • P0.4:HC595_SCK
  • P0.5:HC595_RCK
  • P0.6:HC595_RST
  • P0.7:HC595_DAT

蜂鸣器引脚:

  • P1.3:BEEP(置零时响)

键盘扫描引脚:

  • P2.4:改变模式键(Change_mode
  • P2.5:设置键(Setting
  • P2.6:上键(Up
  • P2.7:下贱~(Down

按键逻辑

一共有四个按键,分别为:

  • c:change mode 功能切换
  • s:setting 设置
  • u:up 上
  • d:down 下
  1. 在任何情况下按 c:切换模式并停止闪烁;

  2. 在时间、日期、倒计时下按 s:前两位LED闪烁;再按 s:后两位LED闪烁;再按 s:不闪烁;

    在年下按 s:四位一起闪烁;再按 s:不闪烁;

    在秒表下按 s:停止计时并清零;

  3. 在闪烁时按下 u/d:对应数据增/减;

    在闹钟下按 u:开启闹钟;

    在闹钟下按 d:关闭闹钟;

    在秒表下按 u:开始计时;

    在秒表下按 d:暂停计时;

    在倒计时下按 u:开始计时

    在倒计时下按 d:暂停计时

为了方便理解,可以参考下面的状态转换图:

graph LR
Time[时间] -->|c| Date[日期] -->|c| Year[年] 
Year-->|c| Alarm[闹钟] -->|c| Stopwatch[秒表] --> |c| Countdown[倒计时] --> |c| Time
graph LR
Time[时间] -->|s| Time_blink_min[分闪烁] --> |s| Time_blink_hour[时闪烁] --> |s| Time
Time_blink_min --> |u| Time_add_min[分增加] 
Time_blink_min --> |d| Time_sub_min[分减少]
Time_blink_hour --> |u| Time_add_hour[时增加]
Time_blink_hour --> |d| Time_sub_hour[时减少]
graph LR
Alarm[闹钟] -->|s| Alarm_blink_min[分闪烁] --> |s| Alarm_blink_hour[时闪烁] --> |s| Alarm
Alarm_blink_min --> |u| Alarm_add_min[分增加] 
Alarm_blink_min --> |d| Alarm_sub_min[分减少]
Alarm_blink_hour --> |u| Alarm_add_hour[时增加]
Alarm_blink_hour --> |d| Alarm_sub_hour[时减少]
Alarm --> |u| Alarm_start[闹钟开启]
Alarm --> |d| Alarm_stop[闹钟关闭]
graph LR
Stopwatch[秒表] -->|s| Stopwatch_clear[秒表清零]
Stopwatch --> |u| Stopwatch_start[秒表开始]
Stopwatch --> |d| Stopwatch_stop[秒表关闭]
graph LR
Countdown[倒计时] -->|s| Countdown_blink_min[分闪烁] --> |s| Countdown_blink_hour[时闪烁] --> |s| Countdown
Countdown_blink_min --> |u| Countdown_add_min[分增加] 
Countdown_blink_min --> |d| Countdown_sub_min[分减少]
Countdown_blink_hour --> |u| Countdown_add_hour[时增加]
Countdown_blink_hour --> |d| Countdown_sub_hour[时减少]
Countdown --> |u| Countdown_start[倒计时开始]
Countdown --> |d| Countdown_stop[倒计时暂停]

当然里面还有很多细节(比如蜂鸣器等)并没有展示出来。

编程

;程序结构一览:
; 51Watch/
;├─ 定义RAM区变量/
;├─ 程序跳转/
;├─ 定义ROM区数据/
;├─ MAIN主程序/
;│  ├─ 设置寄存器初值
;│  ├─ 键盘扫描/
;│  │  ├─ 读取按键
;│  │  ├─ 消抖
;│  │  ├─ 响应按键/
;│  │  │  ├─ 响应change mode键
;│  │  │  ├─ 响应setting键
;│  │  │  ├─ 响应up键
;│  │  │  ├─ 响应down键
;├─ 定时器0(计时)/
;│  ├─ 增加毫秒、秒、分、时、日、月、年
;│  ├─ 检查闹钟
;│  ├─ 增加秒表
;│  ├─ 减少倒计时
;├─ 定时器1(显示)/
;│  ├─ 读取当前模式&闪烁模式
;│  ├─ 读取待显示数据
;│  ├─ 数据转换
;│  ├─ 输出到HC595
;│  ├─ 显示
;├─ 中断0(增加亮度)/
;├─ 中断1(减少亮度)/

定义RAM区变量

  这一部分和前面“设计”的类似,但为了实现额外的功能,可能会增加几个变量。

详细代码
;;;;;;;; 定义RAM区变量 开始  ;;;;;;;;;;;

;命名规则:
;	首字母大写,后面字母小写的都是RAM变量;
;	后面带bit的都是位变量

;Time 当前时间;
Time_ms            EQU 50H ;实际为 msx10,后面的ms都一样
Time_second        EQU 51H
Time_minute        EQU 52H
Time_hour          EQU 53H
Time_day           EQU 54H
Time_month         EQU 55H
Time_year          EQU 56H

;Alarm 闹钟;
Alarm_minute       EQU 57H
Alarm_hour         EQU 58H

;Stopwatch 秒表;
Stopwatch_ms       EQU 59H
Stopwatch_second   EQU 5AH
Stopwatch_minute   EQU 5BH

;Countdown 倒计时;
Countdown_ms       EQU 5CH
Countdown_second   EQU 5DH
Countdown_minute   EQU 5EH
	
;Show 待显示的二进制数;
Show_low           EQU 5FH
Show_high          EQU 60H

;BCD 待显示的BCD字符
BCD1               EQU 61H
BCD2               EQU 62H
BCD3               EQU 63H
BCD4               EQU 64H
	
;Brightness 亮度;
Brightness         EQU 65H

;Last_button 上一个按键;(用于防止按键过快响应)
Last_button        EQU 66H

;Mode 当前模式;
Mode               EQU 20H
Mode_time_bit      EQU 00H
Mode_date_bit      EQU 01H
Mode_year_bit      EQU 02H
Mode_alarm_bit     EQU 03H
Mode_stopwatch_bit EQU 04H
Mode_countdown_bit EQU 05H

;Flag 开关标志;
Flag               EQU 21H
Flag_alarm_bit     EQU 08H
Flag_stopwatch_bit EQU 09H
Flag_countdown_bit EQU 0AH
Flag_mute_bit      EQU 0BH ;判断蜂鸣器响后是否有按下按钮
Flag_show_stopwatch_ms EQU 0CH


;Blink LED闪烁;
Blink              EQU 22H
Blink_low_bit      EQU 10H
Blink_high_bit     EQU 11H

;Blink_count0 闪烁计数0; 一个不够用,所以要三个(其实我甚至想要四个)
Blink_count0       EQU 23H
Blink_count0_bit   EQU 1FH

;Blink_count1 闪烁计数1;
Blink_count1       EQU 24H
Blink_count1_bit   EQU 27H

;Blink_count2 闪烁计数2;
Blink_count2       EQU 25H
Blink_count2_bit   EQU 2FH


;数码管选中端;
LED1        BIT P0.3 ;最右边
LED2        BIT P0.2
LED3        BIT P0.1
LED4        BIT P0.0 ;最左边

;HC595 芯片相关引脚;
HC595_SCK   BIT P0.4
HC595_RCK   BIT P0.5
HC595_RST   BIT P0.6
HC595_DAT   BIT P0.7

;蜂鸣器引脚;
BEEP        BIT P1.3

;键盘扫描引脚;
Change_mode BIT P2.4
Setting     BIT P2.5
Up          BIT P2.6
Down        BIT P2.7

;;;;;;;;; 定义RAM变量 结束 ;;;;;;;;;;;;

初始化程序跳转

  这个地址是固定的,也就没啥好说的。反正为了炫技,两个中断,两个定时器都用上了。

详细代码
;;;;;;;;; 程序跳转 开始 ;;;;;;;;;;;

ORG 0000H
	LJMP MAIN ;主程序(键盘扫描)
ORG 0003H
	LJMP BRIGHTER ;外部中断0(亮度增加)
ORG 000BH
	LJMP INT_T0 ;定时器0(计时)
ORG 0013H
	LJMP DARKER ;外部中断1(显示)
ORG 001BH
	LJMP INT_T1 ;定时器1(亮度减少)
	
;;;;;;;; 程序跳转 结束 ;;;;;;;;;;;

定义ROM区

  这里存储的是固定的数据。

  • DAYS_OF_MONTH:每个月的日数。由于这个是用于比较日期有无超过当月日数,所以实际是当月日数再+1。
  • LED_SEGMENT_CODELED_SEGMENT_CODE_DOT:LED显示编码,即相应数字需要点亮的LED。带 _DOT 表示会点亮数码管右下角的小数点。
详细代码
;;;;;;;; ROM区数据 开始 ;;;;;;;;;;

ORG 0030H

DAYS_OF_MONTH:
	DB 0 ;0月(不存在)
	DB 32,29,32,31 ;一、二、三、四
	DB 32,31,32,32 ;五、六、七、八
	DB 31,32,31,32 ;九、十、十一、十二

;学校的开发板
LED_SEGMENT_CODE:
	DB 0FCH,60H,0DAH,0F2H,66H,0B6H,0BEH,0E0H,0FEH,0F6H,00H ;0~9 不带点
CHECK_COUNTDOWN_BUG:
	DB 0FCH,60H,0DAH,0F2H,66H,0B6H,0BEH,0E0H,0FEH,0F6H,00H ;0~9 不带点
LED_SEGMENT_CODE_DOT:
	DB 0FDH,61H,0DBH,0F3H,67H,0B7H,0BFH,0E1H,0FFH,0F7H ;0.~9. 带点
		
;;;;;;;; ROM区数据 结束 ;;;;;;;;;

设置变量初值&按键

  预设好开机时显示的时间、中断开关(IE)、中断优先级(IP)、计时器模式(TMOD)等。然后就开始主程序——按键扫描。

  按键扫描的过程很简单。我们对行引脚赋0,列引脚赋1,按下按键时,对应的列引脚与行引脚导通,使得列引脚的输入为0,读出当前引脚状态,就知道哪个键被按下。

  知道后就根据当前模式和闪烁状态来作出相应的响应(这句话有点绕)。我们一个一个讲:

  1. 按下了 C 键:
    • Mode 左移一位,如果到了最高位,则赋值 01H
    • Blink 清零
  2. 按下了 S 键:
    1. 如果是在时间、日期、闹钟、倒计时模式:
      • Blink 左移一位,如果到了最高位,则幅值 00H
    2. 如果是在年模式:
      • Blink 在 11H 和 00H 之间相互转换
    3. 如果是在秒表模式:
      • Flag_stopwatch_bit 清零,停止计时
      • Stopwatch_msStopwatch_secondStopwatch_minute 全部清零,计时清零
  3. 按下了 U
    1. 如果是在时间、日期、年、闹钟、倒计时模式且 Blink 不等于0:
      • 对应数据(分/时/日/月/年)加 1
    2. 如果是在倒计时且 Blink 等于 0:
      • 开始倒计时,Flag_countdown_bit 置 1
    3. 如果是在闹钟且 Blink 等于 0:
      • 打开闹钟,Flag_alarm_bit 置 1
    4. 如果是在秒表:
      • 开始计时,Flag_stopwatch_bit 置 1
  4. 按下了 D
    1. 如果是在时间、日期、年、闹钟、倒计时模式且 Blink 不等于0:
      • 对应数据(分/时/日/月/年)减 1
    2. 如果是在倒计时且 Blink 等于 0:
      • 暂停倒计时,Flag_countdown_bit 置 0
    3. 如果是在闹钟且 Blink 等于 0:
      • 关闭闹钟,Flag_alarm_bit 置 0
    4. 如果是在秒表:
      • 暂停计时,Flag_stopwatch_bit 置 0

  还有一些小细节,比如:如果现在是 2020年2月29日,现在我们增加年份,变成 2021年2月,此时日要改为 28.

详细代码
;;;;;;;; MAIN主程序 开始 ;;;;;;;;;;

ORG 100H

MAIN:
	;设置年月日时分秒初值
	MOV Time_year,   #20 ;2020 ;年只存储个位和十位
	MOV Time_month,  #08 ;8月29日晚20点10分在科学馆406会议室验收
	MOV Time_day,    #29
	MOV Time_hour,   #20
	MOV Time_minute, #10
	MOV Time_second, #50
	MOV Time_ms,     #0
	
	;设置闹钟初值
	MOV Alarm_hour,   #20
	MOV Alarm_minute, #11
	
	;设置秒表初值
	MOV Stopwatch_minute, #0
	MOV Stopwatch_second, #0
	MOV Stopwatch_ms,     #0
	
	;设置倒计时初值
	MOV Countdown_minute, #0
	MOV Countdown_second, #10
	MOV Countdown_ms,     #0
	
	;设置亮度初值
	MOV Brightness, #1000B
	
	;设置模式Mode初值
	MOV Mode, #01H
	
	;设置开关Flag初值
	MOV Flag, #00H
	SETB Flag_alarm_bit ;打开闹钟
	
	;设置LED闪烁Blink初值
	MOV Blink, #00B
	
	;设置闪烁计数Blink_count初值
	MOV Blink_count0, #00H
	MOV Blink_count1, #00H
	
	;设置HC595
	SETB HC595_RST
	MOV P0, #00H
	
	;MOV IE, #10001010B ;不开启亮度调节
	MOV IE, #10001111B ;开启亮度调节
	
	;计时(定时器0)的优先级高于其他
	MOV IP, #00000010B
	
	;定时器0定时时间较长,故为模式1
	;定时器1用于显示,需要定时刷新,为模式2
	MOV TMOD, #00100001B
	
	;定时器0的计算过程在定时器0中断子程序处
	;定时器1就是取最大时间
	MOV TL0, #006H
	MOV TH0, #0D9H
	MOV TL1, #00H
	MOV TH1, #00H
	
	;开启定时器0、定时器1
	SETB TR1
	SETB TR0
	
	;;;;;;;;;;;;;;;; 键盘 开始 ;;;;;;;;;;;;;;;;;
	
	LOOP:
		MOV P2, #0FH ;矩阵键盘行赋1,列赋0
		MOV R0, P2 ;获取矩阵键盘输入
		;本来想着“没有按下按键”就不用消抖了,直接重新扫描就行
		;但学校的开发板的按键抖动得太厉害
		;不得已连“没有按下按键”也要先消抖再判断
		;如果开发板比较新,可以去掉下面三行的注释
		;CJNE R0, #0FH, DOUBLE_CHECK ;如果按下按键则二次检查
		;MOV Last_button, #0 ;清除上一个按键
		;SJMP LOOP
		
		DOUBLE_CHECK: ;二次检查,消抖
			MOV A, #10 ;两次检查的间隔时长 ;可根据抖动情况适当修改为1~99,建议不要超过10
			ADD A, Time_ms
			MOV B, 100
			DIV AB
			DELAY_MS: ;延时
				MOV A, #50
				CLR C
				SUBB A,Time_ms
				JZ DELAY_MS
			MOV A, P2
			XRL A, R0 ;第一次和第二次检查的值是否相同
			;JZ  CHECK_NO_BUTTON ;如果抖动得比较厉害需要增加三次检查
			JZ  TRIPLE_CHECK ;三次检查
			;MOV Last_button, #0 ;这行可以有,但没必要
			SJMP LOOP ;两次检查的值不同,重新扫描
			
		TRIPLE_CHECK: ;三次检查,还是消抖 ;只针对抖动比较厉害的开发板
			MOV A, #10 ;两次检查的间隔时长 ;可根据抖动情况适当修改为1~99,建议不要超过10
			ADD A, Time_ms
			MOV B, 100
			DIV AB
			DELAY_MS1: ;延时
				MOV A, #50
				CLR C
				SUBB A,Time_ms
				JZ DELAY_MS1
			MOV A, P2
			XRL A, R0 ;第一次和第三次检查的值是否相同
			JZ  CHECK_NO_BUTTON
			;MOV Last_button, #0 ;这行可以有,但没必要
			SJMP LOOP ;两次检查的值不同,重新扫描
			
		CHECK_NO_BUTTON:
			MOV A, R0
			CJNE A, #0FH, CHECK_LAST_BUTTON
			MOV Last_button, #0 ;清除上一个按键
			SJMP LOOP
		

		CHECK_LAST_BUTTON: ;检查上次按键与这次按键是否相同,用于解决按键响应过快的问题
			MOV A, R0
			CJNE A, Last_button, CHECK_CHANGE_MODE ;不相同,处理按键
			MOV Last_button, R0 ;保存该次的按键
			SJMP LOOP ;相同,等到松手再按才处理
			
		
		CHECK_CHANGE_MODE:
			SETB Flag_mute_bit ;打开静音开关(用于关闭闹钟)
			SETB BEEP ;关闭蜂鸣器
			MOV Last_button, R0
			CJNE R0,#07H,CHECK_SETTING ;不是 Change Mode 键就检查下一个键
			SJMP CHANGE_MODE_DOWN ;是就执行相应的程序
		CHECK_SETTING:
			CJNE R0,#0BH,CHECK_UP ;不是 Setting 键就检查下一个键
			SJMP SETTING_DOWN ;是就执行相应的程序
		CHECK_UP:
			CJNE R0,#0DH,CHECK_DOWN ;不是 Up 键就检查下一个键
			SJMP UP_DOWN ;是就执行相应的程序
		CHECK_DOWN:
			CJNE R0,#0EH,LOOP ;不是 Down 键就重新扫描
			LJMP DOWN_DOWN ;是就执行相应的程序
			
		CHANGE_MODE_DOWN: ;按下 Change Mode 键
			MOV Blink,#0H ;取消闪烁
			JB Mode_countdown_bit,MODE_HIGH ;已经到了最后一个模式,回到第一个模式
			MOV A, Mode
			RL A
			MOV Mode, A ;切换下一个模式
			SJMP LOOP
			MODE_HIGH:
				MOV Mode,#1
				SJMP LOOP
				
		SETTING_DOWN: ;按下 SETTING 键
			JNB Mode_year_bit,SETTING_STOPWATCH ;当前不是年模式就跳转
			JB Blink_low_bit, SETTING_CLEAR_BLINK ;年模式下要四位LED熄灭
			MOV Blink, #3H ;年模式下要四位LED一起闪烁
			SJMP LOOP
			SETTING_STOPWATCH:
				JNB Mode_stopwatch_bit,SETTING_COUNTDOWN 
				CLR Flag_stopwatch_bit ;先暂停秒表
				MOV Stopwatch_ms,#0H ;后清零秒表
				MOV Stopwatch_second,#0H
				MOV Stopwatch_minute,#0H
				LJMP LOOP
			SETTING_COUNTDOWN:
				JNB Mode_countdown_bit, SETTING_TIME ;如果正在倒计时
				CLR Flag_countdown_bit ;先暂停倒计时再进行设置
			SETTING_TIME:
				JB Blink_low_bit,SETTING_BLINK_HIGH_BIT ;如果现在正在设置分
				JB Blink_high_bit,SETTING_CLEAR_BLINK ;如果现在正在设置时
				CLR Blink_high_bit
				SETB Blink_low_bit
				LJMP LOOP
			SETTING_BLINK_HIGH_BIT: 
				CLR Blink_low_bit
				SETB Blink_high_bit ;变成设计时
				LJMP LOOP
			SETTING_CLEAR_BLINK:
				MOV Blink,#0H ;变成正常显示(非设置模式)
				LJMP LOOP

		LOOP_TEMP: ;跳转中介
			LJMP LOOP
			
		UP_DOWN: ;按下 Up 键
			;秒表、倒计时、闹钟
			JB Mode_stopwatch_bit,START_STOPWATCH
			JB Mode_countdown_bit,START_COUNTDOWN
			JB Mode_alarm_bit,START_ALARM
			;非秒表、倒计时、闹钟
			MOV A,Blink ;非闪烁,不改变时间、日期、年
			;JZ LOOP_TEMP
			JZ UP_BRIGHTER
			JB Mode_time_bit,UP_TIME_TEMP
			JB Mode_date_bit,UP_DATE_TEMP
			JB Mode_year_bit,UP_YEAR
			
			UP_BRIGHTER: ;跳转中介
				MOV A, Brightness ;增加亮度
				ADD A, #10
				MOV Brightness, A
				LJMP LOOP
			
			UP_TIME_TEMP: ;跳转中介
				LJMP UP_TIME
			
			UP_DATE_TEMP: ;跳转中介
				LJMP UP_DATE
			
			UP_YEAR:
				INC Time_year; 增加年
				MOV A, #100
				CJNE A, Time_year, UP_YEAR_JUDGE_0229;
				MOV Time_year, #0; 超出99则回到0
				;;;;;;判断是否是闰年2月29日 开始;;;;;;;
				UP_YEAR_JUDGE_0229:
					MOV A, #2
					CJNE A, Time_month, LOOP_TEMP
					MOV A, #29
					CJNE A, Time_day, LOOP_TEMP
					MOV Time_day, #28
				;;;;;;判断是否是闰年2月29日 结束;;;;;;;
				LJMP LOOP
			
			START_STOPWATCH: ;秒表开始
				SETB Flag_stopwatch_bit 
				LJMP LOOP
					
			START_COUNTDOWN: ;倒计时开始
				MOV A, Blink
				JNZ UP_COUNTDOWN
				;先判断是否为0,为0就不开始
				JUDGE_COUNTDOWN_SECOND_ZERO:
					MOV A, #0
					CJNE A, Countdown_second, JUDGE_COUNTDOWN_MINUTE_ZERO
					LJMP LOOP
				JUDGE_COUNTDOWN_MINUTE_ZERO:
					CJNE A, Countdown_minute, START_COUNTDOWN1
				START_COUNTDOWN1:
				SETB Flag_countdown_bit
				LJMP LOOP
				
			UP_COUNTDOWN:
				JB Blink_low_bit, UP_COUNTDOWN_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_COUNTDOWN_HOUR
				UP_COUNTDOWN_MINUTE:
					INC Countdown_second ;增加秒
					MOV A, #60
					CJNE A, Countdown_second, LOOP_TEMP
					MOV Countdown_second, #0
					LJMP LOOP
				UP_COUNTDOWN_HOUR:
					INC Countdown_minute ;增加分
					MOV A, #100
					CJNE A, Countdown_minute, LOOP_TEMP
					MOV Countdown_minute, #0
					LJMP LOOP
				
			START_ALARM: ;打开闹钟
				MOV A,Blink
				JNZ UP_ALARM
				SETB Flag_alarm_bit
				LJMP LOOP
				
			UP_ALARM: ;修改闹钟
				JB Blink_low_bit, UP_ALARM_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_ALARM_HOUR
				UP_ALARM_MINUTE:
					INC Alarm_minute ;增加分
					MOV A, #60
					CJNE A, Alarm_minute, LOOP_TEMP1
					MOV Alarm_minute, #0
					LJMP LOOP
				UP_ALARM_HOUR:
					INC Alarm_hour ;增加分
					MOV A, #24
					CJNE A, Alarm_hour, LOOP_TEMP1
					MOV Alarm_hour, #0
					LJMP LOOP
					
			UP_TIME: ;修改时间
				JB Blink_low_bit, UP_TIME_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_TIME_HOUR
				UP_TIME_MINUTE:
					INC Time_minute ;增加分
					MOV A, #60
					CJNE A, Time_minute, LOOP_TEMP1
					MOV Time_minute, #0
					LJMP LOOP
				UP_TIME_HOUR:
					INC Time_hour ;增加分
					MOV A, #24
					CJNE A, Time_hour, LOOP_TEMP1
					MOV Time_hour, #0
					LJMP LOOP
					
			LOOP_TEMP1:
				LJMP LOOP
			
			UP_DATE: ;修改日期
				JB Blink_low_bit, UP_TIME_DAY
				JB Blink_high_bit, UP_TIME_MONTH
				LJMP LOOP
			UP_TIME_DAY:
				INC Time_day; 增加日
				MOV A, #2; 
				XRL A, Time_month; 判断是不是二月
				JNZ UP_DAY1; 不是二月,按照一般月份处理
			UP_JUDGE_LEAP_YEAR:
				MOV A, #11B;
				ANL A, Time_year ;判断闰年
				JNZ UP_DAY1; 不是闰年,按照一般二月处理
			UP_FEB29:
				MOV A, #30; 
				CJNE A, Time_day, LOOP_TEMP1; 没超出29
				MOV Time_day, #1
				LJMP LOOP
			UP_DAY1:
				MOV DPTR, #DAYS_OF_MONTH;
				MOV A, Time_month;
				MOVC A, @A+DPTR;
				CJNE A, Time_day, LOOP_TEMP1;
				MOV Time_day, #1
				LJMP LOOP
				
			UP_TIME_MONTH:
				INC Time_month; 增加月
				MOV A, #13
				CJNE A, Time_month, UP_GET_LAST_DAY;
				MOV Time_month, #1
				
				;;;;判断有无超出最后一天 开始;;;;;;
				UP_GET_LAST_DAY: ;获取当月最后一天
					MOV A, #2; 
					XRL A, Time_month; 判断是不是二月
					JNZ UP_GET_LAST_DAY_NORMAL; 不是二月,按照一般月份处理
					MOV A, #11B;
					ANL A, Time_year ;判断闰年
					JNZ UP_GET_LAST_DAY_NORMAL; 不是闰年,按照一般二月处理
					MOV A,#29
					SJMP UP_ABOVE_LAST_DAY
				UP_GET_LAST_DAY_NORMAL:
					MOV DPTR, #DAYS_OF_MONTH;
					MOV A, Time_month;
					MOVC A, @A+DPTR;
					DEC A
				UP_ABOVE_LAST_DAY:
					CLR C
					MOV R1, A
					SUBB A, Time_day
					JNC LOOP_TEMP2
					MOV Time_day, R1 
				;;;;判断有无超出最后一天 结束;;;;;;
				
				LJMP LOOP
			

		LOOP_TEMP2:
			LJMP LOOP	
		
		DOWN_DOWN: ;按下 Down 键
			;秒表、倒计时、闹钟
			JB Mode_stopwatch_bit,STOP_STOPWATCH ;秒表模式,暂停秒表
			JB Mode_countdown_bit,STOP_COUNTDOWN ;倒计时模式,暂停倒计时
			JB Mode_alarm_bit,STOP_ALARM ;闹钟模式,取消闹钟
			;非秒表、倒计时、闹钟
			MOV A,Blink ;非闪烁,不改变时间、日期、年
			;JZ LOOP_TEMP2 ;无反应
			JZ DOWN_DARKER ;减少亮度
			JB Mode_time_bit,DOWN_TIME ;时间模式,减少分或小时
			JB Mode_date_bit,DOWN_DATE_TEMP ;日期模式,减少日或月;靠中转程序跳转
			JB Mode_year_bit,DOWN_YEAR ;年模式,减少年
			
			DOWN_DARKER:
				DEC Brightness ;减少亮度
				LJMP LOOP
			
			DOWN_DATE_TEMP:
				LJMP DOWN_DATE
			
			DOWN_YEAR: ;修改年
				DEC Time_year; 减少年
				MOV A, #0FFH;
				CJNE A, Time_year, DOWN_YEAR_JUDGE_0229;
				MOV Time_year, #99; 超出0则回到99
				;;;;;;判断是否是闰年2月29日;;;;;;;
				DOWN_YEAR_JUDGE_0229:
					MOV A, #2
					CJNE A, Time_month, LOOP_TEMP2
					MOV A, #29
					CJNE A, Time_day, LOOP_TEMP2
					MOV Time_day, #28
				;;;;;;判断是否是闰年2月29日;;;;;;;
				LJMP LOOP

			STOP_STOPWATCH: ;暂停秒表
				CLR Flag_stopwatch_bit
				LJMP LOOP
					
			STOP_COUNTDOWN: ;暂停倒计时
				MOV A,Blink
				JNZ DOWN_COUNTDOWN
				CLR Flag_countdown_bit
				LJMP LOOP
				
			DOWN_COUNTDOWN: ;修改倒计时
				JB Blink_low_bit, DOWN_COUNTDOWN_MINUTE
				JB Blink_high_bit, DOWN_COUNTDOWN_HOUR
				DOWN_COUNTDOWN_MINUTE:
					DEC Countdown_second ;减少秒
					MOV A, #0FFH
					CJNE A, Countdown_second, LOOP_TEMP2
					MOV Countdown_second, #59
					LJMP LOOP
				DOWN_COUNTDOWN_HOUR:
					DEC Countdown_minute ;减少分
					MOV A, #0FFH
					CJNE A, Countdown_minute, LOOP_TEMP2
					MOV Countdown_minute, #99
					LJMP LOOP
				
			STOP_ALARM: ;关闭闹钟
				MOV A,Blink
				JNZ DOWN_ALARM
				CLR Flag_alarm_bit
				LJMP LOOP
				
			DOWN_ALARM: ;修改闹钟
				JB Blink_low_bit, DOWN_ALARM_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_ALARM_HOUR
				DOWN_ALARM_MINUTE:
					DEC Alarm_minute ;减少分
					MOV A, #0FFH
					CJNE A, Alarm_minute, LOOP_TEMP2
					MOV Alarm_minute, #59
					LJMP LOOP
				DOWN_ALARM_HOUR:
					INC Alarm_hour ;减少时
					MOV A, #0FFH
					CJNE A, Alarm_hour, LOOP_TEMP3
					MOV Alarm_hour, #23
					LJMP LOOP
					
			DOWN_TIME: ;修改时间
				JB Blink_low_bit, DOWN_TIME_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_TIME_HOUR
				DOWN_TIME_MINUTE:
					DEC Time_minute ;减少分
					MOV A, #0FFH
					CJNE A, Time_minute, LOOP_TEMP3
					MOV Time_minute, #59
					LJMP LOOP
				DOWN_TIME_HOUR:
					DEC Time_hour ;减少时
					MOV A, #0FFH
					CJNE A, Time_hour, LOOP_TEMP3
					MOV Time_hour, #23
					LJMP LOOP
					
			LOOP_TEMP3:
				LJMP LOOP
			

			DOWN_DATE: ;修改日期
				JB Blink_low_bit, DOWN_TIME_DAY ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_TIME_MONTH
				LJMP LOOP
			DOWN_TIME_DAY:
				DEC Time_day; 减少日
				MOV A, #0H
				CJNE A, Time_day, LOOP_TEMP3
				MOV A, #2; 
				XRL A, Time_month; 判断是不是二月
				JNZ DOWN_DAY1; 不是二月,按照一般月份处理
			DOWN_JUDGE_LEAP_YEAR:
				MOV A, #11B;
				ANL A, Time_year ;判断闰年
				JNZ DOWN_DAY1; 不是闰年,按照一般二月处理
			DOWN_FEB29:
				MOV Time_day, #29
				LJMP LOOP
			DOWN_DAY1:
				MOV DPTR, #DAYS_OF_MONTH;
				MOV A, Time_month;
				MOVC A, @A+DPTR;
				DEC A
				MOV Time_day, A
				LJMP LOOP

			DOWN_TIME_MONTH:
				DEC Time_month; 减少月
				MOV A, #0
				CJNE A, Time_month, DOWN_GET_LAST_DAY;
				MOV Time_month, #12
				
				;;;;判断有无超出最后一天 开始;;;;;;
				DOWN_GET_LAST_DAY: ;获取当月最后一天
					MOV A, #2; 
					XRL A, Time_month; 判断是不是二月
					JNZ DOWN_GET_LAST_DAY_NORMAL; 不是二月,按照一般月份处理
					MOV A, #11B;
					ANL A, Time_year ;判断闰年
					JNZ DOWN_GET_LAST_DAY_NORMAL; 不是闰年,按照一般二月处理
					MOV A,#29
					SJMP DOWN_ABOVE_LAST_DAY
				DOWN_GET_LAST_DAY_NORMAL:
					MOV DPTR, #DAYS_OF_MONTH ;表头
					MOV A, Time_month ;
					MOVC A, @A+DPTR ;查表
					DEC A ;表中存的是天数+1,需要-1才是真实天数
				DOWN_ABOVE_LAST_DAY:
					CLR C
					MOV R1, A
					SUBB A, Time_day
					JNC LOOP_TEMP3
					MOV Time_day, R1 
				;;;; 判断有无超出最后一天 结束 ;;;;;;
				LJMP LOOP
	
	LJMP LOOP
	
	
	;;;;;;;;;;;;;;;; 键盘 结束 ;;;;;;;;;;;;;;;;;



;;;;;;;; MAIN主程序 结束 ;;;;;;;;;;;;;;;;;;;;;;;;

计时

  定时器0 到 10ms 后,开始增加 Time_msStopwatch_msCountdown_ms(前面还要判断一下 Flag_stopwatch_bitFlag_countdown_bit),然后再判断要不要进位,以及有无到闹钟时间。

  计算过程:(2^16-X)×1us=10ms,解得:X=55535。考虑到运行中断子程序也会占用一定时间,平均情况下,中断子程序会占用 23 个机器周期,(平均情况指:无任何进位,无闹钟、秒表、倒计时),故 X=55535+23=55558=0D906H

  说一下为什么设定为 10ms,而不是 1ms 或 5ms 或其他时间。首先我们可以准确掌握定时器计时的时间,但运行指令的时间是较难把握的,如果设定的时间太短,那么中断子程序占用的时间就会造成较大偏差;但如果时间太长,就会在开始秒表瞬间导致误差,因为如果此时定时器记到了 0FEH,就会导致“第一个”ms缩短为 1us,这也是不能接受的。我希望误差在 0.001s 以内,所以就设定为 10ms。

  注意烧录时,不要使用IRC时钟,且不选中振荡器放大增益。否则震荡周期会变慢。

详细代码
;;;;;;;; T0定时器中断子程序(计时) 开始 ;;;;;;;;;

ORG 500H;

INT_T0:
	PUSH 49H;
	MOV 49H, A;
	PUSH 49H ;寄存器A入栈
	INC_MS:
		INC Time_ms ;增加毫秒x10
		MOV A, #100
		CJNE A, Time_ms,  INC_STOPWATCH ;如果不等于100则无需进位
	INC_S:
		MOV Time_ms, #0 ;进位后毫秒等于零
		INC Time_second ;增加秒
		MOV A, #60
		CJNE A, Time_second, JUDGE_ALARM ;如果不等于60则无需进位
		;为什么分没有变化也要去判断闹钟:
		; 因为我们使用了一个 Flag_mute_bit
		; 只要我们按了按键,那么 Flag_mute_bit 就会置1
		; 这样闹钟响后就可以通过按键去关闭闹钟
		; 同时,当我们设置时间时,闹钟也不会突然响
		; 在检查闹钟时,如果未到,就会自动将 Flag_mute_bit 清0
		; 但这就带来一个问题,假如闹钟定在当前时间的后一分钟,同时 Flag_mute_bit 为 1
		; 如果只在分变化时才检查闹钟,那么就会来不及将 Flag_mute_bit 清0
		; 所以由于这种特殊情况,我们必须每变一秒就去检查闹钟(为了将 Flag_mute_bit 清0)
		
	INC_MIN:
		MOV Time_second, #0 ;进位后秒等于零
		INC Time_minute ;增加分
		MOV A, #60
		CJNE A, Time_minute, JUDGE_ALARM;
		
	INC_HOUR:
		MOV Time_minute, #0 ;进位后分等于0
		INC Time_hour ;增加时
		MOV A, #24
		CJNE A, Time_hour, JUDGE_ALARM;
		
	INC_DAY:
		MOV Time_hour, #0; 进位后时等于零
		INC Time_day; 增加日
		MOV A, #2; 
		XRL A, Time_month; 判断是不是二月
		JNZ INC_DAY1; 不是二月,按照一般月份处理
	JUDGE_LEAP_YEAR:
		MOV A, #11B;
		ANL A, Time_year ;判断闰年
		JNZ INC_DAY1; 不是闰年,按照一般二月处理
	FEB29:
		MOV A, #30; 
		CJNE A, Time_day, JUDGE_ALARM; 没超出29
		SJMP INC_MONTH; 超出29
	INC_DAY1:
		MOV DPTR, #DAYS_OF_MONTH;
		MOV A, Time_month;
		MOVC A, @A+DPTR ;查表,获取当月天数(表中存储的是天数+1)
		CJNE A, Time_day, JUDGE_ALARM;

	INC_MONTH:
		MOV Time_day, #1; 进位后日等于1
		INC Time_month; 增加月
		MOV A, #13
		CJNE A, Time_month, JUDGE_ALARM; 

	INC_YEAR:
		MOV Time_month, #1; 进位后月等于1
		INC Time_year; 增加年
		MOV A, #100
		CJNE A, Time_year, JUDGE_ALARM;
		MOV Time_year, #0; 超出99则回到0

	JUDGE_ALARM:
		JNB Flag_alarm_bit, CANCEL_MUTE_ALARM; 未打开闹钟
		MOV A, Time_hour
		CJNE A, Alarm_hour, CANCEL_MUTE_ALARM ;未到闹钟时
		MOV A, Time_minute
		CJNE A, Alarm_minute, CANCEL_MUTE_ALARM ;未到闹钟分
		JB Flag_mute_bit, INC_STOPWATCH ;已经按下按钮,静音开关打开
		CLR BEEP ;蜂鸣器响
		SJMP INC_STOPWATCH
	CANCEL_MUTE_ALARM:
		CLR Flag_mute_bit ;关闭静音开关
		SETB BEEP ;蜂鸣器最多只响1分钟

	INC_STOPWATCH:
		JNB Flag_stopwatch_bit, INC_COUNTDOWN ;秒表开关未打开,跳转到INC_COUNTDOWN
		INC Stopwatch_ms;
		MOV A, #100
		CJNE A, Stopwatch_ms, INC_COUNTDOWN; 如果不等于100则无需进位
	INC_S_STOPWATCH:
		MOV Stopwatch_ms, #0; 进位后毫秒等于零
		INC Stopwatch_second; 增加秒
		MOV A, #60
		CJNE A, Stopwatch_second, INC_COUNTDOWN; 如果不等于60则无需进位
	INC_MIN_STOPWATCH:
		MOV Stopwatch_second, #0; 进位后秒等于零
		INC Stopwatch_minute; 增加分
		MOV A, #100;
		CJNE A, Stopwatch_minute, INC_COUNTDOWN;
		MOV Stopwatch_minute, #0; 进位后分等于0
		

	INC_COUNTDOWN:
		JNB Flag_countdown_bit, END_T0 ;倒计时开关未打开,跳转到END_T0
		INC Countdown_ms ;增加毫秒
		MOV A, #100
		CJNE A, Countdown_ms,  END_T0; 如果不等于100则无需减少秒
	DEC_S_COUNTDOWN:
		MOV Countdown_ms, #0 ;进位后毫秒等于零
		DEC Countdown_second ;减少秒
		MOV A, #00H
		CJNE A, Countdown_minute,  DEC_S_COUNTDOWN1 ;分不等于0则跳转到正常减少秒
		CJNE A, Countdown_second, END_T0 ;秒不等于0则跳转
		;分秒都等于0,蜂鸣器响;
		CLR Flag_countdown_bit ;关闭倒计时开关
		CLR BEEP; 蜂鸣器响
	DEC_S_COUNTDOWN1:
		MOV A, #0FFH
		CJNE A, Countdown_second, END_T0; 如果不等于-1则无需减少分
	DEC_MIN_COUNTDOWN:
		MOV Countdown_second, #59; 秒等于59
		DEC Countdown_minute; 减少分

	END_T0:
		;计算过程
		; (2^16-X)*1us=10ms
		; 解得:X=55535
		; 考虑到运行中断子程序也会占用一定时间
		; 平均情况下,中断子程序会占用 23 个机器周期
		; (平均情况指:无任何进位,无闹钟、秒表、倒计时)
		; 故 X=55535+23=55558=D906H
		MOV TL0, #06H 
		MOV TH0, #0D9H 
		POP 49H; 寄存器A出栈
		MOV A,49H;
		POP 49H
		SETB TR0
		
		RETI
	

;;;;;;; T0定时器中断子程序(计时) 结束 ;;;;;;;;;;

显示

  这个就没啥好说的,就是取数据,转 BCD,转LED显示编码,然后显示就行。我只解释一下闪烁的思路。

  闪烁利用 Blink_count 来确定当前是亮,还是暗,如果 Blink_count 的最高位是0,就暗,如果是 1,就亮。然后每次刷新,Blink_count 都会+1,最高位不断循环 0/1,从而达到闪烁的效果。如果闪烁较快,可以用多个 Blink_count

详细代码
;;;;;;;; T1定时器中断子程序 开始 ;;;;;;;;;;
ORG 1000H

INT_T1:
	PUSH 49H;
	MOV 49H, A;
	PUSH 49H ;寄存器A入栈
	MOV A, Mode ;获取当前模式
	ANL A,#11111110B
	JZ GET_TIME ;时间模式
	ANL A,#11111100B
	JZ GET_DATE ;日期模式
	ANL A,#11111000B
	JZ GET_YEAR ;年模式
	ANL A,#11110000B
	JZ GET_ALARM ;闹钟模式
	ANL A,#11100000B
	JZ GET_STOPWATCH ;秒表模式
	ANL A,#11000000B
	JZ GET_COUNTDOWN ;倒计时模式
	GET_TIME:
		MOV Show_low, Time_minute ;将分读入待显示字符
		MOV Show_high, Time_hour ;将时读入待显示字符
		SJMP BIN_2_BCD
	GET_DATE:
		MOV Show_low, Time_day ;将日读入待显示字符
		MOV Show_high, Time_month ;将月读入待显示字符
		SJMP BIN_2_BCD
	GET_YEAR:
		MOV Show_low, Time_year ;将年读入待显示字符
		MOV Show_high, #20
		SJMP BIN_2_BCD
	GET_ALARM:
		MOV Show_low, Alarm_minute ;将闹钟分读入待显示字符
		MOV Show_high, Alarm_hour ;将闹钟时读入待显示字符
		SJMP BIN_2_BCD
	GET_STOPWATCH:
		MOV A, Stopwatch_minute 
		JZ GET_STOPWATCH_S_MS
			MOV Show_low, Stopwatch_second ;将秒表秒读入待显示字符
			MOV Show_high, Stopwatch_minute ;将秒表分读入待显示字符
			SJMP BIN_2_BCD
		GET_STOPWATCH_S_MS:
			MOV Show_low, Stopwatch_ms ;将秒表毫秒
			MOV Show_high, Stopwatch_second ;将秒表秒
			SJMP BIN_2_BCD
	GET_COUNTDOWN:
		MOV Show_low, Countdown_second ;将倒计时秒读入待显示字符
		MOV Show_high, Countdown_minute ;将倒计时分读入待显示字符
		SJMP BIN_2_BCD
	
	BIN_2_BCD:
		MOV A,Show_low
		MOV B,#10
		DIV AB
		MOV BCD1,B
		MOV B, #10 ;为了防bug除两次,也可以用下面的方法
		DIV AB
		MOV BCD2,B
		
		MOV A,Show_high
		MOV B,#10
		DIV AB
		MOV BCD3,B
		MOV BCD4,A ;懒得防bug了,不用除两次
	
	JUDGE_BLINK_LOW: ;判断低两位是否需要闪烁
		JNB Blink_low_bit, SHOW_LED1_DOT
		INC Blink_count0
		JB Blink_count0_bit, ADD_BLINK_COUNT1_0
	ADD_BLINK_COUNT1_0: ;闪烁计数,用于记录当前闪/不闪之间的间隔
		INC Blink_count1
		MOV Blink_count0, #0
		JB Blink_count1_bit, ADD_BLINK_COUNT2_0
	ADD_BLINK_COUNT2_0:
		INC Blink_count2
		MOV Blink_count1, #0
		JNB Blink_count2_bit, SHOW_LED1_DOT
		SJMP JUDGE_BLINK_HIGH

	SHOW_LED1_DOT:
		JNB Mode_alarm_bit, SHOW_LED1
		JNB Flag_alarm_bit, SHOW_LED1
		MOV R4,BCD1
		MOV R5, #1
		LCALL SEND_BYTE
		SETB LED1
		LCALL DELAY
		CLR LED1
		SJMP SHOW_LED2

	SHOW_LED1:
		MOV R4, BCD1
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED1
		LCALL DELAY
		CLR LED1

	SHOW_LED2:
		MOV R4,BCD2
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED2
		LCALL DELAY
		CLR LED2

	JUDGE_BLINK_HIGH: ;判断高两位是否需要闪烁
		JNB Blink_high_bit, SHOW_LED3_DOT
		JB Blink_low_bit, NOT_REPEAT_INC
		INC Blink_count0
		JB Blink_count0_bit, ADD_BLINK_COUNT1_1
	ADD_BLINK_COUNT1_1: ;闪烁计数,用于记录当前闪/不闪之间的间隔
		INC Blink_count1
		MOV Blink_count0, #0
		JB Blink_count1_bit, ADD_BLINK_COUNT2_1
	ADD_BLINK_COUNT2_1:
		INC Blink_count2
		MOV Blink_count1, #0
	NOT_REPEAT_INC:
		JNB Blink_count2_bit, SHOW_LED3_DOT
		SJMP INT_T1_END

	SHOW_LED3_DOT:
		JB Mode_year_bit,SHOW_LED3
		MOV R4,BCD3
		MOV R5, #1
		LCALL SEND_BYTE
		SETB LED3
		LCALL DELAY
		CLR LED3
		SJMP SHOW_LED4
		
	SHOW_LED3:
		MOV R4,BCD3
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED3
		LCALL DELAY
		CLR LED3

	SHOW_LED4:
		MOV R4, BCD4
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED4
		LCALL DELAY
		CLR LED4

	INT_T1_END:
		POP 49H; 寄存器A出栈
		MOV A,49H;
		POP 49H
		RETI
	

DELAY:
	MOV 49H, Brightness
	DELAY_LOOP:
		DJNZ 49H, DELAY_LOOP
	RET

SEND_BYTE:
	MOV A,R4 	;将数据移入寄存器A
	DJNZ R5,USE_LED_SEGMENT_CODE	;R5为1显示小数点
	MOV DPTR, #LED_SEGMENT_CODE_DOT
	SJMP GET_LED_SEGMENT_CODE
USE_LED_SEGMENT_CODE:
	MOV DPTR,#LED_SEGMENT_CODE  ;将表头移到DPTR
GET_LED_SEGMENT_CODE:
	MOVC A,@A+DPTR 		;查LED编码表
	MOV R5,A
	MOV R6,#08H  ;循环8次
	SEND_BYTE_LOOP:
		ANL A,#01H ;取第一位
		CLR HC595_DAT ;清0
		JZ LED_NOT_ZERO ;如果第一位是0则跳转
		SETB HC595_DAT ;置1
		LED_NOT_ZERO:
			SETB HC595_SCK ;时序
			CLR HC595_SCK ;时序
		MOV A,R5
		RR A
		MOV R5,A
		DJNZ R6,SEND_BYTE_LOOP	
	CLR HC595_RCK ;时序
	SETB HC595_RCK ;时序

	RET
;;;;;;;;; T1定时器中断子程序 结束 ;;;;;;;;;

亮度调节

  这个也没啥好说,就是中断,然后修改 LED 延时的时间(Brightness)。

详细代码
;;;;;;;;; 增加亮度 开始 ;;;;;;;;;;

BRIGHTER:
	PUSH 49H
	MOV 49H, A
	PUSH 49H
	MOV A, Brightness
	CJNE A, #100000B, NOT_BRIGHTEST
	SJMP BRIGHTER_END
	NOT_BRIGHTEST:
		INC A
		;RL A
		MOV Brightness, A
	BRIGHTER_END:
		POP 49H
		MOV A, 49H
		POP 49H
		RETI
;;;;;;;;; 增加亮度 结束 ;;;;;;;;;;


;;;;;;;;; 减少亮度 开始 ;;;;;;;;;;

DARKER:
	PUSH 49H
	MOV 49H, A
	PUSH 49H
	MOV A, Brightness
	CJNE A, #00000001B, NOT_DARKEST
	SJMP DARKER_END
	NOT_DARKEST:
		DEC A
		;RR A
		MOV Brightness, A
	DARKER_END:
		POP 49H
		MOV A, 49H
		POP 49H
		RETI
;;;;;;;;; 减少亮度 结束 ;;;;;;;;;;

完整代码

详细代码
; ########   ##   ##      ##    ###   ########  ######  ##     ## 
; ##       ####   ##  ##  ##   ## ##     ##    ##    ## ##     ## 
; ##         ##   ##  ##  ##  ##   ##    ##    ##       ##     ## 
; #######    ##   ##  ##  ## ##     ##   ##    ##       ######### 
;       ##   ##   ##  ##  ## #########   ##    ##       ##     ## 
; ##    ##   ##   ##  ##  ## ##     ##   ##    ##    ## ##     ## 
;  ######  ######  ###  ###  ##     ##   ##     ######  ##     ## 
; 
; Author: ZZF,LDF,ZJC
; Date: 2020/08/28
; Note: 
;	1. 请确保P0、P2每个管脚上均有跳线帽
;	2. 烧录时不使用IRC时钟,且不选中振荡器放大增益


;程序结构一览:
; 51Watch/
;├─ 定义RAM区变量/
;├─ 程序跳转/
;├─ 定义ROM区数据/
;├─ MAIN主程序/
;│  ├─ 设置寄存器初值
;│  ├─ 键盘扫描/
;│  │  ├─ 读取按键
;│  │  ├─ 消抖
;│  │  ├─ 响应按键/
;│  │  │  ├─ 响应change mode键
;│  │  │  ├─ 响应setting键
;│  │  │  ├─ 响应up键
;│  │  │  ├─ 响应down键
;├─ 定时器0(计时)/
;│  ├─ 增加毫秒、秒、分、时、日、月、年
;│  ├─ 检查闹钟
;│  ├─ 增加秒表
;│  ├─ 减少倒计时
;├─ 定时器1(显示)/
;│  ├─ 读取当前模式&闪烁
;│  ├─ 读取待显示数据
;│  ├─ 数据转换
;│  ├─ 输出到HC595
;│  ├─ 显示
;├─ 中断0(增加亮度)/
;├─ 中断1(减少亮度)/



;;;;;;;; 定义RAM区变量 开始  ;;;;;;;;;;;

;命名规则:
;	首字母大写,后面字母小写的都是RAM变量;
;	后面带bit的都是位变量

;Time 当前时间;
Time_ms            EQU 50H ;实际为 msx10,后面的ms都一样
Time_second        EQU 51H
Time_minute        EQU 52H
Time_hour          EQU 53H
Time_day           EQU 54H
Time_month         EQU 55H
Time_year          EQU 56H

;Alarm 闹钟;
Alarm_minute       EQU 57H
Alarm_hour         EQU 58H

;Stopwatch 秒表;
Stopwatch_ms       EQU 59H
Stopwatch_second   EQU 5AH
Stopwatch_minute   EQU 5BH

;Countdown 倒计时;
Countdown_ms       EQU 5CH
Countdown_second   EQU 5DH
Countdown_minute   EQU 5EH
	
;Show 待显示的二进制数;
Show_low           EQU 5FH
Show_high          EQU 60H

;BCD 待显示的BCD字符
BCD1               EQU 61H
BCD2               EQU 62H
BCD3               EQU 63H
BCD4               EQU 64H
	
;Brightness 亮度;
Brightness         EQU 65H

;Last_button 上一个按键;(用于防止按键过快响应)
Last_button        EQU 66H
	
;Long_press_second 长按时的第一秒
Long_press_second EQU 67H
	
;Long_press_ms 长按每隔十毫秒响应一次
Long_press_ms EQU 68H

;Mode 当前模式;
Mode               EQU 20H
Mode_time_bit      EQU 00H
Mode_date_bit      EQU 01H
Mode_year_bit      EQU 02H
Mode_alarm_bit     EQU 03H
Mode_stopwatch_bit EQU 04H
Mode_countdown_bit EQU 05H

;Flag 开关标志;
Flag               EQU 21H
Flag_alarm_bit     EQU 08H
Flag_stopwatch_bit EQU 09H
Flag_countdown_bit EQU 0AH
Flag_mute_bit      EQU 0BH ;判断蜂鸣器响后是否有按下按钮
Flag_long_press EQU 0CH


;Blink LED闪烁;
Blink              EQU 22H
Blink_low_bit      EQU 10H
Blink_high_bit     EQU 11H

;Blink_count0 闪烁计数0; 一个不够用,所以要三个(其实我甚至想要四个)
Blink_count0       EQU 23H
Blink_count0_bit   EQU 1FH

;Blink_count1 闪烁计数1;
Blink_count1       EQU 24H
Blink_count1_bit   EQU 27H

;Blink_count2 闪烁计数2;
Blink_count2       EQU 25H
Blink_count2_bit   EQU 2FH


;数码管选中端;
LED1        BIT P0.3 ;最右边
LED2        BIT P0.2
LED3        BIT P0.1
LED4        BIT P0.0 ;最左边

;HC595 芯片相关引脚;
HC595_SCK   BIT P0.4
HC595_RCK   BIT P0.5
HC595_RST   BIT P0.6
HC595_DAT   BIT P0.7

;蜂鸣器引脚;
BEEP        BIT P1.3

;键盘扫描引脚;
Change_mode BIT P2.4
Setting     BIT P2.5
Up          BIT P2.6
Down        BIT P2.7

;;;;;;;;; 定义RAM变量 结束 ;;;;;;;;;;;;


;;;;;;;;; 程序跳转 开始 ;;;;;;;;;;;

ORG 0000H
	LJMP MAIN ;主程序(键盘扫描)
ORG 0003H
	LJMP BRIGHTER ;外部中断0(亮度增加)
ORG 000BH
	LJMP INT_T0 ;定时器0(计时)
ORG 0013H
	LJMP DARKER ;外部中断1(显示)
ORG 001BH
	LJMP INT_T1 ;定时器1(亮度减少)
	
;;;;;;;; 程序跳转 结束 ;;;;;;;;;;;


;;;;;;;; ROM区数据 开始 ;;;;;;;;;;

ORG 0030H

DAYS_OF_MONTH:
	DB 0 ;0月(不存在)
	DB 32,29,32,31 ;一、二、三、四
	DB 32,31,32,32 ;五、六、七、八
	DB 31,32,31,32 ;九、十、十一、十二

;学校的开发板
LED_SEGMENT_CODE:
	DB 0FCH,60H,0DAH,0F2H,66H,0B6H,0BEH,0E0H,0FEH,0F6H,00H ;0~9 不带点
CHECK_COUNTDOWN_BUG:
	DB 0FCH,60H,0DAH,0F2H,66H,0B6H,0BEH,0E0H,0FEH,0F6H,00H ;0~9 不带点
LED_SEGMENT_CODE_DOT:
	DB 0FDH,61H,0DBH,0F3H,67H,0B7H,0BFH,0E1H,0FFH,0F7H ;0.~9. 带点
		
;;;;;;;; ROM区数据 结束 ;;;;;;;;;
 

;;;;;;;; MAIN主程序 开始 ;;;;;;;;;;

ORG 100H

MAIN:
	;设置年月日时分秒初值
	MOV Time_year,   #20 ;2020 ;年只存储个位和十位
	MOV Time_month,  #08 ;8月29日晚20点10分在科学馆406会议室验收
	MOV Time_day,    #29
	MOV Time_hour,   #20
	MOV Time_minute, #10
	MOV Time_second, #50
	MOV Time_ms,     #0
	
	;设置闹钟初值
	MOV Alarm_hour,   #20
	MOV Alarm_minute, #11
	
	;设置秒表初值
	MOV Stopwatch_minute, #0
	MOV Stopwatch_second, #0
	MOV Stopwatch_ms,     #0
	
	;设置倒计时初值
	MOV Countdown_minute, #0
	MOV Countdown_second, #10
	MOV Countdown_ms,     #0
	
	;设置亮度初值
	MOV Brightness, #1000B
	
	;设置模式Mode初值
	MOV Mode, #01H
	
	;设置开关Flag初值
	MOV Flag, #00H
	SETB Flag_alarm_bit ;打开闹钟
	
	;设置LED闪烁Blink初值
	MOV Blink, #00B
	
	;设置闪烁计数Blink_count初值
	MOV Blink_count0, #00H
	MOV Blink_count1, #00H
	
	;设置HC595
	SETB HC595_RST
	MOV P0, #00H
	
	;MOV IE, #10001010B ;不开启亮度调节
	MOV IE, #10001111B ;开启亮度调节
	
	;计时(定时器0)的优先级高于其他
	MOV IP, #00000010B
	
	;定时器0定时时间较长,故为模式1
	;定时器1用于显示,需要定时刷新,为模式2
	MOV TMOD, #00100001B
	
	;定时器0的计算过程在定时器0中断子程序处
	;定时器1就是取最大时间
	MOV TL0, #006H
	MOV TH0, #0D9H
	MOV TL1, #00H
	MOV TH1, #00H
	
	;开启定时器0、定时器1
	SETB TR1
	SETB TR0
	
	;;;;;;;;;;;;;;;; 键盘 开始 ;;;;;;;;;;;;;;;;;
	
	LOOP:
		MOV P2, #0FH ;矩阵键盘行赋1,列赋0
		MOV R0, P2 ;获取矩阵键盘输入
		;本来想着“没有按下按键”就不用消抖了,直接重新扫描就行
		;但学校的开发板的按键抖动得太厉害
		;不得已连“没有按下按键”也要先消抖再判断
		;如果开发板比较新,可以去掉下面三行的注释
		;CJNE R0, #0FH, DOUBLE_CHECK ;如果按下按键则二次检查
		;MOV Last_button, #0 ;清除上一个按键
		;SJMP LOOP
		
		DOUBLE_CHECK: ;二次检查,消抖
			MOV A, #10 ;两次检查的间隔时长 ;可根据抖动情况适当修改为1~99,建议不要超过10
			ADD A, Time_ms
			MOV B, 100
			DIV AB
			DELAY_MS: ;延时
				MOV A, #50
				CLR C
				SUBB A,Time_ms
				JZ DELAY_MS
			MOV A, P2
			XRL A, R0 ;第一次和第二次检查的值是否相同
			;JZ  CHECK_NO_BUTTON ;如果抖动得比较厉害需要增加三次检查
			JZ  TRIPLE_CHECK ;三次检查
			;MOV Last_button, #0 ;这行可以有,但没必要
			SJMP LOOP ;两次检查的值不同,重新扫描
			
		TRIPLE_CHECK: ;三次检查,还是消抖 ;只针对抖动比较厉害的开发板
			MOV A, #10 ;两次检查的间隔时长 ;可根据抖动情况适当修改为1~99,建议不要超过10
			ADD A, Time_ms
			MOV B, 100
			DIV AB
			DELAY_MS1: ;延时
				MOV A, #50
				CLR C
				SUBB A,Time_ms
				JZ DELAY_MS1
			MOV A, P2
			XRL A, R0 ;第一次和第三次检查的值是否相同
			JZ  CHECK_NO_BUTTON
			;MOV Last_button, #0 ;这行可以有,但没必要
			SJMP LOOP ;两次检查的值不同,重新扫描
			
		CHECK_NO_BUTTON:
			MOV A, R0
			CJNE A, #0FH, CHECK_LAST_BUTTON
			MOV Last_button, #0 ;清除上一个按键
			MOV Long_press_second, #0FFH;
			MOV Long_press_ms, #0FFH
			SJMP LOOP
		

		CHECK_LAST_BUTTON: ;检查上次按键与这次按键是否相同,用于解决按键响应过快的问题
			MOV A, #0FFH;
			CJNE A, Long_press_second, CHECK_LONG_PRESS;
			MOV Long_press_second, Time_second;
			SJMP CHECK_CHANGE_MODE
			CHECK_LONG_PRESS:
				MOV A, Time_second;
				CJNE A, Long_press_second, CHECK_LONG_PRESS_MS_1
				SJMP CHECK_SHORT_PRESS
			CHECK_LONG_PRESS_MS_1:
				MOV A, #0FFH;
				CJNE A, Long_press_ms, CHECK_LONG_PRESS_MS;
				MOV Long_press_ms, Time_ms
				SJMP CHECK_CHANGE_MODE
			CHECK_LONG_PRESS_MS:
				;MOV Long_press_second, Time_second;
				MOV A, Time_ms;
				CLR CY
				SUBB A, Long_press_ms
				MOV B, #15 ;间隔15ms响应一次
				DIV AB
				JNZ CHECK_CHANGE_MODE
				SJMP LOOP
			CHECK_SHORT_PRESS:
				MOV A, R0
				CJNE A, Last_button, CHECK_CHANGE_MODE ;不相同,处理按键
				MOV Last_button, R0 ;保存该次的按键
				SJMP LOOP ;相同,等到松手再按才处理
			
		
		CHECK_CHANGE_MODE:
			MOV Long_press_ms, Time_ms
			SETB Flag_mute_bit ;打开静音开关(用于关闭闹钟)
			SETB BEEP ;关闭蜂鸣器
			MOV Last_button, R0
			CJNE R0,#07H,CHECK_SETTING ;不是 Change Mode 键就检查下一个键
			SJMP CHANGE_MODE_DOWN ;是就执行相应的程序
		CHECK_SETTING:
			CJNE R0,#0BH,CHECK_UP ;不是 Setting 键就检查下一个键
			SJMP SETTING_DOWN ;是就执行相应的程序
		CHECK_UP:
			CJNE R0,#0DH,CHECK_DOWN ;不是 Up 键就检查下一个键
			SJMP UP_DOWN ;是就执行相应的程序
		CHECK_DOWN:
			CJNE R0,#0EH,LOOP_TEMP ;不是 Down 键就重新扫描
			LJMP DOWN_DOWN ;是就执行相应的程序
			
		CHANGE_MODE_DOWN: ;按下 Change Mode 键
			MOV Blink,#0H ;取消闪烁
			JB Mode_countdown_bit,MODE_HIGH ;已经到了最后一个模式,回到第一个模式
			MOV A, Mode
			RL A
			MOV Mode, A ;切换下一个模式
			LJMP LOOP
			MODE_HIGH:
				MOV Mode,#1
				LJMP LOOP
				
		SETTING_DOWN: ;按下 SETTING 键
			JNB Mode_year_bit,SETTING_STOPWATCH ;当前不是年模式就跳转
			JB Blink_low_bit, SETTING_CLEAR_BLINK ;年模式下要四位LED熄灭
			MOV Blink, #3H ;年模式下要四位LED一起闪烁
			LJMP LOOP
			SETTING_STOPWATCH:
				JNB Mode_stopwatch_bit,SETTING_COUNTDOWN 
				CLR Flag_stopwatch_bit ;先暂停秒表
				MOV Stopwatch_ms,#0H ;后清零秒表
				MOV Stopwatch_second,#0H
				MOV Stopwatch_minute,#0H
				LJMP LOOP
			SETTING_COUNTDOWN:
				JNB Mode_countdown_bit, SETTING_TIME ;如果正在倒计时
				CLR Flag_countdown_bit ;先暂停倒计时再进行设置
			SETTING_TIME:
				JB Blink_low_bit,SETTING_BLINK_HIGH_BIT ;如果现在正在设置分
				JB Blink_high_bit,SETTING_CLEAR_BLINK ;如果现在正在设置时
				CLR Blink_high_bit
				SETB Blink_low_bit
				LJMP LOOP
			SETTING_BLINK_HIGH_BIT: 
				CLR Blink_low_bit
				SETB Blink_high_bit ;变成设计时
				LJMP LOOP
			SETTING_CLEAR_BLINK:
				MOV Blink,#0H ;变成正常显示(非设置模式)
				LJMP LOOP

		LOOP_TEMP: ;跳转中介
			LJMP LOOP
			
		UP_DOWN: ;按下 Up 键
			;秒表、倒计时、闹钟
			JB Mode_stopwatch_bit,START_STOPWATCH
			JB Mode_countdown_bit,START_COUNTDOWN
			JB Mode_alarm_bit,START_ALARM
			;非秒表、倒计时、闹钟
			MOV A,Blink ;非闪烁,不改变时间、日期、年
			;JZ LOOP_TEMP
			JZ UP_BRIGHTER
			JB Mode_time_bit,UP_TIME_TEMP
			JB Mode_date_bit,UP_DATE_TEMP
			JB Mode_year_bit,UP_YEAR
			
			UP_BRIGHTER: ;跳转中介
				MOV A, Brightness ;增加亮度
				ADD A, #10
				MOV Brightness, A
				LJMP LOOP
			
			UP_TIME_TEMP: ;跳转中介
				LJMP UP_TIME
			
			UP_DATE_TEMP: ;跳转中介
				LJMP UP_DATE
			
			UP_YEAR:
				INC Time_year; 增加年
				MOV A, #100
				CJNE A, Time_year, UP_YEAR_JUDGE_0229;
				MOV Time_year, #0; 超出99则回到0
				;;;;;;判断是否是闰年2月29日 开始;;;;;;;
				UP_YEAR_JUDGE_0229:
					MOV A, #2
					CJNE A, Time_month, LOOP_TEMP
					MOV A, #29
					CJNE A, Time_day, LOOP_TEMP
					MOV Time_day, #28
				;;;;;;判断是否是闰年2月29日 结束;;;;;;;
				LJMP LOOP
			
			START_STOPWATCH: ;秒表开始
				SETB Flag_stopwatch_bit 
				LJMP LOOP
					
			START_COUNTDOWN: ;倒计时开始
				MOV A, Blink
				JNZ UP_COUNTDOWN
				;先判断是否为0,为0就不开始
				JUDGE_COUNTDOWN_SECOND_ZERO:
					MOV A, #0
					CJNE A, Countdown_second, JUDGE_COUNTDOWN_MINUTE_ZERO
					LJMP LOOP
				JUDGE_COUNTDOWN_MINUTE_ZERO:
					CJNE A, Countdown_minute, START_COUNTDOWN1
				START_COUNTDOWN1:
				SETB Flag_countdown_bit
				LJMP LOOP
				
			UP_COUNTDOWN:
				JB Blink_low_bit, UP_COUNTDOWN_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_COUNTDOWN_HOUR
				UP_COUNTDOWN_MINUTE:
					INC Countdown_second ;增加秒
					MOV A, #60
					CJNE A, Countdown_second, LOOP_TEMP
					MOV Countdown_second, #0
					LJMP LOOP
				UP_COUNTDOWN_HOUR:
					INC Countdown_minute ;增加分
					MOV A, #100
					CJNE A, Countdown_minute, LOOP_TEMP
					MOV Countdown_minute, #0
					LJMP LOOP
				
			START_ALARM: ;打开闹钟
				MOV A,Blink
				JNZ UP_ALARM
				SETB Flag_alarm_bit
				LJMP LOOP
				
			UP_ALARM: ;修改闹钟
				JB Blink_low_bit, UP_ALARM_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_ALARM_HOUR
				UP_ALARM_MINUTE:
					INC Alarm_minute ;增加分
					MOV A, #60
					CJNE A, Alarm_minute, LOOP_TEMP1
					MOV Alarm_minute, #0
					LJMP LOOP
				UP_ALARM_HOUR:
					INC Alarm_hour ;增加分
					MOV A, #24
					CJNE A, Alarm_hour, LOOP_TEMP1
					MOV Alarm_hour, #0
					LJMP LOOP
					
			UP_TIME: ;修改时间
				JB Blink_low_bit, UP_TIME_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, UP_TIME_HOUR
				UP_TIME_MINUTE:
					INC Time_minute ;增加分
					MOV A, #60
					CJNE A, Time_minute, LOOP_TEMP1
					MOV Time_minute, #0
					LJMP LOOP
				UP_TIME_HOUR:
					INC Time_hour ;增加分
					MOV A, #24
					CJNE A, Time_hour, LOOP_TEMP1
					MOV Time_hour, #0
					LJMP LOOP
					
			LOOP_TEMP1:
				LJMP LOOP
			
			UP_DATE: ;修改日期
				JB Blink_low_bit, UP_TIME_DAY
				JB Blink_high_bit, UP_TIME_MONTH
				LJMP LOOP
			UP_TIME_DAY:
				INC Time_day; 增加日
				MOV A, #2; 
				XRL A, Time_month; 判断是不是二月
				JNZ UP_DAY1; 不是二月,按照一般月份处理
			UP_JUDGE_LEAP_YEAR:
				MOV A, #11B;
				ANL A, Time_year ;判断闰年
				JNZ UP_DAY1; 不是闰年,按照一般二月处理
			UP_FEB29:
				MOV A, #30; 
				CJNE A, Time_day, LOOP_TEMP1; 没超出29
				MOV Time_day, #1
				LJMP LOOP
			UP_DAY1:
				MOV DPTR, #DAYS_OF_MONTH;
				MOV A, Time_month;
				MOVC A, @A+DPTR;
				CJNE A, Time_day, LOOP_TEMP1;
				MOV Time_day, #1
				LJMP LOOP
				
			UP_TIME_MONTH:
				INC Time_month; 增加月
				MOV A, #13
				CJNE A, Time_month, UP_GET_LAST_DAY;
				MOV Time_month, #1
				
				;;;;判断有无超出最后一天 开始;;;;;;
				UP_GET_LAST_DAY: ;获取当月最后一天
					MOV A, #2; 
					XRL A, Time_month; 判断是不是二月
					JNZ UP_GET_LAST_DAY_NORMAL; 不是二月,按照一般月份处理
					MOV A, #11B;
					ANL A, Time_year ;判断闰年
					JNZ UP_GET_LAST_DAY_NORMAL; 不是闰年,按照一般二月处理
					MOV A,#29
					SJMP UP_ABOVE_LAST_DAY
				UP_GET_LAST_DAY_NORMAL:
					MOV DPTR, #DAYS_OF_MONTH;
					MOV A, Time_month;
					MOVC A, @A+DPTR;
					DEC A
				UP_ABOVE_LAST_DAY:
					CLR C
					MOV R1, A
					SUBB A, Time_day
					JNC LOOP_TEMP2
					MOV Time_day, R1 
				;;;;判断有无超出最后一天 结束;;;;;;
				
				LJMP LOOP
			

		LOOP_TEMP2:
			LJMP LOOP	
		
		DOWN_DOWN: ;按下 Down 键
			;秒表、倒计时、闹钟
			JB Mode_stopwatch_bit,STOP_STOPWATCH ;秒表模式,暂停秒表
			JB Mode_countdown_bit,STOP_COUNTDOWN ;倒计时模式,暂停倒计时
			JB Mode_alarm_bit,STOP_ALARM ;闹钟模式,取消闹钟
			;非秒表、倒计时、闹钟
			MOV A,Blink ;非闪烁,不改变时间、日期、年
			;JZ LOOP_TEMP2 ;无反应
			JZ DOWN_DARKER ;减少亮度
			JB Mode_time_bit,DOWN_TIME ;时间模式,减少分或小时
			JB Mode_date_bit,DOWN_DATE_TEMP ;日期模式,减少日或月;靠中转程序跳转
			JB Mode_year_bit,DOWN_YEAR ;年模式,减少年
			
			DOWN_DARKER:
				DEC Brightness ;减少亮度
				LJMP LOOP
			
			DOWN_DATE_TEMP:
				LJMP DOWN_DATE
			
			DOWN_YEAR: ;修改年
				DEC Time_year; 减少年
				MOV A, #0FFH;
				CJNE A, Time_year, DOWN_YEAR_JUDGE_0229;
				MOV Time_year, #99; 超出0则回到99
				;;;;;;判断是否是闰年2月29日;;;;;;;
				DOWN_YEAR_JUDGE_0229:
					MOV A, #2
					CJNE A, Time_month, LOOP_TEMP2
					MOV A, #29
					CJNE A, Time_day, LOOP_TEMP2
					MOV Time_day, #28
				;;;;;;判断是否是闰年2月29日;;;;;;;
				LJMP LOOP

			STOP_STOPWATCH: ;暂停秒表
				CLR Flag_stopwatch_bit
				LJMP LOOP
					
			STOP_COUNTDOWN: ;暂停倒计时
				MOV A,Blink
				JNZ DOWN_COUNTDOWN
				CLR Flag_countdown_bit
				LJMP LOOP
				
			DOWN_COUNTDOWN: ;修改倒计时
				JB Blink_low_bit, DOWN_COUNTDOWN_MINUTE
				JB Blink_high_bit, DOWN_COUNTDOWN_HOUR
				DOWN_COUNTDOWN_MINUTE:
					DEC Countdown_second ;减少秒
					MOV A, #0FFH
					CJNE A, Countdown_second, LOOP_TEMP2
					MOV Countdown_second, #59
					LJMP LOOP
				DOWN_COUNTDOWN_HOUR:
					DEC Countdown_minute ;减少分
					MOV A, #0FFH
					CJNE A, Countdown_minute, LOOP_TEMP2
					MOV Countdown_minute, #99
					LJMP LOOP
				
			STOP_ALARM: ;关闭闹钟
				MOV A,Blink
				JNZ DOWN_ALARM
				CLR Flag_alarm_bit
				LJMP LOOP
				
			DOWN_ALARM: ;修改闹钟
				JB Blink_low_bit, DOWN_ALARM_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_ALARM_HOUR
				DOWN_ALARM_MINUTE:
					DEC Alarm_minute ;减少分
					MOV A, #0FFH
					CJNE A, Alarm_minute, LOOP_TEMP2
					MOV Alarm_minute, #59
					LJMP LOOP
				DOWN_ALARM_HOUR:
					INC Alarm_hour ;减少时
					MOV A, #0FFH
					CJNE A, Alarm_hour, LOOP_TEMP3
					MOV Alarm_hour, #23
					LJMP LOOP
					
			DOWN_TIME: ;修改时间
				JB Blink_low_bit, DOWN_TIME_MINUTE ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_TIME_HOUR
				DOWN_TIME_MINUTE:
					DEC Time_minute ;减少分
					MOV A, #0FFH
					CJNE A, Time_minute, LOOP_TEMP3
					MOV Time_minute, #59
					LJMP LOOP
				DOWN_TIME_HOUR:
					DEC Time_hour ;减少时
					MOV A, #0FFH
					CJNE A, Time_hour, LOOP_TEMP3
					MOV Time_hour, #23
					LJMP LOOP
					
			LOOP_TEMP3:
				LJMP LOOP
			

			DOWN_DATE: ;修改日期
				JB Blink_low_bit, DOWN_TIME_DAY ;根据闪烁来判断是修改哪位
				JB Blink_high_bit, DOWN_TIME_MONTH
				LJMP LOOP
			DOWN_TIME_DAY:
				DEC Time_day; 减少日
				MOV A, #0H
				CJNE A, Time_day, LOOP_TEMP3
				MOV A, #2; 
				XRL A, Time_month; 判断是不是二月
				JNZ DOWN_DAY1; 不是二月,按照一般月份处理
			DOWN_JUDGE_LEAP_YEAR:
				MOV A, #11B;
				ANL A, Time_year ;判断闰年
				JNZ DOWN_DAY1; 不是闰年,按照一般二月处理
			DOWN_FEB29:
				MOV Time_day, #29
				LJMP LOOP
			DOWN_DAY1:
				MOV DPTR, #DAYS_OF_MONTH;
				MOV A, Time_month;
				MOVC A, @A+DPTR;
				DEC A
				MOV Time_day, A
				LJMP LOOP

			DOWN_TIME_MONTH:
				DEC Time_month; 减少月
				MOV A, #0
				CJNE A, Time_month, DOWN_GET_LAST_DAY;
				MOV Time_month, #12
				
				;;;;判断有无超出最后一天 开始;;;;;;
				DOWN_GET_LAST_DAY: ;获取当月最后一天
					MOV A, #2; 
					XRL A, Time_month; 判断是不是二月
					JNZ DOWN_GET_LAST_DAY_NORMAL; 不是二月,按照一般月份处理
					MOV A, #11B;
					ANL A, Time_year ;判断闰年
					JNZ DOWN_GET_LAST_DAY_NORMAL; 不是闰年,按照一般二月处理
					MOV A,#29
					SJMP DOWN_ABOVE_LAST_DAY
				DOWN_GET_LAST_DAY_NORMAL:
					MOV DPTR, #DAYS_OF_MONTH ;表头
					MOV A, Time_month ;
					MOVC A, @A+DPTR ;查表
					DEC A ;表中存的是天数+1,需要-1才是真实天数
				DOWN_ABOVE_LAST_DAY:
					CLR C
					MOV R1, A
					SUBB A, Time_day
					JNC LOOP_TEMP3
					MOV Time_day, R1 
				;;;; 判断有无超出最后一天 结束 ;;;;;;
				LJMP LOOP
	
	LJMP LOOP
	
	
	;;;;;;;;;;;;;;;; 键盘 结束 ;;;;;;;;;;;;;;;;;



;;;;;;;; MAIN主程序 结束 ;;;;;;;;;;;;;;;;;;;;;;;;


;;;;;;;; T0定时器中断子程序(计时) 开始 ;;;;;;;;;

ORG 500H;

INT_T0:
	PUSH 49H;
	MOV 49H, A;
	PUSH 49H ;寄存器A入栈
	INC_MS:
		INC Time_ms ;增加毫秒x10
		MOV A, #100
		CJNE A, Time_ms,  INC_STOPWATCH ;如果不等于100则无需进位
	INC_S:
		MOV Time_ms, #0 ;进位后毫秒等于零
		INC Time_second ;增加秒
		MOV A, #60
		CJNE A, Time_second, JUDGE_ALARM ;如果不等于60则无需进位
		;为什么分没有变化也要去判断闹钟:
		; 因为我们使用了一个 Flag_mute_bit
		; 只要我们按了按键,那么 Flag_mute_bit 就会置1
		; 这样闹钟响后就可以通过按键去关闭闹钟
		; 同时,当我们设置时间时,闹钟也不会突然响
		; 在检查闹钟时,如果未到,就会自动将 Flag_mute_bit 清0
		; 但这就带来一个问题,假如闹钟定在当前时间的后一分钟,同时 Flag_mute_bit 为 1
		; 如果只在分变化时才检查闹钟,那么就会来不及将 Flag_mute_bit 清0
		; 所以由于这种特殊情况,我们必须每变一秒就去检查闹钟(为了将 Flag_mute_bit 清0)
		
	INC_MIN:
		MOV Time_second, #0 ;进位后秒等于零
		INC Time_minute ;增加分
		MOV A, #60
		CJNE A, Time_minute, JUDGE_ALARM;
		
	INC_HOUR:
		MOV Time_minute, #0 ;进位后分等于0
		INC Time_hour ;增加时
		MOV A, #24
		CJNE A, Time_hour, JUDGE_ALARM;
		
	INC_DAY:
		MOV Time_hour, #0; 进位后时等于零
		INC Time_day; 增加日
		MOV A, #2; 
		XRL A, Time_month; 判断是不是二月
		JNZ INC_DAY1; 不是二月,按照一般月份处理
	JUDGE_LEAP_YEAR:
		MOV A, #11B;
		ANL A, Time_year ;判断闰年
		JNZ INC_DAY1; 不是闰年,按照一般二月处理
	FEB29:
		MOV A, #30; 
		CJNE A, Time_day, JUDGE_ALARM; 没超出29
		SJMP INC_MONTH; 超出29
	INC_DAY1:
		MOV DPTR, #DAYS_OF_MONTH;
		MOV A, Time_month;
		MOVC A, @A+DPTR ;查表,获取当月天数(表中存储的是天数+1)
		CJNE A, Time_day, JUDGE_ALARM;

	INC_MONTH:
		MOV Time_day, #1; 进位后日等于1
		INC Time_month; 增加月
		MOV A, #13
		CJNE A, Time_month, JUDGE_ALARM; 

	INC_YEAR:
		MOV Time_month, #1; 进位后月等于1
		INC Time_year; 增加年
		MOV A, #100
		CJNE A, Time_year, JUDGE_ALARM;
		MOV Time_year, #0; 超出99则回到0

	JUDGE_ALARM:
		JNB Flag_alarm_bit, CANCEL_MUTE_ALARM; 未打开闹钟
		MOV A, Time_hour
		CJNE A, Alarm_hour, CANCEL_MUTE_ALARM ;未到闹钟时
		MOV A, Time_minute
		CJNE A, Alarm_minute, CANCEL_MUTE_ALARM ;未到闹钟分
		JB Flag_mute_bit, INC_STOPWATCH ;已经按下按钮,静音开关打开
		CLR BEEP ;蜂鸣器响
		SJMP INC_STOPWATCH
	CANCEL_MUTE_ALARM:
		CLR Flag_mute_bit ;关闭静音开关
		SETB BEEP ;蜂鸣器最多只响1分钟

	INC_STOPWATCH:
		JNB Flag_stopwatch_bit, INC_COUNTDOWN ;秒表开关未打开,跳转到INC_COUNTDOWN
		INC Stopwatch_ms;
		MOV A, #100
		CJNE A, Stopwatch_ms, INC_COUNTDOWN; 如果不等于100则无需进位
	INC_S_STOPWATCH:
		MOV Stopwatch_ms, #0; 进位后毫秒等于零
		INC Stopwatch_second; 增加秒
		MOV A, #60
		CJNE A, Stopwatch_second, INC_COUNTDOWN; 如果不等于60则无需进位
	INC_MIN_STOPWATCH:
		MOV Stopwatch_second, #0; 进位后秒等于零
		INC Stopwatch_minute; 增加分
		MOV A, #100;
		CJNE A, Stopwatch_minute, INC_COUNTDOWN;
		MOV Stopwatch_minute, #0; 进位后分等于0
		

	INC_COUNTDOWN:
		JNB Flag_countdown_bit, END_T0 ;倒计时开关未打开,跳转到END_T0
		INC Countdown_ms ;增加毫秒
		MOV A, #100
		CJNE A, Countdown_ms,  END_T0; 如果不等于100则无需减少秒
	DEC_S_COUNTDOWN:
		MOV Countdown_ms, #0 ;进位后毫秒等于零
		DEC Countdown_second ;减少秒
		MOV A, #00H
		CJNE A, Countdown_minute,  DEC_S_COUNTDOWN1 ;分不等于0则跳转到正常减少秒
		CJNE A, Countdown_second, END_T0 ;秒不等于0则跳转
		;分秒都等于0,蜂鸣器响;
		CLR Flag_countdown_bit ;关闭倒计时开关
		CLR BEEP; 蜂鸣器响
	DEC_S_COUNTDOWN1:
		MOV A, #0FFH
		CJNE A, Countdown_second, END_T0; 如果不等于-1则无需减少分
	DEC_MIN_COUNTDOWN:
		MOV Countdown_second, #59; 秒等于59
		DEC Countdown_minute; 减少分

	END_T0:
		;计算过程
		; (2^16-X)*1us=10ms
		; 解得:X=55535
		; 考虑到运行中断子程序也会占用一定时间
		; 平均情况下,中断子程序会占用 23 个机器周期
		; (平均情况指:无任何进位,无闹钟、秒表、倒计时)
		; 故 X=55535+23=55558=D906H
		MOV TL0, #06H 
		MOV TH0, #0D9H 
		POP 49H; 寄存器A出栈
		MOV A,49H;
		POP 49H
		SETB TR0
		
		RETI
	

;;;;;;; T0定时器中断子程序(计时) 结束 ;;;;;;;;;;


;;;;;;;; T1定时器中断子程序 开始 ;;;;;;;;;;
ORG 1000H

INT_T1:
	PUSH 49H;
	MOV 49H, A;
	PUSH 49H ;寄存器A入栈
	MOV A, Mode ;获取当前模式
	ANL A,#11111110B
	JZ GET_TIME ;时间模式
	ANL A,#11111100B
	JZ GET_DATE ;日期模式
	ANL A,#11111000B
	JZ GET_YEAR ;年模式
	ANL A,#11110000B
	JZ GET_ALARM ;闹钟模式
	ANL A,#11100000B
	JZ GET_STOPWATCH ;秒表模式
	ANL A,#11000000B
	JZ GET_COUNTDOWN ;倒计时模式
	GET_TIME:
		MOV Show_low, Time_minute ;将分读入待显示字符
		MOV Show_high, Time_hour ;将时读入待显示字符
		SJMP BIN_2_BCD
	GET_DATE:
		MOV Show_low, Time_day ;将日读入待显示字符
		MOV Show_high, Time_month ;将月读入待显示字符
		SJMP BIN_2_BCD
	GET_YEAR:
		MOV Show_low, Time_year ;将年读入待显示字符
		MOV Show_high, #20
		SJMP BIN_2_BCD
	GET_ALARM:
		MOV Show_low, Alarm_minute ;将闹钟分读入待显示字符
		MOV Show_high, Alarm_hour ;将闹钟时读入待显示字符
		SJMP BIN_2_BCD
	GET_STOPWATCH:
		MOV A, Stopwatch_minute 
		JZ GET_STOPWATCH_S_MS
			MOV Show_low, Stopwatch_second ;将秒表秒读入待显示字符
			MOV Show_high, Stopwatch_minute ;将秒表分读入待显示字符
			SJMP BIN_2_BCD
		GET_STOPWATCH_S_MS:
			MOV Show_low, Stopwatch_ms ;将秒表毫秒
			MOV Show_high, Stopwatch_second ;将秒表秒
			SJMP BIN_2_BCD
	GET_COUNTDOWN:
		MOV Show_low, Countdown_second ;将倒计时秒读入待显示字符
		MOV Show_high, Countdown_minute ;将倒计时分读入待显示字符
		SJMP BIN_2_BCD
	
	BIN_2_BCD:
		MOV A,Show_low
		MOV B,#10
		DIV AB
		MOV BCD1,B
		MOV B, #10 ;为了防bug除两次,也可以用下面的方法
		DIV AB
		MOV BCD2,B
		
		MOV A,Show_high
		MOV B,#10
		DIV AB
		MOV BCD3,B
		MOV BCD4,A ;懒得防bug了,不用除两次
	
	JUDGE_BLINK_LOW: ;判断低两位是否需要闪烁
		JNB Blink_low_bit, SHOW_LED1_DOT
		INC Blink_count0
		JB Blink_count0_bit, ADD_BLINK_COUNT1_0
	ADD_BLINK_COUNT1_0: ;闪烁计数,用于记录当前闪/不闪之间的间隔
		INC Blink_count1
		MOV Blink_count0, #0
		JB Blink_count1_bit, ADD_BLINK_COUNT2_0
	ADD_BLINK_COUNT2_0:
		INC Blink_count2
		MOV Blink_count1, #0
		JNB Blink_count2_bit, SHOW_LED1_DOT
		SJMP JUDGE_BLINK_HIGH

	SHOW_LED1_DOT:
		JNB Mode_alarm_bit, SHOW_LED1
		JNB Flag_alarm_bit, SHOW_LED1
		MOV R4,BCD1
		MOV R5, #1
		LCALL SEND_BYTE
		SETB LED1
		LCALL DELAY
		CLR LED1
		SJMP SHOW_LED2

	SHOW_LED1:
		MOV R4, BCD1
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED1
		LCALL DELAY
		CLR LED1

	SHOW_LED2:
		MOV R4,BCD2
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED2
		LCALL DELAY
		CLR LED2

	JUDGE_BLINK_HIGH: ;判断高两位是否需要闪烁
		JNB Blink_high_bit, SHOW_LED3_DOT
		JB Blink_low_bit, NOT_REPEAT_INC
		INC Blink_count0
		JB Blink_count0_bit, ADD_BLINK_COUNT1_1
	ADD_BLINK_COUNT1_1: ;闪烁计数,用于记录当前闪/不闪之间的间隔
		INC Blink_count1
		MOV Blink_count0, #0
		JB Blink_count1_bit, ADD_BLINK_COUNT2_1
	ADD_BLINK_COUNT2_1:
		INC Blink_count2
		MOV Blink_count1, #0
	NOT_REPEAT_INC:
		JNB Blink_count2_bit, SHOW_LED3_DOT
		SJMP INT_T1_END

	SHOW_LED3_DOT:
		JB Mode_year_bit,SHOW_LED3
		MOV R4,BCD3
		MOV R5, #1
		LCALL SEND_BYTE
		SETB LED3
		LCALL DELAY
		CLR LED3
		SJMP SHOW_LED4
		
	SHOW_LED3:
		MOV R4,BCD3
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED3
		LCALL DELAY
		CLR LED3

	SHOW_LED4:
		MOV R4, BCD4
		MOV R5, #0
		LCALL SEND_BYTE
		SETB LED4
		LCALL DELAY
		CLR LED4

	INT_T1_END:
		POP 49H; 寄存器A出栈
		MOV A,49H;
		POP 49H
		RETI
	

DELAY:
	MOV 49H, Brightness
	DELAY_LOOP:
		DJNZ 49H, DELAY_LOOP
	RET

SEND_BYTE:
	MOV A,R4 	;将数据移入寄存器A
	DJNZ R5,USE_LED_SEGMENT_CODE	;R5为1显示小数点
	MOV DPTR, #LED_SEGMENT_CODE_DOT
	SJMP GET_LED_SEGMENT_CODE
USE_LED_SEGMENT_CODE:
	MOV DPTR,#LED_SEGMENT_CODE  ;将表头移到DPTR
GET_LED_SEGMENT_CODE:
	MOVC A,@A+DPTR 		;查LED编码表
	MOV R5,A
	MOV R6,#08H  ;循环8次
	SEND_BYTE_LOOP:
		ANL A,#01H ;取第一位
		CLR HC595_DAT ;清0
		JZ LED_NOT_ZERO ;如果第一位是0则跳转
		SETB HC595_DAT ;置1
		LED_NOT_ZERO:
			SETB HC595_SCK ;时序
			CLR HC595_SCK ;时序
		MOV A,R5
		RR A
		MOV R5,A
		DJNZ R6,SEND_BYTE_LOOP	
	CLR HC595_RCK ;时序
	SETB HC595_RCK ;时序

	RET
;;;;;;;;; T1定时器中断子程序 结束 ;;;;;;;;;


;;;;;;;;; 增加亮度 开始 ;;;;;;;;;;

BRIGHTER:
	PUSH 49H
	MOV 49H, A
	PUSH 49H
	MOV A, Brightness
	CJNE A, #100000B, NOT_BRIGHTEST
	SJMP BRIGHTER_END
	NOT_BRIGHTEST:
		INC A
		;RL A
		MOV Brightness, A
	BRIGHTER_END:
		POP 49H
		MOV A, 49H
		POP 49H
		RETI
;;;;;;;;; 增加亮度 结束 ;;;;;;;;;;


;;;;;;;;; 减少亮度 开始 ;;;;;;;;;;

DARKER:
	PUSH 49H
	MOV 49H, A
	PUSH 49H
	MOV A, Brightness
	CJNE A, #00000001B, NOT_DARKEST
	SJMP DARKER_END
	NOT_DARKEST:
		DEC A
		;RR A
		MOV Brightness, A
	DARKER_END:
		POP 49H
		MOV A, 49H
		POP 49H
		RETI
;;;;;;;;; 减少亮度 结束 ;;;;;;;;;;


END

感想

  首先要谢谢我们组的其他两位成员,若不是ta俩耐心听我的唠叨,我估计会迷失在各种LJMP、CJNE、JB中。同时ta俩也帮我找了很多bug,让我这个完美主义者得到莫大的满足。

  再说说写汇编程序的收获。对单片机有了更深的理解。然后,对操作系统中的“进程”这一概念也有了更深的理解。当然,我觉得消除了对底层硬件的恐惧,这点才是最深刻的体会。

  总之就是觉得自己好棒棒~

后续补充(2021/06/22):这个程序是我临时学了一下指令写的(当时疫情没怎么认真听课),有非常多很蠢的操作,比如吧,明明可以 push Acc,我硬是要把 A 保存到 ram 再 push……各位请不要学我。

参考

最后更新于