NASM(六)硬件中断处理
中断
可屏蔽中断:
对于那些不紧急,不用着急处理的中断信号,应该从INTR引脚输入。在处理器内部,根据需要,可以屏蔽掉从这个引脚来的中断信号,不对它们进行处理。因此,从INTR输入的中断信号叫作可屏蔽中断。
可屏蔽中断通常与处理器的中断控制器相关联。中断控制器是一个硬件设备,负责接收来自外部设备的中断请求,并将其传递给处理器。通过配置中断控制器,可以选择性地屏蔽特定的中断信号。
INTEL处理器允许256个中断,中断号的范围是0~255,8259中断控制器芯片负责提供其中的15个,但中断号并不固定,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括各引脚的中断号。正是因为这样,它又叫可编程中断控制器(Programmable Interrupt Controller, PIC)。
不可屏蔽中断:
相反地,所有严重事件都必须无条件地加以处理,由这类事件引发的中断信号应当通过NMI引脚送入处理器,这些严重的事件包括不间断电源的后备电池即将耗尽、内存校验错误、I/O检验错误,等等。在处理器内部,对于从NMI引脚来的中断信号不会作屏蔽和过滤,而是必须进行处理。因为这个原因,从NMI引脚来的中断信号称为非屏蔽中断(Non Maskable Interrupt, NMI)。
中断向量表
在实模式下,处理器要求将它们的入口点集中存放到内存中从物理地址0x00000开始到0x003ff结束,共1KB的空间内,这就是所谓的中断向量表(Interrupt Vector Table, IVT)。每个中断在中断向量表中占2个字,分别是中断处理程序的偏移地址和逻辑段地址。注册中断程序时,需要将中断程序的段地址和偏移地址写入中断向量表。
RTC 实时时钟
实时时钟的读写需要访问CMOS RAM,这需要通过两个端口来进行。0x70或者0x74是索引端口,用来指定CMOS RAM内的单元;0x71或者0x75是数据端口,用来读写相应单元里的内容。
偏移地址 | 内容 |
---|---|
0x00 | 秒 |
0x01 | 闹钟秒 |
0x02 | 分 |
0x03 | 闹钟分 |
0x04 | 时 |
0x05 | 脑中时 |
0x06 | 星期 |
0x07 | 日 |
0x08 | 月 |
0x09 | 年 |
0x0a | 寄存器A |
0x0b | 寄存器B |
0x0c | 寄存器C |
0x0d | 寄存器D |
实时时钟RTC电路可以产生三种中断信号,分别是:
周期性中断(Periodic Interrupt, PF):每隔一段时间重复发生一次,速度在寄存器A中可调节,是否发生在寄存器B中控制。
- 寄存器A的位6~位4用来选择外部时钟频率,而位3~位0则用来选择周期性中断信号发生的速率。
- 寄存器B的位6控制周期性中断是否允许发生。这一位是周期性中断允许位(Periodic Interrupt Enable, PIE)。如果此位是0,表示不允许周期性中断;如果是1,表示允许发生周期性中断信号。
- 更新周期结束中断(Update-ended Interrupt, UI):每隔一秒,实时时钟电路将更新CMOS RAM里面的时间和日期。在每个更新周期结束时,实时时钟电路可以发出一个中断信号,表示本次更新周期已经结束。更新周期是否会进行,是由寄存器B的位7来控制。一旦更新周期被禁止,RTC将不会自动更新时间和日期。
- 闹钟中断(Alarm Interrupt, AI):当实时时钟到达指定的闹点时,如果允许的话,将产生闹钟中断信号。闹钟中断信号是否会产生,是由寄存器B的位5来控制的,这一位叫作闹钟中断允许(Alarm Interrupt Enable, AIE)位。如果此位是0,意味着不产生闹钟中断;如果此位是1,意味着允许产生闹钟中断信号。
实时时钟芯片的中断信号通过一根线连接到8259A从片的第一个引脚IR0。在计算机启动后,BIOS程序将它的中断号初始化为0x70。要想知道中断是否发生,以及发生的是什么中断,可以通过读寄存器C来做出判断
- 寄存器C的位7是中断请求标志(Interrupt Request Flag, IRQF),如果有中断发生,则位7是1,否则是0。如果位7是1,有中断发生,则还需要判断位4、位5和位6来检查是哪种中断。对寄存器C的读操作将导致此位清零。
- 寄存器C的位6是周期性中断标志(Periodic Interrupt Flag, PF),如果此位是1,意味着发生了周期性中断;0意味着不是周期性中断。对寄存器C的读操作将导致此位清零。
- 寄存器C的位5是闹钟标志(Alarm Flag, AF),如果此位是1,意味着发生了闹钟中断;0意味着不是闹钟中断。对寄存器C的读操作将导致此位清零。
- 寄存器C的位4是更新结束标志(Update-ended Flag, UF)。如果此位是1,意味着发生了更新周期结束中断;0意味着不是更新周期结束中断。对寄存器C的读操作将导致此位清零。
- 寄存器C的低4位,即位0到位3是保留的,始终为0。注意,寄存器C是只读的,不能写入。寄存器C对读操作是敏感的,读操作将导致所有比特清零。
Nasm Code
引导程序见 NASM(五)加载用户程序及用户程序
section header align=16 vstart=0
program_length dd program_end
code_entry dw start
dd section.code.start
realloc_tb_len dw (realloc_tb_end - realloc_tb_begin) / 4
realloc_tb_begin:
code_segment dd section.code.start
data_segment dd section.data.start
stack_segment dd section.stack.start
realloc_tb_end:
section code align=16 vstart=0
; ---------flush_screen----------
; 屏幕置空
flush_screen:
push ax
push cx
push si
push es
mov ax, 0xb800
mov es, ax
mov si, 0
mov cx, 4000
mov al, ' '
mov ah, 0x07
loop_flush:
mov [es:si], ax
add si, 2
loop loop_flush
pop es
pop si
pop cx
pop ax
ret
; -------to_screen----------
to_screen:
put:
mov cl, [ds:si] ; 拿字符
cmp cl, 0
je exit
add si, 1 ; 偏移+1
push si ; 入栈保存
push ax ; 入栈保存
call put_char
pop ax
pop si
dec ax
cmp ax, 0 ; 判断数据是否传输完
jne put
exit:
ret
get_cursor:
mov dx, 0x3d4 ; 索引寄存器
mov al, 0x0e ; 0x0e光标寄存器
out dx, al ; 选择 0x0e光标寄存器, 高8位
mov dx, 0x3d5 ; 读数据
in al, dx ; 读
mov ah, al ; 放到高8位
mov dx, 0x3d4 ; 索引寄存器
mov al, 0x0f ; 0x0f光标寄存器
out dx, al ; 选择 0x0e光标寄存器, 高8位
mov dx, 0x3d5 ; 读数据
in al, dx ; 读
; mov bx, ax ; 把光标信息存储在bx中
ret
put_char: ; 接受参数cl, 用于提供要显示的ASCII码。
call get_cursor
cmp cl, 0x0d ; 判断是否是0x0d
jne check_0x0a ; 判断是否是0x0a
mov bl, 80 ; 判断行列
div bl ; 被除数存储在"AX"寄存器中 "AL"中得到的是当前行的行号。
; 商(结果的整数部分)将存储在"AL"寄存器中
; 余数(结果的小数部分)将存储在"AH"寄存器中
mul bl ; 乘数应存储在AL寄存器中
; 结果的低8位存储在"AL"寄存器中
; 结果的高8位存储在"AH"寄存器中
mov bx, ax ; 保存到bx中
jmp set_cursor
check_0x0a:
cmp cl, 0x0a
jne put_c
add bx, 80 ; 换行
cmp bx, 2000
jl set_cursor ; 屏幕只能显示2000个字符, 多余的滚行
mov ax,0xb800
mov ds,ax
mov es,ax
cld ; 清除标志位,使movsw向正方向前进
mov si,0xa0
mov di,0x00
mov cx,1920 ; 位置前挪,空出最后一行
rep movsw ; 该指令需要配合源地址寄存器(SI),目标地址寄存器(DI)和计数寄存器(CX)一起使用
; 它会根据CX寄存器中的计数值重复执行movsw指令
clean: ; 最后一行全部置为空格
mov bx, 3840 ; 屏幕上第25行第1列在显存中的偏移地址是3840
mov cx, 80
mov word[es:bx], 0x0720 ; 清除第25行的内容
add bx, 2
loop clean
mov bx,1920 ; 设置光标
set_cursor:
mov dx,0x3d4 ; 索引寄存器
mov al,0x0e ; 0x0e光标寄存器
out dx,al ; 选择0x0e光标寄存器, 高8位
mov dx,0x3d5 ; 选择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
ret
put_c:
mov ax,0xb800
mov es,ax
shl bx,1 ; bx 乘2
mov byte [es:bx], cl
inc bx
mov byte [es:bx], 0x07 ; 白字黑底
dec bx
shr bx,1 ; bx 除2
add bx,1
jmp set_cursor
; --------int70 code ----------
int70:
push ax
push bx
push cx
push dx
push es
w:
mov al, 0x0a ; 选择寄存器A
or al, 0x80 ; 阻断NMI
out 0x70, al ; 选择寄存器a
in al, 0x71 ; 读寄存器a
test al, 0x80 ; 测试最高位是否为1,是否处更新周期在更新周期中
jnz w ; 处在更新周期中,则等待
mov cx, 3
mov bl, 0
read_rtc:
mov al, bl
add bl, 2
or al, 0x80
out 0x70, al ; 读秒,分,时
in al, 0x71
push ax
loop read_rtc
mov al, 0x0c
out 0x70, al ; 告诉RTC,中断已经得到处理,可以继续下一次中断。
in al, 0x71 ; 读一下寄存器c,把标志位清0
mov ax, 0xb800 ; 显示时间
mov es, ax
mov bx, 12 * 160 + 36 * 2 ; 从屏幕上的12行36列开始显示
mov cx, 3
show_rtc:
pop ax
call BCD2ASCII
mov [es:bx], ah
mov [es:bx + 2], al
mov al, ':'
mov [es:bx + 4], al
add bx, 6
loop show_rtc
mov al, ' ' ; 消去最后一个':'
mov [es:bx - 2], al
; 在8259芯片内部,有一个中断服务寄存器(Interrupt Service Register, ISR)
; 这是一个8位寄存器,每一位都对应着一个中断输入引脚。
; 当中断处理过程开始时,8259芯片会将相应的位置1,表明正在服务从该引脚来的中断。
; 一旦响应了中断,8259中断控制器无法知道该中断什么时候才能处理结束。
; 同时,如果不清除相应的位,下次从同一个引脚出现的中断将得不到处理。
; 在这种情况下,需要程序在中断处理过程的结尾,显式地对8259芯片编程来清除该标志。
; 方法是向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
BCD2ASCII:
mov ah, al
and al, 0x0f
add al, 0x30
shr ah, 4
and ah, 0x0f
add ah, 0x30
ret
; --------int70 set-----------
int70_set:
mov al, 0x70
mov bl, 4
mul bl
mov bx, ax ; 计算在实模式下0x70号中断在中断向量表中的偏移
cli ; 关闭中断
mov ax, 0
mov es, ax
mov word [es:bx], int70 ; 写入偏移地址
mov ax, cs ; 注意不要用[code_segment],因为ds已经指向该程序数据段,而不是程序开头
mov word [es:bx + 2], ax ; 写入段地址
mov al, 0x0b
or al, 0x80
out 0x70, al
mov al, 0x12 ; 禁止周期性中断,开发更新结束后中断,BCD码,24h制
out 0x71, al
mov al, 0x0c ; 选择c寄存器
out 0x70, al
in al, 0x71 ; 复位未决的中断状态
in al, 0xa1 ; 读8259从片的IMR寄存器
and al, 0xfe ; 清除bit 0(此位连接RTC)
out 0xa1, al
sti
ret
; --------------------------
start:
mov ax, [stack_segment] ; 引导程序现在使ds指向程序开头,先更新ss
mov ss, ax
mov sp, stack_pointer
mov ax, [data_segment]
mov ds, ax
call flush_screen
mov bx, 0 ; 光标存储在bx中,先初始化到0处
call set_cursor
mov si, Init_msg
call to_screen
mov si, inst_msg
call to_screen
push bx
call int70_set
pop bx
mov si, Done_msg
call to_screen
mov si, Tips_msg
call to_screen
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
section data align=16 vstart=0
Init_msg db 'Starting...',0x0d,0x0a,0
inst_msg db 'Installing a new interrupt 70H...',0x0d,0x0a,0
Done_msg db 'Done.',0x0d,0x0a,0
Tips_msg db 'Clock is now working.',0
section stack align=16 vstart=0
resb 256
stack_pointer:
section trail align=16
program_end:
Comments | NOTHING