Today’s microcontrollers (like the STM32 Blue Pill) pack so many features in a tiny package… yet few embedded programmers are capable of exploiting the full potential of modern microcontrollers. Many of us (my IoT students included) seem to be stuck in the 1980s — painstakingly writing C programs for small computers.
Mynewt is a modern realtime operating system that runs on many microcontroller platforms, even on devices with little RAM and ROM like Blue Pill. It has an excellent Sensor Framework for creating IoT devices.
Stretching Mynewt on Blue Pill to the limit, I have created sophisticated sensor networks with CoAP encoding (JSON and CBOR), transmitting and receiving simultaneously on both ESP8266 and nRF24L01 modules.
In my previous experiments with Embedded Rust (including this one), I know that Rust flies light and fast just like C. Rust has great support in coding tools like Visual Studio Code, which makes it easier to code for newbies.
Rust advocates “Safe Coding”. Rust disallows bad code that may cause our device to crash. Rust could be the perfect match for Mynewt!
In this article we’ll mix Mynewt with Rust and learn…
1️⃣ Why is Rust better than C for embedded programming
2️⃣ How to call C functions from Rust and vice versa
3️⃣ What’s Unsafe Coding and how to make coding safer
4️⃣ How Rust compiles programs
5️⃣ How to inject Rust code into the Mynewt build
6️⃣ How to install and configure the Rust and Mynewt code
7️⃣ And finally we’ll see an actual Rust application running on Mynewt with Blue Pill
Here’s the Rust application code that we’ll be running: A sensor application that polls Blue Pill’s internal temperature sensor every 10 seconds and displays the result…
The complete Rust and Mynewt code may be found here…
Apache Mynewt Sensor Network Application for STM32 Blue Pill with ESP8266 and nRF24L01 (includes Geolocation) …
💎 Sections marked with a diamond are meant for advanced developers. If you’re new to embedded programming, you may skip these sections
Embedded Programming in Rust vs C
Why is Rust better than C for Embedded Programming? Here’s what I think…
Rust is Strict and Stubborn
Why would Rust allow
let rc = ... without forcing us to declare
rc as an
int like in C? Does it mean that
rc can change its type from
Nope, that’s because Rust has Type Inference — the Rust compiler analyses our source code and deduces that
rc must be an
int (which is named
i32 in Rust). Why? Because
sensor_set_poll_rate_ms() returns an
i32 (32-bit integer). So Rust enforces variable types as strongly as C, just smarter.
Rust is actually stricter than C — All Rust variables are assumed to be constant, unchanging, immutable (“stubborn”) unless we declare the variable as
mut (mutable). Check out the two examples of
mut in the Rust code above.
C is the opposite (“loose and relaxed”). All C variables are mutable unless you declare the variable as
Check “The Rust Programming Language” for details.
Rust is Lean and Lightweight
According to this actual build log of our combined Rust and Mynewt Sensor application, the entire compiled executable fits into 56 KB of ROM. The compiled Rust application and Rust libraries occupy under 4 KB of ROM. (Most of the ROM space was taken up by the ESP8266 and nRF24L01 drivers written in C.)
The lightweight nature of Rust makes it an excellent replacement for embedded C programming on devices with constrained resources. Like Blue Pill!
Rust Plays Nice with C
Rust was designed for low-level systems programming, like for coding an operating system or web browser (Mozilla Firefox). Just like C, Rust is used for bare-metal programming, so it makes sense for Rust and C functions to be interoperable — We may call C functions from Rust, and Rust functions from C. Even pass the same
structs from Rust to C and back!
We’ll cover the Rust and C interoperability in a while.
Rust is Smarter than C
Check out this comparison of Rust vs C coding in Visual Studio Code. Why is Rust better than C for coding embedded applications?
Remember that Rust supports Type Inference. The Rust compiler keeps deducing all the types of the variables as we type. So Rust provides more helpful code completion and error highlighting than C. Rust generates documentation for our functions too.
This is great for preventing programming errors in Rust, especially for beginners. Often in embedded programs we make undocumented assumptions and take shortcuts to make the programs run on tiny devices. With Type Inference, Rust can warn new embedded developers about these traps.
Rust is Safer than C
Helping us to write safer, crash-proof programs is a key feature of Rust, that’s why we see
unsafe keywords in the Rust code above. We’ll cover
unsafe in a while…
Rust is evolving rapidly, especially for embedded platforms like Blue Pill. A year ago I wrote two tutorials on Rust embedded programming…
Coding the STM32 Blue Pill with Rust and Visual Studio Code
[UPDATE — I have written a new tutorial on STM32 Blue Pill coding with C++, libopencm3 and Visual Studio Code, click…
Running Rust and FreeRTOS on the PADI IoT Stamp
Now is the best time to experiment with low-cost, low-power IoT microcontrollers.
…And they have already become obsolete.
Be very patient if things don’t work quite right in Rust. I may not have the answer for some deep issues, but I’m confident that Rust maintainers will have the answer for us someday.
Mixing Mynewt and Rust
Built in C, Mynewt is a modern, well-designed operating system for microcontrollers. Rust is a modern, smarter, safer programming language for embedded systems. How shall we enjoy the best of both worlds — Mynewt and Rust? I propose to…
1️⃣ Leave the Mynewt operating system untouched, in C. Same goes for Mynewt drivers and libraries, since they were coded by experienced C programmers.
2️⃣ Expose the popular Mynewt APIs through safe Rust interop libraries
3️⃣ Allow Rust applications to be hosted on Mynewt with these exposed APIs
So new embedded developers (and my IoT students) can start coding Mynewt applications in Rust, the smarter, safer way. No more crashing pointers. Yay!
To host a Rust application on Mynewt, we need to be sure that C and Rust can really call each other seamlessly…
1️⃣ Rust must allow importing of Mynewt’s C API. So Rust functions must be able to call C functions: Rust ⟶ C
2️⃣ Rust must allow callbacks from Mynewt’s C API. So Rust must allow its functions to be called from C functions: Rust ⟵ C
3️⃣ Rust must allow
structs to be passed from Rust to C and back:
Rust ⟵📦⟶ C
Fortunately, Rust fulfils all the criteria above. For example, to import the following Mynewt Sensor API functions into our Rust application…
int sensor_set_poll_rate_ms(const char * devname, uint32_t poll_rate)
…We just compile this Rust code with our Rust application, then we can call the Mynewt functions as though they were Rust functions!
💎 How did we derive the Rust import declarations above from the Mynewt C declarations? This is explained in the section “Calling C from Rust and back” below.
Is This Code Unsafe?
Question: What’s wrong with this C function that calls the Mynewt API
listener is a variable created on the stack frame for
start_sensor_listener(). When passed to
listener is appended directly to the global list of sensor listeners, without copying.
start_sensor_listener() returns, the stack frame is reused by other functions and the
listener may contain garbage. When the sensor is polled 10 seconds later, Mynewt fetches the garbled
listener to call the listener function. Which results in a catastrophic device failure. One IoT Device Down!
This is one key reason why my IoT students find C programming so hard… Every time we define a variable in C, we need to pick wisely whether it should be a 1️⃣ Static Variable, 2️⃣ Stack Variable or 3️⃣ Heap Variable.
Choosing the wrong one will have dire consequences… that’s why Rust calls this “Unsafe” code.
Unsafe Coding in Rust
Rust doesn’t prevent us from writing unsafe code though — it requires us to flag the code as unsafe using the
unsafe keyword. Otherwise the Rust compiler politely terminates and refuses to generate any unsafe executables, for our safety.
In the Rust code above we are required to ring the alarm bells and flag as
unsafe the call to
1️⃣ We are passing the address of the
listener to a function
sensor_register_listener(). Which may cause problems if the
listener was allocated incorrectly (like on the stack).
&mut listener is equivalent to
&listener in C, just that it also declares to Rust that the
listener contents may change (mutable).
sensor_register_listener() is a C function defined by Mynewt, that we have imported into Rust. All imported functions must be flagged as
unsafe because, as we know, C functions are capable of doing really weird things.
But we don’t really want
unsafe alarm bells to ring in our heads every time we call the Mynewt API. Is there a safer way to silence the alarms?
Safer Coding in Rust
The safer way to call
unsafe Mynewt APIs in Rust is to create wrapper functions to check the parameters and the return values.
Check this out… we can now register a sensor listener without flagging as
unsafe! And the listener was allocated on the stack!
This is the perfect kind of API that my IoT students should be calling, without fear of crashing their devices. What is the magic that makes this happen?
Here’s how we created
register_listener() as the safe version of the
sensor_register_listener() API in Mynewt…
register_listener() makes a local static copy of the listener, and passes the local copy to the unsafe
sensor_register_listener(). So even if the listener was created on the stack, we are actually passing a static copy to the Mynewt API. Which won’t crash the device.
We have created a simple Rust wrapper that reuses the same types used by the Mynewt API. (That’s why
register_listener() returns an integer as the result code.) But Rust APIs generally use a different convention to return results — the
Result type. We’ll cover this in the next article.
💎 This implementation looks simplistic… what if we need more sensor listeners? But in reality our device RAM is highly constrained and we should plan in advance how many sensor listeners we really need (and set it as a
#define). Then we create an array of listeners that’s allocated by
What about allocating listeners on the heap? That could be a good solution but I’m not keen on using the heap on constrained devices (because we never know when we’ll run out of heap space). The
allocheap allocator needs to be implemented for our Rust environment.
The Typical Rust Build
To understand how we merged the Rust build with Mynewt, let’s look at the typical Rust build process.
The following files are present in a standard Rust build. You can find them in your
stm32bluepill-mynewt-sensor folder too…
Cargo.toml: Just like building a typical Rust application, in Mynewt we run the command
cargo build to build our embedded Rust application.
cargo performs the build according to the settings file
Cargo.toml. This file must be present at the root of the workspace folder.
We’ll check the contents of
Cargo.toml in a while.
.cargo/config: Our Rust build is for an embedded platform: STM32 Blue Pill. The target platform is specified in this file.
We’ll check the contents of
.cargo/config in a while.
cargo build command compiles the
src files and generates in this
debug folder the Rust executable or library (according to
In our build,
cargo generates a library archive file
libmylib.rlib. If we peek at the contents of the archive file …
arm-none-eabi-ar t target/thumbv7m-none-eabi/debug/libmylib.rlib
…we’ll see that it contains
*.o object files, the compiled Rust code.
To include the external Rust libraries for the Mynewt build, we need to look one level deeper:
This folder contains the compiled
*.rlib archive files for our Rust application (
libmylib-*.rlib) as well as external Rust libraries.
As we’ll see later, all the
*.rlib files in
debug/deps will be injected into the Mynewt build.
5️⃣ Rust Core Library
libcore: There’s one important Rust library that’s missing — the Rust Core Library
libcore. This library contains the fundamental code needed to implement the core Rust functions: data structures, math, panicking, text formatting, …
libcore is automatically included during the
cargo build if we use
cargo to generate the executable. But here we are using Mynewt to generate the executable, so we need to add
libcore located? It’s actually located together with the Rust compiler (not with the
cargo installation). Our build script runs this command to get the location…
On my Mac,
libcore is located at
libcore is specific to the target platform (Blue Pill is
thumbv7m-none-eabi). We’ll link the
libcore library in the Mynewt build.
Rust Build Settings
Let’s look at the simple Rust build settings for our project (which was inspired by this sample)…
Here we indicate to
cargo that we’re generating a library
[lib] (instead of an executable
To keep the ROM size small, we have included only a few small libraries.
Cargo.toml produces the compiled Rust library
libmylib.rlib that we’ll link with Mynewt.
Here we select the target platform for the Rust compiler.
STM32 Blue Pill runs on an Arm Cortex-M3 processor (which is based on the ARMv7-M architecture), so we have chosen the target
cargo build will then generate compiled code in
libmylib.rlib that will run on our Blue Pill.
As we have seen, we haven’t changed anything in the Rust
cargo build process. We used the standard
cargo build command to generate a Rust library
libmylib.rlib for our Rust application.
The integration of Rust with Mynewt is actually done by our custom build script that injects the compiled Rust files into the Mynewt build. Before going into the integration details, let’s learn about the Mynewt build…
The Typical Mynewt Build
The normal Mynewt build command
newt build compiles some C and assembly files from these folders…
apps/my_sensor_app: Our custom C application for Mynewt
libs: Custom drivers (e.g. ESP8266) and libraries (e.g. Sensor Network) for Mynewt
repos: Mynewt OS source code
newt build compiles each source module (e.g.
esp8266) into a separate
*.a library (e.g.
libs_esp8266.a), located at
newt build links the
*.a libraries together to create the Blue Pill executable image
This is the image that get flashed into the Blue Pill ROM.
Merging the Rust build with Mynewt
To host a Rust application on Mynewt, we just need to inject three pieces of compiled code into the Mynewt build…
1️⃣ Compiled Rust application:
2️⃣ Compiled external Rust libraries:
target/thumbv7m-none-eabi/debug/deps/*.rlib. This folder also contains
3️⃣ Rust Core Library
To do that, we have a super build script
scripts/build-app.sh that runs the Rust
cargo build command and injects the above files at these Mynewt build locations…
1️⃣ Rust application and external Rust libraries: Copy and overwrite
libcore: Copy and overwrite
But because we instructed Mynewt to include them as part of the Mynewt build, Mynewt compiles them and generates the tiny libraries
Which gives us the perfect opportunity to substitute
libs_rust_libcore.a with our compiled Rust code.
Our super build script overwrites
libs_rust_app.a with the compiled Rust application and libraries, and overwrites
libs_rust_libcore.a with the Rust
libcore library. So when
newt build links all the
*.a libraries, the Rust code gets injected into
my_sensor_app.elf, which will be flashed into the Blue Pill ROM. Sneaky!
But how do we replace
libs_rust_app.a by multiple
libcstr_core.rlib + ...?
Our super build script extracts the
*.o object files from every
*.rlib file and archives them into a single
That’s how we create the Mynewt ROM for Blue Pill that includes the Rust application and libraries!
This video explains the combined Rust and Mynewt build…
Installation and Configuration
If you have the STM32 Blue Pill or Super Blue Pill and you wish to install and configure the Rust and Mynewt demo code, follow the instructions here. The steps are quite lengthy and you may run into hiccups, so I don’t recommend this if you’re new to embedded development.
Make sure that you’re using the
rust branch of the code, not the
master branch (that was used in previous articles)…
Verify that the
Note: This is the rust branch that contains a Rust application hosted on Mynewt
Here’s a video of our Rust application, hosted on Mynewt, running on an actual Blue Pill. It works as expected: Reading the Blue Pill’s internal temperature sensor every 10 seconds and displaying the results— Yay!
With some simple build integration, we have proven that it’s indeed possible to host Rust applications on Mynewt (and probably other embedded operating systems too). And it requires very little RAM and ROM to support embedded Rust.
Perhaps the bigger challenge is to design a Rust API for Mynewt that’s safe and easy to use.
In the meantime, what we have created is probably sufficient for new embedded developers (and my students) to create IoT devices in Rust and Mynewt without fear of memory corruption. Saving the world from unsafe embedded C programming… one microstep at a time!
Further Reading for Embedded Rust
1️⃣ “The Embedded Rust Book” was highly informative for embedded Rust programming on Arm Cortex M3
2️⃣ “Writing an OS in Rust” has plenty of tips for coding operating systems in Rust
💎 The following Advanced Topic sections are for advanced developers. If you’re new to embedded programming, you may stop here.
💎 Advanced Topic: Calling C from Rust and back
In this section we go into detail to see how C functions and
structs are imported into Rust, and how to export Rust functions to C.
Import C Functions into Rust: Rust ⟶ C
For our demo Rust application, we shall be calling the Mynewt Sensor API to configure the onboard temperature sensor to be polled every 10 seconds. After polling the sensor, Mynewt should call a Listener Function (defined in Rust) to display the result.
To import the Mynewt Sensor API functions into our Rust application, we should include this code…
How did we derive the Rust import declarations above? Let’s look at the C declarations for the three Mynewt functions (right column) and see how they map to the Rust import declaration (left column)…
#[link(name = “hw_sensor”)]declares to Rust that the C functions to be imported will be located in the compiled C library archive
hw_sensor.a(which will be generated by the Mynewt build)
i32in Rust is equivalent to
intin C (signed 32-bit integer)
u32in Rust is equivalent to
uint32_tin C (unsigned 32-bit integer)
u8in Rust is equivalent to
uint8_tin C (unsigned byte)
*const u8in Rust is equivalent to
const uint8_t *in C. We use this to pass
const uint8_t *has the same size as
const char *
structwe imported from Mynewt to pass the listener info from Rust to C (to be explained in a while)
*mut SensorListenerin Rust is equivalent to
struct SensorListener *in C
*mut SensorListenerand not
*const SensorListener? Because
SensorListenerwill be stored and updated by Mynewt.
- We define
SensorPtrrepresents a pointer to a Mynewt
struct. Since we are just passing the pointer through from Rust to Mynewt, without touching its contents, we declare
void *type, which is
*const CVoidin Rust.
CVoidis our Rust equivalent of
Import C Structs into Rust: Rust ⟵📦⟶ C
The Rust code above should be included into our Rust application so that the
struct may be passed through
sensor_register_listener(). To understand how the Rust import declaration was derived, let’s compare the Rust import declaration with the Mynewt declaration in C…
#[repr(C)]tells Rust that this
structis used by both Rust and C functions. The
structmemory layout shall be fixed according to the C convention.
#[repr(C, packed)]is used for C
SensorDataFuncare defined in the Sensor Types Definition code above
SensorDataFuncis a pointer to a callback function in Rust that Mynewt will call from a C function. This is explained below.
sl_nextis defined as
u32(32-bit unsigned integer) because we should initialise the field to
0in our Rust code.
sl_nextis updated by Mynewt to store the pointer to the next
SensorListenerin the linked list
Export Rust Functions to C (Callback): Rust ⟵ C
We have seen how C functions and structs may be imported into Rust. Now let’s see how Rust functions may be called from C.
sensor_register_listener() registers a Listener Function (defined in Rust) that will be called by Mynewt when the temperature sensor has been polled.
sensor_register_listener() accepts a
LISTENER here) as a parameter. The
sl_func, the pointer to the Listener Function.
Here we pass the Rust function
read_temperature() as the Listener Function.
read_temperature() is declared as
extern so that it may be called from C as a callback function (instead of calling by function name). Note that
read_temperature() has the same function signature as
sl_func (which has type
Export Rust Functions to C (Call By Name): Rust ⟵ C
Previously we have seen how C may call a Rust function via callback (i.e. the Rust function is passed to C as a pointer). What if our C function wishes to call a Rust function by its function name?
Normally the Rust compiler converts Rust function names into a “mangled” format that includes the function signature. So the
read_temperature() function would be compiled and renamed as:
But if we are calling
read_temperature() from C, this becomes a problem because the function name has changed. To prevent the mangling of the Rust function name, use the
#[no_mangle] directive like this:
So in the above code, the function
main() will not be renamed by the Rust compiler. The
pub extern “C” directive tells the Rust compiler to export the Rust function
main() to C functions.
From a C function, we may now call the function by its Rust name,
main(). In fact that’s how Mynewt starts our Rust application.
This interoperability of Rust and C is known as the Foreign Function Interface. Read the details here.
💎 Advanced Topic: Enhancements
Although we have done a lot in this article, we haven’t provided a comprehensive and maintainable environment in Mynewt for hosting all kinds of Embedded Rust applications.
Here are my suggestions for improving the integration of Rust with Mynewt. If you’re keen to work on the integration of Rust with Mynewt, lemme know!
- Rust folders should follow the Mynewt folder structure:
Move Rust source files from
apps/my_sensor_app/srcGenerate compiled Rust object files in
- Auto-generate the Rust code for importing Mynewt C functions and
bindgenis a tool that we could use to generate the Rust import declarations by feeding in the Mynewt header files. More details here.
- Create safe Rust wrappers for the complete Mynewt API. We may need lots of thinking to create a safe Rust API for Mynewt that’s easy to use and error-proof.
- Rust wrappers for Mynewt should return
Resulttype instead of integers (thanks to Marcel Hellwig). Some APIs may benefit from a chainable API like this
- CoAP (JSON and CBOR) API for Rust, similar to the network-agnostic Sensor Network API
- Allow Mynewt drivers and libraries to be created with Rust
- Support other microcontroller platforms besides Blue Pill
- Support text formatting and heap allocator in Rust. While keeping RAM and ROM usage low.
- Keep monitoring for RAM and ROM bloat as we implement enhancements. For example, the ROM size dropped from 73 KB to 55 KB after removing the
unwrap() / panic()error checking. Check the memory map before and after. (Learn more about memory maps)