На дворе 2014 год, а для связи микроконтроллеров с ПК самым популярным средством является обычный последовательный порт. С ним легко начать работать, он до примитивности прост в понимании — просто поток байт.
Однако все современные стандарты исключили COM порт из состава ПК и приходится использовать USB-UART переходники, чтобы получить доступ к своему проекту на МК. Не всегда он есть под рукой. Не всегда такой переходник работает стабильно из-за проблем с драйверами. Есть и другие недостатки.
Но каждый раз, когда заходит разговор о том, применять USB или последовательный порт, находится множество поклонников логической простоты UART. И у них есть на то основания. Однако, хорошо ведь иметь альтернативу?
Меня давно просили рассказать как организовать пакетный обмен данными между ПК и МК на примере STM32F103. Я дам готовый рабочий проект и расскажу как его адаптировать для своих нужд. А уж вы сами решите — нужно оно вам или нет.
У нас есть плата с современным недорогим микроконтроллером STM32F103C8 со встроенной аппаратной поддержкой USB, я рассказывал о ней ранее
Я сказал, что у последовательного порта есть и другие недостатки:
-часто COM порт отсутствует в ПК или ноутбуке
-питание устройству нужно подавать отдельно
-даже при наличии COM порта в ПК необходимо согласовывать уровни сигналов: ПК использует интерфейс RS232 с дифференциальными уровнями сигналов +15В и -15В, а микроконтроллеры — TTL уровни (+5В, +3.3В, униполярные).
-часто в системе образуются десятки виртуальных COM портов и найти тот порт, что соответствует вашему устройству может оказаться непросто.
В свою очередь USB с нами уже многие годы и имеет свои преимущества:
-Возможность подачи питания от HOST устройства
-Удобная реализация пакетного обмена
-Возможность одновременного подключения к устройству несколькими программами
-Возможность однозначной идентификации подключенного устройства
-Аппаратная поддержка во многих современных МК, что исключает необходимость переходников
Функционал USB чрезвычайно богатый, но это порождает проблему — разобраться не так просто, как с последовательным интерфейсом. Есть отдельный класс устройств — USB-HID, которые не требуют установки драйверов, специально предназначены для взаимодействия с человеком и различными устройствами ввода-вывода. Идеально для организации обмена данными с МК. Лично мне нравится пакетный режим обмена. Это удобная абстракция. К тому же разбирать пакетные сообщения несколько проще и удобнее, чем работать с простым потоком байт.
USB-HID — довольно обширный класс устройств, поэтому прежде всего придется выбрать какое именно устройство мы будем создавать.
Мы можем создать эмуляцию клавиатуры, мыши, джойстика и других устройств ввода, а можем создать свое устройство, чтобы не зависеть от довольно жестких рамок стандарта и свободно обмениваться данными с ПК.
Я расскажу как cделать Custom HID device. Это дает максимальную свободу. Чтобы не затягивать статью, постараюсь рассказать максимально кратко — описаний стандарта в сети и без меня много, но лично мне они слабо помогли, когда понадобилось решить конкретную задачу.
Я использую EmBlocks для разработки под STM32. Вы можете использовать любую удобную среду, проект не очень сложно адаптировать.
К базовой структуре проекта добавлены:
Все эти файлы мы добавляем в любой проект, использующий USB.
Для корректной работы USB модуля важна частота работы МК. Далеко не все частоты позволяют правильно задать тактирование USB. В нашем случае используется кварцевый генератор на 8МГц и МК работает на частоте 72МГц, а USB модуль на 48МГц.
В main.c достаточно включить всего несколько строк кода
В функции Set_System() производится настройка пина подтяжки линии D+ к питанию для программного подключения/отключения устройства от ПК (в нашей плате не используется), настраивается прерывание и инициализируются светодиоды и кнопки для демонстрационного проекта.
В USB_Interrupts_Config() настраиваются прерывания в зависимости от семейства МК (поддерживаются F10x, F37x, L1x).
Функция USB_Init() запускает работу USB модуля. Если временно нужно отключить для отладки работу с USB, просто закомментируйте эту строку.
Далее в бесконечном цикле проверяется, удалось ли сконфигурировать USB модуль при подключении к ПК. Если все сработало верно и устройство успешно подключилось, ПК включен и не находится в режиме энергосбережения, то состояние будет CONFIGURED.
Далее проверяется, была ли закончена предыдущая передача данных в ПК и если да, то готовится к отправке новый пакет в функции RHIDCheckState()
USB-HID девайс не может сам инициировать передачу, т.к. координацией шины занимается host устройство — ПК. Поэтому при подготовке USB дескриптора нашего устройства, мы пишем, как часто нужно опрашивать наше устройство. По спецификации максимальная частота опроса — 1кГц и максимальный размер передаваемого за раз пакета — 64 байта. Если этого недостаточно — придется использовать другие режимы работы — вроде USB bulk, но там уже без драйверов не обойтись.
За настройку взаимодействия с ПК отвечают 3 дескриптора:
В комментариях все довольно прозрачно. Обратите внимание на DEVICE_VER_L, DEVICE_VER_H — это константы из usb_desc.h, которые вы можете изменить для идентификации версии своего устройства.
Здесь стоит обратить внимание на константу wMaxPacketSize — она определяет максимальный размер пакета, которым мы будем обмениваться с ПК. Проект так настроен, чтобы при ее изменении менялись и размеры буферов. Но не забывайте, что больше 0x40 по стандарту указывать не стоит. С этой константой будьте осторожны — если передаваемый пакет будет отличаться по размеру — будут проблемы!
Следующая за ним константа с комментарием bInterval — это период опроса устройства в миллисекундах. Для нашего устройства задано 32мс.
Это самый важный дескриптор — он описывает протокол обмена и функционал устройства. Его формирование — не самая простая задача. Если допустить ошибку при формировании дескриптора — устройство перестанет работать. Формат дескриптора очень жесткий. Есть даже специальная утилита HID Descriptor tool. А в корне проекта лежит файл «RHID.hid» с описанным выше дескриптором для редактирования в этой утилите. Но если вы не понимаете, что делаете, лучше не лезть.
Для простоты я сделал две константы:
RPT3_COUNT — размер OUTPUT буфера в байтах для передачи пакета в МК (в примере — 1 байт)
RPT4_COUNT — размер INPUT буфера в байтах для передачи пакета в ПК (в примере — 4 байта)
Размер любого из этих буферов не должен превышать wMaxPacketSize. Меньше — можно.
Кстати, превратить Custom HID в другой HID девайс, например, клавиатуру или джойстик можно фактически только переписав ReportDescriptor и изменив класс и подкласс устройства в дескрипторе конфигурации.
Хост (ПК) и девайс (МК) обмениваются пакетами данных заранее оговоренной структуры — report. Пакетов может быть весьма много, их можно предусмотреть на все случаи жизни — например пакет с данными о каких-то событиях в устройстве, пакет с данными, которые запрашивал ПК, пакет с командой для МК. Все, что угодно. Но структура всех пакетов должна быть описана в структуре RHID_ReportDescriptor.
ПК и МК различают репорты по ID, который идет первым байтом в пакете.
В нашем примере 4 типа репортов:
Если вы не до конца разобрались в том, как формировать дескриптор репортов, то просто меняйте константы RPT3_COUNT и RPT4_COUNT, устанавливая нужный размер исходящих и входящих (с точки зрения ПК) пакетов. Остальные репорты можно просто не трогать, они не помешают. Не забывайте, что первым байтом должен быть ID репорта.
Итак, мы сконфигурировали наше устройство, установив PID, VID, номер версии, настроили размеры входящих и исходящих пакетов и готовы к работе.
Каждые 32мс, как мы и просили в дескрипторе конфигурации, хост будет нас опрашивать и в функции RHIDCheckState мы проверяем — если у нас есть, что отправить, то формируем пакет данных для хоста.
Массив uint8_t Buffer[RPT4_COUNT+1] определен как размер полезных данных входящего (рассматривается всегда с точки зрения хоста) пакета + байт ID. Это важно — если размер буфера будет отличаться — будут проблемы. Поэтому для изменения размеров буфера редактируйте значение константы в usb_desc.h.
В функции мы собираем данные в пакет, устанавливаем флаг PrevXferComplete = 0, говорящий о том, что данные отправляются и вызываем функциии библиотеки USB_SIL_Write и SetEPTxValid для отправки данных хосту.
Все, на этом передача данных хосту закончена.
С приемом данных немного сложнее — есть два способа послать данные девайсу — один из них заключается в использовании описанных в дескрипторе репорта возможностей устройства (Features), с соответствующими параметрами посредством функции SET_FEAUTRE. Это некоторая абстракция, для красивого управления устройством с кучей функций, чтобы можно было вызывать осмысленные функции, а не просто слать поток байт.
Второй способ — это работа с устройством как с файлом — просто записываем в него пакет как в файл. Этот метод называется SET_REPORT. На деле работает чуть-чуть медленнее.
Наше устройство поддерживает оба метода, о чем мы и сказали хосту в дескрипторе репортов.
Данные, отправленные методом SET_FEAUTRE обрабатываются в usb_prop.c
Здесь мы проверяем первый байт в репорте и в соответствии с ним обрабатываем остаток пакета — управляем светодиодами или просто берем байт, отправленный нам хостом и кладем в пакет для последующей отправки обратно в функции RHIDCheckState.
Под Report_Buf зарезервировано wMaxPacketSize байт, чтобы влез любой пакет, который нам отправит хост.
Данные, отправленные методом SET_REPORT обрабатываются в usb_endp.c
Здесь почти то же самое, только нужно самостоятельно забрать данные вызовом USB_SIL_Read(EP1_OUT, Receive_Buffer) и в конце сообщить, что мы закончили вызовом SetEPRxStatus(ENDP1, EP_RX_VALID);
Настраивать устройство, передавать и принимать данные в пакетах нужного размера с нужной нам периодичностью мы научились.
Собираем проект и прошиваем в устройство.
Работать, это будет примерно так:
Проект поддерживает взаимодействие с утилитой USB HID Demonstrator от ST Microelectronics.
Страница Device capabilities отображает возможности, описанные в Report Descriptor.
Input/Output transfer позволяет вручную поотправлять данные девайсу и посмотреть пакет, который от него приходит.
Graphic view позволяет управлять светодиодами, чекбоксами Led 1, Led 2, настроив соответствующий им Report ID, а также передавать байт ползунком (ReportID=3)
Также я написал маленькую демо-софтинку, которая автоматически определяет подключение к компу и отключение нашего девайса по его VID и PID, отображает статус — подключено/отключено индикатором рядом с чекбоксом Auto Connect
Радиокнока Send using позволяет выбрать метод отправки данных девайсу.
Report: отображает полученный от девайса пакет побайтно, начиная с ReportID.
Щелкая по светодиодам ниже — управляем светодиодами девайса. Их состояние отображает текущее состояние девайса. Считывается из репорта от девайса.
Перемещая ползунок, мы отправляем Report с ID=3 и значением, соответствующим позиции ползунка. Девайс вернет это значение в 4 байте репорта.
В выпадающем комбобоксе отображаются HID девайсы, найденные в системе и если найден наш девайс, то отображается его название.
Скачать все, что необходимо, можно на GitHub. В составе:
DT — HID Descriptor tool
tstHID-STM32F103 — проект для EmBlocks
USB HID Demonstrator — утилита от ST Microelectronics
HIDSTM32.exe — моя демо-софтинка на Delphi аналогичного фукнционала, но не требующая настройки
Если остались вопросы — пишите в комментариях. Постараюсь ответить. Я постарался не утопить суть в куче мелочей, чтобы сложилось общее понимание. Остальное уже можно понять, изучая проект. Но если вам нужно быстро сделать свое устройство, а лезть в дебри некогда — все, что вам нужно, я описал.
P.S. По умолчанию при уходе хоста в режим энергосбережения, девайс засыпает вместе с ним, а если подключить девайс к спящему ПК, то он тоже уйдет в слип. Поэтому если мы просто воткнем в девайс блок питания или запитаем от батареи, то работать он не будет, считая, что подключен к спящему ПК (пакетов конфигурации то от БП не придет точно). Я изменил библиотеку так, чтобы устройство работало и при подключении просто БП. Поэтому девайс будет работать как при подключениии к ПК так и автономно. (У меня ушло немало времени, чтобы разобраться с этим.)
Однако все современные стандарты исключили COM порт из состава ПК и приходится использовать USB-UART переходники, чтобы получить доступ к своему проекту на МК. Не всегда он есть под рукой. Не всегда такой переходник работает стабильно из-за проблем с драйверами. Есть и другие недостатки.
Но каждый раз, когда заходит разговор о том, применять USB или последовательный порт, находится множество поклонников логической простоты UART. И у них есть на то основания. Однако, хорошо ведь иметь альтернативу?
Меня давно просили рассказать как организовать пакетный обмен данными между ПК и МК на примере STM32F103. Я дам готовый рабочий проект и расскажу как его адаптировать для своих нужд. А уж вы сами решите — нужно оно вам или нет.
У нас есть плата с современным недорогим микроконтроллером STM32F103C8 со встроенной аппаратной поддержкой USB, я рассказывал о ней ранее
Я сказал, что у последовательного порта есть и другие недостатки:
-часто COM порт отсутствует в ПК или ноутбуке
-питание устройству нужно подавать отдельно
-даже при наличии COM порта в ПК необходимо согласовывать уровни сигналов: ПК использует интерфейс RS232 с дифференциальными уровнями сигналов +15В и -15В, а микроконтроллеры — TTL уровни (+5В, +3.3В, униполярные).
-часто в системе образуются десятки виртуальных COM портов и найти тот порт, что соответствует вашему устройству может оказаться непросто.
В свою очередь USB с нами уже многие годы и имеет свои преимущества:
-Возможность подачи питания от HOST устройства
-Удобная реализация пакетного обмена
-Возможность одновременного подключения к устройству несколькими программами
-Возможность однозначной идентификации подключенного устройства
-Аппаратная поддержка во многих современных МК, что исключает необходимость переходников
Функционал USB чрезвычайно богатый, но это порождает проблему — разобраться не так просто, как с последовательным интерфейсом. Есть отдельный класс устройств — USB-HID, которые не требуют установки драйверов, специально предназначены для взаимодействия с человеком и различными устройствами ввода-вывода. Идеально для организации обмена данными с МК. Лично мне нравится пакетный режим обмена. Это удобная абстракция. К тому же разбирать пакетные сообщения несколько проще и удобнее, чем работать с простым потоком байт.
Выбор профиля HID
USB-HID — довольно обширный класс устройств, поэтому прежде всего придется выбрать какое именно устройство мы будем создавать.
Мы можем создать эмуляцию клавиатуры, мыши, джойстика и других устройств ввода, а можем создать свое устройство, чтобы не зависеть от довольно жестких рамок стандарта и свободно обмениваться данными с ПК.
Я расскажу как cделать Custom HID device. Это дает максимальную свободу. Чтобы не затягивать статью, постараюсь рассказать максимально кратко — описаний стандарта в сети и без меня много, но лично мне они слабо помогли, когда понадобилось решить конкретную задачу.
Структура проекта
Я использую EmBlocks для разработки под STM32. Вы можете использовать любую удобную среду, проект не очень сложно адаптировать.
К базовой структуре проекта добавлены:
- Папка USB-FS с библиотекой «STM32F10x, STM32L1xx and STM32F3xx USB-FS-Device Driver» версии 4.0.0.
- В папках Inc и Src файлы:
platform_config.h — здесь собраны определения, касающиеся конкретной платы и семейства МК
stm32_it.h, stm32_it.c — здесь определены обработчики прерываний
usb_conf.h, usb_endp.c — здесь определяются конечные точки (Endpoint), размеры и адреса их буферов, функции-обработчики
usb_desc.h, usb_desc.c — здесь собрана информаци о самом устройстве — как оно будет определяться при подключении к ПК и определены размеры и формат пакетов данных
hw_config.c — здесь собрана вся работа с отправкой данных на ПК
hw_config.h, usb_istr.h, usb_prop.h, usb_pwr.h
usb_istr.c, usb_prop.c, usb_pwr.c — нужны для работы USB-FS библиотеки, но лезть в них необязательно
Все эти файлы мы добавляем в любой проект, использующий USB.
Инициализация USB
Для корректной работы USB модуля важна частота работы МК. Далеко не все частоты позволяют правильно задать тактирование USB. В нашем случае используется кварцевый генератор на 8МГц и МК работает на частоте 72МГц, а USB модуль на 48МГц.
В main.c достаточно включить всего несколько строк кода
main.c
/* Includes ------------------------------------------------------------------*/
#include "hw_config.h"
#include "usb_lib.h"
#include "usb_pwr.h"
/* Private variables ---------------------------------------------------------*/
__IO uint8_t PrevXferComplete = 1;
int main(void)
{
Set_System();
USB_Interrupts_Config();
Set_USBClock();
USB_Init();
while (1)
{
if (bDeviceState == CONFIGURED)
{
if (PrevXferComplete)
{
RHIDCheckState();
}
}
}
}
В функции Set_System() производится настройка пина подтяжки линии D+ к питанию для программного подключения/отключения устройства от ПК (в нашей плате не используется), настраивается прерывание и инициализируются светодиоды и кнопки для демонстрационного проекта.
В USB_Interrupts_Config() настраиваются прерывания в зависимости от семейства МК (поддерживаются F10x, F37x, L1x).
Функция USB_Init() запускает работу USB модуля. Если временно нужно отключить для отладки работу с USB, просто закомментируйте эту строку.
Далее в бесконечном цикле проверяется, удалось ли сконфигурировать USB модуль при подключении к ПК. Если все сработало верно и устройство успешно подключилось, ПК включен и не находится в режиме энергосбережения, то состояние будет CONFIGURED.
Далее проверяется, была ли закончена предыдущая передача данных в ПК и если да, то готовится к отправке новый пакет в функции RHIDCheckState()
Размер пакета и частота передачи
USB-HID девайс не может сам инициировать передачу, т.к. координацией шины занимается host устройство — ПК. Поэтому при подготовке USB дескриптора нашего устройства, мы пишем, как часто нужно опрашивать наше устройство. По спецификации максимальная частота опроса — 1кГц и максимальный размер передаваемого за раз пакета — 64 байта. Если этого недостаточно — придется использовать другие режимы работы — вроде USB bulk, но там уже без драйверов не обойтись.
За настройку взаимодействия с ПК отвечают 3 дескриптора:
Дескриптор устройства
/* USB Standard Device Descriptor */
const uint8_t RHID_DeviceDescriptor[RHID_SIZ_DEVICE_DESC] =
{
RHID_SIZ_DEVICE_DESC, // общая длина дескриптора устройства в байтах
USB_DEVICE_DESCRIPTOR_TYPE, // bDescriptorType - показывает, что это за дескриптор. В данном случае - Device descriptor
0x00, 0x02, // bcdUSB - какую версию стандарта USB поддерживает устройство. 2.0
// класс, подкласс устройства и протокол, по стандарту USB. У нас нули, означает каждый интерфейс сам за себя
0x00, //bDeviceClass
0x00, //bDeviceSubClass
0x00, //bDeviceProtocol
0x40, //bMaxPacketSize - максимальный размер пакетов для Endpoint 0 (при конфигурировании)
// те самые пресловутые VID и PID, по которым и определяется, что же это за устройство.
0x83, 0x04, //idVendor (0x0483)
0x11, 0x57, //idProduct (0x5711)
DEVICE_VER_L, DEVICE_VER_H, // bcdDevice rel. DEVICE_VER_H.DEVICE_VER_L номер релиза устройства
// дальше идут индексы строк, описывающих производителя, устройство и серийный номер.
// Отображаются в свойствах устройства в диспетчере устройств
// А по серийному номеру подключенные устройства с одинаковым VID/PID различаются системой.
1, //Index of string descriptor describing manufacturer
2, //Index of string descriptor describing product
3, //Index of string descriptor describing the device serial number
0x01 // bNumConfigurations - количество возможных конфигураций. У нас одна.
}
; /* CustomHID_DeviceDescriptor */
В комментариях все довольно прозрачно. Обратите внимание на DEVICE_VER_L, DEVICE_VER_H — это константы из usb_desc.h, которые вы можете изменить для идентификации версии своего устройства.
Дескриптор конфигурации (описывает возможности устройства)
/* USB Configuration Descriptor */
/* All Descriptors (Configuration, Interface, Endpoint, Class, Vendor */
const uint8_t RHID_ConfigDescriptor[RHID_SIZ_CONFIG_DESC] =
{
0x09, // bLength: длина дескриптора конфигурации
USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - конфигурация
RHID_SIZ_CONFIG_DESC, 0x00, // wTotalLength: общий размер всего дерева под данной конфигурацией в байтах
0x01, // bNumInterfaces: в конфигурации всего один интерфейс
0x01, // bConfigurationValue: индекс данной конфигурации
0x00, // iConfiguration: индекс строки, которая описывает эту конфигурацию
0xE0, // bmAttributes: признак того, что устройство будет питаться от шины USB
0x32, // MaxPower 100 mA: и ему хватит 100 мА
/************** Дескриптор интерфейса ****************/
0x09, // bLength: размер дескриптора интерфейса
USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - интерфейс
0x00, // bInterfaceNumber: порядковый номер интерфейса - 0
0x00, // bAlternateSetting: признак альтернативного интерфейса, у нас не используется
0x02, // bNumEndpoints - количество эндпоинтов.
0x03, // bInterfaceClass: класс интерфеса - HID
// если бы мы косили под стандартное устройство, например клавиатуру или мышь, то надо было бы указать правильно класс и подкласс
// а так у нас общее HID-устройство
0x00, // bInterfaceSubClass : подкласс интерфейса.
0x00, // nInterfaceProtocol : протокол интерфейса
0, // iInterface: индекс строки, описывающей интерфейс
// теперь отдельный дескриптор для уточнения того, что данный интерфейс - это HID устройство
/******************** HID дескриптор ********************/
0x09, // bLength: длина HID-дескриптора
HID_DESCRIPTOR_TYPE, // bDescriptorType: тип дескриптора - HID
0x01, 0x01, // bcdHID: номер версии HID 1.1
0x00, // bCountryCode: код страны (если нужен)
0x01, // bNumDescriptors: Сколько дальше будет report дескрипторов
HID_REPORT_DESCRIPTOR_TYPE, // bDescriptorType: Тип дескриптора - report
RHID_SIZ_REPORT_DESC, 0x00, // wItemLength: длина report-дескриптора
/******************** дескриптор конечных точек (endpoints) ********************/
0x07, // bLength: длина дескриптора
USB_ENDPOINT_DESCRIPTOR_TYPE, // тип дескриптора - endpoints
0x81, // bEndpointAddress: адрес конечной точки и направление 1(IN)
0x03, // bmAttributes: тип конечной точки - Interrupt endpoint
wMaxPacketSize, 0x00, // wMaxPacketSize: Bytes max
0x20, // bInterval: Polling Interval (32 ms)
0x07, /* bLength: Endpoint Descriptor size */
USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType: */
/* Endpoint descriptor type */
0x01, /* bEndpointAddress: */
/* Endpoint Address (OUT) */
0x03, /* bmAttributes: Interrupt endpoint */
wMaxPacketSize, /* wMaxPacketSize: Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (32 ms) */
}
; /* RHID_ConfigDescriptor */
Здесь стоит обратить внимание на константу wMaxPacketSize — она определяет максимальный размер пакета, которым мы будем обмениваться с ПК. Проект так настроен, чтобы при ее изменении менялись и размеры буферов. Но не забывайте, что больше 0x40 по стандарту указывать не стоит. С этой константой будьте осторожны — если передаваемый пакет будет отличаться по размеру — будут проблемы!
Следующая за ним константа с комментарием bInterval — это период опроса устройства в миллисекундах. Для нашего устройства задано 32мс.
Дескриптор репорта (описывает протокол)
const uint8_t RHID_ReportDescriptor[RHID_SIZ_REPORT_DESC] =
{
0x06, 0x00, 0xff, // USAGE_PAGE (Generic Desktop)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x09, 0x01, // USAGE (Vendor Usage 1)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0xb1, 0x82, // FEATURE (Data,Var,Abs,Vol)
0x85, 0x01, // REPORT_ID (1)
0x09, 0x01, // USAGE (Vendor Usage 1)
0x91, 0x82, // OUTPUT (Data,Var,Abs,Vol)
0x85, 0x02, // REPORT_ID (2)
0x09, 0x02, // USAGE (Vendor Usage 2)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x01, // REPORT_COUNT (1)
0xb1, 0x82, // FEATURE (Data,Var,Abs,Vol)
0x85, 0x02, // REPORT_ID (2)
0x09, 0x02, // USAGE (Vendor Usage 2)
0x91, 0x82, // OUTPUT (Data,Var,Abs,Vol)
0x85, 0x03, // REPORT_ID (3)
0x09, 0x03, // USAGE (Vendor Usage 3)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, RPT3_COUNT, // REPORT_COUNT (N)
0xb1, 0x82, // FEATURE (Data,Var,Abs,Vol)
0x85, 0x03, // REPORT_ID (3)
0x09, 0x03, // USAGE (Vendor Usage 3)
0x91, 0x82, // OUTPUT (Data,Var,Abs,Vol)
0x85, 0x04, // REPORT_ID (4)
0x09, 0x04, // USAGE (Vendor Usage 4)
0x75, 0x08, // REPORT_SIZE (8)
0x95, RPT4_COUNT, // REPORT_COUNT (N)
0x81, 0x82, // INPUT (Data,Var,Abs,Vol)
0xc0 // END_COLLECTION
}
Это самый важный дескриптор — он описывает протокол обмена и функционал устройства. Его формирование — не самая простая задача. Если допустить ошибку при формировании дескриптора — устройство перестанет работать. Формат дескриптора очень жесткий. Есть даже специальная утилита HID Descriptor tool. А в корне проекта лежит файл «RHID.hid» с описанным выше дескриптором для редактирования в этой утилите. Но если вы не понимаете, что делаете, лучше не лезть.
Для простоты я сделал две константы:
RPT3_COUNT — размер OUTPUT буфера в байтах для передачи пакета в МК (в примере — 1 байт)
RPT4_COUNT — размер INPUT буфера в байтах для передачи пакета в ПК (в примере — 4 байта)
Размер любого из этих буферов не должен превышать wMaxPacketSize. Меньше — можно.
Кстати, превратить Custom HID в другой HID девайс, например, клавиатуру или джойстик можно фактически только переписав ReportDescriptor и изменив класс и подкласс устройства в дескрипторе конфигурации.
Что такое Report
Хост (ПК) и девайс (МК) обмениваются пакетами данных заранее оговоренной структуры — report. Пакетов может быть весьма много, их можно предусмотреть на все случаи жизни — например пакет с данными о каких-то событиях в устройстве, пакет с данными, которые запрашивал ПК, пакет с командой для МК. Все, что угодно. Но структура всех пакетов должна быть описана в структуре RHID_ReportDescriptor.
ПК и МК различают репорты по ID, который идет первым байтом в пакете.
В нашем примере 4 типа репортов:
- REPORT_ID = 1 и 2 — команда МК включить/выключить LED1/LED2. Содержит поле размером 1 бит с желаемым состоянием светодиода и поддерживает отправку как методом SET_REPORT так и методом SET_FEATURE (об этом чуть позже).
- REPORT_ID = 3 — передает один байт в МК. Просто, чтобы показать, как передать данные МК. Мы будем передавать положение ползунка.
- REPORT_ID = 4 — это репорт для передачи данных ПК. Возвращает информацию о текущем состоянии светодиодов, кнопок (если они есть) и возвращает переданный в репорте с ID=3 байт, чтобы показать, что данные приняты.
Если вы не до конца разобрались в том, как формировать дескриптор репортов, то просто меняйте константы RPT3_COUNT и RPT4_COUNT, устанавливая нужный размер исходящих и входящих (с точки зрения ПК) пакетов. Остальные репорты можно просто не трогать, они не помешают. Не забывайте, что первым байтом должен быть ID репорта.
Цикл обмена
Итак, мы сконфигурировали наше устройство, установив PID, VID, номер версии, настроили размеры входящих и исходящих пакетов и готовы к работе.
Каждые 32мс, как мы и просили в дескрипторе конфигурации, хост будет нас опрашивать и в функции RHIDCheckState мы проверяем — если у нас есть, что отправить, то формируем пакет данных для хоста.
RHIDCheckState - функция отправки данных
/*******************************************************************************
* Function Name : RHIDCheckState.
* Description : Decodes the RHID state.
* Input : None.
* Output : None.
* Return value : The state value.
*******************************************************************************/
uint16_t btn1_prev, btn2_prev;
uint8_t Buffer[RPT4_COUNT+1];
uint8_t RHIDCheckState(void)
{
uint16_t btn1=0, btn2=0;
btn1 = GPIO_ReadInputDataBit(BTN1_PORT, BTN1_PIN);
btn2 = GPIO_ReadInputDataBit(BTN2_PORT, BTN2_PIN);
Buffer[0] = 4;
Buffer[1] = btn1;
Buffer[2] = btn2;
Buffer[3] = (GPIO_ReadInputDataBit(LED_PORT, LED1_PIN) | GPIO_ReadInputDataBit(LED_PORT, LED2_PIN)<<1);
/* Reset the control token to inform upper layer that a transfer is ongoing */
PrevXferComplete = 0;
/* Copy buffer date info in ENDP1 Tx Packet Memory Area*/
USB_SIL_Write(EP1_IN, Buffer, RPT4_COUNT+1);
/* Enable endpoint for transmission */
SetEPTxValid(ENDP1);
return (btn1 | btn2<<1);
}
Массив uint8_t Buffer[RPT4_COUNT+1] определен как размер полезных данных входящего (рассматривается всегда с точки зрения хоста) пакета + байт ID. Это важно — если размер буфера будет отличаться — будут проблемы. Поэтому для изменения размеров буфера редактируйте значение константы в usb_desc.h.
В функции мы собираем данные в пакет, устанавливаем флаг PrevXferComplete = 0, говорящий о том, что данные отправляются и вызываем функциии библиотеки USB_SIL_Write и SetEPTxValid для отправки данных хосту.
Все, на этом передача данных хосту закончена.
С приемом данных немного сложнее — есть два способа послать данные девайсу — один из них заключается в использовании описанных в дескрипторе репорта возможностей устройства (Features), с соответствующими параметрами посредством функции SET_FEAUTRE. Это некоторая абстракция, для красивого управления устройством с кучей функций, чтобы можно было вызывать осмысленные функции, а не просто слать поток байт.
Второй способ — это работа с устройством как с файлом — просто записываем в него пакет как в файл. Этот метод называется SET_REPORT. На деле работает чуть-чуть медленнее.
Наше устройство поддерживает оба метода, о чем мы и сказали хосту в дескрипторе репортов.
Обработка SET_FEATURE
Данные, отправленные методом SET_FEAUTRE обрабатываются в usb_prop.c
функция HID_Status_In
/*******************************************************************************
* Function Name : HID_Status_In.
* Description : HID status IN routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void HID_Status_In(void)
{
BitAction Led_State;
if (Report_Buf[1] == 0)
{
Led_State = Bit_RESET;
}
else
{
Led_State = Bit_SET;
}
switch (Report_Buf[0])
{
case 1: /* Led 1 */
if (Led_State != Bit_RESET)
{
GPIO_SetBits(LED_PORT,LED1_PIN);
}
else
{
GPIO_ResetBits(LED_PORT,LED1_PIN);
}
break;
case 2: /* Led 2 */
if (Led_State != Bit_RESET)
{
GPIO_SetBits(LED_PORT,LED2_PIN);
}
else
{
GPIO_ResetBits(LED_PORT,LED2_PIN);
}
break;
case 3: /* Led 1&2 */
Buffer[4]=Report_Buf[1];
break;
}
}
Здесь мы проверяем первый байт в репорте и в соответствии с ним обрабатываем остаток пакета — управляем светодиодами или просто берем байт, отправленный нам хостом и кладем в пакет для последующей отправки обратно в функции RHIDCheckState.
Под Report_Buf зарезервировано wMaxPacketSize байт, чтобы влез любой пакет, который нам отправит хост.
Данные, отправленные методом SET_REPORT обрабатываются в usb_endp.c
функция EP1_OUT_Callback
/*******************************************************************************
* Function Name : EP1_OUT_Callback.
* Description : EP1 OUT Callback Routine.
* Input : None.
* Output : None.
* Return : None.
*******************************************************************************/
void EP1_OUT_Callback(void)
{
BitAction Led_State;
/* Read received data (2 bytes) */
USB_SIL_Read(EP1_OUT, Receive_Buffer);
if (Receive_Buffer[1] == 0)
{
Led_State = Bit_RESET;
}
else
{
Led_State = Bit_SET;
}
switch (Receive_Buffer[0])
{
case 1: /* Led 1 */
if (Led_State != Bit_RESET)
{
GPIO_SetBits(LED_PORT,LED1_PIN);
}
else
{
GPIO_ResetBits(LED_PORT,LED1_PIN);
}
break;
case 2: /* Led 2 */
if (Led_State != Bit_RESET)
{
GPIO_SetBits(LED_PORT,LED2_PIN);
}
else
{
GPIO_ResetBits(LED_PORT,LED2_PIN);
}
break;
case 3: /* Led 1&2 */
Buffer[4]=Receive_Buffer[1];
break;
}
SetEPRxStatus(ENDP1, EP_RX_VALID);
}
Здесь почти то же самое, только нужно самостоятельно забрать данные вызовом USB_SIL_Read(EP1_OUT, Receive_Buffer) и в конце сообщить, что мы закончили вызовом SetEPRxStatus(ENDP1, EP_RX_VALID);
Настраивать устройство, передавать и принимать данные в пакетах нужного размера с нужной нам периодичностью мы научились.
Собираем проект и прошиваем в устройство.
Работать, это будет примерно так:
Проект поддерживает взаимодействие с утилитой USB HID Demonstrator от ST Microelectronics.
Страница Device capabilities отображает возможности, описанные в Report Descriptor.
Input/Output transfer позволяет вручную поотправлять данные девайсу и посмотреть пакет, который от него приходит.
Graphic view позволяет управлять светодиодами, чекбоксами Led 1, Led 2, настроив соответствующий им Report ID, а также передавать байт ползунком (ReportID=3)
Также я написал маленькую демо-софтинку, которая автоматически определяет подключение к компу и отключение нашего девайса по его VID и PID, отображает статус — подключено/отключено индикатором рядом с чекбоксом Auto Connect
Радиокнока Send using позволяет выбрать метод отправки данных девайсу.
Report: отображает полученный от девайса пакет побайтно, начиная с ReportID.
Щелкая по светодиодам ниже — управляем светодиодами девайса. Их состояние отображает текущее состояние девайса. Считывается из репорта от девайса.
Перемещая ползунок, мы отправляем Report с ID=3 и значением, соответствующим позиции ползунка. Девайс вернет это значение в 4 байте репорта.
В выпадающем комбобоксе отображаются HID девайсы, найденные в системе и если найден наш девайс, то отображается его название.
Скачать все, что необходимо, можно на GitHub. В составе:
DT — HID Descriptor tool
tstHID-STM32F103 — проект для EmBlocks
USB HID Demonstrator — утилита от ST Microelectronics
HIDSTM32.exe — моя демо-софтинка на Delphi аналогичного фукнционала, но не требующая настройки
Если остались вопросы — пишите в комментариях. Постараюсь ответить. Я постарался не утопить суть в куче мелочей, чтобы сложилось общее понимание. Остальное уже можно понять, изучая проект. Но если вам нужно быстро сделать свое устройство, а лезть в дебри некогда — все, что вам нужно, я описал.
P.S. По умолчанию при уходе хоста в режим энергосбережения, девайс засыпает вместе с ним, а если подключить девайс к спящему ПК, то он тоже уйдет в слип. Поэтому если мы просто воткнем в девайс блок питания или запитаем от батареи, то работать он не будет, считая, что подключен к спящему ПК (пакетов конфигурации то от БП не придет точно). Я изменил библиотеку так, чтобы устройство работало и при подключении просто БП. Поэтому девайс будет работать как при подключениии к ПК так и автономно. (У меня ушло немало времени, чтобы разобраться с этим.)