Posts

How to drive USB keyboard from Arduino

USB Host Shield driving a keyboard

USB Host Shield driving a keyboard


I am continuing on topic of using USB Host shield to drive USB peripherals started in the previous article. The code presented here performs keyboard polls, character translation and management of “LOCK” keys – NumLock, CAPSlock and ScrollLock.

A keyboard is hard to use if results of typing can’t bee seen, so my first step was to add an output device. Even though HD44780-compatible character LCD can be driven with Arduino pins, I wrote a little library which uses GPOUT port of MAX3421E for LCD control. This library is now a part of USB Host Shield repo on github. I used LCD high-level routines from official Arduino distro and developed low-level functions specific to MAX3421E hardware, as well as LCD initialization. Since function syntax and behaviour (and source code ) is identical to official LCD functions, the user manual is already written and can be found in the library reference by title LiquidCrystal. The only difference is in the constructor; since only 4-bit mode is possible and only one pinout is supported, Max_LCD constructor is not accepting any parameters. The pinout of LCD connections to GPOUT is given in the Max_LCD.h file; see also title image of this article.

The LCD data lines are driven directly by 3.3V logical levels of MAX3421E. At the same time, VDD is connected to 5V, this way LCD contrast can be adequately biased. This arrangement worked so far with all various LCDs that I own, however, if 5V-only LCD needs to be driven, 74HCT245 level shifter can be soldered to the place provided.

Let’s take a look at the sketch. Since it is quite large, the whole text is stored in the examples directory of the repo. Excerpts from it with my comments are posted later in the text.

The sketch implements the following:

1. Initialize keyboard and LCD
2. Poll the keyboard and output results on LCD
3. Clear LCD screen if DELETE key is pressed
4. Alternate lines when ENTER key is pressed
5. Keep states of NumLock and CAPSLock and indicate them on keyboard LCD
6. Turn ScrollLock LCD on/off when ScrollLock key is pressed.

To understand the code better, please get a copy of USB HID specification and HID Usage tables. Also, it helps to get familiar with a sketch in my previous article about USB peripherals control.

USB keyboard in boot protocol mode is unsophisticated piece of hardware. When requested, it sends out 8-byte packet showing which keys are pressed. Also, you can send it a byte stating which LEDs have to be turned on and which should be turned off. If you press more than 6 non-modifier keys, you’ll get an error code instead of key code. If you poll keyboard too fast, you’ll get error instead of the buffer. If you don’t poll keyboard often enough, it is possible to lose some key presses and receive an error instead of the buffer. Such errors don’t require any further processing and can be ignored.

Let’s look at the sketch. You can see that setup() and loop() parts look very much like a copy from the sketch published in the previous article. The only difference is occasional LCD-related call here and there. The real difference starts showing in functions placed after the loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* 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;
 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
            break;
          case DELETE:
            LCD.clear();
            LCD.home();
            line = false;
            break;
          case RETURN:
            line =! line;
            LCD.setCursor( 0, line );
            break;
          default:
            //LCD.print("A");                          //output
            LCD.print( HIDtoA( buf[ i ], buf[ 0 ] ));
            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...
    for( i = 2; i < 8; i++ ) {                    //copy new buffer to old
      old_buf[ i ] = buf[ i ];
    }
}

Let’s see what happens during keyboard poll. The buffer request is sent on line 9. Result code is analyzed and processing continues if poll was successful. This error processing is rather simplified and the only reason for it to be here is detection of NAKs which keyboard sends if polled too frequently.

The for loop starting at line 13 first checks for new characters. Since keyboard sends its state rather than key presses, we want to track contents of the buffer and output a character only if it was not present in the buffer sent in the previous poll. Also, if one byte of the buffer contains 0, all subsequent bytes are also 0 and further processing can be skipped. If you want a visual clue of what is happening, load a sketch from previous article, press and hold 3 alphanumeric keys on the keyboard, then release the middle one.

Once a new key is found, the key code is checked against several control keys. First, we need to intercept and store “Lock” keys, since keyboard has no clue about their state. That’s what first 3 cases in a switch statement do. Then we process “Delete” and “Enter” keys, and finally the rest of the key codes are sent to LCD after being converted to ASCII by HIDtoA function. Lastly, in line 47, I send state of Lock keys back to the keyboard to turn LEDs on/off.

