1. 外部硬件中断
硬件中断一般是从处理器外面来的中断信号。当外部设备发生错误,或者有数据要传送(比如从网络接收到一个针对当前主机的数据包),或者处理器交给它的事情处理完了。
如下图,外部硬件中断是通过两个信号线引入处理器内部的。这两根线的名字叫做NMI和INTR。
中断信号的来源,或者说是产生中断的设备,称为中断源。当一个中断发生时,处理器将会通过中断引脚 NMI 和 INTR 得到通知。除此之外,处理器还需要知道发生了什么事,以便采取适当的处理措施。每种类型的中断都被统一编号,这称为中断类型号、中断向量或者中断号。
1.1 非屏蔽中断
NMI(Non Maskable Interrupt)接受的是非屏蔽中断。
实模式下,NMI 被赋予了统一的中断号 2,因为几乎所有触发非屏蔽中断的事件对处理器来说都是致命的,甚至是不可纠正的,所有处理器干脆不会处理,很可能只是由软件系统给出一个提示信息。
1.2 可屏蔽中断
可屏蔽中断一般是通过 INTR 引脚进入处理器的。大多数的中断也是可屏蔽中断,由一个中断代理来接收外部设备发起的中断信号。
Intel 处理器允许 256 个中断,中断号范围 0-255,如下图是一个单处理器的中断控制器 8259 芯片。
以上两个8259芯片负责15个中断号的管理。但是可以使用 in 和 out 指令来访问 0-255 个中断号。
如上图,8259 的主片引脚 0(IR0)接的是系统定时器/计数器芯片;从片引脚 0(IR0)接的是实时时钟芯片 RTC。
中断是否被处理?
在 8259 芯片内部:有中断屏蔽器(Interrupt Mask Register,IMR),这是一个8位寄存器,对应着该芯片的 8 个中断输入引脚,对应的位是0还是1决定了该引脚输入的中断是否能通过8259芯片。0表示允许 1表示阻断。8259 芯片是可编程的,主片的端口号是 0x20,0x21,从片的端口号是 0xa,0xa1。通过这些端口访问芯片。
处理器内部:除了要看 8259 芯片内部,最终的决定权在于处理器。处理器内部的标志寄存器 FLAGS 有一个标志 IF。这是中断标志。当 IF 为 0 时所有从 INTR 来的中断都被忽略。当 IF 为 1 时处理器可以接收和响应中断。
IF 标志位可以通过指令 cli 和 sti 改变。cli(clear interrupt flag)用于清除 IF 标志位,sti(set interrupt flag)用于置位 IF 标志。
1.3 中断向量表
在实模式下处理器将 256 个中断程序对应的入口地址集中存放在物理地址 0x00000 - 0x003ff 共 1KB 的空间内,这就是中断向量表(Interrupt Vector Table)。
如下图所示,每个中断在中断向量表中占两个字,分别中断处理程序的偏移地址和段地址。
在 8259 芯片那里,每个引脚都赋予了一个中断号,这些中断号以芯片为单位可以进行改变,比如直到主片的中断号从 0x08开始,那个它的每个引脚 IR0-IR7 所对应的中断号为 0x08 - 0x0e。
中断信号来自哪个引脚,8259芯片是最清楚的,所以他会把相应的中断号告诉处理器,处理器拿着这个中断号,要顺序做以下几件事:
- 保护断点的现场
- 将标志寄存器 FLAGS 压栈,然后清除它的 IF 位和 TF 位。注意,由于 IF 标志被清除,处理器将不不再响应硬件中断,如果想要更高优先级的的中断嵌套,可以在编写中断处理程序时,适时用 sti 指令开发中断。
- 将当前代码寄存器 CS 和指令指针寄存器 IP 压栈。
- 执行中断程序
- 由于处理器已经拿到中断号,它用中断号乘以4(见上图)就得到了该中断的入口点的偏移地址和段地址在中断向量表中的偏移地址。
- 从表中依次取出中断程序的偏移地址和短地址,并分别传送到 IP 和 CS。
- 返回到断点接着执行
- 所有中断的程序的最后是一条iret指令。这将导致处理器一次从栈中弹出 IP、CS、FLAGS 的原始内容,于是转到主程序继续执行。由于恢复了 FLAGS 的原始内容,所有 IF 位也恢复了,即可以接收新的中断。
- 和可屏蔽中断不同,NMI 发生时,处理器不会从外部获得中断号,它会自动生成中断号码 2。其他处理过程和可屏蔽中断一致。
1.4 实时时钟显示
为什么计算机能够准确的显示日期和时间?这是因为在外围设备控制器芯片 ICH 内部,集成了实时时钟电路(Real Time Clock,RTC)和两小块由互补金属氧化物(CMOS)材料组成的静态存储器(CMOS RAM)。实时时钟电路负责计时,而日期和时间的数值则存储在这块存储器中。
日期和时钟信息保存在 CMOS RAM 中,通常有 128 字节,除了日期和时间信息,其它空间用于保存整机配置信息,如各种硬件的类型和工作参数、开机密码和辅助存储设备的启动顺序等。这些参数的修改通常在 BIOS SETUP 开机程序中进行。
如下表所示,常规的日期和时间信息占据了 CMOS RAM 开始部分的 10 字节。
偏移地址 | 内容 | 偏移地址 | 内容 |
---|---|---|---|
0x00 | 秒 | 0x07 | 日 |
0x01 | 闹钟秒 | 0x08 | 月 |
0x02 | 分 | 0x09 | 年 |
0x03 | 闹钟分 | 0x0A | 寄存器A |
0x04 | 时 | 0x0B | 寄存器B |
0x05 | 闹钟时 | 0x0C | 寄存器C |
0x06 | 星期 | 0x0D | 寄存器D |
CMOS RAM 的访问,需要通过两个端口来进行。0x70 或 0x74 是索引端口,用来指定 CMOS RAM 内的单元;0x71 和 0x75 是数据端口,用来读写需要单元的内容。下面是读取今天是星期几的代码。
mov al, 0x06
out 0x70, al
in al, 0x71
不得不说的是,在早期,端口 0x07 的 bit 7 是控制 NMI 中断的开关 。为 0 时允许 NMI 中断到达处理器,为 1 时则阻断所有 NMI 信号,其它 7 位用于指定 CMOS RAM 单元的索引号。在访问 RTC 时,我们直接关闭 NMI,访问结束后,再打开 NMI。如下图所示。
单元 0x0A - 0x0D 被定义成 4 个寄存器的索引号,这 4 个寄存器用于设置实时时钟电路的参数和工作状态。
寄存器A功能如下图。
寄存器B功能如下图。
寄存器C功能如下图。
寄存器D功能如下图。
使用中断动态显示时钟的程序源码见代码清单 9-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. 内部中断
和硬件中断不同,软中断是处理器内部产生的,是由执行指令引起的。内部中断不受标志寄存器 IF 位的影响,也不需要中断识别总线周期,它们的中断类型是固定的,可以立即转入相应的处理过程。
3. 软中断
软中断是由 int 指令引起的。这类中断不需要中断识别总线周期,中断号在指令中给出。最有名的软中断是 BIOS 中断,这些中断功能是计算机加电之后,BIOS 程序执行期间建立起来的