скрытое меню

Как создать свой usb девайс

Не будем долго ходить вокруг да около. Как сделать свое USB устройство, переопределив стандартно сгенерированный кубом код для Custom Human Interface Device. Работаем в Atolic True Studio на STM32F205VG.

Допустим мы хотим создать RNDIS адаптер .

Что такое RNDIS ? Это (под Windows) когда вы втыкаете USB устройство и у вас на компьютере появляется новый сетевой адаптер.
Зачем сетевой адаптер нужен? Затем что там далее могут быть http сервера например и еще очень много каких серверов (ресурсы сети).

Поскольку в Cube Mx нет реализации шаблона для RNDIS адаптера , а нам хочется , то мы сделаем его сами на основе стандартного HAL-ского шаблона кода (по состоянию на 2020г.). То есть сделаем нашу реализацию RNDIS сетевого адаптера, переделав USB Custom HID и заодно разберемся с идеалогией HAL USB .

В кубе выберем Custom HID , а потом уже будем править под наш RNDIS случай. Все дефайны , что мы будем изменять будем помечать префиксом MY_ для наглядности.

Другая часть кода будет взята из примера Сергея Фетисова с гитхаба (LRNDIS). LWIP 2.1.2 - только RAW вариант. И еще у нас все будет под FreeRTOS - вариант stastic.

фотка 1

Генерируем проект под Atollic True Studio ибо это реально бесплатная на сегодня среда разработки . Далее переходим в проект.

MX_USB_DEVICE_Init

Все начинается с функции MX_USB_DEVICE_Init . MX_USB_DEVICE_Init - это код стандартной инициализации любого USB устройства .

void MX_USB_DEVICE_Init(void)
{
  /* USER CODE BEGIN USB_DEVICE_Init_PreTreatment */
  
  /* USER CODE END USB_DEVICE_Init_PreTreatment */
  
  /* Init Device Library, add supported class and start the library. */
  if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
  {
    // 
    Error_Handler();
  }
  if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_HID) != USBD_OK)
  {
    
    Error_Handler();
  }
  if (USBD_CUSTOM_HID_RegisterInterface(&hUsbDeviceFS, &USBD_CustomHID_fops_FS) != USBD_OK)
  {
    Error_Handler();
  }
  if (USBD_Start(&hUsbDeviceFS) != USBD_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USB_DEVICE_Init_PostTreatment */
  
  /* USER CODE END USB_DEVICE_Init_PostTreatment */
}

Первая функция USBD_Init

Самая первая функция , вызываемая для инициализации USB это USBD_Init . Далее смотрим отладчиком в каком порядке вызываются функции инициализации. Мы работаем под FreeRTOS поэтому у нас StartDefaultTask идет первой функцией.

StartDefaultTask() [http_server.c]
  MX_USB_DEVICE_Init() [usb_device.c]
    USBD_Init() [usbd_core.c]
      USBD_LL_Init() [usbd_conf.c]
        HAL_PCD_Init() [stm32f2xx_hal_pcd.c]
          HAL_PCD_MspInit() [usbd_conf.c:] 
            // тут устанавливаются GPIO и тактирование 
            // ничего не меняем , стандартно как в кубе установили
          USB_CoreInit() [stm32f2xx_ll_usb.c]    
            // тут регистры USB , инициализируются стандартно как для всех USB устройств
            // ничего не меняем , стандартно как в кубе установили
          USB_DevInit() [stm32f2xx_ll_usb.c]
            // тут ТОЖЕ регистры USB , инициализируются стандартно как для всех USB устройств
            // ничего не меняем , стандартно как в кубе установили

Вся прелесть первой функции USBD_Init , что НАМ там тут ничего менять не надо.

Вторая функция USBD_RegisterClass

USBD_RegisterClass - вот тут уже начинается инициализация нашего устройства. Только на самом деле мы еще только передаем указатели на функции, которые будут вызваны позже (каждый новый раз при подключении устройства по USB):

USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_HID)

