Embedded HandbookApps
All sections › III. Peripherals
STM32F072, STM32F401, STM32F411 · level intermediate · 36 min read
Open in the interactive handbook

20г. A keyboard on STM32: dissecting QMK — matrix, debounce, layers and USB HID

In section 20в the keyboard was a teaching one: a report descriptor, eight bytes, sending from a loop. A real keyboard needs more. It has to scan a hundred keys, filter out bounce without losing fast typing, hold several keymap layers, tell a short press from a hold and deliver to the host within milliseconds.

Here we dissect the open-source QMK firmware: its tree has 3562 keyboard descriptions with a specified microcontroller, 784 of them on STM32, most often the F072, F401 and F411. We follow the path of one key press: matrix, debounce, layers, report, descriptor, USB endpoint. The real QMK files were built by gcc on the computer and run against a model of a bouncing contact and a sequence of key presses; their numbers are in the tables below. Next to each piece of QMK is our own code for the STM32F401 in three files (kbd.h, kbd.c, kbd_hw_f401.c): matrix scanning with registers, debounce, layers, reports and descriptors. It can be copied in full and built.

Sources: the QMK sources (github.com/qmk/qmk_firmware, version 0.34.5, snapshot b1aea25); the paths below are given from the repository root. The matrix and false presses without diodes are covered in section 22.5, input pull-ups in section 8, the report descriptor language and the boot keyboard in section 20в, the system bootloader and reflashing in sections 11 and 48. Our own code is built with the CMSIS headers for the STM32F401xC from the STM32F4xx_DFP 2.17.1 pack.

Licence: QMK is distributed under GPL-2.0 or later (the file headers, the text is in LICENSE); usb_descriptor.c additionally carries the LUFA permissive notice. The code fragments below are short quotations with the file named; copies of the needed files lie next to the check together with the licence text.

What it is called in the documentation

Original Meaning
matrix scan scanning the key matrix
diode direction: COL2ROW, ROW2COL diode direction: from column to row, from row to column
ghosting a false ("ghost") key press
debounce: defer, eager debounce: deferred (wait for quiet), eager (accept and stop listening)
global, per-row, per-key one for the whole matrix, per row, per key
keymap, layer the key layout, a layer
transparent key (KC_TRNS) a transparent key: look at the layer below
momentary, toggle, layer-tap a layer while held, a toggle, a layer when held and a key when tapped
tapping term the "tap or hold" threshold
6KRO, NKRO six simultaneous keys; any number of keys
shared endpoint one endpoint for several numbered reports
bootloader jump jumping into the bootloader from the firmware

20г.1 The matrix: drive a row low, read the columns

The keys sit at the nodes of a grid, as in section 22.5. QMK knows two diode directions. With COL2ROW the firmware pulls one row at a time to ground and reads the columns, with ROW2COL it is the other way round. Everything else is the same. Here is the reading of one row from quantum/matrix.c:

    matrix_row_t row_shifter = MATRIX_ROW_SHIFTER;
    for (uint8_t col_index = 0; col_index < MATRIX_COLS; col_index++, row_shifter <<= 1) {
        uint8_t pin_state = readMatrixPin(col_pins[col_index]);

        // Populate the matrix row with the state of the col pin
        current_row_value |= pin_state ? 0 : row_shifter;
    }

    // Unselect row
    unselect_row(current_row);
    matrix_output_unselect_delay(current_row, current_row_value != 0); // wait for all Col signals to go HIGH

Three details. The columns are inputs with pull-ups, and a pressed key reads as zero. QMK does not hold a released row high but switches it to an input with a pull-up (gpio_set_pin_input_high): two keys in one column will not short an output-zero to an output-one. Holding it high is possible, but only explicitly — with the MATRIX_UNSELECT_DRIVE_HIGH macro. And after each row the firmware waits: matrix_output_unselect_delay by default calls wait_us(MATRIX_IO_DELAY), and MATRIX_IO_DELAY is 30 µs (quantum/matrix_common.c). The columns must have time to be pulled up to one, otherwise the next row reads the tail of the previous one. A matrix of six rows therefore costs at least 180 µs per pass.

Diodes are needed for the same reason as in section 22.5: three pressed keys at the corners of a rectangle without diodes "light up" the fourth. DIODE_DIRECTION in the keyboard description tells the firmware which way the cathodes face; a mistake in it gives a matrix that does not see a single key press. There is also a matrix without a grid: DIRECT_PINS, each key on its own pin.

Our own scan on the F401. The same scheme with registers — for an STM32F401 board (like blackpill_f401 in QMK) with a 4 × 8 matrix: rows PB12…PB15, columns PA0…PA7, diodes from column to row. The common header of our keyboard, kbd.h:

/* Раздел 20г: своя клавиатура по мотивам QMK — общая часть без железа.
 * Матрица 4 × 8, антидребезг «на клавишу, ждать тишины», два слоя,
 * отчёты 6KRO (8 байт) и NKRO (32 байта), дескрипторы отчётов. */
#ifndef KBD_H
#define KBD_H
#include <stdint.h>

#define KBD_ROWS 4
#define KBD_COLS 8
#define KBD_DEBOUNCE_MS 5u          /* столько миллисекунд клавиша должна молчать */
#define KBD_NKRO_BYTES 30           /* коды 0x00…0xEF, как NKRO_REPORT_BITS в QMK */
#define KBD_NKRO_ID 6

/* коды клавиш: 0x04…0xE7 — коды HID, выше — свои служебные */
#define K_NO   0x0000
#define K_TRNS 0x0001               /* прозрачная: смотреть слой ниже */
#define K_MO1  0x0100               /* слой 1, пока нажата */

typedef uint8_t kbd_row_t;          /* бит c — столбец c */

typedef struct {
    uint8_t mods, reserved, keys[6];
} kbd_boot_report_t;

typedef struct {
    uint8_t id, mods, bits[KBD_NKRO_BYTES];
} kbd_nkro_report_t;