The next code fragment is HIDtoA function. It is just a bunch of switch statements converting HID key codes to ASCII. Not all keys are supported; arrows and the rest of screen navigation keys are omitted for obvious reason. However, if any of these keys are needed for the application, they can easily be added in HIDtoA().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* HID to ASCII converter. Takes HID keyboard scancode, returns ASCII code */
byte HIDtoA( byte HIDbyte, byte mod )
{
  /* upper row of the keyboard, numbers and special symbols */
  if( HIDbyte >= 0x1e && HIDbyte <= 0x27 ) {
    if(( mod & SHIFT ) || numLock ) {    //shift key pressed
      switch( HIDbyte ) {
        case BANG:  return( 0x21 );
        case AT:    return( 0x40 );
        case POUND: return( 0x23 );
        case DOLLAR: return( 0x24 );
        case PERCENT: return( 0x25 );
        case CAP: return( 0x5e );
        case AND: return( 0x26 );
        case STAR: return( 0x2a );
        case OPENBKT: return( 0x28 );
        case CLOSEBKT: return( 0x29 );
      }//switch( HIDbyte...
    }
    else {                  //numbers
      if( HIDbyte == 0x27 ) {  //zero
        return( 0x30 );
      }
      else {
        return( HIDbyte + 0x13 );
      }
    }//numbers
  }//if( HIDbyte >= 0x1e && HIDbyte <= 0x27
  /**/
  /* number pad. Arrows are not supported */
  if(( HIDbyte >= 0x59 && HIDbyte <= 0x61 ) && ( numLock == true )) {  // numbers 1-9
    return( HIDbyte - 0x28 );
  }
  if(( HIDbyte == 0x62 ) && ( numLock == true )) {                      //zero
    return( 0x30 );
  }
  /* Letters a-z */
  if( HIDbyte >= 0x04 && HIDbyte <= 0x1d ) {
    if((( capsLock == true ) && ( mod & SHIFT ) == 0 ) || (( capsLock == false ) && ( mod & SHIFT ))) {  //upper case
      return( HIDbyte + 0x3d );
    }
    else {  //lower case
      return( HIDbyte + 0x5d );
    }
  }//if( HIDbyte >= 0x04 && HIDbyte <= 0x1d...
  /* Other special symbols */
  if( HIDbyte >= 0x2c && HIDbyte <= 0x38 ) {
    switch( HIDbyte ) {
      case SPACE: return( 0x20 );
      case HYPHEN:
        if(( mod & SHIFT ) == false ) {
         return( 0x2d );
        }
        else {
          return( 0x5f );
        }
      case EQUAL:
       if(( mod & SHIFT ) == false ) {
        return( 0x3d );
       }
       else {
        return( 0x2b );
       }
       case SQBKTOPEN:
         if(( mod & SHIFT ) == false ) {
          return( 0x5b );
         }
         else {
          return( 0x7b );
         }
       case SQBKTCLOSE:
         if(( mod & SHIFT ) == false ) {
          return( 0x5d );
         }
         else {
          return( 0x7d );
         }
       case BACKSLASH:
         if(( mod & SHIFT ) == false ) {
           return( 0x5c );
         }
         else {
           return( 0x7c );
         }
       case SEMICOLON:
         if(( mod & SHIFT ) == false ) {
           return( 0x3b );
         }
         else {
           return( 0x3a );
         }
      case INVCOMMA:
        if(( mod & SHIFT ) == false ) {
          return( 0x27 );
        }
        else {
          return( 0x22 );
        }
      case TILDE:
        if(( mod & SHIFT ) == false ) {
          return( 0x60 );
        }
        else {
          return( 0x7e );
        }
      case COMMA:
        if(( mod & SHIFT ) == false ) {
          return( 0x2c );
        }
        else {
          return( 0x3c );
        }
      case PERIOD:
        if(( mod & SHIFT ) == false ) {
          return( 0x2e );
        }
        else {
          return( 0x3e );
        }
      case FRONTSLASH:
        if(( mod & SHIFT ) == false ) {
          return( 0x2f );
        }
        else {
          return( 0x3f );
        }
      default:
        break;
    }//switch( HIDbyte..
  }//if( HIDbye >= 0x2d && HIDbyte <= 0x38..
  return( 0 );
}