Ниже представлен список указателей на функции, порядок функций и передаваемые параметры установлены стандартным образом для всех USB устройств (обратите на это внимание). Это своеобразный стандарт в HAL Cube MX.

USBD_ClassTypeDef  USBD_CUSTOM_HID = 
{
		// указатели на функции
		USBD_CUSTOM_HID_Init, 		// 0
		// static uint8_t  USBD_CUSTOM_HID_Init (USBD_HandleTypeDef *pdev, 	uint8_t cfgidx)
		USBD_CUSTOM_HID_DeInit,		// 1
		USBD_CUSTOM_HID_Setup,		// 2
		// static uint8_t  USBD_CUSTOM_HID_Setup (USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
                // это управляющие команды (8 байт)
		NULL, /*EP0_TxSent*/		// 3
		USBD_CUSTOM_HID_EP0_RxReady, /*EP0_RxReady*/ /* STATUS STAGE IN */
                // вызывается когда кроме USBD_CUSTOM_HID_Setup еще идут данные на EP0
                // для настройки нашего устройства
		USBD_CUSTOM_HID_DataIn, /*DataIn*/
		USBD_CUSTOM_HID_DataOut,	//6
		NULL, /*SOF */				// 7
		NULL,						// 8
		NULL,						// 9
		USBD_CUSTOM_HID_GetCfgDesc,	// 10
		USBD_CUSTOM_HID_GetCfgDesc,	// 11
		USBD_CUSTOM_HID_GetCfgDesc,	// 12
		// static uint8_t  *USBD_CUSTOM_HID_GetCfgDesc (uint16_t *length)
		USBD_CUSTOM_HID_GetDeviceQualifierDesc, // 13
		// static uint8_t  *USBD_CUSTOM_HID_GetDeviceQualifierDesc (uint16_t *length)
               // тут будет отдаваться содержание дескриптора устройства
};

Третья функция USBD_CUSTOM_HID_RegisterInterface

Тут инициализируется содержание контента , которое мы потом будем отдавать хосту. Это просто байтовые массивы ( объедененные в структуре USBD_CustomHID_fops_FS ) со строгим типизированным контентом.

USBD_CUSTOM_HID_RegisterInterface(&hUsbDeviceFS, &USBD_CustomHID_fops_FS)

USBD_CUSTOM_HID_ItfTypeDef USBD_CustomHID_fops_FS =
{
  CUSTOM_HID_ReportDesc_FS, // главный тут определяется тип устройства и др.
  CUSTOM_HID_Init_FS,
  CUSTOM_HID_DeInit_FS,
  CUSTOM_HID_OutEvent_FS
};

Нам надо внимательно прописать CUSTOM_HID_ReportDesc_FS .

USBD_Start пока пропускаем.

Физически втыкаем USB в гнездо

И тут вся суть в отработке прерывания по USB каналу.
Получаем первый вызов из прерывания USB - это сработало физическое подключение устройства.
Данные хост еще реально не передает.

prvPortStartFirstTask() at port.c:270 0x80155ac	
  OTG_FS_IRQHandler() at stm32f2xx_it.c:306 0x80227a4	
      HAL_PCD_IRQHandler() at stm32f2xx_hal_pcd.c:364 0x8011e50	
        HAL_PCD_SetupStageCallback() at usbd_conf.c:144 0x80229c6	
          USBD_LL_SetupStage() at usbd_core.c:274 0x8014d32	
            USBD_StdDevReq() at usbd_ctlreq.c:135 0x8015258	
              USBD_SetConfig() at usbd_ctlreq.c:512 0x801512e	
                USBD_SetClassConfig() at usbd_core.c:234 0x8014cd4	
                  USBD_CUSTOM_HID_Init() at usbd_customhid.c:251 0x8014c38	
                    USBD_LL_OpenEP
                        // тут инициализируем наши конечные точки end points (у нас 2)
                        // надо прописать их адреса CUSTOM_HID_EPIN_ADDR, CUSTOM_HID_EPOUT_ADDR
                        // максимальный размер передаваемых данных в одном пакете для точки CUSTOM_HID_EPOUT_SIZE
                        // тип конечной точки , у нас USBD_EP_TYPE_BULK
                    USBD_malloc // внимание тут malloc
                        // обязательно заменяем malloc, 
                        // т.к. каждые раз при подключении по USB будет выделяться новая память из RAM
                   pdev->pUserData)->Init(); // не важно
                   USBD_LL_PrepareReceive // это значит , что мы ждем данные по USB

