作为微机原理终极大作业的一部分,个人感觉这个模块是最难的,花了我一个晚上+一个上午。因为有计时中断,也没办法开调试,Debug难度上了一个新高度,而且有时代码一点没动,运行的结果还不一样,甚至会闪退卡死,就很迷,好在最后还是被我碰出来了。
题目要求如下:
输入时分秒处理好做,输出也好做,难点在于计时和定点显示,还有返回重新对时。
使用8253芯片做计时
首先先关闭计时器中断
然后设置中断向量,将08H中断服务地址更改为我们自己的代码段INT08H,该代码段是中断处理
对计时器分频10ms,设置控制字。注意需要在中断处理处再次分频100,才能做到1s一显示。因为AX最大65536,没法直接分频到1s
最后在收完键盘数据和显示完提示字符串之后,开启中断,自动计时显示
其中,DX是中断内部计时数,100分频。
参考文章:点这里
定点显示
使用BIOS显示服务中断10H控制光标位置,在定时器中断代码段INT08H中显示当前时间。
在输出字符前线记录光标信息入栈中:
在显示完数字后,设置光标位置回去
注意MOV BH,0这个代码必须要写上,之前参考一篇博客没有这个代码,我实际运行时光标根本没有移动,这个bug找了两个小时。
当然,每次显示完一次之后要对秒数加1,然后判断进位,相对简单,不再赘述。
键盘判断返回重新对时
这块是最迷的,到现在我也没搞明白,代码10分钟敲完,却Debug了一上午。
我的想法:AH=0BH的DOS中断检测键盘状态,若无输入则循环,若有输入,使用AH=08H的DOS中断检测键盘回显输入,判断是否为ESC,若是则跳出,若不是则重新对时。
现实:直接重新对时。
前面写的检测逻辑代码根本没用!
折腾了几个小时,没办法开调试模式-t,最后我试着把检测到的字符直接输出,发现是个ASCII码为252的一个字符,导致键盘什么操作也没有,系统却认为我按下键了。
就是红圈里的这个坏小子,甚至不是标准ASCII码
BUG找到了,写两行代码直接拍死:
如果是这个奇怪的字符,就接着循环等待键盘输入,如果不是,说明用户按下键盘了,就按照正常逻辑处理。
最终代码如下:
DATAS SEGMENT
SINPUT DB 'Please enter hours, minutes, and seconds with spaces: $'
SOUTPUT DB 'Any key to again and ESC to return. Now time is : $'
BUF1 DB 20H
DB 0
DB 20H DUP(0)
TIMEHOUR DW 23
TIMEMINUTE DW 56
TIMESECOND DW 45
NUM DW ?
DATAS ENDS
STACKS SEGMENT PARA STACK
DW 30H DUP(0)
STACKS ENDS
CODES SEGMENT
ASSUME CS:CODES,DS:DATAS,SS:STACKS
START:
MOV AX,DATAS
MOV DS,AX
MOV AL,11111101B;关定时器中断
OUT 21H,AL;中断屏蔽寄存器数据传回去
PUSH DS;保存数据段地址
MOV AX,SEG INT08H;SEG取得标号的段地址
MOV DS,AX;获得08H号中断的段地址,放在中断向量中
MOV DX, OFFSET INT08H;获得08H号中断的偏移地址,放在中断向量中
MOV AL,08H;设置中断号
MOV AH,25h;设置中断向量
int 21h;调用系统dos中断
POP DS;恢复数据段地址
MOV AL,00110110B;设置通道0的方式3
OUT 43H,AL;输出控制字,43H是8253定时器芯片的控制寄存器地址
MOV AX,11932;定时器的时钟频率为1.1931817MHz,计数初值=1193182/100,10ms中断一次
OUT 40H,AL;40H为计数器地址
MOV AL,AH;先输出低位,再输出高位
OUT 40H,AL;40H为计数器地址
TIMING:
;换行
MOV DL,0DH;CR
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
MOV DL,0AH;LF
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
MOV AH,09H;显示字符串
LEA DX,SINPUT;取段内偏移地址
INT 21H;调用系统dos中断
MOV AH,0AH;键盘输入到缓冲区
LEA DX,BUF1;取段内偏移地址
INT 21H;调用系统dos中断
CALL INPUTTIME;调用子程序处理字符串
MOV AH,09H;显示字符串
LEA DX,SOUTPUT;取段内偏移地址
INT 21H;调用系统dos中断
MOV DX,100;初始化为0
MOV AL,11111100B;开键盘和定时器中断
OUT 21H,AL;中断屏蔽寄存器数据传回去
RE:
MOV AH,08H;无回显键盘输入到AL
INT 21H;调用系统dos中断
;MOV AH,0;
;MOV NUM,AX;
;MOV AH,03H;获取光标位置信息
;INT 10H;BIOS中断
;ADD DL,10;
;MOV BH,0;显示到第一页
;MOV AH,02H;设置光标位置
;INT 10H;BIOS中断
;CALL SHOWNUM
CMP AL,27;按键是否ESC
JE DO_ESC;是退出
CMP AL,252;按键是否是奇怪的那个字符
JE RE;是奇怪的,循环
;MOV AH,0BH;检测键盘状态,有输入AL=00,无输入AL=FF
;INT 21H;调用系统dos中断
;CMP AL,0FFH;判断按键无输入
;JE RE;确实无输入,重复
MOV AL,11111101B;关定时器中断
OUT 21H,AL;中断屏蔽寄存器数据传回去
JMP TIMING;不是就重新对时
DO_ESC:
MOV AL,11111101B;关定时器中断
OUT 21H,AL;中断屏蔽寄存器数据传回去
;退出代码
MOV AH,4CH
INT 21H
;这是一段子程序,用来输入数字
INPUTTIME PROC
;初始化
MOV DX,0
MOV BX,10
MOV SI,2
MOV NUM,0
MOV AX,0
MOV CX,0;
LOP:
MOV AL,BUF1[SI];寄存器相对寻址,从缓冲区取一个字符
CMP AL,0DH;是否是CR
JE FINAL;等于就跳转,JNE相反
SUB AL,30H;减48,从ASCII码转数字
CMP NUM,0;与0比较,相当于判断初始化
JE DO_DEAL;等于就跳转,JNE相反
PUSH AX;当前数字压入栈中
MOV AX,NUM;当前数送入运算寄存器中
MUL BX ;隐含寻址,在AX中,相当于NUM乘以10
MOV NUM,AX;运算结果存进NUM中
POP AX;之前的数据弹出
DO_DEAL:
ADD NUM,AX;加上之前的数据
MOV AX,0;清零
INC SI;自加1
MOV AL,BUF1[SI];寄存器相对寻址,从缓冲区取一个字符
CMP AL,' ';是否是空格
JE DO_SPACE;等于就跳转,JNE相反
JMP LOP;跳转,处理下一个
DO_SPACE:
INC SI;偏移量再自加1,跳过空格
MOV DX,NUM;把NUM存起来
MOV NUM,0;原先数清零
INC CX;标志自加1
CMP CX,1;与1比较,判断是不是时
JE HOUR;等于就跳转,JNE相反
CMP CX,2;与2比较,判断是不是分
JE MINUTE;等于就跳转,JNE相反
JMP LOP;跳转,处理下一个
HOUR:
MOV TIMEHOUR,DX;把时传送到变量
JMP LOP;跳转,处理下一个
MINUTE:
MOV TIMEMINUTE,DX;把分钟传送到变量
JMP LOP;跳转,处理下一个
FINAL:
MOV DX,NUM;把NUM存起来
MOV TIMESECOND,DX;把秒传送到变量
;换行
MOV DL,0DH;CR
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
MOV DL,0AH;LF
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
RET;子程序退出重置堆栈
INPUTTIME ENDP
;这是一段子程序,用来显示数字NUM
SHOWNUM PROC
;其实正常这里显示不到第一位,懒得改了
MOV BL,100;%先除100
MOV AX,NUM;送入运算寄存器
DIV BL;除法,AX隐含寻址
ADD AL,30H;商转换为ASCII码
PUSH AX;将余数先压入栈中
CMP AL,30H;看一看首位是不是0
JE BITTWO;若是,则不显示直接跳转到下一个
MOV DL,AL;传送字符
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
BITTWO:
POP AX;将余数取出
;第二位
MOV AL,AH;送入运算寄存器
MOV AH,0;高位清零
MOV BL,10;%再除以10
DIV BL;除法,AX隐含寻址
ADD AL,30H;商转换为ASCII码
PUSH AX;将余数先压入栈中
MOV DL,AL;传送字符
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
POP AX;将余数取出
;第三位
ADD AH,30H;余数转换为ASCII码
MOV DL,AH;传送字符
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
RET;子程序退出重置堆栈
SHOWNUM ENDP
INT08H PROC NEAR;08H计数器中断
;保护现场
PUSH AX
PUSH BX
PUSH CX
CMP DX,100;;计数达到100,执行一次,相当于1s一次
JGE COUNT;大于等于100就跳转
JMP ENDINT;没达到就结束中断响应
COUNT:
MOV DX,0;计数值清零
PUSH DX;保存计数值
MOV AH,03H;获取光标位置信息
INT 10H;BIOS中断
PUSH DX;将获得的信息压入栈中
MOV AX,TIMEHOUR;传送指令
MOV NUM,AX;传送指令
CALL SHOWNUM;调用子程序显示数字
MOV DL,':'
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
MOV AX,TIMEMINUTE;传送指令
MOV NUM,AX;传送指令
CALL SHOWNUM;调用子程序显示数字
MOV DL,':'
MOV AH,2;显示一个字符
INT 21H;调用系统dos中断
MOV AX,TIMESECOND;传送指令
MOV NUM,AX;传送指令
INC AX;秒数自加1
MOV TIMESECOND,AX;传送指令
CALL SHOWNUM;调用子程序显示数字
POP DX;将之前的光标信息从栈中取出
MOV BH,0;显示到第一页
MOV AH,02H;设置光标位置
INT 10H;BIOS中断
POP DX;
;进位处理
CMP TIMESECOND,60;大于等于60,进一位
JGE SECONDCARRY;秒数进位
JMP ENDINT;中断返回
SECONDCARRY:
MOV TIMESECOND,0;清零
INC TIMEMINUTE;分钟自加一
CMP TIMEMINUTE,60;大于等于60,进一位
JGE MINUTECARRY;分钟进位
JMP ENDINT;中断返回
MINUTECARRY:
MOV TIMEMINUTE,0;清零
INC TIMEHOUR;小时自加一
CMP TIMEHOUR,24;大于等于24,进一位
JGE HOURCARRY;小时进位
JMP ENDINT;中断返回
HOURCARRY:
MOV TIMEHOUR,0;清零
JMP ENDINT;中断返回
ENDINT:
;中断返回
MOV AL,20H;
OUT 20H,AL;发送中断结束指令
;恢复现场
POP AX
POP BX
POP CX
INC DX;计数加一
IRET;;中断返回
INT08H ENDP
CODES ENDS
END START