Превращаем старую клавиатуру от ноутбука в полноценную USB-клавиатуру на RP2040 без QMK

в 13:01, , рубрики: DIY, ghosting, HID, keyboard, pico, prototyping, rp2040, ruvds_статьи, tinyusb, usb
Превращаем старую клавиатуру от ноутбука в полноценную USB-клавиатуру на RP2040 без QMK - 1

По созданию клавиатур на Хабре написано много статей, но, как правило, они подразумевают кастомизацию прошивки QMK или использование уже готовой.

Использование готовой прошивки удобно тем, что большинство шишек за вас набили её разработчики. Такой подход годится, если вы хотите получить отлаженное решение в сжатые сроки.

Для обучения и понимания работы клавиатуры, периферийных устройств и протоколов взаимодействия лучше изобрести свой велосипед. Так я и поступил.

У меня была клавиатура от ноутбука ASUS K53E, у которой немного стёрлись надписи на клавишах, я её заменил на новую, а эту положил на полку. Сейчас она мне пригодилась.

Особенность ноутбучных клавиатур в том, что они, как правило, являются пассивными устройствами, и на FPC-шлейф выведены пины для строк и столбцов матрицы клавиатуры. Чтобы сделать из неё PS/2, USB или Bluetooth-клавиатуру нужно добавить контроллер клавиатуры и написать для него прошивку. В статье рассматривается только создание USB-клавиатуры. Исходный код разработанной прошивки вы можете найти в моём Git-репозитории.

На серийно производимые клавиатуры ставят специализированные контроллеры, на которые один раз на заводе ставится прошивка, например, Holtek HT82K94E.

Существуют масочные (прошивка записывается при производстве микроконтроллера) и OTP-версии (прошивка может только один раз быть записана производителем клавиатуры) контроллеров. С такими контроллерами не поэкспериментируешь, поэтому воспользуемся универсальным решением — Raspberry Pi Pico.

Постановка задачи

Основная задача, которая стояла передо мной — как передать информацию о нажатии клавиш клавиатуры ноутбука по интерфейсу USB. При решении этой задачи затрагивается много интересных тем, которые бы я хотел осветить.

Как всегда, хочется поделиться всем, что знаешь по теме, но, увы, приходится чем-то жертвовать, чтобы сохранить повествование. Начнём с клавиатуры.

Что из себя представляет матричная клавиатура

Печатая текст, мы не задумываемся, как нажатия клавиш попадают в компьютер, где они уже обрабатываются BIOS/UEFI или операционной системой.

Большинство пользователей знают только то, что существуют PS/2, USB и Bluetooth-клавиатуры, а принципы работы и как передаются данные через эти интерфейсы, является магией.

На полноразмерных клавиатурах 104 или 105 клавиш, в зависимости от стандарта (ISO или ANSI). Производители часто немного отклоняются от стандарта, и вы можете увидеть больше или меньше клавиш. Например, на моей клавиатуре от ноутбука всего 102 клавиши.

Если подсчитаете количество пинов микроконтроллера, вы заметите, что количество их гораздо меньше сотни. Как же он способен обработать больше 100 клавиш?

Чтобы сократить количество пинов контроллера для клавиатуры, используется подход с матричной клавиатурой.

Матричные клавиатуры используются повсеместно: в старых играх типа «Ну, погоди», калькуляторах — там, где стоит задача сэкономить количество пинов.

Использование матричной клавиатуры для экономии пинов — не единственное решение, например возможен вариант со сдвиговыми регистрами. Так, например, реализован джойстик NES (Dendy).

Клавиатура называется матричной, потому что клавиши располагаются матричным способом, на пересечении столбцов и строк. Когда нажимается клавиша, она замыкает контакты строки и столбца.

Схема матричной клавиатуры из 4 строк и 5 столбцов приведена ниже.

Схема матричной клавиатуры

Схема матричной клавиатуры

Из схемы видно, что для работы с такой клавиатурой из 20 клавиш понадобится всего 9 пинов микроконтроллера.

