La fabrique mobile Viseo Toulouse

Arduino Leonardo fully-featured keyboard

The Leonardo has a simple keyboard API. I needed a way to emulate a keyboard (from a joystick and arcade buttons - you see where I'm going now). Here's how I did it.

First try

Starting with an Arduino sample, we can make a first attempt. The circuit is the same as the sample - simply adjust the pins to your needs. It won't need any change until the end of this post.

// Basic keyboard
const int upButton = 2;    
const int downButton = 3;        
const int leftButton = 4;
const int rightButton = 5;

void setup() {
    pinMode(upButton, INPUT);      
    pinMode(downButton, INPUT);      
    pinMode(leftButton, INPUT);      
    pinMode(rightButton, INPUT);      
    pinMode(mouseButton, INPUT);
   
    Keyboard.begin();
}

void loop() {
    if (digitalRead(upButton) == HIGH) {
        Keyboard.write(KEY_UP_ARROW);
    }
    if (digitalRead(downButton) == HIGH) {
        Keyboard.write(KEY_DOWN_ARROW);
    }
    if (digitalRead(leftButton) == HIGH) {
        Keyboard.write(KEY_LEFT_ARROW);
    }
    if (digitalRead(rightButton) == HIGH) {
        Keyboard.write(KEY_RIGHT_ARROW);
    }
}

This has a major issue however. Each Keyboard.write() call generates a press/release cycle. If you keep a button pushed, instead of a single, long key press, the computer will receive a ton of press/release events. We need to keep the buttons states between loop() calls.

Adding memory to the keyboard

Here's a second attempt, with two modifications. First, to ease the addition/removal of a button, the code uses arrays instead of doing all steps four times. Second thing changed: each button now remember its state.

// Stateful keyboard
// Number of buttons to handle
const int buttonsCount = 4;

// Arduino PINs to use
const int pins[buttonsCount] = {
    2,
    3,
    4,
    5
};

// Keys to send (order has to match the pins array)
const byte keys[buttonsCount] = {
    KEY_UP_ARROW,
    KEY_DOWN_ARROW,
    KEY_LEFT_ARROW,
    KEY_RIGHT_ARROW
};

bool status[buttonsCount] = {LOW};

void setup() {
    for (int i = 0; i < buttonsCount; ++i) {
        pinMode(pins[i], INPUT);
    }
    
    Keyboard.begin();
}

void loop() {
    for (int i = 0; i < buttonsCount; ++i) {
        const int pinStatus = digitalRead(pins[i]);
        if (pinStatus != status[i]) {
            status[i] = pinStatus;
            if (pinStatus == HIGH) {
                Keyboard.press(keys[i]);
            } else {
                Keyboard.release(keys[i]);
            }
        }
    }
}

So… the keyboard now remembers which buttons are pressed, and should generate a single couple of events for each button press/release. Should. There's still an issue: mechanical buttons are not perfect. Many events are still generated. This is due to a phenomenon called bounce.

Debouncing the keyboard

A simple way to debounce a button is, well, really simple: ignore all changes to the state of the button during a short delay after an initial change. While it's not the most precise way and could be problematic in a more complex scenario, it's perfectly fine to do this for a keyboard, given we keep this delay short enough.

Let's throw in an array to remember the last event acknowledged by the keyboard:

// Debounced keyboard
// Number of buttons to handle
const int buttonsCount = 4;

// Arduino PINs to use
const int pins[buttonsCount] = {
    2,
    3,
    4,
    5
};

// Keys to send (order has to match the pins array)
const byte keys[buttonsCount] = {
    KEY_UP_ARROW,
    KEY_DOWN_ARROW,
    KEY_LEFT_ARROW,
    KEY_RIGHT_ARROW
};

// Debounce delay
const long debounceDelay = 50;

bool status[buttonsCount] = {LOW};
long lastDebounces[buttonsCount] = {0};

void setup() {
    for (int i = 0; i < buttonsCount; ++i) {
        pinMode(pins[i], INPUT);
    }
    
    Keyboard.begin();
}

void loop() {
    for (int i = 0; i < buttonsCount; ++i) {
        const int pinStatus = digitalRead(pins[i]);
        if (pinStatus != status[i] && millis() - debounceDelay > lastDebounces[i]) {
            status[i] = pinStatus;
            if (pinStatus == HIGH) {
                Keyboard.press(keys[i]);
            } else {
                Keyboard.release(keys[i]);
            }
            lastDebounces[buttonNumber] = millis();
        }
    }
}

You'll maybe need to adjust the debounce delay according to your buttons. Try to keep it as short as possible.

Conclusion

And voilà! We now have a fully functional keyboard, to which it's easy to add/remove/change buttons. There's still room for improvement: it would be easy to allow it to send key sequences instead of single key presses, for example.

You can find the full code on GitHub.

Tagged with: