125 lines
5.1 KiB
Zig
125 lines
5.1 KiB
Zig
//! Serial console (16550-compatible UART) — the kernel's machine-readable output
|
|
//! channel. Unlike the framebuffer console, serial text can be captured to a file
|
|
//! by QEMU (`-serial file:...`), which is what the test harness asserts on.
|
|
//!
|
|
//! The UART defaults to the legacy PC COM1 at I/O port `0x3F8`, but a UEFI Class 3
|
|
//! (legacy-free) machine may have no COM1 — or its debug UART somewhere else, and
|
|
//! reachable via MMIO rather than port I/O. So the location is a runtime value:
|
|
//! `reconfigure` repoints it once ACPI's SPCR table has been read. Early boot logs
|
|
//! optimistically to COM1 (harmless if absent); the framebuffer console is the
|
|
//! always-present log.
|
|
|
|
const paging = @import("paging.zig");
|
|
|
|
/// How the UART registers are reached: legacy I/O ports or memory-mapped.
|
|
const Access = enum { port, mmio };
|
|
|
|
var access: Access = .port;
|
|
var base: u64 = 0x3F8; // COM1
|
|
|
|
/// Whether `init`/`reconfigure` found a *working* UART at `base`. False on a
|
|
/// legacy-free machine whose COM1 is decoded but dead: writing to it is then a
|
|
/// no-op, so `write` never spins waiting for a transmit register that will never
|
|
/// drain. Cleared until proven by the loopback probe.
|
|
var uart_present: bool = false;
|
|
|
|
fn portOut(p: u16, value: u8) void {
|
|
asm volatile ("outb %[value], %[p]"
|
|
:
|
|
: [value] "{al}" (value),
|
|
[p] "{dx}" (p),
|
|
);
|
|
}
|
|
|
|
fn portIn(p: u16) u8 {
|
|
return asm volatile ("inb %[p], %[value]"
|
|
: [value] "={al}" (-> u8),
|
|
: [p] "{dx}" (p),
|
|
);
|
|
}
|
|
|
|
/// Read UART register `off` through the active access method.
|
|
fn register(off: u64) u8 {
|
|
if (access == .mmio) return @as(*volatile u8, @ptrFromInt(base + off)).*;
|
|
return portIn(@intCast(base + off));
|
|
}
|
|
|
|
/// Write UART register `off` through the active access method.
|
|
fn setRegister(off: u64, value: u8) void {
|
|
if (access == .mmio) {
|
|
@as(*volatile u8, @ptrFromInt(base + off)).* = value;
|
|
} else {
|
|
portOut(@intCast(base + off), value);
|
|
}
|
|
}
|
|
|
|
/// Configure the UART: 38400 baud, 8N1, FIFO on. Safe to call before anything
|
|
/// else; it has no dependencies, and is a harmless no-op if the port is absent.
|
|
pub fn init() void {
|
|
setRegister(1, 0x00); // disable interrupts
|
|
setRegister(3, 0x80); // enable DLAB (set baud divisor)
|
|
setRegister(0, 0x03); // divisor low: 38400 baud
|
|
setRegister(1, 0x00); // divisor high
|
|
setRegister(3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
|
setRegister(2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
|
setRegister(4, 0x0B); // RTS/DSR set
|
|
uart_present = probe();
|
|
}
|
|
|
|
/// Detect a *working* UART by internal loopback: route the transmitter back to
|
|
/// the receiver (MCR bit 4), send a byte, and check it comes back. A port that is
|
|
/// merely decoded but has nothing behind it (the common case on a legacy-free
|
|
/// board that still answers I/O at 0x3F8) never echoes, so this returns false.
|
|
///
|
|
/// This matters for speed, not just correctness: a dead UART's line-status
|
|
/// register reads back 0x00, so its transmit-holding-empty bit never sets, and
|
|
/// `writeByte` would otherwise spin its full guard — tens of milliseconds — on
|
|
/// *every* logged byte. On real hardware that alone can add ~a minute to boot.
|
|
fn probe() bool {
|
|
const saved_mcr = register(4);
|
|
setRegister(4, 0x1E); // MCR: LOOP | OUT2 | OUT1 | RTS — internal loopback
|
|
setRegister(0, 0xAE); // push a distinctive byte into the loopback path
|
|
var guard: u32 = 0;
|
|
while (register(5) & 0x01 == 0 and guard < 10_000) : (guard += 1) {} // await Data Ready
|
|
const echo = register(0);
|
|
setRegister(4, saved_mcr); // restore the modem-control lines
|
|
return echo == 0xAE;
|
|
}
|
|
|
|
/// Whether a working UART was detected (see `probe`). The log sink stays
|
|
/// registered regardless — it simply does nothing until this is true — so a UART
|
|
/// that only `reconfigure` discovers (via SPCR) still starts logging.
|
|
pub fn present() bool {
|
|
return uart_present;
|
|
}
|
|
|
|
/// Point the console at the UART ACPI's SPCR table names (MMIO or I/O port) and
|
|
/// re-run the UART setup there. Called after discovery when an SPCR entry exists.
|
|
pub fn reconfigure(is_mmio: bool, address: u64) void {
|
|
access = if (is_mmio) .mmio else .port;
|
|
// An MMIO UART is reached through the physmap; an I/O-port UART keeps its
|
|
// port number unchanged.
|
|
base = if (is_mmio) paging.mapMmio(address, 0x100, true) else address;
|
|
init();
|
|
}
|
|
|
|
fn writeByte(c: u8) void {
|
|
// Wait for the transmit-holding register to empty. `write` only reaches here
|
|
// for a UART the loopback probe proved live, so this bounds a momentary stall
|
|
// (e.g. deasserted flow control), not an absent port: ~5000 legacy-port reads
|
|
// is a few ms — comfortably longer than one 38400-baud byte-time (~260 µs).
|
|
var guard: u32 = 0;
|
|
while (register(5) & 0x20 == 0 and guard < 5_000) : (guard += 1) {}
|
|
setRegister(0, c);
|
|
}
|
|
|
|
/// Write bytes, translating LF to CRLF so terminals and logs line up. A no-op
|
|
/// when no working UART was detected, so a dead COM1 costs nothing per byte.
|
|
pub fn write(bytes: []const u8) void {
|
|
if (!uart_present) return;
|
|
for (bytes) |c| {
|
|
if (c == '\n') writeByte('\r');
|
|
writeByte(c);
|
|
}
|
|
}
|