本文介绍如何使用 Arduino 打造一个设备,能够将你的USB键盘转化为蓝牙键盘。
键盘可以算作PC上最古老的设备了,他的出现使得人类可以用非常简单的方法与电脑进行交互。同样的,由于各种历史原因,键盘也是PC上最复杂,兼容性问题最多的设备之一(类似的还有硬盘,不过从IDE到SATA的进化过程中,标准明确,兼容性问题少多了)。
网上流传着一篇DIY USB键盘转换为无线的文章,非常不幸的是,那篇文章是错误的,很明显的错误是作者认为键盘是单向传输,而实际上传输是双向的。比如,USB每次通讯都需要HOST和SLAVE的参与,即便是PS2键盘的通讯也同样如此。此外,大小写键之类切换是主机端进行控制的。
硬件部分Arduino UNO , USB Host Shield 和 HID 蓝牙芯片。强调一下这里使用的是 HID 蓝牙芯片,并非普通的蓝牙串口透传芯片【特别注意是“蓝牙键盘芯片”也不是“蓝牙条码模块”】。关于这个模块可以参考我在【参考1】中的实验。
硬件连接很简单,USB HOST Shield插在 Arduino上,然后VCC/GND/TX/RX将Arduino 和 HID蓝牙模块连接在一起。
原理:首先,为了通用性和编程简单,我们用USB HOST发送命令把键盘切换到 Boot Protocol 模式下。这样即使不同的键盘,每次发出来的数据也都是统一的格式。然后,我们直接读取缓冲数据就可以解析出按键信息了。最后,将取下来的按键信息(Scan Code)按照HID蓝牙模块的格式要求通过串口送到模块上,主机端就收到了。
上述连接就可以正常工作了,但是为了美观和提高可靠性,我找到之前买的一个面包板Shield。
插好之后就是这样
具体代码:
/* MAX3421E USB Host controller LCD/keyboard demonstration */ //#include <Spi.h> #include "Max3421e.h" #include "Usb.h" /* keyboard data taken from configuration descriptor */ #define KBD_ADDR 1 #define KBD_EP 1 #define KBD_IF 0 #define EP_MAXPKTSIZE 8 #define EP_POLL 0x0a /**/ //****************************************************************************** // macros to identify special charaters(other than Digits and Alphabets) //****************************************************************************** #define BANG (0x1E) #define AT (0x1F) #define POUND (0x20) #define DOLLAR (0x21) #define PERCENT (0x22) #define CAP (0x23) #define AND (0x24) #define STAR (0x25) #define OPENBKT (0x26) #define CLOSEBKT (0x27) #define RETURN (0x28) #define ESCAPE (0x29) #define BACKSPACE (0x2A) #define TAB (0x2B) #define SPACE (0x2C) #define HYPHEN (0x2D) #define EQUAL (0x2E) #define SQBKTOPEN (0x2F) #define SQBKTCLOSE (0x30) #define BACKSLASH (0x31) #define SEMICOLON (0x33) #define INVCOMMA (0x34) #define TILDE (0x35) #define COMMA (0x36) #define PERIOD (0x37) #define FRONTSLASH (0x38) #define DELETE (0x4c) /**/ /* Modifier masks. One for both modifiers */ #define SHIFT 0x22 #define CTRL 0x11 #define ALT 0x44 #define GUI 0x88 /**/ /* "Sticky keys */ #define CAPSLOCK (0x39) #define NUMLOCK (0x53) #define SCROLLLOCK (0x47) /* Sticky keys output report bitmasks */ #define bmNUMLOCK 0x01 #define bmCAPSLOCK 0x02 #define bmSCROLLLOCK 0x04 /**/ EP_RECORD ep_record[ 2 ]; //endpoint record structure for the keyboard char buf[ 8 ] = { 0 }; //keyboard buffer char old_buf[ 8 ] = { 0 }; //last poll /* Sticky key state */ bool numLock = false; bool capsLock = false; bool scrollLock = false; bool line = false; void setup(); void loop(); MAX3421E Max; USB Usb; void setup() { Serial.begin( 9600 ); Serial.println("Start"); Max.powerOn(); delay( 200 ); } void loop() { Max.Task(); Usb.Task(); if( Usb.getUsbTaskState() == USB_STATE_CONFIGURING ) { //wait for addressing state kbd_init(); Usb.setUsbTaskState( USB_STATE_RUNNING ); } if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) { //poll the keyboard kbd_poll(); } } /* Initialize keyboard */ void kbd_init( void ) { byte rcode = 0; //return code /**/ /* Initialize data structures */ ep_record[ 0 ] = *( Usb.getDevTableEntry( 0,0 )); //copy endpoint 0 parameters ep_record[ 1 ].MaxPktSize = EP_MAXPKTSIZE; ep_record[ 1 ].Interval = EP_POLL; ep_record[ 1 ].sndToggle = bmSNDTOG0; ep_record[ 1 ].rcvToggle = bmRCVTOG0; Usb.setDevTableEntry( 1, ep_record ); //plug kbd.endpoint parameters to devtable /* Configure device */ rcode = Usb.setConf( KBD_ADDR, 0, 1 ); if( rcode ) { Serial.print("Error attempting to configure keyboard. Return code :"); Serial.println( rcode, HEX ); while(1); //stop } /* Set boot protocol */ rcode = Usb.setProto( KBD_ADDR, 0, 0, 0 ); if( rcode ) { Serial.print("Error attempting to configure boot protocol. Return code :"); Serial.println( rcode, HEX ); while( 1 ); //stop } delay(2000); Serial.println("Keyboard initialized"); } /* Poll keyboard and print result */ /* buffer starts at position 2, 0 is modifier key state and 1 is irrelevant */ void kbd_poll( void ) { char i; boolean samemark=true; static char leds = 0; byte rcode = 0; //return code /* poll keyboard */ rcode = Usb.inTransfer( KBD_ADDR, KBD_EP, 8, buf ); if( rcode != 0 ) { return; }//if ( rcode.. for( i = 2; i < 8; i++ ) { if( buf[ i ] == 0 ) { //end of non-empty space break; } if( buf_compare( buf[ i ] ) == false ) { //if new key switch( buf[ i ] ) { case CAPSLOCK: capsLock =! capsLock; leds = ( capsLock ) ? leds |= bmCAPSLOCK : leds &= ~bmCAPSLOCK; // set or clear bit 1 of LED report byte break; case NUMLOCK: numLock =! numLock; leds = ( numLock ) ? leds |= bmNUMLOCK : leds &= ~bmNUMLOCK; // set or clear bit 0 of LED report byte break; case SCROLLLOCK: scrollLock =! scrollLock; leds = ( scrollLock ) ? leds |= bmSCROLLLOCK : leds &= ~bmSCROLLLOCK; // set or clear bit 2 of LED report byte Serial.write(0x0c); //BYTE1 Serial.write(0x00); //BYTE2 Serial.write(0xA1); //BYTE3 Serial.write(0x01); //BYTE4 Serial.write(00); //BYTE5 Serial.write(0x00); //BYTE6 Serial.write(0x1e); //BYTE7 Serial.write(0); //BYTE8 Serial.write(0); //BYTE9 Serial.write(0); //BYTE10 Serial.write(0); //BYTE11 Serial.write(0); //BYTE12 delay(500); Serial.write(0x0c); //BYTE1 Serial.write(0x00); //BYTE2 Serial.write(0xA1); //BYTE3 Serial.write(0x00); //BYTE4 Serial.write(0); //BYTE5 Serial.write(0x00); //BYTE6 Serial.write(0); //BYTE7 Serial.write(0); //BYTE8 Serial.write(0); //BYTE9 Serial.write(0); //BYTE10 Serial.write(0); //BYTE11 Serial.write(0); //BYTE12 break; case DELETE: line = false; break; case RETURN: line =! line; break; //default: //Serial.print(HIDtoA( buf[ i ], buf[ 0 ] )); // break; }//switch( buf[ i ... rcode = Usb.setReport( KBD_ADDR, 0, 1, KBD_IF, 0x02, 0, &leds ); if( rcode ) { Serial.print("Set report error: "); Serial.println( rcode, HEX ); }//if( rcode ... }//if( buf_compare( buf[ i ] ) == false ... }//for( i = 2... i=0; while (i<8) { if (old_buf[i]!=buf[i]) { i=12; } i++; } if (i==13) { // for (i=0;i<8;i++) { // Serial.print(buf[ i ],HEX); // Serial.print(']'); // } // Serial.println(' '); Serial.write(0x0c); //BYTE1 Serial.write(0x00); //BYTE2 Serial.write(0xA1); //BYTE3 Serial.write(0x01); //BYTE4 //Labz_Debug Serial.write(buf[1]); //BYTE5 Serial.write(buf[0]); //BYTE5 //Labz_Debug Serial.write(0x00); //BYTE6 Serial.write(buf[2]); //BYTE7 Serial.write(buf[3]); //BYTE8 Serial.write(buf[4]); //BYTE9 Serial.write(buf[5]); //BYTE10 Serial.write(buf[6]); //BYTE11 Serial.write(buf[7]); //BYTE12 } //Labz_Debug for( i = 2; i < 8; i++ ) { //copy new buffer to old for( i = 0; i < 8; i++ ) { //copy new buffer to old //Labz_Debug old_buf[ i ] = buf[ i ]; } } /* compare byte against bytes in old buffer */ bool buf_compare( byte data ) { char i; for( i = 2; i < 8; i++ ) { if( old_buf[ i ] == data ) { return( true ); } } return( false ); }
我在处理SCROLLLOCK 键的地方插入了一个测试代码,理论上按下这个键的时候,主机还会收到 1 这个字符,这样是为了测试工作是否正常。
我在 x86 台式机上实测过,工作正常;小米4手机上实测过,工作正常; iPad 上是测过,工作也正常。
在iPad上工作的视频在下面:
完整代码下载
特别注意:
1. 因为我们使用的是最简单的Boot Protocol,所以如果你的键盘上有音量键之类的有可能失效;
2. 我不确定是否所有的键盘都会支持 Boot Protocol ,从之前玩USB鼠标的经验来看,确实有可能;
3. 供电部分没有经过优化,不知道电力消耗如何,不确定一个充电宝能够工作的时间;
最后讲一个小故事:有一次我去实验室,发现他们在折腾键盘。那是一款带着音量控制功能的键盘。系统测试的时候发现,按一下键盘音量键之后,屏幕上显示的音量会跳2格。从原理上说,按下那个键之后,键盘发出特定的Scan Code,系统中还有个专门响应这个Scan Code的程序然后在屏幕上绘制音量指示方块。蛮有意思的一件事情是:很多人认为大公司有操控供应商的能力,供应商在大厂面前会唯唯诺诺,这也是高层会有的想法,问题是底层人员未必吃这一套。每次想起这个事情,我都要想起敏感字关于矛盾的辩证法的论证。这个事情就是双方的下层在不停的扯,更准确的说,是键盘厂商,软件开发商和我们在一起纠缠,键盘厂商说同样的键盘在其他人家用起来没问题,软件开发商说我的软件在之前的机型上一直用,我们的人说,少扯淡,赶紧解决,前后一个多月都没有搞定…….那时候,组里刚买了一个usb逻辑分析仪,我用着感觉很好玩。于是,我就用逻辑分析仪测试了一下键盘,测试的结果是,键盘发出来的 Scan Code没有问题,每次按键都是一个Press一个Release,所以真相肯定是写上位机程序的软件厂商搞错了什么。截图附带着数据包一起丢给三方。这是最底层的传输,如果依然嘴硬,那只能落下笑柄而已。然后很快软件厂商就服软自己去修改了。只是说说我经历的事情,如果非要说出一些道理的话这个故事是为了说明:USB逻辑分析仪很有用……
就是这样.
=========================2017年5月11日更新=========================
有朋友说 Win ,Shift,Alt 都不工作,今天正好有空研究了一下,是我的代码有问题。解析出来的USB 键盘按键信息中 Buf[0] 是这些Key 的标志位,我没有正确Pass给模块。因此,修正下面2处代码,一个是发送,一个是每次保存当前的按键状态的位置:
Serial.write(0x0c); //BYTE1 Serial.write(0x00); //BYTE2 Serial.write(0xA1); //BYTE3 Serial.write(0x01); //BYTE4 Serial.write(buf[0]); //BYTE5 Serial.write(0x00); //BYTE6 Serial.write(buf[2]); //BYTE7 Serial.write(buf[3]); //BYTE8 Serial.write(buf[4]); //BYTE9 Serial.write(buf[5]); //BYTE10 Serial.write(buf[6]); //BYTE11 Serial.write(buf[7]); //BYTE12 } for( i = 0; i < 8; i++ ) { //copy new buffer to old old_buf[ i ] = buf[ i ]; }
修改后可以正常工作的代码:
参考:
1. http://www.lab-z.com/btkeyboard/ 蓝牙键盘模块的实验