DellaOS内核程序篇——初级中断处理
本文参照《Orange’S:一个操作系统的实现》与《一个64位操作系统的设计与实现》实现(部分内容有删改),如有描述不清或错误处,请阅读原著或联系本人
在学习过程中,由于操作系统知识过于庞大,我们奉行”懒加载“的原则,用到什么学习什么
2021-12.15
1、中断处理
中断大多是由外部硬件设备(如鼠标、键盘、硬盘、光驱等)产生,并向处理器发送事件请求信号。中断请求信号可能是关于数据读写操作的,也可能是关于对外部设备控制的。由于Intel处理器只有一个外部中断引脚INTR,为了使处理器能够同时接收多个硬件设备发送来的中断请求信号,特将所有外部设备的中断请求信号汇总到中断控制器,再经由中断控制器的仲裁后,有选择性地将中断请求信号依次发往处理器的外部中断引脚INTR。
在多核处理器出现之前,8259A PIC(Programmable Interrupt Controller,可编程中断控制器)是PC机使用最为普遍的中断控制器,在很多教科书和资料中经常会提及它。自从多核处理器面世后,8259A PIC对多核处理器的支持已逐渐变得力不从心,从而出现了后来的APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cAjhWGO-1639554370292)(C:\Users\xumeng03\AppData\Roaming\Typora\typora-user-images\image-20211215111150127.png)]
在两个8259A芯片级联的过程中,一个8259A中断控制器作为主芯片,与CPU的INTR引脚相连;另一个8259A中断控制器作为从芯片,与主8259A芯片的IR2引脚相连。其他中断请求引脚IR将与外部硬件设备的中断请求引脚相连。
8259A | PIN | 典型中断请求源 |
---|
主芯片 | IR0 | timer时钟 | | IR1 | 键盘 | | IR2 | 级联从8259A芯片 | | IR3 | 串口2 | | IR4 | 串口1 | | IR5 | 并口2 | | IR6 | 软盘驱动器 | | IR7 | 并口1 | 从芯片 | IR0 | CMOS RTC实时时钟 | | IR1 | 重定向到主8259A芯片的IR2引脚 | | IR2 | 保留 | | IR3 | 保留 | | IR4 | PS/2鼠标 | | IR5 | 协处理器 | | IR6 | SATA主硬盘 | | IR7 | SATA从硬盘 |
8259A被叫作可编程中断控制器,那么它必定拥有一系列可配置的寄存器。一个8259A PIC包含两组寄存器,分别是ICW(Initialization Command Word,初始化命令字)寄存器组和OCW(Operational Control Word,操作控制字)寄存器组。其中,ICW寄存器组用于初始化中断控制器,在8259A芯片正常工作前必须先对ICW寄存器组进行设置;OCW寄存器组用于操作中断控制器,可随时通过OCW寄存器组设置和管理中断控制器的工作方式
PC机采用I/O地址映射方式,将8259A PIC的寄存器映射到I/O端口地址空间,因此必须借助IN和OUT汇编指令才能访问8259A PIC。主8259A芯片的I/O端口地址是20h和21h,从8259A芯片的I/O端口地址是A0h和A1h
2、配置ICW寄存器组和OCW寄存器组
ICW寄存器组共包含ICW1、ICW2、ICW3、ICW4四个寄存器,它们必须按照从ICW1到ICW4的顺序进行初始化。主8259A芯片的ICW1寄存器映射到I/O端口20h地址处,ICW2、ICW3、ICW4寄存器映射到I/O端口21h地址处;从8259A芯片的ICW1寄存器映射到I/O端口A0h地址处,ICW2、ICW3、ICW4寄存器映射到I/O端口A1h地址处。对主/从8259A芯片的初始化顺序可以是先后式的(先配置主芯片的ICW寄存器组,再配置从芯片的ICW寄存器组)或交替式的(先配置主/从芯片的ICW1寄存器,再设置主/从芯片的ICW2寄存器,依次类推直至ICW4寄存器)。
2.1、初始化命令字ICW
ICW1寄存器
位 | 描述 |
---|
5~7 | 对于PC机,该位必须为0 | 4 | 对于ICW,该位必须为1 | 3 | 触发模式,现已忽略,必须为0 | 2 | 忽略,必须为0 | 1 | 1=单片8259A,0=级联8259A | 0 | 1=使用ICW4,0=不使用ICW4 |
主/从8259A芯片的ICW1寄存器都固定初始化为00010001B(11h)
ICW2寄存器
对于中断向量号并没有特殊要求,通常情况下,主8259A芯片的中断向量号设置为20h(占用中断向量号20h27h),从8259A芯片的中断向量号设置为28h(占用中断向量号28h2fh)
ICW3寄存器
主
位 | 描述 |
---|
7 | 1=IR7级联从芯片,0=无从芯片 | 6 | 1=IR6级联从芯片,0=无从芯片 | 5 | 1=IR5级联从芯片,0=无从芯片 | 4 | 1=IR4级联从芯片,0=无从芯片 | 3 | 1=IR3级联从芯片,0=无从芯片 | 2 | 1=IR2级联从芯片,0=无从芯片 | 1 | 1=IR1级联从芯片,0=无从芯片 | 0 | 1=IR0级联从芯片,0=无从芯片 |
从
位 | 描述 |
---|
3~7 | 必须为0 | 0~2 | 从芯片连接到主芯片的IR引脚号 |
ICW4寄存器
位 | 描述 |
---|
5~7 | 恒为0 | 4 | 1=SFNM模式,0=FNM模式 | 3、2 | 缓冲模式:00=无缓冲模式,10=从芯片缓冲模式,11=主芯片缓冲模式 | 1 | 1=AEOI模式,0=EOI模式 | 0 | 1=8086/88模式,0=MCS 80/85模式 |
- AEOI模式:此模式可使中断控制器收到CPU发送来的第2个INTA中断响应脉冲后,自动复位ISR寄存器的对应位。
- EOI模式:在EOI模式下,处理器执行完中断处理程序后,必须手动向中断控制器发送中断结束
EOI 指令,来复位ISR寄存器的对应位。 - FNM(Fully Nested Mode,全嵌套模式):在此模式下,中断请求的优先级按引脚名从高到低依次为IR0~IR7。如果从8259A芯片的中断请求正在被处理,那么该从芯片将被主芯片屏蔽直至处理结束,即使从芯片产生更高优先级的中断请求也不会得到执行。
- SFNM(Special Fully Nested Mode,特殊全嵌套模式):该模式与FNM基本相同,不同点是当从芯片的中断请求正在被处理时,主芯片不会屏蔽该从芯片,这使得主芯片可以接收来自从芯片的更高优先级中断请求。在中断处理程序返回时,需要先向从芯片发送
EOI 命令,并检测从芯片的ISR寄存器值,如果ISR寄存器仍有其他中断请求,则无需向主芯片发送EOI 命令。
2.2、操作控制字OCW
OCW1寄存器
位 | 描述 |
---|
7 | 1=屏蔽IRQ7中断请求,0=允许IRQ7中断请求 | 6 | 1=屏蔽IRQ6中断请求,0=允许IRQ6中断请求 | 5 | 1=屏蔽IRQ5中断请求,0=允许IRQ5中断请求 | 4 | 1=屏蔽IRQ4中断请求,0=允许IRQ4中断请求 | 3 | 1=屏蔽IRQ3中断请求,0=允许IRQ3中断请求 | 2 | 1=屏蔽IRQ2中断请求,0=允许IRQ2中断请求 | 1 | 1=屏蔽IRQ1中断请求,0=允许IRQ1中断请求 | 0 | 1=屏蔽IRQ0中断请求,0=允许IRQ0中断请求 |
OCW2寄存器
位 | 描述 |
---|
7 | 优先级循环状态 | 6 | 特殊设定标志 | 5 | 非自动结束标志 | 4 | 恒为0 | 3 | 恒为0 | 0~2 | 优先级设定 |
OCW2寄存器的D5~D7位组合值含义
D7 | D6 | D5 | 含义 |
---|
0 | 0 | 0 | 循环AEOI模式(清除) | 0 | 0 | 1 | 非特殊EOI 命令(全嵌套方式) | 0 | 1 | 0 | 无操作 | 0 | 1 | 1 | 特殊EOI 命令(非全嵌套方式) | 1 | 0 | 0 | 循环AEOI模式(设置) | 1 | 0 | 1 | 循环非特殊EOI 命令 | 1 | 1 | 0 | 设置优先级命令 | 1 | 1 | 1 | 循环特殊EOI 命令 |
OCW3寄存器
位 | 描述 |
---|
7 | 恒为0 | 6、5 | 特殊屏蔽模式:11=开启特殊屏蔽,10=关闭特殊屏蔽 | 4 | 恒为0 | 3 | 恒为1 | 2 | 1=轮询,0=无查询 | 1、0 | 10=读IRR寄存器,11=读ISR寄存器 |
2.3、初始化后数据
主/从8259A | ICWx | I/O端口 | 数值 |
---|
主8259A | ICW1 | 20h | 11h | | ICW2 | 21h | 20h | | ICW3 | 21h | 04h | | ICW4 | 21h | 01h | 从8259A | ICW1 | A0h | 11h | | ICW2 | A1h | 28h | | ICW3 | A1h | 02h | | ICW4 | A1h | 01h |
3、键盘驱动
键盘设备也是拥有控制芯片的,如果希望键盘可以持续接收到按键中断请求,则必须对键盘控制器芯片有所了解才能实现
目前,市面上的键盘控制器芯片大多采用Intel 8042以及兼容芯片,键盘控制器芯片通过PS/2接口(或一些USB接口)与外部键盘设备相连。键盘设备通常会包含一个Intel 8048或兼容芯片,这个芯片会时刻扫描键盘设备的每个按键,并将扫描到的按键进行编码,每个按键的编码是唯一的,不会重复,所以8048芯片也被称为键盘编码芯片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bp22vpZJ-1639554370294)(C:\Users\xumeng03\AppData\Roaming\Typora\typora-user-images\image-20211215150441829.png)]
当8048键盘编码器扫描到按键被按下时,它会将按键对应的编码值通过PS/2接口发送到8042键盘控制器芯片中。8042键盘控制器在接收到编码值后,会将其解析并转换成统一的键盘扫描码(第1套XT扫描码集),并存放到输出缓冲区中待处理器读取。如果此时键盘又有新的键被按下,8042芯片将不再接收新的数据,直至输出缓冲区被清空后,8042芯片才会继续接收按键编码数据。
键盘扫描码一共有3套:第1套为原始的XT扫描码集;第2套为AT扫描码集;第3套为PS/2扫描码集(很少使用)。现在,键盘皆默认使用第2套AT键盘扫描码,但是出于兼容性考虑,第2套扫描码最终都转换成第1套XT扫描码集供处理器使用(也可以设置成不转换成第1套XT扫描码集,但这样需要另作特殊配置)
第1套扫描码的特点是,每个按键扫描码由1 B数据组成,这1 B数据的低7位(位6到位0)代表按键的扫描码,最高位(位7)代表按键的状态(0:按下,1:松开)。当某个键被按下时,键盘控制器输出的扫描码叫作Make Code码,而松开按键时的扫描码则叫作Break Code码。例如,按键B的Make Code码是0x30 ,则它的Break Code码便是0xb0
4、实现
4.1、目录结构
├── a.img
├── boot
│ ├── boot.asm
│ ├── fat12.inc
│ └── loader.asm
├── kernel
│ ├── head.S
│ ├── entry.S
│ ├── entry.S
│ ├── font.h
│ ├── printf.h
│ ├── printf.c
│ ├── lib.h
│ ├── string.h
│ ├── trap.h
│ ├── trap.c
│ ├── memory.h
│ ├── memory.c
│ ├── interrupt.h
│ ├── interrupt.c
│ ├── Kernel.lds
│ └── main.c
├── clear.sh
└── run.sh
interrupt.h
#ifndef __INTERRUPT_H__
#define __INTERRUPT_H__
void init_interrupt();
void do_IRQ(unsigned long regs,unsigned long nr);
#endif
interrupt.c
#include "interrupt.h"
#include "lib.h"
#include "printf.h"
#include "memory.h"
#include "gate.h"
#define SYMBOL_NAME_STR(X) #X
#define IRQ_NAME(nr) nr##_interrupt(void)
#define IRQ_NA(nr) IRQ_NAME(IRQ##nr)
#define SAVE_ALL \
"cld; \n\t" \
"pushq %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %es, %rax; \n\t" \
"pushq %rax; \n\t" \
"movq %ds, %rax; \n\t" \
"pushq %rax; \n\t" \
"xorq %rax, %rax; \n\t" \
"pushq %rbp; \n\t" \
"pushq %rdi; \n\t" \
"pushq %rsi; \n\t" \
"pushq %rdx; \n\t" \
"pushq %rcx; \n\t" \
"pushq %rbx; \n\t" \
"pushq %r8; \n\t" \
"pushq %r9; \n\t" \
"pushq %r10; \n\t" \
"pushq %r11; \n\t" \
"pushq %r12; \n\t" \
"pushq %r13; \n\t" \
"pushq %r14; \n\t" \
"pushq %r15; \n\t" \
"movq $0x10, %rdx; \n\t" \
"movq %rdx, %ds; \n\t" \
"movq %rdx, %es; \n\t"
#define Build_IRQ(nr) \
void IRQ_NA(nr); \
__asm__ ( SYMBOL_NAME_STR(IRQ)#nr"_interrupt: \n\t" \
"pushq $0x00 \n\t" \
SAVE_ALL \
"movq %rsp, %rdi \n\t" \
"leaq ret_from_intr(%rip), %rax \n\t" \
"pushq %rax \n\t" \
"movq $"#nr", %rsi \n\t" \
"jmp do_IRQ \n\t");
Build_IRQ(0x20)
Build_IRQ(0x21)
Build_IRQ(0x22)
Build_IRQ(0x23)
Build_IRQ(0x24)
Build_IRQ(0x25)
Build_IRQ(0x26)
Build_IRQ(0x27)
Build_IRQ(0x28)
Build_IRQ(0x29)
Build_IRQ(0x2a)
Build_IRQ(0x2b)
Build_IRQ(0x2c)
Build_IRQ(0x2d)
Build_IRQ(0x2e)
Build_IRQ(0x2f)
Build_IRQ(0x30)
Build_IRQ(0x31)
Build_IRQ(0x32)
Build_IRQ(0x33)
Build_IRQ(0x34)
Build_IRQ(0x35)
Build_IRQ(0x36)
Build_IRQ(0x37)
void (* interrupt[24])(void)=
{
IRQ0x20_interrupt,
IRQ0x21_interrupt,
IRQ0x22_interrupt,
IRQ0x23_interrupt,
IRQ0x24_interrupt,
IRQ0x25_interrupt,
IRQ0x26_interrupt,
IRQ0x27_interrupt,
IRQ0x28_interrupt,
IRQ0x29_interrupt,
IRQ0x2a_interrupt,
IRQ0x2b_interrupt,
IRQ0x2c_interrupt,
IRQ0x2d_interrupt,
IRQ0x2e_interrupt,
IRQ0x2f_interrupt,
IRQ0x30_interrupt,
IRQ0x31_interrupt,
IRQ0x32_interrupt,
IRQ0x33_interrupt,
IRQ0x34_interrupt,
IRQ0x35_interrupt,
IRQ0x36_interrupt,
IRQ0x37_interrupt,
};
void init_interrupt()
{
int i;
for(i = 32;i < 56;i++)
{
set_intr_gate(i , 2 , interrupt[i - 32]);
}
Printf("8259A init \n");
io_out8(0x20,0x11);
io_out8(0x21,0x20);
io_out8(0x21,0x04);
io_out8(0x21,0x01);
io_out8(0xa0,0x11);
io_out8(0xa1,0x28);
io_out8(0xa1,0x02);
io_out8(0xa1,0x01);
io_out8(0x21,0xfd);
io_out8(0xa1,0xff);
sti();
}
void do_IRQ(unsigned long regs,unsigned long nr)
{
unsigned char x;
Printf("do_IRQ:%#08x ",nr);
x = io_in8(0x60);
Printf("key code:%#08x\n",x);
io_out8(0x20,0x20);
}
|