Embedded Abstractions — The Layer Cake
~15 min
Embedded Rust is built in layers. Understanding these layers is the single most important thing you'll learn today — because once you see the pattern, you can navigate any crate in the ecosystem.
The Five Layers
From lowest (closest to hardware) to highest (most convenient):
┌─────────────────────────────────┐
│ BSP │ Board Support Package
│ (board-specific config) │ e.g., uferris-bsp
├─────────────────────────────────┤
│ Driver Crates │ Hardware-agnostic drivers
│ (sensor/device drivers) │ e.g., icm42670, bmp180
├─────────────────────────────────┤
│ embedded-hal Traits │ The portable interface
│ (the "contract") │ e.g., InputPin, I2c
├─────────────────────────────────┤
│ HAL │ Hardware Abstraction Layer
│ (chip-specific safe API) │ e.g., esp-hal
├─────────────────────────────────┤
│ PAC │ Peripheral Access Crate
│ (raw register access) │ e.g., esp32c3 (auto-generated)
└─────────────────────────────────┘
↓ Hardware ↓
PAC — Peripheral Access Crate
The lowest layer. Auto-generated from SVD (System View Description) files — basically, a Rust representation of every register in the chip. You can use it directly, but you almost never need to.
#![allow(unused)] fn main() { // PAC-level: writing directly to a register // You don't want to do this. But it's good to know it exists. peripherals.GPIO.out_w1ts.write(|w| unsafe { w.bits(1 << 5) }); }
HAL — Hardware Abstraction Layer
This is where we'll spend most of our time. The HAL provides safe, ergonomic Rust APIs on top of the PAC. For ESP32 chips, this is esp-hal.
#![allow(unused)] fn main() { // 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 contract — a standard interface that any HAL can implement. If your code uses embedded_hal::digital::OutputPin instead of esp_hal::gpio::Output directly, it can work on any chip.
#![allow(unused)] fn main() { // This function works on ANY microcontroller that implements OutputPin 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); } }
Driver Crates
Built on top of embedded-hal traits. A driver crate provides a high-level API for a specific sensor or device — and it works on any chip because it only depends on the traits, not on any specific HAL.
#![allow(unused)] fn main() { // 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(); }
BSP — Board Support Package
The highest layer. A BSP pre-configures everything for a specific board — pin assignments, peripheral setup, default configurations. Instead of remembering pin numbers, you use meaningful names.
#![allow(unused)] fn main() { // Without BSP: which pin is the LED again? let led = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default()); // With BSP: the board knows let led = board.led(); }
When you're reading documentation, knowing which layer you're looking at tells you *what kind of information to expect*. HAL docs show chip-specific APIs. Driver crate docs show device-specific APIs. embedded-hal docs show the contract between them.
How the Layers Connect
The power of this architecture:
- esp-hal implements
embedded-haltraits for ESP32-C3 hardware - Driver crates are written against
embedded-haltraits - So any driver crate works with any HAL — no glue code needed
This is why you can take an IMU driver written by someone who's never touched an ESP32, plug it into esp-hal's I2C, and it just works. The traits are the contract that makes this possible.
In the next section, we'll see how this maps to a pattern you'll use for every peripheral.