Keyboards

USB HID keyboard with V-USB

There still seems to be a lot of traffic to my V-USB tutorials, so I thought I’d write a short follow-up post on USB keyboards. I already did a USB HID mouse post earlier, so you might want to check that out to understand a bit about HID descriptors and associated V-USB settings (in short, human interface devices send a binary descriptor to PC telling what kind of “reports” they send to the host on user activities).

As a basic setup, you’ll need a working V-USB circuit with one switch and one LED attached. Here, I’m using ATtiny2313 with the LED wired to PB0 and switch to PB1. The ATtiny is using 20 MHz crystal, so if you’re following my USB tutorial series and have that circuit at hand, remember to change that frequency in usbconfig.c before trying this out. Note the cool breadboard header I have, there will be more posts about that one to follow soon!

USB HID keyboard basics

A USB keyboard is a normal USB device, very much like a mouse, but it has an interrupt endpoint, which is used to send keyboard events to the host PC (instead of mouse movements). In this tutorial, I’m using a full “boot compliant HID” specification that can have up to six keys pressed down at a time. The bonus side is that these types of devices receive (at least on Windows, probably on Linux, not on OS X) keyboard LED state changes, so we can do cool things with our device, like I did in my USB password generator, where password generation is triggered by repeated CAPS LOCK presses.

To make the operating system recognize our device as HID keyboard, we need to make mostly the same changes to usbconfig.h as we did in my mouse tutorial. Here’s the overview of things you’ll probably need to change from my USB tutorial series:

#define USB_CFG_HAVE_INTRIN_ENDPOINT 1 #define USB_CFG_IMPLEMENT_FN_WRITE 1 #define USB_CFG_VENDOR_ID 0x42, 0x42 #define USB_CFG_DEVICE_ID 0x31, 0xe1 #define USB_CFG_DEVICE_CLASS 0 #define USB_CFG_DEVICE_SUBCLASS 0 #define USB_CFG_INTERFACE_CLASS 0x03 // HID #define USB_CFG_INTERFACE_SUBCLASS 0x01 // Boot #define USB_CFG_INTERFACE_PROTOCOL 0x01 // Keyboard #define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH 63

On the main.c side, the keyboard descriptor is needed. It’s omitted here for brevity, see the project zip for details. What matters is, that the descriptor describes the following data structure that is used to send keypresses to PC:

typedef struct { uint8_t modifier; uint8_t reserved; uint8_t keycode[6]; } keyboard_report_t; static keyboard_report_t keyboard_report; // sent to PC

Every time a new button is pressed, we send a report. We also need to send a report when keys are no longer pressed – so to send a single ‘a’, we first send a report containing that character, and then send a report with only zeroes to tell that letter has been released. Forget to do that, and the device floods your system with a’s – pretty hard to reflash! If you happen to make that mistake, here’s how to fix it:

  1. Unplug the device
  2. Fix the code
  3. Clear the command-line and prepare the ‘make flash’ command
  4. Plug the device in and immediately hit ‘enter’ before the PC has time to recognize the flooder
  5. Once the flashing starts, the device resets and will not send any offending letters

From the report structure you can also see that “modifier keys” such as control, alt and shift are sent separately from “normal keys” – the PC driver does the actual work so that when we send “shift” as a modifier and ‘a’ as a letter, ‘A’ will be output to any program currently active. The keycodes are also specific to the USB HID standard, for example ‘a’ is 4. You can find the full code set from “HID Usage tables” (google it or find it here), chapter 10, page 53.

Basic HID keyboard code

To pass as a functional keyboard, a few things need to be implemented in the firmware:

  1. Our device needs to be able to return the keyboard descriptor to PC when it is requested
  2. Repeat rate needs to be set and returned when asked
  3. The host can also ask the keyboard report via usbFunctionSetup, in which case we just send “no keys pressed”
  4. Also, to receive LED state changes, we need to be able to receive some data on. A custom usbFunctionWrite is used to do this.

