Embedded HAL Traits in Rust and Writing Portable Drivers
embedded-hal defines traits for pins, buses and delays so one sensor driver runs on any chip. Write a driver generic over I2C and see how to test it with a mock.

Before embedded-hal, every sensor library was written for one chip, and moving to another board meant rewriting the driver. The crate fixes that with a set of small traits. A chip's HAL implements them, a driver consumes them, and neither knows about the other. This post writes a temperature sensor driver generic over the I2C bus and tests it without hardware. It is a practical follow-up to the embedded Rust guide and an example of the principles in traits, generics and trait objects.
embedded-hal 1.0, Rust 1.90.
The driver
#![no_std]
use embedded_hal::i2c::I2c;
const ADDR: u8 = 0x48;
const REG_TEMP: u8 = 0x00;
pub struct Tmp<I2C> {
bus: I2C,
}
impl<I2C: I2c> Tmp<I2C> {
pub fn new(bus: I2C) -> Self {
Tmp { bus }
}
/// Temperature in hundredths of a degree Celsius.
pub fn read_centi_celsius(&mut self) -> Result<i32, I2C::Error> {
let mut buf = [0u8; 2];
self.bus.write_read(ADDR, &[REG_TEMP], &mut buf)?;
let raw = i16::from_be_bytes(buf) >> 4;
Ok(raw as i32 * 625 / 100)
}
pub fn release(self) -> I2C {
self.bus
}
}I2C: I2c accepts any bus type that implements the trait. The error type comes from the bus, I2C::Error, so a chip with a richer error enum passes it through untouched. Big endian parsing is explained in endianness and byte order.
Test it on the host
#[cfg(test)]
mod tests {
use super::*;
use embedded_hal_mock::eh1::i2c::{Mock, Transaction};
#[test]
fn reads_25_degrees() {
let expectations = [Transaction::write_read(0x48, vec![0x00], vec![0x19, 0x00])];
let mut sensor = Tmp::new(Mock::new(&expectations));
assert_eq!(sensor.read_centi_celsius().unwrap(), 2500);
sensor.release().done();
}
}The mock records the exact bytes the driver must send and returns scripted replies; done() asserts every expectation was used. 0x19 0x00 is 25 degrees in this sensor's format, giving 0x190 * 625 / 100 = 2500. This test runs with cargo test on your laptop, requires the std feature of the mock only in dev dependencies, and finishes in milliseconds.
The traits you will meet
| Trait | Purpose |
|---|---|
digital::OutputPin, InputPin | GPIO |
i2c::I2c | I2C bus |
spi::SpiDevice, SpiBus | SPI with chip select handling |
delay::DelayNs | delays |
pwm::SetDutyCycle | PWM |
Sharing a bus
Several sensors on one I2C bus need shared access. Wrap the bus with embedded-hal-bus types (RefCellDevice on single core, CriticalSectionDevice with interrupts), and give each driver its own handle. The driver code does not change, which is the point.
Limits
Traits cover the common peripherals, not everything: DMA, clock trees and chip specific modes stay in the HAL. For async executors, use embedded-hal-async with Embassy. The specification is at docs.rs for embedded-hal.


