刚开始玩Arduino 的时候,我尝试试验过一个 PS2键盘记录器:将PS2线剥开,接入Arduino Uno。通过分析PS2协议,键盘上按键随时会被记录在Arduino的EEPROM中【参考1】。 完成之后,我就在思考是否有能记录USB键盘的方法。Arduino 本身使用的主控芯片只有16Mhz,作为USBLow Speed设备发送已经力不从心,更不要说直接对USB 信号采样。后来偶然的机会接触到了 Arduino USB Host Shield,发现这个Shield 作为 USBHost具有解析USB协议的能力,可以控制USB设备,解析USBKeyboard/Mouse 更是不在话下。 抓到了USB 键盘的数据,下面的问题就是如何发送出去。最传统的方法是直接用 ArduinoLeonardo 这样带有USB控制器的板子,但是经过考察,这个型号的SPI部分引脚与Uno差别很大,导致无法直接使用(后面我还会介绍为什么有差别,以及如何解决)。最终找到可以将Uno模拟为USBKeyboard的方法:将Uno上面的Usb转串口芯片代码替换为特殊的Firmware【参考2】,从PC端看去是一个USB Keyboard设备。这个方法的优点是百分百兼容USB Host Shield,缺点是无法直接使用IDE下载,必须用USBISP 之类的设备刷写Uno上的328P 芯片。具体操作可以在【参考3】看到。 最终方案如下: 第一步,使用 USB Host Shield将键盘切换到 Boot Protocol 模式,这样保证所有的USB键盘按照统一的格式输出按键信息; 第二步,Arduino 解析USB键盘的按键信息,解析之后直接存储到内存中; 第三步,接收到特定的组合键后,将USB 键盘的按键信息从Atmel16u2发出去 整个过程对于PC也是透明的。 下面是USB HID Keyboard Boot Protocol 使用的格式,上面提到的解析过程和最后的再次发送的过程都会遵循该格式。 位置
Modifier keys:
Bit 0 – 左 CTRL
Bit 1 - 左 SHIFT
Bit 2 - 左 ALT
Bit 3 - 左 GUI
Bit 4 – 右 CTRL
Bit 5 - 右 SHIFT
Bit 6 - 右 ALT
Bit 7 - 右 GUI
HID协议定义的键值。 这里有6个bytes,可以同时容纳6个按键 例如: 直接按下 左 Ctrl ,Usb Host Shield 将会解析出 01 00 0000 00 00 00 00,抬起后还会解析出0000 00 00 00 00 00 00。 分别按下 左 Alt 左 Shift 和 P 键后,UsbHost Shield 将会解析出06 00 13 00 00 00 00 00 ,抬起后还会输出出0000 00 00 00 00 00 00。我们会将这个组合键作为输出记录值的触发条件。 [mw_shl_code=c,true]/* 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
#define RECORDBUFSIZE 60
/**/
/* "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;
int addr = 0;
char p = 0;
char KeyRecord[RECORDBUFSIZE];
bool OutputMark = false;
MAX3421E Max;
USB Usb;
void setup() {
Serial.begin( 9600 );
//Serial.println("Start");
Max.powerOn();
delay( 200 );
for (int i = 0; i < RECORDBUFSIZE; i++)
{
KeyRecord = 0x1D; //Init with 'z'
}
}
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();
}
if (OutputMark) {
//Send all data to 16U2
for (int i = 0; i < RECORDBUFSIZE; i++) {
//Send all data to 16u2
Serial.write(0); //Byte 0 == 0
Serial.write(0); //Byte 1 == 0
Serial.write(KeyRecord); //Byte 2
Serial.write(0); //Byte 3
Serial.write(0); //Byte 4
Serial.write(0); //Byte 5
Serial.write(0); //Byte 6
Serial.write(0); //Byte 7
Serial.write(0); //Byte 0
Serial.write(0); //Byte 1
Serial.write(0); //Byte 2
Serial.write(0); //Byte 3
Serial.write(0); //Byte 4
Serial.write(0); //Byte 5
Serial.write(0); //Byte 6
Serial.write(0); //Byte 7
}
OutputMark = false;
}
}
/* 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 )
{
byte i;
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..
i = 0;
while (i < 8) {
if (old_buf != buf) {
i = 0xff;
break;
}
i++;
}
if (i == 0xff) { //if new key
switch ( buf[ 0 ] ) {
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
break;
default:
//By pass all data to 16u2
Serial.write(buf,8);
//Save the keyvalue to memory
for (i = 2; i < 8; i++) {
if ((buf >= 4) && (buf <= 27)) {
KeyRecord[p] = buf;
p = (p + 1) % RECORDBUFSIZE;
}
}
//If we get 'Output command', we will output all the data in eeprom
if ((buf[0] == 0x06) && (buf[2] == 0x13) && (buf[3] == 0x00)) {
OutputMark = true;
}
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 ...
for ( i = 0; i < 8; i++ ) { //copy new buffer to old
old_buf[ i ] = buf[ i ];
}
}//if (i==0xff) { //if new key
}
/* compare byte against bytes in old buffer */
bool buf_compare( byte data )
{
char i;
for ( i = 0; i < 8; i++ ) {
if ( old_buf[ i ] == data ) {
return ( true );
}
}
return ( false );
}[/mw_shl_code]
上面的程序架构和之前的USB键盘转蓝牙键盘的架构是一样的,具体的设计有兴趣的读者可以在【参考5】看到。 对于按键的记录是这样的:如果发现解析出来的按键信息Byte2-7 不为零才主动存储在内容中,进行循环覆盖。当检测到有左 Alt+Shift+P 按下时,将存储在内存中的键值从USB口再次输出,此时在PC端打开一个记事本工具即可看到内容。本文主要目标是演示思路,所以这部分代码并没有优化,实用性方面较差。
完整代码下载
参考:
|