USB on STM32F10x µCs Mar 2016
Every µC from the STM32F10x family has hardware built-in to support USB. The earlier (i.e. smaller) STM32F103’s have a more limited implementation that more recent models. There are some really strange design choices in it - they look like a rushed-to-market implementation:
- there’s a 512-byte buffer where everything USB-related happens, but this memory must be accessed as 256 16-bit words on 32-bit address boundaries, i.e. there’s a gap every 2 bytes
- some registers live on the APB1 bus like the rest, others live inside this “Packet Memory”
- some bits in the registers can’t be set directly, only toggled - this means you have to read the current value, determine what to toggle (via XOR), and then send those toggle bits (while carefully keeping others in a do-nothing state)
Having seen some FPGA designs recently, it’s clear that this is caused by the two different clock domains of the µC on the one hand (normally 72 MHz) and the USB hardware on the other (48 MHz, but not necessarily synchronised). Improving this would probably have required more silicon and engineering time, which perhaps wasn’t avaialable when the STM32F103 came out.
It all leads to very convoluted code!
USB is a fairly complex protocol. There is an excellent USB in a NutShell resource on the web which goes into all the details. A brief summary and some notes:
- USB is driven by the host, the device side only needs to respond to requests
- there are control messages and actual data, these use different “endpoints”, a bit like different signals in a multi-pin connector - control is always via endpoint zero
- all low-level bit-stuffing, framing, and checksumming is handled by the µC’s hardware
- a “Full Speed” USB link runs at 12 Mbps, with a “Start Of Frame” packet every 1 ms
- the USB driver requires little CPU overhead - idling is fully handled by the hardware
- two pins (D+/D-) encode the data differentially and signal special states, e.g. bus reset
- there’s a complex “enumeration” phase on startup, for the host to learn what device it is
[98361.944068] usb 2-1: new full-speed USB device number 6 using uhci_hcd [98362.144095] usb 2-1: New USB device found, idVendor=0483, idProduct=5740 [98362.144106] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [98362.144112] usb 2-1: Product: Forth Serial Port [98362.144118] usb 2-1: Manufacturer: Mecrisp (STM32F10x) [98362.144124] usb 2-1: SerialNumber: C934CC37 [98362.149505] cdc_acm 2-1:1.0: ttyACM0: USB ACM device [98443.520186] usb 2-1: USB disconnect, device number 6
And here’s what it looks like on Mac OSX:
(this is an earlier version, the serial number was not yet filled in)
As you can see, the STM32F103 presents itself as an “ACM” modem-like device (with device names chosen to identify this as a Mecrisp Forth context). The huge advantage here, is that such devices do not need any additional drivers - at least not on Linux and Mac OSX.
There is one quirk on Linux: if there’s a ModemManager running, it’ll start
sending “AT” commands to the device the moment it is plugged in. This will
confuse the Forth interpreter - you have to kill that
first (who uses modems anyway, nowadays?).
The other good news is that serial I/O actually works - with the driver set up to send back everything it receives, you can open the serial port, type at it, and see everything echoed back.
In short: the USB driver is essentially already working!
But now comes the tricky bit: for this to become the main interface to Mecrisp
Forth, we will need to re-route its input and output to use the USB device.
There are four words defined in Forth to help with this:
emit. There are also four “hooks” which allow pointing these to other
words to “re-vector” all keyboard output and console output through.
Right now, a first attempt to redefine these words is still failing: for some reason, Mecrisp Forth refuses to start listening to the USB device. This may be related to some circular “ring buffers” added to solve the impedance mismatch between packet-oriented USB streams and the character-oriented mode required by Forth. It’s not clear at the moment where the problem originates - clearly more debugging will be needed to figure this out.
But then what? We can load the new USB driver in flash memory, on top of
Mecrisp, and we can set it up to switch its I/O permanently to this code.
But what if someone types “
eraseflash”? That would wipe out our setup,
and revert the entire system to its original serial-port-only state.
Whoops, bye bye USB - we will effectively have lost control of the system!
Here is one possible way out (but see below) - it requires a change in Mecrisp Forth:
- introduce a new type of variable, perhaps “
otp-var” (for “One-Time Variable”)
- it behaves like any other variable, but its initial value can be changed once in Forth
hook-key, etc vectors are changed to such otp-vars
- the Mecrisp build uses the current settings, i.e.
- two more variables are introduced, let’s calll them
flash-startmarks the lowest erase point for Mecrisp’s flash erasure
flash-limitmarks the top limit, beyond which it never erases
- as shipped, these are set to $04000 and $10000, i.e. 16K and 64K, as they are now
- and while we’re at it, let’s also add
ram-limit, preset to $5000 (20K) on STM32F103’s
The above changes would not alter the way Mecrisp Forth operates, as shipped. But they will make it possible to alter the system (once!) to include more Forth code in Flash memory, and to make sure that code can never be lost again.
The trade-off is that once you make such a change, it becomes permanent (which is the whole point) - to change it again, the entire chip will have to be erased and re-flashed with a clean Mecrisp firmware image.
ram-limit variables are not strictly necessary, but they
allow altering the system to use more or less of the available flash and RAM.
This neatly addresses the fact that there are variations of the STM32F103 (and
others), which differ only in flash and RAM size.
It also allows reserving flash and/or RAM for other uses. And even do crazy things like putting the RAM limit very very high and allow Forth to allocate memory in external RAM chips attached via FSMC, for example.
On to the implementation of
otp-var: this might not be very difficult: variables
defined in flash already contain a copy of their initial value. Of course, these
variables end up being allocated in RAM (as part of Mecrisp’s startup sequence),
so maybe all that needs to be done is to add one extra 32-bit field to these
variables in flash, preset to
$FFFFFFFF, with logic to check this extra value
first. If it’s still
$FFFFFFFF, we use the other intial value, but if it
isn’t, we use this new value instead. And then some code in Forth (it need not
be part of the Mecrisp core) can figure out how to re-write that special value -
One could even consider changing all variables in this way, i.e. allowing every variable defined in flash to be changed once. So the Mecrisp core can continue to set them up as usual, while still allowing an application to change them, once. This may lead to more cases where the entire chip will have to be erased and re-flashed (over serial or via SWD), but this reconfigurability really isn’t intended for mainstream use - it just adds the option to create an enhanced core, such as a USB-enabled variant of Mecrisp. It would also allow creating a smaller Mecrisp core, FWIW.
Update - Matthias Koch, Mecrisp’s author, has suggested some alternatives, which will avoid having to ever reflash the core (although you may still need a serial hookup to get out of one of the possible failure modes). Consider the above shelved: keeping the core as is, is preferable!