Перейти к содержанию

USB-HID на STM32F103. Примочка для управления треками и громкостью

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

В первую очередь, определимся, что нам нужно от готового устройства:

  • Поворотная голова для регулировки громкости
  • Кнопки Пуск/Пауза, Следующий трек, Предыдущий
  • Какая-нибудь световая индикация – что устройство вообще еще живое
  • Питание от USB
  • Отсутствие драйверов
  • Небольшой размер – по желанию

До глобального кризиса микросхем (2020-2021) я закупал пачками STM32F103 (в народе “BluePill“). Весной 2021 цена BluePill’ы стала на 30% выше, чем BlackPill’ы. Которая сделана намного более аккуратно, с TypeC и с STM32F4 на борту.

Но у меня завалялось много F103, даже в виде чипов, поэтому решил делать на этом камушке.

В первую очередь – нужно развести плату. Прошли те веселые годы, когда позволить себе печатку промышленного качества могли лишь богатые любители попаять. Более того, даже необязательно использовать какой-нибудь полу-легальный САПР для проектирования своих поделок. Уже давно есть отличный сервис проектирования печатных плат EasyEda, который интегрирован с JLCPCB. EasyEda есть и оффлайн-версия, которую я использую – лагов намного меньше. Да и весит она примерно 500 Мб. Да и свободные проекты можно открывать и копировать – как в гитхабе. Это не реклама – сервис действительно хорош. Особенно наличием огромной библиотеки ПОСТОЯННО ПОПОЛНЯЕМЫХ компонентов. Порог вхождения для комфортного использования, требовательность к ресурсам ПК намного ниже, чем в Altium. Altium хорош, но для быстрых самоделок для неспециалиста, по моему мнению, слишком серьезен.

Итак. Вся суть платы – упрощенная BluePill с энкодером и какой-никакой защитой по USB.

На данном МК есть возможность аппаратной работы с энкодером. Выбирается тут:

Более подробное видео по подключению энекодера

https://www.youtube.com/watch?v=xqzWQgpqHmI

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

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

Посе успешной генерации кода кубом начинаем его редактировать. Нас интересуют следующие файлы

Куб генерирует рабочий дескриптор устройства – мышь, который можно сразу же использовать. Мы будем делать клавиатуру. Причем, добавим нормальную клавиатуру + спец-клавиши. Таким образом можно совмещать несколько устройств в одном – например мышь+клавиатура и т.д.

В файле usbd_hid.c найдем строку

__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE]  __ALIGN_END = {

Именно в данном массиве описывается что у нас за устройство такое. Очень подробно разбирать каждую строку не буду – это уже сделано на многих ресурсах(например, хабр). Стоит лишь обратить внимание что РАЗНЫЕ УСТРОЙСТВА ОТЛИЧАЮТСЯ ПО Report ID. И описано более 1 устройства, то добавляется самым первым байтом его идентификатор.

Для составления собственного дескриптора имеет смысл воспользоваться программой HID Descriptor Tool.

HID Descriptor Tool

Мой дескриптор следующий (спасибо доброму человеку, который его сделал и дописал комментарии. В первый раз такое было тяжело переварить):

__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_CUSTOM_REPORT_DESC_SIZE]  __ALIGN_END = {
  // 78 bytes
  0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  0x09, 0x06,        // Usage (Keyboard)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x01,        //   Report ID (1)
  0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x08,        //   Report Count (8)
  0x19, 0xE0,        //   Usage Minimum (0xE0)
  0x29, 0xE7,        //   Usage Maximum (0xE7)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x95, 0x03,        //   Report Count (3)
  0x75, 0x08,        //   Report Size (8)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x64,        //   Logical Maximum (100)
  0x05, 0x07,        //   Usage Page (Kbrd/Keypad)
  0x19, 0x00,        //   Usage Minimum (0x00)
  0x29, 0x65,        //   Usage Maximum (0x65)
  0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              // End Collection
  0x05, 0x0C,        // Usage Page (Consumer)
  0x09, 0x01,        // Usage (Consumer Control)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x02,        //   Report ID (2)
  0x05, 0x0C,        //   Usage Page (Consumer)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x08,        //   Report Count (8)
  0x09, 0xB5,        //   Usage (Scan Next Track)
  0x09, 0xB6,        //   Usage (Scan Previous Track)
  0x09, 0xB7,        //   Usage (Stop)
  0x09, 0xB8,        //   Usage (Eject)
  0x09, 0xCD,        //   Usage (Play/Pause)
  0x09, 0xE2,        //   Usage (Mute)
  0x09, 0xE9,        //   Usage (Volume Increment)
  0x09, 0xEA,        //   Usage (Volume Decrement)
  0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,              // End Collection
};