Похожая схема существует и для клавиатуры ноутбука ASUS K53E из 8 строк и 16 столбцов, только в ней не все строки соединены через кнопки со столбцами, так как 8 строк * 16 столбцов = 128 клавиш, а у нас всего 102 клавиши.

Если подать сигнал на строку при нажатой клавише, тот же сигнал будет и на столбце. Справедливо и обратное: если сигнал будет подан на столбец при нажатии клавиши, такой же сигнал будет и на строке.

Чтобы узнать текущее состояние клавиатуры, микроконтроллер последовательно подаёт сигнал на строку и считывает сигналы на столбцах. Сопоставляя столбец и строку, можно определить, какие клавиши нажаты.

Процесс называется сканированием, отсюда и термин сканкод. В USB и Bluetooth-клавиатурах термин сканкод заменили на Usage ID.

Я намеренно упростил описание работы матричной клавиатуры, чтобы как можно раньше приступить к разработке своей клавиатуры.

Клавиатура ноутбука

Клавиатура ASUS K53E

Клавиатура ASUS K53E

Клавиатура моего ноутбука ASUS K53E является матричной клавиатурой с 24 контактами, выведенными на FPC-шлейф с графитовыми дорожками.

Обратная сторона клавиатуры и FPC-шлейф

Обратная сторона клавиатуры и FPC-шлейф

На клавиатуре нет индикаторов, поэтому часть дорожек — это выводы строк, оставшиеся — это выводы столбцов.

Моя клавиатура оказалась неразборной (по крайней мере, я не нашёл способа её разобрать недеструктивным способом), поэтому определял я разводку методом прозвонки.

Графитовые дорожки FPC-шлейфа имеют небольшое сопротивление, звуковой сигнал мультиметр не выдавал, поэтому я замерял сопротивление между двумя дорожками при нажатии клавиш. Сопротивление в 200–300 Ом означает, что клавиша замыкает контакты.

Это занятие потребовало внимания, терпения и небольших ухищрений, к которым неизбежно приходишь, выполняя монотонную, повторяющуюся работу, а я ощутил комбинаторику на практике.

Уже после того, как я построил всю матрицу своей клавиатуры, я нашёл распиновку разъёма для клавиатуры похожего ноутбука. Могу сказать, что пины я определил правильно).

Чтобы не повредить дорожки щупами мультиметра при прозвонке, а также подключить клавиатуру к микроконтроллеру на макетной плате, я использовал вот такой FPC-адаптер.

FPC-адаптер

FPC-адаптер

Выбор микроконтроллера

Не имея клавиатуры и не разобрав её, сложно сказать, какой микроконтроллер в ней используется, часто в клавиатуре микроконтроллер просто залит компаундом.

Использование специализированных микроконтроллеров клавиатуры проще и дешевле в серийном производстве клавиатур. Но я ставил целью разобраться с работой клавиатуры, поэтому использовал Raspberry Pi Pico.

При выборе микроконтроллера я исходил из того, какие у меня были микроконтроллеры, и того факта, что у микроконтроллера должно быть как минимум 24 GPIO-пина.

Raspberry Pi Pico подошёл в качестве микроконтроллера для клавиатуры, так как у него больше 26 выведенных GPIO-пинов и есть аппаратный USB.

USB можно использовать и программный, но это забирает вычислительный ресурс и потребует подключение дополнительного разъёма к GPIO-пинам.

Матрица клавиатуры

Хорошо, когда есть возможность самому спроектировать клавиатуру или хотя бы увидеть разводку дорожек. Мне же пришлось получить матрицу клавиатуры при помощи мультиметра. Ниже матрица клавиатуры приведена в наглядном виде.

Матрица клавиатуры

Матрица клавиатуры

Я выделил клавиши различными цветами в зависимости от их назначения. Клавиша Fn и клавиши Num Lock и Caps Lock , имеют поведение очень отличающееся от остальных клавиш, поэтому для них отдельные цвета.

Первая строка таблицы и первый столбец — номера пинов FPC-разъёма.

Схема подключения матричной клавиатуры

