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.
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.
| Layer | Owns | Examples |
|---|---|---|
| Kernel/runtime | Scheduling, shell commands, fault reporting, program model | kernel_main(), scheduler tasks, monitor commands |
| Driver interface | Small functions with kernel-facing intent | uart_getc(), timer_init(), disk_hw_read_lba() |
| Board support | MMIO addresses, GPIO alternate functions, GIC IDs, cache/DMA details | BCM2711 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 family | Purpose |
|---|---|
GPFSELn | Select input, output, or alternate function for each GPIO pin. |
GPSETn / GPCLRn | Set or clear output pins without read-modify-write races on the output latch. |
GPPUPPDNn | Configure 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.
| Function | Contract |
|---|---|
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.
| IRQ | Meaning | Notes |
|---|---|---|
30 | CNTPNS / physical timer | PPI, used by hardware examples at EL1. |
26 | CNTHP / hypervisor physical timer | Used by some QEMU/EL2 paths. |
125 | AUX / mini UART | SPI, 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.
| Function | Contract |
|---|---|
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.
| Area | Driver responsibility |
|---|---|
| Descriptors | Initialize ring memory, clear stale state, set ownership exactly when ready. |
| Buffers | Use DMA-safe alignment and physical addresses the device can access. |
| Cache | Clean before device reads, invalidate before CPU reads device-written data. |
| Tests | Verify 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:
- What physical address does the device see?
- Who owns the buffer right now: CPU or device?
- 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.
| Driver | Hardware path | QEMU path |
|---|---|---|
| UART | AUX mini UART on GPIO14/15 | PL011 UART |
| Timer | EL1 physical timer IRQ 30 | Often EL2 physical timer in early examples |
| Storage | BCM2711 eMMC2 / SD card | Separate emulated SD backend required |
| LED | GPIO42 ACT LED | Usually 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.
- Before booting bare metal from SD, check recovery USB and EEPROM boot order.
- For UART RX, test fast command bursts, not only slow human typing.
- For storage, keep early commands read-only and print explicit phase breadcrumbs.
- For DMA, verify externally where possible: packet capture, screen output, or sector contents.
- For exception paths, always dump ESR/ELR/FAR/SPSR to UART before halting.
Current Driver Map
| Driver area | Representative source | Status |
|---|---|---|
| GPIO / LED | src/kernel/led.c, tutorial drivers/led.c | Heartbeat and diagnostics. |
| UART | src/kernel/hal.c, tutorial drivers/uart.c | TX/RX console, RX interrupt ring in interactive examples. |
| Mailbox | src/kernel/hal.c, tutorial drivers/mailbox.c | Clock, power, framebuffer/reboot-style firmware calls. |
| Timer | src/kernel/timer.c, tutorial drivers/timer.c | Generic timer ticks and scheduler drive. |
| GIC | src/kernel/irq.c, tutorial drivers/timer.c | Timer IRQ and AUX UART IRQ routing. |
| SD/eMMC | src/kernel/sd_runtime.c, storage example drivers/rpi/rpi_disk.c | Read-only bring-up and MBR/FAT32 exploration. |
| Ethernet | src/drv/eth_genet.c | GENET RX/TX proof-of-life, DMA ring work continuing. |