Device Drivers

Drivers hide hardware details behind clean interfaces. Learn driver layers and APIs, initialisation and error handling, interrupt-driven I/O with ring buffers, sizing buffers from data rates, blocking vs non-blocking designs, RTOS-aware drivers, Linux kernel/device-tree basics, and testing drivers.

Good drivers make applications portable and robust: the application calls uart_write() without caring whether the bytes go out by polling, interrupt or DMA, on STM32 or NXP. Bad drivers lose data, block forever or corrupt memory.

An analogy

A driver is a translator between the application (who speaks 'send this message') and the hardware (who speaks 'write bit 7 of register 0x40011004').

Driver structure

init(config), read/write, control (ioctl-like), status/error queries, and deinit. Hide registers in the .c file; expose a small, documented API in the .h file.

Buffers and concurrency

Ring buffers decouple ISRs from applications. Size them from data rate × worst-case delay. Use volatile indices, single-writer rules, or critical sections; with an RTOS, signal tasks with semaphores/queues from ISRs.

Error handling

Detect and report overruns, framing/parity errors, timeouts and NACKs. Return error codes, never block forever — use timeouts. Provide recovery (peripheral reset, bus unlock for I²C).

OS drivers and testing

Linux drivers register with subsystems (char, I²C, SPI, IIO, net) and get hardware info from the device tree. Zephyr uses devicetree and a generic driver model. Test with hardware-in-the-loop, mocks for host unit tests, and fault injection.

Formulas

Worked example: Buffer size

Given: 115 200 baud 8N1; Main loop may block up to 20 ms

Find: RX buffer size.

Answer: 256 bytes

Worked example: Ring occupancy

Given: N = 256, head = 10, tail = 250

Find: Bytes available.

Answer: 16 bytes

Interrupt-driven UART RX driver with a ring buffer (C)

#include <stdint.h>
#include <stdbool.h>

#define RX_SIZE 256u                        /* power of two */
static volatile uint8_t  rx_buf[RX_SIZE];
static volatile uint16_t rx_head, rx_tail;  /* ISR writes head, app reads tail */
static volatile uint32_t rx_overruns;

void USART2_IRQHandler(void) {              /* producer */
    uint8_t byte = (uint8_t)USART2->DR;     /* reading DR clears RXNE */
    uint16_t next = (rx_head + 1u) & (RX_SIZE - 1u);
    if (next == rx_tail) { rx_overruns++; return; }   /* full: count and drop */
    rx_buf[rx_head] = byte;
    rx_head = next;
}

bool uart_read(uint8_t *out) {               /* consumer, non-blocking */
    if (rx_tail == rx_head) return false;    /* empty */
    *out = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1u) & (RX_SIZE - 1u);
    return true;
}

uint16_t uart_available(void) { return (rx_head - rx_tail) & (RX_SIZE - 1u); }

A single-producer/single-consumer ring buffer needs no locks: only the ISR changes head and only the application changes tail. Overruns are counted instead of silently lost, making problems diagnosable.

Common mistakes

Applications

Key takeaways

Going further