Я не ставил целью получить клавиатуру, пригодную для повседневного использования, поэтому не разрабатывал корпус и не разводил печатную плату — всё было собрано на макетной плате.

Распиновка Raspberry Pi Pico

Распиновка Raspberry Pi Pico

Для удобства отладки я использовал вторую Raspberry Pico в качестве Debug Probe. Два её пина задействовал под UART, чтобы выводить отладочные сообщения.

Ноутбучная клавиатура не содержала диодов для предотвращения ghosting, поэтому я добавил 8 диодов на выходы микроконтроллера. Эти диоды не устраняют проблему ghosting, но предотвращают короткое замыкание выходов микроконтроллера при одновременном нажатии нескольких клавиш. Кроме того, без диодов ghosting вы всё равно не увидите, а вот риск повредить выходы микроконтроллера вполне реален.

Схема, иллюстрирующая короткое замыкание выходов микроконтроллера

Схема, иллюстрирующая короткое замыкание выходов микроконтроллера

Например, если в клавиатуре, приведённой выше, вы одновременно зажмёте клавиши SW2 и SW7, а на выходе Row1 микроконтроллера “0”, на выходе Row2 микроконтроллера “1”, ток потечёт от Row2 к Row1.

Тестовый стенд, фотография которого на заглавном рисунке имеет следующую схему:

Схема тестового стенда

Схема тестового стенда

Реализация опроса матричной клавиатуры с использованием GPIO

Реализовать опрос матричной клавиатуры с использованием GPIO просто.

В микроконтроллере количество GPIO-пинов должно равняться или быть больше, чем количество выводов матричной клавиатуры.

Одна часть выводов является строками, оставшаяся строками. Выбираем способ сканирования по строкам. На пины строк микроконтроллера подаём сигнал, а на пинах столбцов считываем и определяем, какие клавиши нажаты.

Схему можно собрать и сконфигурировать так, что нажатая клавиша будет представляться «1», а можно — «0». Обычно используют последний способ. Для этого необходимо подтянуть к питанию входные пины микроконтроллера с использованием подтягивающих резисторов. Как правило, для работы клавиатуры будет достаточно подтягивающих резисторов, встроенных в микроконтроллер

В случае RP2040 код инициализации GPIO выглядит следующим образом:

const uint OUT_PINS[ROWS] = {2,3,4,5,6,7,8,9};

const uint IN_PINS[COLS] = {28, 27, 26, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10};

void scanner_init(void) {
    for (int i = 0; i < COLS; i++) {
        gpio_init(IN_PINS[i]);
        gpio_set_dir(IN_PINS[i], GPIO_IN);
        gpio_set_pulls(IN_PINS[i], true, false);
    }

    for (int i = 0; i < ROWS; i++) {
        gpio_init(OUT_PINS[i]);
        gpio_set_dir(OUT_PINS[i], GPIO_OUT);
        gpio_put(OUT_PINS[i], 1);
    }
}

Код определения нажатой клавиши:

void scan(void) {
        ks.changed = false;

        uint16_t* temp = ks.new_keys_state;
        ks.new_keys_state = ks.old_keys_state;
        ks.old_keys_state = temp;        
        for (int i = 0; i < ROWS; i++) {
            gpio_put(OUT_PINS[i],0);
            uint16_t state = 0; 
            sleep_us(10);
            for (int j = 0; j < COLS; j++) {
               if (!gpio_get(IN_PINS[j])) {
                    state = state | (1<<j);
                    printf("%d n", matrix[i][j]);
               }
            }
            if (state != ks.old_keys_state[i]) {
                ks.changed = true;
            }
            ks.new_keys_state[i] = state;
            gpio_put(OUT_PINS[i],1);
        }
}

Мы зная индекс строки и индекс столбца, из матрицы извлекаем код нажатой клавиши. При опросе мы получаем текущее состояние клавиатуры — то есть знаем, какие клавиши нажаты в данный момент.

Однако для взаимодействия с хостом необходимо преобразовывать это состояние в события. Передавать данные имеет смысл только при изменении состояния клавиш.

