1. PS/2 键盘底层驱动技术解析

PS/2 接口虽已退出消费级主机主流,但在工业控制面板、嵌入式人机界面(HMI)、安全启动终端及教学实验平台中仍具不可替代性。其单总线双向串行协议、低资源占用、确定性时序与硬件级扫描码映射机制,使其在无操作系统或实时性严苛场景下远超USB HID的工程价值。本文基于标准 PS/2 协议规范(IBM PS/2 Technical Reference, 1991)与主流 MCU 实现实践,系统剖析 PS/2 键盘驱动的硬件接口、协议栈、状态机设计、抗干扰策略及与嵌入式生态(HAL/FreeRTOS)的深度集成方法。

1.1 物理层与电气特性

PS/2 接口采用 6 针 Mini-DIN 连接器,实际仅使用 4 根信号线:

引脚 名称 方向 电平标准 关键参数
1 DATA 双向 TTL(5V 或 3.3V) 开漏输出,需上拉电阻(2.2kΩ–10kΩ)
3 GND 公共参考点
4 VCC 输入 +5V ±5% 键盘供电(部分键盘支持 3.3V)
5 CLK 输出 TTL 开漏输出,需上拉电阻

关键设计约束

  • CLK 由键盘主控 :所有通信时序均由键盘生成,MCU 仅为从设备,必须严格同步 CLK 边沿采样;
  • 开漏结构强制上拉 :未上拉将导致信号浮空,通信完全失效;上拉阻值需兼顾上升时间(≤1μs)与功耗;
  • VCC 供电能力 :标准键盘最大电流 100mA,MCU 的 VCC 引脚若为 LDO 输出,需确认其带载能力,否则须外接稳压模块;
  • ESD 防护 :暴露于外部接口,DATA/CLK 线必须串联 TVS 二极管(如 PESD5V0S1BA),接地电容 ≤100pF。

1.2 协议帧结构与时序规范

PS/2 采用异步串行协议,无起始位,以 下降沿触发 帧传输。一帧完整数据包含:

[START] [D0] [D1] [D2] [D3] [D4] [D5] [D6] [D7] [PARITY] [STOP]
   ↓       ↓    ↓    ↓    ↓    ↓    ↓    ↓    ↓     ↓       ↓
  0       LSB  ...              ...        MSB     奇校验     1
  • START(起始位) :固定为逻辑 0 ,由键盘在 CLK 下降沿后约 8–16μs 内置低;
  • DATA(8 位数据) :LSB 在前,MSB 在后,对应扫描码(Scan Code);
  • PARITY(奇校验位) :确保 DATA 位中 1 的个数为奇数,用于检错;
  • STOP(停止位) :固定为逻辑 1
  • 时序核心参数 (键盘侧保证):
    • 位周期:≈ 110μs(9.1kHz),允许 ±10% 偏差;
    • CLK 高/低电平宽度:各 ≈ 55μs;
    • START 到第一个 DATA 位延迟:≤ 10μs;
    • STOP 位后至下一帧 START 最小间隔:≥ 100μs。

MCU 采样窗口要求 :必须在每个 CLK 周期的 后半段(tSU ≥ 5μs,tH ≥ 5μs) 读取 DATA 线电平,避开边沿抖动区。此为软件解码可靠性的物理基础。

2. 状态机驱动架构设计

PS/2 驱动本质是事件驱动的状态机,需在无中断丢失前提下处理三种核心事件:按键按下(Make)、按键释放(Break)、重发(Resend)。标准扫描码集定义如下:

类型 前缀 示例(按键 A) 含义
Make 0x1C 按下事件
Break 0xF0 0xF0 0x1C 释放事件(两字节)
Resend 0xFE 请求重发上一帧(因 ACK 失败)

2.1 四状态核心机

驱动实现一个紧凑的四状态机,完全运行于 GPIO 中断上下文,避免任何阻塞操作:

typedef enum {
    PS2_IDLE,      // 等待 START 位(CLK 高,DATA 高)
    PS2_START,     // 检测到 START(CLK 下降沿,DATA=0)
    PS2_DATA,      // 接收 8 位数据 + 校验位(CLK 下降沿采样)
    PS2_STOP       // 等待 STOP 位(CLK 上升沿,DATA=1)
} ps2_state_t;

static ps2_state_t g_ps2_state = PS2_IDLE;
static uint8_t g_ps2_data = 0;
static uint8_t g_ps2_bit_cnt = 0;
static uint8_t g_ps2_parity = 0;

