天天看點

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

1.代碼清單

;代碼清單9-1
         ;檔案名:c09_1.asm
         ;檔案說明:使用者程式 
          ;建立日期:2011-4-16 22:03
         
;===============================================================================
SECTION header vstart=0                     ;定義使用者程式頭部段 
    program_length  dd program_end          ;程式總長度[0x00]
    
    ;使用者程式入口點
    code_entry      dw start                ;偏移位址[0x04]
                    dd section.code.start   ;段位址[0x06] 
    
    realloc_tbl_len dw (header_end-realloc_begin)/4
                                            ;段重定位表項個數[0x0a]
    
    realloc_begin:
    ;段重定位表           
     code_segment    dd section.code.start   ;[0x0c]
    data_segment    dd section.data.start   ;[0x14]
    stack_segment   dd section.stack.start  ;[0x1c]
    
header_end:                
    
;===============================================================================
SECTION code align=16 vstart=0           ;定義代碼段(16位元組對齊) 
new_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

      mov al,0x0c                        ;寄存器C的索引。且開放NMI 
      out 0x70,al
      in al,0x71                         ;讀一下RTC的寄存器C,否則隻發生一次中斷
                                         ;此處不考慮鬧鐘和周期性中斷的情況 
      mov ax,0xb800
      mov es,ax

      pop ax
      call bcd_to_ascii
      mov bx,12*160 + 36*2               ;從螢幕上的12行36列開始顯示

      mov [es:bx],ah
      mov [es:bx+2],al                   ;顯示兩位小時數字

      mov al,':'
      mov [es:bx+4],al                   ;顯示分隔符':'
      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                  ;顯示兩位小時數字
      
      mov al,0x20                        ;中斷結束指令EOI 
      out 0xa0,al                        ;向從片發送 
      out 0x20,al                        ;向主片發送 

      pop es
      pop dx
      pop cx
      pop bx
      pop ax

      iret

;-------------------------------------------------------------------------------
bcd_to_ascii:                            ;BCD碼轉ASCII
                                         ;輸入:AL=bcd碼
                                         ;輸出:AX=ascii
      mov ah,al                          ;分拆成兩個數字 
      and al,0x0f                        ;僅保留低4位 
      add al,0x30                        ;轉換成ASCII 

      shr ah,4                           ;邏輯右移4位 
      and ah,0x0f                        
      add ah,0x30

      ret

;-------------------------------------------------------------------------------
start:
      mov ax,[stack_segment]
      mov ss,ax
      mov sp,ss_pointer
      mov ax,[data_segment]
      mov ds,ax
      
      mov bx,init_msg                    ;顯示初始資訊 
      call put_string

      mov bx,inst_msg                    ;顯示安裝資訊 
      call put_string
      
      mov al,0x70
      mov bl,4
      mul bl                             ;計算0x70号中斷在IVT中的偏移
      mov bx,ax                          

      cli                                ;防止改動期間發生新的0x70号中斷

      push es
      mov ax,0x0000
      mov es,ax
      mov word [es:bx],new_int_0x70      ;偏移位址。
                                          
      mov word [es:bx+2],cs              ;段位址
      pop es

      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                                ;重新開放中斷 

      mov bx,done_msg                    ;顯示安裝完成資訊 
      call put_string

      mov bx,tips_msg                    ;顯示提示資訊
      call put_string
      
      mov cx,0xb800
      mov ds,cx
      mov byte [12*160 + 33*2],'@'       ;螢幕第12行,35列
       
 .idle:
      hlt                                ;使CPU進入低功耗狀态,直到用中斷喚醒
      not byte [12*160 + 33*2+1]         ;反轉顯示屬性 
      jmp .idle

;-------------------------------------------------------------------------------
put_string:                              ;顯示串(0結尾)。
                                         ;輸入:DS:BX=串位址
         mov cl,[bx]
         or cl,cl                        ;cl=0 ?
         jz .exit                        ;是的,傳回主程式 
         call put_char
         inc bx                          ;下一個字元 
         jmp put_string

   .exit:
         ret

