STM32 USB Device Interface

Device Class Initialization (WORK IN PROGRESS)

Setting endpoint FIFO buffers

If we wish to use certain endpoints for transfer, we first must assign FIFO buffer sizes. Otherwise, the transfer wont work. Example code:

  HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 0x80);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 0x40);
  HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 3, 0x80);

Device class specification struct

Device class initialization is done within USBD_ClassTypeDef structure.

typedef struct _Device_cb
{
  uint8_t (*Init)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  uint8_t (*DeInit)(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx);
  /* Control Endpoints*/
  uint8_t (*Setup)(struct _USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef  *req);
  uint8_t (*EP0_TxSent)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*EP0_RxReady)(struct _USBD_HandleTypeDef *pdev);
  /* Class Specific Endpoints*/
  uint8_t (*DataIn)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*DataOut)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*SOF)(struct _USBD_HandleTypeDef *pdev);
  uint8_t (*IsoINIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);
  uint8_t (*IsoOUTIncomplete)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum);

  uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
  uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
  uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev, uint8_t index,  uint16_t *length);
#endif

} USBD_ClassTypeDef;

STM’s USB middleware usually takes care of the basic USB enumeration, and calls into the function pointers stored here once the device class needs to be initialized or when the actual transfer (business logic) is kicked-in.

Initialization

Initialization of device class is done by function of the following signature:


uint8_t Init(struct _USBD_HandleTypeDef *pdev, uint8_t cfgidx) {

  // We need to "open" all endpoints that are provided by this device class. 
  // Obviously endpoint 0 is already defined, so we can skip it and just implement
  // class/device specific endpoints
  USBD_LL_OpenEP(pdev, ep_addr_1_in.value, EP_TYPE_BULK, EP_MPS_32);
  USBD_LL_OpenEP(pdev, ep_addr_1_out.value, EP_TYPE_BULK, EP_MPS_32);

  // Middleware requires us to set the is_used to true (1U)
  pdev->ep_in[ep_addr_1_in.address()].is_used  = 1U;
  pdev->ep_in[ep_addr_1_out.address()].is_used = 1U;

  // After endpoints have been opened, we need to register receive buffers for 
  // endpoints that are going to receive data (OUT)


  USBD_LL_PrepareReceive(pdev, rx_buf_ptr, EP_MPS_32);
}

In short, this function needs to open endpoints (non-zero),set is_used flag to true and for OUT endpoints (receiving data from host), set buffer that should be filled when data is sent by the host.

Reading Data (writing by host using OUT transaction)

Once host sends data using OUT transaction to device, the MCU will write data to buffer provided via USBD_LL_PrepareReceive(…), and will call our endpoint registered with signature (uint8_t (*DataOut)(struct _USBD_HandleTypeDef *pdev, uint8_t epnum)). Then once we handle the data, and wish to receive other piece of data, we must again call USBD_LL_PrepareReceive(…) using same buffer, or different buffer if we wish to receive the data but postpone processing of current buffer.

uint8_t buf[ENDP_MAX_BYTES];
uint8_t DataOut(struct _USBD_HandleTypeDef *pdev, uint8_t epnum) {
  (void) buf; // Do something with buffer
  USBD_LL_PrepareReceive(pdev, EP_ADDR, buf, ENDP_MAX_BYTES)
}

Writing data (reading by host using IN transaction)

USBD_LL_Transmit(pdev, EP_IN_ADDR, buf, buf_len)

Reading data sent by USB Host

Callback into our application describe as code call tree

// OTG_FS_IRQHandler() at stm32l4xx_it.c:209 0x80008c6
void OTG_FS_IRQHandler(void) {
    // HAL_PCD_IRQHandler() at stm32l4xx_hal_pcd.c:1,150 0x8001448
    auto HAL_PCD_IRQHandler =[](PCD_HandleTypeDef *hpcd) {
        uint32_t epnum = 0;
        // PCD_EP_OutXfrComplete_int() at stm32l4xx_hal_pcd.c:2,364 0x8002200
        auto PCD_EP_OutXfrComplete_int = [](PCD_HandleTypeDef *hpcd, uint32_t epnum){
            // HAL_PCD_DataOutStageCallback() at usbd_conf.c:205 0x80078de
            auto HAL_PCD_DataOutStageCallback = [](PCD_HandleTypeDef *hpcd, uint8_t epnum) {
                // USBD_LL_DataOutStage() at usbd_core.c:379 0x80062d4
                auto USBD_LL_DataOutStage = [](USBD_HandleTypeDef *pdev, uint8_t epnum, uint8_t *pdata) {
                    // USBD_CDC_DataOut() at usbd_cdc.c:746 0x8005e66
                    auto (USBD_StatusTypeDef)pdev->pClass->DataOut = USBD_CDC_DataOut = [](USBD_HandleTypeDef *pdev, uint8_t epnum) {
                        void * buf = nullptr;
                        uint32_t len = 0;
                        auto CDC_Receive_FS = ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->Receive(hcdc->RxBuffer, &hcdc->RxLength);
                        CDC_Receive_FS(buf, len);
                    };
                };
            };
        };
    };
}

Calls exported as call stack:

Thread #1 [main] 1 [core: 0] (Suspended : Breakpoint)	  
  CDC_Receive_FS() at usbd_cdc_if.c:265	
  USBD_CDC_DataOut() at usbd_cdc.c:746	
  USBD_LL_DataOutStage() at usbd_core.c:379	
  HAL_PCD_DataOutStageCallback() at usbd_conf.c:205	
  PCD_EP_OutXfrComplete_int() at stm32l4xx_hal_pcd.c:2,364	
  HAL_PCD_IRQHandler() at stm32l4xx_hal_pcd.c:1,150	
  OTG_FS_IRQHandler() at stm32l4xx_it.c:209	
  <signal handler called>()	
  main() at main.c:98 	