// CLK 下降沿中断服务程序(配置为 FALLING edge trigger)
void PS2_CLK_IRQHandler(void) {
    uint8_t data_level = HAL_GPIO_ReadPin(PS2_DATA_GPIO_PORT, PS2_DATA_PIN);

    switch (g_ps2_state) {
        case PS2_IDLE:
            if (data_level == GPIO_PIN_RESET) { // 检测 START
                g_ps2_state = PS2_START;
                g_ps2_bit_cnt = 0;
                g_ps2_parity = 0;
            }
            break;

        case PS2_START:
            // 忽略 START 期间的 CLK 边沿,进入 DATA 接收
            g_ps2_state = PS2_DATA;
            break;

        case PS2_DATA:
            // 在 CLK 下降沿后 >5μs 采样(利用 MCU 执行延迟或 NOP)
            __NOP(); __NOP(); // 确保采样点落在稳定区
            if (data_level == GPIO_PIN_SET) {
                g_ps2_data |= (1U << g_ps2_bit_cnt);
                g_ps2_parity ^= 1U;
            }
            if (++g_ps2_bit_cnt == 8) {
                g_ps2_state = PS2_STOP;
            }
            break;

        case PS2_STOP:
            // STOP 位必须为 1,否则丢弃整帧
            if (data_level == GPIO_PIN_SET) {
                // 校验 PARITY 位(下一个 CLK 下降沿采样)
                // 此处需在下一个 CLK 中断中完成最终校验与交付
                g_ps2_state = PS2_IDLE;
                ps2_process_frame(g_ps2_data, g_ps2_parity);
            } else {
                g_ps2_state = PS2_IDLE; // 帧错误,丢弃
            }
            break;
    }
}

2.2 扫描码解析与键值映射

PS/2 不传输 ASCII,而是原始扫描码。驱动需构建映射表,将扫描码转换为应用层可识别的键值。典型映射逻辑:

扫描码 修饰键状态 输出键值 说明
0x1C Shift=0 'a' 标准按键
0x1C Shift=1 'A' Shift+A
0xF0 0x1C KEY_A_RELEASE 释放事件
0xE0 0x4F KEY_RIGHT E0 前缀扩展键(方向键)

状态管理关键点

  • 修饰键跟踪 :维护 shift , ctrl , alt , caps_lock 等标志位,通过 0x12 (LShift), 0x59 (RCtrl) 等扫描码更新;
  • E0/E1 前缀处理 0xE0 表示第二套扫描码(如方向键、功能键), 0xE1 为 Pause/Break 序列(6 字节),需缓冲暂存;
  • 重复键抑制 :检测连续相同 Make 码(间隔 < 500ms),仅上报一次,避免误触发。

3. 抗干扰与鲁棒性增强策略

工业现场电磁干扰(EMI)易导致 CLK/Data 线毛刺,引发误帧。必须在驱动层植入多重防护:

3.1 硬件滤波与软件消抖协同

  • 硬件端 :CLK/DATA 线并联 100pF 陶瓷电容至地,抑制高频噪声;
  • 软件端 :在 CLK 中断中增加 边沿稳定性验证
// 修改 CLK 中断入口,增加去抖
static uint32_t g_clk_last_tick = 0;
#define DEBOUNCE_MS 2

void PS2_CLK_IRQHandler(void) {
    uint32_t now = HAL_GetTick();
    if ((now - g_clk_last_tick) < DEBOUNCE_MS) return; // 消抖
    g_clk_last_tick = now;

    // ... 原有状态机逻辑
}

3.2 帧完整性校验强化

除协议规定的奇校验外,增加两级校验:

  1. STOP 位确认 :STOP 必须为 1 ,且持续至少 2 个 CLK 周期(在 STOP 状态等待第二个 CLK 上升沿验证);
  2. 帧间隔监控 :记录上帧结束时刻,若新 START 距离过近(< 80μs),判定为噪声,强制复位状态机。

3.3 键盘复位与重同步机制

当连续 3 帧校验失败,或检测到非法序列(如 0xFF ),执行软复位:

void ps2_keyboard_reset(void) {
    // 发送复位命令(0xFF)——需 MCU 主动拉低 CLK+DATA 模拟
    // 实际中更常用:禁用 CLK 中断 10ms,再重新使能,强制键盘重发自检码
    HAL_NVIC_DisableIRQ(PS2_CLK_IRQn);
    HAL_Delay(10);
    HAL_NVIC_EnableIRQ(PS2_CLK_IRQn);
    g_ps2_state = PS2_IDLE;
}

4. 与嵌入式生态的深度集成

4.1 HAL 库适配层设计

基于 STM32 HAL,封装为可移植驱动模块:

// ps2_driver.h
typedef struct {
    GPIO_TypeDef* data_port;
    uint16_t data_pin;
    GPIO_TypeDef* clk_port;
    uint16_t clk_pin;
    void (*on_key_event)(uint8_t scancode, uint8_t is_release);
} ps2_handle_t;

HAL_StatusTypeDef PS2_Init(ps2_handle_t* hps2);
HAL_StatusTypeDef PS2_DeInit(ps2_handle_t* hps2);

初始化关键步骤

  • DATA/CLK 引脚配置为 GPIO_MODE_INPUT + GPIO_PULLUP
  • CLK 引脚额外配置 GPIO_MODE_IT_FALLING
  • 使能 SYSCFG 时钟,映射 EXTI 线至 NVIC;
  • 设置中断优先级高于 FreeRTOS 内核(如 NVIC_SetPriority(EXTI0_IRQn, 5) )。

4.2 FreeRTOS 集成:事件队列与任务解耦