;-------------------------------------------------------------------------------
put_char:                                ;顯示一個字元
                                         ;輸入:cl=字元ascii
         push ax
         push bx
         push cx
         push dx
         push ds
         push es

         ;以下取目前光标位置
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;高8位 
         mov ah,al

         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;低8位 
         mov bx,ax                       ;BX=代表光标位置的16位數

         cmp cl,0x0d                     ;回車符?
         jnz .put_0a                     ;不是。看看是不是換行等字元 
         mov ax,bx                       ; 
         mov bl,80                       
         div bl
         mul bl
         mov bx,ax
         jmp .set_cursor

 .put_0a:
         cmp cl,0x0a                     ;換行符?
          jnz .put_other                  ;不是,那就正常顯示字元 
          add bx,80
         jmp .roll_screen

 .put_other:                             ;正常顯示字元
          mov ax,0xb800
         mov es,ax
         shl bx,1
         mov [es:bx],cl

         ;以下将光标位置推進一個字元
          shr bx,1
         add bx,1

 .roll_screen:
         cmp bx,2000                     ;光标超出螢幕?滾屏
          jl .set_cursor

         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
         mov bx,3840                     ;清除螢幕最底一行
          mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

         mov bx,1920

 .set_cursor:
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         mov al,bh
         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

;===============================================================================
SECTION data align=16 vstart=0

    init_msg       db 'Starting...',0x0d,0x0a,0
                   
    inst_msg       db 'Installing a new interrupt 70H...',0
    
    done_msg       db 'Done.',0x0d,0x0a,0

    tips_msg       db 'Clock is now working.',0
                   
;===============================================================================
SECTION stack align=16 vstart=0
           
                 resb 256
ss_pointer:
 
;===============================================================================
SECTION program_trail
program_end:      

以上就是全部的代碼了(加載器采用第八章的)

也不知道我這個插件怎麼了,顯示出的源碼歪歪扭扭,沒有對齊

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

好吧,咱們就湊合看吧。

2.使用者程式結構圖

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

3.中斷處理程式

最開始的部分是頭部,嚴格遵循第八章作者約定的格式,我們就不多說了。

;===============================================================================
SECTION code align=16 vstart=0           ;定義代碼段(16位元組對齊) 
new_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      

這段代碼要細說,有很多新知識。

(1)CMOS RAM

在外圍裝置控制晶片(ICH)内部,內建了實時時鐘電路(RTC)和兩小塊由互補金屬氧化物(CMOS)材料組成的靜态存儲器(CMOS RAM)。實時時鐘電路負責計時,而日期和時間的數值則存儲在這塊存儲器中,它們由電腦主機闆上的一個小紐扣電池提供能量。

日期和時間資訊存儲在CMOS RAM中,通常CMOS RAM有128個存儲單元,而日期和時間資訊隻占了一小部分容量,其餘空間則儲存整機的配置資訊。

RTC晶片由一個頻率為32.768kHz的晶振驅動,經過分頻後,用于對CMOS RAM進行每秒一次的時間重新整理。

表格9-1 CMOS RAM中的時間資訊

偏移位址 内容 偏移位址 内容
0x00 0x07
0x01 鬧鐘秒 0x08
0x02 0x09
0x03 鬧鐘分 0x0a 寄存器A
0x04 0x0b 寄存器B
0x05 鬧鐘時 0x0c 寄存器C
0x06 星期 0x0d 寄存器D

CMOS RAM的通路,需要兩個端口:0x70是索引端口,用來指定記憶體單元;0x71是資料端口,用來讀寫相應單元裡的内容。

舉例:

mov al,2

out 0x70,al ;指定記憶體單元為2

in al,0x71 ;讀RTC目前時間(分)

