- Device Class Initialization (WORK IN PROGRESS)
- Setting endpoint FIFO buffers
- Device class specification struct
- Initialization
- Reading Data (writing by host using OUT transaction)
- Writing data (reading by host using IN transaction)
- Reading data sent by USB Host
- Writing data (reading by USB Host)
- Correlating code with logic analyzer traces
- Notes
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

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.