March 15, 2025

Black Pill boards - F411 USB -- USB endpoints, part 5 - reading

We start with the routine VCPGetBytes() in vcp/usbd_cdc_vcp.c

Just as for writing, we find some global variables being manipulated.

#define UsbRecBufferSize 2048
#define UsbRecBufferSizeMask (UsbRecBufferSize-1)
uint8_t __CCMRAM__ UsbRecBuffer[UsbRecBufferSize];
volatile int UsbRecRead = 0;
volatile int UsbRecWrite = 0;
So this looks to be a 2048 byte buffer.

Data gets placed into this buffer by VCP_DataRx() in the same file. This is accessed as a callback via the VCP_fops structure. The name in the callback is pIf_DataRx, and it gets used in only one place -- usbd_cdc_DataOut() in library/usbd_cdc_core.c

This is referenced via another callback as "DataOut". It is called in only one place in library/usbd_core.c in USBD_DataOutStage() and yes, this gets called via another callback. The reference is "DataOutStage".

This finally takes us to the driver in driver/usb_dcd_int.c in DCD_HandleOutEP_ISR(). We see:

		/* Inform upper layer: data ready */
        /* RX COMPLETE */
        USBD_DCD_INT_fops->DataOutStage(pdev , epnum);
So, we have gotten an endpoint interrupt indicating "transfer complete" on an OUT endpoint (i.e. data coming our way).

Let's go back through this chain and see where the data actually gets pulled out of the endpoint (the special USB fifo ram).

Removing data

We see this code in usbd_cdc_DataOu() in library/usbd_cdc_core.c
uint16_t USB_Rx_Cnt = ((USB_OTG_CORE_HANDLE*)pdev)->dev.out_ep[epnum].xfer_count;
APP_FOPS.pIf_DataRx(USB_Rx_Buffer, USB_Rx_Cnt);
I am curious about the buffer, I see:
#define CDC_DATA_MAX_PACKET_SIZE       64   /* Endpoint IN & OUT Packet size */
uint8_t __CCMRAM__ USB_Rx_Buffer[CDC_DATA_MAX_PACKET_SIZE]
And what about this "CCMRAM" business? This is defined as:
// Variable attributes, instructs the linker to place the marked
// variable in CCRAM instead of RAM.
#define __CCMRAM__ __attr_ccmram
#define __attr_ccmram __attribute__((section (".ccmdata")))
There are mysteries here I don't understand. But the code works, so there is something missing that I have yet to figure out.

An experiment

Setting that aside for now, the business happens in VCP_DataRx() in vcp/usbd_cdc_vcp.c Let's put some debug here and see what happens. I add this line:

usb_debug ( DM_READ1, "VCP_DataRx %X %d\n", Buf, Len );
I need to have a terminal connected, but I do, and when I type a character, I see:
VCP_DataRx 20000F40 1
This address is interesting (see below). I can only send a single character at a time by typing into picocom. I will have to learn to do some USB programming on the linux side to do bigger writes. I can probably just write a simple C program that opens /dev/ttyACM0 and then does writes of various sizes.

What is CCMRAM

Our ram starts at 0x2000_0000, so this buffer is in plain old ram. This surprises me. I expected the packet to be in the special 1.25K area of USB dedicated FIFO memory. I figured this printf would tell me what the address of that area was. Well, I was wrong. Next, I had thought that maybe I had made a mistake setting up my linker script and not properly dealing with the ccmdata section No again.

Note that the USB FS hardware addresses start at 0x5000_0000.

I dig into the linker scripts in Arduino_STM32. CCMRAM just seems to be the first part of the regular ram area. When I do some searching I learn that CCMRAM is: "core coupled memory", which is a special section (usually 64K) of ram that is more closely connected to the CPU and faster for some purposes.

Some STM32 CPUs include two banks of memory: the standard SRAM and another bank of Core-Coupled-Memory (a.k.a. CCM), which is faster than the regular SRAM.

So, I could take more care where I place this ccmram section, but in truth, it can go into ram anywhere and ensuring that it does go into CCM is just an optimization.

The F411 has 64K of CCM ram. CCM is not eligible for DMA. The F411 has 128K of ram total. I don't see any mention of CCM ram in connection with the F429, but I suspect I am not looking in the right place.
This whole topic is interesting, but not what I thought it would be.

I am still curious about the dedicated FIFO ram. Our file driver/template.h has a big comment that talks about the sizes of the endpoint FIFOs in this special ram, wherever it is.
It shows:

 #define RX_FIFO_FS_SIZE                          128
 #define TX0_FIFO_FS_SIZE                          64
 #define TX1_FIFO_FS_SIZE                         128
 #define TX2_FIFO_FS_SIZE                          0
 #define TX3_FIFO_FS_SIZE                          0
These counts are 32 bit words (4 bytes each) so this is 1280 bytes, i.e. 1024 + 256 bytes.

These values get handled in ./driver/usb_core.c in USB_OTG_CoreInitDev(). The values get loaded into device registers and that is the last we hear about them. In other words, we just use these values to tell the USB device how to partition its internal FIFO ram.

Perhaps this FIFO ram is entirely hidden away inside the USB device and never gets directly accessed. It seems so.

How does the USB device know about our buffer

It happens in library/usbd_cdc_core.c by calls like this:
DCD_EP_PrepareRx(pdev, CDC_OUT_EP, (uint8_t*)(USB_Rx_Buffer), CDC_DATA_OUT_PACKET_SIZE);
Note that the third argument is the buffer address (and the fourth is the size). We tell the USB device where it should place data. This routine is in driver/usb_dcd.c

The "prepare" routine loads the buffer address and length into and endpoint structure, then calls USB_OTG_EPStartXfer(), which merits future study.

I'll note that there is a flag "dma_enable" that this routine checks. It looks to me like it gets set to 0 in driver/usb_core.c -- which would make sense given the statement that CCM ram is not DMA capable.

So how does data get into our memory buffer if DMA cannot be used. That is a good topic for a future study.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org