需要說明的是,從很早的時候開始,端口0x70的最高位是控制NMI中斷的開關,當它為0時,允許NMI中斷;為1時,阻斷所有的NMI信号。其他7個bit,實際上用來指定CMOS RAM單元的索引号。

作者為了簡化問題,是以在通路RTC時,直接關閉NMI,通路結束後,再打開NMI(不管它之前是不是打開的)。

查閱資料,有的朋友說“通路CMOS RAM可能導緻産生NMI,是以需要關閉NMI。”

還有一點要注意:CMOS RAM中儲存的日期和時間,預設是8421 BCD編碼,也就是用0000~1001分别代表它所對應的十進制數。

.w0:                                    
    mov al,0x0a                        ;通路寄存器A
    or al,0x80                         ;阻斷NMI
    out 0x70,al
    in al,0x71                         ;讀寄存器A
    test al,0x80                       ;測試第7位UIP 
    jnz .w0                            ;以上代碼對于更新周期結束中斷來說是不必要的 
                                                 

test al,0x80 ,這句是測試寄存器A的bit7

正如書上155頁所說:

CMOS RAM中的時間和日期會由RTC周期性地更新,在此期間,使用者程式不應當通路它們。

寄存器A的bit7為0時,表示更新周期至少在488us内不會啟動。換句話說,此時通路時間資訊是安全的。

寄存器A的bit7為1時,表示正處于更新周期或者馬上就要啟動。

可以看到,上面的代碼就是反複測試寄存器A的bit7,如果是0,可以向下執行。

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      

道理同上。

mov al,0x0c                        ;寄存器C的索引。且開放NMI 
    out 0x70,al
    in al,0x71                         ;讀一下RTC的寄存器C,否則隻發生一次中斷
                                       ;此處不考慮鬧鐘和周期性中斷的情況      

這裡要說一下寄存器C,這個寄存器是隻讀寄存器。可以通過讀取這個寄存器,知道中斷是否發生,如果發生,還可以知道中斷原因。

寄存器C是8位寄存器。

[3:0]:保留;

[7]:中斷請求标志,周期性中斷/鬧鐘中斷/更新結束中斷,任何一種發生都會使這位置1;

[6]:周期性中斷标志,置1則表示發生了周期性中斷

[5]:鬧鐘中斷标志,置1則表示發生了鬧鐘中斷

[4]:更新結束中斷标志,置1則表示發生了更新結束中斷

注意,對寄存器的讀操作将導緻[7:4]清零。在中斷發生後,我們應該讀取這個寄存器,将其清零,否則同樣的中斷不再産生。

(2)把BCD碼轉換為ascii碼

前面的代碼中,把時分秒都讀取出來并且壓棧了。下一步的工作就是出棧,在螢幕上顯示。前文已經說過,CMOS RAM中儲存的日期和時間,預設是8421 BCD編碼,是以我們可以利用一個過程,把BCD編碼轉換成與其對應的ascii碼。

bcd_to_ascii:                            ;BCD碼轉ASCII
                                         ;輸入:AL=bcd碼
                                         ;輸出:AX=ascii
      mov ah,al                          ;分拆成兩個數字 
      and al,0x0f                        ;僅保留低4位 
      add al,0x30                        ;轉換成ASCII 

      shr ah,4                           ;邏輯右移4位 
      and ah,0x0f                        
      add ah,0x30

      ret      

舉個例子來說吧,比如前面我們讀取了小時到AL中,比如是12時,那麼al=00010010b;前文我們壓棧是把AX壓進去,也就是說AX的低8位(AL)是有用的。現在我們需要調用這個過程,把00010010b轉換成0x3132(因為字元‘1’對應的ASCII碼是0x31,字元‘2’對應的ASCII碼是0x32)。

mov ah,al ;分拆成兩個數字

and al,0x0f ;僅保留低4位(就是個位)

add al,0x30 ;把個位轉換成ASCII

shr ah,4 ;邏輯右移4位 ,ah中是十位數字

and ah,0x0f

