I’m very new to Rust, but I’ve been spending my evenings with Rust for the past 2 months. My primary motivation for exploring Rust is its potential for programming everthing from micocontrollers to cloud services, and to do so with safety, efficiency, and productivity.
In general, I think the term embedded systems, as it is often thrown around today, can be broken into 2 platforms:
- Application Processors
- Single Board Computers - Raspberry Pi, Beagleboard, and many others
- Industrial Panel PCs/HMI
- Navigation Systems
I find that software development for these systems is only slightly different than that for your typical desktop PC, however…
- Developing with a Cross-toolchain - Your toolchain typically does not run on the target (though for some targets, it probably could). You typically develop on a much more powerful desktop PC and deploy/debug the target remotely (USB, Serial, or Ethernet)
- Bare Metal or OS - These products are almost always running an operating system like Linux, Android, Windows CE/Embedded Compact/10 IoT, or a spcialized RTOS (e.g. VxWorks). Though, they can be bare-metal programmed also.
- 3D Graphics Acceleration - Nowadays, these system often have a built-in GPU, but not always.
- Single-Purpose Application - It is not uncommon for them to run only 1 application and run it all day every day
- Energy Efficiency - Energy efficiency is sometimes a concern, sometimes not; it depends on device, application, and environment
- Boot Time - Very fast boot times can be an excellent selling point for these devices, depending, as always, on the type of application.
- Teensy (32-bit ARM Cortex-M)
- TI Launchpad (16-bit TI MSP430)
- Espressif (ESP8266/ESP32 - 32-bit Tensilica)
- Too many others to mention
- ARM Cortex-M
- Intel Quark
- ESP8266/ESP32 - 32-bit Tensilica Xtensa Architecture
- Atmel AVR
- MicroChip PIC
- Too many others to mention
I primarily work with ARM Cortex-M microcontrollers. Here’s a summary of the different profiles (ordered by least performant to most performant) so you can see their vast range.
- High energy efficiency, low cost
- Cortex-M0 - 48MHz, 16KB~256KB ROM, 4KB~32KB RAM
- Balance between cost, efficiency and performance. May have DSP instruction set.
- High performance. DSP instruction set. Hardware floating point. 2D Graphics Acceleration
- Cortex-M7 - 216MHz, 512KB~2MB ROM, 256KB~512KB RAM
Software development for these microcontrollers is quite a bit different than that for desktop PCs and the previous mentioned application processors.
- Bare Metal or OS - In my experience microcontrollers are most often bare-metal programmed, but it is also common to see them employing a specialized RTOS (e.g. FreeRTOS, µC/OS, ChibiOS/RT, NuttX, etc…). These RTOSs are also quite different from those used with the previously mentioned application processors.
- Connectivity - These products are typically a node in a larger system or serve as a peripheral to a larger system. They often interface to their larger system via USB, WiFi, Bluetooth, Radio, Infrared, RS-232/422/485, CAN, SPI, just to name a few.
- Developing with a Cross-toolchain - Very unlikely these systems will run your toolchain. You typically develop on a much more powerful desktop PC and deploy/debug the target remotely most often using JTAG or SWD with an in-circuit-emulator/debug probe (J-Link, ST-Link). That being said, I’m sure someone somewhere has embedded a C compiler in their microcontroller already.
- Binary Size - Binary size is important due to the very limited amount of Flash ROM (can be less than 16KB)
- Dynamic Memory Allocation - Some applications need dynamic memory allocation, some don’t; it depends on the application.
- Energy Efficiency - Engery efficieny is sometimes important, sometimes not; it depends on the application. That being said, you will find energy efficiency to be more often a concern with microcontrollers than with the application processors. Software efficiency will play an important roll in energy efficiency.
- Single-Purpose Application - These products most often execute only a single binary, and it is often required to run all day, everyday. For power efficient systems, the processor spends much of its time in a standby state to conserve energy.
Output Console (a.k.a stdout) - Almost all of these systems have some kind of console port used for debugging/monitoring the application. It is most often used for output, but is sometimes used for input also
- ARM Semihosting - A clever trick using the BKPT instruction to trigger an operation on a host computer. Demonstration in Rust.
- ARM Instruction Trace Macrocell - ARM proprietary peripheral for monitoring the execution of code. Faster than semihosting and less intrusive to the running application than other alternatives.
- Serial (UART) - All microcontrollers that I’ve used have always had a UART that can be used for this purpose
- USB - Nowadays many of these microcontrollers have a built-in USB device peripheral, so it can be programmed for serial communication (CDC device), or as some USB class for this purpose.
- Floating Point - Some mcirocontrollers have hardware floating point some don’t. Some applications need floating point, some don’t. Some can get by with software floating point, some can’t.
- Linker Scripts - Developers will almost always need to write a linker script (a.k.a scatter file) to tell the linker how to organize the binary.
- Memory-mapped IO - Almost all peripherals are controlled via memory-mapped IO. Volatile semantics are important here, and so is some way of modeling bitfields.
My suggestions to those paving the future of Rust
- No Runtime - Rust’s “no runtime” feature is great in this domain. Please keep it that way. There’s no reason for “Hello World!” to be more than a 100 byte binary, or more than 20 lines of code. Bare-metal development should be pay-as-you-go.
- No Dependencies - Rust’s libcore “no dependencies” library is also great. Again, please keep it that way. Don’t make developers pay too heavy a penalty for adding a crate to their project, especially when they may only need a small part of it.
- Inline ASM - Inline ASM is important. It is often needed to take full advantage of the hardware (e.g. Semihosting, synchronization/memory barriers, DSP, SIMD, etc…)
- Binary Size - Don’t be careless with binary size; someone might want to run your code on a microcontroller with only 16KB of flash memory or even less.
- Modular Software Components - Keep things modular with as few dependencies as possible. The range of applications and hardware capabilities is vast. Let the developer aggregate only what they need for their application (i.e. pay-as-you-go). Principles of good software engineering like high cohesion, low coupling are especially valuable here.
- Setting Up a Development Environment - Don’t make setting up the toolchain or adding software components a hassle. rustup and cargo have been a breath of fresh air for me; thank you! Also, the recent ARM Thumb target additions were most welcome; thank you!
- Memory-mapped IO and Bitfields - Almost all aspects of the hardware are controlled by manipulating bitfields in memory-mapped IO registers. Some kind of bitfield support would be nice, but it needs to be done right for people to want to use it. Bitfields are typically described in the MCU’s datasheet with most-significant bit index and least-significant bit index or width; not as fields relative to each other. Developers will want to easily cross-reference their code with the MCU’s datasheet; don’t make them count bits.
- CTFE and Metaprogramming - Compile-time execution and metaprogramming can be used very effectively to reduce binary size, increase runtime efficiency, and reduce boiler-plate. Zinc’s ioreg in Rust, Ken Smith’s register access in C++, and my own memory mapped IO experiment in D are good examples of this. It would be nice if we didn’t have to learn another metalanguage or API to do this. D is very nice in this regard (especially with features like CTFE and static-if). There’s no separate language to learn for metaprogramming; it all feels like one cohesive language in D.
- Hardware Abstraction - Most libraries written for microcontrollers leave some functions unimplemented so they can be ported/adapted to the underlying hardware platform. This may not be the best way to create a hardware abstraction, and I think there are opportunities to do this differently with more modern tools and programming techniques. The software that comes to mind for me here is Anti-Grain Geometry (AGG).
Anti-Grain Geometry is not a solid graphic library and it’s not very easy to use. I consider AGG as a “tool to create other tools”. It means that there’s no “Graphics” object or something like that, instead, AGG consists of a number of loosely coupled algorithms that can be used together or separately. All of them have well defined interfaces and absolute minimum of implicit or explicit dependencies.
RAM (The Stack and the Heap) - My latest project has 3 discontiguous RAM regions:
- Core-coupled memory (CCRAM) - Super fast SRAM coupled right to the processor core… but small (64KB).
- Inernal SRAM - Not as fast as CCRAM, but still fast…(192KB)
- External DRAM - Slower than I’d like it to be…(4MB) I use the CCRAM for my stack. I had to write a custom malloc to make use of both the Internal SRAM and External DRAM for my heap so I could control what types are allocated where. I’d like to rethink dynamic memory allocation to support different, discontiguous heaps for allocation based on type, size, etc…
- Microcontrollers are Different and More Diverse Today - Microcontrollers and their applications are very different today from what they were just 10 years ago. Many microcontrollers today have more resources and power than the PCs I used to use, while some are still very constrained. I caution against making decisions based on tired, old rules of thumb. Dyanamic memory allocation, exceptions, runtime type information, etc… can all be useful or harmful in the this domain; it all depends on the application. Let the developer choose.
My Current Project
I am currently working on a microcontroller-based application. It has two components: an Editor that runs on a host PC, and a microcontroller with a runtime in its firmware. The user creates a project using the Editor, uploads project files and data to the microcontroller, and their project runs on the microcontroller.
The microcontroller firmware/runtime is written in C++. The Editor application is written in C# and C++. The C++ firmware is recompiled for the PC so the user can simulate their project in the editor.
I almost always have to write my own tools that run on the PC to help me develop, test, and make good use of my microcontroller products. I also have to write tools for my customers to make using and interfacing with my microcontroller products more seemless and productive.
I love the power of C++, but it’s too inconvenient to use. I love the convenience of C#, but it’s just not powerful and portable. I’m tired of having to learn different programming languages for different domains. I wish I could program everything from tiny microcontrollers with 4KB of RAM to huge distributed cloud services with one and only one safe, efficient, and productive programming language. Maybe I can with Rust…we’ll see.