diff options
Diffstat (limited to 'na/drivers')
| -rw-r--r-- | na/drivers/base/bus.cc | 32 | ||||
| -rw-r--r-- | na/drivers/base/core.cc | 156 | ||||
| -rw-r--r-- | na/drivers/base/driver.cc | 72 | ||||
| -rw-r--r-- | na/drivers/base/module.mk | 9 | ||||
| -rw-r--r-- | na/drivers/base/platform.cc | 231 | ||||
| -rw-r--r-- | na/drivers/console.cc | 119 | ||||
| -rw-r--r-- | na/drivers/driver.cc | 27 | ||||
| -rw-r--r-- | na/drivers/module.mk | 2 | ||||
| -rw-r--r-- | na/drivers/plic.cc | 28 | ||||
| -rw-r--r-- | na/drivers/tty/module.mk | 5 | ||||
| -rw-r--r-- | na/drivers/tty/n_tty.cc | 539 | ||||
| -rw-r--r-- | na/drivers/tty/tty_core.cc | 536 | ||||
| -rw-r--r-- | na/drivers/tty/tty_uart.cc | 251 | ||||
| -rw-r--r-- | na/drivers/uart.cc | 113 |
14 files changed, 2085 insertions, 35 deletions
diff --git a/na/drivers/base/bus.cc b/na/drivers/base/bus.cc new file mode 100644 index 0000000..975f676 --- /dev/null +++ b/na/drivers/base/bus.cc @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * drivers/base/bus.cc — bus_type registration. + * + * Mirrors Linux drivers/base/bus.c. Trivial in this iteration: + * initialize the device/driver lists and the per-bus spinlock. + * + * Reference: docs/specs/storage_stack.md §4.3 + */ + +#include <lume/device.h> +#include <lume/klog.h> +#include <lume/kprintf.h> +#include <lume/errno.h> + +int bus_register(bus_type* bus) +{ + if (bus == nullptr || bus->name == nullptr) { + return -EINVAL; + } + if (bus->registered) { + return 0; + } + bus->devices.init(); + bus->drivers.init(); + bus->lock.init(bus->name); + bus->registered = true; + kprintf("[bus] '%s' registered\n", bus->name); + return 0; +} diff --git a/na/drivers/base/core.cc b/na/drivers/base/core.cc new file mode 100644 index 0000000..f92b2a4 --- /dev/null +++ b/na/drivers/base/core.cc @@ -0,0 +1,156 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * drivers/base/core.cc — device core. + * + * Mirrors Linux drivers/base/core.c (greatly simplified). Provides + * device_initialize, device_register, device_unregister, and the + * try_bind helper shared with driver registration. + * + * Reference: docs/specs/storage_stack.md §4.3 + */ + +#include <lume/device.h> +#include <lume/container_of.h> +#include <lume/klog.h> +#include <lume/kprintf.h> +#include <lume/errno.h> +#include <lume/list.h> + +/* ------------------------------------------------------------------ */ +/* device ktype — generic release (no-op; subclasses override) */ +/* ------------------------------------------------------------------ */ + +static void device_release(kobject* kobj) +{ + /* Concrete device flavors (platform_device, virtio_device, ...) are + * expected to allocate via kmalloc and free in their own release + * paths after device_unregister + final kobject_put. The generic + * device kobj_type only fires for stack-allocated devices used in + * tests — nothing to do. */ + (void)kobj; +} + +static const kobj_type device_ktype = { + .release = device_release, +}; + +/* ------------------------------------------------------------------ */ +/* device_initialize / device_register */ +/* ------------------------------------------------------------------ */ + +void device_initialize(device* dev) +{ + if (dev == nullptr) { + return; + } + kobject_init(&dev->kobj, &device_ktype); + dev->parent = nullptr; + dev->driver = nullptr; + dev->driver_data = nullptr; + dev->platform_data = nullptr; + dev->devt = 0; + dev->bus_list.init(); + dev->driver_list.init(); +} + +const char* dev_name(const device* dev) +{ + if (dev == nullptr) { + return "(null)"; + } + if (dev->kobj.name != nullptr) { + return dev->kobj.name; + } + if (dev->init_name != nullptr) { + return dev->init_name; + } + return "(unnamed)"; +} + +/* try_bind: attempt to match `dev` against `drv` and probe. + * Returns 1 on successful bind, 0 otherwise. + * Caller must hold bus->lock. */ +int device_try_bind(device* dev, device_driver* drv); + +int device_try_bind(device* dev, device_driver* drv) +{ + bus_type* bus = dev->bus; + + if (bus->match != nullptr && bus->match(dev, drv) == 0) { + return 0; + } + + dev->driver = drv; + + int rc = 0; + if (bus->probe != nullptr) { + rc = bus->probe(dev); + } else if (drv->probe != nullptr) { + rc = drv->probe(dev); + } + + if (rc == 0) { + list_add_tail(&dev->driver_list, &drv->devices); + return 1; + } + dev->driver = nullptr; + return 0; +} + +int device_register(device* dev) +{ + if (dev == nullptr || dev->bus == nullptr) { + return -EINVAL; + } + if (!dev->bus->registered) { + kprintf("[device] register '%s' fail: bus '%s' not registered\n", + dev_name(dev), + dev->bus->name != nullptr ? dev->bus->name : "?"); + return -ENODEV; + } + + /* device_initialize is idempotent — safe to call again, but most + * paths will already have initialized. Detect via ktype set. */ + if (dev->kobj.ktype == nullptr) { + device_initialize(dev); + } + if (dev->kobj.name == nullptr) { + kobject_add(&dev->kobj, nullptr, dev->init_name); + } + dev->bus_list.init(); + dev->driver_list.init(); + + LockGuard g(dev->bus->lock); + list_add_tail(&dev->bus_list, &dev->bus->devices); + + list_node* cur = dev->bus->drivers.next; + while (cur != &dev->bus->drivers) { + device_driver* drv = + list_entry<device_driver, &device_driver::bus_node>(cur); + cur = cur->next; + if (device_try_bind(dev, drv) != 0) { + break; + } + } + return 0; +} + +void device_unregister(device* dev) +{ + if (dev == nullptr || dev->bus == nullptr) { + return; + } + + LockGuard g(dev->bus->lock); + + if (dev->driver != nullptr) { + if (dev->driver->remove != nullptr) { + dev->driver->remove(dev); + } + list_del_init(&dev->driver_list); + dev->driver = nullptr; + } + list_del_init(&dev->bus_list); +} diff --git a/na/drivers/base/driver.cc b/na/drivers/base/driver.cc new file mode 100644 index 0000000..ce15b6d --- /dev/null +++ b/na/drivers/base/driver.cc @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * drivers/base/driver.cc — driver_register / driver_unregister. + * + * Mirrors Linux drivers/base/driver.c. Registers a device_driver on + * its bus and tries to bind any unbound matching devices. + * + * Reference: docs/specs/storage_stack.md §4.3 + */ + +#include <lume/device.h> +#include <lume/container_of.h> +#include <lume/klog.h> +#include <lume/kprintf.h> +#include <lume/errno.h> + +/* Implemented in core.cc — bind a device to a driver on its bus. + * Returns 1 on success, 0 on no-match-or-probe-fail. */ +int device_try_bind(device* dev, device_driver* drv); + +int driver_register(device_driver* drv) +{ + if (drv == nullptr || drv->bus == nullptr) { + return -EINVAL; + } + if (!drv->bus->registered) { + kprintf("[driver] register '%s' fail: bus '%s' not registered\n", + drv->name != nullptr ? drv->name : "?", + drv->bus->name != nullptr ? drv->bus->name : "?"); + return -ENODEV; + } + + drv->bus_node.init(); + drv->devices.init(); + + LockGuard g(drv->bus->lock); + list_add_tail(&drv->bus_node, &drv->bus->drivers); + + /* Try to bind any unbound devices already on the bus. */ + list_node* cur = drv->bus->devices.next; + while (cur != &drv->bus->devices) { + device* dev = list_entry<device, &device::bus_list>(cur); + cur = cur->next; + if (dev->driver == nullptr) { + device_try_bind(dev, drv); + } + } + return 0; +} + +void driver_unregister(device_driver* drv) +{ + if (drv == nullptr || drv->bus == nullptr) { + return; + } + + LockGuard g(drv->bus->lock); + + /* Unbind every device bound to this driver. */ + while (!drv->devices.is_empty()) { + list_node* first = drv->devices.next; + device* dev = list_entry<device, &device::driver_list>(first); + if (drv->remove != nullptr) { + drv->remove(dev); + } + list_del_init(&dev->driver_list); + dev->driver = nullptr; + } + list_del_init(&drv->bus_node); +} diff --git a/na/drivers/base/module.mk b/na/drivers/base/module.mk new file mode 100644 index 0000000..1d8fb05 --- /dev/null +++ b/na/drivers/base/module.mk @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Richard Qin +# +# Driver model core (mirrors Linux drivers/base/) + +SRCS_CC += drivers/base/bus.cc +SRCS_CC += drivers/base/core.cc +SRCS_CC += drivers/base/driver.cc +SRCS_CC += drivers/base/platform.cc diff --git a/na/drivers/base/platform.cc b/na/drivers/base/platform.cc new file mode 100644 index 0000000..65ec21e --- /dev/null +++ b/na/drivers/base/platform.cc @@ -0,0 +1,231 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * drivers/base/platform.cc — platform bus + FDT-driven device creation. + * + * Mirrors Linux drivers/base/platform.c. Provides: + * - platform_bus — the bus_type instance + * - platform_match — compatible-string matching via of_match_table + * - platform_dispatch — bus->probe forwarder that down-casts and calls + * platform_driver::probe(platform_device*) + * - driver_probe_all() — boot entry: register bus, register linker-set + * drivers, walk FDT, register platform_devices + * + * Reference: docs/specs/storage_stack.md §4.3 + */ + +#include <lume/platform_device.h> +#include <lume/driver.h> +#include <lume/device_node.h> +#include <lume/fdt.h> +#include <string.h> + +/* kmalloc is declared extern "C" in <lume/slab.h>, but mm/slab.cc defines + * it WITHOUT extern "C" (i.e. C++-mangled). Declare locally with C++ + * linkage so the symbol matches what mm/slab.o exports. */ +void* kmalloc(uint32 size); +#include <lume/klog.h> +#include <lume/kprintf.h> +#include <lume/errno.h> +#include <lume/container_of.h> + +/* ------------------------------------------------------------------ */ +/* Linker-section registry */ +/* ------------------------------------------------------------------ */ + +extern "C" { + extern platform_driver* __drivers_start[]; + extern platform_driver* __drivers_end[]; +} + +/* ------------------------------------------------------------------ */ +/* platform_bus_type */ +/* ------------------------------------------------------------------ */ + +static int platform_match(device* dev, device_driver* drv) +{ + platform_device* pdev = to_platform_device(dev); + platform_driver* pdrv = to_platform_driver(drv); + + if (pdrv->of_match_table == nullptr || pdev->of_node == nullptr) { + return 0; + } + const char* node_compat = pdev->of_node->get_prop_string("compatible"); + if (node_compat == nullptr) { + return 0; + } + for (const of_device_id* id = pdrv->of_match_table; + id->compatible != nullptr; ++id) { + if (strcmp(id->compatible, node_compat) == 0) { + return 1; + } + } + return 0; +} + +static int platform_dispatch(device* dev) +{ + platform_device* pdev = to_platform_device(dev); + platform_driver* pdrv = to_platform_driver(dev->driver); + if (pdrv->probe == nullptr) { + return 0; + } + return pdrv->probe(pdev); +} + +bus_type platform_bus = { + .name = "platform", + .match = platform_match, + .probe = platform_dispatch, + .devices = {}, + .drivers = {}, + .lock = {}, + .registered = false, +}; + +void platform_bus_init() +{ + bus_register(&platform_bus); +} + +/* ------------------------------------------------------------------ */ +/* platform_driver_register / platform_device_register */ +/* ------------------------------------------------------------------ */ + +int platform_driver_register(platform_driver* pdrv) +{ + if (pdrv == nullptr) { + return -EINVAL; + } + pdrv->driver.bus = &platform_bus; + /* driver.probe is unused — we route through bus->probe. Set name + * from the first of_match if not already provided. */ + if (pdrv->driver.name == nullptr && pdrv->of_match_table != nullptr) { + pdrv->driver.name = pdrv->of_match_table[0].compatible; + } + return driver_register(&pdrv->driver); +} + +int platform_device_register(platform_device* pdev) +{ + if (pdev == nullptr) { + return -EINVAL; + } + pdev->dev.bus = &platform_bus; + if (pdev->dev.init_name == nullptr && pdev->of_node != nullptr) { + pdev->dev.init_name = pdev->of_node->get_prop_string("compatible"); + } + device_initialize(&pdev->dev); + return device_register(&pdev->dev); +} + +/* ------------------------------------------------------------------ */ +/* FDT-driven device instantiation */ +/* ------------------------------------------------------------------ */ + +static platform_device* alloc_platform_device_from_node(DeviceNode* node, + const char* compat) +{ + auto* pdev = static_cast<platform_device*>( + kmalloc(static_cast<uint32>(sizeof(platform_device)))); + if (pdev == nullptr) { + return nullptr; + } + memset(pdev, 0, sizeof(*pdev)); + pdev->of_node = node; + pdev->mmio_base = node->get_prop_u64("reg", 0); + /* "reg" second cell is size — for now we don't bother parsing it + * separately; drivers that need it pull from of_node directly. */ + pdev->mmio_size = 0; + pdev->irq = -1; + pdev->dev.init_name = compat; + return pdev; +} + +/* For each registered platform_driver, walk its of_match_table and + * spawn a platform_device for each FDT compatible match found. + * + * NOTE: This is a simplified FDT walker — it only finds the FIRST FDT + * node matching each compatible string (delegating to + * fdt_find_compatible). Devices with multiple FDT instances of the + * same compatible will only see one bound here. A full FDT tree walk + * is left for when virtio-mmio or multi-instance UART arrives. + */ +static void instantiate_devices_for_driver(platform_driver* pdrv) +{ + if (pdrv == nullptr || pdrv->of_match_table == nullptr) { + return; + } + for (const of_device_id* id = pdrv->of_match_table; + id->compatible != nullptr; ++id) { + DeviceNode* node = fdt_find_compatible(id->compatible); + if (node == nullptr) { + continue; + } + platform_device* pdev = + alloc_platform_device_from_node(node, id->compatible); + if (pdev == nullptr) { + kprintf("[platform] OOM allocating pdev for %s\n", id->compatible); + continue; + } + kprintf("[platform] instantiating '%s' -> pdev mmio=0x%llx\n", + id->compatible, pdev->mmio_base); + platform_device_register(pdev); + } +} + +/* ------------------------------------------------------------------ */ +/* Boot entry — replaces the old driver_probe_all() */ +/* ------------------------------------------------------------------ */ + +void driver_probe_all() +{ + kprintf("[dpa] step 1: &platform_bus=%p (link 0xffffffc08033a0a8)\n", + &platform_bus); + kprintf("[dpa] step 1: pre-touch platform_bus.name slot...\n"); + const char** name_slot = &platform_bus.name; + kprintf("[dpa] step 1: name_slot=%p\n", name_slot); + kprintf("[dpa] step 1: about to deref *name_slot\n"); + const char* nm = *name_slot; + kprintf("[dpa] step 1: *name_slot=%p\n", nm); + kprintf("[dpa] step 1: about to read first byte of nm\n"); + char first = nm != nullptr ? nm[0] : '!'; + kprintf("[dpa] step 1: nm[0]='%c'\n", first); + + kprintf("[dpa] step 1: platform_bus_init enter\n"); + platform_bus_init(); + kprintf("[dpa] step 1: platform_bus_init done\n"); + + platform_driver** start = __drivers_start; + platform_driver** end = __drivers_end; + kprintf("[dpa] step 2: __drivers_start=%p __drivers_end=%p count=%lld\n", + start, end, (long long)(end - start)); + + for (platform_driver** slot = start; slot < end; ++slot) { + kprintf("[dpa] step 2: slot=%p *slot=%p\n", slot, *slot); + if (*slot != nullptr) { + platform_driver* pdrv = *slot; + kprintf("[dpa] step 2: pdrv->of_match_table=%p\n", + pdrv->of_match_table); + if (pdrv->of_match_table != nullptr) { + kprintf("[dpa] step 2: pdrv->of_match_table[0].compatible=%p\n", + pdrv->of_match_table[0].compatible); + if (pdrv->of_match_table[0].compatible != nullptr) { + kprintf("[dpa] step 2: compatible='%s'\n", + pdrv->of_match_table[0].compatible); + } + } + platform_driver_register(pdrv); + kprintf("[dpa] step 2: registered\n"); + } + } + + kprintf("[dpa] step 3: instantiate devices\n"); + for (platform_driver** slot = start; slot < end; ++slot) { + if (*slot != nullptr) { + instantiate_devices_for_driver(*slot); + } + } + kprintf("[dpa] done\n"); +} diff --git a/na/drivers/console.cc b/na/drivers/console.cc new file mode 100644 index 0000000..f741dd2 --- /dev/null +++ b/na/drivers/console.cc @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * console.cc — Console character device (/dev/console) + * + * Wraps the existing early_putc() UART driver as a file_operations + * chardev. read() is a stub for now (returns 0 = EOF). + * write() sends each byte through early_putc(). + */ + +#include <lume/file.h> +#include <lume/chardev.h> +#include <lume/types.h> + +extern "C" void early_putc(char c); + +/* ================================================================== */ +/* console_write */ +/* ================================================================== */ + +static int64 console_write(file* fp, const char* buf, uint64 count, int64* pos) +{ + (void)fp; + (void)pos; + + for (uint64 i = 0; i < count; i++) { + early_putc(buf[i]); + } + + return static_cast<int64>(count); +} + +/* ================================================================== */ +/* console_read — stub (returns 0 = EOF for now) */ +/* ================================================================== */ + +static int64 console_read(file* fp, char* buf, uint64 count, int64* pos) +{ + (void)fp; + (void)buf; + (void)count; + (void)pos; + /* TODO: implement UART RX polling/IRQ-based blocking read */ + return 0; +} + +/* ================================================================== */ +/* null device — /dev/null */ +/* ================================================================== */ + +static int64 null_read(file* /*fp*/, char* /*buf*/, uint64 /*count*/, int64* /*pos*/) +{ + return 0; /* EOF */ +} + +static int64 null_write(file* /*fp*/, const char* /*buf*/, uint64 count, int64* /*pos*/) +{ + return static_cast<int64>(count); /* discard */ +} + +/* ================================================================== */ +/* zero device — /dev/zero */ +/* ================================================================== */ + +static int64 zero_read(file* /*fp*/, char* buf, uint64 count, int64* /*pos*/) +{ + for (uint64 i = 0; i < count; i++) { + buf[i] = 0; + } + return static_cast<int64>(count); +} + +static int64 zero_write(file* /*fp*/, const char* /*buf*/, uint64 count, int64* /*pos*/) +{ + return static_cast<int64>(count); /* discard */ +} + +/* ================================================================== */ +/* Registration — runtime init (no static initializers!) */ +/* ================================================================== */ + +static file_operations console_fops; +static file_operations null_fops; +static file_operations zero_fops; + +static cdev console_cdev; +static cdev null_cdev; +static cdev zero_cdev; + +void console_cdev_init() +{ + /* Console: major=5, minor=1 */ + console_fops.read = console_read; + console_fops.write = console_write; + + console_cdev.name = "console"; + console_cdev.dev = MKDEV(5, 1); + console_cdev.fops = &console_fops; + cdev_register(&console_cdev); + + /* /dev/null: major=1, minor=3 */ + null_fops.read = null_read; + null_fops.write = null_write; + + null_cdev.name = "null"; + null_cdev.dev = MKDEV(1, 3); + null_cdev.fops = &null_fops; + cdev_register(&null_cdev); + + /* /dev/zero: major=1, minor=5 */ + zero_fops.read = zero_read; + zero_fops.write = zero_write; + + zero_cdev.name = "zero"; + zero_cdev.dev = MKDEV(1, 5); + zero_cdev.fops = &zero_fops; + cdev_register(&zero_cdev); +} diff --git a/na/drivers/driver.cc b/na/drivers/driver.cc deleted file mode 100644 index 5810645..0000000 --- a/na/drivers/driver.cc +++ /dev/null @@ -1,27 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* Copyright (C) 2026 Richard Qin */ - -#include <lume/driver.h> -#include <lume/fdt.h> -#include <lume/kprintf.h> - -extern "C" { - extern DriverEntry __drivers_start[]; - extern DriverEntry __drivers_end[]; -} - -void driver_probe_all() { - DeviceNode* root = fdt_get_node_by_path("/"); - if (!root) { - kprintf("[driver] WARN: No FDT root found\n"); - return; - } - - for (DriverEntry* drv = __drivers_start; drv < __drivers_end; ++drv) { - DeviceNode* node = fdt_find_compatible(drv->compatible); - if (node) { - kprintf("[driver] Probing %s\n", drv->compatible); - drv->probe(node); - } - } -} diff --git a/na/drivers/module.mk b/na/drivers/module.mk index 6e1396c..9326f30 100644 --- a/na/drivers/module.mk +++ b/na/drivers/module.mk @@ -4,7 +4,7 @@ # Drivers module SRCS_CC += drivers/uart.cc -SRCS_CC += drivers/driver.cc +SRCS_CC += drivers/console.cc ifeq ($(ARCH),riscv) SRCS_CC += drivers/plic.cc diff --git a/na/drivers/plic.cc b/na/drivers/plic.cc index ba6ac44..2818700 100644 --- a/na/drivers/plic.cc +++ b/na/drivers/plic.cc @@ -25,7 +25,7 @@ #include <lume/addr.h> #include <lume/config.h> #include <lume/fdt.h> -#include <lume/driver.h> +#include <lume/platform_device.h> #include <lume/irq.h> #include <lume/types.h> #include <lume/vmm.h> @@ -33,6 +33,8 @@ /* PLIC virtual base (after MMIO mapping) */ static uint64 plic_va; +extern "C" uint32 bsp_hart_id; + /* PLIC register offsets */ static constexpr uint64 PLIC_PRIORITY_BASE = 0x000000; static constexpr uint64 PLIC_PENDING_BASE = 0x001000; @@ -159,7 +161,8 @@ static void plic_probe(DeviceNode* node) { vmm_map_kernel_mmio(plic_pa, 0x4000000); plic_va = pa_to_va(phys_addr(plic_pa)).raw; - uint64 ctx = s_context(0); /* BSP = hart 0 */ + /* Use the actual BSP hart ID (it may not be hart 0) */ + uint64 ctx = s_context(bsp_hart_id); /* Set priority for all sources to 1 (minimum non-zero = enabled) */ for (uint32 irq = 1; irq < IrqManager::kMaxIrqs; irq++) { @@ -233,9 +236,24 @@ static const IrqChip plic_chip = { plic_set_priority }; -static void plic_probe_wrapper(DeviceNode* node) { - plic_probe(node); +static int plic_platform_probe(platform_device* pdev) { + plic_probe(pdev->of_node); g_irq_manager.set_chip(&plic_chip); + return 0; } -REGISTER_DRIVER("riscv,plic0", plic_probe_wrapper); +static const of_device_id plic_of_match[] = { + { .compatible = "riscv,plic0", .data = nullptr }, + { .compatible = nullptr, .data = nullptr }, /* sentinel */ +}; + +platform_driver plic_driver = { + .driver = { .name = "plic", .bus = nullptr, + .probe = nullptr, .remove = nullptr, + .bus_node = {}, .devices = {} }, + .probe = plic_platform_probe, + .remove = nullptr, + .of_match_table = plic_of_match, +}; + +PLATFORM_DRIVER(plic_driver); diff --git a/na/drivers/tty/module.mk b/na/drivers/tty/module.mk new file mode 100644 index 0000000..49190d4 --- /dev/null +++ b/na/drivers/tty/module.mk @@ -0,0 +1,5 @@ +# drivers/tty/module.mk — TTY subsystem build rules + +SRCS_CC += drivers/tty/tty_core.cc +SRCS_CC += drivers/tty/n_tty.cc +SRCS_CC += drivers/tty/tty_uart.cc diff --git a/na/drivers/tty/n_tty.cc b/na/drivers/tty/n_tty.cc new file mode 100644 index 0000000..0e312b0 --- /dev/null +++ b/na/drivers/tty/n_tty.cc @@ -0,0 +1,539 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * n_tty.cc — N_TTY line discipline + * + * Implements the standard terminal line discipline: + * + * Canonical mode (ICANON): + * Input is buffered until newline/EOF. Backspace (VERASE) and + * kill-line (VKILL) editing are supported. + * + * Raw mode (!ICANON): + * Characters are available immediately (subject to VMIN/VTIME). + * + * Signal generation (ISIG): + * VINTR → SIGINT, VQUIT → SIGQUIT, VSUSP → SIGTSTP + * + * Echo (ECHO): + * Typed characters are reflected to output. + * + * Input processing (c_iflag): + * ICRNL: CR → NL mapping + * INLCR: NL → CR mapping + * IGNCR: ignore CR + * ISTRIP: strip high bit + * + * Output processing (c_oflag): + * OPOST + ONLCR: NL → CR+NL on output + * + * Reference: Linux drivers/tty/n_tty.c + */ + +#include <lume/tty.h> +#include <lume/tty_ioctl.h> +#include <lume/signal.h> +#include <lume/signal_nr.h> +#include <lume/slab.h> +#include <lume/new.h> +#include <lume/errno.h> +#include <lume/task.h> +#include <lume/sched.h> + +#ifndef ENOIOCTLCMD +#define ENOIOCTLCMD 515 +#endif + +extern void schedule(); + +/* ================================================================== */ +/* N_TTY internal data */ +/* ================================================================== */ + +inline constexpr int N_TTY_BUF_SIZE = 4096; + +struct n_tty_data { + char read_buf[N_TTY_BUF_SIZE]; + int read_head; /* producer writes here (from receive_buf) */ + int read_tail; /* consumer reads from here (from read()) */ + int canon_head; /* in canonical mode: end of last complete line */ + bool eof_pending; /* Ctrl+D was pressed */ + Spinlock lock; +}; + +static inline int buf_count(const n_tty_data* d) +{ + return (d->read_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; +} + +static inline int canon_count(const n_tty_data* d) +{ + return (d->canon_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; +} + +static inline bool buf_full(const n_tty_data* d) +{ + return buf_count(d) >= N_TTY_BUF_SIZE - 1; +} + +static inline void buf_put(n_tty_data* d, char c) +{ + d->read_buf[d->read_head] = c; + d->read_head = (d->read_head + 1) % N_TTY_BUF_SIZE; +} + +static inline char buf_get(n_tty_data* d) +{ + char c = d->read_buf[d->read_tail]; + d->read_tail = (d->read_tail + 1) % N_TTY_BUF_SIZE; + return c; +} + +/* ================================================================== */ +/* Output helpers */ +/* ================================================================== */ + +static void tty_put_char(tty_struct* tty, char c) +{ + if (tty->driver && tty->driver->ops && tty->driver->ops->put_char) { + tty->driver->ops->put_char(tty, c); + } +} + +/* + * Process and output a single character, applying c_oflag transformations. + */ +static void tty_output_char(tty_struct* tty, char c) +{ + if (tty->termios_data.c_oflag & OPOST) { + if (c == '\n' && (tty->termios_data.c_oflag & ONLCR)) { + tty_put_char(tty, '\r'); + tty_put_char(tty, '\n'); + return; + } + if (c == '\r' && (tty->termios_data.c_oflag & OCRNL)) { + tty_put_char(tty, '\n'); + return; + } + } + tty_put_char(tty, c); +} + +/* + * Echo a character back to the terminal. + * Control characters are displayed as ^X. + */ +static void echo_char(tty_struct* tty, char c) +{ + if (!(tty->termios_data.c_lflag & ECHO)) + return; + + if (c == '\n') { + tty_output_char(tty, '\n'); + return; + } + + if (c == '\t') { + tty_put_char(tty, '\t'); + return; + } + + /* Control characters: echo as ^X */ + if ((tty->termios_data.c_lflag & ECHOCTL) && c < 0x20 && c != '\t' && c != '\n') { + tty_put_char(tty, '^'); + tty_put_char(tty, c + '@'); + return; + } + + if (c == 0x7f) { /* DEL */ + tty_put_char(tty, '^'); + tty_put_char(tty, '?'); + return; + } + + tty_output_char(tty, c); +} + +/* + * Erase one character visually (backspace + space + backspace). + */ +static void erase_char(tty_struct* tty, char erased) +{ + if (!(tty->termios_data.c_lflag & ECHO)) + return; + + if ((tty->termios_data.c_lflag & ECHOE)) { + /* If the erased char was a control char displayed as ^X, erase 2 cols */ + if ((erased < 0x20 && erased != '\t' && erased != '\n') || erased == 0x7f) { + tty_put_char(tty, '\b'); + tty_put_char(tty, ' '); + tty_put_char(tty, '\b'); + tty_put_char(tty, '\b'); + tty_put_char(tty, ' '); + tty_put_char(tty, '\b'); + } else { + tty_put_char(tty, '\b'); + tty_put_char(tty, ' '); + tty_put_char(tty, '\b'); + } + } +} + +/* ================================================================== */ +/* Input processing — receive_buf (called from IRQ or driver) */ +/* ================================================================== */ + +static void n_tty_receive_buf(tty_struct* tty, const char* cp, int count) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (!d) return; + + const termios& t = tty->termios_data; + + LockGuard guard(d->lock); + + for (int i = 0; i < count; i++) { + char c = cp[i]; + + /* Strip high bit if requested */ + if (t.c_iflag & ISTRIP) + c &= 0x7f; + + /* Input CR/NL processing */ + if (c == '\r') { + if (t.c_iflag & IGNCR) + continue; + if (t.c_iflag & ICRNL) + c = '\n'; + } else if (c == '\n') { + if (t.c_iflag & INLCR) + c = '\r'; + } + + /* Signal generation */ + if (t.c_lflag & ISIG) { + if (c == t.c_cc[VINTR]) { + tty_signal_pgrp(tty, SIGINT); + echo_char(tty, c); + if (!(t.c_lflag & NOFLSH)) { + /* Flush input buffer */ + d->read_head = d->read_tail; + d->canon_head = d->read_tail; + } + continue; + } + if (c == t.c_cc[VQUIT]) { + tty_signal_pgrp(tty, SIGQUIT); + echo_char(tty, c); + if (!(t.c_lflag & NOFLSH)) { + d->read_head = d->read_tail; + d->canon_head = d->read_tail; + } + continue; + } + if (c == t.c_cc[VSUSP]) { + tty_signal_pgrp(tty, SIGTSTP); + echo_char(tty, c); + continue; + } + } + + /* Canonical mode processing */ + if (t.c_lflag & ICANON) { + /* EOF (Ctrl+D) */ + if (c == t.c_cc[VEOF]) { + d->eof_pending = true; + d->canon_head = d->read_head; + tty->read_wait.wake_all(); + continue; + } + + /* ERASE (backspace/DEL) */ + if (c == t.c_cc[VERASE]) { + if (d->read_head != d->canon_head && + d->read_head != d->read_tail) { + int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; + char erased = d->read_buf[prev]; + d->read_head = prev; + erase_char(tty, erased); + } + continue; + } + + /* KILL (Ctrl+U — erase entire line) */ + if (c == t.c_cc[VKILL]) { + while (d->read_head != d->canon_head && + d->read_head != d->read_tail) { + int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; + char erased = d->read_buf[prev]; + d->read_head = prev; + erase_char(tty, erased); + } + if (t.c_lflag & ECHOK) { + tty_output_char(tty, '\n'); + } + continue; + } + + /* WERASE (Ctrl+W — erase word) */ + if ((t.c_lflag & IEXTEN) && c == t.c_cc[VWERASE]) { + /* Skip trailing spaces */ + while (d->read_head != d->canon_head) { + int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; + if (d->read_buf[prev] != ' ' && d->read_buf[prev] != '\t') + break; + erase_char(tty, d->read_buf[prev]); + d->read_head = prev; + } + /* Erase word */ + while (d->read_head != d->canon_head) { + int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE; + if (d->read_buf[prev] == ' ' || d->read_buf[prev] == '\t') + break; + erase_char(tty, d->read_buf[prev]); + d->read_head = prev; + } + continue; + } + + /* REPRINT (Ctrl+R) */ + if ((t.c_lflag & IEXTEN) && c == t.c_cc[VREPRINT]) { + tty_output_char(tty, '\n'); + int pos = d->canon_head; + while (pos != d->read_head) { + echo_char(tty, d->read_buf[pos]); + pos = (pos + 1) % N_TTY_BUF_SIZE; + } + continue; + } + + /* Regular character — buffer it */ + if (!buf_full(d)) { + buf_put(d, c); + echo_char(tty, c); + } + + /* Line complete on newline or EOL */ + if (c == '\n' || c == t.c_cc[VEOL] || + (t.c_cc[VEOL2] && c == t.c_cc[VEOL2])) { + d->canon_head = d->read_head; + tty->read_wait.wake_all(); + } + } else { + /* Raw / non-canonical mode */ + if (!buf_full(d)) { + buf_put(d, c); + echo_char(tty, c); + tty->read_wait.wake_all(); + } + } + } +} + +/* ================================================================== */ +/* N_TTY read — blocking read from the input buffer */ +/* ================================================================== */ + +static int n_tty_read(tty_struct* tty, file* fp, char* buf, uint64 count) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (!d) return -EIO; + + const termios& t = tty->termios_data; + TaskControlBlock* curr = current_task(); + int copied = 0; + + if (t.c_lflag & ICANON) { + /* Canonical mode: block until a complete line is available */ + while (canon_count(d) == 0 && !d->eof_pending) { + if (tty->flags & TTY_IO_ERROR) + return -EIO; + + /* Check for pending signals — return -EINTR so the + * trap return path can deliver them via do_signal() */ + if (signal_pending(curr)) + return -EINTR; + + tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping); + if (canon_count(d) == 0 && !d->eof_pending) + schedule(); + tty->read_wait.finish_wait(curr); + } + + if (d->eof_pending && canon_count(d) == 0) { + d->eof_pending = false; + return 0; /* EOF */ + } + + /* Copy up to one line */ + LockGuard guard(d->lock); + while (copied < static_cast<int>(count) && d->read_tail != d->canon_head) { + buf[copied++] = buf_get(d); + } + } else { + /* Non-canonical mode */ + int vmin = t.c_cc[VMIN]; + /* VTIME not implemented (would need timers) */ + + if (vmin == 0) vmin = 1; + + while (buf_count(d) < vmin) { + if (tty->flags & TTY_IO_ERROR) + return -EIO; + + if (signal_pending(curr)) + return -EINTR; + + tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping); + if (buf_count(d) < vmin) + schedule(); + tty->read_wait.finish_wait(curr); + } + + LockGuard guard(d->lock); + while (copied < static_cast<int>(count) && buf_count(d) > 0) { + buf[copied++] = buf_get(d); + } + } + + return copied; +} + +/* ================================================================== */ +/* N_TTY write — output through the driver with post-processing */ +/* ================================================================== */ + +static int n_tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count) +{ + if (!tty->driver || !tty->driver->ops || !tty->driver->ops->write) + return -EIO; + + /* Apply output processing (OPOST) */ + if (tty->termios_data.c_oflag & OPOST) { + for (uint64 i = 0; i < count; i++) { + tty_output_char(tty, buf[i]); + } + return static_cast<int>(count); + } + + /* Raw output: pass directly to driver */ + return tty->driver->ops->write(tty, buf, static_cast<int>(count)); +} + +/* ================================================================== */ +/* N_TTY ioctl */ +/* ================================================================== */ + +static int n_tty_ioctl(tty_struct* tty, unsigned int cmd, uint64 arg) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + + switch (cmd) { + case FIONREAD: { + int count = buf_count(d); + auto* p = reinterpret_cast<int*>(arg); + *p = count; + return 0; + } + default: + return -ENOIOCTLCMD; + } +} + +/* ================================================================== */ +/* N_TTY open / close */ +/* ================================================================== */ + +static int n_tty_open(tty_struct* tty) +{ + auto* d = new n_tty_data{}; + if (!d) return -ENOMEM; + + d->read_head = 0; + d->read_tail = 0; + d->canon_head = 0; + d->eof_pending = false; + d->lock.init("n_tty"); + + tty->ldisc.data = d; + return 0; +} + +static void n_tty_close(tty_struct* tty) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (d) { + delete d; + tty->ldisc.data = nullptr; + } +} + +static void n_tty_flush_buffer(tty_struct* tty) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (!d) return; + LockGuard guard(d->lock); + d->read_head = 0; + d->read_tail = 0; + d->canon_head = 0; + d->eof_pending = false; +} + +static int n_tty_chars_in_buffer(tty_struct* tty) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (!d) return 0; + return buf_count(d); +} + +/* + * Probe input readiness for poll(). In canonical mode the read syscall + * blocks until a whole line is queued, so we mirror that: POLLIN only + * when canon_count() shows a finalized line (or pending EOF). In raw + * mode any character satisfies VMIN==1 immediately. Without this + * mirror, ppoll() would falsely signal POLLIN on partial canonical + * input, then read() would block — exactly the busy-loop the shell's + * ppoll-then-read pattern is trying to avoid. + */ +bool n_tty_input_ready(tty_struct* tty) +{ + auto* d = static_cast<n_tty_data*>(tty->ldisc.data); + if (!d) return false; + const termios& t = tty->termios_data; + if (t.c_lflag & ICANON) + return canon_count(d) != 0 || d->eof_pending; + int vmin = t.c_cc[VMIN]; + if (vmin == 0) vmin = 1; + return buf_count(d) >= vmin; +} + +static void n_tty_set_termios(tty_struct* /*tty*/, const termios* /*old*/) +{ + /* Nothing special needed for now */ +} + +static void n_tty_hangup(tty_struct* tty) +{ + n_tty_flush_buffer(tty); + tty->read_wait.wake_all(); +} + +/* ================================================================== */ +/* N_TTY ops table */ +/* ================================================================== */ + +const tty_ldisc_ops n_tty_ops = { + .name = "n_tty", + .num = 0, /* N_TTY */ + .open = n_tty_open, + .close = n_tty_close, + .read = n_tty_read, + .write = n_tty_write, + .receive_buf = n_tty_receive_buf, + .ioctl = n_tty_ioctl, + .flush_buffer = n_tty_flush_buffer, + .chars_in_buffer = n_tty_chars_in_buffer, + .set_termios = n_tty_set_termios, + .hangup = n_tty_hangup, +}; diff --git a/na/drivers/tty/tty_core.cc b/na/drivers/tty/tty_core.cc new file mode 100644 index 0000000..5985bd9 --- /dev/null +++ b/na/drivers/tty/tty_core.cc @@ -0,0 +1,536 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * tty_core.cc — TTY subsystem core + * + * Provides the tty_struct lifecycle, file_operations dispatch, + * and ioctl routing between the line discipline and driver. + * + * Call chain: + * sys_read → tty_fops.read → tty_read → ldisc.read + * sys_write → tty_fops.write → tty_write → ldisc.write + * sys_ioctl → tty_fops.ioctl → tty_ioctl → {core / ldisc / driver} + */ + +#include <lume/tty.h> +#include <lume/tty_ioctl.h> +#include <lume/file.h> +#include <lume/fs_types.h> +#include <lume/chardev.h> +#include <lume/signal.h> +#include <lume/signal_types.h> +#include <lume/signal_nr.h> +#include <lume/task.h> +#include <lume/pgrp.h> +#include <lume/poll.h> +#include <lume/new.h> +#include <lume/errno.h> +#include <lume/klog.h> +#include <lume/slab.h> + +#ifndef ENOIOCTLCMD +#define ENOIOCTLCMD 515 +#endif + +/* ================================================================== */ +/* Global console TTY */ +/* ================================================================== */ + +tty_struct* g_console_tty = nullptr; + +/* ================================================================== */ +/* tty_struct allocation */ +/* ================================================================== */ + +tty_struct* tty_alloc(const tty_driver* driver, int index) +{ + auto* tty = new tty_struct{}; + if (!tty) return nullptr; + + tty->index = index; + tty->driver = driver; + tty->link = nullptr; + tty->pgrp = 0; + tty->session = 0; + tty->write_cnt = 0; + tty->flags = 0; + tty->open_count = 0; + tty->driver_data = nullptr; + + tty_default_termios(&tty->termios_data); + + tty->ws.ws_row = 24; + tty->ws.ws_col = 80; + tty->ws.ws_xpixel = 0; + tty->ws.ws_ypixel = 0; + + tty->lock.init("tty"); + tty->read_wait.init("tty_rd"); + tty->write_wait.init("tty_wr"); + + /* Attach default line discipline (N_TTY) */ + tty->ldisc.ops = &n_tty_ops; + tty->ldisc.data = nullptr; + + return tty; +} + +/* ================================================================== */ +/* tty_open / tty_close */ +/* ================================================================== */ + +int tty_open(tty_struct* tty) +{ + if (!tty) return -EINVAL; + + if (tty->open_count == 0) { + /* Initialize line discipline */ + if (tty->ldisc.ops && tty->ldisc.ops->open) { + int ret = tty->ldisc.ops->open(tty); + if (ret < 0) return ret; + } + + /* Initialize driver */ + if (tty->driver && tty->driver->ops && tty->driver->ops->open) { + int ret = tty->driver->ops->open(tty); + if (ret < 0) { + if (tty->ldisc.ops && tty->ldisc.ops->close) + tty->ldisc.ops->close(tty); + return ret; + } + } + } + + tty->open_count++; + return 0; +} + +void tty_close(tty_struct* tty) +{ + if (!tty) return; + + tty->open_count--; + if (tty->open_count <= 0) { + tty->open_count = 0; + + if (tty->driver && tty->driver->ops && tty->driver->ops->close) + tty->driver->ops->close(tty); + + if (tty->ldisc.ops && tty->ldisc.ops->close) + tty->ldisc.ops->close(tty); + } +} + +/* ================================================================== */ +/* Core I/O */ +/* ================================================================== */ + +int64 tty_read(tty_struct* tty, file* fp, char* buf, uint64 count) +{ + if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->read) + return -EIO; + + return tty->ldisc.ops->read(tty, fp, buf, count); +} + +int64 tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count) +{ + if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->write) + return -EIO; + + return tty->ldisc.ops->write(tty, fp, buf, count); +} + +/* ================================================================== */ +/* tty_receive_buf — called by driver on RX */ +/* ================================================================== */ + +void tty_receive_buf(tty_struct* tty, const char* buf, int count) +{ + if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->receive_buf) + return; + + tty->ldisc.ops->receive_buf(tty, buf, count); +} + +/* ================================================================== */ +/* tty_signal_pgrp — send signal to foreground process group */ +/* ================================================================== */ + +extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task); + +void tty_signal_pgrp(tty_struct* tty, int sig) +{ + if (!tty) return; + + int32 pgid; + { + LockGuard guard(tty->lock); + pgid = tty->pgrp; + } + if (pgid <= 0) return; + + /* Deliver to the leader of every thread group in the foreground + * process group. ldisc paths call this for SIGINT/SIGQUIT/SIGWINCH + * etc.; previously we only signalled the current task, which was + * incorrect when the fg pgrp had multiple processes. */ + kernel_siginfo info{}; + info.si_signo = sig; + info.si_code = 0; /* SI_KERNEL would be 0x80; user rarely checks */ + for_each_pgrp_member(pgid, [&](signal_struct* s) { + if (s->leader) + send_signal(sig, &info, s->leader); + }); +} + +/* ================================================================== */ +/* tty_ioctl — ioctl dispatch */ +/* ================================================================== */ + +long tty_ioctl(tty_struct* tty, file* /*fp*/, unsigned int cmd, uint64 arg) +{ + if (!tty) return -EINVAL; + + switch (cmd) { + case TCGETS: { + auto* p = reinterpret_cast<termios*>(arg); + if (!p) return -EINVAL; + *p = tty->termios_data; + return 0; + } + + case TCSETS: + case TCSETSW: + case TCSETSF: { + auto* p = reinterpret_cast<const termios*>(arg); + if (!p) return -EINVAL; + termios old = tty->termios_data; + tty->termios_data = *p; + + /* Notify line discipline of termios change */ + if (tty->ldisc.ops && tty->ldisc.ops->set_termios) + tty->ldisc.ops->set_termios(tty, &old); + + /* If TCSETSF, flush input buffer */ + if (cmd == TCSETSF) { + if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer) + tty->ldisc.ops->flush_buffer(tty); + } + return 0; + } + + case TIOCGWINSZ: { + auto* p = reinterpret_cast<winsize*>(arg); + if (!p) return -EINVAL; + *p = tty->ws; + return 0; + } + + case TIOCSWINSZ: { + auto* p = reinterpret_cast<const winsize*>(arg); + if (!p) return -EINVAL; + winsize old = tty->ws; + tty->ws = *p; + if (old.ws_row != tty->ws.ws_row || old.ws_col != tty->ws.ws_col) { + tty_signal_pgrp(tty, SIGWINCH); + } + return 0; + } + + case TIOCGPGRP: { + auto* p = reinterpret_cast<int32*>(arg); + if (!p) return -EINVAL; + + TaskControlBlock* curr = current_task(); + if (!curr || !curr->signal) return -ENOTTY; + if (curr->signal->tty != tty) return -ENOTTY; + + int32 pgid; + { + LockGuard guard(tty->lock); + pgid = tty->pgrp != 0 ? tty->pgrp : curr->signal->pgid; + } + *p = pgid; + return 0; + } + + case TIOCSPGRP: { + auto* p = reinterpret_cast<const int32*>(arg); + if (!p) return -EINVAL; + + TaskControlBlock* curr = current_task(); + if (!curr || !curr->signal) return -ENOTTY; + if (curr->signal->tty != tty) return -ENOTTY; + + int32 new_pgid = *p; + if (new_pgid <= 0) return -EINVAL; + + /* new_pgid must be in the caller's session. */ + bool ok = false; + for_each_pgrp_member(new_pgid, [&](signal_struct* s) { + if (s->sid == curr->signal->sid) ok = true; + }); + if (!ok) return -EPERM; + + LockGuard guard(tty->lock); + tty->pgrp = new_pgid; + return 0; + } + + case TIOCSCTTY: { + /* Become the controlling terminal of the caller's session. + * Linux: only session leader without ctty may do this; with + * arg==1, CAP_SYS_ADMIN may steal a ctty from another session. + * We support the basic path; "steal" is left unimplemented. */ + TaskControlBlock* curr = current_task(); + if (!curr || !curr->signal) return -EPERM; + signal_struct* sig = curr->signal; + + if (sig->sid != curr->pid) return -EPERM; /* not leader */ + + LockGuard sg(sig->lock); + if (sig->tty != nullptr) return -EPERM; /* already have one */ + + LockGuard tg(tty->lock); + if (tty->session != 0 && tty->session != sig->sid) { + return -EPERM; /* owned elsewhere */ + } + tty->session = sig->sid; + tty->pgrp = sig->pgid; + sig->tty = tty; + return 0; + } + + case TIOCNOTTY: { + /* Detach from controlling terminal. */ + TaskControlBlock* curr = current_task(); + if (!curr || !curr->signal) return -ENOTTY; + signal_struct* sig = curr->signal; + if (sig->tty != tty) return -ENOTTY; + + /* Session leader: HUP everyone left in the old fg pgrp. + * Job-control conformance — without this, daemons spawned by + * a relinquishing leader keep blocking on a dead terminal. */ + bool was_leader = (sig->sid == curr->pid); + int32 hup_pgrp = 0; + { + LockGuard tg(tty->lock); + hup_pgrp = tty->pgrp; + if (was_leader) { + tty->session = 0; + tty->pgrp = 0; + } + } + { + LockGuard sg(sig->lock); + sig->tty = nullptr; + } + if (was_leader && hup_pgrp > 0) { + tty_signal_pgrp(tty, SIGHUP); + tty_signal_pgrp(tty, SIGCONT); + (void)hup_pgrp; + } + return 0; + } + + case FIONREAD: { + /* Delegate to line discipline */ + if (tty->ldisc.ops && tty->ldisc.ops->ioctl) { + int ret = tty->ldisc.ops->ioctl(tty, cmd, arg); + if (ret != -ENOIOCTLCMD) + return ret; + } + return 0; + } + + case TCFLSH: { + int queue = static_cast<int>(arg); + if (queue == 0 || queue == 2) { + /* Flush input */ + if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer) + tty->ldisc.ops->flush_buffer(tty); + } + if (queue == 1 || queue == 2) { + /* Flush output */ + if (tty->driver && tty->driver->ops && tty->driver->ops->flush_buffer) + tty->driver->ops->flush_buffer(tty); + } + return 0; + } + + default: + break; + } + + /* Try line discipline ioctl */ + if (tty->ldisc.ops && tty->ldisc.ops->ioctl) { + int ret = tty->ldisc.ops->ioctl(tty, cmd, arg); + if (ret != -ENOIOCTLCMD) + return ret; + } + + /* Try driver ioctl */ + if (tty->driver && tty->driver->ops && tty->driver->ops->ioctl) { + int ret = tty->driver->ops->ioctl(tty, cmd, arg); + if (ret != -ENOIOCTLCMD) + return ret; + } + + return -ENOTTY; +} + +/* ================================================================== */ +/* file_operations for TTY devices */ +/* ================================================================== */ + +static int64 tty_fops_read(file* fp, char* buf, uint64 count, int64* /*pos*/) +{ + auto* tty = static_cast<tty_struct*>(fp->private_data); + return tty_read(tty, fp, buf, count); +} + +static int64 tty_fops_write(file* fp, const char* buf, uint64 count, int64* /*pos*/) +{ + auto* tty = static_cast<tty_struct*>(fp->private_data); + return tty_write(tty, fp, buf, count); +} + +static long tty_fops_ioctl(file* fp, unsigned int cmd, uint64 arg) +{ + auto* tty = static_cast<tty_struct*>(fp->private_data); + return tty_ioctl(tty, fp, cmd, arg); +} + +/* + * tty poll callback — Linux drivers/tty/tty_io.c:tty_poll() does the + * same thing: register tty->read_wait so the poller is woken on input, + * then ask the ldisc whether data is currently available. Write side + * is always considered ready since our writes don't block. + */ +extern bool n_tty_input_ready(tty_struct* tty); +extern void poll_wait(file*, WaitQueue*, poll_table*); + +static uint32 tty_fops_poll(file* fp, void* pt) +{ + auto* tty = static_cast<tty_struct*>(fp->private_data); + if (!tty) return POLLNVAL; + + poll_wait(fp, &tty->read_wait, static_cast<poll_table*>(pt)); + + uint32 mask = POLLOUT | POLLWRNORM; /* always writable */ + if (n_tty_input_ready(tty)) + mask |= POLLIN | POLLRDNORM; + return mask; +} + +/* + * Auto-acquire a controlling terminal on open. + * + * POSIX rule: when a session leader without a controlling tty opens a + * terminal that itself has no controlling session, and the caller did + * not pass O_NOCTTY, that terminal becomes the controlling terminal of + * the caller's session. tty->session and tty->pgrp are seeded from the + * caller's sid/pgid so that the very first tcgetpgrp() call returns a + * value matching the caller's pgid — which is precisely what BusyBox + * ash:setjobctl() needs to exit its setup loop. + * + * Reference: Linux drivers/tty/tty_jobctrl.c:tty_open_proc_set_tty() + */ +static void tty_acquire_ctty(tty_struct* tty, file* fp) +{ + TaskControlBlock* curr = current_task(); + if (!curr || !curr->signal) return; + if (fp->f_flags & O_NOCTTY) return; + + signal_struct* sig = curr->signal; + + /* Must be session leader (sid == pid). */ + if (sig->sid != curr->pid) return; + + LockGuard sig_guard(sig->lock); + if (sig->tty != nullptr) return; /* already have one */ + + LockGuard tty_guard(tty->lock); + if (tty->session != 0) return; /* tty owned by another sess */ + + tty->session = sig->sid; + tty->pgrp = sig->pgid; + sig->tty = tty; +} + +static int tty_fops_open(inode* /*ip*/, file* fp) +{ + /* The console TTY is pre-allocated; just attach it */ + if (!g_console_tty) return -ENXIO; + fp->private_data = g_console_tty; + int err = tty_open(g_console_tty); + if (err == 0) { + tty_acquire_ctty(g_console_tty, fp); + } + return err; +} + +static int tty_fops_release(inode* /*ip*/, file* fp) +{ + auto* tty = static_cast<tty_struct*>(fp->private_data); + tty_close(tty); + return 0; +} + +file_operations tty_fops; + +static void init_tty_fops() +{ + tty_fops.read = tty_fops_read; + tty_fops.write = tty_fops_write; + tty_fops.unlocked_ioctl = tty_fops_ioctl; + tty_fops.poll = tty_fops_poll; + tty_fops.open = tty_fops_open; + tty_fops.release = tty_fops_release; +} + +/* ================================================================== */ +/* TTY chardev registration */ +/* ================================================================== */ + +static cdev tty_cdev; + +void tty_init() +{ + init_tty_fops(); + + /* Allocate the console TTY */ + g_console_tty = tty_alloc(&uart_tty_driver, 0); + if (!g_console_tty) { + return; + } + + /* Open the TTY (initializes ldisc + driver) */ + tty_open(g_console_tty); + + /* Leave tty->session and tty->pgrp at 0. The first session-leader + * to open /dev/console without O_NOCTTY claims it as its controlling + * terminal via tty_acquire_ctty(), which fills both fields. Pre- + * seeding them here (as we used to with the hardcoded pid==1) would + * make every subsequent acquire path see "owned by another session" + * and bail out, leaving sh's tcgetpgrp() returning -ENOTTY and + * disabling job control. */ + + /* Initialize UART RX interrupts */ + uart_tty_init(g_console_tty); + + /* Register UART as a struct console driver. + * After this, printk/klog output goes through console_write_all() + * → uart_console_write() instead of raw early_putc(). */ + extern void uart_register_console(); + uart_register_console(); + + /* Register as /dev/console chardev — replace the old console_cdev. + * We use the same major/minor (5,1) so that existing opens + * to /dev/console will get the TTY fops. */ + tty_cdev.name = "console"; + tty_cdev.dev = MKDEV(5, 1); + tty_cdev.fops = &tty_fops; + cdev_register(&tty_cdev); +} diff --git a/na/drivers/tty/tty_uart.cc b/na/drivers/tty/tty_uart.cc new file mode 100644 index 0000000..973a6e9 --- /dev/null +++ b/na/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)); +} diff --git a/na/drivers/uart.cc b/na/drivers/uart.cc index 264ec6a..467b496 100644 --- a/na/drivers/uart.cc +++ b/na/drivers/uart.cc @@ -2,9 +2,18 @@ /* Copyright (C) 2026 Richard Qin */ /* - * uart.cc — 16550a UART driver (early console) + * uart.cc — 16550a UART driver + console registry * - * Implements the early UART output mapping required by panic and kernel_main. + * Two roles: + * 1. Early console: early_putc() for pre-init output (panic, kprintf) + * 2. Runtime console: struct console registered with register_console(). + * klog/printk route through console_write_all() → uart_console_write(). + * + * The console registry is a simple linked list of struct console. + * console_write_all() iterates the list and calls each console's write(). + * If no console is registered, it falls back to early_putc(). + * + * Reference: Linux kernel/printk/printk.c, drivers/tty/serial/8250/ */ #include <lume/console.h> @@ -13,11 +22,19 @@ #include <lume/addr.h> #include <arch/config.h> +/* ================================================================== */ +/* UART state */ +/* ================================================================== */ + static uint64 g_uart_pa = 0; static uint64 g_uart_size = 0; static bool g_runtime_mapping_ready = false; static bool g_mmu_enabled = false; +/* ================================================================== */ +/* Early console (Phase 1) */ +/* ================================================================== */ + extern "C" void console_set_mmu_enabled() { g_mmu_enabled = true; } @@ -70,3 +87,95 @@ void console_init() { void console_use_runtime_mapping() { g_runtime_mapping_ready = true; } + +/* ================================================================== */ +/* Console registry (Phase 2) — linked list of struct console */ +/* ================================================================== */ + +static list_node g_console_list; +static bool g_console_list_init = false; + +void register_console(console* con) +{ + if (!con) return; + + if (!g_console_list_init) { + g_console_list.init(); + g_console_list_init = true; + } + + con->flags |= CON_ENABLED; + con->list.init(); + + /* Insert at tail */ + list_add_tail(&con->list, &g_console_list); +} + +void unregister_console(console* con) +{ + if (!con) return; + + list_del_init(&con->list); + con->flags &= ~CON_ENABLED; +} + +bool console_drivers_registered() +{ + if (!g_console_list_init) return false; + return !list_empty(&g_console_list); +} + +void console_write_all(const char* s, int count) +{ + if (g_console_list_init && !list_empty(&g_console_list)) { + list_node* pos; + list_for_each(pos, &g_console_list) { + auto* con = container_of(pos, console, list); + if ((con->flags & CON_ENABLED) && con->write) { + con->write(con, s, static_cast<unsigned int>(count)); + } + } + } else { + /* Fallback: direct early_putc (pre-registration) */ + for (int i = 0; i < count; i++) { + early_putc(s[i]); + } + } +} + +/* ================================================================== */ +/* UART console driver */ +/* ================================================================== */ + +static void uart_console_write(console* /*con*/, const char* s, unsigned int count) +{ + for (unsigned int i = 0; i < count; i++) { + if (s[i] == '\n') { + early_putc('\r'); + } + early_putc(s[i]); + } +} + +/* + * uart_console must be initialized at RUNTIME, not with static + * aggregate initializers. Reason: KASLR self-relocation processes + * .rela.dyn entries for simple pointer variables, but may not cover + * function pointer fields inside static aggregate initializers. + * A static `{ .write = uart_console_write }` would store the + * pre-relocation address, causing a crash when called. + */ +static console uart_console; + +void uart_register_console() +{ + uart_console.name = "ttyS"; + uart_console.write = uart_console_write; + uart_console.setup = nullptr; + uart_console.flags = CON_ENABLED | CON_BOOT; + uart_console.index = 0; + uart_console.data = nullptr; + uart_console.list.init(); + register_console(&uart_console); +} + |
