summaryrefslogtreecommitdiff
path: root/drivers/tty/tty_uart.cc
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/tty/tty_uart.cc')
-rw-r--r--drivers/tty/tty_uart.cc251
1 files changed, 251 insertions, 0 deletions
diff --git a/drivers/tty/tty_uart.cc b/drivers/tty/tty_uart.cc
new file mode 100644
index 0000000..973a6e9
--- /dev/null
+++ b/drivers/tty/tty_uart.cc
@@ -0,0 +1,251 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tty_uart.cc — UART → tty_driver adapter
+ *
+ * Bridges the 16550A UART hardware to the TTY subsystem.
+ *
+ * TX: Calls early_putc() for each character (same as before).
+ * RX: Registers a PLIC IRQ handler. On UART RX interrupt,
+ * reads available bytes and feeds them to the line discipline
+ * via tty_receive_buf().
+ *
+ * QEMU virt UART:
+ * - IRQ 10 on the PLIC
+ * - 16550A register layout at the base address discovered from FDT
+ */
+
+#include <lume/tty.h>
+#include <lume/irq.h>
+#include <lume/addr.h>
+#include <lume/fdt.h>
+#include <lume/vmm.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+extern "C" void early_putc(char c);
+
+/* 16550A register offsets */
+static constexpr int UART_RBR = 0; /* Receive Buffer Register */
+static constexpr int UART_THR = 0; /* Transmit Holding Register */
+static constexpr int UART_IER = 1; /* Interrupt Enable Register */
+static constexpr int UART_IIR = 2; /* Interrupt Identification Register */
+static constexpr int UART_FCR = 2; /* FIFO Control Register */
+static constexpr int UART_LCR = 3; /* Line Control Register */
+static constexpr int UART_MCR = 4; /* Modem Control Register */
+static constexpr int UART_LSR = 5; /* Line Status Register */
+
+/* LSR bits */
+static constexpr uint8 UART_LSR_DR = 0x01; /* Data Ready */
+static constexpr uint8 UART_LSR_THRE = 0x20; /* TX Holding Register Empty */
+
+/* IER bits */
+static constexpr uint8 UART_IER_RDI = 0x01; /* Received Data Available */
+
+/* MCR bits */
+static constexpr uint8 UART_MCR_DTR = 0x01; /* Data Terminal Ready */
+static constexpr uint8 UART_MCR_RTS = 0x02; /* Request To Send */
+static constexpr uint8 UART_MCR_OUT2 = 0x08; /* Auxiliary output 2 — gates the
+ IRQ pin on PC-style 16550A.
+ Without this the UART
+ internally asserts but does
+ NOT propagate to the PLIC. */
+
+/* QEMU virt default UART IRQ */
+static constexpr uint32 UART_IRQ = 10;
+
+/* UART MMIO base (virtual address) */
+static volatile uint8* g_uart_base;
+static tty_struct* g_uart_tty;
+
+/* ================================================================== */
+/* UART MMIO helpers */
+/* ================================================================== */
+
+static inline uint8 uart_read(int reg)
+{
+ return g_uart_base[reg];
+}
+
+static inline void uart_write(int reg, uint8 val)
+{
+ g_uart_base[reg] = val;
+}
+
+static inline bool uart_rx_ready()
+{
+ return (uart_read(UART_LSR) & UART_LSR_DR) != 0;
+}
+
+static inline char uart_rx_char()
+{
+ return static_cast<char>(uart_read(UART_RBR));
+}
+
+/* ================================================================== */
+/* UART RX interrupt handler */
+/* ================================================================== */
+
+static void uart_irq_handler(uint32 /*irq*/, void* arg)
+{
+ auto* tty = static_cast<tty_struct*>(arg);
+
+ /* Read all available bytes from the FIFO */
+ char buf[16];
+ int count = 0;
+
+ while (uart_rx_ready() && count < 16) {
+ buf[count++] = uart_rx_char();
+ }
+
+ if (count > 0) {
+ tty_receive_buf(tty, buf, count);
+ }
+}
+
+/* ================================================================== */
+/* tty_driver_ops for UART */
+/* ================================================================== */
+
+static int uart_tty_open_op(tty_struct* /*tty*/)
+{
+ return 0;
+}
+
+static void uart_tty_close_op(tty_struct* /*tty*/)
+{
+ /* Nothing to do */
+}
+
+static int uart_tty_write_op(tty_struct* /*tty*/, const char* buf, int count)
+{
+ for (int i = 0; i < count; i++) {
+ early_putc(buf[i]);
+ }
+ return count;
+}
+
+static int uart_tty_write_room_op(tty_struct* /*tty*/)
+{
+ return TTY_BUF_SIZE; /* Always room (blocking putc) */
+}
+
+static void uart_tty_put_char_op(tty_struct* /*tty*/, char ch)
+{
+ early_putc(ch);
+}
+
+static void uart_tty_flush_chars_op(tty_struct* /*tty*/)
+{
+ /* No buffering in early_putc, nothing to flush */
+}
+
+static int uart_tty_chars_in_buffer_op(tty_struct* /*tty*/)
+{
+ return 0; /* No TX buffer */
+}
+
+static void uart_tty_flush_buffer_op(tty_struct* /*tty*/)
+{
+ /* No TX buffer to flush */
+}
+
+static int uart_tty_ioctl_op(tty_struct* /*tty*/, unsigned int /*cmd*/, uint64 /*arg*/)
+{
+ return -ENOIOCTLCMD;
+}
+
+static void uart_tty_hangup_op(tty_struct* /*tty*/)
+{
+ /* Disable UART RX interrupts */
+ if (g_uart_base) {
+ uart_write(UART_IER, 0);
+ }
+}
+
+static const tty_driver_ops uart_ops = {
+ .open = uart_tty_open_op,
+ .close = uart_tty_close_op,
+ .write = uart_tty_write_op,
+ .write_room = uart_tty_write_room_op,
+ .put_char = uart_tty_put_char_op,
+ .flush_chars = uart_tty_flush_chars_op,
+ .chars_in_buffer = uart_tty_chars_in_buffer_op,
+ .flush_buffer = uart_tty_flush_buffer_op,
+ .ioctl = uart_tty_ioctl_op,
+ .throttle = nullptr,
+ .unthrottle = nullptr,
+ .hangup = uart_tty_hangup_op,
+};
+
+const tty_driver uart_tty_driver = {
+ .name = "ttyS",
+ .major = 4,
+ .minor_start = 64,
+ .num = 1,
+ .ops = &uart_ops,
+};
+
+/* ================================================================== */
+/* UART TTY init — discover base address, enable RX IRQ */
+/* ================================================================== */
+
+void uart_tty_init(tty_struct* tty)
+{
+ g_uart_tty = tty;
+
+ /* Discover UART base from FDT */
+ DeviceNode* node = fdt_find_compatible("ns16550a");
+ if (!node) {
+ /* Try alternative compatible string */
+ node = fdt_find_compatible("snps,dw-apb-uart");
+ }
+
+ uint64 uart_pa = 0;
+ if (node) {
+ uart_pa = node->get_prop_u64("reg", 0);
+ }
+
+ if (uart_pa == 0) {
+ /* Fallback: QEMU virt default */
+ uart_pa = 0x10000000;
+ }
+
+ g_uart_base = reinterpret_cast<volatile uint8*>(
+ pa_to_va(phys_addr(uart_pa)).raw);
+
+ /* Set FIFO control: enable FIFOs, clear RX/TX FIFOs, 1-byte trigger */
+ uart_write(UART_FCR, 0x07);
+
+ /* MCR: assert OUT2 (gates the IRQ pin on PC-style 16550A), plus
+ * DTR/RTS for completeness. Mirrors Linux drivers/tty/serial/8250/
+ * 8250_port.c: serial8250_do_startup() does the same. */
+ uart_write(UART_MCR, UART_MCR_DTR | UART_MCR_RTS | UART_MCR_OUT2);
+
+ /* Enable UART receive-data-available interrupt */
+ uart_write(UART_IER, UART_IER_RDI);
+
+ /* Register IRQ handler with PLIC */
+ uint32 irq = UART_IRQ;
+ if (node) {
+ /* Try to get IRQ number from FDT */
+ const FdtProperty* irq_prop = node->get_prop("interrupts");
+ if (irq_prop && irq_prop->len >= 4) {
+ const uint8* val = static_cast<const uint8*>(irq_prop->value);
+ irq = (uint32(val[0]) << 24) | (uint32(val[1]) << 16) |
+ (uint32(val[2]) << 8) | val[3];
+ }
+ }
+
+ g_irq_manager.register_handler(irq, uart_irq_handler, tty);
+ g_irq_manager.enable(irq);
+
+ printk(KERN_INFO "uart_tty_init: base=0x%llx irq=%u IER=0x%x LSR=0x%x\n",
+ (unsigned long long)uart_pa, irq,
+ uart_read(UART_IER), uart_read(UART_LSR));
+}