Skip to main content

Overview: The Embedded Rust Ecosystem

·5 mins·
Table of Contents
WS-RUST-ESP - This article is part of a series.
Part 2: This Article

The Problem
#

Embedded Rust is exciting. The ecosystem is growing fast. But that growth creates a real challenge for learners:

  • Tutorials go stale. APIs change. Examples that worked six months ago might not compile today.
  • Examples cover one use case. A blinky tutorial shows you how to blink one LED on one pin. But what if you need a different pin? A different configuration?
  • Device and HAL crates are often underdocumented. You need to know how to extract what you need from the source and API surface.
  • Copy-paste gets you started, but doesn’t get you far. If you can only reproduce what someone else wrote, you’re stuck the moment you need something different.

The solution: learn to fish, not just eat fish. After this section, you’ll know how to navigate the ecosystem, read documentation, and adapt examples independently.

Embedded Development Options
#

There are three main approaches to embedded development:

Bare Metal — Direct hardware access with no OS. Maximum determinism and minimal overhead, but you manage everything yourself. Best for simple, resource-constrained, or timing-critical applications.

RTOS — A lightweight real-time operating system provides task scheduling and timing guarantees. Adds some overhead but enables multitasking with predictable behavior.

Embedded OS — A full operating system (e.g., Linux) running on the device. Highest overhead, but provides rich functionality (networking, filesystems, drivers).

In this workshop, we’ll be doing bare metal Rust using esp-hal, which is officially supported by Espressif.

Standard Library vs. Core Library
#

“Out of the Box” Rust is based on the Standard Library (std), which depends on OS system interfaces. For bare metal, we use the Core Library (core) — a platform-agnostic subset with no heap allocation, concurrency, or I/O assumptions.

ESP supports both approaches. The std approach uses esp-idf-hal (community supported). We use the core (bare-metal) approach with esp-hal (officially supported by Espressif).

The Full Rust “no-std” Stack
#

The Rust no-std stack comprises several layers. Let’s walk through each one.

The Full Rust no-std Stack

The full embedded Rust no-std ecosystem stack

Microarchitecture Crate
#

Provides low-level access to CPU-specific features: interrupt handling, system timer, core registers. For RISC-V (ESP32-C3), this is the riscv crate.

// Example: enabling interrupts at the CPU level
unsafe { riscv::interrupt::enable() };

Peripheral Access Crate (PAC)
#

Auto-generated from SVD files. A Rust representation of every register in the chip. Type-safe but low-level. For ESP32-C3: the esp32c3 crate.

// PAC-level: writing directly to a register
peripherals.GPIO.out_w1ts
    .write(|w| unsafe { w.bits(1 << 5) });

Hardware Abstraction Layer (HAL)
#

Safe, ergonomic Rust APIs on top of the PAC. This is where we’ll spend most of our time. For ESP32 chips: esp-hal.

// HAL-level: safe, readable, type-checked
let mut led = Output::new(
    peripherals.GPIO5,
    Level::Low,
    OutputConfig::default(),
);
led.set_high();

embedded-hal Traits
#

The key to portability. These traits define a standard interface that any HAL can implement — the contract between HALs and driver crates.

// Works on ANY microcontroller that implements embedded-hal
fn blink(
    pin: &mut impl OutputPin,
    delay: &mut impl DelayNs,
) {
    pin.set_high().unwrap();
    delay.delay_ms(500);
    pin.set_low().unwrap();
    delay.delay_ms(500);
}

Component Drivers (Driver Crates)
#

Built on top of embedded-hal traits. A driver crate provides a high-level API for a specific sensor or device, making it hardware-agnostic.

// This driver doesn't know about ESP32 —
// it just needs something that implements I2c
let mut imu = Icm42670::new(i2c, Address::Primary);
let accel = imu.accel_norm().unwrap();

Board Support Packages (BSP)
#

The highest layer. Pre-configures everything for a specific board: pin assignments, peripheral setup, default configurations.

// Without BSP: you manage pin numbers and config
let led = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default());

// With BSP: the board knows
let led = board.led();

The Mental Model
#

Embedded development follows a consistent pattern:

Instantiate → Configure → Control

This mental model works for every peripheral, every HAL, and every driver crate.

Instantiate
#

Microcontroller Diagram

Peripheral singletons in a microcontroller

The singleton pattern ensures only one instance of each peripheral exists. At the PAC level, Peripherals::take() claims ownership of all peripherals at once — this can only be called once.

let peripherals = Peripherals::take().unwrap();

Although the methods differ between HALs, they all do the same thing — take a peripheral singleton and create a driver instance:

esp-hal:

let peripherals = esp_hal::init(Config::default());
let led = Output::new(peripherals.GPIO0, Level::High, OutputConfig::default());

rp2040-hal:

let pac = pac::Peripherals::take().unwrap();
let sio = Sio::new(pac.SIO);
let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
let led = pins.gpio25.into_push_pull_output();

stm32f4xx-hal:

let pac = Peripherals::take().unwrap();
let gpioa = pac.GPIOA.split();
let led = gpioa.pa5.into_push_pull_output();

Configure
#

Once we have an instance, it provides methods to configure it:

esp-hal:

let button = Input::new(
    peripherals.GPIO9,
    InputConfig::default().with_pull(Pull::Up),
);

rp2040-hal:

let button = pins.gpio15.into_pull_up_input();

Control
#

The instance provides methods to control the peripheral:

if button.is_high() {
    led.set_high();
}

Things to Note
#

  • Configuration is sometimes passed as an argument to instantiation rather than as a separate step
  • Off-controller device drivers (e.g., I2C sensors) follow the same concept — they use microcontroller peripheral drivers to provide abstraction
  • HALs often implement embedded-hal traits to provide device drivers with a common interface

Navigating Documentation#

Step 1: Navigate to the Peripheral Module
#

Each peripheral typically gets its own module in a HAL crate. You can find the module list at the bottom of a HAL crate’s documentation page.

Click here to view the esp-hal ESP32-C3 crate modules.

Navigate to the Peripheral Module

Finding peripheral modules in crate documentation

Step 2: Find an Example
#

Simple examples are often shown in the module documentation. Most HAL repos also have a dedicated examples directory on GitHub.

Click here to view the blinky code example in the esp-hal.

Click here to view the examples folder in the esp-hal repository.

Finding examples in documentation

Code examples in module documentation

Step 3: Identify the Driver Struct
#

Use the example to apply the mental model. Identify the driver struct and how it is instantiated, configured, and controlled.

Identifying the Driver Struct

Identifying the driver struct in documentation

Step 4: Access the Driver Struct
#

Navigate to the driver struct’s documentation page to find all configuration and control methods available.

Click here to view the GPIO driver structs list in the esp-hal documentation.

Accessing the Driver Struct

Finding configuration and control methods

Key takeaway: The example shows ONE way. The docs show ALL ways. Your job is to explore.
WS-RUST-ESP - This article is part of a series.
Part 2: This Article

Related