extern const uint16_t kbd_keymap[2][KBD_ROWS][KBD_COLS];
extern const uint8_t kbd_boot_desc[];
extern const uint16_t kbd_boot_desc_len;
extern const uint8_t kbd_nkro_desc[];
extern const uint16_t kbd_nkro_desc_len;

void kbd_debounce(const kbd_row_t raw[KBD_ROWS], kbd_row_t out[KBD_ROWS], uint32_t now_ms);
uint16_t kbd_keycode(const kbd_row_t m[KBD_ROWS], uint8_t row, uint8_t col);
uint8_t kbd_build_boot(const kbd_row_t m[KBD_ROWS], kbd_boot_report_t *r);
void kbd_build_nkro(const kbd_row_t m[KBD_ROWS], kbd_nkro_report_t *r);
#endif

The scan (kbd_hw_f401.c). The selected row is an output with a zero: the zero is written to ODR in advance, and only the mode in MODER is switched. The 30 µs pause is counted with the DWT cycle counter:

#define ROW_PIN0 12u                    /* PB12 — строка 0 */
#define COL_MASK 0xFFu                  /* PA0…PA7 */
#define UNSELECT_US 30u                 /* как MATRIX_IO_DELAY в QMK */

static void delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT, ticks = us * (SystemCoreClock / 1000000u);
    while (DWT->CYCCNT - start < ticks) {
    }
}

void kbd_hw_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN;
    (void)RCC->AHB1ENR;                                  /* такт дошёл до портов */
    GPIOA->MODER &= ~0x0000FFFFu;                        /* PA0…PA7 — входы */
    GPIOA->PUPDR = (GPIOA->PUPDR & ~0x0000FFFFu) | 0x00005555u;   /* подтяжка вверх */
    GPIOB->MODER &= ~0xFF000000u;                        /* PB12…PB15 — входы */
    GPIOB->PUPDR = (GPIOB->PUPDR & ~0xFF000000u) | 0x55000000u;
    GPIOB->BSRR = 0xFu << (ROW_PIN0 + 16u);              /* в ODR заранее нули */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;      /* счётчик тактов для delay_us */
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

/* Один проход матрицы: строку в выход с нулём, прочитать столбцы,
 * вернуть строку во вход и дать столбцам подтянуться. */
void kbd_scan(kbd_row_t raw[KBD_ROWS])
{
    for (uint32_t r = 0; r < KBD_ROWS; r++) {
        uint32_t shift = 2u * (ROW_PIN0 + r);
        GPIOB->MODER |= 1u << shift;                     /* выход, в ODR ноль */
        __DSB();
        delay_us(1);
        raw[r] = (kbd_row_t)(~GPIOA->IDR & COL_MASK);    /* ноль = нажата */
        GPIOB->MODER &= ~(3u << shift);                  /* снова вход с подтяжкой */
        delay_us(UNSELECT_US);
    }
}

After a pass matrix_scan() compares the new snapshot with the old one (memcmp) and passes it to the debounce together with a "something changed" flag. From there the key press goes along the chain in the figure.

The path of a key press in QMK from left to right: the matrix — a row driven low, a 30 µs wait after each row; the sym_defer_g debounce — 5 ms of quiet, or the eager variant; the keymap and layers — top to bottom, a transparent key looks below, TAPPING_TERM 200 ms; the HID report — 6KRO 8 bytes or NKRO 32 bytes; the IN endpoint — the host polls once every 1 ms. At the bottom, the model latencies at a 0.25 ms scan and 3 ms bounce: sym_defer_g 8.06 ms on average and up to 9.25 ms, the eager algorithms 0.76 ms and up to 2.74 ms; the eager ones accept a 0.5 ms noise pulse in 1000 trials out of 1000, the deferred ones in none


20г.2 Debounce: seven algorithms and their cost

The algorithm is chosen by the DEBOUNCE_TYPE line in the keyboard rules, the default is sym_defer_g (builddefs/common_features.mk). The time is the DEBOUNCE macro, 5 ms by default in all six files of quantum/debounce/; the seventh, none.c, filters nothing. The names are read in parts: sym — the same for press and release, asym — different; defer — wait until the contact settles, eager — accept the first change at once and then ignore the key for DEBOUNCE ms; g, pr, pk — one timer for the whole matrix, per row, per key.

The simplest and most common one is the global deferred one (quantum/debounce/sym_defer_g.c):

    if (changed) {
        debouncing      = true;
        debouncing_time = timer_read_fast();
    } else if (debouncing && timer_elapsed_fast(debouncing_time) >= DEBOUNCE) {
        size_t matrix_size = MATRIX_ROWS_PER_HAND * sizeof(matrix_row_t);
        if (memcmp(cooked, raw, matrix_size) != 0) {
            memcpy(cooked, raw, matrix_size);
            cooked_changed = true;
        }
        debouncing = false;
    }

Any change anywhere in the matrix restarts the timer; the snapshot goes further only when the whole matrix has been quiet for DEBOUNCE ms.

The model. The real algorithm files were built by gcc together with a timer stub (a millisecond timer_read32, as on ChibiOS) and a loop that repeats matrix_scan(). A 2 × 2 matrix: A and B in one row, C in the other. The contact bounces on press and release: a window of a given length, with switching inside at random intervals of 20…300 µs. The scan runs once every 0.25 ms (the QMK tests themselves run the algorithms "from 1kHz to 10kHz scan rate" — quantum/debounce/tests/debounce_test_common.cpp), 1000 trials per row. The latency is from the first touch to the change at the output. DEBOUNCE = 5:

Algorithm Press, ms: average / max Release, ms Extra presses: bounce 3 / 8 ms 0.5 ms noise — false A when typing A+B / A+C, ms
none 0.26 / 1.74 0.24 / 1.83 6839 / 18752 1000 0.24 / 0.23
sym_defer_g 7.56 / 8.25 7.55 / 8.25 0 / 0 0 9.54 / 9.55
sym_defer_pr 7.56 / 8.25 7.55 / 8.25 0 / 0 0 9.54 / 9.55
sym_defer_pk 7.56 / 8.25 7.55 / 8.25 0 / 0 0 7.55 / 7.55
sym_eager_pk 0.26 / 1.74 0.24 / 1.83 0 / 2000 1000 0.24 / 0.23
sym_eager_pr 0.26 / 1.74 0.24 / 1.83 0 / 2000 1000 0.24 / 0.23
asym_eager_defer_pk 0.26 / 1.74 7.55 / 8.25 0 / 0 1000 0.24 / 0.23
our own kbd_debounce 7.56 / 8.25 7.55 / 8.25 0 / 0 0 7.55 / 7.55

