В прошлый раз показал один из способов распределение ресурсов между конечными точками, а именно регистров EPnR, памяти под дескрипторы буферов и под сами буферы. Предлагаю продолжить начатое и рассмотреть написанную библиотеку на примере создания простого HID-устройства, позволяющего управлять светодиодом.

Разделение прерывания

Кроме обозначенных ранее ресурсов конечные точки также делят единственное прерывание. Соответственно, общий (главный) обработчик должен корректно передать управление обработчику прерывания нужной конечной точки. Номер конечной точки, к которой обращается хост, записано в битах EP_ID регистра ISTR. Придерживаясь линии реализации полностью шаблонной библиотеки, получил следующий класс:

using EpRequestHandler = std::add_pointer_t<void()>;
template<typename...>
class EndpointHandlersBase;
template<typename... Endpoints, int8_t... Indexes>
class EndpointHandlersBase<TypeList<Endpoints...>, Int8_tArray<Indexes...>>
{
public:
  // Массив указателей на обработчики
  static constexpr EpRequestHandler _handlers[] = {Endpoints::Handler...};
  // Индексы обработчиков
  static constexpr int8_t _handlersIndexes[] = {Indexes...};
public:
  inline static void Handle(uint8_t number, EndpointDirection direction)
  {
    _handlers[_handlersIndexes[2 * number + (direction == EndpointDirection::Out ? 1 : 0)]]();
  }
};

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

template<int8_t Index, typename Endpoints>
class EndpointHandlersIndexes
{
  // Предикат для поиска очередной конечной точки.
  using Predicate = Select<Index % 2 == 0, IsTxOrBidirectionalEndpointWithNumber<Index / 2>, IsRxOrBidirectionalEndpointWithNumber<Index / 2>>::value;
  static const int8_t EndpointIndex = Search<Predicate::template type, Endpoints>::value;
public:
  // В конец массива индекса вставляется номер соответствующей конечной точки или -1 в случае пропуска.
  using type = typename Int8_tArray_InsertBack<typename EndpointHandlersIndexes<Index - 1, Endpoints>::type, EndpointIndex>::type;
};
template<typename Endpoints>
class EndpointHandlersIndexes<-1, Endpoints>
{
public:
  using type = Int8_tArray<>;
};

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

Класс конечной точки

Из-за необходимости распределить ресурсы между конечными точками пришлось разделить код на два класса: базовый, который и нужно инстанцировать:

template <uint8_t _Number, EndpointDirection _Direction, EndpointType _Type, uint16_t _MaxPacketSize, uint8_t _Interval>
class EndpointBase
...

А также расширенный, с адресами буферов и регистра (которые назначаются менеджером, описанным в прошлой статье). Из-за различий между конечными точками разных типов/направлений (например, у однонаправленной конечной точки типа Interrupt будет один буфер, а у двунаправленной или Bulk с двойной буферизацией - два) для каждого типа объявлен свой класс:

template <typename _Base, typename _Reg>
class Endpoint : public _Base
...
template<typename _Base, typename _Reg, uint32_t _TxBufferAddress, uint32_t _TxCountRegAddress, uint32_t _RxBufferAddress, uint32_t _RxCountRegAddress>
class BidirectionalEndpoint : public Endpoint<_Base, _Reg>
...
template<typename _Base, typename _Reg, uint32_t _Buffer0Address, uint32_t _Count0RegAddress, uint32_t _Buffer1Address, uint32_t _Count1RegAddress>
class BulkDoubleBufferedEndpoint : public Endpoint<_Base, _Reg>

Конечная точка на текущий момент реализована простой: экспортирует метод инициализации (в котором заполняется регистр EPnR), метод заполнения дескриптора, методы управления битами регистра (Очистка битов CTR_TX/RX, установка битов TX/RX_STATUS), а также отправку данных.

Класс интерфейса

Следующей сущностью в иерархии является интерфейс, который, по сути (как я понимаю) есть просто контейнер для конечных точек, поэтому реализующий его класс очень простой (прикладываю код полностью, потому что здесь применяется мощь variadic-шаблонов, который позволил исключить лишние зависимости):

template <uint8_t _Number, uint8_t _AlternateSetting = 0, uint8_t _Class = 0, uint8_t _SubClass = 0, uint8_t _Protocol = 0, typename... _Endpoints>
class Interface
{
public:
  using Endpoints = Zhele::TemplateUtils::TypeList<_Endpoints...>;
  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<EndpointDescriptor*>(++descriptor);
    totalLength += (_Endpoints::FillDescriptor(endpointsDescriptors++) + ...);

    return totalLength;
  }
};

Класс конфигурации

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

