PART 1 >> 使用BIOS中断实现键盘输入的读取和显示
; File: c09_2.asm
; Date: 20191222
; ===============================================================================
SECTION head vstart=0 ; 定义用户程序头部段
; 用户程序可能很大,16位可能不够
program_length dd program_end ; 程序总长度[0x00]
; 程序入口点(Entry Point)
program_entry dw beginning ; 偏移地址[0x04]
; 只是编译阶段确定的汇编地址。程序加载到内存后,需要根据加载的实际位置重新计算
; 尽管在16位的环境中,一个段最长为64KB,但它却可以起始于任何20位的物理地址处。
; 不可能用16位来保存20位的地址,所以需要32位
dd section.code.start ; 汇编地址[0x06]
realloc_tbl_size dw (head_end-segment_code_1)/4 ; 段重定位表项个数[0x0a]
segment_code_1 dd section.code.start ; [0x0c]
segment_data_1 dd section.data.start ; [0x10]
segment_stack dd section.stack.start ; [0x14] ; 这里section 和 start 不能用大写 ???
head_end:
; ===============================================================================
SECTION code align=16 vstart=0
beginning:
; 设置用户程序自己的堆栈段
; ds和es依然指向着用户程序头部head段
mov ax, [segment_stack]
mov ss, ax
mov sp, stack_end
; 设置用户程序自己的数据段
; 如果先初始化数据段ds和附加段es,那么头部head段中的数据将无法访问
mov ax, [segment_data_1]
mov ds, ax
mov cx, msg_end-message
mov bx, message
.show_char:
; 在频幕上写字符
; 中断0x10的0x0e号功能。在屏幕光标位置处写一个字符,并推进光标位置。
; Input: al, 字符
mov ah, 0x0e ; ah中指定0x0e号功能
mov al, [bx]
int 0x10
inc bx
loop .show_char
.rw_keyboard:
; 从键盘读字符
; 中断0x10的0x00号功能。
; Output: al, 字符
mov ah, 0x00 ; ah中指定0x00号功能
int 0x16
mov ah, 0x0e
mov bl, 0x07
int 0x10
jmp .rw_keyboard
; ===============================================================================
SECTION data align=16 vstart=0
message db 'Hello, friend!',0x0d,0x0a
db 'This simple procedure used to demonstrate '
db 'the BIOS interrupt.',0x0d,0x0a
db 'Please press the keys on the keyboard ->'
msg_end:
; ===============================================================================
SECTION stack align=16 vstart=0
resb 256
stack_end:
; ===============================================================================
SECTION program_tail
program_end:
===================================================================================================
PART 2 >> 使用RTC芯片实现实时时间的显示
; FILE: c09_1.asm
; DATE: 20191211
; ===============================================================================
SECTION head vstart=0 ; 定义用户程序头部段
; 用户程序可能很大,16位可能不够
program_length dd program_end ; 程序总长度[0x00]
; 程序入口点(Entry Point)
program_entry dw beginning ; 偏移地址[0x04]
; 只是编译阶段确定的汇编地址。程序加载到内存后,需要根据加载的实际位置重新计算
; 尽管在16位的环境中,一个段最长为64KB,但它却可以起始于任何20位的物理地址处。
; 不可能用16位来保存20位的地址,所以需要32位
dd section.code.start ; 汇编地址[0x06]
realloc_tbl_size dw (head_end-segment_code_1)/4 ; 段重定位表项个数[0x0a]
segment_code_1 dd section.code.start ; [0x0c]
segment_data_1 dd section.data.start ; [0x10]
segment_stack dd section.stack.start ; [0x14] ; 这里section 和 start 不能用大写 ???
head_end:
; ===============================================================================
SECTION code align=16 vstart=0
beginning:
; 设置用户程序自己的堆栈段
; ds和es依然指向着用户程序头部head段
mov ax, [segment_stack]
mov ss, ax
mov sp, stack_end
; 设置用户程序自己的数据段
; 如果先初始化数据段ds和附加段es,那么头部head段中的数据将无法访问
mov ax, [segment_data_1]
mov ds, ax
mov bx, msg_init ; 显示初识信息
call show_string
mov bx, msg_install ; 显示安装信息
call show_string
; 计算RTC芯片中断处理过程的段地址和偏移地址
; RTC芯片的中断信号,通向中断控制器8259从片的第1个中断引脚IR0。
; 计算机启动期间,BIOS会初始化中断控制器8259,将主片的中断号设为从0x08开始,从片的中断号从0x70开始。
; 所以,计算机启动后,RTC芯片的中断号默认是0x70(可通过对8259编程来修改默认中断号)
xor ax, ax
mov al, 0x70 ; RTC芯片的默认中断号0x70
mov bl, 4 ; 每个中断向量表项占4字节(段地址:偏移地址), 乘4,得中断像量表内的偏移
mul bl
mov bx, ax
cli ; cli 清楚IF标志位,禁止中断,防止改动期间发生新的0x70号中断
; 设置中断向量表0x70号表项内容
; 实模式下,256个中断程序的入口点集中存放在内存0x00000~0x003FF共1KB的空间内,即中断向量表
push es
xor ax, ax
mov es, ax ; 将es指向中断向量表所在的段
mov word [es:bx], my_int_0x70 ; 中断处理过程的偏移地址
mov word [es:bx+2], cs ; 段地址
pop es
; 不懂 ……
; 设置RTC的工作状态,使它能够产生中断信号给8259中断控制器
mov al,0x0b ;RTC寄存器B
or al,0x80 ;阻断NMI
out 0x70,al
mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更
out 0x71,al ;新结束后中断,BCD码,24小时制
mov al,0x0c
out 0x70,al
in al,0x71 ;读RTC寄存器C,复位未决的中断状态
in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,al ;写回此寄存器
sti ; sti 放开中断,与cli相对应
mov bx, msg_done
call show_string ; 显示中断安装完成信息
mov bx, msg_tips
call show_string ; 显示提示信息
; 屏幕中心显示字符@
mov ax, 0xb800
mov ds, ax
mov byte [12*160 + 33*2], '@' ; 25row*80col
; hlt 使处理器处于停机状态,停止执行指令,
; 可以被外部中断唤醒并恢复执行
.idle:
hlt
not byte [12*160 + 33*2 + 1] ; 反转上面@字符的显示属性
jmp .idle
; Function: 频幕上显示文本
; Input: ds:bx 字符串起始地址,以0结尾
show_string:
mov cl, [bx]
or cl, cl
jz .exit
call show_char
inc bx
jmp show_string
.exit:
ret
; Function:
; Input: cl 字符
show_char:
push ax
push bx
push cx
push dx
push ds
push es
; 读取当前光标位置
; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
; 数据端口0x3d5
mov dx, 0x3d4
mov al, 0x0e
out dx, al
mov dx, 0x3d5
in al, dx
mov ah, al
mov dx, 0x3d4
mov al, 0x0f
out dx, al
mov dx, 0x3d5
in al, dx
mov bx, ax ; 此处用bx存放光标位置的16位数
; 判断是否为回车符0x0d
cmp cl, 0x0d ; 0x0d 为回车符
jnz .show_0a ; 不是回车符0x0d,再判断是否换行符0x0a
mov ax, bx ; 是回车符,则将光标置位到行首
mov bl, 80
div bl
mul bl
mov bx, ax
jmp .set_cursor
; ; 将光标位置移到行首,可以直接减去当前行吗??
; mov ax, bx
; mov dl, 80
; div dl
; sub bx, ah
; jmp .set_cursor
; 判断是否为换行符0x0a
.show_0a:
cmp cl, 0x0a ; 0x0a 为换行符
jnz .show_normal; 不是换行符,则正常显示字符
add bx, 80 ; 是换行符,再判断是否需要滚屏
jmp .roll_screen
; 正常显示字符
; 在写入其它内容之前,显存里全是黑底白字的空白字符0x0720,所以可以不重写黑底白字的属性
.show_normal:
mov ax, 0xb800 ; 显存映射在 0xb8000~0xbffff
mov es, ax
shl bx, 1 ; 光标指示字符位置,显存中一个字符占2字节,光标位置乘2得到该字符在显存中得偏移地址
mov [es:bx], cl
shr bx, 1 ; 恢复bx
inc bx ; 将光标推进到下一个位置
; 判断是否需要向上滚动一行屏幕
.roll_screen:
cmp bx, 2000 ; 25行x80列
jl .set_cursor
mov ax, 0xb800
mov ds, ax ; movsw的源地址ds:si
mov es, ax ; movsw的目的地址es:di
mov si, 0xa0
mov di, 0
cld ; 传送方向cls std
mov cx, 1920 ; rep次数 24行*每行80个字符*每个字符加显示属性占2字节 / 一个字为2字节
rep movsw
; 清除屏幕最底一行,即写入黑底白字的空白字符0x0720
mov bx, 3840 ; 24行*每行80个字符*每个字符加显示属性占2字节
mov cx, 80
.cls:
mov word [es:bx], 0x0720
add bx, 2
loop .cls
mov bx, 1920 ; 重置光标位置为最底一行行首
; 根据bx重置光标位置
; 索引寄存器端口0x3d4,其索引值14(0x0e)和15(0x0f)分别用于提供光标位置的高和低8位
; 数据端口0x3d5
.set_cursor:
mov dx, 0x3d4
mov al, 0x0e
out dx, al
mov dx, 0x3d5
mov al, bh ; in和out 只能用al或者ax
out dx, al
mov dx, 0x3d4
mov al, 0x0f
out dx, al
mov dx, 0x3d5
mov al, bl
out dx, al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
my_int_0x70:
push ax
push bx
push cx
push dx
push es
.w0:
mov al,0x0a ;阻断NMI。当然,通常是不必要的
or al,0x80
out 0x70,al
in al,0x71 ;读寄存器A
test al,0x80 ;测试第7位UIP
jnz .w0 ;以上代码对于更新周期结束中断来说
;是不必要的
xor al,al
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(秒)
push ax
mov al,2
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(分)
push ax
mov al,4
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(时)
push ax
; 读一下RTC的寄存器C,使得所有中断标志复位。相当于,告诉RTC,中断已得到处理,可以继续下一次中断。
; 否则,RTC看到中断未被处理,将不再产生中断信号。
; RTC产生中断的原因有多种,可以在程序中通过读寄存器C来判断。不过,这里不需要,因为除了更新周期结束中断外,其他中断都被关闭了。
mov al,0x0c ;寄存器C的索引。且开放NMI
out 0x70,al
in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断
;此处不考虑闹钟和周期性中断的情况
; 屏幕是黑的,默认的显示属性是0x07,即黑底白字
mov ax,0xb800
mov es,ax ; es指向显示缓冲区
mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示
; 小时
pop ax
call bcd_to_ascii
mov [es:bx],ah
mov [es:bx+2],al ;显示两位小时数字
mov byte [es:bx+4],':' ;显示分隔符':'
not byte [es:bx+5] ;反转显示属性
; 分钟
pop ax
call bcd_to_ascii
mov [es:bx+6],ah
mov [es:bx+8],al ;显示两位分钟数字
mov al,':'
mov [es:bx+10],al ;显示分隔符':'
not byte [es:bx+11] ;反转显示属性
; 秒
pop ax
call bcd_to_ascii
mov [es:bx+12],ah
mov [es:bx+14],al ;显示两位小时数字
; 向8259芯片发送中断结束命令(End Of Interrupt, EOI)
mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向从片发送
out 0x20,al ;向主片发送
pop es
pop dx
pop cx
pop bx
pop ax
iret ; iret 中断返回指令,Interrupt Return,回到中断之前的地方继续执行
; 这里如果用ret,显示的时间将不会更新 ? ? ?
; Function: BCD码转ASCII
; Input: AL, BCD码
; Output: AX, ascii码
bcd_to_ascii:
mov ah, al ; 先复制到ah,用于后面处理十位数,al用于处理个位数
and al, 0x0f ; 仅保留低4位
add al, 0x30 ; 转换成ASCII
shr ah, 4
and ah, 0x0f
add ah, 0x30
ret
;===============================================================================
SECTION data align=16 vstart=0
msg_init db 'Starting...',0x0d,0x0a,0
msg_install db 'Installing a new interrupt 70H...',0
msg_done db 'Done.',0x0d,0x0a,0
msg_tips db 'Clock is now working.',0
; ===============================================================================
SECTION stack align=16 vstart=0
resb 256
stack_end:
; ===============================================================================
SECTION program_tail
program_end: