Содержание
- Stm32 + USB на шаблонах C++. Продолжение. Делаем HID
- Разделение прерывания
- Класс конечной точки
- Класс интерфейса
- Класс конфигурации
- Класс устройства
- Интерфейс HID
- HID-устройство
- Спрятала васаби в соломинку и была съедена мужем
- Заключение
- Благодарности
- Hid что это
- Что такое HID?
- Как получить идентификатор HID?
- Я скрывала это очень давно…I hid this for a long time…
- USB HID CAN контроллер CAR PC Windows / Linux
- При каких условиях HID может изменяться?
- Некоторые часто задаваемые вопросы по ксенону

Далее нужно определить всю ирерахию: конечные точки, интерфейс, конфигурацию и устройство:
Stm32 + USB на шаблонах C++. Продолжение. Делаем HID
В прошлый раз показал один из способов распределение ресурсов между конечными точками, а именно регистров EPnR, памяти под дескрипторы буферов и под сами буферы. Предлагаю продолжить начатое и рассмотреть написанную библиотеку на примере создания простого HID-устройства, позволяющего управлять светодиодом.
Разделение прерывания
Кроме обозначенных ранее ресурсов конечные точки также делят единственное прерывание. Соответственно, общий (главный) обработчик должен корректно передать управление обработчику прерывания нужной конечной точки. Номер конечной точки, к которой обращается хост, записано в битах EP_ID регистра ISTR. Придерживаясь линии реализации полностью шаблонной библиотеки, получил следующий класс:
using EpRequestHandler = std::add_pointer_t; template class EndpointHandlersBase; template class EndpointHandlersBase, Int8_tArray> < public: // Массив указателей на обработчики static constexpr EpRequestHandler _handlers[] = ; // Индексы обработчиков static constexpr int8_t _handlersIndexes[] = ; public: inline static void Handle(uint8_t number, EndpointDirection direction) < _handlers[_handlersIndexes[2 * number + (direction == EndpointDirection::Out ? 1 : 0)]](); >>;
Ключевым элементом класса является массив _handlersIndexes, который отображает номер конечной точки и направление в конкретный обработчик. Для получения данного массива реализован специальный класс:
template class EndpointHandlersIndexes < // Предикат для поиска очередной конечной точки. using Predicate = Select, IsRxOrBidirectionalEndpointWithNumber>::value; static const int8_t EndpointIndex = Search::value; public: // В конец массива индекса вставляется номер соответствующей конечной точки или -1 в случае пропуска. using type = typename Int8_tArray_InsertBack::type, EndpointIndex>::type; >; template class EndpointHandlersIndexes < public: using type = Int8_tArray<>; >;
Из такой реализации, кстати, вытекает рекомендация объявлять конечные точки с номерами по порядку, потому что размер массива индексов обработчиков равен удвоенному максимальному номеру конечной точки.
Класс конечной точки
Из-за необходимости распределить ресурсы между конечными точками пришлось разделить код на два класса: базовый, который и нужно инстанцировать:
template class EndpointBase .
А также расширенный, с адресами буферов и регистра (которые назначаются менеджером, описанным в прошлой статье). Из-за различий между конечными точками разных типов/направлений (например, у однонаправленной конечной точки типа Interrupt будет один буфер, а у двунаправленной или Bulk с двойной буферизацией — два) для каждого типа объявлен свой класс:
template class Endpoint : public _Base . template class BidirectionalEndpoint : public Endpoint . template class BulkDoubleBufferedEndpoint : public Endpoint
Конечная точка на текущий момент реализована простой: экспортирует метод инициализации (в котором заполняется регистр EPnR), метод заполнения дескриптора, методы управления битами регистра (Очистка битов CTR_TX/RX, установка битов TX/RX_STATUS), а также отправку данных.
Класс интерфейса
Следующей сущностью в иерархии является интерфейс, который, по сути (как я понимаю) есть просто контейнер для конечных точек, поэтому реализующий его класс очень простой (прикладываю код полностью, потому что здесь применяется мощь variadic-шаблонов, который позволил исключить лишние зависимости):
template class Interface < public: using Endpoints = Zhele::TemplateUtils::TypeList; static const uint8_t EndpointsCount = ((_Endpoints::Direction == EndpointDirection::Bidirectional ? 2 : 1) + . ); static void Reset() < (_Endpoints::Reset(), . ); >static uint16_t FillDescriptor(InterfaceDescriptor* descriptor) < uint16_t totalLength = sizeof(InterfaceDescriptor); *descriptor = InterfaceDescriptor < .Number = _Number, .AlternateSetting = _AlternateSetting, .EndpointsCount = EndpointsCount, .Class = _Class, .SubClass = _SubClass, .Protocol = _Protocol >; EndpointDescriptor* endpointsDescriptors = reinterpret_cast(++descriptor); totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + . ); return totalLength; > >;
Класс конфигурации
То же самое можно сказать о конфигурации, которая есть контейнер для интерфейсов. Читая описание USB я понял, что все на самом деле сложнее и хост вправе выбирать конфигурации/интерфейсы, однако на данный момент не осознал сути и не поддерживаю подобные механизмы. В то же время предполагаю, что для реализации взаимодействия хоста с конфигурациями и интерфейсами нужно будет сделать немного — добавить диспетчеризацию обращений к ним аналогично диспетчеризации обращений к конечным точкам.
template class Configuration < public: using Endpoints = Zhele::TemplateUtils::Append_t; static void Reset() < (_Interfaces::Reset(), . ); >.
Класс устройства
Наконец, в вершине иерархии находится непосредственно само устройство. В моей реализации соответствующий класс является производным от нулевой конечной точки, поскольку она особая и обмен данными с нулевой конечной точкой — это обмен данными (как правило, управляющими) с самим девайсом.
template < typename _Regs, IRQn_Type _IRQNumber, typename _ClockCtrl, uint16_t _UsbVersion, DeviceClass _Class, uint8_t _SubClass, uint8_t _Protocol, uint16_t _VendorId, uint16_t _ProductId, uint16_t _DeviceReleaseNumber, typename _Ep0, typename. _Configurations>class DeviceBase : public _Ep0 < using This = DeviceBase; using Endpoints = Append_t; using Configurations = TypeList; // Replace Ep0 with this for correct handler register. using EpBufferManager = EndpointsManager>; using EpHandlers = EndpointHandlers>; .
Класс содержит главный обработчик прерывания, диспетчеризуя запросы по различными конечным точкам:
static void CommonHandler() < if(_Regs()->ISTR & USB_ISTR_RESET) < Reset(); >if (_Regs()->ISTR & USB_ISTR_CTR) < uint8_t endpoint = _Regs()->ISTR & USB_ISTR_EP_ID; EpHandlers::Handle(endpoint, ((_Regs()->ISTR & USB_ISTR_DIR) != 0 ? EndpointDirection::Out : EndpointDirection::In)); > NVIC_ClearPendingIRQ(_IRQNumber); >
Являясь одновременно и нулевой конечной точкой, класс Device содержит обработчик обращений к нулевой точке, реализуя, на данный момент, процесс нумерации:
Обработчик прерывания нулевой конечной точки
static void Handler() < if(_Ep0::Reg::Get() & USB_EP_CTR_RX) < _Ep0::ClearCtrRx(); if(_Ep0::Reg::Get() & USB_EP_SETUP) < SetupPacket* setup = reinterpret_cast(_Ep0::RxBuffer); switch (setup->Request) < case StandartRequestCode::GetStatus: < uint16_t status = 0; _Ep0::Writer::SendData(&status, sizeof(status)); break; >case StandartRequestCode::SetAddress: < TempAddressStorage = setup->Value; _Ep0::Writer::SendData(0); break; > case StandartRequestCode::GetDescriptor: < switch (static_cast(setup->Value)) < case GetDescriptorParameter::DeviceDescriptor: < DeviceDescriptor tempDeviceDescriptor; FillDescriptor(reinterpret_cast(&tempDeviceDescriptor)); _Ep0::Writer::SendData(&tempDeviceDescriptor, setup->Length < sizeof(DeviceDescriptor) ? setup->Length : sizeof(DeviceDescriptor)); break; > case GetDescriptorParameter::ConfigurationDescriptor: < uint8_t temp[64]; uint16_t size = GetType::type::FillDescriptor(reinterpret_cast(&temp[0])); _Ep0::Writer::SendData(reinterpret_cast(&temp[0]), setup->Length < size ? setup->Length : size); break; > case GetDescriptorParameter::HidReportDescriptor: < uint16_t size = sizeof(GetType_t::HidReport::Data); _Ep0::Writer::SendData(GetType_t::HidReport::Data, setup->Length < size ? setup->Length : size); break; > default: _Ep0::SetTxStatus(EndpointStatus::Stall); break; > break; > case StandartRequestCode::GetConfiguration: < uint16_t configuration = 0; _Ep0::Writer::SendData(&configuration, 1); break; >case StandartRequestCode::SetConfiguration: < _Ep0::Writer::SendData(0); break; >default: _Ep0::SetTxStatus(EndpointStatus::Stall); break; > > _Ep0::SetRxStatus(EndpointStatus::Valid); > if(_Ep0::Reg::Get() & USB_EP_CTR_TX) < _Ep0::ClearCtrTx(); if(TempAddressStorage != 0) < _Regs()->DADDR = USB_DADDR_EF | (TempAddressStorage & USB_DADDR_ADD); TempAddressStorage = 0; > _Ep0::SetRxStatus(EndpointStatus::Valid); > >
Интерфейс HID
HID-устройство — это устройство как минимум с одним интерфейсом типа HID, поэтому в библиотеке класс HID — это производный от интерфейса:
Класс интерфейса hid
template class HidInterface : public Interface < using Base = Interface; public: using Endpoints = Base::Endpoints; static uint16_t FillDescriptor(InterfaceDescriptor* descriptor) < uint16_t totalLength = sizeof(InterfaceDescriptor); *descriptor = InterfaceDescriptor < .Number = _Number, .AlternateSetting = _AlternateSetting, .EndpointsCount = Base::EndpointsCount, .Class = 0x03, .SubClass = _SubClass, .Protocol = _Protocol >; _Hid* hidDescriptor = reinterpret_cast(++descriptor); *hidDescriptor = _Hid < >; uint8_t* reportsPart = reinterpret_cast(++hidDescriptor); uint16_t bytesWritten = _Hid::FillReports(reportsPart); totalLength += sizeof(_Hid) + bytesWritten; EndpointDescriptor* endpointsDescriptors = reinterpret_cast(&reportsPart[bytesWritten]); totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + . ); return totalLength; > private: >;
Собственно, библиотечная часть на этом окончена, в дальнейшем, конечно, к классу HidInterface будут добавлены другие стандартные классы, что представляет собой задачу чисто техническую, ничего изобретать уже не нужно (надеюсь).
HID-устройство
Теперь давайте из всего этого сделаем устройство, которое будет содержать один светодиод (потому что так удобно, он есть на плате BluePill) и поддерживать возможность управления этим светодиодом с компьютера (через USB HID Demonstrator).
Основной любого HID-устройства является Report, определяющий порядок взаимодействия. В нашем случае он будет достаточно простым:
Спрятала васаби в соломинку и была съедена мужем
using Report = HidReport< 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) 0xc0 // END_COLLECTION >;
Далее нужно определить всю ирерахию: конечные точки, интерфейс, конфигурацию и устройство:
using HidDesc = HidDescriptor; using LedsControlEpBase = OutEndpointBase; using EpInitializer = EndpointsInitializer; using Ep0 = EpInitializer::ExtendEndpoint; using LedsControlEp = EpInitializer::ExtendEndpoint; using Hid = HidInterface; using Config = HidConfiguration; using MyDevice = Device;
В общем-то всё, осталось написать обработчик для конечной точки управления светодиодом:

