简介:本资源是一份面向汇编语言初学者与微机原理课程学习者的万年历显示程序实践项目,聚焦于DOS环境下用8086汇编实现日历核心逻辑,解决键盘输入年月日、自动计算当月首日星期、按公历规则(含闰年判断)生成完整日历并高亮显示周末等典型接口编程问题。压缩包为1个581KB的Word文档(.doc),完整涵盖需求分析、蔡勒公式原理推导、INT 21H 0AH输入处理、INT 10H 06H彩色输出控制、二进制/十进制转换、各子程序流程图及全部源代码注释,内容结构清晰,适合作为课程设计参考或汇编综合实训范例。已有837人学习下载,读者可直接复现带界面的日历程序,深入理解日期计算、光标定位、错误校验与屏幕属性设置等底层编程技巧。
1. 一个在 DOS 下跑通的万年历:不是玩具,是汇编能力的实体化验证
你可能以为“万年历”只是课程设计里交差用的 Demo——但真当你在纯 DOS 环境下,不调任何高级库、不依赖 C 运行时、连 printf 都没有,只靠 INT 21H 和 INT 10H 两个中断,把 2009 年 3 月的日历一行行打在 80×25 的文本屏上,还让星期六/日变红、输入日期高亮、错误输入立刻跳错重输……那一刻你会明白:这不是练习,是汇编语言对「控制力」的终极考卷。这个项目专为《微机接口与汇编语言》课程设计打磨,面向刚啃完寄存器寻址、堆栈平衡、BIOS 中断调用的新手,但它跑起来的那一刻,你就从“能写语法”跨进了“能控硬件”的门槛。它不追求图形界面,但每一行光标定位、每一个颜色设置、每一次除法余数判断,都在逼你直面 CPU 如何真正干活。如果你正卡在“学了指令却不会组织逻辑”,或者调试时被 CL/CX 混用搞到怀疑人生——这项目就是你的后悔药:所有坑都踩过,所有绕不过去的细节都摊开在源码里。
2. 架构拆解:三层驱动模型——输入校验 → 日期计算 → 屏幕渲染
这个万年历表面看是“输个日期→出个表”,实际背后是典型的三层驱动结构:输入层负责人机交互与数据清洗,计算层解决“1号是星期几”这个核心黑匣子,显示层完成像素级排版与色彩控制。三者之间没有抽象层,全靠寄存器传值、内存地址偏移、标志位跳转串联。这种紧耦合不是缺陷,而是 DOS 实时环境下的必然选择——没有操作系统帮你做内存隔离,你写的每一条MOV AX, [DI]都得清楚 DI 指向哪、那个字节是不是刚被上一个子程序改过。
2.1 输入模块:INT 21H 0AH 的缓冲区陷阱与字符解析实战
输入不是简单读键盘,而是和 DOS 缓冲区格式死磕。程序用STR1 DB 9, ?, 9 DUP(?)定义缓冲区,这里 9 是最大允许输入长度(含回车),?是 DOS 写入的实际字符数,后续 9 字节才是存储区。关键点在于:DOS 要求你提前告诉它“最多收几个字符”,且回车符\r(ASCII 0DH)会原样存入缓冲区末尾。所以输入20090327(8 字符+回车=9 字节),缓冲区布局是:
| 偏移 | 内容 | 说明 |
|---|---|---|
| STR1+0 | 09H | 预置最大长度 |
| STR1+1 | 08H | DOS 写入:实际输入字符数(不含回车) |
| STR1+2 | '2' | 字符串起始 |
| ... | ... | ... |
| STR1+9 | 0DH | 回车符 |
解析年份时,代码用P1:循环取前 4 字符:
LEA DI, STR1+2 ; DI → 字符串首地址 MOV CX, 4 ; 循环4次取年 MOV AX, 0 ; 清零AX存年份 P1: MOV BL, [DI] ; 取字符 SUB BL, 30H ; ASCII转数字 MOV BH, 0 ; 清BH,BL→BX低字节 MOV P, 10 ; P=10(十进制权) MUL P ; AX = AX * 10 ADD AX, BX ; AX = AX + 当前数字 INC DI ; 指向下一位 DEC CX JNZ P1 ; 循环结束,AX=2009提示:
MUL P是无符号乘,AX初始为 0,每次AX*10+新数字实现字符串转整数。这里P是内存变量而非立即数,因 8086 不支持MUL 10。新手常误用IMUL或漏清BH,导致高位垃圾数据污染BX。
月份和日期同理,取STR1+6开始 2 字符(月)、STR1+8开始 2 字符(日)。但注意:输入03时,[DI]是'0',SUB 30H后是0,必须参与计算,否则03会被当成3导致逻辑错乱。
2.2 闰年判断子程序 PR:标志位驱动的状态机设计
闰年判断是典型状态分支逻辑,但汇编里不能写if (year%4==0 && year%100!=0) || year%400==0,而要拆成三次除法+条件跳转:
PR PROC PUSH AX PUSH BX PUSH CX PUSH DX MOV AX, YEAR ; 取年份 MOV CX, AX ; 备份到CX MOV DX, 0 ; 清DX(被除数高位) MOV BX, 4 ; 除数4 DIV BX ; AX = year/4, DX = year%4 CMP DX, 0 ; 余数为0? JNZ B1 ; 否→非闰年(CF=0) MOV AX, CX ; 是→检查百年 MOV BX, 100 DIV BX ; AX = year/100, DX = year%100 CMP DX, 0 ; 余数为0? JNZ B2 ; 否→闰年(CF=1) MOV AX, CX ; 是→检查400年 MOV BX, 400 DIV BX CMP DX, 0 ; 余数为0? JZ B2 ; 是→闰年(CF=1) B1: CLC ; CF=0:平年 JMP B3 B2: STC ; CF=1:闰年 B3: POP DX POP CX POP BX POP AX RET PR ENDP逻辑链:先除 4 → 余数非 0 直接平年;余数为 0 再除 100 → 余数非 0 是闰年;余数为 0 再除 400 → 余数为 0 才是闰年。最终用进位标志 CF 表示结果(CF=1 闰年,CF=0 平年),主程序用JC L1跳转,比用内存变量更高效。这里DIV指令的隐式操作(DX:AX为被除数)和CMP DX,0的时机,是新手最容易翻车的点——忘清DX会导致除法结果错乱。
2.3 日历渲染核心:蔡勒公式在 8086 上的手动实现与边界处理
“1号是星期几”是日历对齐的灵魂。本项目采用蔡勒公式(Zeller's Congruence):
W = [C/4] - 2C + y + [y/4] + [13(M+1)/5] + d - 1其中C是世纪数减 1(如 2009 → C=20),y是年份后两位(2009 → y=09),M是月份(1-12),d是日(此处固定为 1)。但公式有玄学规则:1月、2月需视为上一年的13月、14月。比如 2009 年 1 月 → 当作 2008 年 13 月,此时C=20,y=08,M=13。
代码中用JMP J1分支处理:
CMP MONTH, 3 ; 月份≥3?是→走常规分支 JC J1 ; 否(1或2月)→跳J1 ; --- 常规分支:M=MONTH --- XOR AH, AH MOV AX, MONTH MOV DX, 13 MUL DX ; AX = M * 13 MOV CX, 5 DIV CX ; AX = (M*13)/5, DX = 余数 XCHG AX, BX ; BX存商 ; ... 后续累加 JMP J3 J1: ; --- 特殊分支:1/2月转上一年13/14月 --- MOV BL, 100 MOV AX, YEAR DIV BL ; AL=y, AH=C MOV C2, AH ; C存C2 MOV Y, AL ; y存Y CMP Y, 1 ; y是否为00?(如2000年→y=00) JNC J2 ; y≥1 → 正常减1 ADD Y, 100 ; y=00 → y=100(上一年y=99,但DOS中100%100=0,故设y=99) J2: SUB Y, 1 ; y = y-1 ; ... 计算C/4, y, y/4等 SUB MONTH, 12 ; M = M+12(1→13, 2→14) ; ... 继续计算13*(M+1)/5 J3: ; --- 结果处理 --- MOV AX, BX MOV CL, 7 IDIV CL ; AX = W/7, AH = W%7 CMP AH, 0 JG SKIPADD ADD AH, 7 ; 确保AH∈[0,6] SKIPADD: MOV BL, AH ; BL = 星期几(0=Sunday,1=Monday...6=Saturday) MOV N, BL ; 存入N变量注意:
IDIV是有符号除,因蔡勒公式结果可负,需保证余数为正。ADD AH,7是经典补码修正——当AH为负时(如 -1),加 7 得 6(Saturday),符合日历习惯。N最终值:0→SUN 列,1→MON 列…6→SAT 列,直接决定首日打印的列偏移。
3. 屏幕控制实战:INT 10H 06H 卷屏与光标精确定位
DOS 文本模式下,没有“画布坐标系”,只有“屏幕坐标系”:左上角 (0,0),右下角 (24,79)。日历渲染本质是“光标定位 → 写字符 → 改颜色 → 换行”的循环。本项目用GBIAO子程序封装光标设置:
GBIAO PROC PUSH AX PUSH BX MOV AH, 2 ; 功能号:设置光标位置 MOV BH, 0 ; 页面号0(文本页) INT 10H ; 调用BIOS POP BX POP AX RET GBIAO ENDP调用前需预置DH=行号、DL=列号。例如显示标题"*** Calendar ***":
MOV DH, ROW ; ROW初始=1 MOV DL, 2 CALL GBIAO LEA DX, A2 ; A2="*** Calendar ***$" CALL NINE ; NINE是显示字符串子程序(INT 21H 09H) ADD ROW, 2 ; 下一行起始位置而周末高亮用的是INT 10H 06H卷屏功能——本质是给指定矩形区域刷背景色:
; 设置SUN列(第0列)为红色背景 MOV AH, 6 MOV AL, 0 ; 上卷0行(即只刷色不滚动) MOV CH, ROW ; 左上角行 MOV CL, 0 ; 左上角列(SUN列起始) MOV DH, ROW ; 右下角行(单行) MOV DL, 3 ; 右下角列(SUN占4字符:"SUN ") MOV BH, 10110100B ; 背景红(1011)+前景白(0100) → 红底白字 INT 10H ; 设置SAT列(第30列)为红色背景 MOV CL, 30 MOV DL, 33 ; SAT占4字符:"SAT$" INT 10H关键参数:
BH的二进制10110100B中,高4位1011是背景色(1011=红+蓝+绿=洋红?不!DOS颜色编码中1011= 背景红+前景白,因1011的1xxx表示背景红,xxx1表示前景白)。实际效果是红底白字,符合“突出显示”要求。新手常混淆CH/CL与DH/DL,导致刷错区域。
日期数字显示用BUF3预存" 1$"," 2$"…等字符串(含前导空格对齐),通过SI指针遍历:
MOV SI, OFFSET BUF3 MOV AL, 5 MUL N ; AL = N*5 → SUN列偏移0, MON列5, TUE列10... MOV B, AX ; B存列位置 ; ... 定位光标到(DH, B) MOV DX, SI ; DX→" 1$"地址 CALL NINE ; 显示 ADD SI, 5 ; 指向下一个字符串 ADD B, 5 ; 列+5(每个日期占5字符:空格+数字+空格+换行符) DEC G ; G=当月天数,循环计数 JNZ POS ; 未完继续BUF3定义中0AH,0DH是回车换行符,确保每个数字独占一行,避免覆盖。
4. 避坑指南:那些让调试崩溃到凌晨三点的血泪经验
这个项目最折磨人的不是算法,而是 DOS 环境下寄存器、内存、中断的隐式副作用。以下是实测踩过的 5 个高频坑,每一条都附带现象、根因和修复动作,照着查能省 3 小时:
4.1 现象:输入正确日期后,日历第一行空白,第二行才开始显示数字
原因:ROW变量初始值设为 1,但显示"SUN MON..."后执行ADD ROW,1,此时ROW=2;接着计算首日位置时MOV DH,ROW,光标定在第2行(DOS行号从0开始,第2行=屏幕第3行),而BUF3字符串自带0AH,0DH换行符,导致第一个数字打在第4行。
解决:在显示"SUN..."后,ROW应加 2(跳过标题行和星期行),或重置ROW为具体值。代码中ADD ROW,1后应再INC ROW,或直接MOV ROW,5(假设标题占4行)。
4.2 现象:1月、2月日历星期错位,其他月正常
原因:蔡勒公式特殊处理分支J1中,SUB MONTH,12后未恢复MONTH值,导致后续取BUF1月份天数时用DEC MONTH计算索引出错(如输入1月,MONTH变为 -11,DEC MONTH后为 -12,ADD DI,MONTH地址越界)。
解决:在J1分支末尾ADD MONTH,12恢复原值,或用临时寄存器存MONTH,计算完再赋回。
4.3 现象:输入20000101(2000年1月1日)时,程序报错或显示异常
原因:2000年是世纪年,y=00,在J1分支中CMP Y,1后JNC J2跳过ADD Y,100,直接SUB Y,1→Y=255(00-1=FFH),导致y变成 255,闰年判断和蔡勒公式全错。
解决:CMP Y,1应改为CMP Y,0,JZ跳转处理y=0情况,并设Y=99(上一年 y=99)。
4.4 现象:显示日期字符串时出现乱码,如"20090327"后跟一堆@符号
原因:STR1缓冲区定义为DB 9, ?, 9 DUP(?),但输入20090327时,DOS 写入STR1+1=08H,STR1+2~STR1+9存字符,STR1+10未初始化。DATE:循环用MOV CL,8固定输出8字符,若STR1+10是随机值,MOV DL,BL会输出垃圾。
解决:循环前MOV CL,[STR1+1]动态取实际长度,或初始化STR1+10为$。
4.5 现象:调用NINE子程序显示字符串后,光标位置错乱,后续GBIAO定位失效
原因:NINE子程序用INT 21H 09H显示字符串,该功能自动将光标移到字符串末尾($后),而NINE返回时未重置光标。后续MOV DH,ROW/MOV DL,1定位时,ROW值已因光标移动改变。
解决:在NINE返回前,强制重置光标:MOV DH,0 / MOV DL,0 / CALL GBIAO,或在每次CALL NINE后重新MOV DH,ROW / MOV DL,XX。
5. 二次开发实战:从“能跑”到“能改”的 3 个关键改造点
跑通只是起点,真正吃透这个项目,得动手改。我当年在某高校实验室带学生时,发现只要带他们做完以下三件事,汇编调试能力就上了一个台阶——不是背指令,而是建立“寄存器-内存-屏幕”的三维映射直觉。
5.1 改造一:支持“当前月”快捷输入(免输年月日)
需求:按任意键直接显示当前系统日期所在月的日历。
原理:DOS 提供INT 21H 2AH获取系统日期,返回CX=年,DH=月,DL=日。
步骤:
- 在
REINPUT:前插入检测:
MOV AH, 01H ; 检查键盘缓冲区 INT 16H ; 若有键入,ZF=0 JZ GETDATE ; ZF=1(无键)→跳获取系统日期 CMP AL, 13 ; 是回车?跳过手动输入 JE GETDATE JMP REINPUT ; 否→走手动输入流程 GETDATE: MOV AH, 2AH ; 获取系统日期 INT 21H MOV YEAR, CX MOV MONTH, DH MOV DAY, DL- 跳过手动输入,直接调用
PR和GETN。
注意点:INT 21H 2AH返回年份是 16 位(如 2009),无需拆分C/y,直接存YEAR;但GETN中C2/Y计算仍需DIV 100拆解。
5.2 改造二:增加“上下月”翻页功能(键盘方向键控制)
需求:显示日历时,按 ← 显示上月,→ 显示下月,ESC 退出。
原理:监听INT 16H 00H扩展键,扫描码4B00H(←)、4D00H(→)、0100H(ESC)。
步骤:
- 在日历显示后插入循环:
KEYLOOP: MOV AH, 00H ; 读键盘 INT 16H ; 扫描码在 AX CMP AX, 0100H ; ESC? JE EXIT CMP AX, 4B00H ; ←? JE PREV_MONTH CMP AX, 4D00H ; →? JE NEXT_MONTH JMP KEYLOOP PREV_MONTH: DEC MONTH CMP MONTH, 0 JNE REFRESH MOV MONTH, 12 DEC YEAR JMP REFRESH NEXT_MONTH: INC MONTH CMP MONTH, 13 JNE REFRESH MOV MONTH, 1 INC YEAR REFRESH: CALL PR ; 重新判断闰年 CALL GETN ; 重新算1号星期几 CALL DISPLAY_CALENDAR ; 重绘日历(需提取显示逻辑为子程序) JMP KEYLOOP关键点:DISPLAY_CALENDAR必须封装所有显示代码(清屏、打标题、打星期、打日期),否则重复代码爆炸。REFRESH前需CALL CLEAR_SCREEN(用INT 10H 06H全屏刷黑)。
5.3 改造三:将颜色方案参数化(支持用户自定义)
需求:让用户输入颜色代码(如0A表示绿字,1E表示黄底蓝字),动态应用到周末和今日。
原理:DOS 颜色属性是 1 字节,高4位背景,低4位前景。
步骤:
- 在输入日期后,新增颜色输入:
LEA DX, COLOR_PROMPT CALL NINE LEA DI, COLOR_BUF MOV AH, 0AH INT 21H ; 输入2字符,如"0A" ; 解析COLOR_BUF+2的两个字符→AL MOV AL, [DI+2] SUB AL, 30H CMP AL, 10 JB DIGIT1 SUB AL, 7 ; 'A'→10 DIGIT1: MOV BL, AL MOV AL, [DI+3] SUB AL, 30H CMP AL, 10 JB DIGIT2 SUB AL, 7 DIGIT2: MOV CL, 4 SHL BL, CL ; BL<<4 → 高4位 ADD BL, AL ; BL = 属性字节 MOV WEEK_COLOR, BL ; 存周末颜色- 在周末刷色时,用
MOV BH,WEEK_COLOR替代硬编码10110100B。
验证技巧:写个TEST_COLOR子程序,用INT 10H 06H刷满屏测试色块,确认BL值对应预期效果。
从那以后我每次教学生汇编,都强制他们先跑通这个万年历,再必须完成至少一项改造——不是为了炫技,而是逼他们在寄存器里看见数据流动,在屏幕上验证逻辑闭环。当MOV DH,ROW的ROW值和你心里想的行号对上,当ADD AH,7后AH真的变成 0(Sunday),那种掌控感,是任何高级语言都给不了的底层快感。希望帮到你。
本文还有配套的精品资源,点击获取