April 7, 2025

Black Pill boards - F411 USB -- Setup packets - interface requests

What are these interface requests:
Rx setup (8) 0009010000000000
Setup device request
Configured 1-- addressed
Rx setup (8) 2120000000000700
Setup interface request
Tick 1 -- bytes: 0 -- int, sof, xof = 905 831 96
Tick 2 -- bytes: 0 -- int, sof, xof = 1854 1780 96
Tick 3 -- bytes: 0 -- int, sof, xof = 2803 2729 96
Tick 4 -- bytes: 0 -- int, sof, xof = 3752 3678 96
Tick 5 -- bytes: 0 -- int, sof, xof = 4702 4628 96
Tick 6 -- bytes: 0 -- int, sof, xof = 5651 5577 96
Rx setup (8) 2122030000000000
Setup interface request
Rx setup (8) 2122000000000000
Setup interface request
Tick 7 -- bytes: 512 -- int, sof, xof = 6627 6531 120
The above is a short transcript of my debug log showing the very end of enumeration, then later some activity when I send a 512 byte data packet. Ignore the setup device request at the top. I include that simply because it is the last exchange as part of enumeration.

Dig deeper

A couple of weeks have gone by. I have done lots of work, but mostly rearranging and cleaning up the source code so I can study it more easily. Now I am back to digging into these setup requests.

The first message comes from DCD_HandleRxStatusQueueLevel_ISR() in driver/interrupts.c This is called from USBD_OTG_ISR_Handler() in the same file, which is the overall USB interrupt handler. The specific cause of the interrupt is that the "rxstsqlvl" bit is set, which means that something is in the Rx FIFO.

This routine reads a status "header" word from the fifo, then discovers that the STS_SETUP_UPDT bit in that word is set, indicating that a setup packet (of size 8 bytes) follows. It reads this packet into a special buffer "pdev->dev.setup_packet" set aside for setup packets, then considers its work done!

The next message come from CORE_SetupStage() in library/core.c -- how does it know that a setup packet has been read and is waiting to be processed? This routine is called from DCD_HandleOutEP_ISR() in driver/interrupts.c, which is called from USBD_OTG_ISR_Handler() (the overall USB interrupt handler). It is responding now to a different interrupt cause, namely "outepintr". The endpoint interrupt handler has its own status register and it discovers that the "setup" bit is set, indicating that the setup phase is done for a control endpoint and thus that a setup packet is available, so it calls CORE_SetupStage() to process the setup packet.

A setup packet looks like this:

struct  usb_setup_req {
    uint8_t   bmRequest;	0x21
    uint8_t   bRequest;		0x22
    uint16_t  wValue;		0x0003 or 0x0000
    uint16_t  wIndex;		0
    uint16_t  wLength;		0
};
The routine USBD_ParseSetupRequest() is called to move the 8 bytes from the buffer in the pdev area into this structure, performing byte swapping as needed (for the last 3 fields). Then the low 5 bits of the bmRequest field (bmRequest & 0x1f) drives a switch/case and in our case the request is 0x01 for interface. This causes USBD_StdItfReq() to be called -- also in library/core.c

This routine checks the USB status -- if we are not configured, the request is rejected. Index specifies which interface the request is aimed at (here this is interface 0). This is validated, then CLASS_Setup() is called in vcp/cdc.c Now we examine bmRequest again using a mask of 0x60 -- the 0x20 bit indicates that this is a class request, but there is no data (wLength is 0) so we call

VCP_Ctrl(req->bRequest, (uint8_t*)&req->wValue, sizeof(req->wValue));
This routine is in vcp/vcp.c The switch is now on the second byte (bRequest). The value 0x22 is "SET_CONTROL_LINE_STATE" and the code executed is:
linecoding.bitrate = (uint32_t)(Buf[0] | (Buf[1] << 8));
VCP_DTRHIGH = (Buf[0] & 0x1);
VCP_RTSHIGH = (Buf[0] & 0x2)>>1;
It looks to me that (ignoring the linecoding business) the first call with Value "3" sets DTR and RTS high. The second call, with Value "0" sets them low again.

The linecoding information can be set and is also available to be read back, but does nothing whatsoever in the code.

After adding some more messages, I now see this when I send a single 512 byte packet of data to the USB device:

Tick 12 -- bytes: 0 -- int, sof, xof = 11356 11282 96
Rx setup (8) 2122030000000000
Setup interface request
DTR/RTS set: 3
zero length status
Rx setup (8) 2122000000000000
Setup interface request
DTR/RTS set: 0
zero length status
Tick 13 -- bytes: 512 -- int, sof, xof = 12332 12236 120

What use is made of these DTR and RTS flags?

We can see this code in vcp/vcp.c
volatile uint8_t VCP_DTRHIGH = 0;
volatile uint8_t VCP_RTSHIGH = 0;

uint8_t VCPGetDTR(void) { return VCP_DTRHIGH; }
uint8_t VCPGetRTS(void) { return VCP_RTSHIGH; }
So, we have two variables (which ought to be static) and a pair of accessor routines which are currently never used.

To explain DTR and RTS we need to go back in time. Consider at computer and a modem. The computer is called DTE (data terminal equipment) for this scenario and is responsible for control of the DTR and RTS signals. The modem is called DCE (data communication equipment) and is expected to respond to these signals.

Our USB device acts as the DCE here. When it sees DTR (data terminal ready), it knows that the computer (host) is ready to communicate. It would assert DSR (data set ready) to indicate that it is also ready, but I see nothing dealing with that in our code.

The DTE sends the RTS signal (request to send) and expects to see CTS (clear to send) as a response. This is good old hardware handshaking, but again I see nothing with respect to CTS in our code.

Conclusion

We could use the state of the DTR and/or RTS flags to know when we actually have a connection, and this could be quite useful.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org