Practical Integration of Zephyr and Rust: Initial Experience with the Embassy Framework

1. Background and Objectives

In traditional embedded development, C language remains the absolute protagonist. It is efficient and flexible, capable of controlling every detail of the hardware. However, as system complexity increases, the disadvantages of C become increasingly apparent:

  • Lack of memory safety mechanisms: prone to issues such as dangling pointers, memory leaks, and buffer overflows.
  • Weak constraints between modules, leading to unpredictable coupling and side effects.
  • Compilers struggle to catch logical errors, often relying on experience and testing to identify problems.

These issues pose significant risks in complex or safety-sensitive systems (such as medical devices, autonomous driving, and industrial control).

Why Choose Rust?

Rust is a rapidly emerging systems programming language characterized by:

  • Memory Safety: The compiler enforces the elimination of null pointers, dangling references, data races, and other issues at compile time through an “ownership model”.
  • No Runtime Overhead: Capable of achieving a <span>no_std</span> environment, suitable for bare metal.
  • Modern Syntax and Ecosystem: A rich set of crates and a powerful toolchain (such as <span>cargo</span>, <span>clippy</span>, <span>rust-analyzer</span>).
  • Good Concurrency Model: Naturally supports multithreading and asynchronous development.

What is Embassy?

Embassy is an asynchronous runtime framework designed specifically for embedded Rust, aiming to achieve safe and efficient asynchronous task management on low-power, resource-constrained MCUs without heap allocation.

The project aims to:

  • Implement an asynchronous-driven embedded system using Embassy + STM32F4.
  • Read temperature and humidity sensors (such as SHT30) via I2C and asynchronously transmit data via UART.
  • Demonstrate the practical feasibility and advantages of Rust in embedded development.

๐Ÿงต How Does Rust Achieve RTOS-like Task Mechanisms?

Rust itself is not an RTOS; it is a systems-level programming language. However, with the help of the asynchronous programming (<span>async/await</span>) mechanism and the accompanying asynchronous runtime (executor), Rust can achieve task scheduling and concurrency control similar to traditional RTOS in embedded development.

โš™๏ธ Basic Principles

  1. <span>async/await</span> Asynchronous Functions

Rust uses <span>async fn</span> to define a coroutine (task), which are converted into a state machine at compile time, automatically managing the suspension and resumption of tasks.

#[embassy_executor::task]
async fn blink() {
    loop {
        led.set_high();
        Timer::after(Duration::from_millis(500)).await;
        led.set_low();
        Timer::after(Duration::from_millis(500)).await;
    }
}

๐Ÿ” The compiler converts this <span>async fn</span> into a structure + state transition logic.

  1. Asynchronous Executor

Rust’s asynchronous tasks are not automatically run; they must be scheduled by an executor. Embassy provides its own executor, supporting:

  • Multitasking scheduling (configurable priority)
  • Cooperative or preemptive scheduling (experimental)
  • Low power support (via mechanisms like WFI)
#[embassy_executor::main]
async fn main(spawner: Spawner) {
    spawner.spawn(blink()).unwrap();
    spawner.spawn(uart_task()).unwrap();
}

Here, <span>spawner.spawn()</span> is similar to the traditional RTOS’s <span>xTaskCreate()</span><span>.</span>

  1. <span>no_std</span> and Bare Metal Support

Rust’s async runtime can run in environments without an operating system.

  • Using <span>#![no_std]</span> + bare metal startup code (such as <span>cortex-m-rt</span>)
  • Using Embassy’s <span>embassy-executor</span>, <span>embassy-time</span>, <span>embassy-sync</span>, etc. crates to implement scheduler, timer, semaphore, and other functionalities.

โœ… Comparison with Traditional RTOS

Feature Rust Async + Embassy Traditional RTOS (FreeRTOS/Zephyr, etc.)
Task Model <span>async fn</span> state machine, lightweight Thread model, independent stack space
Scheduling Cooperative / Experimental Preemption Typically preemptive scheduling
Synchronization Mechanism <span>embassy-sync</span> provides Mutex/Channel, etc. Semaphore, Queue, Mutex, etc.
Timer <span>embassy-time</span> provides <span>Timer::after()</span> Hardware timer based on Tick
Memory Usage Extremely low (task stack not allocated) Each thread requires independent stack allocation
Safety Rust guarantees memory safety and concurrency correctness at compile time Manual management is error-prone

2. Development Environment Setup

Installing Toolchain

rustup update stable
rustup target add thumbv7em-none-eabihf
cargo install probe-rs cargo-embed

<span>rustup update stable</span>: This command is used to update the Rust toolchain to the latest stable version. <span>rustup</span> is the official version management tool for Rust, similar to <span>pyenv</span> for Python or <span>nvm</span> for Node.js. <span>update stable</span> will download and install the latest stable version of Rust (currently, most embedded development recommends using the stable version). If you previously installed an older version of Rust, this command will help ensure your toolchain is up to date.

<span>rustup target add thumbv7em-none-eabihf</span> This command adds a “cross-compilation target” for the current Rust installation.

Rust by default only supports building for the native (e.g., x86_64-pc-windows-msvc or x86_64-unknown-linux-gnu).

<span>thumbv7em-none-eabihf</span> is a cross-compilation target suitable for MCUs like <span>ARM Cortex-M4/M7</span>, where:

  • <span>thumbv7em</span>: ARM Cortex-M’s Thumb instruction set (used for STM32F4 series)

  • <span>none</span>: Indicates bare metal (no OS)

  • <span>eabihf</span>: Uses hardware floating point (hard float ABI)

<span>cargo install probe-rs cargo-embed</span> This command installs two embedded debugging-related tools via Cargo:

