键盘输入 - 理论讲解

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,而是扫描码。每个物理键有唯一的编号:

扫描码按键扫描码按键
0x01ESC0x1CEnter
0x0210x1D左Ctrl
0x0320x1EA
0x0430x1FS
0x10Q0x20D
0x11W0x2A左Shift
0x12E0x36右Shift
0x13R0x38左Alt
0x14T0x39空格
0x15Y0x0EBackspace

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中自动完成

下一步

有了键盘输入,下一章我们将实现物理内存管理——操作系统最重要的资源管理功能之一。