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 axpush bxpush cxpush dxpush es.w0: mov al,0x0a ;阻斷NMI。當然,通常是不必要的or al,0x80 out 0x70,alin al,0x71 ;讀寄存器Atest al,0x80 ;測試第7位UIP jnz .w0 ;以上代碼對于更新周期結束中斷來說 ;是不必要的 xor al,alor al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(秒)push axmov al,2or al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(分)push axmov al,4or al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(時)push axmov al,0x0c ;寄存器C的索引。且開放NMI out 0x70,alin al,0x71 ;讀一下RTC的寄存器C,否則只發生一次中斷;此處不考慮鬧鐘和周期性中斷的情況 mov ax,0xb800mov es,axpop axcall bcd_to_asciimov bx,12*160 + 36*2 ;從屏幕上的12行36列開始顯示mov [es:bx],ahmov [es:bx+2],al ;顯示兩位小時數字mov al,':'mov [es:bx+4],al ;顯示分隔符':'not byte [es:bx+5] ;反轉顯示屬性 pop axcall bcd_to_asciimov [es:bx+6],ahmov [es:bx+8],al ;顯示兩位分鐘數字mov al,':'mov [es:bx+10],al ;顯示分隔符':'not byte [es:bx+11] ;反轉顯示屬性pop axcall bcd_to_asciimov [es:bx+12],ahmov [es:bx+14],al ;顯示兩位小時數字mov al,0x20 ;中斷結束命令EOI out 0xa0,al ;向從片發送 out 0x20,al ;向主片發送 pop espop dxpop cxpop bxpop axiret;------------------------------------------------------------------------------- bcd_to_ascii: ;BCD碼轉ASCII;輸入:AL=bcd碼;輸出:AX=asciimov ah,al ;分拆成兩個數字 and al,0x0f ;僅保留低4位 add al,0x30 ;轉換成ASCII shr ah,4 ;邏輯右移4位 and ah,0x0f add ah,0x30ret;------------------------------------------------------------------------------- start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointermov ax,[data_segment]mov ds,axmov bx,init_msg ;顯示初始信息 call put_stringmov bx,inst_msg ;顯示安裝信息 call put_stringmov al,0x70mov bl,4mul bl ;計算0x70號中斷在IVT中的偏移mov bx,ax cli ;防止改動期間發生新的0x70號中斷push esmov ax,0x0000mov es,axmov word [es:bx],new_int_0x70 ;偏移地址。mov word [es:bx+2],cs ;段地址pop esmov al,0x0b ;RTC寄存器Bor al,0x80 ;阻斷NMI out 0x70,almov al,0x12 ;設置寄存器B,禁止周期性中斷,開放更 out 0x71,al ;新結束后中斷,BCD碼,24小時制 mov al,0x0cout 0x70,alin 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_stringmov bx,tips_msg ;顯示提示信息call put_stringmov cx,0xb800mov ds,cxmov 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_charinc bx ;下一個字符 jmp put_string.exit:ret;------------------------------------------------------------------------------- put_char: ;顯示一個字符;輸入:cl=字符asciipush axpush bxpush cxpush dxpush dspush es;以下取當前光標位置mov dx,0x3d4mov al,0x0eout dx,almov dx,0x3d5in al,dx ;高8位 mov ah,almov dx,0x3d4mov al,0x0fout dx,almov dx,0x3d5in al,dx ;低8位 mov bx,ax ;BX=代表光標位置的16位數cmp cl,0x0d ;回車符?jnz .put_0a ;不是。看看是不是換行等字符 mov ax,bx ; mov bl,80 div blmul blmov bx,axjmp .set_cursor.put_0a:cmp cl,0x0a ;換行符?jnz .put_other ;不是,那就正常顯示字符 add bx,80jmp .roll_screen.put_other: ;正常顯示字符mov ax,0xb800mov es,axshl bx,1mov [es:bx],cl;以下將光標位置推進一個字符shr bx,1add bx,1.roll_screen:cmp bx,2000 ;光標超出屏幕?滾屏jl .set_cursormov ax,0xb800mov ds,axmov es,axcldmov si,0xa0mov di,0x00mov cx,1920rep movswmov bx,3840 ;清除屏幕最底一行mov cx,80.cls:mov word[es:bx],0x0720add bx,2loop .clsmov bx,1920.set_cursor:mov dx,0x3d4mov al,0x0eout dx,almov dx,0x3d5mov al,bhout dx,almov dx,0x3d4mov al,0x0fout dx,almov dx,0x3d5mov al,blout dx,alpop espop dspop dxpop cxpop bxpop axret;=============================================================================== SECTION data align=16 vstart=0init_msg db 'Starting...',0x0d,0x0a,0inst_msg db 'Installing a new interrupt 70H...',0done_msg db 'Done.',0x0d,0x0a,0tips_msg db 'Clock is now working.',0;=============================================================================== SECTION stack align=16 vstart=0resb 256 ss_pointer:;=============================================================================== SECTION program_trail program_end:以上就是全部的代碼了(加載器采用第八章的)
也不知道我這個插件怎么了,顯示出的源碼歪歪扭扭,沒有對齊好吧,咱們就湊合看吧。
2.用戶程序結構圖
3.中斷處理程序
最開始的部分是頭部,嚴格遵循第八章作者約定的格式,我們就不多說了。
;=============================================================================== SECTION code align=16 vstart=0 ;定義代碼段(16字節對齊) new_int_0x70:push axpush bxpush cxpush dxpush es這里就開始中斷處理程序了。首先是把用到的寄存器入棧,這是必須的。
.w0: mov al,0x0a ;阻斷NMI。當然,通常是不必要的or al,0x80 out 0x70,alin al,0x71 ;讀寄存器Atest al,0x80 ;測試第7位UIP jnz .w0 ;以上代碼對于更新周期結束中斷來說 ;是不必要的 xor al,alor al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(秒)push axmov al,2or al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(分)push axmov al,4or al,0x80out 0x70,alin 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 ;訪問寄存器Aor al,0x80 ;阻斷NMIout 0x70,alin al,0x71 ;讀寄存器Atest 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,alor al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(秒)push ax這段代碼很好理解,就是讀出秒,并且把結果壓棧(壓棧時為了之后顯示在屏幕上)
mov al,2or al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(分)push axmov al,4or al,0x80out 0x70,alin al,0x71 ;讀RTC當前時間(時)push ax 道理同上。 mov al,0x0c ;寄存器C的索引。且開放NMI out 0x70,alin 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=asciimov ah,al ;分拆成兩個數字 and al,0x0f ;僅保留低4位 add al,0x30 ;轉換成ASCII shr ah,4 ;邏輯右移4位 and ah,0x0f add ah,0x30ret舉個例子來說吧,比如前面我們讀取了小時到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,0xb800mov es,axpop axcall bcd_to_asciimov bx,12*160 + 36*2 ;從屏幕上的12行36列開始顯示mov [es:bx],ahmov [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 axcall bcd_to_asciimov [es:bx+6],ahmov [es:bx+8],al ;顯示兩位分鐘數字mov al,':'mov [es:bx+10],al ;顯示分隔符':'not byte [es:bx+11] ;反轉顯示屬性pop axcall bcd_to_asciimov [es:bx+12],ahmov [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 espop dxpop cxpop bxpop axiret寄存器出棧,用iret命令返回。
4.主程序
(1)初始化
start:mov ax,[stack_segment]mov ss,axmov sp,ss_pointermov ax,[data_segment]mov ds,axmov bx,init_msg ;顯示初始信息 call put_stringmov bx,inst_msg ;顯示安裝信息 call put_string這就是程序的入口了。首先,設置棧段,棧段被安排在整個程序的末尾,保留了256字節。
SECTION stack align=16 vstart=0resb 256 ss_pointer:;=============================================================================== SECTION program_trail program_end:之后,設置好DS,令其指向數據段;然后顯示一些信息。
(2)中斷初始化和安裝
書上158頁說:在計算機啟動期間,BIOS會初始化中斷控制器,將主片的中斷號設為從0x08開始,從片的從0x70開始。從上圖可以看出來,實時時鐘連到了從片的IR0,也就是說實時時鐘的中斷號是0x70.
mov al,0x70mov bl,4mul 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 esmov ax,0x0000mov es,axmov word [es:bx],new_int_0x70 ;偏移地址。mov word [es:bx+2],cs ;段地址pop es將ES壓棧(暫時保存),并使它指向中斷向量表所在的段,把偏移地址設置為new_int_0x70 ,把段基地址設置為CS。最后恢復ES。
mov al,0x0b ;RTC寄存器Bor al,0x80 ;阻斷NMI out 0x70,almov 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,0x0cout 0x70,alin 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內部有一個中斷屏蔽寄存器,如下圖所示:
?
IMR是一個8位的寄存器,位0-7對應著引腳中斷IR0-IR7;如果對應的位為0,則允許中斷;為1,則屏蔽中斷。
我們通過端口0xa1讀取從片的IMR寄存器,用and指令清除bit0(其他位保持原樣),然后再寫回去。這樣,關于中斷的初始化就完成了。
最后,sti指令將IF置1,打開中斷。從這時候開始,隨時發生的中斷就可以被處理了。
mov bx,done_msg ;顯示安裝完成信息 call put_stringmov bx,tips_msg ;顯示提示信息call put_string顯示一些信息,表示中斷設置和安裝已完成。
?
mov cx,0xb800mov ds,cxmov byte [12*160 + 33*2],'@' ;屏幕第12行,33列在屏幕12行33列顯示一個“@”;
.idle:hlt ;使CPU進入低功耗狀態,直到用中斷喚醒not byte [12*160 + 33*2+1] ;反轉顯示屬性 jmp .idlehlt是停機指令,使程序停止運行。這時候處理器進入暫停狀態,不執行任何操作。當復位線上有復位信號、CPU響應非屏蔽中斷、CPU響應可屏蔽中斷3種情況之一發生時,CPU就會脫離暫停狀態,執行hlt的下一條指令。
?
代碼分析就到這里吧,下次我們看一下運行結果。
總結
以上是生活随笔為你收集整理的8086实时时钟实验(一)——《x86汇编语言:从实模式到保护模式》05的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java中1和1.0_在Java中如何以
- 下一篇: linux每个版本发布时间,Ubuntu