Writing data (reading by USB Host)

void CDC_Transmit_FS(uint8_t* Buf, uint16_t len) {
    static USBD_HandleTypeDef *pdev;
    auto USBD_CDC_SetTxBuffer = [](USBD_HandleTypeDef *pdev, uint8_t *pbuff, uint32_t length) {
        pdev->TxBuffer = pbuff;
        pdev->TxLength = length;
    };
    USBD_CDC_SetTxBuffer(pdev, Buf, len);

    auto USBD_CDC_TransmitPacket = (USBD_HandleTypeDef *pev) {
        USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)pdev->pClassData;
        pdev->ep_in[CDC_IN_EP & 0xFU].total_length = hcdc->TxLength;
        (void)USBD_LL_Transmit(pdev, CDC_IN_EP, hcdc->TxBuffer, hcdc->TxLength);
        return USBD_OK;
    };
}

Getting confirmation that data is successfully sent, and sending remaining data (since we are sending in max packet size of endpoint) . Do not forget to sent ZLP if data sent size is multiple of max package size.

void OTG_FS_IRQHandler(void) {
    auto HAL_PCD_IRQHandler = [](PCD_HandleTypeDef *hpcd) {
        auto HAL_PCD_DataInStageCallback = [](PCD_HandleTypeDef *hpcd, uint8_t epnum) {
            auto USBD_LL_DataInStage = [](USBD_HandleTypeDef *pdev, uint8_t epnum, uint8_t *pdata){
                pdev->pClass->DataIn(pdev, epnum) = USBD_CDC_DataIn = [] {
                    if ((pdev->ep_in[epnum].total_length > 0U) &&
                        ((pdev->ep_in[epnum].total_length % hpcd->IN_ep[epnum].maxpacket) == 0U))
                    {
                        /* Update the packet total length */
                        pdev->ep_in[epnum].total_length = 0U;

                        /* Send ZLP */
                        (void)USBD_LL_Transmit(pdev, epnum, NULL, 0U);
                    }
                    else
                    {
                        hcdc->TxState = 0U;

                        if (((USBD_CDC_ItfTypeDef *)pdev->pUserData)->TransmitCplt != NULL)
                        {
                            ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->TransmitCplt(hcdc->TxBuffer, &hcdc->TxLength, epnum);
                        }
                    }
                };
            };
        };
    };
}

Send callback as stack trace

Thread #1 [main] 1 [core: 0] (Suspended : Breakpoint)	
	USBD_CDC_DataIn() at usbd_cdc.c:695	
	USBD_LL_DataInStage() at usbd_core.c:470	
	HAL_PCD_DataInStageCallback() at usbd_conf.c:220	9
	HAL_PCD_IRQHandler() at stm32l4xx_hal_pcd.c:1,22	
	OTG_FS_IRQHandler() at stm32l4xx_it.c:20	
	<signal handler called>()	
	main() at main.c:98	

Handling custom Control Transfers

To handle control transfer, when a SETUP packet is received from USB Host, a callback will be called from interrupt handler with intention to perform further logic which depends on data direction.

void OTG_FS_IRQHandler(void) {
    auto HAL_PCD_IRQHandler = [](PCD_HandleTypeDef *hpcd) {
        auto PCD_EP_OutSetupPacket_int =  [](PCD_HandleTypeDef *hpcd, uint32_t epnum) {
            auto HAL_PCD_SetupStageCallback = [](PCD_HandleTypeDef *hpcd) {
                auto PCD_HandleTypeDef = [](USBD_HandleTypeDef *pdev, uint8_t *psetup) {
                    auto USBD_StdItfReq = [](USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) {
                        auto t = [](USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) {
                            // If incoming package (OUT)
                            USBD_CtlPrepareRx(pdev, (uint8_t *)hcdc->data, req->wLength);
                            // If outgoing package (IN)
                            (void)USBD_CtlSendData(pdev, (uint8_t *)hcdc->data, len);
                        }
                    };

                };
            };
        };
    };
}

Here we can see that depending on USBD_SetupReqTypedef, we either use USBD_CtlPrepareRx or USBD_CtlSendData call.

If USB Host is writing data (OUT), then STM Middleware will call our EP0_RxReady Class function

void OTG_FS_IRQHandler(void) {
    auto HAL_PCD_IRQHandler = [](PCD_HandleTypeDef *hpcd) {
        auto PCD_EP_OutXfrComplete_int =  [](PCD_HandleTypeDef *hpcd, uint32_t epnum) {
            auto USBD_LL_DataOutStage = [](USBD_HandleTypeDef *pdev, uint8_t epnum, uint8_t *pdata) {
                pdev->pClass->EP0_RxReady(pdev) = USBD_CDC_EP0_RxReady = [](USBD_HandleTypeDef *pdev){
                    auto CDC_Control_FS = [](uint8_t cmd, uint8_t* pbuf, uint16_t length) {
                    };
                };
            };
        };
    };
}

If USB Host is reading data (IN), then STM Middleware wont call into our function, since there is no callback available for that purpose.

Correlating code with logic analyzer traces

Reading Setup Package Data from USB Host

Here EP0_RxReady handler will get data transmitted in DATA1 block. But to get there, USBD_CDC_Setup had to call USBD_CtlPrepareRx function with buffer to fill the data.

Notes

  • Always send or receive data in multiples of negotiated packet sizes. Also, if reminder is 0, then always send or receive a ZLP as dictated by USB standard.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *