In this lesson, you will learn how to use Rust’s lifetime system to enforce hardware availability, ensuring that registers cannot be accessed after a device is powered down or disconnected.
The Problem: Hardware Liveness
In embedded systems, hardware registers live at fixed memory addresses. A common safety violation occurs when a driver attempts to read or write to a register after the peripheral has been disabled, reset, or removed from the system. If we simply expose these memory addresses as global statics, the compiler has no way of knowing if the hardware is actually "alive."
Encoding Presence with Lifetimes
To bridge this gap, we use a "Token" pattern. Instead of allowing direct access to a peripheral, we create a structure that holds a lifetime 'a, which is tied to the scope of the device’s initialization.
struct Peripheral<'a> {
base_address: *mut u32,
_marker: core::marker::PhantomData<&'a ()>,
}
impl<'a> Peripheral<'a> {
pub fn write_register(&self, offset: usize, value: u32) {
unsafe { self.base_address.add(offset).write_volatile(value) };
}
}
By tagging the Peripheral with a lifetime 'a, we tether the struct to the lifetime of the initialization context. If you wrap the hardware setup in a scope, the compiler ensures the Peripheral cannot outlive the configuration that enabled it.
Tracking Device Availability
We can extend this by creating a "Guard" that consumes the device. If the hardware is powered down, the guard is dropped, and because of the lifetime bounds, any register handles dependent on that guard become invalid.
pub fn run_driver<'a>(device: Peripheral<'a>) {
// We can only interact with hardware as long as 'device' exists.
device.write_register(0x04, 0x1);
}
If you try to move the Peripheral into a background task that outlives the hardware initialization block, the compiler will catch the lifetime mismatch. This enforces at compile-time that registers are strictly bound to the hardware's active state, effectively preventing "use-after-shutdown" bugs that are traditionally impossible to track in C-based drivers.
Key Takeaway: By binding hardware handles to a lifetime parameter representing the device's availability, we use the Rust compiler to guarantee that registers are only accessed while the peripheral is valid and powered.