为避免在中断中处理复杂逻辑(如 ASCII 转换、GUI 更新),采用队列解耦:

// 定义键事件结构
typedef struct {
    uint8_t scancode;
    uint8_t is_release;
    uint32_t timestamp; // HAL_GetTick()
} ps2_event_t;

// 创建队列(在 FreeRTOS 初始化后)
QueueHandle_t ps2_event_queue;

void ps2_process_frame(uint8_t data, uint8_t parity) {
    if (ps2_validate_parity(data, parity)) {
        ps2_event_t evt = {.scancode = data, .is_release = 0};
        // 处理 Break/E0 前缀等,设置 evt.is_release
        xQueueSendFromISR(ps2_event_queue, &evt, NULL);
    }
}

// 应用任务中消费
void keyboard_task(void *pvParameters) {
    ps2_event_t evt;
    while (1) {
        if (xQueueReceive(ps2_event_queue, &evt, portMAX_DELAY) == pdTRUE) {
            char key = ps2_scancode_to_ascii(evt.scancode, g_modifiers);
            printf("Key: %c (%s)\r\n", key, evt.is_release ? "RELEASE" : "PRESS");
        }
    }
}

4.3 LL 库极致优化方案(资源受限 MCU)

对 Cortex-M0/M0+ 等小资源 MCU,绕过 HAL,直接操作寄存器:

// 极简 CLK 中断(LL 版本)
void EXTI0_IRQHandler(void) {
    // 清除中断标志(LL_EXTI_ClearFlag_0_31(EXTI_LINE_0))
    // 直接读取 GPIO_IDR 寄存器获取 DATA 电平
    uint32_t data_val = LL_GPIO_IsInputPinSet(PS2_DATA_GPIO_PORT, PS2_DATA_PIN);
    // ... 状态机逻辑(同前,但无函数调用开销)
    EXTI->PR1 = EXTI_PR1_PIF0; // 手动清标志
}

此方案可将中断响应时间压缩至 < 300ns,满足 2MHz 以上 CLK(超频键盘)需求。

5. 典型问题诊断与调试技巧

5.1 常见故障树

现象 可能原因 诊断方法
完全无响应 VCC 未供电 / 上拉电阻缺失 / CLK 中断未使能 万用表测 VCC/上拉电压;逻辑分析仪看 CLK 是否有波形
随机乱码 DATA 线噪声大 / 未消抖 / 校验未启用 示波器观察 DATA 波形毛刺;关闭校验测试
按键失灵 扫描码映射表错误 / CapsLock 状态未跟踪 打印原始扫描码,比对标准表(https://www.computer-engineering.org/ps2keyboard/)
重复触发 释放事件(0xF0)未正确解析 捕获两字节序列,确认是否被拆分为两个单字节帧

5.2 逻辑分析仪调试法

使用 Saleae Logic 或 Sigrok,设置如下:

  • 采样率 :≥ 10MHz(精确捕获 110μs 位周期);
  • 协议解析器 :启用 PS/2 解码插件,自动标注 START/DATA/PARITY/STOP;
  • 触发条件 :设置 DATA 下降沿触发,捕获完整帧;
  • 关键观察点 :START 到第一 DATA 延迟、位宽一致性、STOP 电平持续时间。

6. 扩展应用场景与进阶实践

6.1 PS/2 键盘作为安全输入设备

在 Secure Boot 或密码输入场景,利用其 无固件可篡改性 (相比 USB 键盘内置微控制器):

  • 驱动层剥离所有非必要功能(LED 控制、重传),仅保留扫描码接收;
  • 将扫描码直接送入加密协处理器(如 STM32 HSM)进行密钥派生;
  • 禁用所有软件缓冲,实现“按键即加密”的零延迟路径。

6.2 多键盘并行接入

通过 GPIO 扩展(如 MCP23017)或 MCU 多组 EXTI,实现 N 键盘输入:

  • 为每键盘分配独立 CLK 中断线;
  • 共享 DATA 总线(需加二极管隔离防冲突);
  • 在中断服务程序中,通过 CLK 线号识别来源键盘,写入不同队列。

6.3 与 OLED/LCD 的实时反馈集成

// 在 key_event 回调中实时更新屏幕
void on_key_event(uint8_t sc, uint8_t release) {
    static char buf[32];
    if (!release) {
        char c = ps2_to_ascii(sc, modifiers);
        if (c && len < sizeof(buf)-1) {
            buf[len++] = c;
            buf[len] = '\0';
            SSD1306_DisplayString(0, 0, buf, FONT_12X24); // 刷新 OLED
        }
    }
}

此方案构成完整的嵌入式终端输入子系统,无需 OS 支持即可运行。

PS/2 驱动的价值不在于其古老,而在于它迫使工程师直面数字电路的本质时序、电平与噪声。当 USB 协议栈在数百行代码中隐藏了所有细节,PS/2 的 110μs 位周期却要求你在寄存器层面与每一个上升沿对话。这种对底层确定性的绝对掌控,正是嵌入式系统可靠性的终极基石。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