Code and Life

Programming, electronics and other cool tech stuff

Supported by

Supported by Picotech

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<<PB1))) // button pressed
        state = STATE_SEND_KEY;

    if(usbInterruptIsReady() && state != STATE_WAIT) {
        buildReport('x'); // fill keyboard_report with 'x' pressed
        usbSetInterrupt((void *)&keyboard_report, sizeof(keyboard_report));
        buildReport(NULL); // clear keyboard report
        usbSetInterrupt((void *)&keyboard_report, sizeof(keyboard_report));
        state = STATE_WAIT;
    }
}

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<<PB1))) { // button pressed
    // also check if some time has elapsed since last button press
    if(state == STATE_WAIT && button_release_counter == 255)
        state = STATE_SEND_KEY;
				
    button_release_counter = 0;
    // now button needs to be released for a while until retrigger
}
		
if(button_release_counter < 255) 
    button_release_counter++; // increase release counter

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!

41 comments

Charles Webb:

Yes! Got it working with an ATMega328P-PU in AVR Studio 6. Thank you so much for these tutorials and examples.

Acid:

Hey man, how did you get it working with the ATMEGA328P-PU I tried and failed….Please help :)

Charles Webb:

I bread boarded the metaboard circuit. I use the pjrc teensy 2.0 to write to the 328p. I created the project in atmel studio v6. Lots of funky compiler errors that had to get fixed but finally got it compiled and it worked. Guess I could export the project and post it somewhere.

eka:

Can i send some data to the avr and have it do some other thing? for example if i send data 0x01 0x03 0x04
then it will switch a pin
tq

jokkebk:

HID keyboards only receive keyboard LED state changes, so signalling back to device is hard. Please see my V-USB tutorial for device that could do more – you could also combine these two, so that once the device has sent the password, it waits for 30 seconds and then resets and shows itself as custom USB device, and you can then use the examples from V-USB tutorial to send any custom data to it.

Hmm, actually now that I think more about that, V-USB probably doesn’t support two alternative descriptors out of the box, so you’d need to hack it a bit to achieve the suggested method. Another way would be to use caps lock and scroll lock to “morse” binary data to the device – if you can toggle those programmatically from windows, it might be rather straightforward.

eka:

aha, morse code
thank you sir
nice idea :)
that’s why when i debug it i only get 1 or 2 byte of data
old fashion ways still do the job,hehehehe
morse code, open up my scout guide book T_T

Danjovic:

Nice tutorials. I have learned a lot with them.
I have a question which I could not find an answer. Do you know how to implement a descriptor that gives me a dual ‘box’ of X-Y axes?

Diego Aguilera:

Did you get errors that looked like this?
usbdrv/usbdrv.h:455:6: error: variable ‘usbDescriptorDevice’ must be const in order to be put into read-only section by means of ‘__attribute__((progmem))’

That’s where I’m at right now.

Neckname:

Hi, just build this project, but I attached the LED to PD0 and the switch to PD1.

I had to make some variables “const” for use with PROGMEM.
The switch is working fine, although I’d like to have that key repeated after short while.

The status led doesn’t work, sadly.
If I switch it on manually in the source code, it lights up when I plug the µC in and is switched off after ~1 second.

int main() {
uchar i, button_release_counter = 0, state = STATE_WAIT;

DDRD |= (1 << PD0); // PD0 as output
PORTD |= (1 << PD1); // PD1 is input with internal pullup resistor activated
PORTD |= 1 << PD0; // switch LED on manually
...
}

I have no idea what could be wrong, except:
The avr-gcc isn’t very happy with some of the pointer related stuff

main.c:74:23: warning: assignment makes integer from pointer without a cast [enabled by default]
main.c:81:23: warning: assignment makes integer from pointer without a cast [enabled by default]
main.c: In function 'main':
main.c:173:5: warning: passing argument 1 of 'buildReport' makes integer from pointer without a cast [enabled by default]
main.c:112:6: note: expected 'unsigned char' but argument is of type 'void *'

