STM32 — USB Custom HID

Эта статья — некоторое продолжение UsbLibrary — C# USB HID Library, в которой довольно много недочётов и ошибок (хотя никто и не написал о багах, кек), но основной посыл же — описать создание USB Custom HID на STM32 с использованием HAL.

В чём проблема-то?

Начну с низкоуровневой части. Сколько я ни видел статей на эту тему, ни в одной так нормально и не разобраны были следующие моменты:

  • Report ID — когда он задаётся, нужен ли он вообще и на что влияет;
  • Может ли превышать размер Report величину в 64 байта (хардварное ограничение, буфер endpoint’a), и что происходит в этом случае;
  • Что происходит, когда в дескрипторе задано одно значение Report ID, а мы пытаемся отправить репорт с другим;
  • Должен ли содержаться Report ID в формируемом буфере репорта при отправке;
  • Нужно ли учитывать Report ID (+ 1 байт) в при указании размера репорта в дескрипторе.

Вот по этим пунктам я постараюсь вкратце пробежаться, объяснить, что и как необходимо сделать, чтобы устройство работало корректно, и показать это на практике. Базовые понятия работы USB HID устройств я не буду объяснять, про это уже много и доступно писали до меня.

Теперь про саму библиотеку. В оригинальной и потом чуть допиленной мною было ещё очень немало багов и недочётов, которые мне довольно сильно мешали в моём неспешном проекте CV Meter (да-да, он НЕ заброшен, и уже много чего поменялось со времён той статьи, постараюсь опубликовать наработки в виде статьи + новой версии в ближайшие месяцы), что только добавляло непонимания или горения пятой точки от того, что работало оно совсем не так, как задумывалось. И в какой то момент меня это всё достало, что и вылилось в полном пересмотре библиотеки, написании своего тестового ПО (UsbHidDemonstrator от ST меня также не устраивал своими ограничениями по VID\PID и логикой работы).

Firmware — STM32

Для примера прошивки я возьму стандартную «чёрную пилюлю» на STM32F401CCU6 и дисплей ST7735S с SPI интерфейсом, для теста этого хватит с головой:

Открываем STM32CubeMX и включаем тактирование, SPI, USB и несколько GPIO на выход и один на вход для кнопки:

Тактирование у меня от кварцевого резонатора 16 МГц, да, он перепаян (для своего удобства, т. к. в проектах часто ставится именно такой номинал), на плате же изначально идёт 25 МГц, учитывайте это (просто поменяйте настройки тактирования):

SPI никаких особенностей не имеет, просто Master Transmit.

USB — включаем Device без дополнительных фич:

Настройки интерфейса можно не трогать, т. к. ничего дополнительного не потребуется:

Не забываем включить реализацию различных классов USB устройств (библиотека от ST):

И выбираем именно USB Custom HID (а не просто USB HID):

В настройках нас интересуют три параметра:

  • CUSTOM_HID_FS_BINTERVAL — 1 мс, это интервал желаемого опроса устройства со стороны ПК;
  • USBD_CUSTOM_HID_REPORT_DESC_SIZE — это размер дескриптора, который будет описывать возможности устройства (скорее всего, вы будете редактировать его размер уже позже генерации проекта, не столь важно, какое значение поставить сейчас);
  • USBD_CUSTOMHID_OUTREPORT_BUF_SIZE — это максимальный размер буфера для принимаемых из ПК репортов (т. е. необходимо выставить максимальный размер Output или Feature репорта + 1 байт на Report ID, иначе устройство не сможет принять репорт).

Далее заходим в настройки дескриптора описания устройства:

Это уже просто настройки представления устройства в ОС, VID, PID (если вы делаете серийное устройство и не хотите потом проблем, то VID необходимо официально получить, став членом usb.org, уплачивая ежегодные взносы, или купить идентификатор VID за 6000$, представленные же выше VID, PID чисто для примера).

В целом и всё, можно сгенерировать проект:

Главное, структура проекта — Basic, стек и кучу я не менял, стандартных размеров хватит более чем, а также — опционально уже — тип проекта Makefile, т. к. я буду работать с прошивкой в Visual Studio Code.

Теперь об небольших изменениях в сгенерированном проекте. Открываем файл Src\usbd_custom_hid_if.c и добавляем объявление на обработчик входящих репортов:

/* USER CODE BEGIN INCLUDE */
extern void Example_ParseReport(uint8_t *report);
/* USER CODE END INCLUDE */

Далее редактируем (точнее, добавляем, изначально дескриптор пуст) дескриптор возможностей устройства:

/** Usb HID report descriptor. / __ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END = { / USER CODE BEGIN 0 */
0x06, 0x00, 0xff, // USAGE_PAGE (Generic Desktop)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xa1, 0x01, // COLLECTION (Application)
// + 7

