March 15, 2025

Black Pill boards - F411 USB -- USB endpoints, part 2

You may be thinking, "wait! - this is supposed to be about endpoints!" Well, there has been plenty of mention of endpoints. Any routine with "EP" in its names does something with or to an endpoint, and we will get to the details eventually.

What we are doing here is getting a "big picture" view of the USB code. I have often done this for other projects, and I would say that it is almost essential when you are dealing with thousands of lines of code.

Let's look at the code in "vcp"

We have 3 directories, vcp, library, and driver. Among other things we would like to understand how they relate to each other.
wc *.c *.h
  473  1505 12401 usbd_cdc_vcp.c
  238   668  7064 usbd_desc.c
   37    99  1078 usbd_cdc_vcp.h
   51   218  2250 usbd_conf.h
   48   163  2076 usbd_desc.h
  847  2653 24869 total
Here we have only 847 lines of code and 2 C files.

There are also 3 header files, but they aren't particularly interesting and I won't have anything to say about them.

vcp/usbd_cdc_vcp.c

VCPBytesAvailable ()
VCPGetBytes () -- outside world call to get data
VCP_SetUSBTxBlocking ()
VCP_DataTx () -- outside world call to send data
VCPBytesAvailable() is wrapped in an inline function usbBytesAvailable(). This is never used. This is clearly a way of "renaming" this VCP specific function to something more generic.

Similarly, VCP_SetUSBTxBlocking () is wrapped in a pair of inline functions:

inline void usbEnableBlockingTx(void) { VCP_SetUSBTxBlocking(1); }
inline void usbDisableBlockingTx(void) { VCP_SetUSBTxBlocking(0); }
neither is called in the current code.

So this file contains the top level routines to read and write USB data once the VCP is enumerated and set up. Both of these would be excellent starting points for detailed study.

this file also contains several (5) static (private) routines that are exposed to the outside world via a "fops" structure. These 5 routines are:

Init, DeInit, Ctrl, DataTx, DataRx
I will note before I move on that the code in this file makes relatively few calls to code in other files, but largely works by manipulating data structures. Even DataTx makes no calls, so other code must monitor data structures and notice when data has been posted to be sent.

One call is made to usbd_cdc_PrepareRx() in library/usbd_cdc_core.c

vcp/usbd_desc.c

This holds the packet data that gets returned during enumeration and provides access to it via a collection of 7 functions that are collected together in a "USR_desc" structure that looks like this:
USBD_DEVICE USR_desc =
{
  USBD_USR_DeviceDescriptor,
  USBD_USR_LangIDStrDescriptor,
  USBD_USR_ManufacturerStrDescriptor,
  USBD_USR_ProductStrDescriptor,
  USBD_USR_SerialStrDescriptor,
  USBD_USR_ConfigStrDescriptor,
  USBD_USR_InterfaceStrDescriptor,
};
These functions aren't declared static, but they really should be.

code in the "library" directory

Here we have significantly more code:
wc *.c *.h
  718  2508 24405 usbd_cdc_core.c
  418   968 10129 usbd_core.c
  162   436  4216 usbd_ioreq.c
  790  1637 18691 usbd_req.c
  159   386  3245 usbd_usr.c
   72   168  2725 usbd_cdc_core.h
   45    87  1236 usbd_core.h
   82   211  3620 usbd_def.h
   44    98  1581 usbd_ioreq.h
   31    90  1139 usbd_req.h
   56   105  1643 usbd_usr.h
 2577  6694 72630 total
2577 lines of code in 5 C files and 6 header files. The header files are all tiny (but interesting)
In some ways usbd_usr.h is the most interesting as it contains prototypes for the functions that are intended to be externally visible.

library/usbd_cdc_core.c

It starts off with a big comment about the CDC Class driver and ACM. This file also has some descriptors that get used during enumeration.

It has a bunch of functions, here nicely declared static, that are grouped together in the following callback structure:

USBD_Class_cb_TypeDef  USBD_CDC_cb =
{
  usbd_cdc_Init,
  usbd_cdc_DeInit,
  usbd_cdc_Setup,
  usbd_cdc_EP0_RxReady,
  usbd_cdc_DataIn,
  usbd_cdc_DataOut,
  usbd_cdc_SOF,
  USBD_cdc_GetCfgDesc,
};
Then we have just one function:
usbd_cdc_PrepareRx () - called from vcp/usbd_cdc_vcp.c

library/usbd_core.c

It also has a big callback structure:
USBD_DCD_INT_cb_TypeDef USBD_DCD_INT_cb =
{
  USBD_DataOutStage,
  USBD_DataInStage,
  USBD_SetupStage,
  USBD_SOF,
  USBD_Reset,
  USBD_Suspend,
  USBD_Resume,
  USBD_IsoINIncomplete,
  USBD_IsoOUTIncomplete,
};
Then:
USBD_Init () - we call this to initialize the driver
USBD_DeInit () -- never called
USBD_DeInitFull () -- could be called during usbPowerOff()
USBD_SetCfg () -- called by usbd_req.c
USBD_ClrCfg () -- never called

library/usbd_ioreq.c

USBD_CtlSendData () -- called by usbd_cdc_core.c and usbd_req.c
USBD_CtlContinueSendData () -- called by usbd_core.c
USBD_CtlPrepareRx () -- called by usbd_cdc_core.c
USBD_CtlContinueRx () -- called by usbd_core.c
USBD_CtlSendStatus () -- called by usbd_core.c and usbd_req.c
USBD_CtlReceiveStatus () -- called by usbd_core.c
USBD_GetRxCount () -- never called
Notice that CtlSendData "sends data on the ctl pipe" -- which suggests that CDC/VCP does use more than one endpoint, with an extra one devoted to ctl information

library/usbd_req.c

USBD_StdDevReq () - called by usbd_core.c duing setup (enumeration)
USBD_StdItfReq () - called by usbd_core.c duing setup (enumeration)
USBD_StdEPReq () - called by usbd_core.c duing setup (enumeration)
USBD_ParseSetupRequest () - called by usbd_core.c duing setup (enumeration)
USBD_CtlError () -- many calls (all from library) to handle low level error
USBD_GetString () -- convert ascii to unicode - many calls from vcp/usbd_desc.c
USBD_GetLen () - once call from library/usbd_req.c 

library/usbd_usr.c

Starts with a callback structure:
SBD_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 above ought to be static, but aren't -- so I made it so. It has these important one line functions:
uint8_t usb_isConfigured(void) { return (usbd_status&USB_CONFIGURED); }
uint8_t usb_getStatus(void) { return usbd_status; }

Conclusion

There you have it, the big picture. Go back and take another look at the callback structures and you will have it all, now we can perhaps focus on reading and writing from/to endpoints.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org