Ничего сложного.

Первое , что из данных получит наш девайс это последовательность байт 80 06 00 01 00 00 40 00 на end point 0. Смотрим на USBD_SetupReqTypedef usb_setup_req - это оно и есть. Нас интересуют первые три параметра :
80 = bmRequest
06 = bRequest =(USB_REQ_GET_DESCRIPTOR)
00 01 case: USB_DESC_TYPE_DEVICE = GetDeviceDescriptor(..)
....
00 40 wLength запрашиваемый размер ответа
Кстати все служебные пакеты будут по 8 байт (на ep0).

Примечание: на самом деле пакет передаваемых данных предворяет так называемый пакет токен (логи обмена будут приложены ниже) , выглядит он примерно так в логах :
SETUP,0x00,0x00,,,0x1B
тут важно понимать , что первый байт 0x00 это адрес устройства. Cначала он равен 0x00 , а после enumaration (перенумеровки) все устройств на шине USB через несколько пакетов обмена хост присваиваивает нашему устройсту уникальный адрес и далее наше устройство отвечает только запросы по этому адресу. Второй байт это номер точки (end point) на которую идет пакет и это важно . Забегая вперед все управляющие пакеты пойдут на точку ep0. (третий констрольная сумма).

Мы ему ответим содержанием USBD_FS_DeviceDesc (USB standard device descriptor) :
<--12 01 00 02 00 00 00 40 83 04 50 57 00 02 01 02 03 01
.
12 ...
01 = USB_DESC_TYPE_DEVICE
...
40 = USB_MAX_EP0_SIZE (=64) - это макс.размер пакета на служебный EP0.
VID
PID
...
01 = USBD_MAX_NUM_CONFIGURATION максимальное количество конфигураций

И это будет неправильный ответ , так как пятый байт должен быть 0xE0 /* bDeviceClass = Wireless Controller */. Должно быть так :
<-- 12 01 00 02 e0 00 00 40 44 4e 53 49 ff ff 01 02 03 01

__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
		0x12,                        		/* bLength = 18 bytes */
		USB_DESC_TYPE_DEVICE,         /* bDescriptorType = DEVICE */
		0x00, 0x02,                         /* bcdUSB          = 1.1 0x10,0x01  2.0 0x00,0x02 */
		0xE0,                               /* bDeviceClass    = Wireless Controller */
		0x00,                               /* bDeviceSubClass = Unused at this time */
		0x00,                               /* bDeviceProtocol = Unused at this time */
		USB_MAX_EP0_SIZE, //x40             /* bMaxPacketSize0 = EP0 buffer size */
		LOBYTE(USBD_VID), HIBYTE(USBD_VID), /* Vendor ID */
		LOBYTE(USBD_PID_FS), HIBYTE(USBD_PID_FS), /* Product ID */
		0xFF, 0xFF,                         /* bcdDevice */
		USBD_IDX_MFC_STR,
		USBD_IDX_PRODUCT_STR,
		USBD_IDX_SERIAL_STR,
		USBD_MAX_NUM_CONFIGURATION
};

Таким образом хост теперь знает какой максимальный размер управляющего пакета мы поддерживаем (USB_MAX_EP0_SIZE = 64 ) и количество наших конфигураций (1). А также какой тип у нашего устройтсва , и соответственно хост может закгрузить себе (на ПК в Windows) соответствующий драйвер для работы с нашим устройством.

Кстати , чтобы посмотреть что получает контроллер на служебный ep0 надо смотреть USBD_LL_SetupStage . Тут будет нюанс, что размер пришедших данных неизвестен, но не больше pdev->request.wLength (у нас 64) , uint8_t *psetup - это указатель на пришедшие байты.