0x85, 0x01, // REPORT_ID
0x09, 0x01, // USAGE
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 20, // REPORT_COUNT
0xb1, 0x82, // FEATURE (Data,Var,Abs,Vol)
// + 15

0x85, 0x01, // REPORT_ID
0x09, 0x01, // USAGE
0x95, 10, // REPORT_COUNT
0x91, 0x82, // OUTPUT (Data,Var,Abs,Vol)
// + 8

0x85, 0x02, // REPORT_ID
0x09, 0x02, // USAGE
0x75, 0x08, // REPORT_SIZE (8)
0x95, (33-1), // REPORT_COUNT
0x81, 0x82, // INPUT (Data,Var,Abs,Vol)
// + 10

// + 1
/* USER CODE END 0 / 0xC0 / END_COLLECTION */
};

Примечание: для удобства я пишу рядом с каждым объявлением типа репорта количество байт, которое содержит это описание, чтобы потом можно было быстро посчитать и отредактировать объявление USBD_CUSTOM_HID_REPORT_DESC_SIZE .

Важное замечание: в описании Input Report я написал размер (33-1), как напоминалку, т. е. 33 байта — это полный размер репорта, но -1 байт (Report ID) = 32 байта полезная нагрузка. Аналогично всё работает и для остальных типов. Ниже будет объяснено, почему так, и в тестовом приложении мы увидим настоящий размер репортов.

И ближе к концу этого файла вставляем вызов обработчика репортов:

static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
/* USER CODE BEGIN 6 */
UNUSED(event_idx);
UNUSED(state);
/* Start next USB packet transfer once data processing is completed */
USBD_CUSTOM_HID_ReceivePacket(&hUsbDeviceFS);
USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef*)hUsbDeviceFS.pClassData;

Example_ParseReport((uint8_t *)&hhid->Report_buf[0]); // Report_buf[0] = Report ID
return (USBD_OK);
/* USER CODE END 6 */
}

Примечение: в обработчике CUSTOM_HID_OutEvent_FS параметр event_idx является Report ID полученного репорта, который содержится в hhid->Report_buf[0].

В файле Src\usbd_desc.c можно отредактировать VID, PID, описания устройства.

В файле Inc\usbd_conf.h содержится, собственно, USBD_CUSTOM_HID_REPORT_DESC_SIZE, задающее размер дескриптора возможностей, а также можно проверить значения USBD_CUSTOMHID_OUTREPORT_BUF_SIZE и CUSTOM_HID_FS_BINTERVAL.

И последнее, в файле Middlewares\ST\STM32_USB_Device_Library\Class\CustomHID\Inc\usbd_customhid.h содержатся объявления хардварных endpoint’ов на приём и отправку, меняем их на максимально возможные (для Full Speed USB):

define CUSTOM_HID_EPIN_ADDR 0x81U
define CUSTOM_HID_EPIN_SIZE 64u//0x02U

define CUSTOM_HID_EPOUT_ADDR 0x01U
define CUSTOM_HID_EPOUT_SIZE 64u//0x02U

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

Также стоит отметить два важных замечания по поводу Report ID, которые часто упускают в других туториалах:

  • Если в дескрипторе НЕ указано значение Report ID для какого-либо типа репорта, то будут пропускаться все, а если указано, то репорты, не соответствующие этому значению, для конкретного типа будут игнорироваться автоматически, как на стороне устройства, так и на стороне ПК.
  • Исходя из первого замечания, выходит, что часто, не указывая значение Report ID для любого типа репорта, можно подумать, что выделяемый буфер (к примеру, 32 байта) полностью и есть данные, которые отправляются\принимаются, но на самом деле первый байт в репорте всегда = Report ID (т. е. фактически полезной нагрузки в репорте 31 байт), и если он не используется по своему прямому назначению, то да, его как бы можно использовать в качестве байта данных.

Ну и файл Src\main.c, добавляем обработчик репортов с дополнительными переменными:

volatile uint16_t count = 0;
volatile uint16_t color_font = COLOR565_AQUA;
volatile uint16_t color_back = COLOR565_BLACK;
extern USBD_HandleTypeDef hUsbDeviceFS;
void Example_ParseReport(uint8_t *report)
{
count++;
if(report[0] == 0x01) // Report ID
{
color_font = ((report[3] & 0x1f) | ((report[2] & 0x3f) << 5) | ((report[1] & 0x1f) << 11)); // B5 G6 R5
color_back = ((report[6] & 0x1f) | ((report[5] & 0x3f) << 5) | ((report[4] & 0x1f) << 11)); // B5 G6 R5 }
}

И в главном цикле добавляем отображение данных на экране (счетчики отправленных и принятых репортов, а также применение цветов на тестовую строку) и обработку нажатия на кнопку (отправка Input Report):

while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
ST7735_PutStr(30, 0, "TEST STRING", color_font, color_back); ST7735_PutDec(60, 0, count, 4, COLOR565_FIRE_BRICK, COLOR565_BLACK); ST7735_PutDec(90, 0, usr_count, 4, COLOR565_GREEN, COLOR565_BLACK); HAL_Delay(250);

if(HAL_GPIO_ReadPin(USR_BT_GPIO_Port, USR_BT_Pin) == GPIO_PIN_RESET) {
uint8_t report[33];
usr_count++;
report[0] = 0x02;
report[1] = (usr_count >> 8);
report[2] = (usr_count & 0xFF); USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&report, 33);
}
}
/* USER CODE END 3 */

