Hardware Abstraction and Device Drivers
Device drivers translate application intent into peripheral operations. Good drivers hide register details without hiding important timing, error, and ownership rules.
Learning Objectives
By the end of this lesson, you should be able to define a driver interface, decide what belongs in a HAL, handle errors consistently, and make hardware-facing code testable.
Driver Boundaries
The application should ask for a temperature reading, not manipulate I2C start bits.
Interface Design
enum temp_status {
TEMP_OK,
TEMP_BUS,
TEMP_CRC
};
typedef struct {
int32_t milli_c;
} temp_sample;
int temp_init(void);
int temp_read(temp_sample *s);
The driver reports meaningful errors and uses fixed-point units so callers do not guess scaling.
Register Access
For MCU peripherals, keep bit manipulation close to the register layer. Use named masks, avoid magic numbers, and document required ordering when hardware demands it.
#define TXE (1u << 3)
void uart_tx_on(UART_Type *u)
{
u->CR |= TXE;
}
Volatile is required for memory-mapped registers because the value can change outside normal program flow.
Blocking, Nonblocking, and Interrupt Drivers
- Blocking: simple startup transactions, but can miss deadlines.
- Polling state machine: cooperative firmware, but needs more state code.
- Interrupt driven: low-latency events, but can create races.
- DMA: high throughput, but needs clear buffer ownership.
Worked Example: SPI Flash Read
A robust external flash driver records chip-select timing, maximum SPI frequency, address length, busy timeout, erase-before-write rule, page size, and verify failure behavior. Timeouts should use monotonic time rather than open-ended loops.
Testability
Abstract bus operations so unit tests can inject fake responses.
typedef struct {
bool (*xfer)(uint8_t addr);
} i2c_bus_t;
Common Mistakes
- Letting application code write peripheral registers directly.
- Returning only
trueorfalsefor many different failures. - No timeout in driver loops.
- Hiding units and scaling.
- Doing too much work in interrupt handlers.
Summary
Drivers should provide stable, testable contracts around hardware behavior. Good interfaces expose units, errors, timing expectations, and ownership while keeping register details localized.
Further Reading
- MCU vendor HAL and low-level driver examples.
- Barr Group, "Embedded C Coding Standard" driver interface guidance.
- Memfault, "Practical Firmware Driver Design" articles.