鼠标键盘在bios系统能用进入系统后不能用?

一、故事开始

第八轮真机测试。

系统启动后,屏幕显示了令人困惑的画面:

[0] > [6] >

这不是我期望的。正常情况下应该只显示一个提示符[0] > ,等待用户输入。

但现在,STI(开启中断)后立即出现了6次虚假回车,导致Shell显示了6个新的提示符。

问题很明确:键盘控制器输出缓冲区中残留着BIOS POST阶段留下的扫描码。当我们开启中断后,这些残留的扫描码被当作真实的键盘输入处理,其中一些被转换成了回车符。

二、什么是BIOS残留扫描码?

当计算机开机时,BIOS会执行POST(Power-On Self Test),其中包括键盘控制器的初始化和测试。在这个过程中,BIOS可能会向键盘控制器发送命令,或者键盘控制器可能会产生一些扫描码。

这些扫描码被存储在键盘控制器的输出缓冲区中,但BIOS并没有完全清空它们。当我们的kernel接管系统并开启中断后,这些残留的扫描码会立即触发IRQ1(键盘中断),被我们的键盘驱动处理。

三、排查过程

3.1 确认现象

真机屏幕显示[0] > [6] >,说明:

  1. 第一个提示符[0] > 正常显示
  2. STI后立即产生了6次回车(或换行)
  3. 每次回车都触发了新的提示符显示

3.2 分析扫描码来源

通过调试,我发现:

  1. STI前,键盘控制器输出缓冲区(端口0x60)中有数据
  2. STI后,IRQ1立即触发,读取到的扫描码是0x1C(回车的扫描码)
  3. 这些扫描码不是用户按下的,而是BIOS留下的

3.3 理解问题根源

BIOS POST过程中,键盘控制器可能产生以下扫描码:

  • 测试过程中的按键模拟
  • 键盘控制器的初始化响应
  • 用户在BIOS设置中按下的键

这些扫描码被存储在输出缓冲区中,等待被读取。但BIOS在退出前没有清空缓冲区。

当我们的kernel执行STI开启中断后:

  1. 键盘控制器检测到缓冲区中有数据
  2. 触发IRQ1(键盘中断)
  3. 我们的键盘驱动读取扫描码
  4. 扫描码0x1C被转换成回车符
  5. 回车符被写入键盘缓冲区
  6. Shell读取到回车,执行空命令并显示新提示符

四、为什么会出错?

4.1 修复前的键盘初始化

修复前,键盘初始化代码只做了基本的设置:

; 修复前:键盘初始化
定义 函数 键盘_初始化() {
    ;; 设置键盘控制器命令
    端口输出(96, 0x60)        ;; 写入命令:设置扫描码集
    端口输出(96, 0x01)        ;; 使用扫描码集1
    ;; ... 其他初始化
}

没有清空键盘控制器的输出缓冲区!

4.2 修复前的STI流程

; 修复前:STI流程
call 键盘_初始化()          ;; 初始化键盘
sti                         ;; ★开启中断 → IRQ1立即触发!
call Shell_主循环()         ;; 进入Shell

STI后,键盘控制器中的残留扫描码立即触发IRQ1,被当作真实输入处理。

4.3 扫描码0x1C的含义

在PS/2键盘扫描码集1中:

  • 0x1C:回车键按下(Make Code)
  • 0x9C:回车键释放(Break Code)

BIOS残留的扫描码恰好包含0x1C,被我们的驱动转换成了回车符。

五、修复方案

修复涉及三个方面:

5.1 新增键盘_清空控制器函数

kernel/键盘驱动.zl 中,新增清空键盘控制器输出缓冲区的函数:

定义 函数 键盘_清空控制器() {
    定义 整数 计数 为 0
    定义 整数 状态 为 0
    当 (计数 小于 16) {
        状态 为 调用 键盘_读状态()    ;; 读0x64状态端口
        状态 与 1                      ;; 检查bit0(OBF=输出缓冲区满)
        如果 (状态 不等于 0) {
            端口输入(96)               ;; 读0x60数据端口清空
        }
        计数 加 1
    }
}

原理:

  • 读0x64状态端口的bit0(OBF,Output Buffer Full)
  • 如果OBF=1,说明输出缓冲区有数据
  • 读0x60数据端口,清空缓冲区
  • 循环16次,确保所有残留数据都被清空

5.2 新增键盘_清空缓冲区函数

kernel/键盘驱动.zl 中,新增清空kernel键盘环形缓冲区的函数:

定义 函数 键盘_清空缓冲区() {
    调用 内存_写字节(328192, 0)    ;; 缓冲区头 = 0
    调用 内存_写字节(328193, 0)    ;; 缓冲区尾 = 0
    调用 内存_写字节(328194, 0)    ;; 缓冲区计数 = 0
    调用 内存_写字节(328198, 0)    ;; 命令缓冲区头 = 0
    调用 内存_写字节(328200, 0)    ;; 命令缓冲区尾 = 0
    调用 内存_写字节(328448, 0)    ;; 命令缓冲区计数 = 0
    返回 0
}

5.3 在STI前后调用清空函数

build_kernel.py 中,修改STI流程:

# 修复后:STI流程
调用 键盘_初始化()              ;; 初始化键盘
调用 键盘_清空控制器()          ;; ★清空BIOS残留扫描码
sti                             ;; 开启中断
调用 键盘_清空缓冲区()          ;; ★清空STI瞬间产生的虚假输入
调用 Shell_主循环()             ;; 进入Shell

修复后的完整流程:

键盘_初始化() → 键盘_清空控制器() → STI → 键盘_清空缓冲区() → Shell_主循环()
                ↑                                    ↑
                │                                    │
                └─ 清空键盘控制器硬件缓冲区          └─ 清空kernel软件缓冲区

六、验证

修复后,真机测试结果:

测试项结果
虚假回车消除✅ 不再显示[0] > [6] >
正常输入✅ 用户按键正常响应
系统稳定性✅ 不再出现虚假输入
QEMU兼容性✅ QEMU中也正常工作

七、经验总结

7.1 STI前必须清空硬件缓冲区

BIOS POST残留扫描码是裸机开发中常见的问题,必须在STI前清空键盘控制器输出缓冲区。

;; 正确做法:STI前清空键盘控制器
调用 键盘_清空控制器()
sti

7.2 STI后必须清空软件缓冲区

STI后,可能有少量扫描码已经被读取并写入kernel缓冲区,需要清空这些虚假输入。

;; 正确做法:STI后清空kernel缓冲区
sti
调用 键盘_清空缓冲区()

7.3 键盘控制器状态端口的重要性

键盘控制器有两个重要端口:

  • 0x60(数据端口):读取扫描码,写入命令参数
  • 0x64(状态端口):读取控制器状态,写入命令

状态端口的bit0(OBF)表示输出缓冲区是否有数据,必须在读取0x60之前检查。

7.4 QEMU与真机的差异

QEMU通常不会在POST后留下残留扫描码,但真机不同。这是另一个QEMU与真机行为不一致的例子。


本文为原创技术文章,版权所有。欢迎转发分享,但请注明出处。

作者:大强

日期:2026年7月14日

项目地址:x86-64 中文汇编器

编辑于 2026-07-22 · 著作权归作者所有