March 3, 2025

Black Pill boards - F411 USB -- initialization - USBD_CDC_cb

As a reminder, our starting point is the first call we make:
USBD_Init ( &USB_OTG_dev,
            USB_OTG_FS_CORE_ID,
            &USR_desc,
            &USBD_CDC_cb,
            &USR_cb);
Now we want to examine in detail the second to last argument, USBD_CDC_cb. We know how the game is played from the last page where we looked at USR_cb.

Using ctags takes us to library/usbd_cdc_core.c where we see this: /* CDC interface class callbacks structure */ USBD_Class_cb_TypeDef USBD_CDC_cb = { usbd_cdc_Init, usbd_cdc_DeInit, usbd_cdc_Setup, NULL, /* EP0_TxSent, */ usbd_cdc_EP0_RxReady, /* EP0_RxReady, */ usbd_cdc_DataIn, usbd_cdc_DataOut, usbd_cdc_SOF, NULL, /* IsoINIncomplete */ NULL, /* IsoOUTIncomplete */ USBD_cdc_GetCfgDesc, }; The first thing to take note of is that we are now dealing with routines in library/usbd_cdc_core.c rather than in VCP code as we were when looking at USB_cb. So we have here a more "generic" layer of code. VCP as a specific sort of CDC device, and here we deal general code for the "communication device class". All of the functions listed in the above structure initialization are in the file usbd_cdc_core.c.

The structure itself (along with the corresponding typedef) is defined in driver/usb_core.h -- this is interesting in itself because the definition is in the "driver" directory rather than the "library" directory.

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

  uint8_t  *(*GetConfigDescriptor)( uint8_t speed , uint16_t *length);

#ifdef USB_SUPPORT_USER_STRING_DESC
  uint8_t  *(*GetUsrStrDescriptor)( uint8_t speed ,uint8_t index,  uint16_t *length);
#endif
} USBD_Class_cb_TypeDef;
The Init routine saves a pointer to this structure in pdev as follows:
pdev->dev.class_cb = class_cb;
So we can expect these callbacks to be referenced in the following fashion:
pdev->dev.class_cb->Init(pdev, cfgidx);
When I do a search for "class_cb", I get the following:
./library/usbd_req.c:      pdev->dev.class_cb->Setup (pdev, req); 
./library/usbd_req.c:      pdev->dev.class_cb->Setup (pdev, req);   
./library/usbd_req.c:          pdev->dev.class_cb->Setup (pdev, req);
./library/usbd_req.c:      pbuf   = (uint8_t *)pdev->dev.class_cb->GetConfigDescriptor(pdev->cfg.speed, &len);
./library/usbd_req.c:      pbuf   = (uint8_t *)pdev->dev.class_cb->GetOtherConfigDescriptor(pdev->cfg.speed, &len);
./library/usbd_req.c:      pbuf = pdev->dev.class_cb->GetUsrStrDescriptor(pdev->cfg.speed, (req->wValue) , &len);
./library/usbd_req.c:      pbuf   = (uint8_t *)pdev->dev.class_cb->GetConfigDescriptor(pdev->cfg.speed, &len);
./library/usbd_req.c:      pbuf   = (uint8_t *)pdev->dev.class_cb->GetOtherConfigDescriptor(pdev->cfg.speed, &len);
./library/usbd_req.c:    pdev->dev.class_cb->Setup (pdev, req);   
./library/usbd_req.c:      pdev->dev.class_cb->Setup (pdev, req);   

