目录
0. 前文概述
前面两篇文章中我们编写了一个可以自行启动计算机,不需要在现有操作系统环境中运行的程序。
第一部分我们实现了程序在虚拟模式DOS下的任务逻辑部分:
第二部分我们实现了程序在实模式DOS下的安装实测部分:
关于课设2的任务内容,我们已经全部完成,接下来我们来对这个程序的界面方面做一些优化。
这一部分的内容不涉及软硬盘操作,因此没有要求在实模式下进行
1. 子界面文字说明
让我们给子功能界面加上标题与按键说明。
1.1. 时钟显示
show_clock_title: db "Current time is:"
show_clock_func_str: db "'F1' to change color,'Esc' to return to main menu."
show_clock: ;时间显示
我们通过地址标号相减来计算字符串长度cx,将显示行数赋给ah,而后调用 mid_showstr:
show_clock: ;时间显示
;寄存器压栈
mov si,offset show_clock_title
mov cx,offset show_clock_func_str-offset show_clock_title
mov ah,10 ;显示在第10行
call mid_showstr
mov si,offset show_clock_func_str
mov cx,offset show_clock-offset show_clock_func_str
mov ah,16 ;显示在第16行
call mid_showstr
show_clock_run: ;其它需要实时更新的操作,如:时间字符串显示,键入响应
......
效果展示:
如果希望标题与按键说明字符串也能实时发生颜色变化,可以将这两个字符串的显示操作放进循环运行的代码块当中:
......
show_clock_run: ;循环运行代码块,标题与按键说明字符串也进行实时更新
mov si,offset show_clock_title
mov cx,offset show_clock_func_str-offset show_clock_title
mov ah,10 ;显示在第10行
call mid_showstr
mov si,offset show_clock_func_str
mov cx,offset show_clock-offset show_clock_func_str
mov ah,16 ;显示在第16行
call mid_showstr
;显示时钟字符串、按键响应
......
效果展示:
1.2. 时钟设置
与上一个子程序的修改方法类似:
需注意字符串的长度不要算错
set_time_title: db "Input number string to modify the clock:"
set_time_func_str: db "'Backspace' to delete char, 'Enter' to finish modification."
set_time: ;设置时间
push ax
push cx
push si
mov si,offset set_time_title
mov cx,offset set_time_func_str-offset set_time_title
mov ah,10 ;显示在第10行
call mid_showstr
mov si,offset set_time_func_str
mov cx,offset set_time-offset set_time_func_str
mov ah,16 ;显示在第16行
call mid_showstr
......
效果展示:
2. 光标处理
2.1. 字符串输入光标
我们发现时钟设置程序中输入字符串时没有光标提示,观感不是很好,因此我们来添加一个输入字符串末实时显示光标功能。
这里我们使用 int 10H中断例程的2号功能 置光标功能
参数:bh放页数,dh放行号,dl放列号
由于字符串在输入过程中长度不断改变,想要在字符串末实时显示光标,则需要获取字符串末的位置信息。我们可以想到,程序中的所有字符串显示操作都调用了 mid_showstr 子功能来完成,所以最容易获取字符串位置信息的方法应该是依靠这个子程序,那么我们就可以通过该子程序返回字符串末的位置信息来使用。
现在来为 mid_showstr 子程序添加一点逻辑:
;参数:行数ah,字符串首地址为 ds:si,长度为cx,最大为80,dh传入颜色属性
;返回值:ah返回行数,al返回字符串后一格列数,方便置光标
mid_showstr: ;字符串居行中显示
push cx
push si
push di
⭐ push ax
mov al,160
mul ah ;行数
mov di,80
sub di,cx
add di,ax ;根据字符串长度计算显示位置
shr di,1
shl di,1 ;对齐偶数单元地址
cld
mov byte ptr es:[di-2],' ' ;将字符串前一格置空格,至少保证执行一次,以清除最后一个字符
jcxz short showstr_ret ;如果长度为0就不用显示
showstr_s:
movsb
mov es:[di],dh
inc di
loop showstr_s
mov byte ptr es:[di],' ' ;将字符串后一格置空格
showstr_ret:
⭐⭐
sub di,ax ;减去行数*160的偏移量
shr di,1 ;/2计算当前di单元格所在列数
⭐ pop ax ;恢复行数ah
sub al,al
add ax,di ;al保存列数
⭐⭐
pop di
pop si
pop cx
ret
标注⭐的代码为修改部分,重点关注此处逻辑
可以想象,为了利用mid_showstr的返回值,我们会将置光标操作紧跟着mid_showstr使用:
call mid_showstr ;打印字符串
;利用返回值坐标(ah,al)进行置光标操作
在set_time中执行打印字符串的操作后面添加上这一步:
set_time: ;设置时间
......
mov ah,12 ;第12行
call mid_showstr ;打印字符串
;利用返回值坐标(ah,al)赋值dh,dl置光标
push dx ;dh还保存着颜色属性
mov dx,ax
mov ah,2
int 10H
pop dx
......
注意:dh保存着全局颜色属性,修改后要及时恢复
2.2. 新的问题
测试一下。子程序中,光标能够显示在输入字符串末尾,这下合理多了^_^:
但是!这里又产生了两个新的问题:
- 当字符串输入到达最大长度时,光标还是显示在字符串的后面一格,我们却不能输入了;而我希望遇到这种情况时,光标能显示在最后一个字符的位置(光标前面那个'3'),表示已达最大输入
这当然不是什么大问题,但是我觉得 报看>_<
这个解决方式很简单,加上一个最大长度判断条件即可:
mov ah,12 ;显示在第12行
call mid_showstr ;打印字符串
cmp cx,12 ;判断是否到达最大长度
jne short set_cursor
dec al ;此时al保存着将要设置光标的列数
set_cursor:
;利用返回值坐标(ah,al)赋值dh,dl置光标
push dx ;dh还保存着全局颜色属性
mov dx,ax
mov ah,2
int 10H
pop dx
嗯,顺眼多了~
另一个问题就是:
- 当字符串输入完毕返回主菜单时,发现光标竟然跑到天上来了
原本好好待在角落里倒还可以选择性忽略,但是现在这样……嗯,不能忍
让我们在每次显示主菜单的时候把光标扔到屏幕外边去:
main_menu: ;主菜单
call screen_clear ;清屏
call menu_show ;显示菜单
;将光标置于屏幕外
push dx
mov dx,0ffffH ;超过最大行列数即可
mov ah,2
int 10H
pop dx
......
好了,这下再看主菜单终于清爽多了~
然而,在虚拟机刚开机时还是无法避免光标的显示,这可能是CPU开机处理的流程原因,不过至少执行操作以后光标都能正常隐藏,因此就别在意这个小细节啦~
3. 完整代码
附上完整代码:
根据测试目的设置⭐处指令
assume cs:code
code segment
start:
⭐ ;jmp main ;直接测试任务程序就使用跳转指令,在实模式上执行安装操作可以忽略这条指令
mov bx,cs
mov es,bx
mov bx,offset lead ;将引导程序写入软盘0道0面1扇区
mov al,1 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,1 ;扇区号
int 13H
mov bx,offset main ;将主程序写入软盘0道0面2扇区开始的2个扇区
mov al,2 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
mov ax,4c00H
int 21H
lead: ;引导程序,被保存在软盘0道0面1扇区,由操作系统加载到 0:7c00H 处,负责被加载后从0道0面2扇区开始的2个扇区加载主程序
sub bx,bx
mov ss,bx
mov sp,7f00H ;0:7e00H到0:7f00H是安全的栈空间
push cs
pop es
mov bx,7f00H ;将主程序加载到 0:7f00H 处
mov al,2 ;操作扇区数量
mov ah,2 ;读取操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
sub bx,bx
push bx
mov bx,7f00H
push bx
retf ;跳转到 0:7f00H 处开始执行主程序
org 7f00H ;防止数据标号错乱
main: ;主程序,被保存在软盘0道0面2扇区开始的2个扇区,由引导程序加载到 0:7f00H
push cs
pop ds ;数据标号是按代码段计算的,将数据段与代码段对齐
mov ax,0b800H
mov es,ax ;es固定指向显存段
mov dh,70h ;白底黑字
jmp short main_menu
table dw reset_pc,start_system,show_clock,set_time ;子程序入口定址表
main_menu: ;主菜单
call screen_clear ;清屏
call menu_show ;显示菜单
;将光标置于屏幕外
push dx
mov dx,0ffffH ;超过最大行列数即可
mov ah,2
int 10H
pop dx
sub ah,ah
int 16H ;等待输入
;ah返回键盘扫描码,al返回字符ASCII码
cmp ah,1 ;Esc的扫描码
je sret ;为了方便测试,我们令 输入Esc结束程序
cmp al,'1'
jb short main_menu ;无效输入
cmp al,'4'
ja short main_menu ;无效输入
sub bh,bh
mov bl,al
sub bl,'1' ;直接从字符'1'的ASCII码上计算偏移量
shl bx,1
call screen_clear ;跳转子程序前先清屏
call word ptr table[bx] ;选择跳转子程序
jmp short main_menu ;子程序结束回到主菜单选择
sret:
mov ax,4c00H
int 21H
reset_pc: ;重启
mov bx,0ffffH
push bx
sub bx,bx
push bx
retf ;跳转 ffff:0000H
start_system: ;引导现有操作系统
sub bx,bx
mov es,bx
mov bx,7c00H
mov al,1 ;操作扇区数量
mov ah,2 ;读取操作
mov dl,80H ;驱动器号 硬盘C
mov dh,0 ;面号 0
mov ch,0 ;磁道号 0
mov cl,1 ;扇区号 1
int 13H
sub bx,bx
push bx
mov bx,7c00H
push bx
retf ;跳转 0000:7c00H
timestr: db 0,0,'/',0,0,'/',0,0,' ',0,0,':',0,0,':',0,0 ;时间字符串
CMOS_adr db 9,8,7,4,2,0 ;端口地址定址表
show_clock_title: db "Current time is:"
show_clock_func_str: db "'F1' to change color, 'Esc' to return to main menu."
show_clock: ;时间显示
push ax
push bx
push cx
push dx
push si
mov si,offset show_clock_title
mov cx,offset show_clock_func_str-offset show_clock_title
mov ah,10 ;显示在第10行
call mid_showstr
mov si,offset show_clock_func_str
mov cx,offset show_clock-offset show_clock_func_str
mov ah,16 ;显示在第16行
call mid_showstr
show_clock_run: ;将压栈部分放在重复运行段外面
sub bx,bx ;作为端口地址表的偏移量
mov si,offset timestr ;这里使用地址标号是因为si不是字节类型
mov cx,6
get_CMOS_s:
mov al,CMOS_adr[bx]
call get_CMOS ;在 ds:si 处写入CMOS在al地址处的数据
inc bx
add si,3
loop get_CMOS_s
mov si,offset timestr ;重新指向字符串开头
mov ah,12 ;显示在第12行
mov cx,17 ;时间字符串长度为17字节
call mid_showstr ;居中显示
mov ah,1
int 16H ;int 16H 的 1 号功能:用来查询键盘缓冲区,对键盘扫描但不等待,并设置 ZF 标志位(0有输入,1无输入)
je short show_clock_run ;无键盘输入,继续时钟模式
sub ah,ah
int 16H ;当且仅当有键盘输入,使用 int 16H 的 0 号功能获取输入信息
cmp ah,1 ;Esc的扫描码
je short show_clock_ret ;当输入为Esc,退出时钟模式
cmp ah,3bH ;F1的扫描码
;cmp ah,1cH ;Enter的扫描码
jne short show_clock_run ;其它为无效输入,不进行操作,继续时钟模式
inc dh ;dh保存显示属性,当输入为F1,改变显示属性
jmp short show_clock_run ;循环执行读取CMOS与显示
show_clock_ret:
pop si
pop dx
pop cx
pop bx
pop ax
ret
;参数:al传端口地址,ds:si 传写入地址
get_CMOS: ;从端口获取时间数据并写入 ds:si 处
out 70H,al ;操作单元地址送入地址端口70H
in al,71H ;将该单元从数据端口71H中读取到al
mov ah,al
and ah,1111b ;ah存时间数据低位(个位)
shr al,1
shr al,1
shr al,1
shr al,1 ;al存时间数据高位(十位)
add ax,3030H ;转化为ASCII码
mov [si],ax
ret
clock_setstr: db 12 dup('0') ;时间设置字符串
set_time_title: db "Input number string to modify the clock:"
set_time_func_str: db "'Backspace' to delete char, 'Enter' to finish modification."
set_time: ;设置时间
push ax
push cx
push si
mov si,offset set_time_title
mov cx,offset set_time_func_str-offset set_time_title
mov ah,10 ;显示在第10行
call mid_showstr
mov si,offset set_time_func_str
mov cx,offset set_time-offset set_time_func_str
mov ah,16 ;显示在第16行
call mid_showstr
mov si,offset clock_setstr ;指向时间设置字符串
sub cx,cx ;字符串初始长度为0
input_char: ;输入字符
mov ah,12 ;显示在第12行
call mid_showstr ;打印字符串
cmp cx,12 ;判断是否到达最大长度
jne short set_cursor
dec al ;此时al保存着将要设置光标的列数
set_cursor:
;利用返回值坐标(ah,al)赋值dh,dl置光标
push dx ;dh还保存着全局颜色属性
mov dx,ax
mov ah,2
int 10H
pop dx
sub ah,ah
int 16H ;监听键盘输入
cmp al,'0'
jb short not_digit
cmp al,'9'
ja short not_digit ;判断是否为数字
call char_push ;添加字符并显示
jmp short input_char ;下一轮输入
not_digit:
cmp ah,0eH ;撤回键
je short backspace
cmp ah,1cH ;回车键
je short enter
jmp short input_char ;非法键,下一轮输入
backspace:
call char_pop ;撤销字符并显示
jmp short input_char ;下一轮输入
enter: ;结束字符串输入,修改CMOS并返回主菜单
push bx
sub bx,bx ;bx记录 CMOS_adr 偏移量
mov cx,6
set_CMOS_s:
push bx
mov al,CMOS_adr[bx] ;可以复用 CMOS_adr 数据标号
mov bx,[si]
call set_CMOS ;在CMOS的al地址处写入bx数据
pop bx
inc bx
add si,2
loop set_CMOS_s
pop bx
pop si
pop cx
pop ax
ret
;bx传时间ASCII码(bl存十位,bh存个位),al传端口号
set_CMOS: ;修改CMOS
push bx
out 70H,al
sub bx,3030H ;ASCII码转BCD码
shl bl,1
shl bl,1
shl bl,1
shl bl,1 ;bl高位存十位
or bl,bh ;bl低位存个位
mov al,bl
out 71H,al ;通过数据端口写入CMOS
pop bx
ret
;参数:al入栈字符,字符串长度首地址 ds:si,长度 cx
;返回值:字符串新长度 cx
char_push: ;字符入栈,指针后移,最多12位
cmp cx,12
je short char_push_ret ;最多12位
push bx
mov bx,cx
mov [bx][si],al ;寻找当前指针所指向字符
inc cx
pop bx
char_push_ret:
ret
;字符串长度首地址 ds:si,长度 cx
;返回值:al返回出栈字符,字符串新长度 cx
char_pop: ;字符出栈,指针前移,至少0位
cmp cx,0
je short char_pop_ret ;至少0位
push bx
mov bx,cx
mov al,[bx][si] ;寻找当前指针所指向字符
dec cx
pop bx
char_pop_ret:
ret
;参数:dh传入颜色属性
screen_clear: ;清屏
push bx
push cx
push dx
sub bx,bx
mov dl,' ' ;dl 传入字符' ',dh 传入颜色属性
mov cx,2000
clears:
mov es:[bx],dx
add bx,2
loop clears
pop dx
pop cx
pop bx
ret
;参数:行数ah,字符串首地址为 ds:si,长度为cx,最大为80,dh传入颜色属性
;返回值:ah返回行数,al返回字符串后一格列数,方便置光标
mid_showstr: ;字符串居行中显示
push cx
push si
push di
push ax
mov al,160
mul ah ;行数
mov di,80
sub di,cx
add di,ax ;根据字符串长度计算显示位置
shr di,1
shl di,1 ;对齐偶数单元地址
cld
mov byte ptr es:[di-2],' ' ;将字符串前一格置空格,至少保证执行一次,以清除最后一个字符
jcxz short showstr_ret ;如果长度为0就不用显示
showstr_s:
movsb
mov es:[di],dh
inc di
loop showstr_s
mov byte ptr es:[di],' ' ;将字符串后一格置空格
showstr_ret:
sub di,ax ;减去行数*160的偏移量
shr di,1 ;/2计算当前di单元格所在列数
pop ax ;恢复行数ah
sub al,al
add ax,di ;al保存列数
pop di
pop si
pop cx
ret
line1 db "Press (1) - RESET PC"
line2 db "Press (2) - START SYSTEM"
line3 db "Press (3) - SHOW CLOCK"
line4 db "Press (4) - SET TIME"
lines dw line1,line2,line3,line4
lengths db line2-line1,line3-line2,line4-line3,lines-line4
;参数:通过dh传入字符颜色属性
menu_show: ;主菜单显示
push ax
push cx
push si
mov ah,15 ;将四个字符串分别显示到9、11、13、15行
mov cx,4
menu_show_s:
push cx ;调用 mid_showstr 前会修改cl,先把循环次数保存起来
mov si,cx
dec si ;通过循环次数计算偏移量
mov cl,lengths[si] ;lengths为字节型数据组,使用cl保存长度
shl si,1 ;注意数据类型,偏移量乘2
mov si,lines[si] ;lines为字型数据组,使用si保存字符串首地址
call mid_showstr
sub ah,2 ;自下而上
pop cx
loop menu_show_s
pop si
pop cx
pop ax
ret
code ends
end start
4. 总结
这一篇文章的内容不是很多,仅作拓展选看用。
我们在之前任务程序的基础上优化了界面以及光标显示,总体来说我们的程序已经具备符合课设2任务要求的功能和较为清晰的使用界面,倘若还有其它改进想法就靠各位自己去发挥了(比如觉得白板界面太空旷,可以自己添加边框等等)。
这也是关于《汇编语言》课程设计2 系列的最后一篇文章,能将该课题做到如此完成度已经达到我自己的满意水平。在此过程中我追求代码简洁的同时尽量保证不失规范性,因而做了多次调整,如果发现前后代码不完全一致可能是因为没来得及同步所致,一切以本文的完整代码为准。
参考材料:《汇编语言》(第4版)王爽
标签:课程设计,汇编语言,ah,mov,al,cx,push,bx,王爽 From: https://blog.csdn.net/m0_73327328/article/details/136932599