Почему я это знаю - не потому ,что я хорошо понимаю код, просто у меня есть логический анализатор LA1010 (2000р.). Без анализатора наверное трудно разбираться почему что-то не работает.

(Примечание : на самом деле сколько байт пришло на EP в USBD_LL_SetupStage посмотреть можно , это будет описано ниже.)

Вот примерный стек функций, чтобы отдать содержание USBD_FS_DeviceDesc :

xPortStartScheduler() at port.c:350 0x80157ba	
// тут уже из планировщика начинается вызов
prvPortStartFirstTask() at port.c:270 0x80155ac	
  OTG_FS_IRQHandler() at stm32f2xx_it.c:306 0x80227a4	
   // все начинается как всегда с очередного прерывания
      HAL_PCD_IRQHandler() at stm32f2xx_hal_pcd.c:364 0x8011e50	
        HAL_PCD_SetupStageCallback() at usbd_conf.c:144 0x80229c6	
          USBD_LL_SetupStage() at usbd_core.c:274 0x8014d32	
            USBD_StdItfReq() at usbd_ctlreq.c:180 0x80152c0	
              USBD_GetDescriptor() at usbd_ctlreq.c:346 0x8015012	
                  USBD_FS_DeviceDescriptor() at usbd_desc.c:230 0x801b450	
 

Десятая позиция структуры указателей на функции USBD_ClassTypeDef (вспоминаем). Она вызывается до USBD_CUSTOM_HID_Setup.
USBD_CUSTOM_HID_GetCfgDesc подготавливает содержание дескриптора к ответу.

Такое примерно уже можно увидеть в программе USBLyzer , но она не очень помогает, так как на самом деле скрывает огромное количество передаваемых реально пакетов (байт) .

фотка 1

Следующий пакет , который посылает хост будет 00 05 1E 00 00 00 00 00 (00 = bmRequest , 05 = bRequest). В коде HAL это case : USB_REQ_SET_ADDRESS. На него отвечать не надо. Нашему устройству теперь установлен адрес и только на него мы будем откликаться. Смотрите USBD_SetAddress.

Далее мы получим опять 80 06 00 01 00 00 12 00 . Опять нас интерессуют парамеры 08,06,0001, остальное неважно. И мы опять отвечает содержанием GetDeviceDescriptor.

И вот теперь мы получим запрос 80 06 00 02 00 00 FF 00. Это case USB_DESC_TYPE_CONFIGURATION. Тут мы должны отдать уже другой дескриптор - дескриптор конфигурации (GetFSConfigDescriptor по шаблону указателей на функции, или как у нас это определено USBD_CUSTOM_HID_GetCfgDesc).

Вот его-то мы и должны полность перелопатить, так как у нас Custom HID , а нужен RNDIS. То есть это просто байтовый массив , который надо правильно заполнить. Если ошибится , то хост далее будет тупо постылать RESET контроллеру много раз. И мы естественно ошиблись первый раз и потеряли уйму времени , чтобы хотя бы где проблема.

Далее , если все хосту понравилось , он посылает 80 06 03 03 09 04 FF 00 (case USB_DESC_TYPE_STRING + case USBD_IDX_SERIAL_STR = GetSerialStrDescriptor).

Далее получаем 80 06 03 03 09 04 FF 00 (case USBD_IDX_LANGID_STR = GetLangIDStrDescriptor(..))
<-- : 1A 03 33 00 38 00 36 00 42 00 33 00 33 00 35 00 30 00 33 00 31 00 33 00 37 00

Далее получаем 80 06 00 03 00 00 FF 00 (case USBD_IDX_LANGID_STR = GetLangIDStrDescriptor(..))
<-- : 04 03 09 04

