The Keyboard - Part 4 : Firmware
Writing the firmware is the fun bit.
Setup
Basic operation, as in the previous post, is to ‘scan’ the keyboard matrix for keypresses. We have X rows and Y columns, each attached to a pin on the microcontroller. Some setup is needed;
//Row Column pin numbers
#define NUM_ROWS 5
#define NUM_COLS 16
//number of iterations of identical keyscan values before we trigger a keypress
#define DEBOUNCE_ITER 5
//milliseconds between each scan. SCAN_PERIOD * DEBOUNCE_ITER = minimum response time
#define SCAN_PERIOD 3
byte rowPins[NUM_ROWS] = {13,21,20,18,19};
byte colPins[NUM_COLS] = {12,11,10,9,8,7,6,5,4,3,2,1,17,16,15,14};
byte keyIterCount[NUM_ROWS][NUM_COLS];
byte keyState[NUM_ROWS][NUM_COLS];
This defines the number of rows and columns, sets up some constants around the debounce and scan behaviour, and provides two arrays, one mapping the pins on the Teensy that the rows are attached to, and another mapping the pins on the Teensy that the columns are attached to. There are also two arrays to hold the state of the matrix, specifically the iter count for each key (explained in the debounce section below) and the current stored keystate for each key, either pressed or released.
Yes this could be much more compact. The iter counts for 2 keys could be stuffed into 1 byte, and the entire keystate for each row could be 1 bitmasked int. I’ll leave the optimizations for later, when I start experimenting and start running out of the 8K memory on the Teensy.
From this point, the intitial setup is as follows:
for(int row=0; row < NUM_ROWS; row++) {
pinMode(rowPins[row], INPUT);
}
for (int col=0; col < NUM_COLS; col++) {
pinMode(colPins[col], INPUT_PULLUP);
}
//set the initial values on the iter count and state arrays.
for (int row = 0; row < NUM_ROWS; row++) {
for (int col = 0; col < NUM_COLS; col++) {
//initial iter value is debounce + 1 so that a key transition isn't immediately detected on startup.
keyIterCount[row][col] = DEBOUNCE_ITER + 1;
keyState[row][col] = KEY_RELEASED;
}
}
So the initial mode for all the Column pins is INPUT_PULLUP i.e. they’re INPUTs, and they are normally at digital 0 when not connected to anything. The PULLUP bit is meant to guarantee that, tying the pin to an internal pullup resistor in the Teensy.
The initial mode for the Row pins is INPUT which is basically the default unconfigured mode for a pin on the Teensy. It’s basically floating, and not tied to anything.
Lastly the two state arrays are initialised, the keyState to a default of KEY_RELEASED i.e. 1, or not pressed, and the keyIterCount set to an initial value of DEBOUNCE_ITER + 1. Why this is will become apparent later on.
Scanning
The scan algorithm iterates over the rows, switches each row pin in turn to an OUTPUT, pulls it LOW, then checks each column pin in turn. If the column pin is LOW then we can assume the switch is closed at the point of reading for that row and column. After iterating over all the columns, the row pin is set again to INPUT, floating it, and we move onto the next pin.
This is basically it:
//First loop runs through each of the rows,
for (int row=0; row < NUM_ROWS; row++) {
//for each row pin, set to output LOW
pinMode(rowPins[row], OUTPUT);
digitalWrite(rowPins[row], LOW);
//now iterate through each of the columns, set to input_pullup,
//the Row is output and low, and we have input pullup on the column pins,
//so a '1' is an un pressed switch, and a '0' is a pressed switch.
for (int col=0; col < NUM_COLS; col++) {
byte value = digitalRead(colPins[col]);
if(value == KEY_RELEASED) {
//do something
} else if (value == KEY_PRESSED) {
//do something else
}
}
//now just reset the pin mode (effectively disabling it)
pinMode(rowPins[row], INPUT);
}
HOWEVER things are not so simple. There’s one more fundamental problem we have to deal with, Debounce.
Debounce
I’ve gone over this a bit here but basically the upshot is that physical switches are NOISY. When you hit a switch it will boomerang around the correct value fpr some period of time, which from the POV of a digital input will look like a stream of 1’s and 0’s i.e. multiple transitions. This isn’t good.
There are a bunch of different strategies around this, hardware and software based. The Gateron switches I’m using are pretty well behaved, similar Cherry switches claim debounce times of less than 5ms, so I assumed 2x for the Gaterons wasn’t too bad.
Cherry actually publish specs for their switches online, I have searched and searched for similar for the Gateron switches but they’re nowhere to be found. If anyone has uncovered official specs I’d love to see them.
From an initial setup where, for each key the keyState is RELEASED and the keyIterCount is 0, the strategy is to: 1. Read the key value, this is the ‘current state’ of the key. 2. If it’s different to the stored state, reset the count to 0, and set it to the current state. 3. If it’s the same as the stored state, then … 1. Compare the iter count to our configured max DEBOUNCE_ITER 2. If it’s LESS then just increment it and continue. 3. If it’s EQUAL then increment it once more and trigger a key transistion event, this can be either a key pressed or key released depending.
Upshot is that a Key must be in the same state for at least DEBOUNCE_ITER scans before it will register as a press or release. Each scan happens every SCAN_PERIOD ms, so the amount of time before a key transition is registered in an ideal case is DEBOUNCE_ITER x SCAN_PERIOD, or 15ms in our case currently.
This leaves our inner loop looking like this:
if(value == KEY_PRESSED && keyState[row][col] == KEY_RELEASED) {
keyState[row][col] = KEY_PRESSED;
keyIterCount[row][col] = 0;
} else if (value == KEY_RELEASED && keyState[row][col] == KEY_PRESSED) {
keyState[row][col] = KEY_RELEASED;
keyIterCount[row][col] = 0;
} else {
//Stored value is the same as the current value, this is where our debounce magic happens.
//if the keyIterCount < debounce iter then increment the keyIterCount and move on
//if it's == debounce iter then trigger the key & increment it so the trigger doesn't happen again.
//if it's > debounce iter then we do nothing, except check for the FN key being pressed.
if(keyIterCount[row][col] < DEBOUNCE_ITER) {
keyIterCount[row][col] ++;
} else if (keyIterCount[row][col] == DEBOUNCE_ITER) {
//increment this once more so we don't hit the DEBOUNCE_ITER value
//again next loop, BUT we don't want it incrementing nonstop, it's only a wee byte.
keyIterCount[row][col] ++;
transitionHandler(keyState[row][col], row, col);
}
}
With the ‘transitionHandler being called with the current keyState, and the row and column index of the key we’re currently scanning. It calls either the ‘press’ or ‘release’ methods for the specific scancode on the Keyboard object. This directly sends a HID event out on the USB wire to the host machine.
//we have a debounced key transition event, either pressed or released.
void transitionHandler(int state, int row, int col) {
//pick which keyMap we're using based on whether we're in func mode
int scanCode = keyMap[row][col];
if(state == KEY_PRESSED) {
Keyboard.press(scanCode);
} else if (state == KEY_RELEASED) {
Keyboard.release(scanCode);
}
}
Where to get the scanCode ? That’s where the keyMap (or maps) come in.
Keymaps
There are a long list of codes corresponding to different keys in the USB HID spec. These are sent along with the relevent state (pressed/released) as part of the HID event sent to the host. Here’s the HID spec in all it’s gory detail if you’re interested, keycodes on page 81.
There are … wrinkles though, of course. I.E. ‘KEY_2’ is the constant in the code for the ‘2’ key as you’d expect. However, what happens if any of the modifiers are pressed (SHIFT for example) when you hit the key depends on what the OS wants to do with it, which is normally set using some sort of locale or keyboard layout. I.E. on my UK ISO board SHIFT + 2 is “ whereas on a US ANSI board SHIFT + 2 is @
We define a Key Map defining, for every row and column, the corresponding scan code to send via USB. Some complications, we need some definition for no key, our Fn key (which isn’t intended ever to be sent on the wire), and some extra #defines for two UK ISO specific keys that aren’t defined in the library I’m using.
#define KEY_FUNCTION -1
#define NOK 0
#define KC_NONUS_BACKSLASH ( 100 | 0xF000 )
#define KC_NONUS_HASH ( 50 | 0xF000 )
We need a code for every row/column as below, mapped literally from the wiring connecting the switches to the pins. There are gaps of NOK’s obviously, the last row for example has no key on a bunch of the columns.
int keyMap[NUM_ROWS][NUM_COLS] = {
{KEY_ESC,KEY_1,KEY_2,KEY_3,KEY_4,KEY_5,KEY_6,KEY_7,KEY_8,KEY_9,KEY_0,KEY_MINUS,KEY_EQUAL,KEY_BACKSPACE,KEY_INSERT,KEY_HOME},
{KEY_TAB,KEY_Q,KEY_W,KEY_E,KEY_R,KEY_T,KEY_Y,KEY_U,KEY_I,KEY_O,KEY_P,KEY_LEFT_BRACE,KEY_RIGHT_BRACE,NOK,KEY_DELETE,KEY_END},
{KEY_CAPS_LOCK,KEY_A,KEY_S,KEY_D,KEY_F,KEY_G,KEY_H,KEY_J,KEY_K,KEY_L,KEY_SEMICOLON,KEY_QUOTE,KC_NONUS_HASH,KEY_ENTER,KEY_FUNCTION,KEY_PAGE_UP},
{MODIFIERKEY_SHIFT,KC_NONUS_BACKSLASH,KEY_Z,KEY_X,KEY_C,KEY_V,KEY_B,KEY_N,KEY_M,KEY_COMMA,KEY_PERIOD,KEY_SLASH,MODIFIERKEY_RIGHT_SHIFT,NOK,KEY_UP,KEY_PAGE_DOWN},
{MODIFIERKEY_CTRL,MODIFIERKEY_GUI,MODIFIERKEY_ALT,NOK,NOK,NOK,KEY_SPACE,NOK,NOK,NOK,MODIFIERKEY_RIGHT_ALT,KEY_FUNCTION,MODIFIERKEY_RIGHT_CTRL,KEY_LEFT,KEY_DOWN,KEY_RIGHT}
};
Putting it all together
Put it all together annnd, that’s pretty much it, a functional (if basic) Keyboard implementation. The specific revision here: https://github.com/dairequinlan/the-keyboard/blob/494dd48f6a87dd398ca0f11eaf12c4beeb77e66a/boardware.ino
… is pretty much the above, with one important addendum, the addition of a second keyMap which is actuated when the ‘Fn’ or ‘Function’ key is pressed.
int funcKeyMap[NUM_ROWS][NUM_COLS] = {
{KEY_TILDE,KEY_F1,KEY_F2,KEY_F3,KEY_F4,KEY_F5,KEY_F6,KEY_F7,KEY_F8,KEY_F9,KEY_F10,KEY_F11,KEY_F12,KEY_BACKSPACE,KEY_PRINTSCREEN,KEY_SCROLL_LOCK},
{KEY_TAB,NOK,KEY_UP,NOK,NOK,NOK,NOK,KEY_INSERT,KEY_HOME,KEY_PAGE_UP,NOK,NOK,NOK,NOK,KEY_DELETE,KEY_END},
{NOK,KEY_LEFT,KEY_DOWN,KEY_RIGHT,NOK,NOK,NOK,KEY_DELETE,KEY_END,KEY_PAGE_DOWN,NOK,NOK,NOK,KEY_ENTER,KEY_FUNCTION,KEY_PAGE_UP},
{MODIFIERKEY_SHIFT,NOK,NOK,NOK,NOK,NOK,NOK,NOK,NOK,NOK,NOK,NOK,MODIFIERKEY_RIGHT_SHIFT,NOK,KEY_UP,KEY_PAGE_DOWN},
{MODIFIERKEY_CTRL,MODIFIERKEY_GUI,MODIFIERKEY_ALT,NOK,NOK,NOK,KEY_SPACE,NOK,NOK,NOK,MODIFIERKEY_RIGHT_ALT,KEY_FUNCTION,MODIFIERKEY_RIGHT_CTRL,KEY_LEFT,KEY_DOWN,KEY_RIGHT}
};
This, when any of the Function keys are pressed, maps ‘Esc’ to the ‘~’ key, the number keys and ‘-’, ‘=’ to F1 -> F12.
It also maps Fn + WASD to Cursors, FN + UJIKOL to the RHS Nav. Cluster, this is a bit of an experiment to see if it’ll work satisfactorily, if so a 60% layout is next in line.
Future work.
Part of the fun in writing your own firmware is messing about, currently I’m adding in support for sets of ‘sticky’ keys actuated by a quick double press. I.E. no need for caps lock if the left Shift can be ‘stickified’ with a quick double click. Similarly making the Function key sticky is a help if you’re using the navigation keys on the Alphas.
Medium term plan is a split Ergo Keyboard, communicating over I2C, Nav clusters on Alphas, so this Keyboard partly a plan to see if this all works out …