USB serial in Forth, progress! May 2016
A while back, an article was posted about the lack of USB on STM32F103 µCs, when it comes to Mecrisp Forth, that is. Unfortunately, getting the built-in USB device-mode hardware working is quite a challenging task. Not only is the USB protocol fairly complex - the actual USB interface hardware on that particular model STM µC is in fact ridiculously messy!
It has all the appearances of a rushed-to-market design, just to get that USB feature shipping!
Fortunately, there are a number of solid working implementations for this hardware, including the libopencm3 and ChibiOS code (both in C), as well as an older (tentative?) implementation in Forth by Eckhart Köppen. This last one has turned out to be the catalyst to move forward on implementing a serial-over-USB driver for (and in) Mecrisp Forth.
First the good news - it works! - even though there are still a few quirks:
$ folie -p /dev/cu.usbmodemC934CC31
Connected to: /dev/cu.usbmodemC934CC31
ok.
1 2 + . 3 ok.
^D
$
USB output appears to work flawlessly, and is a lot faster than the normal serial console’s 115,200 Baud setting (this is not surprising, given that the USB full-speed rate is 12 MHz).
A few important bugs still remain as of this writing (May 2016):
- input only works when data is coming in as chunks of 16 bytes or less - which includes normal typing, and even auto-repeat - the problem only occurs with larger chunks, in which case all characters are lost, for some as yet unexplained reason…
- output stalls when there is no connection, i.e. when USB is not plugged in - this means that Forth code will wait (and appear to hang) once the 128-character buffer fills up
The first problem is very unfortunate: it means that Folie can’t be used to upload source code over USB yet. The second problem is more benign, but will become important for unattended use (i.e. powering up and running when no USB host is listening).
The problem with the dropped input is probably related to the lack of back-pressure, i.e. Mecrisp has to let the host know when it can or cannot accept more incoming data. The stalled output is probably solvable by keeping track of the attach state, and then simply dropping all output when there is no active connection (just like a serial port, there is not much else we could do anyway).
It’s important to note that USB is a host-driven protocol. Devices may only respond when the host polls them - including data from device to host! Luckily, the hardware takes care of that.
The new USB driver implementation is 100% pure Forth and can be found on GitHub. At ≈ 350 lines of code, it’s quite long and far from readable or optimised. The main tasks of this code are:
- configure the µC clock to run at 72 MHz and derive the 48 MHz clock required by USB
- set up the USB hardware interface on the STM32F103 µC, with all its quirks
- get through the initial USB “enumeration” sequence to tell the host who we are
- receive all incoming packets and place the data in a 128-byte ring buffer
- send data out whenever present, using a (probably superfluous) 64-byte ring-buffer
- deal with all the possible idling, attach, detach, and re-attach scenarios
- keep the USB protocol going through polling (interrupts are not enabled in this driver!)
- provide a set of I/O hooks for
key?
,key
,emit?
, andemit
as a public API for this driver - define a custom
init
word, which sets up and redirects all console I/O to USB
The result is the session shown above. The current implementation, with some boilerplate code not strictly required for USB, needs ≈ 9 KB of extra flash memory. With the extended Mecrisp “RA” core image, the entire build fills just under 29 KB of flash memory. That still leaves over 32 KB of flash for application use, on even the smallest and very common STM32F103C8 boards.
Note that there is a small dependency on the type of board, since each board has a different way of tying the 1.5 kΩ resistor onto the USB lines, as needed for the reset phase of USB signalling. On a HyTiny, the PA0 pin is handled by this code. Slight mods will be needed for other boards.
With a warm thank you to Matthias Koch, Mecrisp’s author, for his tips, support, and continued encouragement to get all this going. Although the current code is not quite ready for prime time and still needs a major cleanup round, that “only-USB” moment sure is getting a lot closer now!