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: