Documentation · Drivers

Drivers

Alquist drivers are small, explicit hardware adapters. They turn registers into stable kernel services without pretending that board-specific code is portable kernel policy.

[o-o] |=| /_\

A driver is a treaty with hardware. Keep the treaty short, write down every clause, and never trust a register you have not read back.

Driver Boundary

The core rule is simple: the kernel should call functions that describe intent, while the driver owns the board-specific register sequence. A console wants uart_puts() and uart_getc(), not AUX_MU_IO. A program wants disk_hw_read_lba(), not the eMMC command register dance.

Current Raspberry Pi 4 support is intentionally hardcoded for BCM2711. There is no device tree handoff in the Alquist examples. The board support code knows the MMIO base, the peripheral layout, and the interrupt IDs it needs. That makes the early system understandable and keeps firmware contracts visible.

LayerOwnsExamples
Kernel/runtimeScheduling, shell commands, fault reporting, program modelkernel_main(), scheduler tasks, monitor commands
Driver interfaceSmall functions with kernel-facing intentuart_getc(), timer_init(), disk_hw_read_lba()
Board supportMMIO addresses, GPIO alternate functions, GIC IDs, cache/DMA detailsBCM2711 mini UART, eMMC2, GENET

MMIO Rules

Memory-mapped registers are accessed through volatile 32-bit reads and writes. Drivers should keep the accessors boring: no cached pointer objects, no clever batching, no structure overlays for registers that may have side effects. A register write is an event, not just a store.

static inline void mmio_write(uintptr_t reg, uint32_t value)
{
    *(volatile uint32_t *)reg = value;
}

static inline uint32_t mmio_read(uintptr_t reg)
{
    return *(volatile uint32_t *)reg;
}

Early boot may run with the MMU disabled. On Cortex-A72 that means unaligned paired loads can fault even if SCTLR_EL1.A is clear later. Driver parsers that inspect external byte formats should prefer byte-wise helpers or compile with strict alignment when the data may be unaligned.

GPIO and LED

GPIO support begins with two operations: select a pin function and write a pin level. For the Raspberry Pi 4 ACT LED path, GPIO42 is configured as output and then toggled from a heartbeat task or timer-aware LED helper. The driver should not be a busy-loop animation buried inside IRQ bookkeeping. A visible LED is a health signal: if it stops, something important stopped making progress.

Register familyPurpose
GPFSELnSelect input, output, or alternate function for each GPIO pin.
GPSETn / GPCLRnSet or clear output pins without read-modify-write races on the output latch.
GPPUPPDNnConfigure pull-up/pull-down state for input and alternate-function pins.

GPIO setup code should name the pin, the function, and the electrical expectation. For UART, GPIO14 and GPIO15 use ALT5. For the ACT LED, GPIO42 uses output mode. Diagnostic pins used for adapters should document whether the external signal is active-high or active-low.

UART

UART is both the first console and the last fallback. The Raspberry Pi 4 hardware path uses the mini UART in the AUX block on GPIO14/15. QEMU uses PL011 because the emulated raspi4b serial path is different. The public interface stays the same.

FunctionContract
uart_init()Configure pins, clocks, baud rate, line format, and enable RX/TX.
uart_putc() / uart_puts()Write text to the console. Newline output normally expands to CRLF for terminals.
uart_getc()Wait for one buffered input byte while keeping output service alive where possible.
uart_try_getc() / uart_try_read()Non-blocking input for shells and monitor loops.
uart_rx_drain()Move every currently available hardware byte into the software RX ring.

Polling the mini UART receive FIFO is not enough for an interactive system. At 115200 baud a byte arrives in roughly 0.1 ms, and the mini UART FIFO is only eight bytes deep. If the kernel waits 1 ms before polling, a fast terminal can overrun the FIFO. Alquist now treats received characters as interrupt-driven data: AUX RX interrupt drains the hardware FIFO into a software ring buffer, and the shell reads from that ring.

if (interrupt_id == AUX_IRQ) {
    uart_rx_drain();
    gic_finish(iar);
    return old_stack_pointer;
}

The interrupt handler must not parse commands. It only drains bytes. Command editing, backspace handling, aliases, and shell dispatch remain task-level work. This keeps IRQ latency predictable and prevents a noisy UART session from running arbitrary monitor logic at interrupt level.

The mini UART hardware enable sequence is short but order-sensitive: enable AUX, disable UART while configuring, clear interrupts, configure 8-bit mode, set baud, choose GPIO alternate functions, then enable RX and TX. The interrupt enable bit is separate from RX enable; both must be set for interrupt-driven input to work.

Mailbox

The VideoCore property mailbox is the firmware call path used while the system is still small. It is not a general IPC layer. Drivers use it for board services that are difficult or undocumented as direct registers: core clock setup, power-state requests, framebuffer allocation, and reboot handoff sequences.

Property messages are 16-byte aligned buffers. The driver writes the physical address plus channel number to mailbox 1, then waits for the same token to appear from mailbox 0. The response code must be checked; a returned message address alone does not mean the property tag succeeded.

message = (buffer_address & ~0xFu) | MAILBOX_CHANNEL_PROP;
write MAILBOX1_WRITE
wait until MAILBOX0_READ == message
check buffer[1] == 0x80000000

Mailbox calls should be kept out of high-frequency paths. They are firmware transactions, not cheap register writes. Use them during initialization, display setup, SD power setup, and controlled reboot flows.

Timer

The generic timer gives Alquist a stable heartbeat. On hardware, examples use the physical timer at EL1 (CNTP_TVAL_EL0 and CNTP_CTL_EL0) with IRQ 30. In QEMU, some examples start higher and use the EL2 physical timer path. The timer driver hides that difference behind timer_init() and timer_rearm().

Timer IRQ work should be short: acknowledge by rearming, advance ticks, wake sleeping tasks, and request a context switch where the example has a real context-switching scheduler. Long diagnostics belong in task level. Printing every tick is a denial-of-service attack on your own serial console.

GIC Interrupts

The Raspberry Pi 4 examples use the GICv2 distributor and CPU interface. Timer interrupts are PPIs; peripheral interrupts such as AUX UART are SPIs. This distinction matters. A PPI is per-core and can appear to work even if SPI target registers are never configured. A peripheral SPI must be routed to a CPU with GICD_ITARGETSR before enabling it.

target_index = interrupt_id / 4;
target_shift = (interrupt_id % 4) * 8;
GICD_ITARGETSR[target_index] = CPU0 << target_shift;
GICD_ISENABLER[interrupt_id / 32] = bit;

The recent UART RX fix came from exactly this rule. The AUX interrupt was enabled in the UART, and enabled in the GIC, but it was not targeted to CPU0. The result looked like "UART interrupts do not work" while the timer continued to tick. The fix is now part of the generic gic_enable_irq() path in the examples that receive UART input.

IRQMeaningNotes
30CNTPNS / physical timerPPI, used by hardware examples at EL1.
26CNTHP / hypervisor physical timerUsed by some QEMU/EL2 paths.
125AUX / mini UARTSPI, must be targeted to CPU0 for RX IRQ delivery.

SD and eMMC Storage

Storage is split into a hardware interface and a program-level parser. The hardware side talks to the Raspberry Pi 4 eMMC2 host and exposes read-only block operations. The program side reads LBA 0, parses MBR partition entries, and later walks FAT32 structures. Keeping those layers apart makes QEMU support possible: the parser does not care which controller produced the 512-byte sector.

FunctionContract
disk_hw_init()Power and initialize the controller/card path.
disk_hw_read_lba(lba, buffer)Read one 512-byte sector into a caller-provided buffer.
disk_hw_get_info()Return media facts such as sector size and availability.

The current disk tutorial intentionally keeps the driver read-only. No write command, no formatter, no FAT mutation, no partition editor. That rule is what makes hardware experiments safe on the system SD card.

Block-format parsers must not cast raw disk bytes to C structures and hope the CPU agrees. MBR partition entries begin at byte offset 446, which is not naturally aligned for every optimized access the compiler might choose. Use byte-wise little-endian helpers for disk formats.

Ethernet GENET

The GENET Ethernet work is lower-level and more sensitive than UART or GPIO. It involves descriptor rings, DMA buffers, cache maintenance, and hardware ownership bits. The driver boundary should make those details explicit: ring setup, descriptor ownership, buffer alignment, and cache clean/invalidate operations are not optional.

Hardware bring-up proved RX and TX separately. RX uses a descriptor ring and receives broadcast traffic into low physical buffers. TX writes a descriptor only after the payload buffer has been populated and cleaned for DMA. The important rule is sequencing: do not enable DMA before descriptors and buffers are coherent from the device's point of view.

AreaDriver responsibility
DescriptorsInitialize ring memory, clear stale state, set ownership exactly when ready.
BuffersUse DMA-safe alignment and physical addresses the device can access.
CacheClean before device reads, invalidate before CPU reads device-written data.
TestsVerify with external packet capture, not only internal counters.

DMA and Cache Discipline

Any driver that hands memory to a device must answer three questions before the first packet or block moves:

  1. What physical address does the device see?
  2. Who owns the buffer right now: CPU or device?
  3. Which cache operation is required before ownership changes?

Simple MMIO drivers can ignore this. Ethernet, storage DMA paths, framebuffer memory, and future USB/storage drivers cannot. If a driver works once and then fails after a rebuild or after caches are enabled, suspect an ownership or cache-maintenance bug before blaming the parser above it.

QEMU Versus Hardware

QEMU is a useful smoke test, but it is not a Raspberry Pi 4. The UART path differs. Timer exception level may differ. The storage controller model may differ. QEMU can also tolerate mistakes that real Cortex-A72 hardware rejects, especially around alignment and early synchronization. Treat QEMU as a fast first pass, not a substitute for the board.

DriverHardware pathQEMU path
UARTAUX mini UART on GPIO14/15PL011 UART
TimerEL1 physical timer IRQ 30Often EL2 physical timer in early examples
StorageBCM2711 eMMC2 / SD cardSeparate emulated SD backend required
LEDGPIO42 ACT LEDUsually omitted or simulated by UART logs

Testing Rules

Driver tests should prove progress, failure behavior, and recovery. A driver that works only when every device answers immediately is not finished. A failed SD read should return to the prompt. A bad UART command should not wedge the shell. An interrupt that is not recognized should be acknowledged or safely ignored without corrupting the interrupted task frame.

Current Driver Map

Driver areaRepresentative sourceStatus
GPIO / LEDsrc/kernel/led.c, tutorial drivers/led.cHeartbeat and diagnostics.
UARTsrc/kernel/hal.c, tutorial drivers/uart.cTX/RX console, RX interrupt ring in interactive examples.
Mailboxsrc/kernel/hal.c, tutorial drivers/mailbox.cClock, power, framebuffer/reboot-style firmware calls.
Timersrc/kernel/timer.c, tutorial drivers/timer.cGeneric timer ticks and scheduler drive.
GICsrc/kernel/irq.c, tutorial drivers/timer.cTimer IRQ and AUX UART IRQ routing.
SD/eMMCsrc/kernel/sd_runtime.c, storage example drivers/rpi/rpi_disk.cRead-only bring-up and MBR/FAT32 exploration.
Ethernetsrc/drv/eth_genet.cGENET RX/TX proof-of-life, DMA ring work continuing.