add ah,0x30 ;把十位轉換成ASCII

OK,這樣之後,AX的高八位就是十位的ASCII,低八位就是個位的ASCII;

(3)把時間資訊顯示在螢幕上

mov ax,0xb800
    mov es,ax

    pop ax
    call bcd_to_ascii
    mov bx,12*160 + 36*2               ;從螢幕上的12行36列開始顯示

    mov [es:bx],ah
    mov [es:bx+2],al                   ;顯示兩位小時數字

    mov al,':'
    mov [es:bx+4],al                   ;顯示分隔符':'
    not byte [es:bx+5]                 ;反轉顯示屬性      

前兩句讓es指向了顯示緩沖區;

pop ax ;小時出棧

call bcd_to_ascii ;轉為ASCII碼

mov bx,12*160 + 36*2 ;從螢幕上的12行36列開始顯示

mov [es:bx],ah ;顯示小時的十位

mov [es:bx+2],al ;顯示小時的個位

mov al,':'

mov [es:bx+4],al ;顯示分隔符':'

not byte [es:bx+5] ;反轉顯示屬性

其實前兩句可以寫成

mov [es:bx+4],':’ ;顯示分隔符':'

not是按位取反指令,假如之前屬性是0x07(黑底白字),那麼Not之後就是0xf8(閃爍白底灰色字)。

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                  ;顯示兩位小時數字      

上面的代碼同理。

mov al,0x20                        ;中斷結束指令EOI 
    out 0xa0,al                        ;向從片發送 
    out 0x20,al                        ;向主片發送      

書上162頁已經說明:在中斷處理過程的結尾,我們要顯式地向8259晶片寫中斷結束指令EOI(至于具體原因,可以參考361頁,圖17-17:8259A的初始化指令字)。如果外部中斷是8259主片處理的,那麼僅發送給主片即可,端口号是0x20;如果外部中斷是由從片處理的,那麼指令既要發給主片也要發給從片,端口号是0xa0. 中斷結束指令的代碼是0x20.

pop es
    pop dx
    pop cx
    pop bx
    pop ax

    iret      

寄存器出棧,用iret指令傳回。

4.主程式

(1)初始化

start:
      mov ax,[stack_segment]
      mov ss,ax
      mov sp,ss_pointer
      mov ax,[data_segment]
      mov ds,ax
      
      mov bx,init_msg                    ;顯示初始資訊 
      call put_string

      mov bx,inst_msg                    ;顯示安裝資訊 
      call put_string      

這就是程式的入口了。首先,設定棧段,棧段被安排在整個程式的末尾,保留了256位元組。

SECTION stack align=16 vstart=0
           
                 resb 256
ss_pointer:
 
;===============================================================================
SECTION program_trail
program_end:      

之後,設定好DS,令其指向資料段;然後顯示一些資訊。

(2)中斷初始化和安裝

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

書上158頁說:在計算機啟動期間,BIOS會初始化中斷控制器,将主片的中斷号設為從0x08開始,從片的從0x70開始。從上圖可以看出來,實時時鐘連到了從片的IR0,也就是說實時時鐘的中斷号是0x70.

mov al,0x70
      mov bl,4
      mul bl                             ;計算0x70号中斷在IVT中的偏移
      mov bx,ax                          

      cli                                ;防止改動期間發生新的0x70号中斷      

前文已經說過:

中斷向量在中斷向量表中的位置=中斷類型号×4

N*4的字單元存放偏移位址;

N*4+2的字單元存放段基址。

我們已經知道中斷類型号是0x70了,下面要計算它在中斷向量表中的位置(也就是計算0x70*4):用乘法指令, AX=AL*r8; 前四句執行後,BX中就是0x70号中斷向量在向量表中的偏移。

cli這個指令用來清除IF位标志,相當于屏蔽外部中斷。因為在修改中斷向量表時,如果表項資訊隻修改了一部分,這時候發生0x70号中斷,将會産生不可預料的問題。

