// Mini-UART driver for Raspberry Pi 4 const std = #import("std") const console = #import("console") extern fn irq_disable() void extern fn irq_enable() void const LINEAR_MAP_BASE u64 = 0xFFFF000000000000 const DEVICE_BASE u64 = 0xFE000000 // Calculate kernel virtual address from physical address fn pa_to_kva(pa u64) u64 { return pa + LINEAR_MAP_BASE } // Mini-UART registers structure (volatile for MMIO) const AuxRegs = extern struct { irq_status u32, enables u32, reserved [14]u32, mu_io u32, mu_ier u32, mu_iir u32, mu_lcr u32, mu_mcr u32, mu_lsr u32, mu_msr u32, mu_scratch u32, mu_control u32, mu_status u32, mu_baud_rate u32 } // Get AuxRegs pointer (MMIO) fn getAuxRegs() *mut volatile AuxRegs { const aux_addr = pa_to_kva(DEVICE_BASE + 0x00215000) return #as(*mut volatile AuxRegs, #ptrFromInt(aux_addr)) } // GPIO functions (C interface) extern fn gpio_pin_set_func(pin u8, func u8) void extern fn gpio_pin_enable(pin u8) void const GFAlt5 u8 = 2 const TXD0 u8 = 14 const RXD0 u8 = 15 /// Initialize mini-UART export fn mini_uart_init() void { const aux = getAuxRegs() // Set GPIO 14 and 15 to UART1 (mini-uart) gpio_pin_set_func(TXD0, GFAlt5) gpio_pin_set_func(RXD0, GFAlt5) // Clear pull-up/pull-down resistors. gpio_pin_enable(TXD0) gpio_pin_enable(RXD0) // Enable mini-uart aux.enables = 1 // Disable TX and RX and auto flow control aux.mu_control = 0 // Enable receive interrupts, check bcm errata aux.mu_ier = 0xD // Set 8-bit mode aux.mu_lcr = 3 // Set RTS to always high aux.mu_mcr = 0 // 115200 @ 500 MHz aux.mu_baud_rate = 541 // Enable TX and RX aux.mu_control = 3 // Drain any garbage from the RX FIFO so the interrupt line goes low, // ensuring we don't miss the first edge-triggered RX interrupt. while ((aux.mu_lsr & 1) != 0) { _ = aux.mu_io } mini_uart_send('\r') mini_uart_send('\n') mini_uart_send('\n') } /// Send a single character via UART export fn mini_uart_send(c u8) void { const aux = getAuxRegs() // Keep looping if the 5th bit is 0 (TX FIFO full) while ((aux.mu_lsr & 0x20) == 0) {} aux.mu_io = c } /// Receive a character via UART export fn mini_uart_recv() u8 { const aux = getAuxRegs() // Keep looping if the 1st bit is 0 (RX FIFO empty) while ((aux.mu_lsr & 1) == 0) {} return #as(u8, #truncate(aux.mu_io)) } /// True iff the mini-UART RX FIFO has at least one byte ready. /// mu_lsr bit 0 = "data ready" (BCM2837/2711 ARM-Peripherals §2.2.1). /// Non-blocking — feeds the IRQ-side drain loop in board/rpi4b/irq.zig /// so console_push can collect every queued byte in one IRQ slot /// instead of relying on the level-triggered AUX line re-firing. /// `export` (not `pub`) so irq.zig consumes it via the same /// `extern fn` discipline as the rest of the UART helpers. export fn mini_uart_rx_pending() bool { const aux = getAuxRegs() return (aux.mu_lsr & 1) != 0 } /// Drain the mini-UART RX FIFO into the console ring from a non-IRQ /// (idle-loop) caller — a defensive backstop. The AUX RX interrupt /// (VC_AUX_IRQ) is the primary delivery path and does reach handle_irq on /// real hardware, draining into console_push from irq.zig; this poll only /// matters if a byte is ever left between IRQ slots. IRQs are masked across /// the drain: console_push assumes an IRQ-masked caller (src/console.zig), /// so this can't race the AUX handler on the same FIFO/ring. Cheap LSR /// poll; no-op when the FIFO is empty. pub fn poll_rx_into_console() void { irq_disable() while (mini_uart_rx_pending()) { console.console_push(mini_uart_recv()) } irq_enable() } /// Send a null-terminated string via UART export fn mini_uart_send_string(str [*:0]u8) void { var i usize = 0 while (str[i] != 0) { const c = str[i] if (c == '\n') { // Also do CR if there's a '\n' mini_uart_send('\r') } mini_uart_send(c) i += 1 } } /// Print function compatible with Zig's std.fmt interface pub fn print(comptime format []u8, args anytype) void { var buffer [256]u8 = undefined const formatted = std.fmt.bufPrint(&buffer, format, args) catch { mini_uart_send_string("format error") return } for c in formatted { if (c == '\n') { mini_uart_send('\r') } mini_uart_send(c) } }