The press and release latencies are for a 3 ms bounce window; "A+B / A+C" is the latency of A if B (the same row) or C (another row) is pressed 2 ms after it, both with a 3 ms bounce.

What you can see:

Our own debounce (kbd.c) is per key and waits for quiet, like sym_defer_pk, but shorter: each key has a byte with the moment of its last change. In the same model it gave exactly the same numbers as sym_defer_pk (the last row of the table):

/* Антидребезг на клавишу с ожиданием тишины: изменение сырого бита
 * запоминает время; выход меняется, когда бит отличается от выхода
 * и не менялся KBD_DEBOUNCE_MS миллисекунд. */
void kbd_debounce(const kbd_row_t raw[KBD_ROWS], kbd_row_t out[KBD_ROWS], uint32_t now_ms)
{
    static kbd_row_t last[KBD_ROWS];
    static uint8_t stamp[KBD_ROWS][KBD_COLS];
    for (uint8_t r = 0; r < KBD_ROWS; r++) {
        kbd_row_t moved = (kbd_row_t)(raw[r] ^ last[r]);
        kbd_row_t differ = (kbd_row_t)(raw[r] ^ out[r]);
        last[r] = raw[r];
        for (uint8_t c = 0; c < KBD_COLS; c++) {
            kbd_row_t bit = (kbd_row_t)(1u << c);
            if (moved & bit)
                stamp[r][c] = (uint8_t)now_ms;
            else if ((differ & bit) && (uint8_t)((uint8_t)now_ms - stamp[r][c]) >= KBD_DEBOUNCE_MS)
                out[r] ^= bit;
        }
    }
}

The difference in uint8_t survives the wraparound of the millisecond count: the output changes after 5 ms of quiet, long before 256 ms wrap around.

What value to set. The same run for other DEBOUNCE values, scan 0.25 ms:

Algorithm, DEBOUNCE Press without bounce, ms: min / max Extra presses: bounce 1 / 3 / 8 ms 0.5 ms noise — false
sym_defer_g, 1 0.25 / 1.24 0 / 166 / 1104 256
sym_defer_g, 5 4.25 / 5.24 0 / 0 / 0 0
sym_defer_g, 10 9.25 / 10.24 0 / 0 / 0 0
sym_eager_pk, 1 0.00 / 0.25 924 / 2832 / 7751 1000
sym_eager_pk, 5 0.00 / 0.25 0 / 0 / 2000 1000
sym_eager_pk, 10 0.00 / 0.25 0 / 0 / 0 1000

The timer counts milliseconds, so "DEBOUNCE ms of quiet" is in fact from DEBOUNCE − 1 to DEBOUNCE ms plus the scan period: 4.25…5.24 ms at 5, and at 1 — from 0.25 ms, and half of the 0.5 ms noise pulses get through (256 out of 1000). The rule is simple: DEBOUNCE must be longer than the longest bounce with a margin of a millisecond. With a scan once every 1 ms an eager algorithm catches a 0.5 ms noise pulse in 500 trials out of 1000 — exactly in those where the pulse covered the scan moment.

What this costs in memory is shown by a build for Cortex-M0 (F072) for a 5 × 15 = 75 key matrix: sym_defer_g — 84 bytes of code and 8 bytes of RAM, sym_defer_pk — 316 and 83 (a counter byte per key), sym_defer_pr — 228 and 13, asym_eager_defer_pk — 456 and 83. The difference is hundreds of bytes of code and counters in RAM; choose the algorithm by its behaviour, not by its size.


20г.3 Keymap and layers

The keymap is an array of codes keymaps[layer][row][column]. The layers lie in a stack: the enabled ones are marked by bits in layer_state (16 bits by default, MAX_LAYER = 16, quantum/action_layer.h), the bottom one is default_layer_state. For each key the firmware searches from top to bottom for the first enabled layer where the code at this position is not transparent (quantum/action_layer.c):

    layer_state_t layers = layer_state | default_layer_state;
    /* check top layer first */
    for (int8_t i = MAX_LAYER - 1; i >= 0; i--) {
        if (layers & ((layer_state_t)1 << i)) {
            action = action_for_key(i, key);
            if (action.code != ACTION_TRANSPARENT) {
                return i;
            }
        }
    }

The transparent code KC_TRNS (0x0001) is a hole in a layer: look below. This way a number layer can define only the top row and take the rest from the base layer. The layer is remembered at the moment of the press (store_or_get_action, unless STRICT_LAYER_RELEASE is set): if the layer is turned off while the key is held, the release goes to the same layer, and the modifier does not get stuck.

In our own keyboard the same thing is shorter: a two-layer keymap, the K_MO1 key enables layer 1 while held, K_TRNS passes down (kbd.c):

/* Раскладка: слой 0 — буквы, слой 1 — цифры в первом ряду, остальное прозрачно. */
const uint16_t kbd_keymap[2][KBD_ROWS][KBD_COLS] = {
    {{0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B},   /* A B C D E F G H */
     {0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},   /* I J K L M N O P */
     {0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B},   /* Q R S T U V W X */
     {0xE1, 0xE0, K_MO1, 0x2C, 0x28, 0x2A, 0x1C, 0x1D}}, /* LShift LCtrl MO(1) Space Enter Bksp Y Z */
    {{0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25},   /* 1 … 8 */
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS},
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS},
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS}},
};

The code lookup goes from top to bottom, as in QMK:

/* Код клавиши с учётом слоёв: слой 1 включён, пока нажата K_MO1;
 * ищем сверху вниз первый непрозрачный код. */
