Of ancient runes,
a cursed mess -
It's everywhere. It replaced many different connectors of the past - sometimes by just being able to emulate them, as is the case with serial ports. Some devices use it just for the power. But it's still an obtuse piece of design-by-committee which is difficult to implement right even if you've been doing this sort of thing for a long time indeed. Just ask the Raspberry Pi people!
I'm going to skip over a lot of the USB spec itself, because it's huge. Indeed, even a website calling itself USB Made Simple comes up to seven parts, not including extra material. I would try to skim through the USB 2.0 standard but apparently it's hosted by someone who doesn't understand how to set up TLS in nginx:
No, I'm going to focus only on the bits that I found I needed in order to get my device to work. So starting from the electricals, a compliant-ish cable will have the following wires inside of it:
- Red for VBUS (nominal 5V), as expected
- Black for GND, also as expected
- Green for D+
- White for D-
Everything is wrapped in a thin metallised film which acts as a shield against RF interference, which is a good job that I slit everything open and started messing about with the wires. Here's one of my breakout boards I've made. I added a Schottky diode to prevent stupid things from happening, like the computer trying push current to itself.
For a brief mention of the theory - USB is a single-master, packet-switched, tiered star network where the host controls a number of peripherals that each have a hardware address (ish) and a network address (ish). The hardware address is composed of the Vendor ID and Product ID (VID and PID). If you want your device to be fully legit, you'll probably need to buy yourself a VID which is kinda expensive - either $5000 per year if you want to be a member of the USB Implementors Forum or $6000 one off purchase (source), and there's still lots of hoops you have to jump through. Or for fewer hoops, most of them based around your product needing to be open source, you can get yourself a PID from pid.codes for free. And of course just for testing, you can use any VID/PID you'd like but remember this is not compliant with anything.
There's also USB OTG ("On The Go") which allows connections between two embedded devices, but I don't think I ever saw it used.
There are actually two different USB implementations in the STM32 range: a USB Device core available on lower tier MCUs like F0-F3, and USB OTG cores available on higher tier MCUs like F4 and up. To make matters worse, there are two flavours of the USB OTG core. There's the Full Speed version which allows for up to 12Mbit/s data rates with a transceiver (or PHY) built into the MCU silicon, and a High Speed version which allows for up to 480 Mbit/s rates but requires an external transceiver, connected via a parallel interface to the MCU. Or, the High Speed core can also use the built-in Full Speed transceiver to achieve 12Mbit/s. Since both cores support OTG they automatically also support USB 2.0. Slightly confused? Yeah, me too.
It should also be noted that very similar USB OTG cores can be found in RISC-V MCUs made by the company GigaDevice which tries to compete with ST by just straight up cloning their chips. The reason is that they were originally designed by a company called Synopsys.
Is there Rust support for these cores? Kinda. There is a project aiming to create a library for writing USB peripherals with the Synopsys USB OTG cores, which would support both the STM32F4 and up, as well as the weird GigaDevice parts. It can be found here:
To use the USB OTG FS peripheral (plenty fast enough for a keyboard and serial port), you need to route D- to PA11 and D+ to PA12. You should also connect the USB 5V to the E5V pin on the Nucleo board, and switch the jumper for where the board will take its 5V from from U5V to E5V (Note that the Nucleo F446ZE board has USB OTG FS broken out already into a connector). There are some requirements as to how fast a particular clock needs to be. Also, you need to run a forked version of the HAL crate because it has some modifications that are required to support the USB functionality. This is an edge so bleeding, all the local vampires are trying to get in on the action.
As luck would have it, there is some example code for the F446 to borrow from. However, the luck ends here.
From here onwards, it's all pain.
It's surprisingly difficult to get any low level information about USB connections from Windows, even if you go through the trouble of installing USBPcap - a useful piece of software for capturing connections between functional devices. Sometimes you won't even get Windows to grace you with an error message - the device you are connecting will just get power and that's about it. Furthermore, it seems like most other example libraries only support the OTG FS peripheral. I tried that with Mbed, a C++ based environment from Arm, and it does actually work on the Nucleo F446RE board, provided that you connect the pins right - I had a dummy USB keyboard enumerating in no time. The Rust library, however, gave me nothing.
I got stuck. The code compiled but nothing actually happened. And at the time of writing this I didn't have any test gear handy so I couldn't stick a logic analyser on the USB lines. I did open a bug on the Synopsys driver crate, and then started properly looking at the code inside, as well as other crates that together create some sort of support for the F446.
I also started looking at C/C++ implementations of the low-level drivers in an attempt to fix the bug, and from there I noticed that there are actually some inconsistencies between the C header files and support libraries that ST provides, and the System View Description XML files that it also has to provide as per ARM licence terms. Namely, the SVD files can be, shall we say, utter garbage, with mislabelled or even missing registers. For instance one of the registers for the "IN endpoint 0" - a control endpoint reserved by the USB spec - that should be called
FS_DIEPTXF0 is actually called
FS_GNPTXFSIZ in the SVD, and therefore, in all the crates generated from the SVD.
Oh and two other endpoint registers are missing, which means that on this microcontroller that has 6 endpoints (1 control + 5 for application use), only 4 are actually usable from Rust. I have filed a PR with the appropriate base crate to add those endpoints back, however even after applying those changes locally didn't fix my issues. So I need to think about several different strategies to deal with this. First, however, I'd really need to sit down and verify that the generated register maps match what is in the Reference Manual, in case there are more subtle errors within the SVD files.
- Try to fix the synopsys-usb-otg crate.
- Write my own Rust USB drivers using a model much closer to what the Reference Manual offers, hoping that this would somehow work better than the existing attempt.
- Steal the low-level drivers from somewhere else - either the official C/C++ HAL provided by ST or something like the ChibiOS HAL, which do support USB on the F446, and then write a crate that wraps it and exposes it to Rust world.
- Abandon the idea of using Rust for everything, use an RTOS, write application code in Rust.
All these options seem kinda daunting. The last one would be perhaps the most productive - after all, I would get a proper HAL, an actual RTOS, and community support from other users of said RTOS. However, it means using a large chunk of code that doesn't have the safety guarantees of Rust. I can minimise that chunk by only using an external USB HAL, which is probably the second most productive, and I may even be able to fit it into the usb-device model allowing me to write my actual keyboard implementation in Rust. I definitely don't want to be writing my own USB drivers given that I don't fully know what I'm doing yet, and I think the preferred option is fixing
synopsys-usb-otg, which would also hopefully help other people too. I will however need to buy a logic analyser capable of monitoring and decoding USB Full Speed communications and spend some time learning the usb-device model preferred in the world of embedded Rust.
It's a shame that the F446 implementation of USB is getting in the way, because from what I've seen the device side of the protocol is fairly reasonable. And not having proper test gear or much in the way of debugging capabilities doesn't really help either - sure, I can use on-chip debugging to set breakpoints in my code but I still have no visibility into what the USB core itself is doing. I can see that the
enable() routine runs but then nothing really happens, and the OS doesn't even see the device.
It also seems that Rust isn't quite as ready for embedded use as I had thought - sure, most of the stuff is there for a few lines of microcontrollers, but it's all third party support, and from what I've seen no manufacturer of MCUs has looked at supporting Rust natively. The lack of support for USB is also very annoying considering that quite a lot of what Rust has to offer would work very well on USB devices. And it seems that nobody has really invested in making a credible USB Host implementation for embedded Rust either.
On the other hand, maybe it would be enough to use Rust for application code and use a C-based library here and there?
What do you think?