I recently published a story about bitflyer, a simple game for the BBC micro:bit.
Off the back of it, Mr Zbit, the maker of @ZbitConnect, got in touch, and kindly sent through some of his boards to play with.
These boards make it simple to add different attachments to the micro:bit, and are cleverly designed to be chain-able too.
They work by sandwiching a piece of (I think) conductive elastomer between the various boards’ contacts. The rubber needs a certain amount of pressure to make a connection, hence all the screws an bolts.
The basic zbit:builder gives a whole lot of standard through-hole connections to solder things to, but the creator is also developing several other boards that do different things, including a speaker board.
I dusted off the bitflyer code, loaded it in, and it just worked first time. For about 30 seconds. Before it ran out of memory and the whole thing crashed. After several hours of stepping through the layers of code, stripping out anything that could go, and trimming things, I gave up and decided on a more radical approach: Rust.
Of the languages I considered, Rust made the most sense. The list went something like:
- C: Too many lines of code
- C++: Too passé
- Python: Too memory
- Go: Too basic — I struggle with the lack of syntactic sugar in the language
- Java: Too java
- Rust: New, shiny, aimed at systems-level programming
Having chosen Rust, I had to work out how to get rust running on an embedded board. Luckily there’s a project to do just that: Zinc.
Zinc’s goal is a project to produce compiled rust binaries that can run directly on a computer without an OS helping out (bare metal). The project has support for a number of ARM chips/embedded boards, but not the micro:bit nrf51822 (yet!).
It provides a great platform for getting started, with examples, and a build tool called
xargo which handles the linking and organisation. They have a light-weight HAL module, and macros to help with board configuration.
As mentioned, zinc has some nice macros for configuring safe access to various things, including peripheral access.
These chips typically allow access to their different systems (I/O, RNG, clocks, etc..) by reading and writing to magic memory locations.
The following snippet shows the GPIO pin definition for the micro:bit:
Which can then be used to control GPIO pins:
I spent a long time working out how things like interrupt handling worked, and the subtleties of peripheral control (Turns out that the micro:bit doesn’t actually have a systick interrupt, and the docs don’t make that entirely clear!). But once I’d understood the details, and how to express them using the zinc macros, the implementation was very simple and straight-forward.
Mr Zbit also sent along a board with a tiny speaker and amp on it. The speaker is attached to one of the gpio pins, allowing it to play very basic, square-wave based sounds.
Interrupt vectors have always slightly scared me, so doing this was a great learning excercise. ARM chips typically have a table of function pointers, held at a known location in memory, when an interrupt fires, the chip saves all the CPU state, and jumps to the address pointed to by the corresponding entry in this table. After the interrupt function returns, the state is restored, and everything continues as normal.
How does this look in rust?
The hardest part of this was working out the exact order/offset of the interrupt table. Once this was done, then my isr_rtc0 function was called every few ms.
The rest was just a case of converting MIDI files into a rust array of frequencies and times, and writing a simple function to toggle GPIO 1 at the right speed to make notes.
The code is not very clean, and should be tidied up/significantly refactored (hence not being pushed upstream). But may be found here:
Thoughts on Rust
I actually really loved writing this in Rust. Programming on bare metal meant that I was forced to be a bit cavalier with
unsafe (With work, this could have been reduced), and with everything happening on-chip, with little in the way of feedback (The speaker & display are both strictly write-only) I didn’t have to deal with optionals much. Ownership complexity was avoided by having shared global state (This chip was never going to do much in the way of multitasking)
Given that I was able to avoid many of the pain points mentioned above, and running on nightly giving macros the power to provide nice abstractions, the code became fun to play with, and pleasant to use.
Speed and memory usage were, of course, blistering compared with Python, and (at times being forced to) looking at and stepping through the generated Assembly, the true value of zero cost abstractions became really clear.