汇编语言万年历课设:蔡勒公式与INT 10H彩色日历实现

发布时间:2026/10/24 8:46:07
汇编语言万年历课设:蔡勒公式与INT 10H彩色日历实现

简介:这份资源是一份用汇编语言编写的万年历显示程序,面向正在学习《微机接口与汇编语言》课程的学生及对底层编程感兴趣的开发者,帮助理解如何通过键盘输入年月日后完整显示当月日历。压缩包内共1个doc文档,约581KB,以课程设计报告形式呈现,涵盖需求分析、概要设计、详细设计与程序实现全过程。文档重点讲解蔡勒公式计算某天星期几、INT 21H的0AH功能调用输入年月日、闰年判断、二进制转十进制子程序,以及利用INT 10H设置星期六、日字体为红色等界面细节,并给出错误输入提示与处理思路。目前已有836人学习,适合需要完成汇编课程设计或想深入掌握汇编界面编程与日期算法的读者参考。

1. 从一次答辩翻车说起:这套汇编万年历到底解决什么问题

有位同学在《微机接口与汇编语言》课程设计答辩现场被导师问住了:日历能显示,但输入 2000 年 1 月 1 日时星期几算错了,星期六日的红色标记也串了位。问题不在界面,而在蔡勒公式对 1 月、2 月的跨年处理和世纪数取值。这套用汇编语言编写的万年历显示程序,正是冲着这类“能跑但经不起追问”的痛点来的——它把输入校验、闰年判断、蔡勒公式求星期、彩色日历排版串成一条完整链路,适合正在做汇编课设、微机接口课程设计的学生,也适合想拿一份可演示、可讲原理的完整方案直接参考的人。核心不是“显示一个日历”,而是让你在答辩时能把每个模块的设计动机说清楚。

2. 需求拆解与技术选型:为什么用 INT 21H + INT 10H 而不是纯 DOS 调用

2.1 功能需求与非功能需求分开列

先把题目要求翻译成可验收的条目,不然后面写代码容易漏。功能需求有四条:键盘输入年月日、显示该月完整日历并标注星期、星期六和星期日用不同颜色、输入非法时提示并允许重新输入。非功能需求常被忽略,但答辩时导师最爱问:界面要清晰、光标定位要准、一位数和两位数日期不能串列、程序要能连续运行不崩溃。

我一般会把它们整理成一张验收对照表,写代码时逐条打勾:

编号需求描述验收方式对应模块
F1输入年月日,格式如 20090101键盘输入后缓冲区数据正确输入模块
F2显示当月日历,含星期标注屏幕输出与真实日历比对显示模块
F3周六周日列红色显示观察颜色属性颜色控制
F4非法输入提示并重输输入 20091301 应报错校验模块
N1一位数与两位数对齐检查 1 号和 10 号列位置格式化输出
N2闰年 2 月显示 29 天输入 20000201 验证闰年判断

这张表后面会反复用到——每完成一个模块就回来核对,避免最后联调时才发现漏了需求。

2.2 技术选型:BIOS 中断与 DOS 中断的分工

汇编课设里最容易纠结的是“用哪个中断”。这套方案的分工很明确:输入输出走 DOS 的 INT 21H,屏幕属性控制走 BIOS 的 INT 10H。原因不复杂——INT 21H 的 0AH 功能自带缓冲区管理和回车结束标志,省去自己写键盘扫描;而颜色、光标、清屏这些底层显示控制,DOS 中断给不了,必须用 INT 10H 的 06H 功能(按属性滚屏/填色)和 02H 功能(定位光标)。

具体用到的功能号如下:

  • INT 21H, AH=0AH:带缓冲区的字符串输入,回车结束
  • INT 21H, AH=09H:显示以$结尾的字符串
  • INT 21H, AH=02H:显示单个字符
  • INT 21H, AH=4CH:程序正常退出
  • INT 10H, AH=06H:按指定属性初始化或滚动窗口,用来设置颜色
  • INT 10H, AH=02H:设置光标位置

提示:INT 10H 的 06H 功能入口参数里,BH 的高四位是背景色、低四位是前景色。设红色文字用10110100B,设蓝底白字用10110001B,这个属性字节的拼法在调试时最容易写反。

2.3 为什么用蔡勒公式而不是查表法

求某月第一天是星期几,常见做法有两种:查万年历表或公式计算。查表法要预存大量数据,汇编里定义大数组既占空间又容易在数据段偏移上出错。蔡勒公式只需要几个除法和移位就能算出结果,代码量小,而且答辩时能讲出数学依据,导师更认可。

公式原型是:

W = [C/4] - 2C + y + [y/4] + [13*(M+1)/5] + d - 1

