Peeling off layers of complexity Dec 2016
As you know, compilers generate code. They take one or more source files and turn them into an executable. In the case of embedded software development, the compiler is actually a cross-compiler because it’s not generating code for the machine it’s running on (your laptop), but for the attached board (a little “target” microcontroller).
The generated code is simply a chunk of binary data - gibberish, as far as we humans are concerned. We upload it to the target board, and then it presumably starts doing what we instructed it to do in the original source code. We rely totally on this 1-to-1 mapping.
There are several implications and drawbacks with this widely-used approach:
- partial linking is uncommon in this context, so the binary we have to send each time is usually the entire application
- there’s no way to change the code once it has been uploaded - every little change means going through the same cycle again: edit, compile, link, upload, run
- interfering with the flow of the code requires a hardware debugger (i.e. gdb), which in turns requires a lot of extra debugger information (supplied by the GCC compiler), to make sense of all the machine code once it has been loaded onto the target
- all this is fairly complex, especially when done well and transparently, so that the debugger is able to tie things back to locations in the original source code text
- the debugger requires a connection to the target board, using JTAG or SWD, which requires a separate set of I/O pins and a separate cable
- on the “host” side, e.g. the laptop, all this complexity adds up to a hefty “toolchain” - with over two dozen different tools in a 400+ MB install to make it happen
But there’s a way to avoid such complexity – and it was invented half a century ago.
It’s called “Forth”. Here’s how Forth manages to avoid the issues mentioned above:
- the host is little more than a terminal front end, i.e. a keyboard plus screen
- the code is sent as source code, i.e. text, to the target system
- there’s no longer a need to distinguish host and target, it all happens on the target
- code is executed on the fly, line by line, as it is received
- some code has an immediate effect, other code gets compiled for use later
- all the code is defined bottom up, there’s no need for a separate linking step
- everything that is compiled can be run immediately, from the same command line
- redefinitions are simply appended during development, and override all future uses
- code can be saved to flash memory, so that it survives a crash or hard reset
- tentative code can be compiled to RAM and easily discared by forcing a reset
- there’s no interpreter overhead, Forth is compiled on-the-fly to machine code
- code gets added in very small steps, many functions are just one or two lines
- crashes are not the end of the world, they simply signal that there was a mistake
- recovery from a crash is streamlined, resetting to a known state is instant
The central theme is: “learn as you go”. Go ahead: try something, crash, learn, tweak, repeat. Or to put it even more succinctly: Fail Fast! - there is no big wheel turning - sure, there is a workflow, but you can forget about the “work” part of it, it’s all about staying in the flow!
In Forth, there’s no fundamental distinction between compilation, running, and debugging. It’s all interaction, and everything “does something”. Some effects will be internal: viewing or changing values, other effects will be more permanent: defining new functions (Forth calls them “words”) in RAM or in flash memory, and yet other effects will cause I/O pins to change, some data to be sent or received, etc.
Forth is a programming language with its own syntax and conventions (to be described in an upcoming article), but it’s also a programming environment in that it prescribes how the entire development process takes place. All of it - yes, all of it! - lives on the microcontroller.
In a way, there are no safety nets - programming errors and even small mistakes can lead to fatal crashes where the µC stops responding with no clue as to what happened. But while this could be a hassle with a compiled approach, it’s very easily overcome in Forth: reset, and retry things in smaller steps, then inspect what is going on just before the failure you just ran into.
Yet – surprising as it may sound – an embedded Forth environment is also extra-ordinarily robust: all that can happen is that it stops. Simply reset the µC and you’re back on track!
Since the host is no more than a view into the microcontroller, there is no state on the host which can be affected by crashes or resets on the target board. There’s no toolchain, there are no apps to launch or re-launch when the target fails.
What about source code? And editing that code? And version control? And backups?
Ah, this is where we leave the realm of the actual Forth setup. There’s a tool called Folie which addresses all that. The name “Folie” stands for “Forth Live Explorer” – it acts as the terminal and bridge from host to target. In essence, Folie is simply a line-oriented terminal emulator.
But Folie also adds a number of features to make life in Forth-land more convenient:
- line editing can be done in Folie, before it’s even sent off to Forth
- there’s command history, with up-/down-arrow to access previously-entered text
- hitting CTRL-C can reset the attached board - this is a huge time-saver
- Folie can upload new firmware to an STM32 µC via the ROM boot loader
- you can send the contents of a file to the target board, as if typed in
That last feature is what brings the target board into the 21st century. While you could just manually enter all your code and get it working and saved in flash memory that way, it would be extremely inconvenient to not have a permanent record of the final version saved on file.
With Folie, the idea is to write the main parts of your application as source code in your own preferred editor on your own comfortable host system. Then send this over to Forth and start using it. There are various ways to streamline this, so that the actual sending only takes place occasionally. In short: think on the target, try out things there (using the command line), but as soon as you have some code which is working well, enter it as source code definitions in the editor, and send these definitions over as needed - and whenever they need to be updated.
The normal modus operandi is to keep both your editor and a Folie session open, side by side.
There’s a lot more to say about Folie and the Mecrisp Forth implementation by Matthias Koch, which we’ll be using on ARM microcontrollers (Mecrisp is also available for ARM Linux, and there are variants for MSP430 and even FPGA’s). But that’ll have to wait for another time.
Anyway - the point of this story is that the traditional edit, compile, link, upload, run, debug cycle is not the only way to do things. The next few articles will try to illustrate how Forth’s interactive approach can be very effective and great fun for embedded software development.