x86操作系统19——键盘驱动

一、键盘输入的芯片级底层原理

要理解键盘驱动,首先要搞清楚键盘和 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);
}
相关推荐
路溪非溪2 小时前
关于蓝牙技术的再补充
linux
爱宇阳3 小时前
Linux 安全加固:设置命令行无操作超时退出
linux·运维·安全
呆萌小新@渊洁3 小时前
声纹模型全流程实践-开发(训练 - 微调 - 部署 - 调用)
linux·服务器·python·语音识别
RisunJan3 小时前
Linux命令-getenforce命令(快速检查 Linux 系统中 SELinux 的当前运行模式)
linux·运维·服务器
SMF19193 小时前
解决在 Linux 系统中,当你尝试以 root 用户登录时遇到 “Access denied“ 的错误
java·linux·服务器
qq_479875433 小时前
systemd-resolved.service实验实战3
linux·服务器·c++
森焱森4 小时前
GD32F4 DSP
linux·c语言·arm开发·驱动开发·嵌入式硬件
d111111111d4 小时前
C语言中static修斯局部变量,全局变量和函数时分别由什么特性
c语言·javascript·笔记·stm32·单片机·嵌入式硬件·学习
天天进步20154 小时前
CentOS 实战:如何查看和分析信号量 (Semaphore) 的值
linux·运维·centos