Это реализуется путём сохранения предыдущего состояния клавиатуры и сравнения состояния с текущим. На основе различий определяется, какие события (нажатия или отпускания клавиш) необходимо отправить хосту.

Мы знаем, какие клавиши нажимаются на клавиатуре, теперь нужно разобраться, как передать информацию о нажатых клавишах по протоколу USB.

USB

Как реализовано взаимодействие с USB-устройствами, для многих является загадкой.

Если работу устройства через UART-интерфейс можно понять очень быстро, то понимание USB требует больше времени и больше ментальных усилий.

Когда читаешь спецификацию USB, чётко видишь overengineering, присущий технологиям конца 90-х — начала 2000-х годов.

При разработке собственной USB-клавиатуры часть завесы таинственности USB исчезает.

У USB-разъёма 4 контакта: питание, земля и два для передачи данных.

Распиновка USB разъёма

Распиновка USB разъёма

Микроконтроллер упаковывает информацию о нажимаемых клавишах для передачи по USB. Микроконтроллер получает питание от компьютера и передаёт/получает данные компьютеру по двум линиям D+ и D-.

Для разработки USB-клавиатуры не нужно досконально знать спецификацию USB. Многие абстракции из спецификаций USB и HID реализованы в библиотеке TinyUSB.

При сопряжении устройств нужно понимать роли, которые они выполняют. Таких ролей две: хост и периферийное устройство.

В случае USB-клавиатуры компьютер является хостом, клавиатура — периферийным устройством.

USB-устройство определяется его классом, наличием и количеством поддерживаемых конфигураций и интерфейсов, количеством и видами конечных точек.

Класс USB-устройства определяет назначение и функции устройства. Клавиатура относится к классу HID (Human Interface Device).

Обычно класс задаётся не для устройства, а для интерфейса, так как, например, одно физическое устройство может поддерживать несколько интерфейсов и быть, например, клавиатурой и мышью (USB-dongle для беспроводных мыши и клавиатуры). Такое устройство называется составным.

Конфигурации используются для логической группировки интерфейсов, поддерживаемых периферийным устройством интерфейсов. Но часто у устройства только одна конфигурация с одним или несколькими интерфейсами.

Для обмена данными и управляющей информацией между хостом и USB‑устройством используются конечные точки; управляющие запросы идут через control endpoint, а данные — через bulk, interrupt или isochronous endpoints

Для передачи данных от устройства к хосту используется IN Endpoint, для получения данных от устройства OUT Endpoint. Каждое периферийное устройство должно иметь особую конечную точку control endpoint с номером 0, через которую идёт обмен управляющей информацией, то есть конечная точка работает и на вход, и на выход.

В случае USB-клавиатуры контроллер клавиатуры помещает информацию о нажатых клавишах в interrupt IN endpoint, а управляющие сигналы для светодиодов Caps Lock и Num Lock помещаются хостом в control endpoint 0 (может, для вас будет открытием, но эти светодиоды включает не клавиатура, а компьютер).

Чтобы хост мог работать с периферийным устройством, оно должно как-то представиться. Устройство описывается древовидной структурой дескрипторов, которая передаётся хосту при подключении и содержит информацию об устройстве, конфигурациях, интерфейсах и конечных точках.

В Linux эту информацию можно посмотреть при помощи команды lsusb.

lsusb -v -d VendorId:ProductId

VendorId — уникальный идентификатор, который выдаётся за плату производителю USB-устройств организацией USB-IF. ProductId — идентификатор, который присваивает производитель модели устройства.

Рассмотрим более подробно дескрипторы, чтобы у вас появилось понимание того, что выводит команда lsusb -v -d.

Дескрипторы

Подключение USB-устройства к порту хоста инициирует Enumeration of USB devices. В процессе Enumeration хост получает древовидную структуру дескрипторов.

Благодаря USB-дескрипторам хост имеет информацию о том, что из себя представляет устройство и как работать с устройством.

Дескрипторы организованы в иерархию, которая приведена на рисунке.

Иерархия USB-дескрипторов

Иерархия USB-дескрипторов

С точки зрения программиста, USB-дескриптор — это структура данных.

