ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

51单片机读取PS2键盘例程源码详解:从协议原理到调试排错

51单片机读取PS2键盘例程源码详解:从协议原理到调试排错 简介本资源是一套面向嵌入式初学者与单片机课程实践者的51单片机PS2键盘接口开发例程聚焦硬件通信协议解析、扫描码映射与抗抖动处理等核心难点助力读者掌握串行外设驱动开发全流程。压缩包共21个文件含3个关键头文件.h、3个C源码.c、3个目标文件.obj及.hex可执行镜像、.uv2工程配置、.lst编译列表等完整覆盖Keil C51开发环境下的编码、编译、调试与烧录环节总大小仅40KB轻量易用。已有59人学习下载适合高校实验课、电子设计入门及小型人机交互项目拓展。读者可直接导入Keil工程运行通过LCD1602实时显示按键字符结合KeyBoard.c中扫描码表与中断轮询逻辑深入理解PS2时钟同步机制、位采集时序控制及ASCII映射实现是少有的兼顾原理讲解与可运行代码的综合性教学资源。 拿到这块板子的时候我第一反应是51单片机例程源码里居然还藏着PS2键盘实验。说实话做过几个单片机项目之后你会发现PS2键盘这套协议虽然老但在课程设计、毕设、甚至工业设备改造里一直没消失。它的价值在于你把PS2协议吃透后面接矩阵键盘、接触摸屏、接各种串行外设思路都是通的。这篇我就把这套例程源码的完整拆解、硬件接线、代码逻辑、调试踩坑全部写出来给正在做51单片机实验或者打算拿PS2键盘做交互的同学一份能直接抄的作业。先说清楚这套例程能干什么51单片机通过PS2接口读取标准键盘的按键扫描码在数码管或LCD上显示键值按下和松开都能识别支持组合键扩展。适合的人群很明确——学过C语言基础、正在入门51单片机的中级选手尤其是课程设计选中了“键盘输入显示输出”这个经典组合的同学。硬件成本极低一块51开发板加一个旧键盘就能跑起来但软件里的时序处理、中断嵌套、数据校验这些点恰恰是很多教程没讲透的地方。1. 整体设计与思路拆解为什么PS2协议值得手动写一遍1.1 PS2协议的核心价值两根线闯天下最开始接触PS2接口的时候我总想着是不是要什么高大上的协议芯片。实际上PS2键盘通信只有两根线——时钟线CLK和数据线DATA外加电源和地。这个“串行协议”的精髓在于所有数据都是按位传输时钟线负责节奏数据线负责内容。主机端的51单片机只需要在时钟的边沿去采样数据线的电平就能拿到键盘发来的完整字节。这套机制对51单片机来说特别友好。51的IO口速度虽然不快但PS2的时钟频率通常在10kHz到16.7kHz之间换算下来每bit大约60到100微秒单片机完全能靠查询或者中断跟上这个节奏。对比I2C或者SPIPS2不需要额外的地址概念键盘是唯一的从设备主从关系简单清晰。所以很多老工程师到现在还是喜欢用PS2键盘做“人机交互输入”就是因为它稳定且协议透明出了问题自己就能定位。1.2 从例程源码看作者的协议设计思路拿到这套例程源码我一般先不看程序主体而是看它的头文件定义和引脚配置。这样能最快推断出作者的硬件接法和设计意图。例程里通常会有这么几个关键点一是CLK接在哪个引脚这直接决定了用中断还是查询二是DATA接在哪个引脚决定了读取的IO操作方式三是有没有定义键盘状态标志位比如按下标志、松开标志、扩展码标志。这三个点想清楚了整个程序的骨架也就出来了。这套例程的设计思路很典型用外部中断接收CLK下降沿在中断服务函数里读取DATA电平拼装成完整的字节再用一个状态机处理扫描码。这种“中断接收主循环解析”的结构在高实时性场景里很常见因为它既能保证不丢数据又不会让主循环卡在读键盘上。相比纯查询方式中断方式让单片机在按键间隙可以腾出精力去做别的事情比如刷新数码管、扫描传感器、跑定时器任务。1.3 为什么不用矩阵键盘而选PS2有同学会问51单片机实验不是经常用4x4矩阵键盘吗为什么还要拿PS2键盘来折腾。我的理解是矩阵键盘适合按键数量少、布局固定的场景但一旦你需要在项目里输入字母、数字混合的字符串比如密码锁、温度报警阈值设置、简易计算器4x4矩阵键盘的键值映射能把人逼疯。PS2键盘一次能上报上百个按键通码、断码自带识别按下和松开天然区分这在做交互类项目时省掉大量编码工作。而且PS2协议学一遍的价值在于“通用性”。你学会了怎么接收PS2键盘的数据基本上也就学会了怎么驱动PS2鼠标两者的帧格式类似只是数据含义不同。以后如果项目中要接条码枪、接收费键盘很多设备都支持PS2接口协议到时候你就知道当初这份例程源码有多值钱了。2. 硬件连接与电路设计2.1 引脚分配CLK和DATA接到哪个IO更合理先看我实际测试过的一套引脚分配方案这是大多数开发板的默认接法信号51单片机引脚说明CLKP3.2INT0外部中断0输入端下降沿触发DATAP3.3普通IO口读取VCC5VPS2键盘供电GNDGND共地选择P3.2而不是其他IO口的理由是51单片机的外部中断0正好映射到P3.2PS2键盘的CLK产生下降沿时硬件自动跳转到中断服务函数不需要主循环反复查询实时性最好。如果不用中断改成CLK接P1.0、DATA接P1.1程序里死循环等待时钟跳变也可以但处理器占用率高不适合复杂的项目逻辑。需要注意P3.2和P3.3在标准51里都有第二功能串口和外部中断相关但作为普通IO使用时不影响。如果是STC增强型51还有更灵活的引脚选择但例程通常以标准89C52为基准照着P3.2/P3.3接是最稳的。2.2 上拉电阻和电平匹配问题PS2键盘的CLK和DATA都是开漏输出正常工作状态下需要外接上拉电阻到VCC典型阻值在1k到10k之间。51单片机的P1、P2、P3口内部有上拉但上拉能力有限实测中如果线长超过30厘米信号边沿会变缓导致采样错误。这时候就体现出外部上拉电阻的重要性了。开发板上一般已经集成了4.7k或者10k的上拉电阻如果你是自己搭的电路务必在CLK和DATA线上各接一个4.7k电阻到5V。另外要注意电平匹配。现在很多51开发板用的还是5V供电而PS2键盘本身就是5V逻辑直接兼容。如果你用的是3.3V版单片机比如STM32或者新出的低压51就需要做电平转换否则键盘输出的高电平对3.3V单片机来说可能识别不了严重时还会倒灌电流烧坏IO口。这属于老生常谈但每次项目里都有人栽在这上面。2.3 接线实操从PS2母座到单片机杜邦线PS2接口常见的有两种一种是6针圆形母座一种是4针排针扩展口。6针定义里第1脚是DATA第4脚是VCC第5脚是CLK第3脚是GND剩下的第2脚和第6脚是保留位不用管。从母座往单片机接线时记得把金属外壳的屏蔽层接到GND尤其是线比较长的场合屏蔽能减少干扰。我自己搭线的时候习惯用杜邦线和面包板先验证确认程序正常后再做成PCB或者焊接到洞洞板上。这样分两步走的习惯能避免很多低级错误——先排除硬件问题再优化结构。还有一点PS2键盘的线序不同厂家可能有细微差别拿到母座后先用万用表量一下哪根针接的是GND、哪根是VCC千万不要看图就往上怼。3. 软件框架与核心代码解析3.1 数据帧格式起始位、数据位、校验位、停止位PS2协议每次发送11位数据1个起始位低电平、8个数据位LSB在前、1个奇偶校验位、1个停止位高电平。时钟线在整个传输过程中连续输出11个脉冲接收方在时钟的下降沿采样数据线。举个例子如果键盘要发送通码0x1CA键按下二进制是00011100LSB在前发送实际线上看到的顺序是0,0,0,1,1,1,0,0,0最后两位是高位。再加上起始位、校验位、停止位一共就是11个bit。这就是为什么很多人第一次看PS2波形会觉得“对不上号”——因为你得先把字节按位反转再逐位点着波形数。例程源码里通常有一段移位组装的代码每收到一个时钟下降沿就把DATA的电平值移到暂存字节的高位或低位收完8个数据位之后再做反转处理。理解了这一点整个接收逻辑就通了。3.2 中断接收代码不丢数据的核心下面这段代码是例程里中断接收的核心片段我加了详细注释sbit PS2_CLK P3^2; sbit PS2_DATA P3^3; unsigned char ps2_byte 0; // 当前正在接收的字节 unsigned char ps2_bit_count 0; // 已接收的位数 unsigned char ps2_buf 0; // 完整收到的8位数据 bit ps2_ready 0; // 一帧接收完成标志 void ps2_isr(void) interrupt 0 { unsigned char sample; sample PS2_DATA; // 注意在下降沿读数据线 if (ps2_bit_count 0 ps2_bit_count 8) { if (sample) { ps2_byte | (0x01 (ps2_bit_count - 1)); } } ps2_bit_count; if (ps2_bit_count 10) { // 起始位1位 数据位8位 校验位1位 停止位1位 11位 // 当计数为11时说明一帧结束 ps2_buf ps2_byte; ps2_ready 1; ps2_bit_count 0; ps2_byte 0; } }这里最容易被忽略的是采样时机和位序。PS2协议中键盘在CLK的低电平期间改变DATA状态在上升沿让接收方读取最稳。但51的习惯是在下降沿进入中断这个时刻DATA可能还在建立过程中所以代码里在中断ISR刚进入时立即读取DATA不要加多余的延时。如果用查询方式就在CLK变低后延时20微秒左右再去读DATA这个值是实践试出来的太短读到电平不稳定太长会错过下一拍。3.3 通码、断码与扩展码的处理PS2键盘发给主机的不是ASCII码而是扫描码。扫描码分三种情况通码make code表示按键按下断码break code表示按键松开前导码表示扩展按键。拿常规按键A来说按下时发送0x1C松开时发送0xF0 0x1C先发0xF0再发通码。这里的0xF0就是“断开前缀”。而右Alt、方向键这类键会多一个0xE0前缀例如右Alt按下是0xE0 0x11松开是0xE0 0xF0 0x11。例程里如果只是简单显示键值一个字节就够。但如果你要识别组合键比如CtrlC、ShiftA就必须维护一个“当前按键状态表”——收到通码时把对应位清零收到断码时把对应位置1。这样才能知道某一时刻Shift是不是按着的进而决定对后续扫描码做怎么样的映射。这套状态机看起来不复杂但初学者经常栽在处理多字节序列上0xE0和0xF0都是特殊标志位必须缓存到状态变量里不能当成普通键值直接显示。3.4 键盘状态机的实现细节我比较推荐在主循环里用“事件”的方式处理扫描码而不是在中断里直接做复杂的逻辑。中断只负责把原始字节放进缓冲区主循环再解析。这样做的好处是中断服务函数尽量短不会影响键盘的时序。拿例程来说它通常有一个缓冲区数组保存最近几个字节然后主循环按“普通键 / 0xE0前缀 / 0xF0前缀”三种状态转移来解析。伪代码逻辑如下unsigned char key_buffer[4]; unsigned char key_index 0; bit last_was_e0 0; bit last_was_f0 0; void main() { while (1) { if (ps2_ready) { key_buffer[key_index] ps2_buf; ps2_ready 0; parse_key(key_buffer[key_index]); } // 其他任务显示刷新、报警、通信等 } } void parse_key(unsigned char code) { if (code 0xE0) { last_was_e0 1; return; } if (code 0xF0) { last_was_f0 1; return; } // 如果last_was_f0为1说明当前code是断码可以复位状态 // 如果last_was_e0为1说明当前code是扩展键 // 最后清标志位 last_was_e0 0; last_was_f0 0; }这里有一个关键点0xE0和0xF0可能连续出现比如扩展键的断码是0xE0, 0xF0, code所以标志位的处理要小心不能在遇到0xF0时就把E0标志清掉。例程源码中用了独立标志位而不是一个枚举状态就是为了避免这类多字节组合的“状态爆炸”。我自己在写的时候还会加一个字节超时机制如果收到0xE0后超过几十毫秒没有后续字节就清掉标志防止键盘复位或线接触不良时程序卡死。4. 实操过程与核心环节实现4.1 开发环境准备与工程配置我用的是Keil C51这是51单片机最主流的开发工具。新建工程时选择芯片型号比如AT89C52或者STC89C52RC然后添加一个主程序文件和一个头文件。编译选项里最重要的是开启“生成HEX文件”因为烧录器需要HEX格式。另外留意一下晶振频率设置如果板子上是11.0592MHz就在Option for Target里的Xtal(MHz)填11.0592如果是12MHz就填12。这个参数会影响定时器初值和波特率计算PS2实验里虽然不直接用定时器但系统性配置别出错。例程源码的main函数一般结构是初始化IO口、初始化外部中断、清空显示缓冲区、然后进入死循环。初始化代码里要设置IT01把外部中断0设为下降沿触发这是PS2接收的关键。有些开发板还需要设置EA1总中断、EX01允许外部中断0。中断优先级不用改默认优先级足够。4.2 显示部分用数码管显示扫描码实验例程一般会配一个数码管显示模块把收到的扫描码以十六进制形式显示出来。比如按一下A键数码管显示“1C”按一下B键显示“32”。原理很简单把收到的字节拆成高四位和低四位每个四位对应一个十六进制字符查表转换成段码送到数码管。如果你的板子是4位数码管还可以做成“按下显示通码松开显示断码”的模式这样调试时能直观看到通码和断码的完整数据流。unsigned char code hex_table[] { 0xC0, 0xF9, 0xA4, 0xB0, // 0 1 2 3 0x99, 0x92, 0x82, 0xF8, // 4 5 6 7 0x80, 0x90, 0x88, 0x83, // 8 9 A b 0xC6, 0xA1, 0x86, 0x8E // C d E F }; void display_hex(unsigned char val) { unsigned char h val 4; unsigned char l val 0x0F; P0 hex_table[h]; // 高位段码 delay_ms(5); P0 hex_table[l]; // 低位段码 delay_ms(5); }这段代码里有一个工程细节数码管是共阳还是共阴直接影响段码表。我给的段码表是共阳数码管的如果你手里是共阴就得把所有段码取反。很多同学第一次做实验发现数码管显示的数字不对不是程序逻辑错了而是段码表极性搞反了。这个排查点几乎能解决80%的显示异常问题。4.3 用LCD1602显示字符按下就显示对应ASCII数码管显示十六进制扫描码适合调试真正做“键盘输入”项目时大多数人还是愿意接一块LCD1602直接显示字符。例程里会有扫描码转ASCII的映射表核心思路就是把键盘扫描码作为索引查表得到对应的ASCII字符再送到LCD显示。比如在标准映射表里0x1C对应‘A’keycode 0x1C是A键0x1B对应‘S’0x23对应‘D’0x2B对应‘F’。数字键0到9则分布在0x45到0x4E字母键则以QWERTY布局排列。实际做中文菜单的时候需要配合Shift状态去切换大小写不加Shift就显示小写加Shift显示大写这个功能需要上一节说的组合键状态机支持。LCD1602的驱动这里不多展开例程里通常会有专门的lcd.c文件提供lcd_init()、lcd_showchar()、lcd_showstring()几个接口。你只需要在解析到有效字符时调用显示函数设置好显示坐标即可。4.4 增加蜂鸣器按键反馈实验如果只有显示反馈感比较弱。我习惯在代码里加一个蜂鸣器反馈每次检测到有效按键通码时让蜂鸣器响一声。实现方法是解析到新的通码后启动一个蜂鸣器延时比如10毫秒然后关闭。注意不要在中断里做延时因为延时会阻塞PS2时钟的接收导致后续字节丢失。正确做法是设置一个beep_tick变量在主循环里递减直到减到0后关蜂鸣器。unsigned int beep_tick 0; // 在解析到有效通码时调用 void beep(int duration_ms) { beep_tick duration_ms; BEEP 1; // 开蜂鸣器 } // 主循环里 if (beep_tick) { beep_tick--; if (beep_tick 0) { BEEP 0; // 关蜂鸣器 } }这种“非阻塞延时”的思路是单片机开发的基础课很多初学者习惯用delay_ms()阻塞延时一旦放在中断里或者主循环关键路径上整个系统的实时性就崩了。例程源码能跑得稳很大程度上就是因为这类细节处理得好。5. 常见问题与排查技巧实录5.1 按键没反应先从电平开始查这是PS2实验里最常遇到的问题。按键后数码管毫无反应一般排查路径是先量PS2键盘的VCC和GND是否正常再量CLK和DATA的静态电平。正常情况下两根信号线都应该被上拉到高电平接近5V。如果其中一根是低电平很可能是上拉电阻没接或者接错了。然后按下键用示波器或逻辑分析仪看CLK是否有脉冲。如果不方便用示波器可以先写一个最简单的GPIO翻转程序把CLK引脚配置成普通输入不断读取并取反输出到另一个引脚接LED看LED是否闪烁来间接判断是否有时钟信号。这个查法不算“高科技”但效率极高。很多同学一上来就把代码翻来覆去改实际上硬件问题没有解决代码改到天亮也没用。5.2 显示乱码校验位和采样时机显示出来了但数据完全不对比如按A显示了0xFF或者乱跳这种情况十有八九是采样时机不对。PS2键盘的时序是CLK下降沿时DATA已经稳定但有些键盘在下降沿前后会有短暂的不稳定窗口。如果你的中断服务函数里延迟太久才读DATA读到的可能是下一个bit或者毛刺。解决方法是进入中断后第一条语句就读取DATA不要在中断里做任何可能耗时的工作甚至不要调用函数。如果这样还是有误码可以尝试在下降沿之后加一个很小的延时比如10微秒再采样因为不同键盘时序存在差异需要微调。实际项目里我习惯把读DATA和位组装的代码写成内联避免函数调用进出栈消耗时间。5.3 键盘能显示但无法区分按下和松开这个问题的根源是程序没有处理0xF0断码前缀。如果你只做了一个“收到字节就显示”的功能按下A时收到0x1C松开时收到0xF0然后0x1C这两个字节会被当作两次独立的按键处理之前说的状态机派上用场了。必须在解析时判断当前字节是否为0xF0如果是说明下一个字节是断码应该标记为“松开”而不是“按下”。例程里通常会有一个结构体或者状态变量保存当前按键状态只有通码才更新显示和蜂鸣器断码只清理状态表里的对应位。这样按下A显示“A”松开A不会重复显示“A”窗口上的表现就像正常键盘一样。5.4 长线连接时的抗干扰处理实验台一般桌面距离到板子也就几十厘米但如果你的项目和PS2键盘之间线长超过1米就要考虑干扰问题。解决方法有两个方向一是把CLK和DATA两根线用双绞线的形式绞在一起同时让它们靠近GND走线减少环路面积二是在单片机端对CLK和DATA各加一个10nF左右的电容到地滤除高频毛刺但这会让信号边沿变缓不是电容越大越好小容量起步测试。我在一次给设备键盘改装的经历中遇到过长线接上后开机不认键盘把线缩短到50厘米就一切正常后来加了一级74HC14施密特触发器整形才解决。如果你不想那么麻烦最简单的办法就是供电、时钟、数据三根线一起做屏蔽外层屏蔽层单端接地基本能解决90%的干扰问题。5.5 常见问题速查表现象可能原因解决方向完全没反应VCC/GND接反或未供电万用表量电压CLK静态低电平上拉电阻缺失或接错接4.7k或10k上拉到5V数据显示乱码采样时机不对、位序反转错误中断首条语句读DATA检查LSB位序按一次显示多个字符键盘处在连发状态/状态机未清标志检查通码、断码的释放处理松开后还会重复触发没有识别0xF0前缀完善状态机处理断码数码管显示段码奇怪共阳共阴段码表不对确认数码管类型必要时取反段码偶尔丢键中断中有长延时/中断被屏蔽缩短ISR只在主循环做解析6. 项目扩展与进阶思路6.1 用PS2键盘实现密码锁拿到例程源码后很多人第一个扩展方向是密码锁。思路是把键盘输入缓冲到数组里按下回车键时比对输入内容和预设密码。这里需要额外处理退格键0x66和回车键0x5A以及小键盘区或者字母区输入的区分。密码锁项目的难点在于“输入缓冲区的溢出保护”和“密码比对时机”比单纯显示字符又高了一个层次。我做这个扩展的时候花了不少时间在“按下回车后清空缓冲区”这个细节上。如果不清空下一次输入会把上一轮的密码残留拼接在一起永远比对不成功。所以代码里要明确定义输入一个字符追加到缓冲区按下回车检查并清空按下退格删除末尾字符超过最大长度拒绝输入。这套逻辑放到现在做任何键盘交互产品都一样适用。6.2 组合OSSHIFT状态切换大小写刚才提到的Shift组合键实现方式比想象中简单在状态表里给Shift键通码0x12是左Shift0x59是右Shift设置一个标志位。每次收到普通键时先查这个标志如果为1就用大写字母映射表为0就用小写映射表。关键是Shift键自身的通断也走同样的状态机按下时置1松开时清零。这个机制还能扩展成“功能键方向键”控制小车、控制菜单翻页等等。比如方向键UP的扫描码是0xE0 0x75你可以在解析到E0前缀后对后续字节判0x75然后执行上位动作。例程源码里已经预留了E0标志位你只需要在状态机里增加对应的处理分支。6.3 和传感器模块联动做一个“带键盘输入的监控终端”51单片机课程设计里很常见的题目是温湿度检测报警系统、电磁炉简易控制这类项目。如果把PS2键盘加进去就可以从“固定阈值报警”升级成“用户可设定阈值的交互终端”。操作逻辑可以是按下键盘上的数字键输入目标温度按下回车确认按下ESC取消。这个过程中键盘显示、数码管/LCD刷新、温度传感器读取可以并行工作正好发挥中断接收键盘数据的优势。我觉得这是例程源码最有潜力的应用场景。因为纯传感器项目“只能看不能改”而PS2键盘给了用户“输入”的能力让整套系统从单向展示变成双向交互。毕设里要是能把这层逻辑讲清楚评委印象分会高不少。6.4 用51单片机模拟PS2主机模式还有一种进阶玩法是角色互换让51单片机模拟成PS2主机主动向键盘发送控制命令比如启用/禁用NumLock指示灯、请求键盘发送设备ID等。这需要向CLK线主动拉低至少100微秒来暂停键盘通信然后往DATA线上写入命令字节再释放CLK让键盘接收。PS2协议中主机向设备发送数据时数据的改变发生在CLK低电平设备在CLK上升沿采样方向和键盘到主机正好相反。这个玩法已经把PS2协议理解得很深了。虽然对课程设计可能用不上但如果你打算做键盘转接器或者键盘测试工具这是必经之路。例程源码里没有这部分不过基于给你看的接收代码反推主机模式只是把读改成写结构类似。最后的操作体会讲真PS2键盘实验在51单片机例程源码里属于那种“不起眼但非常耐嚼”的项目。整份代码量不大原理也不复杂但它把中断、时序、状态机、串行协议这些嵌入式开发的基本功全串起来了。我建议你拿到例程后不要只烧录跑通就完事把CLK和DATA的波形用逻辑分析仪完整抓一遍看着11个脉冲如何在示波器上流动键盘协议就真正变成你自己的基本功了。如果你刚做完这个实验下一步试试把例程里的中断接收改成查询方式观察两种方式在运行其他任务时的区别再把状态机从“通码断码处理”扩展到“E0扩展码处理”。这么折腾一遍之后我看好你直接就能上手做更复杂的单片机交互项目。本文还有配套的精品资源点击获取
返回列表