March 15, 2025

Black Pill boards - F411 USB -- USB endpoints, part 3 - writing

Along the way, we learned that a call to VCP_DataTx() in vcp/usbd_cdc_vcp.c is how an applications sends data. Let's examine that in detail.

When I wrote code in usb.c to send data I did this:

(void) VCP_DataTx ( ubuf, len );
This routine works with 3 global variables:
#define APP_TX_DATA_SIZE               512
volatile uint16_t APP_Tx_ptr_in  = 0;
volatile uint16_t APP_Tx_ptr_out = 0;
uint8_t APP_Tx_Buffer   [APP_TX_DATA_SIZE];
The pointers are in library/usbd_cdc_core.c, the buffer is in library/usbd_cdc_core.c The buffer size is in vcp/usbd_conf.h. If we were working with the HS driver, the size would be 2048 Note that the buffer is NOT in the dedicated on-chip USB ram, that will come later.

look at code in library/usbd_cdc_core.c

We will ignore details of how VCP_DataTx works with these variables. All we need to know for now is that it puts the data into the APP_Tx_Buffer. It does not make any function calls, it just manipulates the "in" pointer. If there is not room in the buffer, it will spin.

What is going on here is that these pointers set up a software FIFO. Data gets loaded by calls to VCP_DataTx, then extracted and transmitted by routines in the driver in response to interrupts.

Two places in usbd_cdc_core.c deal with this buffer and these pointers. The first is in usbd_cdc_DataIn(). Remember that we are dealing with an IN endpoint to send data. This routine calculates how much data is in the buffer, and if the result is non-zero, it will call:

DCD_EP_Tx (pdev, CDC_IN_EP, (uint8_t*)&APP_Tx_Buffer[USB_Tx_ptr], USB_Tx_length);
This is in driver/usb_dcd.c. It gets a pointer to an endpoint structure, sets up a bunch of information, and then (in our case where the endpoint is not 0) calls:
USB_OTG_EPStartXfer(pdev, ep );
This routine is in driver/usb_core.c and talks to the USB hardware registers.

The second place in library/usbd_cdc_core.c that monitors these buffer pointers is the routine Handle_USBAsynchXfer(). Just like the case above, when this routine discovers there is a non-zero length waiting, it calls:

DCD_EP_Tx (pdev, CDC_IN_EP, (uint8_t*)&APP_Tx_Buffer[USB_Tx_ptr], USB_Tx_length);

This second case is handled as a result of a call to usbd_cdc_SOF() -- so in response to an SOF interrupt.

The first case is called by a "data in" event, which is a "well known thing" in the USB protocol. A callback "DataIn" is what calls us. And it is called by yet another callback "DataInStage"

This callback is called in driver/usb_dcd_int.c in the routine DCD_HandleInEP_ISR().

So, we are handling an endpoint interrupt and we see the "xfercompl" bit set. In other words, some previous transfer has completed, so the endpoint is now idle and available and we can put data on it, if we have data.

But, what if a transfer completes, we don't have data to send, so the endpoint just goes idle? That must be what the second case with the SOF interrupt takes care of. Maybe. As I understand it at this time.

Conclusion

The answer to understanding all of this is to do some experimenting. Find out how a write with 80 bytes gets handled. Does the SOF start it and the end of transfer continue it, or just what. Try some bigger writes, even some bigger than the 512 byte buffer.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org