template <uint8_t _Number, uint8_t _MaxPower, bool _RemoteWakeup = false, bool _SelfPowered = false, typename... _Interfaces>
class Configuration
{
public:
  using Endpoints = Zhele::TemplateUtils::Append_t<typename _Interfaces::Endpoints...>;
  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<_Regs, _IRQNumber, _ClockCtrl, _UsbVersion, _Class, _SubClass, _Protocol, _VendorId, _ProductId, _DeviceReleaseNumber, _Ep0, _Configurations...>;
  using Endpoints = Append_t<typename _Configurations::Endpoints...>;
  using Configurations = TypeList<_Configurations...>;

  // Replace Ep0 with this for correct handler register.
  using EpBufferManager = EndpointsManager<Append_t<_Ep0, Endpoints>>;
  using EpHandlers = EndpointHandlers<Append_t<This, Endpoints>>;
...

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

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<SetupPacket*>(_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<GetDescriptorParameter>(setup->Value)) {
        case GetDescriptorParameter::DeviceDescriptor: {
          DeviceDescriptor tempDeviceDescriptor;
          FillDescriptor(reinterpret_cast<DeviceDescriptor*>(&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<0, Configurations>::type::FillDescriptor(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]));
          _Ep0::Writer::SendData(reinterpret_cast<ConfigurationDescriptor*>(&temp[0]), setup->Length < size ? setup->Length : size);
          break;
        }
        case GetDescriptorParameter::HidReportDescriptor: {
          uint16_t size = sizeof(GetType_t<0, Configurations>::HidReport::Data);
          _Ep0::Writer::SendData(GetType_t<0, Configurations>::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 <uint8_t _Number, uint8_t _AlternateSetting, uint8_t _SubClass, uint8_t _Protocol, typename _Hid, typename... _Endpoints>
class HidInterface : public Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>
{
  using Base = Interface<_Number, _AlternateSetting, 0x03, _SubClass, _Protocol, _Endpoints...>;
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<_Hid*>(++descriptor);
    *hidDescriptor = _Hid {
    };
    uint8_t* reportsPart = reinterpret_cast<uint8_t*>(++hidDescriptor);
    uint16_t bytesWritten = _Hid::FillReports(reportsPart);

    totalLength += sizeof(_Hid) + bytesWritten;

    EndpointDescriptor* endpointsDescriptors = reinterpret_cast<EndpointDescriptor*>(&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<0x1001, Report>;

using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;
using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;

using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;
using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;

using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;
using Config = HidConfiguration<0, 250, false, false, Report, Hid>;
using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

В общем-то всё, осталось написать обработчик для конечной точки управления светодиодом:

using Led = IO::Pc13Inv; // Inv - инвертированный.

template<>
void LedsControlEp::Handler()
{
  LedsControlEp::ClearCtrRx();
  uint8_t* buffer = reinterpret_cast<uint8_t*>(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 <clock.h>
#include <iopins.h>
#include <usb.h>

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<0x1001, Report>;

using LedsControlEpBase = OutEndpointBase<1, EndpointType::Interrupt, 4, 32>;
using EpInitializer = EndpointsInitializer<DefaultEp0, LedsControlEpBase>;

using Ep0 = EpInitializer::ExtendEndpoint<DefaultEp0>;
using LedsControlEp = EpInitializer::ExtendEndpoint<LedsControlEpBase>;

using Hid = HidInterface<0, 0, 0, 0, HidDesc, LedsControlEp>;
using Config = HidConfiguration<0, 250, false, false, Report, Hid>;
using MyDevice = Device<0x0200, DeviceClass::InterfaceSpecified, 0, 0, 0x0483, 0x5711, 0, Ep0, Config>;

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::Configuration::Out>();
  Led::SetDriverType<Led::DriverType::PushPull>();
  Led::Set();
}

template<>
void LedsControlEp::Handler()
{
  LedsControlEp::ClearCtrRx();
  uint8_t* buffer = reinterpret_cast<uint8_t*>(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()
{
  MyDevice::CommonHandler();
}

Заключение

Не совсем очевидная реализация библиотечного кода (в прошлой статье получил заслуженные комментарии в стиле "Не хотел бы увидеть такой код в продакшне", "Как это поддерживать" и т.п.) позволила максимально упростить непосредственно реализацию устройства, не нужно даже вручную объявлять дескрипторы: все генерируется из подставленных в шаблоны аргументов. Использование variadic-шаблонов помогло избавиться от лишних зависимостей. Прошивка тоже получается компактной, код из примера выше с оптимизацией Og вышел в 2360 байтов Flash и 36 байтов RAM (с оптимизацией Os прошивка весит 1712 байтов, но не работает. Пока не разобрался, почему именно), что я считаю неплохим результатом.

Благодарности

За замечательный пост про HID благодарен @RaJa. Также менее, чем за неделю до написания этого поста вышел еще крутой материал по HID от @COKPOWEHEU. Без этих постов я бы ничего не осилил. Еще большую помощь оказали пользователи с форума radiokot (COKPOWEHEU и VladislavS), был приятно удивлен оперативностью ответов и желанием помочь.