Skip to content

Latest commit

 

History

History
692 lines (501 loc) · 24.3 KB

25_rust_on_embedded_systems.md

File metadata and controls

692 lines (501 loc) · 24.3 KB

🦀 30 Days of Rust: Day 25 - Rust on Embedded Systems 🛠️

LinkedIn Follow me on GitHub

Author: Het Patel

October, 2024

<< Day 24 | Day 26 >>

30DaysOfRust


📘 Day 25 - Rust on Embedded Systems 🛠

Welcome to Day 25 of the 30 Days of Rust Challenge! 🎉

Today, we step into the realm where software meets hardware: embedded systems. Whether it's blinking LEDs, managing sensors, or building IoT devices, Rust’s features make it a standout choice for embedded programming. With its focus on safety, performance, and control, Rust is redefining how we write firmware.

Let’s dive into the intricate and exciting world of Rust for Embedded Systems! 🚀

👋 Welcome

Welcome to Day 25 of the 30 Days of Rust Challenge! 🎉

Today, we’re diving into Embedded Systems Development with Rust, where Rust shines with its focus on:

  • Safety in systems programming.
  • Memory efficiency.
  • Low-level control and concurrency.
  • How to configure Rust for embedded development.
  • Key concepts like no_std, interrupts, and peripherals.
  • Building, flashing, and debugging firmware.

By the end of this lesson, you will:

  • Set up a Rust environment for embedded systems development.
  • Write a basic Rust program for a microcontroller.
  • Learn tools and frameworks to streamline embedded development.

🔍 What Are Embedded Systems?

Embedded systems are computers designed for specific tasks, often integrated into larger systems. Unlike general-purpose computers, they operate under strict constraints: limited memory, real-time requirements, and low power consumption.

Examples of Embedded Systems:

  • Home automation (e.g., smart thermostats 🌡️, light controls 💡).
  • Automotive systems (e.g., ABS 🚗, engine control units).
  • Consumer electronics (e.g., washing machines 🧺, smart TVs 📺).
  • Robotics 🤖 and drones 🚁.

Common Characteristics of Embedded Systems

  • Real-Time Performance: Must meet strict timing deadlines.
  • Resource Constraints: Limited CPU, memory, and storage.
  • Direct Hardware Access: Close interaction with peripherals like GPIO, ADC, and UART.

🤔 Why Rust for Embedded Systems?

Rust addresses many challenges in embedded development by offering:

Feature Rust C/C++
Safety Memory safety by default, no null pointers Manual memory management
Concurrency Fearless concurrency with borrow checker Prone to data races
Zero-Cost Abstractions High-level features with no runtime cost Requires manual optimization
Tooling Cargo, Clippy, Rustfmt Less standardized

Rust’s ownership model prevents common bugs like null pointer dereferences and data races, making it an excellent choice for writing secure firmware.

Embedded systems often have strict constraints on:

  • Memory usage
  • Power consumption
  • Real-time performance

Rust is ideal for embedded systems because:

  1. Zero-cost abstractions ensure minimal overhead.
  2. Safety features like memory and concurrency safety help prevent bugs.
  3. No runtime means Rust can run on bare metal.
  4. Modern tooling enhances productivity.

🛠 Setting Up Your Environment

💻 Installing Rust

Install Rust with the nightly toolchain, which is required for embedded development:

rustup install nightly
rustup default nightly
rustup target add thumbv7em-none-eabihf

⚙ Installing Cargo Tools

Install these essential tools for embedded Rust:

cargo install cargo-generate cargo-embed

🛡 Installing a Cross-Compiler

Install the arm-none-eabi-gcc cross-compiler:

  • Ubuntu:
    sudo apt install gcc-arm-none-eabi
  • macOS:
    brew install arm-none-eabi-gcc

🔗 Setting Up OpenOCD (Optional)

Install OpenOCD for advanced debugging:

  • Ubuntu:
    sudo apt install openocd
  • macOS:
    brew install openocd

🧰 Hardware Requirements

You’ll need:

  • A development board (e.g., STM32, ESP32, Raspberry Pi Pico).
  • A USB-to-serial programmer or built-in debugger.

⚡ Writing Your First Embedded Program

1. Blink an LED on a Microcontroller

Hardware Setup

  • Use an ARM Cortex-M-based microcontroller (e.g., STM32).
  • Connect an LED to a GPIO pin.

Rust Code

Create a new project with the --bin flag:

cargo new led_blink --bin
cd led_blink

Modify Cargo.toml:

[dependencies]
cortex-m-rt = "0.7.3"
stm32f3xx-hal = "0.7.0"
panic-halt = "0.2.0"