Далее получаем 80 06 02 03 09 04 FF 00 (case USBD_IDX_PRODUCT_STR= GetProductStrDescriptor(..))
<-- : 3A 03 53 00 54 00 4D 00 33 00 32 00 20 00 43 00 75 00 73 00 74 00 6F 00 6D 00 20 00 48 00 75 00 6D 00 61 00 6E 00 20 00 69 00 6E 00 74 00 65 00 72 00 66 00 61 00 63 00 65 00

80 06 00 06 00 00 0A 00 USB_DESC_TYPE_DEVICE_QUALIFIER , тут мы ничего не отвечает , так как это только для USBD_SPEED_HIGH.

80 06 00 01 00 00 12 00 опять GetDeviceDescriptor
<-- : 12 01 00 02 00 00 00 40 83 04 50 57 00 02 01 02 03 01

80 06 00 02 00 00 09 00 опять GetFSConfigDescriptor
<-- : 09 02 4B 00 02 01 00 40 01 вот тут обратите внимание , что запрошены всего 09 байт дексриптора конфигурации, то есть не полностью , такое практикуется в протоколе USB.

80 06 00 02 00 00 4B 00 опять GetFSConfigDescriptor, но теперь полностью 4b размер
<-- 09 02 4B 00 02 01 00 40 01 08 0B 00 02 E0 01 03 00 09 04 00 00 01 E0 01 03 00 05 24 00 10 01 05 24 01 00 01 04 24 02 00 05 24 06 00 01 07 05 81 03 08 00 01 09 04 01 00 02 0A 00 00 00 07 05 82 02 40 00 00 07 05 02 02 40 00 00 02 40 00 00 07 05 02 02 40 00 00

00 09 01 00 00 00 00 00 case CUSTOM_HID_REQ_SET_REPORT . Это USB_REQ_SET_CONFIGURATION , на него не отвечаем.

21 00 00 00 00 00 18 00 - что это ?

И вот тут пора посмотреть , что придет в USBD_CUSTOM_HID_EP0_RxReady (структура USBD_ClassTypeDef USBD_CUSTOM_HID) потому как сюда кое-что должно прилететь , это видно из лога анализатора.
<-- 02 00 00 00 18 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00 00 40 00 00 .

Но смотрим EP0_RxReady и ничего не приходит. Далее хост переводит нас в SUSPEND. Значит надо разбираться. Поскольку у нас есть рабочий проект от Сергея Фетисова с гитхаба с LRNDIS, то мы можем сравнить логи и это есть самый простой путь.

Выясняется , что для USB-HID реализации просто нет необходимости отрабатывать такой запрос 21 00 00 00 00 00 18 00. Это запрос на передачу для EP0 дополнительных данных. Мы можем реализовать эту ветку кода сами. Начинаем разбираться:

USB_REQ_RECIPIENT_INTERFACE

Для RNDIS адаптера просто надо добавить сначала отработку запросов USB_REQ_RECIPIENT_INTERFACE. У USB-HID этого нет и ему не надо.

Для нас это первый запрос непосредственно уже к RNDIS адаптеру от драйвера Windows по его протоколу. Смотрите USBD_CUSTOM_HID_Setup / кейс USB_REQ_TYPE_CLASS .

Тут для отработки этого запроса создаем свой новый case CUSTOM_RNDIS_INIT и отвечаем хосту как надо :

case CUSTOM_RNDIS_INIT:
	if( (req->bmRequest & 0x80) == 0)
		rndisPrepareRxEP0(pdev , req);
         // эту функцию реализуем самостоятельно 
         // в соответсвие с протоколом RNDIS
         // REMOTE_NDIS_INITIALIZE_MSG
	break;

Хост нам посылает данные для настройки RNDIS , мы получаем , делаем что-надо и отвечаем стандартным ответом 01 00 00 00 00 00 00 00, типа все ОК. Далее в двух словах идет несколько запросов REMOTE_NDIS_QUERY_MSG , мы не убудем углубляться, так как для нас надо понять общую логику и заставить устройство как-то уже шевелиться.