Here’s the code to do that and keep track of LED state (and mirror the CAPS LOCK to a LED in PB0):

volatile static uchar LED_state = 0xff; // received from PC static uchar idleRate; // repeat rate for keyboards usbMsgLen_t usbFunctionSetup(uchar data[8]) { usbRequest_t *rq = (void *)data; if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) { switch(rq->bRequest) { case USBRQ_HID_GET_REPORT: // send “no keys pressed” if asked here // wValue: ReportType (highbyte), ReportID (lowbyte) usbMsgPtr = (void *)&keyboard_report; // we only have this one keyboard_report.modifier = 0; keyboard_report.keycode[0] = 0; return sizeof(keyboard_report); case USBRQ_HID_SET_REPORT: // if wLength == 1, should be LED state return (rq->wLength.word == 1) ? USB_NO_MSG : 0; case USBRQ_HID_GET_IDLE: // send idle rate to PC as required by spec usbMsgPtr = &idleRate; return 1; case USBRQ_HID_SET_IDLE: // save idle rate as required by spec idleRate = rq->wValue.bytes[1]; return 0; } } return 0; // by default don’t return any data } #define NUM_LOCK 1 #define CAPS_LOCK 2 #define SCROLL_LOCK 4 usbMsgLen_t usbFunctionWrite(uint8_t * data, uchar len) { if (data[0] == LED_state) return 1; else LED_state = data[0]; // LED state changed if(LED_state & CAPS_LOCK) PORTB |= 1 << PB0; // LED on else PORTB &= ~(1 << PB0); // LED off return 1; // Data read, not expecting more }

Adding a switch to the mix

After fulfilling these basic requirements, the code in main method stays pretty much the same as in my V-USB tutorials. If all we wanted to do was to monitor CAPS LOCK, we’d be done by now, but let’s make the switch connected to PB1 send a letter “x” when it’s clicked. In my test circuit, I have enabled a pullup resistor at PB1 and wired to switch so that when it’s pressed, PB1 is connected to ground and PB1 bit goes from 1 to 0. So in an ideal world, the main loop would look like this (I’m using a really simple helper function buildReport() to fill keyboard_report, see the actual code for that):

while(1) { wdt_reset(); // keep the watchdog happy usbPoll(); if(!(PINB & (1<Unfortunately, we don’t live in an ideal world. First of all, it takes some time after sending one report via the interrupt endpoint, so instead of two usbSetInterrupt() calls right after each other, we need to have a simple state machine inside the “if(ready)” code that first sends the key and changes state, then sends the “no keys pressed” message and changes the state. That’s rather easy to do:

if(usbInterruptIsReady() && state != STATE_WAIT && LED_state != 0xff){ switch(state) { case STATE_SEND_KEY: buildReport(‘x’); state = STATE_RELEASE_KEY; // release next break; case STATE_RELEASE_KEY: buildReport(NULL); state = STATE_WAIT; // go back to waiting break; } }

Second problem arises from the fact that a physical switch does not toggle cleanly from 1 to 0, but flickers for several times every time when pressed/released. Therefore, the current code would probably send the ‘x’ a dozen times per switch toggle. An easy way to fix this is to have a simple release counter that counts the number of iterations since last switch release – only after enough time has elapsed, is a new event generated. Like this:

if(!(PINB & (1<Now everything should work, more or less. For your enjoyment, I have compiled a nice little zip file also for this project, where you can find both main.c and usbconfig.h and a nice makefile to build and flash the project with a single invocation of make flash.

If you’ve done everything correctly, after flashing the MCU you should have device which toggles the LED in sync with your normal keyboard when you press CAPS LOCK on and off, and sends ‘x’ whenever you toggle the switch. This project is easily expandable to a data logger, full keyboard, or any cool project you can think of. Enjoy!

Source

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *