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的设备就是键盘。
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指令之后才受理,
缓冲区建立优点:
int.c
的 PIC 初始化中)int.
// PIC初始化时设置中断向量号
io_out8(PIC0_ICW2, 0x20); // IRQ0-7对应INT 0x20-0x27
io_out8(PIC1_ICW2, 0x28); // IRQ8-15对应INT 0x28-0x2f
naskfunc.nas
中实现)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
int.c
中实现)int.c
// 键盘中断处理程序
void inthandler21(int *esp) {
unsigned char data;
io_out8(PIC0_OCW2, 0x61); // 通知PIC中断处理完成
data = io_in8(PORT_KEYDAT); // 读取键盘数据
// ... 缓冲区处理 ...
}
完整的中断调用流程:
就是将缓冲区的数据部分弄成链表,但这里是静态数组,到下面整理缓冲区就是动态分配内存了
struct KEYBUF {
unsigned char data[32];
int next;
};
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;
}
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);
}
}
但这个程序,中断处理的时候涉及数据移送操作,如果在数据移送前中断的话,数据就会乱
所以下面就改善一下
其实就是一个双指针,一个读数据指针,一个写数据指针
写的时候直接覆盖写,到了len最大容量就归0
struct FIFO8 {
unsigned char *buf;
int p, q, size, free, flags;
};
/*可变缓冲区,size存入缓冲区的总字节数,变量free保存缓冲区里美欧数据的字节数,
buf存缓冲区的地址,p代表下一个数据写入地址(next_w),q代表下一个数据读出地址
(next_r)。
*/
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;
}
#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;
}
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;
}
int fifo8_status(struct FIFO8 *fifo)
/* 报告一下到底积攒了多少数据 */
{
return fifo->size - fifo->free;
}
#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;
}
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()
void wait_KBC_sendready(void) {
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
0x0064
)。KEYSTA_SEND_NOTREADY
):
2. init_keyboard()
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),允许键盘事件触发中断。 |
五、硬件交互原理
io_in8(port)
:从指定端口读取1字节数据。io_out8(port, data)
:向指定端口写入1字节数据。IN
/OUT
指令访问。PORT_KEYCMD
(0x0064
)写入命令字节。PORT_KEYDAT
(0x0060
)写入命令参数(如模式字节)。PORT_KEYSTA
(0x0064
)读取状态,确保操作安全。六、代码的意义
这段代码的目标是配置键盘控制器以支持键盘和鼠标输入,具体作用包括:
七、实际应用场景
init_keyboard()
,确保键盘可用。总结
这段代码通过操作键盘控制器的端口,配置其工作模式,为后续处理键盘和鼠标输入奠定基础。理解端口地址、状态标志和配置字节的细节,是开发操作系统输入子系统的关键一步。
#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()
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()
0x64
),直到状态寄存器的“发送未就绪”位(KEYSTA_SEND_NOTREADY=0x02
)为0。2. 发送命令 0xD4
到命令端口(0x64
)
0x60
)的字节是发送给鼠标的指令(而非键盘)。3. 发送命令 0xF4
到数据端口(0x60
)
0xFA
(ACK确认),表示指令已接收。四、硬件交互逻辑
KBC与设备的通信链
CPU → KBC命令端口(0x64) → 发送命令0xD4 → KBC → 转发到鼠标
CPU → KBC数据端口(0x60) → 发送数据0xF4 → KBC → 转发到鼠标
ACK确认机制
0xFA
(需通过中断或轮询读取数据端口获取)。五、技术细节
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轴移动量(补码)
六、实际应用场景
七、注意事项
0xFA
,否则可能错过鼠标数据。总结
此代码通过键盘控制器向鼠标发送启用指令,使其进入数据报告模式。核心步骤为:
PS/2协议是早期计算机用于连接键盘和鼠标的通信标准,尽管逐渐被USB取代,但在许多传统系统和嵌入式设备中仍有应用。以下是其核心机制和关键细节的详解:
一、物理接口与电气特性
PS/2接口为6针Mini-DIN,实际使用4针:
引脚 | 功能 |
---|---|
1 | 数据线(DATA) |
3 | 地线(GND) |
4 | VCC(+5V) |
5 | 时钟线(CLK) |
二、数据帧格式
每帧数据包含 11位,按顺序如下:
位 | 名称 | 值 | 说明 |
---|---|---|---|
1 | 起始位 | 0 | 标志数据开始 |
8 | 数据位 | 0/1 | LSB(最低位)优先发送 |
1 | 奇偶校验位 | 0/1 | 奇校验(数据位+校验位的1个数为奇) |
1 | 停止位 | 1 | 标志数据结束 |
示例:键盘发送按键按下码 0x1C
(字母’A’的扫描码)的帧结构:
0
0011100
1
1
→ 起始位 + 数据(二进制0011100,LSB优先为0011100
) + 奇偶位 + 停止位。
三、通信方向与流程
1. 设备到主机(如按键事件)
2. 主机到设备(如发送命令)
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
)。0xFA
时,主机应重发命令或初始化设备。七、PS/2与USB的对比
特性 | PS/2 | USB |
---|---|---|
热插拔 | 不支持(可能损坏设备) | 支持 |
中断机制 | 专用IRQ(低延迟) | 轮询或中断传输 |
协议复杂度 | 简单(固定数据包) | 复杂(多种传输类型、描述符) |
应用场景 | 传统设备、嵌入式系统 | 现代外设、即插即用 |
八、操作系统中的实现
0x60
) → 解析扫描码或鼠标包 → 传递事件到上层。总结
PS/2协议通过同步串行通信实现低延迟输入,其核心在于时钟同步、数据帧格式和命令响应机制。
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),主要完成以下操作:
二、关键代码解析
1. 中断处理流程
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的端口与命令
0xA0
。0x20
。0x64
和0x62
是特殊EOI命令,通知PIC中断已处理。三、PIC的EOI通知机制
1. 主从PIC的级联
2. 发送EOI命令的必要性
3. 特殊EOI命令详解
OCW2格式:
位 | 7 ® | 6 (SL) | 5 (EOI) | 4-0 (L3-L0) |
---|---|---|---|---|
值 | 0 | 1 | 1 | 中断级别 |
从PIC(IRQ12)的EOI命令0x64
:
01100100
主PIC(IRQ2)的EOI命令0x62
:
01100010
四、代码执行步骤
发送EOI到从PIC:
io_out8(0xA0, 0x64); // 通知从PIC,IRQ4(即IRQ12)已处理
发送EOI到主PIC:
io_out8(0x20, 0x62); // 通知主PIC,IRQ2(级联中断)已处理
读取鼠标数据:
data = io_in8(0x60); // 从键盘控制器数据端口读取鼠标数据包
0x60
用于传输键盘和鼠标的输入数据。存储数据到缓冲区:
fifo8_put(&mousefifo, data); // 将数据存入队列供后续解析
五、关联硬件行为
六、常见问题
1. 为何需要同时通知主从PIC?
2. 0x64
和0x62
如何计算?
OCW2 = 0x60 | (IRQ_LEVEL & 0x07)
0x60 | 4 = 0x64
0x60 | 2 = 0x62
3. 是否必须使用特殊EOI?
0x20
)仅适用于非特殊模式,级联中断需特殊EOI明确指定中断级别。总结
这段代码通过向主从PIC发送特殊EOI命令,确保中断处理正确完成,并读取鼠标数据存入缓冲区。理解PIC的级联机制和OCW2命令格式,是编写可靠中断处理程序的关键。