一、键盘输入的芯片级底层原理
要理解键盘驱动,首先要搞清楚键盘和 CPU 之间的通信机制 ------ 这一切都围绕经典的 8042 键盘控制器(也叫 PS/2 控制器)展开,它是 x86 架构中连接键盘、鼠标与 CPU 的核心桥梁。
1.1 8042 控制器的核心作用
8042 控制器有两个专属 I/O 端口,是驱动与硬件交互的唯一入口:
- 0x60(数据端口):双向端口,用于读取键盘发送的扫描码,或向键盘发送控制命令(如设置 LED);
- 0x64(控制端口):双向端口,用于读取控制器状态(如缓冲区是否满),或向控制器发送指令。
当用户按下 / 释放按键时,键盘会向 8042 发送扫描码,8042 接收到后会触发 CPU 的 IRQ1 中断(对应中断向量 0x21)。CPU 响应中断后,内核的中断处理函数会读取扫描码并解析,这是键盘输入的核心硬件流程。
1.2 扫描码
扫描码是键盘对按键的唯一编码,分为三类:
- 通码(Make Code):按键按下时发送的编码(如 Esc 键是 0x01,A 键是 0x1E);
- 断码(Break Code):按键释放时发送的编码;
- 扩展码:特殊键(如 PrintScreen、方向键)会先发送 0xE0 前缀,后续才是真正的扫描码,需特殊处理。
二、键盘驱动实现
2.1 核心数据定义
通过一个枚举类型定义键盘所有的扫描码。通过结构体keymap定义扫描码对应的字符,其中数组的第三个元素判断shift是否被按下,第四元素代表扩展按键是否被按下。
c
#define INV 0 // 不可见字符
#define CODE_PRINT_SCREEN_DOWN 0xB7
typedef enum
{
KEY_NONE,
KEY_ESC,
KEY_1,
...
// 以下为自定义按键,为和 keymap 索引匹配
KEY_PRINT_SCREEN,
} KEY;
static char keymap[][4] = {
/* 扫描码 未与 shift 组合 与 shift 组合 以及相关状态 */
/* ---------------------------------- */
/* 0x00 */ {INV, INV, false, false}, // NULL
/* 0x01 */ {0x1b, 0x1b, false, false}, // ESC
/* 0x02 */ {'1', '!', false, false},
....
};
static bool capslock_state; // 大写锁定
static bool scrlock_state; // 滚动锁定
static bool numlock_state; // 数字锁定
static bool extcode_state; // 扩展码状态
#define ctrl_state (keymap[KEY_CTRL_L][2] || keymap[KEY_CTRL_L][3]) // CTRL 键状态
#define alt_state (keymap[KEY_ALT_L][2] || keymap[KEY_ALT_L][3]) // ALT 键状态
#define shift_state (keymap[KEY_SHIFT_L][2] || keymap[KEY_SHIFT_R][2]) // SHIFT 键状态
2.2 中断处理函数
接下来就是键盘中断的处理函数,核心逻辑是根据按下的扫描码在控制台打印对应的字符。长但逻辑简单
c
void keyboard_handler(int vector){
# 1:读取扫描码并发送 EOI
assert(vector == 0x21);
send_eoi(vector); // 发送中断处理完成信号
u16 scancode = inb(KEYBOARD_DATA_PORT); // 读取扫描码;>0x80 表示按键释放
u8 ext = 2; // 扩展码偏移量
# 2:处理扩展码
// 处理扩展码
if(scancode == 0xE0){
extcode_state = true;
return;
}
if(extcode_state){
scancode |= 0xE000; // 标记为扩展码
extcode_state = false; // 重置扩展码状态
ext = 3; // 扩展码偏移量
}
# 3:区分通码 / 断码,更新按键状态
// 处理按键释放事件
u16 makecode = (scancode & 0x7f); // 获取按键按下时的扫描码
if(makecode == CODE_PRINT_SCREEN_DOWN) makecode = KEY_PRINT_SCREEN; // 处理 Print Screen 键
if(makecode > KEY_PRINT_SCREEN) return; // 非法扫描码,直接返回
if(scancode & 0x80){ // 按键释放事件
keymap[makecode][ext] = false; // 更新按键状态
return;
}
// 处理按键按下事件
keymap[makecode][ext] = true; // 更新按键状态
# 4:控制 LED 灯状态
// 是否需要设置 LED 灯
bool led = false;
if (makecode == KEY_NUMLOCK){
numlock_state = !numlock_state;
led = true;
}
else if (makecode == KEY_CAPSLOCK){
capslock_state = !capslock_state;
led = true;
}
else if (makecode == KEY_SCRLOCK){
scrlock_state = !scrlock_state;
led = true;
}
if(led) set_leds();
// 处理普通按键输入
bool shift = false;
if(capslock_state && ('a' <= keymap[makecode][0] && keymap[makecode][0] <= 'z')){
shift = !shift; // 大写锁定影响字母键
}
if(shift_state) shift = !shift; // Shift 键影响所有按键
char ch;
if(ext == 3 && makecode == KEY_SPACE) ch = keymap[makecode][1]; // 处理扩展空格键
else ch = keymap[makecode][shift]; // 根据 shift 状态选择字符
if(ch == INV) return; // 不可见字符,直接返回
// LOGK("keyboard input 0x%c\n", ch);
fifo_put(&fifo, ch);
if (waiter != NULL){
task_unlock(waiter);
waiter = NULL;
}
}
u32 keyboard_read(char *buf, u32 count)
{
reentrant_mutex_lock(&lock); // 加锁
int nr = 0;
while (nr < count)
{
while (fifo_empty(&fifo)){
waiter = running_task(); // 如果队列没有数据,就阻塞进行等待。
task_block(waiter, NULL, TASK_WAITING); // 阻塞当前任务,等待键盘输入
}
buf[nr++] = fifo_get(&fifo); // 从缓冲区获取一个字符
}
reentrant_mutex_unlock(&lock); // 解锁
return count;
}
set_leds函数体现了 8042 与键盘的标准交互协议:
c
static void set_leds()
{
// 组合LED状态:Caps(bit2)、Num(bit1)、Scroll(bit0)
u8 leds = (capslock_state << 2) | (numlock_state << 1) | scrlock_state;
keyboard_wait(); // 等待控制器就绪(缓冲区为空)
outb(KEYBOARD_DATA_PORT, KEYBOARD_CMD_LED); // 发送设置LED命令(0xED)
keyboard_ack(); // 等待键盘返回ACK(0xFA)确认
keyboard_wait();
outb(KEYBOARD_DATA_PORT, leds); // 发送LED状态值
keyboard_ack();
}
keyboard_wait循环读取 0x64 端口状态,直到状态位 0x02(缓冲区满)为 0,确保控制器就绪;
c
// 等待键盘控制器准备就绪
static void keyboard_wait()
{
u8 state;
do{
state = inb(KEYBOARD_CTRL_PORT);
} while (state & 0x02); // 读取键盘缓冲区,直到为空
}
keyboard_ack则等待键盘返回 0xFA(ACK),保证命令被正确接收。
c
// 等待键盘控制器发送 ACK 确认
static void keyboard_ack()
{
u8 state;
do{
state = inb(KEYBOARD_DATA_PORT); // 读取键盘数据端口
} while (state != KEYBOARD_CMD_ACK); // 直到收到 ACK 确认
}
2.3 FIFO 解决输入与处理的速度差
由于键盘输入是突发式的(用户可能快速按多个键),而任务读取是按需式的,FIFO 循环队列完美解决了两者的速度不匹配问题,我这里贴出核心函数实现:
c
// FIFO核心入队逻辑
void fifo_put(fifo_t *fifo, char byte)
{
while (fifo_full(fifo))
{
fifo_get(fifo); // 缓冲区满时,丢弃最旧字符
}
fifo->buf[fifo->head] = byte;
fifo->head = fifo_next(fifo, fifo->head); // 循环移动头指针
}
// FIFO核心出队逻辑
char fifo_get(fifo_t *fifo)
{
assert(!fifo_empty(fifo));
char byte = fifo->buf[fifo->tail];
fifo->tail = fifo_next(fifo, fifo->tail); // 循环移动尾指针
return byte;
}
2.4 多任务同步机制
多任务环境下,多个任务可能同时读取键盘,因此通过互斥锁 + 任务阻塞保证同步安全:
c
u32 keyboard_read(char *buf, u32 count)
{
reentrant_mutex_lock(&lock); // 加锁
int nr = 0;
while (nr < count)
{
while (fifo_empty(&fifo)){
waiter = running_task(); // 如果队列没有数据,就阻塞进行等待。
task_block(waiter, NULL, TASK_WAITING); // 阻塞当前任务,等待键盘输入
}
buf[nr++] = fifo_get(&fifo); // 从缓冲区获取一个字符
}
reentrant_mutex_unlock(&lock); // 解锁
return count;
}
2.5 初始化模块:搭建驱动基础环境
keyboard_init是驱动的入口,负责初始化所有核心资源,为后续输入处理做准备:
c
void keyboard_init(){
// 重置锁定键和扩展码状态
capslock_state = false;
scrlock_state = false;
numlock_state = false;
extcode_state = false;
// 初始化FIFO缓冲、可重入互斥锁
fifo_init(&fifo, buf, BUFFER_SIZE);
reentrant_mutex_init(&lock);
waiter = NULL;
// 初始化LED状态(全部关闭)
set_leds();
// 注册中断处理函数,开启键盘中断
set_interrupt_handler(IRQ_KEYBOARD, keyboard_handler);
set_interrupt_mask(IRQ_KEYBOARD, true);
}