In this lesson, you will learn how to design type-safe wrappers for I2C and SPI buses to prevent logical errors when interacting with unreliable external peripherals.
The Problem: External Non-Determinism
In embedded systems, external sensors are the most common source of "garbage" data. A malfunctioning sensor might hold the I2C clock line (SCL) low indefinitely, return malformed packets, or violate expected timing protocols. When your driver blindly trusts the bus, these glitches can propagate into your application logic, potentially causing panics or incorrect state transitions.
Enforcing Safety at the Type Level
To mitigate this, we treat the communication bus not as a raw interface, but as a fallible channel that requires validation at the entry point. We can use the Newtype pattern to encapsulate raw registers and mandate a validation step before the data is accessible to the rest of the system.
Consider a temperature sensor reading:
struct RawSensorData([u8; 2]);
pub struct ValidatedReading(u16);
impl ValidatedReading {
pub fn from_raw(raw: RawSensorData) -> Result<Self, BusError> {
// Perform protocol-level validation (e.g., CRC checks or parity)
let crc_ok = verify_checksum(raw.0);
let valid_range = (raw.0[0] as u16) < 0x0FFF;
if crc_ok && valid_range {
Ok(ValidatedReading((raw.0[0] as u16) << 8 | raw.0[1] as u16))
} else {
Err(BusError::InvalidData)
}
}
}
Protocol-Level Constraints
Beyond simple validation, we must handle bus hang-ups using the hardware watchdogs we discussed in previous lessons. When initiating an SPI transaction, we should use a bounded scope or a timer-based lock. If the peripheral fails to acknowledge or keeps the MISO line high during a transaction, the driver must be able to trigger a bus reset (toggling the peripheral's power or resetting the controller) rather than deadlocking the system.
By coupling these validation types with the embedded-hal traits, we ensure that the rest of our application code can only operate on ValidatedReading objects. This moves the burden of safety from the runtime logic to the compiler, effectively isolating the "danger zone" of the physical bus.
Key Takeaway: Never pass raw peripheral data into your application logic; force all bus inputs through a validation layer that transforms untrusted wire bytes into checked, domain-specific types.