PS/2 键盘不会发送按键中断,但会响应命令
PS/2 keyboard won't send keypress interrupts, but does respond to commands
我是 OS 开发的新手,我最近开始了一个业余爱好项目,即创建一个尽可能简单的纯文本操作系统。它是在汇编的帮助下用 C 语言编写的,并使用 GRUB 进行引导,我一直在 VirtualBox 中测试它,偶尔也会将它放在闪存驱动器上,以便在一台古老的(~2009 年)笔记本电脑上进行测试。到目前为止,我已经实现了一些基本的文本输出功能,鉴于最近没有出现崩溃,我认为我的 GDT 和 IDT 实现还不错。目前我正在尝试让中断驱动的键盘驱动程序工作。
我想我已经正确设置了 PIC,看来我很幸运地向 PS/2 控制器和键盘发出命令并通过中断处理程序捕获响应。例如,这是给键盘一个识别命令时的调试输出:
Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF2
Keyboard interrupt: 0xFA
Keyboard interrupt: 0xAB
Keyboard interrupt: 0x83
返回的数据似乎是正确的,这证明我的中断处理程序能够连续多次工作而不会崩溃或发生任何事情,所以我不太担心我的 IDT 或 ISR 实现。现在这是我向键盘发送 0xF4 命令以开始扫描按键时的输出:
Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF4
Keyboard interrupt: 0xFA
"acknowledge" 状态代码为 0xFA 的中断似乎很有希望,但之后当我按下按键时没有任何反应。对于这两个示例,当 运行 在 VirtualBox 和我一直在使用的笔记本电脑上时,我得到了相同的结果。
这是键盘驱动程序的一些相关代码:
#define KEYBD_DATA 0x60
#define KEYBD_CMD 0x64
// wrapper for interrupt service routine written in assembly
extern void keyboard_interrupt();
// called from assembly ISR
void keyboard_handler() {
u8 data = read_port(KEYBD_DATA);
print("Keyboard interrupt: 0x");
printx(data);
putc('\n');
pic_eoi();
}
// functions to print command before sending it to the port
void keyboard_command(u8 cmd) {
print("Sending keyboard command: 0x");
printx(cmd);
putc('\n');
write_port(KEYBD_DATA, cmd);
}
void controller_command(u8 cmd) {
print("Sending controller command: 0x");
printx(cmd);
putc('\n');
write_port(KEYBD_CMD, cmd);
}
void setup_keyboard() {
// flush keyboard output
while(read_port(KEYBD_CMD) & 1)
read_port(KEYBD_DATA);
// set interrupt descriptor table entry (default code segment and access flags)
set_idt_entry(0x21, &keyboard_interrupt);
// activate device
write_port(KEYBD_CMD, 0xAE);
wait();
// get status
write_port(KEYBD_CMD, 0x20);
wait();
u8 status = (read_port(KEYBD_DATA) | 1) & 0x05;
print("Setting PS/2 controller status: 0x");
printx(status);
putc('\n');
wait();
// set status
write_port(KEYBD_CMD, 0x60);
wait();
write_port(KEYBD_DATA, status);
wait();
// enable keyboard scanning
keyboard_command(0xf4);
}
我认为这不是问题的根源,但这里是中断处理程序的汇编部分以防万一(在 GNU 汇编中):
.extern keyboard_handler
.global keyboard_interrupt
keyboard_interrupt:
cli
pusha
cld
call keyboard_handler
popa
sti
iret
这是预先设置 PIC 的代码:
#define MASTER_CMD 0x20
#define MASTER_DATA 0x21
#define SLAVE_CMD 0xA0
#define SLAVE_DATA 0xA1
#define PIC_EOI 0x20
// hopefully this gives a long enough delay
void wait() {
for (u8 i = 0; i < 255; i++);
}
// alert the PICs that the interrupt handling is done
// (later I'll check whether the slave PIC needs to be sent the EOI, but for now it doesn't seem to hurt to give it anyway)
void pic_eoi() {
write_port(MASTER_CMD, PIC_EOI);
write_port(SLAVE_CMD, PIC_EOI);
wait();
}
void setup_pic() {
write_port(MASTER_CMD, 0x11);
write_port(SLAVE_CMD, 0x11);
wait();
write_port(MASTER_DATA, 0x20);
write_port(SLAVE_DATA, 0x28);
wait();
write_port(MASTER_DATA, 0x4);
write_port(SLAVE_DATA, 0x2);
wait();
write_port(MASTER_DATA, 0x1);
write_port(SLAVE_DATA, 0x1);
wait();
write_port(MASTER_DATA, 0x0);
write_port(SLAVE_DATA, 0x0);
wait();
}
内核主要部分的初始化顺序如下:
// initialize global descriptor table and interrupt descriptor table
setup_gdt();
setup_idt();
// setup hardware interrupts
setup_pic();
setup_keyboard();
activate_idt(); // assembly routine with lidt and sti
我也知道键盘实际上正在做它的事情并将扫描代码放在端口 0x60 上,我已经能够得到一种让按键工作的轮询方法,但它很混乱而且会增加很多更难处理诸如按键重复和跟踪 shift 键之类的事情。让我知道是否需要更多代码。希望有一些明显的事情我忘记了或者做错了:)
特定 IRQ、某些 IRQ 或所有 IRQ 可能无法正常工作的一般原因:
- 您还没有在 CPU 上使用
sti
(或等效的) 启用中断
- 当您initialise它们时,您没有使用发送到主从 PIC 的掩码来启用中断。
- 在确实发生中断时未正确确认 EOI 可能会禁用部分或所有中断,具体取决于中断的优先级。
- 你有disabled张照片
- 您不会从 PS/2 键盘收到键盘中断,除非您发送了 PS/2 controller configuration byte 并设置了位 0(位 1 是鼠标中断)
我会通过屏蔽除您正在测试的中断之外的所有外部中断来缩小问题范围 space。在您的情况下,您对 IRQ1 感兴趣。要屏蔽除 IRQ1 之外的所有外部中断,您可以更改 setup_pic
以便:
write_port(MASTER_DATA, 0x0);
write_port(SLAVE_DATA, 0x0);
变成:
write_port(MASTER_DATA, ~0x2);
write_port(SLAVE_DATA, ~0x0);
设置为屏蔽中断的位,为零的位启用它们。 ~0x2
是位掩码 0b11111101
,~0x0
是位掩码 0b11111111
。那应该禁用除 IRQ1(主 PIC 的位 1)之外的所有内容。
您发现使用上述建议问题消失了,然后提到您的默认中断处理程序只是执行 IRET
。即使在默认情况下不执行任何操作的 IRQ 处理程序,您也需要发送适当的 EOI。不要发送中断意向书,除非它们来自 PIC。在您的情况下,IDT 条目 0x20 到 0x2f(含)需要有发送正确 EOI 的处理程序。有关正确处理 EOI 的更多详细信息,请访问 OSDev Wiki
我猜这是怎么回事,在第一个定时器中断 (IRQ0) 上您没有发送 EOI,这将有效地禁用所有外部中断。在发送 EOI 之前,所有同等或更低优先级的外部中断都将被禁用。 IRQ0(定时器)具有最高优先级,因此在发送 EOI 之前不发送 EOI 会有效地禁用所有外部中断。
我是 OS 开发的新手,我最近开始了一个业余爱好项目,即创建一个尽可能简单的纯文本操作系统。它是在汇编的帮助下用 C 语言编写的,并使用 GRUB 进行引导,我一直在 VirtualBox 中测试它,偶尔也会将它放在闪存驱动器上,以便在一台古老的(~2009 年)笔记本电脑上进行测试。到目前为止,我已经实现了一些基本的文本输出功能,鉴于最近没有出现崩溃,我认为我的 GDT 和 IDT 实现还不错。目前我正在尝试让中断驱动的键盘驱动程序工作。
我想我已经正确设置了 PIC,看来我很幸运地向 PS/2 控制器和键盘发出命令并通过中断处理程序捕获响应。例如,这是给键盘一个识别命令时的调试输出:
Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF2
Keyboard interrupt: 0xFA
Keyboard interrupt: 0xAB
Keyboard interrupt: 0x83
返回的数据似乎是正确的,这证明我的中断处理程序能够连续多次工作而不会崩溃或发生任何事情,所以我不太担心我的 IDT 或 ISR 实现。现在这是我向键盘发送 0xF4 命令以开始扫描按键时的输出:
Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF4
Keyboard interrupt: 0xFA
"acknowledge" 状态代码为 0xFA 的中断似乎很有希望,但之后当我按下按键时没有任何反应。对于这两个示例,当 运行 在 VirtualBox 和我一直在使用的笔记本电脑上时,我得到了相同的结果。
这是键盘驱动程序的一些相关代码:
#define KEYBD_DATA 0x60
#define KEYBD_CMD 0x64
// wrapper for interrupt service routine written in assembly
extern void keyboard_interrupt();
// called from assembly ISR
void keyboard_handler() {
u8 data = read_port(KEYBD_DATA);
print("Keyboard interrupt: 0x");
printx(data);
putc('\n');
pic_eoi();
}
// functions to print command before sending it to the port
void keyboard_command(u8 cmd) {
print("Sending keyboard command: 0x");
printx(cmd);
putc('\n');
write_port(KEYBD_DATA, cmd);
}
void controller_command(u8 cmd) {
print("Sending controller command: 0x");
printx(cmd);
putc('\n');
write_port(KEYBD_CMD, cmd);
}
void setup_keyboard() {
// flush keyboard output
while(read_port(KEYBD_CMD) & 1)
read_port(KEYBD_DATA);
// set interrupt descriptor table entry (default code segment and access flags)
set_idt_entry(0x21, &keyboard_interrupt);
// activate device
write_port(KEYBD_CMD, 0xAE);
wait();
// get status
write_port(KEYBD_CMD, 0x20);
wait();
u8 status = (read_port(KEYBD_DATA) | 1) & 0x05;
print("Setting PS/2 controller status: 0x");
printx(status);
putc('\n');
wait();
// set status
write_port(KEYBD_CMD, 0x60);
wait();
write_port(KEYBD_DATA, status);
wait();
// enable keyboard scanning
keyboard_command(0xf4);
}
我认为这不是问题的根源,但这里是中断处理程序的汇编部分以防万一(在 GNU 汇编中):
.extern keyboard_handler
.global keyboard_interrupt
keyboard_interrupt:
cli
pusha
cld
call keyboard_handler
popa
sti
iret
这是预先设置 PIC 的代码:
#define MASTER_CMD 0x20
#define MASTER_DATA 0x21
#define SLAVE_CMD 0xA0
#define SLAVE_DATA 0xA1
#define PIC_EOI 0x20
// hopefully this gives a long enough delay
void wait() {
for (u8 i = 0; i < 255; i++);
}
// alert the PICs that the interrupt handling is done
// (later I'll check whether the slave PIC needs to be sent the EOI, but for now it doesn't seem to hurt to give it anyway)
void pic_eoi() {
write_port(MASTER_CMD, PIC_EOI);
write_port(SLAVE_CMD, PIC_EOI);
wait();
}
void setup_pic() {
write_port(MASTER_CMD, 0x11);
write_port(SLAVE_CMD, 0x11);
wait();
write_port(MASTER_DATA, 0x20);
write_port(SLAVE_DATA, 0x28);
wait();
write_port(MASTER_DATA, 0x4);
write_port(SLAVE_DATA, 0x2);
wait();
write_port(MASTER_DATA, 0x1);
write_port(SLAVE_DATA, 0x1);
wait();
write_port(MASTER_DATA, 0x0);
write_port(SLAVE_DATA, 0x0);
wait();
}
内核主要部分的初始化顺序如下:
// initialize global descriptor table and interrupt descriptor table
setup_gdt();
setup_idt();
// setup hardware interrupts
setup_pic();
setup_keyboard();
activate_idt(); // assembly routine with lidt and sti
我也知道键盘实际上正在做它的事情并将扫描代码放在端口 0x60 上,我已经能够得到一种让按键工作的轮询方法,但它很混乱而且会增加很多更难处理诸如按键重复和跟踪 shift 键之类的事情。让我知道是否需要更多代码。希望有一些明显的事情我忘记了或者做错了:)
特定 IRQ、某些 IRQ 或所有 IRQ 可能无法正常工作的一般原因:
- 您还没有在 CPU 上使用
sti
(或等效的) 启用中断
- 当您initialise它们时,您没有使用发送到主从 PIC 的掩码来启用中断。
- 在确实发生中断时未正确确认 EOI 可能会禁用部分或所有中断,具体取决于中断的优先级。
- 你有disabled张照片
- 您不会从 PS/2 键盘收到键盘中断,除非您发送了 PS/2 controller configuration byte 并设置了位 0(位 1 是鼠标中断)
我会通过屏蔽除您正在测试的中断之外的所有外部中断来缩小问题范围 space。在您的情况下,您对 IRQ1 感兴趣。要屏蔽除 IRQ1 之外的所有外部中断,您可以更改 setup_pic
以便:
write_port(MASTER_DATA, 0x0);
write_port(SLAVE_DATA, 0x0);
变成:
write_port(MASTER_DATA, ~0x2);
write_port(SLAVE_DATA, ~0x0);
设置为屏蔽中断的位,为零的位启用它们。 ~0x2
是位掩码 0b11111101
,~0x0
是位掩码 0b11111111
。那应该禁用除 IRQ1(主 PIC 的位 1)之外的所有内容。
您发现使用上述建议问题消失了,然后提到您的默认中断处理程序只是执行 IRET
。即使在默认情况下不执行任何操作的 IRQ 处理程序,您也需要发送适当的 EOI。不要发送中断意向书,除非它们来自 PIC。在您的情况下,IDT 条目 0x20 到 0x2f(含)需要有发送正确 EOI 的处理程序。有关正确处理 EOI 的更多详细信息,请访问 OSDev Wiki
我猜这是怎么回事,在第一个定时器中断 (IRQ0) 上您没有发送 EOI,这将有效地禁用所有外部中断。在发送 EOI 之前,所有同等或更低优先级的外部中断都将被禁用。 IRQ0(定时器)具有最高优先级,因此在发送 EOI 之前不发送 EOI 会有效地禁用所有外部中断。