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:
以上就是全部的代码了(加载器采用第八章的)
也不知道我这个插件怎么了,显示出的源码歪歪扭扭,没有对齐好吧,咱们就凑合看吧。
2.用户程序结构图
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)中断初始化和安装
书上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内部有一个中断屏蔽寄存器,如下图所示:
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的下一条指令。
代码分析就到这里吧,下次我们看一下运行结果。