В строковых дескрипторах содержится текстовая информация о том, кто является производителем устройства, название устройства, текстовые описания интерфейсов и конфигураций, серийный номер, а также на каких языках доступна эта информация.

Чтобы получить значение строкового дескриптора, хост должен передать индекс дескриптора и код языка.

Строковый дескриптор — это массив байтов, нулевой элемент которого содержит размер массива в байтах, следующий элемент — это тип дескриптора (0x03), далее — Unicode-строка в кодировке UTF-16LE.

Строковый дескриптор с индексом 0 содержит массив из поддерживаемых языков вместо Unicode-строки.

USB-устройство должно иметь:

  • дескриптор устройства,

  • дескриптор конфигурации,

  • дескриптор интерфейса,

  • дескриптор конечной точки

  • строковые дескрипторы.

Для клавиатуры, которая является HID-устройством, ещё необходимы HID class descriptor и HID report descriptor, описывающий формат данных, которыми будет обмениваться клавиатура с хостом.

HID class descriptor и hid report descriptor — не одно и то же. Hid class descriptor декларирует, что USB-интерфейс представляет HID-устройство, а hid report descriptor подробно описывает возможности этого устройства.

При использовании библиотеки TinyUSB работа с дескрипторами упрощается.

HID

HID описан в объёмной спецификации, но нам из неё нужна лишь небольшая часть.

Чтобы USB-устройство было распознано операционной системой как USB-клавиатура, в процессе USB-enumeration оно должно вернуть корректные HID class descriptor и HID report descriptor.

HID-устройства могут использовать либо Boot Protocol, либо Report Protocol. Чтобы устройство могло работать в BIOS/UEFI, оно должно поддерживать Boot Protocol.

Вот пример корректного HID class descriptor, который выводится командой lsusb:

  Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      1 Boot Interface Subclass
      bInterfaceProtocol      1 Keyboard
      iInterface              0

Можно HID report descriptor для клавиатуры написать более корректный и свой, а можно воспользоваться стандартным, который предоставляет TinyUSB. Я воспользовался вторым вариантом.

Использование USB HID накладывает определённые ограничения. В частности, структура HID Input Report ограничивает количество одновременно передаваемых нажатий: до 6 обычных клавиш и до 8 модифицирующих (например, Shift, Ctrl, Alt).

Модифицирующие клавиши передаются в отдельном байте HID Input Reportа. Для каждой клавиши отведён один бит. Поэтому они обрабатываются отдельно от основных и упаковываются в один байт.

Отсылка кодов от клавиатуры — это HID Input Report, приём управляющих сигналов для светодиодов клавиатуры — HID Output Report

Структура HID Input Report и HID Output Report

Структура HID Input Report и HID Output Report

Отладка USВ

USB — высокоскоростной протокол. Даже для медленного USB Full Speed, используемого клавиатурой, не на каждом осциллографе можно посмотреть сигнал. Да и осциллограф даст только форму электрического сигнала, а больший интерес представляет именно декодированный сигнал.

Полезные команды Linux для отладки USB

  1. Посмотреть USB-контроллеры в системе:

    lspci -vnn | grep -Ei 'usb|xhci'
    
  2. Посмотреть шины USB:

    lsusb -t
    

    или

    ls /sys/bus/usb/devices/usb*
    

Использование Wireshark для анализа USB-траффика

На высоком уровне работу USB-протокола можно посмотреть на Linux-хосте, используя Wireshark. В этом случае вы наблюдаете трафик в виде USB Request Blocks. USB Request Block по смыслу близок IP-пакету.

Для исследования USB Request Block с помощью Wireshark должен быть загружен модуль ядра usbmon.

  1. Убеждаемся, что usbmon загружен:

    lsmod | grep usbmon
    
  2. Если модуль не загружен, загружаем:

    sudo modprobe usbmon
    
  3. Определяем номер USB-шины и номер нашего устройства. Подключаем устройство и вводим команду:

    lsusb
    
  4. Запускаем Wireshark на сетевом интерфейсе, который был создан для USB-шины, и где располагается наше устройство.

    sudo wireshark -i usbmon1 -k
    

    usbmon1 - потому что номер USB-шины 001.