jokkebk:

Weird issue – if you make a piece of code with only those lines, does the LED stay on indefinitely? Is any other piece of code accessing PORTD that could interfere with the LED? If the standalone version works and nothing is messing with PD0, it sounds like either power issue or the device going haywire after ~1 second (in which case the keyboard functionality should also stop working).

jokkebk:

It seems never versions of avr-gcc don’t work that well with V-USB (or older versions of it?) and you need to add const to several places to get it to compile… Haven’t revisited these projects in a while so I don’t have first hand experience on the needed fix, and neither knowledge if V-USB guys have solved it in a newer version.

jokkebk:

No, sorry. You’d probably need a custom descriptor, and you’ll need to dig into the USB HID spec yourself or find a good tutorial – I barely understood enough myself to copy-paste ready-made descriptor definitions to my projects. :)

Oscar Alcantara:

Do you know the way of detect more than 6 keys simultaneous??, I want use an atmega16 with vusb and have a lot of input keys that can be pressed simultaneously … I only see the way of send the report with status of 6 keys and no have idea how to inform to computer more keys :S

jokkebk:

Normal USB keyboard reports have a limitation of 6 simultaneous keys. I’ve understood there are options to expand it, but not sure if a driver is required (I think one of my own NKRO-capable keyboards has a separate mode for it, but Windows did recognize it automatically).

I suggest you google for “USB HID over 6 keys” and similar ones, I’m sure this isn’t the first time this question is asked. :)

Dewsworld:

Thanks for writing this awesome (and life saving) tutorial :)

I fall into a situation to send multiple modifiers key at a time. Is it possible to send them while keeping the six keys at a time?

Joonas Pihlajamaa:

Yes the modifier field is separate from the six keycodes, so you can combine any modifiers into that field without decreasing the six key limit.

seetharaman:

Hi
i’m new to this. I need to interface my mcu to pc via usb to act as hid keyboard and mouse, is there any ic or module that takes care of the interface protocol part?

thanks

Joonas Pihlajamaa:

The article you’re commenting on tells hwo to make a keyboard with only ATtiny, but some newer models have hardware support for much of the same (if you google around for avr and usb support, you’ll find those). Also, FTDI and other have various ICs that can handle similar tasks. You might also want to check out LUA project, it’s for AVR chips with USB support if I recall correctly.

Pino:

Please send me fuse setting of attiny2313 for this project. Thanks

Pino:

Can we make universal HID IO device that can set pins to hi/low or get pins state, even reading analog input?

So then we can switch relay, drive motor, reading sensor, etc.

I hope you can make tutorial about this :)

Joonas Pihlajamaa:

You can use the good AVR fuse calculator at http://www.engbedded.com/fusecalc/ – if you need help, my USB tutorial part 2 has fuse settings for 12 MHz clock, if I recall correctly, all crystals over 8 MHz will work with the same fuse bits as in part 2.

http://codeandlife.com/2012/01/25/avr-attiny-usb-tutorial-part-2/

Dominique:

This is awesome. Worked right away on an Attiny2313 and by now I’ve expanded it to 12 buttons, read in thru shift registers, further expansion pending.

However with the original buildReport() function I believe only one keypress at a time can be transferred? I asume so because it always writes to keycode[0].

I’m trying to get it to work to send multiple keys in parallel but I’m getting nowhere.

I rewrote the buildReport to this:
void buildReport(uchar keyCode, uchar offset) {
keyboard_report.modifier = 0;
keyboard_report.keycode[offset] = keyCode;
}

Note that keyCode has to be the usb key code (4 for ‘a’ etc).

I was hope that someone might point me in the right direction how to send multiple keys with a single report?

thanks a ton.

Joonas Pihlajamaa:

I haven’t tried sending multiple keypresses, but note that sending ‘a’, ‘b’ and ‘c’ with single report is treated like user had simultaneously pressed all those keys down. It might be that because O/S doesn’t know if it was “abc”, “bac” or “cab”, the letters are discarded in text input type situations. This is pure speculation, though. :)