Далее получаем A1 01 00 00 00 00 00 10, это REMOTE_NDIS_SET_MSG и это уже задача устройству установить некоторые параметры переданные хостом ( второй байт = 01). Нам потребуется реализовать case CUSTOM_RNDIS_SETUP (это мы его так обозвали, вы можете по своему).

case CUSTOM_RNDIS_SETUP:
	host2rndis_Setup(pdev , req);
	break;

Для начинается долбежка хостом запросами REMOTE_NDIS_KEEPALIVE_MSG . Мы отвечаем 08 00 00 00 0c 00 00 00 0e 00 00 00 , байт 0e (тут счетчик , каждый раз следующий раз увеличивается на 1).

То , что хост постоянно долбит посылая 21 00 00 00 00 00 0C 00 плюс данные - неправильно . Просто мы ему не отвечаем . Надо реализовать прием данных от хоста на наш EP OUT для балк данных (MY_RNDIS_DATA_OUT_EP_ADDR).

Для этого надо просто в функции USBD_CUSTOM_HID_DataOut сделать обработку принятия данных.

На данном этапе Windows уже видит наш RNDIS адаптер , но только в состоянии выключен.

фотка 1

Разбираемся дальше. Картинки на самом деле никакой существенной помощи не окажут ,так как только байты гоняемые по USB имеет смысл.

pClassData, pData , pUserData

Настало время понять , что это за такие загадочные (void *) неопределенные указатели pClassData, pData , pUserData .

typedef struct _USBD_HandleTypeDef
{
  uint8_t                 id;
  uint32_t                dev_config;
  uint32_t                dev_default_config;
  uint32_t                dev_config_status; 
  USBD_SpeedTypeDef       dev_speed; 
  USBD_EndpointTypeDef    ep_in[15];
  USBD_EndpointTypeDef    ep_out[15];  
  uint32_t                ep0_state;  
  uint32_t                ep0_data_len;     
  uint8_t                 dev_state;
  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_SetupReqTypedef    request;
  USBD_DescriptorsTypeDef *pDesc;
  USBD_ClassTypeDef       *pClass;
  void                    *pClassData;  
  // почему тут указатели неопределенного типа void
  // дело в том , что это ваш реализуемый класс устройства
  // и никто не знает , что вы там изобретете
  void                    *pUserData;    
  void                    *pData;    
} USBD_HandleTypeDef;

pClassData

Указатели на функции обработчики , которые будут вывзваться в коде HAL, просто есть стандартный набор указателей на функции , которые надо реализовать. Делается это один раз из функции USBD_CUSTOM_HID_Init .

Pdev->pClassData = USBD_malloc(sizeof (USBD_CUSTOM_HID_HandleTypeDef));

Ищем где инициализируется pData :

Внимание : существуют два указателя pData в разных структурах (очень прикольно для понимания).

USBD_LL_Init

  hpcd_USB_OTG_FS.pData = pdev;
  // pdev это USBD_HandleTypeDef *
  // hpcd_USB_OTG_FS это структура PCD_HandleTypeDef (глобальная)

  pdev->pData = &hpcd_USB_OTG_FS;

  // в результате  pData в hpcd_USB_OTG_FS указывает на структуру USBD_HandleTypeDef 
  // а pData в USBD_HandleTypeDef указывает НАОБОРОТ на hpcd_USB_OTG_FS 

OTG_FS_IRQHandler

Все начинает становиться логичным , когда выходим на начало обработчика прерывания . Именно тут мы понимаем , что на обработку всех прерываний мы передаем как параметр указатель на глобально объявленную структруру hpcd_USB_OTG_FS. То есть кто-то до прерывания уже эту структуру заполняет (аппаратно по-видимому).

void OTG_FS_IRQHandler(void)
{
  HAL_PCD_IRQHandler(&hpcd_USB_OTG_FS);
 // вот ее глобальное создание PCD_HandleTypeDef hpcd_USB_OTG_FS;
}

Смотрим структуру PCD_HandleTypeDef :

