Back in 2013 I started designing and implementing a new programming language aimed at systems programming for "small" systems, like microcontrollers. I continued that work on and off for a couple of years afterwards, but I had to take some time away from it for other priorities and then had trouble getting the motivation back.
I've been thinking about several of my older projects again this year, and have concluded that the conditions that motivated me to start working on Alamatic no longer apply. I don't expect to be doing any more work on Alamatic, and this article is a sort of wrap up and retrospective to see the project off.
What was Alamatic anyway?
I started working on Alamatic because I felt that folks writing code for microcontrollers and other small systems were being underserved by C as the primary programming language. C's very light abstractions over the underlying system and its ability to run freestanding (with little or no runtime library) serve small systems programming very well, but the concepts offered by the language for system decomposition and reusability are rather lacking.
Firstly, it lacks a first-class module system, instead relying on the C preprocessor to concatenate source files together and perform simple macro substitutions. In the context of small systems, this tends to discourage using abstractions at all, and a codebase for an embedded system will tend to be very tightly coupled to the exact hardware it was built for, making a port to different hardware far harder than I'd like.
Secondly, the total lack of memory safety has plagued embedded applications with obscure bugs and security vulnerabilities. The ability to access memory directly is important when writing drivers, but it's unfortunate that with C this is an "all or nothing" decision: if you use C, you'll need to take care of memory safety yourself (and you'll probably get it wrong!).
My initial experiments in this area were in using C++ instead of C and using
its additional language features to create low-cost or zero-cost abstractions,
leaning heavily on C++ template metaprogramming and the then-rather-new
C++11
features
like
constexpr
.
The general methodology here was to define standard interfaces for common
system buses like SPI, I²C, UART, etc and then have implementations
of those interfaces for each target microcontroller/board. Device drivers
would then consume these to allow reusing them across many different target
systems.
I used this early C++ library in a few different projects at the time, composing device drivers I wrote with implementations of the interfaces targetting both Atmel AVR and later ARM Cortex M microcontollers from NXP. Here's an example of some "wiring" code (that is, hard-wired peripheral configuration code) from a project that was rendering Conway's Game of Life on a large pixel display built from zig-zagging LED strips with LPD8806 chips driving them:
#include <alambre/system/avr.h> #include <alambre/bus/spi.h> #include <alambre/capability/1dgraphics.h> #include <alambre/capability/2dgraphics.h> #include <alambre/device/lpd8806.h> #include "gameoflife.h" #include "gameofliferenderer.h" #include "ledmatrix.h" SoftwareSpiLeaderOutputBus <typeof(*avr_system.B3), typeof(*avr_system.B5)> spi_bus(avr_system.B3, avr_system.B5); Lpd8806Device<typeof(spi_bus)> strip_device(&spi_bus); typedef typeof(strip_device) strip_device_type; Lpd8806Color palette[] = { Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 48), Lpd8806Color::get_closest(0, 0, 56), Lpd8806Color::get_closest(0, 0, 64), Lpd8806Color::get_closest(0, 0, 72), Lpd8806Color::get_closest(0, 0, 80), Lpd8806Color::get_closest(0, 0, 88), Lpd8806Color::get_closest(0, 0, 96), Lpd8806Color::get_closest(0, 0, 104), Lpd8806Color::get_closest(0, 0, 112), Lpd8806Color::get_closest(0, 0, 120), Lpd8806Color::get_closest(0, 0, 128), Lpd8806Color::get_closest(0, 0, 136), Lpd8806Color::get_closest(0, 0, 144), Lpd8806Color::get_closest(0, 0, 152), Lpd8806Color::get_closest(0, 0, 160), Lpd8806Color::get_closest(0, 0, 168), Lpd8806Color::get_closest(0, 0, 176), Lpd8806Color::get_closest(0, 0, 184), Lpd8806Color::get_closest(0, 0, 192), Lpd8806Color::get_closest(0, 0, 200), Lpd8806Color::get_closest(0, 0, 208), Lpd8806Color::get_closest(0, 0, 216), Lpd8806Color::get_closest(0, 0, 224), Lpd8806Color::get_closest(0, 0, 232), Lpd8806Color::get_closest(0, 0, 240), Lpd8806Color::get_closest(0, 0, 254) }; PaletteBitmap1d<unsigned int, strip_device_type::raw_color, 161, sizeof(palette) / sizeof(palette[0])> bitmap1d(palette); Lpd8806Bitmap1dDisplay<typeof(bitmap1d.render_bitmap), strip_device_type, 161> display(&strip_device); RadiantMuralZigZagMutableBitmap1dAsBitmap2dAdapter<typeof(bitmap1d)> bitmap(&bitmap1d); GameOfLife<24, 9> game_of_life; GameOfLifeRenderer<typeof(game_of_life), typeof(bitmap)> renderer(&game_of_life, &bitmap); auto white = strip_device.get_closest_color(255, 255, 255); auto blue = strip_device.get_closest_color(0, 0, 127); void main() { game_of_life.next_frame(); renderer.update(); display.update(&bitmap1d.render_bitmap); }
In this particular example, we are passing te LPD886 driver a software ("bigbang") SPI implementation using the GPIO interface for an AVR microcontroller, then wrapping that driver in a "1D graphics" interface. Then we have an application-specific implementation of the 2D graphics interface that understands the weird/special way I sliced up and arranged the 1D LED strip to create a 2D grid, which finally allows for generic Game of Life and Game of Life Renderer implementations that are unaware of the physical layout of the pixels and the communication protocols needed to update them.
From a technical standpoint, this approach worked very well. Through the use of template metaprogramming the above all reduced down into small machine code directly accessing the AVR's GPIO registers.
From a usability standpoint though, I was unhappy. Although C++11 type
inference via the auto
keyword allowed hiding some of the ugly generic
types, some of them still ended up visible. Furthermore, any mistakes in
this sort of wiring code would often produce multiple screenfuls of
confusing and un-actionable error messages from the C++ compiler, because
C++ template metaprogramming tends to fail deep in the implementation, rather
than at the surface.
From this experience, I started designing a new programming language that would be both designed to make the above sort of automatic wiring code more readable and intuitive, and also to include a built-in lightweight cooperative scheduler to help with the event-driven processing that is typical of this sort of system. I did several iterations of the design, so there isn't any single language I can show an example of, but I can illustrate the general idea with a hypothetical transliteration of the above C++ example:
import avr import spi import graphics_1d import lpd8806 import led_matrix import game_of_life const spi_bus = spi.new_software_leader_bus(mosi=avr.B3, sclk=avr.B5) const led_strip_dev = lpd8806.new_spi(spi_bus) const palette = []lpd8806.Color{ # (long color palette definition omitted for brevity) } const bitmap = graphics_1d.new_palette_bitmap(palette, length=161) const disp_1d = led_strip_dev.new_1d_display(bitmap) const disp_2d = led_matrix.new_zigzag_adapter(disp_1d) const game = game_of_life.new(bitmap.size) const renderer = game_of_life.new_renderer(bitmap) proc main(): loop: game.next_frame() renderer.update() disp_2d.update(renderer.bitmap) yield
The first obvious difference compared to the C++ example is that Alamatic
was to have a Python-like syntax with blocks indicated by whitespace. That
cosmetic decision aside, the two initial motivations here were to automatically
determine the types of constants and variables through inference (to hide the
generics) and to put the main application code in so-called "processes" (the
proc
keyword above) that can potentially run concurrently using a
co-operative scheduler.
There were some other ideas in the mix here too, such as a programmable compiler (allowing the platform-specific build and deploy process to be encoded as part of the main program), a language-syntax-aware macro system built into the language, and automatic memory management via a mixture of lifetime inference and reference counting, but those parts did not actually make it from the drawing board.
At the time I was initially working on this, C and C++ were the only portable languages in widespread use for microcontrollers and other small systems. My decision to explicitly end work on Alamatic now comes from a number of changes in the ecosystem in the intervening years, which either already address or are showing the potential to address many of the problems I was aiming to solve. I'll explore these in the remaining sections.
(I would be remiss not to mention that some other languages were attacking similar problems but that were arguably seeing more niche use. The most visible one was D, which remains somewhat popular in a small community but I would not consider it to be in mainstream use.)
Rust
The programming language Rust already had development well underway by the time I was starting work on Alamatic, but the Rust language was still in its early design iterations, with several features not yet well baked and things changing rapidly.
As a consequence, although I was aware of and paying attention to Rust during my early Alamatic work, at that time it didn't feel to me like it was addressing the same set of problems I was. Part of that reaction was honestly misunderstanding on my part: with things moving so quickly, I can see that in retrospect there were details I didn't see clearly.
Rust 1.0 was released in 2015, about two years after I'd started work on Alamatic. By that time, the language had been refined quite a bit and it had shed many concepts that had turned out to be superflous or better represented as library features.
Although a typical Rust "crate" assumes access to an operating system via
functionality in Rust's std
libraries, it's straightforward to write a
crate that uses only the low-level runtime library features, by annotating
the crate as no_std
. The program can then provide its own system
bootstrapping code, memory allocators, etc in a way that's appropriate to
target "bare metal" programming on typical embedded systems.
Rust's concept of traits (originally inherited from Self), when combined
with generics, end up achieving a very similar set of capabilities to
Alamatic interfaces. The Rust embedded programming ecosystem includes
embedded-hal
, which
defines a set of traits abstracting over common buses, allowing drivers to
be written against these traits and thus run on any platform that offers
implementations of those traits.
Rust also has a well-integrated macro system that works with the grammar constructs of the Rust language itself, and which can encapsulate code generation tasks that can allow for more complex abstractions while retaining readability and safety.
The main Alamatic idea that has been lacking in Rust is some analog to
Alamatic's cooperative scheduler. However, in recent releases Rust has
introduced the long-awaited
async/await
features that provide language-level building blocks to enable both OS-based
and bare metal cooperative concurrency. There is still some remaining work
to make this usable in a no_std
context for embedded, systems, but that
work does seem to be underway. I'm excited to see where this leads. In the
meantime, I had some good success with the less-general model offered
by the rtfm
framework, which
offers event dispatch and task priorities using interrupts on ARM Cortex M
microcontrollers.
The rust-embedded community has already achieved good coverage of a number of embedded platforms and drivers for various devices. I've written some Rust libraries for embedded systems myself, and have broadly enjoyed the experience:
lpc81x_hal
: HAL implementations for NXP's LPC81x series of microcontrollers, which I'd previously targetted in Alamatic and its predecessor C++ library.ssd1322
: A portable device driver for the SSD1322 OLED display driver. (Not yet released in isolation; included as part of a larger project in progress.)buspirate
: HAL implementations that are intended to run on a full operating system and communicate with devices using a Bus Pirate. I've used this to do initial development of device drivers directly on my full PC, prior to integrating them into a microcontroller-oriented codebase.spidriver
: Similar tobuspirate
, but using a SPIDriver interface instead.
In particular, the Embedded Rust community uses
typestate programming
to get a similar effect to the Alamatic system wiring model, where the compiler
can help ensure that the system hardware is configured in a correct and
safe manner with little or no runtime overhead. Using my buspirate
library
as an example, switching the Bus Pirate into a different mode selects a
different API at compile time, so the compiler can help ensure that the
intended mode is selected before issuing commands.
Espruino, MicroPython and CircuitPython
Rust covers or will cover many of the features I had in mind for Alamatic, but I think it's fair to say that Rust is quite a complex language that is hard to learn.
Usability was also a big motivator for Alamatic, and part of that was selecting a Python-like syntax and aiming to make tradeoffs that would keep the more complex features like generics and metaprogramming confined to board-support and driver code, with "normal" user code focusing on composition and logic.
When I started Alamatic, I had considered it mandatory to have a static type system and ahead-of-time compilation. I was pleasantly surprised to see that in subsequent years the resources on typical low-cost microcontrollers got big enough to accommodate cut-down ports of common dynamic programming languages, including JavaScript via Espruino and Python via MicroPython. Popular electronics hobbiest vendor Adafruit later embraced MicroPython (via their fork, CircuitPython) as a primary development environment for many of their microcontroller development boards, popular with hobbiests and education.
While I would still love to see a blend of these into a single language, I think MicroPython and similar have done well to fill a gap of providing an accessible, memory-safe programming environment that enables composition and abstractions, and have done so using dynamic language VM rather than via increasingly-complex static type systems.
import time import board import busio import displayio import adafruit_ssd1322 spi = busio.SPI(board.SCL, board.SDA) tft_cs = board.D6 tft_dc = board.D9 tft_reset = board.D5 display_bus = displayio.FourWire( spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset, baudrate=1000000, ) time.sleep(1) display = adafruit_ssd1322.SSD1322( display_bus, width=256, height=64, colstart=28, )
I personally prefer working with Rust because I enjoy the static type system, but these VM implementations seem to work really well for those who prefer dynamic programming languages, and they seem to have been really successful as successors to the C++-based Arduino programming environment for hobbiests.
WebAssembly
In an article that has been primarily talking about microcontroller programming, a section on WebAssembly might feel out of place. However, I was careful while introducing Alamatic above to use the more general term "small systems".
My reason for this is that in my initial design phase for Alamatic I'd also observed that code running in web browsers as part of web applications was increasingly taking on characteristics similar to embedded systems, albeit with a generalized rendering engine alongside rather than specialized hardware.
For early Alamatic work, the likely target would've been Emscripten, which in those days was compiling to compact, efficient JavaScript code but has since shifted its focus to asm.js and then WebAssembly.
Modern WebAssembly provides a programming environment that has constraints quite similar to embedded systems: we want to keep the code size relatively small so it can be delivered efficiently over the network, we want to detect events from the user interface and respond with as little latency as possible, and we interact with the outside world (e.g. the host browser) via a low-level, constrained interface.
I'm excited to watch the development of the WebAssembly ecosystem, because the web development space has a history of investing in excellent, productive development tools. I'm expecting we'll see similar investment in tools for productive use of WebAssembly.
A promising project already in progress is AssemblyScript, which is a statically-typed language derived from TypeScript that focuses on efficient generation of WebAssembly code. AssemblyScript programmers can mix use of the thin AssemblyScript runtime library and direct interactions with the WebAssembly runtime, using a familiar JavaScript-like syntax.
Although WebAssembly's initial focus is delivery of code to run in web browsers as part of web applications, there are already efforts to define a set of conventions for using WebAssembly for applications running on traditional operating systems, via WASI.
I expect we'll see WebAssembly continue to broaden its application space, including ultimately taking on use-cases similar to Espruino and MicroPython with code running on Microcontrollers. This could either take the form of running a WebAssembly VM on a microcontroller itself, or we might see ahead-of-time compilation of WebAssembly into ARM or RISC-V native code that could run with similar performance characteristics as code written in C and Rust.
I'm particularly excited to see if this could be the missing piece to bring together the use-cases served by MicroPython and the use-cases served by Rust today. AssemblyScript, or something like it, could occupy a space similar to where I'd targeted Alamatic. WebAssembly could serve as a new cross-language ABI allowing application code written in AssemblyScript to call into driver code written in Rust with little or no additional overhead.
Onward!
The most important difference between all of the projects I described above and my own Alamatic project is their momentum: there are thousands of people working on these projects, and they are improving fast.
I do of course have some sadness around declaring my own project as dead, but it's outweighed by my optimism about what is coming.