<span>probe-rs</span>

  • A general debugging tool written in Rust, supporting various JTAG/SWD debuggers (such as ST-Link, J-Link).
  • Similar to OpenOCD, but more modern, cross-platform, and easy to install.
  • Provides subcommands like probe-rs run, probe-rs flash, probe-rs info, etc.

<span>cargo-embed</span>

  • A command-line tool wrapped around probe-rs, providing configurable debugging features.
  • Supports running, flashing, and viewing RTT logs, commonly used during the development and debugging phase.
  • It reads the Embed.toml configuration file, automatically selecting the probe, device, loading elf files, etc.

Creating an Embassy Project

Using <span>cargo generate</span> can quickly create a new Rust project from a Git template. Below is the recommended way to generate a project template from Embassy:

cargo generate --git https://github.com/embassy-rs/embassy-template.git
cd my-embassy-project

๐Ÿ”ง Command Explanation

<span>cargo generate --git https://github.com/embassy-rs/embassy-template.git</span>

  • This is a command for generating project templates.
  • It pulls template code from the specified Git repository and prompts you to fill in the project name.
  • This template is provided by Embassy and has integrated async runtime, hardware support configuration, memory layout, and other basic structures, suitable for quickly starting development.

๐Ÿ“ฆ Install <span>cargo-generate</span> (if not installed):

cargo install cargo-generate

<span>cd my-embassy-project</span>

  • Enter the project directory you just generated from the template.
  • The default project directory name depends on the project name you entered during the generation process, for example, <span>my-embassy-project</span>.

Configuring `.cargo/config.toml`

[target.thumbv7em-none-eabihf]
runner = "probe-rs run"

[build]
target = "thumbv7em-none-eabihf"

<span>[target.thumbv7em-none-eabihf]</span> section

is used to configure the behavior for the STM32F4 architecture (i.e., thumbv7em-none-eabihf).

<span>runner = "probe-rs run"</span>

  • Specifies the method of executing the program.
  • When you run the <span>cargo run</span> command, <span>cargo</span> will not attempt to execute the program on the PC, but will call <span>probe-rs run</span> to flash the program into the MCU and execute it.

Equivalent to:

cargo build --target=thumbv7em-none-eabihf
probe-rs run target/thumbv7em-none-eabihf/debug/your_project

<span>[build]</span> section

<span>target = "thumbv7em-none-eabihf"</span>

  • Sets the default build target platform to the embedded architecture STM32F4 (i.e., ARM Cortex-M4).
  • Eliminates the need to add <span>--target</span> every time you build.

Equivalent to:

cargo build --target=thumbv7em-none-eabihf

3. Hardware Connection Instructions

Device Pin Description
SHT30 I2C1 SDA/SCL Connected to the corresponding I2C pins of STM32F4
USB to Serial USART2 TX sends data to the PC port

4. Implementation of Asynchronous I2C and UART Drivers

I2C Initialization

let i2c = I2c::new(p.I2C1, p.PB8, p.PB9, Irqs, config);

Asynchronous SHT30 Temperature Reading Function

pub async fn read_temperature(i2c: &mut I2c&lt;'_&gt;) -&gt; Result&lt;f32, Error&gt; {
    let cmd = [0x2C, 0x06];
    i2c.write(0x44, &cmd).await?;
    Timer::after_millis(20).await;
    let mut buf = [0u8; 6];
    i2c.read(0x44, &mut buf).await?;
    let temp_raw = ((buf[0] as u16) &lt;&lt; 8) | (buf[1] as u16);
    Ok(-45.0 + 175.0 * (temp_raw as f32) / 65535.0)
}

UART Output Initialization

let mut uart = Uart::new(p.USART2, p.PA2, p.PA3, Irqs, uart_config);
uart.write("Temperature: 23.1\r\n").await?;

5. Complete Main Loop Task Implementation

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(config);
    let mut i2c = I2c::new(...);
    let mut uart = Uart::new(...);

    loop {
        match read_temperature(&mut i2c).await {
            Ok(temp) =&gt; {
                let mut buf = heapless::String::&lt;64&gt;::new();
                write!(buf, "Temp: {:.2}ยฐC\r\n", temp).unwrap();
                uart.write(buf.as_bytes()).await.ok();
            }
            Err(_) =&gt; {
                uart.write(b"Read error\r\n").await.ok();
            }
        }
        Timer::after_secs(2).await;
    }
}

6. Running and Debugging

  • Flashing command: <span>cargo embed</span> or <span>cargo flash</span>
  • Use serial port debugging tools to view UART output, such as <span>minicom</span>, <span>CoolTerm</span>, or <span>Putty</span>

7. Comparison Analysis of Embassy and Zephyr

Feature Zephyr Embassy
Language C Rust
Asynchronous Support Based on threads/callbacks async/await
Memory Model Heap can be used Default no heap
Safety Depends on developer experience Compile-time strong constraints
Learning Curve Medium Relatively high
Real-time Performance High High

Conclusion: For developers familiar with Rust and pursuing high safety, Embassy is a powerful choice; while Zephyr is more suitable for traditional engineering teams and scenarios requiring mature ecosystem support.

8. Future Expansion Directions

  • Implement MQTT network communication using Embassy (via ESP32/NINA WiFi module)
  • Use USB HID to transmit sensor data to the host
  • Compile Embassy into a static library, called by the Zephyr core, achieving a Rust+C mixed architecture
  • Research low-power management strategies based on Embassy

If you want to learn more about the practical deployment of Embassy + Rust, or if you would like me to provide a complete code repository link, please leave a comment or like this article, and I will gradually update detailed tutorials.

Leave a Comment