March 1, 2025

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

Depending on how you want to look at it, the device has either 4 or 7 endpoints. Endpoint 0 is a bidirectional endpoint used for control transfers, as is always the case with USB. Then we have 3 IN and 3 OUT endpoints

Remember that IN and OUT are defined from the point of view of the host. Even though the F411 is an "OTG" device and can be set up as a host or a device, I have no interest in the host role and will always be talking about using it as a device. Therefore from our point of view, OUT will be data being received by the F411 and IN will be data being sent by the F411.

Endpoint initialization

Take a look at usbF4/driver/usb_core.c,

There is code here for both Host and Device initialization. The routine we want is USB_OTG_CoreInitDev() and it has the following:

	/* set Rx FIFO size */
    USB_OTG_WRITE_REG32(&pdev->regs.GREGS->GRXFSIZ, RX_FIFO_FS_SIZE);

    /* EP0 TX*/
    nptxfifosize.b.depth     = TX0_FIFO_FS_SIZE;
    nptxfifosize.b.startaddr = RX_FIFO_FS_SIZE;
    USB_OTG_WRITE_REG32( &pdev->regs.GREGS->DIEPTXF0_HNPTXFSIZ, nptxfifosize.d32 );


    /* EP1 TX*/
    txfifosize.b.startaddr = nptxfifosize.b.startaddr + nptxfifosize.b.depth;
    txfifosize.b.depth = TX1_FIFO_FS_SIZE;
    USB_OTG_WRITE_REG32( &pdev->regs.GREGS->DIEPTXF[0], txfifosize.d32 );


    /* EP2 TX*/
    txfifosize.b.startaddr += txfifosize.b.depth;
    txfifosize.b.depth = TX2_FIFO_FS_SIZE;
    USB_OTG_WRITE_REG32( &pdev->regs.GREGS->DIEPTXF[1], txfifosize.d32 );


    /* EP3 TX*/
    txfifosize.b.startaddr += txfifosize.b.depth;
    txfifosize.b.depth = TX3_FIFO_FS_SIZE;
    USB_OTG_WRITE_REG32( &pdev->regs.GREGS->DIEPTXF[2], txfifosize.d32 );
There is plenty here to digest. First those sizes, which are set in usb_conf_template.h
 #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
Note the use of macros to read/write device registers, something I generally avoid:
#define USB_OTG_READ_REG32(reg)  (*(__IO uint32_t *)(reg))
#define USB_OTG_WRITE_REG32(reg,value) (*(__IO uint32_t *)(reg) = (value))
Now the registers themselves. First we have GRXFSIZ. This is at address 0x024 (see usb_regs.h). The "G" indicates this is a global register. Here we set the size of the receive FIFO in 32 bit words (so 512 bytes)

Next we set values of 64 (256 bytes) and 128 (512 bytes) for a total of 1024+256 bytes, i.e. 1.25K -- exhausting the actual RAM that exists for endpoint FIFOs.

Now look at the shenanigans that go on with the TX registers. The DIEPTXF0_HNPTXFSIZ register is also in the global group (note the GREG base pointer), and it is at address 0x028. This 32 bit register has two 16 bit fields. One is the start address in FIFO ram, the other is the size (i.e. depth). What the code does to set these two fields is to use a union to build the 32 bit value. The union looks like so:

typedef union _USB_OTG_FSIZ_TypeDef
{
  uint32_t d32;
  struct
  {
	uint32_t startaddr : 16;
	uint32_t depth : 16;
  } b;
This is certainly one way to do things.

How does this fit into the general scheme of initialization

In other words, who calls the routine USB_OTG_CoreInitDev() and how does this fit into the whole business of USB initialization?

It is called by DCD_Init() in usb_dcd.c
This is called by USBD_Init() in library/usbd_core.c
This is called by usb_init() in usbF4/usb.c which is my top level code.

It would be worthwhile to start in usb.c and follow the calls to initialize everything.

After that is done, it would be worthwhile to follow the path from the interrupt handler to the various things that handle those events.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org