鼠标键盘在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] >,说明:
- 第一个提示符
[0] >正常显示 - STI后立即产生了6次回车(或换行)
- 每次回车都触发了新的提示符显示
3.2 分析扫描码来源
通过调试,我发现:
- STI前,键盘控制器输出缓冲区(端口0x60)中有数据
- STI后,IRQ1立即触发,读取到的扫描码是0x1C(回车的扫描码)
- 这些扫描码不是用户按下的,而是BIOS留下的
3.3 理解问题根源
BIOS POST过程中,键盘控制器可能产生以下扫描码:
- 测试过程中的按键模拟
- 键盘控制器的初始化响应
- 用户在BIOS设置中按下的键
这些扫描码被存储在输出缓冲区中,等待被读取。但BIOS在退出前没有清空缓冲区。
当我们的kernel执行STI开启中断后:
- 键盘控制器检测到缓冲区中有数据
- 触发IRQ1(键盘中断)
- 我们的键盘驱动读取扫描码
- 扫描码0x1C被转换成回车符
- 回车符被写入键盘缓冲区
- Shell读取到回车,执行空命令并显示新提示符
四、为什么会出错?
4.1 修复前的键盘初始化
修复前,键盘初始化代码只做了基本的设置:
; 修复前:键盘初始化
定义 函数 键盘_初始化() {
;; 设置键盘控制器命令
端口输出(96, 0x60) ;; 写入命令:设置扫描码集
端口输出(96, 0x01) ;; 使用扫描码集1
;; ... 其他初始化
}没有清空键盘控制器的输出缓冲区!
4.2 修复前的STI流程
; 修复前:STI流程
call 键盘_初始化() ;; 初始化键盘
sti ;; ★开启中断 → IRQ1立即触发!
call Shell_主循环() ;; 进入ShellSTI后,键盘控制器中的残留扫描码立即触发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前清空键盘控制器
调用 键盘_清空控制器()
sti7.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 中文汇编器