It’s theory time !
This is the “eat your veggies” part of learning to use ST’s USB libraries. If you’re new to USB programming you absolutely need to read and understand this entire page. If you don’t, you won’t understand half of what I’m talking about when I start getting into specifics. If you’re a “TL;DR” kind of person, just give up right now : you’re trying to win Mister Olympia without lifting weights.
If you’ve already programmed USB firmware and know what an endpoint is, you can skip this page. I’m basically going to take official USB specifications and translate some of the important points into common English.
If you think you can handle the official spec, you can find it there : https://www.usb.org/document-library/usb-20-specification
1.1. Peeling the Onion
So here’s the thing : USB is a total mess. There’s no denying it. It’s the “U” part that’s really to blame : you can’t ever make anything that’s truly universal. When you try, stuff like USB happens. Thus, forget the idea of using “educated guesses” or “intuition” to picture how things work. The crazy starts right there in the name.
Despite what Universal Serial Bus might imply, USB is not a bus : all connections are strictly point-to-point, as you’ve no doubt experienced since there’s no such thing as a “Y” USB cable. The actual topology of a USB “bus” looks like this :
USB also has a history of tacking-on new features like a schoolgirl who’s just discovered the awesome power of the hot glue gun : first there was USB 1, with “low speed” and “full speed” modes; and then it wasn’t enough, so USB 2.0 added “high speed” just to confuse people. I mean, who can honestly guess that “full speed” is actually slower than “high speed” ?
Then phone manufacturers wanted to fit both USB host and device capabilities on the same connector (as part of their lifelong quest to eliminate the headphone jack), so they added a pin to some USB connectors and called it USB OTG… and right then is when you had to start looking into which type of USB cable you needed depending on which devices you were connecting together. Doesn’t sound universal anymore, now does it ?
Then we got to USB 3.0, which added “super speed” because why not, at this point ? It’s like whoever wrote those specs also makes money on YouTube posting 50-minute long videos on how to use USB correctly. Of course, the laws of physics being what they are, they couldn’t fit five gigabits per second through connectors and cables originally meant for signals 400 times slower… so they added five more pins and dedicated them to a completely separate full-duplex channel :
Yup, they basically Frankensteined the existing connectors in an attempt to still keep some backward compatibility and hope the term “universal” could still be loosely applied to their creature.
Now we’re in 2020. There’s yet another type of USB connector : Type C, which has even more pins. It can also carry up to 20 volts of power, replace a Display Port cable, or even some PCI Express, but some brands use it for Thunderbolt… we’re at a point where you can’t really know what’s going to happen when you plug two new USB devices together anymore. I’ve started taking bets on the outcome like those are boxing fights, it’s one hell of way to make money on the side.
Now the good news is, all this recursive tacking-on of new protocols and signals onto previous versions of USB means that USB is like an onion : you can peel off the outer layers and keep only the core functionality.
On this website, and in the scope of STM32 microcontrollers, I’m only going to discuss USB 2.0 device design and programming. Maybe in the future I will explore the host functionality, but I have no need for it now. I won’t explore OTG. As for USB 3.0, it doesn’t exist on STM32 chips at this time.
1.2. USB 2.0 Physical Interface
USB on microcontrollers is typically a peripheral block like any other (UART, timer, ADC…)
That means a microcontroller may have zero, one or more USB controllers inside, and those controllers may implement the full USB 2.0 spec or just a subset of it (for example, only the device capability, or only the Low Speed mode).
While it is in theory possible to bit-bang a USB interface on a microcontroller devoid of a USB controller, you will never be doing that. Besides the enormous complexity of the task, the asynchronous programming it requires makes the endeavor utterly pointless. As for doing it just to impress the ladies, come on, we both know you’ll have more luck pumping iron.
This gets us to the first point to address : USB availability on your microcontroller.
Most, but not all STM32 devices carry at least one USB controller that can be used to implement a USB host and/or device.
Furthermore, the smaller STM32 MCU’s only provide a Full Speed interface. “Only” might seem weird when associated to the word “full”, but that’s because the term dates back to the early days of USB 1.1 :
- USB Low Speed is a 1.5 Mbit/s mode suitable for devices like keyboards. You’re not going to produce a megabit of data per second just typing on a keyboard unless you were bitten by a radioactive secretary.
- USB Full Speed is a 12 Mbit/s mode. Seems very slow by today’s standards but back when it was invented, we were still using floppy disks with only 1.44 MB of storage capacity. USB Flash drives wouldn’t exist for a few more years.
- USB High Speed is a 480 Mbit/s mode that got introduced with USB 2.0 and finally made external hard drives a viable proposition.
As with the absence of USB 3.x support, the main reason for limiting the slowest STM32 to Full Speed is their limited processing power. 480 Mbit/s is equivalent to maybe 48 MB/s once you strip protocol overhead. This just doesn’t make any sense on a 72 MHz STM32F103.
If you need to select an STM32 microcontroller to use USB with, how do you know which ones have a Full Speed or High Speed interface ? There’s a simple rule of thumb that’s always been true in my experience : any STM32 slower than 100 MHz will only have Full Speed USB. Also, STM32 with an Ethernet MAC will also have USB High Speed.
1.2.2. The Connectors
The USB 2.0 connectors are very simple. If you’ve ever looked down one of those big old USB plugs you’ve noticed there are only four pins in there, and that one pair is shorter than the other.
The two long pins carry 5 V electrical power. The positive is called VBUS, the ground is GND.
The two short pins carry a differential pair for the data. Thus, the pins are called D+ and D-.
The power pins are longer so that whenever you plug a USB device, that device receives power before the data connection is established. Likewise, when you remove a USB device, the data connection is severed before the power. Among other things, this gives the device enough time to initialize to a state where it won’t send gibberish up the USB cable.
Unless you’re making your own board, you don’t need to know the exact pinout of the USB connectors. You can easily Google that information, and there are just too many different connectors for me to make a comprehensive description of each of them. I’m long-winded as it is.
You may have noticed a fifth pin in the smaller USB 2.0 connectors (Mini and Micro USB). That’s the ID pin, which is used in OTG (“On The Go”) scenarios. As I’ve said before, I’m not going to cover OTG in the foreseeable future. Just know that this pin should be left unconnected if you don’t use it.
STM32 chips equipped with a Full Speed interface carry all the electronics needed to implement a USB interface. All you need to do is connect the right pins on the MCU to the right pins on the USB connector and you’re good to go. At 12 Mbit/s you don’t even really need to pay close attention to the impedance of the data pair. You will also need to add a 1.5 K pull-up on the D+ line to force the host to detect your STM32 and recognize it as a Full Speed device.
STM32 equipped with a High Speed interface aren’t so lucky : they require an external ULPI PHY chip. They also require considerably more attention to detail when routing your board. This ain’t the kind of interface that’ll work on breadboards and over loose wires.
I will be focusing on Full Speed devices because it’s supported by all USB-capable STM32 chips and also because its 12 Mbit/s data rate is adequate for almost any applications of STM32 microcontrollers. That being said, the information on this page will still be useful to High Speed users.
1.2.3. USB Electrical Power
Once plugged into a host, a USB 2.0 device is allowed to draw as much as 100 mA from the USB host. This limit rises to 500 mA once the device has been configured, provided the device asks the host for it.
USB 2.0 devices are thus allowed to draw as much as 500 mA from a USB host, at 5 V. That’s 2.5 W total. If you’re using a bus-powered hub to which several bus-powered devices are attached, then their total power consumption must be under 500 mA. Bad Things â„¢ happen when you exceed that. Even if your PC has a fancy USB port surrounded by crazy logos that looks like it should be able to provide more than 500 mA, additional power output needs to be negotiated for using specific protocols such as USB Power Delivery. By default, 500 mA is your maximum.
Luckily, you don’t have to power your device from the USB port. That’s just a convenience for simple devices like keyboards and mice. It’s perfectly fine to leave the USB VBUS line unconnected on your board if you’re using some other source of power.
It is definitely not fine to tie VBUS to your board’s own 5 V power source : a device feeding power into a host is considered extremely rude. In some cultures, it’s like belching or eating with your fingers. Mostly, it’s going to mess up your host, your device, possibly both. Magic smoke may be released, the fire brigade may get involved, and you may end-up with shame and PTSD that you will carry to your grave.
I just realized you might have no idea how much power an STM32 board might need. To give you a sense of scale, an STM32F103 running at 72 MHz displaying data on an OLED display draws less than 50 mA when powered from USB.
1.2.4. Dual-Power Designs
That being said, there is at least one type of device that requires both an external power supply and USB power : battery-powered devices. Like your phone :
- It is normally powered by its own internal battery
- But if you connect it to your PC, it’ll use its VBUS to power itself and charge the battery
There is a simple way to do that. Just use a couple of Schottky diodes to protect VBUS and your board’s 5V rail, as in this example :
Schottky diodes have a low forward voltage, which means your 5 V will only be lowered to 4.5 V : that will still be enough for powering the STM32 through a low drop-out (LDO) 3.3 V regulator as well as for charging a Lithium battery since their voltage should never exceed 4.2 V. Do keep in mind that a diode’s forward voltage increases with how much current passes through it. If this is a concern, simply oversize your diodes until it gives you the best performance even under 500 mA.
1.3. USB Protocol Concepts
Note that I’m only touching on concepts you’re likely to have to come across while using ST’s USB libraries. Even good old USB 2.0 is way too complex to be described in detail on a single webpage.
1.3.1. Hosts and Devices
USB is a strict master-slave protocol, much like IÂ²C : the host is the master, the devices are the slaves, and only the host can initiate communications.
The connection between host and device can be direct (a USB cable) or go through USB hubs. For the purposes of STM32 device programming, hubs are transparent. Meaning, you don’t need to worry about any other devices connected to the same USB hub(s) as your STM32 USB device.
I can’t stress enough the master-slave aspect of USB : one of its implications is that sending data from device (STM32) to host actually requires placing that data into a buffer and waiting for the host to read it. This means (among other things) that your device-to-host bandwidth will be very dependent on how much of your STM32’s RAM you can allocate to USB buffers.
A lot of things happen behind the scenes whenever you connect a USB device to a host. The first one is enumeration. This covers three vital steps to making the device available to software running on the host :
- Detecting that a new USB device has been plugged in.
- Identifying that device.
- Loading drivers for that device.
For an STM32 to be enumerated correctly as a USB device, it needs to be running software that will respond correctly to the enumeration process. From ten kilometers away, that means answering a few questions the host has, such as “how fast can you talk ?”, “what’s your name ?”, “what can you do ?” and “who made you ?”
From a bit closer, the host and device exchange standard messages, especially descriptors.
Good news : ST’s USB libraries will take care of all that for you. You can specify your own answers to most of those questions directly into STM32CubeIDE, in its graphical MCU configuration tool. I’ll go into the details later on, when things get real.
Once the host has received your STM32 firmware’s descriptors, it has all the information it needs in order to load the appropriate driver. That’s essentially determined by the vendor ID and product ID that your firmware returned to the host. In plain English : if for some reason you’ve written a driver for your STM32 project, then you can use those parameters to make sure than when you plug in your project, your PC will load the correct driver for that project.
After the host’s operating system has loaded the driver for your STM32 board, applications on the host are free to access the board over USB.
Just to keep you from going crazy wondering : you most likely won’t need to write a device driver for your STM32 project. You’ll understand why when we get to device classes. Patience, young grasshopper.
Note : It is possible to force the host to “re-enumerate” your STM32 by software. Doing so will require additional electronics between the STM32 and your board’s USB socket. For an example of how to achieve this, I’ll refer you to my work on the STM32F1-based Maple Mini module which you can find by clicking here.
1.3.3. Transactions and Packets
This information will be transparent to you if you stick to ST’s USB libraries. Nevertheless, it’s important to know about it. It will inform your coding, and it’ll also come in handy when you try to observe USB traffic using Wireshark, for example.
RS232 lets you just pump bytes onto the cable without any protocol. USB is more complex than that. Similar to Ethernet, it has a stack of protocols you need to be aware of.
Closest to the cable are packets. There are three types of packet that are used to form transactions :
- Token packet
- Data packet (optional)
- Handshake packet
- Special packets cover everything that doesn’t fit in a transaction
Since the host is the only side of a USB cable from which transactions can be started, token packets can only come from a host. A token tells a specific device (using its address) what kind of transfer will be performed next :
- IN : the host will read information from the device
- OUT : the host will write information to the device
- SETUP : the host will perform a control transfer
The data packet comes next. Depending on the token packet, it’ll either go from host to device or the opposite. I trust you’re old enough to figure out which is which. Data packets in USB Full Speed can carry up to 1023 bytes.
Finally, whichever end of the transaction receives the data packet must acknowledge its reception. The two most common handshake packets you’ll see are ACK and NAK, for “acknowledge”, meaning the data was received and correct; and “not acknowledge”, meaning the data was either not received or the CRC failed.
At the risk of sounding like a broken record, USB is strictly master-slave. I’ve said it before, that means you need buffers on your STM32 to hold both incoming and outgoing USB traffic because a device can’t just send data to the host whenever it wants.
This brings us to the concept of endpoint. An endpoint can be a source or sink of data. Endpoints exist only on the device side of a USB connection, since the host controls all USB traffic and therefore has no need for such a buffering mechanism.
An endpoint is essentially a buffer in which you’ll write data that’s supposed to go to the host, and read data that comes from the host. On the firmware side of the endpoint, where your code lives, you can access each endpoint’s contents whenever you need… but on the USB side, only the host can read and write data.
There are two types of endpoint, named in a host-centric way :
- IN : endpoint for sending data into the host.
- OUT : endpoint for receiving data coming out of the host.
A High Speed device can have up to 16 pairs of IN/OUT endpoints, and the USB protocol allows the host to address each endpoint on a device. This allows for separating the traffic of each function of a USB device. Each device must have at least one endpoint pair, EP0, because that endpoint is used during enumeration. It’s also used for all control and status requests as long as the device is connected.
Good news : the ST libraries handle end-point for you. Reading this still wasn’t a waste of your precious time because those libraries will require you to setup the buffers for those endpoints. So it’s best to understand how they’ll be used.
1.3.5. Device Classes
The one great promise of USB that has been somewhat fulfilled is the ability to plug-and-play. This was hyped so much it got rapidly abbreviated into “PnP” because everyone was talking about it.
To cut a long story short, PnP on USB was achieved primarily through the concept of device classes. A device class defines a set of features common to all devices of that class. For example, a mouse :
- Must be capable of returning X-Y coordinates at regular intervals.
- Must be capable of returning the state (pressed or released) of all its buttons.
Thus, a generic mouse driver could be written that expects this specific type of data, presented in a specific way. If you program the firmware of your mouse to generate and transmit such data, it can identify itself as a mouse during enumeration. Then the host can load the generic mouse driver and suddenly your mouse works. You may still install a driver specific to your mouse, especially if it has functionality beyond what is defined in the device class, but that’s no longer a requirement.
The list of device classes resides at https://www.usb.org/defined-class-codes
The ST USB libraries implement some of those device classes. The list is basically limited to the most common use cases for USB on a microcontroller :
- Audio Device. You basic USB-enabled microphone or headset.
- Communication Device. That’s your basic COM port over USB, for example.
- DFU. Download Firmware Update. For people who’re in such a hurry to bring their device to market that they can’t be bothered to get their firmware right first. You know who you are.
- HID. Human Interface Device. For plugging USB cables into the necks of Neo, Trinity and Morpheus. Also for keyboards, mice and joysticks.
- Mass Storage. Lets your STM32 behave like a USB Flash stick.
Good news : if your STM32 USB application fits one or several of those classes, then you won’t have to write any driver for your host’s operating system. Those are classes that Windows and Linux (including Macs and Raspberry Pi’s) know how to use natively.
If your application seems like it won’t fit in any of those classes, I strongly suggest you get creative with the interpretation of what each class is for. It sure beats writing a driver from scratch.
1.4. Bottom Line
Don’t get too worried if you’ve read this far and still have trouble intuiting USB. This page is just to introduce you to core concepts and keywords that you are guaranteed to see everywhere as soon as you start working on a USB project. It’s also here to give you a first idea of where the ST libraries will come in and help you. Come back here once in a while when you’re not sure what fits where, and don’t ever hesitate to Google. You can read the USB 2.0 specification too, but it’s 650 pages long and not exactly a page-turner.
Now we can move on to actually using ST’s USB libraries.