In this lesson, you will learn how to decouple your driver logic from specific silicon by programming against embedded-hal traits instead of raw Peripheral Access Crates (PACs).
Beyond the Silicon
When you write a driver by interacting directly with a PAC—for example, toggling a specific register bit in an STM32's GPIOA_ODR—you create "silicon-locked" code. This code is impossible to port to an nRF or RP2040 chip without a complete rewrite. The embedded-hal crate solves this by defining standard interface traits for common hardware operations, such as OutputPin, I2c, or Spi.
Programming Against Traits
Instead of requiring a concrete type like stm32f4::pac::GPIOA, your driver accepts any type that implements the trait. This creates a contract: your driver doesn't care how a pin is toggled, only that the provided object satisfies the OutputPin trait's set_low() and set_high() methods.
Consider this sensor driver structure:
use embedded_hal::digital::OutputPin;
pub struct SensorDriver<P> {
pin: P,
}
impl<P: OutputPin> SensorDriver<P> {
pub fn new(pin: P) -> Self {
Self { pin }
}
pub fn trigger(&mut self) -> Result<(), P::Error> {
self.pin.set_high()?;
// ... internal delay logic
self.pin.set_low()
}
}
The Power of Generic Drivers
Because SensorDriver is generic over P, it is completely platform-agnostic. You can instantiate it with an STM32 GPIO pin, an I2C expander pin, or even a dummy pin used during unit testing on your host machine.
This approach leverages Rust's monomorphization; the compiler will generate a specialized version of trigger for each specific pin type used in your final application. Because these traits are designed as zero-cost abstractions, the generated assembly for set_high() usually boils down to the exact same bitwise register manipulation you would have written manually, but with the added benefit of being reusable across any hardware that supports the embedded-hal.
By targeting these traits, you ensure your drivers remain portable and maintainable as you rotate through different microcontrollers.
Key Takeaway: By coding against embedded-hal traits rather than hardware registers, you transform vendor-specific logic into portable, reusable, and testable drivers.