PS/2键盘驱动设计:嵌入式底层时序与状态机实现
PS/2接口是一种经典的双向串行通信协议,广泛应用于工业控制、HMI和教学实验等对实时性与确定性要求严苛的嵌入式场景。其核心在于硬件级时序同步、开漏信号电平约束及扫描码映射机制,区别于USB HID的复杂协议栈。理解CLK主控、下降沿触发、奇校验帧结构等原理,是构建高鲁棒性键盘驱动的基础。技术价值体现在极低资源占用、无OS依赖、抗干扰能力强,适用于FreeRTOS、HAL库乃至裸机环境。典型应用场
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 帧完整性校验强化
除协议规定的奇校验外,增加两级校验:
- STOP 位确认 :STOP 必须为
1,且持续至少 2 个 CLK 周期(在 STOP 状态等待第二个 CLK 上升沿验证); - 帧间隔监控 :记录上帧结束时刻,若新 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 位周期却要求你在寄存器层面与每一个上升沿对话。这种对底层确定性的绝对掌控,正是嵌入式系统可靠性的终极基石。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)