1. 从底层开始:为什么今天还要折腾PS/2鼠标协议
提到PS/2鼠标接口,不少年轻开发者的第一反应可能是:这不是博物馆里的老古董吗?我刚开始接触单片机时也是这么想的,直到有一次做一个嵌入式项目,需要从一台旧工业主机上采集鼠标轨迹,被迫开始啃PS/2协议,才意识到这个"老古董"身上藏着很多底层通信的通用智慧。
PS/2接口协议指的是IBM在1987年随PS/2系列个人电脑推出的键盘鼠标通信规范,后来被广泛用于各类台式机的圆形6针接口上。在USB鼠标一统天下的今天,PS/2接口依然在工业控制设备、嵌入式系统、特种计算机、老旧设备维护等领域存在大量存量设备。很多工控主板至今保留着PS/2接口,因为它在实时性和确定性上有着USB难以替代的优势——PS/2是基于中断驱动的双向同步串行协议,没有USB那样的轮询延迟和枚举过程。
这篇博文适合三类人看:一是初学单片机、想通过实际协议案例巩固时序概念的同学,二是需要做老旧设备改造或嵌入式鼠标采集的工程师,三是对计算机底层通信机制有好奇心的技术爱好者。读完你不仅能够徒手解析PS/2鼠标发出的每一个数据位,还能自己用逻辑分析仪抓时序、用单片机模拟PC主机读取鼠标数据,踩坑经验也一并奉上。
2. 核心原理拆解:PS/2协议到底在传什么
2.1 物理层:4根有效线,双向同步通信
PS/2鼠标物理接口通常是一个6针mini-DIN插座,但真正起到作用的只有4根线:DATA(数据线)、CLOCK(时钟线)、VCC(+5V电源)、GND(地线),另外两个脚是预留的。这个设计对比USB的四线制(VBUS、D+、D-、GND)要朴素得多。
关键点在于:DATA和CLOCK都是双向开漏输出,平时由上拉电阻拉高到5V,无论是主机侧还是设备侧,都可以主动拉低这两根线。这一点决定了PS/2协议的通信控制权归属——主机(Host)是时序控制的主宰者,鼠标(Device)属于被动角色。主机通过拉低CLOCK一段时间来"抑制"鼠标发送,鼠标在发送数据前必须检查CLOCK电平,这就是后面讲述时序时的核心逻辑。
2.2 数据帧格式:起始位+8数据位+校验+停止位
PS/2鼠标采用**LSB优先(最低有效位在前)**的串行数据帧,一帧共11位,如下图所示的排列顺序:
起始位(0) | Bit0 | Bit1 | Bit2 | Bit3 | Bit4 | Bit5 | Bit6 | Bit7 | 奇偶校验 | 停止位(1)这个帧结构和UART串口很像,但有一个明显差异:UART空闲时数据线为高,起始位是下降沿;PS/2同样空闲为高,起始位也是低电平,不过PS/2的时钟线由设备产生(设备到主机方向时),而且每位数据都在时钟的下降沿被采样。通信速率在10kHz到16.7kHz之间,一位数据大约持续60到100微秒。
校验位采用奇校验,也就是8个数据位加上校验位后,总共1的个数必须为奇数。比如发送字节0x00(全0),校验位就是1,这样总共1的个数是1(奇数)。若发送字节0xFF(全1),8个数据位已经有8个1了,校验位就是0,保持奇数个1。主机侧收到数据后如果校验失败,通常会忽略该帧或要求重发。
2.3 三层通讯流程:设备到主机、主机到设备、抑制通讯
PS/2协议通信方向分三种情况,理解这三者的切换是全协议的核心。
设备到主机(鼠标发数据给PC):鼠标检测到运动或按键状态变化后,先检查CLOCK是否为高,如果高则开始发送。发送时鼠标自身产生时钟信号,每次拉低时钟线时同步在DATA线上置位一位数据,主机在时钟下降沿读取DATA。数据发完后鼠标释放两根线,等待主机回复ACK(0xFA)。
主机到设备(PC发命令给鼠标):主机想给鼠标发指令时,先拉低CLOCK至少100微秒,抑制鼠标的任何数据。然后主机拉低DATA,再释放CLOCK,这个操作表示"我要开始发命令了"。随后主机在时钟线的高电平期间将数据按位放到DATA上,由主机产生时钟信号,设备在时钟上升沿读取数据。命令发送完成后,设备必须回复ACK(0xFA)。
抑制通讯:主机的时钟线电平任何时候被拉低,鼠标都不能发送数据。这个机制让主机可以随时暂停鼠标的数据流,避免冲突和丢包。
2.4 鼠标数据包:3字节还是4字节?
PS/2鼠标发送给主机的不是裸数据流,而是固定格式的数据包。最经典的是标准PS/2鼠标(ID=0x00)的3字节包,结构如下:
| 字节 | Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 |
|---|---|---|---|---|---|---|---|---|
| Byte0 | Y溢出 | X溢出 | Y符号位 | X符号位 | 保留1 | 保留0 | 中键 | 右键 |
| Byte1 | X位移(有符号数,-255 ~ +255) | |||||||
| Byte2 | Y位移(有符号数,-255 ~ +255) |
按键状态和位移量都在里面。注意位移量是有符号数,使用二进制补码表示,最大单次移动范围是-255到+255。而带滚轮的IntelliMouse扩展协议(ID=0x03)使用4字节包,多出来的Byte3就是滚轮位移,同样是有符号数,正值表示向上滚,负值表示向下滚。
要鼠标从3字节模式切换成4字节模式,主机需要发送一组特定的初始化序列(后面实操章节会细说)。很多DIY改装PS/2鼠标的初学者抓到的数据包混乱,大概率就是没搞懂当前鼠标处于什么模式。
3. 实操准备与工具选择:如何把实验室变成PS/2调试台
3.1 硬件工具清单
做PS/2协议调试,不需要特别昂贵的设备,我的建议是:
- 逻辑分析仪:最低8通道、采样率不低于20MHz的即可,价格几十到几百元不等。如果手头只有示波器也能凑合用,但要两只通道同时抓CLOCK和DATA,而且示波器触发设置比逻辑分析仪麻烦得多。逻辑分析仪的优势是可以实时解码,许多型号自带的软件支持PS/2协议解析,省去手动数位的痛苦。
- PS/2鼠标一个:尽量找带滚轮的旧鼠标,可以同时测试3字节和4字节模式。实在没有的话,在二手市场几块钱就能收到。
- 杜邦线或小夹子:需要把鼠标接口的信号引出来。如果不想破坏鼠标线,用测试钩子夹住引脚最方便。
- 5V供电:部分逻辑分析仪的信号输入容忍5V,但有些只支持3.3V。PS/2信号是5V电平,如果逻辑分析仪不支持5V输入,就必须加电平转换或者串联电阻分压,否则大概率烧掉分析仪的输入端口。
3.2 接线与引脚的确认
PS/2鼠标接口的引脚定义在网上能找到,但一定要以实物为准。6针mini-DIN的引脚排列如下(从焊接面看):
| 引脚 | 信号 | 引脚 | 信号 |
|---|---|---|---|
| 1 | DATA | 4 | VCC (+5V) |
| 2 | 未连接 | 5 | CLOCK |
| 3 | GND | 6 | 未连接 |
注意:未连接的两个脚在不同厂商的鼠标上可能有差异,不要盲目接东西。接法一旦错了,优先损坏的是鼠标端主控芯片或主机PS/2控制器,我调试时就烧过一只鼠标。正确操作是:先把GND接通,再量VCC是否5V,然后分别量CLOCK和DATA对地电压,正常空闲状态下这两根线都应该是接近5V的高电平。如果哪根线是0V,说明线序接错或鼠标已损坏。
3.3 软件工具配置
逻辑分析仪品牌不同软件界面也有差异,以我常用的Saleae Logic为例,抓取PS/2时序时建议这样设置:
- 采样率设置不低于10MHz,实际我习惯用20MHz,保证解析器能准确识别60多微秒一位的数据窗口。
- 添加两个数字通道,分别接CLOCK和DATA。
- 在分析仪软件中启用PS/2协议解析器,只需告诉软件哪个通道是CLOCK、哪个是DATA,以及解析方向是"设备到主机"还是"主机到设备"。
- 触发条件设置为CLOCK下降沿触发,开始捕获后移动鼠标即可。
如果软件不带PS/2解析器,也没关系,把原始波形导出来自己手动一位一位数,过程虽然痛苦,但对理解协议极其有帮助,我强烈建议初学者至少手动解码一帧数据。
4. 动手实战一:逻辑分析仪抓取鼠标数据包
4.1 捕获标准3字节包
硬件接好、软件配好后,用逻辑分析仪抓一次鼠标移动。正常情况会看到类似这样的波形特征:CLOCK输出一串周期性的下降沿,DATA在时钟下降沿附近保持稳定电平。协议解析器会直接给出数据内容,但这里有必要手动核对一次。
我抓到的第一帧数据是鼠标右移一小段距离,解码结果如下:
起始位: 0 数据位: 10000001 (即0x81) 奇偶校验: 1 停止位: 1后续两帧数据分别是0x00和0x03。对照前面的数据包格式,Byte0=0x81的二进制是1000 0001,Bit0=1表示左键被按下,Bit7=1说明X位移为负值(鼠标其实是向左移动)。当时我以为解析错了,后来检查鼠标的安装方向才意识到移动方向判定没毛病。很多时候解析结果和主观感受不符,不是协议问题,而是坐标系统的方向定义问题。
4.2 手动数位验证与常见误区
手动数位有几个需要注意的坑:
- 空闲电平是高,所以一帧起始位的那一下拉低非常明显。但如果鼠标移动量很小,数据帧之间可能间隔很长时间,触发抓取时容易只抓到半个帧,建议把触发放到CLOCK下降沿,然后采集窗口设大一点。
- 每一位都要在CLOCK下降沿处读取DATA电平,上升沿处虽然DATA也会变化,但那已经是下一位数据了。初学者最容易犯的错就是在上升沿采样,结果整个字节反序。
- 奇偶校验位的极性:有的文章说"校验位让1的个数为偶数",这属于把奇校验概念记反了。PS/2协议严格要求是奇校验,即数据位+校验位的1的总数是奇数。如果校验位判断错误,后续数据解析全乱。
4.3 滚轮鼠标的4字节数据包捕获
带滚轮的鼠标默认不一定工作在4字节模式。要让鼠标从ID=0x00切换到ID=0x03模式,需要主机发送一组"魔法序列":向鼠标发送0xF3(设置采样率)三次,采样率参数依次是200、100、80。这个顺序不能乱,因为鼠标固件通过识别这个特定的采样率序列来判断主机是否支持滚轮扩展协议。
执行完初始化后,再抓取数据包就是4字节了。滚轮数据在Byte3,向上滚动一格为+1,向下滚动一格为-1,快速滚动时数值更大。注意Byte3和Byte1、Byte2一样使用有符号数,不是"255表示向下滚一格的常规编码",抓数据时不要被数值吓到。
5. 动手实战二:用单片机模拟主机读取PS/2鼠标
5.1 方案设计与硬件连接
逻辑分析仪只能看数据,真要理解协议还得自己当一次"主机"。我的做法是用STM32F103模拟PS/2主机,读取鼠标数据并通过串口打印到电脑上。
硬件连接很简单:STM32的PA0接DATA、PA1接CLOCK,两个引脚都配置为开漏输出模式,并外接两个4.7k欧姆上拉电阻到5V。这里要注意,STM32的GPIO通常不是5V容忍的(具体要看数据手册),所以我的做法是用一个电平转换模块,把5V侧的PS/2信号转换到3.3V再接MCU。没有电平转换模块的话,也可以用两个10k欧姆电阻分压,实测稳定,但上拉电阻的取值会影响信号沿的陡峭程度,如果波形太缓,MCU采样会出错。
VCC直接接5V电源给鼠标供电,GND与MCU共地。整个系统通电后,鼠标内部的LED传感器会启动,自检完成后按PS/2协议等待主机发命令。
5.2 初始化流程:复位、自检与设备识别
主机代码的第一步是复位鼠标。复位命令是0xFF,主机拉低时钟线至少100微秒,然后按主机到设备的时序发送这个字节。鼠标收到复位命令后,会先回复ACK(0xFA),然后进行自检,自检完成后发送结果:0xAA表示自检通过,0xFC表示自检失败。复位完成后的2秒内,鼠标还会发送自身的设备ID:标准鼠标发0x00,带滚轮的IntelliMouse发0x03,五键鼠标发0x04。
要注意的是:复位结束后鼠标会恢复默认配置,也就是采样率100Hz、分辨率4 counts/mm、禁用数据上报(Disable模式)。所以主板在开机时对鼠标做的那一堆初始化不是没意义的,每一步都有对应目的。
5.3 启用上报与读取数据
复位和设备ID识别之后,主机需要发送0xF4(Enable Data Reporting),鼠标才会开始上报运动数据。如果不发这条命令,鼠标会一直沉默,这也是很多初学者移植代码后一帧数据都读不到的原因。
在启用上报后,主程序要做的事情就是:检测CLOCK的下降沿中断,在下降沿中读取DATA电平(读取时机要稳定,可以在下降沿后用延时读取,或者更稳妥的做法是在CLOCK低电平中间读取)。完整一帧11位读完后,按数据包格式组装出Byte0、Byte1、Byte2,再通过串口打印。
这是我调通过的一个简化版读取代码的核心逻辑:
// PS/2鼠标数据接收(STM32 + 外部中断) volatile uint8_t ps2_bit_count = 0; volatile uint16_t ps2_frame = 0; volatile uint8_t ps2_frame_ready = 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { // PA0 -> DATA,初始时CLOCK接PA1 EXTI_ClearITPendingBit(EXTI_Line0); } } // 实际中把CLOCK接到EXTI线,下降沿触发 void EXTI1_IRQHandler(void) { EXTI_ClearITPendingBit(EXTI_Line1); uint8_t data_level = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); if (ps2_bit_count == 0) { // 起始位应为0,忽略或做校验 } else if (ps2_bit_count >= 1 && ps2_bit_count <= 8) { // 数据位LSB first if (data_level) { ps2_frame |= (1 << (ps2_bit_count - 1)); } } ps2_bit_count++; if (ps2_bit_count == 11) { ps2_bit_count = 0; ps2_frame_ready = 1; } }这段代码的核心价值在于演示了以CLOCK下降沿为基准的采样思路。实际工程中还要加超时保护、毛刺滤波,防止接触不良产生的抖动把状态机搞乱。
5.4 发送命令的时序陷阱
接收数据只是"读"的一半,完整的主机实现还要发送命令。发送命令时,主机要抢占总线,流程是:
- 拉低CLOCK,持续至少100微秒。
- 拉低DATA,然后释放CLOCK。这个"先时钟低、再数据低、再时钟高"的顺序就是传说中的"请求发送"握手信号。
- 主机在CLOCK高电平期间把数据放到DATA线上。拉高时钟的瞬间,鼠标就知道主机要发送数据了,它会在时钟下降沿采样数据,但此时的时钟由主机产生。
- 发送完停止位后,释放DATA,鼠标接管总线,回复ACK(0xFA)。
发送过程最麻烦的是时钟由谁产生的切换:前一帧还是鼠标产生时钟,后一帧就变成主机产生时钟。如果代码不严格按顺序切换GPIO方向,总线就会发生争用,轻则数据错误,重则损坏硬件。
我的经验是发送命令前先把CLOCK和DATA都初始化为输出模式,拉低CLOCK,然后按位操作数据线。命令发送完成后立刻把两根线切回输入模式,等待鼠标的ACK。这个"输出发命令、输入收数据"的方向切换,必须在帧边界处干净利落,否则总线状态马上乱掉。
5.5 设置采样率与分辨率
除了复位和启用上报,有两个常用命令值得自己动手测一下:
- 0xF3 + 采样率参数:设置鼠标采样率,参数为10、20、40、60、80、100、200,单位Hz。数值越大,鼠标越灵敏,但实际效果受鼠标内部传感器能力限制,超过上限时鼠标会忽略。
- 0xE8 + 分辨率参数:分辨率单位为counts/mm,参数0对应1 count/mm,参数1对应2 counts/mm,参数2对应4 counts/mm,参数3对应8 counts/mm。调高分辨率后,同样的物理移动会产生更大的位移数值,应用层做视角控制时通常通过这个命令来调节灵敏度,而不是改数据包里的数值。
这些命令都是字节序列,鼠标执行完每条命令后都会回ACK。命令没有"立即生效"机制,最终效果要等下次运动上报时才体现。
6. 实战中的焊接与接线经验
很多人觉得PS/2接线很简单,实际翻车率极高。三个高频故障点:
- 热插拔烧接口:PS/2接口设计不支持热插拔(虽然有些主板做了保护),带电插拔容易在瞬间产生电流冲击,烧毁主板上的保险或鼠标主控。我在调试时一直遵守"先断电,再插拔,最后上电"的顺序,换鼠标时宁愿多花30秒,也别赌一次运气。
- 线序接反而导致设备不识别:DATA和CLOCK一旦接反,最大的特征是鼠标完全不报数据,但用万用表量电压又都正常。如果逻辑分析仪抓不到任何时钟脉冲,先怀疑线序,不要急着怀疑鼠标坏了。
- 上拉电阻取值不当:PS/2规范建议上拉电阻在1k到10k之间。取值太大会导致信号上升沿变缓,高速模式下位采样出错;取值太小又会增加功耗和反射。我之前用1k欧姆电阻调试时,近距离可以工作,换了一根30厘米的杜邦线后信号反射严重,数据全错。换回4.7k欧姆并缩短杜邦线长度后恢复正常。
7. 常见问题与排查技巧实录
7.1 为什么逻辑分析仪抓不到任何信号?
按优先级排查:
- 查供电:VCC与GND之间的电压是否为5V?有些USB转PS/2线提供的电流只有几十毫安,带不动鼠标传感器,表现为鼠标灯不亮或闪一下就灭。用万用表直接量鼠标端VCC电压,空载和接负载时电压会跌很多,这种情况必须换独立5V电源。
- 查空闲电平:CLOCK和DATA空闲时都应为高,如果有一根线被拉死到0V,说明鼠标内部短路或接线错误,断电后测两线对GND的电阻,正常都有几十千欧以上。
- 确认鼠标本身是PS/2原生接口:市面上很多"PS/2鼠标"其实是USB鼠标加了转接头,这类鼠标在纯PS/2主机上根本无法工作。识别方法很简单:看鼠标底部或标签,如果标注了"PS/2 ONLY",那就是原生;如果写的是"USB+PS/2兼容",接转接头只适用于少数支持双模式的主板。
7.2 数据一直乱码或者错位怎么办?
乱码的本质是位采样点不对或者位序搞反。优先检查:
- 采样点是否在时钟下降沿?有的逻辑分析仪解析器可能默认上升沿采样,手动调整后即可解码正确。
- 数据位序是LSB first,不是MSB first。很多第一次接触的人拿着0x81解码成0x81,其实LSB first先收到的0x01代表左键,不是0x80代表的X符号位。
- 时钟线上有毛刺,可能是接触不良或线太长。把杜邦线换成屏蔽线或尽量短接,毛刺会明显减少。
7.3 鼠标偶尔丢包或者卡顿
PS/2协议的实时性很好,丢包多半出在主机侧。如果用单片机读取,中断服务函数里不能做耗时操作,比如在中断里调用printf、延时函数,都会导致下一个时钟下降沿来时还没处理完,直接丢位。正确做法是中断里只存位数据,帧拼好了置标志位,主循环里再处理。
另外,检查CLOCK和DATA是否与其他信号串扰。我之前把PS/2线跟一根25V电源线扎在一起,鼠标移动时波形成片状跳动,分离后立刻恢复正常。低电压信号线跟电源线保持距离是基本常识,但实践中最容易被忽略。
7.4 鼠标能识别但移动无反应
能识别说明复位和设备ID都成功了,但无反应可能是:
- 没发0xF4启用上报:这是最常犯的错。复位恢复默认状态是禁用上报的,不发0xF4,鼠标不会主动发数据。
- 数据模式设置成了3字节但使用了4字节结构来解析:如果移位处理错误,会看到第一字节正常、第二字节有时对有时完全不对。这种情况先确认鼠标ID:3字节包对应ID=0x00,4字节带滚轮对应ID=0x03。
- 采样率设置太低:0xF3设成10Hz时,一秒钟才上报10次位置,快速移动时看起来就像没反应。初始化时设成100Hz或200Hz,体感就正常了。
8. 从PS/2到现代接口的延伸思考
调试完PS/2鼠标后,我最大的感受是:老协议其实是最好的底层教科书。
PS/2协议把双向通信、时钟同步、校验机制、状态机、总线抢占这些概念全部浓缩在一个11位的小帧里。相比之下,USB协议复杂的描述符、端点、传输类型常常让初学者一头雾水。但有了PS/2的基础,你在看USB HID协议时会有一种豁然开朗的感觉——原来鼠标上报的按键、位移、滚轮数据,本质上就是某种格式的数据包,只是传输方式从同步串行变成了差分串行、从主机轮询变成了设备中断传输。
如果你接下来要做USB鼠标实验,强烈建议先用PS/2把"数据包格式"这件事彻底吃透,再去看USB HID报文描述符,你会发现很多概念是相通的。对于涉及低速传感器、按键扫描、编码器读取等嵌入式场景,PS/2的同步时序思想也可以直接迁移——遇到一个新的同步串行协议,比如SPI或I2C,一句话概括就是:找到时钟,跟着采样,按位拼帧,按帧解析。
我个人在实际项目中的体会是:解决PS/2问题最大的门槛不是协议本身有多难,而是"想象不到出错在哪里"。花半小时用逻辑分析仪亲眼看到一次波形、手动数出11个位,比看十篇文档都管用。调试过程虽然折腾,但这种亲手把一堆0和1还原成鼠标动作的成就感,也是做上层应用开发时很难体会到的。