using Led = IO::Pc13Inv; // Inv - инвертированный. template<> void LedsControlEp::Handler() < LedsControlEp::ClearCtrRx(); uint8_t* buffer = reinterpret_cast(LedsControlEp::Buffer); bool needSet = buffer[1] != 0; // Код почти целиком позаимствован из поста "STM32 и USB-HID — это просто". // Не стал изменять его для удобной навигации. switch(buffer[0]) < case 1: needSet ? Led::Set() : Led::Clear(); break; >LedsControlEp::SetRxStatus(EndpointStatus::Valid); >
Целиком файл main.c для Stm32f103 выглядит так (по-моему, достаточно компактно):
Полный код программы
#include #include #include using namespace Zhele; using namespace Zhele::Clock; using namespace Zhele::IO; using namespace Zhele::Usb; using Report = HidReport< 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) 0xc0 // END_COLLECTION >; using HidDesc = HidDescriptor; using LedsControlEpBase = OutEndpointBase; using EpInitializer = EndpointsInitializer; using Ep0 = EpInitializer::ExtendEndpoint; using LedsControlEp = EpInitializer::ExtendEndpoint; using Hid = HidInterface; using Config = HidConfiguration; using MyDevice = Device; using Led = IO::Pc13Inv; void ConfigureClock(); void ConfigureLeds(); int main() < ConfigureClock(); ConfigureLeds(); Zhele::IO::Porta::Enable(); MyDevice::Enable(); for(;;) < >> void ConfigureClock() < PllClock::SelectClockSource(PllClock::ClockSource::External); PllClock::SetMultiplier(9); Apb1Clock::SetPrescaler(Apb1Clock::Div2); SysClock::SelectClockSource(SysClock::Pll); MyDevice::SelectClockSource(Zhele::Usb::ClockSource::PllDividedOneAndHalf); >void ConfigureLeds() < Led::Port::Enable(); Led::SetConfiguration(); Led::SetDriverType(); Led::Set(); > template<> void LedsControlEp::Handler() < LedsControlEp::ClearCtrRx(); uint8_t* buffer = reinterpret_cast(LedsControlEp::Buffer); bool needSet = buffer[1] != 0; switch(buffer[0]) < case 1: needSet ? Led::Set() : Led::Clear(); break; >LedsControlEp::SetRxStatus(EndpointStatus::Valid); > extern "C" void USB_LP_IRQHandler()
Заключение
Не совсем очевидная реализация библиотечного кода (в прошлой статье получил заслуженные комментарии в стиле «Не хотел бы увидеть такой код в продакшне», «Как это поддерживать» и т.п.) позволила максимально упростить непосредственно реализацию устройства, не нужно даже вручную объявлять дескрипторы: все генерируется из подставленных в шаблоны аргументов. Использование variadic-шаблонов помогло избавиться от лишних зависимостей. Прошивка тоже получается компактной, код из примера выше с оптимизацией Og вышел в 2360 байтов Flash и 36 байтов RAM (с оптимизацией Os прошивка весит 1712 байтов, но не работает. Пока не разобрался, почему именно), что я считаю неплохим результатом.
Благодарности
За замечательный пост про HID благодарен @RaJa. Также менее, чем за неделю до написания этого поста вышел еще крутой материал по HID от @COKPOWEHEU. Без этих постов я бы ничего не осилил. Еще большую помощь оказали пользователи с форума radiokot (COKPOWEHEU и VladislavS), был приятно удивлен оперативностью ответов и желанием помочь.
Hid что это

