20г. Клавиатура на STM32: разбор QMK — матрица, антидребезг, слои и USB HID
В разделе 20в клавиатура была учебной: дескриптор отчёта, восемь байт, отправка из цикла. Настоящей клавиатуре этого мало. Ей надо опросить сотню клавиш, отсеять дребезг и не потерять быстрый набор, держать несколько слоёв раскладки, отличить короткое нажатие от удержания и уложиться в миллисекунды до хоста.
Здесь разобрана открытая прошивка QMK: в её дереве 3562 описания
клавиатур с указанным микроконтроллером, из них 784 — на STM32, чаще
всего F072, F401 и F411. Идём по пути одного нажатия: матрица, антидребезг, слои, отчёт,
дескриптор, точка USB. Настоящие файлы QMK собраны gcc на компьютере и
прогнаны против модели дребезжащего контакта и последовательности
нажатий; их числа — в таблицах ниже. Рядом с каждым куском QMK — свой
код для STM32F401 в трёх файлах (kbd.h, kbd.c, kbd_hw_f401.c):
опрос матрицы регистрами, антидребезг, слои, отчёты и дескрипторы.
Его можно скопировать целиком и собрать.
Опоры: исходники QMK (github.com/qmk/qmk_firmware, версия 0.34.5,
снимок b1aea25); пути ниже даны от корня репозитория.
Матрица и ложные нажатия без диодов разобраны в разделе 22.5, подтяжки
входов — в разделе 8, язык дескриптора отчёта и загрузочная клавиатура —
в разделе 20в, системный загрузчик и перепрошивка — в разделах 11 и 48.
Свой код собран с заголовками CMSIS для STM32F401xC из пакета
STM32F4xx_DFP 2.17.1.
Лицензия: QMK распространяется по GPL-2.0 или новее (заголовки файлов,
текст — LICENSE); usb_descriptor.c вдобавок несёт разрешительное
уведомление LUFA. Фрагменты кода ниже — короткие цитаты с указанием
файла; копии нужных файлов лежат рядом с проверкой вместе с текстом
лицензии.
Как это называется в документации
Оригинал Перевод matrix scan опрос матрицы diode direction: COL2ROW, ROW2COL направление диодов: от столбца к строке, от строки к столбцу ghosting ложное («призрачное») нажатие debounce: defer, eager антидребезг: отложенный (ждать тишины), немедленный (принять и не слушать) global, per-row, per-key общий на всю матрицу, на строку, на клавишу keymap, layer раскладка, слой transparent key (KC_TRNS) прозрачная клавиша: смотреть слой ниже momentary, toggle, layer-tap слой на время удержания, переключатель, слой при удержании и клавиша при коротком нажатии tapping term порог «короткое или удержание» 6KRO, NKRO шесть одновременных клавиш; любое число клавиш shared endpoint общая точка для нескольких отчётов с номерами bootloader jump переход в загрузчик из прошивки
20г.1 Матрица: строка в ноль, читаем столбцы
Клавиши стоят в узлах сетки, как в разделе 22.5. QMK знает два
направления диодов. При COL2ROW прошивка по очереди прижимает к
земле строку и читает столбцы, при ROW2COL — наоборот. Всё остальное
одинаково. Вот чтение одной строки из 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
Три детали. Столбцы — входы с подтяжкой вверх, нажатая клавиша читается
нулём. Отпущенную строку QMK не держит в единице, а переводит во вход
с подтяжкой (gpio_set_pin_input_high): две клавиши одного столбца не
замкнут выход-ноль на выход-единицу. Держать единицей можно, но только
явно — макросом MATRIX_UNSELECT_DRIVE_HIGH. И после каждой строки
прошивка ждёт: matrix_output_unselect_delay по умолчанию вызывает
wait_us(MATRIX_IO_DELAY), а MATRIX_IO_DELAY — 30 мкс
(quantum/matrix_common.c). Столбцы должны успеть подтянуться к
единице, иначе следующая строка прочитает хвост предыдущей. Матрица из
шести строк стоит поэтому минимум 180 мкс на один проход.
Диоды нужны ради того же, что в разделе 22.5: три нажатые клавиши в
углах прямоугольника без диодов «зажигают» четвёртую. DIODE_DIRECTION
в описании клавиатуры говорит прошивке, куда смотрят катоды; ошибка
в нём — матрица, которая не видит ни одного нажатия. Бывает и
матрица без сетки: DIRECT_PINS, каждая клавиша на своём выводе.
Свой опрос на F401. Та же схема регистрами — для платы на STM32F401
(как blackpill_f401 в QMK) с матрицей 4 × 8: строки PB12…PB15, столбцы
PA0…PA7, диоды от столбца к строке. Общий заголовок своей клавиатуры,
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
Опрос (kbd_hw_f401.c). Выбранная строка — выход с нулём: ноль в ODR
записан заранее, и переключается только режим в MODER. Пауза 30 мкс
отсчитывается счётчиком тактов DWT:
#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);
}
}
После прохода matrix_scan() сравнивает новый снимок со старым
(memcmp) и передаёт его антидребезгу вместе с флагом «что-то
изменилось». Дальше нажатие идёт по цепочке на рисунке.
20г.2 Антидребезг: семь алгоритмов и их цена
Алгоритм выбирают строкой DEBOUNCE_TYPE в правилах клавиатуры, по
умолчанию — sym_defer_g (builddefs/common_features.mk). Время —
макрос DEBOUNCE, по умолчанию 5 мс во всех шести файлах
quantum/debounce/; седьмой, none.c, не фильтрует ничего. Названия
читаются по частям: sym — одинаково для нажатия и отпускания, asym —
по-разному; defer — ждать, пока контакт успокоится, eager — принять
первое изменение сразу и потом не слушать клавишу DEBOUNCE мс; g,
pr, pk — один таймер на всю матрицу, на строку, на клавишу.
Самый простой и самый распространённый — общий отложенный
(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;
}
Любое изменение где угодно в матрице перезапускает таймер; снимок
уходит дальше, только когда вся матрица молчала DEBOUNCE мс.
Модель. Настоящие файлы алгоритмов собраны gcc вместе с заглушкой
таймера (миллисекундный timer_read32, как на ChibiOS) и циклом, который
повторяет matrix_scan(). Матрица 2 × 2: A и B в одной строке, C — в
другой. Контакт при нажатии и отпускании дребезжит: окно заданной длины,
внутри — переключения через случайные 20…300 мкс. Опрос раз в 0,25 мс
(тесты QMK сами гоняют алгоритмы «from 1kHz to 10kHz scan rate» —
quantum/debounce/tests/debounce_test_common.cpp), по 1000 опытов на
строку. Задержка — от первого касания до изменения на выходе.
DEBOUNCE = 5:
| Алгоритм | Нажатие, мс: среднее / макс. | Отпускание, мс | Лишних нажатий: дребезг 3 / 8 мс | Помеха 0,5 мс — ложных | A при наборе A+B / A+C, мс |
|---|---|---|---|---|---|
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 |
свой kbd_debounce |
7,56 / 8,25 | 7,55 / 8,25 | 0 / 0 | 0 | 7,55 / 7,55 |
Задержки нажатия и отпускания — при окне дребезга 3 мс; «A+B / A+C» — задержка A, если через 2 мс после неё нажата B (та же строка) или C (другая), обе с дребезгом 3 мс.
Что видно:
- Отложенные платят за надёжность временем: 7,56 мс в среднем при дребезге 3 мс — примерно сам дребезг плюс 4…5 мс тишины. Зато ни одного лишнего нажатия и ни одного ложного от помехи.
- Немедленные отдают нажатие за 0,26 мс, но принимают любой импульс помехи дольше периода опроса: 0,5 мс — в 1000 опытах из 1000. Максимум 1,74 мс у них не от алгоритма: первые касания короче периода опроса и проскакивают между опросами.
sym_eager_*при дребезге 8 мс дают в среднем по два лишних нажатия на опыт (2000 на 1000): через 5 мс глухоты контакт ещё дребезжит, и первое же изменение снова принимается.asym_eager_defer_pkотпускание ждёт по-отложенному, и лишних нажатий у него 0.- Общий таймер замедляет быстрый набор: у
sym_defer_gсоседнее нажатие через 2 мс сдвинуло A с 7,56 до 9,54 мс.sym_defer_pkэтого не знает — 7,55 мс. sym_defer_prв этой версии ведёт себя как общий, даже если соседняя клавиша в другой строке: 9,55 мс для A+C. Причина вstart_debounce_counters: при любом изменении в матрице счётчик заново заводится у каждой строки, где сырое состояние ещё не совпало с выходным, — а строка A как раз ждёт.
Свой антидребезг (kbd.c) — на клавишу и с ожиданием тишины, как
sym_defer_pk, но короче: у каждой клавиши байт с моментом последнего
изменения. В той же модели он дал ровно те же числа, что sym_defer_pk
(последняя строка таблицы):
/* Антидребезг на клавишу с ожиданием тишины: изменение сырого бита
* запоминает время; выход меняется, когда бит отличается от выхода
* и не менялся 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;
}
}
}
Разность в uint8_t переживает переполнение счёта миллисекунд: выход
меняется через 5 мс тишины, задолго до того, как 256 мс обернутся.
Сколько ставить. Тот же прогон для других DEBOUNCE, опрос 0,25 мс:
Алгоритм, DEBOUNCE |
Нажатие без дребезга, мс: мин. / макс. | Лишних нажатий: дребезг 1 / 3 / 8 мс | Помеха 0,5 мс — ложных |
|---|---|---|---|
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 |
Таймер миллисекундный, поэтому «DEBOUNCE мс тишины» на деле — от
DEBOUNCE − 1 до DEBOUNCE мс плюс период опроса: 4,25…5,24 мс при
5, а при 1 — от 0,25 мс, и половина импульсов помехи 0,5 мс проходит
(256 из 1000). Правило простое: DEBOUNCE должен быть больше самого
длинного дребезга с запасом в миллисекунду. При опросе раз в 1 мс
немедленный алгоритм ловит помеху 0,5 мс в 500 опытах из 1000 — ровно
в тех, где импульс накрыл момент опроса.
Во что это обходится памяти, показывает сборка под Cortex-M0 (F072)
для матрицы 5 × 15 = 75 клавиш: sym_defer_g — 84 байта кода и 8 байт
ОЗУ, sym_defer_pk — 316 и 83 (по байту-счётчику на клавишу),
sym_defer_pr — 228 и 13, asym_eager_defer_pk — 456 и 83. Разница —
сотни байт кода и счётчики в ОЗУ; выбирать алгоритм стоит по поведению,
а не по размеру.
20г.3 Раскладка и слои
Раскладка — массив кодов keymaps[слой][строка][столбец]. Слои лежат
стопкой: включённые отмечены битами в layer_state (по умолчанию 16 бит,
MAX_LAYER = 16, quantum/action_layer.h), нижний — default_layer_state.
Для каждой клавиши прошивка ищет сверху вниз первый включённый слой, где
на этом месте стоит не прозрачный код (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;
}
}
}
Прозрачный код KC_TRNS (0x0001) — дыра в слое: смотри ниже. Так слой
цифр может задать только верхний ряд, а остальное взять из основного.
Слой запоминается в момент нажатия (store_or_get_action, если не задан
STRICT_LAYER_RELEASE): если слой выключили, пока клавиша держится,
отпускание уйдёт тому же слою, и модификатор не залипнет.
В своей клавиатуре то же самое короче: раскладка на два слоя, клавиша
K_MO1 включает слой 1 на время удержания, K_TRNS пропускает вниз
(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}},
};
Поиск кода — сверху вниз, как в 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;
}
Прогон на ПК: без K_MO1 клавиша (0,0) даёт 0x0004 (A), с ней — 0x001E
(цифра 1); клавиша (1,0) на слое 1 прозрачна и в обоих случаях даёт
0x000C (I).
Слоями в QMK управляют коды клавиш — 16-битные числа из
quantum/keycodes.h и quantum/quantum_keycodes.h; их значения
напечатала программа, собранная с этими заголовками:
| Код | Значение | Что делает |
|---|---|---|
MO(1) |
0x5221 | слой 1, пока клавиша нажата |
TG(2) |
0x5262 | слой 2 включить или выключить |
TO(0) |
0x5200 | оставить только слой 0 |
LT(1, KC_SPACE) |
0x412C | коротко — пробел, удержание — слой 1 |
MT(MOD_LCTL, KC_ESCAPE) |
0x2129 | коротко — Esc, удержание — левый Ctrl |
В LT на номер слоя отведено 4 бита: LT(15, KC_SPACE) — 0x4F2C,
а LT(16, KC_SPACE) — 0x402C, то есть молча LT(0, KC_SPACE). MO
и TG берут 5 бит.
Короткое или удержание. Клавиша с двумя ролями (LT, MT) ждёт.
Отпустили раньше порога TAPPING_TERM — это короткое нажатие, и в
отчёт уходит пробел; держат дольше — это удержание, включается слой
или модификатор. Порог по умолчанию — 200 мс (quantum/action_tapping.h).
Цена — задержка: пробел на такой клавише появляется не при нажатии,
а при отпускании. Опции PERMISSIVE_HOLD, HOLD_ON_OTHER_KEY_PRESS
и другие решают, что делать, если во время ожидания нажата вторая
клавиша; модель этого раздела их не прогоняла.
20г.4 Отчёт: шесть клавиш или все
Коды клавиш из раскладки собираются в отчёт функциями
tmk_core/protocol/report.c. Обычный отчёт — загрузочная клавиатура
из раздела 20в: байт модификаторов, резерв, шесть кодов, 8 байт
(sizeof(report_keyboard_t) = 8, KEYBOARD_REPORT_KEYS = 6). Новый
код add_key_byte кладёт в первую пустую ячейку; цикл перед этим
ищет, нет ли кода в отчёте, и запоминает пустую ячейку в empty. Конец
функции:
if (i == KEYBOARD_REPORT_KEYS) {
if (empty != -1) {
keyboard_report->keys[empty] = code;
}
}
}
Если пустой нет — код просто не попадает в отчёт. Настоящий report.c,
собранный на компьютере, на нажатиях A…H подряд:
+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
Седьмая и восьмая клавиши потерялись. Хуже: после отпускания A ячейка освободилась, а G, которую всё ещё держат, в отчёт так и не вернулась — код добавляется только в момент нажатия.
Своя сборка отчёта устроена иначе: отчёт каждый раз строится заново из
всей матрицы, а не правится по событиям (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;
}
Тот же опыт с нажатиями A…H, потом отпускание 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
Как только A отпущена, в освободившееся место встала G (0x0A): её продолжают держать, а отчёт собран из матрицы. H так и не видна — мест всё равно шесть.
NKRO снимает предел. Отчёт — битовая карта: номер 6
(REPORT_ID_NKRO), байт модификаторов и NKRO_REPORT_BITS = 30 байт,
по биту на код 0x00…0xEF; sizeof(report_nkro_t) = 32. Код 0x04 (A) —
бит 4 первого байта карты. Тот же прогон с 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
Все восемь на месте: 0xF0 — A…D, 0x0F — E…H. Своя функция даёт те же байты для тех же восьми клавиш:
/* Отчёт 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));
}
}
В выводе QMK видна мелочь: has_anykey() в режиме NKRO считает ненулевые байты, а не клавиши,
поэтому «keys=2» при восьми нажатых. Как логический признак «что-то
нажато» она работает верно.
NKRO включится не всегда. host_can_send_nkro() (tmk_core/protocol/host.c)
требует, чтобы хост перевёл устройство в протокол отчётов; хост,
который понимает только загрузочную клавиатуру, получает обычные
8 байт. И ещё флаг в настройках: NKRO_DEFAULT_ON по умолчанию false
(quantum/eeconfig.c), переключают его клавишей NK_TOGG.
20г.5 Дескриптор и точки: что видит хост
Дескрипторы отчёта QMK написаны макросами LUFA HID_RI_* в
tmk_core/protocol/usb_descriptor.c. Подмодуль LUFA в снимке пуст,
поэтому макросы переписаны заново по тегам элементов из hid.h Zephyr
(как в разделе 20в), а сами строки дескрипторов вырезаны из файла без
изменений и собраны gcc. Свой разборщик на Python прочитал байты:
| Дескриптор | Длина | Элементов | Вход | Выход |
|---|---|---|---|---|
KeyboardReport |
68 байт | 34 | 8 × 1 бит модификаторов, 1 × 8 резерв, 6 × 8 кодов = 8 байт | 5 бит светодиодов + 3 бита заполнения = 1 байт |
NKRO в SharedReport |
59 байт | 30 | номер 6, 8 × 1 бит, 240 × 1 бит = 32 байта | номер 6 + 1 байт = 2 байта |
Свои дескрипторы — обычные массивы байт с разбором в комментариях;
загрузочный целиком (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;
Тот же разборщик прочитал его как 64 байта и 32 элемента: вход 8 байт,
выход 1 байт. Светодиоды здесь описаны раньше кодов клавиш: порядок
главных элементов разных видов не важен, отчёт входа всё равно
«модификаторы, резерв, шесть кодов». Свой NKRO (kbd_nkro_desc
в том же файле) — 53 байта, 27 элементов, вход 32 байта с номером 6,
выход 2 байта: Logical Minimum и Maximum второй раз не повторяются,
они глобальные и живут до перезаписи (раздел 20в).
Размеры совпали с report.h и с размерами точек в usb_descriptor.h:
у отдельного интерфейса клавиатуры KEYBOARD_EPSIZE = 8, у общей точки
SHARED_EPSIZE = 32. В общую точку QMK складывает всё, что идёт с
номером отчёта: NKRO, мышь (MOUSE_SHARED_EP по умолчанию включён),
системные и потребительские клавиши; как только включено хоть что-то
из этого, появляется второй интерфейс (tmk_core/protocol.mk).
Загрузочная клавиатура остаётся на своём интерфейсе без номера отчёта —
её понимает BIOS.
Одна находка при сверке: комментарий в report.h до сих пор говорит,
что в NKRO отчёт растягивается до 16 байт на 120 клавиш. Код давно
другой: 30 байт карты, 240 бит.
Период опроса — USB_POLLING_INTERVAL_MS = 1, он же bInterval у точек
клавиатуры, мыши и общей. На полной скорости bInterval считают в
миллисекундах от 1 до 255 (раздел 20в.6), так что это самый частый
опрос.
20г.6 Бюджет задержки, платформа STM32 и загрузчик
От контакта до хоста складываются три слагаемых: пока матрица увидит изменение (до периода опроса), пока антидребезг его пропустит, и пока хост заберёт отчёт (в среднем полпериода опроса USB, до целого). По модели при опросе 0,25 мс и дребезге 3 мс:
| Алгоритм | Нажатие до отчёта, мс: среднее / макс. | С опросом USB 1 мс: среднее / макс. |
|---|---|---|
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 |
Всё вместе — одна функция, которую зовут раз в миллисекунду; отчёт,
если он изменился, отдают в точку IN так же, как в разделе 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;
}
Под Cortex-M4 (armclang, -Os) kbd.c занимает 466 байт кода, 281 байт
констант (раскладка и два дескриптора) и 36 байт ОЗУ, kbd_hw_f401.c —
402 байта кода и 16 байт ОЗУ.
Опрос USB — не главное слагаемое: 0,5 мс против 7,56 мс антидребезга.
Кому нужна скорость, меняет алгоритм, а не bInterval. Сколько стоит
сам проход матрицы на конкретной плате, модель не знает: к 30 мкс на
строку добавляется чтение выводов, и мерить это надо на железе.
ChibiOS. На ARM QMK работает поверх ChibiOS (platforms/chibios).
Какие кристаллы понимает сборка, видно в platforms/chibios/mcu_selection.mk:
20 имён STM32 — от F042 и F072 до G474, L4 и H723. Для трёх из заголовка:
| Кристалл | Ядро | Плата по умолчанию | Адрес системного загрузчика | Описаний клавиатур |
|---|---|---|---|---|
| STM32F072 | Cortex-M0 | GENERIC_STM32_F072XB |
0x1FFFC800 | 277 |
| STM32F401 | Cortex-M4 | GENERIC_STM32_F401XC |
0x1FFF0000 | 90 |
| STM32F411 | Cortex-M4 | GENERIC_STM32_F411XE |
0x1FFF0000 | 69 |
Последний столбец — файлы keyboard.json и info.json в keyboards/,
где процессор указан прямо или через плату-донор: 25 описаний F401
и 28 описаний F411 — на платах blackpill_f401 и blackpill_f411. Всего
на STM32 — 784 описания; для сравнения: ATmega32U4 — 2103, RP2040 — 333.
F072 — самый частый STM32 в QMK; USB у него может работать без кварца,
от HSI48 с подстройкой CRS (раздел 6.7).
Загрузчик. Прошивка клавиатуры обновляется по USB, и для STM32
способов три (platforms/chibios/bootloader.mk, docs/flashing.md):
stm32-dfu— заводской загрузчик в системной памяти; поdocs/flashing.mdон есть у всех STM32 с USB, кроме немногих, среди них F103. Войти — клавишейQK_BOOT(на F042 может не сработать), кнопкой сброса, если на плате есть такая схема, или перемычкой BOOT0 на питание при сбросе. ПоQK_BOOTпрошивка пишет метку в конец ОЗУ и сбрасывает кристалл, а код запуска видит метку и прыгает по адресу системной памяти (platforms/chibios/bootloaders/stm32_dfu.c):
__attribute__((weak)) void bootloader_jump(void) {
bootloader_marker_enable();
NVIC_SystemReset();
}
Метка — 0xDEADBEEF в последнем слове ОЗУ; шьют dfu-util -a 0 -d 0483:DF11.
- tinyuf2 — свой загрузчик, пока только для F303, F401 и F411: плата
появляется как USB-диск, и файл .uf2 на него просто копируют. Из
прошивки в него входят меткой 0xF01669EF и сбросом.
- stm32duino — для F103, у которого заводского загрузчика по USB нет
(плата bluepill).
Собрать прошивку целиком не вышло. Подмодули lib/chibios,
lib/chibios-contrib, lib/lufa, lib/printf и lib/googletest в
снимке пусты и для раздела не выкачивались. Без ChibiOS нет ни HAL, ни драйвера
USB, так что размер образа для F072 не измерен. Собраны только
отдельные файлы: алгоритмы антидребезга под Cortex-M0 и M4 (20г.2) и
модели на компьютере.
20г.7 Грабли
Дребезг проходит, хотя DEBOUNCE стоит. Таймер миллисекундный,
и DEBOUNCE 1 ждёт тишины от 0 до 1 мс. В модели так прошли 166 лишних
нажатий на 1000 при дребезге 3 мс и 256 ложных от помехи 0,5 мс.
Меньше 5 без замеров контакта не ставить.
Немедленный антидребезг «печатает сам». sym_eager_* и
asym_eager_defer_pk принимают любой импульс длиннее периода опроса:
наводка на длинном шлейфе матрицы — уже нажатие. Если клавиатура
«нажимает» в тишине, первым делом вернуть sym_defer_*.
Двойные нажатия на старых переключателях. Немедленный алгоритм с
DEBOUNCE 5 при дребезге 8 мс дал 2000 лишних нажатий на 1000 опытов.
Лечится увеличением DEBOUNCE (при 10 — ноль) или asym_eager_defer_pk.
Быстрый набор тормозит. Общий таймер sym_defer_g и
sym_defer_pr ждут тишины всей матрицы: соседняя клавиша через 2 мс
добавила 2 мс задержки. Помогает sym_defer_pk.
Седьмая клавиша не печатается и после отпускания первой. Отчёт
6KRO полон, add_key_byte молча выбросил код, а заново его никто не
добавит. Нужен NKRO — и хост в протоколе отчётов.
LT на 16-й слой ведёт в нулевой. В LT четыре бита на слой;
LT(16, KC_SPACE) равен LT(0, KC_SPACE).
Пробел появляется при отпускании. На клавише LT или MT короткое
нажатие распознаётся только после отпускания, а удержание — только
через TAPPING_TERM (200 мс). Для быстрой печати порог подбирают
под руку или переносят двойные роли с букв.
Ни одна клавиша не нажимается. Перепутан DIODE_DIRECTION: диоды
закрыты в ту сторону, куда прошивка тянет ток.
Прошивка не собирается из архива. Подмодули ChibiOS, LUFA и остальные в архиве пусты; сборке нужен полный клон с подмодулями.
20г.8 Итоги
- QMK 0.34.5 — открытая прошивка клавиатур (GPL-2.0 или новее). На STM32
она идёт поверх ChibiOS;
mcu_selection.mkзнает 20 кристаллов STM32. Описаний клавиатур: F072 — 277, F401 — 90, F411 — 69, всего на STM32 — 784. - Матрица: строка в 0, столбцы с подтяжкой, отпущенная строка — вход с подтяжкой, после каждой строки 30 мкс ожидания. Диоды против ложных нажатий — раздел 22.5.
- Антидребезг: по умолчанию
sym_defer_g,DEBOUNCE5 мс. На модели (опрос 0,25 мс, дребезг 3 мс) отложенные — 7,56 мс и ни одного ложного; немедленные — 0,26 мс, но помеху 0,5 мс принимают в 1000 опытах из 1000. Общий таймер при наборе через 2 мс — 9,54 мс, на клавишу — 7,55 мс. - Слои — стопка битов, поиск сверху вниз,
KC_TRNSсмотрит ниже.MO(1)= 0x5221,LT(1, KC_SPACE)= 0x412C, уLT16 слоёв;TAPPING_TERM— 200 мс. - Отчёт 6KRO — 8 байт, седьмая клавиша теряется насовсем; NKRO — 32 байта с номером 6, 240 бит. Дескрипторы: 68 и 59 байт; точки 8 и 32 байта; опрос раз в 1 мс.
- Бюджет от контакта до хоста: 8,06 мс в среднем с
sym_defer_g, 0,76 мс с немедленным алгоритмом; опрос USB даёт из них 0,5 мс. - Своя клавиатура для F401 (
kbd.h,kbd.c,kbd_hw_f401.c): опрос регистрами, антидребезг на клавишу (в модели — какsym_defer_pk), два слоя, отчёт из всей матрицы (седьмая клавиша встаёт на освободившееся место), дескрипторы 64 и 53 байта;kbd.c— 466 байт кода под Cortex-M4. - Загрузчик: заводской DFU (метка 0xDEADBEEF, адрес 0x1FFFC800 у F072 и 0x1FFF0000 у F401/F411), tinyuf2 (F303, F401, F411) или stm32duino (F103).
Полные исходники
kbd.c — своя клавиатура: антидребезг, слои, отчёты, дескрипторы.
/* Раздел 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 — опрос матрицы 4 × 8 на STM32F401 регистрами.
/* Раздел 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;
}
Дальше: раздел 20д «Своё USB-устройство без драйвера: дескрипторы по байтам, MS OS 2.0 и WinUSB» — свой класс и массовые точки, а драйвер Windows ставит сама.