AnsweredAssumed Answered

USB device development from scratch

Question asked by Herrera_Morales.Avel on Jul 27, 2016
Latest reply on Mar 4, 2018 by Dimitris Paraskevopoulos
Hi all,

I am programming the USB device controller of a STM32F103 from scratch (without libraries, only with an arm-none-eabi/newlib toolchain). The STM32 initializes and send the device descriptor succesfully, but fails sending the configuration descriptor (the device indicates a correct transaction but the PC indicates an io error). Here is the code (it only covers the GET_DESCRIPTOR and SET_ADDRESS state machines, I stoped because of this problem I am describing):

void usbDeviceInit() {
    // enable USB clock
    RCC_APB1ENR |= (((uint32_t) 1) << 23);
    // enable USB interrupts
    NVIC_ENABLE_IRQ(20);
    // enable analog transceivers
    USB_CNTR &= ~(((uint16_t) 1) << 1);
    for (uint32_t i = 0; i < 20000; i++)
        ;
    USB_CNTR &= ~(((uint16_t) 1) << 0);
    USB_ISTR = 0;
    // enable and wait for USB RESET interrupt
    USB_CNTR |= (((uint16_t) 1) << 10);
}

void usbDeviceISR() {
    uint16_t interruptStatus = USB_ISTR;
    // clear interrupt
    USB_ISTR = 0;
    if (interruptStatus & (((uint16_t) 1) << 10)) {
        // USB reset
        // prepare buffer descriptor table for endpoint 0 (control)
        USB_BTABLE = 0;
        USB_ADDR0_TX = 12;   // 8
        USB_COUNT0_TX = 64;
        USB_ADDR0_RX = 12 + 64;   // 8 + 64
        USB_COUNT0_RX = (((uint16_t) 1) << 15) | (((uint16_t) 2) << 10);   // 2 * 32 = 64 bytes
        // prepare buffer descriptor table for endpoint 1 (in interrupt)
        USB_ADDR1_TX = 12 + 64 + 64;
        USB_COUNT1_TX = 64;
        // device address = 0
        USB_DADDR = ((uint16_t) 1) << 7;
        // prepare endpoint 0 for rx setup packets: type=control, dtog_tx=1, stat_rx=10 (nak), stat_tx=10 (nak)
        USB_EP0R = (((uint16_t) 1) << 9) | ((USB_EP0R & STAT_RX_MASK) ^ STAT_RX_NAK) | ((USB_EP0R & STAT_TX_MASK) ^ STAT_TX_NAK) | ((USB_EP0R & DTOG_RX_MASK) ^ DTOG_RX_0) | ((USB_EP0R & DTOG_TX_MASK) ^ DTOG_TX_1);
        usbStatus = 0;
        // prepare endpoint 1 for in transfers: type=interrupt, stat_rx=10 (nak), stat_tx=10 (nak)
        USB_EP1R = ((uint16_t) 1) | (((uint16_t) 2) << 12) | (((uint16_t) 2) << 4) | (((uint16_t) 3) << 9);
        // enable complete transfer interrupt
        USB_CNTR |= (((uint16_t) 1) << 15);
    }
    else {
        uint8_t endpoint = interruptStatus & 0x000F;
        bool isHostToDevice = (interruptStatus & 0x0010) ? true : false;
        bool isDeviceToHost = !isHostToDevice;
        if (interruptStatus & (((uint16_t) 1) << 15)) {
            if (endpoint == 0) {
                if (usbStatus == 0) {
                    usbCopyFromPacketSRAM((uint32_t *) USB_EP0RXBUF, usbRxBuffer);
                    UsbSetupPacket *setupPacket = (UsbSetupPacket *) usbRxBuffer;
                    if ((setupPacket->bmRequestType == 0x80) && (setupPacket->bRequest == 0x06)) {
                        // GET_DESCRIPTOR request
                        if ((setupPacket->wValue >> 8) == 1) {
                            // GET_DESCRIPTOR (device)
                            // copy device descriptor to tx buffer
                            usbCopyToPacketSRAM((uint16_t *) &MyUsbDeviceDescriptor, (uint32_t *) USB_EP0TXBUF);
                            // enable endpoint 0: control endpoint + stat_tx=11 (valid)
                            usbStatus = 1;
                            USB_COUNT0_TX = 18;
                            USB_EP0R = (((uint16_t) 1) << 9) | (((uint16_t) 1) << 4);
                        }
                        else if ((setupPacket->wValue >> 8) == 2) {
                            // GET_DESCRIPTOR (configuration)
                            // copy configuration descriptor to tx buffer
                            usbCopyToPacketSRAM((uint16_t *) &MyUsbConfigurationDescriptor, (uint32_t *) USB_EP0TXBUF);
                            // enable endpoint 0: control endpoint + stat_tx=11 (valid)
                            usbStatus = 1;
                            USB_COUNT0_TX = MyUsbConfigurationDescriptor.wTotalLength;
                            USB_EP0R = (((uint16_t) 1) << 9) | (((uint16_t) 1) << 4);
                        }
                        else {
                            NVIC_DISABLE_IRQ(20);
                            usart1SendString("other GET_DESCRIPTOR\r\n");
                            usart1SendHexBytes((uint8_t *) usbRxBuffer, 8);
                            usart1SendString("\r\n");
                        }
                    }
                    else if ((setupPacket->bmRequestType == 0x00) && (setupPacket->bRequest == 0x05)) {
                        // SET_ADDRESS request
                        usbNextAddress = (uint8_t) (setupPacket->wValue & 0x00FF);
                        usbStatus = 3;
                        // send ack (0 bytes packet)
                        USB_COUNT0_TX = 0;
                        // enable endpoint 0: control endpoint + stat_tx=11 (valid)
                        USB_EP0R = (((uint16_t) 1) << 9) | (((uint16_t) 1) << 4);
                    }
                    else {
                        NVIC_DISABLE_IRQ(20);
                        usart1SendString("other setup packet\r\n");
                        usart1SendHexBytes((uint8_t *) usbRxBuffer, 8);
                        usart1SendString("\r\n");
                    }
                }
                else if (usbStatus == 1) {
                    // wait for empty out transfer (GET_DESCRIPTOR ack)
                    usbStatus = 2;
                    USB_EP0R = (((uint16_t) 1) << 9) | (((uint16_t) 1) << 12);
                }
                else if (usbStatus == 2) {
                    // empty out transfer (GET_DESCRIPTOR ack) received
                    // prepare endpoint 0 for rx setup packets: type=control, dtog_tx=1, stat_rx=10 (nak), stat_tx=10 (nak)
                    usbStatus = 0;
                    USB_EP0R = (((uint16_t) 1) << 9) | ((USB_EP0R & STAT_RX_MASK) ^ STAT_RX_NAK) | ((USB_EP0R & STAT_TX_MASK) ^ STAT_TX_NAK) | ((USB_EP0R & DTOG_RX_MASK) ^ DTOG_RX_0) | ((USB_EP0R & DTOG_TX_MASK) ^ DTOG_TX_1);
                }
                else if (usbStatus == 3) {
                    // SET_ADDRESS ack sent
                    // device address = usbNextAddress
                    USB_DADDR = (((uint16_t) 1) << 7) | (usbNextAddress & 0x007F);
                    // prepare endpoint 0 for rx setup packets: type=control, dtog_tx=1, stat_rx=10 (nak), stat_tx=10 (nak)
                    usbStatus = 0;
                    USB_EP0R = (((uint16_t) 1) << 9) | ((USB_EP0R & STAT_RX_MASK) ^ STAT_RX_NAK) | ((USB_EP0R & STAT_TX_MASK) ^ STAT_TX_NAK) | ((USB_EP0R & DTOG_RX_MASK) ^ DTOG_RX_0) | ((USB_EP0R & DTOG_TX_MASK) ^ DTOG_TX_1);
                }
                else {
                    NVIC_DISABLE_IRQ(20);
                    usart1SendString("ERROR 1!\r\n");
                }
            }
            else if (endpoint == 1) {
                NVIC_DISABLE_IRQ(20);
                usart1SendString("ERROR 2!\r\n");
            }
            else {
                NVIC_DISABLE_IRQ(20);
                usart1SendString("ERROR 3!\r\n");
            }
        }
        else {
            NVIC_DISABLE_IRQ(20);
            usart1SendString("ERROR 4!\r\n");
        }
    }
}


