如何在x86_64 C内核中检测按键并传入键码调用指定函数?
自制x86_64内核检测按键操作实现步骤
1. 理解PS/2键盘的工作逻辑
x86平台的PS/2键盘通过端口0x60返回按键扫描码,端口0x64用于状态查询与控制。按键按下时,键盘会把扫描码发送到0x60端口,同时触发IRQ1中断(主PIC的中断1号)。
2. 配置PIC启用键盘中断
IRQ1默认会被PIC屏蔽,需要初始化PIC并开启IRQ1的中断权限:
// 初始化主PIC(8259A),核心是启用IRQ1 void pic_init() { // 省略基础PIC初始化的ICW1-ICW4命令发送步骤(根据你的内核现有代码调整) // 操作主PIC掩码寄存器(端口0x21),清除IRQ1对应的位以允许中断 uint8_t mask = inb(0x21) & ~(1 << 1); outb(0x21, mask); }
注:inb/outb是你内核中需要实现的端口读写函数,用于读取/写入8位端口数据。
3. 注册IRQ1中断处理函数
需要把键盘中断处理函数挂载到IDT(中断描述符表)的IRQ1对应条目(IRQ1映射到中断向量号0x21,因为PIC默认偏移为0x20):
// 汇编包装器:处理中断上下文的寄存器保存与恢复,以及PIC的EOI信号 __attribute__((naked)) void keyboard_irq_wrapper() { asm volatile("push rax\npush rbx\npush rcx\npush rdx\n"); asm volatile("call keyboard_irq_handler"); // 发送EOI信号给主PIC,告知中断处理完成 asm volatile("mov al, 0x20\nout 0x20, al"); asm volatile("pop rdx\npop rcx\npop rbx\npop rax\niretq"); } // C语言实现的键盘中断处理逻辑 void keyboard_irq_handler() { uint8_t scancode = inb(0x60); // 读取端口0x60的扫描码 // 区分按键按下(扫描码无0x80位)与释放(扫描码含0x80位) if (!(scancode & 0x80)) { // 将扫描码转为十六进制字符串传入回调函数 char code_str[4]; snprintf(code_str, sizeof(code_str), "%02X", scancode); on_key_press(code_str); } } // 在IDT初始化时注册中断处理函数 void idt_init() { // 省略IDT基础初始化步骤 idt_set_entry(0x21, (uint64_t)keyboard_irq_wrapper, 0x08, 0x8E); }
注:idt_set_entry是你内核中设置IDT条目的函数,参数分别为中断向量号、处理函数地址、代码段选择子、门类型(0x8E对应32位中断门,x86_64环境可调整为对应64位门类型)。
4. 可选:实现扫描码到字符的映射
如果需要输出实际按键字符而非十六进制扫描码,可定义扫描码到ASCII的映射表:
// 基础扫描码到ASCII的映射表(可参考PS/2键盘标准补全) char scancode_to_ascii[] = { 0, 0x1B, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', // 省略其余扫描码映射 }; // 修改keyboard_irq_handler中的处理逻辑 if (!(scancode & 0x80)) { char c = scancode_to_ascii[scancode]; if (c != 0) { char code_str[2] = {c, '\0'}; on_key_press(code_str); } }
5. 启用全局中断
在内核初始化的最后一步开启全局中断:
void kernel_main() { pic_init(); idt_init(); // 省略其他内核初始化步骤 asm volatile("sti"); // 开启全局中断 }
关键细节补充
- 必须确保内核已正确初始化GDT、IDT和PIC,这是中断处理的基础。
- 端口读写函数的汇编实现示例:
static inline uint8_t inb(uint16_t port) { uint8_t ret; asm volatile("inb %1, %0" : "=a"(ret) : "Nd"(port)); return ret; } static inline void outb(uint16_t port, uint8_t data) { asm volatile("outb %0, %1" : : "a"(data), "Nd"(port)); }
- QEMU默认会模拟PS/2键盘并传递按键事件,无需额外配置Docker或QEMU参数,直接按键即可触发中断。
内容的提问来源于stack exchange,提问作者user21729863
相关产品推荐
相关产品推荐

