Loading header...

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

flowchart TB APP[Application] --> SENSOR[Temperature service] SENSOR --> I2CDRV[I2C temperature driver] I2CDRV --> HAL[I2C HAL] HAL --> REG[MCU registers]

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 true or false for 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.

Mind Map

mindmap root((Device Drivers)) Core concept Hardware behind contract Registers localized Errors explicit Applications Sensors UART SPI I2C Flash memory Motor drivers Calculations Timeout ticks Bus frequency Buffer length Fixed point scale Design rules Name units Use timeouts Define ownership Keep ISR short Practical checks Fake bus tests Register masks Error paths Datasheet timing Common mistakes Magic registers No timeout Boolean errors Hidden scaling