Sorry about the horrible code style (in some lines I use constants, in some lines I use numbers directly, the code is still under development).

Just after USB reset STM32 configures EP0 as the documentation specifies: control ep, stat_rx=nak, stat_tx=nak, dtog_rx=0, dtog_tx=1 and set usbStatus = 0. Being in this usbStatus (usbStatus == 0) the PC sends the GET_DESCRIPTOR(device) setup packet, then STM32 copies the device descriptor to the tx buffer on packet RAM and set STAT_TX=VALID to prepare for the IN transaction, when this IN transaction completes (usbStatus == 1) STM32 prepares EP0 for the ack OUT transfer from host. When this empty transfer completes (usbStatus == 2) EP0 is configured exactly after USB reset: control ep, stat_rx=nak, stat_tx=nak, dtog_rx=0, dtog_tx=1. This way the STM32 is prepared for another setup transfer on endpoint 0.

In practical tests, after the USB reset condition, the STM32 sends the device descriptor ok (on the PC side I can see the device descriptor using an USB sniffer), and the PC sends the SET_ADDRESS setup. The STM32 configures ok the address because of the PC on Linux sends the GET_DESCRIPTOR(device) again using the new address and the device descriptor is readed on PC correctly. The problem comes with the configuration descriptor: On the STM32 side the transfer is signaled as "ok" (no error bits set) but on the PC side Linux gives an IO error reading the config descriptor.

The EP0 is configured as 64 bytes length so both device and configuration descriptors need only one packet to be sent from STM32 to PC (no fragmentation required).

Can anybody help me? What am I doing wrong?

Thank you very much!

Outcomes