Rust Embedded Example: Building Embedded Applications with Rust

Rust is rapidly gaining popularity in the embedded systems domain due to its safety, performance, and modern language features. It offers a powerful alternative to C/C++ for developing reliable embedded applications.

In this guide, we’ll walk through setting up a simple embedded application using Rust, specifically targeting a microcontroller.


Why Use Rust for Embedded Development?

  1. Memory Safety: Prevents common bugs like buffer overflows and null pointer dereferences.
  2. Concurrency: Modern concurrency primitives with no data races.
  3. Ecosystem: A growing ecosystem of crates like embedded-hal and cortex-m.
  4. Tooling: Rust’s tooling, such as cargo, simplifies project management.

Prerequisites

  1. Hardware:
    For this example, we’ll target an STM32 microcontroller. Any ARM Cortex-M-based microcontroller will work with minimal changes.

Install Required Tools:
Install cargo-embed and probe-rs for flashing and debugging:

cargo install cargo-embed

Add the Embedded Target:
Install the target for ARM Cortex-M microcontrollers (commonly used in embedded systems):

rustup target add thumbv7em-none-eabihf

Install Rust:
Ensure you have Rust installed via rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Setting Up the Project

Step 1: Create a New Project

Create a new Rust project:

cargo new rust_embedded_example --bin
cd rust_embedded_example

Step 2: Update Cargo.toml

Modify the Cargo.toml file to include the following dependencies:

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "0.2"

[dependencies.embedded-hal]
version = "0.2"

[dependencies.stm32f4xx-hal]
version = "0.12"
features = ["rt", "stm32f411"]

[profile.dev]
debug = true

[profile.release]
lto = true
  • cortex-m: Provides low-level ARM Cortex-M support.
  • cortex-m-rt: Runtime support for ARM Cortex-M.
  • panic-halt: Halts the program on a panic.
  • embedded-hal: Hardware Abstraction Layer (HAL) for embedded peripherals.
  • stm32f4xx-hal: HAL for STM32F4 microcontrollers.

Step 3: Configure the Embedded Target

Create a .cargo/config.toml file to specify the embedded target:

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

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

Step 4: Write the Code

Replace the content of src/main.rs with:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _; // Halts on panic
use stm32f4xx_hal::{
    gpio::{gpioc::PC13, Output, PushPull},
    prelude::*,
    stm32,
};

#[entry]
fn main() -> ! {
    // Get access to the device's peripherals
    let dp = stm32::Peripherals::take().unwrap();

    // Configure the clock
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.sysclk(48.mhz()).freeze();

    // Set up the GPIO pin
    let gpioc = dp.GPIOC.split();
    let mut led = gpioc.pc13.into_push_pull_output();

    // Blink the LED
    loop {
        led.set_high().unwrap();
        cortex_m::asm::delay(clocks.sysclk().0 / 2); // Delay
        led.set_low().unwrap();
        cortex_m::asm::delay(clocks.sysclk().0 / 2); // Delay
    }
}

Explanation of the Code

  1. No Standard Library:
    • #![no_std] indicates the application does not use Rust's standard library, suitable for embedded systems.
  2. Runtime Entry Point:
    • The #[entry] attribute defines the main entry point for the program.
  3. GPIO Configuration:
    • Configures the GPIO pin connected to an onboard LED as an output.
  4. Blinking Logic:
    • Toggles the LED state with a delay loop.

Building and Flashing the Program

Flash the Microcontroller:
Connect your STM32 microcontroller and run:

cargo embed

Build the Project:

cargo build --release

Testing the Application

Once flashed, the onboard LED should start blinking at a 1 Hz rate (1 second on, 1 second off). Modify the delay value to adjust the blinking frequency.


Extending the Example

  1. Add Peripherals:
    • Use the embedded-hal crate to interact with peripherals like UART, SPI, or I2C.
  2. Use RTIC for Concurrency:
    • RTIC (Real-Time Interrupt-driven Concurrency) is a framework for managing tasks and interrupts in embedded Rust.
  3. Integrate Sensors:
    • Add drivers for sensors like accelerometers or temperature sensors.
  4. Debugging:
    • Use probe-rs for debugging and monitoring.

Best Practices for Rust Embedded Development

  1. Minimize Resource Usage:
    • Optimize for limited memory and compute resources.
  2. Handle Panics Gracefully:
    • Use panic-halt or panic-reset to define behavior on panics.
  3. Use HAL Libraries:
    • Leverage hardware abstraction layers for cleaner code.
  4. Test on Hardware:
    • Always test your code on actual hardware to ensure proper functionality.

Conclusion

Rust’s safety and modern language features make it an excellent choice for embedded systems. By following this guide, you can build and deploy a simple embedded application on a microcontroller. As the Rust embedded ecosystem grows, it is increasingly suitable for a wide range of embedded projects, from simple tasks to complex IoT applications. Start building your embedded Rust projects today!