Минимально-простой функционал для тестирования реализован.

Software — UsbHid

Собственно, библиотека, написанная на C#, предназначена для работы с USB HID устройствами, которую можно использовать и в WinForms и в WPF проектах, она претерпела следующие изменения:

  • Исправлен баг, когда не работает поиск устройств, если собрать программу под x64 или All Platforms;
  • Исправлен баг, когда размеры репортов различаются, библиотека зависала при попытке отправить репорт (т. е., допустим, Input = 16 байт, Output = 8 байт, при попытке отправить Output Report получаем зависание, что я долгое время не понимал, и думал, что это баг на стороне устройства);
  • Добавлен функционал получения списка доступных USB HID устройств;
  • Пересмотрен код отправки репортов, убран быдлокод (но не весь), добавлены новые функции отправки RAW репортов;
  • Пересмотрена работа с системной hid.dll, реализована корректная отправка Output репортов, добавлены функции чтения статусов устройства;
  • Мелкие улучшения и добавление новых багов.

Итак, теперь библиотеку можно подключать фактически из любого места (обычно я во ViewModel это делаю), раньше же только из класса окна можно было:

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(Application.Current.MainWindow).Handle);
IntPtr handle = new WindowInteropHelper(Application.Current.MainWindow).Handle; 
source.AddHook(new HwndSourceHook(WndProc));
USB.RegisterHandle(handle);
}

IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
USB.ParseMessages(msg, wParam);
return IntPtr.Zero;
}

Сам обработчик MainWindow_Loaded назначается так:

public MainPageViewModel()
{
Application.Current.MainWindow.Loaded += MainWindow_Loaded;
}

Появился статичный метод GetDevicesList(), который возвращает список доступных устройств в виде List<HIDDeviceAvalible>:

region AvalibleDevices
List<HIDDeviceAvalible> _AvalibleDevices; public List<HIDDeviceAvalible> AvalibleDevices
{
get { return _AvalibleDevices; }
set { SetProperty<List<HIDDeviceAvalible>>(ref _AvalibleDevices, value); } }
#endregion
...
AvalibleDevices = UsbHidPort.GetDevicesList();

И теперь можно подключаться к устройству различными способами: VID/PID, Device Path или экземпляр HIDDeviceAvalible.

Из ключевых особенностей это, вроде, всё.

Software — Custom HID App (WPF)

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

Про реализацию приложения особо нечего сказать, почти всё стандартно, могу отметить только то, что при приёме Input Report намеренно сделано окно в 100 мс (по коду смотрите DispatcherTimer refreshtimer), которое отбрасывает все Input Report, которые пришли на время действия этого окна, сделано это для того, чтобы интерфейс не залипал, если устройство шлёт репорты каждые 10 мс или даже чаще.

Выглядит приложение вот так:

Выбираю из списка доступных устройств созданный только что пример:

После подключения к нему отображаются размеры репортов (если какой-то тип репорта не поддерживается, его размер = 0):

И тут мы видим размеры репортов, которые больше на +1, чем объявлено в CUSTOM_HID_ReportDesc_FS, всё сходится!

Переключаемся на вкладку с RAW данными:

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

Примечание: да, тут видно, что Input Report содержит далеко не только счетчик в первых байтах, который мы пишем туда в прошивке, а ещё какие-то статичные значения в последующих байтах. Догадались, почему так? ;)

В динамике это выглядит так:

И отправка репорта в устройство, для наглядного примера прошивка разбирает пришедший Output\Feature Report следующим образом:

И применяет полученные RGB565 цвета на нижнюю строку «TEST STRING»:

Примечание: реализация Feature Report не совсем полноценная в прошивке, не приходит ответ от устройства, но это уже оставлю для тех, кому это действительно нужно, сделать это несложно, но решение пока не буду выкладывать.

На этом всё. Мог что-то забыть или ошибиться, буду рад, если напишете об этом. Спасибо, что прочитали! ;)

Ссылки

  • ADElectronics/STM32-CustomHID-Example — исходники проектов;
  • USB — официальная документация на устройства с USB интерфейсом, покупка VID;
  • linux-usb.org/usb.ids — список всех официальных VID\PID;
  • PID.codes — сайт получения бесплатного PID (с определённым VID) для OpenSource проекта.

Добавить комментарий

Please log in using one of these methods to post your comment:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s