[build-dependencies]
cc = "1.0"

Write the LED blinking program (src/main.rs):

#![no_std]
#![no_main]

use panic_halt as _; // Halt the program on panic
use cortex_m_rt::entry;
use stm32f3xx_hal::{
    prelude::*,
    pac,
};

#[entry]
fn main() -> ! {
    // Access the peripherals of the microcontroller
    let dp = pac::Peripherals::take().unwrap();
    
    let mut rcc = dp.RCC.constrain();
    let mut gpioc = dp.GPIOC.split(&mut rcc.ahb);
    
    let mut led = gpioc.pc13.into_push_pull_output();
    
    loop {
        led.set_high().unwrap();
        cortex_m::asm::delay(8_000_000); // Delay
        led.set_low().unwrap();
        cortex_m::asm::delay(8_000_000); // Delay
    }
}

2. Key Concepts

  1. no_std and no_main

    • no_std: Excludes Rust's standard library, as it requires an OS.
    • no_main: Provides custom entry points for bare-metal programs.
  2. Hardware Abstraction Layer (HAL)

    • HAL crates like stm32f3xx-hal provide high-level abstractions for peripherals.
  3. Delay and GPIO Control

    • Use asm::delay for precise timing.
    • Configure GPIO pins for output and control their states (high or low).
  4. Panic Handling

    • Use panic-halt to halt execution on panic.

🧰 Tools and Frameworks for Embedded Rust

  • cargo-embed: Tool for flashing and debugging embedded Rust programs.

    cargo install cargo-embed
  • probe-rs: Multi-architecture debugging tool.

  • Real-Time Operating Systems (RTOS):

    • Integrate real-time kernels like RTIC or FreeRTOS with Rust for multitasking.
  • rtt-target: Debug output over the SWD interface.

📊 Understanding Memory Management in Embedded Systems

Embedded systems often deal with limited memory:

  1. Static Memory Allocation

    • Use global or stack-based variables.
  2. Heap Allocation

    • Avoid unless necessary. Embedded environments have constrained dynamic memory.
  3. Interrupt Handlers

    • Use interrupt handlers sparingly to avoid stack overflow.
  4. Memory Protection

    • Rust’s borrow checker ensures no dangling pointers or data races.

🔗 Key Concepts in Embedded Rust

🚀 no_std and no_main

Embedded Rust often operates without an OS, so it excludes the standard library using the no_std attribute.

Example: Minimal no_std Application

#![no_std]
#![no_main]

use panic_halt as _;

#[cortex_m_rt::entry]
fn main() -> ! {
    loop {
        // Your embedded logic
    }
}

📟 Hardware Peripherals

Accessing peripherals like GPIO, UART, and I2C is fundamental in embedded systems. Rust uses Hardware Abstraction Layer (HAL) crates for this.

Example: Controlling GPIO

use stm32f4xx_hal::{pac, prelude::*};

#[cortex_m_rt::entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();

    loop {
        led.set_high();
        cortex_m::asm::delay(1_000_000);
        led.set_low();
        cortex_m::asm::delay(1_000_000);
    }
}

⚡ Interrupts

Interrupts handle hardware events asynchronously.

Example: Handling an Interrupt

#[interrupt]
fn TIM2() {
    // Handle Timer 2 interrupt
}

📦 Starting an Embedded Rust Project

🛠️ Create a New Project

Use cargo-generate to scaffold a project:

cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart

🛡️ Configure Your Target

Edit .cargo/config.toml to specify your target:

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

🖋️ Write Your Firmware

Edit src/main.rs to add your embedded logic.

📤 Build and Flash

Build and flash your firmware:

cargo build --release
cargo embed

🔄 Essential Tools and Crates

Tool/Crate Purpose
cortex-m Core support for Cortex-M processors.
cortex-m-rt Runtime for ARM Cortex-M chips.
embedded-hal Abstractions for hardware peripherals.
panic-halt Minimal panic handler for embedded systems.
probe-rs On-chip debugging and flashing.
defmt Lightweight logging for embedded systems.

Here are some tools and crates that simplify embedded development with Rust:

  • embedded-hal: A set of hardware abstraction interfaces for common peripherals like GPIO, UART, and I2C.

  • cortex-m-rtic: Enables real-time interrupt handling for ARM Cortex-M microcontrollers.

  • probe-rs: A tool for flashing and debugging embedded systems.

  • cortex-m: Provides low-level access to ARM Cortex-M microcontrollers.

💻 Example Project: Blinking an LED

