diff --git a/src/devices/ns16550a.c b/src/devices/ns16550a.c index 25238a27d..a4887849e 100644 --- a/src/devices/ns16550a.c +++ b/src/devices/ns16550a.c @@ -1,6 +1,7 @@ /* ns16550a.c - NS16550A UART Copyright (C) 2021 LekKit +Copyright (C) 2026 Sol Astrius This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this @@ -16,178 +17,41 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. #include "chardev.h" #include "mem_ops.h" #include "ns16550a.h" +#include "uart16550_core.h" #include "utils.h" PUSH_OPTIMIZATION_SIZE typedef struct { - chardev_t* chardev; - - rvvm_irq_dev_t* irq_dev; - rvvm_irq_t irq; - - uint32_t flags; - uint32_t ier; - uint32_t lcr; - uint32_t mcr; - uint32_t scr; - uint32_t dll; - uint32_t dlm; + uart16550_core_t core; + rvvm_irq_dev_t* irq_dev; + rvvm_irq_t irq; } ns16550a_dev_t; -// Read -#define NS16550A_REG_RBR_DLL 0x0 -#define NS16550A_REG_IIR 0x2 -// Write -#define NS16550A_REG_THR_DLL 0x0 -#define NS16550A_REG_FCR 0x2 -// RW -#define NS16550A_REG_IER_DLM 0x1 -#define NS16550A_REG_LCR 0x3 -#define NS16550A_REG_MCR 0x4 -#define NS16550A_REG_LSR 0x5 -#define NS16550A_REG_MSR 0x6 -#define NS16550A_REG_SCR 0x7 - -#define NS16550A_IER_RECV 0x1 -#define NS16550A_IER_THR 0x2 -#define NS16550A_IER_LSR 0x4 -#define NS16550A_IER_MSR 0x8 - -#define NS16550A_IIR_FIFO 0xC0 -#define NS16550A_IIR_NONE 0x1 -#define NS16550A_IIR_MSR 0x0 -#define NS16550A_IIR_THR 0x2 -#define NS16550A_IIR_RECV 0x4 -#define NS16550A_IIR_LSR 0x6 - -#define NS16550A_LSR_RECV 0x1 -#define NS16550A_LSR_THR 0x60 - -#define NS16550A_LCR_DLAB 0x80 - -// Handle IIR/IER update -static void ns16550a_update_irq(ns16550a_dev_t* uart) -{ - uint32_t flags = atomic_load_uint32_relax(&uart->flags); - uint32_t ier = atomic_load_uint32_relax(&uart->ier); - if (((flags & CHARDEV_RX) && (ier & NS16550A_IER_RECV)) || ((flags & CHARDEV_TX) && (ier & NS16550A_IER_THR))) { - rvvm_irq_raise(uart->irq_dev, uart->irq); - } else { - rvvm_irq_lower(uart->irq_dev, uart->irq); - } -} - -// Handle RX/TX chardev flags update -static void ns16550a_notify(void* io_dev, uint32_t flags) -{ - ns16550a_dev_t* uart = io_dev; - if (atomic_swap_uint32(&uart->flags, flags) != flags) { - ns16550a_update_irq(uart); - } -} - -// Poll for possible RX/TX chardev flags update -static void ns16550a_poll_rxtx(ns16550a_dev_t* uart) +static void ns16550a_irq_fn(void* ctx, bool level) { - uint32_t flags = chardev_poll(uart->chardev); - if (flags != atomic_load_uint32_relax(&uart->flags)) { - ns16550a_notify(uart, flags); - } + ns16550a_dev_t* uart = ctx; + rvvm_irq_set(uart->irq_dev, uart->irq, level); } static void ns16550a_read(rvvm_reg_dev_t* dev, void* data, size_t size, size_t off) { ns16550a_dev_t* uart = rvvm_region_data(dev); UNUSED(size); - - switch (off) { - case NS16550A_REG_RBR_DLL: - if (atomic_load_uint32_relax(&uart->lcr) & NS16550A_LCR_DLAB) { - write_uint8(data, atomic_load_uint32_relax(&uart->dll)); - } else if (chardev_poll(uart->chardev) & CHARDEV_RX) { - chardev_read(uart->chardev, data, 1); - ns16550a_poll_rxtx(uart); - } - break; - case NS16550A_REG_IER_DLM: - if (atomic_load_uint32_relax(&uart->lcr) & NS16550A_LCR_DLAB) { - write_uint8(data, atomic_load_uint32_relax(&uart->dlm)); - } else { - write_uint8(data, atomic_load_uint32_relax(&uart->ier)); - } - break; - case NS16550A_REG_IIR: { - uint32_t flags = chardev_poll(uart->chardev); - uint32_t ier = atomic_load_uint32_relax(&uart->ier); - if ((flags & CHARDEV_RX) && (ier & NS16550A_IER_RECV)) { - write_uint8(data, NS16550A_IIR_RECV | NS16550A_IIR_FIFO); - } else if ((flags & CHARDEV_TX) && (ier & NS16550A_IER_THR)) { - write_uint8(data, NS16550A_IIR_THR | NS16550A_IIR_FIFO); - } else { - write_uint8(data, NS16550A_IIR_NONE | NS16550A_IIR_FIFO); - } - break; - } - case NS16550A_REG_LCR: - write_uint8(data, atomic_load_uint32_relax(&uart->lcr)); - break; - case NS16550A_REG_MCR: - write_uint8(data, atomic_load_uint32_relax(&uart->mcr)); - break; - case NS16550A_REG_LSR: { - uint32_t flags = chardev_poll(uart->chardev); - write_uint8(data, - ((flags & CHARDEV_RX) ? NS16550A_LSR_RECV : 0) | ((flags & CHARDEV_TX) ? NS16550A_LSR_THR : 0)); - break; - } - case NS16550A_REG_MSR: - write_uint8(data, 0xB0); - break; - case NS16550A_REG_SCR: - write_uint8(data, atomic_load_uint32_relax(&uart->scr)); - break; - } + write_uint8(data, uart16550_core_read(&uart->core, off)); } static void ns16550a_write(rvvm_reg_dev_t* dev, const void* data, size_t size, size_t off) { ns16550a_dev_t* uart = rvvm_region_data(dev); UNUSED(size); - - switch (off) { - case NS16550A_REG_THR_DLL: - if (atomic_load_uint32_relax(&uart->lcr) & NS16550A_LCR_DLAB) { - atomic_store_uint32_relax(&uart->dll, read_uint8(data)); - } else { - chardev_write(uart->chardev, data, 1); - ns16550a_poll_rxtx(uart); - } - break; - case NS16550A_REG_IER_DLM: - if (atomic_load_uint32_relax(&uart->lcr) & NS16550A_LCR_DLAB) { - atomic_store_uint32_relax(&uart->dlm, read_uint8(data)); - } else { - atomic_store_uint32_relax(&uart->ier, read_uint8(data)); - ns16550a_update_irq(uart); - } - break; - case NS16550A_REG_LCR: - atomic_store_uint32_relax(&uart->lcr, read_uint8(data)); - break; - case NS16550A_REG_MCR: - atomic_store_uint32_relax(&uart->mcr, read_uint8(data)); - break; - case NS16550A_REG_SCR: - atomic_store_uint32_relax(&uart->scr, read_uint8(data)); - break; - } + uart16550_core_write(&uart->core, off, read_uint8(data)); } static void ns16550a_poll(rvvm_reg_dev_t* dev) { ns16550a_dev_t* uart = rvvm_region_data(dev); - chardev_update(uart->chardev); + uart16550_core_update(&uart->core); } static void ns16550a_suspend(rvvm_reg_dev_t* dev, rvvm_snapshot_t* snap, bool resume) @@ -195,13 +59,7 @@ static void ns16550a_suspend(rvvm_reg_dev_t* dev, rvvm_snapshot_t* snap, bool re if (snap) { ns16550a_dev_t* uart = rvvm_region_data(dev); rvvm_snapshot_section(snap, "serial-ns16550a"); - rvvm_snapshot_field(snap, uart->flags); - rvvm_snapshot_field(snap, uart->ier); - rvvm_snapshot_field(snap, uart->lcr); - rvvm_snapshot_field(snap, uart->mcr); - rvvm_snapshot_field(snap, uart->scr); - rvvm_snapshot_field(snap, uart->dll); - rvvm_snapshot_field(snap, uart->dlm); + uart16550_core_suspend(&uart->core, snap); } UNUSED(resume); } @@ -210,7 +68,7 @@ static void ns16550a_cleanup(rvvm_reg_dev_t* dev) { ns16550a_dev_t* uart = rvvm_region_data(dev); rvvm_irq_dealloc(uart->irq_dev, uart->irq); - chardev_free(uart->chardev); + uart16550_core_cleanup(&uart->core); free(uart); } @@ -245,14 +103,10 @@ RVVM_PUBLIC rvvm_reg_dev_t* rvvm_ns16550a_init(rvvm_machine_t* machine, // irq_dev = rvvm_get_intc(machine); } - uart->chardev = chardev; uart->irq_dev = irq_dev; uart->irq = rvvm_irq_alloc(irq_dev, irq); - if (chardev) { - chardev->io_dev = uart; - chardev->notify = ns16550a_notify; - } + uart16550_core_init(&uart->core, chardev, ns16550a_irq_fn, uart); rvvm_reg_dev_t* dev = rvvm_region_init_auto(machine, &desc); rvvm_fdt_node_t* soc = rvvm_get_fdt_soc(machine); diff --git a/src/devices/uart16550_core.c b/src/devices/uart16550_core.c new file mode 100644 index 000000000..ffd3f68a1 --- /dev/null +++ b/src/devices/uart16550_core.c @@ -0,0 +1,230 @@ +/* +uart16550_core.c - Reusable 16550-family UART register core +Copyright (C) 2021 LekKit +Copyright (C) 2026 Sol Astrius + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +#include "uart16550_core.h" + +#include "atomics.h" +#include "compiler.h" +#include "utils.h" + +#include + +PUSH_OPTIMIZATION_SIZE + +// IER bits — interrupt enable mask. THR (TX-empty) and RECV (RX-ready) +// are the only two we wire up; LSR/MSR change interrupts are listed +// for register-level fidelity but never asserted. +#define UART16550_IER_RECV 0x1 +#define UART16550_IER_THR 0x2 +#define UART16550_IER_LSR 0x4 +#define UART16550_IER_MSR 0x8 + +// IIR identification codes. FIFO bits are advertised so guests probing +// for a 16550A vs an older 16450 see "FIFOs enabled" — actual buffering +// happens in the chardev backend, not in the core. +#define UART16550_IIR_FIFO 0xC0 +#define UART16550_IIR_NONE 0x1 +#define UART16550_IIR_THR 0x2 +#define UART16550_IIR_RECV 0x4 + +#define UART16550_LSR_RECV 0x1 +#define UART16550_LSR_THR 0x60 + +#define UART16550_LCR_DLAB 0x80 + +static void uart16550_set_irq(uart16550_core_t* core, bool level) +{ + if (core->irq_fn) { + core->irq_fn(core->irq_ctx, level); + } +} + +// Mask of chardev flags the guest is currently allowed to observe. +// RX is gated on `rx_armed` (kernel has enabled IER.RECV at least once); +// TX always passes through — the guest can transmit before it's ready +// to receive, and our backend never back-pressures so there's no risk. +static uint32_t uart16550_visible_flags(uart16550_core_t* core) +{ + uint32_t flags = chardev_poll(core->chardev); + if (!atomic_load_uint32_relax(&core->rx_armed)) { + flags &= ~CHARDEV_RX; + } + return flags; +} + +// Recompute IRQ assertion from current flags ∧ IER. Called whenever +// either side changes: backend notify (flag delta) or guest IER write. +static void uart16550_update_irq(uart16550_core_t* core) +{ + uint32_t flags = atomic_load_uint32_relax(&core->flags); + uint32_t ier = atomic_load_uint32_relax(&core->ier); + bool level = ((flags & CHARDEV_RX) && (ier & UART16550_IER_RECV)) + || ((flags & CHARDEV_TX) && (ier & UART16550_IER_THR)); + uart16550_set_irq(core, level); +} + +// Chardev → core notify hook. Stored flags are updated atomically; if +// they actually changed, re-evaluate the IRQ. RX is masked off until +// `rx_armed` flips so pre-init chardev traffic doesn't raise spurious +// IRQs the kernel would then drain via junk-RBR-reads. +static void uart16550_notify(void* io_dev, uint32_t flags) +{ + uart16550_core_t* core = io_dev; + if (!atomic_load_uint32_relax(&core->rx_armed)) { + flags &= ~CHARDEV_RX; + } + if (atomic_swap_uint32(&core->flags, flags) != flags) { + uart16550_update_irq(core); + } +} + +// Re-poll the chardev for current readiness and route through notify +// if anything changed. Used after RBR drains and THR pushes, where +// the backend won't have raised an edge on its own. +static void uart16550_poll_chardev(uart16550_core_t* core) +{ + uint32_t flags = uart16550_visible_flags(core); + if (flags != atomic_load_uint32_relax(&core->flags)) { + uart16550_notify(core, flags); + } +} + +void uart16550_core_init(uart16550_core_t* core, chardev_t* chardev, + uart16550_irq_fn irq_fn, void* irq_ctx) +{ + memset(core, 0, sizeof(*core)); + core->chardev = chardev; + core->irq_fn = irq_fn; + core->irq_ctx = irq_ctx; + + if (chardev) { + chardev->io_dev = core; + chardev->notify = uart16550_notify; + } +} + +void uart16550_core_cleanup(uart16550_core_t* core) +{ + chardev_free(core->chardev); + core->chardev = NULL; +} + +void uart16550_core_update(uart16550_core_t* core) +{ + chardev_update(core->chardev); +} + +uint8_t uart16550_core_read(uart16550_core_t* core, uint32_t reg) +{ + switch (reg) { + case UART16550_REG_RBR_DLL: + if (atomic_load_uint32_relax(&core->lcr) & UART16550_LCR_DLAB) { + return atomic_load_uint32_relax(&core->dll); + } else if (uart16550_visible_flags(core) & CHARDEV_RX) { + uint8_t byte = 0; + chardev_read(core->chardev, &byte, 1); + uart16550_poll_chardev(core); + return byte; + } + return 0; + case UART16550_REG_IER_DLM: + if (atomic_load_uint32_relax(&core->lcr) & UART16550_LCR_DLAB) { + return atomic_load_uint32_relax(&core->dlm); + } + return atomic_load_uint32_relax(&core->ier); + case UART16550_REG_IIR: { + uint32_t flags = uart16550_visible_flags(core); + uint32_t ier = atomic_load_uint32_relax(&core->ier); + if ((flags & CHARDEV_RX) && (ier & UART16550_IER_RECV)) { + return UART16550_IIR_RECV | UART16550_IIR_FIFO; + } else if ((flags & CHARDEV_TX) && (ier & UART16550_IER_THR)) { + return UART16550_IIR_THR | UART16550_IIR_FIFO; + } + return UART16550_IIR_NONE | UART16550_IIR_FIFO; + } + case UART16550_REG_LCR: + return atomic_load_uint32_relax(&core->lcr); + case UART16550_REG_MCR: + return atomic_load_uint32_relax(&core->mcr); + case UART16550_REG_LSR: { + uint32_t flags = uart16550_visible_flags(core); + return ((flags & CHARDEV_RX) ? UART16550_LSR_RECV : 0) + | ((flags & CHARDEV_TX) ? UART16550_LSR_THR : 0); + } + case UART16550_REG_MSR: + // CTS+DSR+DCD asserted; matches the original ns16550a value + // so guests probing modem status see a permanently-connected + // line rather than a hung modem. + return 0xB0; + case UART16550_REG_SCR: + return atomic_load_uint32_relax(&core->scr); + default: + return 0; + } +} + +void uart16550_core_write(uart16550_core_t* core, uint32_t reg, uint8_t val) +{ + switch (reg) { + case UART16550_REG_THR_DLL: + if (atomic_load_uint32_relax(&core->lcr) & UART16550_LCR_DLAB) { + atomic_store_uint32_relax(&core->dll, val); + } else { + chardev_write(core->chardev, &val, 1); + uart16550_poll_chardev(core); + } + break; + case UART16550_REG_IER_DLM: + if (atomic_load_uint32_relax(&core->lcr) & UART16550_LCR_DLAB) { + atomic_store_uint32_relax(&core->dlm, val); + } else { + atomic_store_uint32_relax(&core->ier, val); + // Arm the RX gate the moment the kernel signals it + // wants to receive. Sticky — flow control later + // toggling RECV off doesn't silently re-enable junk + // draining at the next port reopen. + if (val & UART16550_IER_RECV) { + atomic_store_uint32_relax(&core->rx_armed, 1); + // Backfill flags from chardev. By now RX may have + // been pending for a while with no IRQ raised; sync + // through notify so the fresh-armed line raises if + // data is already waiting. + uart16550_poll_chardev(core); + } + uart16550_update_irq(core); + } + break; + case UART16550_REG_LCR: + atomic_store_uint32_relax(&core->lcr, val); + break; + case UART16550_REG_MCR: + atomic_store_uint32_relax(&core->mcr, val); + break; + case UART16550_REG_SCR: + atomic_store_uint32_relax(&core->scr, val); + break; + default: + break; + } +} + +void uart16550_core_suspend(uart16550_core_t* core, rvvm_snapshot_t* snap) +{ + rvvm_snapshot_field(snap, core->flags); + rvvm_snapshot_field(snap, core->ier); + rvvm_snapshot_field(snap, core->lcr); + rvvm_snapshot_field(snap, core->mcr); + rvvm_snapshot_field(snap, core->scr); + rvvm_snapshot_field(snap, core->dll); + rvvm_snapshot_field(snap, core->dlm); + rvvm_snapshot_field(snap, core->rx_armed); +} + +POP_OPTIMIZATION_SIZE diff --git a/src/devices/uart16550_core.h b/src/devices/uart16550_core.h new file mode 100644 index 000000000..b4e546141 --- /dev/null +++ b/src/devices/uart16550_core.h @@ -0,0 +1,95 @@ +/* +uart16550_core.h - Reusable 16550-family UART register core +Copyright (C) 2021 LekKit +Copyright (C) 2026 Sol Astrius + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +#ifndef RVVM_UART16550_CORE_H +#define RVVM_UART16550_CORE_H + +#include + +#include "chardev.h" + +/* + * Backend-agnostic 16550A register core. The bus wrapper (rvvm_reg_dev_t + * MMIO/PIO for ns16550a, BAR-windowed multi-port for an Exar PCIe combo + * card, etc.) embeds a `uart16550_core_t`, drives byte-wide register + * access via `uart16550_core_read/write`, and supplies an IRQ callback + * so the core stays unaware of whether the line is wired INTx, PCI INTx, + * or MSI. + */ + +// Register offsets within an 8-byte 16550 window. +// +// DLAB (LCR bit 7) banks RBR/THR/IER under the divisor latch low/high +// at offsets 0/1; FCR is write-only at 2 with IIR read-only there. +#define UART16550_REG_RBR_DLL 0x0 +#define UART16550_REG_THR_DLL 0x0 +#define UART16550_REG_IER_DLM 0x1 +#define UART16550_REG_IIR 0x2 +#define UART16550_REG_FCR 0x2 +#define UART16550_REG_LCR 0x3 +#define UART16550_REG_MCR 0x4 +#define UART16550_REG_LSR 0x5 +#define UART16550_REG_MSR 0x6 +#define UART16550_REG_SCR 0x7 + +// IRQ callback. `level=true` asserts the line, `false` deasserts. +// The core may call this from any thread that pokes registers or +// receives a chardev notify; wrappers must be reentrant-safe. +typedef void (*uart16550_irq_fn)(void* ctx, bool level); + +typedef struct { + chardev_t* chardev; + uart16550_irq_fn irq_fn; + void* irq_ctx; + + uint32_t flags; + uint32_t ier; + uint32_t lcr; + uint32_t mcr; + uint32_t scr; + uint32_t dll; + uint32_t dlm; + + // Sticky "kernel has enabled RX" gate. Real silicon's RBR is empty + // after reset; pre-init RBR reads return floating bus values which + // the kernel discards as junk. Our chardev backend never resets, so + // bytes that arrived before the guest finishes startup would get + // eaten by `serial8250_do_startup`'s two junk-drain RBR reads. We + // suppress chardev draining until the guest writes IER with the + // RECV bit (kernel signalling "I'm ready to receive"); flips true + // once and stays true until the core is destroyed. + uint32_t rx_armed; +} uart16550_core_t; + +// Bind a chardev to a core in caller-owned storage. Hooks the chardev's +// notify path back to this core, so flag changes raise/lower the IRQ +// line via `irq_fn`. `chardev` and `irq_fn` may be NULL. +void uart16550_core_init(uart16550_core_t* core, chardev_t* chardev, + uart16550_irq_fn irq_fn, void* irq_ctx); + +// Free the chardev owned by this core (if any). The core storage +// itself is owned by the caller. +void uart16550_core_cleanup(uart16550_core_t* core); + +// Single-byte register read/write. `reg` is a UART16550_REG_* offset. +// Out-of-range reads return 0; out-of-range writes are dropped. +uint8_t uart16550_core_read(uart16550_core_t* core, uint32_t reg); +void uart16550_core_write(uart16550_core_t* core, uint32_t reg, uint8_t val); + +// Force a re-poll of the chardev backend; useful for periodic update +// hooks where the bus wrapper wants to push pending TX or pull RX. +void uart16550_core_update(uart16550_core_t* core); + +// Serialize/deserialize core register state into the current snapshot +// section (caller opens the section so multi-instance wrappers can +// distinguish their ports). Direction is implicit in `snap`'s mode. +void uart16550_core_suspend(uart16550_core_t* core, rvvm_snapshot_t* snap); + +#endif