push es
      mov ax,0x0000
      mov es,ax
      mov word [es:bx],new_int_0x70      ;偏移位址。
                                          
      mov word [es:bx+2],cs              ;段位址
      pop es      

将ES壓棧(暫時儲存),并使它指向中斷向量表所在的段,把偏移位址設定為new_int_0x70 ,把段基位址設定為CS。最後恢複ES。

mov al,0x0b                        ;RTC寄存器B
      or al,0x80                         ;阻斷NMI 
      out 0x70,al
      mov al,0x12                        ;設定寄存器B,禁止周期性中斷,開放更 
      out 0x71,al                        ;新結束後中斷,BCD碼,24小時制      

上面的代碼用來設定寄存器B;寄存器B與本實驗相關的位有:

[7]: 0表示更新周期每秒都會發生;1表示中止目前的更新周期,此後也不再産生更新周期;

[6]: 0表示禁止周期性中斷,1表示允許周期性中斷;

[5]: 0表示鬧鐘中斷禁止,1表示鬧鐘中斷允許;

[4]: 0表示禁止更新結束中斷,1表示允許更新結束中斷;

[3]:該位空着不用;

[2]:資料模式,0表示BCD,1表示2進制;

[1]: 小時格式,0表示12小時制(bit7為0時表示AM,為1表示PM,舉例:在BCD模式下,10010001b表示上午11點),1表示24小時制;

[0]:該位空着不用;

從代碼可以看出,我們寫入寄存器B的值是0x12,也就是:

[7]:0,允許更新周期發生;

[6]:0,禁止周期性中斷;

[5]:0,禁止鬧鐘中斷;

[4]:1,允許更新結束中斷;

[3]:0

[2]:0,BCD模式

[1]:1,24小時制

[0]:0

mov al,0x0c
      out 0x70,al
      in al,0x71                         ;讀RTC寄存器C,複位未決的中斷狀态      

讀寄存器C, 使之開始産生中斷信号。注意,在向端口0x70寫入al的同時,也打開了NMI,因為這是最後一次在主程式中通路RTC。到此,RTC晶片設定完畢。

in al,0xa1                         ;讀8259從片的IMR寄存器 
      and al,0xfe                        ;清除bit 0(此位連接配接RTC)
      out 0xa1,al                        ;寫回此寄存器 

      sti                                ;重新開放中斷      

8259A内部有一個中斷屏蔽寄存器,如下圖所示:

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

IMR是一個8位的寄存器,位0-7對應着引腳中斷IR0-IR7;如果對應的位為0,則允許中斷;為1,則屏蔽中斷。

我們通過端口0xa1讀取從片的IMR寄存器,用and指令清除bit0(其他位保持原樣),然後再寫回去。這樣,關于中斷的初始化就完成了。

最後,sti指令将IF置1,打開中斷。從這時候開始,随時發生的中斷就可以被處理了。

mov bx,done_msg                    ;顯示安裝完成資訊 
      call put_string

      mov bx,tips_msg                    ;顯示提示資訊
      call put_string      

顯示一些資訊,表示中斷設定和安裝已完成。

mov cx,0xb800
      mov ds,cx
      mov byte [12*160 + 33*2],'@'       ;螢幕第12行,33列      

在螢幕12行33列顯示一個“@”;

.idle:
      hlt                                ;使CPU進入低功耗狀态,直到用中斷喚醒
      not byte [12*160 + 33*2+1]         ;反轉顯示屬性 
      jmp .idle      

hlt是停機指令,使程式停止運作。這時候處理器進入暫停狀态,不執行任何操作。當複位線上有複位信号、CPU響應非屏蔽中斷、CPU響應可屏蔽中斷3種情況之一發生時,CPU就會脫離暫停狀态,執行hlt的下一條指令。

代碼分析就到這裡吧,下次我們看一下運作結果。

8086實時時鐘實驗(一)——《x86彙編語言:從實模式到保護模式》05

繼續閱讀