March 3, 2025

Black Pill boards - F411 USB -- initialization - USR_cb

The first call we make (from my usb_init() routine) is:
USBD_Init ( &USB_OTG_dev,
            USB_OTG_FS_CORE_ID,
            &USR_desc,
            &USBD_CDC_cb,
            &USR_cb);
That last argument -- USR_cb -- contains several function pointers (callbacks) that get registered and used by the code. A scheme like this allows flexibility as it is possible to specify different callbacks at different times and for reasons that hopefully will be made clear as an outcome of this study.

We can use ctags to discover that the structure itself is initialized in the routine vcp/usbd_usr.c. That this takes place in the VCP code makes sense, as different callbacks might be used for some other USB class.

USBD_Usr_cb_TypeDef USR_cb =
{
  USBD_USR_Init,
  USBD_USR_DeviceReset,
  USBD_USR_DeviceConfigured,
  USBD_USR_DeviceSuspended,
  USBD_USR_DeviceResumed,

  USBD_USR_DeviceConnected,
  USBD_USR_DeviceDisconnected,
};
The structure itself is defined in driver/usb_core.h as follows.
typedef struct _USBD_USR_PROP
{
  void (*Init)(void);
  void (*DeviceReset)(uint8_t speed);
  void (*DeviceConfigured)(void);
  void (*DeviceSuspended)(void);
  void (*DeviceResumed)(void);

  void (*DeviceConnected)(void);
  void (*DeviceDisconnected)(void);

}
USBD_Usr_cb_TypeDef;
When the routine USBD_Init gets called, it places a pointer to the callback structure in the "dev" structure. This way we can use the dev structure to find anything we want at any time. This happens like so:
pdev->dev.usr_cb = usr_cb;
Knowing this, we can use grep to find every line that uses "usr_cb" and the results are as follows:
./library/usbd_core.c:  pdev->dev.usr_cb = usr_cb;
./library/usbd_core.c:  pdev->dev.usr_cb->Init();
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceReset(pdev->cfg.speed);
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceResumed();
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceSuspended();
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceConfigured();
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceConnected();
./library/usbd_core.c:  pdev->dev.usr_cb->DeviceDisconnected();
So every call is from usbd_core.c -- the same file that contains our USBD_Init() routine. This is nice, as we don't need to chase around through different files and directories to figure out what is going on. The above list gives us all the places where the callback routines get called! The first call (to Init()) takes place in USBD_Init() itself, and results in a call to USBD_USR_Init().

The routine USBD_USR_Init() is in vcp/usbd_usr.c, as are all of the callback routines. So we can see the big idea. The VCP module is telling the generic USB library about a list of routines within itself that should be called when certain things take place. We now could, for each of these, study both when and why it gets called, along with what it does. The routine names pretty much tell us, and this study would boil down to understanding what these words mean in the USB scheme of things.

It turns out that the actions are quite simple. Various bits in a variable "usbd_status" get set or cleared. This variable is static (so not visible outside of the file vcp/usbd_usr.c -- this is nice clean encapsulated design.
Access to this variable is done using one of 3 routines:

uint8_t usb_isConfigured(void) { return (usbd_status&USB_CONFIGURED); }
uint8_t usb_isConnected(void) { return USB_CONNECTED; }
uint8_t usb_getStatus(void) { return usbd_status; }
The last of the 3 is never used. The other two are used in the following way in usb.h --
extern uint8_t usb_isConnected(void);
extern uint8_t usb_isConfigured(void);
inline uint8_t usbIsConnected(void) { return usb_isConnected(); }
inline uint8_t usbIsConfigured(void) { return usb_isConfigured(); }
As near as I can tell, none of these are ever used. All of this for nothing! Maybe so in this case, but of course I may have missed something. But now I understand the "big idea" in this arrangement.

All this jumping around and searching through files makes me wish for some tool to aid such things. There is a tool called "cscope" that I should perhaps investigate. I find ctags a big help, and using it is part of my habits now.

Take a look at DeviceReset

We have looked at what happens when these callbacks get called, but what about the other side of the story -- how and why they get called. We could equally well look at any of the others. They each get called when various interesting things happen on our USB connection.
But let's pick one and look at DeviceReset.

This callback gets called in usbd_core.c from the following routine

static uint8_t USBD_Reset(USB_OTG_CORE_HANDLE  *pdev)
{
  /* Open EP0 OUT */
  DCD_EP_Open(pdev,
              0x00,
              USB_OTG_MAX_EP0_SIZE,
              EP_TYPE_CTRL);

  /* Open EP0 IN */
  DCD_EP_Open(pdev,
              0x80,
              USB_OTG_MAX_EP0_SIZE,
              EP_TYPE_CTRL);

  /* Upon Reset call usr call back */
  pdev->dev.device_status = USB_OTG_DEFAULT;
  pdev->dev.usr_cb->DeviceReset(pdev->cfg.speed);

  return USBD_OK;
}
This is interesting code, even if the callback to DeviceReset doesn't do anything exciting. And it turns out USBD_Reset is set up in exactly the same way in the other callback structure, namely USBD_DCD_INT_cb !! We plan to make a study of that structure the topic of the next page.

All of this is going to lead us to consideration of USB events. All of USB seems to be event driven, and that is handled by a variety of interrupts. We can talk about housekeeping events (like reset/connect/disconnect) and data events (packets arriving or completion of transmitting).


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org