The code presented here is an example of tasks necessary to control a HID peripheral. As you can see, all that is necessary is a pair of USB transfers and some simple data processing. If you want to use this code, don’t forget to download the latest library code from github. Shields are available for sale at the store. As always, your comments and suggestions are most welcome.

Oleg.

35 comments to How to drive USB keyboard from Arduino

  • […] have managed to host a USB keyboard with an Arduino and display the keyboard inputs on a character LCD. This uses the USB host shield we covered in […]

  • Jason Schorr

    Can you please link to where the pinouts for that lcd you have? i think its the same one i have and dont know how to interface with it since i dont know what the i/o are.

    thanks 🙂

    • The pinout of my LCD looks like the fist 1×14 pin pinout mentioned on this page http://www.geocities.com/p9019/lcd.html ( Seiko L1692 ).

      • Nate

        The LCD pinout link dosn’t work anymore. Could you please provide me with the pinout to the LCD? Also, as I understand it, the current Arduino usb shield requires digital pins 7-13 (so I can’t use them for anything else), is that correct? Thanks.

        • That’s another one -> http://en.wikipedia.org/wiki/HD44780_Character_LCD I hope it will stay longer 🙂 LCD connections to the shield are documented in the beginning of Max_LCD.h file. Pins 7 and 8 can be freed for other uses. GPX is not used by the code, you need to remove the GPX jumper from the shield and then you can use pin 8. Pin 7 ( RESET ) takes a little more time to free. You need a) disconnect it from Arduino bu removing a jumper b) route RESET to RESET pin of Arduino with a piece of wire, and c) remove operations with MAX_RESET from Max3421e.cpp code (just comment out #define MAX_RESET in Max3421e_constants.h and follow compiler errors, there will be about 3-4 of them ).

  • dkb

    Am I understanding this correctly that basically with this USB host shield you theoretically can connect almost any USB device to an arduino? And the limiting factor here is “drivers” for each device?

    One of the other USB host shield entries mentioned testing it with a hard drive. Was the hard drive actually used to access data via the arduino or just tested the detection of the hard drive?

    • Dkb,

      You can drive any USB device with this shield if you write the code; MAX3421E takes care of the bus and my library supports all necessary low-level USB requests. Microchip USB stack is a good place to look at examples of high-level USB code, they have examples for HID, CDC, and mass storage. As far as USB hard drive support, I only used it for testing as an example of self-powered device, nothing has been done to it past enumeration.

  • dkb

    It looks like you are requiring pin 13 for SPI to communicate to the shield. What options are there if you want to use the USB Host shield with an ethernet shield which needs dedicated access to digital pin 13?

    • If you are talking about this shield ->http://www.arduino.cc/en/Main/ArduinoEthernetShield , it uses SPI also. SCK, MOSI and MISO can be shared between two shields, SS on USB Host shield is routed via a jumper, it can be re-routed on any unused pin, if necessary.

  • dkb

    For what its worth I contacted Maxim since the 3421E is not recommended for new designs and got this reply:
    | There is no replacement or upgrade recommended for the MAX3421.
    | No other company is known to 2nd source this device.

  • dkb

    My point is that after the end of this year there will not be a MAX3421 in production and there is no replacement so is there a plan for what to use for the USB Host Shield instead? I’d hate to spend resources writing drivers for a device whose near future is uncertain.

    • dkb,

      Where did you get this “end of this year” idea from? I e-mailed Maxim today about end of life of MAX3421E and got the following reply “No final discontinuance date set as yet for this product, but it is Not Recommended for New Designs”, which, according to their policy http://www.maxim-ic.com/company/policy/product_discontinuance_policy.pdf , would give us at least 6 months to leave if Maxim announces discontinuance tomorrow. Design of the next version of the shield is in progress, it can be finished very quickly if needs be. The compatibility will be maintained on the library level – if your app calls USB class functions, it will continue working.

  • Richard

    IF the MAX3421E is discontinued it will be sad because it is a great entry level USB host device and great for use with Arduino. Samples still seem to be available, worth getting some spares !

    The good news is that even if this device is discontinued, then it is unlikely that the resources you spend will be wasted. USB generally and the MAX3421 device provide a strongly layered architecture, and the lower hardware layers are suprisingly easy to replace. Most of the low level drivers ara already done, so the part you develop will likely work on other hardware with minor adaption. The emergence of USB host controllers within microcontrollers (AVR32, PIC32) will give us plenty of future alternatives.

  • Richard,

    I agree. In fact, that’s exactly my plan in case Maxim stops producing MAX3421E – build a SIE out of PIC24, implement everything up to transfers in PIC firmware and slightly modify usb library. But frankly, I doubt MAX3421E will disappear any time soon; I think they just need an excuse for not supporting it.

    BTW, thank you for sharing your code – I learned a couple of things!

    Oleg.

  • rgm

    Is it possible to read the state of “multimedia” keys if the USB keyboard is in boot protocol mode? My keyboard, a cheap Dynex DX-WKBD, has several “extra” keys that I would like to use.

    • I tried multimedia keys on my cheap HP keyboard and they didn’t work in boot protocol mode. Most likely, you would need to do HID report parsing. Read Richard’s game controller articles to get an idea how to do it.

  • Ryan

    Dumb question – can you use this USB controller with a USB HUB and then do multiple things with it?

    Like… use a USB keyboard (or more practically, a USB numeric keypad for size reason) and use that to get input and also have a camera connected using the camera control code you posted earlier?

    • Hub is not supported in this version of the library. It will be supported in the next version, due (hopefully) early next year.

  • Einar

    Is there a way to use this code make text commands to activate digitalWrite function?
    I’m fairly new to programming, but I’m looking to use my wireless keyboard for basic switching.

    Einar.

    • This can be done rather easily – just replace printing functions in the first listing ( under ‘default’ ) to whatever you need. Since you won’t need ASCII codes of keys, you can also simplify the whole thing by removing keycode to ASCII conversion.

  • Einar

    I have one more question 😛
    Can I change the code so buttons on the keyboard work as momentary switches so HIGH and LOW state can be read on each button?

  • Einar

    Alright, thanks :). I think I’ve learned enough about the keyboard function, now I just need to figure out how to loop digitalWrite functions for two stepper motors while a button is pressed. It only runs one time through it, as in only one step when button is pressed. If you have the time I would really appreciate the help. I’m using switch case function. I’ve read about a few methods but it’s a bit over my head at the moment.

    -Einar

  • Einar

    I got the whole thing running but there’s one problem, it seems like the keyboard put a lag into the motor run, kinda like the button checking is taking too long in the loop. Any idea how I can smooth it out?

    • If you use interrupt endpoint to poll the Keyboard, it replies immediately or returns a NAK. You may want to play with NAK_LIMIT to see if delay is caused by NAKs.

  • amos

    Hi,

    I am trying to get a USB DELL keyboard to work with the most updated USB Host Shield codes. Tried the board_test but it keeps giving packet error D.

    I am using the old version of the USB Host Shield, but have already swapped the MAX_GPX and _INT and rewired the SPI since I am using Mega1280.

    Any ideas on how I should go about doing it?

    This is the USB properties of the keyboard:

    Connection Status Device connected
    Current Configuration 1
    Speed Low
    Device Address 1
    Number Of Open Pipes 1

    Device Descriptor Dell USB Keyboard
    Offset Field Size Value Description
    0. bLength 1 12h
    1. bDescriptorType 1 01h Device
    2. bcdUSB 2 0110h USB Spec 1.1
    4. bDeviceClass 1 00h Class info in Ifc Descriptors
    5. bDeviceSubClass 1 00h
    6. bDeviceProtocol 1 00h
    7. bMaxPacketSize0 1 08h 8 bytes
    8. idVendor 2 413Ch Dell Computer Corp.
    10. idProduct 2 2003h
    12. bcdDevice 2 0100h 1.00
    14. iManufacturer 1 01h “Dell”
    15. iProduct 1 02h “Dell USB Keyboard”
    16. iSerialNumber 1 00h
    17. bNumConfigurations 1 01h

    Configuration Descriptor 1 Bus Powered, 70 mA
    Offset Field Size Value Description
    0. bLength 1 09h
    1. bDescriptorType 1 02h Configuration
    2. wTotalLength 2 0022h
    4. bNumInterfaces 1 01h
    5. bConfigurationValue 1 01h
    6. iConfiguration 1 04h “HID Keyboard”
    7. bmAttributes 1 A0h Bus Powered, Remote Wakeup
    4..0: Reserved …00000
    5: Remote Wakeup ..1….. Yes
    6: Self Powered .0…… No, Bus Powered
    7: Reserved (set to one)
    (bus-powered for 1.0) 1…….
    8. bMaxPower 1 23h 70 mA

    Interface Descriptor 0/0 HID, 1 Endpoint
    Offset Field Size Value Description
    0. bLength 1 09h
    1. bDescriptorType 1 04h Interface
    2. bInterfaceNumber 1 00h
    3. bAlternateSetting 1 00h
    4. bNumEndpoints 1 01h
    5. bInterfaceClass 1 03h HID
    6. bInterfaceSubClass 1 01h Boot Interface
    7. bInterfaceProtocol 1 01h Keyboard
    8. iInterface 1 05h “EP1 Interrupt”

    HID Descriptor
    Offset Field Size Value Description
    0. bLength 1 09h
    1. bDescriptorType 1 21h HID
    2. bcdHID 2 0110h 1.10
    4. bCountryCode 1 00h
    5. bNumDescriptors 1 01h
    6. bDescriptorType 1 22h Report
    7. wDescriptorLength 2 0041h 65 bytes

    Endpoint Descriptor 81 1 In, Interrupt, 24 ms
    Offset Field Size Value Description
    0. bLength 1 07h
    1. bDescriptorType 1 05h Endpoint
    2. bEndpointAddress 1 81h 1 In
    3. bmAttributes 1 03h Interrupt
    1..0: Transfer Type ……11 Interrupt
    7..2: Reserved 000000..
    4. wMaxPacketSize 2 0008h 8 bytes
    6. bInterval 1 18h 24 ms

    Interface 0 HID Report Descriptor Keyboard
    Item Tag (Value) Raw Data
    Usage Page (Generic Desktop) 05 01
    Usage (Keyboard) 09 06
    Collection (Application) A1 01
    Usage Page (Keyboard/Keypad) 05 07
    Usage Minimum (Keyboard Left Control) 19 E0
    Usage Maximum (Keyboard Right GUI) 29 E7
    Logical Minimum (0) 15 00
    Logical Maximum (1) 25 01
    Report Size (1) 75 01
    Report Count (8) 95 08
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) 81 02
    Report Count (1) 95 01
    Report Size (8) 75 08
    Input (Cnst,Ary,Abs) 81 01
    Report Count (3) 95 03
    Report Size (1) 75 01
    Usage Page (LEDs) 05 08
    Usage Minimum (Num Lock) 19 01
    Usage Maximum (Scroll Lock) 29 03
    Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit) 91 02
    Report Count (1) 95 01
    Report Size (5) 75 05
    Output (Cnst,Ary,Abs,NWrp,Lin,Pref,NNul,NVol,Bit) 91 01
    Report Count (6) 95 06
    Report Size (8) 75 08
    Logical Minimum (0) 15 00
    Logical Maximum (255) 26 FF 00
    Usage Page (Keyboard/Keypad) 05 07
    Usage Minimum (Undefined) 19 00
    Usage Maximum 2A FF 00
    Input (Data,Ary,Abs) 81 00
    End Collection C0

    -Amos

  • How is the LCD connected to the USB Shield? Can you share that information? I tried looking into the max_LCD.h file and connected RS, E and the four pins but was not able to figure out how to connect the VCC and Vo pins. Also do we have to make any change to the library to use LCD?

    • Power pins are not specific to the shield, you can connect the as usual, i.e., VCC to 5V, GND to GND and Vo to a wiper of a potentiometer connected between VCC and GND. You don’t need to change the library.

  • Sebo

    Hi oleg,
    the Function “OemToAscii(mod, key)” is called several times, but i can’t find it? In the hidboot.h in Line 144 is written “uint8_t OemToAscii(uint8_t mod, uint8_t key);” – i think this is the important place. Can you explain it for me?

  • Anyone have a version of this for the Version 2 Library. (ADK board)…