ST USB

5 Apr 2017

I mentioned in my post on the MIDI Guerrilla that I’d write a post about the ST USB stack, as it’s not the simplest thing to get your head around.

A bit about USB

There’s a few things you need to know about USB or nothing is going to make sense. Ideally you’d read the USB spec, but if you haven’t got time then here’s the important bits. We’re only going to talk about full-speed devices here, high-speed is slightly different and the STM32F1 series doesn’t support it anyway (the F4 range does however).

As a first note, all directions mentioned are usually from the hosts point of view, as it’s the one driving everything. So an OUT endpoint is sending from the host to the device and an IN endpoint is device to host. This can be confusing when you’re writing a device as you have to reverse everything.

Timing and frames

One thing to always keep in mind is that everything is host driven, if you, as a device, want to send a message you stick it in a buffer and wait for your turn to send it back.

Bandwidth on the bus is split into frames, with each lasting one millisecond. Frames get split into transactions with some space being reserved for certain types of pipes, more on that in the next section. From the devices point of view, if someone’s kindly provided most of the USB stack for you, all you do is stick data in a buffer for a given pipe and it should be picked up by the host. You can find out when stuff’s been sent, but there’s no way of making it be sent immediately.

Endpoints and pipes

A single device can have multiple endpoints, with each being a different type. Endpoint 0, both IN and OUT, is the Default Control Pipe (DCP) which every device must have and is used for the host to control basic USB features and to work out exactly what is plugged in.

Generally only endpoint 0 is a control pipe, the other ones will one of the next three types:

Descriptors

Once some basic setup for a device has been performed the next thing the host needs to do is work out what a device is and what it can do. It does this by sending commands to the DCP and asking it to describe itself. There’s three main descriptors that every device requires:

There’s two main ways of defining a USB device:

Most devices fall into the interface defined type as these allows them to work with standard class drivers, which should work everywhere without you needing to write a driver yourself. When one of these is plugged in the host looks at all the interfaces and uses the ones it understands.

ST’s device library

That’s enough about USB to have a reasonable idea of what’s going on. I'm now going to go through the odd bits of how you use the library.

First it’s in several parts, some you just include into your project, and some you copy and modify a bit.

Assuming you’re using some form of build system you should compile all the files in CUBEPATH/Middlewares/ST/STM32_USB_Device_Library/Core/Src and make sure that CUBEPATH/Middlewares/ST/STM32_USB_Device_Library/Core/Inc is in your include path.

Exclude the template files in these directories as these are supposed to be copied into your project and customized, but if you’re using one of the standard device classes there’s an easier way, described in the next section.

The core files contain the platform independent basic device library that calls into the rest of the USB code, everything that the same across all USB devices and all ST device families.

HAL_PCD and HAL_LL

This is the first weird part of the ST library that might catch you out. The USB library in Cube is shared between all the families so there’s a layer that sits between it and the hardware to abstract it. Due to this there’s a chunk of code that just wraps the commands for your hardware in generic functions, these are the HAL_PCD and HAL_LL functions. In the F1 and F4 devices these pretty much just pass the given arguments straight through to the device hal functions.

ST recommend that you take a copy of these from one of their examples and use that as the starting point. It’s only a starting point as some parts need changing depending on the endpoints you need.

What isn’t obvious is that these aren’t in the examples folder for each board (e.g. CUBEPATH/Projects/STM32F103RB-Nucleo/Examples) but under Applications. It doesn’t matter the exact board you copy the example from, as long as it’s for the correct family.

Once you’ve found the file containing all of these you can copy it into your project then check, and modify, HAL_PCD_MspInit, where Pins get enabled (in case you need to use some of the optional pins elsewhere), and HAL_LL_Init where the endpoints are configured.

Which endpoints you need depends on which device class you’re implementing so you might need to look at the other examples ST gives and go from there.

usbd_desc

This can be copied from the device type example and tweaked a little as necessary.

In most cases it just contains the strings that get presented to the host OS and the vendor/product IDs.

usbd_something_interface

If you’re using the ST device classes then you’ll need to copy and modify this as well.

The interface file contains the code that’s app specific to a device class. They tend to contain a structure of type something_ItfTypeDef and an instance of it ending in _fops. This is the callback object where the device and app specific callbacks are defined.

For instance in the CDC device class there’s, CDC_Itf_Init, CDC_Itf_DeInit, CDC_Itf_Control, and CDC_Itf_Receive. You can guess what each of them does, and why they could be different per application.

These are different for every device class, so you’ll need to look at the example given to work out if you need to change it, and at worst read the class implementation.

You own device class?

As this post is already pretty long I’ll leave writing your own device class to another time. I think the best way would be to post my MIDI implementation and try and explain how it all goes together.