Aaron Christiansen

Embracing Embedded Rust - from Serial.println to defmt

2025-02-20

Having started out with the Arduino ecosystem, I usually want to reach for serial UART communication when I need debugging or logging from an embedded system:

Serial.begin(9600);

// later:
Serial.println("Doing a thing");

Many development boards expose a UART interface over their USB port, making this a straightforward way to get text between the host and the device.

When I started learning embedded Rust, I noticed this pattern was far less common. Instead, authors were reaching for a library called defmt. A minimal usage of defmt looks like this:

use defmt::*;
use defmt_rtt as _;

// later:
info!("Doing a thing");

I had a lot of questions about this. Why do we need to pull in an extra library, in addition to our HAL crate, to get logging? Where’s the log text actually going, and how do we read it? Why are we importing defmt_rtt without using anything from it?

At first, this perceived complexity of defmt scared me away. After finally trying it in an example project, I was completely sold.

For anybody else on the fence, I’m going to cover the basics of defmt, and why it works great in the embedded Rust ecosystem.

What is defmt?

defmt was created by Ferrous Systems, a heavy contributor to the embedded Rust ecosystem, under their Knurling project.

Both a serial UART and defmt will end up displaying some text on your host, but the mechanism they use to build and transmit that text is very different.

A device using serial UART is typically expected to send complete ASCII bytes down the wire. If you need to interpolate variables into the message, then that is the device’s responsibility - possibly by formatting into some kind of fixed-size buffer, or concatenating heap-allocated strings.

defmt takes an entirely different approach. The “de” stands for “deferred”, meaning that message formatting happens on the host.

As a developer, we just write standard Rust string formatting:

info!("Task completed in {}ms", duration_ms);

On the device, defmt sends the individual pieces of this message - the string template to use, and the duration_ms value. The host is responsible for putting them together into a complete log message. 1

How do I receive defmt messages?

Receiving defmt messages requires more setup than a typical serial UART. The host needs to be running a client that is capable of decoding defmt’s message format, and reconstructing the plain-text message.

Fortunately, the tooling around defmt is really good! The flashing and debugging tool probe-rs integrates with defmt, so immediately after uploading your program to your microcontroller, it’ll start showing a stream of defmt output.

Console window showing a probe-rs upload, and then two messages - "Started" and "Initialised" - logged from defmt

There isn’t a single specific protocol which defmt uses under the hood. You select the “transport” you like by importing a crate which provides a global_logger - or implement your own - and defmt will automatically use that. 2

That’s why our code example from earlier has this seemingly-unused import. The defmt_rtt crate provides a global_logger, and we must reference the crate from our code to ensure it gets included in the build:

use defmt_rtt as _;

Specifically, this crate provides a transport based on the SEGGER Real Time Transfer (RTT) protocol. This is a good default choice for many ARM microcontrollers.

For the RTT transport, no additional configuration is needed - just include the crate and you’re ready to go. (You’ll never specify the wrong baud rate again!)

Can’t I just keep using serial UART?

You sure can - but if you’re coming from Arduino-land like me, there are a few points to be aware of.

Using serial UART feels like it should be easy, especially since all it takes on Arduino is a quick setup call to Serial.begin, and then you have access to Serial.println everywhere.

The HAL crates within the embedded Rust ecosystem typically require a little more setup code to get a UART up-and-running. That’s not a problem, though - easy to grab from an example or stumble through IDE autocomplete.

Now you’ve got your UART peripheral, you want to emit log messages from two different components in your software. However the embedded_io::Write trait takes an exclusive &mut self 3, so you can’t share it… oh no, it’s 🦀 the borrow checker! 🦀

You’ll need to reach for some kind of synchronisation primitive, like a hardware-specific Mutex implementation wrapping a RefCell, so you can access your UART from anywhere.

You really want your debugging and logging infrastructure to Just Work™, so by doing all this DIY setup, you risk building something brittle that gets in the way when you’re trying to solve an actual issue.

defmt’s logging macros are available everywhere, to use no-strings-attached, right out of the box - much like println! in a desktop Rust program. 4

You also need to think about how you’re going to build your log messages. Rust embedded projects with #[no_std] can’t allocate heap memory by default, so don’t have access to conveniences like String or the format! macro.

Crate are available which aim to provide allocation-free string formatting. The heapless crate provides a fixed-length string which implements Write for formatting capabilities. 5

With defmt’s namesake deferred formatting, you don’t need to think about this at all.

Are there any caveats with defmt?

Serial UART has a huge amount of library and OS support, with every major language offering a choice of cross-platform serial libraries. This gives you flexibility to write quick scripts to automatically gather and process data.

With defmt, you need support for both your transport and a defmt decoder. This isn’t a problem if you’re writing code in Rust, but you don’t necessarily have the freedom to grab your scripting language of choice.

You might also need external hardware to use the most popular transports. My go-to development board, the Raspberry Pi Pico, doesn’t have a debugger on-board, so I use a Raspberry Pi Debug Probe to connect with RTT.

Finally, defmt strictly deals with one-way communication, from the device to the host. If you want some kind of interactive console where the host can relay commands to the device, you might want to stick with serial UART for the lowest-friction approach.

  1. In reality, defmt doesn’t even transmit the entire string template. The strings are interned, so an index into a string table is sent instead. 

  2. global_logger defines some linker symbols with specific names, using the unsafe no_mangle attribute. defmt references these names without defining them. If you forget to include a transport crate, you’ll get a linker error - a rare occurrence in Rust! 

  3. Some HAL crates might give you a writer which only needs &self, such as rp-hal. But this trait doesn’t fit into the wider embedded ecosystem, and may still need you to pass lifetimes around in your structs. 

  4. I really value cases where languages break from their usual restrictions to give you easy ways to debug your program.
    There’s a world where Rust might require you to pass some kind of &mut Console around to print to stdout - but fortunately, that’s all abstracted away by the println! macro.
    One particular example I like is Haskell’s Debug.Trace package. Use of I/O in Haskell is encoded in the type system as a monad, so printing from a deeply-nested function requires that you propagate extra type information through your program. Debug.Trace provides an escape hatch without this impact. 

  5. Sometimes the UART peripheral might also implement Rust core’s Write - rp-hal does. This saves the need for a buffer.