Skip to content

03. USB Reset

Nathanael Schneider edited this page Apr 6, 2023 · 3 revisions

On Reset, the peripheral will clear all EPnR registers. Also, we need to initialize the USB-SRAM and enable USB communications.

3.0 Theory

3.0.1 USB-Memory

The USB periphery can't access the SRAM directly, but instead uses a dedicated SRAM called USB-SRAM. This area is special, because it can't be accessed in whole words (4 bytes) but only half-words (2 bytes) at a time. All information that the peripheral needs will be stored in this area, namely the BTable and the IN/OUT Buffers.

Some architectures circumvent this problem by giving the user word sized memory access, but only the lower half contains the actual data and the upper half being empty unmapped data (STM32F103), while the periphery sees the memory in half-words. (German/English explanation).

The STM32G4 does only allow half-word sized access and does thus not provide a filler. Doing a word-access will corrupt the memory, during reading and writing. Also accessing single bytes does only work if the byte is half-word aligned (i.e. the first byte of a half-word). Accessing the second byte might actually yield the first byte. This means that all access has to be done in half-words wherever possible.

3.0.2 BTable

The BTable contains the memory regions of the endpoint buffers and their size. The reference manual explains them pretty clearly. The address of the BTable and the memory regions in it are relative to the USB-SRAM, not the system memory, so a conversion is necessary.

3.0.3 USB-Reset

To properly handle a USB-Reset, you'll need to initialize the BTable and all required memory regions, initialize Endpoint 0, which always has to be a control endpoint with which the host will communicate USB-Protocol related information. Lastly, the peripheral needs to be enabled for reception and the USB-Address reset to 0.

3.1 USB-SRAM

First we expand the linker script STM32G551RBTX_FLASH.ld to include the USB-SRAM region. We define this region as (NOLOAD) to prevent the programmer from trying to zero out this memory region, which won't work. Otherwise you can't flash the image using DFU.

MEMORY
{
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 32K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 128K
  USBRAM  (rw)    : ORIGIN = 0x40006000, LENGTH = 1K
}

...

SECTIONS
{
  ...

  .usbbuffer (NOLOAD):
  {
    *(.usbbuf)
    *(.usbbuf*)
  } > USBRAM

  ...
}

Then we define a macro to use this region and to recalculate the memory offset for the periphery, as it will start counting at 0x40006000.

// STM32G4
#define __USB_MEM __attribute__((section(".usbbuf")))
#define __USBBUF_BEGIN 0x40006000
#define __MEM2USB(X) (((int)X - __USBBUF_BEGIN))
#define __USB2MEM(X) (((int)X + __USBBUF_BEGIN))

// STM32F103 (untested)
#define __USB_MEM __attribute__((section(".usbbuf")))
#define __USBBUF_BEGIN 0x40006000
#define __MEM2USB(X) (((int)X - __USBBUF_BEGIN) / 2)
#define __USB2MEM(X) (((int)X * 2 + __USBBUF_BEGIN))

The memory is not automatically zeroed out, so we will have to zero it out ourselves. This will help with finding errors and viewing the memory later.

static void USB_ClearSRAM() {
    char *buffer = __USBBUF_BEGIN;

    for (int i = 0; i < 1024; i++) {
        buffer[i] = 0;
    }
}

3.2 BTable

All Information regarding the endpoints and their respective memory is stored in the BTable. This table needs to be initialized on a reset. The table needs to have as many entries as there will be endpoints. Unidirectional endpoints can share an entry, refer to the reference manual for more information. The STM32G4 supports 8 bidirectional endpoints, so that's what we'll allocate. The BTable needs to be aligned to an 8-byte boundary as per the reference manual. The variable will be defined as volatile to prevent optimization, as it basically acts like a register.

typedef struct {
    unsigned short ADDR_TX;
    unsigned short COUNT_TX;
    unsigned short ADDR_RX;
    unsigned short COUNT_RX;
} USB_BTABLE_ENTRY;

__ALIGNED(8)
__USB_MEM
__IO static USB_BTABLE_ENTRY BTable[8] = {0};

3.3 Endpoint Buffers

After a reset, the USB device has to initialize Endpoint 0, which has to be a control endpoint. It will be used to communicate with the device as a whole instead of a single Endpoint, e.g. for getting the configuration, number of endpoints etc.

The EP0-Buffer will only have to deal with 64 bytes of data at maximum, so that's what we'll allocate. The memory needs to be aligned to 2 bytes.

__ALIGNED(2)
__USB_MEM
__IO static char EP0_Buf[2][64] = {0};

3.4 Endpoint Registers

The endpoint registers are a bit stupid. Some bits you can toggle, some you can write to directly. This requires you to use a special way to alter the registers so that you don't mess up.

static void USB_SetEP(short *ep, short value, short mask) {
    short toggle = 0b0111000001110000;
    short rc_w0 = 0b1000000010000000;
    short rw = 0b0000011100001111;

    short wr0 = rc_w0 & (~mask | value);
    short wr1 = (mask & toggle) & (*ep ^ value);
    short wr2 = rw & ((*ep & ~mask) | value);

    *ep = wr0 | wr1 | wr2;
}

This helper will take the target values and a mask to alter the desired bits each in their own way and keeping everything else the same.

3.5 RESET-Handler

Now we will put everything together inside the RESET-Handler. We also implement the CTR-Handler for debugging purposes.

void USB_LP_IRQHandler() {
    if((USB->ISTR & USB_ISTR_RESET) != 0) {
        // Clear interrupt
        USB->ISTR = ~USB_ISTR_RESET;

        // Clear SRAM for readability
        USB_ClearSRAM();

        // Prepare BTable
        USB->BTABLE = __MEM2USB(BTable);

        BTable[0].ADDR_RX = __MEM2USB(EP0_Buf[0]);
        BTable[0].ADDR_TX = __MEM2USB(EP0_Buf[1]);
        BTable[0].COUNT_TX = 0;
        BTable[0].COUNT_RX = (1 << 15) | (1 << 10);

        // Prepare for a setup packet (RX = Valid, TX = NAK)
        USB_SetEP(&USB->EP0R, USB_EP_CONTROL | USB_EP_RX_VALID | USB_EP_TX_NAK, USB_EP_TYPE_MASK | USB_EP_RX_VALID | USB_EP_TX_VALID);

        // Enable USB functionality and set address to 0
        USB->DADDR = USB_DADDR_EF;
    } else if ((USB->ISTR & USB_ISTR_CTR) != 0) {
        __BKPT(); // Breakpoint for debugging
    }
}

3.6 Verification & Debugging

3.6.1 Memory layout

Get the address of the BTable, either by storing it in a variable or using a debugger that can fetch the address. It should be 0x40006000 if the BTable is the first Variable you declared in the USB-SRAM region.

Similarly the EP0_Buf should be around the same ballpark, a bit higher up.

3.6.2 Memory contents

Debugging memory contents is a bit more involved, as the Memory analyzer in the STM32CubeIDE will fetch memory in words, not half words. Therefore every second half-word will contain garbage. To view the memory accurately, copy it into a regular SRAM-region using char- or short-sized transfers. A function for this will follow later.

3.6.3 Functionality

The code should break in the CTR, indicating a successful transfer of the first data packet. If it does not and the USB_ISTR register shows an ERR-Event, check your clocks.

Windows should still show the same message, but take a couple seconds longer to do so.

Clone this wiki locally