[书]x86汇编语言:从实模式到保护模式 -- 第九章 硬中断,使用RTC芯片实现实时时间的显示;软中断,使用BIOS中断实现键盘输入的读取和显示

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:

 

发布了120 篇原创文章 · 获赞 43 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章