104键PS2接口标准键盘C语言驱动程序

一、PS2键盘通信协议概述

PS2键盘采用双向同步串行通信协议,通过CLK(时钟线) 和DATA(数据线) 与主机通信,核心特性:

  • 数据传输格式:11位/帧(1起始位(0) + 8数据位(LSB先发) + 1奇校验位 + 1停止位(1)),速率10-16.7kHz(位宽60-100μs)。

  • 通信模式:键盘主动发送(通码/断码),主机被动接收;主机可发送命令(如复位、设置LED)。

  • 扫描码 :104键键盘使用Make Code(通码,按下)Break Code(断码,释放),断码=通码最高位置1(如通码0x1C→断码0x9C),扩展码以0xE0前缀标识(如右Ctrl通码0xE014)。

二、硬件设计

2.1 核心组件

模块 说明
微控制器 51系列/STM8/STM32(GPIO控制)
PS2键盘 104键标准键盘(PS2接口)
电路连接 CLK→PA0(输入,上拉4.7kΩ),DATA→PA1(输入/输出,上拉4.7kΩ)

三、软件设计(C语言,模块化实现)

3.1 头文件与宏定义

c 复制代码
#include "reg52.h"  // 以51单片机为例,其他平台需调整
#include "intrins.h"

// PS2引脚定义(可修改为实际硬件引脚)
sbit PS2_CLK = P1^0;  // 时钟线(输入/输出)
sbit PS2_DAT = P1^1;  // 数据线(输入/输出)

// 扫描码表(部分常用键,完整104键需扩展)
#define KEY_ESC    0x76
#define KEY_1      0x16
#define KEY_A      0x1C
#define KEY_SPACE  0x29
#define KEY_SHIFT_L 0x12
#define KEY_CTRL_L  0x14
#define KEY_EXT    0xE0  // 扩展码前缀

// 键值映射(通码→ASCII/功能码)
unsigned char code keyMap[128] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x00-0x0F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x10-0x1F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x20-0x2F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x30-0x3F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x40-0x4F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x50-0x5F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  // 0x60-0x6F
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0   // 0x70-0x7F
};

3.2 核心函数实现

3.2.1 微秒级延时(时序控制)
c 复制代码
void delay_us(unsigned int us) {
    while (us--) {
        _nop_(); _nop_(); _nop_(); _nop_();  // 12MHz晶振下,4个_nop_≈1μs
    }
}
3.2.2 PS2数据接收(键盘→主机)

功能:按PS2协议接收1字节数据(含起始/数据/校验/停止位),返回接收值(0xFF表示错误)。

c 复制代码
unsigned char ps2_receive(void) {
    unsigned char data = 0, bit = 0, parity = 1;  // 奇校验初始为1
    unsigned int timeout = 1000;  // 超时计数(防死等)
    
    // 等待起始位(CLK高,DATA低)
    while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    delay_us(5);  // 等待数据稳定
    
    // 接收8位数据位(LSB先发)
    for (bit=0; bit<8; bit++) {
        while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;  // 等待CLK高
        data >>= 1;  // 右移(LSB先收)
        if (PS2_DAT) { data |= 0x80; parity ^= 1; }  // 记录数据位,更新校验
        while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;  // 等待CLK低
    }
    
    // 接收校验位
    while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    if (PS2_DAT) parity ^= 1;  // 校验位为1则翻转parity
    while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    
    // 接收停止位(应为1)
    while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    if (!PS2_DAT) return 0xFF;  // 停止位错误
    while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    
    // 校验结果(奇校验:1的个数为奇数)
    if (parity != 1) return 0xFF;  // 校验失败
    return data;
}
3.2.3 PS2命令发送(主机→键盘)

功能:向键盘发送1字节命令(如复位0xFF、设置LED 0xED),返回键盘回应(0xFA为成功)。