Тут стоит остановиться и внимательно посмотреть на комментарии. Видно, что тут есть Report ID 1 и Report ID 2. Это значит, что при вызове функции отправки данных в USB, я добавлю первым байтом этот ID. Устройство Report ID 1 – это очень усечённая клавиатура.

Я не стал менять название массива, а лишь поменял дефайн размера массива на соответствующий данному дескриптору.

Везде в данном файле меняем все HID_MOUSE_REPORT_DESC_SIZE на HID_CUSTOM_REPORT_DESC_SIZE.

Так же стоит найти данные строчки (их 3)

  0x00,                                               /* nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse */

И выставить в 0x01. У нас жеж клавиатура.

В файле usbd_hid.h добавляем новый дефайн, который отвечает за размер нового массива

define HID_CUSTOM_REPORT_DESC_SIZE 78U

Так же имеет смысл увеличить максимальный размер пакета. (Максимум 0x40)

#define HID_EPIN_SIZE                              0x08U

Кстати, все ваши изменения CubeMX сотрёт при перегенерации проекта. Так что аккуратней с этим. На этом работа с модификацией HID устройства закончена. Можно приступать непосредственно к коду. Переходим в main.c. После #include’ов добавляем описание структуры.

struct mediaHID_t {
    uint8_t id; // Report ID
    uint8_t keys; //Key 
  };

Так, с Report ID, более менее понятно, а как понять что писать в key? Добавим дефайнов.

#define USB_HID_SCAN_NEXT 0x01
#define USB_HID_SCAN_PREV 0x02
#define USB_HID_STOP      0x04
#define USB_HID_EJECT     0x08
#define USB_HID_PAUSE     0x10
#define USB_HID_MUTE      0x20
#define USB_HID_VOL_UP    0x40
#define USB_HID_VOL_DEC   0x80

Как можно заметить, каждый из дефайнов для управления звуком – это 1, которая сдвигается влево (если смотреть сверху вниз). То есть функциональное назначение действия кнопки это единичка в массиве. Если записать две рядом – то и будут нажаты 2 кнопки.

Я думаю, следующая картинка поможет пониманию того, что я написал.

Добавим еще дефайнов для нашей мини-клавиатуры

// USB keyboard codes
#define USB_HID_MODIFIER_LEFT_CTRL   0x01
#define USB_HID_MODIFIER_LEFT_SHIFT  0x02
#define USB_HID_MODIFIER_LEFT_ALT    0x04
#define USB_HID_MODIFIER_LEFT_GUI    0x08 // (Win/Apple/Meta)
#define USB_HID_MODIFIER_RIGHT_CTRL  0x10
#define USB_HID_MODIFIER_RIGHT_SHIFT 0x20
#define USB_HID_MODIFIER_RIGHT_ALT   0x40
#define USB_HID_MODIFIER_RIGHT_GUI   0x80
#define USB_HID_KEY_L     0x0F

Обязательно стоит указать, что хэндлер usb уже где-то есть.

extern USBD_HandleTypeDef hUsbDeviceFS;

А он объявлен в файле usbdevice.c

В мэйне создаем объявленную ранее структуру и выставляем id в 2. Ща будем звуком управлять

int main(void)
{
  /* USER CODE BEGIN 1 */
	struct mediaHID_t mediaHID;
	  mediaHID.id = 2;
	  mediaHID.keys = 0;

В цикле делаем следующее:

while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
  if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET){
        mediaHID.keys = USB_HID_VOL_UP;
        USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&mediaHID, sizeof(struct mediaHID_t));
        HAL_Delay(20);
        mediaHID.keys = 0;
        USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&mediaHID, sizeof(struct mediaHID_t));
        HAL_Delay(2000);
  }
}

Что тут? Если нажата кнопка – отправим сообщение ” нажата кнопка прибавить звук”, чуть ждем, отправим сообщение “кнопка прибавить звук отжата”.

Заливаем, подключаем. Если всё сделано правильно, то в диспетчере устройств будет добавлено новое HID-устройство. Пробуем нажать кнопку – звук должен увеличится.

Далее ваш полет фантазии ничем не ограничен. У меня получилось такое

Ссылка на GitHub: (Только как пример – у меня был энкодер с приколом, пришлось делать всякие гадости, чтобы он заработал)

Литература:

https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/

Если ссылка отвалится – сохранил в pdf

https://programel.ru/files/Tutorial about USB HID Report Descriptors _ Eleccelerator.pdf

Лучшее, что есть по дескрипторам человеческим языком

https://github.com/FreeJoy-Team/FreeJoy

Отличный конфигуратор устройств HID для STM32

http://microsin.ru/content/view/1107/44/

https://www.usbmadesimple.co.uk/

Замечательная литература для более глубокого изучения работы USB

Comment

programel