键盘输入 - 理论讲解
1. PS/2 键盘架构
硬件接口
PS/2键盘通过8042键盘控制器与CPU通信:
键盘 ──→ 8042控制器 ──→ PIC(IRQ1) ──→ CPU
│
端口 0x60 (数据)
端口 0x64 (状态/命令)
端口说明
| 端口 | 读取 | 写入 |
|---|---|---|
| 0x60 | 读扫描码 | 发送命令给键盘 |
| 0x64 | 读状态寄存器 | 发送命令给控制器 |
状态寄存器(端口0x64读取)
第0位: 输出缓冲区满 (1=有数据可读)
第1位: 输入缓冲区满 (1=控制器忙)
第5位: 鼠标输出缓冲区满
2. 扫描码(Scan Code)
扫描码集1(Set 1)
键盘发送的不是ASCII,而是扫描码。每个物理键有唯一的编号:
| 扫描码 | 按键 | 扫描码 | 按键 |
|---|---|---|---|
| 0x01 | ESC | 0x1C | Enter |
| 0x02 | 1 | 0x1D | 左Ctrl |
| 0x03 | 2 | 0x1E | A |
| 0x04 | 3 | 0x1F | S |
| 0x10 | Q | 0x20 | D |
| 0x11 | W | 0x2A | 左Shift |
| 0x12 | E | 0x36 | 右Shift |
| 0x13 | R | 0x38 | 左Alt |
| 0x14 | T | 0x39 | 空格 |
| 0x15 | Y | 0x0E | Backspace |
Make Code 和 Break Code
- 按下键: 发送 Make Code(如 A =
0x1E) - 释放键: 发送 Break Code = Make Code | 0x80(如 A释放 =
0x9E)
扫描码转ASCII映射表
static const char scancode_ascii[] = {
0, 27, '1', '2', '3', '4', '5', '6', // 0x00-0x07
'7', '8', '9', '0', '-', '=', '\b', '\t', // 0x08-0x0F
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 0x10-0x17
'o', 'p', '[', ']', '\n', 0, 'a', 's', // 0x18-0x1F
'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 0x20-0x27
'\'', '`', 0, '\\', 'z', 'x', 'c', 'v', // 0x28-0x2F
'b', 'n', 'm', ',', '.', '/', 0, '*', // 0x30-0x37
0, ' ', 0, 0, 0, 0, 0, 0, // 0x38-0x3F
};3. 修饰键处理
Shift键状态跟踪
static bool shift_pressed = false;
void keyboard_handler(registers_t *regs) {
uint8_t scancode = inb(0x60);
// 检查Shift键
if (scancode == 0x2A || scancode == 0x36) {
shift_pressed = true;
return;
}
if (scancode == 0xAA || scancode == 0xB6) {
shift_pressed = false;
return;
}
if (!(scancode & 0x80)) {
char c = scancode_ascii[scancode];
if (shift_pressed && c >= 'a' && c <= 'z') {
c -= 32; // 转大写
}
// 处理字符...
}
}4. 环形缓冲区
为什么需要缓冲区?
键盘中断可能在任何时候触发,但内核可能正忙于其他事情。缓冲区暂存输入,让内核在方便时读取。
实现原理
写入位置(head)
↓
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
↑
读取位置(tail)
#define BUFFER_SIZE 256
static char buffer[BUFFER_SIZE];
static volatile uint32_t head = 0; // 写入位置
static volatile uint32_t tail = 0; // 读取位置
bool buffer_put(char c) {
uint32_t next = (head + 1) % BUFFER_SIZE;
if (next == tail) return false; // 缓冲区满
buffer[head] = c;
head = next;
return true;
}
char buffer_get(void) {
if (head == tail) return 0; // 缓冲区空
char c = buffer[tail];
tail = (tail + 1) % BUFFER_SIZE;
return c;
}5. 完整键盘驱动流程
1. 注册IRQ1处理程序 → register_interrupt_handler(33, keyboard_handler)
2. 键盘按键 → 8042控制器产生IRQ1
3. PIC转发 → CPU跳转到向量33的ISR
4. 读取扫描码 → inb(0x60)
5. 转换为ASCII → 查表
6. 存入缓冲区 → buffer_put(c)
7. 发送EOI → irq_handler中自动完成
下一步
有了键盘输入,下一章我们将实现物理内存管理——操作系统最重要的资源管理功能之一。