diff --git a/include/serial.h b/include/serial.h new file mode 100644 index 0000000..784a92a --- /dev/null +++ b/include/serial.h @@ -0,0 +1,35 @@ +/* Nadir serial log: COM1 (16550 UART) polling driver. + * + * Runs in 64-bit long mode at Ring 0 (CPL 0); uses `inb`/`outb` which fault + * with #GP if executed above CPL 0, so this driver is kernel-only. + * Ports: COM1 0x3F8-0x3FF (data, int-enable, divisor latch, FIFO, line + * control, modem control, line status). QEMU/SeaBIOS provides COM1; the + * VGA console stays primary and serial is the secondary log. + * Fault behavior: port I/O cannot fault at CPL 0. The TX poll spins on + * LSR bit 5 and hangs only if the UART hardware is absent or wedged — + * call serial_ok() first to check LSR sanity. + * + * Preconditions: call serial_init() once before any put. serial_puts() + * requires a valid NUL-terminated string (NULL is ignored, not crashed + * on). Failure modes: on absent hardware, init writes go nowhere and + * serial_ok() returns 0; TX polls may then spin forever. + */ + +#ifndef NADIR_SERIAL_H +#define NADIR_SERIAL_H + +/* Program COM1 for 38400 8N1 polling (interrupts off, FIFO on). + * Ring 0 only. Safe to call again to re-program the port. */ +void serial_init(void); + +/* Block until the THR is empty (LSR bit 5), then emit one byte. */ +void serial_putchar(char c); + +/* Emit a NUL-terminated string via serial_putchar(). NULL is a no-op. */ +void serial_puts(const char *s); + +/* Nonzero if a UART looks present (LSR reads back sane, not 0xFF + * as an open bus returns). Zero means "do not trust TX polls". */ +int serial_ok(void); + +#endif /* NADIR_SERIAL_H */ diff --git a/kernel/arch/x86_64/serial.c b/kernel/arch/x86_64/serial.c new file mode 100644 index 0000000..ebc3810 --- /dev/null +++ b/kernel/arch/x86_64/serial.c @@ -0,0 +1,65 @@ +/* COM1 16550 UART polling log (see serial.h). */ + +#include + +#include "io.h" +#include "serial.h" + +/* Register offsets from the COM1 base. */ +#define COM1_BASE 0x3F8U +#define COM_DATA 0x3F8U /* THR/RBR (DLAB=0) / divisor low (DLAB=1) */ +#define COM_IER 0x3F9U /* interrupt enable / divisor high (DLAB=1) */ +#define COM_FIFO 0x3FAU /* FIFO control */ +#define COM_LCTL 0x3FBU /* line control + DLAB bit 7 */ +#define COM_MCTL 0x3FCU /* modem control */ +#define COM_LSR 0x3FDU /* line status: bit 5 = THR empty, bit 0 = RX ready */ + +#define LSR_TX_EMPTY 0x20U /* THR holds no byte: safe to write */ +#define LSR_RX_READY 0x01U /* RBR holds a byte (reserved for future RX) */ + +/* Port I/O owned by include/io.h; no local inb/outb duplicates. */ + +void serial_init(void) +{ + /* No UART interrupts: pure polling, so the 8259/IDT stays untouched. */ + outb(COM_IER, 0x00); + /* DLAB on to program the divisor. */ + outb(COM_LCTL, 0x80); + /* Divisor 3 = 115200/3 = 38400 baud. */ + outb(COM_DATA, 0x03); + outb(COM_IER, 0x00); + /* 8N1: 8 bits, no parity, one stop bit; DLAB back off. */ + outb(COM_LCTL, 0x03); + /* FIFO on, clear both queues, 14-byte threshold. */ + outb(COM_FIFO, 0xC7); + /* DTR+RTS on, OUT2 on so the UART drives interrupts (unused). */ + outb(COM_MCTL, 0x0B); +} + +void serial_putchar(char c) +{ + /* Spin until THR is empty; hangs only if hardware is absent/wedged, + * which is why callers gate on serial_ok() when unsure. */ + while ((inb(COM_LSR) & LSR_TX_EMPTY) == 0) { + /* Poll. */ + } + outb(COM_DATA, (uint8_t)c); +} + +void serial_puts(const char *s) +{ + if (s == 0) { + return; + } + while (*s != '\0') { + serial_putchar(*s); + s++; + } +} + +int serial_ok(void) +{ + /* An absent port reads back 0xFF (open bus); a real 16550 never + * reports all status bits set at once, so that means no UART. */ + return inb(COM_LSR) != 0xFFU; +}