从零写OS内核 | 键盘——按键是怎么变成字符的
你有没有想过这个过程:你按下一个键 → 屏幕上出现一个字符。这中间操作系统经历了什么?键盘扫描码怎么变成 scancode?USB HID 和 PS/2 有什么区别?
从机械键盘矩阵到 Linux input 子系统,本文把”按键到字符”的完整链路拆解清楚,给你一张可以照着调试的地图。
一、键盘的物理层:矩阵扫描原理
普通键盘内部是一个 N×M 的行/列矩阵,每个按键是矩阵交点上的一个开关。
Column 0 Column 1 Column 2 Column 3
│ │ │ │
Row 0 ──○─────────○─────────○─────────○ ← ESC, 1, 2, 3...
Row 1 ──○─────────○─────────○─────────○ ← Tab, Q, W, E, R...
Row 2 ──○─────────○─────────○─────────○ ← CapsLock, A, S, D, F...
...扫描过程:
- 键盘控制器输出 Row 0 低电平,其余 Row 高电平
- 读取 Column 电平,确定哪个 Column 有低电平(被按下)
- 交点确定 → 按键坐标 (Row, Column) 确认
- 通过查表把 (Row, Column) 转换为 Scan Code
PS/2 键盘的 Scan Code Set 2 例子:
- 按
A:键盘发0x1C - 按
Shift+A:先发0x12(Left Shift Make),再发0x1C - 松开
A:发0xF0+0x1C(Break Code)
# Linux 查看当前键盘的 scan code(按实际按键测试)
$ showkey -s
# 按下任意键会显示该键的 scancode(十进制)
# 按 ESC 或等待 10 秒退出
# 查看键码映射
$ dumpkeys | head -20
keycode 1 = Escape
keycode 2 = one
keycode 3 = two二、PS/2 协议:老式键盘的通信语言
PS/2 键盘使用一种简单的同步串行协议:每帧 11 位(1 Start + 8 Data + 1 Parity + 1 Stop),时钟由键盘提供(频率 ~10kHz)。
Start D0 D1 D2 D3 D4 D5 D6 D7 Parity Stop
┌──────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬──────┐
│ 0 │ x │ x │ x │ x │ x │ x │ x │ x │ p │ 1 │
└──────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴──────┘PS/2 键盘控制器(i8042)的工作流程:
PS/2 Keyboard ──→ i8042 Controller ──→ PS/2 Controller IRQ1 ──→ Keyboard Driver
│ │ │
发出 scan code 存入 output buffer 触发键盘中断
│
读 port 0x60Linux 查看 i8042 控制器状态:
$ dmesg | grep -i "8042|keyboard|ps/2"
[ 0.921] i8042: PNP: No KBD controller at 00:06
# 或正常情况:
[ 1.234] i8042: Keyboard port at 0x060,0x064, irq 1
[ 1.235] input: AT Translated Set 2 keyboard as /devices/platform/i8042/serio0/input/input0三、USB HID:现代键盘的通用协议
USB 键盘使用 HID(Human Interface Device) 类协议。HID 的核心是 报告描述符(Report Descriptor),它告诉主机这个设备发送什么数据、什么格式。
键盘 HID 报告描述符(简化版):
// HID Report Descriptor 片段(键盘)
0x05, 0x07, // Usage Page (Keycodes)
0x19, 0xE0, // Usage Min (Left Ctrl)
0x29, 0xE7, // Usage Max (Right GUI)
0x15, 0x00, // Logical Min (0)
0x25, 0x01, // Logical Max (1)
0x75, 0x01, // Report Size (1 bit per modifier key)
// Modifier keys (Ctrl, Shift, Alt, GUI)...
0x81, 0x02, // Input (Data, Variable, Absolute)
// 6 bytes for keycode array (同时按下的键)
0x26, 0xFF, 0x00, // Logical Max (255)
0x75, 0x08, // Report Size (8 bits)
0x95, 0x06, // Report Count (6 keycodes)
0x81, 0x00, // Input (Data, Array)一个 USB 键盘中断传输的 8 字节数据:
| Byte 0 | Byte 1 | Byte 2-7 |
|---|---|---|
| Modifiers (Ctrl/Shift/Alt) | Reserved | 最多 6 个同时按下的键码 |
例如按 A:[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]
0x04= HID Usage ID for ‘A’(HUT spec 定义)
四、Linux input 子系统:统一的输入抽象
Linux 为了统一键盘、鼠标、触摸板等各种输入设备,设计了 input 子系统。所有输入设备最终都通过 /dev/input/eventX 向用户空间提供事件。
键盘硬件 (PS/2 / USB HID)
│
▼
input_set_drvdata()
│
▼
┌─────────────────────────────────┐
│ input subsystem core │
│ (input_register_device/device) │
└──────────────┬──────────────────┘
│ evdev
┌────────┴────────┐
│ │
/dev/input/event0 /dev/input/event1
│ │
按键事件 鼠标事件实战:用 evtest 测试键盘事件:
$ evtest /dev/input/event0
Input driver version is 1.0.1
Input device ID: bus 0x11 vendor 0x1d2b product 0x1 version 0x100
Input device name: "AT Translated Set 2 keyboard"
Testing ... (interrupt to exit)
Event: time 1699999999.123456, type 4 (EV_MSC), code 4 (MSC_SCAN), value 28
# 28 = 0x1C = 'A' 的 scan code
Event: time 1699999999.123467, type 1 (EV_KEY), code 30 (KEY_A), value 1
# 按下 A,value=1 表示按下,value=0 表示松开
Event: time 1699999999.123478, type 4 (EV_MSC), code 4 (MSC_SCAN), value 28
Event: time 1699999999.123489, type 1 (EV_KEY), code 30 (KEY_A), value 0
# 松开 A,value=0查看所有输入设备:
$ ls /dev/input/
by-id/ by-path/ event0 event1 event2 mice mouse0
$ xinput list
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=3 [slave pointer (2)]
⎣ Virtual core keyboard id=1 [master keyboard (2)]
↳ Power Button id=6 [slave keyboard (2)]
↳ AT Translated Set 2 keyboard id=7 [slave keyboard (2)]五、wandos 的键盘输入
wandos 目前在 kernel/input.cpp 实现了简单的 PS/2 键盘中断处理:
// wandos kernel/input.cpp(简化)
void keyboard_interrupt() {
uint8_t scancode = inb(0x60); // 读 i8042 数据端口
// 简单的 scan code → ASCII 转换
if (scancode < 0x80) {
char c = scancode_to_ascii(scancode);
if (c) {
putchar(c); // 输出到 VGA 或串口
}
}
}wandos 当前支持的键盘功能:
- PS/2 键盘中断接收
- Scan Code Set 2 到 ASCII 的基本转换
- 通过 VGA 文本模式输出到屏幕
wandos 尚未实现的:
- USB HID 键盘(需要 USB 协议栈)
- input 子系统(统一输入抽象层)
- TTY 线路规范(ICRNL、IXON 等)
公众号 footer
相关阅读:
- 《从零写OS内核 | 总线与地址空间——CPU 是怎么找到每个设备的》(343)
- 《从零写OS内核 | I2C 总线——嵌入式系统的血管》(348)
动手环节:
运行 evtest /dev/input/eventX(X 依次尝试),找到你的键盘设备,然后按几个键观察 EV_MSC/SCANCODE 和 EV_KEY 的对应关系。
仓库地址:
github.com/golang12306/os-kernel-from-scratch
关注公众号「AI不着急」,回复”资料”获取内核学习路线图。
评论