其中 C 是世纪数减一,y 是年份后两位,M 是月份,d 是日数,结果对 7 取模。关键坑点:1 月和 2 月要当作上一年的 13 月和 14 月来算,此时 C 和 y 都取上一年的值。如果年份后两位是 00,减一后会变成负数,需要特殊处理成 99。这个细节在后面的 GETN 子程序里会具体展开。

3. 总体架构设计:四个子程序撑起整个日历

3.1 模块划分与调用关系

整个程序按“主控 + 四个功能子程序”组织,主程序负责流程调度和界面框架,子程序各管一件事。这样划分的好处是每个子程序可以单独测试,符合课设里“先验证再集成”的调试习惯。

四个核心子程序:

  • GBIAO:光标定位。入口 DH=行号、DL=列号,内部调用 INT 10H 的 02H 功能。所有显示前都要先调它。
  • NINE:字符串显示。入口 DX=字符串偏移地址,内部调用 INT 21H 的 09H 功能。把重复的显示调用收拢成一处。
  • PR:闰年判断。入口 YEAR 变量,出口 CF=0 表示平年、CF=1 表示闰年。
  • GETN:求某月第一天是星期几。入口 YEAR、MONTH,出口 N 存星期值(0 表示星期日,1 表示星期一,依此类推)。

主程序流程是:设置显示模式 → 清屏 → 画界面框架 → 循环读取输入 → 校验 → 调 PR 定 2 月天数 → 调 GETN 求星期 → 输出日历 → 退出。

3.2 数据段设计:缓冲区与月份天数表

数据段里几个关键定义决定了程序能不能正确运行,逐个说明。

输入缓冲区按 INT 21H 0AH 功能的格式要求定义:

STR1 DB 9 ; 预置允许接收的字符个数(含回车) DB ? ; 实际键入字符数,由系统回填 DB 9 DUP(?) ; 字符存放区,从 STR1+2 开始

第一字节写 9 表示最多接收 9 个字符(8 位日期加回车)。输入 20090101 后,STR1+1 存 8,STR1+2 到 STR1+9 依次是 '2''0''0''9''0''1''0''1'。

月份天数表用 12 字节数组,2 月先留空,等闰年判断后再填:

BUF1 DB 31,?,31,30,31,30,31,31,30,31,30,31

取某月天数时,用月份减一作为偏移量:MOV DI, OFFSET BUF1后ADD DI, MONTH-1,再MOV CL, [DI]就得到该月最大天数。这个“减一取偏移”是汇编里数组访问的固定套路,写错一位就会取到相邻月份的天数。

星期标注字符串和日期字符串都以$结尾,这是 INT 21H 09H 功能的硬性要求:

WEEK DB "SUN MON TUE WED THU FRI SAT$"

注意:字符串末尾漏掉$会一直显示到内存里下一个$为止,屏幕上出现一堆乱码,这是汇编课设里最常见的翻车点之一。

3.3 界面布局与颜色属性规划

界面分四层:顶部标题栏、输入提示行、日期回显行、日历主体。颜色用 INT 10H 06H 功能按矩形区域填充,每个区域的属性字节不同。

  • 标题栏:蓝底白字,属性10110001B
  • 输入提示区:蓝底白字,属性10110001B
  • 星期六、日两列:黑底红字,属性10110100B
  • 日历主体:默认黑底白字

设置红色列时,用 06H 功能在对应列位置填一个 1 行高的矩形:

MOV AH,6 MOV AL,0 ; AL=0 表示用属性填充整个窗口 MOV CH,ROW ; 左上角行 MOV CL,3 ; 左上角列(星期日列) MOV DH,ROW ; 右下角行 MOV DL,0 ; 右下角列 MOV BH,10110100B ; 黑底红字 INT 10H

这里 CL 和 DL 的取值要根据实际列坐标调整,星期六列在右侧,列号约 30 到 33。填色区域的行列坐标算错,红色就会盖到别的日期上,调试时建议先用单步观察光标位置。

4. 关键模块实现:输入校验、闰年判断与蔡勒公式落地

4.1 输入年月日与合法性校验

输入用 INT 21H 的 0AH 功能,一次读入 8 位字符串,然后分三段转成数值:前 4 位是年,中间 2 位是月,后 2 位是日。转换逻辑是“逐位取字符减 30H 再累加乘 10”:

LEA DI,STR1+2 ; DI 指向日期串首地址 MOV CX,4 ; 年份 4 位 MOV AX,0 P1: MOV BL,[DI] SUB BL,30H ; 字符转真值 MOV BH,0 MOV P,10 MUL P ; AX = AX * 10 ADD AX,BX ; 加上当前位 INC DI DEC CX JNZ P1 MOV YEAR,AX

月份和日期同理,只是位数改成 2、起始偏移改成 STR1+6 和 STR1+8。