Let’s implement a simple project to blink an LED on your microcontroller.

🖊️ Writing the Firmware

Here’s the Rust code for blinking an LED:

#![no_std]
#![no_main]

use panic_halt as _; 
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let gpioc =

 dp.GPIOC.split();
    let mut led = gpioc.pc13.into_push_pull_output();

    loop {
        led.set_high();
        cortex_m::asm::delay(8_000_000);
        led.set_low();
        cortex_m::asm::delay(8_000_000);
    }
}

🚀 Flashing the Firmware

cargo embed

To deploy the firmware to your device:

cargo run --release

Ensure the debugger or flashing tool is connected to the microcontroller.

🛡 Debugging Tips

Use probe-rs or OpenOCD for debugging.
Debugging embedded systems can be challenging. Here are some tips:

  • Use Serial Output: Print debug messages to the serial monitor for runtime inspection.
  • Check Connections: Ensure your GPIO and power connections are secure.
  • Debugging Tools: Use tools like probe-rs for flashing and debugging:
    probe-rs debug

🎯 Hands-On Challenge

Objective:

This hands-on challenge will guide you through using Rust for embedded systems development, specifically targeting microcontrollers. The focus will be on learning the basic steps to set up a Rust project for embedded systems, writing simple embedded code, and using debugging tools to test your code.

Challenge 1: Setting Up Your Embedded Rust Project

To get started, you'll first need to set up a basic Rust project that targets an embedded system. This will involve:

Step 1: Install Rust and Required Toolchains

  1. Install Rust: Ensure you have Rust installed by running:

    rustup --version

    If it's not installed, download it from the official website.

  2. Install the nightly version of Rust (required for embedded development):

    rustup install nightly
    rustup default nightly
  3. Install the target architecture for the embedded system. If you're working with a common microcontroller like ARM Cortex-M, add the following target:

    rustup target add thumbv7em-none-eabihf

Step 2: Create a New Project

Now, create a new project using cargo:

cargo new --bin embedded_rust_project
cd embedded_rust_project

Step 3: Update Cargo.toml

In the Cargo.toml file, add the dependencies for embedded systems. For example, if you’re using the cortex-m and cortex-m-rt crates for ARM Cortex-M:

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"

Challenge 2: Writing Simple Embedded Code

Now that your project is set up, let’s write a simple embedded program that toggles an LED (assuming your hardware supports it).

Step 1: Write the Main Code

Open the src/main.rs file, and add the code for blinking an LED. For this example, you can use the cortex-m crate to interact with GPIO pins.

Here’s an example of basic code that runs on a microcontroller and toggles an LED:

#![no_std]
#![no_main]

use cortex_m::prelude::*;
use cortex_m_rt::entry;
use stm32f4xx_hal as hal; // Example for STM32F4 series

#[entry]
fn main() -> ! {
    let dp = hal::pac::Peripherals::take().unwrap(); // Get the peripherals
    let rcc = dp.RCC.constrain(); // Clock control
    let gpioa = dp.GPIOA.split(); // GPIO port A

    let mut led = gpioa.pa5.into_push_pull_output(); // Set pin A5 as output (LED pin)

    loop {
        led.set_high().unwrap(); // Turn LED on
        cortex_m::asm::delay(8_000_000); // Delay for a while
        led.set_low().unwrap(); // Turn LED off
        cortex_m::asm::delay(8_000_000); // Delay for a while
    }
}

This code assumes you're working with an STM32F4 microcontroller, but the logic is similar across most microcontrollers: configure the GPIO, and toggle the LED on and off.

Step 2: Build and Flash the Code

  1. Build your code for the embedded target:

    cargo build --release --target thumbv7em-none-eabihf
  2. Flash the firmware to your microcontroller (depending on your setup, you may use tools like OpenOCD, STM32CubeProgrammer, etc.).

Challenge 3: Debugging the Embedded System

Debugging embedded systems can be challenging, especially without the right tools. Here’s how to approach debugging:

Step 1: Use Debugging Tools

  1. Use GDB for Debugging:

    • Ensure you have GDB installed (you may need a version of GDB that supports your embedded system).
    • Use the following command to start GDB:
      gdb target/thumbv7em-none-eabihf/debug/embedded_rust_project
  2. Monitor Logs and Debug Output:

    • You can use probe-rs (a Rust crate) for debugging and flashing embedded systems.
    • Run the following to install probe-rs:
      cargo install probe-rs-cli
    • Flash and debug the system:
      probe-rs flash --chip <your-chip> --verbose

Challenge 4: Exploring Interrupts (Advanced)