typedef struct
{
  // по видимому тут все аппаратные адреса 
  PCD_TypeDef             *Instance;    /*!< Register base address              */
  PCD_InitTypeDef         Init;         /*!< PCD required parameters            */
  PCD_EPTypeDef           IN_ep[16U];   /*!< IN endpoint parameters             */
  PCD_EPTypeDef           OUT_ep[16U];  /*!< OUT endpoint parameters            */
  HAL_LockTypeDef         Lock;         /*!< PCD peripheral status              */
  __IO PCD_StateTypeDef   State;        /*!< PCD communication state            */
  uint32_t                Setup[12];    /*!< Setup packet buffer                */
  void                    *pData;       /*!< Pointer to upper stack Handler     */
  // pData будет указывать на наш  USBD_HandleTypeDef *pdev 
} PCD_HandleTypeDef;

То есть получается , что все что происходит на линии USB, мы смело смотрим здесь в PCD_HandleTypeDef из прерывания.

pUserData инициализируется указателем на некую структуру USBD_CUSTOM_HID_ItfTypeDef с указателями на разные типы данных, функций и т.д. Там по сути пустышки , которые нам предлагается заполнить кодом инициализации своего устройства и др.. Мы ей не будем пользоваться .

USBD_CUSTOM_HID_RegisterInterface
  pdev->pUserData= fops;
  // fops это USBD_CUSTOM_HID_ItfTypeDef *

DataOut

И вот только теперь можно перейти к реализации функции DataOut для нашего устройства. Имеем вот такой кусок лога , в котором первый раз будет вызвана функция чтения EP3 (не EP0):

<-- A1 01 00 00 00 00 00 10 это хост шлет SETUP пакет

--> 04 00 00 80 1C 00 00 00 0F 00 00 00 00 00 00 00 04 00 00 00 10 00 00 00 EA 05 00 00 мы отвечаем на EP0

И далее по протоколу RNDIS нам приходят первые данные на EP3 , причем довольно большое количество, разбитое на несколько пакетов:

<-- : 01 00 00 00 86 00 00 00 24 00 00 00 5A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 33 33 00 00 00 16 20 89 84 6A 96 AA 86 DD 60 00 00 00 00 24

<-- : 01 00 00 00 86 00 00 00 24 00 00 00 5A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 33 33 00 00 00 16 20 89 84 6A 96 AA 86 DD 60 00 00 00 00 24

<-- : 00 01 FE 80 00 00 00 00 00 00 25 7E B4 13 14 2E E9 FD FF 02 00 00 00 00 00 00 00 00 00 00 00 00 00 16 3A 00 05 02 00 00 01 00 8F 00 98 41 00 00 00 01 04 00 00 00 FF 02 00 00 00 00 00 00 00 00

<-- : 00 00 00 00 00 0C

И далее продолжают сыпаться данные на EP3. Но мы еще не умеем отвечать как надо и наше устройство поэтому показано в Windows в состоянии инициализация... :

фотка 1

Это означает , что RNDIS драйвер Windows уже шлет пакеты в наше устройство, но мы еще просто не отвечаем. То есть надо переходить к обработке ehternet пакетов или по другому надо реализовать TCP стек. Это к USB прямого отношения не имеет.

Мостом , который у нас будет связывать USB и Ethernet канал будут просто две функции . Для передачи принятого из USB канала пакета в Ethernet stack пишем функцию например rndis2ethernetStack и закидываем туда пакет (лучше через очередь FreeRTOS).
Для ответа в USB rndis канал после отработки Ethernet стеком используем функцию например answerFromEthernetStack .

Итак ethernet пакеты мы стали получать нормально с уровня USB на EP3 (это наш bulk OUT). И картинка изменилась , теперь RNDIS адаптер имеет присвоенный Windows ip адрес.

И есть такой затык : получаем команду на EP3 (bulk OUT) и после этого хост должен перейти к приему по EP bulk IN. Но хост продолжает упорно посылать данные на EP3 (bulk OUT). Почему? Есть предполпожение ,что хосту надо как-то ответить NAK, чтобы он понял ,что мы заняты чем-то и работаем с поледней принятой командой. Разбираемся с этой темой.