校验分三步走。第一步查月份范围,小于 1 或大于等于 13 直接报错。第二步查日期下限,小于 1 报错。第三步查日期上限,先调 PR 判断闰年确定 2 月天数,再从 BUF1 取出该月最大天数,输入的日超过就报错。

CMP MONTH,1 JC ERROR ; 月份 < 1 CMP MONTH,13 JNC ERROR ; 月份 > 12 CMP DAY,1 JC ERROR ; 日 < 1 CALL PR ; 判断闰年 JC L1 LEA DI,BUF1 MOV [DI+1],BYTE PTR 28 ; 平年 2 月 28 天 JMP L2 L1: LEA DI,BUF1 MOV [DI+1],BYTE PTR 29 ; 闰年 2 月 29 天 L2: ; 取该月最大天数与 DAY 比较

报错时显示 WRONG 字符串并跳回 REINPUT 重新输入,形成输入循环。

4.2 闰年判断子程序 PR

闰年规则是:能被 4 整除但不能被 100 整除,或者能被 400 整除。用 DIV 指令逐级判断,结果通过 CF 标志位返回。

PR PROC PUSH AX PUSH BX PUSH CX PUSH DX MOV AX,YEAR MOV CX,AX MOV DX,0 MOV BX,4 DIV BX ; 除以 4 CMP DX,0 JNZ B1 ; 余数不为 0,平年 MOV AX,CX MOV BX,100 DIV BX ; 除以 100 CMP DX,0 JNZ B2 ; 不能被 100 整除,闰年 MOV AX,CX MOV BX,400 DIV BX ; 除以 400 CMP DX,0 JZ B2 ; 能被 400 整除,闰年 B1: CLC ; CF=0,平年 JMP B3 B2: STC ; CF=1,闰年 B3: POP DX POP CX POP BX POP AX RET PR ENDP

用 CF 传递结果比用寄存器更省事,调用方一个JC就能分支。注意子程序开头把用到的寄存器全压栈,结束时逆序弹出,否则主程序里的值会被改掉。

4.3 蔡勒公式求星期:GETN 子程序与跨年处理

GETN 是整个程序最烧脑的部分。先取年份的世纪数和后两位:

MOV BL,100 MOV AX,YEAR DIV BL ; AL=世纪数, AH=年后两位 MOV C2,AL MOV Y,AH

然后按公式逐项累加。[C/4]用逻辑右移两位实现,2C用左移一位实现:

MOV CL,2 MOV BL,C2 SHR BL,CL ; int(C/4) SHL C2,1 ; 2C SUB BL,C2 ; int(C/4) - 2C ADD BL,Y ; + y SHR Y,CL ; int(y/4) ADD BL,Y ; + int(y/4)

接下来处理月份项[13*(M+1)/5]。这里有个关键分支:如果月份是 1 月或 2 月,要按上一年的 13 月和 14 月算,同时年份后两位要减一。如果后两位是 00,减一后变负数,需要先加 100 再减。

INC MONTH CMP MONTH,3 JC J1 ; 原月份是 1 月或 2 月,跳转特殊处理 CMP MONTH,4 JC J1 ; 3 月到 12 月,正常计算 XOR AH,AH MOV AX,MONTH MOV DX,13 MUL DX MOV CX,5 DIV CX XCHG AX,BX CBW XCHG AX,BX ADD BX,AX

1 月和 2 月的分支里,先把年份后两位减一,再给月份加 12 变成 13 或 14:

J1: MOV BL,100 MOV AX,YEAR DIV BL MOV C2,AL MOV Y,AH CMP Y,1 JNC J2 ADD Y,100 ; 后两位是 00,先加 100 避免负数 J2: SUB Y,1 ; 重新计算 [C/4] - 2C + y + [y/4] ADD MONTH,12 ; 再算 [13*(M+1)/5]

最后对 7 取模,余数为 0 时加 7 保证结果在 1 到 7 之间,再存入 N。N 的值对应关系是:0 表示星期日,1 到 6 表示星期一到星期六。

提示:调试 GETN 时,用几个已知日期验证——2009 年 1 月 1 日是星期四,2000 年 1 月 1 日是星期六。如果这两个算不对,先检查跨年分支里的年份减一逻辑。

4.4 日历排版:光标定位与一位数对齐

排版的核心是算出每月 1 号的光标位置。N 值决定列偏移,每个日期占 5 个字符宽度(含空格),所以列坐标是N * 5。行号从星期标注行的下一行开始。

MOV SI,OFFSET BUF3 ; 日期字符串表 MOV AL,5 MUL N ; 列偏移 = N * 5 MOV B,AX CMP B,35 ; 1 号是星期日时特殊处理 JC POS SUB ROW,1 POS: MOV DH,ROW MOV DL,BYTE PTR B CALL GBIAO

