day7
获取按键编码(hiarib04a)
c
void inthandler21(int *esp)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; // 获取系统启动信息结构体指针
unsigned char data, s[4]; // data: 键盘数据缓存,s: 格式化字符串缓存
io_out8(PIC0_OCW2, 0x61); // 发送EOI命令(0x61)通知PIC中断处理完成
// 具体说明:
// 1. PIC0_OCW2 是主PIC的操作命令字寄存器(端口地址0x20)
// 2. 0x61 的二进制形式是 01100001,其中:
// - 高三位 011 表示「指定IRQ级别的EOI命令」
// - 低五位 00001 表示IRQ1(键盘中断)
// 3. 该操作完成两个功能:
// a. 清除PIC的中断服务寄存器(ISR)对应位
// b. 允许PIC继续接收新的中断请求
data = io_in8(PORT_KEYDAT); // 从键盘数据端口读取扫描码
sprintf(s, "%02X", data); // 将扫描码转为16进制字符串
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); // 清空显示区域(青灰色背景)
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); // 显示白色文本的扫描码
return;
}
这句话用来通知PIC"已经知道发生了IRQ1中断哦"。如果是IRQ3,则写成0x63。也就是说,将"0x60+IRQ号码"输出给OCW2就可以。执行这句话之后,PIC继续时刻监视IRQ1中断是否发生。
如果忘记了执行这句话,PIC就不再监视IRQ1中断,不管下次由键盘输入什么信息,系统都感知不到了。
从编号为0x0060的设备输入的8位信息是按键编码。编号为0x0060的设备就是键盘。
加快中断处理(hiarib04b)
c
struct KEYBUF {
unsigned char data, flag;
};
#define PORT_KEYDAT 0x0060
struct KEYBUF keybuf;
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61);
data = io_in8(PORT_KEYDAT);
if (keybuf.flag == 0) {
keybuf.data = data;
keybuf.flag = 1;
}
return;
}
for (;;) {
io_cli();
//如果flag是0,
//表示缓冲区为空;如果flag是1,就表示缓冲区中存有数据
if (keybuf.flag == 0) {
io_stihlt();
} else {
i = keybuf.data;
keybuf.flag = 0;
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
}
}
io_sti();io_hlt();不等于io_stihlt();
如果io_sti()之后产生了中断,keybuf里就会存入数据,这时候让CPU进入HLT状态,keybuf里存入的数据就不会被觉察到。根据CPU的规范,机器语言的STI指令之后,如果紧跟着HLT指令,那么就暂不受理这两条指令之间的中断,而要等到HLT指令之后才受理,
缓冲区建立优点:
- 异步处理机制:键盘中断(IRQ1)发生时需要立即响应,但此时系统可能正在处理其他任务。缓冲区作为数据的中转站,允许中断处理程序快速保存数据后立即返回
- 防止数据丢失:当用户快速连续按键时,中断可能连续触发。缓冲区可以存储多个按键数据(虽然当前实现是单缓冲区,后续可以扩展为队列)
- 线程安全:通过flag标志位(keybuf.flag)实现简单的同步机制,避免主程序读取数据时与中断程序发生竞争
- 解耦硬件操作:将底层硬件读取(io_in8)与上层逻辑处理分离,提高代码的可维护性
自此中断处理和调用流程
- 中断向量表初始化 (在
int.c
的 PIC 初始化中)
c
int.
// PIC初始化时设置中断向量号
io_out8(PIC0_ICW2, 0x20); // IRQ0-7对应INT 0x20-0x27
io_out8(PIC1_ICW2, 0x28); // IRQ8-15对应INT 0x28-0x2f
- 汇编层中断门处理 (在
naskfunc.nas
中实现)
nasm
naskfunc.nas
_asm_inthandler21: ; 对应键盘中断(IRQ1)
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21 ; 调用C语言处理函数
POP EAX
POPAD
POP DS
POP ES
IRETD
- C语言中断服务程序 (在
int.c
中实现)
c
int.c
// 键盘中断处理程序
void inthandler21(int *esp) {
unsigned char data;
io_out8(PIC0_OCW2, 0x61); // 通知PIC中断处理完成
data = io_in8(PORT_KEYDAT); // 读取键盘数据
// ... 缓冲区处理 ...
}
完整的中断调用流程:
- 硬件中断触发 → 2. CPU查IDT表 → 3. 跳转至_asm_inthandlerXX → 4. 保存上下文 → 5. 调用C处理函数 → 6. 恢复上下文 → 7. IRETD返回
制作FIFO 缓冲区(hiarib04c)
就是将缓冲区的数据部分弄成链表,但这里是静态数组,到下面整理缓冲区就是动态分配内存了
c
struct KEYBUF {
unsigned char data[32];
int next;
};
c
void inthandler21(int *esp)
{
unsigned char data;
io_out8(PIC0_OCW2, 0x61);
data = io_in8(PORT_KEYDAT);
if (keybuf.next < 32) {
keybuf.data[keybuf.next] = data;
keybuf.next++;
}
return;
}
c
for (;;) {
io_cli();
if (keybuf.next == 0) {
io_stihlt();
} else {
i = keybuf.data[0];
keybuf.next--;
for (j = 0; j < keybuf.next; j++) {
keybuf.data[j] = keybuf.data[j + 1];
}
io_sti();
sprintf(s, "%02X", i);
boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
}
}
但这个程序,中断处理的时候涉及数据移送操作,如果在数据移送前中断的话,数据就会乱
所以下面就改善一下
改善FIFO 缓冲区(hiarib04d)
其实就是一个双指针,一个读数据指针,一个写数据指针
写的时候直接覆盖写,到了len最大容量就归0
整理FIFO 缓冲区(hiarib04e)
c
struct FIFO8 {
unsigned char *buf;
int p, q, size, free, flags;
};
/*可变缓冲区,size存入缓冲区的总字节数,变量free保存缓冲区里美欧数据的字节数,
buf存缓冲区的地址,p代表下一个数据写入地址(next_w),q代表下一个数据读出地址
(next_r)。
*/
c
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)
/* 初始化FIFO缓冲区 */
{
fifo->size = size;
fifo->buf = buf;
fifo->free = size; /* 缓冲区的大小 */
fifo->flags = 0;
fifo->p = 0; /* 下一个数据写入位置 */
fifo->q = 0; /* 下一个数据读出位置 */
return;
}
c
#define FLAGS_OVERRUN 0x0001
int fifo8_put(struct FIFO8 *fifo, unsigned char data)
/* 向FIFO传送数据并保存 */
{
if (fifo->free == 0) {
/* 空余没有了,溢出 */
fifo->flags |= FLAGS_OVERRUN;
return -1;
}
fifo->buf[fifo->p] = data;
fifo->p++;
if (fifo->p == fifo->size) {
fifo->p = 0;
}
fifo->free--;
return 0;
}
c
int fifo8_get(struct FIFO8 *fifo)
/* 从FIFO取得一个数据 */
{
int data;
if (fifo->free == fifo->size) {
/* 如果缓冲区为空,则返回 -1 */
return -1;
}
data = fifo->buf[fifo->q];
fifo->q++;
if (fifo->q == fifo->size) {
fifo->q = 0;
}
fifo->free++;
return data;
}
c
int fifo8_status(struct FIFO8 *fifo)
/* 报告一下到底积攒了多少数据 */
{
return fifo->size - fifo->free;
}
鼠标(harib04f)
c
#define PORT_KEYDAT 0x0060
#define PORT_KEYSTA 0x0064
#define PORT_KEYCMD 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
void wait_KBC_sendready(void)
{
/* 等待键盘控制电路准备完毕 */
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
//键盘控制电路(keyboard controller, KBC)做好准备动作,等待控制指令的到来。
//因为虽然CPU的电路很快,但键盘控制电路却没有那么快。
//如果键盘控制电路可以接受CPU指令了,CPU从设备号码0x0064处所读
//取的数据的倒数第二位(从低位开始数的第二位)应该是0。在确认到这一位是0之前,程序一直通过for语句循环查询。
}
void init_keyboard(void)
{
/* 初始化键盘控制电路
一边确认可否往键盘
控制电路传送信息,一边发送模式设定指令,指令中包含着要设定为何种模式。模
式设定的指令是0x60,利用鼠标模式的模式号码是0x47,*/
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE);
return;
}
键盘控制器(Keyboard Controller, KBC)
i8042 键盘控制器-------详细介绍 - LinKArftc - 博客园 (cnblogs.com)
在x86架构中,键盘控制器(Keyboard Controller, KBC)通过特定端口与CPU通信。以下是代码中涉及的端口地址、宏定义和函数的详细解释:
一、端口地址定义
宏定义 | 端口地址 | 功能描述 |
---|---|---|
PORT_KEYDAT |
0x0060 |
键盘数据端口:用于读取键盘扫描码或发送键盘配置参数。 |
PORT_KEYSTA |
0x0064 |
键盘状态端口:读取键盘控制器的状态(如是否准备好接收命令)。 |
PORT_KEYCMD |
0x0064 |
键盘命令端口:向键盘控制器发送控制命令(与状态端口共享地址,操作区分方向)。 |
二、关键宏定义
宏定义 | 值 | 功能描述 |
---|---|---|
KEYSTA_SEND_NOTREADY |
0x02 |
状态寄存器中的"发送未就绪"标志位:若该位为1,表示控制器忙,不能接收新命令。 |
KEYCMD_WRITE_MODE |
0x60 |
键盘控制器的命令:写入操作模式到配置字节。 |
KBC_MODE |
0x47 |
键盘控制器的工作模式参数:启用键盘和鼠标接口,并设置扫描码转换模式。 |
三、函数解析
1. wait_KBC_sendready()
c
void wait_KBC_sendready(void) {
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
- 功能:等待键盘控制器准备好接收命令。
- 实现 :
- 循环读取状态端口(
0x0064
)。 - 检查状态寄存器的第1位(
KEYSTA_SEND_NOTREADY
):- 若为0,表示控制器就绪,退出循环。
- 若为1,继续等待。
- 循环读取状态端口(
2. init_keyboard()
c
void init_keyboard(void) {
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE); // 发送模式设置命令
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE); // 写入模式参数
return;
}
- 功能:初始化键盘控制器的工作模式。
- 流程 :
- 等待控制器就绪。
- 向命令端口(
0x0064
)发送命令0x60
(KEYCMD_WRITE_MODE
),表示要写入配置字节。 - 再次等待控制器就绪。
- 向数据端口(
0x0060
)写入模式参数0x47
(KBC_MODE
),配置控制器行为。
四、配置字节 0x47
的位定义
位 | 名称 | 功能 |
---|---|---|
7 | Reserved | 保留位,必须设为0。 |
6 | Translation Mode | 1=启用扫描码转换(将XT键盘扫描码集1转换为AT键盘扫描码集2)。 |
5 | Second Port Clock | 0=启用鼠标接口(PS/2 Port 2)。 |
4 | First Port Clock | 0=启用键盘接口(PS/2 Port 1)。 |
3 | Ignore Lock Keys | 0=正常处理锁定键(如Caps Lock)。 |
2 | System Flag | 由BIOS设置的系统标志(通常设为0)。 |
1 | Second Port IRQ | 1=启用鼠标中断(IRQ12)。 |
0 | First Port IRQ | 1=启用键盘中断(IRQ1)。 |
0x47
(二进制 01000111
)的具体配置
位 | 值 | 作用 |
---|---|---|
7 | 0 | 保留位为0,符合规范。 |
6 | 1 | 启用扫描码转换(将XT扫描码转换为AT扫描码,增强兼容性)。 |
5 | 0 | 启用鼠标接口(允许鼠标数据传输)。 |
4 | 0 | 启用键盘接口(允许键盘数据传输)。 |
3 | 0 | 正常处理Caps Lock等锁定键。 |
2 | 0 | 系统标志为0(通常由BIOS设置)。 |
1 | 1 | 启用鼠标中断(IRQ12),允许鼠标事件触发中断。 |
0 | 1 | 启用键盘中断(IRQ1),允许键盘事件触发中断。 |
五、硬件交互原理
- 端口I/O操作 :
io_in8(port)
:从指定端口读取1字节数据。io_out8(port, data)
:向指定端口写入1字节数据。- 在x86中,端口地址空间独立于内存地址空间,需通过
IN
/OUT
指令访问。
- 键盘控制器流程 :
- 发送命令 :向
PORT_KEYCMD
(0x0064
)写入命令字节。 - 写入参数 :向
PORT_KEYDAT
(0x0060
)写入命令参数(如模式字节)。 - 状态检查 :从
PORT_KEYSTA
(0x0064
)读取状态,确保操作安全。
- 发送命令 :向
六、代码的意义
这段代码的目标是配置键盘控制器以支持键盘和鼠标输入,具体作用包括:
- 启用键盘接口(允许接收按键扫描码)。
- 启用鼠标接口(为后续鼠标驱动做准备)。
- 设置扫描码转换模式,确保兼容性。
七、实际应用场景
- 操作系统启动初期 :在初始化输入设备时调用
init_keyboard()
,确保键盘可用。 - 外设驱动开发:理解端口通信是编写键盘/鼠标驱动的基础。
总结
这段代码通过操作键盘控制器的端口,配置其工作模式,为后续处理键盘和鼠标输入奠定基础。理解端口地址、状态标志和配置字节的细节,是开发操作系统输入子系统的关键一步。
发送鼠标激活指令
c
#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
void enable_mouse(void)
{
/* 激活鼠标 */
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
return; /* 顺利的话,键盘控制其会返送回ACK(0xfa)*/
}
在x86架构中,PS/2键盘控制器(KBC)负责管理键盘和鼠标的通信。以下是代码的逐层解析:
一、关键宏定义
宏定义 | 值 | 功能描述 |
---|---|---|
KEYCMD_SENDTO_MOUSE |
0xD4 |
键盘控制器命令:指示下一个写入数据端口的字节将发送到鼠标(而非键盘)。 |
MOUSECMD_ENABLE |
0xF4 |
鼠标命令:启用鼠标数据报告模式(开始发送移动/按键事件)。 |
二、代码流程分析
函数 enable_mouse()
c
void enable_mouse(void) {
wait_KBC_sendready(); // 等待KBC就绪
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); // 发送命令0xD4到命令端口(0x64)
wait_KBC_sendready(); // 再次等待KBC就绪
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE); // 发送命令0xF4到数据端口(0x60)
return; // 正常情况下,KBC会返回ACK(0xFA)
}
三、分步详解
1. wait_KBC_sendready()
- 作用:确保键盘控制器(KBC)可接收新命令。
- 实现 :循环读取状态端口(
0x64
),直到状态寄存器的"发送未就绪"位(KEYSTA_SEND_NOTREADY=0x02
)为0。
2. 发送命令 0xD4
到命令端口(0x64
)
- 目的 :通知KBC,下一个写入数据端口(
0x60
)的字节是发送给鼠标的指令(而非键盘)。 - PS/2协议:键盘和鼠标共享同一控制器,通过此命令区分目标设备。
3. 发送命令 0xF4
到数据端口(0x60
)
- 作用:向鼠标发送"启用数据报告"指令。此后,鼠标将开始发送移动和按键事件数据包。
- 响应 :鼠标应返回
0xFA
(ACK确认),表示指令已接收。
四、硬件交互逻辑
-
KBC与设备的通信链
CPU → KBC命令端口(0x64) → 发送命令0xD4 → KBC → 转发到鼠标 CPU → KBC数据端口(0x60) → 发送数据0xF4 → KBC → 转发到鼠标
-
ACK确认机制
- 鼠标收到有效指令后,会通过KBC返回
0xFA
(需通过中断或轮询读取数据端口获取)。 - 此代码未处理ACK,可能依赖后续中断服务程序(ISR)处理。
- 鼠标收到有效指令后,会通过KBC返回
五、技术细节
1. 键盘控制器状态寄存器(PORT_KEYSTA=0x64)
位 | 名称 | 说明 |
---|---|---|
0 | Output Buffer Full | 1=数据端口(0x60)有数据待读取。 |
1 | Input Buffer Full | 1=控制器忙,不能接收命令(代码检查此位)。 |
2-7 | 保留 |
2. 鼠标命令 0xF4
的作用
-
启用数据报告模式:鼠标持续发送数据包,格式为3字节:
Byte 1: Y溢出 | X溢出 | Y符号位 | X符号位 | 保留 | 中键 | 右键 | 左键 Byte 2: X轴移动量(补码) Byte 3: Y轴移动量(补码)
六、实际应用场景
- 操作系统初始化阶段:在键盘初始化后调用此函数,激活鼠标输入。
- 驱动程序开发:理解PS/2协议是处理传统输入设备的基础。
七、注意事项
- ACK处理 :需在中断处理中捕获
0xFA
,否则可能错过鼠标数据。 - 兼容性:现代硬件可能使用USB替代PS/2,需额外判断设备存在性。
- 错误处理:未收到ACK时需重试或报错。
总结
此代码通过键盘控制器向鼠标发送启用指令,使其进入数据报告模式。核心步骤为:
- 等待控制器就绪 → 2. 声明目标为鼠标 → 3. 发送启用命令。
理解PS/2协议和端口I/O操作是开发输入设备驱动的关键。
PS/2协议解释
PS/2协议是早期计算机用于连接键盘和鼠标的通信标准,尽管逐渐被USB取代,但在许多传统系统和嵌入式设备中仍有应用。以下是其核心机制和关键细节的详解:
一、物理接口与电气特性
-
接口定义
-
PS/2接口为6针Mini-DIN,实际使用4针:
引脚 功能 1 数据线(DATA) 3 地线(GND) 4 VCC(+5V) 5 时钟线(CLK)
-
-
通信方式
- 同步串行传输:设备(键盘/鼠标)与主机(键盘控制器)通过CLK同步,DATA线传输数据。
- 主从模式:默认设备控制CLK,主机可接管时钟(如发送命令时)。
二、数据帧格式
每帧数据包含 11位,按顺序如下:
位 | 名称 | 值 | 说明 |
---|---|---|---|
1 | 起始位 | 0 | 标志数据开始 |
8 | 数据位 | 0/1 | LSB(最低位)优先发送 |
1 | 奇偶校验位 | 0/1 | 奇校验(数据位+校验位的1个数为奇) |
1 | 停止位 | 1 | 标志数据结束 |
示例 :键盘发送按键按下码 0x1C
(字母'A'的扫描码)的帧结构:
0
0011100
1
1
→ 起始位 + 数据(二进制0011100,LSB优先为0011100
) + 奇偶位 + 停止位。
三、通信方向与流程
1. 设备到主机(如按键事件)
- 设备检测到动作(如按键按下),生成中断(IRQ1键盘/IRQ12鼠标)。
- 设备拉低CLK并发送数据帧。
- 主机读取DATA线,在CLK下降沿采样数据位。
2. 主机到设备(如发送命令)
- 主机拉低CLK至少100μs,通知设备准备接收命令。
- 主机控制CLK,逐位发送命令帧。
- 设备在接收到命令后,返回ACK(
0xFA
)或错误码。
四、关键命令与响应
1. 键盘常用命令
命令 | 值 | 功能 | 响应 |
---|---|---|---|
重置 | 0xFF |
复位键盘 | 0xFA + 0xAA |
启用扫描 | 0xF4 |
开始发送按键事件 | 0xFA |
设置LED | 0xED |
控制Num/Caps/Scroll灯 | 0xFA |
2. 鼠标常用命令
命令 | 值 | 功能 | 响应 |
---|---|---|---|
重置 | 0xFF |
复位鼠标 | 0xFA + 0xAA |
启用报告 | 0xF4 |
开始发送移动/按键数据 | 0xFA |
设置分辨率 | 0xE8 |
调整移动灵敏度 | 0xFA |
五、数据包格式
1. 键盘数据包
- 单字节 :传输按键的扫描码(按下或释放)。
- 例如:
0x1C
(A键按下),0x9C
(A键释放,前缀0xF0
表示释放)。
- 例如:
2. 鼠标数据包
-
标准3字节格式(兼容模式):
字节 位7-0 Byte1 Y溢出 Byte2 X轴移动量(8位补码,-128~127) Byte3 Y轴移动量(8位补码,-128~127)
六、错误处理与重试
- 奇偶校验错误 :主机检测到校验错误时,可发送重传命令(
0xFE
)。 - 超时:若设备未响应,主机可复位设备或记录错误。
- ACK缺失 :未收到
0xFA
时,主机应重发命令或初始化设备。
七、PS/2与USB的对比
特性 | PS/2 | USB |
---|---|---|
热插拔 | 不支持(可能损坏设备) | 支持 |
中断机制 | 专用IRQ(低延迟) | 轮询或中断传输 |
协议复杂度 | 简单(固定数据包) | 复杂(多种传输类型、描述符) |
应用场景 | 传统设备、嵌入式系统 | 现代外设、即插即用 |
八、操作系统中的实现
- 初始化流程 :
- 启用键盘/鼠标接口 → 发送重置命令 → 配置扫描码模式。
- 中断处理程序 :
- 读取数据端口(
0x60
) → 解析扫描码或鼠标包 → 传递事件到上层。
- 读取数据端口(
- 缓冲区管理 :
- 维护循环队列存储输入事件,避免数据丢失。
总结
PS/2协议通过同步串行通信实现低延迟输入,其核心在于时钟同步、数据帧格式和命令响应机制。
从鼠标接受数据(harib04g)
c
struct FIFO8 mousefifo;
void inthandler2c(int *esp)
/* 来自PS/2鼠标的中断 */
{
unsigned char data;
io_out8(PIC1_OCW2, 0x64); /* 通知PIC1 IRQ-12的受理已经完成 */
io_out8(PIC0_OCW2, 0x62); /* 通知PIC0 IRQ-02的受理已经完成 */
data = io_in8(PORT_KEYDAT);
fifo8_put(&mousefifo, data);
return;
}
在x86系统中,处理来自PS/2鼠标的中断(IRQ12)时,需要与可编程中断控制器(PIC)交互以确认中断处理完成。以下是代码的详细解释:
一、代码功能概述
这段代码是处理PS/2鼠标中断的中断服务程序(ISR),主要完成以下操作:
- 通知PIC中断处理完成:通过向主PIC和从PIC发送EOI(End of Interrupt)命令。
- 读取鼠标数据:从键盘控制器数据端口获取鼠标事件数据。
- 存储数据到缓冲区:将数据存入FIFO队列供后续处理。
二、关键代码解析
1. 中断处理流程
c
void inthandler2c(int *esp) {
unsigned char data;
// 通知PIC中断处理完成
io_out8(PIC1_OCW2, 0x64); // 从PIC(IRQ12)的EOI
io_out8(PIC0_OCW2, 0x62); // 主PIC(IRQ2)的EOI
// 读取鼠标数据
data = io_in8(PORT_KEYDAT);
// 存入FIFO队列
fifo8_put(&mousefifo, data);
return;
}
2. PIC的端口与命令
- PIC1_OCW2 :从PIC(PIC1)的操作命令字端口,地址为
0xA0
。 - PIC0_OCW2 :主PIC(PIC0)的操作命令字端口,地址为
0x20
。 - EOI命令值 :
0x64
和0x62
是特殊EOI命令,通知PIC中断已处理。
三、PIC的EOI通知机制
1. 主从PIC的级联
- 主PIC(PIC0):处理IRQ0-IRQ7。
- 从PIC(PIC1):处理IRQ8-IRQ15,其输出连接到主PIC的IRQ2。
- 中断传递:当从PIC的IRQ12(鼠标中断)触发时,主PIC的IRQ2也会被激活。
2. 发送EOI命令的必要性
- 目的:告诉PIC中断处理已完成,允许其继续响应新中断。
- 级联场景:处理从PIC的中断(如IRQ12)时,需向主从PIC均发送EOI。
3. 特殊EOI命令详解
-
OCW2格式:
位 7 ® 6 (SL) 5 (EOI) 4-0 (L3-L0) 值 0 1 1 中断级别 -
从PIC(IRQ12)的EOI命令
0x64
:- 二进制:
01100100
- SL=1, EOI=1:表示发送特殊EOI命令。
- L3-L0=0100:指定从PIC的中断级别4(对应IRQ12 = IRQ8 + 4)。
- 二进制:
-
主PIC(IRQ2)的EOI命令
0x62
:- 二进制:
01100010
- SL=1, EOI=1:特殊EOI命令。
- L3-L0=0010:指定主PIC的中断级别2(IRQ2)。
- 二进制:
四、代码执行步骤
-
发送EOI到从PIC:
cio_out8(0xA0, 0x64); // 通知从PIC,IRQ4(即IRQ12)已处理
- 从PIC收到命令后,清除IRQ4的中断状态。
-
发送EOI到主PIC:
cio_out8(0x20, 0x62); // 通知主PIC,IRQ2(级联中断)已处理
- 主PIC收到命令后,清除IRQ2的中断状态。
-
读取鼠标数据:
cdata = io_in8(0x60); // 从键盘控制器数据端口读取鼠标数据包
- 数据端口
0x60
用于传输键盘和鼠标的输入数据。
- 数据端口
-
存储数据到缓冲区:
cfifo8_put(&mousefifo, data); // 将数据存入队列供后续解析
- 使用FIFO队列避免中断处理过程中数据丢失。
五、关联硬件行为
- 中断触发:鼠标移动或点击时,键盘控制器通过IRQ12通知CPU。
- PIC级联:从PIC通过主PIC的IRQ2向CPU传递中断。
- 数据包格式:PS/2鼠标数据为3字节,需在后续代码中解析。
六、常见问题
1. 为何需要同时通知主从PIC?
- 从PIC的中断通过主PIC的IRQ2级联,需两者均确认中断完成,否则后续中断会被阻塞。
2. 0x64
和0x62
如何计算?
- 特殊EOI命令公式:
OCW2 = 0x60 | (IRQ_LEVEL & 0x07)
- 从PIC IRQ4:
0x60 | 4 = 0x64
- 主PIC IRQ2:
0x60 | 2 = 0x62
- 从PIC IRQ4:
3. 是否必须使用特殊EOI?
- 常规EOI(
0x20
)仅适用于非特殊模式,级联中断需特殊EOI明确指定中断级别。
总结
这段代码通过向主从PIC发送特殊EOI命令,确保中断处理正确完成,并读取鼠标数据存入缓冲区。理解PIC的级联机制和OCW2命令格式,是编写可靠中断处理程序的关键。