STM32 Modular USB Composite device
Проект является логическим продолжением другого проекта на Хабре — CDC+MSC USB Composite Device на STM32 HAL и рассказыват как на STM32 создать проект с несколькими USB устройствами, с читаемой структурой и используя типовые модули. Конкретно рассмотрен пример комбинации HID + CDC UART + CDC, а также рассказано как этот проект возможно расширить другими интерфейсами.
Мотивация
Once upon a time… Появилась задача сделать компактное устройство с USB Serial конвертером на борту плюс немного простых ф-ций. Ок, есть готовая библиотека для STM32. Затем аппетиты стали расти и «немного» простых ф-ций перерасло в полноценное USB HID устройство. Ок, библиотека HID тоже есть, но оказалось, что это не такая уж тривиальная задача объединить два USB устройства на одном интерфейсе. На удачу, на Хабре нашлась статья которая как раз и решала подобную задачу — CDC+MSC USB Composite Device на STM32 HAL (ссылка выше). Спасибо автору за его труд! Все заработало. Однако, аппетиты все росли и росли и добавление дополнительных интерфейсов в том же стиле превращало код в нечто ужасное и не читаемое. То, что нормально смотрелось для двух различных интерфейсов уже для трех, причем два из которых были однотипными, смотрелось совсем плохо. А на горизонте у шефа появлялись все новые и новые фантазии на тему функционала «стмки». Также, поскольку функционал все увеличивался и его отладка становилась сложнее и сложнее, то назрел еще один тип USB Serial устройства для вывода печати и отправки управляющих команд с консоли на сам контроллер. Так появился Internal Control (ICTRL). В общем — наболело и была поставлена новая задача…
Задача
На базе ST библиотеки создать каркас на уровне USB HAL, в который можно отдельными блоками добавлять различные функции/устройства по необходимости, без особых изменений в коде самого HAL. В идеале, для добавления нового функционала в проект должен линковаться соответствующий модуль, плюс пара строк в main — инициализация и вызовы в главном цикле.
Для начала, такими блоками были выбраны два наиболее часто используемых типа устройств — HID и USB Serial конвертер. Причем конвертер мог быть как UART, так и I2C, SPI, 1Wire. ICTRL подразумевался во всех последующих устройствах уже по-умолчанию.
Для кого предназначен проект
Описание ниже подразумевает, что читатель полностью прочитал и осознал статью «CDC+MSC USB Composite Device на STM32 HAL», где структура USB HAL библиотеки от ST описана достаточно подробно. Данный проект посвящен именно переходу от структуры предложенной ST к более модульному варианту без разбора основ работы USB. Также предполагается, что у читателя родной язык С или Assembler и в строках кода читаются не буквы, а блоки их связи и взаимозависимости. Для тех у кого программа на микроконтроллере это галочки в автогенераторе или наоборот только запись/чтение регистров, тем будет не просто. Но, как говорится, что нас не убивает, то делает сильнее.
Введение
В качестве примера была выбрана одна из тривиальных конфигураций — USB UART конвертер + HID устройство и еще одно USB Serial устройство для управления самим контроллером и отладочной печати. Основная специфика HID устройства и ICTRL была сознательно опущена и оставлен только минимальный функционал, так сказать на рассаду и чтоб не перегружать код при чтении.
Описание структуры устройства — USB Composite
UBS Composite — USB устройство которое состоит из 3 интерфейсов, где каждый интерфейс, с точки зрения пользователя на хост системе, является отдельным устройством.
Определены следующие устройства:
CDC UART
CDC ICTRL
HID
CDC
CDC (Communication Device Class) — это класс USB устройств общий для CDC UART и CDC ICTRL. В данном проекте используется PSTN120 профиль класса USB CDC. Если по-простому, то с точки зрения хост системы, это обычный USB Serial конвертер и не требует специальных драйверов. Описание CDC на usb.org:
https://www.usb.org/document-library/class-definitions-communication-devices-12
Устройство понимает команды конфигурации скорости и формата посылки (старт, стоп биты, биты четности и т.д.). В зависимости от типа нижестоящего интерфейса (downward facing interface, aka DFI) может выполнять различные ф-ции, например, такие как UART, SPI или I2C конвертер. В данном проекте реализовано две разновидности CDC устройств — CDC UART и CDC ICTRL.
CDC UART
Транслирует полученные данные через аппаратный UART порт контроллера в USB CDC устройство и обратно. Т.е. работает как тривиальный USB UART конвертер.
CDC ICTRL
Устройство не связано с внешними интерфейсами, а генерирует и обрабатывает данные локально. Может использоваться, например, для вывода отладочной печати и передачи управляющих команд на контроллер.
HID
Не стандартное (vendor specific) USB HID устройство которое периодически передает на хост данные со встроенного температурного датчика и напряжение питания контроллера. Описание HID интерфейса:
usb.org/sites/default/files/hid1_11.pdf
Описания структуры HID устройства (USBD_HidDesc
) и формата пакетов (Dev0_HID_ReportDesc
hid report descriptor) отсылается на хост в момент подключения и инициализации HID интерфейса.
Основные типы данных, объекты и их зависимости
Связь объектов и их иерархия
USBD_Handle hUsbDevice
— USB устройство верхнего уровня (aka Middleware)
Содержит структуры описания оконечных точек (end points ep_in/ep_out), статуса, дескриптора устройства (pDesc
), указатели на ф-ции обработки запросов (pClass
), указатель на USB устройство HAL уровня (pPCDHandle
), а также указатели на отдельные интерфейсы (intf
).
Описание типа USBD_Handle
typedef struct _USBD_Handle {
uint8_t id;
uint32_t dev_config;
uint32_t dev_default_config;
uint32_t dev_config_status;
USBD_Speed dev_speed;
USBD_Endpoint ep_in[16];
USBD_Endpoint ep_out[16];
__IO uint32_t ep0_state;
uint32_t ep0_data_len;
__IO uint8_t dev_state;
__IO uint8_t dev_old_state;
uint8_t dev_address;
uint8_t dev_connection_status;
uint8_t dev_test_mode;
uint32_t dev_remote_wakeup;
USBD_SetupReq request;
USBD_Descriptors *pDesc;
USBD_Class *pClass;
uint8_t ConfIdx;
USBD_ConfigDesc *config_desc;
usbd_intf_t intf[COMPOSITE_INTF_NUM];
PCD_HandleTypeDef *pPCDHandle;
} USBD_Handle;
Структура основана на автоматически сгенерированном коде CubeIDE, однако содержит существенные изменения. Первое — полностью типизированные и именованные дескрипторы устройства, интерфейсов, оконечных точек и т.п. В оригинальном коде дескриптор устройства это байтовый массив (спасибо хоть в комменты название полей вставили), который при любом изменении надо заново руками пересчитывать длину и по сто раз перепроверять. В предложенном коде дескриптор устройства состоит из определенных стандартом полей и массивом неопределенного размера, который, в свою очередь, дополняется другими дескрипторами по мере регистрации интерфейсов. В качестве примера ниже приведен заголовок дескриптора устройства с полями именованными в соответствии со стандартом USB.
Типизированное описание дескриптора устройства (USBD_ConfigDesc)
typedef struct {
uint8_t bLength; /* Size of this descriptor in bytes. 0x09 */
uint8_t bDescriptorType; /* Configuration (assigned by USB). 0x02 */
uint16_t wTotalLength; /* Total length of data returned for this configuration. */
uint8_t bNumInterfaces; /* Number of interfaces supported by this configuration. */
uint8_t bConfigurationValue; /* Value to use as an argument to Set Configuration to
select this configuration.*/
uint8_t iConfiguration; /* Index of string descriptor describing this configuration.
In this case there is none*/
uint8_t bmAttributes; /* Configuration characteristics */
uint8_t bMaxPower; /* Maximum power consumption of USB device from bus
in this specific configuration when the device is fully
operational. Expressed in 2 mA units */
uint8_t data[0]; /* Place holder for other descriptors */
} USBD_ConfigDesc;
А так выглядит инициализация HID дескриптора с типизированными дескрипторами.
/************** Descriptor of CUSTOM HID interface ****************/
USBD_Dev0_HID_ConfigDesc dev0_hid_config_desc_template = {
.interface_desc = {
.bLength = sizeof(USBD_InterfaceDesc),
.bDescriptorType = USB_DESC_TYPE_INTERFACE,
.bInterfaceNumber = TBD, /* Initialized at HID_Register() */
.bAlternateSetting = 0x00,
.bNumEndpoints = 0x02,
.bInterfaceClass = 0x03, /* USB Class HID = 3 */
.bInterfaceSubClass = 0x00, /* bInterfaceSubClass : 1=BOOT, 0=no boot*/
.nInterfaceProtocol = 0x00, /* nInterfaceProtocol : 0=none, 1=keyb, 2=mouse*/
.iInterface = 0x00,
},
.hid_desc = {
.bLength = sizeof(USBD_HidDesc),
.bDescriptorType = HID_DESCRIPTOR_TYPE, /* bDescriptorType: HID */
.bcdHID = 0x0111, /* HID Class Spec release number 1.11 */
.bCountryCode = 0x00, /* 0x17 */
.bNumDescriptors = 0x01, /* Number of class descriptors to follow */
.bRepDescriptorType = HID_REPORT_DESC, /* bDescriptorType */
.wRepDescriptorLength = host2usb_u16(sizeof(Dev0_HID_ReportDesc)),
/* wItemLength: Tot len of Report descriptor*/
},
.ep_in = {
.bLength = sizeof(USBD_EpDesc), /* bLength: Endpoint Descriptor size */
.bDescriptorType = USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
.bEndpointAddress = TBD, /* Initialized at HID_Register */
.bmAttributes = 0x03, /* bmAttributes: Interrupt endpoint */
.wMaxPacketSize = host2usb_u16(sizeof(dev0_in_report_t)), /* wMaxPacketSize: 2 Byte max */
.bInterval = DEV0_HID_FS_BINTERVAL, /* bInterval: Polling Interval */
},
.ep_out = {
.bLength = sizeof(USBD_EpDesc), /* bLength: Endpoint Descriptor size */
.bDescriptorType = USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
.bEndpointAddress = TBD, /* Initialized at HID_Register */
.bmAttributes = 0x03, /* bmAttributes: Interrupt endpoint */
.wMaxPacketSize = host2usb_u16(sizeof(dev0_out_report_t)), /* wMaxPacketSize: 2 Byte max */
.bInterval = DEV0_HID_FS_BINTERVAL, /* bInterval: Polling Interval */
}
};
Где инициализация поля в значение «TBD» означает, что оно будет проинициализированно во время работы в зависимости от положения этого дескриптора в структуре дескриптора устройства.
Второе существенное отличие, ради которого всё и затевалось — это массив интерфейсов в структуре USBD_Handle:
usbd_intf_t intf[COMPOSITE_INTF_NUM];
Где каждый интерфейс это одно из подустройств — CDC UART, CDC ICTRL или HID.
Каждый интерфейс содержит в себе набор указателей на функции для обработки стандартных USB запросов (callbacks), а также контекст специфичный для конкретного устройства. В оригинальном коде каждый интерфейс и его контекст были отдельными полям структуры, что значительно усложняло добавление новых или изменение существующих интерфейсов. Переход к унифицированному описанию интерфейсов убирает этот недостаток.
Описание структуры интерфейса usbd_intf_t
typedef struct usbd_intf_s {
/* Interface’s context */
union intf_dev_handle_u {
void *ctx;
struct _USBD_HID_Handle *hid;
struct _USBD_CDC_Handle *cdc;
} h;
void (*Init)(union intf_dev_handle_u h, struct _USBD_Handle *pdev, uint8_t cfgidx);
void (*DeInit)(union intf_dev_handle_u h, uint8_t cfgidx);
uint8_t (*EP0_RxReady)(union intf_dev_handle_u h);
uint8_t (*Setup)(union intf_dev_handle_u h, enum setup_recp_e, uint8_t recp_idx, USBD_SetupReq *req);
uint8_t (*DataIn)(union intf_dev_handle_u h, uint8_t epnum);
uint8_t (*DataOut)(union intf_dev_handle_u h, uint8_t epnum);
} usbd_intf_t;
По приходу USB пакета вызывается соответствующая функция из набора USBD_Class и если пакет предназначен индивидуально для какого-либо из устройств, то вызывается функция соответствующего интерфейса с передачей его личного контекста.
USBD_xxx_Handle
— контексты интерфейсов.
В проекте определены следующие объекты этого типа (по одному на каждый интерфейс):
USBD_CDC_Handle g_cdc0;
USBD_CDC_Handle g_cdc1;
USBD_HID_Handle g_hid0;
USBD_CDC_Handle
— g_cdc0, g_cdc1
Описание типа USBD_CDC_Handle
typedef struct _USBD_CDC_Handle
{
/* Initialized @ USBD_Composite_Init -> USBD_CDC_Init */
uint8_t ifnum_cmd;
uint8_t epnum_cmd;
uint8_t ifnum_data;
uint8_t epnum_data;
struct _USBD_Handle *pdev;
/* Initialized @ cdc_uart_init */
cdc_dfi_t *dfi;
uint8_t data[CDC_DATA_MAX_PACKET_SIZE] __attribute__ ((aligned (4)));
uint8_t CmdOpCode;
uint8_t CmdLength;
__IO uint32_t TxState;
__IO uint32_t RxState;
USBD_CDC_ConfigDesc *cfg_desc;
} USBD_CDC_Handle;
С точки зрения USB, оба CDC устройства одинаковые, поэтому и тип у них один — USBD_CDC_Handle
. Отличаются они вниз смотрящим интерфейсом (DFI) — UART или ICTRL, который прилинковывается к CDC устройству в процессе инициализации.
DFI (downward facing interface) — интерфейс для связи CDC устройства с его функционалом. В данном проекте это UART и ICTRL.
Описание DFI структуры cdc_dfi_t
/* CDC downward facing interface - DFI */
typedef struct cdc_dfi_s {
/* Functional specific part – individual for each DFI type */
union {
struct cdc_ictrl_s *cdc_ictrl;
struct cdc_uart_s *cdc_uart;
} ctx;
/* USB specific part common for all CDC interfaces */
void (*start_rx) (struct cdc_dfi_s *dfi);
void (*stop_rx) (struct cdc_dfi_s *dfi);
void (*on_idle) (struct cdc_dfi_s *dfi);
void (*on_control) (struct cdc_dfi_s *dfi, uint8_t cmd, uint8_t* buf, uint16_t len);
void (*on_rx) (struct cdc_dfi_s *dfi, uint32_t len);
uint8_t *(*get_ds_buffer) (struct cdc_dfi_s *dfi);
} cdc_dfi_t;
Объекты типа cdc_dfi_t
сами по себе не существуют, а всегда являются составной частью какого-то функционала — cdc_uart_t
или cdc_ictrl_t
, см. описание ниже.
cdc_uart_t — g_cdc_uart;
typedef struct cdc_uart_s {
uart_cdc_upstream_t us;
uart_cdc_downstream_t ds;
cdc_dfi_t dfi;
} cdc_uart_t;
cdc_uart_t
— Модуль UART предназначенный для работы с CDC интерфейсом. Поскольку CDC не заботит какой функционал к нему подключен, то он не связан напрямую с cdc_uart_t, а только через DFI интерфейс. CDC_UART состоит из 3 основных частей:
uart_cdc_upstream_t us;
Отвечает за трафик от uart к хосту.uart_cdc_downstream_t ds;
Отвечает за трафик от хоста к uart.cdc_dfi_t dfi;
Набор функций для обработки вызовов со стороны USB. Например таких как обработка комманд управления, передачи буферов и т.п.
cdc_ictrl_t — g_cdc_ictrl;
typedef struct cdc_ictrl_s {
ictrl_cdc_upstream_t us;
ictrl_cdc_downstream_t ds;
cdc_dfi_t dfi;
} cdc_ictrl_t;
Модуль Internal Control (ICTRL) также как и cdc_uart_t
описанный выше связан с CDC через DFI.
USBD_HID_Handle - g_hid0
Описание типа USBD_HID_Handle
typedef struct _USBD_HID_Handle {
union {
USBD_Dev0_HID_ConfigDesc *dev0; /* Pointer to HID part in Composite device
descriptor Initialized during HID
registration upon startup */
} hid_cfg_desc;
/********** Configuration specific parameters *****/
/* Initialized @ USBD_Composite_Init -> USBD_HID_Init */
uint8_t ifnum;
uint8_t epnum;
size_t epin_size;
size_t epout_size;
uint8_t *ReportBuf;
size_t ReportBufLen;
uint8_t *ReportDesc;
size_t ReportDescLen;
struct _USBD_Handle *pdev;
/***********************************************/
uint8_t *pReport;
uint32_t Protocol;
uint32_t IdleState;
uint32_t AltSetting;
uint32_t IsReportAvailable;
CUSTOM_HID_StateTypeDef state;
void (*Register)(struct _USBD_HID_Handle *hhid, USBD_ConfigDesc *config_desc, …
USBD_HidDesc* (*GetHidDescr)(struct _USBD_HID_Handle *hhid);
void (*Init)(struct _USBD_HID_Handle *hhid, uint8_t cfgidx);
void (*DeInit)(struct _USBD_HID_Handle *hhid);
int8_t (*OutEvent)(struct _USBD_HID_Handle *hhid, uint8_t *buf, int len);
} USBD_HID_Handle;
HID устройство — это общий класс устройств, функционал которого определяется соответствующим дескриптором. HID устройство связывается со своим функционалом (в данном проекте dev0
) при начальной инициализации.
dev0_t g_dev0
— фукционал HID устройства
Описание типа dev0_t
typedef struct {
uint32_t last_report_tick;
USBD_HID_Handle *hid;
dev0_in_report_t hid_in_report;
dev0_out_report_t hid_out_report;
} dev0_t;
Занимается тем, что периодически забирает данные из модуля внутренних датчиков (imon) и оправляет их в HID для дальнейшей отправки уже в виде USB пакета (HID Report). В данном проекте функции dev0 сознательно сведены к минимуму, чтобы не перегружать код тем, что не относится к основной теме.
Основные этапы работы программы
Базовая инициализация
HAL_Init();
SystemClock_Config();
__disable_irq();
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM3_Init(); /* Used by ADC as a trigger source */
MX_USART3_UART_Init();
Типовая автосгенерированная инициализация переферии. Ничего интересного.
MX_USB_DEVICE_Init (); Инициализация USB интерфейса
За основу взят автоматически сгенерированный код CubeIDE.
По мимо всего прочего вызывает функции регистрации отдельных интерфейсов внутри составного устройства (USB Composite device).
{ /* Link interfaces into composite class */
int ep_in_use = 1;
int if_in_use = 0;
HID_Register(&g_hid0, &pdev->intf[0], pdev->config_desc, &if_in_use, &ep_in_use);
CDC_Register(&g_cdc0, &pdev->intf[1], pdev->config_desc, &if_in_use, &ep_in_use);
CDC_Register(&g_cdc1, &pdev->intf[2], pdev->config_desc, &if_in_use, &ep_in_use);
pdev->config_desc->bNumInterfaces = if_in_use;
}
Во время регистрации, каждый из интерфейсов добавляет к дескриптору устройства (pdev->config_desc
) свою индивидуальную часть и увеличивает счетчик использованных конечных точек (ep_in_use
) и их интерфейсов (if_in_use
). Указатель на свою часть в дескрипторе устройства каждый интерфейс хранит у себя и использует его во время инициализации в ф-ции pClass->Init()
.
Соответственно, при добавлении новых интерфейсов сюда надо добавить его регистрацию.
Пример ф-ции регистрации HID интерфейсаvoid Dev0_HID_Register (
USBD_HID_Handle *hhid, USBD_ConfigDesc *config_desc,
int *ifnum, int *epnum)
{
USBD_Dev0_HID_ConfigDesc *desc =
&dev0_hid_config_desc_template;
desc->interface_desc.bInterfaceNumber = *ifnum;
desc->ep_in.bEndpointAddress = EP_IN_ADDR(*epnum);
desc->ep_out.bEndpointAddress = EP_OUT_ADDR(*epnum);
hhid->hid_cfg_desc.dev0 = USBD_CfgDescAppend(
config_desc,
(uint8_t*)desc,
sizeof(USBD_Dev0_HID_ConfigDesc));
*ifnum += 1;
*epnum += 1;
}
Ф-ция USBD_CfgDescAppend()
добавляет дескриптор переданный в виде аргумента (desc
) к общему дескриптору устройства (config_desc
) и возвращает адрес позиции куда он был добавлен.
В приведенном коде дескриптор HID устройства desc
типа USBD_Dev0_HID_ConfigDesc
— это шаблон (dev0_hid_config_desc_template
) который состоит из других дескрипторов и в которых некоторые поля заменяются в зависимости от количества уже задействованных интерфейсов и оконечных точек.
#pragma pack(push, 1)
typedef struct _USBD_Dev0_HID_ConfigDesc {
USBD_InterfaceDesc interface_desc;
USBD_HidDesc hid_desc;
USBD_EpDesc ep_in;
USBD_EpDesc ep_out;
} USBD_Dev0_HID_ConfigDesc;
#pragma pack(pop)
А, например, дескриптор CDC устройства выглядит вот так:
#pragma pack(push, 1)
typedef struct _USBD_CDC_ConfigDesc {
USBD_IADDesc if_assoc_desc;
USBD_InterfaceDesc interface_desc_cmd;
USBD_FuncDescHdr func_desc_header;
USBD_FuncDescCallMng func_desc_call_mng;
USBD_FuncDescACM func_desc_acm;
USBD_FuncDescUnion func_desc_union;
USBD_EpDesc cmd_ep;
USBD_InterfaceDesc interface_desc_data;
USBD_EpDesc data_ep_out;
USBD_EpDesc data_ep_in;
} USBD_CDC_ConfigDesc;
#pragma pack(pop)
dev0_init (); Инициализация функционала HID устройства
Функции этого устройства сведены к минимуму, поэтому и в инициализации ничего интересного.
imon_init (); Инициализация модуля Internal Monitor —
Инициализация коэффициентов для вычисления показаний с внутреннего датчика температуры и напряжения с учетом калибровочных значений.
cdc_uart_init (&g_cdc_uart3, &g_cdc0, &huart3); Инициализация CDC_UART
Ф-ция связвает вместе CDC устройство (g_cdc0
) с функционалом USB UART конвертера (g_cdc_uart3
) и сам конвертер с аппаратным портом UART (huart3
). Также инициализируются функции DFI итерфейса.
cdc_ictrl_init (&g_cdc1); Инициализация CDC_ICTRL
Ф-ция связывает CDC устройство (g_cdc1
) c функционалом ictrl — g_cdc_ictrl
. В отличии от UART конвертера, ictrl не предназначен иметь несколько инстанций и поэтому он не передается в ф-цию как параметр, а используется непосредственно как глобальный объект.
HAL_ADC_Start_DMA (&hadc, (uint32_t*)&g_adc_samples[0], ADC_CH_NUM); Запуск АЦП.
АЦП работает по сигналу от таймера и по кругу записывает результат в глобальный массив.
HAL_TIM_Base_Start_IT (&htim3); Запуск таймера
Инициализация таймера с периодом 4.5 мс и генерацией сигнала для АЦП
Основной цикл выполнения программы
while (1) {
now = HAL_GetTick();
g_cnt ++;
dev0_on_idle(now);
imon_on_idle(now);
g_cdc_uart3.dfi.on_idle(&g_cdc_uart3.dfi);
g_cdc_ictrl.dfi.on_idle(&g_cdc_ictrl.dfi);
#if NAVIG
cdc_uart_dfi_on_idle();
cdc_ictrl_dfi_on_idle();
#endif
}
dev0_on_idle()
— периодически собирает данные от imon и отправляет их в USB.
imon_on_idle()
— периодически конвертирует данные с каналов АЦП в показания температуры и напряжения питания.
g_cdc_uart3.dfi.on_idle()
— функция проверяет состояния приемника UART и если предыдущая транзакция закончилась, то вычисляется оставшееся место в приемном буфере и инициируется новая DMA транзакция с генерацией прерывания по завершению. Также, функция проверяет наличие данных в приемном UART буфере и отправляет их хосту. Фактически, в основном цикле обслуживается только upstream поток. Downstream обслуживается исключительно по прерыванию от USB, т.е. по приходу нового пакета данных.
g_cdc_ictrl.dfi.on_idle()
— проверяет наличие данных для отправки на хост и если данных накопилось достаточно много (64 байта) или они залежались (>16 мс), то формируется новый пакет и отправляется в CDC интерфейс (USBD_CDC_TransmitPacket()
).
Здесь и далее все вызовы функций под препроцессорным условием NAVIG не компилируются и необходимы только для удобного чтения и навигации по коду. Так например косвенный вызов
g_cdc_uart3.dfi.on_idle()
в ходе работы выполняется какcdc_uart_dfi_on_idle()
, аg_cdc_ictrl.dfi.on_idle()
какcdc_ictrl_dfi_on_idle()
и так далее.
HAL_PCD_IRQHandler () — обработчик прерываний от USB.
Каждый приходящий пакет от USB сперва обрабатывается типовым кодом сгенерированным CubeIDE. Этот обработчик включает в себя разбор служебных пакетов, запросов дескрипторов, обработку ошибок и т.п. По приходу пакета данных, для всех оконечных точек вызываются зарегистрированные функции USB класса pClass->DataOut()
или pClass->EP0_RxReady()
, они же USBD_Composite_DataOut()
и USBD_Composite_EP0_RxReady()
, соответственно.
Для служебных пакетов которые специфичны для конкретного интерфейса вызываются ф-ции USBD_Composite_Init()
и USBD_Composite_Setup()
.
Основная суть перехода к конфигурируемому массиву USB интерфейсов состоит в том, чтобы не менять обработчик прерываний при добавлении/удалении интерфейсов из кода. Т.е. пакеты поступают на обработчики составного устройства (USBD_Composite_xxx
), а затем в цикле передаются на все зарегистрированные интерфейсы пока какой-либо из интерфейсов не заберет пакет себе (т.е. вернет код USBD_BUSY
).
USBD_Composite_Init () — Обработчик служебных пакетов
for (i = 0; i < COMPOSITE_INTF_NUM; i ++) {
usbd_intf_t *intf = &pdev->intf[i];
if (!intf || !intf->Init || !intf->h.ctx) continue;
intf->Init(intf->h, pdev, cfgidx);
#if NAVIG
USBD_HID_Init(hhid, pdev, cfgidx);
USBD_CDC_Init(hcdc, pdev, cfgidx);
#endif
}
Обработчик по очереди вызывает ф-ции Init () для всех зарегистрированных интерфейсов. Эти ф-ции инициализируют оконечные точки USB устройства на уровне периферии микроконтроллера в зависимости от того, что записано в принадлежащей им части дескриптора устройства, которая была создана во время регистрации интерфейса.
Ещё одним важным отличием от оригинального кода CubeIde является выделение специализированной области памяти под оконечные точки (PMA буфера). В оригинальном коде память распределяется полностью вручную и статически, тогда как в предложенном варианте динамически — см. ф-цию HAL_PCD_PMA_Alloc()
. Память выделяется при открытии оконечной точки в цепочке вызовов pClass->Init() => USBD_Composite_Init() => USBD_xxx_Init() => USBD_LL_OpenEP() => HAL_PCD_EP_Open()
и освобождается при закрытии USB устройства в цепочке pClass->DeInit() => ... => HAL_PCD_EP_Close().
USBD_Composite_Setup () — Обработчик служебных пакетов
for (i = 0; i < COMPOSITE_INTF_NUM; i++) {
usbd_intf_t *intf = &pdev->intf[i];
if (!intf || !intf->Setup) continue;
rc = intf->Setup(intf->h, recp, recp_idx, req);
#if NAVIG
USBD_CDC_Setup();
USBD_HID_Setup();
#endif
if (rc == USBD_BUSY || rc == USBD_FAIL) break;
}
Обработчик Setup фазы в соответствии с протоколом обмена USB. Для CDC устройств на этой стадии могут быть сконфигурированны параметры UART интерфейса — скорость, кол-во бит и т.п. Для HID устройства на этой фазе хост запрашивает дескриптор формата пакетов (ака ReportDesc
), а также дескриптор HID устройства — USBD_HidDesc
.
USBD_Composite_DataOut () — Обработчик входных данных
Функция обработчик входных данных для интерфейсов. Не забываем что название потоков в USB всегда относительно хоста. Т.е. DataOut для USB клиента — это входные данные.
В качестве параметров принимает номер интерфейса и номер оконечной точки данных для которых эти данные предназначены. Как и в других функциях композитного устройства, параметры передаются на все зарегистрированные интерфейсы и если какой-то из интерфейсов признает эти данные своими, то возвращает код USBD_BUSY
и на этом обработка пакета заканчивается.
USBD_Composite_DataIn () — Обработчик исходящих данных
Выглядит аналогично USBD_Composite_DataOut()
Компиляция и запуск
Компиляция без каких-либо особенностей все параметры в дефолтных значениях. Ниже приведен размер используемой памяти после компиляции с включенной оптимизацией и минимальным функционалом. Размер ОЗУ может варьироваться в зависимости от размеров буферов CDC конвертеров. В этом проекте особо не жадничал. Объем кода ~32кБ.
arm-none-eabi-objcopy -O binary USB-PD.elf "USB-PD.bin"
text data bss dec hex filename
33480 988 6804 41272 a138 USB-PD.elf
По совместимости с хост системами проект проверен на двух системах — Windows10 и OpenWrt.
Пример использования под Linux системой OpenWrt
Для поддержки HID и CDC устройств в конфигурацию сборки Linux необходимо добавить модули ACM и HID. Опционально usbutils.
<*> kmod-usb2................................... Support for USB2 controllers
<*> kmod-usb-hid......................... Support for USB Human Input Devices
<*> kmod-usb-acm......................... Support for modems/isdn controllers
<*> usbutils................................... USB devices listing utilities
Во время запуска OpenWrt детектирует устройства следующим образом:
[ 13.319863] hidraw: raw HID events driver (C) Jiri Kosina
[ 13.441134] cdc_acm 1-1:1.1: ttyACM0: USB ACM device
[ 13.455548] cdc_acm 1-1:1.3: ttyACM1: USB ACM device
[ 13.462624] usbcore: registered new interface driver cdc_acm
[ 13.468568] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
...
[ 13.682913] hid-generic 0003:0483:5732.0001: hiddev96,hidraw0: USB HID v1.11 Device [AV 2xCDC HID Composite device] on usb-ehci-platform-1/input0
[ 13.696697] usbcore: registered new interface driver usbhid
[ 13.702451] usbhid: USB HID core driver
После загрузки можно посмотреть вывод утилиты lsusb.
Листинг lsusb
root@OpenWrt:~#
root@OpenWrt:~# lsusb
Bus 001 Device 005: ID 0483:5732 STMicroelectronics
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
root@OpenWrt:~#
root@OpenWrt:~#
root@OpenWrt:~# lsusb -v -d 0483:5732
Bus 001 Device 005: ID 0483:5732 STMicroelectronics
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 239 Miscellaneous Device
bDeviceSubClass 2 ?
bDeviceProtocol 1 Interface Association
bMaxPacketSize0 64
idVendor 0x0483 STMicroelectronics
idProduct 0x5732
bcdDevice 0.01
iManufacturer 1 AV
iProduct 2 2xCDC HID Composite device
iSerial 3 104731433332
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 173
bNumInterfaces 5
bConfigurationValue 1
iConfiguration 2 2xCDC HID Composite device
bmAttributes 0xc0
Self Powered
MaxPower 100mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 0 No Subclass
bInterfaceProtocol 0 None
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 31
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0002 1x 2 bytes
bInterval 5
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x01 EP 1 OUT
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0001 1x 1 bytes
bInterval 5
Interface Association:
bLength 8
bDescriptorType 11
bFirstInterface 1
bInterfaceCount 2
bFunctionClass 2 Communications
bFunctionSubClass 2 Abstract (modem)
bFunctionProtocol 1 AT-commands (v.25ter)
iFunction 0
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 2 Communications
bInterfaceSubClass 2 Abstract (modem)
bInterfaceProtocol 1 AT-commands (v.25ter)
iInterface 0
CDC Header:
bcdCDC 1.10
CDC Call Management:
bmCapabilities 0x00
bDataInterface 2
CDC ACM:
bmCapabilities 0x02
line coding and serial state
CDC Union:
bMasterInterface 1
bSlaveInterface 2
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 16
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 2
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x03 EP 3 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x83 EP 3 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
Interface Association:
bLength 8
bDescriptorType 11
bFirstInterface 3
bInterfaceCount 2
bFunctionClass 2 Communications
bFunctionSubClass 2 Abstract (modem)
bFunctionProtocol 1 AT-commands (v.25ter)
iFunction 0
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 3
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 2 Communications
bInterfaceSubClass 2 Abstract (modem)
bInterfaceProtocol 1 AT-commands (v.25ter)
iInterface 0
CDC Header:
bcdCDC 1.10
CDC Call Management:
bmCapabilities 0x00
bDataInterface 4
CDC ACM:
bmCapabilities 0x02
line coding and serial state
CDC Union:
bMasterInterface 3
bSlaveInterface 4
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x84 EP 4 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 16
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 4
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x05 EP 5 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x85 EP 5 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
Device Status: 0x0001
Self Powered
root@OpenWrt:~#
root@OpenWrt:~#
root@OpenWrt:~#
Для работы с модулем ICTRL можно использовать любое терминальное приложение — screen, picocom, Minicom, Putty, и т.д. вплоть хоть до socat.
Для работы с HID устройством, лично я, использую библиотеку libusb — https://libusb.info/
Запуск и проверка на Windows
После подключения к Windows хосту в системе появляется 3 новых устройства (см. ниже) — два USB Serial устройства COM8 и COM9, а также USB Input Device.
Состояние менеджера устройств Windows при подключенном устройстве.
Для проверки USB Serial конвертера (он же CDC UART) необходимо подключить внешнее устройство.
Для проверки CDC ICTRL ничего подключать не надо. Достаточно открыть терминал на порту COM8. Например PuTTY.
Пример вывода дебаговой печати ICTRL (температура и рабочее напряжение).
В терминал выводится текущая температура и напряжение питания контроллера, которое выводится как пример дебаговой печати из ф-ции dev0_on_idle()
.
dev0_on_idle ()
void dev0_on_idle (uint32_t now_tick)
{
....
if ((imon->temp_degc != INT16_MAX) && g_dbg0 == 0){
static int cnt = 0;
ictrl_printf("[%d] %dC, %dmV\n",
cnt++, imon->temp_degc, imon->vref);
}
...
}
Этот же порт можно использовать и для передачи управляющих команд. Однако, использовать для этого терминал не совсем удобно т.к. при постоянном выводе в консоль набирать команды приходится практически в слепую. Для удобства, в некоторых случаях, можно использовать RealTerm у которого есть два специальных поля для отсылки коротких посылок.
Пример использования RealTerm для отправки коротких команд на дебаг интерфейс.
Для проверки USB HID устройства самое простое что нашлось это небольшая тулза hidapi (https://github.com/libusb/hidapi.git).
Пример использования HIDAPI Test Tool для диагностики USB HID интерфейса.
В окне Input выводятся HID пакеты от контроллера которые генерируются в ф-ции dev0_on_idle()
каждые 100 мс.
dev0_on_idle ()
#pragma pack(push, 1)
typedef struct {
uint8_t temperature;
uint8_t voltage;
} dev0_in_report_t;
#pragma pack(pop)
void dev0_on_idle (uint32_t now_tick)
{
dev0_t *dev0 = &g_dev0;
imon_t *imon = &g_imon;
if (now_tick - dev0->last_report_tick > 100 ){
dev0->last_report_tick = now_tick;
if (dev0->hid && imon->temp_degc != INT16_MAX) {
dev0->hid_in_report.temperature = (int8_t)imon->temp_degc;
dev0->hid_in_report.voltage =
(uint8_t)((imon->vref + 50) / 100) ;
HID_SendReport(
dev0->hid,
(uint8_t*)&dev0->hid_in_report,
sizeof(dev0->hid_in_report));
}
}
...
}
Значения 0×1А 0×21 — температура 26 градусов Цельсия, напряжение 3.3В.
В окне Output Data можно отправить данные на контроллер. В данном проекте Output HID report используется для управления светодиодами, просто в качестве примера.
Для написание своих программ общения с HID устройствами рекомендую библиотеку libusb. Отлично работает как на Windows, так и на OpenWrt. Заявлена еще поддержка macOS и Android, но лично не проверял.
Пример портирования на платформу STM32L072
В качестве примера портирования на другие серии STM рассмотрим портирование с использованной в проекте серии STM32G4 на STM32L072.
Создаем новый проект в два этапа:
Создание автосгенерированного проекта для инициализации генератора тактовых частот, и их распределения, переферии, прерываний и т.п. Результат можно посмотреть здесь, ветка Autogen.
Изменение модуля USB и main для поддержки составного устройства. https://github.com/avasilje/CompositeUSB_L072K.git, ветка main.
Создание автосгенерированного проекта
Создаем сгенерированный проект визардом STM32CubeIDE со следующей конфигурацией (CompositeUSB-Autogen.ioc ветки Autogen).
1.1. Timer3
Используется как задающий таймер для АЦП, с периодом 4.5 мс
1.2. ADC + DMA Channel 1
АЦП работает в цикличном режиме (mode = CIRC) по сигналу от таймера Т3.
Используются два внутренних канала — VrefInt, Temperature Sensor.
1.3. USB Custom HID device
Включем прерывание, конфигурируем пины на PA11, PA12. Параметры HID устройства особого значения не имеют т.к. код будет в последствии заменен.
1.4. USART1 + DMA Channel 2,3
1.5. 3xGPIO
1.6. Clocks — internal High Speed (USB crystal less configuration)
Добавляем код модуля «imon» (imon.c, imon.h)
Помимо файлов самого imon, ещё необходимо добавить файл stm32l0xx_ll_adc.h в директорию CompositeUSB_L072K\Drivers\STM32L0xx_HAL_Driver\Inc.
Файл можно взять из SDK соответствующей архитектуры. Например отсюда.
Файл необходим для описания некоторых констант АЦП (TEMPSENSOR_CAL1_ADDR
, и т.п.)Добавляем инициализацию и запуск переферии, глобальный масив для данных АЦП, а также инициализацию и обработчик модуля imon.
/* USER CODE BEGIN Includes */
#include "imon.h"
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
int16_t g_adc_samples[ADC_CH_NUM]; /* DMA destination */
/* USER CODE END PV */
...
int main(void) {
...
/* USER CODE BEGIN Init */
__disable_irq();
/* USER CODE END Init */
/* USER CODE BEGIN 2 */
imon_init(g_adc_samples);
HAL_TIM_Base_Start_IT(&htim6);
HAL_ADC_Start_DMA(&hadc, (uint32_t*) &g_adc_samples[0], 2);
HAL_TIM_Base_Start_IT(&htim3);
__enable_irq();
/* USER CODE END 2 */
...
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
uint32_t now_tick = HAL_GetTick();
imon_on_idle(now_tick);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Добавляем калибровку АЦП в ф-цию void
MX_ADC_Init(void)
. Без калибровку датчик температуры будет показывать неправильные значения.
/* USER CODE BEGIN ADC_Init 2 */
LL_ADC_StartCalibration(ADC1);
while (LL_ADC_IsCalibrationOnGoing(ADC1)) {
__asm__ __volatile__ ("nop;nop;nop;nop;" ::);
};
/* USER CODE END ADC_Init 2 */
Компилируем и проверяем что таймер и АЦП работает как надо — температура +/- 2С, напряжение питания соответствует действительному, показания в милливольтах.
Изменение модуля USB и main
1.