Dominique:

Hi Joonas,

I think I know what you mean. However I’m testing this as part of a custom game panel setup:

For example ‘A’ moves a character left, ‘D’ moves it right.

When I press both ‘A’ and ‘D’ on my normal (!) keyboard they are both registered and cancel each other out (left+right == stand still ;) )

However when I press both buttons on my project controller it only registered the first key.

This leads me to the conclusion that something is wrong with what I’m sending although I can’t see what the problem is, especially because the C64 keyboard project over at the VUSB website is using a similar approach to send multiple key presses, although the code is different.

Oh well… guess I have to find myself an USB sniffer after all… :)

Dominique:

consider this one closed, I had a floating input somewhere that messed things up. The code is working just fine.

Once I got all parts done and assembled I might put a project up for the community projects list.

Joonas Pihlajamaa:

Great! And thanks for sharing the success, mostly people comment when they encounter problems, so this was a welcome change. :) Good luck with your gaming project, sounds cool!

Dominique:

Heh, thanks :). I know its always a bit frustrating to see the questions but not the *eureka, the sucker works* closures ;)

I’m waiting for a few parts (namely, buttons… incredible how difficult good looking buttons with a reasonable price tag are to get).

Might take me a bit to complete the whole thing since I have only the weekends to work on the projects but I will get back.

vpapanik:

Thanks for your excellent tutorials, VERY informative and well written !

Just a note, with the latest version of VUSB, a small change should be done in code, by adding ‘const’ here :

PROGMEM const char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH]

I am compiling fine with the latest version, with only one warning (I don’t know how to fix it) :

main.c:173: warning: passing argument 1 of ‘buildReport’ makes integer from pointer without a cast

Joonas Pihlajamaa:

buildReport wants an uchar, that line seems to be buildReport(NULL) so the compiler is thinking (quite correctly) that NULL (probably of type void *, i.e. a void pointer) is being cast into an integer.

NULL is (void *)0 so buildReport(0) should compile without issues. Using NULL is maybe not so elegant in any case.

Jonas Sam:

Hello !

my keyboard is azerty, how to convert ?

Joonas Pihlajamaa:

You can just try what happens when you send keycode for Q — does it show up as Q on screen or as an A. It might not need any changes, or then you can build a conversion table. If you are really making hardware buttons for certain keys, you’ll need to specify the code sent in any case, so I don’t really understand your problem.

Jonas Sam:

Thanks for reply.

I have a french azerty keyboard and when i put the key in my computer it shows up q instead of a.

the text “starting generation” i have to convert it in qwerty and it’s not easy with a french keyboard.

Is there a part of the code to be modified ?

is this part of the code make qwerty ?