uint16_t kbd_keycode(const kbd_row_t m[KBD_ROWS], uint8_t row, uint8_t col)
{
    uint8_t top = 0;
    for (uint8_t r = 0; r < KBD_ROWS; r++)
        for (uint8_t c = 0; c < KBD_COLS; c++)
            if ((m[r] >> c & 1u) && kbd_keymap[0][r][c] == K_MO1)
                top = 1;
    for (int8_t layer = (int8_t)top; layer >= 0; layer--) {
        uint16_t k = kbd_keymap[layer][row][col];
        if (k != K_TRNS)
            return k;
    }
    return K_NO;
}

A run on the PC: without K_MO1 key (0,0) gives 0x0004 (A), with it — 0x001E (digit 1); key (1,0) is transparent on layer 1 and gives 0x000C (I) in both cases.

In QMK layers are controlled by key codes — 16-bit numbers from quantum/keycodes.h and quantum/quantum_keycodes.h; their values were printed by a program built with these headers:

Code Value What it does
MO(1) 0x5221 layer 1 while the key is held
TG(2) 0x5262 toggles layer 2 on or off
TO(0) 0x5200 leaves only layer 0
LT(1, KC_SPACE) 0x412C tap — space, hold — layer 1
MT(MOD_LCTL, KC_ESCAPE) 0x2129 tap — Esc, hold — left Ctrl

In LT 4 bits are allotted to the layer number: LT(15, KC_SPACE) is 0x4F2C, and LT(16, KC_SPACE) is 0x402C, that is, silently LT(0, KC_SPACE). MO and TG take 5 bits.

Tap or hold. A key with two roles (LT, MT) waits. Released before the TAPPING_TERM threshold — it is a tap, and a space goes into the report; held longer — it is a hold, and the layer or the modifier is enabled. The default threshold is 200 ms (quantum/action_tapping.h). The cost is latency: the space on such a key appears not on the press but on the release. The options PERMISSIVE_HOLD, HOLD_ON_OTHER_KEY_PRESS and others decide what to do if a second key is pressed during the wait; the model of this section did not run them.


20г.4 The report: six keys or all of them

The key codes from the keymap are assembled into a report by the functions of tmk_core/protocol/report.c. The ordinary report is the boot keyboard from section 20в: a modifier byte, a reserved byte, six codes, 8 bytes (sizeof(report_keyboard_t) = 8, KEYBOARD_REPORT_KEYS = 6). A new code is put by add_key_byte into the first empty slot; the loop before that checks whether the code is already in the report and remembers an empty slot in empty. The end of the function:

    if (i == KEYBOARD_REPORT_KEYS) {
        if (empty != -1) {
            keyboard_report->keys[empty] = code;
        }
    }
}

If there is no empty slot, the code simply does not get into the report. The real report.c, built on the computer, on pressing A…H in a row:

+F         00 00 04 05 06 07 08 09 | keys=6
+G         00 00 04 05 06 07 08 09 | keys=6
+H         00 00 04 05 06 07 08 09 | keys=6
G pressed=0 H pressed=0
-A         00 00 00 05 06 07 08 09 | keys=5
after -A: G pressed=0

The seventh and eighth keys were lost. Worse: after A was released the slot became free, but G, which is still held, never returned to the report — a code is added only at the moment of the press.

Our own report assembly works differently: the report is built anew each time from the whole matrix, not edited by events (kbd.c):

/* Отчёт 6KRO собирается заново из всей матрицы: освободилось место —
 * в него встаёт следующая удерживаемая клавиша. Возвращает число
 * нажатых обычных клавиш (больше 6 — часть не поместилась). */
uint8_t kbd_build_boot(const kbd_row_t m[KBD_ROWS], kbd_boot_report_t *r)
{
    uint8_t n = 0;
    memset(r, 0, sizeof *r);
    for (uint8_t row = 0; row < KBD_ROWS; row++)
        for (uint8_t col = 0; col < KBD_COLS; col++) {
            if (!(m[row] >> col & 1u))
                continue;
            uint16_t k = kbd_keycode(m, row, col);
            if (k >= 0xE0 && k <= 0xE7)
                r->mods |= (uint8_t)(1u << (k - 0xE0));
            else if (k >= 0x04 && k < 0xE0) {
                if (n < 6)
                    r->keys[n] = (uint8_t)k;
                n++;
            }
        }
    return n;
}

The same experiment with pressing A…H, then releasing A:

+F         00 00 04 05 06 07 08 09 | pressed=6
+G         00 00 04 05 06 07 08 09 | pressed=7
+H         00 00 04 05 06 07 08 09 | pressed=8
-A         00 00 05 06 07 08 09 0A | pressed=7

As soon as A is released, G (0x0A) took the freed slot: it is still held, and the report is assembled from the matrix. H is still not visible — there are only six slots anyway.

NKRO removes the limit. The report is a bitmap: number 6 (REPORT_ID_NKRO), a modifier byte and NKRO_REPORT_BITS = 30 bytes, one bit per code 0x00…0xEF; sizeof(report_nkro_t) = 32. Code 0x04 (A) is bit 4 of the first byte of the map. The same run with NKRO_ENABLE:

+H         06 00 F0 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | keys=2
G pressed=1 H pressed=1
-A         06 00 E0 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | keys=2

All eight are there: 0xF0 — A…D, 0x0F — E…H. Our own function gives the same bytes for the same eight keys:

/* Отчёт NKRO: номер, модификаторы и по биту на код 0x00…0xEF. */
void kbd_build_nkro(const kbd_row_t m[KBD_ROWS], kbd_nkro_report_t *r)
{
    memset(r, 0, sizeof *r);
    r->id = KBD_NKRO_ID;
    for (uint8_t row = 0; row < KBD_ROWS; row++)
        for (uint8_t col = 0; col < KBD_COLS; col++) {
            if (!(m[row] >> col & 1u))
                continue;
            uint16_t k = kbd_keycode(m, row, col);
            if (k >= 0xE0 && k <= 0xE7)
                r->mods |= (uint8_t)(1u << (k - 0xE0));
            else if (k >= 0x04 && k < KBD_NKRO_BYTES * 8)
                r->bits[k >> 3] |= (uint8_t)(1u << (k & 7));
        }
}

