STM32 Modular USB Composite device

f6cb21f4c086b6de685292bc2025740b.png

Проект является логическим продолжением другого проекта на Хабре — 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 интерфейсов, где каждый интерфейс, с точки зрения пользователя на хост системе, является отдельным устройством.

Определены следующие устройства:

  1. CDC UART

  2. CDC ICTRL

  3. 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 при подключенном устройстве.Состояние менеджера устройств Windows при подключенном устройстве.

Для проверки USB Serial  конвертера (он же CDC UART) необходимо подключить внешнее устройство.

Для проверки CDC ICTRL ничего подключать не надо. Достаточно открыть терминал на порту COM8. Например PuTTY.

Пример вывода дебаговой печати ICTRL (температура и рабочее напряжение).Пример вывода дебаговой печати 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 для отправки коротких команд на дебаг интерфейс.Пример использования RealTerm для отправки коротких команд на дебаг интерфейс.

Для проверки USB HID устройства самое простое что нашлось это небольшая тулза hidapi (https://github.com/libusb/hidapi.git).

Пример использования HIDAPI Test Tool для диагностики USB HID интерфейса.Пример использования 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.

Создание автосгенерированного проекта

  1. Создаем сгенерированный проект визардом 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)

fcb7324e2e438fa825806742dbbf6cf3.png
  1. Добавляем код модуля «imon» (imon.c, imon.h)
    Помимо файлов самого imon, ещё необходимо добавить файл stm32l0xx_ll_adc.h в директорию  CompositeUSB_L072K\Drivers\STM32L0xx_HAL_Driver\Inc.
    Файл можно взять из SDK соответствующей архитектуры. Например отсюда.
    Файл необходим для описания некоторых констант АЦП (TEMPSENSOR_CAL1_ADDR, и т.п.)

  2. Добавляем инициализацию и запуск переферии, глобальный масив для данных АЦП, а также инициализацию и обработчик модуля imon.

Пример изменений в main ()
/* 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 */
  1. Добавляем калибровку АЦП в ф-цию 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 */
  1. Компилируем  и проверяем что таймер и АЦП работает как надо — температура +/- 2С, напряжение питания соответствует действительному, показания в милливольтах.

Изменение модуля USB и main

1.   

© Habrahabr.ru