Чтобы не видеть пакеты от других устройств, можно в Wireshark использовать фильтры или можно просто минимизировать количество подключённых USB-устройств, а исследуемое устройство подключать непосредственно к USB-порту хоста, не используя внешние USB-хабы.

В моём ноутбуке USB-трафик Bluetooth-модуля мешал просмотру USB Request Blocks клавиатуры в Wireshark, поэтому я отключил Bluetooth-модуль в Linux.

При просмотре трафика на уровне USB Request Blocks вы увидите не всю информацию, передаваемую по протоколу USB, например, вы не увидите полную информацию USB-фреймов, которые в случае USB Full Speed передаются каждую миллисекунду.

На низком уровне работу протокола USB можно посмотреть с использованием логического анализатора, который необходимо подключить к линиям D+ и D-.

Типичный алгоритм работы USB-клавиатуры

  1. Сканируется матрица клавиатуры.

  2. Определяется текущее состояние клавиш.

  3. Сравнивается текущее состояние с предыдущим и на основании этого формируется HID-репорт.

  4. HID-репорт отсылается хосту по USB с использованием библиотеки, например, TinyUSB.

Чтобы можно было отослать HID-репорт с помощью TinyUSB, нужно написать немного типичного кода, который потом будет использован TinyUSB для реализации механизмов USB, в частности USB Enumeration. Код с описанием дескрипторов имеет декларативный характер.

Реализация взаимодействия с хостом по USB при помощи библиотека TinyUSB

Библиотека TinyUSB сильно упрощает разработку USB-прошивки для клавиатуры. Библиотека уже включена в состав Pico SDK, поэтому ничего дополнительно устанавливать не нужно.

В официальных примерах Pico SDK и в репозитории TinyUSB есть готовые шаблоны для HID-устройств, в том числе для клавиатуры.

Основные шаги, которые нужно выполнить:

  1. Подключить TinyUSB в системе сборки в файле ``CMakeLists.txt:

    target_link_libraries(pico_usb_keyboard
    pico_stdlib
    pico_unique_id
    tinyusb_device
    tinyusb_board)
    
  2. Описать USB-дескрипторы (device, configuration, HID report и строковые).

    #define USB_PID   0x0001
    #define USB_VID   0x1209
    #define USB_BCD   0x0200
    
    //--------------------------------------------------------------------+
    // Device Descriptors
    //--------------------------------------------------------------------+
    tusb_desc_device_t const desc_device =
    {
        .bLength            = sizeof(tusb_desc_device_t),
        .bDescriptorType    = TUSB_DESC_DEVICE,
        .bcdUSB             = USB_BCD,
        .bDeviceClass       = 0x00,
        .bDeviceSubClass    = 0x00,
        .bDeviceProtocol    = 0x00,
        .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,
    
        .idVendor           = USB_VID,
        .idProduct          = USB_PID,
        .bcdDevice          = 0x0100,
    
        .iManufacturer      = 0x01,
        .iProduct           = 0x02,
        .iSerialNumber      = 0x03,
    
        .bNumConfigurations = 0x01
    };
    
    //--------------------------------------------------------------------+
    // HID Report Descriptor
    //--------------------------------------------------------------------+
    
    uint8_t const desc_hid_report[] =
    {
     TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD         ))
    };
    
    uint8_t const * tud_hid_descriptor_report_cb(uint8_t instance)
    {
      (void) instance;
      return desc_hid_report;
    }
    
    //--------------------------------------------------------------------+
    // Configuration Descriptor
    //--------------------------------------------------------------------+
    
    enum
    {
      ITF_NUM_HID,
      ITF_NUM_TOTAL
    };
    
    #define  CONFIG_TOTAL_LEN  (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN)
    
    
    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_HID,
        0,
        HID_ITF_PROTOCOL_KEYBOARD,
        sizeof(desc_hid_report),
        0x81,
        CFG_TUD_HID_EP_BUFSIZE,
        5)
    };
    
    #define USB_STR_DESC(name, val) 
    struct {
         uint8_t  len;
         uint8_t  type;
         uint16_t arr[sizeof(u##val)/2-1];
     } _##name = {
         .len  = sizeof(u##val),
         .type = 0x03,
         .arr  = u##val
     };
     uint16_t const * const name = (uint16_t*) &_##name
    
     uint16_t lang[] = (uint16_t []) { 4, 0x3, 0x09, 0x04 };
     USB_STR_DESC( manufacturer, "Home Ltd.");
     USB_STR_DESC( product, "USB Pico-based Keyboard");
     USB_STR_DESC( serial, "1234");
    
     const uint16_t * string_desc_arr [] =  { lang, manufacturer, product, serial};
    
    
  3. Настроить конфигурацию библиотеки (tusb_config.h).

  4. Реализовать callback-функции, которые TinyUSB будет вызывать автоматически.

    uint8_t const * tud_descriptor_device_cb(void)
    {
      return (uint8_t const *) &desc_device;
    }
    
    uint8_t const * tud_descriptor_configuration_cb(uint8_t index)
    {
      (void) index;
      return desc_configuration;
    }
    
    uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
    {
      (void) langid;
      return string_desc_arr[index];
    }
    
    void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_t len)
    {
      (void) instance;
      (void) len;
    }
    
    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;
    }
    