A small detail is visible in the QMK output: has_anykey() in NKRO mode counts non-zero bytes, not keys, hence "keys=2" with eight keys pressed. As a logical "something is pressed" flag it works correctly.

NKRO does not always turn on. host_can_send_nkro() (tmk_core/protocol/host.c) requires the host to switch the device to the report protocol; a host that understands only a boot keyboard gets the ordinary 8 bytes. There is also a flag in the settings: NKRO_DEFAULT_ON is false by default (quantum/eeconfig.c), and it is toggled with the NK_TOGG key.


20г.5 The descriptor and endpoints: what the host sees

The QMK report descriptors are written with the LUFA HID_RI_* macros in tmk_core/protocol/usb_descriptor.c. The LUFA submodule in the snapshot is empty, so the macros were rewritten from the item tags in Zephyr's hid.h (as in section 20в), and the descriptor lines themselves were cut out of the file without changes and built by gcc. Our own Python parser read the bytes:

Descriptor Length Items Input Output
KeyboardReport 68 bytes 34 8 × 1 modifier bit, 1 × 8 reserved, 6 × 8 codes = 8 bytes 5 LED bits + 3 padding bits = 1 byte
NKRO in SharedReport 59 bytes 30 number 6, 8 × 1 bit, 240 × 1 bit = 32 bytes number 6 + 1 byte = 2 bytes

Our own descriptors are ordinary byte arrays with the parse in the comments; the boot one in full (kbd.c):

/* Дескриптор загрузочной клавиатуры: вход 8 байт, выход 1 байт (светодиоды). */
const uint8_t kbd_boot_desc[] = {
    0x05, 0x01,       /* Usage Page (Generic Desktop) */
    0x09, 0x06,       /* Usage (Keyboard) */
    0xA1, 0x01,       /* Collection (Application) */
    0x05, 0x07,       /*   Usage Page (Keyboard) */
    0x19, 0xE0,       /*   Usage Minimum (Left Control) */
    0x29, 0xE7,       /*   Usage Maximum (Right GUI) */
    0x15, 0x00,       /*   Logical Minimum (0) */
    0x25, 0x01,       /*   Logical Maximum (1) */
    0x95, 0x08,       /*   Report Count (8) */
    0x75, 0x01,       /*   Report Size (1) */
    0x81, 0x02,       /*   Input (Data, Var, Abs): модификаторы */
    0x95, 0x01,       /*   Report Count (1) */
    0x75, 0x08,       /*   Report Size (8) */
    0x81, 0x01,       /*   Input (Const): резерв */
    0x05, 0x08,       /*   Usage Page (LEDs) */
    0x19, 0x01,       /*   Usage Minimum (Num Lock) */
    0x29, 0x05,       /*   Usage Maximum (Kana) */
    0x95, 0x05,       /*   Report Count (5) */
    0x75, 0x01,       /*   Report Size (1) */
    0x91, 0x02,       /*   Output (Data, Var, Abs): светодиоды */
    0x95, 0x01,       /*   Report Count (1) */
    0x75, 0x03,       /*   Report Size (3) */
    0x91, 0x01,       /*   Output (Const): добивка до байта */
    0x05, 0x07,       /*   Usage Page (Keyboard) */
    0x19, 0x00,       /*   Usage Minimum (0) */
    0x29, 0xFF,       /*   Usage Maximum (255) */
    0x15, 0x00,       /*   Logical Minimum (0) */
    0x26, 0xFF, 0x00, /*   Logical Maximum (255) */
    0x95, 0x06,       /*   Report Count (6) */
    0x75, 0x08,       /*   Report Size (8) */
    0x81, 0x00,       /*   Input (Data, Array, Abs): шесть кодов */
    0xC0,             /* End Collection */
};
const uint16_t kbd_boot_desc_len = sizeof kbd_boot_desc;

The same parser read it as 64 bytes and 32 items: an 8-byte input, a 1-byte output. The LEDs are described here before the key codes: the order of main items of different types does not matter, the input report is still "modifiers, reserved, six codes". Our own NKRO (kbd_nkro_desc in the same file) is 53 bytes, 27 items, a 32-byte input with number 6, a 2-byte output: Logical Minimum and Maximum are not repeated a second time, they are global and live until overwritten (section 20в).

The sizes matched report.h and the endpoint sizes in usb_descriptor.h: the separate keyboard interface has KEYBOARD_EPSIZE = 8, the shared endpoint SHARED_EPSIZE = 32. Into the shared endpoint QMK puts everything that goes with a report number: NKRO, the mouse (MOUSE_SHARED_EP is on by default), system and consumer keys; as soon as any of these is enabled, a second interface appears (tmk_core/protocol.mk). The boot keyboard stays on its own interface without a report number — the BIOS understands it.

One finding during the check: a comment in report.h still says that in NKRO the report is stretched to 16 bytes for 120 keys. The code has long been different: 30 bytes of map, 240 bits.

The polling period is USB_POLLING_INTERVAL_MS = 1, which is also the bInterval of the keyboard, mouse and shared endpoints. At full speed bInterval is counted in milliseconds from 1 to 255 (section 20в.6), so this is the most frequent polling.


20г.6 The latency budget, the STM32 platform and the bootloader

From contact to host three terms add up: until the matrix sees the change (up to a scan period), until the debounce lets it through, and until the host picks up the report (on average half a USB polling period, up to a whole one). Per the model at a 0.25 ms scan and 3 ms bounce:

Algorithm Press to report, ms: average / max With 1 ms USB polling: average / max
sym_defer_g 7.56 / 8.25 8.06 / 9.25
sym_eager_pk 0.26 / 1.74 0.76 / 2.74
asym_eager_defer_pk 0.26 / 1.74 0.76 / 2.74

All together it is one function called once every millisecond; the report, if it has changed, is handed to the IN endpoint the same way as in section 20в (kbd_hw_f401.c):