if (ascii >= ‘A’ && ascii <= 'Z')
{
keyboard_report.keycode[0] = 4 + ascii – 'A'; // set letter

Regards

Joonas Pihlajamaa:

Seems like the operating system does the conversion from keycodes sent, assuming that your device sends “qwerty” keycodes and converting them to azerty. You can check this hypothesis by switching for a second to qwerty layout and plugging in the device.

The code you highlighted is the exact place where keycodes are calculated from ASCII values. You’d need some kind of conversion table (or just big if-set) to make the conversion. Quite painful, that’s true. :(

Yovan:

Thank you very much for writing this awesome tutorial! I learned a lot from this and the other V-USB tutorials.

Drew Read:

Woohoo! Got it working on a Digispark :)

I had to change all the pins/ports around (obviously) and define a few things in usbconfig.h:
#define USB_INTR_CFG PCMSK
#define USB_INTR_CFG_SET (1<<USB_CFG_DPLUS_BIT)
#define USB_INTR_ENABLE_BIT PCIE
#define USB_INTR_PENDING_BIT PCIF
#define USB_INTR_VECTOR SIG_PIN_CHANGE

I also enabled oscillator calibration for the internal RC clock.
(in usbconfig.h):
#define USB_CFG_HAVE_MEASURE_FRAME_LENGTH 1
#include "osccal.h"

And grabbed osccal.h, osccal.c and osctune.h from another project (part of v-usb/libs-device).

Thankyou so much for sharing this!
Now I'm one step closer to having my nrf24L01+ based wireless keyboard :D

Why:

May be one of the worst guides i have read to date.
There is no useful information here.
Trying to get my digispark to work on boot lead me here.

I was told:
1. Mark device as boot device in configuration descriptor:
And my question is.
Where the fuck is the configuration descriptor? how would i go about marking something that you don’t show where is. Nice

2. Use a HID report descriptor indicating boot support (see http://www.usb.org/developers/devclass_docs/HID1_11.pdf , Appendix B: Boot Interface
Descriptors) (Update USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH to the new size, mine is 63 bytes):

Nice so how would i go about doing that then my m8?

3. Fill a report structure for boot protocol. Send it when needed. Up to 6 at the same time. For example

Sounds awesome how do i do that?

-Thanks for nothing sounds like more of a “hey look what i can do” than a guide.
No information about how to do any of the stuff just bragging about how much greater this guy is than everybody else and a few showcases of code

Joonas Pihlajamaa:

Thanks for the comments. I agree that this article is horrible as a Digispark tutorial, this is more like a continuation article to my AVR V-USB -tutorial (see http://codeandlife.com/2012/01/25/avr-attiny-usb-tutorial-part-2/ for example) that is somewhat outdated after 6 years. If you already did an USB device with V-USB library successfully, this might make more sense.

Now I’m quite sure I cannot help you much with your issue, but just a few ideas about the questions:

1. I assume you are trying to create a USB HID keyboard, in which case “boot” and “descriptor” would refer to the data structure an USB device will send to PC about itself. Basically a data structure defined in USB HID (Human Interface Device) standard, with right values to tell the computer that OK this device is a keyboard, and it supports the boot protocol (behaves closely enough like a normal keyboard so the computer can use it upon boot).

Creating USB descriptor by hand is very tedious and involves hours of reading the USB HID specification (google it :), so V-USB has some macros to help with that. My article describes the changes to usbconfig.h (V-USB configuration file), like this:

#define USB_CFG_INTERFACE_CLASS 0x03 // HID
#define USB_CFG_INTERFACE_SUBCLASS 0x01 // Boot
#define USB_CFG_INTERFACE_PROTOCOL 0x01 // Keyboard

But as said, this article kind of makes sense if you have studied V-USB previously on your own and know what usbconfig.h is and maybe even a bit about USB standard. I wrote the article after digging into those and have forgotten most myself, so I might have trouble following it now…

2. If your m8 is some 8-bit AVR microcontroller based (ATtiny85 for example) and V-USB supports it, you can use the code in this example (or then not, V-USB might be obsolete by now and not compile on modern AVR Studio) to handle the USB nitty gritty. The full code is in http://codeandlife.com/data/usb_keydemo.zip so take a peek and maybe it helps.

I recall Adafruit had some Arduino library for USB keyboard for their Trinkets, if the Digispark is close enough, you might be able to adapt that as well.

Another alternative is more USB-friendly ATmega32u4 platform where you have LUFA library to handle all the ugly parts. They cost like $3.50 on AliExpress and are quite easy to program. See this one for a mouse example, keyboard is much the same (LUFA includes ready demos for kb as well)

http://codeandlife.com/2016/01/30/usb-mouse-with-atmega32u4-pro-micro-clone-and-lufa/

3. This filling the report is also part of the low-level USB stuff. Basically USB host sends a request to the device “give me your descriptor so I know what kind of device you are”, and you send a stream of bytes with certain values meaning a HID keyboard. V-USB documentation covers these to some extent (I think, it must be where I learned it). But this being 2018, I’ve veered off myself from really low level things, as Teensy LC, Pro Micro and other sub-10 dollar platforms exist that have native USB and there’s less of a hassle.

The Digispark is very small footprint but goes quickly difficult as you are more bare bones (I assume it’s similar to the Adafruit Trinkets I own). If you come from a background of just programming AVR chips yourself it might be a simpler solution with bootloaders and internal power handling and USB connector with required resistors etc. , but for everyone else it’s definitely a challenge. I wish you luck with your spark, hope you get it done!

Why:

a person had used your guide to succesfully get the digispark to type during boot and linked of this guide.

https://digistump.com/board/index.php?topic=1096.0

i got the step 1 and step 2 down (at least i assume so)
by modifiying
https://github.com/digistump/DigisparkArduinoIntegration/blob/master/libraries/DigisparkKeyboard/usbconfig.h
(just changed 0x03 etc etc to 3 ? isn’t that the same tho ?)

changed the usbHidReportDescriptor in
https://github.com/digistump/DigisparkArduinoIntegration/blob/8d0b0de6d208c55c77d88c09409d619309e85201/libraries/DigisparkKeyboard/DigiKeyboard.h

to what he said to and changed the byte value to what he said

however step 3

3. Fill a report structure for boot protocol. Send it when needed. Up to 6 at the same time. For example

typedef struct {
uint8_t modifier;
uint8_t reserved;
uint8_t keycode[6];
} keyboard_report_t;

am kinda lost on this.
And it appears to be from your guide.
Any input on this?
how would i go about calling this
where is this suppose to even go?

Joonas Pihlajamaa:

Ah I see, now the original question makes more sense. The Arduino Keyboard example you sent fills the structure in sendKeyStroke() function in https://github.com/digistump/DigisparkArduinoIntegration/blob/master/libraries/DigisparkKeyboard/DigiKeyboard.h — it does the sending in a really linear way, first waiting until USB interrupt is ready, then filling the report with key pressed data and using usbSetInterrupt to schedule sending the buffer, then waiting again until interrupt is ready, filling the report with zeroes (i.e. no keys pressed, otherwise PC thinks the key is forever down).

The keyboard report contains any modifiers in the “modifier” field (left shift, left alt, right shift, right alt, etc.), and keycode array contains 0-6 keys (a-z, 0-9 etc.) that are down. With one key you fill keycode[0] and leave others to zero, then keycode[1] with second key if any, and so on.

If you look into my project zip, the main.c contains similar code but is a self-contained version without the Arduino wrappers, there are same structs and I’m just sending the keypresses inside a loop in main() when a button is pressed and usb interrupt is ready. Using that example requires you to compile the files with avr-gcc and then flashing the firmware yourself instead of Arduino, though. And since my tutorial is somewhat old, V-USB library version may be outdated, and compiler might throw out some errors, I don’t have AVR chips at hand so I could test (and have limited time so maybe I’ll just write instead :).

George Foot:

Thanks for this guide, it was incredibly useful, even eight years later.

I was able to get this working on an Arduino Nano with some minor modifications:

1. Missing “const” specs in the V-USB libraries, which are required with the new compiler – I just added this everywhere the compiler was complaining

2. Update gcc and avrdude command lines in the Makefile to values appropriate for the Nano

3. Note pin assignments in the code. The V-USB snapshot included in the zip actually uses pins 2 and 3 for USB, not 2 and 1 as I’d expected from the ATtiny circuits; and it uses pins 8 and 9 for the LED and button. I changed from pin 8 (B0) to pin 13 (B5) so that the built-in LED would light up.

Other than these minor changes, it works great even on the modern PCs I’ve tried so far, including at boot-up. Nice one!

George Foot:

Following on from this, I’ve also migrated the whole thing into the Arduino IDE so that I could use the Serial module. Now one PC can send serial commands to the module and have it type strings on another PC.

Migrating into the Arduino IDE was awkward – the main things needed there, beyond what I’d already fixed above:
* Put the whole usbdrv folder in a src subdirectory, so the include paths work
* Rename all .inc files to .h and update the #include statements – .inc is not supported by Arduino IDE
* Move the main.c code into the sketch’s setup() and loop() methods
* Overlook all the signed vs unsigned truncation errors – I haven’t found a way to suppress them