Как видно из исходного кода, нужно только уметь верно использовать макросы и прописывать код колбек-функции.

Проверка работы клавиатуры

Удобно проверять работу клавиатуры при помощи онлайн-сервисов, например, en.key-test.ru

Дополнительные вопросы реализации прошивки микроконтроллера

Ghosting

Если в клавиатуре не используется диод для каждой клавиши, то пользователь будет сталкиваться с проблемой ghostingа. Ghosting — это когда контроллер распознаёт при сканировании клавиши, которые не нажимали.

В моей клавиатуре, если убрать логику программного подавления ghosting и, например, одновременно нажать клавиши ‘6’, ‘7’, ‘0’, то будет распознано нажатие клавиши ‘P’, которую не нажимали.

Для решения таких проблем в прошивку клавиатуры добавляется antighosting-логика.

Самая простая реализация antighosting-логики - игнорировать нажатия всех клавиш, если при сканировании встречается комбинация с ghosting клавишей. Для упрощения прошивки я использовал именно этот вариант.

Дребезг контактов

Также в случае быстрого сканирования клавиатуры для корректного определения нажатой клавиши может оказывать влияние дребезг контактов, когда при нажатии на клавишу генерируется последовательность случайных замыканий размыканий.

Существуют и аппаратные, и программные варианта решения этой проблемы. Зачастую, если у вас не геймерская клавиатура, достаточно просто поставить небольшую задержку перед считыванием столбца матрицы клавиатуры.

Выводы

Ну вот мы и получили работающую USB-клавиатуру. Многие вопросы остались открытыми, чтобы была пища для ума и интерес к дальнейшему изучению.

Например, я не рассказывал, как можно избавиться от ограничения 6 одновременно нажатых клавиш, накладываемое HID.

Не рассказал, как реализовывается обработка клавиши Fn, которая присутствует на многих клавиатурах. Её особенностью является то, что у неё нет Usage ID.

В статье USB рассмотрен в упрощённом виде, достаточном для того, чтобы можно было собрать свою USB-клавиатуру и понимать, какая информация передаётся и что из себя представляет. Я не углублялся в низкоуровневые основы USB, хотя там очень много интересного.

Как видите, вроде бы простая тема создания клавиатуры имеет множество нюансов и служит хорошей основой для получения практических основ работы с протоколом USB и не только.

Разработанная прошивка и электронная схема позволяют разобраться, как работает клавиатура по протоколу USB.

Естественно, что в случае ежедневной эксплуатации клавиатуру нужно поместить в корпус. Также логично использовать более дешёвый и компактный RP2040 Zero.

© 2026 ООО «МТ ФИНАНС»

Автор: artyomsoft

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js