/* Вызывать раз в миллисекунду (или чаще): опрос, антидребезг, отчёт.
 * Возвращает 1, если отчёт изменился и его пора отдать в точку IN. */
int kbd_task(uint32_t now_ms, kbd_boot_report_t *report)
{
    static kbd_row_t raw[KBD_ROWS], cooked[KBD_ROWS];
    static kbd_boot_report_t last;
    kbd_scan(raw);
    kbd_debounce(raw, cooked, now_ms);
    kbd_build_boot(cooked, report);
    if (report->mods == last.mods && report->keys[0] == last.keys[0] && report->keys[1] == last.keys[1]
        && report->keys[2] == last.keys[2] && report->keys[3] == last.keys[3] && report->keys[4] == last.keys[4]
        && report->keys[5] == last.keys[5])
        return 0;
    last = *report;
    return 1;
}

For Cortex-M4 (armclang, -Os) kbd.c takes 466 bytes of code, 281 bytes of constants (the keymap and two descriptors) and 36 bytes of RAM, kbd_hw_f401.c — 402 bytes of code and 16 bytes of RAM.

USB polling is not the main term: 0.5 ms against 7.56 ms of debounce. Whoever needs speed changes the algorithm, not bInterval. How much the matrix pass itself costs on a particular board the model does not know: reading the pins is added to the 30 µs per row, and this has to be measured on hardware.

ChibiOS. On ARM, QMK runs on top of ChibiOS (platforms/chibios). Which chips the build understands can be seen in platforms/chibios/mcu_selection.mk: 20 STM32 names — from the F042 and F072 to the G474, L4 and H723. For the three from the title:

Chip Core Default board System bootloader address Keyboard descriptions
STM32F072 Cortex-M0 GENERIC_STM32_F072XB 0x1FFFC800 277
STM32F401 Cortex-M4 GENERIC_STM32_F401XC 0x1FFF0000 90
STM32F411 Cortex-M4 GENERIC_STM32_F411XE 0x1FFF0000 69

The last column counts the keyboard.json and info.json files in keyboards/ where the processor is given directly or via a donor board: 25 F401 descriptions and 28 F411 descriptions are on the blackpill_f401 and blackpill_f411 boards. In total there are 784 descriptions on STM32; for comparison: ATmega32U4 — 2103, RP2040 — 333. The F072 is the most frequent STM32 in QMK; its USB can work without a crystal, from HSI48 with CRS trimming (section 6.7).

Bootloader. Keyboard firmware is updated over USB, and for STM32 there are three ways (platforms/chibios/bootloader.mk, docs/flashing.md):

__attribute__((weak)) void bootloader_jump(void) {
    bootloader_marker_enable();
    NVIC_SystemReset();
}

The marker is 0xDEADBEEF in the last word of RAM; flash with dfu-util -a 0 -d 0483:DF11. - tinyuf2 — a separate bootloader, so far only for the F303, F401 and F411: the board appears as a USB drive, and the .uf2 file is simply copied onto it. From the firmware it is entered with the marker 0xF01669EF and a reset. - stm32duino — for the F103, which has no factory USB bootloader (the bluepill board).

Building the whole firmware did not work out. The submodules lib/chibios, lib/chibios-contrib, lib/lufa, lib/printf and lib/googletest in the snapshot are empty and were not downloaded for this section. Without ChibiOS there is neither the HAL nor the USB driver, so the image size for the F072 was not measured. Only separate files were built: the debounce algorithms for Cortex-M0 and M4 (20г.2) and the models on the computer.


20г.7 Pitfalls

Bounce gets through although DEBOUNCE is set. The timer counts milliseconds, and DEBOUNCE 1 waits for quiet from 0 to 1 ms. In the model this let through 166 extra presses per 1000 with a 3 ms bounce and 256 false ones from 0.5 ms noise. Do not set less than 5 without measuring the contacts.

Eager debounce "types by itself". sym_eager_* and asym_eager_defer_pk accept any pulse longer than the scan period: pickup on a long matrix ribbon cable is already a key press. If the keyboard "presses keys" in silence, first go back to sym_defer_*.

Double presses on old switches. An eager algorithm with DEBOUNCE 5 at an 8 ms bounce gave 2000 extra presses per 1000 trials. The cure is a larger DEBOUNCE (at 10 — zero) or asym_eager_defer_pk.

Fast typing lags. The global timer of sym_defer_g and sym_defer_pr wait for the whole matrix to be quiet: a neighbouring key 2 ms later added 2 ms of latency. sym_defer_pk helps.

The seventh key is not typed even after the first is released. The 6KRO report is full, add_key_byte silently dropped the code, and nobody will add it again. You need NKRO — and a host in the report protocol.

LT to the 16th layer leads to layer zero. LT has four bits for the layer; LT(16, KC_SPACE) equals LT(0, KC_SPACE).

The space appears on release. On an LT or MT key a tap is recognized only after the release, and a hold only after TAPPING_TERM (200 ms). For fast typing the threshold is tuned to the hand, or the dual roles are moved off letter keys.

No key registers at all. DIODE_DIRECTION is mixed up: the diodes block current in the direction the firmware drives it.

The firmware does not build from the archive. The ChibiOS, LUFA and other submodules in the archive are empty; the build needs a full clone with submodules.


20г.8 Summary

Full sources

kbd.c — our own keyboard: debounce, layers, reports, descriptors.

/* Раздел 20г: своя клавиатура — антидребезг, слои, отчёты, дескрипторы.
 * Работает с готовым снимком матрицы, поэтому собирается и на ПК, и под STM32. */
#include <string.h>
#include "kbd.h"

/* Раскладка: слой 0 — буквы, слой 1 — цифры в первом ряду, остальное прозрачно. */
const uint16_t kbd_keymap[2][KBD_ROWS][KBD_COLS] = {
    {{0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B},   /* A B C D E F G H */
     {0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},   /* I J K L M N O P */
     {0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B},   /* Q R S T U V W X */
     {0xE1, 0xE0, K_MO1, 0x2C, 0x28, 0x2A, 0x1C, 0x1D}}, /* LShift LCtrl MO(1) Space Enter Bksp Y Z */
    {{0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25},   /* 1 … 8 */
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS},
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS},
     {K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS, K_TRNS}},
};