Что такое HID?
Hardware ID (HID) — это идентификатор вашего компьютера, который формируется на основании данных об оборудовании. Он состоит из 5 блоков букв диапазона A-F и цифр 0-9.
HID не содержит кириллицу или буквы O. Пожалуйста, копируйте HID в буфер обмена во избежание ошибок ручного ввода.
В комплекс этих данных входят материнская плата, оперативная память, процессор, жесткие диски и др.
Для каждого компьютера формируется уникальный идентификатор. Не исключены редкие совпадения, но это сотые доли процента в общей массе компьютеров.
Как получить идентификатор HID?
Запустите программу Key Collector без файла лицензии (скачать Key Collector по ссылке: install.exe). Использовать для этого какие-либо сторонние программы не требуется.
Для запуска программы могут потребоваться дополнительные компоненты. Подробнее о системных требованиях. Пошаговое руководство по установке.
Текущий HID отобразится на стартовом окне. Скопируйте его в буфер обмена.

Key Collector выдает 4 варианта идентификатора HID: первый является основным, остальные — резервные.

В заявке на покупку мы рекомендуем указывать именно первый (основной), однако указание любого из трех других не будет являться ошибкой.
Я скрывала это очень давно…I hid this for a long time…
Первая цифра в первом блоке символов является служебной. Она определяет номер HID. Поэтому если по заявке на выдачу лицензии 1 BA03-XXXX-XXXX-XXXX-XXXX вам присылаются файл лицензии для BA03-XXXX-XXXX-XXXX-XXXX (без ведущего символа), то это не является ошибкой.
USB HID CAN контроллер CAR PC Windows / Linux
В редких случаях после заполнения заявки на приобретение программы или смену лицензии техническая поддержка может запросить у вас дополнительный идентификатор. В этом случае по запросу потребуется прислать один из резервных HID (не только первый HID в списке, но и другой, например, второй).
При каких условиях HID может изменяться?
Идентификатор HID может измениться при изменении конфигурации оборудования ПК (добавление нового диска, оперативной памяти, переустановка ОС и пр.). При этом ранее выданная лицензия перестает работать. Это проявляется в виде окна с сообщением: « Файл лицензии отсутствует в папке с программой или не соответствует конфигурации ПК » .
В случае изменения конфигурации ПК и невозможности запустить программу вам требуется выполнить бесплатную замену лицензионного файла.
После замены лицензии вы получите новый актуальный файл лицензии lic.license, который необходимо поместить в папку с программой (по умолчанию программа устанавливается в папку «C:/Program Files/Key Collector 4»).
Информируем работающих на виртуальных машинах или VPS/VDS пользователей о том, что изменение HID может быть вызвано обновлением виртуальной машины или драйверов.
В этом случае нужно также обновить лицензию на новую конфигурацию компьютера и поместить актуальный файл лицензии lic.license в рабочую директорию Key Collector.
Некоторые часто задаваемые вопросы по ксенону

