制作缘由
公司内网只有PS/2接口,希望可以使用无线鼠标(貌似没有PS/2接口的)。而那种USB转PS/2的转接头只是简单的连线,需要键盘或鼠标本身支持PS/2模式才可以正常工作,现代的USB鼠标接收器显然没有考虑这一点。无意中发现有人用Arduino制作过USB键盘转PS/2的装置,那么鼠标一定也可以。这个装置,从原理上来说,就是一个转发器。对于USB鼠标(非蓝牙无线鼠标对主机来说仍是USB鼠标,和有线的没啥区别),他是一个主机,接收来自鼠标的数据;对于真正的主机,他是一个PS/2鼠标,负责向其发送转换过的数据。
PS/2部分
物理接口
如下图,实际有用的只有4根线,1 - Data:用于传输数据(双向);5 - Clock:用于向主机发送脉冲,控制读写数据;3 - Ground:接地;4 - Vcc (+5V) :电源。其中1和2可以接在Arduino任意的数字引脚上(但要避开USB Host Sheild占用的那些引脚,我实际使用的是2号、3号引脚),而3和4分别接在Arduino的5V和GND引脚上,这样不用额外供电,连到主机的PS/2接口就能正常工作。
数据传输(1位)
设备 > 主机 :由设备控制Clock以产生脉冲,首先,设置Data为要发送的位(高/低电平表示1/0),同时保持Clock为高电平一段时间,然后拉低Clock为低电平,产生一个下降沿,通知主机读取(锁存)Data,继续保持Clock为低电平一段时间后释放Clock(恢复为高电平,为传输下1位数据做准备)。。
主机 > 设备:仍然由设备控制Clock脉冲。当主机拉低Data或Clock时表示想要向设备发送(命令)数据,此时设备应该停止发送数据,优先读取来自主机的数据,待主机释放Clock(变为高电平)时,表示设备可以开始读入数据。首先,设备让Clock保持高电平一段时间,然后拉低Clock,产生一个下降沿,通知主机设置Data,继续保持Clock为低电平一段时间后,释放Clock为高电平(此时设备可以锁存当前Data,并且准备读入下1位数据)
数据传输(1字节)
每传输1字节数据,需要额外附带起始位(0),奇校验位,停止位(1),共11位
代码实现
最初在Arduino官方网站找到了一个叫做ps2dev的示例代码,里面的PS2dev类看上去实现了单个字节的读写,但是版本过于老旧。后来发现这个ps2dev已经是Arduino的一个库了,在库管理器中就可以搜索安装,但还是不够新,存在一些问题。他的最新版本在这里 https://github.com/Harvie/ps2dev,其中简单模拟鼠标的例子在某些时候会死循环出不来,我的修正已经被作者合并进去了。进一步地,为了提高通讯速度,我修改了时钟频率以及发送单个字节的间隔,目前工作正常。:)
鼠标协议
总的来说,主机向鼠标发送各种命令,以设置或询问鼠标的状态或参数;而鼠标呢,除了答复来自主机的命令,就是向主机发送位移数据包(处于Stream模式并且“数据报告”使能状态下,可以主动上报);更具体的可以参考网上的文档。(有中文版哦,搜索“PS2 鼠标 键盘技术参考”)下面列出我抓到的命令序列:
19:44:22.522 -> process_cmd FF 重置命令,进入Reset模式,重置各种状态,回复自检完成、设备ID,进入Steam模式
19:44:22.556 -> process_cmd F2 询问设备ID,首次询问应该回复00(标准鼠标),但我一律回复的03(滚轮鼠标)也没事哦
19:44:22.556 -> process_cmd E8 设置解析度
19:44:22.556 -> set resolution 0 读取解析度,1count/mm
19:44:22.556 -> process_cmd E6 设置缩放比例1:1
19:44:22.556 -> process_cmd E6 设置缩放比例1:1
19:44:22.556 -> process_cmd E6 设置缩放比例1:1,为啥连续发了3次?
19:44:22.556 -> process_cmd E9 询问鼠标状态,此时应该回复3字节的状态包
19:44:22.556 -> process_cmd E8 设置解析度
19:44:22.589 -> set resolution 3 读取解析度,8count/mm
19:44:22.589 -> process_cmd F3 设置采样率
19:44:22.589 -> set sample rate 200 读取采样率 200/sec
19:44:22.589 -> process_cmd F3 设置采样率
19:44:22.589 -> set sample rate 100 读取采样率 100/sec
19:44:22.589 -> process_cmd F3 设置采样率
19:44:22.589 -> set sample rate 80 读取采样率 80/sec,注意,当主机按此顺序设置采样率,表示他支持03鼠标(滚轮鼠标)
19:44:22.589 -> process_cmd F2 询问设备ID,此时应该回复03,如果鼠标支持的话
19:44:27.052 -> process_cmd FF 重置命令
19:44:27.052 -> process_cmd F2 询问设备ID
19:44:27.052 -> process_cmd E8 设置解析度
19:44:27.052 -> set resolution 0 读取解析度,1count/mm
19:44:27.052 -> process_cmd E6 设置缩放比例1:1
19:44:27.052 -> process_cmd E6 设置缩放比例1:1
19:44:27.052 -> process_cmd E6 设置缩放比例1:1,同样的,为啥连续发了3次?我猜可能有含义的。
19:44:27.086 -> process_cmd E9 询问鼠标状态,此时应该回复3字节的状态包
19:44:27.086 -> process_cmd E8 设置解析度
19:44:27.086 -> set resolution 3 读取解析度,8count/mm
19:44:27.086 -> process_cmd F3 设置采样率
19:44:27.086 -> set sample rate 200 读取采样率 200/sec
19:44:27.086 -> process_cmd F3 设置采样率
19:44:27.086 -> set sample rate 100 读取采样率 100/sec
19:44:27.086 -> process_cmd F3 设置采样率
19:44:27.086 -> set sample rate 80 读取采样率 80/sec,为啥又来一遍?囧
19:44:27.086 -> process_cmd F2 询问设备ID,此时应该回复03,如果鼠标支持的话
19:44:27.120 -> process_cmd F3 设置采样率
19:44:27.120 -> set sample rate 200 读取采样率 200/sec
19:44:27.120 -> process_cmd F3 设置采样率
19:44:27.120 -> set sample rate 200 读取采样率 200/sec
19:44:27.120 -> process_cmd F3 设置采样率
19:44:27.120 -> set sample rate 80 读取采样率 80/sec,注意,当主机按此顺序设置采样率,表示他支持04鼠标(5键滚轮)
19:44:27.120 -> process_cmd F2 询问设备ID,此时应该回复04,如果鼠标支持的话,但是我没有。:)
19:44:27.120 -> process_cmd F3 设置采样率
19:44:27.120 -> set sample rate 100 读取采样率 100/sec
19:44:27.120 -> process_cmd E8 设置解析度
19:44:27.154 -> set resolution 3 读取解析度,8count/mm
19:44:27.154 -> process_cmd F4 使能“数据报告”,下面鼠标就可以开始主动上报“位移数据包”了。。
USB部分
使用USB Host Shield扩展板(采用MAX3421E芯片,支持USB2.0),加上USB Host Shield 2.0(它也是Arduino的一个库),最新版本在这里 https://github.com/felis/USB_Host_Shield_2.0,只要继承HIDUniversal类并且覆盖虚函数ParseHIDData就能得到HID设备的数据(数据格式可用USBBlyzer分析)。实际使用中发现,如果本次数据和上次的完全一致,则不会调用ParseHIDData。(比如同方向滚动滚轮,后面的都被忽略了)只需略微修改hiduniversal.cpp,已提合并请求,作者暂时没理我。^_^ (我的修改见 https://github.com/felis/USB_Host_Shield_2.0/pull/522)
USB是一种主从结构,只有当主机向设备发送IN令牌包(见上述库中 USB::InTransfer)时,设备才可以向主机发送数据。在鼠标设备内部,分为USB芯片(用于与USB主机通讯)和MCU(用于执行鼠标固件程序)。当检测到鼠标状态变化(比如按键按下或发生移动),MCU将状态数据写入USB芯片的缓冲区,待收到IN令牌包时由USB芯片将数据发给主机。如果主机请求数据速度过慢会怎样?我猜数据会丢失?从目前能找到的固件程序代码看,都是直接向芯片缓冲区写数据的,并没有额外的处理。或许当缓冲区满了之后,合并后续数据会好一点?
鼠标回报率
也就是上面提到的采样率。
PS/2理论上支持:10、20、40、60、80、100、200。我的主机最终设置的值为100,目前USB部分耗时2ms,PS/2部分耗时6-7ms,已经可以满足。曾经试过把时钟频率调的更快、发送单个字节的间隔改的更短,没成功。:(
USB1.1支持:125
USB2.0支持:250、500、1000
考虑到USB和PS/2收发速率可能不匹配,所以实现了一个循环队列用于存放来自USB鼠标的数据,并尽可能地将新的数据与队尾元素合并。但是,实际测试中,队列长度从未超过1并且合并函数从未被调用,即便在loop()中尝试读取多次USB数据再向PS/2发送也是如此,Why?可能是因为我的USB鼠标只有125的回报率吧。
呼吸灯
为了了解转换器的工作状态(以及更炫酷一点),额外加了一个全彩LED灯在盒子顶部。繁忙时,以红色快速闪烁;而空闲时,则以绿色缓慢呼吸;
最终成品
完整源代码在这里 https://github.com/liumazi/MzMouse
参考资料
https://www.zhihu.com/question/46855816/answer/619886526
https://www.arduino.cn/forum.php?mod=viewthread&tid=22851
http://www.burtonsys.com/ps2_chapweske.htm
http://usb.baiheee.com/usb_projects/easy_usb_51_programer_plus/usb_hid_mouse.html
https://blog.csdn.net/suipingsp/article/details/30238891