/* Антидребезг на клавишу с ожиданием тишины: изменение сырого бита
 * запоминает время; выход меняется, когда бит отличается от выхода
 * и не менялся KBD_DEBOUNCE_MS миллисекунд. */
void kbd_debounce(const kbd_row_t raw[KBD_ROWS], kbd_row_t out[KBD_ROWS], uint32_t now_ms)
{
    static kbd_row_t last[KBD_ROWS];
    static uint8_t stamp[KBD_ROWS][KBD_COLS];
    for (uint8_t r = 0; r < KBD_ROWS; r++) {
        kbd_row_t moved = (kbd_row_t)(raw[r] ^ last[r]);
        kbd_row_t differ = (kbd_row_t)(raw[r] ^ out[r]);
        last[r] = raw[r];
        for (uint8_t c = 0; c < KBD_COLS; c++) {
            kbd_row_t bit = (kbd_row_t)(1u << c);
            if (moved & bit)
                stamp[r][c] = (uint8_t)now_ms;
            else if ((differ & bit) && (uint8_t)((uint8_t)now_ms - stamp[r][c]) >= KBD_DEBOUNCE_MS)
                out[r] ^= bit;
        }
    }
}

/* Код клавиши с учётом слоёв: слой 1 включён, пока нажата K_MO1;
 * ищем сверху вниз первый непрозрачный код. */
uint16_t kbd_keycode(const kbd_row_t m[KBD_ROWS], uint8_t row, uint8_t col)
{
    uint8_t top = 0;
    for (uint8_t r = 0; r < KBD_ROWS; r++)
        for (uint8_t c = 0; c < KBD_COLS; c++)
            if ((m[r] >> c & 1u) && kbd_keymap[0][r][c] == K_MO1)
                top = 1;
    for (int8_t layer = (int8_t)top; layer >= 0; layer--) {
        uint16_t k = kbd_keymap[layer][row][col];
        if (k != K_TRNS)
            return k;
    }
    return K_NO;
}

/* Отчёт 6KRO собирается заново из всей матрицы: освободилось место —
 * в него встаёт следующая удерживаемая клавиша. Возвращает число
 * нажатых обычных клавиш (больше 6 — часть не поместилась). */
uint8_t kbd_build_boot(const kbd_row_t m[KBD_ROWS], kbd_boot_report_t *r)
{
    uint8_t n = 0;
    memset(r, 0, sizeof *r);
    for (uint8_t row = 0; row < KBD_ROWS; row++)
        for (uint8_t col = 0; col < KBD_COLS; col++) {
            if (!(m[row] >> col & 1u))
                continue;
            uint16_t k = kbd_keycode(m, row, col);
            if (k >= 0xE0 && k <= 0xE7)
                r->mods |= (uint8_t)(1u << (k - 0xE0));
            else if (k >= 0x04 && k < 0xE0) {
                if (n < 6)
                    r->keys[n] = (uint8_t)k;
                n++;
            }
        }
    return n;
}

/* Отчёт NKRO: номер, модификаторы и по биту на код 0x00…0xEF. */
void kbd_build_nkro(const kbd_row_t m[KBD_ROWS], kbd_nkro_report_t *r)
{
    memset(r, 0, sizeof *r);
    r->id = KBD_NKRO_ID;
    for (uint8_t row = 0; row < KBD_ROWS; row++)
        for (uint8_t col = 0; col < KBD_COLS; col++) {
            if (!(m[row] >> col & 1u))
                continue;
            uint16_t k = kbd_keycode(m, row, col);
            if (k >= 0xE0 && k <= 0xE7)
                r->mods |= (uint8_t)(1u << (k - 0xE0));
            else if (k >= 0x04 && k < KBD_NKRO_BYTES * 8)
                r->bits[k >> 3] |= (uint8_t)(1u << (k & 7));
        }
}

/* Дескриптор загрузочной клавиатуры: вход 8 байт, выход 1 байт (светодиоды). */
const uint8_t kbd_boot_desc[] = {
    0x05, 0x01,       /* Usage Page (Generic Desktop) */
    0x09, 0x06,       /* Usage (Keyboard) */
    0xA1, 0x01,       /* Collection (Application) */
    0x05, 0x07,       /*   Usage Page (Keyboard) */
    0x19, 0xE0,       /*   Usage Minimum (Left Control) */
    0x29, 0xE7,       /*   Usage Maximum (Right GUI) */
    0x15, 0x00,       /*   Logical Minimum (0) */
    0x25, 0x01,       /*   Logical Maximum (1) */
    0x95, 0x08,       /*   Report Count (8) */
    0x75, 0x01,       /*   Report Size (1) */
    0x81, 0x02,       /*   Input (Data, Var, Abs): модификаторы */
    0x95, 0x01,       /*   Report Count (1) */
    0x75, 0x08,       /*   Report Size (8) */
    0x81, 0x01,       /*   Input (Const): резерв */
    0x05, 0x08,       /*   Usage Page (LEDs) */
    0x19, 0x01,       /*   Usage Minimum (Num Lock) */
    0x29, 0x05,       /*   Usage Maximum (Kana) */
    0x95, 0x05,       /*   Report Count (5) */
    0x75, 0x01,       /*   Report Size (1) */
    0x91, 0x02,       /*   Output (Data, Var, Abs): светодиоды */
    0x95, 0x01,       /*   Report Count (1) */
    0x75, 0x03,       /*   Report Size (3) */
    0x91, 0x01,       /*   Output (Const): добивка до байта */
    0x05, 0x07,       /*   Usage Page (Keyboard) */
    0x19, 0x00,       /*   Usage Minimum (0) */
    0x29, 0xFF,       /*   Usage Maximum (255) */
    0x15, 0x00,       /*   Logical Minimum (0) */
    0x26, 0xFF, 0x00, /*   Logical Maximum (255) */
    0x95, 0x06,       /*   Report Count (6) */
    0x75, 0x08,       /*   Report Size (8) */
    0x81, 0x00,       /*   Input (Data, Array, Abs): шесть кодов */
    0xC0,             /* End Collection */
};
const uint16_t kbd_boot_desc_len = sizeof kbd_boot_desc;