Что означает приставка HID в названии ксеноновых комплектов?
Приставка HID — это аббревиатура трех английских слов High Intensity Discharge (пер. Разряд высокой интенсивности). Эти слова говорят о принципе работы лампы.
Почему HID- оборудование называют «Ксеноном»?
Действительно, HID-комплекты головного автомобильного света в народе называют Ксенон. Это произошло по причине того, что основным газом, которым наполнена колба является инертный газ ксенон. Хотя помимо этого газа в колбе есть и другие элементы — например соли-галиды.
Какие преимущества ксенона перед галогеновой лампой?
Основными преимущества ксенона:
Ксеноновая лампа ярче галогеновой минимум в 2 раза, максимум в 2.7 раза в зависимости от цветовой температуры.
Ксенон не образует «световой стены» при дождливой или туманной погоде. Излучение газоразрядной дуги высококогерентно, то есть все возможные частоты излучения лежат в узком диапазоне и рассеяние на каплях воды минимально.
Ксеноновая лампа служит дольше галогеновой. Заявляемый производителями срок службы галогеновой лампы — 500 часов, ксеноновой — 2000 часов. Плюс благодаря отсутствию спирали накала в газоразрядной колбе — лампа не боится тряски
Ксенон повышает комфорт и безопасность вождения. Статистика показывает, что большинство аварий происходит либо в темное время суток либо в плохую погоду. И причиной аварий является плохая видимость. Ксенон отлично решает эти проблемы. Дополнительно, благодаря улучшению видимости, глаза меньше устают. Так что ксенон еще и экономит здоровье водителя.
Что такое цветовая температура и как она влияет свет?
Цветовая температура — это физическая величина, которая говорит о спектре излучения. К сожалению в народе распространено ошибочное мнение, что чем выше цветовая температура, тем мощнее лампа. Цветовая температура это всего лишь модель, которая говорит о том, какой должна быть температура нагретого идеального тела, чтобы оно светилось тем или иным цветом. Так солнце дает нам белый(в нашем понимании) свет, температура на поверхности солнца 5500К, поэтому можно говорить, что лампа, дающая такой же свет по спектру как и солнце имеет цветовую температуру 5500К. Наиболее часто встречающиеся цветовые температуры ламп: 4300К (бело-желтый цвет), 4500 К(белый цвет), 6000К(бело-голубой цвет), 7500К(небесно-голубой). То есть чем выше цветовая температура, тем ближе цвет лампы к фиолетовому, а чем ниже — к желтому.

Диаграмма зависимости светового потока (яркости) лампы от цветовой температуры
Как видно из диаграммы, самой яркой лампой является лампа с цветовой температурой 4200-4300К.
Не расплавится ли моя фара после установки ксенона?
Нет. Так как температура на поверхности ксеноновой лампы ниже, чем у галогеновой. Это возможно благодаря тому, что эффективность ксеноновой лампы гораздо выше чем у галогеновой, то есть большинство энергии уходит в свет, а не в нагрев нити накаливания.