// console: board-agnostic console RX layer. // // 256-byte single-producer / single-consumer ring buffered between the // board IRQ handler (mini-UART RX on Pi, PL011 RX on virt) and the // unified read syscall (sys_read, slot 32) when it targets a console // fd. WaitQueue covers the empty-ring blocking // path; the wake-side fires from console_push when the IRQ-handler // delivers a byte. Read-side drains short — caller loops if it needs N>1 // bytes — matching POSIX TTY-read semantics and keeping the WaitQueue // discipline spurious-wake-free in one edge. // // Push/read are single-producer / single-consumer on single core: // only the entry path enters console_push, only EL1 syscall context // enters console_read. Future work brackets both sides in spinlocks // once SMP and nested IRQs land; the API surface stays stable. // // Counter discipline mirrors src/pipe.flash: monotone u64 byte counters // with modulo-indexed slot access, so is_full vs. is_empty are // distinguishable without a reserved slot. // // Echo policy lives in user space (future fsh) — console_push does // NOT loop the byte back through the TX path. const layout = #import("task_layout") const wq_mod = #import("wait_queue") const WaitQueue = wq_mod.WaitQueue extern fn preempt_disable() void extern fn preempt_enable() void extern fn schedule() void pub const RX_RING_SIZE u64 = 256 var rx_ring [RX_RING_SIZE]u8 = [_]u8{0} ** RX_RING_SIZE var rx_head u64 = 0 var rx_tail u64 = 0 var rx_wq WaitQueue = .{} fn count() u64 { return rx_head -% rx_tail } fn is_empty() bool { return rx_head == rx_tail } fn is_full() bool { return count() == RX_RING_SIZE } // Called from board/{rpi4b,virt}/irq.zig with IRQs masked at the CPU // level by the exception entry. Drops the byte silently if the ring // is full — a future shell keeps up at human typing rate; the burst // / stress case is future work once spinlocks discriminate the // wait-side from the wake-side properly. pub fn console_push(byte u8) void { if (is_full()) { return } rx_ring[rx_head % RX_RING_SIZE] = byte rx_head +%= 1 rx_wq.wake_one() } // Block until at least one byte is available, then drain up to `len` // bytes (no waiting for the full `len` — short reads are fine, the // user wrapper loops if it wants more). Returns the number of bytes // copied. POSIX TTY-style semantics: line / char-mode flags are // future work. pub fn console_read(buf [*]mut u8, len u64) i64 { if (len == 0) { return 0 } var copied u64 = 0 while (copied == 0) { rx_wq.prepare_to_wait() if (!is_empty()) { rx_wq.finish_wait() preempt_disable() while (copied < len && !is_empty()) { buf[copied] = rx_ring[rx_tail % RX_RING_SIZE] rx_tail +%= 1 copied += 1 } preempt_enable() break } schedule() } rx_wq.finish_wait() return #intCast(copied) } // Debug-only sibling of console_push, called from EL1 syscall // context (sys_console_inject) — no IRQ-masking assumption. // Identical wake path. Powers deterministic console-echo coverage // on QEMU where there is no external input driver. Retire alongside // sys_console_inject once a real host-input driver lands on QEMU; // the in-kernel test harness is the only consumer. pub fn console_test_push(byte u8) void { preempt_disable() if (!is_full()) { rx_ring[rx_head % RX_RING_SIZE] = byte rx_head +%= 1 } preempt_enable() rx_wq.wake_one() } // ---- Host tests ---- // // The blocking path through `console_read` exercises the WaitQueue // `wait`; `schedule` is a host-side no-op (tests/host_stubs.zig), so // blocking is not host-testable. Coverage lives in the kernel-side // run_console_echo scenario. These drive push/read directly and // assert ring bookkeeping + wake-side wq state transitions. const std = #import("std") const TaskStruct = layout.TaskStruct const TASK_RUNNING = layout.TASK_RUNNING const TASK_INTERRUPTIBLE = layout.TASK_INTERRUPTIBLE fn reset() void { rx_head = 0 rx_tail = 0 rx_wq = .{} var i usize = 0 while (i < RX_RING_SIZE) { rx_ring[i] = 0 i += 1 } } test "push then read returns the byte" { reset() console_test_push(0x42) var buf [1]u8 = undefined const n = console_read(&buf, 1) try std.testing.expectEqual(#as(i64, 1), n) try std.testing.expectEqual(#as(u8, 0x42), buf[0]) try std.testing.expect(is_empty()) } test "ring wraps cleanly when head crosses RX_RING_SIZE" { reset() // Seed near the wrap boundary so 8 pushes straddle modulo. rx_head = RX_RING_SIZE - 4 rx_tail = RX_RING_SIZE - 4 var i u8 = 0 while (i < 8) { console_test_push(0xC0 + i) i += 1 } var buf [8]u8 = undefined const n = console_read(&buf, 8) try std.testing.expectEqual(#as(i64, 8), n) i = 0 while (i < 8) { try std.testing.expectEqual(#as(u8, 0xC0 + i), buf[i]) i += 1 } } test "is_full rejects further pushes silently" { reset() var i u32 = 0 while (i < RX_RING_SIZE) { console_test_push(#truncate(i)) i += 1 } try std.testing.expect(is_full()) // Extra push must be a no-op — head stays put. const head_before = rx_head console_test_push(0xFF) try std.testing.expectEqual(head_before, rx_head) } test "console_test_push wakes a fake waiter" { reset() var t TaskStruct = .{} t.state = TASK_INTERRUPTIBLE t.wq_next = null rx_wq.head = &t console_test_push(0x55) try std.testing.expectEqual(#as(?*mut TaskStruct, null), rx_wq.head) try std.testing.expectEqual(TASK_RUNNING, t.state) try std.testing.expectEqual(#as(?*mut TaskStruct, null), t.wq_next) } test "short read: drains what's there, returns even if < len" { reset() console_test_push(0xAA) console_test_push(0xBB) var buf [8]u8 = undefined const n = console_read(&buf, 8) try std.testing.expectEqual(#as(i64, 2), n) try std.testing.expectEqual(#as(u8, 0xAA), buf[0]) try std.testing.expectEqual(#as(u8, 0xBB), buf[1]) } test "empty after full drain restores is_empty == true" { reset() console_test_push(0x01) console_test_push(0x02) var buf [2]u8 = undefined _ = console_read(&buf, 2) try std.testing.expect(is_empty()) try std.testing.expectEqual(#as(u64, 0), count()) } test "len == 0 read returns 0 without blocking" { reset() var buf [1]u8 = undefined const n = console_read(&buf, 0) try std.testing.expectEqual(#as(i64, 0), n) }