Кто решает , что делать на шине ? Если только однозначно хост решает , почему он не переходит на прием IN? Значит хосту что-то не нравится в ответе устройства. А может хост и не должен переходить на прием IN?. На самом деле сразу после открытия EP IN хост начинает перидически посылать запросы на получение данных, то есть хост всегда готов принять данные IN c открытой конечной точки EP. Это видно только на логическом анализаторе (типа LA1010).

Как послать NAK

Очень не простая тема оказалась. Мы принимаем команду от ПК в виде нескольких пакетов. После какого пакета мы должны выставить на EP OUT сигнал NAK. И когда он срабоает ? На следующем только пакете ?

Процесс усложняется еще тем , что коллбек срабатывающий на получение данных EP OUT ( у нас usbd_rndis_data_out) срабатывает не на каждый пакет в отдельности , а может сработать на несколько пакетов (идущих по-видимому быстро подряд) .

Видно это например так : ждем приема 0x40 байт , а приходят х240 и только после этого происходит usbd_rndis_data_out (кстати команда в этом случае обычно передается полностью).

Трассировка прерываний

Разобраться в этой каше пакетов можно только трассировкой прерываний , то есть спокойно отслеживаем все прерывания в обработчике всех прерываний по ( у нас USB HAL_PCD_IRQHandler) и также не торопясь спокойно анализируем. Главное терпение...

Тут в обработчике прерываний HAL_PCD_IRQHandler для приема (именно данных) есть две ветки обработки: первая срабатывает по USB_OTG_GINTSTS_OEPINT (тут проверяется USB_OTG_DOEPINT_XFRC = Transfer completed interrupt) и вторая смотрите ниже срабатывает по USB_OTG_GINTSTS_RXFLVL (Handle RxQLevel Interrupt). Так вот именно во второй ветке мы и получаем непрерывные данные большого неожидаемого размера (то есть размер гораздо больше, чем мы запрашивали последний раз командой USBD_LL_PrepareReceive).

В принципе здесь МОЖНО организовать управление установкой NAK в нужный момент.

Еще интерсное открытие , что RXFLVL всегда сработает первым , а потом только OEPINT.

Момент истины

И вот выясняется интересное открытие - NAK для EP3 OUT устанавливается котроллером USB в микроконтрллере STM32 при срабатывании самого первого прерывания RXFLVL . То есть NAK сам установится в 1.

Далее будет несколько OEPINT и NAK будет меняться так : получаем OEPINT , вызывется коллбек usbd_rndis_data_out , потом мы обязаны запустить опять прием следующих данных (USB_EPStartXfer) и тут в конце NAK очищается.

Потом при следующем приходе RXFLVL NAK опять устанавливается в 1 и т.д. Теперь становится понятно ,что в наших действиях есть только один правильный рычаг : вызывать прием новых данных (USB_EPStartXfer) , но с задержкой после выполнения необходимых действий . Пока мы не вызвали USB_EPStartXfer хосту будет отгружаться NAK (по EP3).

host2rndis_data_out
  USBD_LL_PrepareReceive  // вот тут надо отложить этот вызов !
  //до завершения обработки полученных данных 
    HAL_PCD_EP_Receive 
      USB_EPStartXfer

Выводы по NAK

Смысл оказался таким : как только запускаешь прием команды по EP OUT (BULK) , контроллер USB (не главный STM32) сам выставляет NAK( 1) по прилету первого прерывания RXFLVL (не OEPINT).

Потом его (NAK) надо не забывать сбрасывать, что HAL и делает (в USB_EPStartXfer [по приему]). Если не сбрасывать получается нормально задержка , в результате которой можно спокойно обработать команду и все будет гут. в это время хосту будет отдаваться NAK по EP OUT.

То есть надо просто отложить вызов USBD_LL_PrepareReceive на нужное время и все. Что мне не нравится - чтобы понять эту простую казалось бы вещь надо все-таки въезжать в регистры. А это не один день мягко говоря... (но зато на всю жизнь - такое не забывается)