/* Дескриптор NKRO: номер 6, модификаторы и 240 бит — по биту на код. */
const uint8_t kbd_nkro_desc[] = {
    0x05, 0x01,       /* Usage Page (Generic Desktop) */
    0x09, 0x06,       /* Usage (Keyboard) */
    0xA1, 0x01,       /* Collection (Application) */
    0x85, KBD_NKRO_ID, /*  Report ID (6) */
    0x05, 0x07,       /*   Usage Page (Keyboard) */
    0x19, 0xE0,       /*   Usage Minimum (Left Control) */
    0x29, 0xE7,       /*   Usage Maximum (Right GUI) */
    0x15, 0x00,       /*   Logical Minimum (0) */
    0x25, 0x01,       /*   Logical Maximum (1) */
    0x95, 0x08,       /*   Report Count (8) */
    0x75, 0x01,       /*   Report Size (1) */
    0x81, 0x02,       /*   Input (Data, Var, Abs): модификаторы */
    0x19, 0x00,       /*   Usage Minimum (0) */
    0x29, KBD_NKRO_BYTES * 8 - 1, /* Usage Maximum (0xEF) */
    0x95, KBD_NKRO_BYTES * 8,     /* Report Count (240) */
    0x75, 0x01,       /*   Report Size (1) */
    0x81, 0x02,       /*   Input (Data, Var, Abs): карта клавиш */
    0x05, 0x08,       /*   Usage Page (LEDs) */
    0x19, 0x01,       /*   Usage Minimum (Num Lock) */
    0x29, 0x05,       /*   Usage Maximum (Kana) */
    0x95, 0x05,       /*   Report Count (5) */
    0x75, 0x01,       /*   Report Size (1) */
    0x91, 0x02,       /*   Output (Data, Var, Abs): светодиоды */
    0x95, 0x01,       /*   Report Count (1) */
    0x75, 0x03,       /*   Report Size (3) */
    0x91, 0x01,       /*   Output (Const) */
    0xC0,             /* End Collection */
};
const uint16_t kbd_nkro_desc_len = sizeof kbd_nkro_desc;

_Static_assert(sizeof(kbd_boot_report_t) == 8, "boot report: 8 bytes");
_Static_assert(sizeof(kbd_nkro_report_t) == 32, "NKRO report: 32 bytes");

kbd_hw_f401.c — scanning a 4 × 8 matrix on the STM32F401 with registers.

/* Раздел 20г: опрос матрицы 4 × 8 на STM32F401 регистрами (CMSIS).
 * Строки PB12…PB15, столбцы PA0…PA7, диоды от столбца к строке (COL2ROW):
 * выбранная строка — выход с нулём, остальные — входы с подтяжкой,
 * столбцы — входы с подтяжкой, нажатая клавиша читается нулём. */
#include "stm32f4xx.h"
#include "kbd.h"

#define ROW_PIN0 12u                    /* PB12 — строка 0 */
#define COL_MASK 0xFFu                  /* PA0…PA7 */
#define UNSELECT_US 30u                 /* как MATRIX_IO_DELAY в QMK */

static void delay_us(uint32_t us)
{
    uint32_t start = DWT->CYCCNT, ticks = us * (SystemCoreClock / 1000000u);
    while (DWT->CYCCNT - start < ticks) {
    }
}

void kbd_hw_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN;
    (void)RCC->AHB1ENR;                                  /* такт дошёл до портов */
    GPIOA->MODER &= ~0x0000FFFFu;                        /* PA0…PA7 — входы */
    GPIOA->PUPDR = (GPIOA->PUPDR & ~0x0000FFFFu) | 0x00005555u;   /* подтяжка вверх */
    GPIOB->MODER &= ~0xFF000000u;                        /* PB12…PB15 — входы */
    GPIOB->PUPDR = (GPIOB->PUPDR & ~0xFF000000u) | 0x55000000u;
    GPIOB->BSRR = 0xFu << (ROW_PIN0 + 16u);              /* в ODR заранее нули */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;      /* счётчик тактов для delay_us */
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

/* Один проход матрицы: строку в выход с нулём, прочитать столбцы,
 * вернуть строку во вход и дать столбцам подтянуться. */
void kbd_scan(kbd_row_t raw[KBD_ROWS])
{
    for (uint32_t r = 0; r < KBD_ROWS; r++) {
        uint32_t shift = 2u * (ROW_PIN0 + r);
        GPIOB->MODER |= 1u << shift;                     /* выход, в ODR ноль */
        __DSB();
        delay_us(1);
        raw[r] = (kbd_row_t)(~GPIOA->IDR & COL_MASK);    /* ноль = нажата */
        GPIOB->MODER &= ~(3u << shift);                  /* снова вход с подтяжкой */
        delay_us(UNSELECT_US);
    }
}

/* Вызывать раз в миллисекунду (или чаще): опрос, антидребезг, отчёт.
 * Возвращает 1, если отчёт изменился и его пора отдать в точку IN. */
int kbd_task(uint32_t now_ms, kbd_boot_report_t *report)
{
    static kbd_row_t raw[KBD_ROWS], cooked[KBD_ROWS];
    static kbd_boot_report_t last;
    kbd_scan(raw);
    kbd_debounce(raw, cooked, now_ms);
    kbd_build_boot(cooked, report);
    if (report->mods == last.mods && report->keys[0] == last.keys[0] && report->keys[1] == last.keys[1]
        && report->keys[2] == last.keys[2] && report->keys[3] == last.keys[3] && report->keys[4] == last.keys[4]
        && report->keys[5] == last.keys[5])
        return 0;
    last = *report;
    return 1;
}

Next: section 20д "Your own USB device without a driver: descriptors byte by byte, MS OS 2.0 and WinUSB" — your own class and bulk endpoints, with Windows installing the driver by itself.

Open in the interactive handbook