2023-07-08 12:06 PM - edited 2023-07-08 12:07 PM
For learning (and suffering) purposes I am trying to write my own USB Driver in device mode. Right now I am just trying to successfully enumerate the device. So far I am able to read SETUP packets but when writing to EP0 (IN transfer) the requests seem to timeout at 5 seconds (seeing this through Wireshark). I am using a STM32F401CCU6 chip.
For example, this is what happens when a GET_DESCRIPTOR request is made:
USB URB
[Source: host]
[Destination: 3.0.0]
URB id: 0xffff9d142a6f8900
URB type: URB_SUBMIT ('S')
URB transfer type: URB_CONTROL (0x02)
Endpoint: 0x80, Direction: IN
Device: 0
URB bus id: 3
Device setup request: relevant ('\0')
Data: not present ('<')
URB sec: 1688842172
URB usec: 857980
URB status: Operation now in progress (-EINPROGRESS) (-115)
URB length [bytes]: 64
Data length [bytes]: 0
[Response in: 58]
Interval: 0
Start frame: 0
Copy of Transfer Flags: 0x00000200, Dir IN
Number of ISO descriptors: 0
Setup Data
bmRequestType: 0x80
bRequest: GET DESCRIPTOR (6)
Descriptor Index: 0x00
bDescriptorType: DEVICE (0x01)
Language Id: no language specified (0x0000)
wLength: 64
And the answer generated by my MCU (and code):
USB URB
[Source: 3.0.0]
[Destination: host]
URB id: 0xffff9d142a6f8900
URB type: URB_COMPLETE ('C')
URB transfer type: URB_CONTROL (0x02)
Endpoint: 0x80, Direction: IN
Device: 0
URB bus id: 3
Device setup request: not relevant ('-')
Data: present ('\0')
URB sec: 1688842178
URB usec: 31400
URB status: No such file or directory (-ENOENT) (-2)
URB length [bytes]: 64
Data length [bytes]: 64
[Request in: 57]
[Time from request: 5.173420000 seconds]
Unused Setup Header
Interval: 0
Start frame: 0
Copy of Transfer Flags: 0x00000200, Dir IN
Number of ISO descriptors: 0
DEVICE DESCRIPTOR
bLength: 18
bDescriptorType: 0x01 (DEVICE)
bcdUSB: 0x0200
bDeviceClass: Communications and CDC Control (0x02)
bDeviceSubClass: 2
bDeviceProtocol: 0
bMaxPacketSize0: 64
idVendor: STMicroelectronics (0x0483)
idProduct: Virtual COM Port (0x5740)
bcdDevice: 0x0100
iManufacturer: 0
iProduct: 0
iSerialNumber: 0
bNumConfigurations: 1
Leftover Capture Data: 000000000000000000000000000000000000000000000000000000000000000000000000…
I don't have a hardware analyzer but it seems like the write request is timing out and is not properly being ended after transmitting the 18 bytes of the device descriptor, therefore the host just fills everything with zeroes until it times outs.
This is how I write to the endpoint:
void usbWrite(uint8_t ep, void* data, uint8_t len) {
USB_OTG_INEndpointTypeDef* endpoint = usbEpin(ep);
volatile uint32_t* fifo = usbEpFifo(ep);
uint16_t wordLen = (len + 3) >> 2;
if (wordLen > endpoint->DTXFSTS) {
return;
}
if ((ep != 0) && (endpoint->DIEPCTL & USB_OTG_DIEPCTL_EPENA)) {
return;
}
endpoint->DIEPTSIZ = (1 << USB_OTG_DIEPTSIZ_PKTCNT_Pos) | len;
endpoint->DIEPCTL &= ~USB_OTG_DIEPCTL_STALL;
endpoint->DIEPCTL |= USB_OTG_DIEPCTL_EPENA | USB_OTG_DIEPCTL_CNAK;
usbRawWrite(fifo, data, len);
}
void usbRawWrite(volatile uint32_t* fifo, void* data, uint8_t len) {
uint32_t fifoWord;
uint32_t* buffer = (uint32_t*)data;
uint8_t remains = len;
for (uint8_t idx = 0; idx < len; idx += 4, remains -= 4, buffer++) {
switch (remains) {
case 0:
break;
case 1:
fifoWord = *buffer & 0xFF;
*fifo = fifoWord;
break;
case 2:
fifoWord = *buffer & 0xFFFF;
*fifo = fifoWord;
break;
case 3:
fifoWord = *buffer & 0xFFFFFF;
*fifo = fifoWord;
break;
default:
*fifo = *buffer;
break;
}
}
And here is how I am setting up the control endpoints and FIFOS:
#define USB_FIFO_EP0_SZ 0x20
#define MPSIZ_64B 0x00
OTG->GRXFSIZ = USB_FIFO_EP0_SZ;
OTG->DIEPTXF0_HNPTXFSIZ = (USB_FIFO_EP0_SZ << 16) | USB_FIFO_EP0_SZ;
USB_OTG_INEndpointTypeDef* in0 = usbEpin(0);
in0->DIEPCTL |= MPSIZ_64B | USB_OTG_DIEPCTL_SNAK;
USB_OTG_OUTEndpointTypeDef* out0 = usbEpout(0);
out0->DOEPTSIZ = (1 << USB_OTG_DOEPTSIZ_STUPCNT_Pos) | (1 << USB_OTG_DOEPTSIZ_PKTCNT_Pos) | 64;
out0->DOEPCTL |= MPSIZ_64B | USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA;
Any idea as to what I am doing wrong? It is not a board issue as Cube auto generated code works (at least it enumerates)
Solved! Go to Solution.
2023-07-13 12:07 AM
Event 5. is not the Status stage; it's still Setup stage.
At the signaling level, Setup stage consists of three packets: Setup token, Data (8 byts), and ACK (from Device (a.k.a. Function in USB2.0) to Host) - see USB 2.0, 8.5.3 Control Transfers.
.What happens in the OTG machine (which consists of the "USB core", that is the part facing the bus which contains the USB transactions decoding machine; and "APB interface" which is what you as a programmer see; these run at potentially two different clocks (on STM32 it happens only in the HS interface when using external PHY) with a bunch of resynchronizers and FIFO-like interfaces i between them):
JW
2023-07-09 06:37 AM
How do you handle the end-of-Tx event (DIEPINT.XFRC=1)? The SETUP IN sequence ends with Status Stage which is an OUT packet, and the you have to prepare the device to receive it (otherwise it NAKs thus host eventually times out).
"Leftover Capture Data" is IMO a red herring.
Btw. you are not supposed to clear the STALL bit in DIEPCTL manually.
JW
2023-07-12 06:19 PM
Interesting, I don't handle it, so I guess I need to look into that. During debug sessions the flow looked something like this:
Step 5 was unexpected for me but I guess it is the Status Stage you mention. I am looking at other USB implementations and for example, on dmitrystu/libusb_stm32 a fake read is performed, as far as I can see the only thing done is GRXSTSP is popped which I already do on every read. Should I do something else? This is how I read when RXFLVL is triggered:
static void usbRead() {
uint32_t readStatus = OTG->GRXSTSP;
uint8_t epNum = readStatus & 0xF;
uint16_t byteCount = (readStatus >> 4) & 0x7FF;
if (byteCount == 0) {
return;
}
switch ((readStatus & USB_OTG_GRXSTSP_PKTSTS) >> 17) {
case PKTSTS_OUT_RX:
break;
case PKTSTS_SETUP_RX:
if (epNum != 0) {
return;
}
usbReadSetup(usbEpFifo(0), &controlRequest);
usbProcessSetup(&controlRequest);
break;
}
}
2023-07-12 09:48 PM
The value of GRXSTSP on read 5 basically denotes a SETUP transaction completed (BCNT = 0 and PKTSTS = 0100)
2023-07-13 12:07 AM
Event 5. is not the Status stage; it's still Setup stage.
At the signaling level, Setup stage consists of three packets: Setup token, Data (8 byts), and ACK (from Device (a.k.a. Function in USB2.0) to Host) - see USB 2.0, 8.5.3 Control Transfers.
.What happens in the OTG machine (which consists of the "USB core", that is the part facing the bus which contains the USB transactions decoding machine; and "APB interface" which is what you as a programmer see; these run at potentially two different clocks (on STM32 it happens only in the HS interface when using external PHY) with a bunch of resynchronizers and FIFO-like interfaces i between them):
JW
2023-07-14 10:17 AM - edited 2023-07-14 10:19 AM
Thanks for the explanation. I have worked on it and added handlers for DOEPINT.STUP and DIEPINT.XFRC (endpoints hardcoded for the time being):
static void usbEpInterruptOut() {
USB_OTG_OUTEndpointTypeDef *ep = usbEpout(0);
if (ep->DOEPINT & USB_OTG_DOEPINT_STUP) {
usbProcessSetup(&controlRequest);
}
ep->DOEPINT = ep->DOEPINT;
}
static void usbEpInterruptIn() {
USB_OTG_INEndpointTypeDef *ep = usbEpin(0);
USB_OTG_OUTEndpointTypeDef *epOut = usbEpout(0);
if (ep->DIEPINT & USB_OTG_DIEPINT_XFRC) {
// Write complete. Clear NAK and enable to receive STATUS OUT
epOut->DOEPCTL |= USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA;
epOut->DOEPTSIZ |= (1 << USB_OTG_DOEPTSIZ_PKTCNT_Pos);
}
ep->DIEPINT = ep->DIEPINT;
}
Also as you recommended I no longer perform any writes on my read routine. I simply read the FIFO (if required) or pop the mini FIFO (GRXSTSP). However on Wireshark everything is looking the same (I can see the descriptor but it seems it still times out).
Here is the flow that now happens upon USB being connected:
Breakpoint 1, usbReset () at ../lib/usb.c:219
219 OTGD->DCFG &= ~USB_OTG_DCFG_DAD;
halted: PC: 0x08001116
Breakpoint 2, usbAfterReset () at ../lib/usb.c:224
224 static void usbAfterReset(void) {
halted: PC: 0x0800114e
Breakpoint 3, usbRead () at ../lib/usb.c:142
142 static void usbRead() {
GRXSTSR: 18c0080
halted: PC: 0x080012fe
Breakpoint 3, usbRead () at ../lib/usb.c:142
142 static void usbRead() {
GRXSTSR: 1a80000
halted: PC: 0x080012fe
Breakpoint 4, usbWrite (ep=ep@entry=0 '\000', data=data@entry=0x20000008 <stellDeviceDescriptor>, len=len@entry=18 '\022') at ../lib/usb.c:49
49 void usbWrite(uint8_t ep, void* data, uint8_t len) {
halted: PC: 0x08001216
Breakpoint 5, usbEpInterruptIn () at ../lib/usb.c:173
173 epOut->DOEPCTL |= USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA;
halted: PC: 0x0800119e
Breakpoint 1, usbReset () at ../lib/usb.c:219
219 OTGD->DCFG &= ~USB_OTG_DCFG_DAD;
halted: PC: 0x08001116
As it can be seen, I successfully reach the DIEPINT.XFRC interrupt, but apparently I am not setting up EP0 to receive OUT packages correctly. Do you have any ideas as to what I may be doing wrong? I looked at RM0638 but can't find a concrete example or section where it states how to prepare endpoint to receive OUT packets. The RESET I get after XFRC is not inmediate, but takes around 5 seconds after the IN transfer completes.
2023-07-15 12:36 AM - edited 2023-07-15 12:38 AM
Almost there.
If you did everything correctly, you should see again breakpoint 3 for the OUT token (I'm not sure if you'll see it twice, once with zero length data packet and second time upon the ACK/completion, or just the latter one), and then a OUT XFCR interrupt.
As you don't see those, you did something incorrectly. I'd try to swap
epOut->DOEPTSIZ |= (1 << USB_OTG_DOEPTSIZ_PKTCNT_Pos);
epOut->DOEPCTL |= USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA;
JW
2023-07-16 12:19 AM
Thanks for your continuous support. You helped me a lot, specially in better understanding the micro's internal USB core flow. I was debugging a little bit more and realized that EP0.DOEPINT indicated a NAK was being sent, and an OUT package was received while EP0 was disabled, this interrupt was triggered after the IN write, this all matched your initial analysis.
Going through the datasheet once again, I found the following under Programming model - Generic non-isochronous OUT data transfers:
If there is no space in the receive FIFO, isochronous or non-isochronous data
packets are ignored and not written to the receive FIFO. Additionally, non-
isochronous OUT tokens receive a NAK handshake reply.
So I changed my RX Fifo size from 0x20 to 0x30 and it worked :downcast_face_with_sweat:. The descriptor packet no longer times out on Wirehsark, appears immediately (0.001232 seconds after request) and with the correct size (18 bytes instead of 64).
For future reference for anyone else going through this. Here is the complete code which worked for me. Far from perfect but at least gets you started with correctly providing GET_DESCRIPTOR requests.
https://github.com/crazybolillo/stell/commit/5fe772f595b7cbd70d1f89e3bd92ee4b2608b33d
Now I only need to implement GET_CONFIGURATION, SET_CONFIGURATION and a bunch of CDC stuff :face_with_tears_of_joy:.
Thanks again!