c 复制代码
unsigned char ps2_send(unsigned char cmd) {
    unsigned char data = cmd, bit = 0, parity = 1;
    unsigned int timeout = 1000;
    
    // 主机控制CLK为低(请求发送)
    PS2_CLK = 0;
    delay_us(100);  // 保持低电平>100μs
    PS2_DAT = 0;    // 起始位(0)
    delay_us(10);
    PS2_CLK = 1;    // 释放CLK,由键盘控制
    
    // 发送8位数据位(LSB先发)
    for (bit=0; bit<8; bit++) {
        while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;  // 等待CLK低
        PS2_DAT = (data & 0x01) ? 1 : 0;  // 发送最低位
        data >>= 1;
        parity ^= (data & 0x01);  // 更新校验(注意:此处需先取位再移位)
        while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;  // 等待CLK高
    }
    
    // 发送校验位(奇校验)
    while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    PS2_DAT = parity;
    while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    
    // 发送停止位(1)
    while (PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    PS2_DAT = 1;
    while (!PS2_CLK && timeout--); if (timeout == 0) return 0xFF;
    
    // 接收键盘回应(0xFA)
    return ps2_receive();
}
3.2.4 键盘初始化

功能:发送复位命令,等待键盘回应(0xAA=自检通过,0x00=设备ID)。

c 复制代码
bit keyboard_init(void) {
    unsigned char ack, bat;
    ps2_send(0xFF);  // 发送复位命令
    ack = ps2_receive();  // 应返回0xFA(确认)
    if (ack != 0xFA) return 0;
    bat = ps2_receive();  // 应返回0xAA(BAT通过)
    if (bat != 0xAA) return 0;
    ps2_receive();  // 接收设备ID(0x00)
    return 1;  // 初始化成功
}
3.2.5 扫描码处理(通码/断码→键值)

功能:维护修饰键状态(Shift/Ctrl),将扫描码转换为ASCII/功能码。

c 复制代码
unsigned char shift_flag = 0, ctrl_flag = 0;  // 修饰键状态
unsigned char key_value = 0;  // 当前键值

void process_scancode(unsigned char code) {
    static unsigned char ext_flag = 0;  // 扩展码标志
    
    if (code == KEY_EXT) {  // 扩展码前缀
        ext_flag = 1;
        return;
    }
    
    if (ext_flag) {  // 处理扩展码第二字节
        ext_flag = 0;
        switch (code) {
            case 0x14: key_value = (shift_flag ? KEY_CTRL_R : KEY_CTRL_L); break;  // 右Ctrl
            // 其他扩展键(如右Alt、数字小键盘)需补充
        }
        return;
    }
    
    if (code & 0x80) {  // 断码(释放)
        code &= 0x7F;  // 清除最高位
        switch (code) {
            case KEY_SHIFT_L: shift_flag = 0; break;
            case KEY_CTRL_L: ctrl_flag = 0; break;
        }
    } else {  // 通码(按下)
        switch (code) {
            case KEY_SHIFT_L: shift_flag = 1; break;
            case KEY_CTRL_L: ctrl_flag = 1; break;
            case KEY_A: key_value = (shift_flag ? 'A' : 'a'); break;
            case KEY_1: key_value = (shift_flag ? '!' : '1'); break;
            // 其他键映射需补充完整扫描码表
        }
    }
}

3.3 主程序流程

c 复制代码
void main(void) {
    unsigned char scancode;
    
    // 初始化:引脚设为输入,上拉
    PS2_CLK = 1; PS2_DAT = 1;
    
    // 键盘初始化
    if (!keyboard_init()) {
        // 初始化失败(如LED报警)
        while(1);
    }
    
    // 主循环:接收扫描码并处理
    while (1) {
        if (PS2_CLK == 0) {  // 检测键盘发送数据(CLK低)
            scancode = ps2_receive();
            if (scancode != 0xFF) {  // 接收成功
                process_scancode(scancode);
                // 输出键值(如LCD显示、串口发送)
                // printf("Key: 0x%02X, Value: %c\n", scancode, key_value);
            }
        }
    }
}

参考代码 104键PS2接口标准键盘程序(C语言) www.youwenfan.com/contentcss/161311.html

四、关键问题与解决方案

4.1 时序错误导致数据接收失败

  • 原因:延时不足或CLK信号检测错误。

  • 解决 :用示波器测量CLK/DATA波形,调整delay_us参数,确保位宽符合10-16.7kHz要求。

4.2 扫描码解析遗漏扩展键

  • 原因:未处理0xE0前缀的扩展码。

  • 解决 :在process_scancode中增加ext_flag状态机,记录扩展码前缀,分两次接收完整扫描码。

4.3 键盘无响应

  • 原因:未通过复位命令初始化,或电源/接线错误。

  • 解决 :用ps2_send(0xFF)发送复位命令,检查键盘是否返回0xFA+0xAA,确保VCC(5V)、GND、CLK、DATA连接正确。

五、测试与验证

  1. 硬件连接:按2.1节连接键盘与微控制器,确保上拉电阻(4.7kΩ)接5V。

  2. 功能测试:按下字母键,通过串口助手观察输出的ASCII码(如按"A"显示0x41或0x61,取决于Shift状态)。

  3. 扩展测试:测试数字小键盘、功能键(F1-F12),补充完整扫描码表。

六、总结

基于PS2协议实现了104键键盘的驱动,核心是时序精确的收发函数和扫描码解析逻辑。通过模块化设计,可扩展支持全键扫描码映射、LED控制(如Num Lock指示灯)、连击检测等功能,适用于嵌入式设备的人机交互接口。

相关推荐
爱编码的小八嘎11 小时前
C语言完美演绎6-17
c语言
DfromY13 小时前
【随手记】YOCTO下MQTT使用简记
c语言·网络协议
计算机安禾15 小时前
【数据结构与算法】第22篇:线索二叉树(Threaded Binary Tree)
c语言·开发语言·数据结构·学习·算法·链表·visual studio code
算法鑫探15 小时前
解密2025数字密码:数位统计之谜
c语言·数据结构·算法·新人首发
:mnong15 小时前
Superpowers 项目设计分析
java·c语言·c++·python·c#·php·skills
Saniffer_SH15 小时前
【每日一题】一台可编程的PCIe 6.0主机 + 一套自动化CTS验证平台 + 一个轻量级链路分析系统
运维·服务器·测试工具·fpga开发·自动化·计算机外设·硬件架构
计算机安禾15 小时前
【数据结构与算法】第21篇:二叉树遍历的经典问题:由遍历序列重构二叉树
c语言·数据结构·学习·算法·重构·visual studio code·visual studio
笨笨饿17 小时前
26_为什么工程上必须使用拉普拉斯变换
c语言·开发语言·人工智能·嵌入式硬件·机器学习·编辑器·概率论
初生牛犊不怕苦18 小时前
与AI一起学习《C专家编程》:数组与指针
c语言·学习·算法