./library/usbd_core.c:        if((pdev->dev.class_cb->EP0_RxReady != NULL)&&
./library/usbd_core.c:          pdev->dev.class_cb->EP0_RxReady(pdev); 
./library/usbd_core.c:  else if((pdev->dev.class_cb->DataOut != NULL)&&
./library/usbd_core.c:    pdev->dev.class_cb->DataOut(pdev, epnum); 
./library/usbd_core.c:          if((pdev->dev.class_cb->EP0_TxSent != NULL)&&
./library/usbd_core.c:            pdev->dev.class_cb->EP0_TxSent(pdev); 
./library/usbd_core.c:  else if((pdev->dev.class_cb->DataIn != NULL)&& 
./library/usbd_core.c:    pdev->dev.class_cb->DataIn(pdev, epnum); 
./library/usbd_core.c:  if(pdev->dev.class_cb->SOF)
./library/usbd_core.c:    pdev->dev.class_cb->SOF(pdev); 
./library/usbd_core.c:  pdev->dev.class_cb->Init(pdev, cfgidx); 
./library/usbd_core.c:  pdev->dev.class_cb->DeInit(pdev, cfgidx);   
./library/usbd_core.c:  pdev->dev.class_cb->IsoINIncomplete(pdev);   
./library/usbd_core.c:  pdev->dev.class_cb->IsoOUTIncomplete(pdev);   
./library/usbd_core.c:  pdev->dev.class_cb->DeInit(pdev, 0);
So, these callbacks are "called" from only two files -- both in the "library" directory, as per the above.

Callbacks used in usbd_req.c

A look at usbd_req.c shows that it has only 3 externally visible entry points:

USBD_StdDevReq() --- Handle standard usb device requests
USBD_StdItfReq() --- Handle standard usb interface requests
USBD_StdEPReq()  --- Handle standard usb endpoint requests
Hence the "req" in the name -- the routines in this file handles various requests. But who makes these requests? It ends up being surprisingly simple. There is one call to each from library/usbd_core.c
./library/usbd_core.c:    USBD_StdDevReq (pdev, &req);
./library/usbd_core.c:    USBD_StdItfReq(pdev, &req);
./library/usbd_core.c:    USBD_StdEPReq(pdev, &req);
Looking at usbd_core.c, these calls are all from one routine:
USBD_SetupStage() --- Handle the setup stage
So they are all being called during SETUP in response to requests in the setup packets. This routine -- USBD_SetupStage -- is itself one of the callbacks we are investigating! This takes us to an examination of the callback references in usbd_core.c

NULL callbacks used in usbd_core.c

First, let's look at the 3 callbacks that are marked as "NULL". These are guarded in the code by tests of this sort:
	if((pdev->dev.class_cb->EP0_TxSent != NULL)&&
             (pdev->dev.device_status == USB_OTG_CONFIGURED))
			  {
				pdev->dev.class_cb->EP0_TxSent(pdev);
			  }
So if the callback pointer is NULL, the call will not be made. This is done for all of the calls except Init and DeInit.

A look at the SOF callback

The code in library/usbd_core.c (namely USBD_SOF()) handles this. This relays the call to the routine usbd_cdc_SOF() in library/usbd_cdc_core.c via the following:
pdev->dev.class_cb->SOF(pdev);
Who calls USBD_SOF() ??
The call is in driver/usb_dcd_int.c in the routine DCD_HandleSof_ISR() and looks like this:
USBD_DCD_INT_fops->SOF(pdev);
This is called by USBD_OTG_ISR_Handler()

A quick note on interrupts

This deserves its own section at some point, but here is a quick orientation.

I wrote code in usbF4/usb.c that looks like this:

void
usb_irq_handler ( void )
{
    USBD_OTG_ISR_Handler (&USB_OTG_dev);
}
This is setup as an interrupt vector for IRQ 67 in locore_411.s as follows:
.word   usb_irq_handler     /* IRQ 67 */
To actually enable interrupts, we do this in usbF4/usb.c:
#define IRQ_USB_WAKEUP  42
#define IRQ_USB_FS      67

        nvic_enable ( IRQ_USB_WAKEUP );
        nvic_enable ( IRQ_USB_FS );
I have never yet seen a wakeup interrupt. It is the interrupts on IRQ 67 that drive the USB system. The "nvic" is the Nested Vectored Interrupt Controller. It is apparently identical on both the F103 and F411 (and perhaps other F4xx chips).

Bigger ARM chips use the "gic", which is a fancier interrupt controller that allows for multiple processors, but talking about that gets us off topic.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org