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.
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:
- The deferred ones pay for reliability with time: 7.56 ms on average with a 3 ms bounce — roughly the bounce itself plus 4…5 ms of quiet. But not a single extra press and not a single false one from noise.
- The eager ones deliver a press in 0.26 ms, but accept any noise pulse longer than the scan period: 0.5 ms — in 1000 trials out of 1000. Their maximum of 1.74 ms does not come from the algorithm: the first touches are shorter than the scan period and slip between scans.
sym_eager_*with an 8 ms bounce give on average two extra presses per trial (2000 per 1000): after 5 ms of deafness the contact is still bouncing, and the very first change is accepted again.asym_eager_defer_pkwaits for the release the deferred way, and it has 0 extra presses.- A global timer slows down fast typing: with
sym_defer_ga neighbouring press 2 ms later moved A from 7.56 to 9.54 ms.sym_defer_pkdoes not have this — 7.55 ms. sym_defer_prin this version behaves like the global one, even if the neighbouring key is in another row: 9.55 ms for A+C. The reason is instart_debounce_counters: on any change in the matrix the counter is restarted for every row where the raw state does not yet match the output — and row A is exactly waiting.
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):
stm32-dfu— the factory bootloader in system memory; according todocs/flashing.mdall STM32 with USB have it except a few, among them the F103. You enter it with theQK_BOOTkey (it may not work on the F042), with the reset button if the board has such a circuit, or with the BOOT0 jumper to the supply during reset. OnQK_BOOTthe firmware writes a marker at the end of RAM and resets the chip, and the startup code sees the marker and jumps to the system memory address (platforms/chibios/bootloaders/stm32_dfu.c):
__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
- QMK 0.34.5 is open-source keyboard firmware (GPL-2.0 or later). On STM32
it runs on top of ChibiOS;
mcu_selection.mkknows 20 STM32 chips. Keyboard descriptions: F072 — 277, F401 — 90, F411 — 69, 784 on STM32 in total. - Matrix: a row driven to 0, columns with pull-ups, a released row is an input with a pull-up, a 30 µs wait after each row. Diodes against false presses — section 22.5.
- Debounce:
sym_defer_gby default,DEBOUNCE5 ms. In the model (scan 0.25 ms, bounce 3 ms) the deferred ones — 7.56 ms and not a single false press; the eager ones — 0.26 ms, but they accept 0.5 ms noise in 1000 trials out of 1000. A global timer when typing 2 ms apart — 9.54 ms, per key — 7.55 ms. - Layers are a stack of bits, searched top to bottom,
KC_TRNSlooks below.MO(1)= 0x5221,LT(1, KC_SPACE)= 0x412C,LThas 16 layers;TAPPING_TERMis 200 ms. - The 6KRO report is 8 bytes, the seventh key is lost for good; NKRO is 32 bytes with number 6, 240 bits. Descriptors: 68 and 59 bytes; endpoints 8 and 32 bytes; polling once every 1 ms.
- The budget from contact to host: 8.06 ms on average with
sym_defer_g, 0.76 ms with an eager algorithm; USB polling accounts for 0.5 ms of that. - Our own keyboard for the F401 (
kbd.h,kbd.c,kbd_hw_f401.c): scanning with registers, per-key debounce (in the model — likesym_defer_pk), two layers, a report from the whole matrix (the seventh key takes the freed slot), descriptors of 64 and 53 bytes;kbd.cis 466 bytes of code for Cortex-M4. - Bootloader: the factory DFU (marker 0xDEADBEEF, address 0x1FFFC800 on the F072 and 0x1FFF0000 on the F401/F411), tinyuf2 (F303, F401, F411) or stm32duino (F103).
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.