USB HID-клавіатура на мікроконтролері RP2040
Почнемо здалеку й перенесімося на 30 років у минуле, до часів перших процесорів Pentium.
Тоді для кожного пристрою на ПК був свій окремий інтерфейс:
DIN — для клавіатури.
Вузький COM-порт — для миші.
Широкий COM-порт — для модема.
На брендових комп’ютерах також зустрічалися порти PS/2 — окремі для клавіатури та миші.
Паралельний порт (LPT) — для принтера, сканера або зовнішнього CD-ROM.
Зовнішній SCSI — для накопичувачів, CD-ROM або сканерів.
MIDI/Game port — для музичного обладнання та геймпадів.
І це лише стандартні роз’єми. Окрім них, багато виробників створювали власні пропрієтарні інтерфейси для свого обладнання.
Такий стан речей був незручним для користувачів та і виробникам теж додавав проблем. Щоб вирішити цю проблему, був створений USB консорциум з виробників заліза та софту. Його метою стало впровадження єдиної універсальної шини для підключення зовнішніх пристроїв.
Звісно, ніхто не очікував, що перехід буде швидким. У користувачів залишалося багато старого заліза та програмного забезпечення (наприклад, Windows 95 чи MS-DOS), яке ще не підтримувало USB.
І якщо з апаратною частиною нічого вдіяти було не можна, то для програмного забезпечення деякі рішення все ж існували. Наприклад, для клавіатур до стандарту додали спеціальний Boot Protocol. Він гарантує, що клавіатура буде функціонувати в будь-якому середовищі, не вимагаючи спеціалізованих драйверів. У цьому режимі пристрій надсилає дані у строго фіксованому форматі, який може зрозуміти будь-який хост. Тож навіть BIOS міг використовувати вбудований спрощений драйвер та емулювати роботу класичної клавіатури. Головним обмеженням протоколу є можливість одночасно передавати до 6 скан-кодів звичайних клавіш, а також стан 8 клавіш-модифікаторів (Ctrl, Shift, Alt, GUI). Ця особливість відома як 6KRO (6-Key Rollover), що є стандартом для більшості офісних та непрофесійних ігрових клавіатур.
У цій статті я хочу показати як використати мікроконтролер RP2040 для створення власного USB-пристрою. Метою буде емуляція клавіатури, що працює саме в режимі Boot Protocol. Для реалзіції USB стеку використаємо бібліотеку Tinyusb, яка значно спрощує взаємодію з USB на низькому рівні.
Налаштування RP2040 проекту з Tinyusb
Для початку створимо новий проєкт за допомогою інструментів PicoSDK. У вікні налаштувань потрібно задати початкові параметри.
Виконайте наступні кроки:
- Project Name: Вкажіть ім’я проєкту, наприклад, UsbHid.
- Board Type: Виберіть тип вашої плати. В мене це стандартна Raspberry Pi Pico.
- Features: У цьому розділі зніміть усі стандартні прапорці. Ми створюємо мінімалістичний проєкт і не потребуємо додаткових бібліотек, як-от printf чи UART, які вмикаються за замовчуванням.
- Code Language: Обов’язково встановіть прапорець “Generate C++ code”.
Після налаштування натисніть “OK”, щоб згенерувати структуру проєкту.
Після створення порожнього проєкту ми отримуємо теку з кількома файлами. Наразі для нас важливим є файл конфігурації компіляції проєкту - CMakeLists.txt:cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
...
====================================================================================
set(PICO_BOARD pico CACHE STRING "Board type")
include(pico_sdk_import.cmake)
project(UsbHid C CXX ASM)
pico_sdk_init()
add_executable(UsbHid UsbHid.cpp )
pico_set_program_name(UsbHid "UsbHid")
pico_set_program_version(UsbHid "0.1")
pico_enable_stdio_uart(UsbHid 0)
pico_enable_stdio_usb(UsbHid 0)
target_link_libraries(UsbHid
pico_stdlib)
target_include_directories(UsbHid PRIVATE
${CMAKE_CURRENT_LIST_DIR}
)
pico_add_extra_outputs(UsbHid)
Тепер потрібно додати до залежностей дві бібліотеки: tinyusb_device та tinyusb_board. Для цього відредагуємо директиву target_link_libraries:
target_link_libraries(UsbHid
pico_stdlib
tinyusb_device
tinyusb_board
)
Спроба скомпілювати проєкт на цьому етапі призведе до помилки, оскільки компілятор не зможе знайти необхідні файли конфігурації:
tusb_option.h:243:12: fatal error: tusb_config.h: No such file or directory
243 | #include "tusb_config.h"
Це відбувається тому, що нам потрібно додати файли з описом конфігурації USB-пристрою. Ми не будемо створювати їх з нуля, а візьмемо готові зразки з офіційного репозиторію бібліотеки. Нам знадобляться три файли:
- tusb_config.h
- usb_descriptors.h
- usb_desctiptors.c
Ці файли можна знайти у прикладах до бібліотеки TinyUSB за цим посиланням: https://github.com/hathach/tinyusb/tree/master/examples/device/hid_boot_interface/src.
Для початку копіюємо tusb_config.h у проєкт. В оригінальному прикладі з репозиторію TinyUSB емулюються два USB-пристрої — клавіатура та миша. Для простоти ми залишимо тільки клавіатуру, тому потрібно змінити відповідне значення на 1.
#define CFG_TUD_HID 1
Наступним кроком скопіюйте usb_descriptors.h. У цьому файлі потрібно прибрати згадку про мишу (ITF_NUM_MOUSE) та додати інтервал опитування (POLLING_INTERVAL_MS) для клавіатури:
enum
{
ITF_NUM_KEYBOARD,
ITF_NUM_MOUSE,
ITF_NUM_TOTAL
};
#define POLLING_INTERVAL_MS 10
Останній файл, usb_descriptors.c, також потрібно повністю скопіювати у ваш проєкт.
Тепер додамо usb_descriptors.c до списку файлів для компіляції. Для цього відредагуйте директиву add_executable у файлі CMakeLists.txt:
add_executable(HelloUsb HelloUsb.cpp usb_descriptors.c)
Якщо зараз спробувати скомпілювати проєкт, ви отримаєте помилку.
usb_decriptors.c:118:22: error 'ITF_NUM_MOUSE' undeclared here (not in a function); did you mean 'ITF_NUM_TOTAL'?
Помилка компіляції виникає через те, що в usb_descriptors.c залишилися посилання на ITF_NUM_MOUSE, який ми видалили з .h файлу. Щоб це виправити, потрібно ретельно відредагувати usb_descriptors.c і прибрати з нього весь код, що стосується миші.
Спочатку знаходимо блок, де визначаються HID-дескриптори звітів. Тут потрібно повністю видалити масив desc_hid_mouse_report і спростити функцію tud_hid_descriptor_report_cb, щоб вона повертала лише дескриптор клавіатури.
uint8_t const desc_hid_keyboard_report[] =
{
TUD_HID_REPORT_DESC_KEYBOARD()
};
uint8_t const desc_hid_mouse_report[] =
{
TUD_HID_REPORT_DESC_MOUSE()
};
uint8_t const * tud_hid_descriptor_report_cb(uint8_t instance)
{
return (instance == 0) ? desc_hid_keyboard_report : desc_hid_mouse_report;
return desc_hid_keyboard_report;
}
Далі потрібно оновити головний дескриптор конфігурації
(desc_configuration). Ми видалимо все, що пов’язано з мишею: визначення
довжини, номер кінцевої точки (endpoint) та сам дескриптор інтерфейсу.
Тож замість цього блоку:
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + 2*TUD_HID_DESC_LEN)
#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX
// LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number
// 1 Interrupt, 2 Bulk, 3 Iso, 4 Interrupt, 5 Bulk etc ...
#define EPNUM_KEYBOARD 0x81
#define EPNUM_MOUSE 0x84
#else
#define EPNUM_KEYBOARD 0x81
#define EPNUM_MOUSE 0x82
#endif
uint8_t const desc_configuration[] =
{
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval
TUD_HID_DESCRIPTOR(ITF_NUM_KEYBOARD, 0, HID_ITF_PROTOCOL_KEYBOARD, sizeof(desc_hid_keyboard_report), EPNUM_KEYBOARD, CFG_TUD_HID_EP_BUFSIZE, 10),
// Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval
TUD_HID_DESCRIPTOR(ITF_NUM_MOUSE, 0, HID_ITF_PROTOCOL_MOUSE, sizeof(desc_hid_mouse_report), EPNUM_MOUSE, CFG_TUD_HID_EP_BUFSIZE, 10)
};
має бути:
// Залишаємо лише один TUD_HID_DESC_LEN
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN)
// У нас залишилась тільки кінцева точка для клавіатури
#define EPNUM_KEYBOARD 0x81
uint8_t const desc_configuration[] =
{
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1,
ITF_NUM_TOTAL,
0,
CONFIG_TOTAL_LEN,
TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP,
100),
// Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval
TUD_HID_DESCRIPTOR(
ITF_NUM_KEYBOARD,
0,
HID_ITF_PROTOCOL_KEYBOARD,
sizeof(desc_hid_keyboard_report),
EPNUM_KEYBOARD,
CFG_TUD_HID_EP_BUFSIZE,
// Використовуємо константу, задану в usb_descriptors.h
POLLING_INTERVAL_MS
)
};
Після ціх змін проєкт вже має компілюватись без помилок.
Тепер перейдімо до редагування головного файлу проєкту — UsbHid.cpp. Спочатку потрібно додати необхідні заголовкові файли (headers) для роботи з TinyUSB:#include "bsp/board_api.h"
#include "tusb.h"
#include "usb_descriptors.h"
Далі у функцію main() додамо виклики для ініціалізації
плати (board_init) та стека TinyUSB
(tusb_init):
int main()
{
stdio_init_all();
board_init();
tusb_init();
Після ціх змін компіляція знову буде видавати помилки.
Перша помилка буде пов’язана з відсутністю конфігурації CFG_TUSB_RHPORT0_MODE. Нам потрібно вказати, в якому режимі та на якій швидкості працюватиме USB-порт мікроконтролера.
Для цього відкрийте файл tusb_config.h і додайте в секцію "Board Specific Configuration" наступний рядок:
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED
Тут ми вказуємо дві опції:
- OPT_MODE_DEVICE — порт працюватиме в режимі пристрою (device mode), а не хоста.
- OPT_MODE_FULL_SPEED — швидкість роботи порту становитиме 12 Мбіт/с. Це максимальна швидкість, яку підтримує RP2040.
Після виправлення першої помилки компілятор повідомить про відсутність двох функцій: tud_hid_get_report_cb та tud_hid_set_report_cb. Це обов’язкові функції зворотного виклику (callbacks), які хост може використовувати для роботи зі звітами (reports).
Для нашого простого пристрою їх можна залишити порожніми. Скопіюйте реалізацію з офіційного прикладу та додайте в середину файлу UsbHid.cpp:
uint16_t tud_hid_get_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t* buffer,
uint16_t reqlen)
{
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) reqlen;
return 0;
}
void tud_hid_set_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t const* buffer,
uint16_t bufsize)
{
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) bufsize;
}
Після додавання цих функцій проєкт має нарешті успішно скомпілюватися, але пристрій все ще не виконує жодних корисних дій. Давайте додамо логіку для ініціалізації та роботи HID-клавіатури.
Перед тим, як почати надсилати дані, потрібно дочекатися, поки USB HID-інтерфейс буде повністю готовий до роботи. Для цього у функцію main() після tusb_init() додамо цикл очікування:int main()
{
stdio_init_all();
board_init();
tusb_init();
// Чекаємо, поки HID-інтерфейс буде готовий
while (!tud_hid_ready())
{
tud_task(); // Ця функція має викликатись постійно для роботи стека
sleep_ms(POLLING_INTERVAL_MS);
}
// Невелика пауза після ініціалізації для стабільності
sleep_ms(10 * POLLING_INTERVAL_MS);
Тепер реалізуємо головний нескінченний цикл. У ньому ми будемо періодично надсилати звіт про стан клавіатури. На цьому етапі ми просто надсилатимемо порожній буфер, що сигналізує системі про те, що жодна клавіша не натиснута.
// Буфер для скан-кодів 6 одночасно натиснутих клавіш
uint8_t scancodes[6] = {0};
while (true)
{
tud_task(); // Продовжуємо викликати фонову задачу TinyUSB
// Надсилаємо звіт про стан клавіатури
tud_hid_keyboard_report(
ITF_NUM_KEYBOARD, // Номер інтерфейсу
0, // Модифікатори (Shift, Ctrl, Alt)
scancodes // Буфер клавіш
);
sleep_ms(POLLING_INTERVAL_MS);
}
Якщо тепер завантажити скомпільовану прошивку на мікроконтролер, операційна система має автоматично розпізнати новий пристрій як стандартну USB-клавіатуру.
В Linux це виглядає якось так:
Останнім кроком додамо фінальний функціонал: нехай наша віртуальна клавіатура автоматично надсилає натискання клавіші «A» кожні дві секунди. Це дозволить наочно перевірити, що все працює як слід.
Для цього нам потрібно буде відстежувати час за допомогою функції board_millis().
uint32_t lastTime = 0;
uint8_t resetCounter = 0;
while (true) {
uint32_t currentTime = board_millis();
tud_task();
// Кожні 2 секунди імітуємо натискання клавіші 'A'
if (currentTime - lastTime > 2000) {
scancodes[0] = HID_KEY_A;
// Встановлюємо лічильник, щоб "відпустити" клавішу через 10 ітерацій
resetCounter = 10;
lastTime = currentTime;
}
if (resetCounter > 0) {
resetCounter --;
if (resetCounter == 0) {
// Очищуємо скан-код, що сигналізує про відпускання
scancodes[0] = 0;
}
}
tud_hid_keyboard_report(
ITF_NUM_KEYBOARD,
0,
scancodes
);
sleep_ms(POLLING_INTERVAL_MS);
}
Тепер, якщо ви завантажите прошивку і відкриєте будь-який текстовий редактор, ви побачите, як кожні дві секунди автоматично друкується літера «a». Вітаю, ми створили свій HID-пристрій!
А в мене літера почала друкуватись безпосередньо в VSCode:




Коментарі
Дописати коментар