November 9, 2023

Let's learn USB! -- Endpoints

Understanding endpoints is a vital aspect of understanding USB.

The following is how I understand them, with specific reference to the STM32F103.

The STM32F103 supports 8 endpoints. An endpoint can have two "flavors" -- IN and OUT. IN or OUT is always with reference to the host controller. Since the STM32F103 is a device and not a host, we write to an IN endpoint and we read from an OUT endpoint. This seems backwards, at least if you are working on a device, but you just have to get used to it.

It generally takes a pair of endpoints (IN and OUT) to create what I like to call a "functionality". A specific example will make this clearer, if it isn't clear already. I want to make the usb on my STM32F103 act as a serial console. I will need an IN endpoint that I can write to and an OUT endpoint that I can read from - hence two endpoints.

The endpoint 0 pair is special and is used for "housekeeping". In particular enumeration is done using endpoint 0 IN and OUT. So I use the endpoint 1 pair for my serial port functionality. and so I will be using 4 endpoints to set up my console serial port and I will have 4 left over (2 extra pairs).

Note that USB is quite happy to support multiple functionalities on the same device. Here are two examples. The STM32F103 had 3 hardware serial ports. We could set up USB to handle all three of these as a fancy 3 headed USB to serial device. This would use 2 endpoints for the control, and the six remaining endpoints (3 pairs) for the serial ports.

As another example, I use Pi Pico (with the RP2040 chip) as a dual function device. I supports a USB to serial function alongside a USB to SWD/JTAG function (like the ST-LINK). If someone was ambitious they could do the same with an STM32F103, since the ST-LINK firmare (or something like it) is open cource.

Endpoints and the F103

Sometimes they claim 8, sometimes they claim 16. This gets confusing. There are 8 endpoint registers, so that is something concrete we can depend on. Each endpoint register can be assigned a number and can control both a Tx and Rx buffer. This sounds a lot like an IN and OUT.

So here is what I think is true. We have 8 endpoint registers and thus we can have 8 of what I like to call an "endpoint pair". This is not official lingo, but it works for me. For the controller, everything starts with the endpoint registers, so if you aren't using all of them, be sure to mark the Tx and Rx status field as NAK or STALL, but in no case VALID. Best of all set the status fields 00 which is DISABLE. If you are only using half of an endpoint pair, zero the status field for the half you aren't using.

From there, you set up BTABLE entries only for those endpoints for which you will ever mark the buffers VALID in the endpoint registers. In our ACM example that we are working with, thanks to papoon,

Somewhat surprisingly, papoon sets up things in the following way. Endpoint 0 is of course a control endpoint with both IN and OUT set up. It uses endpoint 1 along with a tiny (8 byte?) transmit buffer, and endpoint 2 along with a receive buffer. It also sets up endpoint 3 with a transmit buffer.

It leaves the last 4 endpoint registers at their reset state (all zeros). When Tx or Rx status is zero, this is "DISABLE", so this ought to be just fine.

Also Papoon lets the BTABLE grow from the start of PMA ram, while it allocates chunks for buffers from the tail end of PMA ram, checking to be sure than they do not collide.

So the Papoon BTABLE setup is like so:

EP-0 Rx at 1c0 size 64 bytes
EP-0 Tx at 180 size 64 bytes
EP-1 Tx at 178 size  8 bytes
EP-2 Rx at 138 size 64 bytes
EP-3 Tx at 0f8 size 64 bytes
My guess is that EP-3 Tx is used to send ACM data and EP-2 Rx is used to receive it. Ep-1 Tx may be for some out of band signalling or some such. Perhaps this is dictated by the ACM specification.

in usb_dev_cdc_acm.h we see the following -- and the endpoint numbering seems wrong, or at least does not match the above setup! Nobody said that Papoon was mature or even well tested.

static const uint8_t    // have to be public for clients, static descriptors
                            ACM_ENDPOINT             =  2,
                            CDC_ENDPOINT_IN          =  1,
                            CDC_ENDPOINT_OUT         =  3,
                            // have to be public for extern static definition
                            CDC_IN_DATA_SIZE         = 64,
                            CDC_OUT_DATA_SIZE        = CDC_OUT_EP_SIZE,
                            ACM_DATA_SIZE            =  8;

This raises another topic, which is entirely out of place here, and that is why we have ttyUSB and ttyACM on linux systems. I have a page to clarify that issue elsewhere.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org