In this challenge, you’ll work with interrupts to handle events like external button presses.

  1. Set Up the Interrupt: Configure the GPIO pin for an external button press (interrupt).
  2. Implement the Interrupt Handler: Write the code to trigger an event (e.g., toggle the LED) when the button is pressed.

Here’s a simplified example of handling an interrupt:

use cortex_m::peripheral::NVIC;
use cortex_m_rt::entry;
use stm32f4xx_hal::gpio::{Edge, Input, PullUp};
use stm32f4xx_hal::prelude::*;

#[entry]
fn main() -> ! {
    let dp = stm32f4xx_hal::pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();

    // Set PA0 as input for the button with pull-up resistor
    let mut button = gpioa.pa0.into_pull_up_input();

    // Enable interrupt on rising edge (button press)
    button.make_interrupt_source(&dp.SYSCFG);
    button.enable_interrupt(&dp.EXTI);
    NVIC::unmask(stm32f4xx_hal::pac::Interrupt::EXTI0);

    loop {
        // Wait for interrupt to trigger
        cortex_m::asm::wfi();
    }
}

#[interrupt]
fn EXTI0() {
    // Interrupt handler for button press
    // Toggle the LED or perform other actions
}

Challenge 5: Further Exploration and Optimization

After completing the basic setup and challenges, dive deeper by optimizing the system, exploring real-time operating systems (RTOS), or adding communication protocols like SPI or I2C.

💻 Exercises - Day 25

Exercise: Level 1

  1. Blink an LED: Use embedded-hal to toggle GPIO pins on and off.
  2. Read Sensor Data: Connect an I2C temperature sensor, such as the DS3231, and read temperature values.
  3. Use a Timer: Implement a delay function using a microcontroller timer.

🚀 Exercise: Level 2

  1. Interrupt Handling:

    • Configure an interrupt for a button press using the cortex-m-rtic crate.
  2. Serial Communication:

    • Transmit and receive data over UART and display debug information.
  3. PWM Control:

    • Control the brightness of an LED using Pulse Width Modulation (PWM).

🏆 Exercise: Level 3 (Advanced)

  1. Build a Sensor Network:

    • Use SPI or I2C to communicate with multiple sensors and display data on an OLED screen.
  2. Real-Time Operating System (RTOS):

    • Use the RTIC framework to create a task scheduler for handling multiple concurrent tasks.
  3. Device Drivers:

    • Write a Rust driver for a specific sensor or actuator, such as a DHT22 temperature and humidity sensor.

🎥 Helpful Video References

  1. Getting Started with Embedded Rust
  2. Rust for Embedded Developers

📚 Further Reading

Topic Resource
Embedded Rust Guide Embedded Rust Book
Peripheral Access Crates Awesome Embedded Rust
Using no_std Rust no_std Guide
Hardware Abstraction embedded-hal Documentation
Flashing and Debugging probe-rs Guide

📝 Day 25 Summary

Today, we explored the exciting world of Embedded Systems with Rust. Here's what we covered:

  • Key Concepts: no_std, no_main, GPIO, and interrupt handling.
  • Real Hardware Interfacing: Blinking LEDs, reading sensors, and using timers.
  • Advanced Topics: PWM control, RTOS basics, and creating device drivers.
  • Challenges: Building real-world applications using embedded-hal and RTIC.

Embedded Rust is a game-changer for building safe, efficient, and low-level systems. Practice the hands-on challenges to solidify your understanding.

You’ve unlocked a powerful domain with Rust! Continue experimenting and building!

Today, we learned how to develop embedded systems using Rust. We set up our development environment, explored key libraries like embedded-hal and cortex-m, and wrote our first embedded application to blink an LED. Rust is a powerful tool for embedded programming, offering memory safety and performance while allowing low-level control over hardware.

Why Rust for Embedded Systems?

Rust's memory safety, zero-cost abstractions, and powerful concurrency features make it a perfect fit for embedded development. With today's exercises, you're now equipped to start building reliable and high-performance firmware for microcontrollers.

As always, keep exploring and happy coding! 🚀

Stay tuned for Day 26, where we will explore Rust and WebAssembly in Rust! 🚀

🌟 Great job on completing Day 25! Keep practicing, and get ready for Day 26!

Thank you for joining Day 25 of the 30 Days of Rust challenge! If you found this helpful, don’t forget to Star GIF star this repository, share it with your friends, and stay tuned for more exciting lessons ahead!

Stay Connected
📧 Email: Hunterdii
🐦 Twitter: @HetPate94938685
🌐 Website: Working On It(Temporary)

<< Day 24 | Day 26 >>