日期字符串表 BUF3 里每个日期都预格式化成两位宽度,比如' 1'、'10',这样一位数和两位数在屏幕上自然对齐,不用在运行时判断补空格。每个日期字符串后跟0AH,0DH实现换行,但实际显示时靠光标定位控制,换行符主要保证字符串独立。

输出循环里,每显示一个日期,SI 加 5 指向下一个字符串,B 加 5 更新列坐标,G 减一计数。当列坐标超过 30 时,说明到了星期六列,触发红色填充,然后行号加一、列坐标归零,开始下一行。

NORED: MOV DX,SI CALL NINE ; 显示日期 ADD SI,5 ADD B,5 DEC G JNZ POS

星期六和星期日的红色标记在每次换行时重新填充,保证整列都是红色。这个“逐行填色”的做法比一次性填整个矩形更稳,因为日历行数不固定,一次性填容易盖到空白区。

5. 避坑与常见问题排查:那些调试时最容易翻车的地方

5.1 现象:程序运行后屏幕花屏或乱码

原因通常是字符串末尾漏了$,或者数据段偏移算错导致显示到无关内存。汇编里字符串显示靠$定位结束,漏一个就会一直往后读。

解决:定义字符串时逐个检查末尾$,尤其是 WEEK 和 WRONG 这类长字符串。另外用LEA DX, 标号取偏移,不要手算地址。

5.2 现象:输入正确日期但日历星期整体偏移一天

原因多半是蔡勒公式取模后的修正没做。余数为 0 时如果不加 7,N 会变成 0,而 0 对应星期日,但公式算出的 0 在某些月份实际对应星期六。

解决:取模后统一判断CMP AH,0,等于 0 就ADD AH,7,保证 N 落在 1 到 7 之间,再映射到列偏移。

5.3 现象:1 月和 2 月星期算错,其他月正常

原因是没有处理跨年。蔡勒公式要求 1 月按上一年 13 月、2 月按上一年 14 月计算,年份后两位要减一。如果后两位是 00,减一后变 -1,除法会出错。

解决:在 J1 分支里先判断 Y 是否小于 1,是就先加 100 再减一。这个细节在原始代码里已经处理,但移植时容易漏掉。

5.4 现象:红色标记盖住日期或串到相邻列

原因是 INT 10H 06H 功能的矩形坐标算错。CL 和 DL 是列号,CH 和 DH 是行号,顺序写反或列宽算错都会导致填色区域偏移。

解决:填色前先用 GBIAO 把光标移到目标位置,观察实际列号,再反推 CL 和 DL 的值。星期六列大约在 30 到 33,星期日列在 3 到 5,具体值取决于日期字符串宽度。

5.5 现象:子程序单独运行正常,集成后调用失败

原因是寄存器冲突或数据被覆盖。汇编里子程序共用寄存器,如果主程序在调用前把值放在 AX 里,子程序内部又改了 AX,返回后主程序拿到的就是错的值。

解决:每个子程序开头把用到的寄存器压栈,结束时弹出。调用前明确哪些寄存器传参、哪些寄存器返回结果,不要依赖“碰巧没被改”的寄存器。

6. 验收与答辩:怎么证明这套日历真的对

验收不是“能跑就行”,而是拿边界数据逐条打。我一般会准备一组测试用例,覆盖闰年、跨世纪、月初月末、非法输入四类场景:

测试输入预期结果验证点
200901011 月 1 日是星期四蔡勒公式基本正确性
200002012 月有 29 天闰年判断(能被 400 整除)
190002012 月有 28 天闰年判断(能被 100 不整除 400)
20091301提示错误并重输月份上限校验
20090230提示错误并重输日期上限校验
20090100提示错误并重输日期下限校验
200001011 月 1 日是星期六跨世纪 + 1 月跨年处理

答辩时导师最爱追问三个点:蔡勒公式里 1 月和 2 月为什么要按 13、14 月算;闰年判断为什么先除 4 再除 100 再除 400;红色标记是怎么实现的。前两个能讲清数学依据,第三个能说清 INT 10H 06H 功能的入口参数含义,基本就稳了。

演示时建议按这个顺序走:先输入一个普通月份展示完整日历,再输入 20000201 展示闰年 2 月,然后输入 20091301 展示错误处理,最后输入 20000101 展示跨世纪场景。每一步都主动说出“这一步在验证什么”,把答辩变成引导式讲解,比被动等提问从容得多。

从那以后我每次做课设,都会先把边界测试用例列出来,写完一个模块就跑一遍,而不是等全部写完再联调。这套万年历的完整源码、流程图和测试数据都在资源里,照着结构改一改就能交出一份能讲清原理的课设。希望帮到你。

本文还有配套的精品资源,点击获取