From 5338f61e2178a44bade640c15728ea67ba119a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 12:24:25 +0800 Subject: [PATCH 01/16] refactor(serial): use irq-driven runtime core --- .claude/skills/arch-platform-porting/SKILL.md | 1 + .claude/skills/cross-kernel-driver/SKILL.md | 9 +- .../references/architecture.md | 23 +- Cargo.lock | 2 +- components/irq-framework/src/types.rs | 5 + components/irq-framework/tests/std_sim.rs | 17 + components/someboot/src/acpi/earlycon.rs | 44 +- .../someboot/src/arch/aarch64/paging/mod.rs | 3 +- components/someboot/src/arch/riscv64/mod.rs | 20 +- .../someboot/src/arch/x86_64/console.rs | 19 +- components/someboot/src/console/mod.rs | 271 +++-- components/someboot/src/fdt/earlycon.rs | 20 +- components/someboot/src/lib.rs | 1 + components/someboot/src/mem/mmu.rs | 2 +- components/starry-process/src/process.rs | 15 + components/starry-process/tests/process.rs | 8 +- drivers/ax-driver/Cargo.toml | 3 +- drivers/ax-driver/src/binding_info.rs | 2 +- drivers/ax-driver/src/serial/mod.rs | 508 +++++----- drivers/ax-driver/src/serial/ns16550.rs | 178 ++++ drivers/ax-driver/src/serial/pl011.rs | 46 + drivers/ax-driver/src/serial/rockchip_fiq.rs | 324 ++++++ drivers/ax-driver/src/serial/runtime.rs | 120 +++ drivers/blk/nvme-driver/src/block.rs | 54 +- drivers/blk/nvme-driver/src/command.rs | 29 +- drivers/blk/nvme-driver/src/nvme.rs | 27 +- drivers/interface/rdif-serial/Cargo.toml | 1 - drivers/interface/rdif-serial/src/core.rs | 546 +++++++++++ drivers/interface/rdif-serial/src/lib.rs | 306 +----- drivers/interface/rdif-serial/src/queue.rs | 61 ++ drivers/interface/rdif-serial/src/raw.rs | 96 ++ drivers/interface/rdif-serial/src/serial.rs | 301 ------ drivers/interface/rdif-serial/src/types.rs | 98 ++ drivers/rdrive/src/driver/mod.rs | 4 + drivers/rdrive/src/lib.rs | 22 + drivers/rdrive/src/probe/acpi.rs | 111 +++ drivers/rdrive/src/probe/fdt/mod.rs | 37 +- drivers/rdrive/tests/fdt_probe.rs | 149 +++ drivers/serial/some-serial/README.md | 229 ++--- drivers/serial/some-serial/src/lib.rs | 103 +- .../serial/some-serial/src/ns16550/dw_apb.rs | 85 +- .../serial/some-serial/src/ns16550/mmio.rs | 27 +- drivers/serial/some-serial/src/ns16550/mod.rs | 922 ++++++++++++++---- drivers/serial/some-serial/src/ns16550/pio.rs | 26 +- .../some-serial/src/ns16550/registers.rs | 1 + .../some-serial/src/ns16550/rockchip_fiq.rs | 187 ++-- drivers/serial/some-serial/src/pl011.rs | 552 +++++++---- .../test_crates/driver-tests/tests/serial.rs | 389 -------- os/StarryOS/kernel/Cargo.toml | 2 +- os/StarryOS/kernel/src/entry.rs | 16 +- .../kernel/src/pseudofs/dev/cvi_camera.rs | 32 +- .../kernel/src/pseudofs/dev/irq_byte_ring.rs | 11 +- os/StarryOS/kernel/src/pseudofs/dev/mod.rs | 40 +- .../kernel/src/pseudofs/dev/tty/mod.rs | 139 ++- .../kernel/src/pseudofs/dev/tty/ntty.rs | 462 --------- .../kernel/src/pseudofs/dev/tty/pty.rs | 5 +- .../kernel/src/pseudofs/dev/tty/serial.rs | 699 +++++++++++++ .../src/pseudofs/dev/tty/terminal/ldisc.rs | 403 +++++++- .../src/pseudofs/dev/tty/terminal/termios.rs | 68 +- .../kernel/src/pseudofs/dev/tty_serial.rs | 408 -------- os/StarryOS/kernel/src/syscall/fs/fd_ops.rs | 11 +- os/StarryOS/kernel/src/syscall/task/ptrace.rs | 37 +- os/StarryOS/kernel/src/task/ops.rs | 18 +- os/StarryOS/kernel/src/task/user.rs | 19 +- os/arceos/modules/axhal/src/dummy.rs | 6 +- os/arceos/modules/axhal/src/lib.rs | 5 +- os/arceos/modules/axtask/src/wait_queue.rs | 10 +- .../vms/qemu/x86_64/linux-svm-smp1.toml | 14 +- .../vms/qemu/x86_64/linux-vmx-smp1.toml | 14 +- .../src/console.rs | 6 +- .../ax-plat-riscv64-sg2002/src/console.rs | 29 +- .../src/console.rs | 6 +- platforms/ax-plat/src/console.rs | 21 + platforms/ax-plat/src/irq.rs | 5 +- platforms/axplat-dyn/src/console.rs | 12 +- platforms/axplat-dyn/src/irq.rs | 9 +- platforms/somehal/src/boot_console.rs | 259 +++++ platforms/somehal/src/lib.rs | 2 + scripts/axbuild/src/axvisor/test/tests.rs | 28 +- scripts/axbuild/src/starry/config.rs | 26 +- .../build-aarch64-unknown-none-softfloat.toml | 1 + .../src/main.c | 59 +- .../system/test-gdb-native-batch/src/tracer.c | 20 +- .../tty-console-input-burst/qemu-aarch64.toml | 88 ++ .../qemu-loongarch64.toml | 80 ++ .../tty-console-input-burst/qemu-riscv64.toml | 88 ++ .../tty-console-input-burst/qemu-x86_64.toml | 66 ++ 87 files changed, 5949 insertions(+), 3279 deletions(-) create mode 100644 drivers/ax-driver/src/serial/ns16550.rs create mode 100644 drivers/ax-driver/src/serial/pl011.rs create mode 100644 drivers/ax-driver/src/serial/rockchip_fiq.rs create mode 100644 drivers/ax-driver/src/serial/runtime.rs create mode 100644 drivers/interface/rdif-serial/src/core.rs create mode 100644 drivers/interface/rdif-serial/src/queue.rs create mode 100644 drivers/interface/rdif-serial/src/raw.rs delete mode 100644 drivers/interface/rdif-serial/src/serial.rs create mode 100644 drivers/interface/rdif-serial/src/types.rs create mode 100644 drivers/rdrive/tests/fdt_probe.rs delete mode 100644 drivers/test_crates/driver-tests/tests/serial.rs delete mode 100644 os/StarryOS/kernel/src/pseudofs/dev/tty/ntty.rs create mode 100644 os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs delete mode 100644 os/StarryOS/kernel/src/pseudofs/dev/tty_serial.rs create mode 100644 platforms/somehal/src/boot_console.rs create mode 100644 test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-aarch64.toml create mode 100644 test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-loongarch64.toml create mode 100644 test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-riscv64.toml create mode 100644 test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-x86_64.toml diff --git a/.claude/skills/arch-platform-porting/SKILL.md b/.claude/skills/arch-platform-porting/SKILL.md index b416499a3a..6ea58dfbbd 100644 --- a/.claude/skills/arch-platform-porting/SKILL.md +++ b/.claude/skills/arch-platform-porting/SKILL.md @@ -28,6 +28,7 @@ Current Axvisor LoongArch QEMU tests intentionally use the static `ax-hal/loonga - **CPU runtime**: update `components/axcpu/src/` for trap entry, context switch, user/kernel context, syscall return path, FP/SIMD state, and per-CPU assumptions. - **Platform bridge**: update `platforms/axplat-dyn`, `platforms/somehal`, platform config, memory regions, IRQ routing, timer source, power operations, and CPU boot operations. - **Runtime IRQ ownership**: ArceOS runtime IRQ traps are owned by `ax-cpu` and dispatched through `ax_hal::irq::handle_irq`. `somehal` must stay OS-free and expose controller transactions through `somehal::irq::begin_irq(raw) -> ActiveIrq`; `ActiveIrq` is held while `axplat-dyn` dispatches the IRQ and its `Drop` performs the architecture-specific EOI/complete. Do not reintroduce `_someboot_handle_irq` or `#[somehal::irq_handler]` as runtime dispatch glue. +- **Runtime console selection**: Dynamic platforms expose the firmware-selected hardware console through `somehal::console_device_id()` and `ax_hal::console::device_id()`. The value is `Result` derived from bootargs `console=`, ACPI SPCR, or FDT `stdout-path`; static platforms return `Err(NotSpecified)`. OS code such as Starry should match `Ok(id)` against probed serial devices, use `ttyS0` as the Linux-style hardware-console fallback only for `Err(NotSpecified)`, and leave `/dev/console` unbound (`ENODEV`) for non-hardware console selections, unmatched selected hardware devices, or when no serial console TTY exists. Do not reparse FDT or bootargs in the tty layer. - **Dynamic firmware devices**: for `rdrive` ACPI probes, real non-empty ACPI ID lists enumerate namespace `Device` nodes and expose `_CRS` memory, I/O port, and IRQ resources through `AcpiInfo`; empty ID lists or synthetic root IDs are reserved for root-table style callbacks. - **Page tables and memory**: check PTE flags, huge page support, direct map, kernel high map, MMIO map, TLB/cache barriers, and early `phys_to_virt` behavior before MMU state is fully recorded. - **Drivers and rootfs**: check PCI command bits, MMIO/iomap, DMA address width, virtio transport, block device visibility, rootfs patching, and console/input feature flags. diff --git a/.claude/skills/cross-kernel-driver/SKILL.md b/.claude/skills/cross-kernel-driver/SKILL.md index 84e26dee84..c5f300e797 100644 --- a/.claude/skills/cross-kernel-driver/SKILL.md +++ b/.claude/skills/cross-kernel-driver/SKILL.md @@ -20,9 +20,10 @@ For nontrivial driver design or refactoring, read `references/architecture.md` b 5. For ArceOS/dynamic-platform integration, keep adapters in the existing platform module names such as `platform/axplat-dyn/src/drivers/blk`, even if the reusable crate lives under `drivers/block`. 6. Use small capability traits or API objects instead of a monolithic `KernelHal`. Split MMIO, DMA, IRQ event, queue contract, and wake/poll boundaries. 7. Model queues as independent running units. Prefer APIs such as `submit`, `reclaim`, `poll`, `submit_request`, and `poll_request`. -8. Make IRQ paths return stable events, normally `handle_irq() -> Event`. OS Glue decides whether to wake a thread, wake a future, schedule a worker, or set a pending flag. -9. When IRQ and task paths share mutable driver state, look for an explicit exclusion protocol: task-side mutation masks the exact interrupt source before taking the lock, while IRQ only touches pre-registered stable state. Document the lifetime/safety contract; otherwise prefer atomics/pending bits plus a deferred worker. -10. Validate the changed crate with formatting and targeted clippy before finishing. +8. For IRQ-driven devices, keep IRQ endpoints and queue endpoints separate. IRQ handlers should synchronize hardware events into queue-local completion state; queues should advance their own work without locking the IRQ handler or re-reading shared/destructive IRQ status. +9. Make IRQ paths return stable events, normally `handle_irq() -> Event`. OS Glue decides whether to wake a thread, wake a future, schedule a worker, or set a pending flag. +10. When IRQ and task paths share mutable driver state, look for an explicit exclusion protocol: task-side mutation masks the exact interrupt source before taking the lock, while IRQ only touches pre-registered stable state. Document the lifetime/safety contract; otherwise prefer atomics/pending bits plus a deferred worker. +11. Validate the changed crate with formatting and targeted clippy before finishing. ## Dependency Rules @@ -70,6 +71,8 @@ IRQ handlers should identify/clear the interrupt source and extract an `Event`. When a driver intentionally shares registries or queue maps between task setup and IRQ completion paths, prefer an xHCI-style exclusion protocol over taking the same spinlock in IRQ: task context masks the same device interrupter/MSI source before mutation; IRQ context does not take that lock and only touches entries whose lifetime was established before interrupts were enabled. This avoids same-lock IRQ reentry deadlocks, but it does not make allocation, blocking, arbitrary wakers, or unrelated OS callbacks safe in hard IRQ. +For split queue designs, do not make an IRQ handler lock a queue mutex that task context can hold. If IRQ and queues share one hardware register block, put exclusive register access behind one short, non-blocking core/gate, let the IRQ endpoint be the sole reader/clearer of shared or destructive IRQ status, and fan out results into independent per-queue completion state. Queue `poll` should normally mean "consume synchronized completion state", not "peek the global IRQ/status register again". + ## Validation Run: diff --git a/.claude/skills/cross-kernel-driver/references/architecture.md b/.claude/skills/cross-kernel-driver/references/architecture.md index 47d707e846..12a223b6e4 100644 --- a/.claude/skills/cross-kernel-driver/references/architecture.md +++ b/.claude/skills/cross-kernel-driver/references/architecture.md @@ -178,6 +178,16 @@ wakers, heap allocation, sleeping locks, or unrelated subsystem locks safe in a hard IRQ. If the driver cannot prove this protocol, use atomics/pending bits and an OS Glue deferred worker instead. +### IRQ/Queue Isolation Pattern + +For devices with split runtime endpoints, treat the IRQ handle as a state synchronizer, not as a queue owner: + +- Give the IRQ handle its own endpoint object, separate from TX/RX queues, completion queues, block queues, network rings, or accelerator engines. +- Let the IRQ handle be the only runtime path that reads and clears shared or destructive interrupt/status registers. Queue-side code should not rediscover readiness by peeking the same global register, because that can clear or consume another queue's event. +- Fan out IRQ results into queue-local completion state, for example per-queue atomics, bitmaps, counters, or pending lists. The state should name the affected queue or engine and preserve errors separately from readiness. +- If an IRQ arrives while another context owns the raw register block, record a pending IRQ bit and return quickly. Drain it from a safe context or the next IRQ pass instead of spinning in interrupt context. +- Keep raw driver event snapshots close to hardware semantics. Put OS wakeups, task scheduling, and per-queue completion ownership in the adapter/runtime layer above the raw register code. + ## Queue/Runtime Pattern Model queues as independent running units. This matches network TX/RX queues, NVMe admin/IO queues, block request queues, and many accelerator command queues. @@ -199,6 +209,14 @@ Runtime wrappers can then choose: Avoid a single global `Driver::poll` if the hardware naturally exposes multiple queues or engines. Avoid a "big object + big lock + callbacks" shape unless the device is truly that simple. +In an IRQ-driven split design, queue operations consume synchronized queue-local state: + +- Queue `poll` should answer whether that queue has a synchronized completion, budget, error, or readiness state. It should not normally read or clear global hardware IRQ status. +- Queue `submit`/`try_write` should consume that queue's own permits or descriptor budget and then program only the register path needed to advance that queue. +- Queue `reclaim`/`try_read` should consume that queue's own completion or error state. Do not let one queue consume another queue's event because a shared register reported combined status. +- If a hardware status register reports multiple queues or directions in one destructive read, split that status immediately in the IRQ/event layer and store independent queue-local state before any queue code runs. +- For FIFO-style devices where one readiness interrupt may cover a bounded burst, model the budget explicitly if more than one operation can be performed. Avoid hidden loops that re-read global status from a queue path. + For a block queue adapter, align portable queue state with `rdif_block::IQueue`: - `buffer_config()` should expose block-size, alignment, and DMA mask constraints. @@ -210,11 +228,14 @@ For a block queue adapter, align portable queue state with `rdif_block::IQueue`: - Prefer `&mut self` for externally visible operations that require exclusive access. - Do not make OS locks part of the portable Driver Trait. -- Use internal locks only for short critical sections such as pending flags or small status updates. +- Do not take a blocking mutex from an IRQ handler when task context can hold the same mutex. Use a non-blocking borrow gate, try-lock with explicit pending state, or a small atomic/interrupt-safe state handoff. +- Use internal locks only for short non-IRQ critical sections such as pending flags or small status updates. In IRQ context, prefer atomics, per-queue pending bits, or an explicit deferred drain path. - If task and IRQ contexts share a lock-protected registry, require the IRQ/task exclusion protocol above: mask the same interrupt source before task-side mutation, keep IRQ lock-free for that registry, and document why the fast path cannot race lifetime or structure changes. +- When one raw register block is shared by several queues or endpoints, centralize mutable register access in one core object. Wrap it in `UnsafeCell` or another narrow unsafe primitive only in the adapter/runtime layer, document the exclusion rule, and avoid exposing unsynchronized raw access to queues. +- Separate synchronization ownership from hardware logic. The raw driver should expose register-level primitives and stable event snapshots; the runtime/adapter should decide how IRQ, queues, pending state, and wakeups are synchronized. - Keep `unsafe` in callback bridges, MMIO construction, and DMA glue boundaries where possible. - Do slow work in task/worker/executor/polling context, not in IRQ context. diff --git a/Cargo.lock b/Cargo.lock index 78f80d2353..0473f70dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,6 +709,7 @@ dependencies = [ "rdif-input", "rdif-intc", "rdif-pcie", + "rdif-serial", "rdif-vsock", "rdrive", "rdrive-macros", @@ -6447,7 +6448,6 @@ dependencies = [ "futures", "heapless 0.9.3", "rdif-base", - "spin 0.12.0", "thiserror 2.0.18", ] diff --git a/components/irq-framework/src/types.rs b/components/irq-framework/src/types.rs index 8458b8c88d..573e960626 100644 --- a/components/irq-framework/src/types.rs +++ b/components/irq-framework/src/types.rs @@ -275,6 +275,11 @@ impl IrqRequest { self.auto_enable = auto_enable; self } + + /// Returns whether the action should be enabled after request. + pub const fn auto_enable_mode(&self) -> AutoEnable { + self.auto_enable + } } /// Token returned from request and used for later lifecycle operations. diff --git a/components/irq-framework/tests/std_sim.rs b/components/irq-framework/tests/std_sim.rs index da47e57db4..df47492a70 100644 --- a/components/irq-framework/tests/std_sim.rs +++ b/components/irq-framework/tests/std_sim.rs @@ -323,6 +323,23 @@ fn request_auto_enable_no_restores_line_but_keeps_action_disabled() { assert_eq!(registry.dispatch(IrqNumber(32), CpuId(0)).called, 0); } +#[test] +fn irq_request_exposes_auto_enable_mode() { + let counter = AtomicUsize::new(0); + let data = NonNull::from(&counter).cast(); + + assert_eq!( + IrqRequest::new(count_handler, data).auto_enable_mode(), + AutoEnable::Yes + ); + assert_eq!( + IrqRequest::new(count_handler, data) + .auto_enable(AutoEnable::No) + .auto_enable_mode(), + AutoEnable::No + ); +} + #[test] fn shared_request_temporarily_disables_existing_line_and_restores_it() { let ops = MockOps::with_cpus(1); diff --git a/components/someboot/src/acpi/earlycon.rs b/components/someboot/src/acpi/earlycon.rs index 4232332bba..fffa22af2d 100644 --- a/components/someboot/src/acpi/earlycon.rs +++ b/components/someboot/src/acpi/earlycon.rs @@ -1,9 +1,12 @@ -use core::{cell::UnsafeCell, ptr::NonNull}; +use core::ptr::NonNull; use acpi::{AcpiError, Handler, PhysicalMapping, address::AddressSpace, sdt::spcr::Spcr}; -use some_serial::{ns16550::Ns16550, *}; +use some_serial::ns16550::Ns16550; -use crate::{console::Con, mem::_fixmap_io}; +use crate::{ + console::{EarlySerial, EarlySerialRaw}, + mem::_fixmap_io, +}; pub(crate) fn acpi_setup_earlycon() -> Result<(), AcpiError> { let tb = crate::acpi::tables()?; @@ -45,8 +48,9 @@ fn deal_with_spsr(spsr: &PhysicalMapping) -> Option<()> { { let mut uart = Ns16550::new_port(base_address.address as u16, clock); uart.open(); - let tx = uart.take_tx().unwrap(); - set_sender(tx); + crate::console::set_earlycon_serial(EarlySerial::new( + EarlySerialRaw::Ns16550Port(uart), + )); (None, false) } #[cfg(not(target_arch = "x86_64"))] @@ -63,8 +67,9 @@ fn deal_with_spsr(spsr: &PhysicalMapping) -> Option<()> { base_address.access_size as _, ); uart.open(); - let tx = uart.take_tx().unwrap(); - set_sender(tx); + crate::console::set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Ns16550Mmio( + uart, + ))); (Some(mapped), true) } space => { @@ -78,7 +83,6 @@ fn deal_with_spsr(spsr: &PhysicalMapping) -> Option<()> { } }; - unsafe { crate::console::set_out(&SENDER) }; unsafe { crate::console::DEBUG_BASE = base_address.address as usize; crate::console::DEBUG_IS_MMIO = is_mmio; @@ -95,27 +99,3 @@ fn deal_with_spsr(spsr: &PhysicalMapping) -> Option<()> { Some(()) } - -fn set_sender(sender: some_serial::Sender) { - unsafe { - *SENDER.0.get() = Some(sender); - } -} - -static SENDER: SenderCell = SenderCell(UnsafeCell::new(None)); - -struct SenderCell(UnsafeCell>); - -unsafe impl Sync for SenderCell {} - -impl Con for SenderCell { - fn write_bytes(&self, bytes: &[u8]) -> usize { - unsafe { - if let Some(ref mut sender) = *self.0.get() { - sender.write_bytes(bytes) - } else { - bytes.len() - } - } - } -} diff --git a/components/someboot/src/arch/aarch64/paging/mod.rs b/components/someboot/src/arch/aarch64/paging/mod.rs index daf1e919f3..713e252e07 100644 --- a/components/someboot/src/arch/aarch64/paging/mod.rs +++ b/components/someboot/src/arch/aarch64/paging/mod.rs @@ -32,7 +32,8 @@ pub fn enable_mmu() -> ! { // Do not touch the debug UART in this final pre-relocation window. Some // boards can leave the early UART TX FIFO full here, and any console access - // after SCTLR.M is set can also observe software MMU state too early. + // after SCTLR.M is set can observe hardware MMU state before the kernel has + // actually jumped to the relocated virtual entry. setup_sctlr(); super::relocate::reset(); diff --git a/components/someboot/src/arch/riscv64/mod.rs b/components/someboot/src/arch/riscv64/mod.rs index c6c6938135..d73351c692 100644 --- a/components/someboot/src/arch/riscv64/mod.rs +++ b/components/someboot/src/arch/riscv64/mod.rs @@ -247,7 +247,7 @@ impl ArchTrait for Arch { } fn is_mmu_enabled() -> bool { - satp_mode() != 0 + current_satp_mode() != 0 } fn kernel_page_table() -> PageTableInfo { @@ -445,8 +445,8 @@ pub(crate) fn disable_local_irqs() { } pub(crate) fn current_page_table() -> PageTableInfo { - let satp = read_satp(); - let mode = satp_mode_from(satp); + let satp = current_satp(); + let mode = satp >> 60; let addr = if mode == 0 { KERNEL_PAGE_TABLE_ADDR.load(Ordering::Relaxed) } else { @@ -455,7 +455,11 @@ pub(crate) fn current_page_table() -> PageTableInfo { PageTableInfo { asid: 0, addr } } -fn read_satp() -> usize { +fn current_satp_mode() -> usize { + current_satp() >> 60 +} + +fn current_satp() -> usize { let satp: usize; unsafe { core::arch::asm!("csrr {satp}, satp", satp = out(reg) satp, options(nostack, preserves_flags)); @@ -463,14 +467,6 @@ fn read_satp() -> usize { satp } -fn satp_mode() -> usize { - satp_mode_from(read_satp()) -} - -fn satp_mode_from(satp: usize) -> usize { - satp >> 60 -} - pub(crate) fn write_satp(root_paddr: usize) { let satp = SATP_MODE_SV39 | (root_paddr >> 12); unsafe { diff --git a/components/someboot/src/arch/x86_64/console.rs b/components/someboot/src/arch/x86_64/console.rs index 536dffa1f8..ac514f284b 100644 --- a/components/someboot/src/arch/x86_64/console.rs +++ b/components/someboot/src/arch/x86_64/console.rs @@ -28,20 +28,15 @@ const LSR_RX_ERROR_MASK: u8 = 0x1e; impl crate::console::ArchConsoleOps for Console { fn init() -> bool { - use some_serial::InterfaceRaw; - + // someboot runs before the normal allocator is available, so the early + // serial path must stay on the raw register-level driver. Do not use + // the rdif/ax-driver runtime wrapper here: it allocates `Box`/`Arc` + // state around the raw UART for the OS driver model. let mut uart = some_serial::ns16550::Ns16550::new_port(COM1_PORT, COM1_CLOCK_HZ); uart.open(); - - let Some(tx) = uart.take_tx() else { - return false; - }; - let Some(rx) = uart.take_rx() else { - return false; - }; - - crate::console::set_earlycon_sender(tx); - crate::console::set_earlycon_receiver(rx); + crate::console::set_earlycon_serial(crate::console::EarlySerial::new( + crate::console::EarlySerialRaw::Ns16550Port(uart), + )); true } diff --git a/components/someboot/src/console/mod.rs b/components/someboot/src/console/mod.rs index 2d37fb1167..072ad0e540 100644 --- a/components/someboot/src/console/mod.rs +++ b/components/someboot/src/console/mod.rs @@ -1,8 +1,19 @@ -use core::{cell::UnsafeCell, fmt::Write, ptr::NonNull}; +use core::{ + cell::UnsafeCell, + fmt::Write, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, +}; use byte_unit::{Byte, UnitType}; use kernutil::memory::{MemoryDescriptor, MemoryType}; -use some_serial::*; +#[cfg(target_arch = "x86_64")] +use some_serial::ns16550::Port; +use some_serial::{ + RawUart, SerialEvent, TransferError, + ns16550::{self, Mmio, Ns16550}, + pl011, +}; use crate::{ cmdline::EarlyconConfig, @@ -169,36 +180,110 @@ pub(crate) unsafe fn set_out(v: &'static dyn Con) { } } -pub fn set_earlycon_sender(sender: Sender) { - unsafe { - *EARLYCON_SENDER.0.get() = Some(sender); - set_out(&EARLYCON_SENDER); - } +pub struct EarlySerial { + raw: EarlySerialRaw, + tx_state: SerialEvent, + rx_state: SerialEvent, } -pub fn set_earlycon_receiver(receiver: Receiver) { - unsafe { - *EARLYCON_RECEIVER.0.get() = Some(receiver); - } +pub enum EarlySerialRaw { + Ns16550Mmio(Ns16550), + #[cfg(target_arch = "x86_64")] + Ns16550Port(Ns16550), + Pl011(pl011::Pl011), } -pub fn read_byte() -> Option { - if let Some(byte) = ::Console::read_byte() { - return Some(byte); +impl EarlySerial { + pub fn new(raw: EarlySerialRaw) -> Self { + Self { + raw, + tx_state: SerialEvent::empty(), + rx_state: SerialEvent::empty(), + } } - unsafe { - if let Some(ref mut receiver) = *EARLYCON_RECEIVER.0.get() { - match receiver.read_byte() { - Some(Ok(byte)) => Some(byte), - _ => None, + pub fn try_write(&mut self, bytes: &[u8]) -> usize { + let mut written = 0; + while written < bytes.len() { + self.refresh_status(); + if !self.tx_state.tx_ready() { + break; + } + self.with_raw(|serial| serial.write_byte(bytes[written])); + self.tx_state + .remove(SerialEvent::TX_READY | SerialEvent::TX_ERROR); + written += 1; + } + written + } + + pub fn try_read(&mut self, bytes: &mut [u8]) -> Result { + let mut read = 0; + let mut first_error = None; + for byte in bytes.iter_mut() { + self.refresh_status(); + if !self.rx_state.rx_ready() && !self.rx_state.rx_error() { + break; } + let status = self.rx_state; + self.rx_state + .remove(SerialEvent::RX_READY | SerialEvent::RX_ERROR | SerialEvent::OVERRUN); + match self.with_raw(|serial| serial.read_byte(status)) { + Some(Ok(b)) => { + *byte = b; + read += 1; + } + Some(Err(TransferError::Overrun(b))) => { + *byte = b; + read += 1; + first_error.get_or_insert(TransferError::Overrun(b)); + } + Some(Err(err)) => { + first_error.get_or_insert(err); + } + None => break, + } + } + if let Some(kind) = first_error { + Err(some_serial::TransBytesError { + bytes_transferred: read, + kind, + }) } else { - None + Ok(read) + } + } + + fn refresh_status(&mut self) { + let event = self.with_raw(|serial| serial.poll_status()); + self.tx_state |= event & (SerialEvent::TX_READY | SerialEvent::TX_ERROR); + self.rx_state |= + event & (SerialEvent::RX_READY | SerialEvent::RX_ERROR | SerialEvent::OVERRUN); + } + + fn with_raw(&mut self, f: impl FnOnce(&mut dyn RawUart) -> R) -> R { + match &mut self.raw { + EarlySerialRaw::Ns16550Mmio(serial) => f(serial), + #[cfg(target_arch = "x86_64")] + EarlySerialRaw::Ns16550Port(serial) => f(serial), + EarlySerialRaw::Pl011(serial) => f(serial), } } } +pub fn set_earlycon_serial(serial: EarlySerial) { + EARLYCON.set_serial(serial); + unsafe { set_out(&EARLYCON) }; +} + +pub fn read_byte() -> Option { + if let Some(byte) = ::Console::read_byte() { + return Some(byte); + } + + EARLYCON.read_byte() +} + pub fn irq_num() -> Option { ::Console::irq_num() } @@ -211,51 +296,109 @@ pub fn handle_irq() -> u32 { ::Console::handle_irq() } -static EARLYCON_SENDER: EarlyconSenderCell = EarlyconSenderCell(UnsafeCell::new(None)); +static EARLYCON: EarlyconCell = EarlyconCell(EarlyconMutex::new(None)); -struct EarlyconSenderCell(UnsafeCell>); +struct EarlyconMutex { + locked: AtomicBool, + inner: UnsafeCell, +} -unsafe impl Sync for EarlyconSenderCell {} +unsafe impl Sync for EarlyconMutex {} -impl Con for EarlyconSenderCell { - fn write_bytes(&self, bytes: &[u8]) -> usize { - const MAX_NO_PROGRESS_SPINS: usize = 1 << 20; +impl EarlyconMutex { + const fn new(value: T) -> Self { + Self { + locked: AtomicBool::new(false), + inner: UnsafeCell::new(value), + } + } - unsafe { - if let Some(ref mut sender) = *self.0.get() { - let mut written = 0; - let mut no_progress_spins = 0; - while written < bytes.len() { - let n = sender.write_bytes(&bytes[written..]); - if n == 0 { - no_progress_spins += 1; - if no_progress_spins >= MAX_NO_PROGRESS_SPINS { - // Early console output is best-effort. If the UART - // stops accepting bytes, report the rest as - // consumed so boot does not hang inside logging. - return bytes.len(); - } - core::hint::spin_loop(); - continue; - } - no_progress_spins = 0; - written += n; - } - written - } else { - // No sender available, simply return the length of bytes to indicate all bytes "written" - bytes.len() + fn with_lock(&self, f: impl FnOnce(&mut T) -> R) -> R { + // Do not replace this with spin::Mutex or the rdif runtime wrapper. + // someboot runs before the normal allocator is available, so early + // serial cannot allocate Box/Arc-backed runtime state and must keep a + // raw register-level enum here. On AArch64, exclusive atomic + // instructions such as LDXR/LDAXR are not reliable before the MMU is + // enabled, so the early console must also avoid touching the atomic + // lock word on that path. Before MMU setup, someboot is still in the + // single-core early-output phase and can access the serial object + // directly; after MMU setup, the custom atomic lock below provides real + // exclusion for later console users. + if !crate::mem::mmu::is_mmu_enabled() { + return unsafe { f(&mut *self.inner.get()) }; + } + + let irq_enabled = crate::irq::irq_local_is_enabled(); + crate::irq::irq_local_set_enable(false); + while self + .locked + .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + while self.locked.load(Ordering::Acquire) { + core::hint::spin_loop(); } } + let ret = unsafe { f(&mut *self.inner.get()) }; + self.locked.store(false, Ordering::Release); + crate::irq::irq_local_set_enable(irq_enabled); + ret } } -static EARLYCON_RECEIVER: EarlyconReceiverCell = EarlyconReceiverCell(UnsafeCell::new(None)); +struct EarlyconCell(EarlyconMutex>); -#[allow(dead_code)] -struct EarlyconReceiverCell(UnsafeCell>); +impl EarlyconCell { + fn set_serial(&self, serial: EarlySerial) { + self.0.with_lock(|earlycon| *earlycon = Some(serial)); + } -unsafe impl Sync for EarlyconReceiverCell {} + fn read_byte(&self) -> Option { + self.0.with_lock(|earlycon| { + let serial = earlycon.as_mut()?; + + let mut byte = [0]; + match serial.try_read(&mut byte) { + Ok(1) => Some(byte[0]), + Err(err) if err.bytes_transferred == 1 => Some(byte[0]), + _ => None, + } + }) + } + + fn try_write(&self, bytes: &[u8]) -> Option { + self.0 + .with_lock(|earlycon| earlycon.as_mut().map(|serial| serial.try_write(bytes))) + } +} + +impl Con for EarlyconCell { + fn write_bytes(&self, bytes: &[u8]) -> usize { + const MAX_NO_PROGRESS_SPINS: usize = 1 << 20; + + let mut written = 0; + let mut no_progress_spins = 0; + while written < bytes.len() { + let Some(n) = self.try_write(&bytes[written..]) else { + return bytes.len(); + }; + if n == 0 { + no_progress_spins += 1; + if no_progress_spins >= MAX_NO_PROGRESS_SPINS { + // Early console output is best-effort. If the UART stops + // accepting bytes, report the rest as consumed so boot does + // not hang inside logging. + return bytes.len(); + } + core::hint::spin_loop(); + continue; + } + no_progress_spins = 0; + written += n; + } + written + } +} pub fn set_earlycon_by_cmdline() -> Result<(), &'static str> { let config = crate::cmdline::earlycon().ok_or("No earlycon parameter found")?; @@ -266,10 +409,8 @@ pub fn set_earlycon_by_cmdline() -> Result<(), &'static str> { { let base = config.base_addr.ok_or("missing io base address")? as u16; let mut uart = some_serial::ns16550::Ns16550::new_port(base, 1_843_200); - let tx = uart.take_tx().ok_or("failed to take io sender")?; - let rx = uart.take_rx().ok_or("failed to take io receiver")?; - set_earlycon_sender(tx); - set_earlycon_receiver(rx); + uart.open(); + set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Ns16550Port(uart))); false } #[cfg(not(target_arch = "x86_64"))] @@ -305,11 +446,8 @@ fn set_pl011(config: &EarlyconConfig) -> Result<(), &'static str> { NonNull::new(_fixmap_io(base_addr)).ok_or("Invalid base address for pl011 earlycon")?; let mut serial = pl011::Pl011::new(base_addr, 0); - let tx = serial.take_tx().ok_or("no tx")?; - let rx = serial.take_rx().ok_or("no rx")?; - - set_earlycon_sender(tx); - set_earlycon_receiver(rx); + serial.open(); + set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Pl011(serial))); Ok(()) } @@ -328,11 +466,8 @@ fn set_16550_mmio(config: &EarlyconConfig) -> Result<(), &'static str> { }; let mut serial = ns16550::Ns16550::new_mmio(base_addr, 0, width); - let tx = serial.take_tx().ok_or("no tx")?; - let rx = serial.take_rx().ok_or("no rx")?; - - set_earlycon_sender(tx); - set_earlycon_receiver(rx); + serial.open(); + set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Ns16550Mmio(serial))); Ok(()) } diff --git a/components/someboot/src/fdt/earlycon.rs b/components/someboot/src/fdt/earlycon.rs index 4161086d53..04bfb895e8 100644 --- a/components/someboot/src/fdt/earlycon.rs +++ b/components/someboot/src/fdt/earlycon.rs @@ -1,9 +1,9 @@ use core::ptr::NonNull; -use some_serial::*; +use some_serial::{ns16550, pl011}; use crate::{ - console::{DEBUG_BASE, DEBUG_IS_MMIO}, + console::{DEBUG_BASE, DEBUG_IS_MMIO, EarlySerial, EarlySerialRaw}, mem::_fixmap_io, }; @@ -51,22 +51,18 @@ fn set_by_stdout() -> Option<()> { "arm,pl011" | "arm,primecell" => { let mut serial = pl011::Pl011::new(addr, clock); serial.open(); - let tx = serial.take_tx()?; - let rx = serial.take_rx()?; - - crate::console::set_earlycon_sender(tx); - crate::console::set_earlycon_receiver(rx); + crate::console::set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Pl011( + serial, + ))); installed = true; break; } "snps,dw-apb-uart" | "ns16550a" | "ns16550" => { let mut serial = ns16550::Ns16550::new_mmio(addr, clock, reg_width); serial.open(); - let tx = serial.take_tx()?; - let rx = serial.take_rx()?; - - crate::console::set_earlycon_sender(tx); - crate::console::set_earlycon_receiver(rx); + crate::console::set_earlycon_serial(EarlySerial::new(EarlySerialRaw::Ns16550Mmio( + serial, + ))); installed = true; break; } diff --git a/components/someboot/src/lib.rs b/components/someboot/src/lib.rs index 60b9c2298f..3f6a47ce0e 100644 --- a/components/someboot/src/lib.rs +++ b/components/someboot/src/lib.rs @@ -48,6 +48,7 @@ pub mod smp; pub mod timer; pub use acpi::rsdp_addr_phys; +pub use cmdline::cmdline; pub use fdt::{fdt_addr, fdt_addr_phys}; pub use page_table_generic::*; pub use somehal_macros::{entry, someboot_secondary_entry as secondary_entry}; diff --git a/components/someboot/src/mem/mmu.rs b/components/someboot/src/mem/mmu.rs index d52f6b161c..bf2bd00de0 100644 --- a/components/someboot/src/mem/mmu.rs +++ b/components/someboot/src/mem/mmu.rs @@ -2,7 +2,7 @@ use kernutil::StaticCell; use page_table_generic::PageTable; pub use page_table_generic::{PagingError, PagingResult}; -use crate::mem::ram::Ram; +use crate::{ArchTrait, mem::ram::Ram}; pub type ArchPageTable = PageTable<::P, A>; diff --git a/components/starry-process/src/process.rs b/components/starry-process/src/process.rs index 4e9dae21ae..d0e49384e3 100644 --- a/components/starry-process/src/process.rs +++ b/components/starry-process/src/process.rs @@ -203,6 +203,21 @@ impl Process { self.tg.lock().group_exited } + /// Starts a process-wide exit if one is not already in progress. + /// + /// Returns a snapshot of the thread group at the point where the group-exit + /// state was first published. Later exiting threads must not overwrite the + /// recorded process exit code. + pub fn start_group_exit(&self, exit_code: i32) -> Option> { + let mut tg = self.tg.lock(); + if tg.group_exited { + return None; + } + tg.group_exited = true; + tg.exit_code = exit_code; + Some(tg.threads.iter().cloned().collect()) + } + /// Marks the [`Process`] as group exited. pub fn group_exit(&self) { self.tg.lock().group_exited = true; diff --git a/components/starry-process/tests/process.rs b/components/starry-process/tests/process.rs index c31dcadae8..a932b0b26e 100644 --- a/components/starry-process/tests/process.rs +++ b/components/starry-process/tests/process.rs @@ -128,10 +128,14 @@ fn thread_exit() { assert!(!last); assert_eq!(child.exit_code(), 7); - child.group_exit(); + let mut snapshot = child.start_group_exit(9).unwrap(); + snapshot.sort(); + assert_eq!(snapshot, vec![102]); assert!(child.is_group_exited()); let last2 = child.exit_thread(102, 3); assert!(last2); - assert_eq!(child.exit_code(), 7); + assert_eq!(child.exit_code(), 9); + assert!(child.start_group_exit(11).is_none()); + assert_eq!(child.exit_code(), 9); } diff --git a/drivers/ax-driver/Cargo.toml b/drivers/ax-driver/Cargo.toml index 7decf7474a..8c2d02b669 100644 --- a/drivers/ax-driver/Cargo.toml +++ b/drivers/ax-driver/Cargo.toml @@ -54,7 +54,7 @@ usb = ["dep:crab-usb"] rockchip-dwc-xhci = ["usb", "plat-dyn", "rockchip-soc", "rockchip-pm"] xhci-mmio = ["usb", "plat-dyn"] xhci-pci = ["usb", "pci"] -serial = ["plat-dyn", "dep:some-serial"] +serial = ["plat-dyn", "dep:ax-errno", "dep:ax-kspin", "dep:rdif-serial", "dep:some-serial"] sg2002-placeholder = ["plat-dyn"] rknpu = ["plat-dyn", "rockchip-pm", "rockchip-soc", "dep:rockchip-npu"] rga = ["plat-dyn", "dep:rockchip-rga"] @@ -135,6 +135,7 @@ rdif-block = { workspace = true, optional = true } rdif-clk = { workspace = true, optional = true } rdif-display = { workspace = true, optional = true } rdif-input = { workspace = true, optional = true } +rdif-serial = { workspace = true, optional = true } rdif-intc.workspace = true rdif-pcie = { workspace = true, optional = true } rdif-vsock = { workspace = true, optional = true } diff --git a/drivers/ax-driver/src/binding_info.rs b/drivers/ax-driver/src/binding_info.rs index 6a227cc036..779e14dcff 100644 --- a/drivers/ax-driver/src/binding_info.rs +++ b/drivers/ax-driver/src/binding_info.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct BindingInfo { irq: Option, } diff --git a/drivers/ax-driver/src/serial/mod.rs b/drivers/ax-driver/src/serial/mod.rs index f93d7eb9ba..87d447234e 100644 --- a/drivers/ax-driver/src/serial/mod.rs +++ b/drivers/ax-driver/src/serial/mod.rs @@ -1,193 +1,262 @@ -use alloc::{format, string::String}; - -use fdt_edit::{Fdt, NodeType, RegFixed, Status}; -use log::{info, warn}; -use rdrive::{probe::OnProbeError, register::ProbeFdt}; -use some_serial::{ - BSerial, ns16550, - ns16550::rockchip_fiq::{ROCKCHIP_FIQ_RK3588_UART_CLOCK, RockchipFiqConfig, RockchipFiqSerial}, - pl011, -}; - -crate::model_register!( - name: "common serial", - level: ProbeLevel::PreKernel, - priority: ProbePriority::DEFAULT, - probe_kinds: &[ProbeKind::Fdt { - compatibles: &["arm,pl011", "snps,dw-apb-uart", "ns16550a", "ns16550"], - on_probe: probe - }], -); - -crate::model_register!( - name: "rockchip fiq debugger serial", - level: ProbeLevel::PreKernel, - priority: ProbePriority::DEFAULT, - probe_kinds: &[ProbeKind::Fdt { - compatibles: &["rockchip,fiq-debugger"], - on_probe: probe_rockchip_fiq - }], -); - -fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { - let (info, plat_dev) = probe.into_parts(); - info!("Probing serial device: {}", info.node.name()); - let base_reg = - info.node.regs().into_iter().next().ok_or_else(|| { - OnProbeError::other(alloc::format!("[{}] has no reg", info.node.name())) - })?; - - let mmio_size = base_reg.size.unwrap_or(0x1000); - let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size as usize)?; - - let node = info.node.as_node(); - let clock_freq = prop_u32(node, "clock-frequency").unwrap_or(24_000_000); - let reg_width = prop_u32(node, "reg-io-width").unwrap_or(1) as usize; - let mut serial: Option = None; - for compatible in node.compatibles() { - if compatible == "arm,pl011" { - serial = Some(pl011::Pl011::new_boxed(mmio_base, clock_freq)); - break; - } +use alloc::{string::String, vec::Vec}; + +use ax_errno::AxError; +use fdt_edit::{Fdt, RegFixed}; +use log::warn; +pub use rdif_serial::{Config, ConfigError, RxFlag, RxItem, SerialCounters, SerialIrqOutcome}; +use rdrive::{Device, DeviceId, DriverGeneric, probe::acpi::AcpiInfo, register::FdtInfo}; + +mod ns16550; +mod pl011; +mod rockchip_fiq; +mod runtime; + +pub use runtime::{BInterruptSerial, InterruptSerial, KernelSerialPort}; + +use crate::{BindingInfo, binding_info_from_acpi, binding_info_from_fdt}; + +struct PlatformSerialDevice { + name: String, + info: SerialDeviceInfo, + interface: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SerialDeviceInfo { + pub fdt_path: String, + pub alias_index: Option, + pub paddr: usize, + pub mapped_base: usize, + pub baudrate: u32, + pub irq_num: Option, + pub binding_info: BindingInfo, +} + +pub struct SerialDevice { + name: String, + rdrive_device_id: DeviceId, + info: SerialDeviceInfo, + interface: BInterruptSerial, +} + +pub struct SerialRuntimePort { + name: String, + rdrive_device_id: DeviceId, + info: SerialDeviceInfo, + irq_num: usize, + port: BInterruptSerial, +} - if matches!(compatible, "snps,dw-apb-uart" | "ns16550a" | "ns16550") { - serial = Some(ns16550::Ns16550::new_mmio_boxed( - mmio_base, clock_freq, reg_width, - )); - break; +impl PlatformSerialDevice { + fn new(name: String, info: SerialDeviceInfo, interface: BInterruptSerial) -> Self { + Self { + name, + info, + interface: Some(interface), } } +} - if let Some(serial) = serial { - let base = serial.base_addr(); - info!("Serial@{base:#x} registered successfully"); - plat_dev.register(serial); +impl DriverGeneric for PlatformSerialDevice { + fn name(&self) -> &str { + &self.name } - - Ok(()) } -fn probe_rockchip_fiq(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { - let (info, plat_dev) = probe.into_parts(); - let live_fdt = - rdrive::with_fdt(Clone::clone).ok_or_else(|| OnProbeError::other("live FDT not found"))?; - let fdt_config = rockchip_fiq_fdt_config(&live_fdt, info.node)?; - let mmio_base = crate::mmio::iomap( - fdt_config.reg.address as usize, - fdt_config.reg.size.unwrap_or(0x100) as usize, - )?; - - if fdt_config.target_disabled { - info!( - "Rockchip FIQ debugger takes disabled UART alias serial{} at {}", - fdt_config.config.serial_id, fdt_config.uart_path - ); +impl SerialDevice { + pub fn name(&self) -> &str { + &self.name } - let serial = RockchipFiqSerial::new_boxed(mmio_base, fdt_config.config); - let base = serial.base_addr(); - info!( - "Rockchip FIQ debugger UART@{base:#x} registered successfully, serial-id={}, baudrate={}, \ - irq-mode={}", - fdt_config.config.serial_id, fdt_config.config.baudrate, fdt_config.config.irq_mode_enabled - ); - plat_dev.register(serial); - Ok(()) -} + pub fn info(&self) -> &SerialDeviceInfo { + &self.info + } -fn prop_u32(node: &fdt_edit::Node, name: &str) -> Option { - node.get_property(name).and_then(|prop| prop.get_u32()) -} + pub fn rdrive_device_id(&self) -> DeviceId { + self.rdrive_device_id + } + + pub fn fdt_path(&self) -> &str { + &self.info.fdt_path + } -struct RockchipFiqFdtConfig { - config: RockchipFiqConfig, - reg: RegFixed, - uart_path: String, - target_disabled: bool, + pub fn alias_index(&self) -> Option { + self.info.alias_index + } + + pub fn paddr(&self) -> usize { + self.info.paddr + } + + pub fn mapped_base(&self) -> usize { + self.info.mapped_base + } + + pub fn baudrate(&self) -> u32 { + self.interface.baudrate() + } + + pub fn irq_num(&self) -> Option { + self.info.irq_num + } + + pub fn set_config(&self, config: &Config) -> Result<(), ConfigError> { + self.interface.set_config(config) + } + + pub fn set_baudrate(&self, baudrate: u32) -> Result<(), ConfigError> { + self.interface.set_config(&Config::new().baudrate(baudrate)) + } + + pub fn into_runtime_port(self) -> Result { + let irq_num = self.info.irq_num.ok_or(AxError::Unsupported)?; + Ok(SerialRuntimePort { + name: self.name, + rdrive_device_id: self.rdrive_device_id, + info: self.info, + irq_num, + port: self.interface, + }) + } } -fn rockchip_fiq_fdt_config( - fdt: &Fdt, - fiq: NodeType<'_>, -) -> Result { - let fiq_node = fiq.as_node(); - let serial_id = prop_u32(fiq_node, "rockchip,serial-id").ok_or_else(|| { - OnProbeError::other(format!("[{}] has no rockchip,serial-id", fiq.name())) - })?; - - if serial_id == u32::MAX { - return Err(OnProbeError::NotMatch); +impl SerialRuntimePort { + pub fn name(&self) -> &str { + &self.name + } + + pub fn info(&self) -> &SerialDeviceInfo { + &self.info + } + + pub fn rdrive_device_id(&self) -> DeviceId { + self.rdrive_device_id + } + + pub fn fdt_path(&self) -> &str { + &self.info.fdt_path + } + + pub fn alias_index(&self) -> Option { + self.info.alias_index + } + + pub fn irq_num(&self) -> Option { + Some(self.irq_num) + } + + pub fn port(&self) -> BInterruptSerial { + self.port.clone() } +} - let alias = format!("serial{serial_id}"); - let uart_path = fdt - .resolve_alias(&alias) - .map(String::from) - .ok_or_else(|| OnProbeError::other(format!("{alias} alias not found")))?; - let uart_node = fdt - .get_by_path(&uart_path) - .ok_or_else(|| OnProbeError::other(format!("{uart_path} node not found")))?; - let uart = uart_node.as_node(); - - if !uart - .compatibles() - .any(|compatible| compatible == "snps,dw-apb-uart") - { - return Err(OnProbeError::other(format!( - "{uart_path} is not a snps,dw-apb-uart node" - ))); +impl TryFrom> for SerialDevice { + type Error = AxError; + + fn try_from(base: Device) -> Result { + let rdrive_device_id = base.descriptor().device_id(); + let mut dev = base.lock().map_err(|_| AxError::BadState)?; + let name = dev.name.clone(); + let info = dev.info.clone(); + let interface = dev.interface.take().ok_or(AxError::BadState)?; + Ok(Self { + name, + rdrive_device_id, + info, + interface, + }) } +} - let reg_width = prop_u32(uart, "reg-io-width").unwrap_or(4); - let reg_shift = prop_u32(uart, "reg-shift").unwrap_or(2); - if reg_width != 4 || reg_shift != 2 { - return Err(OnProbeError::other(format!( - "{uart_path} has unsupported reg-io-width/reg-shift {reg_width}/{reg_shift}" - ))); +pub fn take_serial_devices() -> Vec { + if !rdrive::is_initialized() { + warn!("rdrive is not initialized; no serial devices available"); + return Vec::new(); } - let reg = uart_node - .regs() + rdrive::get_list::() .into_iter() - .next() - .ok_or_else(|| OnProbeError::other(format!("[{uart_path}] has no reg")))?; - - let baudrate = normalise_fiq_baudrate( - prop_u32(fiq_node, "rockchip,baudrate") - .unwrap_or(some_serial::ns16550::rockchip_fiq::ROCKCHIP_FIQ_DEFAULT_BAUDRATE), - ); - let clock_hz = prop_u32(uart, "clock-frequency").unwrap_or(ROCKCHIP_FIQ_RK3588_UART_CLOCK); - let irq_mode_enabled = prop_u32(fiq_node, "rockchip,irq-mode-enable").unwrap_or(0) != 0; - let target_disabled = matches!(uart.status(), Some(Status::Disabled)); - - if matches!(uart.status(), Some(status) if status != Status::Disabled && status != Status::Okay) - { - warn!("{uart_path} has unrecognised status; proceeding for FIQ debugger"); + .filter_map(|dev| match SerialDevice::try_from(dev) { + Ok(serial) => Some(serial), + Err(err) => { + warn!("failed to take serial device: {err:?}"); + None + } + }) + .collect() +} + +fn serial_device_info( + info: &FdtInfo<'_>, + base_reg: &RegFixed, + mapped_base: usize, + baudrate: u32, +) -> SerialDeviceInfo { + let fdt_path = info.node.path(); + let alias_index = rdrive::with_fdt(|fdt| serial_alias_index(fdt, &fdt_path)).flatten(); + let binding_info = serial_binding_info(info, &fdt_path); + SerialDeviceInfo { + fdt_path, + alias_index, + paddr: base_reg.address as usize, + mapped_base, + baudrate, + irq_num: binding_info.irq_num(), + binding_info, + } +} + +fn acpi_serial_device_info( + info: &AcpiInfo<'_>, + paddr: usize, + mapped_base: usize, + baudrate: u32, +) -> SerialDeviceInfo { + let binding_info = acpi_serial_binding_info(info); + SerialDeviceInfo { + fdt_path: info.path.into(), + alias_index: None, + paddr, + mapped_base, + baudrate, + irq_num: binding_info.irq_num(), + binding_info, } +} - Ok(RockchipFiqFdtConfig { - config: RockchipFiqConfig { - serial_id, - baudrate, - clock_hz, - irq_mode_enabled, - debug_enable: true, - console_enable: true, - }, - reg, - uart_path, - target_disabled, +fn serial_binding_info(info: &FdtInfo<'_>, fdt_path: &str) -> BindingInfo { + binding_info_from_fdt(info).unwrap_or_else(|err| { + warn!("failed to resolve serial IRQ for {fdt_path}: {err:?}"); + BindingInfo::empty() }) } -fn normalise_fiq_baudrate(baudrate: u32) -> u32 { - match baudrate { - 115_200 | 1_500_000 => baudrate, - other => { - warn!("unsupported rockchip fiq baudrate {other}, falling back to 115200"); - 115_200 - } - } +fn acpi_serial_binding_info(info: &AcpiInfo<'_>) -> BindingInfo { + binding_info_from_acpi(info).unwrap_or_else(|err| { + warn!( + "failed to resolve ACPI serial IRQ for {}: {err:?}", + info.path + ); + BindingInfo::empty() + }) +} + +fn serial_alias_index(fdt: &Fdt, node_path: &str) -> Option { + let aliases = fdt.get_by_path("/aliases")?; + aliases + .as_node() + .properties() + .iter() + .filter_map(|prop| { + let index = prop.name().strip_prefix("serial")?.parse::().ok()?; + let path = prop.as_str()?; + (path == node_path).then_some(index) + }) + .next() +} + +fn prop_u32(node: &fdt_edit::Node, name: &str) -> Option { + node.get_property(name).and_then(|prop| prop.get_u32()) } #[cfg(test)] @@ -199,105 +268,38 @@ mod tests { use super::*; #[test] - fn resolves_fiq_debugger_target_uart_from_alias_even_when_uart_disabled() { - let fdt = minimal_fiq_fdt(true, true); - let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); - - let config = rockchip_fiq_fdt_config(&fdt, fiq).expect("parse fiq config"); - - assert_eq!(config.config.serial_id, 2); - assert_eq!(config.config.baudrate, 1_500_000); - assert_eq!(config.config.clock_hz, ROCKCHIP_FIQ_RK3588_UART_CLOCK); - assert!(config.config.irq_mode_enabled); - assert_eq!(config.uart_path, "/serial@feb50000"); - assert!(config.target_disabled); - assert_eq!(config.reg.address, 0xfeb5_0000); - assert_eq!(config.reg.size, Some(0x100)); + fn resolves_serial_alias_index_by_node_path() { + let fdt = minimal_serial_alias_fdt(); + + assert_eq!(serial_alias_index(&fdt, "/soc/uart@1000"), Some(0)); + assert_eq!(serial_alias_index(&fdt, "/soc/uart@2000"), Some(2)); + assert_eq!(serial_alias_index(&fdt, "/soc/uart@3000"), None); } - #[test] - fn rejects_missing_alias_or_non_dw_apb_target() { - let fdt = minimal_fiq_fdt(false, true); - let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); - assert!(rockchip_fiq_fdt_config(&fdt, fiq).is_err()); - - let fdt = minimal_fiq_fdt(true, false); - let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); - assert!(rockchip_fiq_fdt_config(&fdt, fiq).is_err()); + fn minimal_serial_alias_fdt() -> Fdt { + minimal_serial_alias_fdt_with_root_compatible(&[]) } - fn minimal_fiq_fdt(with_alias: bool, dw_apb: bool) -> Fdt { + fn minimal_serial_alias_fdt_with_root_compatible(compatibles: &[&str]) -> Fdt { let mut fdt = Fdt::new(); let root = fdt.root_id(); - fdt.node_mut(root) - .unwrap() - .set_property(prop_u32_ls("#address-cells", &[2])); - fdt.node_mut(root) - .unwrap() - .set_property(prop_u32_ls("#size-cells", &[1])); - - let aliases = fdt.add_node(root, Node::new("aliases")); - if with_alias { - fdt.node_mut(aliases) + if !compatibles.is_empty() { + fdt.node_mut(root) .unwrap() - .set_property(prop_str("serial2", "/serial@feb50000")); + .set_property(prop_strs("compatible", compatibles)); } - - let fiq = fdt.add_node(root, Node::new("fiq-debugger")); - fdt.node_mut(fiq) - .unwrap() - .set_property(prop_strs("compatible", &["rockchip,fiq-debugger"])); - fdt.node_mut(fiq) - .unwrap() - .set_property(prop_u32_ls("rockchip,serial-id", &[2])); - fdt.node_mut(fiq) - .unwrap() - .set_property(prop_u32_ls("rockchip,baudrate", &[1_500_000])); - fdt.node_mut(fiq) - .unwrap() - .set_property(prop_u32_ls("rockchip,irq-mode-enable", &[1])); - fdt.node_mut(fiq) - .unwrap() - .set_property(prop_str("status", "okay")); - - let uart = fdt.add_node(root, Node::new("serial@feb50000")); - fdt.node_mut(uart).unwrap().set_property(prop_strs( - "compatible", - if dw_apb { - &["rockchip,rk3588-uart", "snps,dw-apb-uart"] - } else { - &["rockchip,rk3588-uart"] - }, - )); - fdt.node_mut(uart) - .unwrap() - .set_property(prop_reg(0xfeb5_0000, 0x100)); - fdt.node_mut(uart) - .unwrap() - .set_property(prop_u32_ls("reg-io-width", &[4])); - fdt.node_mut(uart) + let aliases = fdt.add_node(root, Node::new("aliases")); + fdt.node_mut(aliases) .unwrap() - .set_property(prop_u32_ls("reg-shift", &[2])); - fdt.node_mut(uart) + .set_property(prop_str("serial0", "/soc/uart@1000")); + fdt.node_mut(aliases) .unwrap() - .set_property(prop_str("status", "disabled")); - fdt - } - - fn prop_u32_ls(name: &str, values: &[u32]) -> Property { - let mut data = Vec::new(); - for value in values { - data.extend_from_slice(&value.to_be_bytes()); - } - Property::new(name, data) - } + .set_property(prop_str("serial2", "/soc/uart@2000")); - fn prop_reg(address: u64, size: u32) -> Property { - let mut data = Vec::new(); - data.extend_from_slice(&((address >> 32) as u32).to_be_bytes()); - data.extend_from_slice(&(address as u32).to_be_bytes()); - data.extend_from_slice(&size.to_be_bytes()); - Property::new("reg", data) + let soc = fdt.add_node(root, Node::new("soc")); + fdt.add_node(soc, Node::new("uart@1000")); + fdt.add_node(soc, Node::new("uart@2000")); + fdt } fn prop_str(name: &str, value: &str) -> Property { diff --git a/drivers/ax-driver/src/serial/ns16550.rs b/drivers/ax-driver/src/serial/ns16550.rs new file mode 100644 index 0000000000..a4c23ad81b --- /dev/null +++ b/drivers/ax-driver/src/serial/ns16550.rs @@ -0,0 +1,178 @@ +use alloc::format; + +use log::info; +use rdrive::{ + probe::{ + OnProbeError, + acpi::{AcpiId, AcpiInfo, ProbeAcpi}, + }, + register::ProbeFdt, +}; +use some_serial::ns16550 as serial_ns16550; + +use super::{ + BInterruptSerial, KernelSerialPort, PlatformSerialDevice, acpi_serial_device_info, prop_u32, + serial_device_info, +}; + +const ACPI_NS16550_CLOCK: u32 = 1_843_200; +const ACPI_NS16550_REG_WIDTH: usize = 1; + +model_register!( + name: "NS16550 serial", + level: ProbeLevel::PreKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ + ProbeKind::Fdt { + compatibles: &["snps,dw-apb-uart", "ns16550a", "ns16550"], + on_probe: probe + }, + ProbeKind::Acpi { + ids: &[ + AcpiId { + hid: "PNP0501", + cids: &[], + }, + AcpiId { + hid: "PNP0500", + cids: &[], + }, + ], + on_probe: probe_acpi + }, + ], +); + +fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { + let (info, plat_dev) = probe.into_parts(); + + info!("Probing NS16550 serial device: {}", info.node.name()); + let base_reg = info + .node + .regs() + .into_iter() + .next() + .ok_or_else(|| OnProbeError::other(format!("[{}] has no reg", info.node.name())))?; + + let mmio_size = base_reg.size.unwrap_or(0x1000); + let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size as usize)?; + let node = info.node.as_node(); + let reg_width = prop_u32(node, "reg-io-width").unwrap_or(1) as usize; + let reg_shift = prop_u32(node, "reg-shift").map(|shift| 1usize << shift); + let ns16550_width = reg_shift.unwrap_or(reg_width); + let mut serial: Option = None; + + for compatible in node.compatibles() { + if compatible == "snps,dw-apb-uart" { + let clock_freq = prop_u32(node, "clock-frequency") + .unwrap_or(serial_ns16550::dw_apb::SG2002_UART_CLOCK); + let raw = serial_ns16550::DwApbUart::new_raw(mmio_base, clock_freq); + serial = Some(KernelSerialPort::new_dyn(raw)); + break; + } + + if matches!(compatible, "ns16550a" | "ns16550") { + let clock_freq = prop_u32(node, "clock-frequency").unwrap_or(24_000_000); + let raw = serial_ns16550::Ns16550::new_mmio(mmio_base, clock_freq, ns16550_width); + serial = Some(KernelSerialPort::new_dyn(raw)); + break; + } + } + + let serial = serial.ok_or(OnProbeError::NotMatch)?; + let base = serial.base_addr(); + let baudrate = serial.baudrate(); + let device_info = serial_device_info(&info, &base_reg, base, baudrate); + + info!("NS16550 serial@{base:#x} registered successfully"); + plat_dev.register(PlatformSerialDevice::new( + serial.name().into(), + device_info, + serial, + )); + Ok(()) +} + +struct AcpiSerialResource { + serial: BInterruptSerial, + paddr: usize, + mapped_base: usize, +} + +fn probe_acpi(probe: ProbeAcpi<'_>) -> Result<(), OnProbeError> { + let info = probe.info(); + + info!("Probing ACPI NS16550 serial device: {}", info.path); + let resource = if let Some(resource) = acpi_io_serial(info)? { + resource + } else { + acpi_mmio_serial(info)? + }; + let baudrate = resource.serial.baudrate(); + let device_info = acpi_serial_device_info(info, resource.paddr, resource.mapped_base, baudrate); + let serial_name = resource.serial.name().into(); + let plat_dev = probe.into_platform_device(); + + info!( + "ACPI NS16550 serial@{:#x} registered successfully", + resource.paddr + ); + plat_dev.register(PlatformSerialDevice::new( + serial_name, + device_info, + resource.serial, + )); + Ok(()) +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +fn acpi_io_serial(info: &AcpiInfo<'_>) -> Result, OnProbeError> { + let Some(range) = info.io_ranges().first() else { + return Ok(None); + }; + let port = u16::try_from(range.base).map_err(|_| { + OnProbeError::other(format!( + "{} has invalid ACPI serial I/O base {:#x}", + info.path, range.base + )) + })?; + let raw = serial_ns16550::Ns16550::new_port(port, ACPI_NS16550_CLOCK); + let serial = KernelSerialPort::new_dyn(raw); + let mapped_base = serial.base_addr(); + Ok(Some(AcpiSerialResource { + serial, + paddr: usize::from(port), + mapped_base, + })) +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +fn acpi_io_serial(_info: &AcpiInfo<'_>) -> Result, OnProbeError> { + Ok(None) +} + +fn acpi_mmio_serial(info: &AcpiInfo<'_>) -> Result { + let range = info.memory_ranges().first().ok_or_else(|| { + OnProbeError::other(format!( + "{} has no ACPI serial I/O port or MMIO resource", + info.path + )) + })?; + let paddr = usize::try_from(range.base).map_err(|_| { + OnProbeError::other(format!( + "{} has invalid ACPI serial MMIO base {:#x}", + info.path, range.base + )) + })?; + let mmio_size = usize::try_from(range.size).unwrap_or(0x1000).max(0x1000); + let mmio_base = crate::mmio::iomap(paddr, mmio_size)?; + let raw = + serial_ns16550::Ns16550::new_mmio(mmio_base, ACPI_NS16550_CLOCK, ACPI_NS16550_REG_WIDTH); + let serial = KernelSerialPort::new_dyn(raw); + let mapped_base = serial.base_addr(); + Ok(AcpiSerialResource { + serial, + paddr, + mapped_base, + }) +} diff --git a/drivers/ax-driver/src/serial/pl011.rs b/drivers/ax-driver/src/serial/pl011.rs new file mode 100644 index 0000000000..c636360043 --- /dev/null +++ b/drivers/ax-driver/src/serial/pl011.rs @@ -0,0 +1,46 @@ +use alloc::format; + +use log::info; +use rdrive::{probe::OnProbeError, register::ProbeFdt}; +use some_serial::pl011; + +use super::{KernelSerialPort, PlatformSerialDevice, prop_u32, serial_device_info}; + +model_register!( + name: "PL011 serial", + level: ProbeLevel::PreKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ProbeKind::Fdt { + compatibles: &["arm,pl011"], + on_probe: probe + }], +); + +fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { + let (info, plat_dev) = probe.into_parts(); + + info!("Probing PL011 serial device: {}", info.node.name()); + let base_reg = info + .node + .regs() + .into_iter() + .next() + .ok_or_else(|| OnProbeError::other(format!("[{}] has no reg", info.node.name())))?; + + let mmio_size = base_reg.size.unwrap_or(0x1000); + let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size as usize)?; + let clock_freq = prop_u32(info.node.as_node(), "clock-frequency").unwrap_or(24_000_000); + let raw = pl011::Pl011::new(mmio_base, clock_freq); + let serial = KernelSerialPort::new_dyn(raw); + let base = serial.base_addr(); + let baudrate = serial.baudrate(); + let device_info = serial_device_info(&info, &base_reg, base, baudrate); + + info!("PL011 serial@{base:#x} registered successfully"); + plat_dev.register(PlatformSerialDevice::new( + serial.name().into(), + device_info, + serial, + )); + Ok(()) +} diff --git a/drivers/ax-driver/src/serial/rockchip_fiq.rs b/drivers/ax-driver/src/serial/rockchip_fiq.rs new file mode 100644 index 0000000000..6883e8b3c2 --- /dev/null +++ b/drivers/ax-driver/src/serial/rockchip_fiq.rs @@ -0,0 +1,324 @@ +use alloc::{format, string::String}; + +use fdt_edit::{Fdt, NodeType, RegFixed, Status}; +use log::{info, warn}; +use rdrive::{probe::OnProbeError, register::ProbeFdt}; +use some_serial::ns16550::rockchip_fiq::{ + ROCKCHIP_FIQ_DEFAULT_BAUDRATE, ROCKCHIP_FIQ_RK3588_UART_CLOCK, RockchipFiqConfig, + RockchipFiqSerial, +}; + +use super::{KernelSerialPort, PlatformSerialDevice, SerialDeviceInfo, prop_u32}; +use crate::BindingInfo; + +model_register!( + name: "rockchip fiq debugger serial", + level: ProbeLevel::PreKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ProbeKind::Fdt { + compatibles: &["rockchip,fiq-debugger"], + on_probe: probe + }], +); + +fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { + let (info, plat_dev) = probe.into_parts(); + let live_fdt = + rdrive::with_fdt(Clone::clone).ok_or_else(|| OnProbeError::other("live FDT not found"))?; + let fdt_config = rockchip_fiq_fdt_config(&live_fdt, info.node)?; + let mmio_base = crate::mmio::iomap( + fdt_config.reg.address as usize, + fdt_config.reg.size.unwrap_or(0x100) as usize, + )?; + + if fdt_config.target_disabled { + info!( + "Rockchip FIQ debugger takes disabled UART alias serial{} at {}", + fdt_config.config.serial_id, fdt_config.uart_path + ); + } + + let raw = RockchipFiqSerial::new(mmio_base, fdt_config.config); + let serial = KernelSerialPort::new_dyn(raw); + let base = serial.base_addr(); + info!( + "Rockchip FIQ debugger UART@{base:#x} registered successfully, serial-id={}, baudrate={}, \ + irq-mode={}", + fdt_config.config.serial_id, fdt_config.config.baudrate, fdt_config.config.irq_mode_enabled + ); + let binding_info = if fdt_config.config.irq_mode_enabled { + uart_binding_info(&live_fdt, &fdt_config.uart_path)? + } else { + BindingInfo::empty() + }; + let device_id = plat_dev.descriptor().device_id(); + if !rdrive::note_fdt_device_path(&fdt_config.uart_path, device_id) { + warn!( + "failed to map Rockchip FIQ target UART path {} to serial device id {:?}", + fdt_config.uart_path, device_id + ); + } + plat_dev.register(PlatformSerialDevice::new( + serial.name().into(), + SerialDeviceInfo { + fdt_path: fdt_config.uart_path, + alias_index: Some(fdt_config.config.serial_id as usize), + paddr: fdt_config.reg.address as usize, + mapped_base: base, + baudrate: serial.baudrate(), + irq_num: binding_info.irq_num(), + binding_info, + }, + serial, + )); + Ok(()) +} + +fn uart_binding_info(fdt: &Fdt, uart_path: &str) -> Result { + let uart = fdt + .get_by_path(uart_path) + .ok_or_else(|| OnProbeError::other(format!("{uart_path} node not found")))?; + let Some(interrupt) = uart.interrupts().into_iter().next() else { + warn!("{uart_path} has no UART IRQ; FIQ serial tty will not be interrupt driven"); + return Ok(BindingInfo::empty()); + }; + let interrupt_parent = rdrive::fdt_phandle_to_device_id(interrupt.interrupt_parent) + .ok_or_else(|| { + OnProbeError::other(format!( + "failed to resolve interrupt parent {:?} for {uart_path}", + interrupt.interrupt_parent + )) + })?; + let intc = rdrive::get::(interrupt_parent).map_err(|err| { + OnProbeError::other(format!( + "failed to get interrupt controller {:?} for {uart_path}: {err:?}", + interrupt_parent + )) + })?; + let mut intc = intc.lock().map_err(|err| { + OnProbeError::other(format!( + "failed to lock interrupt controller {:?} for {uart_path}: {err:?}", + interrupt_parent + )) + })?; + Ok(BindingInfo::with_irq(Some( + intc.setup_irq_by_fdt(&interrupt.specifier).into(), + ))) +} + +struct RockchipFiqFdtConfig { + config: RockchipFiqConfig, + reg: RegFixed, + uart_path: String, + target_disabled: bool, +} + +fn rockchip_fiq_fdt_config( + fdt: &Fdt, + fiq: NodeType<'_>, +) -> Result { + let fiq_node = fiq.as_node(); + let serial_id = prop_u32(fiq_node, "rockchip,serial-id").ok_or_else(|| { + OnProbeError::other(format!("[{}] has no rockchip,serial-id", fiq.name())) + })?; + + if serial_id == u32::MAX { + return Err(OnProbeError::NotMatch); + } + + let alias = format!("serial{serial_id}"); + let uart_path = fdt + .resolve_alias(&alias) + .map(String::from) + .ok_or_else(|| OnProbeError::other(format!("{alias} alias not found")))?; + let uart_node = fdt + .get_by_path(&uart_path) + .ok_or_else(|| OnProbeError::other(format!("{uart_path} node not found")))?; + let uart = uart_node.as_node(); + + if !uart + .compatibles() + .any(|compatible| compatible == "snps,dw-apb-uart") + { + return Err(OnProbeError::other(format!( + "{uart_path} is not a snps,dw-apb-uart node" + ))); + } + + let reg_width = prop_u32(uart, "reg-io-width").unwrap_or(4); + let reg_shift = prop_u32(uart, "reg-shift").unwrap_or(2); + if reg_width != 4 || reg_shift != 2 { + return Err(OnProbeError::other(format!( + "{uart_path} has unsupported reg-io-width/reg-shift {reg_width}/{reg_shift}" + ))); + } + + let reg = uart_node + .regs() + .into_iter() + .next() + .ok_or_else(|| OnProbeError::other(format!("[{uart_path}] has no reg")))?; + + let baudrate = normalise_fiq_baudrate( + prop_u32(fiq_node, "rockchip,baudrate").unwrap_or(ROCKCHIP_FIQ_DEFAULT_BAUDRATE), + ); + let clock_hz = prop_u32(uart, "clock-frequency").unwrap_or(ROCKCHIP_FIQ_RK3588_UART_CLOCK); + let irq_mode_enabled = prop_u32(fiq_node, "rockchip,irq-mode-enable").unwrap_or(0) != 0; + let target_disabled = matches!(uart.status(), Some(Status::Disabled)); + + if matches!(uart.status(), Some(status) if status != Status::Disabled && status != Status::Okay) + { + warn!("{uart_path} has unrecognised status; proceeding for FIQ debugger"); + } + + Ok(RockchipFiqFdtConfig { + config: RockchipFiqConfig { + serial_id, + baudrate, + clock_hz, + irq_mode_enabled, + debug_enable: true, + console_enable: true, + }, + reg, + uart_path, + target_disabled, + }) +} + +fn normalise_fiq_baudrate(baudrate: u32) -> u32 { + match baudrate { + 115_200 | 1_500_000 => baudrate, + other => { + warn!("unsupported rockchip fiq baudrate {other}, falling back to 115200"); + 115_200 + } + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use fdt_edit::{Fdt, Node, Property}; + + use super::*; + + #[test] + fn resolves_fiq_debugger_target_uart_from_alias_even_when_uart_disabled() { + let fdt = minimal_fiq_fdt(true, true); + let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); + + let config = rockchip_fiq_fdt_config(&fdt, fiq).expect("parse fiq config"); + + assert_eq!(config.config.serial_id, 2); + assert_eq!(config.config.baudrate, 1_500_000); + assert_eq!(config.config.clock_hz, ROCKCHIP_FIQ_RK3588_UART_CLOCK); + assert!(config.config.irq_mode_enabled); + assert_eq!(config.uart_path, "/serial@feb50000"); + assert!(config.target_disabled); + assert_eq!(config.reg.address, 0xfeb5_0000); + assert_eq!(config.reg.size, Some(0x100)); + } + + #[test] + fn rejects_missing_alias_or_non_dw_apb_target() { + let fdt = minimal_fiq_fdt(false, true); + let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); + assert!(rockchip_fiq_fdt_config(&fdt, fiq).is_err()); + + let fdt = minimal_fiq_fdt(true, false); + let fiq = fdt.get_by_path("/fiq-debugger").expect("fiq node missing"); + assert!(rockchip_fiq_fdt_config(&fdt, fiq).is_err()); + } + + fn minimal_fiq_fdt(with_alias: bool, dw_apb: bool) -> Fdt { + let mut fdt = Fdt::new(); + let root = fdt.root_id(); + fdt.node_mut(root) + .unwrap() + .set_property(prop_u32_ls("#address-cells", &[2])); + fdt.node_mut(root) + .unwrap() + .set_property(prop_u32_ls("#size-cells", &[1])); + + let aliases = fdt.add_node(root, Node::new("aliases")); + if with_alias { + fdt.node_mut(aliases) + .unwrap() + .set_property(prop_str("serial2", "/serial@feb50000")); + } + + let fiq = fdt.add_node(root, Node::new("fiq-debugger")); + fdt.node_mut(fiq) + .unwrap() + .set_property(prop_strs("compatible", &["rockchip,fiq-debugger"])); + fdt.node_mut(fiq) + .unwrap() + .set_property(prop_u32_ls("rockchip,serial-id", &[2])); + fdt.node_mut(fiq) + .unwrap() + .set_property(prop_u32_ls("rockchip,baudrate", &[1_500_000])); + fdt.node_mut(fiq) + .unwrap() + .set_property(prop_u32_ls("rockchip,irq-mode-enable", &[1])); + fdt.node_mut(fiq) + .unwrap() + .set_property(prop_str("status", "okay")); + + let uart = fdt.add_node(root, Node::new("serial@feb50000")); + fdt.node_mut(uart).unwrap().set_property(prop_strs( + "compatible", + if dw_apb { + &["rockchip,rk3588-uart", "snps,dw-apb-uart"] + } else { + &["rockchip,rk3588-uart"] + }, + )); + fdt.node_mut(uart) + .unwrap() + .set_property(prop_reg(0xfeb5_0000, 0x100)); + fdt.node_mut(uart) + .unwrap() + .set_property(prop_u32_ls("reg-io-width", &[4])); + fdt.node_mut(uart) + .unwrap() + .set_property(prop_u32_ls("reg-shift", &[2])); + fdt.node_mut(uart) + .unwrap() + .set_property(prop_str("status", "disabled")); + fdt + } + + fn prop_u32_ls(name: &str, values: &[u32]) -> Property { + let mut data = Vec::new(); + for value in values { + data.extend_from_slice(&value.to_be_bytes()); + } + Property::new(name, data) + } + + fn prop_reg(address: u64, size: u32) -> Property { + let mut data = Vec::new(); + data.extend_from_slice(&((address >> 32) as u32).to_be_bytes()); + data.extend_from_slice(&(address as u32).to_be_bytes()); + data.extend_from_slice(&size.to_be_bytes()); + Property::new("reg", data) + } + + fn prop_str(name: &str, value: &str) -> Property { + let mut data = Vec::new(); + data.extend_from_slice(value.as_bytes()); + data.push(0); + Property::new(name, data) + } + + fn prop_strs(name: &str, values: &[&str]) -> Property { + let mut data = Vec::new(); + for value in values { + data.extend_from_slice(value.as_bytes()); + data.push(0); + } + Property::new(name, data) + } +} diff --git a/drivers/ax-driver/src/serial/runtime.rs b/drivers/ax-driver/src/serial/runtime.rs new file mode 100644 index 0000000000..9a34c8f3e9 --- /dev/null +++ b/drivers/ax-driver/src/serial/runtime.rs @@ -0,0 +1,120 @@ +use alloc::{string::String, sync::Arc}; + +use ax_kspin::SpinNoIrq; +use rdif_serial::{ + Config, ConfigError, RawUart, RxItem, SerialCore, SerialCounters, SerialIrqOutcome, +}; + +pub type BInterruptSerial = Arc; + +pub trait InterruptSerial: Send + Sync + 'static { + fn name(&self) -> &str; + fn base_addr(&self) -> usize; + fn baudrate(&self) -> u32; + + fn startup(&self, config: &Config) -> Result<(), ConfigError>; + fn shutdown(&self); + fn set_config(&self, config: &Config) -> Result<(), ConfigError>; + + fn try_write(&self, bytes: &[u8]) -> usize; + fn write_room(&self) -> usize; + fn chars_in_buffer(&self) -> usize; + fn flush_tx_buffer(&self); + fn tx_idle(&self) -> bool; + + fn drain_rx(&self, out: &mut [RxItem]) -> usize; + fn rx_pending(&self) -> bool; + + fn handle_irq(&self) -> SerialIrqOutcome; + fn startup_catch_up(&self) -> SerialIrqOutcome; + + fn counters(&self) -> SerialCounters; +} + +pub struct KernelSerialPort { + name: String, + base_addr: usize, + inner: SpinNoIrq>, +} + +impl KernelSerialPort { + pub fn new(raw: T) -> Self { + let name = raw.name().into(); + let base_addr = raw.base_addr(); + Self { + name, + base_addr, + inner: SpinNoIrq::new(SerialCore::new(raw)), + } + } + + pub fn new_dyn(raw: T) -> BInterruptSerial { + Arc::new(Self::new(raw)) + } +} + +impl InterruptSerial for KernelSerialPort { + fn name(&self) -> &str { + &self.name + } + + fn base_addr(&self) -> usize { + self.base_addr + } + + fn baudrate(&self) -> u32 { + self.inner.lock().baudrate() + } + + fn startup(&self, config: &Config) -> Result<(), ConfigError> { + self.inner.lock().startup(config) + } + + fn shutdown(&self) { + self.inner.lock().shutdown(); + } + + fn set_config(&self, config: &Config) -> Result<(), ConfigError> { + self.inner.lock().set_config(config) + } + + fn try_write(&self, bytes: &[u8]) -> usize { + self.inner.lock().enqueue_tx(bytes).accepted + } + + fn write_room(&self) -> usize { + self.inner.lock().write_room() + } + + fn chars_in_buffer(&self) -> usize { + self.inner.lock().chars_in_buffer() + } + + fn flush_tx_buffer(&self) { + self.inner.lock().flush_tx_buffer(); + } + + fn tx_idle(&self) -> bool { + self.inner.lock().tx_idle() + } + + fn drain_rx(&self, out: &mut [RxItem]) -> usize { + self.inner.lock().drain_rx(out) + } + + fn rx_pending(&self) -> bool { + self.inner.lock().rx_pending() + } + + fn handle_irq(&self) -> SerialIrqOutcome { + self.inner.lock().handle_irq() + } + + fn startup_catch_up(&self) -> SerialIrqOutcome { + self.inner.lock().startup_catch_up() + } + + fn counters(&self) -> SerialCounters { + self.inner.lock().counters() + } +} diff --git a/drivers/blk/nvme-driver/src/block.rs b/drivers/blk/nvme-driver/src/block.rs index f55343f7e2..b9a9ab4bb5 100644 --- a/drivers/blk/nvme-driver/src/block.rs +++ b/drivers/blk/nvme-driver/src/block.rs @@ -117,6 +117,7 @@ impl NvmeBlockDriver { limits( inner.nvme.dma_mask(), inner.nvme.page_size(), + inner.nvme.max_transfer_bytes(), inner.namespace, self.queue_depth, ) @@ -190,6 +191,7 @@ impl Interface for NvmeBlockDriver { inner.namespace, inner.nvme.dma_mask(), inner.nvme.page_size(), + inner.nvme.max_transfer_bytes(), queue, prp_lists, )) @@ -279,6 +281,7 @@ struct NvmeQueueCore { namespace: Namespace, dma_mask: u64, page_size: usize, + max_transfer_bytes: Option, depth: usize, queue: UnsafeCell, state: UnsafeCell, @@ -345,6 +348,7 @@ impl NvmeQueueCore { namespace: Namespace, dma_mask: u64, page_size: usize, + max_transfer_bytes: Option, queue: HardwareQueue, prp_lists: Vec>, ) -> Arc { @@ -361,6 +365,7 @@ impl NvmeQueueCore { namespace, dma_mask, page_size, + max_transfer_bytes, depth, queue: UnsafeCell::new(queue), state: UnsafeCell::new(NvmeQueueState { @@ -382,7 +387,13 @@ impl NvmeQueueCore { QueueInfo { id: self.id, device: device_info(self.name, self.namespace), - limits: limits(self.dma_mask, self.page_size, self.namespace, self.depth), + limits: limits( + self.dma_mask, + self.page_size, + self.max_transfer_bytes, + self.namespace, + self.depth, + ), } } @@ -835,17 +846,25 @@ fn device_info(name: &'static str, namespace: Namespace) -> DeviceInfo { fn limits( dma_mask: u64, page_size: usize, + controller_max_transfer_bytes: Option, namespace: Namespace, max_inflight: usize, ) -> QueueLimits { - let dma_alignment = page_size.max(namespace.lba_size.max(1)); + let lba_size = namespace.lba_size.max(1); + let dma_alignment = page_size.max(lba_size); let prp_entries = page_size / core::mem::size_of::(); - let max_bytes = page_size.saturating_mul(prp_entries + 1); + let prp_capacity_bytes = page_size.saturating_mul(prp_entries + 1); + let max_bytes = controller_max_transfer_bytes + .map_or(prp_capacity_bytes, |max_transfer| { + prp_capacity_bytes.min(max_transfer) + }) + .max(lba_size); let max_blocks = max_bytes - .checked_div(namespace.lba_size.max(1)) + .checked_div(lba_size) .unwrap_or(1) .max(1) .min(u16::MAX as usize + 1) as u32; + let max_bytes = (max_blocks as usize).saturating_mul(lba_size); QueueLimits { dma_mask, dma_alignment, @@ -854,7 +873,11 @@ fn limits( max_segments: prp_entries + 1, max_segment_size: max_bytes, supported_flags: RequestFlags::NONE, - supports_flush: true, + // Do not advertise flush until the driver plumbs a reliable capability + // check from Identify/Feature data. Some QEMU NVMe backends reject the + // Flush command with "Invalid Field", which must not surface as fsync + // I/O errors. + supports_flush: false, supports_discard: false, supports_write_zeroes: false, } @@ -876,12 +899,13 @@ mod tests { lba_count: 1024, metadata_size: 0, }; - let limits = limits(u64::MAX, 4096, namespace, 8); + let limits = limits(u64::MAX, 4096, None, namespace, 8); assert_eq!(limits.dma_alignment, 4096); assert_eq!(limits.max_segments, 513); assert_eq!(limits.max_segment_size, 4096 * 513); assert!(limits.max_blocks_per_request >= 8); + assert!(!limits.supports_flush); } #[test] @@ -892,14 +916,28 @@ mod tests { lba_count: 1024, metadata_size: 0, }; - let limits = limits(u64::MAX, 4096, namespace, 8); + let limits = limits(u64::MAX, 4096, None, namespace, 8); assert_eq!(limits.dma_alignment, 8192); assert_eq!(limits.max_segments, 513); - assert_eq!(limits.max_segment_size, 4096 * 513); + assert_eq!(limits.max_segment_size, 8192 * 256); assert_eq!(limits.max_blocks_per_request, 256); } + #[test] + fn queue_limits_respect_controller_transfer_limit() { + let namespace = Namespace { + id: 1, + lba_size: 512, + lba_count: 1024, + metadata_size: 0, + }; + let limits = limits(u64::MAX, 4096, Some(512 * 1024), namespace, 8); + + assert_eq!(limits.max_blocks_per_request, 1024); + assert_eq!(limits.max_segment_size, 512 * 1024); + } + #[test] fn prp_pages_split_at_controller_page_boundaries() { let mut pages = PrpPageAccumulator::new(); diff --git a/drivers/blk/nvme-driver/src/command.rs b/drivers/blk/nvme-driver/src/command.rs index bf365476b9..cc1593f44b 100644 --- a/drivers/blk/nvme-driver/src/command.rs +++ b/drivers/blk/nvme-driver/src/command.rs @@ -215,6 +215,7 @@ impl Identify for IdentifyController { ControllerInfo { vendor_id: raw.vendor_id, product_id: raw.product_id, + mdts: raw.mdts, sqes_max: raw.sqes >> 4, sqes_min: raw.sqes & 0b1111, cqes_max: raw.cqes >> 4, @@ -236,7 +237,9 @@ pub struct ControllerData { pub serial_number: [u8; 20], pub model_number: [u8; 40], pub firmware_revision: [u8; 8], - pub rsv: [u8; 512 - 8 - 40 - 20 - 2 - 2], + pub rsv_before_mdts: [u8; 5], + pub mdts: u8, + pub rsv: [u8; 512 - 8 - 40 - 20 - 2 - 2 - 5 - 1], pub sqes: u8, pub cqes: u8, pub max_cmd: u16, @@ -247,6 +250,7 @@ pub struct ControllerData { pub struct ControllerInfo { pub vendor_id: u16, pub product_id: u16, + pub mdts: u8, pub sqes_max: u8, pub sqes_min: u8, pub cqes_max: u8, @@ -254,3 +258,26 @@ pub struct ControllerInfo { pub max_cmd: u16, pub number_of_namespaces: u32, } + +#[cfg(test)] +mod tests { + use super::{Identify, IdentifyController}; + + #[test] + fn identify_controller_reads_mdts_from_spec_offset() { + let mut data = [0_u8; 4096]; + data[77] = 7; + data[512] = 0x66; + data[513] = 0x44; + data[516..520].copy_from_slice(&3_u32.to_le_bytes()); + + let info = IdentifyController::new().parse(&data); + + assert_eq!(info.mdts, 7); + assert_eq!(info.sqes_min, 6); + assert_eq!(info.sqes_max, 6); + assert_eq!(info.cqes_min, 4); + assert_eq!(info.cqes_max, 4); + assert_eq!(info.number_of_namespaces, 3); + } +} diff --git a/drivers/blk/nvme-driver/src/nvme.rs b/drivers/blk/nvme-driver/src/nvme.rs index 4610b51c86..f8804b96d4 100644 --- a/drivers/blk/nvme-driver/src/nvme.rs +++ b/drivers/blk/nvme-driver/src/nvme.rs @@ -25,6 +25,7 @@ pub struct Nvme { sqes: u32, cqes: u32, page_size: usize, + max_transfer_bytes: Option, io_queue_interrupts: bool, interrupt_vector: u32, } @@ -94,6 +95,7 @@ impl Nvme { sqes: 6, cqes: 4, page_size: config.page_size, + max_transfer_bytes: None, io_queue_interrupts: config.io_queue_interrupts, interrupt_vector: config.interrupt_vector, }; @@ -141,6 +143,7 @@ impl Nvme { debug!("Controller: {:?}", controller); self.num_ns = controller.number_of_namespaces as _; + self.max_transfer_bytes = controller_max_transfer_bytes(config.page_size, controller.mdts); if config.io_queue_interrupts { self.mask_interrupt_vector(config.interrupt_vector); } @@ -251,6 +254,10 @@ impl Nvme { self.page_size } + pub(crate) const fn max_transfer_bytes(&self) -> Option { + self.max_transfer_bytes + } + pub fn io_queue_interrupts_enabled(&self) -> bool { self.io_queue_interrupts } @@ -382,6 +389,14 @@ impl Nvme { unsafe impl Send for Nvme {} +fn controller_max_transfer_bytes(page_size: usize, mdts: u8) -> Option { + if mdts == 0 { + None + } else { + Some(page_size.checked_shl(u32::from(mdts)).unwrap_or(usize::MAX)) + } +} + #[derive(Debug, Clone, Copy)] pub struct Namespace { pub id: u32, @@ -392,7 +407,7 @@ pub struct Namespace { #[cfg(test)] mod tests { - use super::Config; + use super::{Config, controller_max_transfer_bytes}; #[test] fn config_defaults_to_polling_and_can_enable_intx() { @@ -404,4 +419,14 @@ mod tests { assert!(irq_config.io_queue_interrupts); assert_eq!(irq_config.interrupt_vector, 0); } + + #[test] + fn controller_mdts_zero_means_unrestricted_transfer_size() { + assert_eq!(controller_max_transfer_bytes(4096, 0), None); + } + + #[test] + fn controller_mdts_scales_with_controller_page_size() { + assert_eq!(controller_max_transfer_bytes(4096, 7), Some(512 * 1024)); + } } diff --git a/drivers/interface/rdif-serial/Cargo.toml b/drivers/interface/rdif-serial/Cargo.toml index 965dcfe992..9148104f97 100644 --- a/drivers/interface/rdif-serial/Cargo.toml +++ b/drivers/interface/rdif-serial/Cargo.toml @@ -13,6 +13,5 @@ version = "0.8.2" bitflags = "2.8" futures = {version = "0.3", default-features = false, features = ["alloc"]} rdif-base = {workspace = true} -spin.workspace = true thiserror = {version = "2", default-features = false} heapless = "0.9" diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs new file mode 100644 index 0000000000..3ac11eb0a1 --- /dev/null +++ b/drivers/interface/rdif-serial/src/core.rs @@ -0,0 +1,546 @@ +use crate::{ + Config, ConfigError, FixedQueue, InterruptMask, IrqSource, RawUart, RxFlag, RxItem, + SerialCounters, SerialIrqOutcome, +}; + +pub const DEFAULT_TX_CAP: usize = 4096; +pub const DEFAULT_RX_CAP: usize = 4096; + +pub const RX_IRQ_BUDGET: usize = 256; +pub const TX_IRQ_BUDGET: usize = 64; +pub const IRQ_PASS_BUDGET: usize = 32; +pub const TX_WAKEUP_WATERMARK: usize = 256; +pub const TX_KICK_BUDGET: usize = 32; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PortState { + Down, + Up, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TxEnqueue { + pub accepted: usize, + pub sent_immediately: usize, +} + +#[derive(Clone, Copy, Debug, Default)] +struct TxService { + sent: usize, + wake_writers: bool, +} + +/// Runtime UART core. +/// +/// The core itself is lock-free and assumes the caller already holds the port +/// lock. It owns the raw register object, TX software FIFO, and RX flip FIFO. +/// IRQ code and task code must not access `raw` through any other path. +pub struct SerialCore< + T: RawUart, + const TX_CAP: usize = DEFAULT_TX_CAP, + const RX_CAP: usize = DEFAULT_RX_CAP, +> { + raw: T, + tx_fifo: FixedQueue, + rx_fifo: FixedQueue, + irq_mask: InterruptMask, + state: PortState, + counters: SerialCounters, +} + +impl SerialCore { + pub fn new(raw: T) -> Self { + Self { + raw, + tx_fifo: FixedQueue::new(), + rx_fifo: FixedQueue::new(), + irq_mask: InterruptMask::empty(), + state: PortState::Down, + counters: SerialCounters::default(), + } + } + + pub fn raw(&self) -> &T { + &self.raw + } + + pub fn raw_mut(&mut self) -> &mut T { + &mut self.raw + } + + pub fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { + if self.state == PortState::Up { + return Ok(()); + } + + self.raw.startup(config)?; + self.irq_mask = InterruptMask::RX; + self.raw.set_irq_mask(self.irq_mask); + self.state = PortState::Up; + Ok(()) + } + + pub fn shutdown(&mut self) { + if self.state == PortState::Down { + return; + } + + self.set_irq_mask_locked(InterruptMask::empty()); + self.raw.shutdown(); + self.tx_fifo.clear(); + self.rx_fifo.clear(); + self.state = PortState::Down; + } + + pub fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { + let saved_mask = self.irq_mask; + self.raw.set_irq_mask(InterruptMask::empty()); + let result = self.raw.set_config(config); + self.raw.set_irq_mask(saved_mask); + result + } + + pub fn baudrate(&self) -> u32 { + self.raw.baudrate() + } + + pub fn write_room(&self) -> usize { + self.tx_fifo.remaining() + } + + pub fn chars_in_buffer(&self) -> usize { + self.tx_fifo.len() + } + + pub fn flush_tx_buffer(&mut self) { + self.tx_fifo.clear(); + self.stop_tx_irq_locked(); + } + + pub fn tx_idle(&mut self) -> bool { + self.tx_fifo.is_empty() && self.raw.tx_idle() + } + + pub fn enqueue_tx(&mut self, bytes: &[u8]) -> TxEnqueue { + if self.state != PortState::Up || bytes.is_empty() { + return TxEnqueue::default(); + } + + let accepted = self.tx_fifo.push_slice(bytes); + let service = if accepted > 0 { + self.service_tx_locked(TX_KICK_BUDGET) + } else { + TxService::default() + }; + + TxEnqueue { + accepted, + sent_immediately: service.sent, + } + } + + pub fn drain_rx(&mut self, out: &mut [RxItem]) -> usize { + let mut count = 0; + for slot in out { + let Some(item) = self.rx_fifo.pop_front() else { + break; + }; + *slot = item; + count += 1; + } + count + } + + pub fn rx_pending(&self) -> bool { + !self.rx_fifo.is_empty() + } + + pub fn handle_irq(&mut self) -> SerialIrqOutcome { + let mut outcome = SerialIrqOutcome::default(); + + if self.state != PortState::Up { + return outcome; + } + + let mut rx_budget = RX_IRQ_BUDGET; + let mut tx_budget = TX_IRQ_BUDGET; + + for _ in 0..IRQ_PASS_BUDGET { + let snapshot = self.raw.take_irq_snapshot(); + + if !snapshot.claimed { + if !outcome.claimed { + self.counters.irq_spurious += 1; + } + break; + } + + if !outcome.claimed { + self.counters.irq_total += 1; + } + outcome.claimed = true; + + if snapshot + .sources + .intersects(IrqSource::RX_DATA | IrqSource::RX_TIMEOUT | IrqSource::RX_STATUS) + { + let pushed = self.service_rx_locked(rx_budget); + outcome.rx_pushed += pushed; + rx_budget = rx_budget.saturating_sub(pushed); + } + + if snapshot.sources.contains(IrqSource::TX_SPACE) { + let service = self.service_tx_locked(tx_budget); + outcome.tx_sent += service.sent; + outcome.tx_wakeup |= service.wake_writers; + tx_budget = tx_budget.saturating_sub(service.sent); + } + + if snapshot.sources.contains(IrqSource::MODEM_STATUS) { + self.raw.ack_modem_status(); + } + + if snapshot.sources.contains(IrqSource::BUSY_DETECT) { + self.raw.ack_busy_detect(); + } + + if rx_budget == 0 || tx_budget == 0 { + outcome.budget_exhausted = true; + self.counters.irq_budget_exhausted += 1; + break; + } + } + + outcome + } + + pub fn startup_catch_up(&mut self) -> SerialIrqOutcome { + if self.state != PortState::Up { + return SerialIrqOutcome::default(); + } + + let rx_pushed = self.service_rx_locked(RX_IRQ_BUDGET); + SerialIrqOutcome { + claimed: false, + rx_pushed, + tx_sent: 0, + tx_wakeup: false, + budget_exhausted: rx_pushed == RX_IRQ_BUDGET, + } + } + + pub fn counters(&self) -> SerialCounters { + self.counters + } + + fn set_irq_mask_locked(&mut self, mask: InterruptMask) { + if self.irq_mask != mask { + self.raw.set_irq_mask(mask); + self.irq_mask = mask; + } + } + + fn start_tx_irq_locked(&mut self) { + if !self.irq_mask.contains(InterruptMask::TX_SPACE) { + self.set_irq_mask_locked(self.irq_mask | InterruptMask::TX_SPACE); + } + } + + fn stop_tx_irq_locked(&mut self) { + if self.irq_mask.contains(InterruptMask::TX_SPACE) { + self.set_irq_mask_locked(self.irq_mask & !InterruptMask::TX_SPACE); + } + } + + fn service_tx_locked(&mut self, budget: usize) -> TxService { + let before = self.tx_fifo.len(); + if before == 0 { + self.stop_tx_irq_locked(); + return TxService::default(); + } + + let limit = budget.min(self.raw.tx_load_size().max(1)); + let mut sent = 0; + while sent < limit && self.raw.tx_ready() { + let Some(byte) = self.tx_fifo.front().copied() else { + break; + }; + self.raw.write_tx(byte); + self.tx_fifo.pop_front(); + sent += 1; + self.counters.tx_bytes += 1; + } + + let remaining = self.tx_fifo.len(); + if remaining == 0 { + self.stop_tx_irq_locked(); + } else { + self.start_tx_irq_locked(); + } + + let wakeup_threshold = TX_WAKEUP_WATERMARK.min(TX.saturating_sub(1)); + TxService { + sent, + wake_writers: before == TX + || (before > wakeup_threshold && remaining <= wakeup_threshold), + } + } + + fn service_rx_locked(&mut self, budget: usize) -> usize { + let mut pushed = 0; + + for _ in 0..budget { + let Some(sample) = self.raw.read_rx() else { + break; + }; + + if let Some(byte) = sample.byte { + self.counters.rx_bytes += 1; + match sample.flag { + RxFlag::Normal => {} + RxFlag::Break => self.counters.rx_breaks += 1, + RxFlag::Parity => self.counters.rx_parity_errors += 1, + RxFlag::Framing => self.counters.rx_framing_errors += 1, + } + + if self + .rx_fifo + .push_back(RxItem::Byte { + byte, + flag: sample.flag, + }) + .is_ok() + { + pushed += 1; + } else { + self.counters.rx_queue_dropped += 1; + } + } + + if sample.overrun { + self.counters.rx_fifo_overruns += 1; + if self.rx_fifo.push_back(RxItem::Overrun).is_ok() { + pushed += 1; + } else { + self.counters.rx_queue_dropped += 1; + } + } + } + + pushed + } +} + +#[cfg(test)] +mod tests { + use alloc::{collections::VecDeque, vec::Vec}; + use core::num::NonZeroU32; + + use super::*; + use crate::{DataBits, IrqSnapshot, Parity, RxSample, StopBits}; + + struct MockUart { + irq: VecDeque, + rx: VecDeque, + tx_ready_budget: usize, + tx_written: Vec, + mask: InterruptMask, + } + + impl MockUart { + fn new() -> Self { + Self { + irq: VecDeque::new(), + rx: VecDeque::new(), + tx_ready_budget: 0, + tx_written: Vec::new(), + mask: InterruptMask::empty(), + } + } + + fn irq(mut self, sources: IrqSource) -> Self { + self.irq.push_back(IrqSnapshot { + claimed: true, + sources, + }); + self + } + + fn rx_byte(mut self, byte: u8) -> Self { + self.rx.push_back(RxSample { + byte: Some(byte), + flag: RxFlag::Normal, + overrun: false, + }); + self + } + } + + impl RawUart for MockUart { + fn name(&self) -> &'static str { + "mock" + } + + fn base_addr(&self) -> usize { + 0x1000 + } + + fn clock_freq(&self) -> Option { + NonZeroU32::new(1) + } + + fn startup(&mut self, _config: &Config) -> Result<(), ConfigError> { + Ok(()) + } + + fn shutdown(&mut self) {} + + fn set_config(&mut self, _config: &Config) -> Result<(), ConfigError> { + Ok(()) + } + + fn baudrate(&self) -> u32 { + 115_200 + } + + fn data_bits(&self) -> DataBits { + DataBits::Eight + } + + fn stop_bits(&self) -> StopBits { + StopBits::One + } + + fn parity(&self) -> Parity { + Parity::None + } + + fn enable_loopback(&mut self) {} + fn disable_loopback(&mut self) {} + + fn is_loopback_enabled(&self) -> bool { + false + } + + fn set_irq_mask(&mut self, mask: InterruptMask) { + self.mask = mask; + } + + fn take_irq_snapshot(&mut self) -> IrqSnapshot { + self.irq.pop_front().unwrap_or_default() + } + + fn read_rx(&mut self) -> Option { + self.rx.pop_front() + } + + fn tx_ready(&mut self) -> bool { + self.tx_ready_budget > 0 + } + + fn write_tx(&mut self, byte: u8) { + assert!(self.tx_ready_budget > 0); + self.tx_ready_budget -= 1; + self.tx_written.push(byte); + } + + fn tx_load_size(&self) -> usize { + 16 + } + + fn tx_idle(&mut self) -> bool { + self.tx_written.is_empty() + } + + fn poll_status(&mut self) -> crate::SerialEvent { + crate::SerialEvent::empty() + } + } + + fn started_core( + uart: MockUart, + ) -> SerialCore { + let mut core = SerialCore::new(uart); + core.startup(&Config::new()).unwrap(); + core + } + + #[test] + fn no_pending_irq_is_unhandled_even_with_buffered_rx() { + let mut core = started_core::<16, 16>(MockUart::new()); + core.rx_fifo + .push_back(RxItem::Byte { + byte: b'x', + flag: RxFlag::Normal, + }) + .unwrap(); + + let outcome = core.handle_irq(); + + assert!(!outcome.claimed); + } + + #[test] + fn one_irq_services_rx_and_tx() { + let mut core = started_core::<16, 16>( + MockUart::new() + .irq(IrqSource::RX_DATA | IrqSource::TX_SPACE) + .rx_byte(b'Z'), + ); + + assert_eq!(core.enqueue_tx(b"abc").accepted, 3); + core.raw_mut().tx_ready_budget = 3; + let outcome = core.handle_irq(); + + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 1); + assert!(outcome.tx_sent > 0); + } + + #[test] + fn tx_keeps_unsent_suffix() { + let mut core = started_core::<16, 16>(MockUart::new().irq(IrqSource::TX_SPACE)); + + assert_eq!(core.enqueue_tx(b"abcdef").accepted, 6); + core.raw_mut().tx_ready_budget = 2; + let outcome = core.handle_irq(); + + assert_eq!(outcome.tx_sent, 2); + assert_eq!(core.chars_in_buffer(), 4); + } + + #[test] + fn rx_irq_is_bounded() { + let mut uart = MockUart::new().irq(IrqSource::RX_DATA); + for _ in 0..(RX_IRQ_BUDGET + 8) { + uart = uart.rx_byte(b'x'); + } + let mut core = started_core::<16, 512>(uart); + + let outcome = core.handle_irq(); + + assert!(outcome.budget_exhausted); + assert!(outcome.rx_pushed <= RX_IRQ_BUDGET); + } + + #[test] + fn rx_full_queue_records_drops_but_drains_hardware() { + let mut uart = MockUart::new().irq(IrqSource::RX_DATA); + for _ in 0..8 { + uart = uart.rx_byte(b'x'); + } + let mut core = started_core::<16, 4>(uart); + for _ in 0..4 { + core.rx_fifo + .push_back(RxItem::Byte { + byte: b'q', + flag: RxFlag::Normal, + }) + .unwrap(); + } + + core.handle_irq(); + + assert!(core.counters().rx_queue_dropped > 0); + } +} diff --git a/drivers/interface/rdif-serial/src/lib.rs b/drivers/interface/rdif-serial/src/lib.rs index 3b62b401fe..6389f8b6a2 100644 --- a/drivers/interface/rdif-serial/src/lib.rs +++ b/drivers/interface/rdif-serial/src/lib.rs @@ -2,52 +2,26 @@ extern crate alloc; -use alloc::boxed::Box; -use core::{ - any::Any, - fmt::{Debug, Display}, - num::NonZeroU32, -}; +use core::fmt::Display; use bitflags::bitflags; pub use rdif_base::{DriverGeneric, KError}; -pub type BIrqHandler = Box; -pub type BSender = Box; -pub type BReceiver = Box; -pub type BSerial = Box; +mod queue; +mod raw; +#[path = "core.rs"] +mod serial_core; +mod types; -impl DriverGeneric for Box { - fn name(&self) -> &str { - self.as_ref().name() - } - - fn raw_any(&self) -> Option<&dyn Any> { - self.as_ref().raw_any() - } - - fn raw_any_mut(&mut self) -> Option<&mut dyn Any> { - self.as_mut().raw_any_mut() - } -} - -mod serial; - -pub use serial::*; +pub use self::{queue::*, raw::*, serial_core::*, types::*}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigError { - /// 无效的波特率 InvalidBaudrate, - /// 不支持的数据位配置 UnsupportedDataBits, - /// 不支持的停止位配置 UnsupportedStopBits, - /// 不支持的奇偶校验配置 UnsupportedParity, - /// 寄存器访问错误 RegisterError, - /// 超时错误 Timeout, } @@ -81,7 +55,6 @@ pub enum TransferError { Closed, } -/// 数据位配置 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum DataBits { @@ -91,7 +64,6 @@ pub enum DataBits { Eight = 8, } -/// 停止位配置 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum StopBits { @@ -99,7 +71,6 @@ pub enum StopBits { Two = 2, } -/// 奇偶校验配置 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Parity { None, @@ -110,25 +81,42 @@ pub enum Parity { } bitflags! { - /// 中断状态标志 - #[derive(Debug, Clone, Copy)] - pub struct InterruptMask: u32 { - /// received data, including error data - const RX_AVAILABLE = 0x01; - const TX_EMPTY = 0x02; + /// Polling-only serial events for direct raw users such as someboot. + /// + /// Runtime `SerialCore` does not use this high-level snapshot type; it uses + /// `IrqSnapshot`, `RxSample`, and TX/RX software FIFOs instead. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct SerialEvent: u32 { + const RX_READY = 0x01; + const TX_READY = 0x02; + const RX_ERROR = 0x04; + const TX_ERROR = 0x08; + const OVERRUN = 0x10; + const MODEM_STATUS = 0x20; + const IRQ_ACK = 0x40; } } -impl InterruptMask { - pub fn rx_available(&self) -> bool { - self.contains(InterruptMask::RX_AVAILABLE) +impl SerialEvent { + pub fn rx_ready(&self) -> bool { + self.contains(Self::RX_READY) + } + + pub fn tx_ready(&self) -> bool { + self.contains(Self::TX_READY) } - pub fn tx_empty(&self) -> bool { - self.contains(InterruptMask::TX_EMPTY) + pub fn rx_error(&self) -> bool { + self.intersects(Self::RX_ERROR | Self::OVERRUN) } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SerialDirection { + Input, + Output, +} + #[derive(Debug, Clone, Default)] pub struct Config { pub baudrate: Option, @@ -163,230 +151,16 @@ impl Config { } } -pub trait InterfaceRaw: Send + Any + 'static { - type IrqHandler: TIrqHandler; - type Sender: TSender; - type Receiver: TReceiver; - - fn name(&self) -> &str; - - fn base_addr(&self) -> usize; - - // ==================== 配置管理 ==================== - fn set_config(&mut self, config: &Config) -> Result<(), ConfigError>; - - fn baudrate(&self) -> u32; - fn data_bits(&self) -> DataBits; - fn stop_bits(&self) -> StopBits; - fn parity(&self) -> Parity; - fn clock_freq(&self) -> Option; - - fn open(&mut self); - fn close(&mut self); - - // ==================== 回环控制 ==================== - /// 启用回环模式 - fn enable_loopback(&mut self); - /// 禁用回环模式 - fn disable_loopback(&mut self); - /// 检查回环模式是否启用 - fn is_loopback_enabled(&self) -> bool; - - // ==================== 中断管理 ==================== - /// 设置中断使能掩码 - fn set_irq_mask(&mut self, mask: InterruptMask); - /// 获取当前中断使能掩码 - fn get_irq_mask(&self) -> InterruptMask; - - fn irq_handler(&mut self) -> Option; - fn take_tx(&mut self) -> Option; - fn take_rx(&mut self) -> Option; - - fn set_tx(&mut self, tx: Self::Sender) -> Result<(), SetBackError>; - fn set_rx(&mut self, rx: Self::Receiver) -> Result<(), SetBackError>; -} - -#[derive(Clone, Copy)] -pub struct SetBackError { - want: usize, - actual: usize, -} - -impl SetBackError { - /// Create a new SetBackError - /// # Safety - pub fn new(want: usize, actual: usize) -> Self { - Self { - want: want as _, - actual: actual as _, - } - } -} - -impl core::error::Error for SetBackError {} - -impl Debug for SetBackError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "Failed to set back, base address not eq {:#x} != {:#x}", - self.want, self.actual - ) - } -} - -impl Display for SetBackError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - Debug::fmt(self, f) - } -} - -pub trait Interface: DriverGeneric { - fn irq_handler(&mut self) -> Option>; - fn take_tx(&mut self) -> Option>; - fn take_rx(&mut self) -> Option>; - /// Base address of the serial port - fn base_addr(&self) -> usize; - - fn set_config(&mut self, config: &Config) -> Result<(), ConfigError>; - - fn baudrate(&self) -> u32; - fn data_bits(&self) -> DataBits; - fn stop_bits(&self) -> StopBits; - fn parity(&self) -> Parity; - fn clock_freq(&self) -> Option; - - fn enable_loopback(&mut self); - fn disable_loopback(&mut self); - fn is_loopback_enabled(&self) -> bool; - - fn enable_interrupts(&mut self, mask: InterruptMask); - fn disable_interrupts(&mut self, mask: InterruptMask); - fn get_enabled_interrupts(&self) -> InterruptMask; -} - -pub trait TIrqHandler: Send + Sync + 'static { - fn clean_interrupt_status(&self) -> InterruptMask; -} - -pub trait TSender: Send + 'static { - fn write_byte(&mut self, byte: u8) -> bool; - - fn write_bytes(&mut self, bytes: &[u8]) -> usize { - let mut written = 0; - for &byte in bytes.iter() { - if !self.write_byte(byte) { - break; - } - written += 1; - } - written - } -} - -pub trait TReceiver: Send + 'static { - fn read_byte(&mut self) -> Option>; - - /// Recv data into buf, return recv bytes. If return bytes is less than buf.len(), it means no more data. - fn read_bytes(&mut self, bytes: &mut [u8]) -> Result { - let mut read_count = 0; - for byte in bytes.iter_mut() { - match self.read_byte() { - Some(Ok(b)) => { - *byte = b; - } - Some(Err(e)) => { - return Err(TransBytesError { - bytes_transferred: read_count, - kind: e, - }); - } - None => break, - } - - read_count += 1; - } - - Ok(read_count) - } -} - #[cfg(test)] mod tests { - use alloc::vec::Vec; - use super::*; - struct LimitedSender { - capacity: usize, - bytes: Vec, - } - - impl TSender for LimitedSender { - fn write_byte(&mut self, byte: u8) -> bool { - if self.bytes.len() == self.capacity { - return false; - } - self.bytes.push(byte); - true - } - } - - struct ScriptedReceiver { - next: Vec>>, - } - - impl TReceiver for ScriptedReceiver { - fn read_byte(&mut self) -> Option> { - if self.next.is_empty() { - None - } else { - self.next.remove(0) - } - } - } - #[test] - fn write_bytes_stops_on_full() { - let mut sender = LimitedSender { - capacity: 2, - bytes: Vec::new(), - }; - - let written = sender.write_bytes(&[0x11, 0x22, 0x33]); - - assert_eq!(written, 2); - assert_eq!(sender.bytes, [0x11, 0x22]); - } - - #[test] - fn read_bytes_stops_on_empty() { - let mut receiver = ScriptedReceiver { - next: Vec::from([Some(Ok(0x11)), Some(Ok(0x22)), None]), - }; - let mut buffer = [0; 4]; - - let read = receiver.read_bytes(&mut buffer).unwrap(); - - assert_eq!(read, 2); - assert_eq!(&buffer[..read], [0x11, 0x22]); - } - - #[test] - fn read_bytes_reports_partial_error() { - let mut receiver = ScriptedReceiver { - next: Vec::from([ - Some(Ok(0x11)), - Some(Ok(0x22)), - Some(Err(TransferError::Parity)), - ]), - }; - let mut buffer = [0; 4]; - - let err = receiver.read_bytes(&mut buffer).unwrap_err(); + fn serial_event_reports_readiness_and_errors() { + let event = SerialEvent::RX_READY | SerialEvent::OVERRUN; - assert_eq!(err.bytes_transferred, 2); - assert_eq!(err.kind, TransferError::Parity); - assert_eq!(&buffer[..err.bytes_transferred], [0x11, 0x22]); + assert!(event.rx_ready()); + assert!(!event.tx_ready()); + assert!(event.rx_error()); } } diff --git a/drivers/interface/rdif-serial/src/queue.rs b/drivers/interface/rdif-serial/src/queue.rs new file mode 100644 index 0000000000..5310d7c6e3 --- /dev/null +++ b/drivers/interface/rdif-serial/src/queue.rs @@ -0,0 +1,61 @@ +use alloc::collections::VecDeque; + +pub struct FixedQueue { + queue: VecDeque, +} + +impl FixedQueue { + pub fn new() -> Self { + assert!(CAP > 0); + Self { + queue: VecDeque::with_capacity(CAP), + } + } + + pub fn len(&self) -> usize { + self.queue.len() + } + + pub fn is_empty(&self) -> bool { + self.queue.is_empty() + } + + pub fn remaining(&self) -> usize { + CAP - self.queue.len() + } + + pub fn front(&self) -> Option<&T> { + self.queue.front() + } + + pub fn pop_front(&mut self) -> Option { + self.queue.pop_front() + } + + pub fn push_back(&mut self, value: T) -> Result<(), T> { + if self.queue.len() == CAP { + Err(value) + } else { + self.queue.push_back(value); + Ok(()) + } + } + + pub fn clear(&mut self) { + self.queue.clear(); + } +} + +impl Default for FixedQueue { + fn default() -> Self { + Self::new() + } +} + +impl FixedQueue { + pub fn push_slice(&mut self, bytes: &[u8]) -> usize { + let count = bytes.len().min(self.remaining()); + self.queue.extend(bytes[..count].iter().copied()); + count + } +} diff --git a/drivers/interface/rdif-serial/src/raw.rs b/drivers/interface/rdif-serial/src/raw.rs new file mode 100644 index 0000000000..861934003f --- /dev/null +++ b/drivers/interface/rdif-serial/src/raw.rs @@ -0,0 +1,96 @@ +use core::{any::Any, num::NonZeroU32}; + +use crate::{ + Config, ConfigError, InterruptMask, IrqSnapshot, RxFlag, RxSample, SerialEvent, TransferError, +}; + +/// 无锁 UART 寄存器接口。 +/// +/// # 并发契约 +/// +/// 所有方法都必须由外层端口锁串行化。实现不得自行引入 Mutex、 +/// SpinNoIrq、Arc、WaitQueue 或任务唤醒逻辑。 +pub trait RawUart: Send + Any + 'static { + fn name(&self) -> &'static str; + fn base_addr(&self) -> usize; + fn clock_freq(&self) -> Option; + + /// 初始化 FIFO、控制寄存器和线路参数。 + /// + /// 返回时所有设备 IRQ 应保持关闭。 + fn startup(&mut self, config: &Config) -> Result<(), ConfigError>; + + /// 关闭所有设备 IRQ 并停止端口。 + fn shutdown(&mut self); + + /// 调整 baud/data bits/parity/stop bits。 + /// + /// 调用方已经持有端口锁,并已临时屏蔽设备 IRQ。 + fn set_config(&mut self, config: &Config) -> Result<(), ConfigError>; + + fn baudrate(&self) -> u32; + fn data_bits(&self) -> crate::DataBits; + fn stop_bits(&self) -> crate::StopBits; + fn parity(&self) -> crate::Parity; + + fn enable_loopback(&mut self); + fn disable_loopback(&mut self); + fn is_loopback_enabled(&self) -> bool; + + /// 写设备侧 IRQ mask。它只管理设备中断,不是同步原语。 + fn set_irq_mask(&mut self, mask: InterruptMask); + + /// 读取并按硬件要求确认当前 IRQ source。 + fn take_irq_snapshot(&mut self) -> IrqSnapshot; + + /// 从 RX FIFO 读取一个 sample;FIFO 空时返回 None。 + fn read_rx(&mut self) -> Option; + + /// 硬件 TX FIFO 是否仍可接收一个字符。 + fn tx_ready(&mut self) -> bool; + + /// 将一个字节写入硬件 TX FIFO。 + /// + /// 调用前必须确认 tx_ready()。 + fn write_tx(&mut self, byte: u8); + + /// Read a raw hardware status snapshot. + /// + /// This is for polling users that directly own the raw UART, such as + /// someboot early console. Runtime `SerialCore` must not call this method. + fn poll_status(&mut self) -> SerialEvent; + + /// Direct polling helper for early console users. + fn write_byte(&mut self, byte: u8) { + self.write_tx(byte); + } + + /// Consume one byte/error according to caller-owned polling state. + fn read_byte(&mut self, status: SerialEvent) -> Option> { + if !status.rx_ready() && !status.rx_error() { + return None; + } + let sample = self.read_rx()?; + if sample.overrun { + return Some(Err(TransferError::Overrun(sample.byte.unwrap_or(0)))); + } + let byte = sample.byte?; + match sample.flag { + RxFlag::Normal => Some(Ok(byte)), + RxFlag::Break => Some(Err(TransferError::Break)), + RxFlag::Parity => Some(Err(TransferError::Parity)), + RxFlag::Framing => Some(Err(TransferError::Framing)), + } + } + + /// 一轮 IRQ 建议写入的最大字节数。 + fn tx_load_size(&self) -> usize { + 1 + } + + /// FIFO 和 shift register 是否都已空。 + fn tx_idle(&mut self) -> bool; + + fn ack_modem_status(&mut self) {} + fn ack_busy_detect(&mut self) {} +} diff --git a/drivers/interface/rdif-serial/src/serial.rs b/drivers/interface/rdif-serial/src/serial.rs deleted file mode 100644 index 8291b7fb9a..0000000000 --- a/drivers/interface/rdif-serial/src/serial.rs +++ /dev/null @@ -1,301 +0,0 @@ -use alloc::{boxed::Box, sync::Arc}; -use core::{cell::UnsafeCell, num::NonZeroU32}; - -use heapless::Deque; -use rdif_base::DriverGeneric; -use spin::Mutex; - -use super::{ - BIrqHandler, BReceiver, BSender, BSerial, InterfaceRaw, InterruptMask, TransBytesError, -}; -use crate::TransferError; - -pub struct SerialDyn { - inner: T, - tx: Arc>>, - - rx: Arc>>>, - irq_handler: Arc>>, - - rx_clone: Arc, -} - -impl SerialDyn { - fn _new(mut inner: T) -> Self { - let tx = inner.take_tx().unwrap(); - let tx: BSender = Box::new(tx); - let tx = Arc::new(Mutex::new(Some(tx))); - - let rx_inner = inner.take_rx().unwrap(); - let rx_inner = SRecv(UnsafeCell::new(ReceiverInner { - inner: Box::new(rx_inner), - fifo: RcvBuff(Deque::new()), - })); - let rx_inner = Arc::new(rx_inner); - let srcv = rx_inner.clone(); - let rx = Arc::new(Mutex::new(Some(rx_inner))); - - let irq_inner = inner.irq_handler().unwrap(); - - let irq_inner: BIrqHandler = Box::new(irq_inner); - let irq_handler = Arc::new(Mutex::new(Some(irq_inner))); - Self { - inner, - tx, - rx, - irq_handler, - rx_clone: srcv, - } - } - - pub fn new_boxed(inner: T) -> BSerial { - Box::new(Self::_new(inner)) as _ - } -} - -impl super::Interface for SerialDyn { - fn base_addr(&self) -> usize { - self.inner.base_addr() - } - - fn take_tx(&mut self) -> Option> { - let tx = self.tx.lock().take()?; - Some(Box::new(Sender { - c: self.tx.clone(), - inner: Some(tx), - })) - } - - fn take_rx(&mut self) -> Option> { - let rx = self.rx.lock().take()?; - Some(Box::new(Receiver { - c: self.rx.clone(), - inner: Some(rx), - })) - } - - fn irq_handler(&mut self) -> Option> { - let h = self.irq_handler.lock().take()?; - Some(Box::new(IrqHandler { - c: self.irq_handler.clone(), - inner: Some(h), - rcv: self.rx_clone.clone(), - })) - } - - fn set_config(&mut self, config: &crate::Config) -> Result<(), crate::ConfigError> { - self.inner.set_config(config) - } - - fn baudrate(&self) -> u32 { - self.inner.baudrate() - } - - fn data_bits(&self) -> crate::DataBits { - self.inner.data_bits() - } - - fn stop_bits(&self) -> crate::StopBits { - self.inner.stop_bits() - } - - fn parity(&self) -> crate::Parity { - self.inner.parity() - } - - fn clock_freq(&self) -> Option { - self.inner.clock_freq() - } - - fn enable_loopback(&mut self) { - self.inner.enable_loopback() - } - - fn disable_loopback(&mut self) { - self.inner.disable_loopback() - } - - fn is_loopback_enabled(&self) -> bool { - self.inner.is_loopback_enabled() - } - - fn enable_interrupts(&mut self, mask: InterruptMask) { - let mut val = self.inner.get_irq_mask(); - val |= mask; - self.inner.set_irq_mask(val); - } - - fn disable_interrupts(&mut self, mask: InterruptMask) { - let mut val = self.inner.get_irq_mask(); - val &= !mask; - self.inner.set_irq_mask(val); - } - - fn get_enabled_interrupts(&self) -> InterruptMask { - self.inner.get_irq_mask() - } -} - -impl DriverGeneric for SerialDyn { - fn name(&self) -> &str { - self.inner.name() - } - - fn raw_any(&self) -> Option<&dyn core::any::Any> { - Some(&self.inner) - } - - fn raw_any_mut(&mut self) -> Option<&mut dyn core::any::Any> { - Some(&mut self.inner) - } -} - -pub struct Sender { - c: Arc>>, - inner: Option, -} - -impl Drop for Sender { - fn drop(&mut self) { - let mut guard = self.c.lock(); - guard.replace(self.inner.take().unwrap()); - } -} - -impl super::TSender for Sender { - fn write_byte(&mut self, byte: u8) -> bool { - let s = self.inner.as_mut().unwrap(); - s.write_byte(byte) - } - - fn write_bytes(&mut self, bytes: &[u8]) -> usize { - let s = self.inner.as_mut().unwrap(); - s.write_bytes(bytes) - } -} - -struct ReceiverInner { - inner: BReceiver, - fifo: RcvBuff, -} - -pub struct Receiver { - c: Arc>>>, - inner: Option>, -} - -impl Receiver { - fn inner(&self) -> &SRecv { - self.inner.as_ref().unwrap() - } -} - -impl super::TReceiver for Receiver { - fn read_byte(&mut self) -> Option> { - if let Some(b) = self.inner().fifo_pop() { - return Some(b); - } - - self.inner().read_byte() - } - - fn read_bytes(&mut self, bytes: &mut [u8]) -> Result { - let recv = self.inner(); - let mut n = 0; - - // 先从 FIFO 读取尽可能多的数据 - while n < bytes.len() { - match recv.fifo_pop() { - Some(Ok(b)) => { - bytes[n] = b; - n += 1; - } - Some(Err(e)) => { - return Err(TransBytesError { - bytes_transferred: n, - kind: e, - }); - } - None => break, - } - } - - // 如果已经填满则返回 - if n == bytes.len() { - return Ok(n); - } - - // FIFO 没有更多数据时,再批量从底层读取 - match recv.read_bytes(&mut bytes[n..]) { - Ok(m) => Ok(n + m), - Err(e) => Err(TransBytesError { - bytes_transferred: n + e.bytes_transferred, - kind: e.kind, - }), - } - } -} - -impl Drop for Receiver { - fn drop(&mut self) { - let mut guard = self.c.lock(); - guard.replace(self.inner.take().unwrap()); - } -} - -pub struct IrqHandler { - c: Arc>>, - inner: Option, - rcv: Arc, -} - -impl super::TIrqHandler for IrqHandler { - fn clean_interrupt_status(&self) -> InterruptMask { - let h = self.inner.as_ref().unwrap(); - let status = h.clean_interrupt_status(); - if status.contains(InterruptMask::RX_AVAILABLE) { - while let Some(b) = self.rcv.read_byte() { - self.rcv.fifo_push(b); - } - } - - status - } -} - -impl Drop for IrqHandler { - fn drop(&mut self) { - let mut guard = self.c.lock(); - guard.replace(self.inner.take().unwrap()); - } -} - -#[repr(align(64))] -struct RcvBuff(Deque, 64>); - -struct SRecv(UnsafeCell); - -unsafe impl Send for SRecv {} -unsafe impl Sync for SRecv {} - -impl SRecv { - fn fifo_push(&self, byte: Result) { - let inner = unsafe { &mut *self.0.get() }; - let _ = inner.fifo.0.push_back(byte); - } - - fn fifo_pop(&self) -> Option> { - let inner = unsafe { &mut *self.0.get() }; - inner.fifo.0.pop_front() - } - - fn read_byte(&self) -> Option> { - let inner = unsafe { &mut *self.0.get() }; - inner.inner.read_byte() - } - - fn read_bytes(&self, bytes: &mut [u8]) -> Result { - let inner = unsafe { &mut *self.0.get() }; - inner.inner.read_bytes(bytes) - } -} diff --git a/drivers/interface/rdif-serial/src/types.rs b/drivers/interface/rdif-serial/src/types.rs new file mode 100644 index 0000000000..ea125ef936 --- /dev/null +++ b/drivers/interface/rdif-serial/src/types.rs @@ -0,0 +1,98 @@ +use bitflags::bitflags; + +bitflags! { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct InterruptMask: u32 { + const RX_DATA = 1 << 0; + const RX_STATUS = 1 << 1; + const TX_SPACE = 1 << 2; + const MODEM_STATUS = 1 << 3; + + const RX = Self::RX_DATA.bits() | Self::RX_STATUS.bits(); + const RX_AVAILABLE = Self::RX.bits(); + const TX_EMPTY = Self::TX_SPACE.bits(); + } +} + +impl InterruptMask { + pub fn rx_available(&self) -> bool { + self.intersects(Self::RX) + } + + pub fn tx_empty(&self) -> bool { + self.contains(Self::TX_SPACE) + } +} + +bitflags! { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct IrqSource: u32 { + const RX_DATA = 1 << 0; + const RX_TIMEOUT = 1 << 1; + const RX_STATUS = 1 << 2; + const TX_SPACE = 1 << 3; + const MODEM_STATUS = 1 << 4; + const BUSY_DETECT = 1 << 5; + const OTHER_ACK = 1 << 6; + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct IrqSnapshot { + pub claimed: bool, + pub sources: IrqSource, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum RxFlag { + #[default] + Normal, + Break, + Parity, + Framing, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RxSample { + pub byte: Option, + pub flag: RxFlag, + pub overrun: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RxItem { + Byte { byte: u8, flag: RxFlag }, + Overrun, +} + +impl Default for RxItem { + fn default() -> Self { + Self::Byte { + byte: 0, + flag: RxFlag::Normal, + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SerialCounters { + pub irq_total: u64, + pub irq_spurious: u64, + pub irq_budget_exhausted: u64, + pub rx_bytes: u64, + pub rx_fifo_overruns: u64, + pub rx_queue_dropped: u64, + pub rx_breaks: u64, + pub rx_parity_errors: u64, + pub rx_framing_errors: u64, + pub tx_bytes: u64, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SerialIrqOutcome { + pub claimed: bool, + pub rx_pushed: usize, + pub tx_sent: usize, + pub tx_wakeup: bool, + pub budget_exhausted: bool, +} diff --git a/drivers/rdrive/src/driver/mod.rs b/drivers/rdrive/src/driver/mod.rs index eea18f7a8e..a6027cfab1 100644 --- a/drivers/rdrive/src/driver/mod.rs +++ b/drivers/rdrive/src/driver/mod.rs @@ -20,6 +20,10 @@ impl PlatformDevice { Self { descriptor } } + pub fn descriptor(&self) -> &Descriptor { + &self.descriptor + } + /// Register a device to the driver manager. /// /// # Panics diff --git a/drivers/rdrive/src/lib.rs b/drivers/rdrive/src/lib.rs index 9e6bf95add..d2793d3471 100644 --- a/drivers/rdrive/src/lib.rs +++ b/drivers/rdrive/src/lib.rs @@ -193,6 +193,28 @@ pub fn fdt_phandle_to_device_id(phandle: Phandle) -> Option { probe::fdt::try_system().and_then(|system| system.phandle_to_device_id(phandle)) } +pub fn fdt_path_to_device_id(path: &str) -> Option { + probe::fdt::try_system().and_then(|system| system.path_to_device_id(path)) +} + +pub fn note_fdt_device_path(path: &str, device_id: DeviceId) -> bool { + probe::fdt::try_system().is_some_and(|system| system.note_device_path(path, device_id)) +} + +pub fn acpi_path_to_device_id(path: &str) -> Option { + probe::acpi::try_system().and_then(|system| system.path_to_device_id(path)) +} + +pub fn acpi_resource_address_to_device_id( + address: probe::acpi::AcpiResourceAddress, +) -> Option { + probe::acpi::try_system().and_then(|system| system.resource_address_to_device_id(address)) +} + +pub fn acpi_spcr_console_device_id() -> Option { + probe::acpi::spcr_console_device_id() +} + pub fn with_fdt(f: impl FnOnce(&Fdt) -> T) -> Option { probe::fdt::try_system().map(|system| f(system.fdt())) } diff --git a/drivers/rdrive/src/probe/acpi.rs b/drivers/rdrive/src/probe/acpi.rs index af5cbff6d3..fa0aae4d6a 100644 --- a/drivers/rdrive/src/probe/acpi.rs +++ b/drivers/rdrive/src/probe/acpi.rs @@ -9,6 +9,7 @@ use core::{ptr::NonNull, str::FromStr}; use acpi::{ AcpiError, AcpiTables, Handler, PhysicalMapping, + address::{AddressSpace, GenericAddress}, aml::{ AmlError, Interpreter, namespace::{AmlName, NamespaceLevelKind}, @@ -25,6 +26,7 @@ use acpi::{ interrupt::{InterruptModel, Polarity, TriggerMode}, pci::PciConfigRegions, }, + sdt::spcr::{Spcr, SpcrInterfaceType}, }; pub use rdif_base::irq::{AcpiGsiController, AcpiGsiRoute, AcpiIrqPolarity, AcpiIrqTrigger}; use spin::{Mutex, Once}; @@ -386,6 +388,8 @@ mod tests { handler, pci: None, probed_names: spin::Mutex::new(alloc::collections::BTreeSet::new()), + populated_paths: spin::Mutex::new(alloc::collections::BTreeMap::new()), + populated_resources: spin::Mutex::new(alloc::collections::BTreeMap::new()), } } @@ -745,6 +749,42 @@ struct AcpiDeviceInfo { irq_routes: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum AcpiResourceAddressSpace { + Memory, + Io, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct AcpiResourceAddress { + pub space: AcpiResourceAddressSpace, + pub base: u64, +} + +impl AcpiResourceAddress { + pub const fn memory(base: u64) -> Self { + Self { + space: AcpiResourceAddressSpace::Memory, + base, + } + } + + pub const fn io(base: u64) -> Self { + Self { + space: AcpiResourceAddressSpace::Io, + base, + } + } + + pub fn from_generic_address(address: GenericAddress) -> Option { + match address.address_space { + AddressSpace::SystemMemory => Some(Self::memory(address.address)), + AddressSpace::SystemIo => Some(Self::io(address.address)), + _ => None, + } + } +} + pub struct AcpiInfo<'a> { pub root: &'a System, pub path: &'a str, @@ -851,6 +891,10 @@ pub fn with_acpi(f: impl FnOnce(&System) -> T) -> Option { try_system().map(f) } +pub fn spcr_console_device_id() -> Option { + try_system().and_then(System::spcr_console_device_id) +} + fn acpi_error(err: AcpiError) -> DriverError { DriverError::Unknown(format!("{err:?}")) } @@ -1070,6 +1114,8 @@ pub struct System { handler: AcpiHandler, pci: Option, probed_names: Mutex>, + populated_paths: Mutex>, + populated_resources: Mutex>, } unsafe impl Send for System {} @@ -1114,6 +1160,8 @@ impl System { handler: namespace_handler, pci, probed_names: Mutex::new(BTreeSet::new()), + populated_paths: Mutex::new(BTreeMap::new()), + populated_resources: Mutex::new(BTreeMap::new()), }) } @@ -1125,6 +1173,27 @@ impl System { &self.routing } + pub fn path_to_device_id(&self, path: &str) -> Option { + self.populated_paths.lock().get(path).copied() + } + + pub fn resource_address_to_device_id(&self, address: AcpiResourceAddress) -> Option { + self.populated_resources.lock().get(&address).copied() + } + + pub fn spcr_console_device_id(&self) -> Option { + let tables = unsafe { AcpiTables::from_rsdp(self.handler.clone(), self.handler.root.rsdp) } + .map_err(acpi_error) + .ok()?; + tables + .find_tables::() + .filter(|spcr| is_supported_spcr_interface(spcr.interface_type())) + .find_map(|spcr| { + spcr_namespace_device_id(self, &spcr) + .or_else(|| spcr_resource_device_id(self, &spcr)) + }) + } + pub fn pci_irq_for_endpoint( &self, info: PciInfo, @@ -1290,6 +1359,7 @@ impl System { device_id: DeviceId::new(), irq_parent: None, }; + let device_id = desc.device_id(); let info = AcpiInfo { root: self, path: &device.path, @@ -1299,6 +1369,7 @@ impl System { let res = on_probe(ProbeAcpi::new(info, PlatformDevice::new(desc))); if res.is_ok() { self.probed_names.lock().insert(register.name); + self.note_populated_device(device_id, &device); } out.push(res); } @@ -1324,6 +1395,46 @@ impl System { } Ok(out) } + + fn note_populated_device(&self, device_id: DeviceId, device: &AcpiDeviceInfo) { + self.populated_paths + .lock() + .insert(device.path.clone(), device_id); + + let mut resources = self.populated_resources.lock(); + for range in &device.memory_ranges { + resources.insert(AcpiResourceAddress::memory(range.base), device_id); + } + for range in &device.io_ranges { + resources.insert(AcpiResourceAddress::io(range.base), device_id); + } + } +} + +fn is_supported_spcr_interface(interface: SpcrInterfaceType) -> bool { + matches!( + interface, + SpcrInterfaceType::Full16550 + | SpcrInterfaceType::Full16450 + | SpcrInterfaceType::Generic16550 + | SpcrInterfaceType::ArmPL011 + | SpcrInterfaceType::ArmSBSAGeneric32bit + | SpcrInterfaceType::ArmSBSAGeneric + ) +} + +fn spcr_namespace_device_id(system: &System, spcr: &Spcr) -> Option { + let namespace = spcr.namespace_string().ok()?.trim_end_matches('\0'); + if namespace.is_empty() || namespace == "." { + return None; + } + system.path_to_device_id(namespace) +} + +fn spcr_resource_device_id(system: &System, spcr: &Spcr) -> Option { + let address = spcr.base_address()?.ok()?; + let address = AcpiResourceAddress::from_generic_address(address)?; + system.resource_address_to_device_id(address) } fn read_pci_ecam_regions( diff --git a/drivers/rdrive/src/probe/fdt/mod.rs b/drivers/rdrive/src/probe/fdt/mod.rs index 0049790b6b..32f25be459 100644 --- a/drivers/rdrive/src/probe/fdt/mod.rs +++ b/drivers/rdrive/src/probe/fdt/mod.rs @@ -1,10 +1,11 @@ use alloc::{ - collections::{BTreeMap, btree_set::BTreeSet}, + collections::{BTreeMap, btree_map::Entry, btree_set::BTreeSet}, + string::String, vec::Vec, }; use core::ptr::NonNull; -pub use fdt_edit::{ClockRef, Fdt, InterruptRef, NodeType, Phandle, RegInfo, Status}; +pub use fdt_edit::{ClockRef, Fdt, InterruptRef, NodeId, NodeType, Phandle, RegInfo, Status}; use spin::{Mutex, Once}; use super::ProbeError; @@ -108,7 +109,8 @@ pub type FnOnProbe = for<'a> fn(ProbeFdt<'a>) -> Result<(), OnProbeError>; pub struct System { fdt: Fdt, phandle_2_device_id: BTreeMap, - probed_names: Mutex>, + populated_paths: Mutex>, + populated_nodes: Mutex>, } unsafe impl Send for System {} @@ -122,6 +124,23 @@ impl System { self.phandle_2_device_id.get(&phandle).copied() } + pub fn path_to_device_id(&self, path: &str) -> Option { + self.populated_paths.lock().get(path).copied() + } + + pub fn note_device_path(&self, path: &str, device_id: DeviceId) -> bool { + if self.fdt.get_by_path(path).is_none() { + return false; + } + match self.populated_paths.lock().entry(String::from(path)) { + Entry::Vacant(entry) => { + entry.insert(device_id); + true + } + Entry::Occupied(entry) => *entry.get() == device_id, + } + } + pub fn get_by_phandle(&self, phandle: Phandle) -> Option> { self.fdt.get_by_phandle(phandle) } @@ -142,7 +161,8 @@ impl System { Ok(Self { fdt, phandle_2_device_id, - probed_names: Mutex::new(BTreeSet::new()), + populated_paths: Mutex::new(BTreeMap::new()), + populated_nodes: Mutex::new(BTreeSet::new()), }) } @@ -156,6 +176,7 @@ impl System { fn get_fdt_match_nodes<'a>(&'a self, register: &DriverRegister) -> Vec> { let mut out = Vec::new(); + let mut matched_nodes = BTreeSet::new(); for node in self.fdt.all_nodes() { if matches!(node.as_node().status(), Some(Status::Disabled)) { continue; @@ -173,7 +194,7 @@ impl System { }; for compatible in &node_compatibles { - if compatibles.contains(compatible) { + if compatibles.contains(compatible) && matched_nodes.insert(node.id()) { out.push(ProbeFdtInfo { name: register.name, node, @@ -193,7 +214,8 @@ impl System { let node_ls = self.get_fdt_match_nodes(register); let mut out = Vec::new(); for node_info in node_ls { - if self.probed_names.lock().contains(node_info.name) { + let node_id = node_info.node.id(); + if self.populated_nodes.lock().contains(&node_id) { continue; } let node = node_info.node; @@ -224,7 +246,8 @@ impl System { )); if res.is_ok() { - self.probed_names.lock().insert(node_info.name); + self.populated_paths.lock().insert(node.path(), id); + self.populated_nodes.lock().insert(node_id); } out.push(res); diff --git a/drivers/rdrive/tests/fdt_probe.rs b/drivers/rdrive/tests/fdt_probe.rs new file mode 100644 index 0000000000..9b7f69607b --- /dev/null +++ b/drivers/rdrive/tests/fdt_probe.rs @@ -0,0 +1,149 @@ +use core::{ + ptr::NonNull, + sync::atomic::{AtomicUsize, Ordering}, +}; + +use fdt_edit::{Fdt, Node, Property}; +use rdrive::{ + DriverGeneric, Platform, PlatformDevice, get_list, + probe::{OnProbeError, fdt::ProbeFdt}, + probe_all, + register::{DriverRegister, ProbeKind, ProbeLevel, ProbePriority}, +}; + +static FAIL_PROBE_COUNT: AtomicUsize = AtomicUsize::new(0); +static PROBE_COUNT: AtomicUsize = AtomicUsize::new(0); + +struct FdtSerialDevice; + +impl DriverGeneric for FdtSerialDevice { + fn name(&self) -> &str { + "FdtSerialDevice" + } +} + +fn string_property(name: &str, value: &str) -> Property { + let mut data = value.as_bytes().to_vec(); + data.push(0); + Property::new(name, data) +} + +fn string_list_property(name: &str, values: &[&str]) -> Property { + let mut data = Vec::new(); + for value in values { + data.extend_from_slice(value.as_bytes()); + data.push(0); + } + Property::new(name, data) +} + +fn serial_node(name: &str, enabled: bool) -> Node { + let mut node = Node::new(name); + node.set_property(string_list_property( + "compatible", + &["test,uart", "ns16550a"], + )); + if !enabled { + node.set_property(string_property("status", "disabled")); + } + node +} + +fn probe_serial(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { + PROBE_COUNT.fetch_add(1, Ordering::SeqCst); + let dev: PlatformDevice = probe.into_platform_device(); + dev.register(FdtSerialDevice); + Ok(()) +} + +fn probe_serial_not_match(_probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { + FAIL_PROBE_COUNT.fetch_add(1, Ordering::SeqCst); + Err(OnProbeError::NotMatch) +} + +static FDT_SERIAL_NOT_MATCH_REGISTER: DriverRegister = DriverRegister { + name: "fdt serial negative test driver", + level: ProbeLevel::PostKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ProbeKind::Fdt { + compatibles: &["test,uart", "ns16550a"], + on_probe: probe_serial_not_match, + }], +}; + +static FDT_SERIAL_REGISTER: DriverRegister = DriverRegister { + name: "fdt serial test driver", + level: ProbeLevel::PostKernel, + priority: ProbePriority::DEFAULT, + probe_kinds: &[ProbeKind::Fdt { + compatibles: &["test,uart", "ns16550a"], + on_probe: probe_serial, + }], +}; + +#[test] +fn fdt_probe_populates_each_enabled_matching_node_once() { + let mut fdt = Fdt::new(); + let root = fdt.root_id(); + let aliases = fdt.add_node(root, Node::new("aliases")); + fdt.node_mut(aliases) + .unwrap() + .set_property(string_property("serial0", "/serial@1000")); + let chosen = fdt.add_node(root, Node::new("chosen")); + fdt.node_mut(chosen) + .unwrap() + .set_property(string_property("stdout-path", "serial0:115200n8")); + fdt.add_node(root, serial_node("serial@1000", true)); + fdt.add_node(root, serial_node("serial@2000", true)); + fdt.add_node(root, serial_node("serial@3000", false)); + + let encoded = fdt.encode(); + let dtb = Box::leak(encoded.as_ref().to_vec().into_boxed_slice()); + rdrive::init(Platform::Fdt { + addr: NonNull::new(dtb.as_mut_ptr()).unwrap(), + }) + .expect("FDT platform should initialize"); + rdrive::register_add(FDT_SERIAL_NOT_MATCH_REGISTER.clone()); + rdrive::register_add(FDT_SERIAL_REGISTER.clone()); + + probe_all(true).expect("FDT probe should succeed"); + + assert_eq!(FAIL_PROBE_COUNT.load(Ordering::SeqCst), 2); + assert_eq!(PROBE_COUNT.load(Ordering::SeqCst), 2); + assert_eq!(get_list::().len(), 2); + assert!(rdrive::fdt_path_to_device_id("/serial@1000").is_some()); + assert!(rdrive::fdt_path_to_device_id("/serial@2000").is_some()); + assert!(rdrive::fdt_path_to_device_id("/serial@3000").is_none()); + let serial0_device = + rdrive::fdt_path_to_device_id("/serial@1000").expect("enabled serial probed"); + assert!(rdrive::note_fdt_device_path("/serial@3000", serial0_device)); + assert_eq!( + rdrive::fdt_path_to_device_id("/serial@3000"), + Some(serial0_device) + ); + assert!(!rdrive::note_fdt_device_path("/missing@0", serial0_device)); + + let stdout_path = rdrive::with_fdt(|fdt| { + fdt.get_by_path("/chosen") + .and_then(|chosen| { + chosen + .as_node() + .get_property("stdout-path") + .and_then(|prop| prop.as_str()) + }) + .and_then(|stdout| stdout.split(':').next()) + .and_then(|alias| { + fdt.get_by_path("/aliases").and_then(|aliases| { + aliases + .as_node() + .get_property(alias) + .and_then(|prop| prop.as_str()) + }) + }) + .map(str::to_owned) + }) + .flatten() + .expect("stdout-path should resolve through aliases"); + + assert!(rdrive::fdt_path_to_device_id(&stdout_path).is_some()); +} diff --git a/drivers/serial/some-serial/README.md b/drivers/serial/some-serial/README.md index 350133545a..24d3e75328 100644 --- a/drivers/serial/some-serial/README.md +++ b/drivers/serial/some-serial/README.md @@ -65,63 +65,47 @@ some-serial = "0.1.0" ``` -### 通用接口使用 +### Raw 单对象 polling 使用 -所有驱动都实现了统一的 `Serial` trait,提供一致的使用体验: +raw concrete 驱动直接持有寄存器和状态;读、写、poll、IRQ sync 都通过同一个对象完成。 +someboot 等 allocator 初始化前路径直接保存这个对象,不需要 `Box`、rdif trait object 或内部锁。 ```rust use core::ptr::NonNull; -use some_serial::{Serial, Config}; - -// 根据平台选择合适的驱动 -#[cfg(target_arch = "aarch64")] -use some_serial::pl011::Pl011; - -#[cfg(not(target_arch = "aarch64"))] -use some_serial::ns16550::Ns16550Mmio; - -// 创建串口实例 -let base_addr = 0x9000000 as *mut u8; // 你的 UART 基地址 -let clock_freq = match target_arch { - "aarch64" => 24_000_000, // ARM PL011: 24MHz - _ => 1_843_200, // NS16550: 1.8432MHz +use some_serial::{ + ns16550::Ns16550, Config, DataBits, InterfaceRaw as _, Parity, SerialDirection, StopBits, }; -let mut uart = match target_arch { - "aarch64" => Pl011::new( - NonNull::new(base_addr).unwrap(), - clock_freq - ), - _ => Ns16550Mmio::new( - NonNull::new(base_addr).unwrap(), - clock_freq - ), -}; +let base_addr = NonNull::new(0x9000000 as *mut u8).unwrap(); +let mut uart = Ns16550::new_mmio(base_addr, 1_843_200, 1); -// 统一配置接口 let config = Config::new() .baudrate(115200) - .data_bits(some_serial::DataBits::Eight) - .stop_bits(some_serial::StopBits::One) - .parity(some_serial::Parity::None); + .data_bits(DataBits::Eight) + .stop_bits(StopBits::One) + .parity(Parity::None); uart.set_config(&config).expect("Failed to configure UART"); -uart.open().expect("Failed to open UART"); - -// 启用回环模式进行测试(如果支持) +uart.open(); uart.enable_loopback(); -// 获取 TX/RX 接口进行数据传输 -let mut tx = uart.take_tx().unwrap(); -let mut rx = uart.take_rx().unwrap(); - -// 发送和接收数据 let test_data = b"Hello, Serial!"; -let sent = tx.send(test_data); +let mut sent = 0; +while sent < test_data.len() { + let n = uart.try_write(&test_data[sent..]); + if n == 0 { + core::hint::spin_loop(); + } + sent += n; +} println!("Sent {} bytes", sent); let mut buffer = [0u8; 64]; -let received = rx.receive(&mut buffer).expect("Failed to receive"); +let received = if uart.pending(SerialDirection::Input) { + uart.try_read(&mut buffer).expect("Failed to receive") +} else { + 0 +}; println!("Received {} bytes: {:?}", received, &buffer[..received]); ``` @@ -130,48 +114,23 @@ println!("Received {} bytes: {:?}", received, &buffer[..received]); 根据硬件平台和访问方式选择合适的驱动: ```rust -// ARM 平台 - 使用 PL011 -#[cfg(target_arch = "aarch64")] -use some_serial::pl011::Pl011; +use core::ptr::NonNull; -// x86_64 平台 - 使用端口 I/O #[cfg(target_arch = "x86_64")] -use some_serial::ns16550::Ns16550Pio; +let mut uart = some_serial::ns16550::Ns16550::new_port(0x3f8, 1_843_200); -// 其他嵌入式平台 - 使用内存映射 I/O -#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] -use some_serial::ns16550::Ns16550Mmio; - -// 平台特定的创建函数 -fn create_uart_for_platform(base_addr: *mut u8, clock_freq: u32) -> Box { - #[cfg(target_arch = "aarch64")] - { - Box::new(Pl011::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) - } - - #[cfg(target_arch = "x86_64")] - { - Box::new(Ns16550Pio::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) - } - - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - { - Box::new(Ns16550Mmio::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) - } -} +#[cfg(target_arch = "aarch64")] +let mut uart = some_serial::pl011::Pl011::new( + NonNull::new(0x9000000 as *mut u8).unwrap(), + 24_000_000, +); -// 统一的创建和使用方式 -let mut uart = create_uart_for_platform(base_addr, clock_freq); -// ... 后续使用方式完全相同 +#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] +let mut uart = some_serial::ns16550::Ns16550::new_mmio( + NonNull::new(0x40000000 as *mut u8).unwrap(), + 16_000_000, + 1, +); ``` ### 高级功能 @@ -179,77 +138,60 @@ let mut uart = create_uart_for_platform(base_addr, clock_freq); #### 中断驱动通信 ```rust -use some_serial::{Serial, InterruptMask}; +use some_serial::{InterfaceRaw as _, InterruptMask}; use some_serial::pl011::Pl011; // 创建并配置 UART let mut uart = Pl011::new(base_addr, clock_freq); uart.set_config(&config).unwrap(); -uart.open().unwrap(); +uart.open(); // 启用中断 -uart.enable_interrupts(InterruptMask::RX_AVAILABLE | InterruptMask::TX_EMPTY); +uart.set_irq_mask(InterruptMask::RX_AVAILABLE | InterruptMask::TX_EMPTY); -// 注册中断处理程序 -let irq_handler = uart.irq_handler().unwrap(); -// 在你的中断控制器中注册 irq_handler... +// 在中断控制器回调中同步硬件 IRQ 状态 +let event = uart.handle_irq(); +if event.rx_ready() { + // 运行时决定唤醒任务或继续轮询 +} -// 现在可以在中断处理中高效处理数据传输 +// 数据搬运仍由任务态通过 try_read/try_write 推进 ``` #### 平台检测与适配 +需要运行时动态分发的 rdrive/Starry 路径可以把 concrete 设备包装成 `rdif_serial::BSerial`。 +这个对象只负责控制和 split/restore;拆出的 TX/RX/IRQ runtime parts 各自持有可复制寄存器入口, +并通过共享原子状态同步 IRQ event 和 read-clear 错误位,不在 rdif adapter 内使用 Mutex。 + ```rust -// 运行时平台检测示例 -fn create_serial_for_platform(base_addr: *mut u8, clock_freq: u32) -> Box { - #[cfg(target_arch = "aarch64")] - { - // ARM64 平台,使用 PL011 - Box::new(Pl011::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) - } +use core::ptr::NonNull; +use rdif_serial::{BSerial, Interface as _, TTxQueue as _}; - #[cfg(target_arch = "x86_64")] - { - // x86_64 平台,使用 NS16550 端口 I/O - Box::new(Ns16550Pio::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) - } +fn create_serial_for_runtime(base_addr: NonNull, clock_freq: u32) -> BSerial { + some_serial::ns16550::Ns16550::new_mmio_boxed(base_addr, clock_freq, 1) +} - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - { - // 其他嵌入式平台,使用 NS16550 内存映射 I/O - Box::new(Ns16550Mmio::new( - NonNull::new(base_addr).unwrap(), - clock_freq - )) +let mut serial = create_serial_for_runtime( + NonNull::new(0x40000000 as *mut u8).unwrap(), + 16_000_000, +); + +let mut tx = serial.take_tx().expect("missing TX queue"); +let mut sent = 0; +let bytes = b"runtime serial\n"; +while sent < bytes.len() { + let n = tx.try_write(&bytes[sent..]); + if n == 0 { + core::hint::spin_loop(); } + sent += n; } +``` -// 系统集成示例 -fn init_system_uart() -> Result, &'static str> { - let (base_addr, clock_freq) = get_platform_uart_config()?; - - let mut uart = create_serial_for_platform(base_addr, clock_freq); - - // 标准配置 - let config = Config::new() - .baudrate(115200) - .data_bits(DataBits::Eight) - .stop_bits(StopBits::One) - .parity(Parity::None); - - uart.set_config(&config).map_err(|_| "Failed to configure UART")?; - uart.open().map_err(|_| "Failed to open UART")?; - - Ok(uart) -} +#### 平台特定配置获取 -// 平台特定配置获取 +```rust fn get_platform_uart_config() -> Result<(*mut u8, u32), &'static str> { #[cfg(target_arch = "aarch64")] { @@ -288,21 +230,15 @@ let config = Config::new() ### 状态查询 ```rust -// 查询线路状态 -let status = uart.line_status(); -if status.contains(some_serial::LineStatus::DATA_READY) { - // 有数据可读 -} - -if status.contains(some_serial::LineStatus::TX_HOLDING_EMPTY) { - // 可以发送数据 -} - -// 查询当前配置 +// 查询当前控制配置 let current_baudrate = uart.baudrate(); let data_bits = uart.data_bits(); let stop_bits = uart.stop_bits(); let parity = uart.parity(); + +// 查询 I/O 就绪事件 +let event = uart.poll(); +let can_write = uart.pending(some_serial::SerialDirection::Output); ``` ## 测试 @@ -353,7 +289,7 @@ cargo test --test test -- --show-output --uboot ### 添加新驱动支持 1. **创建驱动模块**:在 `src/` 目录下创建新的驱动文件 -2. **实现 Serial trait**:确保实现统一的 `rdif-serial` 接口 +2. **实现 raw 接口**:驱动对象实现 `InterfaceRaw` 的配置、IRQ mask、`pending`、`poll`、`try_write`、`try_read`、`handle_irq` 3. **添加测试**:为新驱动编写完整的测试套件 4. **更新文档**:在 README 中添加驱动说明和使用示例 5. **提交 PR**:详细描述新驱动的功能和使用方法 @@ -365,15 +301,12 @@ cargo test --test test -- --show-output --uboot ```rust // 新驱动的基本结构示例 pub struct NewDriver { - // 驱动特定的状态 -} - -impl Serial for NewDriver { - // 实现 Serial trait 的所有方法 + // 驱动寄存器句柄、时钟、saved status、IRQ mask shadow 等状态 } -impl NewDriver { - // 驱动特定的初始化和配置方法 +impl InterfaceRaw for NewDriver { + // 实现配置、开关、IRQ mask + // 实现 pending/poll/try_write/try_read/handle_irq } ``` diff --git a/drivers/serial/some-serial/src/lib.rs b/drivers/serial/some-serial/src/lib.rs index e7924a5825..fdeb067cf1 100644 --- a/drivers/serial/some-serial/src/lib.rs +++ b/drivers/serial/some-serial/src/lib.rs @@ -8,7 +8,7 @@ //! //! ## 特性 //! -//! - 🏗️ 统一抽象接口 - 基于 `rdif-serial` 的统一串口抽象 +//! - 🏗️ 统一抽象接口 - raw 层只提供 UART 寄存器语义,运行期队列由 rdif/OS 层提供 //! - 🛡️ 无标准库设计 (`no_std`) - 适用于裸机和嵌入式系统 //! - 📦 模块化架构 - 每个驱动独立模块,按需选择 //! - 🔒 类型安全 - 使用 Rust 类型系统确保内存安全 @@ -27,16 +27,17 @@ //! //! ## 快速开始 //! -//! ```rust -//! use some_serial::pl011::Pl011; // ARM PL011 -//! use some_serial::{Config, Serial, ns16550::Ns16550Mmio}; // NS16550 MMIO +//! ```rust,no_run +//! use core::ptr::NonNull; +//! +//! use some_serial::{Config, RawUart as _, ns16550::Ns16550}; //! //! // 选择合适的驱动 //! #[cfg(target_arch = "aarch64")] //! let mut uart = Pl011::new(NonNull::new(0x9000000 as *mut u8).unwrap(), 24_000_000); //! //! #[cfg(not(target_arch = "aarch64"))] -//! let mut uart = Ns16550Mmio::new(NonNull::new(0x9000000 as *mut u8).unwrap(), 1_843_200); +//! let mut uart = Ns16550::new_mmio(NonNull::new(0x9000000 as *mut u8).unwrap(), 1_843_200, 1); //! //! // 配置串口 //! let config = Config::new() @@ -46,93 +47,19 @@ //! .parity(some_serial::Parity::None); //! //! uart.set_config(&config).unwrap(); -//! uart.open().unwrap(); +//! uart.open(); +//! +//! while !uart.tx_ready() { +//! core::hint::spin_loop(); +//! } +//! uart.write_tx(b'h'); //! ``` +#[cfg(test)] +extern crate std; + pub mod ns16550; pub mod pl011; -use enum_dispatch::enum_dispatch; // 重新导出 rdif-serial 的所有类型 pub use rdif_serial::*; - -#[enum_dispatch] -pub enum Sender { - #[cfg(target_arch = "x86_64")] - Ns16550Sender(ns16550::Ns16550Sender), - Ns16550MmioSender(ns16550::Ns16550Sender), - Ns16550DwApbSender(ns16550::Ns16550Sender), - Ns16550RockchipFiqSender(ns16550::rockchip_fiq::RockchipFiqSender), - Pl011Sender(pl011::Pl011Sender), -} - -#[enum_dispatch(Sender)] -trait RawSender { - fn write_byte(&mut self, byte: u8) -> bool; - fn write_bytes(&mut self, buffer: &[u8]) -> usize { - let mut written = 0; - for &byte in buffer.iter() { - if !self.write_byte(byte) { - break; - } - written += 1; - } - written - } -} - -impl TSender for Sender { - fn write_byte(&mut self, byte: u8) -> bool { - RawSender::write_byte(self, byte) - } - - fn write_bytes(&mut self, buffer: &[u8]) -> usize { - RawSender::write_bytes(self, buffer) - } -} - -#[enum_dispatch] -pub enum Receiver { - #[cfg(target_arch = "x86_64")] - Ns16550Receiver(ns16550::Ns16550Receiver), - Ns16550MmioReceiver(ns16550::Ns16550Receiver), - Ns16550DwApbReceiver(ns16550::Ns16550Receiver), - Ns16550RockchipFiqReceiver(ns16550::rockchip_fiq::RockchipFiqReceiver), - Pl011Receiver(pl011::Pl011Receiver), -} - -impl TReceiver for Receiver { - fn read_byte(&mut self) -> Option> { - RawReceiver::read_byte(self) - } - - fn read_bytes(&mut self, bytes: &mut [u8]) -> Result { - RawReceiver::read_bytes(self, bytes) - } -} - -#[enum_dispatch(Receiver)] -trait RawReceiver { - fn read_byte(&mut self) -> Option>; - - fn read_bytes(&mut self, bytes: &mut [u8]) -> Result { - let mut read_count = 0; - for byte in bytes.iter_mut() { - match self.read_byte() { - Some(Ok(b)) => { - *byte = b; - } - Some(Err(e)) => { - return Err(TransBytesError { - bytes_transferred: read_count, - kind: e, - }); - } - None => break, - } - - read_count += 1; - } - Ok(read_count) - } -} diff --git a/drivers/serial/some-serial/src/ns16550/dw_apb.rs b/drivers/serial/some-serial/src/ns16550/dw_apb.rs index 84920cc9d7..bcbd1e16f4 100644 --- a/drivers/serial/some-serial/src/ns16550/dw_apb.rs +++ b/drivers/serial/some-serial/src/ns16550/dw_apb.rs @@ -1,11 +1,8 @@ //! Synopsys DesignWare APB UART backend for the NS16550-compatible core. -use rdif_serial::InterfaceRaw; +use rdif_serial::RawUart; -use super::{ - Config, DataBits, Kind, Ns16550, Ns16550IrqHandler, Ns16550Receiver, Ns16550Sender, Parity, - StopBits, registers::*, -}; +use super::{Config, DataBits, Kind, Ns16550, Parity, StopBits, registers::*}; /// Default UART source clock used by SG2002 / CV181x boards. pub const SG2002_UART_CLOCK: u32 = 25_000_000; @@ -81,6 +78,10 @@ impl Kind for DwApb { self.base } + fn ack_busy_detect(&self) { + let _ = self.read_u32(UART_USR_OFFSET); + } + fn set_baudrate(&self, clock_freq: u32, baudrate: u32) -> Result<(), super::ConfigError> { if baudrate == 0 || clock_freq == 0 { return Err(super::ConfigError::InvalidBaudrate); @@ -133,15 +134,7 @@ impl Ns16550 { Ns16550 { base: DwApb::new(base), clock_freq, - irq: Some(Ns16550IrqHandler { - base: DwApb::new(base), - }), - tx: Some(crate::Sender::Ns16550DwApbSender(Ns16550Sender { - base: DwApb::new(base), - })), - rx: Some(crate::Receiver::Ns16550DwApbReceiver(Ns16550Receiver { - base: DwApb::new(base), - })), + saved_lsr: LineStatusFlags::empty(), } } @@ -172,8 +165,8 @@ impl Ns16550 { self.base.write_reg(UART_IER, 0); self.base.write_reg(UART_FCR, UART_FCR_ENABLE_FIFO); - self.base.write_reg(UART_MCR, 0); - self.base.write_reg(UART_MCR, UART_MCR_RTS); + self.base + .write_reg(UART_MCR, UART_MCR_DTR | UART_MCR_RTS | UART_MCR_OUT2); self.set_config( &Config::new() @@ -189,29 +182,6 @@ impl Ns16550 { self.init_with_baud_clk(baud, clk_hz); } - /// Writes one byte, blocking until the transmitter is empty. - pub fn putchar(&mut self, c: u8) { - while self.base.line_status() & UART_LSR_TEMT == 0 { - core::hint::spin_loop(); - } - self.base.write_reg(UART_THR, c); - } - - /// Reads one byte if data is ready. - pub fn getchar(&mut self) -> Option { - if self.base.line_status() & UART_LSR_DR != 0 { - Some(self.base.read_reg(UART_RBR)) - } else { - None - } - } - - /// Enables or disables receive-data interrupts. - pub fn set_ier(&mut self, enabled: bool) { - self.base - .write_reg(UART_IER, if enabled { UART_IER_RDI } else { 0 }); - } - /// Reads the line status register. pub fn line_status(&self) -> u32 { self.base.line_status() as u32 @@ -221,4 +191,41 @@ impl Ns16550 { pub fn cpr(&self) -> u32 { self.base.cpr() } + + pub fn new_raw(base: core::ptr::NonNull, clock_freq: u32) -> Self { + Self::new_with_clock(base.as_ptr() as usize, clock_freq) + } +} + +#[cfg(test)] +mod tests { + use std::boxed::Box; + + use super::*; + + #[test] + fn busy_detect_interrupt_is_claimed_as_irq_ack() { + let regs = Box::leak(Box::new([0u32; 0x100 / 4])); + regs[UART_IIR as usize] = UART_IIR_BUSY as u32; + regs[UART_USR_OFFSET / 4] = 0x1; + + let mut uart = DwApbUart::new(regs.as_ptr() as usize); + + assert_eq!(uart.handle_irq(), rdif_serial::SerialEvent::IRQ_ACK); + assert_eq!(regs[UART_USR_OFFSET / 4], 0x1); + } + + #[test] + fn new_raw_does_not_touch_hardware_registers() { + let regs = Box::leak(Box::new([0u32; 0x100 / 4])); + regs[UART_DLF_OFFSET / 4] = 0x33; + + let base = core::ptr::NonNull::new(regs.as_mut_ptr().cast()).unwrap(); + let serial = DwApbUart::new_raw(base, SG2002_UART_CLOCK); + + assert_eq!(regs[UART_FCR as usize], 0); + assert_eq!(regs[UART_MCR as usize], 0); + assert_eq!(regs[UART_DLF_OFFSET / 4], 0x33); + drop(serial); + } } diff --git a/drivers/serial/some-serial/src/ns16550/mmio.rs b/drivers/serial/some-serial/src/ns16550/mmio.rs index e2bc2a40be..93dd396de0 100644 --- a/drivers/serial/some-serial/src/ns16550/mmio.rs +++ b/drivers/serial/some-serial/src/ns16550/mmio.rs @@ -4,10 +4,7 @@ use core::ptr::NonNull; -use rdif_serial::{BSerial, InterfaceRaw, SerialDyn}; - use super::{Kind, Ns16550}; -use crate::ns16550::{Ns16550IrqHandler, Ns16550Receiver, Ns16550Sender}; #[derive(Clone)] pub struct Mmio { @@ -43,29 +40,9 @@ impl Ns16550 { }; Ns16550 { - base: base.clone(), + base, clock_freq, - irq: Some(Ns16550IrqHandler { base: base.clone() }), - tx: Some(crate::Sender::Ns16550MmioSender(Ns16550Sender { - base: base.clone(), - })), - rx: Some(crate::Receiver::Ns16550MmioReceiver(Ns16550Receiver { - base, - })), + saved_lsr: super::registers::LineStatusFlags::empty(), } } - - pub fn new_mmio_boxed(base: NonNull, clock_freq: u32, reg_width: usize) -> BSerial { - let mut serial = Ns16550::new_mmio(base, clock_freq, reg_width); - serial.open(); - SerialDyn::new_boxed(serial) - } - - pub fn take_tx(&mut self) -> Option { - self.tx.take() - } - - pub fn take_rx(&mut self) -> Option { - self.rx.take() - } } diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index cc85396149..cb944b76c9 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -4,13 +4,15 @@ //! - IO Port 版本(x86_64 架构) //! - MMIO 版本(通用嵌入式平台) +extern crate alloc; + // 公共寄存器定义 mod registers; use bitflags::Flags; use rdif_serial::{ - Config, ConfigError, DataBits, InterfaceRaw, InterruptMask, Parity, SetBackError, StopBits, - TIrqHandler, TSender, TransferError, + Config, ConfigError, DataBits, InterruptMask, IrqSnapshot, IrqSource, Parity, RawUart, RxFlag, + RxSample, SerialDirection, SerialEvent, StopBits, TransBytesError, TransferError, }; use registers::*; @@ -27,13 +29,13 @@ pub use mmio::*; pub use pio::*; pub use rockchip_fiq::*; -use crate::{RawReceiver, RawSender}; - pub trait Kind: Clone + Send + Sync + 'static { fn read_reg(&self, reg: u8) -> u8; fn write_reg(&self, reg: u8, val: u8); fn get_base(&self) -> usize; + fn ack_busy_detect(&self) {} + fn set_baudrate(&self, clock_freq: u32, baudrate: u32) -> Result<(), ConfigError> { if baudrate == 0 || clock_freq == 0 { return Err(ConfigError::InvalidBaudrate); @@ -71,9 +73,20 @@ pub trait Kind: Clone + Send + Sync + 'static { fn init(&self) { self.write_flags(UART_IER, InterruptEnableFlags::empty()); + self.write_flags( + UART_FCR, + FifoControlFlags::ENABLE_FIFO + | FifoControlFlags::CLEAR_RECEIVER_FIFO + | FifoControlFlags::CLEAR_TRANSMITTER_FIFO + | FifoControlFlags::TRIGGER_1_BYTE, + ); let mut mcr: ModemControlFlags = self.read_flags(UART_MCR); - mcr.insert(ModemControlFlags::DATA_TERMINAL_READY | ModemControlFlags::REQUEST_TO_SEND); + mcr.insert( + ModemControlFlags::DATA_TERMINAL_READY + | ModemControlFlags::REQUEST_TO_SEND + | ModemControlFlags::OUT_2, + ); self.write_flags(UART_MCR, mcr); } @@ -90,17 +103,11 @@ pub trait Kind: Clone + Send + Sync + 'static { pub struct Ns16550 { pub(crate) base: T, pub(crate) clock_freq: u32, - pub(crate) irq: Option>, - pub(crate) tx: Option, - pub(crate) rx: Option, + pub(crate) saved_lsr: LineStatusFlags, } -impl InterfaceRaw for Ns16550 { - type Sender = crate::Sender; - type Receiver = crate::Receiver; - type IrqHandler = Ns16550IrqHandler; - - fn name(&self) -> &str { +impl RawUart for Ns16550 { + fn name(&self) -> &'static str { "NS16550 UART" } @@ -108,6 +115,30 @@ impl InterfaceRaw for Ns16550 { self.base.get_base() } + fn clock_freq(&self) -> Option { + self.clock_freq.try_into().ok() + } + + fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { + self.write_flags(UART_IER, InterruptEnableFlags::empty()); + self.set_config(config)?; + self.enable_fifo(true); + + let mut mcr: ModemControlFlags = self.read_flags(UART_MCR); + mcr.insert( + ModemControlFlags::DATA_TERMINAL_READY + | ModemControlFlags::REQUEST_TO_SEND + | ModemControlFlags::OUT_2, + ); + self.write_flags(UART_MCR, mcr); + self.saved_lsr = LineStatusFlags::empty(); + Ok(()) + } + + fn shutdown(&mut self) { + self.close(); + } + fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { // 配置波特率 if let Some(baudrate) = config.baudrate { @@ -180,24 +211,6 @@ impl InterfaceRaw for Ns16550 { } } - fn clock_freq(&self) -> Option { - self.clock_freq.try_into().ok() - } - - fn open(&mut self) { - self.init_core(); - } - - fn close(&mut self) { - // 禁用所有中断 - self.write_flags(UART_IER, InterruptEnableFlags::empty()); - - // 禁用 DTR 和 RTS - let mut mcr: ModemControlFlags = self.read_flags(UART_MCR); - mcr.remove(ModemControlFlags::DATA_TERMINAL_READY | ModemControlFlags::REQUEST_TO_SEND); - self.write_flags(UART_MCR, mcr); - } - fn enable_loopback(&mut self) { let mut mcr: ModemControlFlags = self.read_flags(UART_MCR); mcr.insert(ModemControlFlags::LOOPBACK_ENABLE); @@ -216,128 +229,283 @@ impl InterfaceRaw for Ns16550 { } fn set_irq_mask(&mut self, mask: InterruptMask) { - let mut ier = InterruptEnableFlags::empty(); + Ns16550::set_irq_mask(self, mask); + } - if mask.contains(InterruptMask::RX_AVAILABLE) { - ier.insert(InterruptEnableFlags::RECEIVED_DATA_AVAILABLE); - ier.insert(InterruptEnableFlags::RECEIVER_LINE_STATUS); - } - if mask.contains(InterruptMask::TX_EMPTY) { - ier.insert(InterruptEnableFlags::TRANSMITTER_HOLDING_EMPTY); - } + fn take_irq_snapshot(&mut self) -> IrqSnapshot { + Ns16550::take_irq_snapshot(self) + } - self.write_flags(UART_IER, ier); + fn read_rx(&mut self) -> Option { + Ns16550::read_rx(self) } - fn get_irq_mask(&self) -> InterruptMask { - let ier: InterruptEnableFlags = self.read_flags(UART_IER); - let mut mask = InterruptMask::empty(); + fn tx_ready(&mut self) -> bool { + self.read_flags::(UART_LSR) + .contains(LineStatusFlags::TRANSMITTER_HOLDING_EMPTY) + } - if ier.contains(InterruptEnableFlags::RECEIVED_DATA_AVAILABLE) { - mask |= InterruptMask::RX_AVAILABLE; - } - if ier.contains(InterruptEnableFlags::TRANSMITTER_HOLDING_EMPTY) { - mask |= InterruptMask::TX_EMPTY; + fn write_tx(&mut self, byte: u8) { + self.base.write_reg(UART_THR, byte); + } + + fn tx_load_size(&self) -> usize { + if self.is_fifo_enabled() { + UART_FIFO_SIZE as usize + } else { + 1 } - // 错误中断暂不映射到 InterruptMask - // 用户需要通过状态寄存器检查错误 + } - mask + fn tx_idle(&mut self) -> bool { + let lsr: LineStatusFlags = self.read_flags(UART_LSR); + lsr.contains( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY | LineStatusFlags::TRANSMITTER_EMPTY, + ) } - fn irq_handler(&mut self) -> Option { - self.irq.take() + fn ack_modem_status(&mut self) { + let _: ModemStatusFlags = self.read_flags(UART_MSR); } - fn take_tx(&mut self) -> Option { - self.tx.take() + fn ack_busy_detect(&mut self) { + self.base.ack_busy_detect(); } - fn take_rx(&mut self) -> Option { - self.rx.take() + fn poll_status(&mut self) -> SerialEvent { + Ns16550::poll_status(self) } - fn set_tx(&mut self, tx: Self::Sender) -> Result<(), SetBackError> { - let want = self.base.get_base(); - match tx { - #[cfg(target_arch = "x86_64")] - crate::Sender::Ns16550Sender(ref sender) => { - let actual = sender.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); - } - } - crate::Sender::Ns16550MmioSender(ref sender) => { - let actual = sender.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); - } - } - crate::Sender::Ns16550DwApbSender(ref sender) => { - let actual = sender.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); - } - } - crate::Sender::Ns16550RockchipFiqSender(ref sender) => { - let actual = sender.base_addr(); - if actual != want { - return Err(SetBackError::new(want, actual)); - } - } - _ => { - return Err(SetBackError::new(want, 0)); // 不匹配的类型 + fn write_byte(&mut self, byte: u8) { + Ns16550::write_byte(self, byte); + } + + fn read_byte(&mut self, status: SerialEvent) -> Option> { + Ns16550::read_byte(self, status) + } +} + +impl Ns16550 { + // 类型安全的 bitflags 寄存器访问 + fn read_flags>(&self, reg: u8) -> F { + F::from_bits_retain(self.base.read_reg(reg)) + } + + fn write_flags>(&mut self, reg: u8, val: F) { + self.base.write_reg(reg, val.bits()); + } + + pub fn pending(&mut self, direction: SerialDirection) -> bool { + let lsr = self.read_lsr_preserving(); + match direction { + SerialDirection::Input => lsr.contains(LineStatusFlags::DATA_READY), + SerialDirection::Output => lsr.contains(LineStatusFlags::TRANSMITTER_HOLDING_EMPTY), + } + } + + pub fn poll_status(&mut self) -> SerialEvent { + serial_event_from_lsr(self.read_lsr_preserving()) + } + + pub fn try_write(&mut self, bytes: &[u8]) -> usize { + let mut written = 0; + while written < bytes.len() { + let status = self.poll_status(); + if !status.tx_ready() { + break; } + self.write_byte(bytes[written]); + written += 1; } - self.tx = Some(tx); - Ok(()) + written } - fn set_rx(&mut self, rx: Self::Receiver) -> Result<(), SetBackError> { - let want = self.base.get_base(); - match rx { - #[cfg(target_arch = "x86_64")] - crate::Receiver::Ns16550Receiver(ref receiver) => { - let actual = receiver.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); - } + pub fn try_read(&mut self, bytes: &mut [u8]) -> Result { + let mut read_count = 0; + let mut first_error = None; + for byte in bytes.iter_mut() { + let status = self.poll_status(); + if !status.rx_ready() && !status.rx_error() { + break; } - crate::Receiver::Ns16550MmioReceiver(ref receiver) => { - let actual = receiver.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); + let result = self.read_byte(status); + match result { + Some(Ok(b)) => { + *byte = b; + read_count += 1; } - } - crate::Receiver::Ns16550DwApbReceiver(ref receiver) => { - let actual = receiver.base.get_base(); - if actual != want { - return Err(SetBackError::new(want, actual)); + Some(Err(TransferError::Overrun(b))) => { + *byte = b; + read_count += 1; + first_error.get_or_insert(TransferError::Overrun(b)); } - } - crate::Receiver::Ns16550RockchipFiqReceiver(ref receiver) => { - let actual = receiver.base_addr(); - if actual != want { - return Err(SetBackError::new(want, actual)); + Some(Err(e)) => { + first_error.get_or_insert(e); } - } - _ => { - return Err(SetBackError::new(want, 0)); // 不匹配的类型 + None => break, } } - self.rx = Some(rx); - Ok(()) + if let Some(kind) = first_error { + Err(TransBytesError { + bytes_transferred: read_count, + kind, + }) + } else { + Ok(read_count) + } } -} -impl Ns16550 { - // 类型安全的 bitflags 寄存器访问 - fn read_flags>(&self, reg: u8) -> F { - F::from_bits_retain(self.base.read_reg(reg)) + pub fn handle_irq(&mut self) -> SerialEvent { + serial_event_from_snapshot(self.take_irq_snapshot()) } - fn write_flags>(&mut self, reg: u8, val: F) { - self.base.write_reg(reg, val.bits()); + pub fn write_byte(&mut self, byte: u8) { + self.base.write_reg(UART_THR, byte); + } + + pub fn take_irq_snapshot(&mut self) -> IrqSnapshot { + let iir: InterruptIdentificationFlags = self.read_flags(UART_IIR); + + if iir.bits() & (UART_IIR_ID | UART_IIR_NO_INT) == UART_IIR_BUSY { + return IrqSnapshot { + claimed: true, + sources: IrqSource::BUSY_DETECT, + }; + } + + if iir.contains(InterruptIdentificationFlags::NO_INTERRUPT_PENDING) { + return IrqSnapshot::default(); + } + + let interrupt_id = iir & InterruptIdentificationFlags::INTERRUPT_ID_MASK; + let sources = if interrupt_id == InterruptIdentificationFlags::RECEIVER_LINE_STATUS { + IrqSource::RX_STATUS + } else if interrupt_id == InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE { + IrqSource::RX_DATA + } else if interrupt_id == InterruptIdentificationFlags::CHARACTER_TIMEOUT { + IrqSource::RX_TIMEOUT + } else if interrupt_id == InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY { + IrqSource::TX_SPACE + } else if interrupt_id == InterruptIdentificationFlags::MODEM_STATUS { + IrqSource::MODEM_STATUS + } else { + IrqSource::OTHER_ACK + }; + + if sources.intersects(IrqSource::RX_DATA | IrqSource::RX_TIMEOUT | IrqSource::RX_STATUS) { + let _ = self.read_lsr_preserving(); + } + + IrqSnapshot { + claimed: true, + sources, + } + } + + pub fn read_rx(&mut self) -> Option { + let lsr = self.read_lsr_preserving(); + if !lsr.intersects(LineStatusFlags::DATA_READY | LineStatusFlags::ERROR_MASK) { + return None; + } + + let byte = lsr + .contains(LineStatusFlags::DATA_READY) + .then(|| self.base.read_reg(UART_RBR)); + let flag = if lsr.contains(LineStatusFlags::BREAK_INTERRUPT) { + RxFlag::Break + } else if lsr.contains(LineStatusFlags::PARITY_ERROR) { + RxFlag::Parity + } else if lsr.contains(LineStatusFlags::FRAMING_ERROR) { + RxFlag::Framing + } else { + RxFlag::Normal + }; + let overrun = lsr.contains(LineStatusFlags::OVERRUN_ERROR); + self.saved_lsr + .remove(LineStatusFlags::ERROR_MASK | LineStatusFlags::FIFO_ERROR); + + Some(RxSample { + byte, + flag, + overrun, + }) + } + + fn read_lsr_preserving(&mut self) -> LineStatusFlags { + let lsr: LineStatusFlags = self.read_flags(UART_LSR); + self.saved_lsr + .insert(lsr & (LineStatusFlags::ERROR_MASK | LineStatusFlags::FIFO_ERROR)); + lsr | self.saved_lsr + } + + pub fn read_byte(&mut self, status: SerialEvent) -> Option> { + if !status.rx_ready() && !status.rx_error() { + return None; + } + if self.saved_lsr.contains(LineStatusFlags::OVERRUN_ERROR) { + let b = self.base.read_reg(UART_RBR); + self.saved_lsr.remove(LineStatusFlags::OVERRUN_ERROR); + return Some(Err(TransferError::Overrun(b))); + } + if self.saved_lsr.contains(LineStatusFlags::PARITY_ERROR) { + let _ = self.base.read_reg(UART_RBR); + self.saved_lsr.remove(LineStatusFlags::PARITY_ERROR); + return Some(Err(TransferError::Parity)); + } + if self.saved_lsr.contains(LineStatusFlags::FRAMING_ERROR) { + let _ = self.base.read_reg(UART_RBR); + self.saved_lsr.remove(LineStatusFlags::FRAMING_ERROR); + return Some(Err(TransferError::Framing)); + } + if self.saved_lsr.contains(LineStatusFlags::BREAK_INTERRUPT) { + let _ = self.base.read_reg(UART_RBR); + self.saved_lsr.remove(LineStatusFlags::BREAK_INTERRUPT); + return Some(Err(TransferError::Break)); + } + if status.rx_ready() { + return Some(Ok(self.base.read_reg(UART_RBR))); + } + None + } + + pub fn open(&mut self) { + self.init_core(); + } + + pub fn close(&mut self) { + self.write_flags(UART_IER, InterruptEnableFlags::empty()); + + let mut mcr: ModemControlFlags = self.read_flags(UART_MCR); + mcr.remove(ModemControlFlags::DATA_TERMINAL_READY | ModemControlFlags::REQUEST_TO_SEND); + self.write_flags(UART_MCR, mcr); + } + + pub fn set_irq_mask(&mut self, mask: InterruptMask) { + let mut ier = InterruptEnableFlags::empty(); + + if mask.intersects(InterruptMask::RX) { + ier.insert(InterruptEnableFlags::RECEIVED_DATA_AVAILABLE); + ier.insert(InterruptEnableFlags::RECEIVER_LINE_STATUS); + } + if mask.contains(InterruptMask::TX_SPACE) { + ier.insert(InterruptEnableFlags::TRANSMITTER_HOLDING_EMPTY); + } + + self.write_flags(UART_IER, ier); + } + + pub fn get_irq_mask(&self) -> InterruptMask { + let ier: InterruptEnableFlags = self.read_flags(UART_IER); + let mut mask = InterruptMask::empty(); + + if ier.contains(InterruptEnableFlags::RECEIVED_DATA_AVAILABLE) { + mask |= InterruptMask::RX; + } + if ier.contains(InterruptEnableFlags::TRANSMITTER_HOLDING_EMPTY) { + mask |= InterruptMask::TX_SPACE; + } + + mask } /// 检查是否为 16550+(支持 FIFO) @@ -469,96 +637,466 @@ impl Ns16550 { } } -pub struct Ns16550Sender { - pub(crate) base: T, +fn serial_event_from_lsr(lsr: LineStatusFlags) -> SerialEvent { + let mut event = SerialEvent::empty(); + if lsr.contains(LineStatusFlags::DATA_READY) { + event |= SerialEvent::RX_READY; + } + if lsr.intersects( + LineStatusFlags::PARITY_ERROR + | LineStatusFlags::FRAMING_ERROR + | LineStatusFlags::BREAK_INTERRUPT, + ) { + event |= SerialEvent::RX_ERROR; + } + if lsr.contains(LineStatusFlags::OVERRUN_ERROR) { + event |= SerialEvent::RX_ERROR | SerialEvent::OVERRUN; + } + if lsr.contains(LineStatusFlags::TRANSMITTER_HOLDING_EMPTY) { + event |= SerialEvent::TX_READY; + } + event } -impl TSender for Ns16550Sender { - fn write_byte(&mut self, byte: u8) -> bool { - RawSender::write_byte(self, byte) +fn serial_event_from_snapshot(snapshot: IrqSnapshot) -> SerialEvent { + let mut event = SerialEvent::empty(); + if !snapshot.claimed { + return event; + } + if snapshot + .sources + .intersects(IrqSource::RX_DATA | IrqSource::RX_TIMEOUT) + { + event |= SerialEvent::RX_READY; + } + if snapshot.sources.contains(IrqSource::RX_STATUS) { + event |= SerialEvent::RX_ERROR; + } + if snapshot.sources.contains(IrqSource::TX_SPACE) { + event |= SerialEvent::TX_READY; + } + if snapshot.sources.contains(IrqSource::MODEM_STATUS) { + event |= SerialEvent::MODEM_STATUS; + } + if snapshot + .sources + .intersects(IrqSource::BUSY_DETECT | IrqSource::OTHER_ACK) + { + event |= SerialEvent::IRQ_ACK; } + event } -pub struct Ns16550Receiver { - pub(crate) base: T, -} +#[cfg(test)] +mod tests { + use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; + use std::sync::{Mutex, MutexGuard}; -impl RawReceiver for Ns16550Receiver { - fn read_byte(&mut self) -> Option> { - let lsr: LineStatusFlags = self.base.read_flags(UART_LSR); + use rdif_serial::{RxItem, SerialCore}; - // 按优先级检查错误(从高到低) - if lsr.contains(LineStatusFlags::OVERRUN_ERROR) { - let b = self.base.read_reg(UART_RBR); - return Some(Err(TransferError::Overrun(b))); - } + use super::*; - if lsr.contains(LineStatusFlags::PARITY_ERROR) { - let _b = self.base.read_reg(UART_RBR); - return Some(Err(TransferError::Parity)); + static REGS: [AtomicU8; 8] = [const { AtomicU8::new(0) }; 8]; + static THR_WRITES: AtomicUsize = AtomicUsize::new(0); + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + #[derive(Clone)] + struct MockKind; + + impl Kind for MockKind { + fn read_reg(&self, reg: u8) -> u8 { + let value = REGS[reg as usize].load(Ordering::SeqCst); + if reg == UART_RBR { + REGS[UART_LSR as usize].fetch_and( + !(LineStatusFlags::ERROR_MASK | LineStatusFlags::DATA_READY).bits(), + Ordering::SeqCst, + ); + } else if reg == UART_MSR { + REGS[UART_MSR as usize] + .fetch_and(!ModemStatusFlags::DELTA_MASK.bits(), Ordering::SeqCst); + } + value } - if lsr.contains(LineStatusFlags::FRAMING_ERROR) { - let _b = self.base.read_reg(UART_RBR); - return Some(Err(TransferError::Framing)); + fn write_reg(&self, reg: u8, val: u8) { + REGS[reg as usize].store(val, Ordering::SeqCst); + if reg == UART_THR { + let iir = REGS[UART_IIR as usize].load(Ordering::SeqCst); + if iir & InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits() == 0 { + REGS[UART_LSR as usize].fetch_and( + !LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + } else { + let writes = THR_WRITES.fetch_add(1, Ordering::SeqCst) + 1; + if writes >= UART_FIFO_SIZE as usize { + REGS[UART_LSR as usize].fetch_and( + !LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + } + } + } } - if lsr.contains(LineStatusFlags::BREAK_INTERRUPT) { - let _b = self.base.read_reg(UART_RBR); - return Some(Err(TransferError::Break)); + fn get_base(&self) -> usize { + 0x1000 } + } - if lsr.contains(LineStatusFlags::DATA_READY) { - let b = self.base.read_reg(UART_RBR); - return Some(Ok(b)); + fn reset_regs() { + for reg in ®S { + reg.store(0, Ordering::SeqCst); } - None + THR_WRITES.store(0, Ordering::SeqCst); } -} -pub struct Ns16550IrqHandler { - pub(crate) base: T, -} + fn serial() -> (MutexGuard<'static, ()>, Ns16550) { + let guard = TEST_LOCK.lock().unwrap(); + reset_regs(); + ( + guard, + Ns16550 { + base: MockKind, + clock_freq: 1_843_200, + saved_lsr: LineStatusFlags::empty(), + }, + ) + } -impl TIrqHandler for Ns16550IrqHandler { - fn clean_interrupt_status(&self) -> InterruptMask { - let iir: InterruptIdentificationFlags = self.base.read_flags(UART_IIR); - let mut mask = InterruptMask::empty(); + fn started_core(uart: Ns16550) -> SerialCore, 64, 64> { + let mut core = SerialCore::new(uart); + core.startup(&Config::new()).unwrap(); + core + } - // 检查是否有中断挂起 - if iir.contains(InterruptIdentificationFlags::NO_INTERRUPT_PENDING) { - return mask; - } + #[test] + fn pending_output_preserves_rx_error_latch() { + let (_guard, mut uart) = serial(); + REGS[UART_LSR as usize].store( + (LineStatusFlags::TRANSMITTER_HOLDING_EMPTY | LineStatusFlags::PARITY_ERROR).bits(), + Ordering::SeqCst, + ); - // 获取中断ID(需要提取bit 1-3) - let interrupt_id = iir & InterruptIdentificationFlags::INTERRUPT_ID_MASK; + assert!(uart.pending(SerialDirection::Output)); - // 使用精确匹配而不是 contains - if interrupt_id == InterruptIdentificationFlags::RECEIVER_LINE_STATUS - || interrupt_id == InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE - || interrupt_id == InterruptIdentificationFlags::CHARACTER_TIMEOUT - { - // 接收数据可用中断或字符超时中断 - mask |= InterruptMask::RX_AVAILABLE; - } else if interrupt_id == InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY { - // 发送保持寄存器空中断 - mask |= InterruptMask::TX_EMPTY; - } else if interrupt_id == InterruptIdentificationFlags::MODEM_STATUS { - // Modem 状态中断 - } + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + let mut buf = [0]; + let err = uart + .try_read(&mut buf) + .expect_err("saved parity error should be reported by next read"); + assert_eq!(err.bytes_transferred, 0); + assert_eq!(err.kind, TransferError::Parity); + } - mask + #[test] + fn try_write_stops_when_tx_fifo_becomes_full() { + let (_guard, mut uart) = serial(); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + + assert_eq!(uart.try_write(b"ab"), 1); + assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'a'); } -} -impl RawSender for Ns16550Sender { - fn write_byte(&mut self, byte: u8) -> bool { - let lsr: LineStatusFlags = self.base.read_flags(UART_LSR); - if lsr.contains(LineStatusFlags::TRANSMITTER_HOLDING_EMPTY) { - self.base.write_reg(UART_THR, byte); - true - } else { - false - } + #[test] + fn try_write_fills_enabled_tx_fifo_in_one_pass() { + let (_guard, mut uart) = serial(); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits(), + Ordering::SeqCst, + ); + + assert_eq!(uart.try_write(b"abcdefghijklmnopq"), 16); + assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'p'); + } + + #[test] + fn open_enables_modem_interrupt_output_gate() { + let (_guard, mut uart) = serial(); + + uart.open(); + + let fcr = + FifoControlFlags::from_bits_retain(REGS[UART_FCR as usize].load(Ordering::SeqCst)); + assert!(fcr.contains(FifoControlFlags::ENABLE_FIFO)); + assert!(fcr.contains(FifoControlFlags::CLEAR_RECEIVER_FIFO)); + assert!(fcr.contains(FifoControlFlags::CLEAR_TRANSMITTER_FIFO)); + let mcr = + ModemControlFlags::from_bits_retain(REGS[UART_MCR as usize].load(Ordering::SeqCst)); + assert!(mcr.contains(ModemControlFlags::DATA_TERMINAL_READY)); + assert!(mcr.contains(ModemControlFlags::REQUEST_TO_SEND)); + assert!(mcr.contains(ModemControlFlags::OUT_2)); + } + + #[test] + fn try_read_empty_returns_zero() { + let (_guard, mut uart) = serial(); + let mut buf = [0]; + + assert_eq!(uart.try_read(&mut buf), Ok(0)); + } + + #[test] + fn handle_irq_saves_rx_error_for_task_read() { + let (_guard, mut uart) = serial(); + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + (LineStatusFlags::DATA_READY | LineStatusFlags::OVERRUN_ERROR).bits(), + Ordering::SeqCst, + ); + REGS[UART_RBR as usize].store(0xab, Ordering::SeqCst); + + let event = uart.handle_irq(); + assert!(event.intersects(SerialEvent::RX_ERROR | SerialEvent::OVERRUN)); + + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + let mut buf = [0]; + let err = uart + .try_read(&mut buf) + .expect_err("saved overrun should be reported by task read"); + assert_eq!(buf[0], 0xab); + assert_eq!(err.bytes_transferred, 1); + assert_eq!(err.kind, TransferError::Overrun(0xab)); + } + + #[test] + fn serial_core_single_irq_services_rx_and_tx_fifo() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + assert_eq!(core.enqueue_tx(b"ab").accepted, 2); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.tx_sent, 1); + assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'a'); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + REGS[UART_RBR as usize].store(b'z', Ordering::SeqCst); + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 1); + + let mut rx = [RxItem::default(); 1]; + assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!( + rx[0], + RxItem::Byte { + byte: b'z', + flag: RxFlag::Normal + } + ); + } + + #[test] + fn serial_core_does_not_synthesize_tx_irq_from_plain_lsr_ready() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + assert_eq!(core.enqueue_tx(b"x").accepted, 1); + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::NO_INTERRUPT_PENDING.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + + let outcome = core.handle_irq(); + assert!(!outcome.claimed); + assert_eq!(outcome.tx_sent, 0); + assert_eq!(core.chars_in_buffer(), 1); + } + + #[test] + fn hard_irq_does_not_claim_tx_ready_without_iir_pending() { + let (_guard, mut uart) = serial(); + + uart.set_irq_mask(InterruptMask::TX_EMPTY); + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::NO_INTERRUPT_PENDING.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + + assert!(uart.handle_irq().is_empty()); + assert!(uart.poll_status().tx_ready()); + } + + #[test] + fn hard_irq_does_not_claim_rx_ready_without_iir_pending() { + let (_guard, mut uart) = serial(); + + uart.set_irq_mask(InterruptMask::RX_AVAILABLE); + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::NO_INTERRUPT_PENDING.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + + assert!(uart.handle_irq().is_empty()); + assert!(uart.poll_status().rx_ready()); + } + + #[test] + fn hard_irq_claims_and_clears_modem_status_interrupt() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::MODEM_STATUS.bits() + | InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits(), + Ordering::SeqCst, + ); + REGS[UART_MSR as usize].store( + ModemStatusFlags::DELTA_CLEAR_TO_SEND.bits(), + Ordering::SeqCst, + ); + + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 0); + assert_eq!(outcome.tx_sent, 0); + assert!( + ModemStatusFlags::from_bits_retain(REGS[UART_MSR as usize].load(Ordering::SeqCst)) + .intersection(ModemStatusFlags::DELTA_MASK) + .is_empty() + ); + } + + #[test] + fn serial_core_rx_irq_drains_raw_fifo() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + REGS[UART_RBR as usize].store(b'r', Ordering::SeqCst); + + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 1); + + let mut rx = [RxItem::default(); 1]; + assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!( + rx[0], + RxItem::Byte { + byte: b'r', + flag: RxFlag::Normal + } + ); + } + + #[test] + fn serial_core_tx_irq_uses_software_fifo() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + + assert_eq!(core.enqueue_tx(b"ab").accepted, 2); + assert_eq!(core.chars_in_buffer(), 2); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), + Ordering::SeqCst, + ); + + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.tx_sent, 1); + assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'a'); + assert_eq!(core.chars_in_buffer(), 1); + } + + #[test] + fn serial_core_saved_lsr_error_is_consumed_by_rx_fifo() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + (LineStatusFlags::DATA_READY | LineStatusFlags::PARITY_ERROR).bits(), + Ordering::SeqCst, + ); + REGS[UART_RBR as usize].store(b'p', Ordering::SeqCst); + + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 1); + + let mut rx = [RxItem::default(); 1]; + assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!( + rx[0], + RxItem::Byte { + byte: b'p', + flag: RxFlag::Parity + } + ); + } + + #[test] + fn serial_core_rx_overrun_returns_current_byte_and_marker() { + let (_guard, uart) = serial(); + let mut core = started_core(uart); + + REGS[UART_IIR as usize].store( + InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), + Ordering::SeqCst, + ); + REGS[UART_LSR as usize].store( + (LineStatusFlags::DATA_READY | LineStatusFlags::OVERRUN_ERROR).bits(), + Ordering::SeqCst, + ); + REGS[UART_RBR as usize].store(b'S', Ordering::SeqCst); + + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 2); + + let mut rx = [RxItem::default(); 2]; + assert_eq!(core.drain_rx(&mut rx), 2); + assert_eq!( + rx[0], + RxItem::Byte { + byte: b'S', + flag: RxFlag::Normal + } + ); + assert_eq!(rx[1], RxItem::Overrun); } } diff --git a/drivers/serial/some-serial/src/ns16550/pio.rs b/drivers/serial/some-serial/src/ns16550/pio.rs index 1dd62f8a6b..543eca021a 100644 --- a/drivers/serial/some-serial/src/ns16550/pio.rs +++ b/drivers/serial/some-serial/src/ns16550/pio.rs @@ -2,9 +2,7 @@ //! //! 仅在 x86_64 架构下编译,使用 x86_64 crate 进行端口 I/O -use rdif_serial::InterfaceRaw; - -use super::{Kind, Ns16550, Ns16550IrqHandler, Ns16550Receiver, Ns16550Sender}; +use super::{Kind, Ns16550}; /// NS16550 IO Port 版本驱动 #[derive(Clone, Debug)] @@ -37,27 +35,9 @@ impl Ns16550 { let base = Port { port }; Ns16550 { - base: base.clone(), + base, clock_freq, - irq: Some(Ns16550IrqHandler { base: base.clone() }), - tx: Some(crate::Sender::Ns16550Sender(Ns16550Sender { - base: base.clone(), - })), - rx: Some(crate::Receiver::Ns16550Receiver(Ns16550Receiver { base })), + saved_lsr: super::registers::LineStatusFlags::empty(), } } - - pub fn new_port_boxed(port: u16, clock_freq: u32) -> rdif_serial::BSerial { - let mut serial = Ns16550::new_port(port, clock_freq); - serial.open(); - rdif_serial::SerialDyn::new_boxed(serial) - } - - pub fn take_tx(&mut self) -> Option { - self.tx.take() - } - - pub fn take_rx(&mut self) -> Option { - self.rx.take() - } } diff --git a/drivers/serial/some-serial/src/ns16550/registers.rs b/drivers/serial/some-serial/src/ns16550/registers.rs index b2ed672916..26e6ccf177 100644 --- a/drivers/serial/some-serial/src/ns16550/registers.rs +++ b/drivers/serial/some-serial/src/ns16550/registers.rs @@ -347,6 +347,7 @@ pub const UART_IIR_RDI: u8 = 0x04; // Received Data Available Interrupt pub const UART_IIR_CTI: u8 = 0x0C; // Character Timeout Indicator pub const UART_IIR_THRI: u8 = 0x02; // Transmitter Holding Register Empty Interrupt pub const UART_IIR_MSI: u8 = 0x00; // Modem Status Interrupt +pub const UART_IIR_BUSY: u8 = 0x07; // DesignWare APB Busy Detect Interrupt pub const UART_IIR_FIFO_ENABLE: u8 = 0xC0; // FIFO Enable bits pub const UART_IIR_FIFO_MASK: u8 = 0xC0; // FIFO bits mask diff --git a/drivers/serial/some-serial/src/ns16550/rockchip_fiq.rs b/drivers/serial/some-serial/src/ns16550/rockchip_fiq.rs index d95cae70a0..b43d8e8f35 100644 --- a/drivers/serial/some-serial/src/ns16550/rockchip_fiq.rs +++ b/drivers/serial/some-serial/src/ns16550/rockchip_fiq.rs @@ -2,24 +2,21 @@ extern crate alloc; -use alloc::boxed::Box; use core::{any::Any, num::NonZeroU32, ptr::NonNull}; use heapless::{String, Vec}; use rdif_serial::{ - BSerial, Config, ConfigError, DataBits, DriverGeneric, Interface, InterruptMask, Parity, - SerialDyn, StopBits, TIrqHandler, TReceiver, TSender, TransferError, + Config, ConfigError, DataBits, DriverGeneric, InterruptMask, IrqSnapshot, Parity, RawUart, + RxSample, SerialEvent, StopBits, TransferError, }; use super::{ - Kind, Ns16550, Ns16550IrqHandler, + Kind, Ns16550, registers::{ - UART_DLH, UART_DLL, UART_FCR, UART_IER, UART_IER_RDI, UART_IIR, UART_IIR_CTI, UART_LCR, - UART_LCR_DLAB, UART_LCR_WLEN8, UART_LSR, UART_LSR_BI, UART_LSR_DR, UART_LSR_TEMT, UART_MCR, - UART_RBR, UART_THR, + LineStatusFlags, UART_DLH, UART_DLL, UART_FCR, UART_IER, UART_IER_RDI, UART_LCR, + UART_LCR_DLAB, UART_LCR_WLEN8, UART_LSR, UART_LSR_DR, UART_LSR_THRE, UART_MCR, UART_RBR, }, }; -use crate::{RawReceiver, RawSender}; pub const ROCKCHIP_FIQ_RK3588_UART_CLOCK: u32 = 24_000_000; pub const ROCKCHIP_FIQ_DEFAULT_BAUDRATE: u32 = 1_500_000; @@ -28,10 +25,8 @@ const REG_SHIFT: usize = 2; const DEBUG_MAX: usize = 64; const HISTORY_MAX: usize = 16; const UART_USR: u8 = 0x1f; -const RK_UART_RFL: u8 = 0x21; const UART_SRR: u8 = 0x22; const UART_USR_TX_FIFO_NOT_FULL: u32 = 0x02; -const UART_USR_BUSY: u32 = 0x01; pub type CommandString = String; @@ -477,49 +472,15 @@ impl RockchipFiqPort { self.write_reg(UART_FCR, 0x01); self.write_reg(UART_MCR, 0); } - - fn read_debug_byte(&self) -> Option { - let iir = self.read_u32(UART_IIR); - let usr = self.read_u32(UART_USR); - let lsr = self.read_reg(UART_LSR); - - if (iir & 0x3f) == UART_IIR_CTI as u32 { - let rfl = self.read_u32(RK_UART_RFL); - if lsr & (UART_LSR_DR | UART_LSR_BI) == 0 && usr & UART_USR_BUSY == 0 && rfl == 0 { - let _ = self.read_reg(UART_RBR); - } - } - - if lsr & UART_LSR_DR != 0 { - Some(self.read_reg(UART_RBR)) - } else { - None - } - } - - fn write_debug_byte(&self, byte: u8) -> bool { - let mut count = 10_000; - while self.read_u32(UART_USR) & UART_USR_TX_FIFO_NOT_FULL == 0 { - if count == 0 { - return false; - } - count -= 1; - core::hint::spin_loop(); - } - self.write_reg(UART_THR, byte); - true - } - - fn flush(&self) { - while self.read_reg(UART_LSR) & UART_LSR_TEMT == 0 { - core::hint::spin_loop(); - } - } } impl Kind for RockchipFiqPort { fn read_reg(&self, reg: u8) -> u8 { - (self.read_u32(reg) & 0xff) as u8 + let mut value = (self.read_u32(reg) & 0xff) as u8; + if reg == UART_LSR && self.read_u32(UART_USR) & UART_USR_TX_FIFO_NOT_FULL != 0 { + value |= UART_LSR_THRE; + } + value } fn write_reg(&self, reg: u8, val: u8) { @@ -553,58 +514,19 @@ impl Kind for RockchipFiqPort { } } -pub struct RockchipFiqSender { - pub(crate) base: RockchipFiqPort, -} - -impl RockchipFiqSender { - pub fn base_addr(&self) -> usize { - self.base.base_addr() - } -} - -impl RawSender for RockchipFiqSender { - fn write_byte(&mut self, byte: u8) -> bool { - self.base.write_debug_byte(byte) - } -} - -pub struct RockchipFiqReceiver { - pub(crate) base: RockchipFiqPort, -} - -impl RockchipFiqReceiver { - pub fn base_addr(&self) -> usize { - self.base.base_addr() - } -} - -impl RawReceiver for RockchipFiqReceiver { - fn read_byte(&mut self) -> Option> { - self.base.read_debug_byte().map(Ok) - } -} - impl Ns16550 { pub fn new_rockchip_fiq(base: NonNull, clock_freq: u32) -> Self { let base = RockchipFiqPort::new(base.as_ptr() as usize); Self { base, clock_freq, - irq: Some(Ns16550IrqHandler { base }), - tx: Some(crate::Sender::Ns16550RockchipFiqSender(RockchipFiqSender { - base, - })), - rx: Some(crate::Receiver::Ns16550RockchipFiqReceiver( - RockchipFiqReceiver { base }, - )), + saved_lsr: LineStatusFlags::empty(), } } } pub struct RockchipFiqSerial { - serial: BSerial, - port: RockchipFiqPort, + serial: Ns16550, debugger: FiqDebugger, config: RockchipFiqConfig, } @@ -614,19 +536,14 @@ impl RockchipFiqSerial { let config = config.normalised(); let port = RockchipFiqPort::new(base.as_ptr() as usize); port.init_debug_port(config.baudrate); - let serial = SerialDyn::new_boxed(Ns16550::new_rockchip_fiq(base, config.clock_hz)); + let serial = Ns16550::new_rockchip_fiq(base, config.clock_hz); Self { serial, - port, debugger: FiqDebugger::new(config), config, } } - pub fn new_boxed(base: NonNull, config: RockchipFiqConfig) -> BSerial { - Box::new(Self::new(base, config)) - } - pub fn config(&self) -> RockchipFiqConfig { self.config } @@ -635,18 +552,6 @@ impl RockchipFiqSerial { self.debugger.handle_byte(byte, emit); } - pub fn poll_fiq_events(&mut self, emit: &mut impl FnMut(FiqDebuggerEvent)) -> usize { - let mut count = 0; - while let Some(byte) = self.port.read_debug_byte() { - count += 1; - self.debugger.handle_byte(byte, emit); - } - if !self.debugger.console_enabled() { - self.port.flush(); - } - count - } - pub fn debugger(&self) -> &FiqDebugger { &self.debugger } @@ -670,21 +575,21 @@ impl DriverGeneric for RockchipFiqSerial { } } -impl Interface for RockchipFiqSerial { - fn irq_handler(&mut self) -> Option> { - self.serial.irq_handler() +impl RawUart for RockchipFiqSerial { + fn name(&self) -> &'static str { + "Rockchip FIQ Debugger UART" } - fn take_tx(&mut self) -> Option> { - self.serial.take_tx() + fn base_addr(&self) -> usize { + self.serial.base_addr() } - fn take_rx(&mut self) -> Option> { - self.serial.take_rx() + fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { + self.serial.startup(config) } - fn base_addr(&self) -> usize { - self.serial.base_addr() + fn shutdown(&mut self) { + self.serial.shutdown() } fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { @@ -723,16 +628,52 @@ impl Interface for RockchipFiqSerial { self.serial.is_loopback_enabled() } - fn enable_interrupts(&mut self, mask: InterruptMask) { - self.serial.enable_interrupts(mask) + fn set_irq_mask(&mut self, mask: InterruptMask) { + self.serial.set_irq_mask(mask) + } + + fn poll_status(&mut self) -> SerialEvent { + self.serial.poll_status() + } + + fn take_irq_snapshot(&mut self) -> IrqSnapshot { + self.serial.take_irq_snapshot() + } + + fn read_rx(&mut self) -> Option { + self.serial.read_rx() + } + + fn tx_ready(&mut self) -> bool { + self.serial.tx_ready() + } + + fn write_tx(&mut self, byte: u8) { + self.serial.write_tx(byte) + } + + fn tx_load_size(&self) -> usize { + self.serial.tx_load_size() + } + + fn tx_idle(&mut self) -> bool { + self.serial.tx_idle() + } + + fn ack_modem_status(&mut self) { + self.serial.ack_modem_status() + } + + fn ack_busy_detect(&mut self) { + self.serial.ack_busy_detect() } - fn disable_interrupts(&mut self, mask: InterruptMask) { - self.serial.disable_interrupts(mask) + fn write_byte(&mut self, byte: u8) { + self.serial.write_byte(byte); } - fn get_enabled_interrupts(&self) -> InterruptMask { - self.serial.get_enabled_interrupts() + fn read_byte(&mut self, status: SerialEvent) -> Option> { + self.serial.read_byte(status) } } diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index 8ada69b3e4..08a8cf068c 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -1,14 +1,12 @@ use core::{num::NonZeroU32, ptr::NonNull}; use rdif_serial::{ - BSerial, InterfaceRaw, SerialDyn, SetBackError, TIrqHandler, TSender, TransBytesError, - TransferError, + InterruptMask, IrqSnapshot, IrqSource, RawUart, RxFlag, RxSample, SerialDirection, SerialEvent, + TransBytesError, TransferError, }; use tock_registers::{interfaces::*, register_bitfields, register_structs, registers::*}; -use crate::{ - Config, ConfigError, DataBits, InterruptMask, Parity, RawReceiver, RawSender, StopBits, -}; +use crate::{Config, ConfigError, DataBits, Parity, StopBits}; register_bitfields! [ u32, @@ -144,9 +142,6 @@ unsafe impl Sync for Pl011Registers {} pub struct Pl011 { base: Reg, clock_freq: u32, - tx: Option, - rx: Option, - irq: Option, } impl Pl011 { @@ -163,19 +158,7 @@ impl Pl011 { pub fn new(base: NonNull, clock_freq: u32) -> Self { let base = Reg(base.cast()); - Self { - base, - clock_freq, - tx: Some(Pl011Sender { base }), - rx: Some(Pl011Receiver { base }), - irq: Some(Pl011IrqHandler { base }), - } - } - - pub fn new_boxed(base: NonNull, clock_freq: u32) -> BSerial { - let mut serial = Self::new(base, clock_freq); - serial.open(); - SerialDyn::new_boxed(serial) + Self { base, clock_freq } } fn registers(&self) -> &Pl011Registers { @@ -288,7 +271,7 @@ impl Pl011 { } /// 初始化 PL011 UART - fn init(&self) { + pub fn open(&mut self) { // 禁用 UART self.registers().uartcr.modify(UARTCR::UARTEN::CLEAR); @@ -320,91 +303,103 @@ impl Pl011 { .modify(UARTCR::UARTEN::SET + UARTCR::TXE::SET + UARTCR::RXE::SET); } - pub fn task_tx(&mut self) -> Option { - self.tx.take().map(crate::Sender::Pl011Sender) - } + pub fn set_irq_mask(&mut self, mask: InterruptMask) { + let mut imsc = 0; + if mask.intersects(InterruptMask::RX) { + imsc |= UARTIS::RX::SET.value + | UARTIS::RT::SET.value + | UARTIS::FE::SET.value + | UARTIS::PE::SET.value + | UARTIS::BE::SET.value + | UARTIS::OE::SET.value; + } + if mask.contains(InterruptMask::TX_SPACE) { + imsc |= UARTIS::TX::SET.value; + } - pub fn task_rx(&mut self) -> Option { - self.rx.take().map(crate::Receiver::Pl011Receiver) + self.registers().uartimsc.set(imsc); } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -struct Reg(NonNull); -unsafe impl Send for Reg {} - -impl Reg { - fn registers(&self) -> &Pl011Registers { - unsafe { self.0.as_ref() } - } -} + pub fn get_irq_mask(&self) -> InterruptMask { + let imsc = self.registers().uartimsc.extract(); + let mut mask = InterruptMask::empty(); -pub struct Pl011Sender { - base: Reg, -} + if imsc.is_set(UARTIS::RX) + || imsc.is_set(UARTIS::RT) + || imsc.is_set(UARTIS::FE) + || imsc.is_set(UARTIS::PE) + || imsc.is_set(UARTIS::BE) + || imsc.is_set(UARTIS::OE) + { + mask |= InterruptMask::RX; + } + if imsc.is_set(UARTIS::TX) { + mask |= InterruptMask::TX_SPACE; + } -impl TSender for Pl011Sender { - fn write_byte(&mut self, byte: u8) -> bool { - RawSender::write_byte(self, byte) + mask } -} -impl RawSender for Pl011Sender { - fn write_byte(&mut self, byte: u8) -> bool { - if self.base.registers().uartfr.is_set(UARTFR::TXFF) { - return false; + pub fn pending(&mut self, direction: SerialDirection) -> bool { + match direction { + SerialDirection::Input => !self.registers().uartfr.is_set(UARTFR::RXFE), + SerialDirection::Output => !self.registers().uartfr.is_set(UARTFR::TXFF), } - - self.base.registers().uartdr.set(byte as _); - - true } -} -pub struct Pl011Receiver { - base: Reg, -} - -impl RawReceiver for Pl011Receiver { - fn read_byte(&mut self) -> Option> { - if self.base.registers().uartfr.is_set(UARTFR::RXFE) { - return None; + pub fn poll_status(&mut self) -> SerialEvent { + let mut event = SerialEvent::empty(); + let fr = self.registers().uartfr.extract(); + if !fr.is_set(UARTFR::RXFE) { + event |= SerialEvent::RX_READY; } - - let dr = self.base.registers().uartdr.extract(); - let data = dr.read(UARTDR::DATA) as u8; - - if dr.is_set(UARTDR::FE) { - return Some(Err(TransferError::Framing)); + if !fr.is_set(UARTFR::TXFF) { + event |= SerialEvent::TX_READY; } - if dr.is_set(UARTDR::PE) { - return Some(Err(TransferError::Parity)); + let rsr = self.registers().uartrsr_ecr.extract(); + if rsr.is_set(UARTRSR_ECR::FE) || rsr.is_set(UARTRSR_ECR::PE) || rsr.is_set(UARTRSR_ECR::BE) + { + event |= SerialEvent::RX_ERROR; } - - if dr.is_set(UARTDR::OE) { - return Some(Err(TransferError::Overrun(data))); + if rsr.is_set(UARTRSR_ECR::OE) { + event |= SerialEvent::RX_ERROR | SerialEvent::OVERRUN; } - if dr.is_set(UARTDR::BE) { - return Some(Err(TransferError::Break)); - } + event + } - Some(Ok(data)) + pub fn try_write(&mut self, bytes: &[u8]) -> usize { + let mut written = 0; + for &byte in bytes { + let status = self.poll_status(); + if !status.tx_ready() { + break; + } + self.write_byte(byte); + written += 1; + } + written } - fn read_bytes(&mut self, bytes: &mut [u8]) -> Result { + pub fn try_read(&mut self, bytes: &mut [u8]) -> Result { let mut count = 0; - let mut overrun_data = None; for byte in bytes.iter_mut() { - match self.read_byte() { + let status = self.poll_status(); + if !status.rx_ready() && !status.rx_error() { + break; + } + match self.read_byte(status) { Some(Ok(b)) => { *byte = b; } Some(Err(TransferError::Overrun(b))) => { - overrun_data = Some(b); *byte = b; + count += 1; + return Err(TransBytesError { + bytes_transferred: count, + kind: TransferError::Overrun(b), + }); } Some(Err(e)) => { return Err(TransBytesError { @@ -412,59 +407,166 @@ impl RawReceiver for Pl011Receiver { kind: e, }); } - None => { - if let Some(data) = overrun_data { - count = count.saturating_sub(1); - - return Err(TransBytesError { - bytes_transferred: count, - kind: TransferError::Overrun(data), - }); - } - break; - } + None => break, } count += 1; } Ok(count) } -} -pub struct Pl011IrqHandler { - base: Reg, -} + pub fn handle_irq(&mut self) -> SerialEvent { + serial_event_from_snapshot(self.take_irq_snapshot()) + } -unsafe impl Sync for Pl011IrqHandler {} + pub fn write_byte(&mut self, byte: u8) { + self.registers().uartdr.set(byte as _); + } -impl TIrqHandler for Pl011IrqHandler { - fn clean_interrupt_status(&self) -> InterruptMask { - let mis = self.base.registers().uartmis.extract(); - let mut mask = InterruptMask::empty(); + pub fn read_byte(&mut self, status: SerialEvent) -> Option> { + if !status.rx_ready() && !status.rx_error() { + return None; + } + let dr = self.registers().uartdr.extract(); + let data = dr.read(UARTDR::DATA) as u8; + + if dr.is_set(UARTDR::FE) { + return Some(Err(TransferError::Framing)); + } + if dr.is_set(UARTDR::PE) { + return Some(Err(TransferError::Parity)); + } + if dr.is_set(UARTDR::OE) { + return Some(Err(TransferError::Overrun(data))); + } + if dr.is_set(UARTDR::BE) { + return Some(Err(TransferError::Break)); + } + + Some(Ok(data)) + } + + pub fn take_irq_snapshot(&mut self) -> IrqSnapshot { + let mis = self.registers().uartmis.extract(); + let active = mis.get(); + if active == 0 { + return IrqSnapshot::default(); + } + + let mut sources = IrqSource::empty(); if mis.is_set(UARTIS::RX) { - mask |= InterruptMask::RX_AVAILABLE; + sources |= IrqSource::RX_DATA; + } + if mis.is_set(UARTIS::RT) { + sources |= IrqSource::RX_TIMEOUT; + } + if mis.is_set(UARTIS::FE) + || mis.is_set(UARTIS::PE) + || mis.is_set(UARTIS::BE) + || mis.is_set(UARTIS::OE) + { + sources |= IrqSource::RX_STATUS; } if mis.is_set(UARTIS::TX) { - mask |= InterruptMask::TX_EMPTY; + sources |= IrqSource::TX_SPACE; + } + if mis.is_set(UARTIS::CTSM) + || mis.is_set(UARTIS::DSRM) + || mis.is_set(UARTIS::DCDM) + || mis.is_set(UARTIS::RIM) + { + sources |= IrqSource::MODEM_STATUS; } - self.base.registers().uarticr.set(mis.get()); + self.registers() + .uarticr + .set(active & !(UARTIS::TX::SET.value | UARTIS::RX::SET.value | UARTIS::RT::SET.value)); - mask + IrqSnapshot { + claimed: true, + sources, + } + } + + pub fn read_rx(&mut self) -> Option { + if self.registers().uartfr.is_set(UARTFR::RXFE) { + return None; + } + + let dr = self.registers().uartdr.extract(); + let data = dr.read(UARTDR::DATA) as u8; + let flag = if dr.is_set(UARTDR::BE) { + RxFlag::Break + } else if dr.is_set(UARTDR::PE) { + RxFlag::Parity + } else if dr.is_set(UARTDR::FE) { + RxFlag::Framing + } else { + RxFlag::Normal + }; + + Some(RxSample { + byte: Some(data), + flag, + overrun: dr.is_set(UARTDR::OE), + }) } } -impl InterfaceRaw for Pl011 { - type IrqHandler = Pl011IrqHandler; +fn serial_event_from_snapshot(snapshot: IrqSnapshot) -> SerialEvent { + let mut event = SerialEvent::empty(); + if !snapshot.claimed { + return event; + } + if snapshot + .sources + .intersects(IrqSource::RX_DATA | IrqSource::RX_TIMEOUT) + { + event |= SerialEvent::RX_READY; + } + if snapshot.sources.contains(IrqSource::RX_STATUS) { + event |= SerialEvent::RX_ERROR; + } + if snapshot.sources.contains(IrqSource::TX_SPACE) { + event |= SerialEvent::TX_READY; + } + if snapshot.sources.contains(IrqSource::MODEM_STATUS) { + event |= SerialEvent::MODEM_STATUS; + } + event +} - type Sender = crate::Sender; +#[derive(Clone, Copy, PartialEq, Eq)] +struct Reg(NonNull); - type Receiver = crate::Receiver; +unsafe impl Send for Reg {} +unsafe impl Sync for Reg {} - fn name(&self) -> &str { +impl RawUart for Pl011 { + fn name(&self) -> &'static str { "PL011 UART" } + fn base_addr(&self) -> usize { + self.base.0.as_ptr() as usize + } + + fn clock_freq(&self) -> Option { + self.clock_freq.try_into().ok() + } + + fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { + self.open(); + self.set_config(config)?; + self.set_irq_mask(InterruptMask::empty()); + Ok(()) + } + + fn shutdown(&mut self) { + self.registers().uartimsc.set(0); + self.registers().uartcr.modify(UARTCR::UARTEN::CLEAR); + } + fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { use tock_registers::interfaces::Readable; @@ -560,19 +662,6 @@ impl InterfaceRaw for Pl011 { } } - fn open(&mut self) { - self.init() - } - - fn close(&mut self) { - // 禁用 UART - self.registers().uartcr.modify(UARTCR::UARTEN::CLEAR); - } - - fn clock_freq(&self) -> Option { - self.clock_freq.try_into().ok() - } - fn enable_loopback(&mut self) { self.registers().uartcr.modify(UARTCR::LBE::SET); } @@ -586,87 +675,44 @@ impl InterfaceRaw for Pl011 { } fn set_irq_mask(&mut self, mask: InterruptMask) { - let mut imsc = 0; - if mask.contains(InterruptMask::RX_AVAILABLE) { - imsc += UARTIS::RX::SET.value; - } - if mask.contains(InterruptMask::TX_EMPTY) { - imsc += UARTIS::TX::SET.value; - } - - self.registers().uartimsc.set(imsc); + Pl011::set_irq_mask(self, mask); } - fn get_irq_mask(&self) -> InterruptMask { - let imsc = self.registers().uartimsc.extract(); - let mut mask = InterruptMask::empty(); - - if imsc.is_set(UARTIS::RX) { - mask |= InterruptMask::RX_AVAILABLE; - } - if imsc.is_set(UARTIS::TX) { - mask |= InterruptMask::TX_EMPTY; - } - - mask + fn take_irq_snapshot(&mut self) -> IrqSnapshot { + Pl011::take_irq_snapshot(self) } - fn base_addr(&self) -> usize { - self.base.0.as_ptr() as usize + fn read_rx(&mut self) -> Option { + Pl011::read_rx(self) } - fn irq_handler(&mut self) -> Option { - self.irq.take() + fn tx_ready(&mut self) -> bool { + !self.registers().uartfr.is_set(UARTFR::TXFF) } - fn take_tx(&mut self) -> Option { - self.task_tx() + fn write_tx(&mut self, byte: u8) { + Pl011::write_byte(self, byte); } - fn take_rx(&mut self) -> Option { - self.task_rx() + fn tx_load_size(&self) -> usize { + 16 } - fn set_tx(&mut self, tx: Self::Sender) -> Result<(), SetBackError> { - let tx = match tx { - crate::Sender::Pl011Sender(s) => s, - _ => { - return Err(SetBackError::new( - self.base.0.as_ptr() as _, - 0, // 不匹配的发送器类型 - )); - } - }; + fn tx_idle(&mut self) -> bool { + let fr = self.registers().uartfr.extract(); + !fr.is_set(UARTFR::BUSY) && !fr.is_set(UARTFR::TXFF) + } - if self.base != tx.base { - return Err(SetBackError::new( - self.base.0.as_ptr() as _, - tx.base.0.as_ptr() as _, - )); - } + fn poll_status(&mut self) -> SerialEvent { + Pl011::poll_status(self) + } - self.tx = Some(tx); - Ok(()) + fn write_byte(&mut self, byte: u8) { + Pl011::write_byte(self, byte) } - fn set_rx(&mut self, rx: Self::Receiver) -> Result<(), SetBackError> { - let rx = match rx { - crate::Receiver::Pl011Receiver(r) => r, - _ => { - return Err(SetBackError::new( - self.base.0.as_ptr() as _, - 0, // 不匹配的接收器类型 - )); - } - }; - if self.base != rx.base { - return Err(SetBackError::new( - self.base.0.as_ptr() as _, - rx.base.0.as_ptr() as _, - )); - } - self.rx = Some(rx); - Ok(()) + fn read_byte(&mut self, status: SerialEvent) -> Option> { + Pl011::read_byte(self, status) } } @@ -713,3 +759,133 @@ impl Pl011 { } // ModemStatus 现在在 lib.rs 中定义,这里只是导出 + +#[cfg(test)] +mod tests { + use core::ptr::NonNull; + use std::boxed::Box; + + use rdif_serial::SerialCore; + + use super::*; + + fn pl011_with_registers() -> (Box, Pl011) { + let mut regs = Box::new(unsafe { core::mem::zeroed::() }); + let ptr = NonNull::from(regs.as_mut()).cast::(); + let uart = Pl011::new(ptr, 24_000_000); + (regs, uart) + } + + fn pl011_with_overrun_data() -> (Box, Pl011) { + let (regs, uart) = pl011_with_registers(); + regs.uartdr + .set((UARTDR::DATA.val(0xab) + UARTDR::OE::SET).into()); + (regs, uart) + } + + fn write_test_reg(regs: &mut Pl011Registers, offset: usize, value: u32) { + unsafe { + (regs as *mut Pl011Registers) + .cast::() + .add(offset / core::mem::size_of::()) + .write_volatile(value); + } + } + + fn started_core(uart: Pl011) -> SerialCore { + let mut core = SerialCore::new(uart); + core.startup(&Config::new()).unwrap(); + core + } + + #[test] + fn raw_rx_reports_overrun_instead_of_swallowing_it() { + let (_regs, mut uart) = pl011_with_overrun_data(); + + let mut buf = [0]; + let err = uart + .try_read(&mut buf) + .expect_err("overrun must be reported to the caller"); + + assert_eq!(buf[0], 0xab); + assert_eq!(err.bytes_transferred, 1); + assert_eq!(err.kind, TransferError::Overrun(0xab)); + } + + #[test] + fn raw_rx_sample_reports_overrun_instead_of_swallowing_it() { + let (mut regs, uart) = pl011_with_overrun_data(); + let mut uart = uart; + + write_test_reg(&mut regs, 0x040, UARTIS::OE::SET.value); + let snapshot = uart.take_irq_snapshot(); + assert!(snapshot.claimed); + assert!(snapshot.sources.contains(IrqSource::RX_STATUS)); + + let sample = uart.read_rx().expect("RX sample should be available"); + assert_eq!(sample.byte, Some(0xab)); + assert_eq!(sample.flag, RxFlag::Normal); + assert!(sample.overrun); + } + + #[test] + fn serial_core_tx_irq_drains_software_fifo() { + let (mut regs, uart) = pl011_with_registers(); + let mut core = started_core(uart); + + write_test_reg(&mut regs, 0x018, UARTFR::TXFF::SET.value); + assert_eq!(core.enqueue_tx(b"x").accepted, 1); + assert_eq!(core.chars_in_buffer(), 1); + + write_test_reg(&mut regs, 0x018, 0); + write_test_reg(&mut regs, 0x040, UARTIS::TX::SET.value); + let outcome = core.handle_irq(); + assert!(outcome.claimed); + assert_eq!(outcome.tx_sent, 1); + assert_eq!(regs.uartdr.get() as u8, b'x'); + assert_eq!(core.chars_in_buffer(), 0); + } + + #[test] + fn rx_available_mask_enables_timeout_and_error_interrupts() { + let (regs, mut uart) = pl011_with_registers(); + + uart.set_irq_mask(InterruptMask::RX_AVAILABLE); + + let imsc = regs.uartimsc.extract(); + assert!(imsc.is_set(UARTIS::RX)); + assert!(imsc.is_set(UARTIS::RT)); + assert!(imsc.is_set(UARTIS::FE)); + assert!(imsc.is_set(UARTIS::PE)); + assert!(imsc.is_set(UARTIS::BE)); + assert!(imsc.is_set(UARTIS::OE)); + assert_eq!(uart.get_irq_mask(), InterruptMask::RX_AVAILABLE); + } + + #[test] + fn hard_irq_does_not_claim_rx_ready_without_mis() { + let (mut regs, mut uart) = pl011_with_registers(); + + uart.set_irq_mask(InterruptMask::RX_AVAILABLE); + write_test_reg(&mut regs, 0x040, 0); + write_test_reg(&mut regs, 0x018, 0); + + assert!(uart.handle_irq().is_empty()); + } + + #[test] + fn raw_rx_ready_is_visible_without_irq_snapshot() { + let (mut regs, mut uart) = pl011_with_registers(); + + uart.set_irq_mask(InterruptMask::RX_AVAILABLE); + write_test_reg(&mut regs, 0x040, 0); + write_test_reg(&mut regs, 0x018, 0); + regs.uartdr.set(UARTDR::DATA.val(b'r' as u32).into()); + + let status = uart.poll_status(); + assert!(status.rx_ready()); + let sample = uart.read_rx().expect("RX sample should be available"); + assert_eq!(sample.byte, Some(b'r')); + assert_eq!(sample.flag, RxFlag::Normal); + } +} diff --git a/drivers/test_crates/driver-tests/tests/serial.rs b/drivers/test_crates/driver-tests/tests/serial.rs deleted file mode 100644 index 6ee8a6a9eb..0000000000 --- a/drivers/test_crates/driver-tests/tests/serial.rs +++ /dev/null @@ -1,389 +0,0 @@ -#![no_std] -#![no_main] -#![feature(used_with_arg)] - -extern crate alloc; -extern crate bare_test; - -#[bare_test::tests] -mod tests { - use alloc::string::String; - use core::{ - ptr::NonNull, - sync::atomic::{AtomicUsize, Ordering}, - }; - - use bare_test::{ - hal::al::memory, - os::{ - irq::register_handler, - mem::mmio::{MapError, MmioOp, MmioRaw, kernel_mmio_op}, - platform::{PlatformDescriptor, get_platform_descriptor}, - }, - }; - use fdt_edit::{ClockType, Fdt, InterruptRef, NodeType}; - use ax_kspin::SpinNoIrq as Mutex; - use rdif_intc::Intc; - use rdif_serial::{BIrqHandler, BReceiver, BSender, BSerial, TransferError}; - use rdrive::fdt_phandle_to_device_id; - use some_serial::{Config, DataBits, InterruptMask, Parity, StopBits}; - - static TX_INTERRUPT_COUNT: AtomicUsize = AtomicUsize::new(0); - static RX_INTERRUPT_COUNT: AtomicUsize = AtomicUsize::new(0); - static IRQ_HANDLER_CALL_COUNT: AtomicUsize = AtomicUsize::new(0); - static IRQ_HANDLER: Mutex> = Mutex::new(None); - - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum DriverType { - Pl011, - Ns16550Mmio, - } - - struct TestSerial { - serial: BSerial, - mmio: TestMmio, - irq: rdrive::IrqId, - driver_type: DriverType, - } - - enum TestMmio { - Owned(MmioRaw), - Borrowed(NonNull), - } - - impl TestMmio { - fn base(&self) -> NonNull { - match self { - Self::Owned(mmio) => mmio.as_nonnull_ptr(), - Self::Borrowed(base) => *base, - } - } - } - - impl Drop for TestSerial { - fn drop(&mut self) { - self.serial - .disable_interrupts(InterruptMask::TX_EMPTY | InterruptMask::RX_AVAILABLE); - self.serial.disable_loopback(); - *IRQ_HANDLER.lock() = None; - if let TestMmio::Owned(mmio) = &self.mmio { - kernel_mmio_op().iounmap(mmio); - } - } - } - - #[test] - fn test_serial_basic_loopback() { - let mut ctx = create_test_serial(); - let serial = &mut ctx.serial; - - let config = Config::new() - .baudrate(115200) - .data_bits(DataBits::Eight) - .stop_bits(StopBits::One) - .parity(Parity::None); - serial.set_config(&config).expect("failed to set config"); - - let mut tx = serial.take_tx().expect("missing tx"); - let mut rx = serial.take_rx().expect("missing rx"); - clean_rx(&mut rx); - - test_serial_tx_rx_one(serial, &mut tx, &mut rx, b"Hello\n").expect("loopback failed"); - } - - #[test] - fn test_serial_configuration_roundtrip() { - let mut ctx = create_test_serial(); - let serial = &mut ctx.serial; - - let configs = [ - (115200, DataBits::Eight, StopBits::One, Parity::None), - (9600, DataBits::Seven, StopBits::One, Parity::Even), - (38400, DataBits::Eight, StopBits::Two, Parity::Odd), - ]; - - for (baudrate, data_bits, stop_bits, parity) in configs { - let config = Config::new() - .baudrate(baudrate) - .data_bits(data_bits) - .stop_bits(stop_bits) - .parity(parity); - - serial.set_config(&config).expect("failed to set config"); - - assert_eq!(serial.data_bits(), data_bits); - assert_eq!(serial.stop_bits(), stop_bits); - assert_eq!(serial.parity(), parity); - assert_ne!(serial.baudrate(), 0, "baudrate should be readable"); - } - } - - #[test] - fn test_interrupt_mask_control() { - let mut ctx = create_test_serial(); - let irq = ctx.irq; - let driver_type = ctx.driver_type; - let serial = &mut ctx.serial; - - reset_interrupt_counters(); - - serial.enable_interrupts(InterruptMask::TX_EMPTY); - assert_eq!( - serial.get_enabled_interrupts().bits(), - InterruptMask::TX_EMPTY.bits() - ); - - serial.enable_interrupts(InterruptMask::RX_AVAILABLE); - assert_eq!( - serial.get_enabled_interrupts().bits(), - (InterruptMask::TX_EMPTY | InterruptMask::RX_AVAILABLE).bits() - ); - - serial.disable_interrupts(InterruptMask::TX_EMPTY); - assert_eq!( - serial.get_enabled_interrupts().bits(), - InterruptMask::RX_AVAILABLE.bits() - ); - - let mut tx = serial.take_tx().expect("missing tx"); - let mut rx = serial.take_rx().expect("missing rx"); - - clean_rx(&mut rx); - serial.enable_loopback(); - - let payload = b"irq-loopback"; - let mut remaining = payload.as_slice(); - while !remaining.is_empty() { - let written = tx.write_bytes(remaining); - assert!(written > 0, "failed to write test payload"); - remaining = &remaining[written..]; - } - - assert!( - wait_for_counter(&RX_INTERRUPT_COUNT), - "RX interrupt was not observed on irq {:?} for {:?}", - irq, - driver_type - ); - assert!( - IRQ_HANDLER_CALL_COUNT.load(Ordering::SeqCst) > 0, - "IRQ handler was never invoked" - ); - - let mut buffer = [0u8; 32]; - let received = rx.read_bytes(&mut buffer).expect("failed to read loopback"); - assert_eq!(&buffer[..received], payload); - - serial.disable_loopback(); - serial.disable_interrupts(InterruptMask::TX_EMPTY | InterruptMask::RX_AVAILABLE); - assert_eq!( - serial.get_enabled_interrupts().bits(), - InterruptMask::empty().bits() - ); - } - - fn create_test_serial() -> TestSerial { - let fdt = get_platform_fdt(); - let node = find_test_uart_node(&fdt); - let driver_type = driver_type_for_node(&node); - let reg = node.regs().into_iter().next().expect("uart reg missing"); - let size = reg.size.unwrap_or(0x1000).max(0x1000) as usize; - let mmio = match kernel_mmio_op().ioremap((reg.address as usize).into(), size) { - Ok(mmio) => TestMmio::Owned(mmio), - Err(MapError::Busy) => { - let virt = memory::_io((reg.address as usize).into()); - let base = NonNull::new(virt.raw() as *mut u8).expect("uart virt addr is null"); - TestMmio::Borrowed(base) - } - Err(err) => panic!("failed to map uart mmio: {err:?}"), - }; - let base = mmio.base(); - let clock = clock_frequency(&fdt, &node, driver_type); - let irq_ref = node - .interrupts() - .into_iter() - .next() - .expect("uart interrupt missing"); - - let mut serial = match driver_type { - DriverType::Pl011 => some_serial::pl011::Pl011::new_boxed(base, clock), - DriverType::Ns16550Mmio => { - some_serial::ns16550::Ns16550::new_mmio_boxed(base, clock, 4) - } - }; - - let irq_handler = serial.irq_handler().expect("missing irq handler"); - let irq = register_uart_irq(&irq_ref, irq_handler); - - TestSerial { - serial, - mmio, - irq, - driver_type, - } - } - - fn get_platform_fdt() -> Fdt { - let PlatformDescriptor::DeviceTree(dtb) = get_platform_descriptor() else { - panic!("device tree not found"); - }; - - Fdt::from_bytes(dtb.as_slice()).expect("invalid device tree") - } - - fn find_test_uart_node<'a>(fdt: &'a Fdt) -> NodeType<'a> { - if let Some(path) = chosen_stdout_path(fdt) { - if let Some(node) = fdt.get_by_path(&path) { - return node; - } - } - - fdt.find_compatible(&["arm,pl011", "snps,dw-apb-uart"]) - .into_iter() - .next() - .expect("no supported uart node found") - } - - fn chosen_stdout_path(fdt: &Fdt) -> Option { - let chosen = fdt.get_by_path("/chosen")?; - for key in ["stdout-path", "linux,stdout-path"] { - if let Some(path) = chosen - .as_node() - .get_property(key) - .and_then(|prop| prop.as_str()) - { - let path = path.split(':').next().unwrap_or(path); - if !path.is_empty() { - return Some(path.into()); - } - } - } - None - } - - fn driver_type_for_node(node: &NodeType<'_>) -> DriverType { - for compatible in node.as_node().compatibles() { - match compatible { - "arm,pl011" | "arm,primecell" => return DriverType::Pl011, - "snps,dw-apb-uart" => return DriverType::Ns16550Mmio, - _ => {} - } - } - panic!("unsupported uart compatible set") - } - - fn clock_frequency(fdt: &Fdt, node: &NodeType<'_>, driver_type: DriverType) -> u32 { - node.clocks() - .into_iter() - .find_map(|clock_ref| { - let provider = fdt.get_by_phandle(clock_ref.phandle)?; - match provider { - NodeType::Clock(clock) => match clock.clock_type() { - ClockType::Fixed(clock) if clock.frequency != 0 => Some(clock.frequency), - _ => None, - }, - _ => None, - } - }) - .unwrap_or(match driver_type { - DriverType::Pl011 => 24_000_000, - DriverType::Ns16550Mmio => 1_843_200, - }) - } - - fn register_uart_irq(interrupt: &InterruptRef, handler: BIrqHandler) -> rdrive::IrqId { - let intc_id = fdt_phandle_to_device_id(interrupt.interrupt_parent) - .expect("interrupt parent not registered"); - let intc = rdrive::get::(intc_id).expect("failed to fetch interrupt controller"); - let irq = intc.lock().unwrap().setup_irq_by_fdt(&interrupt.specifier); - - *IRQ_HANDLER.lock() = Some(handler); - - register_handler(irq.raw().into(), || { - IRQ_HANDLER_CALL_COUNT.fetch_add(1, Ordering::SeqCst); - - let status = { - let guard = IRQ_HANDLER.lock(); - let Some(handler) = guard.as_ref() else { - return; - }; - handler.clean_interrupt_status() - }; - - if status.contains(InterruptMask::TX_EMPTY) { - TX_INTERRUPT_COUNT.fetch_add(1, Ordering::SeqCst); - } - if status.contains(InterruptMask::RX_AVAILABLE) { - RX_INTERRUPT_COUNT.fetch_add(1, Ordering::SeqCst); - } - }); - - irq - } - - fn reset_interrupt_counters() { - TX_INTERRUPT_COUNT.store(0, Ordering::SeqCst); - RX_INTERRUPT_COUNT.store(0, Ordering::SeqCst); - IRQ_HANDLER_CALL_COUNT.store(0, Ordering::SeqCst); - } - - fn wait_for_counter(counter: &AtomicUsize) -> bool { - for _ in 0..200_000 { - if counter.load(Ordering::SeqCst) > 0 { - return true; - } - core::hint::spin_loop(); - } - false - } - - fn clean_rx(rx: &mut BReceiver) { - let mut buffer = [0u8; 64]; - loop { - match rx.read_bytes(&mut buffer) { - Ok(0) | Err(_) => break, - Ok(_) => {} - } - } - } - - fn test_serial_tx_rx_one( - serial: &mut BSerial, - tx: &mut BSender, - rx: &mut BReceiver, - expected: &[u8], - ) -> Result<(), TransferError> { - serial.enable_loopback(); - clean_rx(rx); - - let mut received = [0u8; 64]; - let mut total = 0usize; - - for &byte in expected { - let mut written = 0usize; - while written == 0 { - written = tx.write_bytes(&[byte]); - core::hint::spin_loop(); - } - - loop { - match rx.read_bytes(&mut received[total..total + 1]) { - Ok(1) => { - total += 1; - break; - } - Ok(0) => core::hint::spin_loop(), - Ok(_) => unreachable!(), - Err(err) => { - serial.disable_loopback(); - return Err(err.kind); - } - } - } - } - - serial.disable_loopback(); - assert_eq!(&received[..total], expected); - Ok(()) - } -} diff --git a/os/StarryOS/kernel/Cargo.toml b/os/StarryOS/kernel/Cargo.toml index bbfd4d6191..fad2cb78db 100644 --- a/os/StarryOS/kernel/Cargo.toml +++ b/os/StarryOS/kernel/Cargo.toml @@ -78,7 +78,7 @@ ax-lazyinit.workspace = true ax-log.workspace = true ax-mm.workspace = true ax-net = { workspace = true } -ax-driver = { workspace = true, features = ["usb"] } +ax-driver = { workspace = true, features = ["serial", "usb"] } axklib = { workspace = true, optional = true } axplat-dyn = { workspace = true, optional = true } ax-runtime.workspace = true diff --git a/os/StarryOS/kernel/src/entry.rs b/os/StarryOS/kernel/src/entry.rs index 709ba5dc5d..4ac007ecf8 100644 --- a/os/StarryOS/kernel/src/entry.rs +++ b/os/StarryOS/kernel/src/entry.rs @@ -4,6 +4,7 @@ use alloc::{ }; use ax_fs_ng::vfs::FS_CONTEXT; +use ax_kernel_guard::NoPreemptIrqSave; use ax_runtime::hal::cpu::uspace::UserContext; use ax_sync::Mutex; use ax_task::{AxTaskExt, spawn_task}; @@ -12,7 +13,7 @@ use starry_process::{Pid, Process}; use crate::{ file::FD_TABLE, mm::{copy_from_kernel, load_user_app, new_user_aspace_empty}, - pseudofs::{self, dev::tty::N_TTY}, + pseudofs::{self, dev::tty}, task::{ProcessData, ProcessImage, Thread, add_task_to_table, new_user_task, spawn_alarm_task}, tracepoint::tracepoint_init, }; @@ -68,7 +69,9 @@ pub fn init(args: &[String], envs: &[String]) { let proc = Process::new_init(pid); proc.add_thread(pid); - N_TTY.bind_to(&proc).expect("Failed to bind ntty"); + if let Err(err) = tty::bind_console_to(&proc) { + warn!("Failed to bind console tty: {err:?}"); + } let proc = ProcessData::new( proc, @@ -89,8 +92,13 @@ pub fn init(args: &[String], envs: &[String]) { let thr = Thread::new(pid, proc, None, starry_signal::SignalSet::default()); *task.task_ext_mut() = Some(AxTaskExt::from_impl(thr)); - let task = spawn_task(task); - add_task_to_table(&task); + let task = { + let _guard = NoPreemptIrqSave::new(); + let task = spawn_task(task); + add_task_to_table(&task); + tty::arm_console_irq(); + task + }; // TODO: wait for all processes to finish let exit_code = task.join(); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/cvi_camera.rs b/os/StarryOS/kernel/src/pseudofs/dev/cvi_camera.rs index 44115ed258..e3ede84edb 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/cvi_camera.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/cvi_camera.rs @@ -12,6 +12,7 @@ use sg200x_bsp::{ pinmux::{FMUX_SD1_D1, FMUX_SD1_D2, Pinmux}, soc::{FMUX_BASE, IOBLK_BASE, IOBLK_GRTC_BASE}, }; +use some_serial::InterruptMask; use starry_vm::{VmMutPtr, vm_write_slice}; use tock_registers::interfaces::Writeable; @@ -40,11 +41,23 @@ unsafe fn cvi_camera_raw_irq_handler( let mut uart3 = some_serial::ns16550::dw_apb::DwApbUart::new( phys_to_virt(PhysAddr::from(UART3_ADDR)).as_usize(), ); + let _ = uart3.handle_irq(); + let mut scratch = [0u8; 64]; let mut buf = CAMERA_UART_BUF.lock(); - while let Some(c) = uart3.getchar() { - let _ = buf.push_back(c); + loop { + let n = match uart3.try_read(&mut scratch) { + Ok(n) => n, + Err(err) => err.bytes_transferred, + }; + if n == 0 { + break; + } + for &byte in &scratch[..n] { + let _ = buf.push_back(byte); + } } - uart3.set_ier(true); + let mask = uart3.get_irq_mask(); + uart3.set_irq_mask(mask | InterruptMask::RX_AVAILABLE); ax_runtime::hal::irq::IrqReturn::Handled } @@ -332,7 +345,15 @@ impl UartTransport for Uart3 { let mut uart3 = some_serial::ns16550::dw_apb::DwApbUart::new( phys_to_virt(PhysAddr::from(UART3_ADDR)).as_usize(), ); - data.iter().for_each(|x| uart3.putchar(*x)); + let mut written = 0; + while written < data.len() { + let n = uart3.try_write(&data[written..]); + if n == 0 { + core::hint::spin_loop(); + continue; + } + written += n; + } Ok(()) } @@ -387,7 +408,8 @@ impl CviCamera { phys_to_virt(PhysAddr::from(UART3_ADDR)).as_usize(), ); uart3.init_with_baud_clk(1_500_000, some_serial::ns16550::dw_apb::SG2002_UART_CLOCK); - uart3.set_ier(true); + let mask = uart3.get_irq_mask(); + uart3.set_irq_mask(mask | InterruptMask::RX_AVAILABLE); let _ = ax_runtime::hal::irq::request_shared_irq( 47, cvi_camera_raw_irq_handler, diff --git a/os/StarryOS/kernel/src/pseudofs/dev/irq_byte_ring.rs b/os/StarryOS/kernel/src/pseudofs/dev/irq_byte_ring.rs index 5317ca8b05..7b4fe5a379 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/irq_byte_ring.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/irq_byte_ring.rs @@ -13,15 +13,6 @@ impl ByteRing { } } - pub(super) fn clear(&mut self) { - self.head = 0; - self.len = 0; - } - - pub(super) fn is_empty(&self) -> bool { - self.len == 0 - } - pub(super) fn len(&self) -> usize { self.len } @@ -73,6 +64,6 @@ mod tests { let mut out = [0; 4]; assert_eq!(ring.drain_into(&mut out), 3); assert_eq!(&out[..3], &[1, 2, 3]); - assert!(ring.is_empty()); + assert_eq!(ring.len(), 0); } } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs index 010ed38cb8..19b2b541f6 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs @@ -9,7 +9,7 @@ mod drm; #[cfg(feature = "input")] pub mod event; mod fb; -#[cfg(feature = "sg2002")] +#[cfg(all(feature = "sg2002", not(feature = "plat-dyn")))] mod irq_byte_ring; #[cfg(feature = "k230-kpu")] mod kpu; @@ -37,8 +37,6 @@ mod cvi_usb_camera; mod pinmux; #[cfg(all(feature = "sg2002", not(feature = "plat-dyn")))] pub(super) mod pwm; -#[cfg(feature = "sg2002")] -mod tty_serial; use alloc::{format, sync::Arc}; use core::any::Any; @@ -284,13 +282,26 @@ fn builder(fs: Arc) -> DirMaker { Arc::new(tty::CurrentTty), ), ); + for entry in tty::serial_tty_entries() { + let number = entry.number(); + let minor = u32::try_from(64 + number).unwrap_or(u32::MAX); + root.add( + format!("ttyS{number}"), + Device::new( + fs.clone(), + NodeType::CharacterDevice, + DeviceId::new(4, minor), + entry.tty(), + ), + ); + } root.add( "console", Device::new( fs.clone(), NodeType::CharacterDevice, DeviceId::new(5, 1), - tty::N_TTY.clone(), + tty::console_device(), ), ); @@ -479,24 +490,9 @@ fn builder(fs: Arc) -> DirMaker { ion_device, ), ); - root.add( - "ttyS1", - Device::new( - fs.clone(), - NodeType::CharacterDevice, - DeviceId::new(4, 65), - Arc::new(tty_serial::new_tty_s1(115200)), - ), - ); - root.add( - "ttyS2", - Device::new( - fs.clone(), - NodeType::CharacterDevice, - DeviceId::new(4, 66), - Arc::new(tty_serial::new_tty_s2(115200)), - ), - ); + } + #[cfg(feature = "sg2002")] + { root.add( "cvi-usb-camera0", Device::new( diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs index ddc1161e4f..15cff747d1 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs @@ -1,10 +1,15 @@ -mod ntty; mod ptm; mod pts; mod pty; +mod serial; mod terminal; -use alloc::sync::{Arc, Weak}; +use alloc::{ + format, + string::String, + sync::{Arc, Weak}, + vec::Vec, +}; use core::{any::Any, ops::Deref, sync::atomic::Ordering, task::Context}; use ax_errno::{AxError, AxResult}; @@ -22,10 +27,12 @@ use self::terminal::{ termios::{Termios, Termios2}, }; pub use self::{ - ntty::{N_TTY, NTtyDriver}, ptm::Ptmx, pts::PtsDir, pty::PtyDriver, + serial::{ + SerialTtyDriver, arm_console_irq, bind_console_to, console_device, serial_tty_entries, + }, }; use crate::{ pseudofs::DeviceOps, @@ -35,11 +42,21 @@ use crate::{ const ANSI_CURSOR_POSITION_REQUEST: &[u8] = b"\x1b[6n"; const ANSI_CURSOR_POSITION_RESPONSE: &[u8] = b"\x1b[1;1R"; +pub fn terminal_device_path(term: &(dyn Any + Send + Sync)) -> Option { + if let Some(pts) = term.downcast_ref::() { + Some(format!("/dev/pts/{}", pts.pty_number())) + } else { + term.downcast_ref::() + .map(|tty| format!("/dev/ttyS{}", tty.serial_number())) + } +} + /// Tty device pub struct Tty { this: Weak, terminal: Arc, ldisc: Mutex>, + cursor_position_request_match: Mutex, writer: W, is_ptm: bool, } @@ -53,6 +70,7 @@ impl Tty { this: this.clone(), terminal, ldisc, + cursor_position_request_match: Mutex::new(0), writer, is_ptm, }) @@ -94,12 +112,17 @@ impl DeviceOps for Tty { if self.is_ptm { self.writer.write(buf); } else { + let (output, response_count) = { + let mut match_len = self.cursor_position_request_match.lock(); + filter_cursor_position_requests(&mut match_len, buf) + }; let term = self.terminal.load_termios(); - write_output_bytes(&self.writer, term.as_ref(), buf); - if contains_bytes(buf, ANSI_CURSOR_POSITION_REQUEST) { - self.ldisc - .lock() - .inject_input(ANSI_CURSOR_POSITION_RESPONSE); + write_output_bytes(&self.writer, term.as_ref(), &output); + if response_count > 0 { + let mut ldisc = self.ldisc.lock(); + for _ in 0..response_count { + ldisc.inject_input(ANSI_CURSOR_POSITION_RESPONSE); + } } } Ok(buf.len()) @@ -122,7 +145,13 @@ impl DeviceOps for Tty { // Faultable user memory access inside an atomic context (preemption // disabled) will call might_sleep() in handle_page_fault and panic. let termios = Arc::new(Termios2::new((arg as *const Termios).vm_read()?)); - *self.terminal.termios.lock() = termios; + let old = { + let mut guard = self.terminal.termios.lock(); + let old = guard.clone(); + *guard = termios.clone(); + old + }; + self.writer.termios_changed(old.as_ref(), termios.as_ref()); if cmd == TCSETSF { self.ldisc.lock().drain_input(); } @@ -130,7 +159,13 @@ impl DeviceOps for Tty { TCSETS2 | TCSETSF2 | TCSETSW2 => { // TODO: drain output? let termios = Arc::new((arg as *const Termios2).vm_read()?); - *self.terminal.termios.lock() = termios; + let old = { + let mut guard = self.terminal.termios.lock(); + let old = guard.clone(); + *guard = termios.clone(); + old + }; + self.writer.termios_changed(old.as_ref(), termios.as_ref()); if cmd == TCSETSF2 { self.ldisc.lock().drain_input(); } @@ -219,11 +254,30 @@ impl DeviceOps for Tty { } } -fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { - !needle.is_empty() - && haystack - .windows(needle.len()) - .any(|window| window == needle) +fn filter_cursor_position_requests(match_len: &mut usize, bytes: &[u8]) -> (Vec, usize) { + let mut output = Vec::with_capacity(bytes.len()); + let mut count = 0; + for &byte in bytes { + loop { + if byte == ANSI_CURSOR_POSITION_REQUEST[*match_len] { + *match_len += 1; + if *match_len == ANSI_CURSOR_POSITION_REQUEST.len() { + *match_len = 0; + count += 1; + } + break; + } + if *match_len > 0 { + output.extend_from_slice(&ANSI_CURSOR_POSITION_REQUEST[..*match_len]); + *match_len = 0; + continue; + } else { + output.push(byte); + break; + } + } + } + (output, count) } impl Pollable for Tty { @@ -263,3 +317,58 @@ impl DeviceOps for CurrentTty { self } } + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use super::filter_cursor_position_requests; + + #[test] + fn cursor_position_request_matcher_spans_writes() { + let mut match_len = 0; + + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"\x1b["), + (Vec::new(), 0) + ); + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"6"), + (Vec::new(), 0) + ); + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"n"), + (Vec::new(), 1) + ); + assert_eq!(match_len, 0); + } + + #[test] + fn cursor_position_request_matcher_recovers_after_partial_mismatch() { + let mut match_len = 0; + + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"\x1bX"), + (b"\x1bX".to_vec(), 0) + ); + assert_eq!(match_len, 0); + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"\x1b[6n"), + (Vec::new(), 1) + ); + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"\x1b[6n\x1b[6n"), + (Vec::new(), 2) + ); + } + + #[test] + fn cursor_position_request_filter_preserves_other_output() { + let mut match_len = 0; + + assert_eq!( + filter_cursor_position_requests(&mut match_len, b"ab\x1b[6ncd"), + (b"abcd".to_vec(), 1) + ); + } +} diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/ntty.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/ntty.rs deleted file mode 100644 index 1d7153de1a..0000000000 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/ntty.rs +++ /dev/null @@ -1,462 +0,0 @@ -use alloc::{sync::Arc, vec::Vec}; -use core::{ - ptr::NonNull, - sync::atomic::{AtomicBool, Ordering}, -}; - -use ax_task::IrqNotify; -use axpoll::{IoEvents, PollSet}; -use spin::LazyLock; - -use super::{ - Tty, - terminal::ldisc::{ProcessMode, TtyConfig, TtyRead, TtyWrite}, -}; - -pub type NTtyDriver = Tty; - -#[derive(Clone, Copy)] -pub struct Console; - -#[derive(Default)] -pub struct ConsoleReader { - mouse_filter: MouseEscapeFilter, -} - -impl TtyRead for ConsoleReader { - fn read(&mut self, buf: &mut [u8]) -> usize { - let mut written = 0; - let mut raw = [0; 64]; - while written < buf.len() { - let free = buf.len() - written; - let pending = self.mouse_filter.pending_len(); - let read_cap = if pending < free { - (free - pending).min(raw.len()) - } else { - 0 - }; - - if read_cap == 0 { - written += self.mouse_filter.flush_pending(&mut buf[written..]); - break; - } - - let read = ax_runtime::hal::console::read_bytes(&mut raw[..read_cap]); - if read == 0 { - written += self.mouse_filter.flush_pending(&mut buf[written..]); - break; - } - - written += self.mouse_filter.feed(&raw[..read], &mut buf[written..]); - if written > 0 { - break; - } - } - written - } -} - -impl TtyWrite for Console { - fn write(&self, buf: &[u8]) { - ax_runtime::hal::console::write_bytes(buf); - } -} - -#[derive(Default)] -struct MouseEscapeFilter { - pending: Vec, -} - -enum MouseParse { - Mouse(usize), - NonMouse(usize), - NeedMore, -} - -enum NumberParse { - Complete(u32), - Invalid(usize), - NeedMore, -} - -impl MouseEscapeFilter { - fn pending_len(&self) -> usize { - self.pending.len() - } - - fn feed(&mut self, input: &[u8], out: &mut [u8]) -> usize { - self.filter(input, out, false) - } - - #[cfg(test)] - fn filter_chunk(&mut self, input: &[u8], out: &mut [u8]) -> usize { - self.filter(input, out, true) - } - - fn filter(&mut self, input: &[u8], out: &mut [u8], flush_incomplete: bool) -> usize { - self.pending.extend_from_slice(input); - - let mut read = 0; - let mut written = 0; - while read < self.pending.len() { - match parse_mouse_escape(&self.pending[read..]) { - MouseParse::Mouse(len) => { - read += len; - } - MouseParse::NonMouse(len) => { - let end = read + len; - out[written..written + len].copy_from_slice(&self.pending[read..end]); - read = end; - written += len; - } - MouseParse::NeedMore => break, - } - } - - if read > 0 { - self.pending.drain(..read); - } - - if flush_incomplete { - written += self.flush_pending(&mut out[written..]); - } - written - } - - fn flush_pending(&mut self, out: &mut [u8]) -> usize { - let len = self.pending.len().min(out.len()); - out[..len].copy_from_slice(&self.pending[..len]); - self.pending.drain(..len); - len - } -} - -fn parse_mouse_escape(input: &[u8]) -> MouseParse { - if input[0] != b'\x1b' { - return MouseParse::NonMouse(1); - } - if input.len() == 1 { - return MouseParse::NeedMore; - } - if input[1] != b'[' { - return MouseParse::NonMouse(2); - } - if input.len() == 2 { - return MouseParse::NeedMore; - } - - match input[2] { - b'M' => { - if input.len() < 6 { - MouseParse::NeedMore - } else { - MouseParse::Mouse(6) - } - } - b'<' => parse_sgr_mouse(input), - b'0'..=b'9' => parse_urxvt_mouse(input), - _ => MouseParse::NonMouse(3), - } -} - -fn parse_sgr_mouse(input: &[u8]) -> MouseParse { - let mut pos = 3; - for _ in 0..2 { - match parse_number(input, pos) { - NumberParse::Complete(_) => {} - NumberParse::Invalid(len) => return MouseParse::NonMouse(len), - NumberParse::NeedMore => return MouseParse::NeedMore, - } - while pos < input.len() && input[pos].is_ascii_digit() { - pos += 1; - } - if pos == input.len() { - return MouseParse::NeedMore; - } - if input[pos] != b';' { - return MouseParse::NonMouse(pos + 1); - } - pos += 1; - } - - match parse_number(input, pos) { - NumberParse::Complete(_) => {} - NumberParse::Invalid(len) => return MouseParse::NonMouse(len), - NumberParse::NeedMore => return MouseParse::NeedMore, - } - while pos < input.len() && input[pos].is_ascii_digit() { - pos += 1; - } - if pos == input.len() { - return MouseParse::NeedMore; - } - match input[pos] { - b'M' | b'm' => MouseParse::Mouse(pos + 1), - _ => MouseParse::NonMouse(pos + 1), - } -} - -fn parse_urxvt_mouse(input: &[u8]) -> MouseParse { - let mut pos = 2; - let button = match parse_number(input, pos) { - NumberParse::Complete(value) => value, - NumberParse::Invalid(len) => return MouseParse::NonMouse(len), - NumberParse::NeedMore => return MouseParse::NeedMore, - }; - for _ in 0..2 { - while pos < input.len() && input[pos].is_ascii_digit() { - pos += 1; - } - if pos == input.len() { - return MouseParse::NeedMore; - } - if input[pos] != b';' { - return MouseParse::NonMouse(pos + 1); - } - pos += 1; - match parse_number(input, pos) { - NumberParse::Complete(_) => {} - NumberParse::Invalid(len) => return MouseParse::NonMouse(len), - NumberParse::NeedMore => return MouseParse::NeedMore, - } - } - while pos < input.len() && input[pos].is_ascii_digit() { - pos += 1; - } - if pos == input.len() { - return MouseParse::NeedMore; - } - if input[pos] == b'M' && button >= 32 { - MouseParse::Mouse(pos + 1) - } else { - MouseParse::NonMouse(pos + 1) - } -} - -fn parse_number(input: &[u8], start: usize) -> NumberParse { - if start == input.len() { - return NumberParse::NeedMore; - } - if !input[start].is_ascii_digit() { - return NumberParse::Invalid(start + 1); - } - - let mut value = 0u32; - let mut pos = start; - while pos < input.len() && input[pos].is_ascii_digit() { - value = value - .saturating_mul(10) - .saturating_add((input[pos] - b'0') as u32); - pos += 1; - } - NumberParse::Complete(value) -} - -/// The default TTY device. -pub static N_TTY: LazyLock> = LazyLock::new(new_n_tty); -static CONSOLE_INPUT_SOURCE: LazyLock> = LazyLock::new(|| Arc::new(PollSet::new())); -static CONSOLE_INPUT_NOTIFY: LazyLock> = - LazyLock::new(|| Arc::new(IrqNotify::new())); -static CONSOLE_NOTIFY_WORKER: AtomicBool = AtomicBool::new(false); - -fn handle_console_input_irq(_irq_num: usize) { - let events = ax_runtime::hal::console::handle_irq(); - if events.intersects( - ax_runtime::hal::console::ConsoleIrqEvent::RX_READY - | ax_runtime::hal::console::ConsoleIrqEvent::RX_ERROR - | ax_runtime::hal::console::ConsoleIrqEvent::OVERRUN, - ) { - CONSOLE_INPUT_NOTIFY.notify_irq(); - } -} - -unsafe fn handle_console_input_raw_irq( - ctx: ax_runtime::hal::irq::IrqContext, - _data: NonNull<()>, -) -> ax_runtime::hal::irq::IrqReturn { - handle_console_input_irq(ctx.irq.0); - ax_runtime::hal::irq::IrqReturn::Handled -} - -fn new_n_tty() -> Arc { - let terminal = { - let t = super::terminal::Terminal::default(); - - // Synchronously querying the connected terminal only works when the - // firmware/serial path can reliably supply a cursor-position response. - // Dynamic-platform QEMU tests run under ostool pipes, so keep the - // default 24x80 fallback there instead of stalling early Starry boot. - #[cfg(not(feature = "plat-dyn"))] - if let Some((rows, cols)) = query_console_size() { - *t.window_size.lock() = super::terminal::WindowSize { - ws_row: rows, - ws_col: cols, - ws_xpixel: 0, - ws_ypixel: 0, - }; - } - Arc::new(t) - }; - - Tty::new( - terminal, - TtyConfig { - reader: ConsoleReader::default(), - writer: Console, - process_mode: console_irq_mode().unwrap_or(ProcessMode::Manual), - }, - ) -} - -fn start_console_notify_worker() { - if CONSOLE_NOTIFY_WORKER.swap(true, Ordering::AcqRel) { - return; - } - ax_task::spawn_with_name( - || loop { - CONSOLE_INPUT_NOTIFY.wait(); - // Console RX readiness has been published by the IRQ handler. - unsafe { CONSOLE_INPUT_SOURCE.wake(IoEvents::IN) }; - }, - "console-notify".into(), - ); -} - -/// Probe the connected terminal for its current size using the -/// standard cursor-position-report sequence. -/// -/// Sequence: save cursor (DECSC) -> move to (9999, 9999) -> request -/// cursor position (CPR) -> restore cursor (DECRC). The terminal -/// clamps the move to its actual bottom-right corner before reporting -/// back, so the reply `\x1b[rows;colsR` reflects the real geometry. -/// Spin-waits up to roughly 100 ms for the reply and returns `None` -/// on timeout or parse failure. -/// -/// Called once during NTTY initialisation, before the polling reader -/// task is spawned, so there is no concurrent consumer racing on the -/// UART receive FIFO. -#[cfg(not(feature = "plat-dyn"))] -fn query_console_size() -> Option<(u16, u16)> { - ax_runtime::hal::console::write_bytes(b"\x1b7\x1b[9999;9999H\x1b[6n\x1b8"); - - let mut buf = [0u8; 32]; - let mut len = 0usize; - - // Spin up to ~100 ms (in wall time, polled via ax_runtime::hal::time::wall_time) - // for the `R` terminator. Hosts that ignore CPR (jcode running under - // a non-interactive serial, automated CI runners) will time out and - // we fall back to the 24x80 default without blocking boot further. - let deadline = ax_runtime::hal::time::wall_time() + core::time::Duration::from_millis(100); - 'collect: while ax_runtime::hal::time::wall_time() < deadline { - let mut tmp = [0u8; 1]; - if ax_runtime::hal::console::read_bytes(&mut tmp) > 0 { - if len < buf.len() { - buf[len] = tmp[0]; - len += 1; - } else { - // Buffer full without seeing 'R'; give up rather than - // spinning until the deadline on a misbehaving terminal. - break 'collect; - } - if tmp[0] == b'R' { - break 'collect; - } - } - core::hint::spin_loop(); - } - - parse_console_size_response(&buf[..len]) -} - -#[cfg(any(test, not(feature = "plat-dyn")))] -fn parse_console_size_response(buf: &[u8]) -> Option<(u16, u16)> { - let r_pos = buf.iter().rposition(|&b| b == b'R')?; - let escape_pos = buf[..r_pos].windows(2).rposition(|w| w == b"\x1b[")?; - let inner = core::str::from_utf8(&buf[escape_pos + 2..r_pos]).ok()?; - let mut parts = inner.splitn(2, ';'); - let rows: u16 = parts.next()?.parse().ok()?; - let cols: u16 = parts.next()?.parse().ok()?; - if rows == 0 || cols == 0 { - return None; - } - Some((rows, cols)) -} - -fn console_irq_mode() -> Option { - let irq = ax_runtime::hal::console::irq_num()?; - if ax_runtime::hal::irq::request_shared_irq( - irq, - handle_console_input_raw_irq, - NonNull::dangling(), - ) - .is_err() - { - warn!("Failed to register console IRQ handler for irq {irq}, falling back to polling mode"); - return None; - } - - ax_runtime::hal::console::set_input_irq_enabled(true); - start_console_notify_worker(); - Some(ProcessMode::InterruptDriven(CONSOLE_INPUT_SOURCE.clone())) -} - -#[cfg(test)] -mod tests { - use super::{MouseEscapeFilter, parse_console_size_response}; - - #[test] - fn parses_cursor_position_response() { - assert_eq!( - parse_console_size_response(b"\x1b7\x1b[24;80R\x1b8"), - Some((24, 80)) - ); - } - - fn filter(input: &[u8]) -> alloc::vec::Vec { - let mut filter = MouseEscapeFilter::default(); - let mut out = alloc::vec![0; input.len()]; - let len = filter.filter_chunk(input, &mut out); - out.truncate(len); - out - } - - #[test] - fn mouse_filter_drops_sgr_click_wheel_and_side_button_reports() { - assert_eq!(filter(b"\x1b[<0;10;20M"), b""); - assert_eq!(filter(b"\x1b[<0;10;20m"), b""); - assert_eq!(filter(b"\x1b[<64;10;20M"), b""); - assert_eq!(filter(b"\x1b[<128;10;20M"), b""); - } - - #[test] - fn mouse_filter_drops_x10_report() { - assert_eq!(filter(b"\x1b[M !!"), b""); - } - - #[test] - fn mouse_filter_drops_urxvt_style_report() { - assert_eq!(filter(b"\x1b[35;10;20M"), b""); - assert_eq!(filter(b"\x1b[96;10;20M"), b""); - } - - #[test] - fn mouse_filter_preserves_keyboard_and_terminal_control_sequences() { - assert_eq!(filter(b"\x1b[A"), b"\x1b[A"); - assert_eq!(filter(b"\x1b[6n"), b"\x1b[6n"); - assert_eq!(filter(b"\x1b[1;1R"), b"\x1b[1;1R"); - assert_eq!(filter(b"\x1ba"), b"\x1ba"); - } - - #[test] - fn mouse_filter_preserves_incomplete_or_non_mouse_sequences() { - assert_eq!(filter(b"\x1b["), b"\x1b["); - assert_eq!(filter(b"\x1b[M!"), b"\x1b[M!"); - assert_eq!(filter(b"\x1b[1;2;3R"), b"\x1b[1;2;3R"); - assert_eq!(filter(b"\x1b[1;2;3M"), b"\x1b[1;2;3M"); - } - - #[test] - fn mouse_filter_removes_mouse_reports_from_mixed_stream() { - assert_eq!(filter(b"abc \x1b[<64;10;20Mdef\n"), b"abc def\n"); - } -} diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs index d6850958a0..24ad94e913 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs @@ -77,7 +77,10 @@ pub(crate) fn create_pty_pair() -> (Arc, Arc) { TtyConfig { reader: PtyReader::new(master_to_slave), writer: PtyWriter::new(slave_to_master, poll_rx_master), - process_mode: ProcessMode::InterruptDriven(poll_rx_slave), + process_mode: ProcessMode::InterruptDriven { + input: poll_rx_slave, + output: None, + }, }, ); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs new file mode 100644 index 0000000000..6411734625 --- /dev/null +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -0,0 +1,699 @@ +use alloc::{format, string::String, sync::Arc, vec, vec::Vec}; +use core::{ + ptr::NonNull, + sync::atomic::{AtomicBool, AtomicU32, Ordering}, +}; + +use ax_driver::serial::{ + self as ax_serial, BInterruptSerial, Config, RxFlag, RxItem, SerialDevice, SerialIrqOutcome, +}; +use ax_errno::{AxError, AxResult}; +use ax_kspin::SpinNoIrq; +use ax_runtime::hal::{ + console::{ConsoleDeviceIdError, ConsoleDeviceIdResult}, + irq::{AutoEnable, IrqHandle, IrqRequest, ShareMode}, +}; +use ax_sync::Mutex; +use ax_task::IrqNotify; +use axpoll::{IoEvents, PollSet}; +use bitflags::bitflags; +use rdrive::DeviceId as RDriveDeviceId; +use spin::LazyLock; +use starry_process::Process; + +use super::{ + Tty, + terminal::{ + Terminal, + ldisc::{ProcessMode, TtyConfig, TtyRead, TtyWrite}, + termios::Termios2, + }, +}; +use crate::pseudofs::DeviceOps; + +pub type SerialTtyDriver = Tty; + +const SERIAL_RX_DRAIN_CHUNK: usize = 256; +const SERIAL_SYNC_ECHO_LIMIT: usize = 256; + +bitflags! { + #[derive(Clone, Copy, Debug, Default)] + struct SerialEventBits: u32 { + const RX_READY = 1 << 0; + const TX_SPACE = 1 << 1; + const HANGUP = 1 << 2; + } +} + +pub struct SerialTtyEntry { + number: usize, + tty: Arc, + backend: Arc, +} + +impl SerialTtyEntry { + pub fn number(&self) -> usize { + self.number + } + + pub fn tty(&self) -> Arc { + self.tty.clone() + } +} + +struct SerialRegistry { + entries: Vec, + console_index: Option, +} + +struct SerialBackend { + name: String, + tty_name: String, + rdrive_device_id: RDriveDeviceId, + number: usize, + port: BInterruptSerial, + irq_num: usize, + irq_handle: SpinNoIrq>, + started: AtomicBool, + events: SerialEvents, + input_source: Arc, + output_source: Arc, + tx_notify: IrqNotify, + output_lock: Mutex<()>, +} + +struct SerialEvents { + pending: AtomicU32, + notify: IrqNotify, +} + +impl SerialEvents { + const fn new() -> Self { + Self { + pending: AtomicU32::new(0), + notify: IrqNotify::new(), + } + } + + fn publish_irq(&self, events: SerialEventBits) { + if events.is_empty() { + return; + } + self.pending.fetch_or(events.bits(), Ordering::Release); + self.notify.notify_irq(); + } + + fn publish(&self, events: SerialEventBits) { + if events.is_empty() { + return; + } + self.pending.fetch_or(events.bits(), Ordering::Release); + self.notify.notify(); + } + + fn wait(&self) { + self.notify.wait(); + } + + fn take(&self) -> SerialEventBits { + SerialEventBits::from_bits_retain(self.pending.swap(0, Ordering::AcqRel)) + } +} + +struct NoConsole; + +impl DeviceOps for NoConsole { + fn read_at(&self, _buf: &mut [u8], _offset: u64) -> AxResult { + Err(AxError::NoSuchDevice) + } + + fn write_at(&self, _buf: &[u8], _offset: u64) -> AxResult { + Err(AxError::NoSuchDevice) + } + + fn ioctl(&self, _cmd: u32, _arg: usize) -> AxResult { + Err(AxError::NoSuchDevice) + } + + fn open(&self, _exclusive: bool) -> AxResult<()> { + Err(AxError::NoSuchDevice) + } + + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Clone, Copy)] +struct ConsoleCandidate { + number: usize, + device_id: RDriveDeviceId, +} + +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +enum ConsoleSelection { + SelectedDevice(usize), + TtyS0Fallback(usize), +} + +impl ConsoleSelection { + fn index(&self) -> usize { + match self { + Self::SelectedDevice(index) | Self::TtyS0Fallback(index) => *index, + } + } +} + +#[derive(Clone)] +pub struct SerialReader { + backend: Arc, +} + +#[derive(Clone)] +pub struct SerialWriter { + backend: Arc, +} + +static SERIAL_REGISTRY: LazyLock = LazyLock::new(SerialRegistry::discover); + +pub fn serial_tty_entries() -> &'static [SerialTtyEntry] { + &SERIAL_REGISTRY.entries +} + +impl SerialTtyDriver { + pub fn serial_number(&self) -> usize { + self.writer.backend.number + } +} + +pub fn console_device() -> Arc { + SERIAL_REGISTRY + .console_index + .and_then(|index| SERIAL_REGISTRY.entries.get(index)) + .map(|entry| entry.tty() as Arc) + .unwrap_or_else(|| Arc::new(NoConsole)) +} + +pub fn bind_console_to(proc: &Process) -> AxResult<()> { + if let Some(index) = SERIAL_REGISTRY.console_index + && let Some(entry) = SERIAL_REGISTRY.entries.get(index) + { + return entry.tty.bind_to(proc); + } + Err(AxError::NoSuchDevice) +} + +pub fn arm_console_irq() { + if let Some(index) = SERIAL_REGISTRY.console_index + && let Some(entry) = SERIAL_REGISTRY.entries.get(index) + { + entry.backend.start_port(); + } +} + +impl SerialRegistry { + fn discover() -> Self { + let serials = ax_serial::take_serial_devices(); + let numbers = assign_tty_numbers( + serials + .iter() + .map(|serial| serial.alias_index()) + .collect::>() + .as_slice(), + ); + + let mut entries = Vec::new(); + for (serial, number) in serials.into_iter().zip(numbers) { + let Some(number) = number else { + warn!( + "Skipping serial device {} at {} because ttyS number could not be assigned", + serial.name(), + serial.fdt_path() + ); + continue; + }; + match new_serial_tty(number, serial) { + Ok(entry) => entries.push(entry), + Err(err) => warn!("Skipping ttyS{number}: {err:?}"), + } + } + entries.sort_by_key(|entry| entry.number); + + let candidates = entries + .iter() + .map(|entry| ConsoleCandidate { + number: entry.number, + device_id: entry.backend.rdrive_device_id, + }) + .collect::>(); + let console_selection = + select_console_candidate(&candidates, ax_runtime::hal::console::device_id()); + let console_index = console_selection.as_ref().map(ConsoleSelection::index); + if let Some(index) = console_index { + let number = entries[index].number; + match console_selection { + Some(ConsoleSelection::SelectedDevice(_)) => { + info!("/dev/console bound to ttyS{number}"); + } + Some(ConsoleSelection::TtyS0Fallback(_)) => { + info!("/dev/console bound to ttyS0"); + } + None => {} + } + } else { + warn!("/dev/console has no serial TTY binding"); + } + + Self { + entries, + console_index, + } + } +} + +fn new_serial_tty(number: usize, serial: SerialDevice) -> AxResult { + let tty_name = format!("ttyS{number}"); + let runtime = serial.into_runtime_port()?; + let name = runtime.name().into(); + let info = runtime.info().clone(); + let rdrive_device_id = runtime.rdrive_device_id(); + let Some(irq_num) = runtime.irq_num() else { + return Err(AxError::Unsupported); + }; + let port = runtime.port(); + let backend = Arc::new(SerialBackend { + name, + tty_name: tty_name.clone(), + rdrive_device_id, + number, + port, + irq_num, + irq_handle: SpinNoIrq::new(None), + started: AtomicBool::new(false), + events: SerialEvents::new(), + input_source: Arc::new(PollSet::new()), + output_source: Arc::new(PollSet::new()), + tx_notify: IrqNotify::new(), + output_lock: Mutex::new(()), + }); + + backend.register_irq()?; + if !backend.start_port() { + return Err(AxError::Unsupported); + } + spawn_serial_event_worker(backend.clone()); + + let terminal = Arc::new(Terminal::default()); + let entry_backend = backend.clone(); + let tty = Tty::new( + terminal, + TtyConfig { + reader: SerialReader { + backend: backend.clone(), + }, + writer: SerialWriter { backend }, + process_mode: ProcessMode::InterruptDriven { + input: entry_backend.input_source.clone(), + output: Some(entry_backend.output_source.clone()), + }, + }, + ); + info!( + "{} registered: path={}, alias={:?}, paddr={:#x}, mapped={:#x}, irq={:?}, mode=interrupt", + tty_name, info.fdt_path, info.alias_index, info.paddr, info.mapped_base, irq_num + ); + Ok(SerialTtyEntry { + number, + tty, + backend: entry_backend, + }) +} + +impl SerialBackend { + fn register_irq(self: &Arc) -> AxResult<()> { + let data = NonNull::new(Arc::into_raw(self.clone()) as *mut ()).unwrap(); + let request = IrqRequest::new(serial_raw_irq_handler, data) + .share_mode(ShareMode::Shared) + .auto_enable(AutoEnable::No); + match ax_runtime::hal::irq::request_irq(self.irq_num, request) { + Ok(handle) => { + *self.irq_handle.lock() = Some(handle); + Ok(()) + } + Err(err) => { + unsafe { + Arc::decrement_strong_count(data.as_ptr() as *const SerialBackend); + } + warn!( + "Failed to register {} IRQ handler for irq {}: {err:?}", + self.tty_name, self.irq_num + ); + Err(AxError::Unsupported) + } + } + } + + fn start_port(&self) -> bool { + if self.started.load(Ordering::Acquire) { + return true; + } + + let Some(handle) = *self.irq_handle.lock() else { + return false; + }; + + if let Err(err) = self + .port + .startup(&Config::new().baudrate(self.port.baudrate())) + { + warn!( + "{} failed to start serial port {}: {:?}", + self.tty_name, self.name, err + ); + return false; + } + + if let Err(err) = ax_runtime::hal::irq::enable_irq(handle) { + self.port.shutdown(); + warn!( + "Failed to enable {} IRQ handler for irq {}: {err:?}", + self.tty_name, self.irq_num + ); + return false; + } + + publish_serial_outcome(self, self.port.startup_catch_up(), false); + self.started.store(true, Ordering::Release); + true + } +} + +fn spawn_serial_event_worker(backend: Arc) { + let task_name = format!("{}-event", backend.tty_name); + ax_task::spawn_with_name( + move || loop { + backend.events.wait(); + loop { + let pending = backend.events.take(); + if pending.is_empty() { + break; + } + if pending.contains(SerialEventBits::RX_READY) { + unsafe { backend.input_source.wake(IoEvents::IN) }; + } + if pending.contains(SerialEventBits::TX_SPACE) { + backend.tx_notify.notify(); + unsafe { backend.output_source.wake(IoEvents::OUT) }; + } + } + }, + task_name, + ); +} + +unsafe fn serial_raw_irq_handler( + _ctx: ax_runtime::hal::irq::IrqContext, + data: NonNull<()>, +) -> ax_runtime::hal::irq::IrqReturn { + let backend = unsafe { &*(data.as_ptr() as *const SerialBackend) }; + let outcome = backend.port.handle_irq(); + if !outcome.claimed { + return ax_runtime::hal::irq::IrqReturn::Unhandled; + } + let events = publish_serial_outcome(backend, outcome, true); + if events.is_empty() { + ax_runtime::hal::irq::IrqReturn::Handled + } else { + ax_runtime::hal::irq::IrqReturn::Wake + } +} + +fn publish_serial_outcome( + backend: &SerialBackend, + outcome: SerialIrqOutcome, + from_irq: bool, +) -> SerialEventBits { + let mut events = SerialEventBits::empty(); + if outcome.rx_pushed > 0 { + events |= SerialEventBits::RX_READY; + } + if outcome.tx_wakeup { + events |= SerialEventBits::TX_SPACE; + } + + if from_irq { + backend.events.publish_irq(events); + } else { + backend.events.publish(events); + } + events +} + +impl TtyRead for SerialReader { + fn read(&mut self, buf: &mut [u8]) -> usize { + let mut total = 0; + let mut temp = [RxItem::default(); SERIAL_RX_DRAIN_CHUNK]; + + while total < buf.len() { + let limit = (buf.len() - total).min(temp.len()); + let read = self.backend.port.drain_rx(&mut temp[..limit]); + if read == 0 { + break; + } + for item in &temp[..read] { + match *item { + RxItem::Byte { + byte, + flag: RxFlag::Normal, + } => { + buf[total] = byte; + total += 1; + } + RxItem::Byte { byte, flag } => { + warn!( + "{} RX error {:?} while preserving byte {byte:#x}", + self.backend.tty_name, flag + ); + buf[total] = byte; + total += 1; + } + RxItem::Overrun => { + warn!("{} RX overrun", self.backend.tty_name); + } + } + } + } + + total + } +} + +impl TtyWrite for SerialWriter { + fn write(&self, buf: &[u8]) { + if buf.is_empty() { + return; + } + let _guard = self.backend.output_lock.lock(); + let mut written = 0; + while written < buf.len() { + let count = self.backend.port.try_write(&buf[written..]); + if count == 0 { + self.backend.tx_notify.wait(); + continue; + } + written += count; + } + } + + fn flush_echo_before_input(&self) -> bool { + true + } + + fn max_sync_echo_bytes(&self) -> usize { + SERIAL_SYNC_ECHO_LIMIT + } + + fn termios_changed(&self, old: &Termios2, new: &Termios2) { + let Some(new_baud) = new.baudrate() else { + return; + }; + if old.baudrate() == Some(new_baud) { + return; + } + if let Err(err) = self + .backend + .port + .set_config(&Config::new().baudrate(new_baud)) + { + warn!( + "{} failed to set baudrate {new_baud} on {}: {:?}", + self.backend.tty_name, self.backend.name, err + ); + } + } +} + +fn assign_tty_numbers(alias_indices: &[Option]) -> Vec> { + let mut assigned = vec![None; alias_indices.len()]; + let mut used = Vec::new(); + + for (device_index, alias) in alias_indices.iter().copied().enumerate() { + let Some(number) = alias else { + continue; + }; + if used.contains(&number) { + warn!("Duplicate FDT serial{number} alias ignored for later serial device"); + continue; + } + assigned[device_index] = Some(number); + used.push(number); + } + + let mut next = 0usize; + for number in &mut assigned { + if number.is_some() { + continue; + } + while used.contains(&next) { + next += 1; + } + *number = Some(next); + used.push(next); + } + + assigned +} + +fn select_console_candidate( + candidates: &[ConsoleCandidate], + selected_device_id: ConsoleDeviceIdResult, +) -> Option { + match selected_device_id { + Ok(device_id) => { + if let Some(index) = candidates + .iter() + .position(|candidate| candidate.device_id == device_id) + { + return Some(ConsoleSelection::SelectedDevice(index)); + } + warn!("selected console device {device_id:?} did not match a discovered serial TTY"); + None + } + Err(ConsoleDeviceIdError::NotSpecified) => candidates + .iter() + .position(|candidate| candidate.number == 0) + .map(ConsoleSelection::TtyS0Fallback), + Err( + err @ (ConsoleDeviceIdError::NoHardwareDevice | ConsoleDeviceIdError::DeviceNotFound), + ) => { + debug!("No hardware console TTY selected: {err:?}"); + None + } + } +} + +#[cfg(test)] +mod tests { + use rdrive::DeviceId as RDriveDeviceId; + + use super::{ + ConsoleCandidate, ConsoleDeviceIdError, ConsoleSelection, assign_tty_numbers, + select_console_candidate, + }; + + #[test] + fn aliases_keep_linux_ttys_numbering() { + assert_eq!(assign_tty_numbers(&[Some(0), Some(2)]), [Some(0), Some(2)]); + } + + #[test] + fn unaliased_serials_take_first_free_ttys_numbers() { + assert_eq!( + assign_tty_numbers(&[Some(0), None, Some(2), None]), + [Some(0), Some(1), Some(2), Some(3)] + ); + } + + #[test] + fn duplicate_alias_keeps_first_device_and_reassigns_later_one() { + assert_eq!( + assign_tty_numbers(&[Some(1), Some(1), None]), + [Some(1), Some(0), Some(2)] + ); + } + + #[test] + fn matching_device_id_wins_over_ttys0_fallback() { + let tty_s0 = RDriveDeviceId::from(10); + let tty_s1 = RDriveDeviceId::from(11); + let candidates = [ + ConsoleCandidate { + number: 0, + device_id: tty_s0, + }, + ConsoleCandidate { + number: 1, + device_id: tty_s1, + }, + ]; + + assert_eq!( + select_console_candidate(&candidates, Ok(tty_s1)), + Some(ConsoleSelection::SelectedDevice(1)) + ); + } + + #[test] + fn unmatched_device_id_keeps_dev_console_unbound() { + let tty_s0 = RDriveDeviceId::from(10); + let missing = RDriveDeviceId::from(99); + let candidates = [ConsoleCandidate { + number: 0, + device_id: tty_s0, + }]; + + assert_eq!(select_console_candidate(&candidates, Ok(missing)), None); + } + + #[test] + fn missing_device_id_falls_back_to_ttys0() { + let tty_s0 = RDriveDeviceId::from(10); + let candidates = [ConsoleCandidate { + number: 0, + device_id: tty_s0, + }]; + + assert_eq!( + select_console_candidate(&candidates, Err(ConsoleDeviceIdError::NotSpecified)), + Some(ConsoleSelection::TtyS0Fallback(0)) + ); + } + + #[test] + fn no_ttys0_keeps_dev_console_unbound() { + let tty_s1 = RDriveDeviceId::from(11); + let candidates = [ConsoleCandidate { + number: 1, + device_id: tty_s1, + }]; + + assert_eq!( + select_console_candidate(&candidates, Err(ConsoleDeviceIdError::NotSpecified)), + None + ); + } + + #[test] + fn non_hardware_console_keeps_dev_console_unbound() { + let tty_s0 = RDriveDeviceId::from(10); + let candidates = [ConsoleCandidate { + number: 0, + device_id: tty_s0, + }]; + + assert_eq!( + select_console_candidate(&candidates, Err(ConsoleDeviceIdError::NoHardwareDevice)), + None + ); + } +} diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs index ebe1344355..bc062e764f 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs @@ -1,13 +1,14 @@ -use alloc::{collections::VecDeque, sync::Arc, task::Wake, vec::Vec}; +use alloc::{boxed::Box, collections::VecDeque, sync::Arc, task::Wake, vec::Vec}; use core::{ future::poll_fn, marker::PhantomData, ops::Range, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, task::{Poll, Waker}, }; use ax_errno::{AxError, AxResult}; +use ax_kspin::SpinNoIrq; use ax_task::future::block_on; use axpoll::{IoEvents, PollSet}; use linux_raw_sys::general::{ @@ -22,22 +23,20 @@ use starry_signal::SignalInfo; use super::{Terminal, termios::Termios2}; use crate::task::send_signal_to_process_group; -const BUF_SIZE: usize = 80; +const BUF_SIZE: usize = 4096; +const ECHO_QUEUE_CAP: usize = 4096; +const ECHO_WRITE_CHUNK: usize = 256; type ReadBuf = Arc>; /// How should we process inputs? pub enum ProcessMode { - /// Process inputs without an external event source. - /// - /// This is used as the fallback for consoles without an RX interrupt. A - /// background task drains input directly and yields when idle, so signals - /// and serial auto-init commands still work while no user task is blocked - /// in `read()`. - Manual, /// Spawns task for processing inputs, relying on an external event source /// to wake it up. - InterruptDriven(Arc), + InterruptDriven { + input: Arc, + output: Option>, + }, /// Do not process inputs. /// /// This is only used by the master side of pseudo tty. The argument is the @@ -56,6 +55,20 @@ pub trait TtyRead: Send + Sync + 'static { } pub trait TtyWrite: Send + Sync + 'static { fn write(&self, buf: &[u8]); + + fn flush_echo_before_input(&self) -> bool { + false + } + + fn max_sync_echo_bytes(&self) -> usize { + if self.flush_echo_before_input() { + usize::MAX + } else { + 0 + } + } + + fn termios_changed(&self, _old: &Termios2, _new: &Termios2) {} } pub fn write_output_bytes(writer: &W, term: &Termios2, buf: &[u8]) { @@ -89,7 +102,7 @@ struct InputReader { terminal: Arc, reader: R, - writer: W, + echo: Arc>, buf_tx: CachingProd, read_buf: [u8; BUF_SIZE], @@ -113,6 +126,7 @@ impl InputReader { } let term = self.terminal.load_termios(); let mut sent = 0; + let mut echo = Vec::new(); loop { if let Some(offset) = &mut self.line_read { let read = self.buf_tx.push_slice(&self.line_buf[*offset..]); @@ -147,7 +161,7 @@ impl InputReader { let eof = term.canonical() && ch == term.special_char(VEOF); if term.echo() && !eof { - self.output_char(&term, ch); + Self::append_echo_char(&term, ch, &mut echo); } if signaled { self.line_buf.clear(); @@ -192,6 +206,14 @@ impl InputReader { } } + if !echo.is_empty() { + if echo.len() <= self.echo.max_sync_bytes() { + self.echo.write_now(&echo); + } else { + self.echo.enqueue(&echo); + } + } + sent > 0 || progressed } @@ -212,20 +234,23 @@ impl InputReader { } } - fn output_char(&self, term: &Termios2, ch: u8) { + fn append_echo_char(term: &Termios2, ch: u8, out: &mut Vec) { match ch { - b'\n' => write_output_bytes(&self.writer, term, b"\n"), - b'\t' => self.writer.write(b"\t"), - ch if ch == term.special_char(VERASE) => self.writer.write(b"\x08 \x08"), + b'\n' if term.has_oflag(OPOST) && term.has_oflag(ONLCR) => { + out.extend_from_slice(b"\r\n"); + } + b'\n' => out.push(b'\n'), + b'\t' => out.push(b'\t'), + ch if ch == term.special_char(VERASE) => out.extend_from_slice(b"\x08 \x08"), ch if ch == b' ' || ch.is_ascii_graphic() || !ch.is_ascii() => { - self.writer.write(&[ch]); + out.push(ch); } ch if ch.is_ascii_control() && term.has_lflag(ECHOCTL) => { let escaped = if ch == b'\x7f' { b'?' } else { ch + 0x40 }; - self.writer.write(&[b'^', escaped]); + out.extend_from_slice(&[b'^', escaped]); } ch if ch.is_ascii_control() => { - self.writer.write(&[ch]); + out.push(ch); } other => { warn!("Ignored echo char: {other:#x}"); @@ -234,6 +259,79 @@ impl InputReader { } } +struct EchoQueue { + writer: W, + queue: SpinNoIrq>, + wake_source: Arc, + dropped: AtomicUsize, +} + +impl EchoQueue { + fn new(writer: W, wake_source: Arc) -> Arc { + Arc::new(Self { + writer, + queue: SpinNoIrq::new(VecDeque::new()), + wake_source, + dropped: AtomicUsize::new(0), + }) + } + + fn enqueue(&self, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + + let queued = { + let mut queue = self.queue.lock(); + let space = ECHO_QUEUE_CAP.saturating_sub(queue.len()); + let queued = bytes.len().min(space); + queue.extend(bytes[..queued].iter().copied()); + queued + }; + + if queued < bytes.len() { + self.dropped + .fetch_add(bytes.len() - queued, Ordering::AcqRel); + } + unsafe { self.wake_source.wake(IoEvents::OUT) }; + } + + fn max_sync_bytes(&self) -> usize { + self.writer.max_sync_echo_bytes() + } + + fn write_now(&self, bytes: &[u8]) { + self.writer.write(bytes); + } + + fn drain_available(&self) -> bool { + let mut progressed = false; + loop { + let chunk = { + let mut queue = self.queue.lock(); + if queue.is_empty() { + break; + } + let len = queue.len().min(ECHO_WRITE_CHUNK); + let mut chunk = Vec::with_capacity(len); + for _ in 0..len { + chunk.push(queue.pop_front().unwrap()); + } + chunk + }; + self.writer.write(&chunk); + progressed = true; + } + + let dropped = self.dropped.swap(0, Ordering::AcqRel); + if dropped > 0 { + warn!("Dropped {dropped} tty echo byte(s)"); + progressed = true; + } + progressed + } +} + struct SimpleReader { reader: R, read_buf: [u8; BUF_SIZE], @@ -248,7 +346,7 @@ impl SimpleReader { enum Processor { InterruptDriven, - Passive(SimpleReader, Arc), + Passive(Box>, Arc), } pub struct LineDiscipline { @@ -256,7 +354,7 @@ pub struct LineDiscipline { buf_rx: CachingCons, injected_input: VecDeque, input_ready: Arc, - pump_retry: Arc, + worker_source: Arc, eof_ready: Arc, clear_line_buf: Arc, processor: Processor, @@ -282,19 +380,23 @@ impl Wake for WakeSignal { impl LineDiscipline { fn drive_input(reader: &mut InputReader, input_ready: &PollSet) -> bool { let mut progressed = false; + progressed |= reader.echo.drain_available(); while reader.drain_source_into_line_buffer() { progressed = true; + reader.echo.drain_available(); // New line-discipline input is visible before waking readers. unsafe { input_ready.wake(IoEvents::IN) }; } + progressed |= reader.echo.drain_available(); progressed } fn spawn_interrupt_driven_reader( mut reader: InputReader, input_source: Arc, + output_source: Option>, input_ready: Arc, - pump_retry: Arc, + worker_source: Arc, ) { ax_task::spawn_with_name( move || loop { @@ -314,7 +416,10 @@ impl LineDiscipline { })); // The reader task registers from ordinary task context. unsafe { input_source.register(&waker, IoEvents::IN) }; - unsafe { pump_retry.register(&waker, IoEvents::OUT) }; + if let Some(output_source) = output_source.as_ref() { + unsafe { output_source.register(&waker, IoEvents::OUT) }; + } + unsafe { worker_source.register(&waker, IoEvents::OUT) }; if Self::drive_input(&mut reader, input_ready.as_ref()) || fired.swap(false, Ordering::AcqRel) @@ -329,27 +434,19 @@ impl LineDiscipline { ); } - fn spawn_polling_reader(mut reader: InputReader, input_ready: Arc) { - ax_task::spawn_with_name( - move || loop { - if !Self::drive_input(&mut reader, input_ready.as_ref()) { - ax_task::yield_now(); - } - }, - "tty-poll-reader".into(), - ); - } - pub fn new(terminal: Arc, config: TtyConfig) -> Self { let (buf_tx, buf_rx) = ReadBuf::default().split(); let eof_ready = Arc::new(AtomicBool::new(false)); let clear_line_buf = Arc::new(AtomicBool::new(false)); + let input_ready = Arc::new(PollSet::new()); + let worker_source = Arc::new(PollSet::new()); + let echo = EchoQueue::new(config.writer, worker_source.clone()); let reader = InputReader { terminal: terminal.clone(), reader: config.reader, - writer: config.writer, + echo: echo.clone(), buf_tx, read_buf: [0; BUF_SIZE], @@ -361,30 +458,25 @@ impl LineDiscipline { clear_line_buf: clear_line_buf.clone(), }; - let input_ready = Arc::new(PollSet::new()); - let pump_retry = Arc::new(PollSet::new()); let processor = match config.process_mode { - ProcessMode::InterruptDriven(input_source) => { + ProcessMode::InterruptDriven { input, output } => { Self::spawn_interrupt_driven_reader( reader, - input_source, + input, + output, input_ready.clone(), - pump_retry.clone(), + worker_source.clone(), ); Processor::InterruptDriven } - ProcessMode::Manual => { - Self::spawn_polling_reader(reader, input_ready.clone()); - Processor::InterruptDriven - } ProcessMode::Passive(poll_rx) => { let InputReader { reader, buf_tx, .. } = reader; Processor::Passive( - SimpleReader { + Box::new(SimpleReader { reader, read_buf: [0; BUF_SIZE], buf_tx, - }, + }), poll_rx, ) } @@ -394,7 +486,7 @@ impl LineDiscipline { buf_rx, injected_input: VecDeque::new(), input_ready, - pump_retry, + worker_source, eof_ready, clear_line_buf, processor, @@ -503,21 +595,22 @@ impl LineDiscipline { let read = self.buf_rx.pop_slice(buf); // Buffer space was freed before waking the input pump. - unsafe { self.pump_retry.wake(IoEvents::OUT) }; + unsafe { self.worker_source.wake(IoEvents::OUT) }; Ok(read) } } #[cfg(test)] mod tests { - use alloc::{sync::Arc, vec::Vec}; - use core::sync::atomic::AtomicBool; + use alloc::{sync::Arc, vec, vec::Vec}; + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use axpoll::PollSet; use ringbuf::traits::{Observer, Split}; use super::{ - BUF_SIZE, InputReader, LineDiscipline, ProcessMode, ReadBuf, TtyConfig, TtyRead, TtyWrite, + BUF_SIZE, EchoQueue, InputReader, LineDiscipline, ProcessMode, ReadBuf, TtyConfig, TtyRead, + TtyWrite, }; use crate::pseudofs::dev::tty::terminal::Terminal; @@ -545,6 +638,58 @@ mod tests { fn write(&self, _buf: &[u8]) {} } + struct CountingWriter { + calls: Arc, + bytes: Arc, + } + + impl TtyWrite for CountingWriter { + fn write(&self, buf: &[u8]) { + self.calls.fetch_add(1, Ordering::Relaxed); + self.bytes.fetch_add(buf.len(), Ordering::Relaxed); + } + } + + struct OrderedEchoWriter { + calls: Arc, + bytes: Arc, + } + + impl TtyWrite for OrderedEchoWriter { + fn write(&self, buf: &[u8]) { + self.calls.fetch_add(1, Ordering::Relaxed); + self.bytes.fetch_add(buf.len(), Ordering::Relaxed); + } + + fn flush_echo_before_input(&self) -> bool { + true + } + } + + struct LimitedEchoWriter { + calls: Arc, + bytes: Arc, + limit: usize, + } + + impl TtyWrite for LimitedEchoWriter { + fn write(&self, buf: &[u8]) { + self.calls.fetch_add(1, Ordering::Relaxed); + self.bytes.fetch_add(buf.len(), Ordering::Relaxed); + } + + fn max_sync_echo_bytes(&self) -> usize { + self.limit + } + } + + struct PanicWriter; + impl TtyWrite for PanicWriter { + fn write(&self, _buf: &[u8]) { + panic!("canonical input drain must not synchronously write echo bytes"); + } + } + fn make_reader( data: Vec, ) -> ( @@ -555,7 +700,7 @@ mod tests { let reader = InputReader { terminal: Arc::new(Terminal::default()), reader: MockReader::new(data), - writer: MockWriter, + echo: EchoQueue::new(MockWriter, Arc::new(PollSet::new())), buf_tx, read_buf: [0; BUF_SIZE], read_range: 0..0, @@ -603,6 +748,158 @@ mod tests { ); } + #[test] + fn canonical_echo_is_batched_after_input_progress() { + let (buf_tx, rx) = ReadBuf::default().split(); + let calls = Arc::new(AtomicUsize::new(0)); + let bytes = Arc::new(AtomicUsize::new(0)); + let mut reader = InputReader { + terminal: Arc::new(Terminal::default()), + reader: MockReader::new(b"hello\n".to_vec()), + echo: EchoQueue::new( + CountingWriter { + calls: calls.clone(), + bytes: bytes.clone(), + }, + Arc::new(PollSet::new()), + ), + buf_tx, + read_buf: [0; BUF_SIZE], + read_range: 0..0, + line_buf: Vec::new(), + line_read: None, + eof_ready: Arc::new(AtomicBool::new(false)), + clear_line_buf: Arc::new(AtomicBool::new(false)), + }; + + assert!(reader.drain_source_into_line_buffer()); + assert_eq!(rx.occupied_len(), b"hello\n".len()); + assert_eq!(calls.load(Ordering::Relaxed), 0); + assert_eq!(bytes.load(Ordering::Relaxed), 0); + + reader.echo.drain_available(); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(bytes.load(Ordering::Relaxed), b"hello\r\n".len()); + } + + #[test] + fn canonical_echo_can_be_flushed_before_input_is_returned() { + let (buf_tx, rx) = ReadBuf::default().split(); + let calls = Arc::new(AtomicUsize::new(0)); + let bytes = Arc::new(AtomicUsize::new(0)); + let mut reader = InputReader { + terminal: Arc::new(Terminal::default()), + reader: MockReader::new(b"echo marker\n".to_vec()), + echo: EchoQueue::new( + OrderedEchoWriter { + calls: calls.clone(), + bytes: bytes.clone(), + }, + Arc::new(PollSet::new()), + ), + buf_tx, + read_buf: [0; BUF_SIZE], + read_range: 0..0, + line_buf: Vec::new(), + line_read: None, + eof_ready: Arc::new(AtomicBool::new(false)), + clear_line_buf: Arc::new(AtomicBool::new(false)), + }; + + assert!(reader.drain_source_into_line_buffer()); + assert_eq!(rx.occupied_len(), b"echo marker\n".len()); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(bytes.load(Ordering::Relaxed), b"echo marker\r\n".len()); + } + + #[test] + fn canonical_small_echo_respects_sync_limit() { + let (buf_tx, rx) = ReadBuf::default().split(); + let calls = Arc::new(AtomicUsize::new(0)); + let bytes = Arc::new(AtomicUsize::new(0)); + let mut reader = InputReader { + terminal: Arc::new(Terminal::default()), + reader: MockReader::new(b"echo marker\n".to_vec()), + echo: EchoQueue::new( + LimitedEchoWriter { + calls: calls.clone(), + bytes: bytes.clone(), + limit: 64, + }, + Arc::new(PollSet::new()), + ), + buf_tx, + read_buf: [0; BUF_SIZE], + read_range: 0..0, + line_buf: Vec::new(), + line_read: None, + eof_ready: Arc::new(AtomicBool::new(false)), + clear_line_buf: Arc::new(AtomicBool::new(false)), + }; + + assert!(reader.drain_source_into_line_buffer()); + assert_eq!(rx.occupied_len(), b"echo marker\n".len()); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(bytes.load(Ordering::Relaxed), b"echo marker\r\n".len()); + } + + #[test] + fn canonical_large_echo_exceeding_sync_limit_is_queued() { + let (buf_tx, rx) = ReadBuf::default().split(); + let calls = Arc::new(AtomicUsize::new(0)); + let bytes = Arc::new(AtomicUsize::new(0)); + let mut input = vec![b'a'; 128]; + input.push(b'\n'); + let mut reader = InputReader { + terminal: Arc::new(Terminal::default()), + reader: MockReader::new(input), + echo: EchoQueue::new( + LimitedEchoWriter { + calls: calls.clone(), + bytes: bytes.clone(), + limit: 64, + }, + Arc::new(PollSet::new()), + ), + buf_tx, + read_buf: [0; BUF_SIZE], + read_range: 0..0, + line_buf: Vec::new(), + line_read: None, + eof_ready: Arc::new(AtomicBool::new(false)), + clear_line_buf: Arc::new(AtomicBool::new(false)), + }; + + assert!(reader.drain_source_into_line_buffer()); + assert_eq!(rx.occupied_len(), 129); + assert_eq!(calls.load(Ordering::Relaxed), 0); + assert_eq!(bytes.load(Ordering::Relaxed), 0); + + reader.echo.drain_available(); + assert_eq!(calls.load(Ordering::Relaxed), 1); + assert_eq!(bytes.load(Ordering::Relaxed), 130); + } + + #[test] + fn canonical_input_progress_does_not_wait_for_echo_writer() { + let (buf_tx, rx) = ReadBuf::default().split(); + let mut reader = InputReader { + terminal: Arc::new(Terminal::default()), + reader: MockReader::new(b"burst\n".to_vec()), + echo: EchoQueue::new(PanicWriter, Arc::new(PollSet::new())), + buf_tx, + read_buf: [0; BUF_SIZE], + read_range: 0..0, + line_buf: Vec::new(), + line_read: None, + eof_ready: Arc::new(AtomicBool::new(false)), + clear_line_buf: Arc::new(AtomicBool::new(false)), + }; + + assert!(reader.drain_source_into_line_buffer()); + assert_eq!(rx.occupied_len(), b"burst\n".len()); + } + #[test] fn injected_input_is_readable_immediately() { let mut ldisc = LineDiscipline::new( diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/termios.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/termios.rs index 86c10b09b2..142636a921 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/termios.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/termios.rs @@ -4,9 +4,11 @@ use core::ops::{Deref, DerefMut}; use bytemuck::AnyBitPattern; use linux_raw_sys::general::{ - B38400, CREAD, CS8, ECHO, ECHOCTL, ECHOE, ECHOK, ECHOKE, ICANON, ICRNL, IEXTEN, ISIG, IXON, - ONLCR, OPOST, VDISCARD, VEOF, VEOL, VEOL2, VERASE, VINTR, VKILL, VLNEXT, VQUIT, VREPRINT, - VSUSP, VWERASE, speed_t, tcflag_t, + B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, + B38400, B57600, B115200, B230400, B460800, B500000, B576000, B921600, B1000000, B1152000, + B1500000, B2000000, B2500000, B3000000, B3500000, B4000000, BOTHER, CBAUD, CREAD, CS8, ECHO, + ECHOCTL, ECHOE, ECHOK, ECHOKE, ICANON, ICRNL, IEXTEN, ISIG, IXON, ONLCR, OPOST, VDISCARD, VEOF, + VEOL, VEOL2, VERASE, VINTR, VKILL, VLNEXT, VQUIT, VREPRINT, VSUSP, VWERASE, speed_t, tcflag_t, }; use starry_signal::Signo; @@ -73,6 +75,10 @@ impl Termios { self.c_cflag & flag != 0 } + pub fn cflag(&self) -> tcflag_t { + self.c_cflag + } + pub fn has_lflag(&self, flag: u32) -> bool { self.c_lflag & flag != 0 } @@ -132,6 +138,62 @@ impl Termios2 { c_ospeed: B38400, } } + + pub fn input_speed(&self) -> speed_t { + self.c_ispeed + } + + pub fn output_speed(&self) -> speed_t { + self.c_ospeed + } + + pub fn baudrate(&self) -> Option { + let speed = if self.output_speed() != 0 { + self.output_speed() + } else { + self.input_speed() + }; + if speed != 0 && self.cflag() & CBAUD == BOTHER { + return Some(speed); + } + baudrate_from_constant(self.cflag() & CBAUD) + } +} + +fn baudrate_from_constant(speed: speed_t) -> Option { + Some(match speed { + B50 => 50, + B75 => 75, + B110 => 110, + B134 => 134, + B150 => 150, + B200 => 200, + B300 => 300, + B600 => 600, + B1200 => 1200, + B1800 => 1800, + B2400 => 2400, + B4800 => 4800, + B9600 => 9600, + B19200 => 19200, + B38400 => 38400, + B57600 => 57600, + B115200 => 115200, + B230400 => 230400, + B460800 => 460800, + B500000 => 500000, + B576000 => 576000, + B921600 => 921600, + B1000000 => 1000000, + B1152000 => 1152000, + B1500000 => 1500000, + B2000000 => 2000000, + B2500000 => 2500000, + B3000000 => 3000000, + B3500000 => 3500000, + B4000000 => 4000000, + _ => return None, + }) } impl Deref for Termios2 { diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty_serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty_serial.rs deleted file mode 100644 index 9fe79f192a..0000000000 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty_serial.rs +++ /dev/null @@ -1,408 +0,0 @@ -use core::{ - any::Any, - ptr::NonNull, - sync::atomic::{AtomicBool, AtomicUsize, Ordering}, - task::Context, -}; - -use ax_errno::{AxError, LinuxError}; -use ax_kspin::SpinNoIrq; -use ax_memory_addr::{PhysAddr, pa}; -use ax_sync::Mutex; -use ax_task::IrqNotify; -use axfs_ng_vfs::{NodeFlags, VfsResult}; -use axpoll::{IoEvents, PollSet, Pollable}; -use bytemuck::AnyBitPattern; -use sg200x_bsp::{ - pinmux::Pinmux, - soc::{FMUX_BASE, IOBLK_BASE, IOBLK_GRTC_BASE}, -}; -use some_serial::ns16550::dw_apb::{DwApbUart, SG2002_UART_CLOCK}; -use starry_vm::{VmMutPtr, VmPtr}; - -use crate::pseudofs::{DeviceOps, dev::irq_byte_ring::ByteRing}; - -const UART1_PADDR: PhysAddr = pa!(0x04150000); -const UART2_PADDR: PhysAddr = pa!(0x04160000); -const UART1_IRQ: usize = 45; -const UART2_IRQ: usize = 46; -const RX_BUF_CAP: usize = 4096; - -/// MMIO span of a single UART block. Covers the DW APB shadow registers (USR at -/// 0x7c etc.) — one page is more than enough and keeps the mapping page-aligned. -const UART_MMIO_SIZE: usize = 0x1000; -/// MMIO span covering the pinmux register groups. FMUX (0x03001000) and the -/// Active-Domain IOBLK groups (0x03001800 + G1/G7/G10/G12 offsets) live in the -/// same 4K page; GRTC (0x05027000) is mapped separately. -const PINMUX_MMIO_SIZE: usize = 0x1000; - -static UART1_RX_BUF: SpinNoIrq> = SpinNoIrq::new(ByteRing::new()); -static UART2_RX_BUF: SpinNoIrq> = SpinNoIrq::new(ByteRing::new()); -static UART1_POLL: PollSet = PollSet::new(); -static UART2_POLL: PollSet = PollSet::new(); -static UART1_NOTIFY: IrqNotify = IrqNotify::new(); -static UART2_NOTIFY: IrqNotify = IrqNotify::new(); -static UART1_NOTIFY_WORKER: AtomicBool = AtomicBool::new(false); -static UART2_NOTIFY_WORKER: AtomicBool = AtomicBool::new(false); -/// Mapped virtual base of each UART, published by `TtySerial::new` so the raw -/// IRQ handlers can reach the registers without recomputing a (now invalid on -/// dynamic platforms) `phys_to_virt` address. -static UART1_VADDR: AtomicUsize = AtomicUsize::new(0); -static UART2_VADDR: AtomicUsize = AtomicUsize::new(0); - -/// Map a physical MMIO region into the kernel address space and return its -/// virtual base. Unlike `phys_to_virt`, this works on dynamic platforms where -/// `PHYS_VIRT_OFFSET == 0` and there is no static linear MMIO window — `iomap` -/// installs a real device mapping and is idempotent for already-mapped pages. -fn iomap_usize(paddr: PhysAddr, size: usize) -> usize { - ax_mm::iomap(paddr, size) - .unwrap_or_else(|err| panic!("failed to iomap MMIO at {paddr:#x}+{size:#x}: {err:?}")) - .as_usize() -} - -fn uart_irq_handler(vaddr: usize, buf: &SpinNoIrq>, notify: &IrqNotify) { - let mut uart = DwApbUart::new(vaddr); - let mut rx = buf.lock(); - let mut got_data = false; - while let Some(c) = uart.getchar() { - let _ = rx.push_back(c); - got_data = true; - } - uart.set_ier(true); - drop(rx); - if got_data { - notify.notify_irq(); - } -} - -fn uart1_irq_handler(_irq: usize) { - uart_irq_handler( - UART1_VADDR.load(Ordering::Relaxed), - &UART1_RX_BUF, - &UART1_NOTIFY, - ); -} -fn uart2_irq_handler(_irq: usize) { - uart_irq_handler( - UART2_VADDR.load(Ordering::Relaxed), - &UART2_RX_BUF, - &UART2_NOTIFY, - ); -} - -unsafe fn uart1_raw_irq_handler( - ctx: ax_runtime::hal::irq::IrqContext, - _data: NonNull<()>, -) -> ax_runtime::hal::irq::IrqReturn { - uart1_irq_handler(ctx.irq.0); - ax_runtime::hal::irq::IrqReturn::Handled -} - -unsafe fn uart2_raw_irq_handler( - ctx: ax_runtime::hal::irq::IrqContext, - _data: NonNull<()>, -) -> ax_runtime::hal::irq::IrqReturn { - uart2_irq_handler(ctx.irq.0); - ax_runtime::hal::irq::IrqReturn::Handled -} - -#[repr(C)] -#[derive(Clone, Copy, AnyBitPattern)] -struct RawTermios { - c_iflag: u32, - c_oflag: u32, - c_cflag: u32, - c_lflag: u32, - c_line: u8, - c_cc: [u8; 19], -} - -#[repr(C)] -#[derive(Clone, Copy, AnyBitPattern)] -struct RawTermios2 { - base: RawTermios, - c_ispeed: u32, - c_ospeed: u32, -} - -impl RawTermios { - fn raw(baud_cflag: u32) -> Self { - Self { - c_iflag: 0, - c_oflag: 0, - c_cflag: 0o000060 | 0o000200 | baud_cflag, - c_lflag: 0, - c_line: 0, - c_cc: [0; 19], - } - } -} - -impl RawTermios2 { - fn new(base: RawTermios, speed: u32) -> Self { - Self { - base, - c_ispeed: speed, - c_ospeed: speed, - } - } - fn speed(&self) -> u32 { - self.c_ospeed - } -} - -#[repr(C)] -#[derive(Clone, Copy, Default, AnyBitPattern)] -struct WinSize { - ws_row: u16, - ws_col: u16, - ws_xpixel: u16, - ws_ypixel: u16, -} - -struct SerialConfig { - termios2: RawTermios2, - winsize: WinSize, -} - -pub struct TtySerial { - vaddr: usize, - irq: usize, - rx_buf: &'static SpinNoIrq>, - poll_set: &'static PollSet, - config: Mutex, -} - -struct UartPort { - paddr: PhysAddr, - irq: usize, - rx_buf: &'static SpinNoIrq>, - poll_set: &'static PollSet, - notify: &'static IrqNotify, - worker_started: &'static AtomicBool, - vaddr_slot: &'static AtomicUsize, - irq_handler: ax_runtime::hal::irq::RawIrqHandler, - worker_name: &'static str, -} - -fn start_uart_notify_worker( - poll_set: &'static PollSet, - notify: &'static IrqNotify, - started: &'static AtomicBool, - name: &'static str, -) { - if started.swap(true, Ordering::AcqRel) { - return; - } - ax_task::spawn_with_name( - move || loop { - notify.wait(); - // UART bytes are already in the RX buffer before the deferred wake. - unsafe { poll_set.wake(IoEvents::IN) }; - }, - name.into(), - ); -} - -impl TtySerial { - fn new(port: &'static UartPort, baud: u32) -> Self { - let vaddr = iomap_usize(port.paddr, UART_MMIO_SIZE); - // Publish the mapped base before enabling the IRQ so the handler never - // observes a zero (unmapped) address. - port.vaddr_slot.store(vaddr, Ordering::Relaxed); - let mut uart = DwApbUart::new(vaddr); - uart.init_with_baud_clk(baud, SG2002_UART_CLOCK); - uart.set_ier(true); - let _ = ax_runtime::hal::irq::request_shared_irq( - port.irq, - port.irq_handler, - NonNull::dangling(), - ) - .map_err(|err| warn!("failed to request serial IRQ {}: {err:?}", port.irq)); - ax_runtime::hal::irq::set_enable(port.irq, true); - start_uart_notify_worker( - port.poll_set, - port.notify, - port.worker_started, - port.worker_name, - ); - Self { - vaddr, - irq: port.irq, - rx_buf: port.rx_buf, - poll_set: port.poll_set, - config: Mutex::new(SerialConfig { - termios2: RawTermios2::new(RawTermios::raw(0), baud), - winsize: WinSize::default(), - }), - } - } - - fn set_baud(&self, baud: u32) { - let mut uart = DwApbUart::new(self.vaddr); - uart.init_with_baud_clk(baud, SG2002_UART_CLOCK); - uart.set_ier(true); - ax_runtime::hal::irq::set_enable(self.irq, true); - } -} - -impl DeviceOps for TtySerial { - fn read_at(&self, buf: &mut [u8], _offset: u64) -> VfsResult { - if buf.is_empty() { - return Ok(0); - } - // Non-blocking drain only. The blocking/`O_NONBLOCK` semantics are - // handled one layer up in `File::read`, which wraps this in - // `poll_io(.., self.nonblocking(), ..)`. Blocking here too (the old - // `block_on(poll_io(.., false, ..))`) ignored the fd's `O_NONBLOCK` - // and made non-blocking reads of an idle UART hang forever. - let mut rx = self.rx_buf.lock(); - if rx.is_empty() { - return Err(AxError::WouldBlock); - } - let n = buf.len().min(rx.len()); - rx.drain_into(&mut buf[..n]); - Ok(n) - } - - fn write_at(&self, buf: &[u8], _offset: u64) -> VfsResult { - let mut uart = DwApbUart::new(self.vaddr); - for &b in buf { - uart.putchar(b); - } - Ok(buf.len()) - } - - fn ioctl(&self, cmd: u32, arg: usize) -> VfsResult { - use linux_raw_sys::ioctl::*; - match cmd { - TCGETS => { - let cfg = self.config.lock(); - (arg as *mut RawTermios).vm_write(cfg.termios2.base)?; - } - TCGETS2 => { - let cfg = self.config.lock(); - (arg as *mut RawTermios2).vm_write(cfg.termios2)?; - } - TCSETS | TCSETSF | TCSETSW => { - let new_termios: RawTermios = (arg as *const RawTermios).vm_read()?; - let mut cfg = self.config.lock(); - let speed = cfg.termios2.speed(); - cfg.termios2 = RawTermios2::new(new_termios, speed); - if cmd == TCSETSF { - self.rx_buf.lock().clear(); - } - } - TCSETS2 | TCSETSF2 | TCSETSW2 => { - let new_termios2: RawTermios2 = (arg as *const RawTermios2).vm_read()?; - let old_speed = self.config.lock().termios2.speed(); - let new_speed = new_termios2.speed(); - { - let mut cfg = self.config.lock(); - cfg.termios2 = new_termios2; - if cmd == TCSETSF2 { - self.rx_buf.lock().clear(); - } - } - if new_speed != 0 && new_speed != old_speed { - self.set_baud(new_speed); - } - } - TIOCGWINSZ => { - let cfg = self.config.lock(); - (arg as *mut WinSize).vm_write(cfg.winsize)?; - } - TIOCSWINSZ => { - let ws: WinSize = (arg as *const WinSize).vm_read()?; - self.config.lock().winsize = ws; - } - TCFLSH => { - if arg == 0 || arg == 2 { - self.rx_buf.lock().clear(); - } - } - TCSBRK | TCSBRKP | TCXONC => {} - _ => return Err(LinuxError::ENOTTY.into()), - } - Ok(0) - } - - fn as_pollable(&self) -> Option<&dyn Pollable> { - Some(self) - } - fn as_any(&self) -> &dyn Any { - self - } - fn flags(&self) -> NodeFlags { - NodeFlags::NON_CACHEABLE | NodeFlags::STREAM - } -} - -impl Pollable for TtySerial { - fn poll(&self) -> IoEvents { - let rx = self.rx_buf.lock(); - let mut events = IoEvents::OUT; - if !rx.is_empty() { - events |= IoEvents::IN; - } - events - } - - fn register(&self, cx: &mut Context<'_>, events: IoEvents) { - if events.intersects(IoEvents::IN) { - // Serial poll registration happens from task context. - unsafe { self.poll_set.register(cx.waker(), IoEvents::IN) }; - } - } -} - -/// Map the pinmux register groups and build a `Pinmux` over the mapped virtual -/// bases. FMUX and the Active-Domain IOBLK groups share a page, so the two -/// `iomap` calls resolve to the same mapping (idempotent); GRTC is its own page. -fn map_pinmux() -> Pinmux { - let fmux_vaddr = iomap_usize(pa!(FMUX_BASE), PINMUX_MMIO_SIZE); - let ioblk_vaddr = iomap_usize(pa!(IOBLK_BASE), PINMUX_MMIO_SIZE); - let ioblk_grtc_vaddr = iomap_usize(pa!(IOBLK_GRTC_BASE), PINMUX_MMIO_SIZE); - unsafe { Pinmux::new(fmux_vaddr, ioblk_vaddr, ioblk_grtc_vaddr) } -} - -static UART1_PORT: UartPort = UartPort { - paddr: UART1_PADDR, - irq: UART1_IRQ, - rx_buf: &UART1_RX_BUF, - poll_set: &UART1_POLL, - notify: &UART1_NOTIFY, - worker_started: &UART1_NOTIFY_WORKER, - vaddr_slot: &UART1_VADDR, - irq_handler: uart1_raw_irq_handler, - worker_name: "uart1-notify", -}; - -static UART2_PORT: UartPort = UartPort { - paddr: UART2_PADDR, - irq: UART2_IRQ, - rx_buf: &UART2_RX_BUF, - poll_set: &UART2_POLL, - notify: &UART2_NOTIFY, - worker_started: &UART2_NOTIFY_WORKER, - vaddr_slot: &UART2_VADDR, - irq_handler: uart2_raw_irq_handler, - worker_name: "uart2-notify", -}; - -pub fn new_tty_s1(baud: u32) -> TtySerial { - map_pinmux().set_uart1(); - TtySerial::new(&UART1_PORT, baud) -} - -pub fn new_tty_s2(baud: u32) -> TtySerial { - use sg200x_bsp::pinmux::{FMUX_IIC0_SCL, FMUX_IIC0_SDA}; - let pinmux = map_pinmux(); - // Wire UART2 to IIC0_SCL/SDA (0x03001070/74), matching the - // original StarryOS sg2002 board layout: SCL → UART2_TX, - // SDA → UART2_RX. Wrong pinmux (e.g. pwr_gpio0/1) sends bytes - // to floating pads and the connected device never sees them. - pinmux.set_iic0_scl_func(FMUX_IIC0_SCL::FSEL::Value::UART2_TX); - pinmux.set_iic0_sda_func(FMUX_IIC0_SDA::FSEL::Value::UART2_RX); - TtySerial::new(&UART2_PORT, baud) -} diff --git a/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs b/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs index 97a422d496..211dc3b9e8 100644 --- a/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs +++ b/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs @@ -152,13 +152,10 @@ fn add_to_fd(result: OpenResult, flags: u32) -> AxResult { .session() .terminal() .ok_or(AxError::NotFound)?; - let path = if term.is::() { - "/dev/console".to_string() - } else if let Some(pts) = term.downcast_ref::() { - format!("/dev/pts/{}", pts.pty_number()) - } else { - panic!("unknown terminal type") - }; + let path = tty::terminal_device_path(term.as_ref()).ok_or_else(|| { + warn!("unknown controlling terminal type for /dev/tty"); + AxError::BadState + })?; let loc = FS_CONTEXT.lock().resolve(&path)?; file = ax_fs_ng::vfs::File::new(FileBackend::Direct(loc), file.flags()); } diff --git a/os/StarryOS/kernel/src/syscall/task/ptrace.rs b/os/StarryOS/kernel/src/syscall/task/ptrace.rs index 816a083548..93494a460a 100644 --- a/os/StarryOS/kernel/src/syscall/task/ptrace.rs +++ b/os/StarryOS/kernel/src/syscall/task/ptrace.rs @@ -1184,7 +1184,7 @@ pub fn ptrace_setup_singlestep( let _ = ptrace_write_u16_unlocked(&mut aspace, saved_addr, saved_insn as u16); } - let first_half = match ptrace_read_u16_unlocked(&aspace, pc) { + let first_half = match ptrace_read_u16_unlocked(&mut aspace, pc) { Ok(half) => half, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1195,7 +1195,7 @@ pub fn ptrace_setup_singlestep( let current_insn = if insn_len == 2 { first_half as u32 } else { - match ptrace_read_u32_unlocked(&aspace, pc) { + match ptrace_read_u32_unlocked(&mut aspace, pc) { Ok(word) => word, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1208,7 +1208,7 @@ pub fn ptrace_setup_singlestep( tracee.set_ptrace_ss_saved_insn_for(tid, None); return; } - let orig_insn = match ptrace_read_u16_unlocked(&aspace, next_insn_addr) { + let orig_insn = match ptrace_read_u16_unlocked(&mut aspace, next_insn_addr) { Ok(half) => half, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1221,7 +1221,10 @@ pub fn ptrace_setup_singlestep( return; } - let _ = ptrace_write_u16_unlocked(&mut aspace, next_insn_addr, EBREAK_INSN); + if ptrace_write_u16_unlocked(&mut aspace, next_insn_addr, EBREAK_INSN).is_err() { + tracee.set_ptrace_ss_saved_insn_for(tid, None); + return; + } tracee.set_ptrace_ss_saved_insn_for(tid, Some((next_insn_addr, orig_insn as usize))); ax_runtime::hal::cpu::asm::flush_icache_all(); } @@ -1241,7 +1244,7 @@ pub fn ptrace_setup_singlestep( let _ = ptrace_write_u32_unlocked(&mut aspace, saved_addr, saved_insn as u32); } - let current_insn = match ptrace_read_u32_unlocked(&aspace, pc) { + let current_insn = match ptrace_read_u32_unlocked(&mut aspace, pc) { Ok(insn) => insn, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1249,7 +1252,7 @@ pub fn ptrace_setup_singlestep( } }; let next_insn_addr = aarch64_next_pc(current_insn, pc, uctx); - let orig_insn = match ptrace_read_u32_unlocked(&aspace, next_insn_addr) { + let orig_insn = match ptrace_read_u32_unlocked(&mut aspace, next_insn_addr) { Ok(insn) => insn, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1262,7 +1265,10 @@ pub fn ptrace_setup_singlestep( return; } - let _ = ptrace_write_u32_unlocked(&mut aspace, next_insn_addr, AARCH64_BRK_INSN); + if ptrace_write_u32_unlocked(&mut aspace, next_insn_addr, AARCH64_BRK_INSN).is_err() { + tracee.set_ptrace_ss_saved_insn_for(tid, None); + return; + } tracee.set_ptrace_ss_saved_insn_for(tid, Some((next_insn_addr, orig_insn as usize))); ax_runtime::hal::cpu::asm::flush_icache_all(); } @@ -1282,7 +1288,7 @@ pub fn ptrace_setup_singlestep( let _ = ptrace_write_u32_unlocked(&mut aspace, saved_addr, saved_insn as u32); } - let current_insn = match ptrace_read_u32_unlocked(&aspace, pc) { + let current_insn = match ptrace_read_u32_unlocked(&mut aspace, pc) { Ok(insn) => insn, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1290,7 +1296,7 @@ pub fn ptrace_setup_singlestep( } }; let next_insn_addr = loongarch_next_pc(current_insn, pc, uctx); - let orig_insn = match ptrace_read_u32_unlocked(&aspace, next_insn_addr) { + let orig_insn = match ptrace_read_u32_unlocked(&mut aspace, next_insn_addr) { Ok(insn) => insn, Err(_) => { tracee.set_ptrace_ss_saved_insn_for(tid, None); @@ -1303,7 +1309,10 @@ pub fn ptrace_setup_singlestep( return; } - let _ = ptrace_write_u32_unlocked(&mut aspace, next_insn_addr, LOONGARCH_BREAK_INSN); + if ptrace_write_u32_unlocked(&mut aspace, next_insn_addr, LOONGARCH_BREAK_INSN).is_err() { + tracee.set_ptrace_ss_saved_insn_for(tid, None); + return; + } tracee.set_ptrace_ss_saved_insn_for(tid, Some((next_insn_addr, orig_insn as usize))); ax_runtime::hal::cpu::asm::flush_icache_all(); } @@ -1728,7 +1737,8 @@ fn loongarch_reg(uctx: &ax_runtime::hal::cpu::uspace::UserContext, index: usize) } #[cfg(target_arch = "riscv64")] -fn ptrace_read_u16_unlocked(aspace: &AddrSpace, addr: usize) -> AxResult { +fn ptrace_read_u16_unlocked(aspace: &mut AddrSpace, addr: usize) -> AxResult { + ptrace_populate_remote_range(aspace, addr, size_of::(), MappingFlags::READ)?; let mut bytes = [0u8; size_of::()]; aspace.read(VirtAddr::from_usize(addr), &mut bytes)?; Ok(u16::from_ne_bytes(bytes)) @@ -1739,7 +1749,8 @@ fn ptrace_read_u16_unlocked(aspace: &AddrSpace, addr: usize) -> AxResult { target_arch = "aarch64", target_arch = "loongarch64" ))] -fn ptrace_read_u32_unlocked(aspace: &AddrSpace, addr: usize) -> AxResult { +fn ptrace_read_u32_unlocked(aspace: &mut AddrSpace, addr: usize) -> AxResult { + ptrace_populate_remote_range(aspace, addr, size_of::(), MappingFlags::READ)?; let mut bytes = [0u8; size_of::()]; aspace.read(VirtAddr::from_usize(addr), &mut bytes)?; Ok(u32::from_ne_bytes(bytes)) @@ -1747,12 +1758,14 @@ fn ptrace_read_u32_unlocked(aspace: &AddrSpace, addr: usize) -> AxResult { #[cfg(target_arch = "riscv64")] fn ptrace_write_u16_unlocked(aspace: &mut AddrSpace, addr: usize, data: u16) -> AxResult { + ptrace_populate_remote_range(aspace, addr, size_of::(), MappingFlags::WRITE)?; aspace.write(VirtAddr::from_usize(addr), &data.to_ne_bytes())?; Ok(()) } #[cfg(any(target_arch = "aarch64", target_arch = "loongarch64"))] fn ptrace_write_u32_unlocked(aspace: &mut AddrSpace, addr: usize, data: u32) -> AxResult { + ptrace_populate_remote_range(aspace, addr, size_of::(), MappingFlags::WRITE)?; aspace.write(VirtAddr::from_usize(addr), &data.to_ne_bytes())?; Ok(()) } diff --git a/os/StarryOS/kernel/src/task/ops.rs b/os/StarryOS/kernel/src/task/ops.rs index de7e3b9b3d..68891d422b 100644 --- a/os/StarryOS/kernel/src/task/ops.rs +++ b/os/StarryOS/kernel/src/task/ops.rs @@ -538,6 +538,17 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { trace_sched_process_exit(curr.id().as_u64(), exit_code); + if group_exit && let Some(tids) = thr.proc_data.proc.start_group_exit(exit_code) { + let sig = SignalInfo::new_kernel(Signo::SIGKILL); + for tid in tids { + if tid == thr.tid() { + continue; + } + let _ = send_signal_to_thread(None, tid, Some(sig.clone())); + let _ = zap_thread(tid); + } + } + // Robust futex ownership must be released before clone-child-tid wakes a // pthread joiner; otherwise userspace can observe thread exit before the // OWNER_DIED handoff has been written. @@ -674,13 +685,6 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { unsafe { thr.exit_event.wake(axpoll::IoEvents::IN) }; unsafe { thr.proc_data.thread_exit_event.wake(axpoll::IoEvents::IN) }; - if group_exit && !process.is_group_exited() { - process.group_exit(); - let sig = SignalInfo::new_kernel(Signo::SIGKILL); - for tid in process.threads() { - let _ = send_signal_to_thread(None, tid, Some(sig.clone())); - } - } thr.set_exit(); } diff --git a/os/StarryOS/kernel/src/task/user.rs b/os/StarryOS/kernel/src/task/user.rs index d4fd6d0994..c2f2567bb9 100644 --- a/os/StarryOS/kernel/src/task/user.rs +++ b/os/StarryOS/kernel/src/task/user.rs @@ -171,12 +171,19 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> target_arch = "aarch64", target_arch = "loongarch64" ))] - let _ = crate::syscall::ptrace_restore_singlestep_insn( - &thr.proc_data, - thr.tid(), - addr, - insn, - ); + { + let restored = + crate::syscall::ptrace_restore_singlestep_insn( + &thr.proc_data, + thr.tid(), + addr, + insn, + ); + if restored { + thr.proc_data + .set_ptrace_singlestep_for(thr.tid(), false); + } + } #[cfg(not(any( target_arch = "riscv64", target_arch = "aarch64", diff --git a/os/arceos/modules/axhal/src/dummy.rs b/os/arceos/modules/axhal/src/dummy.rs index 309985ba80..4a1643f5a7 100644 --- a/os/arceos/modules/axhal/src/dummy.rs +++ b/os/arceos/modules/axhal/src/dummy.rs @@ -3,7 +3,7 @@ #[cfg(feature = "irq")] use ax_plat::irq::{IpiTarget, IrqIf}; use ax_plat::{ - console::ConsoleIf, + console::{ConsoleDeviceIdError, ConsoleDeviceIdResult, ConsoleIf}, impl_plat_interface, init::InitIf, mem::{MemIf, RawRange}, @@ -42,6 +42,10 @@ impl ConsoleIf for DummyConsole { unimplemented!() } + fn device_id() -> ConsoleDeviceIdResult { + Err(ConsoleDeviceIdError::NotSpecified) + } + #[cfg(feature = "irq")] fn irq_num() -> Option { None diff --git a/os/arceos/modules/axhal/src/lib.rs b/os/arceos/modules/axhal/src/lib.rs index a56c37cc90..60f35cfccd 100644 --- a/os/arceos/modules/axhal/src/lib.rs +++ b/os/arceos/modules/axhal/src/lib.rs @@ -55,9 +55,12 @@ pub mod paging; /// Console input and output. pub mod console { + pub use ax_plat::console::{ + ConsoleDeviceId, ConsoleDeviceIdError, ConsoleDeviceIdResult, device_id, read_bytes, + write_bytes, write_text_bytes, + }; #[cfg(feature = "irq")] pub use ax_plat::console::{ConsoleIrqEvent, handle_irq, irq_num, set_input_irq_enabled}; - pub use ax_plat::console::{read_bytes, write_bytes, write_text_bytes}; } /// CPU power management. diff --git a/os/arceos/modules/axtask/src/wait_queue.rs b/os/arceos/modules/axtask/src/wait_queue.rs index b28f6bd516..0f10d09e00 100644 --- a/os/arceos/modules/axtask/src/wait_queue.rs +++ b/os/arceos/modules/axtask/src/wait_queue.rs @@ -194,10 +194,11 @@ impl WaitQueue { /// Wakes up one task from IRQ context. /// /// This method is intended for low-level deferred notification paths. It - /// does not request an immediate reschedule and it must not be used as a + /// only unblocks the worker and marks the current task for rescheduling + /// after IRQ/preemption guards are released; it must not be used as a /// substitute for publishing the condition that the waiter will observe. pub fn notify_one_from_irq(&self) -> bool { - self.notify_one(false) + self.notify_one(true) } /// Wakes up one task in the wait queue and runs a callback on it. @@ -237,8 +238,9 @@ impl WaitQueue { /// Wakes all tasks from IRQ context. /// /// This method is intended for low-level deferred notification paths. It - /// does not request an immediate reschedule and it must not be used as a - /// substitute for publishing the condition that waiters will observe. + /// only unblocks workers and marks the current task for rescheduling after + /// IRQ/preemption guards are released; it must not be used as a substitute + /// for publishing the condition that waiters will observe. pub fn notify_all_from_irq(&self) { while self.notify_one_from_irq() { // loop until the wait queue is empty diff --git a/os/axvisor/configs/vms/qemu/x86_64/linux-svm-smp1.toml b/os/axvisor/configs/vms/qemu/x86_64/linux-svm-smp1.toml index e6d6e09791..b8093f8eee 100644 --- a/os/axvisor/configs/vms/qemu/x86_64/linux-svm-smp1.toml +++ b/os/axvisor/configs/vms/qemu/x86_64/linux-svm-smp1.toml @@ -29,7 +29,9 @@ kernel_load_addr = 0x20_0000 enable_bios = false # Linux direct boot needs an explicit cmdline; keep no_timer_check from the old # default to avoid PIT/HPET calibration stalls when Linux marks the TSC unstable. -cmdline = "console=ttyS0 root=/dev/vda rw rootwait devtmpfs.mount=1 init=/sbin/getty acpi=off pci=conf1 pci=nomsi nox2apic tsc=unstable no_timer_check initcall_blacklist=ahci_pci_driver_init,i8042_init -- -n -l /bin/sh -L 115200 ttyS0 dumb" +# The smoke guest only needs to reach a shell, so mount the snapshot rootfs +# read-only and skip ext4 journal replay writes on nested SVM hosts. +cmdline = "console=ttyS0 root=/dev/vda ro rootwait rootflags=noload devtmpfs.mount=1 init=/sbin/getty acpi=off pci=conf1 pci=nomsi irqpoll no_timer_check initcall_blacklist=ahci_pci_driver_init,i8042_init -- -n -l /bin/sh -L 115200 ttyS0 dumb" ## The path of the disk image. # disk_path = "" @@ -71,10 +73,12 @@ passthrough_devices = [ # Passthrough addresses. # Base-GPA Length. passthrough_addresses = [ - # QEMU q35 may place PCI BARs around 0x8000_0000 or 0x8_0000_0000 on - # different hosts; both must be identity-mapped for guest PCI probing. - [0x8000_0000, 0x10_0000], - [0x8_0000_0000, 0x10_0000], + # QEMU q35 may place 32-bit PCI MMIO BARs here. Linux can touch the + # AHCI/virtio BARs even when we boot mainly from the emulated virtio disk. + [0x8000_0000, 0x20_0000], + # Some q35/KVM hosts assign a PCI MMIO BAR in the high 64-bit window. + # Linux may touch it during virtio/AHCI probing before the shell starts. + [0x8_0000_0000, 0x20_0000], # QEMU q35 low MMIO window needed by PCI/virtio-blk probing. [0xfe00_0000, 0x00c0_0000], # Linux probes a q35/ICH system MMIO page at 0xfed8_03c0 after the PIT/HLT path is fixed. diff --git a/os/axvisor/configs/vms/qemu/x86_64/linux-vmx-smp1.toml b/os/axvisor/configs/vms/qemu/x86_64/linux-vmx-smp1.toml index ba255b06f6..31e3c66772 100644 --- a/os/axvisor/configs/vms/qemu/x86_64/linux-vmx-smp1.toml +++ b/os/axvisor/configs/vms/qemu/x86_64/linux-vmx-smp1.toml @@ -29,7 +29,9 @@ kernel_load_addr = 0x20_0000 enable_bios = false # Linux direct boot needs an explicit cmdline; keep no_timer_check from the old # default to avoid PIT/HPET calibration stalls when Linux marks the TSC unstable. -cmdline = "console=ttyS0 root=/dev/vda rw rootwait devtmpfs.mount=1 init=/sbin/getty acpi=off pci=conf1 pci=nomsi nox2apic tsc=unstable no_timer_check initcall_blacklist=ahci_pci_driver_init,i8042_init -- -n -l /bin/sh -L 115200 ttyS0 dumb" +# The smoke guest only needs to reach a shell, so mount the snapshot rootfs +# read-only and skip ext4 journal replay writes on nested VMX hosts. +cmdline = "console=ttyS0 root=/dev/vda ro rootflags=noload devtmpfs.mount=1 init=/sbin/getty acpi=off pci=conf1 pci=nomsi irqpoll nox2apic tsc=unstable no_timer_check initcall_blacklist=ahci_pci_driver_init -- -n -l /bin/sh -L 115200 ttyS0" ## The path of the disk image. # disk_path = "" @@ -71,10 +73,12 @@ passthrough_devices = [ # Passthrough addresses. # Base-GPA Length. passthrough_addresses = [ - # QEMU q35 may place PCI BARs around 0x8000_0000 or 0x8_0000_0000 on - # different hosts; both must be identity-mapped for guest PCI probing. - [0x8000_0000, 0x10_0000], - [0x8_0000_0000, 0x10_0000], + # QEMU q35 may place 32-bit PCI MMIO BARs here. Linux can touch the + # AHCI/virtio BARs even when we boot mainly from the emulated virtio disk. + [0x8000_0000, 0x20_0000], + # Some q35/KVM hosts assign a PCI MMIO BAR in the high 64-bit window. + # Linux may touch it during virtio/AHCI probing before the shell starts. + [0x8_0000_0000, 0x20_0000], # QEMU q35 low MMIO window needed by PCI/virtio-blk probing. [0xfe00_0000, 0x00c0_0000], # Linux probes a q35/ICH system MMIO page after the PIT/HLT path is active. diff --git a/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs b/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs index 3cd56f2071..310282f52e 100644 --- a/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs +++ b/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs @@ -1,8 +1,8 @@ use ax_kspin::SpinNoIrq; use ax_lazyinit::LazyInit; -use ax_plat::console::ConsoleIf; #[cfg(feature = "irq")] use ax_plat::console::ConsoleIrqEvent; +use ax_plat::console::{ConsoleDeviceIdError, ConsoleDeviceIdResult, ConsoleIf}; #[cfg(feature = "irq")] use uart_16550::spec::registers::InterruptType; use uart_16550::{Config, Uart16550, backend::MmioBackend, spec::registers::IER}; @@ -63,6 +63,10 @@ impl ConsoleIf for ConsoleIfImpl { uart.try_receive_bytes(bytes) } + fn device_id() -> ConsoleDeviceIdResult { + Err(ConsoleDeviceIdError::NotSpecified) + } + /// Returns the IRQ number for the console, if applicable. #[cfg(feature = "irq")] fn irq_num() -> Option { diff --git a/platforms/ax-plat-riscv64-sg2002/src/console.rs b/platforms/ax-plat-riscv64-sg2002/src/console.rs index 54d46f4f42..5d5f792dc9 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/console.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/console.rs @@ -1,8 +1,8 @@ use ax_kspin::SpinNoIrq; use ax_lazyinit::LazyInit; -use ax_plat::console::ConsoleIf; #[cfg(feature = "irq")] use ax_plat::console::ConsoleIrqEvent; +use ax_plat::console::{ConsoleDeviceIdError, ConsoleDeviceIdResult, ConsoleIf}; use some_serial::ns16550::dw_apb::{DwApbUart, SG2002_UART_CLOCK}; use crate::config::{devices::UART_PADDR, plat::PHYS_VIRT_OFFSET}; @@ -24,14 +24,14 @@ struct ConsoleIfImpl; impl ConsoleIf for ConsoleIfImpl { /// Writes bytes to the console from input u8 slice. fn write_bytes(bytes: &[u8]) { + let mut uart = UART.lock(); for &c in bytes { - let mut uart = UART.lock(); match c { b'\n' => { - uart.putchar(b'\r'); - uart.putchar(b'\n'); + write_byte(&mut uart, b'\r'); + write_byte(&mut uart, b'\n'); } - c => uart.putchar(c), + c => write_byte(&mut uart, c), } } } @@ -40,13 +40,12 @@ impl ConsoleIf for ConsoleIfImpl { /// Returns the number of bytes read. fn read_bytes(bytes: &mut [u8]) -> usize { let mut uart = UART.lock(); - for (i, byte) in bytes.iter_mut().enumerate() { - match uart.getchar() { - Some(c) => *byte = c, - None => return i, - } - } - bytes.len() + uart.try_read(bytes) + .unwrap_or_else(|err| err.bytes_transferred) + } + + fn device_id() -> ConsoleDeviceIdResult { + Err(ConsoleDeviceIdError::NotSpecified) } /// Returns the IRQ number for the console, if applicable. @@ -64,3 +63,9 @@ impl ConsoleIf for ConsoleIfImpl { ConsoleIrqEvent::empty() } } + +fn write_byte(uart: &mut DwApbUart, byte: u8) { + while uart.try_write(&[byte]) == 0 { + core::hint::spin_loop(); + } +} diff --git a/platforms/ax-plat-riscv64-visionfive2/src/console.rs b/platforms/ax-plat-riscv64-visionfive2/src/console.rs index 0794aef569..919db41311 100644 --- a/platforms/ax-plat-riscv64-visionfive2/src/console.rs +++ b/platforms/ax-plat-riscv64-visionfive2/src/console.rs @@ -3,7 +3,7 @@ use ax_lazyinit::LazyInit; #[cfg(feature = "irq")] use ax_plat::console::ConsoleIrqEvent; use ax_plat::{ - console::ConsoleIf, + console::{ConsoleDeviceIdError, ConsoleDeviceIdResult, ConsoleIf}, mem::{pa, phys_to_virt}, }; use uart_16550::MmioSerialPort; @@ -52,6 +52,10 @@ impl ConsoleIf for ConsoleIfImpl { bytes.len() } + fn device_id() -> ConsoleDeviceIdResult { + Err(ConsoleDeviceIdError::NotSpecified) + } + /// Returns the IRQ number for the console, if applicable. #[cfg(feature = "irq")] fn irq_num() -> Option { diff --git a/platforms/ax-plat/src/console.rs b/platforms/ax-plat/src/console.rs index fafe179c08..2edc1a52f4 100644 --- a/platforms/ax-plat/src/console.rs +++ b/platforms/ax-plat/src/console.rs @@ -3,6 +3,21 @@ use core::fmt::{Arguments, Result, Write}; use bitflags::bitflags; +pub use rdrive::DeviceId as ConsoleDeviceId; + +/// Why the platform could not provide a hardware console device id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ConsoleDeviceIdError { + /// No firmware or command-line hardware console was specified. + NotSpecified, + /// A console was specified, but it does not describe a hardware device. + NoHardwareDevice, + /// A hardware console was specified, but no probed device matched it. + DeviceNotFound, +} + +/// Result type returned by the platform console device selector. +pub type ConsoleDeviceIdResult = core::result::Result; bitflags! { /// Console input IRQ events returned by the platform. @@ -28,6 +43,12 @@ pub trait ConsoleIf { /// Returns the number of bytes read. fn read_bytes(bytes: &mut [u8]) -> usize; + /// Returns the runtime-discovered hardware device selected as the console. + /// + /// Static platforms that do not have a runtime device manager should return + /// [`ConsoleDeviceIdError::NotSpecified`]. + fn device_id() -> ConsoleDeviceIdResult; + /// Returns the IRQ number for the console input interrupt. /// /// Returns `None` if input interrupt is not supported. diff --git a/platforms/ax-plat/src/irq.rs b/platforms/ax-plat/src/irq.rs index 4015afa86c..6d89784b31 100644 --- a/platforms/ax-plat/src/irq.rs +++ b/platforms/ax-plat/src/irq.rs @@ -104,8 +104,11 @@ fn registry() -> &'static Registry { /// Requests an IRQ action through the dynamic IRQ framework. pub fn request_irq(irq: usize, request: IrqRequest) -> Result { + let auto_enable = request.auto_enable_mode(); let handle = registry().request(IrqNumber(irq), request)?; - if let Err(err) = registry().enable(handle) { + if auto_enable == AutoEnable::Yes + && let Err(err) = registry().enable(handle) + { let _ = registry().free(handle); return Err(err); } diff --git a/platforms/axplat-dyn/src/console.rs b/platforms/axplat-dyn/src/console.rs index 9be36802ab..9d3935befd 100644 --- a/platforms/axplat-dyn/src/console.rs +++ b/platforms/axplat-dyn/src/console.rs @@ -1,6 +1,6 @@ -use ax_plat::console::ConsoleIf; #[cfg(feature = "irq")] use ax_plat::console::ConsoleIrqEvent; +use ax_plat::console::{ConsoleDeviceIdError, ConsoleDeviceIdResult, ConsoleIf}; struct ConsoleIfImpl; @@ -35,6 +35,16 @@ impl ConsoleIf for ConsoleIfImpl { read_len } + fn device_id() -> ConsoleDeviceIdResult { + somehal::console_device_id().map_err(|err| match err { + somehal::ConsoleDeviceIdError::NotSpecified => ConsoleDeviceIdError::NotSpecified, + somehal::ConsoleDeviceIdError::NoHardwareDevice => { + ConsoleDeviceIdError::NoHardwareDevice + } + somehal::ConsoleDeviceIdError::DeviceNotFound => ConsoleDeviceIdError::DeviceNotFound, + }) + } + /// Returns the IRQ number for the console input interrupt. /// /// Returns `None` if input interrupt is not supported. diff --git a/platforms/axplat-dyn/src/irq.rs b/platforms/axplat-dyn/src/irq.rs index ebaab4a49e..9690d77995 100644 --- a/platforms/axplat-dyn/src/irq.rs +++ b/platforms/axplat-dyn/src/irq.rs @@ -35,8 +35,13 @@ impl IrqIf for IrqIfImpl { return Some(irq_num); } - if !dispatch_irq(irq_num).handled { - warn!("Unhandled IRQ {irq:?}"); + let outcome = dispatch_irq(irq_num); + if !outcome.handled { + if outcome.called == 0 { + warn!("Unhandled IRQ {irq:?}"); + } else { + debug!("Spurious IRQ {irq:?}"); + } } irq_num }; diff --git a/platforms/somehal/src/boot_console.rs b/platforms/somehal/src/boot_console.rs new file mode 100644 index 0000000000..6273dd69ed --- /dev/null +++ b/platforms/somehal/src/boot_console.rs @@ -0,0 +1,259 @@ +use rdrive::{DeviceId, Fdt}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConsoleSpec { + HardwareSerial(usize), + VirtualTty, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConsoleDeviceIdError { + NotSpecified, + NoHardwareDevice, + DeviceNotFound, +} + +pub fn device_id() -> Result { + match device_id_from_bootargs(someboot::cmdline()) { + Ok(device_id) => Ok(device_id), + Err(ConsoleDeviceIdError::NotSpecified) => device_id_from_acpi_spcr() + .or_else(device_id_from_fdt_stdout) + .ok_or(ConsoleDeviceIdError::NotSpecified), + Err( + err @ (ConsoleDeviceIdError::NoHardwareDevice | ConsoleDeviceIdError::DeviceNotFound), + ) => Err(err), + } +} + +fn device_id_from_bootargs(cmdline: Option<&str>) -> Result { + device_id_from_bootargs_with(cmdline, device_id_from_serial_index) +} + +fn device_id_from_bootargs_with( + cmdline: Option<&str>, + serial_device_id: impl Fn(usize) -> Option, +) -> Result { + let cmdline = cmdline.ok_or(ConsoleDeviceIdError::NotSpecified)?; + let mut saw_supported_console = false; + let mut saw_hardware_console = false; + let mut last_hardware_device_id = None; + + for spec in console_specs(cmdline) { + saw_supported_console = true; + if let ConsoleSpec::HardwareSerial(index) = spec { + saw_hardware_console = true; + if let Some(device_id) = serial_device_id(index) { + last_hardware_device_id = Some(device_id); + } + } + } + + match last_hardware_device_id { + Some(device_id) => Ok(device_id), + None if saw_hardware_console => Err(ConsoleDeviceIdError::DeviceNotFound), + None if saw_supported_console => Err(ConsoleDeviceIdError::NoHardwareDevice), + None => Err(ConsoleDeviceIdError::NotSpecified), + } +} + +fn console_specs(cmdline: &str) -> impl Iterator + '_ { + cmdline + .split_ascii_whitespace() + .filter_map(|arg| arg.strip_prefix("console=")) + .filter_map(parse_console_spec) +} + +fn parse_console_spec(spec: &str) -> Option { + let name = spec.split(',').next().unwrap_or(spec); + if name == "tty" + || name == "ttynull" + || name + .strip_prefix("tty") + .is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|c| c.is_ascii_digit())) + { + return Some(ConsoleSpec::VirtualTty); + } + + parse_number_suffix(name, "ttyS") + .or_else(|| parse_number_suffix(name, "ttyAMA")) + .map(ConsoleSpec::HardwareSerial) +} + +fn parse_number_suffix(name: &str, prefix: &str) -> Option { + name.strip_prefix(prefix)?.parse().ok() +} + +fn device_id_from_serial_index(index: usize) -> Option { + device_id_from_serial_index_with(index, fdt_serial_alias_device_id, device_id_from_acpi_spcr) +} + +fn device_id_from_serial_index_with( + index: usize, + fdt_device_id: impl FnOnce(usize) -> Option, + spcr_device_id: impl FnOnce() -> Option, +) -> Option { + fdt_device_id(index).or_else(|| if index == 0 { spcr_device_id() } else { None }) +} + +fn fdt_serial_alias_device_id(index: usize) -> Option { + rdrive::with_fdt(|fdt| { + let alias = alloc::format!("serial{index}"); + let path = alias_path(fdt, &alias)?; + rdrive::fdt_path_to_device_id(path) + }) + .flatten() +} + +fn device_id_from_acpi_spcr() -> Option { + rdrive::acpi_spcr_console_device_id() +} + +fn device_id_from_fdt_stdout() -> Option { + rdrive::with_fdt(stdout_device_id).flatten() +} + +fn stdout_device_id(fdt: &Fdt) -> Option { + let chosen = fdt.get_by_path("/chosen")?; + ["stdout-path", "linux,stdout-path"] + .into_iter() + .find_map(|key| { + let raw = chosen.as_node().get_property(key)?.as_str()?; + let path = split_stdout_options(raw); + if path.is_empty() { + return None; + } + if path.starts_with('/') { + return rdrive::fdt_path_to_device_id(path); + } + alias_path(fdt, path).and_then(rdrive::fdt_path_to_device_id) + }) +} + +fn split_stdout_options(stdout: &str) -> &str { + stdout.split(':').next().unwrap_or(stdout) +} + +fn alias_path<'a>(fdt: &'a Fdt, alias: &str) -> Option<&'a str> { + fdt.get_by_path("/aliases")? + .as_node() + .get_property(alias)? + .as_str() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn console_specs_keep_command_line_order() { + let specs: alloc::vec::Vec<_> = + console_specs("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200").collect(); + + assert_eq!( + specs, + alloc::vec![ + ConsoleSpec::HardwareSerial(2), + ConsoleSpec::VirtualTty, + ConsoleSpec::HardwareSerial(3), + ] + ); + } + + #[test] + fn parses_supported_serial_console_names() { + assert_eq!( + parse_console_spec("ttyS0,115200n8"), + Some(ConsoleSpec::HardwareSerial(0)) + ); + assert_eq!( + parse_console_spec("ttyAMA1"), + Some(ConsoleSpec::HardwareSerial(1)) + ); + assert_eq!(parse_console_spec("ttynull"), Some(ConsoleSpec::VirtualTty)); + assert_eq!(parse_console_spec("tty7"), Some(ConsoleSpec::VirtualTty)); + assert_eq!(parse_console_spec("ttySx"), None); + } + + #[test] + fn bootargs_console_spec_suppresses_firmware_fallback() { + assert_eq!( + device_id_from_bootargs(Some("root=/dev/vda")), + Err(ConsoleDeviceIdError::NotSpecified) + ); + assert_eq!( + device_id_from_bootargs(Some("console=tty1")), + Err(ConsoleDeviceIdError::NoHardwareDevice) + ); + assert_eq!( + device_id_from_bootargs(Some("console=ttyS2 console=tty1")), + Err(ConsoleDeviceIdError::DeviceNotFound) + ); + } + + #[test] + fn virtual_console_does_not_clear_earlier_serial_console() { + let serial2_device = DeviceId::from(42); + + assert_eq!( + device_id_from_bootargs_with(Some("console=ttyS2,1500000 console=tty1"), |index| { + (index == 2).then_some(serial2_device) + }), + Ok(serial2_device) + ); + } + + #[test] + fn last_available_hardware_console_wins_over_earlier_serial_console() { + let serial3_device = DeviceId::from(43); + + assert_eq!( + device_id_from_bootargs_with( + Some("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200"), + |index| (index == 3).then_some(serial3_device), + ), + Ok(serial3_device) + ); + } + + #[test] + fn later_missing_hardware_console_does_not_clear_earlier_available_serial_console() { + let serial2_device = DeviceId::from(42); + + assert_eq!( + device_id_from_bootargs_with( + Some("console=ttyS2,1500000 console=ttyS3,115200 console=tty1"), + |index| (index == 2).then_some(serial2_device), + ), + Ok(serial2_device) + ); + } + + #[test] + fn serial_index_zero_can_use_acpi_spcr_when_fdt_alias_is_absent() { + let spcr_device = DeviceId::from(42); + + assert_eq!( + device_id_from_serial_index_with(0, |_| None, || Some(spcr_device)), + Some(spcr_device) + ); + } + + #[test] + fn non_zero_serial_index_does_not_fallback_to_acpi_spcr() { + let spcr_device = DeviceId::from(42); + + assert_eq!( + device_id_from_serial_index_with(2, |_| None, || Some(spcr_device)), + None + ); + } + + #[test] + fn splits_stdout_options() { + assert_eq!( + split_stdout_options("/soc/serial@1000:115200n8"), + "/soc/serial@1000" + ); + assert_eq!(split_stdout_options("serial0"), "serial0"); + } +} diff --git a/platforms/somehal/src/lib.rs b/platforms/somehal/src/lib.rs index 989a8d5a29..d1b5acbed2 100644 --- a/platforms/somehal/src/lib.rs +++ b/platforms/somehal/src/lib.rs @@ -9,6 +9,7 @@ extern crate alloc; #[macro_use] extern crate log; +mod boot_console; pub(crate) mod common; pub mod cpu; mod driver; @@ -16,6 +17,7 @@ pub mod irq; pub mod rtc; pub mod setup; +pub use boot_console::{ConsoleDeviceIdError, device_id as console_device_id}; pub use page_table_generic::{PagingError, PagingResult}; pub use setup::KernelOp; pub use someboot::{ diff --git a/scripts/axbuild/src/axvisor/test/tests.rs b/scripts/axbuild/src/axvisor/test/tests.rs index 436e20d312..65e9b89d6f 100644 --- a/scripts/axbuild/src/axvisor/test/tests.rs +++ b/scripts/axbuild/src/axvisor/test/tests.rs @@ -8,12 +8,24 @@ use tempfile::tempdir; use super::*; use crate::{axvisor::build, context::ResolvedAxvisorRequest}; +const X86_LINUX_DIRECT_BOOT_CMDLINE_LIMIT: usize = 231; + #[derive(serde::Deserialize)] struct TestBuildConfigVmConfigs { #[serde(default)] vm_configs: Vec, } +#[derive(serde::Deserialize)] +struct TestVmKernelConfig { + kernel: TestVmKernel, +} + +#[derive(serde::Deserialize)] +struct TestVmKernel { + cmdline: String, +} + fn write_qemu_config(root: &Path, case: &str, arch: &str, body: &str) -> PathBuf { write_qemu_config_in_group(root, "normal", "default", case, arch, body) } @@ -655,10 +667,24 @@ fn x86_linux_direct_boot_configs_keep_timer_calibration_bypass() { "os/axvisor/configs/vms/qemu/x86_64/linux-svm-smp1.toml", ] { let content = fs::read_to_string(workspace_root.join(path)).unwrap(); + let config: TestVmKernelConfig = toml::from_str(&content).unwrap(); + let cmdline = config.kernel.cmdline; + assert!( - content.contains("no_timer_check"), + cmdline.contains("no_timer_check"), "{path} should keep no_timer_check to avoid x86 Linux guest timer calibration stalls" ); + assert!( + cmdline.len() <= X86_LINUX_DIRECT_BOOT_CMDLINE_LIMIT, + "{path} cmdline length {} exceeds the currently verified x86 direct-boot limit of {} \ + bytes and can truncate getty arguments", + cmdline.len(), + X86_LINUX_DIRECT_BOOT_CMDLINE_LIMIT + ); + assert!( + cmdline.contains("-- -n -l /bin/sh -L 115200 ttyS0"), + "{path} should keep complete getty arguments after `--` so init does not exit" + ); } } diff --git a/scripts/axbuild/src/starry/config.rs b/scripts/axbuild/src/starry/config.rs index 6e566acdb7..94f00d4657 100644 --- a/scripts/axbuild/src/starry/config.rs +++ b/scripts/axbuild/src/starry/config.rs @@ -77,16 +77,8 @@ fn update_snapshot_for_board( snapshot.arch = Some(starry_arch_for_target_checked(&board.target)?.to_string()); snapshot.target = Some(board.target.clone()); snapshot.config = Some(snapshot_path_value(workspace_root, build_config_path)); - snapshot.qemu.qemu_config = snapshot - .qemu - .qemu_config - .as_ref() - .map(|path| snapshot_path_value(workspace_root, path)); - snapshot.uboot.uboot_config = snapshot - .uboot - .uboot_config - .as_ref() - .map(|path| snapshot_path_value(workspace_root, path)); + snapshot.qemu.qemu_config = None; + snapshot.uboot.uboot_config = None; snapshot.store(workspace_root)?; Ok(()) } @@ -162,7 +154,7 @@ mod tests { } #[test] - fn write_defconfig_generates_build_file_and_updates_snapshot() { + fn write_defconfig_generates_build_file_and_resets_runtime_config() { let root = tempdir().unwrap(); write_workspace(root.path()); let source = write_board( @@ -214,16 +206,8 @@ log = "Warn" "tmp/axbuild/config/starryos/build-riscv64gc-unknown-none-elf.toml" )) ); - assert_eq!( - snapshot.qemu.qemu_config, - Some(PathBuf::from( - "test-suit/starryos/qemu-smp1/system/qemu-riscv64.toml" - )) - ); - assert_eq!( - snapshot.uboot.uboot_config, - Some(PathBuf::from("configs/uboot.toml")) - ); + assert_eq!(snapshot.qemu.qemu_config, None); + assert_eq!(snapshot.uboot.uboot_config, None); } #[test] diff --git a/test-suit/starryos/qemu-smp1/build-aarch64-unknown-none-softfloat.toml b/test-suit/starryos/qemu-smp1/build-aarch64-unknown-none-softfloat.toml index ac99b2b97a..891475d9a2 100644 --- a/test-suit/starryos/qemu-smp1/build-aarch64-unknown-none-softfloat.toml +++ b/test-suit/starryos/qemu-smp1/build-aarch64-unknown-none-softfloat.toml @@ -1,6 +1,7 @@ features = [ "ax-feat/display", "ax-feat/rtc", + "ax-driver/serial", "ax-driver/nvme", "ax-driver/virtio-net", "ax-driver/virtio-gpu", diff --git a/test-suit/starryos/qemu-smp1/system/bugfix-bug-dir-cookie-unlink-rmdir/src/main.c b/test-suit/starryos/qemu-smp1/system/bugfix-bug-dir-cookie-unlink-rmdir/src/main.c index 7cc6be908c..0eda9f99fc 100644 --- a/test-suit/starryos/qemu-smp1/system/bugfix-bug-dir-cookie-unlink-rmdir/src/main.c +++ b/test-suit/starryos/qemu-smp1/system/bugfix-bug-dir-cookie-unlink-rmdir/src/main.c @@ -110,12 +110,64 @@ static int remove_by_batched_getdents(const char *dir) return 0; } +static int cleanup_dir(const char *dir) +{ + char buf[512]; + int fd = open(dir, O_RDONLY | O_DIRECTORY); + if (fd < 0) { + if (errno == ENOENT) { + return 0; + } + if (errno == ENOTDIR && unlink(dir) == 0) { + return 0; + } + return fail_errno("open cleanup dir", dir); + } + + for (;;) { + int nread = (int)syscall(SYS_getdents64, fd, buf, sizeof(buf)); + if (nread < 0) { + close(fd); + return fail_errno("cleanup getdents64", dir); + } + if (nread == 0) { + break; + } + + for (int pos = 0; pos < nread;) { + struct linux_dirent64 *d = (struct linux_dirent64 *)(void *)(buf + pos); + if (d->d_reclen == 0 || pos + d->d_reclen > nread) { + close(fd); + return fail_msg("cleanup parse dirent", dir, "invalid reclen"); + } + if (strcmp(d->d_name, ".") != 0 && strcmp(d->d_name, "..") != 0) { + if (unlinkat(fd, d->d_name, 0) != 0) { + if ((errno != EISDIR && errno != EPERM) || + unlinkat(fd, d->d_name, AT_REMOVEDIR) != 0) { + close(fd); + return fail_errno("cleanup unlinkat", d->d_name); + } + } + } + pos += d->d_reclen; + } + } + + close(fd); + if (rmdir(dir) != 0 && errno != ENOENT) { + return fail_errno("cleanup rmdir", dir); + } + return 0; +} + static int run_case(const char *label, const char *dir) { const int count = 160; printf("[TEST] %s dir=%s count=%d\n", label, dir, count); - rmdir(dir); + if (cleanup_dir(dir) != 0) { + return 1; + } if (mkdir(dir, 0755) != 0) { return fail_errno("mkdir", dir); } @@ -140,8 +192,9 @@ static int run_rename_case(const char *label, const char *src_dir, const char *d printf("[TEST] %s rename dir-cookie src=%s dst=%s count=%d\n", label, src_dir, dst_dir, count); - rmdir(src_dir); - rmdir(dst_dir); + if (cleanup_dir(src_dir) != 0 || cleanup_dir(dst_dir) != 0) { + return 1; + } if (mkdir(src_dir, 0755) != 0) { return fail_errno("mkdir src", src_dir); } diff --git a/test-suit/starryos/qemu-smp1/system/test-gdb-native-batch/src/tracer.c b/test-suit/starryos/qemu-smp1/system/test-gdb-native-batch/src/tracer.c index f8fe294a89..6e82326ff1 100644 --- a/test-suit/starryos/qemu-smp1/system/test-gdb-native-batch/src/tracer.c +++ b/test-suit/starryos/qemu-smp1/system/test-gdb-native-batch/src/tracer.c @@ -83,6 +83,18 @@ static long pt(int request, pid_t pid, void *addr, void *data) return ptrace(request, pid, addr, data); } +static int wait_child(pid_t pid, int *status, int options, const char *msg) +{ + pid_t got; + do { + got = waitpid(pid, status, options); + } while (got == -1 && errno == EINTR); + if (got != pid) { + return fail(msg); + } + return 0; +} + static int getregs(pid_t pid, struct x86_64_user_regs *regs) { struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; @@ -100,8 +112,8 @@ static int setregs(pid_t pid, const struct x86_64_user_regs *regs) static int wait_trap(pid_t pid, int *status) { - if (waitpid(pid, status, 0) != pid) { - return fail("waitpid"); + if (wait_child(pid, status, 0, "waitpid") != 0) { + return 1; } if (!WIFSTOPPED(*status) || WSTOPSIG(*status) != SIGTRAP) { printf("FAIL: expected SIGTRAP stop, status=%#x\n", *status); @@ -234,8 +246,8 @@ static int trace_dynamic_target(void) if (pt(PTRACE_CONT, pid, NULL, NULL) != 0) { return fail("cont to exit"); } - if (waitpid(pid, &status, 0) != pid) { - return fail("waitpid exit"); + if (wait_child(pid, &status, 0, "waitpid exit") != 0) { + return 1; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { printf("FAIL: traced dynamic target did not exit cleanly, status=%#x\n", status); diff --git a/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-aarch64.toml b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-aarch64.toml new file mode 100644 index 0000000000..348870d3a0 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-aarch64.toml @@ -0,0 +1,88 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "nvme,serial=deadbeef,drive=nvm", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-tablet-pci", + "-device", + "qemu-xhci,id=xhci,msi=off,msix=off", + "-audiodev", + "none,id=audio0", + "-device", + "usb-audio,bus=xhci.0,audiodev=audio0", + "-drive", + "id=usbdisk,if=none,format=raw,snapshot=on,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-busybox.img", + "-device", + "usb-storage,bus=xhci.0,drive=usbdisk,serial=starry-usb-storage", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +cat > /tmp/tty-input-burst.sh <<'EOF' +#!/bin/sh +fail=0 +checked=0 +fail_marker=STARRY_TTY_INPUT_BURST_""FAILED +check() { + name="$1" + got="$2" + want="$3" + checked=$((checked + 1)) + if [ "$got" = "$want" ]; then + echo "STARRY_TTY_INPUT_BURST_OK:$name:${#got}" + else + echo "$fail_marker:$name:got=${#got}:want=${#want}:$got" + fail=1 + fi +} +check short "abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz" +check digits "012345678901234567890123456789012345678901234567890123456789" "012345678901234567890123456789012345678901234567890123456789" +check cmd01 "qwertyuiopasdfghjklzxcvbnm" "qwertyuiopasdfghjklzxcvbnm" +check cmd02 "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" +check long01 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +check long02 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +check long03 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +check mix01 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" +i=0 +while [ "$i" -lt 120 ]; do + i=$((i + 1)) + got="line-${i}-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-tty-input-stress-END" + want=$(printf 'line-%s-%s-%s-%s-%s' "$i" "abcdefghijklmnopqrstuvwxyz" "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "tty-input-stress-END") + check "loop${i}" "$got" "$want" +done +if [ "$checked" -ne 128 ]; then + echo "$fail_marker:checked-count:$checked" + fail=1 +fi +if [ "$fail" -eq 0 ]; then + echo STARRY_TTY_INPUT_BURST_PASSED +else + echo "$fail_marker" +fi +EOF +sh /tmp/tty-input-burst.sh +''' +success_regex = ["(?m)^STARRY_TTY_INPUT_BURST_PASSED\\s*$"] +fail_regex = [ + "(?i)panic", + "(?m)^lockdep fatal violation\\s*$", + "/dev/console has no serial TTY binding", + "Failed to bind console tty", + "(?m)^STARRY_TTY_INPUT_BURST_FAILED", +] +timeout = 300 diff --git a/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-loongarch64.toml b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-loongarch64.toml new file mode 100644 index 0000000000..221439528d --- /dev/null +++ b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-loongarch64.toml @@ -0,0 +1,80 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "2G", + "-device", + "nvme,serial=deadbeef,drive=nvm", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-tablet-pci", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +cat > /tmp/tty-input-burst.sh <<'EOF' +#!/bin/sh +fail=0 +checked=0 +fail_marker=STARRY_TTY_INPUT_BURST_""FAILED +check() { + name="$1" + got="$2" + want="$3" + checked=$((checked + 1)) + if [ "$got" = "$want" ]; then + echo "STARRY_TTY_INPUT_BURST_OK:$name:${#got}" + else + echo "$fail_marker:$name:got=${#got}:want=${#want}:$got" + fail=1 + fi +} +check short "abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz" +check digits "012345678901234567890123456789012345678901234567890123456789" "012345678901234567890123456789012345678901234567890123456789" +check cmd01 "qwertyuiopasdfghjklzxcvbnm" "qwertyuiopasdfghjklzxcvbnm" +check cmd02 "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" +check long01 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +check long02 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +check long03 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +check mix01 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" +i=0 +while [ "$i" -lt 120 ]; do + i=$((i + 1)) + got="line-${i}-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-tty-input-stress-END" + want=$(printf 'line-%s-%s-%s-%s-%s' "$i" "abcdefghijklmnopqrstuvwxyz" "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "tty-input-stress-END") + check "loop${i}" "$got" "$want" +done +if [ "$checked" -ne 128 ]; then + echo "$fail_marker:checked-count:$checked" + fail=1 +fi +if [ "$fail" -eq 0 ]; then + echo STARRY_TTY_INPUT_BURST_PASSED +else + echo "$fail_marker" +fi +EOF +sh /tmp/tty-input-burst.sh +''' +success_regex = ["(?m)^STARRY_TTY_INPUT_BURST_PASSED\\s*$"] +fail_regex = [ + "(?i)panic", + "(?m)^lockdep fatal violation\\s*$", + "/dev/console has no serial TTY binding", + "Failed to bind console tty", + "(?m)^STARRY_TTY_INPUT_BURST_FAILED", +] +timeout = 300 diff --git a/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-riscv64.toml b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-riscv64.toml new file mode 100644 index 0000000000..3cbf18fc09 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-riscv64.toml @@ -0,0 +1,88 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "nvme,serial=deadbeef,drive=nvm", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-tablet-pci", + "-device", + "qemu-xhci,id=xhci,msi=off,msix=off", + "-audiodev", + "none,id=audio0", + "-device", + "usb-audio,bus=xhci.0,audiodev=audio0", + "-drive", + "id=usbdisk,if=none,format=raw,snapshot=on,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-busybox.img", + "-device", + "usb-storage,bus=xhci.0,drive=usbdisk,serial=starry-usb-storage", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = ''' +cat > /tmp/tty-input-burst.sh <<'EOF' +#!/bin/sh +fail=0 +checked=0 +fail_marker=STARRY_TTY_INPUT_BURST_""FAILED +check() { + name="$1" + got="$2" + want="$3" + checked=$((checked + 1)) + if [ "$got" = "$want" ]; then + echo "STARRY_TTY_INPUT_BURST_OK:$name:${#got}" + else + echo "$fail_marker:$name:got=${#got}:want=${#want}:$got" + fail=1 + fi +} +check short "abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz" +check digits "012345678901234567890123456789012345678901234567890123456789" "012345678901234567890123456789012345678901234567890123456789" +check cmd01 "qwertyuiopasdfghjklzxcvbnm" "qwertyuiopasdfghjklzxcvbnm" +check cmd02 "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" +check long01 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +check long02 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +check long03 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +check mix01 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" +i=0 +while [ "$i" -lt 120 ]; do + i=$((i + 1)) + got="line-${i}-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-tty-input-stress-END" + want=$(printf 'line-%s-%s-%s-%s-%s' "$i" "abcdefghijklmnopqrstuvwxyz" "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "tty-input-stress-END") + check "loop${i}" "$got" "$want" +done +if [ "$checked" -ne 128 ]; then + echo "$fail_marker:checked-count:$checked" + fail=1 +fi +if [ "$fail" -eq 0 ]; then + echo STARRY_TTY_INPUT_BURST_PASSED +else + echo "$fail_marker" +fi +EOF +sh /tmp/tty-input-burst.sh +''' +success_regex = ["(?m)^STARRY_TTY_INPUT_BURST_PASSED\\s*$"] +fail_regex = [ + "(?i)panic", + "(?m)^lockdep fatal violation\\s*$", + "/dev/console has no serial TTY binding", + "Failed to bind console tty", + "(?m)^STARRY_TTY_INPUT_BURST_FAILED", +] +timeout = 300 diff --git a/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-x86_64.toml b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-x86_64.toml new file mode 100644 index 0000000000..c86fc33dd2 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/tty-console-input-burst/qemu-x86_64.toml @@ -0,0 +1,66 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "nvme,serial=deadbeef,drive=nvm", + "-drive", + "id=nvm,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = ''' +cat > /tmp/tty-input-burst.sh <<'EOF' +#!/bin/sh +fail=0 +checked=0 +fail_marker=STARRY_TTY_INPUT_BURST_""FAILED +check() { + name="$1" + got="$2" + want="$3" + checked=$((checked + 1)) + if [ "$got" = "$want" ]; then + echo "STARRY_TTY_INPUT_BURST_OK:$name:${#got}" + else + echo "$fail_marker:$name:got=${#got}:want=${#want}:$got" + fail=1 + fi +} +check short "abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz" +check digits "012345678901234567890123456789012345678901234567890123456789" "012345678901234567890123456789012345678901234567890123456789" +check cmd01 "qwertyuiopasdfghjklzxcvbnm" "qwertyuiopasdfghjklzxcvbnm" +check cmd02 "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ" +check long01 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +check long02 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +check long03 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +check mix01 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.:/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-END" +i=0 +while [ "$i" -lt 120 ]; do + i=$((i + 1)) + got="line-${i}-abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-tty-input-stress-END" + want=$(printf 'line-%s-%s-%s-%s-%s' "$i" "abcdefghijklmnopqrstuvwxyz" "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "tty-input-stress-END") + check "loop${i}" "$got" "$want" +done +if [ "$checked" -ne 128 ]; then + echo "$fail_marker:checked-count:$checked" + fail=1 +fi +if [ "$fail" -eq 0 ]; then + echo STARRY_TTY_INPUT_BURST_PASSED +else + echo "$fail_marker" +fi +EOF +sh /tmp/tty-input-burst.sh +''' +success_regex = ["(?m)^STARRY_TTY_INPUT_BURST_PASSED\\s*$"] +fail_regex = [ + "(?i)panic", + "(?m)^lockdep fatal violation\\s*$", + "/dev/console has no serial TTY binding", + "Failed to bind console tty", + "(?m)^STARRY_TTY_INPUT_BURST_FAILED", +] +timeout = 300 From 73bf9d2a44227c6a1e6eca1e825f46bea28ebda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 13:04:48 +0800 Subject: [PATCH 02/16] fix(starry-kernel): avoid serial echo backpressure stalls --- drivers/interface/rdif-serial/src/lib.rs | 16 ++++ drivers/serial/some-serial/README.md | 83 ++++++++-------- drivers/serial/some-serial/src/lib.rs | 3 +- .../kernel/src/pseudofs/dev/tty/pty.rs | 11 ++- .../kernel/src/pseudofs/dev/tty/serial.rs | 27 +++++- .../src/pseudofs/dev/tty/terminal/ldisc.rs | 95 ++++++++++++++++++- 6 files changed, 179 insertions(+), 56 deletions(-) diff --git a/drivers/interface/rdif-serial/src/lib.rs b/drivers/interface/rdif-serial/src/lib.rs index 6389f8b6a2..8562d04263 100644 --- a/drivers/interface/rdif-serial/src/lib.rs +++ b/drivers/interface/rdif-serial/src/lib.rs @@ -1,3 +1,19 @@ +//! Portable interrupt-driven serial runtime primitives. +//! +//! The reusable stack is intentionally split by synchronization ownership: +//! raw UART drivers expose only register-level operations, `SerialCore` owns +//! the TX software FIFO and RX flip FIFO, and OS glue wraps the core in the +//! kernel's short port lock. Runtime queues and TTY code must not access UART +//! registers directly; only the locked core may read destructive IRQ/status +//! registers. +//! +//! IRQ handlers call `SerialCore::handle_irq()` to synchronize hardware state +//! into software queues. Task or worker context drains `RxItem`s and enqueues TX +//! bytes, but never polls the shared UART IRQ/status register to rediscover +//! readiness. This keeps the fast path bounded and leaves wakeups, wait queues, +//! poll sets, and line discipline processing to OS-specific layers above this +//! crate. + #![no_std] extern crate alloc; diff --git a/drivers/serial/some-serial/README.md b/drivers/serial/some-serial/README.md index 24d3e75328..c8f469c7c0 100644 --- a/drivers/serial/some-serial/README.md +++ b/drivers/serial/some-serial/README.md @@ -73,7 +73,7 @@ someboot 等 allocator 初始化前路径直接保存这个对象,不需要 `B ```rust use core::ptr::NonNull; use some_serial::{ - ns16550::Ns16550, Config, DataBits, InterfaceRaw as _, Parity, SerialDirection, StopBits, + ns16550::Ns16550, Config, DataBits, Parity, RawUart as _, StopBits, }; let base_addr = NonNull::new(0x9000000 as *mut u8).unwrap(); @@ -85,27 +85,32 @@ let config = Config::new() .stop_bits(StopBits::One) .parity(Parity::None); -uart.set_config(&config).expect("Failed to configure UART"); -uart.open(); +uart.startup(&config).expect("Failed to configure UART"); uart.enable_loopback(); let test_data = b"Hello, Serial!"; let mut sent = 0; while sent < test_data.len() { - let n = uart.try_write(&test_data[sent..]); - if n == 0 { + let status = uart.poll_status(); + if !status.tx_ready() { core::hint::spin_loop(); + continue; } - sent += n; + uart.write_byte(test_data[sent]); + sent += 1; } println!("Sent {} bytes", sent); let mut buffer = [0u8; 64]; -let received = if uart.pending(SerialDirection::Input) { - uart.try_read(&mut buffer).expect("Failed to receive") -} else { - 0 -}; +let mut received = 0; +while received < buffer.len() { + let status = uart.poll_status(); + let Some(result) = uart.read_byte(status) else { + break; + }; + buffer[received] = result.expect("Failed to receive"); + received += 1; +} println!("Received {} bytes: {:?}", received, &buffer[..received]); ``` @@ -138,55 +143,44 @@ let mut uart = some_serial::ns16550::Ns16550::new_mmio( #### 中断驱动通信 ```rust -use some_serial::{InterfaceRaw as _, InterruptMask}; +use rdif_serial::{InterruptMask, RawUart as _}; use some_serial::pl011::Pl011; // 创建并配置 UART let mut uart = Pl011::new(base_addr, clock_freq); -uart.set_config(&config).unwrap(); -uart.open(); +uart.startup(&config).unwrap(); // 启用中断 -uart.set_irq_mask(InterruptMask::RX_AVAILABLE | InterruptMask::TX_EMPTY); +uart.set_irq_mask(InterruptMask::RX | InterruptMask::TX_SPACE); // 在中断控制器回调中同步硬件 IRQ 状态 -let event = uart.handle_irq(); -if event.rx_ready() { - // 运行时决定唤醒任务或继续轮询 +let snapshot = uart.take_irq_snapshot(); +if snapshot.claimed { + // 上层 runtime 决定是否读取 RX FIFO、推进 TX FIFO、唤醒任务。 } - -// 数据搬运仍由任务态通过 try_read/try_write 推进 ``` #### 平台检测与适配 -需要运行时动态分发的 rdrive/Starry 路径可以把 concrete 设备包装成 `rdif_serial::BSerial`。 -这个对象只负责控制和 split/restore;拆出的 TX/RX/IRQ runtime parts 各自持有可复制寄存器入口, -并通过共享原子状态同步 IRQ event 和 read-clear 错误位,不在 rdif adapter 内使用 Mutex。 +需要运行时动态分发的 rdrive/Starry 路径应在 OS glue 层把 concrete raw driver 包装进 +`rdif_serial::SerialCore`,再由目标内核提供端口锁。`some-serial` 自身不包含 mutex、 +wait queue、poll set 或任务唤醒逻辑。 ```rust use core::ptr::NonNull; -use rdif_serial::{BSerial, Interface as _, TTxQueue as _}; - -fn create_serial_for_runtime(base_addr: NonNull, clock_freq: u32) -> BSerial { - some_serial::ns16550::Ns16550::new_mmio_boxed(base_addr, clock_freq, 1) -} +use rdif_serial::{Config, SerialCore}; +use some_serial::ns16550::Ns16550; -let mut serial = create_serial_for_runtime( +let raw = Ns16550::new_mmio( NonNull::new(0x40000000 as *mut u8).unwrap(), 16_000_000, + 1, ); +let mut core = SerialCore::new(raw); +core.startup(&Config::new().baudrate(115200)).unwrap(); -let mut tx = serial.take_tx().expect("missing TX queue"); -let mut sent = 0; -let bytes = b"runtime serial\n"; -while sent < bytes.len() { - let n = tx.try_write(&bytes[sent..]); - if n == 0 { - core::hint::spin_loop(); - } - sent += n; -} +let accepted = core.enqueue_tx(b"runtime serial\n").accepted; +assert!(accepted > 0); ``` #### 平台特定配置获取 @@ -237,8 +231,8 @@ let stop_bits = uart.stop_bits(); let parity = uart.parity(); // 查询 I/O 就绪事件 -let event = uart.poll(); -let can_write = uart.pending(some_serial::SerialDirection::Output); +let event = uart.poll_status(); +let can_write = event.tx_ready(); ``` ## 测试 @@ -289,7 +283,7 @@ cargo test --test test -- --show-output --uboot ### 添加新驱动支持 1. **创建驱动模块**:在 `src/` 目录下创建新的驱动文件 -2. **实现 raw 接口**:驱动对象实现 `InterfaceRaw` 的配置、IRQ mask、`pending`、`poll`、`try_write`、`try_read`、`handle_irq` +2. **实现 raw 接口**:驱动对象实现 `RawUart` 的配置、IRQ mask、IRQ snapshot、RX sample、TX ready/write 等寄存器级方法 3. **添加测试**:为新驱动编写完整的测试套件 4. **更新文档**:在 README 中添加驱动说明和使用示例 5. **提交 PR**:详细描述新驱动的功能和使用方法 @@ -304,9 +298,8 @@ pub struct NewDriver { // 驱动寄存器句柄、时钟、saved status、IRQ mask shadow 等状态 } -impl InterfaceRaw for NewDriver { - // 实现配置、开关、IRQ mask - // 实现 pending/poll/try_write/try_read/handle_irq +impl RawUart for NewDriver { + // 实现配置、IRQ mask、take_irq_snapshot、read_rx、tx_ready、write_tx 等方法 } ``` diff --git a/drivers/serial/some-serial/src/lib.rs b/drivers/serial/some-serial/src/lib.rs index fdeb067cf1..3323a1dd91 100644 --- a/drivers/serial/some-serial/src/lib.rs +++ b/drivers/serial/some-serial/src/lib.rs @@ -46,8 +46,7 @@ //! .stop_bits(some_serial::StopBits::One) //! .parity(some_serial::Parity::None); //! -//! uart.set_config(&config).unwrap(); -//! uart.open(); +//! uart.startup(&config).unwrap(); //! //! while !uart.tx_ready() { //! core::hint::spin_loop(); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs index 24ad94e913..a185d3c70e 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/pty.rs @@ -46,13 +46,18 @@ impl PtyWriter { impl TtyWrite for PtyWriter { fn write(&self, buf: &[u8]) { - let read = self.0.lock().push_slice(buf); - // PTY bytes are committed before waking the peer reader. - unsafe { self.1.wake(IoEvents::IN) }; + let read = self.try_write(buf); if read < buf.len() { warn!("Discarding {} bytes written to pty", buf.len() - read); } } + + fn try_write(&self, buf: &[u8]) -> usize { + let read = self.0.lock().push_slice(buf); + // PTY bytes are committed before waking the peer reader. + unsafe { self.1.wake(IoEvents::IN) }; + read + } } pub(crate) fn create_pty_pair() -> (Arc, Arc) { diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index 6411734625..cacec08fd0 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -35,6 +35,7 @@ pub type SerialTtyDriver = Tty; const SERIAL_RX_DRAIN_CHUNK: usize = 256; const SERIAL_SYNC_ECHO_LIMIT: usize = 256; +const SERIAL_DEFAULT_BAUDRATE: u32 = 115_200; bitflags! { #[derive(Clone, Copy, Debug, Default)] @@ -364,7 +365,7 @@ impl SerialBackend { if let Err(err) = self .port - .startup(&Config::new().baudrate(self.port.baudrate())) + .startup(&Config::new().baudrate(startup_baudrate(self.port.baudrate()))) { warn!( "{} failed to start serial port {}: {:?}", @@ -388,6 +389,14 @@ impl SerialBackend { } } +fn startup_baudrate(current: u32) -> u32 { + if current == 0 { + SERIAL_DEFAULT_BAUDRATE + } else { + current + } +} + fn spawn_serial_event_worker(backend: Arc) { let task_name = format!("{}-event", backend.tty_name); ax_task::spawn_with_name( @@ -505,6 +514,16 @@ impl TtyWrite for SerialWriter { } } + fn try_write(&self, buf: &[u8]) -> usize { + if buf.is_empty() { + return 0; + } + let Some(_guard) = self.backend.output_lock.try_lock() else { + return 0; + }; + self.backend.port.try_write(buf) + } + fn flush_echo_before_input(&self) -> bool { true } @@ -696,4 +715,10 @@ mod tests { None ); } + + #[test] + fn zero_hardware_baudrate_uses_runtime_default() { + assert_eq!(super::startup_baudrate(0), super::SERIAL_DEFAULT_BAUDRATE); + assert_eq!(super::startup_baudrate(1_500_000), 1_500_000); + } } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs index bc062e764f..ef32ff6ab1 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs @@ -56,6 +56,11 @@ pub trait TtyRead: Send + Sync + 'static { pub trait TtyWrite: Send + Sync + 'static { fn write(&self, buf: &[u8]); + fn try_write(&self, buf: &[u8]) -> usize { + self.write(buf); + buf.len() + } + fn flush_echo_before_input(&self) -> bool { false } @@ -301,25 +306,39 @@ impl EchoQueue { } fn write_now(&self, bytes: &[u8]) { - self.writer.write(bytes); + let written = self.writer.try_write(bytes); + if written < bytes.len() { + self.enqueue(&bytes[written..]); + } } fn drain_available(&self) -> bool { let mut progressed = false; loop { let chunk = { - let mut queue = self.queue.lock(); + let queue = self.queue.lock(); if queue.is_empty() { break; } let len = queue.len().min(ECHO_WRITE_CHUNK); let mut chunk = Vec::with_capacity(len); - for _ in 0..len { - chunk.push(queue.pop_front().unwrap()); + for byte in queue.iter().take(len) { + chunk.push(*byte); } chunk }; - self.writer.write(&chunk); + let written = self.writer.try_write(&chunk); + if written == 0 { + break; + } + { + let mut queue = self.queue.lock(); + for _ in 0..written { + if queue.pop_front().is_none() { + break; + } + } + } progressed = true; } @@ -648,6 +667,11 @@ mod tests { self.calls.fetch_add(1, Ordering::Relaxed); self.bytes.fetch_add(buf.len(), Ordering::Relaxed); } + + fn try_write(&self, buf: &[u8]) -> usize { + self.write(buf); + buf.len() + } } struct OrderedEchoWriter { @@ -661,6 +685,11 @@ mod tests { self.bytes.fetch_add(buf.len(), Ordering::Relaxed); } + fn try_write(&self, buf: &[u8]) -> usize { + self.write(buf); + buf.len() + } + fn flush_echo_before_input(&self) -> bool { true } @@ -678,11 +707,41 @@ mod tests { self.bytes.fetch_add(buf.len(), Ordering::Relaxed); } + fn try_write(&self, buf: &[u8]) -> usize { + self.write(buf); + buf.len() + } + fn max_sync_echo_bytes(&self) -> usize { self.limit } } + struct BackpressuredWriter { + calls: Arc, + bytes: Arc, + budget: Arc, + } + + impl TtyWrite for BackpressuredWriter { + fn write(&self, _buf: &[u8]) { + panic!("echo flushing must use non-blocking try_write"); + } + + fn try_write(&self, buf: &[u8]) -> usize { + self.calls.fetch_add(1, Ordering::Relaxed); + let budget = self.budget.load(Ordering::Acquire); + let written = buf.len().min(budget); + self.budget.fetch_sub(written, Ordering::AcqRel); + self.bytes.fetch_add(written, Ordering::Relaxed); + written + } + + fn flush_echo_before_input(&self) -> bool { + true + } + } + struct PanicWriter; impl TtyWrite for PanicWriter { fn write(&self, _buf: &[u8]) { @@ -900,6 +959,32 @@ mod tests { assert_eq!(rx.occupied_len(), b"burst\n".len()); } + #[test] + fn synchronous_echo_backpressure_queues_unsent_suffix() { + let calls = Arc::new(AtomicUsize::new(0)); + let bytes = Arc::new(AtomicUsize::new(0)); + let budget = Arc::new(AtomicUsize::new(2)); + let echo = EchoQueue::new( + BackpressuredWriter { + calls: calls.clone(), + bytes: bytes.clone(), + budget: budget.clone(), + }, + Arc::new(PollSet::new()), + ); + + echo.write_now(b"abcdef"); + + assert_eq!(bytes.load(Ordering::Relaxed), 2); + assert_eq!(echo.queue.lock().len(), 4); + + budget.store(4, Ordering::Release); + assert!(echo.drain_available()); + assert_eq!(bytes.load(Ordering::Relaxed), 6); + assert!(echo.queue.lock().is_empty()); + assert!(calls.load(Ordering::Relaxed) >= 2); + } + #[test] fn injected_input_is_readable_immediately() { let mut ldisc = LineDiscipline::new( From 23c05054511bb13a19da56d5f739ed60f21a2b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 14:57:08 +0800 Subject: [PATCH 03/16] fix(serial): preserve IRQ status and defer tty startup --- drivers/interface/rdif-serial/src/core.rs | 35 +++- .../serial/some-serial/src/ns16550/dw_apb.rs | 12 +- drivers/serial/some-serial/src/ns16550/mod.rs | 66 ++++++- drivers/serial/some-serial/src/pl011.rs | 169 +++++++++++++++--- .../kernel/src/pseudofs/dev/tty/mod.rs | 6 + .../kernel/src/pseudofs/dev/tty/serial.rs | 38 +++- .../src/pseudofs/dev/tty/terminal/ldisc.rs | 4 + 7 files changed, 287 insertions(+), 43 deletions(-) diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index 3ac11eb0a1..6330136d99 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -294,14 +294,15 @@ impl SerialCore { break; }; + match sample.flag { + RxFlag::Normal => {} + RxFlag::Break => self.counters.rx_breaks += 1, + RxFlag::Parity => self.counters.rx_parity_errors += 1, + RxFlag::Framing => self.counters.rx_framing_errors += 1, + } + if let Some(byte) = sample.byte { self.counters.rx_bytes += 1; - match sample.flag { - RxFlag::Normal => {} - RxFlag::Break => self.counters.rx_breaks += 1, - RxFlag::Parity => self.counters.rx_parity_errors += 1, - RxFlag::Framing => self.counters.rx_framing_errors += 1, - } if self .rx_fifo @@ -543,4 +544,26 @@ mod tests { assert!(core.counters().rx_queue_dropped > 0); } + + #[test] + fn rx_status_without_byte_is_preserved_as_queue_event() { + let mut uart = MockUart::new().irq(IrqSource::RX_STATUS); + uart.rx.push_back(RxSample { + byte: None, + flag: RxFlag::Parity, + overrun: true, + }); + let mut core = started_core::<16, 16>(uart); + + let outcome = core.handle_irq(); + + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 1); + assert_eq!(core.counters().rx_parity_errors, 1); + assert_eq!(core.counters().rx_fifo_overruns, 1); + + let mut items = [RxItem::default(); 1]; + assert_eq!(core.drain_rx(&mut items), 1); + assert_eq!(items[0], RxItem::Overrun); + } } diff --git a/drivers/serial/some-serial/src/ns16550/dw_apb.rs b/drivers/serial/some-serial/src/ns16550/dw_apb.rs index bcbd1e16f4..979094b32f 100644 --- a/drivers/serial/some-serial/src/ns16550/dw_apb.rs +++ b/drivers/serial/some-serial/src/ns16550/dw_apb.rs @@ -95,24 +95,28 @@ impl Kind for DwApb { self.wait_not_busy(); - let mut lcr: LineControlFlags = self.read_flags(UART_LCR); - lcr.insert(LineControlFlags::DIVISOR_LATCH_ACCESS); - self.write_flags(UART_LCR, lcr); + let lcr: LineControlFlags = self.read_flags(UART_LCR); + self.write_flags(UART_LCR, lcr | LineControlFlags::DIVISOR_LATCH_ACCESS); self.write_reg(UART_DLL, ((divider >> DLF_LEN) & 0xff) as u8); self.write_reg(UART_DLH, ((divider >> (DLF_LEN + 8)) & 0xff) as u8); self.write_u32(UART_DLF_OFFSET, (divider & ((1 << DLF_LEN) - 1)) as u32); - lcr.remove(LineControlFlags::DIVISOR_LATCH_ACCESS); self.write_flags(UART_LCR, lcr); Ok(()) } fn baudrate(&self, clock_freq: u32) -> u32 { + let lcr: LineControlFlags = self.read_flags(UART_LCR); + self.write_flags(UART_LCR, lcr | LineControlFlags::DIVISOR_LATCH_ACCESS); + let dll = self.read_reg(UART_DLL) as u64; let dlh = self.read_reg(UART_DLH) as u64; let dlf = (self.read_u32(UART_DLF_OFFSET) & ((1 << DLF_LEN) - 1)) as u64; + + self.write_flags(UART_LCR, lcr); + let divider = (dll << DLF_LEN) | (dlh << (DLF_LEN + 8)) | dlf; if divider == 0 { diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index cb944b76c9..e7faea7535 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -46,22 +46,26 @@ pub trait Kind: Clone + Send + Sync + 'static { return Err(ConfigError::InvalidBaudrate); } - let mut lcr: LineControlFlags = self.read_flags(UART_LCR); - lcr.insert(LineControlFlags::DIVISOR_LATCH_ACCESS); - self.write_flags(UART_LCR, lcr); + let lcr: LineControlFlags = self.read_flags(UART_LCR); + self.write_flags(UART_LCR, lcr | LineControlFlags::DIVISOR_LATCH_ACCESS); self.write_reg(UART_DLL, (divisor & 0xFF) as u8); self.write_reg(UART_DLH, ((divisor >> 8) & 0xFF) as u8); - lcr.remove(LineControlFlags::DIVISOR_LATCH_ACCESS); self.write_flags(UART_LCR, lcr); Ok(()) } fn baudrate(&self, clock_freq: u32) -> u32 { + let lcr: LineControlFlags = self.read_flags(UART_LCR); + self.write_flags(UART_LCR, lcr | LineControlFlags::DIVISOR_LATCH_ACCESS); + let dll = self.read_reg(UART_DLL) as u16; let dlh = self.read_reg(UART_DLH) as u16; + + self.write_flags(UART_LCR, lcr); + let divisor = dll | (dlh << 8); if divisor == 0 { @@ -697,6 +701,8 @@ mod tests { use super::*; static REGS: [AtomicU8; 8] = [const { AtomicU8::new(0) }; 8]; + static DLL_REG: AtomicU8 = AtomicU8::new(0); + static DLH_REG: AtomicU8 = AtomicU8::new(0); static THR_WRITES: AtomicUsize = AtomicUsize::new(0); static TEST_LOCK: Mutex<()> = Mutex::new(()); @@ -705,6 +711,17 @@ mod tests { impl Kind for MockKind { fn read_reg(&self, reg: u8) -> u8 { + let dlab = REGS[UART_LCR as usize].load(Ordering::SeqCst) + & LineControlFlags::DIVISOR_LATCH_ACCESS.bits() + != 0; + if dlab { + return match reg { + UART_DLL => DLL_REG.load(Ordering::SeqCst), + UART_DLH => DLH_REG.load(Ordering::SeqCst), + _ => REGS[reg as usize].load(Ordering::SeqCst), + }; + } + let value = REGS[reg as usize].load(Ordering::SeqCst); if reg == UART_RBR { REGS[UART_LSR as usize].fetch_and( @@ -719,6 +736,23 @@ mod tests { } fn write_reg(&self, reg: u8, val: u8) { + let dlab = REGS[UART_LCR as usize].load(Ordering::SeqCst) + & LineControlFlags::DIVISOR_LATCH_ACCESS.bits() + != 0; + if dlab { + match reg { + UART_DLL => { + DLL_REG.store(val, Ordering::SeqCst); + return; + } + UART_DLH => { + DLH_REG.store(val, Ordering::SeqCst); + return; + } + _ => {} + } + } + REGS[reg as usize].store(val, Ordering::SeqCst); if reg == UART_THR { let iir = REGS[UART_IIR as usize].load(Ordering::SeqCst); @@ -748,6 +782,8 @@ mod tests { for reg in ®S { reg.store(0, Ordering::SeqCst); } + DLL_REG.store(0, Ordering::SeqCst); + DLH_REG.store(0, Ordering::SeqCst); THR_WRITES.store(0, Ordering::SeqCst); } @@ -770,6 +806,28 @@ mod tests { core } + #[test] + fn baudrate_reads_divisor_latch_without_consuming_rx_register() { + let (_guard, uart) = serial(); + let original_lcr = LineControlFlags::WORD_LENGTH_8 | LineControlFlags::STOP_BITS; + REGS[UART_LCR as usize].store(original_lcr.bits(), Ordering::SeqCst); + REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); + REGS[UART_RBR as usize].store(0, Ordering::SeqCst); + REGS[UART_IER as usize].store(0, Ordering::SeqCst); + DLL_REG.store(1, Ordering::SeqCst); + DLH_REG.store(0, Ordering::SeqCst); + + assert_eq!(uart.baudrate(), 115_200); + assert_eq!( + REGS[UART_LCR as usize].load(Ordering::SeqCst), + original_lcr.bits() + ); + assert!( + LineStatusFlags::from_bits_retain(REGS[UART_LSR as usize].load(Ordering::SeqCst)) + .contains(LineStatusFlags::DATA_READY) + ); + } + #[test] fn pending_output_preserves_rx_error_latch() { let (_guard, mut uart) = serial(); diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index 08a8cf068c..1287a8c040 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -4,7 +4,9 @@ use rdif_serial::{ InterruptMask, IrqSnapshot, IrqSource, RawUart, RxFlag, RxSample, SerialDirection, SerialEvent, TransBytesError, TransferError, }; -use tock_registers::{interfaces::*, register_bitfields, register_structs, registers::*}; +use tock_registers::{ + LocalRegisterCopy, interfaces::*, register_bitfields, register_structs, registers::*, +}; use crate::{Config, ConfigError, DataBits, Parity, StopBits}; @@ -142,6 +144,7 @@ unsafe impl Sync for Pl011Registers {} pub struct Pl011 { base: Reg, clock_freq: u32, + saved_rx_status: Pl011RxStatus, } impl Pl011 { @@ -158,7 +161,11 @@ impl Pl011 { pub fn new(base: NonNull, clock_freq: u32) -> Self { let base = Reg(base.cast()); - Self { base, clock_freq } + Self { + base, + clock_freq, + saved_rx_status: Pl011RxStatus::empty(), + } } fn registers(&self) -> &Pl011Registers { @@ -357,12 +364,13 @@ impl Pl011 { event |= SerialEvent::TX_READY; } - let rsr = self.registers().uartrsr_ecr.extract(); - if rsr.is_set(UARTRSR_ECR::FE) || rsr.is_set(UARTRSR_ECR::PE) || rsr.is_set(UARTRSR_ECR::BE) + let status = + self.saved_rx_status | Pl011RxStatus::from_rsr(self.registers().uartrsr_ecr.extract()); + if status.intersects(Pl011RxStatus::FRAMING | Pl011RxStatus::PARITY | Pl011RxStatus::BREAK) { event |= SerialEvent::RX_ERROR; } - if rsr.is_set(UARTRSR_ECR::OE) { + if status.contains(Pl011RxStatus::OVERRUN) { event |= SerialEvent::RX_ERROR | SerialEvent::OVERRUN; } @@ -427,23 +435,16 @@ impl Pl011 { return None; } - let dr = self.registers().uartdr.extract(); - let data = dr.read(UARTDR::DATA) as u8; - - if dr.is_set(UARTDR::FE) { - return Some(Err(TransferError::Framing)); - } - if dr.is_set(UARTDR::PE) { - return Some(Err(TransferError::Parity)); + let sample = self.read_rx()?; + if sample.overrun { + return Some(Err(TransferError::Overrun(sample.byte.unwrap_or(0)))); } - if dr.is_set(UARTDR::OE) { - return Some(Err(TransferError::Overrun(data))); - } - if dr.is_set(UARTDR::BE) { - return Some(Err(TransferError::Break)); + match sample.flag { + RxFlag::Normal => sample.byte.map(Ok), + RxFlag::Break => Some(Err(TransferError::Break)), + RxFlag::Parity => Some(Err(TransferError::Parity)), + RxFlag::Framing => Some(Err(TransferError::Framing)), } - - Some(Ok(data)) } pub fn take_irq_snapshot(&mut self) -> IrqSnapshot { @@ -466,6 +467,7 @@ impl Pl011 { || mis.is_set(UARTIS::OE) { sources |= IrqSource::RX_STATUS; + self.saved_rx_status |= Pl011RxStatus::from_irq_status(mis); } if mis.is_set(UARTIS::TX) { sources |= IrqSource::TX_SPACE; @@ -490,16 +492,22 @@ impl Pl011 { pub fn read_rx(&mut self) -> Option { if self.registers().uartfr.is_set(UARTFR::RXFE) { - return None; + self.saved_rx_status |= Pl011RxStatus::from_rsr(self.registers().uartrsr_ecr.extract()); + return self.saved_rx_status.take_status_sample(); } let dr = self.registers().uartdr.extract(); let data = dr.read(UARTDR::DATA) as u8; - let flag = if dr.is_set(UARTDR::BE) { + let status = Pl011RxStatus::from_data(dr); + if !status.is_empty() { + self.saved_rx_status.remove(status); + } + + let flag = if status.contains(Pl011RxStatus::BREAK) { RxFlag::Break - } else if dr.is_set(UARTDR::PE) { + } else if status.contains(Pl011RxStatus::PARITY) { RxFlag::Parity - } else if dr.is_set(UARTDR::FE) { + } else if status.contains(Pl011RxStatus::FRAMING) { RxFlag::Framing } else { RxFlag::Normal @@ -508,7 +516,96 @@ impl Pl011 { Some(RxSample { byte: Some(data), flag, - overrun: dr.is_set(UARTDR::OE), + overrun: status.contains(Pl011RxStatus::OVERRUN), + }) + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + struct Pl011RxStatus: u32 { + const FRAMING = 1 << 0; + const PARITY = 1 << 1; + const BREAK = 1 << 2; + const OVERRUN = 1 << 3; + } +} + +impl Pl011RxStatus { + fn from_data(dr: LocalRegisterCopy) -> Self { + let mut status = Self::empty(); + if dr.is_set(UARTDR::FE) { + status |= Self::FRAMING; + } + if dr.is_set(UARTDR::PE) { + status |= Self::PARITY; + } + if dr.is_set(UARTDR::BE) { + status |= Self::BREAK; + } + if dr.is_set(UARTDR::OE) { + status |= Self::OVERRUN; + } + status + } + + fn from_irq_status(mis: LocalRegisterCopy) -> Self { + let mut status = Self::empty(); + if mis.is_set(UARTIS::FE) { + status |= Self::FRAMING; + } + if mis.is_set(UARTIS::PE) { + status |= Self::PARITY; + } + if mis.is_set(UARTIS::BE) { + status |= Self::BREAK; + } + if mis.is_set(UARTIS::OE) { + status |= Self::OVERRUN; + } + status + } + + fn from_rsr(rsr: LocalRegisterCopy) -> Self { + let mut status = Self::empty(); + if rsr.is_set(UARTRSR_ECR::FE) { + status |= Self::FRAMING; + } + if rsr.is_set(UARTRSR_ECR::PE) { + status |= Self::PARITY; + } + if rsr.is_set(UARTRSR_ECR::BE) { + status |= Self::BREAK; + } + if rsr.is_set(UARTRSR_ECR::OE) { + status |= Self::OVERRUN; + } + status + } + + fn flag(self) -> RxFlag { + if self.contains(Self::BREAK) { + RxFlag::Break + } else if self.contains(Self::PARITY) { + RxFlag::Parity + } else if self.contains(Self::FRAMING) { + RxFlag::Framing + } else { + RxFlag::Normal + } + } + + fn take_status_sample(&mut self) -> Option { + if self.is_empty() { + return None; + } + + let status = *self; + *self = Self::empty(); + Some(RxSample { + byte: None, + flag: status.flag(), + overrun: status.contains(Self::OVERRUN), }) } } @@ -828,6 +925,28 @@ mod tests { assert!(sample.overrun); } + #[test] + fn irq_status_without_rx_byte_is_preserved_after_irq_ack() { + let (mut regs, mut uart) = pl011_with_registers(); + + write_test_reg( + &mut regs, + 0x040, + UARTIS::OE::SET.value | UARTIS::PE::SET.value, + ); + write_test_reg(&mut regs, 0x018, UARTFR::RXFE::SET.value); + + let snapshot = uart.take_irq_snapshot(); + assert!(snapshot.claimed); + assert!(snapshot.sources.contains(IrqSource::RX_STATUS)); + + let sample = uart.read_rx().expect("saved RX status should be available"); + assert_eq!(sample.byte, None); + assert_eq!(sample.flag, RxFlag::Parity); + assert!(sample.overrun); + assert!(uart.read_rx().is_none()); + } + #[test] fn serial_core_tx_irq_drains_software_fifo() { let (mut regs, uart) = pl011_with_registers(); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs index 15cff747d1..26a704c1f9 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs @@ -100,6 +100,10 @@ impl Tty { } impl DeviceOps for Tty { + fn open(&self, _exclusive: bool) -> AxResult<()> { + self.writer.open() + } + fn read_at(&self, buf: &mut [u8], _offset: u64) -> AxResult { if self.is_ptm || self.terminal.job_control.current_in_foreground() { self.ldisc.lock().read(buf) @@ -282,6 +286,7 @@ fn filter_cursor_position_requests(match_len: &mut usize, bytes: &[u8]) -> (Vec< impl Pollable for Tty { fn poll(&self) -> IoEvents { + let _ = self.writer.open(); let mut events = IoEvents::OUT | self.terminal.job_control.poll(); if self.is_ptm || events.contains(IoEvents::IN) { events.set(IoEvents::IN, self.ldisc.lock().poll_read()); @@ -290,6 +295,7 @@ impl Pollable for Tty { } fn register(&self, context: &mut Context<'_>, events: IoEvents) { + let _ = self.writer.open(); if !self.is_ptm { self.terminal.job_control.register(context, events); } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index cacec08fd0..4120c7ea23 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -76,6 +76,7 @@ struct SerialBackend { irq_num: usize, irq_handle: SpinNoIrq>, started: AtomicBool, + start_lock: Mutex<()>, events: SerialEvents, input_source: Arc, output_source: Arc, @@ -199,6 +200,7 @@ pub fn bind_console_to(proc: &Process) -> AxResult<()> { if let Some(index) = SERIAL_REGISTRY.console_index && let Some(entry) = SERIAL_REGISTRY.entries.get(index) { + entry.backend.ensure_started()?; return entry.tty.bind_to(proc); } Err(AxError::NoSuchDevice) @@ -291,6 +293,7 @@ fn new_serial_tty(number: usize, serial: SerialDevice) -> AxResult AxResult AxResult<()> { + if self.start_port() { + Ok(()) + } else { + Err(AxError::Unsupported) + } + } } fn startup_baudrate(current: u32) -> u32 { @@ -460,6 +473,10 @@ fn publish_serial_outcome( impl TtyRead for SerialReader { fn read(&mut self, buf: &mut [u8]) -> usize { + if !self.backend.started.load(Ordering::Acquire) { + return 0; + } + let mut total = 0; let mut temp = [RxItem::default(); SERIAL_RX_DRAIN_CHUNK]; @@ -498,10 +515,17 @@ impl TtyRead for SerialReader { } impl TtyWrite for SerialWriter { + fn open(&self) -> AxResult<()> { + self.backend.ensure_started() + } + fn write(&self, buf: &[u8]) { if buf.is_empty() { return; } + if self.backend.ensure_started().is_err() { + return; + } let _guard = self.backend.output_lock.lock(); let mut written = 0; while written < buf.len() { @@ -518,6 +542,9 @@ impl TtyWrite for SerialWriter { if buf.is_empty() { return 0; } + if self.backend.ensure_started().is_err() { + return 0; + } let Some(_guard) = self.backend.output_lock.try_lock() else { return 0; }; @@ -539,6 +566,9 @@ impl TtyWrite for SerialWriter { if old.baudrate() == Some(new_baud) { return; } + if self.backend.ensure_started().is_err() { + return; + } if let Err(err) = self .backend .port diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs index ef32ff6ab1..d33a7d97eb 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/terminal/ldisc.rs @@ -54,6 +54,10 @@ pub trait TtyRead: Send + Sync + 'static { fn read(&mut self, buf: &mut [u8]) -> usize; } pub trait TtyWrite: Send + Sync + 'static { + fn open(&self) -> AxResult<()> { + Ok(()) + } + fn write(&self, buf: &[u8]); fn try_write(&self, buf: &[u8]) -> usize { From 043e98c46f64d64e4d94c65dd31ccc168d16309e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 15:40:21 +0800 Subject: [PATCH 04/16] fix(someboot): refine kernel relocation detection --- components/someboot/src/arch/loongarch64/mod.rs | 4 ++++ components/someboot/src/mem/mmu.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/someboot/src/arch/loongarch64/mod.rs b/components/someboot/src/arch/loongarch64/mod.rs index e385b6bc95..b667eab8f2 100644 --- a/components/someboot/src/arch/loongarch64/mod.rs +++ b/components/someboot/src/arch/loongarch64/mod.rs @@ -297,6 +297,10 @@ impl ArchTrait for Arch { addrspace::PAGE_OFFSET..usize::MAX } + fn is_kernel_relocated_at(addr: usize) -> bool { + (addrspace::VM_LOAD_ADDRESS..usize::MAX).contains(&addr) + } + fn is_mmu_enabled() -> bool { crmd::read().pg() } diff --git a/components/someboot/src/mem/mmu.rs b/components/someboot/src/mem/mmu.rs index bf2bd00de0..d52f6b161c 100644 --- a/components/someboot/src/mem/mmu.rs +++ b/components/someboot/src/mem/mmu.rs @@ -2,7 +2,7 @@ use kernutil::StaticCell; use page_table_generic::PageTable; pub use page_table_generic::{PagingError, PagingResult}; -use crate::{ArchTrait, mem::ram::Ram}; +use crate::mem::ram::Ram; pub type ArchPageTable = PageTable<::P, A>; From 9ce4f03f51d49d60cfc224fcc4d8a5e79bf0c428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 16:27:35 +0800 Subject: [PATCH 05/16] fix(someboot): generalize kernel relocation detection --- components/someboot/src/arch/loongarch64/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/someboot/src/arch/loongarch64/mod.rs b/components/someboot/src/arch/loongarch64/mod.rs index b667eab8f2..e385b6bc95 100644 --- a/components/someboot/src/arch/loongarch64/mod.rs +++ b/components/someboot/src/arch/loongarch64/mod.rs @@ -297,10 +297,6 @@ impl ArchTrait for Arch { addrspace::PAGE_OFFSET..usize::MAX } - fn is_kernel_relocated_at(addr: usize) -> bool { - (addrspace::VM_LOAD_ADDRESS..usize::MAX).contains(&addr) - } - fn is_mmu_enabled() -> bool { crmd::read().pg() } From a49a589cc05c86351d03742e9a69e70d4b4317dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 17:23:59 +0800 Subject: [PATCH 06/16] refactor(serial): enforce owner irq runtime --- components/axklib/src/lib.rs | 37 +- components/irq-framework/src/action.rs | 6 +- components/irq-framework/src/descriptor.rs | 15 +- components/irq-framework/src/lib.rs | 5 +- components/irq-framework/src/registry.rs | 79 +- components/irq-framework/src/types.rs | 41 + components/irq-framework/tests/std_sim.rs | 217 ++++- drivers/ax-driver/src/serial/mod.rs | 4 +- drivers/ax-driver/src/serial/runtime.rs | 132 ++- drivers/interface/rdif-serial/src/core.rs | 856 ++++++++++++------ drivers/interface/rdif-serial/src/lib.rs | 24 +- drivers/interface/rdif-serial/src/queue.rs | 143 ++- drivers/interface/rdif-serial/src/raw.rs | 2 +- drivers/serial/some-serial/src/ns16550/mod.rs | 80 +- drivers/serial/some-serial/src/pl011.rs | 26 +- .../kernel/src/pseudofs/dev/tty/serial.rs | 17 +- os/arceos/modules/axhal/src/dummy.rs | 7 + os/arceos/modules/axhal/src/irq.rs | 9 +- os/arceos/modules/axruntime/src/klib.rs | 21 +- .../ax-plat-loongarch64-qemu-virt/src/irq.rs | 7 + platforms/ax-plat-riscv64-sg2002/src/irq.rs | 7 + .../ax-plat-riscv64-visionfive2/src/irq.rs | 7 + platforms/ax-plat/src/irq.rs | 34 +- platforms/axplat-dyn/src/irq.rs | 10 +- platforms/somehal/src/arch/aarch64/gic/mod.rs | 18 + platforms/somehal/src/arch/aarch64/gic/v2.rs | 15 + platforms/somehal/src/arch/aarch64/gic/v3.rs | 15 + platforms/somehal/src/arch/aarch64/mod.rs | 7 + platforms/somehal/src/arch/loongarch64/mod.rs | 16 + platforms/somehal/src/arch/riscv64/mod.rs | 7 + platforms/somehal/src/arch/riscv64/plic.rs | 71 +- platforms/somehal/src/arch/x86_64/mod.rs | 94 +- platforms/somehal/src/common.rs | 7 + platforms/somehal/src/irq.rs | 13 + 34 files changed, 1615 insertions(+), 434 deletions(-) diff --git a/components/axklib/src/lib.rs b/components/axklib/src/lib.rs index 360433453a..4080330aff 100644 --- a/components/axklib/src/lib.rs +++ b/components/axklib/src/lib.rs @@ -45,9 +45,9 @@ use core::{ptr::NonNull, time::Duration}; pub use ax_errno::{AxError, AxResult}; pub use ax_memory_addr::{PhysAddr, VirtAddr}; pub use irq_framework::{ - AutoEnable as IrqAutoEnable, CpuId as IrqCpuId, CpuMask as IrqCpuMask, IrqContext, IrqError, - IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, - ShareMode as IrqShareMode, + AutoEnable as IrqAutoEnable, CpuId as IrqCpuId, CpuMask as IrqCpuMask, IrqAffinity, IrqContext, + IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, + IrqStatus, RawIrqHandler, ShareMode as IrqShareMode, }; use trait_ffi::*; @@ -151,6 +151,29 @@ pub trait Klib { /// Disable an IRQ action by handle. fn irq_disable(handle: IrqHandle) -> AxResult; + + /// Runs a raw thunk synchronously on the requested CPU. + /// + /// This is an owner-context bridge for driver runtimes that must keep all + /// register access on a fixed CPU. Platform glue should override this when + /// cross-CPU IPI execution is available. + /// + /// # Safety + /// + /// `arg` must stay valid until the function returns, and `f` must be safe + /// to execute in the target CPU's IRQ/IPI context. + unsafe fn irq_run_on_cpu_sync( + cpu: IrqCpuId, + f: unsafe fn(*mut ()), + arg: *mut (), + ) -> Result<(), IrqError> { + if cpu.0 == 0 { + unsafe { f(arg) }; + Ok(()) + } else { + Err(IrqError::Unsupported) + } + } } /// Convenience re-export for memory IO mapping. @@ -172,13 +195,13 @@ pub mod time { /// Convenience re-exports for IRQ operations. pub mod irq { pub use super::{ - IrqAutoEnable as AutoEnable, IrqContext, IrqCpuId as CpuId, IrqCpuMask as CpuMask, - IrqError, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, - IrqShareMode as ShareMode, IrqStatus, RawIrqHandler, + IrqAffinity, IrqAutoEnable as AutoEnable, IrqContext, IrqCpuId as CpuId, + IrqCpuMask as CpuMask, IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOutcome, + IrqRequest, IrqReturn, IrqScope, IrqShareMode as ShareMode, IrqStatus, RawIrqHandler, klib::{ irq_disable as disable, irq_enable as enable, irq_free as free, irq_request_percpu as request_percpu, irq_request_shared as request_shared, - irq_set_enable as set_enable, + irq_run_on_cpu_sync as run_on_cpu_sync, irq_set_enable as set_enable, }, }; } diff --git a/components/irq-framework/src/action.rs b/components/irq-framework/src/action.rs index fba96d975a..c85f7638bb 100644 --- a/components/irq-framework/src/action.rs +++ b/components/irq-framework/src/action.rs @@ -4,15 +4,17 @@ use core::{ sync::atomic::AtomicBool, }; -use crate::{AutoEnable, CpuId, CpuMask, IrqRequest, IrqScope, RawIrqHandler}; +use crate::{AutoEnable, CpuId, CpuMask, IrqExecution, IrqRequest, IrqScope, RawIrqHandler}; pub(crate) struct Action { pub(crate) id: u64, pub(crate) handler: RawIrqHandler, pub(crate) data: NonNull<()>, pub(crate) scope: IrqScope, + pub(crate) execution: IrqExecution, pub(crate) enabled: AtomicBool, pub(crate) detached: AtomicBool, + pub(crate) running: AtomicBool, pending_enable: UnsafeCell, pub(crate) next: *mut Action, } @@ -29,8 +31,10 @@ impl Action { handler: request.handler, data: request.data, scope: request.scope, + execution: request.execution, enabled: AtomicBool::new(request.auto_enable == AutoEnable::Yes), detached: AtomicBool::new(false), + running: AtomicBool::new(false), pending_enable: UnsafeCell::new(CpuMask::empty()), next: ptr::null_mut(), } diff --git a/components/irq-framework/src/descriptor.rs b/components/irq-framework/src/descriptor.rs index 2e2d0cb80a..8d9a76d796 100644 --- a/components/irq-framework/src/descriptor.rs +++ b/components/irq-framework/src/descriptor.rs @@ -3,11 +3,16 @@ use core::{ sync::atomic::{AtomicUsize, Ordering}, }; -use crate::{CpuId, CpuMask, IrqError, IrqNumber, IrqRequest, IrqScope, ShareMode, action::Action}; +use crate::{ + CpuId, CpuMask, IrqAffinity, IrqError, IrqExecution, IrqNumber, IrqRequest, IrqScope, + ShareMode, action::Action, +}; pub(crate) struct Descriptor { pub(crate) irq: IrqNumber, share_mode: ShareMode, + affinity: IrqAffinity, + execution: IrqExecution, pub(crate) in_flight: AtomicUsize, line_desired: bool, line_applied: bool, @@ -21,6 +26,8 @@ impl Descriptor { Self { irq, share_mode: request.share_mode, + affinity: request.affinity, + execution: request.execution, in_flight: AtomicUsize::new(0), line_desired: false, line_applied: false, @@ -38,6 +45,8 @@ impl Descriptor { if !has_active_actions { self.share_mode = request.share_mode; + self.affinity = request.affinity; + self.execution = request.execution; return Ok(()); } @@ -45,6 +54,10 @@ impl Descriptor { return Err(IrqError::Busy); } + if self.affinity != request.affinity || self.execution != request.execution { + return Err(IrqError::Busy); + } + Ok(()) } diff --git a/components/irq-framework/src/lib.rs b/components/irq-framework/src/lib.rs index 60d0c62631..f41ea5c540 100644 --- a/components/irq-framework/src/lib.rs +++ b/components/irq-framework/src/lib.rs @@ -10,6 +10,7 @@ mod types; pub use registry::Registry; pub use types::{ - AutoEnable, CpuId, CpuMask, CpuMaskIter, IrqContext, IrqError, IrqHandle, IrqNumber, IrqOps, - IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, ShareMode, + AutoEnable, CpuId, CpuMask, CpuMaskIter, IrqAffinity, IrqContext, IrqError, IrqExecution, + IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, + RawIrqHandler, ShareMode, }; diff --git a/components/irq-framework/src/registry.rs b/components/irq-framework/src/registry.rs index 48a8f1f9d5..ec03980f14 100644 --- a/components/irq-framework/src/registry.rs +++ b/components/irq-framework/src/registry.rs @@ -6,8 +6,8 @@ use core::{ }; use crate::{ - CpuId, IrqContext, IrqError, IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, - IrqScope, IrqStatus, + CpuId, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOps, + IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, action::Action, descriptor::{Descriptor, action_matches_cpu, recompute_scope_line_desired}, lock::MetadataLock, @@ -59,17 +59,21 @@ impl Registry { let result = self.insert_action_locked(irq, &request, action); self.lock.unlock(&self.ops, irq_state); - let restore_result = self.restore_scope_line_snapshot(irq, request.scope, &snapshot); - if let Err(err) = result { unsafe { drop(Box::from_raw(action)); } - let _ = restore_result; + let _ = self.restore_scope_line_snapshot(irq, request.scope, &snapshot); return Err(err); } let handle = IrqHandle { irq, id }; + if let Err(err) = self.apply_affinity(irq, request.affinity) { + self.drop_detached_action(handle); + let _ = self.restore_scope_line_snapshot(irq, request.scope, &snapshot); + return Err(err); + } + let restore_result = self.restore_scope_line_snapshot(irq, request.scope, &snapshot); if let Err(err) = restore_result { self.drop_detached_action(handle); return Err(err); @@ -112,6 +116,24 @@ impl Registry { self.apply_enabled(handle, scope, false) } + /// Waits until no handler is in flight for this IRQ descriptor. + pub fn synchronize(&self, handle: IrqHandle) -> Result<(), IrqError> { + if self.ops.in_irq_context() { + return Err(IrqError::InIrqContext); + } + loop { + let in_flight = self.with_action(handle, |_| { + self.descriptor(handle.irq) + .map(|desc| desc.in_flight.load(Ordering::Acquire)) + .unwrap_or(0) + })?; + if in_flight == 0 { + return Ok(()); + } + self.ops.relax(); + } + } + fn set_action_enabled(&self, handle: IrqHandle, enabled: bool) -> Result { let irq_state = self.lock.lock(&self.ops); let result = (|| { @@ -153,6 +175,8 @@ impl Registry { in_flight, ) })?; + let action_running = + self.with_action(handle, |action| action.running.load(Ordering::Acquire))?; let cpu = status_cpu(scope, self.ops.current_cpu()); let line_enabled = match self.ops.is_enabled(handle.irq, cpu) { Ok(enabled) => enabled, @@ -175,6 +199,7 @@ impl Registry { pending, in_service, in_flight, + action_running, }) } @@ -201,6 +226,10 @@ impl Registry { continue; } + let Some(_guard) = ActionRunGuard::enter(action) else { + continue; + }; + outcome.called += 1; match unsafe { (action.handler)(ctx, action.data) } { IrqReturn::Unhandled => {} @@ -242,6 +271,11 @@ impl Registry { { return Err(IrqError::InvalidCpu); } + if let IrqAffinity::Fixed(cpu) = request.affinity + && !self.ops.cpu_online(cpu) + { + return Err(IrqError::CpuOffline); + } Ok(()) } @@ -380,6 +414,16 @@ impl Registry { } } + fn apply_affinity(&self, irq: IrqNumber, affinity: IrqAffinity) -> Result<(), IrqError> { + match affinity { + IrqAffinity::Any => Ok(()), + IrqAffinity::Fixed(cpu) if self.ops.cpu_online(cpu) => { + self.ops.set_affinity(irq, affinity) + } + IrqAffinity::Fixed(_) => Err(IrqError::CpuOffline), + } + } + fn apply_percpu_enabled( &self, handle: IrqHandle, @@ -719,6 +763,31 @@ struct DispatchGuard<'a, O: IrqOps> { irq: IrqNumber, } +struct ActionRunGuard<'a> { + action: &'a Action, +} + +impl<'a> ActionRunGuard<'a> { + fn enter(action: &'a Action) -> Option { + match action.execution { + IrqExecution::Concurrent => Some(Self { action }), + IrqExecution::NonReentrant => action + .running + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .ok() + .map(|_| Self { action }), + } + } +} + +impl Drop for ActionRunGuard<'_> { + fn drop(&mut self) { + if self.action.execution == IrqExecution::NonReentrant { + self.action.running.store(false, Ordering::Release); + } + } +} + impl Drop for DispatchGuard<'_, O> { fn drop(&mut self) { self.registry.end_dispatch(self.irq); diff --git a/components/irq-framework/src/types.rs b/components/irq-framework/src/types.rs index 573e960626..32d8426c8f 100644 --- a/components/irq-framework/src/types.rs +++ b/components/irq-framework/src/types.rs @@ -97,6 +97,24 @@ pub enum IrqScope { }, } +/// Hardware routing preference for an IRQ line. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum IrqAffinity { + /// The platform may route the line to any CPU. + Any, + /// Route the line to one fixed logical CPU. + Fixed(CpuId), +} + +/// Execution contract for an IRQ action. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum IrqExecution { + /// The handler may run concurrently if the controller delivers it that way. + Concurrent, + /// The framework prevents nested/concurrent calls to this action. + NonReentrant, +} + /// Whether an IRQ line is exclusive or shared. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ShareMode { @@ -150,6 +168,8 @@ pub struct IrqStatus { pub in_service: bool, /// Number of in-flight dispatches for this descriptor. pub in_flight: usize, + /// Whether this action is currently running. + pub action_running: bool, } /// IRQ framework errors. @@ -215,6 +235,11 @@ pub trait IrqOps { arg: *mut (), ) -> Result<(), IrqError>; + /// Routes a global IRQ line to the requested CPU affinity. + fn set_affinity(&self, _irq: IrqNumber, _affinity: IrqAffinity) -> Result<(), IrqError> { + Err(IrqError::Unsupported) + } + /// Enables or disables an IRQ line. fn set_enabled( &self, @@ -242,6 +267,8 @@ pub struct IrqRequest { pub(crate) handler: RawIrqHandler, pub(crate) data: NonNull<()>, pub(crate) scope: IrqScope, + pub(crate) affinity: IrqAffinity, + pub(crate) execution: IrqExecution, pub(crate) share_mode: ShareMode, pub(crate) auto_enable: AutoEnable, } @@ -253,6 +280,8 @@ impl IrqRequest { handler, data, scope: IrqScope::Global, + affinity: IrqAffinity::Any, + execution: IrqExecution::Concurrent, share_mode: ShareMode::Exclusive, auto_enable: AutoEnable::Yes, } @@ -264,6 +293,18 @@ impl IrqRequest { self } + /// Sets the IRQ affinity. + pub const fn affinity(mut self, affinity: IrqAffinity) -> Self { + self.affinity = affinity; + self + } + + /// Sets the action execution contract. + pub const fn execution(mut self, execution: IrqExecution) -> Self { + self.execution = execution; + self + } + /// Sets the sharing mode. pub const fn share_mode(mut self, share_mode: ShareMode) -> Self { self.share_mode = share_mode; diff --git a/components/irq-framework/tests/std_sim.rs b/components/irq-framework/tests/std_sim.rs index df47492a70..c34e3c041c 100644 --- a/components/irq-framework/tests/std_sim.rs +++ b/components/irq-framework/tests/std_sim.rs @@ -8,8 +8,8 @@ use std::{ }; use irq_framework::{ - AutoEnable, CpuId, CpuMask, IrqContext, IrqError, IrqNumber, IrqOps, IrqRequest, IrqReturn, - IrqScope, Registry, ShareMode, + AutoEnable, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqNumber, IrqOps, + IrqRequest, IrqReturn, IrqScope, Registry, ShareMode, }; #[derive(Clone, Default)] @@ -26,6 +26,7 @@ struct MockInner { line_enabled: Mutex, bool)>>, calls: Mutex>, fail_set_enabled: Mutex, bool)>>, + fail_set_affinity: AtomicBool, remote_calls: AtomicUsize, } @@ -36,6 +37,10 @@ enum OpCall { cpu: Option, enabled: bool, }, + SetAffinity { + irq: usize, + affinity: IrqAffinity, + }, IsEnabled { irq: usize, cpu: Option, @@ -86,6 +91,10 @@ impl MockOps { .push((irq, cpu, enabled)); } + fn fail_set_affinity(&self) { + self.inner.fail_set_affinity.store(true, Ordering::SeqCst); + } + fn set_line_enabled(&self, irq: usize, cpu: Option, enabled: bool) { let mut states = self.inner.line_enabled.lock().unwrap(); if let Some((_, _, state)) = states @@ -180,6 +189,17 @@ impl IrqOps for MockOps { Ok(()) } + fn set_affinity(&self, irq: IrqNumber, affinity: IrqAffinity) -> Result<(), IrqError> { + self.inner.calls.lock().unwrap().push(OpCall::SetAffinity { + irq: irq.0, + affinity, + }); + if self.inner.fail_set_affinity.load(Ordering::SeqCst) { + return Err(IrqError::Controller); + } + Ok(()) + } + fn is_enabled(&self, irq: IrqNumber, cpu: Option) -> Result { self.inner.calls.lock().unwrap().push(OpCall::IsEnabled { irq: irq.0, @@ -688,6 +708,106 @@ fn exclusive_and_shared_conflict() { assert_eq!(err, IrqError::Busy); } +#[test] +fn fixed_affinity_is_set_before_restoring_enabled_line() { + let ops = MockOps::with_cpus(2); + let registry = Registry::new(ops.clone()); + let counter = AtomicUsize::new(0); + let data = NonNull::from(&counter).cast(); + + registry + .request( + IrqNumber(41), + IrqRequest::new(count_handler, data).affinity(IrqAffinity::Fixed(CpuId(1))), + ) + .unwrap(); + + assert_eq!( + ops.calls(), + vec![ + OpCall::IsEnabled { irq: 41, cpu: None }, + OpCall::SetEnabled { + irq: 41, + cpu: None, + enabled: false, + }, + OpCall::SetAffinity { + irq: 41, + affinity: IrqAffinity::Fixed(CpuId(1)), + }, + OpCall::SetEnabled { + irq: 41, + cpu: None, + enabled: true, + }, + ] + ); +} + +#[test] +fn fixed_affinity_rejects_offline_cpu_and_controller_failure() { + let ops = MockOps::with_cpus(2); + let registry = Registry::new(ops.clone()); + let counter = AtomicUsize::new(0); + let data = NonNull::from(&counter).cast(); + + ops.set_online(1, false); + assert_eq!( + registry.request( + IrqNumber(42), + IrqRequest::new(count_handler, data).affinity(IrqAffinity::Fixed(CpuId(1))), + ), + Err(IrqError::CpuOffline) + ); + + ops.set_online(1, true); + ops.fail_set_affinity(); + assert_eq!( + registry.request( + IrqNumber(42), + IrqRequest::new(count_handler, data).affinity(IrqAffinity::Fixed(CpuId(1))), + ), + Err(IrqError::Controller) + ); +} + +#[test] +fn shared_actions_must_use_same_affinity_and_execution_contract() { + let registry = Registry::new(MockOps::with_cpus(2)); + let first = AtomicUsize::new(0); + let second = AtomicUsize::new(0); + + registry + .request( + IrqNumber(43), + IrqRequest::new(count_handler, NonNull::from(&first).cast()) + .share_mode(ShareMode::Shared) + .affinity(IrqAffinity::Fixed(CpuId(0))) + .execution(IrqExecution::NonReentrant), + ) + .unwrap(); + + assert_eq!( + registry.request( + IrqNumber(43), + IrqRequest::new(count_handler, NonNull::from(&second).cast()) + .share_mode(ShareMode::Shared) + .affinity(IrqAffinity::Fixed(CpuId(1))) + .execution(IrqExecution::NonReentrant), + ), + Err(IrqError::Busy) + ); + assert_eq!( + registry.request( + IrqNumber(43), + IrqRequest::new(count_handler, NonNull::from(&second).cast()) + .share_mode(ShareMode::Shared) + .affinity(IrqAffinity::Fixed(CpuId(0))), + ), + Err(IrqError::Busy) + ); +} + #[test] fn free_waits_for_inflight_dispatch_and_detaches_action() { struct Blocker { @@ -737,6 +857,91 @@ fn free_waits_for_inflight_dispatch_and_detaches_action() { assert_eq!(blocker.calls.load(Ordering::SeqCst), 1); } +#[test] +fn non_reentrant_action_skips_nested_dispatch() { + struct Blocker { + entered: Arc, + release: Arc, + calls: AtomicUsize, + } + + unsafe fn blocking_handler(_ctx: IrqContext, data: NonNull<()>) -> IrqReturn { + let blocker = unsafe { data.cast::().as_ref() }; + blocker.calls.fetch_add(1, Ordering::SeqCst); + blocker.entered.wait(); + blocker.release.wait(); + IrqReturn::Handled + } + + let registry = Arc::new(Registry::new(MockOps::with_cpus(1))); + let blocker = Box::new(Blocker { + entered: Arc::new(Barrier::new(2)), + release: Arc::new(Barrier::new(2)), + calls: AtomicUsize::new(0), + }); + let data = NonNull::from(blocker.as_ref()).cast(); + registry + .request( + IrqNumber(44), + IrqRequest::new(blocking_handler, data).execution(IrqExecution::NonReentrant), + ) + .unwrap(); + + let dispatch_registry = registry.clone(); + let dispatch_thread = + thread::spawn(move || dispatch_registry.dispatch(IrqNumber(44), CpuId(0))); + blocker.entered.wait(); + + let nested = registry.dispatch(IrqNumber(44), CpuId(0)); + assert!(!nested.handled); + assert_eq!(nested.called, 0); + assert_eq!(blocker.calls.load(Ordering::SeqCst), 1); + + blocker.release.wait(); + let outcome = dispatch_thread.join().unwrap(); + assert!(outcome.handled); + assert_eq!(outcome.called, 1); +} + +#[test] +fn synchronize_waits_for_inflight_dispatch() { + struct Blocker { + entered: Arc, + release: Arc, + } + + unsafe fn blocking_handler(_ctx: IrqContext, data: NonNull<()>) -> IrqReturn { + let blocker = unsafe { data.cast::().as_ref() }; + blocker.entered.wait(); + blocker.release.wait(); + IrqReturn::Handled + } + + let registry = Arc::new(Registry::new(MockOps::with_cpus(1))); + let blocker = Box::new(Blocker { + entered: Arc::new(Barrier::new(2)), + release: Arc::new(Barrier::new(2)), + }); + let data = NonNull::from(blocker.as_ref()).cast(); + let handle = registry + .request(IrqNumber(45), IrqRequest::new(blocking_handler, data)) + .unwrap(); + + let dispatch_registry = registry.clone(); + let dispatch_thread = + thread::spawn(move || dispatch_registry.dispatch(IrqNumber(45), CpuId(0))); + blocker.entered.wait(); + + let sync_registry = registry.clone(); + let sync_thread = thread::spawn(move || sync_registry.synchronize(handle)); + thread::sleep(std::time::Duration::from_millis(30)); + assert!(!sync_thread.is_finished()); + + blocker.release.wait(); + dispatch_thread.join().unwrap(); + sync_thread.join().unwrap().unwrap(); +} + #[test] fn per_cpu_action_dispatches_only_on_matching_cpu() { let registry = Registry::new(MockOps::with_cpus(4)); @@ -1080,6 +1285,14 @@ impl IrqOps for BlockingLineOps { Ok(()) } + fn set_affinity(&self, irq: IrqNumber, affinity: IrqAffinity) -> Result<(), IrqError> { + self.inner.calls.lock().unwrap().push(OpCall::SetAffinity { + irq: irq.0, + affinity, + }); + Ok(()) + } + fn is_enabled(&self, _irq: IrqNumber, _cpu: Option) -> Result { Err(IrqError::Unsupported) } diff --git a/drivers/ax-driver/src/serial/mod.rs b/drivers/ax-driver/src/serial/mod.rs index 87d447234e..098b1f5cc4 100644 --- a/drivers/ax-driver/src/serial/mod.rs +++ b/drivers/ax-driver/src/serial/mod.rs @@ -3,7 +3,9 @@ use alloc::{string::String, vec::Vec}; use ax_errno::AxError; use fdt_edit::{Fdt, RegFixed}; use log::warn; -pub use rdif_serial::{Config, ConfigError, RxFlag, RxItem, SerialCounters, SerialIrqOutcome}; +pub use rdif_serial::{ + Config, ConfigError, RxFlag, RxItem, SerialCounters, SerialIrqOutcome, SerialSoftWork, +}; use rdrive::{Device, DeviceId, DriverGeneric, probe::acpi::AcpiInfo, register::FdtInfo}; mod ns16550; diff --git a/drivers/ax-driver/src/serial/runtime.rs b/drivers/ax-driver/src/serial/runtime.rs index 9a34c8f3e9..901694f258 100644 --- a/drivers/ax-driver/src/serial/runtime.rs +++ b/drivers/ax-driver/src/serial/runtime.rs @@ -1,8 +1,11 @@ use alloc::{string::String, sync::Arc}; +use core::cell::UnsafeCell; use ax_kspin::SpinNoIrq; +use axklib::irq::{CpuId, IrqError, run_on_cpu_sync}; use rdif_serial::{ - Config, ConfigError, RawUart, RxItem, SerialCore, SerialCounters, SerialIrqOutcome, + Config, ConfigError, OwnerId, OwnerLease, RawUart, RxItem, RxQueue, SerialCounters, + SerialIrqHandler, SerialIrqOutcome, SerialSoftWork, TSerialIrqHandler, TxQueue, }; pub type BInterruptSerial = Arc; @@ -10,23 +13,23 @@ pub type BInterruptSerial = Arc; pub trait InterruptSerial: Send + Sync + 'static { fn name(&self) -> &str; fn base_addr(&self) -> usize; - fn baudrate(&self) -> u32; + fn owner_cpu(&self) -> usize; - fn startup(&self, config: &Config) -> Result<(), ConfigError>; - fn shutdown(&self); + fn baudrate(&self) -> u32; + fn startup(&self, config: &Config) -> Result; + fn shutdown(&self) -> Result<(), IrqError>; fn set_config(&self, config: &Config) -> Result<(), ConfigError>; fn try_write(&self, bytes: &[u8]) -> usize; fn write_room(&self) -> usize; fn chars_in_buffer(&self) -> usize; - fn flush_tx_buffer(&self); fn tx_idle(&self) -> bool; fn drain_rx(&self, out: &mut [RxItem]) -> usize; fn rx_pending(&self) -> bool; - fn handle_irq(&self) -> SerialIrqOutcome; - fn startup_catch_up(&self) -> SerialIrqOutcome; + fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome; + fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome; fn counters(&self) -> SerialCounters; } @@ -34,23 +37,80 @@ pub trait InterruptSerial: Send + Sync + 'static { pub struct KernelSerialPort { name: String, base_addr: usize, - inner: SpinNoIrq>, + owner: OwnerId, + tx: SpinNoIrq, + rx: SpinNoIrq, + irq: Arc>, } impl KernelSerialPort { pub fn new(raw: T) -> Self { + Self::new_with_owner(raw, 0) + } + + pub fn new_with_owner(raw: T, owner_cpu: usize) -> Self { let name = raw.name().into(); let base_addr = raw.base_addr(); + let owner = OwnerId(owner_cpu); + let parts = SerialIrqHandler::split(raw, owner); Self { name, base_addr, - inner: SpinNoIrq::new(SerialCore::new(raw)), + owner, + tx: SpinNoIrq::new(parts.tx), + rx: SpinNoIrq::new(parts.rx), + irq: parts.irq, } } pub fn new_dyn(raw: T) -> BInterruptSerial { Arc::new(Self::new(raw)) } + + fn run_on_owner(&self, op: F) -> Result + where + F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, + { + struct OwnerCall<'a, T: RawUart, F, R> { + port: &'a KernelSerialPort, + op: UnsafeCell>, + result: UnsafeCell>, + } + + unsafe fn thunk(arg: *mut ()) + where + T: RawUart, + F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, + { + let call = unsafe { &*(arg as *const OwnerCall<'_, T, F, R>) }; + let op = unsafe { &mut *call.op.get() } + .take() + .expect("serial owner call entered twice"); + let lease = unsafe { OwnerLease::new_unchecked(call.port.owner) }; + let result = op(&call.port.irq, lease); + unsafe { *call.result.get() = Some(result) }; + } + + let call = OwnerCall { + port: self, + op: UnsafeCell::new(Some(op)), + result: UnsafeCell::new(None), + }; + unsafe { + run_on_cpu_sync( + CpuId(self.owner.0), + thunk::, + (&call as *const OwnerCall<'_, T, F, R> as *mut ()).cast(), + )?; + } + Ok(unsafe { &mut *call.result.get() } + .take() + .expect("serial owner call did not complete")) + } + + fn owner_lease_for_cpu(&self, cpu: CpuId) -> Option> { + (cpu.0 == self.owner.0).then(|| unsafe { OwnerLease::new_unchecked(self.owner) }) + } } impl InterruptSerial for KernelSerialPort { @@ -62,59 +122,71 @@ impl InterruptSerial for KernelSerialPort { self.base_addr } + fn owner_cpu(&self) -> usize { + self.owner.0 + } + fn baudrate(&self) -> u32 { - self.inner.lock().baudrate() + self.run_on_owner(|irq, lease| irq.baudrate(lease)) + .unwrap_or(0) } - fn startup(&self, config: &Config) -> Result<(), ConfigError> { - self.inner.lock().startup(config) + fn startup(&self, config: &Config) -> Result { + self.run_on_owner(|irq, lease| irq.startup(lease, config)) + .map_err(|_| ConfigError::RegisterError)? } - fn shutdown(&self) { - self.inner.lock().shutdown(); + fn shutdown(&self) -> Result<(), IrqError> { + self.run_on_owner(|irq, lease| irq.shutdown(lease)) } fn set_config(&self, config: &Config) -> Result<(), ConfigError> { - self.inner.lock().set_config(config) + self.run_on_owner(|irq, lease| irq.set_config(lease, config)) + .map_err(|_| ConfigError::RegisterError)? } fn try_write(&self, bytes: &[u8]) -> usize { - self.inner.lock().enqueue_tx(bytes).accepted + let submit = self.tx.lock().submit(bytes); + if submit.needs_kick { + let _ = self.service_on_owner(SerialSoftWork::TX_KICK); + } + submit.accepted } fn write_room(&self) -> usize { - self.inner.lock().write_room() + self.tx.lock().write_room() } fn chars_in_buffer(&self) -> usize { - self.inner.lock().chars_in_buffer() - } - - fn flush_tx_buffer(&self) { - self.inner.lock().flush_tx_buffer(); + self.tx.lock().chars_in_buffer() } fn tx_idle(&self) -> bool { - self.inner.lock().tx_idle() + self.run_on_owner(|irq, lease| irq.tx_idle(lease)) + .unwrap_or(false) } fn drain_rx(&self, out: &mut [RxItem]) -> usize { - self.inner.lock().drain_rx(out) + self.rx.lock().drain(out) } fn rx_pending(&self) -> bool { - self.inner.lock().rx_pending() + self.rx.lock().rx_pending() } - fn handle_irq(&self) -> SerialIrqOutcome { - self.inner.lock().handle_irq() + fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome { + let Some(lease) = self.owner_lease_for_cpu(cpu) else { + return SerialIrqOutcome::default(); + }; + self.irq.handle(lease) } - fn startup_catch_up(&self) -> SerialIrqOutcome { - self.inner.lock().startup_catch_up() + fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome { + self.run_on_owner(|irq, lease| irq.service(lease, work)) + .unwrap_or_default() } fn counters(&self) -> SerialCounters { - self.inner.lock().counters() + self.irq.counters() } } diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index 6330136d99..17bd3ea5fc 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -1,148 +1,219 @@ +use alloc::sync::Arc; +use core::{ + cell::{Cell, UnsafeCell}, + marker::PhantomData, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, +}; + use crate::{ - Config, ConfigError, FixedQueue, InterruptMask, IrqSource, RawUart, RxFlag, RxItem, - SerialCounters, SerialIrqOutcome, + Config, ConfigError, InterruptMask, IrqSource, RawUart, RxFlag, RxItem, SerialCounters, + SerialIrqOutcome, SpscRing, }; -pub const DEFAULT_TX_CAP: usize = 4096; -pub const DEFAULT_RX_CAP: usize = 4096; +pub const DEFAULT_TX_CAP: usize = 4097; +pub const DEFAULT_RX_CAP: usize = 4097; pub const RX_IRQ_BUDGET: usize = 256; pub const TX_IRQ_BUDGET: usize = 64; pub const IRQ_PASS_BUDGET: usize = 32; -pub const TX_WAKEUP_WATERMARK: usize = 256; pub const TX_KICK_BUDGET: usize = 32; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum PortState { - Down, - Up, -} +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct OwnerId(pub usize); -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct TxEnqueue { - pub accepted: usize, - pub sent_immediately: usize, +pub struct OwnerLease<'a> { + owner: OwnerId, + _exclusive: PhantomData<&'a mut ()>, + _not_sync: PhantomData>, } -#[derive(Clone, Copy, Debug, Default)] -struct TxService { - sent: usize, - wake_writers: bool, +impl<'a> OwnerLease<'a> { + /// Creates an owner lease. + /// + /// # Safety + /// + /// The caller must be executing on `owner`, local IRQ/preemption state must + /// prevent another owner lease from being created concurrently for the same + /// UART, and no higher-priority FIQ/NMI/debug path may access this UART. + pub unsafe fn new_unchecked(owner: OwnerId) -> Self { + Self { + owner, + _exclusive: PhantomData, + _not_sync: PhantomData, + } + } + + pub fn owner(&self) -> OwnerId { + self.owner + } } -/// Runtime UART core. -/// -/// The core itself is lock-free and assumes the caller already holds the port -/// lock. It owns the raw register object, TX software FIFO, and RX flip FIFO. -/// IRQ code and task code must not access `raw` through any other path. -pub struct SerialCore< - T: RawUart, - const TX_CAP: usize = DEFAULT_TX_CAP, - const RX_CAP: usize = DEFAULT_RX_CAP, -> { - raw: T, - tx_fifo: FixedQueue, - rx_fifo: FixedQueue, - irq_mask: InterruptMask, - state: PortState, - counters: SerialCounters, +pub struct OwnerCell { + inner: UnsafeCell, + active: AtomicBool, } -impl SerialCore { - pub fn new(raw: T) -> Self { +unsafe impl Send for OwnerCell {} +unsafe impl Sync for OwnerCell {} + +impl OwnerCell { + fn new(value: T) -> Self { Self { - raw, - tx_fifo: FixedQueue::new(), - rx_fifo: FixedQueue::new(), - irq_mask: InterruptMask::empty(), - state: PortState::Down, - counters: SerialCounters::default(), + inner: UnsafeCell::new(value), + active: AtomicBool::new(false), } } - pub fn raw(&self) -> &T { - &self.raw + unsafe fn access<'a>(&'a self, _lease: &'a mut OwnerLease<'_>) -> OwnerAccess<'a, T> { + debug_assert!( + !self.active.swap(true, Ordering::AcqRel), + "serial owner cell re-entered" + ); + OwnerAccess { cell: self } } +} - pub fn raw_mut(&mut self) -> &mut T { - &mut self.raw +pub struct OwnerAccess<'a, T> { + cell: &'a OwnerCell, +} + +impl core::ops::Deref for OwnerAccess<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.cell.inner.get() } } +} - pub fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { - if self.state == PortState::Up { - return Ok(()); - } +impl core::ops::DerefMut for OwnerAccess<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.cell.inner.get() } + } +} - self.raw.startup(config)?; - self.irq_mask = InterruptMask::RX; - self.raw.set_irq_mask(self.irq_mask); - self.state = PortState::Up; - Ok(()) +impl Drop for OwnerAccess<'_, T> { + fn drop(&mut self) { + self.cell.active.store(false, Ordering::Release); } +} - pub fn shutdown(&mut self) { - if self.state == PortState::Down { - return; +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TxSubmit { + pub accepted: usize, + pub needs_kick: bool, +} + +pub struct TxState { + ring: SpscRing, + blocked: AtomicBool, + submitted: AtomicUsize, + sent: AtomicUsize, +} + +impl TxState { + fn new() -> Self { + Self { + ring: SpscRing::new(), + blocked: AtomicBool::new(false), + submitted: AtomicUsize::new(0), + sent: AtomicUsize::new(0), } + } - self.set_irq_mask_locked(InterruptMask::empty()); - self.raw.shutdown(); - self.tx_fifo.clear(); - self.rx_fifo.clear(); - self.state = PortState::Down; + fn write_room(&self) -> usize { + self.ring.remaining_snapshot() } - pub fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { - let saved_mask = self.irq_mask; - self.raw.set_irq_mask(InterruptMask::empty()); - let result = self.raw.set_config(config); - self.raw.set_irq_mask(saved_mask); - result + fn chars_in_buffer(&self) -> usize { + self.ring.len_snapshot() } - pub fn baudrate(&self) -> u32 { - self.raw.baudrate() + fn clear_from_owner(&self) { + self.ring.clear_consumer(); + self.blocked.store(false, Ordering::Release); + } +} + +pub struct TxQueue { + state: Arc>, + _single_producer: PhantomData>, +} + +unsafe impl Send for TxQueue {} + +impl TxQueue { + pub fn submit(&mut self, bytes: &[u8]) -> TxSubmit { + let mut accepted = 0; + for &byte in bytes { + if self.state.ring.push(byte).is_err() { + self.state.blocked.store(true, Ordering::Release); + break; + } + accepted += 1; + } + self.state.submitted.fetch_add(accepted, Ordering::Relaxed); + TxSubmit { + accepted, + needs_kick: accepted > 0, + } } pub fn write_room(&self) -> usize { - self.tx_fifo.remaining() + self.state.write_room() } pub fn chars_in_buffer(&self) -> usize { - self.tx_fifo.len() + self.state.chars_in_buffer() } +} - pub fn flush_tx_buffer(&mut self) { - self.tx_fifo.clear(); - self.stop_tx_irq_locked(); - } +pub struct RxState { + ring: SpscRing, + pushed: AtomicUsize, + dropped: AtomicUsize, + overrun: AtomicUsize, +} - pub fn tx_idle(&mut self) -> bool { - self.tx_fifo.is_empty() && self.raw.tx_idle() +impl RxState { + fn new() -> Self { + Self { + ring: SpscRing::new(), + pushed: AtomicUsize::new(0), + dropped: AtomicUsize::new(0), + overrun: AtomicUsize::new(0), + } } - pub fn enqueue_tx(&mut self, bytes: &[u8]) -> TxEnqueue { - if self.state != PortState::Up || bytes.is_empty() { - return TxEnqueue::default(); + fn push_from_owner(&self, item: RxItem) -> bool { + match self.ring.push(item) { + Ok(()) => { + self.pushed.fetch_add(1, Ordering::Relaxed); + true + } + Err(_) => { + self.dropped.fetch_add(1, Ordering::Relaxed); + false + } } + } - let accepted = self.tx_fifo.push_slice(bytes); - let service = if accepted > 0 { - self.service_tx_locked(TX_KICK_BUDGET) - } else { - TxService::default() - }; - - TxEnqueue { - accepted, - sent_immediately: service.sent, - } + fn clear_from_owner(&self) { + self.ring.clear_consumer(); } +} - pub fn drain_rx(&mut self, out: &mut [RxItem]) -> usize { +pub struct RxQueue { + state: Arc>, + _single_consumer: PhantomData>, +} + +unsafe impl Send for RxQueue {} + +impl RxQueue { + pub fn drain(&mut self, out: &mut [RxItem]) -> usize { let mut count = 0; for slot in out { - let Some(item) = self.rx_fifo.pop_front() else { + let Some(item) = self.state.ring.pop() else { break; }; *slot = item; @@ -152,183 +223,405 @@ impl SerialCore { } pub fn rx_pending(&self) -> bool { - !self.rx_fifo.is_empty() + !self.state.ring.is_empty() } +} - pub fn handle_irq(&mut self) -> SerialIrqOutcome { - let mut outcome = SerialIrqOutcome::default(); +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PortState { + Down, + Running, +} + +struct CoreInner { + raw: T, + irq_mask: InterruptMask, + state: PortState, + tx_irq_enabled: bool, +} - if self.state != PortState::Up { - return outcome; +impl CoreInner { + fn new(raw: T) -> Self { + Self { + raw, + irq_mask: InterruptMask::empty(), + state: PortState::Down, + tx_irq_enabled: false, + } + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct SerialSoftWork: u32 { + const TX_KICK = 1 << 0; + const RESERVICE = 1 << 1; + } +} + +pub trait TSerialIrqHandler: Send + Sync + 'static { + fn owner(&self) -> OwnerId; + fn handle(&self, lease: OwnerLease<'_>) -> SerialIrqOutcome; + fn service(&self, lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome; +} + +pub struct SerialParts< + T: RawUart, + const TX: usize = DEFAULT_TX_CAP, + const RX: usize = DEFAULT_RX_CAP, +> { + pub tx: TxQueue, + pub rx: RxQueue, + pub irq: Arc>, +} + +pub struct SerialIrqHandler< + T: RawUart, + const TX: usize = DEFAULT_TX_CAP, + const RX: usize = DEFAULT_RX_CAP, +> { + owner: OwnerId, + core: Arc>>, + tx: Arc>, + rx: Arc>, + counters: Arc, +} + +impl SerialIrqHandler { + pub fn split(raw: T, owner: OwnerId) -> SerialParts { + let core = Arc::new(OwnerCell::new(CoreInner::new(raw))); + let tx = Arc::new(TxState::new()); + let rx = Arc::new(RxState::new()); + let counters = Arc::new(SerialCountersAtomic::new()); + let irq = Arc::new(Self { + owner, + core, + tx: tx.clone(), + rx: rx.clone(), + counters, + }); + SerialParts { + tx: TxQueue { + state: tx, + _single_producer: PhantomData, + }, + rx: RxQueue { + state: rx, + _single_consumer: PhantomData, + }, + irq, + } + } + + pub fn startup( + &self, + mut lease: OwnerLease<'_>, + config: &Config, + ) -> Result { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + if core.state == PortState::Running { + return Ok(SerialIrqOutcome::default()); + } + + core.raw.startup(config)?; + core.irq_mask = InterruptMask::RX; + let irq_mask = core.irq_mask; + core.raw.set_irq_mask(irq_mask); + core.tx_irq_enabled = false; + core.state = PortState::Running; + Ok(SerialIrqOutcome::default()) + } + + pub fn shutdown(&self, mut lease: OwnerLease<'_>) { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + if core.state == PortState::Down { + return; + } + + core.raw.set_irq_mask(InterruptMask::empty()); + core.raw.shutdown(); + core.irq_mask = InterruptMask::empty(); + core.tx_irq_enabled = false; + core.state = PortState::Down; + self.tx.clear_from_owner(); + self.rx.clear_from_owner(); + } + + pub fn set_config( + &self, + mut lease: OwnerLease<'_>, + config: &Config, + ) -> Result<(), ConfigError> { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + let saved_mask = core.irq_mask; + core.raw.set_irq_mask(InterruptMask::empty()); + let result = core.raw.set_config(config); + core.raw.set_irq_mask(saved_mask); + result + } + + pub fn baudrate(&self, mut lease: OwnerLease<'_>) -> u32 { + self.assert_owner(&lease); + let core = unsafe { self.core.access(&mut lease) }; + core.raw.baudrate() + } + + pub fn tx_idle(&self, mut lease: OwnerLease<'_>) -> bool { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + self.tx.ring.is_empty() && core.raw.tx_idle() + } + + pub fn counters(&self) -> SerialCounters { + self.counters.snapshot() + } + + fn assert_owner(&self, lease: &OwnerLease<'_>) { + assert_eq!(lease.owner(), self.owner); + } + + fn handle_locked(&self, core: &mut CoreInner) -> SerialIrqOutcome { + let mut out = SerialIrqOutcome::default(); + if core.state != PortState::Running { + return out; } let mut rx_budget = RX_IRQ_BUDGET; let mut tx_budget = TX_IRQ_BUDGET; - for _ in 0..IRQ_PASS_BUDGET { - let snapshot = self.raw.take_irq_snapshot(); - + let snapshot = core.raw.take_irq_snapshot(); if !snapshot.claimed { - if !outcome.claimed { - self.counters.irq_spurious += 1; + if !out.claimed { + self.counters.irq_spurious.fetch_add(1, Ordering::Relaxed); } break; } - - if !outcome.claimed { - self.counters.irq_total += 1; + if !out.claimed { + self.counters.irq_total.fetch_add(1, Ordering::Relaxed); } - outcome.claimed = true; + out.claimed = true; if snapshot .sources .intersects(IrqSource::RX_DATA | IrqSource::RX_TIMEOUT | IrqSource::RX_STATUS) { - let pushed = self.service_rx_locked(rx_budget); - outcome.rx_pushed += pushed; - rx_budget = rx_budget.saturating_sub(pushed); + let service = self.service_rx(core, rx_budget); + rx_budget = rx_budget.saturating_sub(service.consumed); + out.rx_pushed += service.published; } if snapshot.sources.contains(IrqSource::TX_SPACE) { - let service = self.service_tx_locked(tx_budget); - outcome.tx_sent += service.sent; - outcome.tx_wakeup |= service.wake_writers; - tx_budget = tx_budget.saturating_sub(service.sent); + let sent = self.service_tx(core, tx_budget, &mut out); + tx_budget = tx_budget.saturating_sub(sent); } if snapshot.sources.contains(IrqSource::MODEM_STATUS) { - self.raw.ack_modem_status(); + core.raw.ack_modem_status(); } if snapshot.sources.contains(IrqSource::BUSY_DETECT) { - self.raw.ack_busy_detect(); + core.raw.ack_busy_detect(); } if rx_budget == 0 || tx_budget == 0 { - outcome.budget_exhausted = true; - self.counters.irq_budget_exhausted += 1; + out.budget_exhausted = true; + self.counters + .irq_budget_exhausted + .fetch_add(1, Ordering::Relaxed); break; } } - - outcome + out } - pub fn startup_catch_up(&mut self) -> SerialIrqOutcome { - if self.state != PortState::Up { - return SerialIrqOutcome::default(); + fn service_soft_locked( + &self, + core: &mut CoreInner, + work: SerialSoftWork, + ) -> SerialIrqOutcome { + let mut out = SerialIrqOutcome::default(); + if core.state != PortState::Running { + return out; } - let rx_pushed = self.service_rx_locked(RX_IRQ_BUDGET); - SerialIrqOutcome { - claimed: false, - rx_pushed, - tx_sent: 0, - tx_wakeup: false, - budget_exhausted: rx_pushed == RX_IRQ_BUDGET, + if work.contains(SerialSoftWork::TX_KICK) { + self.service_tx(core, TX_KICK_BUDGET, &mut out); + } + if work.contains(SerialSoftWork::RESERVICE) { + let rx = self.service_rx(core, RX_IRQ_BUDGET); + out.rx_pushed += rx.published; + self.service_tx(core, TX_IRQ_BUDGET, &mut out); } + out } - pub fn counters(&self) -> SerialCounters { - self.counters - } + fn service_rx(&self, core: &mut CoreInner, budget: usize) -> RxService { + let mut result = RxService::default(); + for _ in 0..budget { + let Some(sample) = core.raw.read_rx() else { + break; + }; + result.consumed += 1; - fn set_irq_mask_locked(&mut self, mask: InterruptMask) { - if self.irq_mask != mask { - self.raw.set_irq_mask(mask); - self.irq_mask = mask; - } - } + match sample.flag { + RxFlag::Normal => {} + RxFlag::Break => { + self.counters.rx_breaks.fetch_add(1, Ordering::Relaxed); + } + RxFlag::Parity => { + self.counters + .rx_parity_errors + .fetch_add(1, Ordering::Relaxed); + } + RxFlag::Framing => { + self.counters + .rx_framing_errors + .fetch_add(1, Ordering::Relaxed); + } + }; - fn start_tx_irq_locked(&mut self) { - if !self.irq_mask.contains(InterruptMask::TX_SPACE) { - self.set_irq_mask_locked(self.irq_mask | InterruptMask::TX_SPACE); - } - } + if let Some(byte) = sample.byte { + self.counters.rx_bytes.fetch_add(1, Ordering::Relaxed); + if self.rx.push_from_owner(RxItem::Byte { + byte, + flag: sample.flag, + }) { + result.published += 1; + } else { + self.counters + .rx_queue_dropped + .fetch_add(1, Ordering::Relaxed); + } + } - fn stop_tx_irq_locked(&mut self) { - if self.irq_mask.contains(InterruptMask::TX_SPACE) { - self.set_irq_mask_locked(self.irq_mask & !InterruptMask::TX_SPACE); + if sample.overrun { + self.rx.overrun.fetch_add(1, Ordering::Relaxed); + self.counters + .rx_fifo_overruns + .fetch_add(1, Ordering::Relaxed); + if self.rx.push_from_owner(RxItem::Overrun) { + result.published += 1; + } else { + self.counters + .rx_queue_dropped + .fetch_add(1, Ordering::Relaxed); + } + } } + result } - fn service_tx_locked(&mut self, budget: usize) -> TxService { - let before = self.tx_fifo.len(); - if before == 0 { - self.stop_tx_irq_locked(); - return TxService::default(); - } - - let limit = budget.min(self.raw.tx_load_size().max(1)); + fn service_tx( + &self, + core: &mut CoreInner, + budget: usize, + out: &mut SerialIrqOutcome, + ) -> usize { + let limit = budget.min(core.raw.tx_load_size().max(1)); let mut sent = 0; - while sent < limit && self.raw.tx_ready() { - let Some(byte) = self.tx_fifo.front().copied() else { + while sent < limit && core.raw.tx_ready() { + let Some(byte) = self.tx.ring.peek_copy() else { break; }; - self.raw.write_tx(byte); - self.tx_fifo.pop_front(); + core.raw.write_tx(byte); + let committed = self.tx.ring.pop(); + debug_assert_eq!(committed, Some(byte)); + self.tx.sent.fetch_add(1, Ordering::Relaxed); + self.counters.tx_bytes.fetch_add(1, Ordering::Relaxed); sent += 1; - self.counters.tx_bytes += 1; } - let remaining = self.tx_fifo.len(); - if remaining == 0 { - self.stop_tx_irq_locked(); - } else { - self.start_tx_irq_locked(); + if self.tx.ring.is_empty() { + if core.tx_irq_enabled { + core.irq_mask.remove(InterruptMask::TX_SPACE); + core.raw.set_irq_mask(core.irq_mask); + core.tx_irq_enabled = false; + } + } else if !core.tx_irq_enabled { + core.irq_mask.insert(InterruptMask::TX_SPACE); + core.raw.set_irq_mask(core.irq_mask); + core.tx_irq_enabled = true; } - let wakeup_threshold = TX_WAKEUP_WATERMARK.min(TX.saturating_sub(1)); - TxService { - sent, - wake_writers: before == TX - || (before > wakeup_threshold && remaining <= wakeup_threshold), + if sent > 0 && self.tx.blocked.swap(false, Ordering::AcqRel) { + out.tx_wakeup = true; } + out.tx_sent += sent; + sent } +} - fn service_rx_locked(&mut self, budget: usize) -> usize { - let mut pushed = 0; +impl TSerialIrqHandler + for SerialIrqHandler +{ + fn owner(&self) -> OwnerId { + self.owner + } - for _ in 0..budget { - let Some(sample) = self.raw.read_rx() else { - break; - }; + fn handle(&self, mut lease: OwnerLease<'_>) -> SerialIrqOutcome { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + self.handle_locked(&mut core) + } - match sample.flag { - RxFlag::Normal => {} - RxFlag::Break => self.counters.rx_breaks += 1, - RxFlag::Parity => self.counters.rx_parity_errors += 1, - RxFlag::Framing => self.counters.rx_framing_errors += 1, - } + fn service(&self, mut lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + self.service_soft_locked(&mut core, work) + } +} - if let Some(byte) = sample.byte { - self.counters.rx_bytes += 1; - - if self - .rx_fifo - .push_back(RxItem::Byte { - byte, - flag: sample.flag, - }) - .is_ok() - { - pushed += 1; - } else { - self.counters.rx_queue_dropped += 1; - } - } +#[derive(Default)] +struct RxService { + consumed: usize, + published: usize, +} - if sample.overrun { - self.counters.rx_fifo_overruns += 1; - if self.rx_fifo.push_back(RxItem::Overrun).is_ok() { - pushed += 1; - } else { - self.counters.rx_queue_dropped += 1; - } - } - } +struct SerialCountersAtomic { + irq_total: AtomicUsize, + irq_spurious: AtomicUsize, + irq_budget_exhausted: AtomicUsize, + rx_bytes: AtomicUsize, + rx_fifo_overruns: AtomicUsize, + rx_queue_dropped: AtomicUsize, + rx_breaks: AtomicUsize, + rx_parity_errors: AtomicUsize, + rx_framing_errors: AtomicUsize, + tx_bytes: AtomicUsize, +} - pushed +impl SerialCountersAtomic { + fn new() -> Self { + Self { + irq_total: AtomicUsize::new(0), + irq_spurious: AtomicUsize::new(0), + irq_budget_exhausted: AtomicUsize::new(0), + rx_bytes: AtomicUsize::new(0), + rx_fifo_overruns: AtomicUsize::new(0), + rx_queue_dropped: AtomicUsize::new(0), + rx_breaks: AtomicUsize::new(0), + rx_parity_errors: AtomicUsize::new(0), + rx_framing_errors: AtomicUsize::new(0), + tx_bytes: AtomicUsize::new(0), + } + } + + fn snapshot(&self) -> SerialCounters { + SerialCounters { + irq_total: self.irq_total.load(Ordering::Relaxed) as u64, + irq_spurious: self.irq_spurious.load(Ordering::Relaxed) as u64, + irq_budget_exhausted: self.irq_budget_exhausted.load(Ordering::Relaxed) as u64, + rx_bytes: self.rx_bytes.load(Ordering::Relaxed) as u64, + rx_fifo_overruns: self.rx_fifo_overruns.load(Ordering::Relaxed) as u64, + rx_queue_dropped: self.rx_queue_dropped.load(Ordering::Relaxed) as u64, + rx_breaks: self.rx_breaks.load(Ordering::Relaxed) as u64, + rx_parity_errors: self.rx_parity_errors.load(Ordering::Relaxed) as u64, + rx_framing_errors: self.rx_framing_errors.load(Ordering::Relaxed) as u64, + tx_bytes: self.tx_bytes.load(Ordering::Relaxed) as u64, + } } } @@ -458,112 +751,111 @@ mod tests { } } - fn started_core( - uart: MockUart, - ) -> SerialCore { - let mut core = SerialCore::new(uart); - core.startup(&Config::new()).unwrap(); - core + fn lease() -> OwnerLease<'static> { + unsafe { OwnerLease::new_unchecked(OwnerId(0)) } } #[test] - fn no_pending_irq_is_unhandled_even_with_buffered_rx() { - let mut core = started_core::<16, 16>(MockUart::new()); - core.rx_fifo - .push_back(RxItem::Byte { - byte: b'x', - flag: RxFlag::Normal, - }) - .unwrap(); + fn tx_queue_only_submits_software_work() { + let parts = SerialIrqHandler::::split(MockUart::new(), OwnerId(0)); + let mut tx = parts.tx; - let outcome = core.handle_irq(); + let submit = tx.submit(b"abc"); - assert!(!outcome.claimed); + assert_eq!(submit.accepted, 3); + assert!(submit.needs_kick); + assert_eq!(tx.chars_in_buffer(), 3); } #[test] - fn one_irq_services_rx_and_tx() { - let mut core = started_core::<16, 16>( - MockUart::new() - .irq(IrqSource::RX_DATA | IrqSource::TX_SPACE) - .rx_byte(b'Z'), - ); + fn owner_service_flushes_tx_queue() { + let mut uart = MockUart::new(); + uart.tx_ready_budget = 3; + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let mut tx = parts.tx; + tx.submit(b"abc"); + parts.irq.startup(lease(), &Config::new()).unwrap(); - assert_eq!(core.enqueue_tx(b"abc").accepted, 3); - core.raw_mut().tx_ready_budget = 3; - let outcome = core.handle_irq(); + let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); - assert!(outcome.claimed); - assert_eq!(outcome.rx_pushed, 1); - assert!(outcome.tx_sent > 0); + assert_eq!(outcome.tx_sent, 3); + assert_eq!(tx.chars_in_buffer(), 0); } #[test] - fn tx_keeps_unsent_suffix() { - let mut core = started_core::<16, 16>(MockUart::new().irq(IrqSource::TX_SPACE)); + fn irq_services_rx_and_preserves_items_for_rx_queue() { + let uart = MockUart::new() + .irq(IrqSource::RX_DATA) + .rx_byte(b'A') + .rx_byte(b'B'); + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + parts.irq.startup(lease(), &Config::new()).unwrap(); - assert_eq!(core.enqueue_tx(b"abcdef").accepted, 6); - core.raw_mut().tx_ready_budget = 2; - let outcome = core.handle_irq(); + let outcome = parts.irq.handle(lease()); - assert_eq!(outcome.tx_sent, 2); - assert_eq!(core.chars_in_buffer(), 4); + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, 2); + let mut rx = parts.rx; + let mut buf = [RxItem::default(); 2]; + assert_eq!(rx.drain(&mut buf), 2); + assert_eq!( + buf, + [ + RxItem::Byte { + byte: b'A', + flag: RxFlag::Normal, + }, + RxItem::Byte { + byte: b'B', + flag: RxFlag::Normal, + }, + ] + ); } #[test] - fn rx_irq_is_bounded() { + fn single_rx_irq_preserves_burst_until_owner_budget() { let mut uart = MockUart::new().irq(IrqSource::RX_DATA); - for _ in 0..(RX_IRQ_BUDGET + 8) { - uart = uart.rx_byte(b'x'); + let burst = b"echo 0123456789abcdefghijklmnopqrstuvwxyz\n"; + for &byte in burst { + uart = uart.rx_byte(byte); } - let mut core = started_core::<16, 512>(uart); - - let outcome = core.handle_irq(); + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + parts.irq.startup(lease(), &Config::new()).unwrap(); - assert!(outcome.budget_exhausted); - assert!(outcome.rx_pushed <= RX_IRQ_BUDGET); - } + let outcome = parts.irq.handle(lease()); - #[test] - fn rx_full_queue_records_drops_but_drains_hardware() { - let mut uart = MockUart::new().irq(IrqSource::RX_DATA); - for _ in 0..8 { - uart = uart.rx_byte(b'x'); - } - let mut core = started_core::<16, 4>(uart); - for _ in 0..4 { - core.rx_fifo - .push_back(RxItem::Byte { - byte: b'q', + assert!(outcome.claimed); + assert_eq!(outcome.rx_pushed, burst.len()); + let mut rx = parts.rx; + let mut items = [RxItem::default(); 64]; + assert_eq!(rx.drain(&mut items[..burst.len()]), burst.len()); + for (item, byte) in items.iter().zip(burst.iter()).take(burst.len()) { + assert_eq!( + *item, + RxItem::Byte { + byte: *byte, flag: RxFlag::Normal, - }) - .unwrap(); + } + ); } - - core.handle_irq(); - - assert!(core.counters().rx_queue_dropped > 0); } #[test] - fn rx_status_without_byte_is_preserved_as_queue_event() { + fn rx_budget_counts_consumed_samples_not_published_items() { let mut uart = MockUart::new().irq(IrqSource::RX_STATUS); uart.rx.push_back(RxSample { - byte: None, + byte: Some(b'x'), flag: RxFlag::Parity, overrun: true, }); - let mut core = started_core::<16, 16>(uart); + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + parts.irq.startup(lease(), &Config::new()).unwrap(); - let outcome = core.handle_irq(); + let outcome = parts.irq.handle(lease()); assert!(outcome.claimed); - assert_eq!(outcome.rx_pushed, 1); - assert_eq!(core.counters().rx_parity_errors, 1); - assert_eq!(core.counters().rx_fifo_overruns, 1); - - let mut items = [RxItem::default(); 1]; - assert_eq!(core.drain_rx(&mut items), 1); - assert_eq!(items[0], RxItem::Overrun); + assert_eq!(outcome.rx_pushed, 2); + assert!(!outcome.budget_exhausted); } } diff --git a/drivers/interface/rdif-serial/src/lib.rs b/drivers/interface/rdif-serial/src/lib.rs index 8562d04263..42ba679572 100644 --- a/drivers/interface/rdif-serial/src/lib.rs +++ b/drivers/interface/rdif-serial/src/lib.rs @@ -1,18 +1,16 @@ //! Portable interrupt-driven serial runtime primitives. //! //! The reusable stack is intentionally split by synchronization ownership: -//! raw UART drivers expose only register-level operations, `SerialCore` owns -//! the TX software FIFO and RX flip FIFO, and OS glue wraps the core in the -//! kernel's short port lock. Runtime queues and TTY code must not access UART -//! registers directly; only the locked core may read destructive IRQ/status -//! registers. +//! raw UART drivers expose only register-level operations, `TxQueue` and +//! `RxQueue` own independent lock-free software queues, and `SerialIrqHandler` +//! is the only endpoint allowed to touch runtime UART registers. //! -//! IRQ handlers call `SerialCore::handle_irq()` to synchronize hardware state -//! into software queues. Task or worker context drains `RxItem`s and enqueues TX -//! bytes, but never polls the shared UART IRQ/status register to rediscover -//! readiness. This keeps the fast path bounded and leaves wakeups, wait queues, -//! poll sets, and line discipline processing to OS-specific layers above this -//! crate. +//! OS glue must route the hardware IRQ and software TX kick to the handler's +//! owner CPU and pass an `OwnerLease`. Task or worker context drains `RxItem`s +//! and enqueues TX bytes, but never polls the shared UART IRQ/status register +//! to rediscover readiness. This keeps the fast path bounded and leaves wakeups, +//! wait queues, poll sets, and line discipline processing to OS-specific layers +//! above this crate. #![no_std] @@ -99,8 +97,8 @@ pub enum Parity { bitflags! { /// Polling-only serial events for direct raw users such as someboot. /// - /// Runtime `SerialCore` does not use this high-level snapshot type; it uses - /// `IrqSnapshot`, `RxSample`, and TX/RX software FIFOs instead. + /// Runtime `SerialIrqHandler` does not use this high-level snapshot type; + /// it uses `IrqSnapshot`, `RxSample`, and TX/RX software FIFOs instead. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SerialEvent: u32 { const RX_READY = 0x01; diff --git a/drivers/interface/rdif-serial/src/queue.rs b/drivers/interface/rdif-serial/src/queue.rs index 5310d7c6e3..b269bf93af 100644 --- a/drivers/interface/rdif-serial/src/queue.rs +++ b/drivers/interface/rdif-serial/src/queue.rs @@ -1,61 +1,146 @@ -use alloc::collections::VecDeque; +use core::{ + cell::UnsafeCell, + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; -pub struct FixedQueue { - queue: VecDeque, +struct Slot(UnsafeCell>); + +unsafe impl Send for Slot {} +unsafe impl Sync for Slot {} + +impl Slot { + const fn uninit() -> Self { + Self(UnsafeCell::new(MaybeUninit::uninit())) + } } -impl FixedQueue { +/// Single-producer single-consumer ring. +/// +/// One slot is reserved to distinguish full from empty, so the effective +/// capacity is `N - 1`. +pub struct SpscRing { + slots: [Slot; N], + head: AtomicUsize, + tail: AtomicUsize, +} + +unsafe impl Send for SpscRing {} +unsafe impl Sync for SpscRing {} + +impl SpscRing { pub fn new() -> Self { - assert!(CAP > 0); + assert!(N >= 2); Self { - queue: VecDeque::with_capacity(CAP), + slots: [const { Slot::uninit() }; N], + head: AtomicUsize::new(0), + tail: AtomicUsize::new(0), } } - pub fn len(&self) -> usize { - self.queue.len() + pub fn capacity(&self) -> usize { + N - 1 } - pub fn is_empty(&self) -> bool { - self.queue.is_empty() + pub fn push(&self, value: T) -> Result<(), T> { + let tail = self.tail.load(Ordering::Relaxed); + let next_tail = Self::advance(tail); + if next_tail == self.head.load(Ordering::Acquire) { + return Err(value); + } + + unsafe { (*self.slots[tail].0.get()).write(value) }; + self.tail.store(next_tail, Ordering::Release); + Ok(()) + } + + pub fn pop(&self) -> Option { + let head = self.head.load(Ordering::Relaxed); + if head == self.tail.load(Ordering::Acquire) { + return None; + } + + let value = unsafe { (*self.slots[head].0.get()).assume_init_read() }; + self.head.store(Self::advance(head), Ordering::Release); + Some(value) } - pub fn remaining(&self) -> usize { - CAP - self.queue.len() + pub fn peek_copy(&self) -> Option + where + T: Copy, + { + let head = self.head.load(Ordering::Relaxed); + if head == self.tail.load(Ordering::Acquire) { + return None; + } + + Some(unsafe { *(*self.slots[head].0.get()).assume_init_ref() }) } - pub fn front(&self) -> Option<&T> { - self.queue.front() + pub fn is_empty(&self) -> bool { + self.head.load(Ordering::Acquire) == self.tail.load(Ordering::Acquire) } - pub fn pop_front(&mut self) -> Option { - self.queue.pop_front() + pub fn clear_consumer(&self) { + while self.pop().is_some() {} } - pub fn push_back(&mut self, value: T) -> Result<(), T> { - if self.queue.len() == CAP { - Err(value) + pub fn len_snapshot(&self) -> usize { + let head = self.head.load(Ordering::Acquire); + let tail = self.tail.load(Ordering::Acquire); + if tail >= head { + tail - head } else { - self.queue.push_back(value); - Ok(()) + N - head + tail } } - pub fn clear(&mut self) { - self.queue.clear(); + pub fn remaining_snapshot(&self) -> usize { + self.capacity().saturating_sub(self.len_snapshot()) + } + + const fn advance(index: usize) -> usize { + let next = index + 1; + if next == N { 0 } else { next } } } -impl Default for FixedQueue { +impl Default for SpscRing { fn default() -> Self { Self::new() } } -impl FixedQueue { - pub fn push_slice(&mut self, bytes: &[u8]) -> usize { - let count = bytes.len().min(self.remaining()); - self.queue.extend(bytes[..count].iter().copied()); - count +impl Drop for SpscRing { + fn drop(&mut self) { + while self.pop().is_some() {} + } +} + +#[cfg(test)] +mod tests { + use super::SpscRing; + + #[test] + fn keeps_one_slot_empty() { + let ring = SpscRing::::new(); + assert_eq!(ring.capacity(), 3); + assert_eq!(ring.push(1), Ok(())); + assert_eq!(ring.push(2), Ok(())); + assert_eq!(ring.push(3), Ok(())); + assert_eq!(ring.push(4), Err(4)); + assert_eq!(ring.pop(), Some(1)); + assert_eq!(ring.pop(), Some(2)); + assert_eq!(ring.pop(), Some(3)); + assert_eq!(ring.pop(), None); + } + + #[test] + fn peek_does_not_consume() { + let ring = SpscRing::::new(); + ring.push(7).unwrap(); + assert_eq!(ring.peek_copy(), Some(7)); + assert_eq!(ring.peek_copy(), Some(7)); + assert_eq!(ring.pop(), Some(7)); } } diff --git a/drivers/interface/rdif-serial/src/raw.rs b/drivers/interface/rdif-serial/src/raw.rs index 861934003f..5e94411a85 100644 --- a/drivers/interface/rdif-serial/src/raw.rs +++ b/drivers/interface/rdif-serial/src/raw.rs @@ -57,7 +57,7 @@ pub trait RawUart: Send + Any + 'static { /// Read a raw hardware status snapshot. /// /// This is for polling users that directly own the raw UART, such as - /// someboot early console. Runtime `SerialCore` must not call this method. + /// someboot early console. Runtime TX/RX queues must not call this method. fn poll_status(&mut self) -> SerialEvent; /// Direct polling helper for early console users. diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index e7faea7535..009f113352 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -696,7 +696,9 @@ mod tests { use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; use std::sync::{Mutex, MutexGuard}; - use rdif_serial::{RxItem, SerialCore}; + use rdif_serial::{ + OwnerId, OwnerLease, RxItem, SerialIrqHandler, SerialParts, TSerialIrqHandler, + }; use super::*; @@ -800,10 +802,14 @@ mod tests { ) } - fn started_core(uart: Ns16550) -> SerialCore, 64, 64> { - let mut core = SerialCore::new(uart); - core.startup(&Config::new()).unwrap(); - core + fn owner_lease() -> OwnerLease<'static> { + unsafe { OwnerLease::new_unchecked(OwnerId(0)) } + } + + fn started_parts(uart: Ns16550) -> SerialParts, 64, 64> { + let parts = SerialIrqHandler::<_, 64, 64>::split(uart, OwnerId(0)); + parts.irq.startup(owner_lease(), &Config::new()).unwrap(); + parts } #[test] @@ -930,8 +936,11 @@ mod tests { #[test] fn serial_core_single_irq_services_rx_and_tx_fifo() { let (_guard, uart) = serial(); - let mut core = started_core(uart); - assert_eq!(core.enqueue_tx(b"ab").accepted, 2); + let parts = started_parts(uart); + let mut tx = parts.tx; + let mut rx_queue = parts.rx; + let irq = parts.irq; + assert_eq!(tx.submit(b"ab").accepted, 2); REGS[UART_IIR as usize].store( InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY.bits(), @@ -941,7 +950,7 @@ mod tests { LineStatusFlags::TRANSMITTER_HOLDING_EMPTY.bits(), Ordering::SeqCst, ); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.tx_sent, 1); assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'a'); @@ -952,12 +961,12 @@ mod tests { ); REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); REGS[UART_RBR as usize].store(b'z', Ordering::SeqCst); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 1); let mut rx = [RxItem::default(); 1]; - assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!(rx_queue.drain(&mut rx), 1); assert_eq!( rx[0], RxItem::Byte { @@ -970,8 +979,10 @@ mod tests { #[test] fn serial_core_does_not_synthesize_tx_irq_from_plain_lsr_ready() { let (_guard, uart) = serial(); - let mut core = started_core(uart); - assert_eq!(core.enqueue_tx(b"x").accepted, 1); + let parts = started_parts(uart); + let mut tx = parts.tx; + let irq = parts.irq; + assert_eq!(tx.submit(b"x").accepted, 1); REGS[UART_IIR as usize].store( InterruptIdentificationFlags::NO_INTERRUPT_PENDING.bits(), Ordering::SeqCst, @@ -981,10 +992,10 @@ mod tests { Ordering::SeqCst, ); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(!outcome.claimed); assert_eq!(outcome.tx_sent, 0); - assert_eq!(core.chars_in_buffer(), 1); + assert_eq!(tx.chars_in_buffer(), 1); } #[test] @@ -1023,7 +1034,8 @@ mod tests { #[test] fn hard_irq_claims_and_clears_modem_status_interrupt() { let (_guard, uart) = serial(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::MODEM_STATUS.bits() @@ -1035,7 +1047,7 @@ mod tests { Ordering::SeqCst, ); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 0); assert_eq!(outcome.tx_sent, 0); @@ -1049,7 +1061,9 @@ mod tests { #[test] fn serial_core_rx_irq_drains_raw_fifo() { let (_guard, uart) = serial(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let mut rx_queue = parts.rx; + let irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE.bits(), @@ -1058,12 +1072,12 @@ mod tests { REGS[UART_LSR as usize].store(LineStatusFlags::DATA_READY.bits(), Ordering::SeqCst); REGS[UART_RBR as usize].store(b'r', Ordering::SeqCst); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 1); let mut rx = [RxItem::default(); 1]; - assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!(rx_queue.drain(&mut rx), 1); assert_eq!( rx[0], RxItem::Byte { @@ -1076,10 +1090,12 @@ mod tests { #[test] fn serial_core_tx_irq_uses_software_fifo() { let (_guard, uart) = serial(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let mut tx = parts.tx; + let irq = parts.irq; - assert_eq!(core.enqueue_tx(b"ab").accepted, 2); - assert_eq!(core.chars_in_buffer(), 2); + assert_eq!(tx.submit(b"ab").accepted, 2); + assert_eq!(tx.chars_in_buffer(), 2); REGS[UART_IIR as usize].store( InterruptIdentificationFlags::TRANSMITTER_HOLDING_EMPTY.bits(), @@ -1090,17 +1106,19 @@ mod tests { Ordering::SeqCst, ); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.tx_sent, 1); assert_eq!(REGS[UART_THR as usize].load(Ordering::SeqCst), b'a'); - assert_eq!(core.chars_in_buffer(), 1); + assert_eq!(tx.chars_in_buffer(), 1); } #[test] fn serial_core_saved_lsr_error_is_consumed_by_rx_fifo() { let (_guard, uart) = serial(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let mut rx_queue = parts.rx; + let irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), @@ -1112,12 +1130,12 @@ mod tests { ); REGS[UART_RBR as usize].store(b'p', Ordering::SeqCst); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 1); let mut rx = [RxItem::default(); 1]; - assert_eq!(core.drain_rx(&mut rx), 1); + assert_eq!(rx_queue.drain(&mut rx), 1); assert_eq!( rx[0], RxItem::Byte { @@ -1130,7 +1148,9 @@ mod tests { #[test] fn serial_core_rx_overrun_returns_current_byte_and_marker() { let (_guard, uart) = serial(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let mut rx_queue = parts.rx; + let irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), @@ -1142,12 +1162,12 @@ mod tests { ); REGS[UART_RBR as usize].store(b'S', Ordering::SeqCst); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 2); let mut rx = [RxItem::default(); 2]; - assert_eq!(core.drain_rx(&mut rx), 2); + assert_eq!(rx_queue.drain(&mut rx), 2); assert_eq!( rx[0], RxItem::Byte { diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index 1287a8c040..436230a24c 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -862,7 +862,7 @@ mod tests { use core::ptr::NonNull; use std::boxed::Box; - use rdif_serial::SerialCore; + use rdif_serial::{OwnerId, OwnerLease, SerialIrqHandler, SerialParts, TSerialIrqHandler}; use super::*; @@ -889,10 +889,14 @@ mod tests { } } - fn started_core(uart: Pl011) -> SerialCore { - let mut core = SerialCore::new(uart); - core.startup(&Config::new()).unwrap(); - core + fn owner_lease() -> OwnerLease<'static> { + unsafe { OwnerLease::new_unchecked(OwnerId(0)) } + } + + fn started_parts(uart: Pl011) -> SerialParts { + let parts = SerialIrqHandler::<_, 64, 64>::split(uart, OwnerId(0)); + parts.irq.startup(owner_lease(), &Config::new()).unwrap(); + parts } #[test] @@ -950,19 +954,21 @@ mod tests { #[test] fn serial_core_tx_irq_drains_software_fifo() { let (mut regs, uart) = pl011_with_registers(); - let mut core = started_core(uart); + let parts = started_parts(uart); + let mut tx = parts.tx; + let irq = parts.irq; write_test_reg(&mut regs, 0x018, UARTFR::TXFF::SET.value); - assert_eq!(core.enqueue_tx(b"x").accepted, 1); - assert_eq!(core.chars_in_buffer(), 1); + assert_eq!(tx.submit(b"x").accepted, 1); + assert_eq!(tx.chars_in_buffer(), 1); write_test_reg(&mut regs, 0x018, 0); write_test_reg(&mut regs, 0x040, UARTIS::TX::SET.value); - let outcome = core.handle_irq(); + let outcome = irq.handle(owner_lease()); assert!(outcome.claimed); assert_eq!(outcome.tx_sent, 1); assert_eq!(regs.uartdr.get() as u8, b'x'); - assert_eq!(core.chars_in_buffer(), 0); + assert_eq!(tx.chars_in_buffer(), 0); } #[test] diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index 4120c7ea23..c4df1c5e00 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -6,12 +6,13 @@ use core::{ use ax_driver::serial::{ self as ax_serial, BInterruptSerial, Config, RxFlag, RxItem, SerialDevice, SerialIrqOutcome, + SerialSoftWork, }; use ax_errno::{AxError, AxResult}; use ax_kspin::SpinNoIrq; use ax_runtime::hal::{ console::{ConsoleDeviceIdError, ConsoleDeviceIdResult}, - irq::{AutoEnable, IrqHandle, IrqRequest, ShareMode}, + irq::{AutoEnable, CpuId, IrqAffinity, IrqExecution, IrqHandle, IrqRequest, ShareMode}, }; use ax_sync::Mutex; use ax_task::IrqNotify; @@ -335,6 +336,8 @@ impl SerialBackend { let data = NonNull::new(Arc::into_raw(self.clone()) as *mut ()).unwrap(); let request = IrqRequest::new(serial_raw_irq_handler, data) .share_mode(ShareMode::Shared) + .affinity(IrqAffinity::Fixed(CpuId(self.port.owner_cpu()))) + .execution(IrqExecution::NonReentrant) .auto_enable(AutoEnable::No); match ax_runtime::hal::irq::request_irq(self.irq_num, request) { Ok(handle) => { @@ -379,7 +382,7 @@ impl SerialBackend { } if let Err(err) = ax_runtime::hal::irq::enable_irq(handle) { - self.port.shutdown(); + let _ = self.port.shutdown(); warn!( "Failed to enable {} IRQ handler for irq {}: {err:?}", self.tty_name, self.irq_num @@ -388,7 +391,11 @@ impl SerialBackend { } self.started.store(true, Ordering::Release); - publish_serial_outcome(self, self.port.startup_catch_up(), false); + publish_serial_outcome( + self, + self.port.service_on_owner(SerialSoftWork::RESERVICE), + false, + ); self.events.publish(SerialEventBits::RX_READY); true } @@ -434,11 +441,11 @@ fn spawn_serial_event_worker(backend: Arc) { } unsafe fn serial_raw_irq_handler( - _ctx: ax_runtime::hal::irq::IrqContext, + ctx: ax_runtime::hal::irq::IrqContext, data: NonNull<()>, ) -> ax_runtime::hal::irq::IrqReturn { let backend = unsafe { &*(data.as_ptr() as *const SerialBackend) }; - let outcome = backend.port.handle_irq(); + let outcome = backend.port.handle_irq_on_owner(ctx.cpu); if !outcome.claimed { return ax_runtime::hal::irq::IrqReturn::Unhandled; } diff --git a/os/arceos/modules/axhal/src/dummy.rs b/os/arceos/modules/axhal/src/dummy.rs index 4a1643f5a7..04cb691bc3 100644 --- a/os/arceos/modules/axhal/src/dummy.rs +++ b/os/arceos/modules/axhal/src/dummy.rs @@ -137,6 +137,13 @@ impl PowerIf for DummyPower { impl IrqIf for DummyIrq { fn set_enable(_irq: usize, _enabled: bool) {} + fn set_affinity( + _irq: usize, + _affinity: ax_plat::irq::IrqAffinity, + ) -> Result<(), ax_plat::irq::IrqError> { + Err(ax_plat::irq::IrqError::Unsupported) + } + fn handle(_irq: usize) -> Option { None } diff --git a/os/arceos/modules/axhal/src/irq.rs b/os/arceos/modules/axhal/src/irq.rs index 45bf81cd0d..9c6faec782 100644 --- a/os/arceos/modules/axhal/src/irq.rs +++ b/os/arceos/modules/axhal/src/irq.rs @@ -4,10 +4,11 @@ pub use ax_config::devices::IPI_IRQ; use ax_cpu::trap::set_irq_handler; pub use ax_plat::irq::{ - AutoEnable, CpuId, CpuMask, IrqContext, IrqError, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, - IrqReturn, IrqScope, IrqStatus, RawIrqHandler, ShareMode, cpu_online, disable_irq, - dispatch_irq, enable_irq, free_irq, handle, irq_status, request_irq, request_percpu_irq, - request_shared_irq, set_enable, set_run_on_cpu_sync, + AutoEnable, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, + IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, ShareMode, + cpu_online, disable_irq, dispatch_irq, enable_irq, free_irq, handle, irq_status, request_irq, + request_percpu_irq, request_shared_irq, run_on_cpu_sync, set_enable, set_run_on_cpu_sync, + synchronize_irq, }; #[cfg(feature = "ipi")] pub use ax_plat::irq::{IpiTarget, send_ipi}; diff --git a/os/arceos/modules/axruntime/src/klib.rs b/os/arceos/modules/axruntime/src/klib.rs index e77e07b5d0..f9f60e9f7c 100644 --- a/os/arceos/modules/axruntime/src/klib.rs +++ b/os/arceos/modules/axruntime/src/klib.rs @@ -14,10 +14,9 @@ use core::time::Duration; #[cfg(feature = "paging")] use ax_memory_addr::MemoryAddr; -#[cfg(feature = "irq")] -use axklib::IrqError; use axklib::{ - AxError, AxResult, IrqCpuMask, IrqHandle, Klib, PhysAddr, RawIrqHandler, VirtAddr, impl_trait, + AxError, AxResult, IrqCpuId, IrqCpuMask, IrqError, IrqHandle, Klib, PhysAddr, RawIrqHandler, + VirtAddr, impl_trait, }; struct KlibImpl; @@ -276,5 +275,21 @@ impl_trait! { Err(AxError::Unsupported) } } + + unsafe fn irq_run_on_cpu_sync( + _cpu: IrqCpuId, + _f: unsafe fn(*mut ()), + _arg: *mut (), + ) -> Result<(), IrqError> { + #[cfg(feature = "irq")] + { + unsafe { ax_hal::irq::run_on_cpu_sync(_cpu, _f, _arg) } + } + #[cfg(not(feature = "irq"))] + { + let _ = (_cpu, _f, _arg); + Err(IrqError::Unsupported) + } + } } } diff --git a/platforms/ax-plat-loongarch64-qemu-virt/src/irq.rs b/platforms/ax-plat-loongarch64-qemu-virt/src/irq.rs index 0196e99880..a24172d892 100644 --- a/platforms/ax-plat-loongarch64-qemu-virt/src/irq.rs +++ b/platforms/ax-plat-loongarch64-qemu-virt/src/irq.rs @@ -107,6 +107,13 @@ impl IrqIf for IrqIfImpl { } } + fn set_affinity( + _irq: usize, + _affinity: ax_plat::irq::IrqAffinity, + ) -> Result<(), ax_plat::irq::IrqError> { + Err(ax_plat::irq::IrqError::Unsupported) + } + /// Handles the IRQ. fn handle(irq: usize) -> Option { let mut irq = IrqType::new(irq); diff --git a/platforms/ax-plat-riscv64-sg2002/src/irq.rs b/platforms/ax-plat-riscv64-sg2002/src/irq.rs index 33c5a19667..04b50318e2 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/irq.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/irq.rs @@ -103,6 +103,13 @@ impl IrqIf for IrqIfImpl { ); } + fn set_affinity( + _irq: usize, + _affinity: ax_plat::irq::IrqAffinity, + ) -> Result<(), ax_plat::irq::IrqError> { + Err(ax_plat::irq::IrqError::Unsupported) + } + /// Handles the IRQ. fn handle(irq: usize) -> Option { with_cause!( diff --git a/platforms/ax-plat-riscv64-visionfive2/src/irq.rs b/platforms/ax-plat-riscv64-visionfive2/src/irq.rs index 0558300ec6..73a2f8fe07 100644 --- a/platforms/ax-plat-riscv64-visionfive2/src/irq.rs +++ b/platforms/ax-plat-riscv64-visionfive2/src/irq.rs @@ -103,6 +103,13 @@ impl IrqIf for IrqIfImpl { ); } + fn set_affinity( + _irq: usize, + _affinity: ax_plat::irq::IrqAffinity, + ) -> Result<(), ax_plat::irq::IrqError> { + Err(ax_plat::irq::IrqError::Unsupported) + } + /// Handles the IRQ. fn handle(irq: usize) -> Option { with_cause!( diff --git a/platforms/ax-plat/src/irq.rs b/platforms/ax-plat/src/irq.rs index 6d89784b31..1555e8d50d 100644 --- a/platforms/ax-plat/src/irq.rs +++ b/platforms/ax-plat/src/irq.rs @@ -4,8 +4,9 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use ax_kernel_guard::BaseGuard; pub use irq_framework::{ - AutoEnable, CpuId, CpuMask, IrqContext, IrqError, IrqHandle, IrqNumber, IrqOps, IrqOutcome, - IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, Registry, ShareMode, + AutoEnable, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, + IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, + Registry, ShareMode, }; use spin::Once; @@ -19,6 +20,23 @@ pub fn set_run_on_cpu_sync(run_on_cpu_sync: RunOnCpuSync) { RUN_ON_CPU_SYNC.store(run_on_cpu_sync as usize, Ordering::Release); } +/// Runs a raw thunk synchronously on the requested CPU. +/// +/// This is the generic owner-CPU execution bridge used by device runtimes that +/// must keep register access on one non-reentrant CPU context. +/// +/// # Safety +/// +/// `arg` must stay valid until this function returns, and `f` must be safe to +/// execute in the target CPU's IRQ/IPI context. +pub unsafe fn run_on_cpu_sync( + cpu: CpuId, + f: unsafe fn(*mut ()), + arg: *mut (), +) -> Result<(), IrqError> { + PlatIrqOps.run_on_cpu_sync(cpu, f, arg) +} + struct PlatIrqOps; impl IrqOps for PlatIrqOps { @@ -75,6 +93,10 @@ impl IrqOps for PlatIrqOps { Ok(()) } + fn set_affinity(&self, irq: IrqNumber, affinity: IrqAffinity) -> Result<(), IrqError> { + set_affinity(irq.0, affinity) + } + fn is_enabled(&self, _irq: IrqNumber, _cpu: Option) -> Result { Err(IrqError::Unsupported) } @@ -155,6 +177,11 @@ pub fn disable_irq(handle: IrqHandle) -> Result<(), IrqError> { registry().disable(handle) } +/// Waits until no handler for this IRQ descriptor is in flight. +pub fn synchronize_irq(handle: IrqHandle) -> Result<(), IrqError> { + registry().synchronize(handle) +} + /// Returns the status of an IRQ action. pub fn irq_status(handle: IrqHandle) -> Result { registry().status(handle) @@ -208,6 +235,9 @@ pub trait IrqIf { /// Enables or disables the given IRQ. fn set_enable(irq: usize, enabled: bool); + /// Routes a global IRQ to a fixed CPU when supported. + fn set_affinity(irq: usize, affinity: IrqAffinity) -> Result<(), IrqError>; + /// Handles the IRQ. /// /// It is called by the common interrupt handler. Platform implementations diff --git a/platforms/axplat-dyn/src/irq.rs b/platforms/axplat-dyn/src/irq.rs index 9690d77995..758fb67f63 100644 --- a/platforms/axplat-dyn/src/irq.rs +++ b/platforms/axplat-dyn/src/irq.rs @@ -1,7 +1,7 @@ #[cfg(all(target_arch = "riscv64", feature = "hv"))] use core::sync::atomic::{AtomicPtr, Ordering}; -use ax_plat::irq::{IrqIf, dispatch_irq}; +use ax_plat::irq::{IrqAffinity, IrqError, IrqIf, dispatch_irq}; #[cfg(all(target_arch = "riscv64", feature = "hv"))] const RISCV_INTERRUPT_BIT: usize = 1usize << (usize::BITS as usize - 1); @@ -23,6 +23,14 @@ impl IrqIf for IrqIfImpl { somehal::irq::irq_set_enable(irq_raw.into(), enabled); } + fn set_affinity(irq_raw: usize, affinity: IrqAffinity) -> Result<(), IrqError> { + let affinity = match affinity { + IrqAffinity::Any => somehal::irq::IrqAffinity::Any, + IrqAffinity::Fixed(cpu) => somehal::irq::IrqAffinity::Fixed { cpu_id: cpu.0 }, + }; + somehal::irq::irq_set_affinity(irq_raw.into(), affinity).map_err(|_| IrqError::Unsupported) + } + /// Handles the IRQ. fn handle(irq_num: usize) -> Option { let irq_num = { diff --git a/platforms/somehal/src/arch/aarch64/gic/mod.rs b/platforms/somehal/src/arch/aarch64/gic/mod.rs index 946043ea6a..c8069bd015 100644 --- a/platforms/somehal/src/arch/aarch64/gic/mod.rs +++ b/platforms/somehal/src/arch/aarch64/gic/mod.rs @@ -63,6 +63,24 @@ pub fn irq_set_enable(irq: rdrive::IrqId, enable: bool) { } } +pub fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, +) -> Result<(), &'static str> { + let raw = irq.into(); + match backend() { + GicBackend::V2 => v2::irq_set_affinity(raw, affinity), + GicBackend::V3 => v3::irq_set_affinity(raw, affinity), + GicBackend::None => { + if v3::is_support_icc() { + v3::irq_set_affinity(raw, affinity) + } else { + v2::irq_set_affinity(raw, affinity) + } + } + } +} + pub fn send_ipi(irq: rdrive::IrqId, target: crate::irq::IpiTarget) { let raw = irq.into(); match backend() { diff --git a/platforms/somehal/src/arch/aarch64/gic/v2.rs b/platforms/somehal/src/arch/aarch64/gic/v2.rs index 9e3b1cdbad..acb2facc42 100644 --- a/platforms/somehal/src/arch/aarch64/gic/v2.rs +++ b/platforms/somehal/src/arch/aarch64/gic/v2.rs @@ -141,6 +141,21 @@ pub fn irq_set_enable(raw: usize, enable: bool) { }); } +pub fn irq_set_affinity(raw: usize, affinity: crate::irq::IrqAffinity) -> Result<(), &'static str> { + let intid = unsafe { IntId::raw(raw as _) }; + if intid.is_private() { + return Err("GICv2 private IRQ affinity cannot be changed"); + } + let crate::irq::IrqAffinity::Fixed { cpu_id } = affinity else { + return Ok(()); + }; + let target_cpu = super::hardware_cpu_id(cpu_id); + with_gic(|gic| { + gic.set_target_cpu(intid, TargetList::new(&mut core::iter::once(target_cpu))); + }); + Ok(()) +} + pub fn send_ipi(raw: usize, target: crate::irq::IpiTarget) { let sgi = IntId::sgi(raw as u32); let target = match target { diff --git a/platforms/somehal/src/arch/aarch64/gic/v3.rs b/platforms/somehal/src/arch/aarch64/gic/v3.rs index 35660f1289..f5d066fbd8 100644 --- a/platforms/somehal/src/arch/aarch64/gic/v3.rs +++ b/platforms/somehal/src/arch/aarch64/gic/v3.rs @@ -141,6 +141,21 @@ pub fn irq_set_enable(raw: usize, enable: bool) { }); } +pub fn irq_set_affinity(raw: usize, affinity: crate::irq::IrqAffinity) -> Result<(), &'static str> { + let intid = unsafe { IntId::raw(raw as _) }; + if intid.is_private() { + return Err("GICv3 private IRQ affinity cannot be changed"); + } + let target = match affinity { + crate::irq::IrqAffinity::Any => None, + crate::irq::IrqAffinity::Fixed { cpu_id } => { + Some(affinity_from_mpidr(super::hardware_cpu_id(cpu_id))) + } + }; + with_gic(|gic| gic.set_target_cpu(intid, target)); + Ok(()) +} + pub fn send_ipi(raw: usize, target: crate::irq::IpiTarget) { let sgi = IntId::sgi(raw as u32); let target = match target { diff --git a/platforms/somehal/src/arch/aarch64/mod.rs b/platforms/somehal/src/arch/aarch64/mod.rs index e982932fb5..ed0d96436b 100644 --- a/platforms/somehal/src/arch/aarch64/mod.rs +++ b/platforms/somehal/src/arch/aarch64/mod.rs @@ -12,6 +12,13 @@ impl PlatOp for Plat { gic::irq_set_enable(irq, enable); } + fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, + ) -> Result<(), &'static str> { + gic::irq_set_affinity(irq, affinity) + } + fn send_ipi(irq: rdrive::IrqId, target: crate::irq::IpiTarget) { gic::send_ipi(irq, target); } diff --git a/platforms/somehal/src/arch/loongarch64/mod.rs b/platforms/somehal/src/arch/loongarch64/mod.rs index 3bb004231a..f6940c6104 100644 --- a/platforms/somehal/src/arch/loongarch64/mod.rs +++ b/platforms/somehal/src/arch/loongarch64/mod.rs @@ -59,6 +59,22 @@ impl PlatOp for Plat { pch_pic::set_irq_enable(raw, enable); } + fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, + ) -> Result<(), &'static str> { + let raw = irq.raw(); + if raw == someboot::irq::systimer_irq().raw() || raw == IPI_IRQ { + return Err("LoongArch local IRQ affinity cannot be changed"); + } + match affinity { + crate::irq::IrqAffinity::Any | crate::irq::IrqAffinity::Fixed { cpu_id: 0 } => Ok(()), + crate::irq::IrqAffinity::Fixed { .. } => { + Err("LoongArch EIOINTC affinity currently supports only CPU0") + } + } + } + fn send_ipi(_irq: rdrive::IrqId, target: crate::irq::IpiTarget) { match target { crate::irq::IpiTarget::Current { cpu_id } | crate::irq::IpiTarget::Other { cpu_id } => { diff --git a/platforms/somehal/src/arch/riscv64/mod.rs b/platforms/somehal/src/arch/riscv64/mod.rs index 43e387e74e..0f866ca74c 100644 --- a/platforms/somehal/src/arch/riscv64/mod.rs +++ b/platforms/somehal/src/arch/riscv64/mod.rs @@ -11,6 +11,13 @@ impl PlatOp for Plat { plic::irq_set_enable(irq, enable); } + fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, + ) -> Result<(), &'static str> { + plic::irq_set_affinity(irq, affinity) + } + fn begin_irq(raw: usize) -> Option { plic::begin_irq(raw) } diff --git a/platforms/somehal/src/arch/riscv64/plic.rs b/platforms/somehal/src/arch/riscv64/plic.rs index 3a8b46693d..b4d9f3fa9a 100644 --- a/platforms/somehal/src/arch/riscv64/plic.rs +++ b/platforms/somehal/src/arch/riscv64/plic.rs @@ -1,4 +1,4 @@ -use alloc::{format, vec::Vec}; +use alloc::{format, vec, vec::Vec}; use core::{num::NonZeroU32, ptr::NonNull}; use ax_riscv_plic::{PLICRegs, Plic, PlicIrqHandler}; @@ -71,6 +71,24 @@ pub fn irq_set_enable(irq: rdrive::IrqId, enable: bool) { } } +pub fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, +) -> Result<(), &'static str> { + let raw: usize = irq.into(); + if raw & INTC_IRQ_BASE != 0 { + return Err("RISC-V local IRQ affinity cannot be changed"); + } + let Some(source) = NonZeroU32::new(raw as u32) else { + return Err("invalid PLIC source 0"); + }; + with_plic("setting PLIC IRQ affinity", |plic| { + plic.set_source_affinity(source, affinity) + }) + .flatten() + .ok_or("RISC-V PLIC is not registered or affinity target is invalid") +} + enum Completion { None, Plic(NonZeroU32), @@ -192,6 +210,7 @@ fn probe_plic(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { let plic = RiscvPlic { inner: plic, context_by_cpu: contexts, + affinity_by_source: vec![crate::irq::IrqAffinity::Any; ndev.saturating_add(1)], sources: ndev, }; enable_local_interrupts(); @@ -299,6 +318,7 @@ fn get_irq_handler() -> Option<&'static RiscvPlicIrqHandler> { struct RiscvPlic { inner: Plic, context_by_cpu: Vec>, + affinity_by_source: Vec, sources: usize, } @@ -362,7 +382,7 @@ impl RiscvPlic { } self.inner.set_priority(source, DEFAULT_PRIORITY); let current = current_context(&self.context_by_cpu); - for context in self.context_by_cpu.iter().filter_map(|context| *context) { + for context in self.contexts_for_source(source) { self.inner.enable(source, context); } if current.is_none() { @@ -375,6 +395,53 @@ impl RiscvPlic { self.inner.disable(source, context); } } + + fn set_source_affinity( + &mut self, + source: NonZeroU32, + affinity: crate::irq::IrqAffinity, + ) -> Option<()> { + if source.get() as usize > self.sources { + warn!( + "skip setting affinity for out-of-range PLIC source {}", + source.get() + ); + return None; + } + if let crate::irq::IrqAffinity::Fixed { cpu_id } = affinity + && self + .context_by_cpu + .get(cpu_id) + .and_then(|ctx| *ctx) + .is_none() + { + warn!("PLIC supervisor context for affinity CPU {cpu_id} is not found"); + return None; + } + + self.disable_source(source); + self.affinity_by_source[source.get() as usize] = affinity; + if self.inner.get_priority(source) != 0 { + for context in self.contexts_for_source(source) { + self.inner.enable(source, context); + } + } + Some(()) + } + + fn contexts_for_source(&self, source: NonZeroU32) -> Vec { + match self.affinity_by_source[source.get() as usize] { + crate::irq::IrqAffinity::Any => { + self.context_by_cpu.iter().filter_map(|ctx| *ctx).collect() + } + crate::irq::IrqAffinity::Fixed { cpu_id } => self + .context_by_cpu + .get(cpu_id) + .and_then(|ctx| *ctx) + .into_iter() + .collect(), + } + } } fn current_context(context_by_cpu: &[Option]) -> Option { diff --git a/platforms/somehal/src/arch/x86_64/mod.rs b/platforms/somehal/src/arch/x86_64/mod.rs index 7746b66a5b..fe170cf6e5 100644 --- a/platforms/somehal/src/arch/x86_64/mod.rs +++ b/platforms/somehal/src/arch/x86_64/mod.rs @@ -40,6 +40,7 @@ module_driver!( struct X86IoApicIntc { ioapics: Vec, routes: Vec, + destinations: Vec<(usize, u8)>, } impl X86IoApicIntc { @@ -47,6 +48,7 @@ impl X86IoApicIntc { Self { ioapics: ioapics.iter().copied().map(X86IoApic::new).collect(), routes: Vec::new(), + destinations: Vec::new(), } } @@ -92,13 +94,48 @@ impl X86IoApicIntc { } fn set_route_enable(&mut self, route: &AcpiGsiRoute, enable: bool) { + let dest = self.destination_for_vector(route.vector); for ioapic in &mut self.ioapics { if ioapic.contains_route(route) { - ioapic.set_route_enable(route, enable); + ioapic.set_route_enable(route, enable, dest); return; } } } + + fn set_vector_destination(&mut self, vector: usize, dest: u8) -> bool { + if let Some((_, existing)) = self + .destinations + .iter_mut() + .find(|(known_vector, _)| *known_vector == vector) + { + *existing = dest; + } else { + self.destinations.push((vector, dest)); + } + + let routes = self.routes_for_vector(vector); + if routes.is_empty() { + return false; + } + + for route in routes { + for ioapic in &mut self.ioapics { + if ioapic.contains_route(&route) { + ioapic.set_route_destination(&route, dest); + break; + } + } + } + true + } + + fn destination_for_vector(&self, vector: usize) -> u8 { + self.destinations + .iter() + .find_map(|(known_vector, dest)| (*known_vector == vector).then_some(*dest)) + .unwrap_or(0) + } } struct X86IoApic { @@ -148,7 +185,7 @@ impl X86IoApic { && self.contains(route.gsi) } - fn set_route_enable(&mut self, route: &AcpiGsiRoute, enable: bool) { + fn set_route_enable(&mut self, route: &AcpiGsiRoute, enable: bool, dest: u8) { if !self.contains_route(route) { return; } @@ -159,7 +196,7 @@ impl X86IoApic { entry.set_vector(route.vector as u8); entry.set_mode(IrqMode::Fixed); entry.set_flags(intx_flags(route.trigger, route.polarity) | IrqFlags::MASKED); - entry.set_dest(0); + entry.set_dest(dest); self.ioapic.set_table_entry(input, entry); if enable { @@ -167,6 +204,19 @@ impl X86IoApic { } } } + + fn set_route_destination(&mut self, route: &AcpiGsiRoute, dest: u8) { + if !self.contains_route(route) { + return; + } + + unsafe { + let input = route.controller_input; + let mut entry = self.ioapic.table_entry(input); + entry.set_dest(dest); + self.ioapic.set_table_entry(input, entry); + } + } } impl DriverGeneric for X86IoApicIntc { @@ -216,6 +266,31 @@ impl PlatOp for Plat { set_ioapic_vector_enable(raw, enable); } + fn irq_set_affinity( + irq: rdrive::IrqId, + affinity: crate::irq::IrqAffinity, + ) -> Result<(), &'static str> { + let raw = irq.raw(); + if raw == someboot::irq::systimer_irq().raw() { + return Err("x86 local APIC timer affinity cannot be changed"); + } + + let dest = match affinity { + crate::irq::IrqAffinity::Any => 0, + crate::irq::IrqAffinity::Fixed { cpu_id } => { + let Some(apic_id) = someboot::smp::cpu_idx_to_id(cpu_id) else { + return Err("x86 IRQ affinity target CPU is not known"); + }; + apic_id as u8 + } + }; + if set_ioapic_vector_destination(raw, dest) { + Ok(()) + } else { + Err("x86 IOAPIC route for IRQ vector was not found") + } + } + fn send_ipi(irq: rdrive::IrqId, target: crate::irq::IpiTarget) { let vector = irq.raw() as u8; @@ -303,6 +378,19 @@ fn set_ioapic_vector_enable(vector: usize, enable: bool) { } } +fn set_ioapic_vector_destination(vector: usize, dest: u8) -> bool { + for intc in rdrive::get_list::() { + if intc.descriptor().name.starts_with("ACPI IOAPIC") + && let Ok(ioapic) = intc.downcast::() + && let Ok(mut ioapic) = ioapic.try_lock() + && ioapic.set_vector_destination(vector, dest) + { + return true; + } + } + false +} + fn intx_flags(trigger: AcpiIrqTrigger, polarity: AcpiIrqPolarity) -> IrqFlags { let mut flags = IrqFlags::empty(); if trigger == AcpiIrqTrigger::Level { diff --git a/platforms/somehal/src/common.rs b/platforms/somehal/src/common.rs index 68a60fad33..fa54c6fbeb 100644 --- a/platforms/somehal/src/common.rs +++ b/platforms/somehal/src/common.rs @@ -9,6 +9,13 @@ pub trait PlatOp { fn irq_set_enable(irq: IrqId, enable: bool); + fn irq_set_affinity( + _irq: IrqId, + _affinity: crate::irq::IrqAffinity, + ) -> Result<(), &'static str> { + Err("IRQ affinity is not supported by this platform") + } + fn send_ipi(_irq: IrqId, _target: crate::irq::IpiTarget) { panic!("IPI is not implemented for this dynamic platform"); } diff --git a/platforms/somehal/src/irq.rs b/platforms/somehal/src/irq.rs index 303d72dcc2..42b02f2e7e 100644 --- a/platforms/somehal/src/irq.rs +++ b/platforms/somehal/src/irq.rs @@ -38,6 +38,15 @@ pub enum IpiTarget { }, } +/// Hardware routing preference for a global IRQ line. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum IrqAffinity { + /// Leave routing unchanged or platform-selected. + Any, + /// Route to one logical CPU. + Fixed { cpu_id: usize }, +} + pub fn irq_setup_by_fdt(irq_parent: DeviceId, irq_cell: &[u32]) -> IrqId { let mut intc = rdrive::get::(irq_parent).unwrap().lock().unwrap(); debug!("Setting up IRQ {:?}", irq_cell); @@ -50,6 +59,10 @@ pub fn irq_set_enable(irq: IrqId, enable: bool) { Plat::irq_set_enable(irq, enable); } +pub fn irq_set_affinity(irq: IrqId, affinity: IrqAffinity) -> Result<(), &'static str> { + Plat::irq_set_affinity(irq, affinity) +} + pub fn send_ipi(irq: IrqId, target: IpiTarget) { Plat::send_ipi(irq, target); } From f2227fb6f826ebc4a160dec38eaaf1bffb67af70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 18:15:17 +0800 Subject: [PATCH 07/16] fix(somehal): avoid stale riscv plic interrupts --- .claude/skills/arch-platform-porting/SKILL.md | 3 +- drivers/intc/riscv_plic/src/lib.rs | 17 ++++++++ platforms/ax-plat-riscv64-sg2002/src/irq.rs | 2 +- platforms/ax-plat-riscv64-sg2002/src/lib.rs | 1 - .../ax-plat-riscv64-visionfive2/src/irq.rs | 2 +- platforms/somehal/src/arch/riscv64/plic.rs | 42 +++++++++++++------ 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.claude/skills/arch-platform-porting/SKILL.md b/.claude/skills/arch-platform-porting/SKILL.md index 6ea58dfbbd..9fd4d2e0d9 100644 --- a/.claude/skills/arch-platform-porting/SKILL.md +++ b/.claude/skills/arch-platform-porting/SKILL.md @@ -101,7 +101,8 @@ Adjust the package list to match the crates touched. 4. Inspect symbols and generated images with `llvm-objdump`, `readelf`, and map files. Confirm runtime addresses, not only link addresses. 5. Compare with local Linux architecture code for ordering of MMU, trap, SMP, and cache/TLB barriers when uncertain. First search for a local Linux source tree, then inspect the matching `arch/` directory; do not assume a fixed path. 6. On one-shot timer platforms, verify the IRQ handler acknowledges the current timer interrupt before dispatching into code that reprograms the next event. In particular, LoongArch timer handlers must not clear `TICLR` after `_handle_irq()` / `dispatch_irq()`, because the timer tick path may already have armed a near-deadline event and a late acknowledge can clear the freshly-pending interrupt, leaving timer-based sleeps stuck. -7. Turn the root cause into a regression test or a focused QEMU case when practical. +7. On RISC-V PLIC platforms, take ownership of every supervisor context before enabling `sie.SEXT`: clear inherited source-enable bits from firmware/bootloader state, initialize thresholds, and keep a software "source enabled" state instead of inferring enablement from non-zero priority. IRQ framework setup may set affinity while an action is still disabled; affinity changes must not enable a source until the framework explicitly enables the line. +8. Turn the root cause into a regression test or a focused QEMU case when practical. ## Common Failure Signals diff --git a/drivers/intc/riscv_plic/src/lib.rs b/drivers/intc/riscv_plic/src/lib.rs index 5de76cf2ab..85416400e4 100644 --- a/drivers/intc/riscv_plic/src/lib.rs +++ b/drivers/intc/riscv_plic/src/lib.rs @@ -95,6 +95,15 @@ impl Plic { self.irq_handler().init_by_context(ctx); } + /// Reset the PLIC context to a kernel-owned baseline. + /// + /// Firmware or a previous boot stage may leave interrupt source enable bits + /// set. Clear them before enabling supervisor external interrupts so stale + /// device IRQs cannot fire before their OS handlers are registered. + pub fn reset_context(&mut self, ctx: usize) { + self.irq_handler().reset_context(ctx); + } + const fn regs(&self) -> &PLICRegs { unsafe { self.base.as_ref() } } @@ -226,6 +235,14 @@ impl PlicIrqHandler { self.regs().contexts[ctx].priority_threshold.set(0); } + /// Reset the PLIC context to a kernel-owned baseline. + pub fn reset_context(&self, ctx: usize) { + for enable in self.regs().interrupt_enable[ctx].iter() { + enable.set(0); + } + self.init_by_context(ctx); + } + /// Claim an interrupt in `context`, returning its source. pub fn claim(&self, ctx: usize) -> Option { NonZeroU32::new(self.regs().contexts[ctx].interrupt_claim_complete.get()) diff --git a/platforms/ax-plat-riscv64-sg2002/src/irq.rs b/platforms/ax-plat-riscv64-sg2002/src/irq.rs index 04b50318e2..9e0a668117 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/irq.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/irq.rs @@ -34,13 +34,13 @@ fn this_context() -> usize { } pub(super) fn init_percpu() { + PLIC.lock().reset_context(this_context()); // enable soft interrupts, timer interrupts, and external interrupts unsafe { sie::set_ssoft(); sie::set_stimer(); sie::set_sext(); } - PLIC.lock().init_by_context(this_context()); } macro_rules! with_cause { diff --git a/platforms/ax-plat-riscv64-sg2002/src/lib.rs b/platforms/ax-plat-riscv64-sg2002/src/lib.rs index 2762b386f9..7aa78b3b49 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/lib.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/lib.rs @@ -1,5 +1,4 @@ #![no_std] -#![feature(used_with_arg)] #![cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] extern crate alloc; diff --git a/platforms/ax-plat-riscv64-visionfive2/src/irq.rs b/platforms/ax-plat-riscv64-visionfive2/src/irq.rs index 73a2f8fe07..0513298409 100644 --- a/platforms/ax-plat-riscv64-visionfive2/src/irq.rs +++ b/platforms/ax-plat-riscv64-visionfive2/src/irq.rs @@ -35,13 +35,13 @@ fn this_context() -> usize { } pub(super) fn init_percpu() { + PLIC.lock().reset_context(this_context()); // enable soft interrupts, timer interrupts, and external interrupts unsafe { sie::set_ssoft(); sie::set_stimer(); sie::set_sext(); } - PLIC.lock().init_by_context(this_context()); } macro_rules! with_cause { diff --git a/platforms/somehal/src/arch/riscv64/plic.rs b/platforms/somehal/src/arch/riscv64/plic.rs index b4d9f3fa9a..b04ab08697 100644 --- a/platforms/somehal/src/arch/riscv64/plic.rs +++ b/platforms/somehal/src/arch/riscv64/plic.rs @@ -157,10 +157,10 @@ fn complete_external_irq_source(source: NonZeroU32) { } pub fn secondary_init_intc(cpu_idx: usize) { - enable_local_interrupts(); if let Some(handler) = get_irq_handler() { handler.init_context(cpu_idx); } + enable_local_interrupts(); } pub fn send_ipi_to_cpu(cpu_id: usize) { @@ -206,11 +206,14 @@ fn probe_plic(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { context_by_cpu: contexts.clone(), }; IRQ_HANDLER.init(irq_handler); - IRQ_HANDLER.init_current_context(); + if let Some(handler) = get_irq_handler() { + handler.reset_all_contexts(); + } let plic = RiscvPlic { inner: plic, context_by_cpu: contexts, affinity_by_source: vec![crate::irq::IrqAffinity::Any; ndev.saturating_add(1)], + enabled_by_source: vec![false; ndev.saturating_add(1)], sources: ndev, }; enable_local_interrupts(); @@ -319,6 +322,7 @@ struct RiscvPlic { inner: Plic, context_by_cpu: Vec>, affinity_by_source: Vec, + enabled_by_source: Vec, sources: usize, } @@ -332,14 +336,6 @@ impl RiscvPlicIrqHandler { current_context(&self.context_by_cpu) } - fn init_current_context(&self) { - if let Some(context) = self.current_context() { - self.init_context_by_context_id(context); - } else { - warn_missing_current_context(); - } - } - fn init_context(&self, cpu_idx: usize) { if let Some(context) = self.context_by_cpu.get(cpu_idx).and_then(|ctx| *ctx) { self.init_context_by_context_id(context); @@ -353,6 +349,17 @@ impl RiscvPlicIrqHandler { trace!("PLIC context {context} initialized"); } + fn reset_all_contexts(&self) { + for context in self.context_by_cpu.iter().filter_map(|context| *context) { + self.reset_context_by_context_id(context); + } + } + + fn reset_context_by_context_id(&self, context: usize) { + self.inner.reset_context(context); + trace!("PLIC context {context} reset"); + } + fn claim_current(&self) -> Option { let Some(context) = self.current_context() else { warn_missing_current_context(); @@ -380,6 +387,7 @@ impl RiscvPlic { warn!("skip enabling out-of-range PLIC source {}", source.get()); return; } + self.enabled_by_source[source.get() as usize] = true; self.inner.set_priority(source, DEFAULT_PRIORITY); let current = current_context(&self.context_by_cpu); for context in self.contexts_for_source(source) { @@ -391,6 +399,15 @@ impl RiscvPlic { } fn disable_source(&mut self, source: NonZeroU32) { + if source.get() as usize > self.sources { + warn!("skip disabling out-of-range PLIC source {}", source.get()); + return; + } + self.enabled_by_source[source.get() as usize] = false; + self.disable_source_contexts(source); + } + + fn disable_source_contexts(&mut self, source: NonZeroU32) { for context in self.context_by_cpu.iter().filter_map(|context| *context) { self.inner.disable(source, context); } @@ -419,9 +436,10 @@ impl RiscvPlic { return None; } - self.disable_source(source); + let was_enabled = self.enabled_by_source[source.get() as usize]; + self.disable_source_contexts(source); self.affinity_by_source[source.get() as usize] = affinity; - if self.inner.get_priority(source) != 0 { + if was_enabled { for context in self.contexts_for_source(source) { self.inner.enable(source, context); } From 272c13b81a5b3e2804f13a48b72dbd5b601937bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 18:35:20 +0800 Subject: [PATCH 08/16] fix(sg2002): gate used linker feature for model register --- platforms/ax-plat-riscv64-sg2002/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platforms/ax-plat-riscv64-sg2002/src/lib.rs b/platforms/ax-plat-riscv64-sg2002/src/lib.rs index 7aa78b3b49..7aeb89db42 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/lib.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/lib.rs @@ -1,5 +1,9 @@ #![no_std] #![cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] +#![cfg_attr( + any(target_arch = "riscv32", target_arch = "riscv64"), + feature(used_with_arg) +)] extern crate alloc; From 71389f08bc3845a996b28f5fee3ccd0389efba3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 20:36:41 +0800 Subject: [PATCH 09/16] fix(serial): keep tty tx backlog draining --- .claude/skills/arch-platform-porting/SKILL.md | 1 + components/someboot/src/console/mod.rs | 70 +++++++++++++++++++ drivers/ax-driver/src/serial/runtime.rs | 18 +++-- drivers/interface/rdif-serial/src/core.rs | 42 ++++++++++- drivers/serial/some-serial/src/ns16550/mod.rs | 34 ++++++++- .../kernel/src/pseudofs/dev/tty/serial.rs | 11 ++- os/arceos/modules/axhal/src/dummy.rs | 2 + os/arceos/modules/axhal/src/lib.rs | 4 +- .../src/console.rs | 2 + .../ax-plat-riscv64-sg2002/src/console.rs | 2 + .../src/console.rs | 2 + platforms/ax-plat/src/console.rs | 7 ++ platforms/axplat-dyn/src/console.rs | 4 ++ .../starry/test/tests/system_case_tests.rs | 10 +++ .../axbuild/src/test/case/grouped_runner.rs | 4 +- scripts/axbuild/src/test/case/tests.rs | 4 +- scripts/axbuild/src/test/case/types.rs | 2 +- .../qemu-smp1/system/qemu-aarch64.toml | 4 +- .../qemu-smp1/system/qemu-loongarch64.toml | 4 +- .../qemu-smp1/system/qemu-riscv64.toml | 4 +- .../qemu-smp1/system/qemu-x86_64.toml | 4 +- .../qemu-smp4/system/qemu-aarch64.toml | 4 +- .../qemu-smp4/system/qemu-loongarch64.toml | 4 +- .../qemu-smp4/system/qemu-riscv64.toml | 4 +- .../qemu-smp4/system/qemu-x86_64.toml | 4 +- .../virtualization-tests/tests/axvm_irq.rs | 6 ++ 26 files changed, 223 insertions(+), 34 deletions(-) diff --git a/.claude/skills/arch-platform-porting/SKILL.md b/.claude/skills/arch-platform-porting/SKILL.md index 9fd4d2e0d9..c5011e1949 100644 --- a/.claude/skills/arch-platform-porting/SKILL.md +++ b/.claude/skills/arch-platform-porting/SKILL.md @@ -29,6 +29,7 @@ Current Axvisor LoongArch QEMU tests intentionally use the static `ax-hal/loonga - **Platform bridge**: update `platforms/axplat-dyn`, `platforms/somehal`, platform config, memory regions, IRQ routing, timer source, power operations, and CPU boot operations. - **Runtime IRQ ownership**: ArceOS runtime IRQ traps are owned by `ax-cpu` and dispatched through `ax_hal::irq::handle_irq`. `somehal` must stay OS-free and expose controller transactions through `somehal::irq::begin_irq(raw) -> ActiveIrq`; `ActiveIrq` is held while `axplat-dyn` dispatches the IRQ and its `Drop` performs the architecture-specific EOI/complete. Do not reintroduce `_someboot_handle_irq` or `#[somehal::irq_handler]` as runtime dispatch glue. - **Runtime console selection**: Dynamic platforms expose the firmware-selected hardware console through `somehal::console_device_id()` and `ax_hal::console::device_id()`. The value is `Result` derived from bootargs `console=`, ACPI SPCR, or FDT `stdout-path`; static platforms return `Err(NotSpecified)`. OS code such as Starry should match `Ok(id)` against probed serial devices, use `ttyS0` as the Linux-style hardware-console fallback only for `Err(NotSpecified)`, and leave `/dev/console` unbound (`ENODEV`) for non-hardware console selections, unmatched selected hardware devices, or when no serial console TTY exists. Do not reparse FDT or bootargs in the tty layer. +- **Runtime console ownership**: once Starry or another OS runtime binds the firmware-selected UART to an interrupt-driven tty/serial driver, call `ax_hal::console::claim_runtime_output()` and stop the low-level boot/platform console path from writing the same UART registers directly. The hardware console must have one runtime register owner; otherwise kernel log output and tty output can interleave at the UART register level and corrupt test markers or user input/output. - **Dynamic firmware devices**: for `rdrive` ACPI probes, real non-empty ACPI ID lists enumerate namespace `Device` nodes and expose `_CRS` memory, I/O port, and IRQ resources through `AcpiInfo`; empty ID lists or synthetic root IDs are reserved for root-table style callbacks. - **Page tables and memory**: check PTE flags, huge page support, direct map, kernel high map, MMIO map, TLB/cache barriers, and early `phys_to_virt` behavior before MMU state is fully recorded. - **Drivers and rootfs**: check PCI command bits, MMIO/iomap, DMA address width, virtio transport, block device visibility, rootfs patching, and console/input feature flags. diff --git a/components/someboot/src/console/mod.rs b/components/someboot/src/console/mod.rs index 072ad0e540..a6033cf35d 100644 --- a/components/someboot/src/console/mod.rs +++ b/components/someboot/src/console/mod.rs @@ -63,14 +63,23 @@ pub(crate) fn debug_to_memory_desc() -> Option { } pub fn _print(args: core::fmt::Arguments) { + if runtime_output_claimed() { + return; + } let _ = ConFmt {}.write_fmt(args); } pub fn _write_bytes(bytes: &[u8]) -> usize { + if runtime_output_claimed() { + return bytes.len(); + } con().write_bytes(bytes) } pub fn _write_str(s: &str) { + if runtime_output_claimed() { + return; + } con().write_str(s); } @@ -173,6 +182,7 @@ impl Con for NoCon { } static mut CON: &dyn Con = &NoCon; +static RUNTIME_OUTPUT_CLAIMED: AtomicBool = AtomicBool::new(false); pub(crate) unsafe fn set_out(v: &'static dyn Con) { unsafe { @@ -180,6 +190,28 @@ pub(crate) unsafe fn set_out(v: &'static dyn Con) { } } +/// Marks the boot console output path as superseded by a runtime console. +/// +/// Once an OS serial/tty runtime owns the UART registers, the boot console must +/// not write the same hardware directly. It still reports bytes as consumed so +/// generic logging paths cannot spin forever after the handoff. +pub fn claim_runtime_output() { + RUNTIME_OUTPUT_CLAIMED.store(true, Ordering::Release); +} + +#[cfg(not(test))] +fn runtime_output_claimed() -> bool { + // On AArch64, exclusive atomic instructions such as LDXR/LDAXR are not + // reliable before the MMU is enabled. Keep the pre-MMU boot console path + // free of atomic reads and only honor the runtime handoff afterwards. + crate::mem::mmu::is_mmu_enabled() && RUNTIME_OUTPUT_CLAIMED.load(Ordering::Acquire) +} + +#[cfg(test)] +fn runtime_output_claimed() -> bool { + RUNTIME_OUTPUT_CLAIMED.load(Ordering::Acquire) +} + pub struct EarlySerial { raw: EarlySerialRaw, tx_state: SerialEvent, @@ -400,6 +432,44 @@ impl Con for EarlyconCell { } } +#[cfg(test)] +mod tests { + use core::sync::atomic::{AtomicUsize, Ordering}; + + use super::*; + + struct CountingCon; + + static WRITE_CALLS: AtomicUsize = AtomicUsize::new(0); + + impl Con for CountingCon { + fn write_bytes(&self, bytes: &[u8]) -> usize { + WRITE_CALLS.fetch_add(1, Ordering::Relaxed); + bytes.len() + } + } + + static COUNTING_CON: CountingCon = CountingCon; + + #[test] + fn runtime_output_claim_consumes_without_touching_boot_console() { + WRITE_CALLS.store(0, Ordering::Relaxed); + RUNTIME_OUTPUT_CLAIMED.store(false, Ordering::Relaxed); + + unsafe { set_out(&COUNTING_CON) }; + + assert_eq!(_write_bytes(b"before"), 6); + assert_eq!(WRITE_CALLS.load(Ordering::Relaxed), 1); + + claim_runtime_output(); + + assert_eq!(_write_bytes(b"after"), 5); + assert_eq!(WRITE_CALLS.load(Ordering::Relaxed), 1); + + RUNTIME_OUTPUT_CLAIMED.store(false, Ordering::Relaxed); + } +} + pub fn set_earlycon_by_cmdline() -> Result<(), &'static str> { let config = crate::cmdline::earlycon().ok_or("No earlycon parameter found")?; let debug_is_mmio = match config.uart_type { diff --git a/drivers/ax-driver/src/serial/runtime.rs b/drivers/ax-driver/src/serial/runtime.rs index 901694f258..e97408a0e0 100644 --- a/drivers/ax-driver/src/serial/runtime.rs +++ b/drivers/ax-driver/src/serial/runtime.rs @@ -20,7 +20,7 @@ pub trait InterruptSerial: Send + Sync + 'static { fn shutdown(&self) -> Result<(), IrqError>; fn set_config(&self, config: &Config) -> Result<(), ConfigError>; - fn try_write(&self, bytes: &[u8]) -> usize; + fn try_write(&self, bytes: &[u8]) -> SerialWriteResult; fn write_room(&self) -> usize; fn chars_in_buffer(&self) -> usize; fn tx_idle(&self) -> bool; @@ -34,6 +34,12 @@ pub trait InterruptSerial: Send + Sync + 'static { fn counters(&self) -> SerialCounters; } +#[derive(Clone, Copy, Debug, Default)] +pub struct SerialWriteResult { + pub accepted: usize, + pub outcome: SerialIrqOutcome, +} + pub struct KernelSerialPort { name: String, base_addr: usize, @@ -145,12 +151,16 @@ impl InterruptSerial for KernelSerialPort { .map_err(|_| ConfigError::RegisterError)? } - fn try_write(&self, bytes: &[u8]) -> usize { + fn try_write(&self, bytes: &[u8]) -> SerialWriteResult { let submit = self.tx.lock().submit(bytes); + let mut outcome = SerialIrqOutcome::default(); if submit.needs_kick { - let _ = self.service_on_owner(SerialSoftWork::TX_KICK); + outcome = self.service_on_owner(SerialSoftWork::TX_KICK); + } + SerialWriteResult { + accepted: submit.accepted, + outcome, } - submit.accepted } fn write_room(&self) -> usize { diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index 17bd3ea5fc..b4f193e436 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -546,7 +546,8 @@ impl SerialIrqHandler { core.tx_irq_enabled = true; } - if sent > 0 && self.tx.blocked.swap(false, Ordering::AcqRel) { + if sent > 0 { + self.tx.blocked.store(false, Ordering::Release); out.tx_wakeup = true; } out.tx_sent += sent; @@ -637,6 +638,7 @@ mod tests { irq: VecDeque, rx: VecDeque, tx_ready_budget: usize, + tx_load_size: usize, tx_written: Vec, mask: InterruptMask, } @@ -647,6 +649,7 @@ mod tests { irq: VecDeque::new(), rx: VecDeque::new(), tx_ready_budget: 0, + tx_load_size: 16, tx_written: Vec::new(), mask: InterruptMask::empty(), } @@ -739,7 +742,7 @@ mod tests { } fn tx_load_size(&self) -> usize { - 16 + self.tx_load_size } fn tx_idle(&mut self) -> bool { @@ -779,9 +782,44 @@ mod tests { let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); assert_eq!(outcome.tx_sent, 3); + assert!(outcome.tx_wakeup); assert_eq!(tx.chars_in_buffer(), 0); } + #[test] + fn tx_kick_wakes_again_when_queue_still_has_data() { + let mut uart = MockUart::new(); + uart.tx_ready_budget = 1; + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let mut tx = parts.tx; + tx.submit(b"abc"); + parts.irq.startup(lease(), &Config::new()).unwrap(); + + let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + + assert_eq!(outcome.tx_sent, 1); + assert!(outcome.tx_wakeup); + assert_eq!(tx.chars_in_buffer(), 2); + } + + #[test] + fn repeated_tx_kicks_eventually_drain_backlog() { + let mut uart = MockUart::new(); + uart.tx_ready_budget = 3; + uart.tx_load_size = 1; + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let mut tx = parts.tx; + tx.submit(b"abc"); + parts.irq.startup(lease(), &Config::new()).unwrap(); + + for remaining in [2, 1, 0] { + let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + assert_eq!(outcome.tx_sent, 1); + assert!(outcome.tx_wakeup); + assert_eq!(tx.chars_in_buffer(), remaining); + } + } + #[test] fn irq_services_rx_and_preserves_items_for_rx_queue() { let uart = MockUart::new() diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index 009f113352..9428f857b4 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -594,15 +594,17 @@ impl Ns16550 { /// 启用或禁用 FIFO pub fn enable_fifo(&mut self, enable: bool) { - if enable && self.is_16550_plus() { + if enable { let mut fcr = FifoControlFlags::ENABLE_FIFO; fcr.insert(FifoControlFlags::CLEAR_RECEIVER_FIFO); fcr.insert(FifoControlFlags::CLEAR_TRANSMITTER_FIFO); fcr.insert(FifoControlFlags::TRIGGER_1_BYTE); self.write_flags(UART_FCR, fcr); - } else { - self.write_flags(UART_FCR, FifoControlFlags::empty()); + if self.is_fifo_enabled() { + return; + } } + self.write_flags(UART_FCR, FifoControlFlags::empty()); } /// 设置 FIFO 触发级别 @@ -756,6 +758,19 @@ mod tests { } REGS[reg as usize].store(val, Ordering::SeqCst); + if reg == UART_FCR { + if val & FifoControlFlags::ENABLE_FIFO.bits() != 0 { + REGS[UART_IIR as usize].fetch_or( + InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits(), + Ordering::SeqCst, + ); + } else { + REGS[UART_IIR as usize].fetch_and( + !InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits(), + Ordering::SeqCst, + ); + } + } if reg == UART_THR { let iir = REGS[UART_IIR as usize].load(Ordering::SeqCst); if iir & InterruptIdentificationFlags::FIFO_ENABLE_MASK.bits() == 0 { @@ -899,6 +914,19 @@ mod tests { assert!(mcr.contains(ModemControlFlags::OUT_2)); } + #[test] + fn startup_enables_fifo_before_checking_fifo_status() { + let (_guard, mut uart) = serial(); + + uart.startup(&Config::new()).unwrap(); + + let iir = InterruptIdentificationFlags::from_bits_retain( + REGS[UART_IIR as usize].load(Ordering::SeqCst), + ); + assert!(iir.contains(InterruptIdentificationFlags::FIFO_ENABLE_MASK)); + assert_eq!(uart.tx_load_size(), UART_FIFO_SIZE as usize); + } + #[test] fn try_read_empty_returns_zero() { let (_guard, mut uart) = serial(); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index c4df1c5e00..5134015924 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -202,6 +202,7 @@ pub fn bind_console_to(proc: &Process) -> AxResult<()> { && let Some(entry) = SERIAL_REGISTRY.entries.get(index) { entry.backend.ensure_started()?; + ax_runtime::hal::console::claim_runtime_output(); return entry.tty.bind_to(proc); } Err(AxError::NoSuchDevice) @@ -433,6 +434,8 @@ fn spawn_serial_event_worker(backend: Arc) { if pending.contains(SerialEventBits::TX_SPACE) { backend.tx_notify.notify(); unsafe { backend.output_source.wake(IoEvents::OUT) }; + let outcome = backend.port.service_on_owner(SerialSoftWork::TX_KICK); + publish_serial_outcome(&backend, outcome, false); } } }, @@ -536,7 +539,9 @@ impl TtyWrite for SerialWriter { let _guard = self.backend.output_lock.lock(); let mut written = 0; while written < buf.len() { - let count = self.backend.port.try_write(&buf[written..]); + let result = self.backend.port.try_write(&buf[written..]); + publish_serial_outcome(&self.backend, result.outcome, false); + let count = result.accepted; if count == 0 { self.backend.tx_notify.wait(); continue; @@ -555,7 +560,9 @@ impl TtyWrite for SerialWriter { let Some(_guard) = self.backend.output_lock.try_lock() else { return 0; }; - self.backend.port.try_write(buf) + let result = self.backend.port.try_write(buf); + publish_serial_outcome(&self.backend, result.outcome, false); + result.accepted } fn flush_echo_before_input(&self) -> bool { diff --git a/os/arceos/modules/axhal/src/dummy.rs b/os/arceos/modules/axhal/src/dummy.rs index 04cb691bc3..4ca899b17e 100644 --- a/os/arceos/modules/axhal/src/dummy.rs +++ b/os/arceos/modules/axhal/src/dummy.rs @@ -46,6 +46,8 @@ impl ConsoleIf for DummyConsole { Err(ConsoleDeviceIdError::NotSpecified) } + fn claim_runtime_output() {} + #[cfg(feature = "irq")] fn irq_num() -> Option { None diff --git a/os/arceos/modules/axhal/src/lib.rs b/os/arceos/modules/axhal/src/lib.rs index 60f35cfccd..86b964bb4d 100644 --- a/os/arceos/modules/axhal/src/lib.rs +++ b/os/arceos/modules/axhal/src/lib.rs @@ -56,8 +56,8 @@ pub mod paging; /// Console input and output. pub mod console { pub use ax_plat::console::{ - ConsoleDeviceId, ConsoleDeviceIdError, ConsoleDeviceIdResult, device_id, read_bytes, - write_bytes, write_text_bytes, + ConsoleDeviceId, ConsoleDeviceIdError, ConsoleDeviceIdResult, claim_runtime_output, + device_id, read_bytes, write_bytes, write_text_bytes, }; #[cfg(feature = "irq")] pub use ax_plat::console::{ConsoleIrqEvent, handle_irq, irq_num, set_input_irq_enabled}; diff --git a/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs b/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs index 310282f52e..3347c717e2 100644 --- a/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs +++ b/platforms/ax-plat-loongarch64-qemu-virt/src/console.rs @@ -67,6 +67,8 @@ impl ConsoleIf for ConsoleIfImpl { Err(ConsoleDeviceIdError::NotSpecified) } + fn claim_runtime_output() {} + /// Returns the IRQ number for the console, if applicable. #[cfg(feature = "irq")] fn irq_num() -> Option { diff --git a/platforms/ax-plat-riscv64-sg2002/src/console.rs b/platforms/ax-plat-riscv64-sg2002/src/console.rs index 5d5f792dc9..f2bac2b304 100644 --- a/platforms/ax-plat-riscv64-sg2002/src/console.rs +++ b/platforms/ax-plat-riscv64-sg2002/src/console.rs @@ -48,6 +48,8 @@ impl ConsoleIf for ConsoleIfImpl { Err(ConsoleDeviceIdError::NotSpecified) } + fn claim_runtime_output() {} + /// Returns the IRQ number for the console, if applicable. #[cfg(feature = "irq")] fn irq_num() -> Option { diff --git a/platforms/ax-plat-riscv64-visionfive2/src/console.rs b/platforms/ax-plat-riscv64-visionfive2/src/console.rs index 919db41311..2afa156320 100644 --- a/platforms/ax-plat-riscv64-visionfive2/src/console.rs +++ b/platforms/ax-plat-riscv64-visionfive2/src/console.rs @@ -56,6 +56,8 @@ impl ConsoleIf for ConsoleIfImpl { Err(ConsoleDeviceIdError::NotSpecified) } + fn claim_runtime_output() {} + /// Returns the IRQ number for the console, if applicable. #[cfg(feature = "irq")] fn irq_num() -> Option { diff --git a/platforms/ax-plat/src/console.rs b/platforms/ax-plat/src/console.rs index 2edc1a52f4..0bbac519c9 100644 --- a/platforms/ax-plat/src/console.rs +++ b/platforms/ax-plat/src/console.rs @@ -49,6 +49,13 @@ pub trait ConsoleIf { /// [`ConsoleDeviceIdError::NotSpecified`]. fn device_id() -> ConsoleDeviceIdResult; + /// Hands platform console output ownership to a higher-level runtime driver. + /// + /// After this call, low-level console write paths must stop touching the + /// same hardware registers if the platform firmware console is backed by a + /// runtime-owned device. + fn claim_runtime_output(); + /// Returns the IRQ number for the console input interrupt. /// /// Returns `None` if input interrupt is not supported. diff --git a/platforms/axplat-dyn/src/console.rs b/platforms/axplat-dyn/src/console.rs index 9d3935befd..6335cf2bcb 100644 --- a/platforms/axplat-dyn/src/console.rs +++ b/platforms/axplat-dyn/src/console.rs @@ -45,6 +45,10 @@ impl ConsoleIf for ConsoleIfImpl { }) } + fn claim_runtime_output() { + somehal::console::claim_runtime_output(); + } + /// Returns the IRQ number for the console input interrupt. /// /// Returns `None` if input interrupt is not supported. diff --git a/scripts/axbuild/src/starry/test/tests/system_case_tests.rs b/scripts/axbuild/src/starry/test/tests/system_case_tests.rs index 5a00fe1d7e..a9f2db9d98 100644 --- a/scripts/axbuild/src/starry/test/tests/system_case_tests.rs +++ b/scripts/axbuild/src/starry/test/tests/system_case_tests.rs @@ -95,6 +95,16 @@ fn starry_system_grouped_qemu_configs_report_subcase_timing() { "{} must end a grouped subcase timing section", path.display() ); + assert!( + !command.contains("sort -nr") && !command.contains("head -n"), + "{} must not depend on external sort/head pipelines in the final timing summary", + path.display() + ); + assert!( + command.contains("done < \"$timing_file\""), + "{} must read grouped subcase timing from the timing file, not from stdin", + path.display() + ); let failure_branch = command.find("else\n").unwrap_or_else(|| { panic!( "{} must contain a failure branch for grouped subcases", diff --git a/scripts/axbuild/src/test/case/grouped_runner.rs b/scripts/axbuild/src/test/case/grouped_runner.rs index bba992f3fe..466dd97a55 100644 --- a/scripts/axbuild/src/test/case/grouped_runner.rs +++ b/scripts/axbuild/src/test/case/grouped_runner.rs @@ -54,9 +54,9 @@ pub(crate) fn write_grouped_case_runner_script( "failed=0\ntotal={}\nstep=0\n", test_commands.len() )); - for command in test_commands { + for (index, command) in test_commands.iter().enumerate() { let quoted = shell_single_quote(command); - let command_label = shell_single_quote(command); + let command_label = shell_single_quote(&format!("command-{}", index + 1)); let begin = shell_single_quote(&config.begin_marker); let passed = shell_single_quote(&config.passed_marker); let failed = shell_single_quote(&config.failed_marker); diff --git a/scripts/axbuild/src/test/case/tests.rs b/scripts/axbuild/src/test/case/tests.rs index a41c9ea237..e79559aaeb 100644 --- a/scripts/axbuild/src/test/case/tests.rs +++ b/scripts/axbuild/src/test/case/tests.rs @@ -157,8 +157,8 @@ fn grouped_runner_script_runs_all_commands_and_reports_summary() { assert!(content.contains("'SUITE_GROUPED_TEST_BEGIN'")); assert!(content.contains("'SUITE_GROUPED_TEST_PASSED'")); assert!(content.contains("'SUITE_GROUPED_TEST_FAILED'")); - assert!(content.contains("'/usr/bin/alpha'")); - assert!(content.contains("'/usr/bin/beta --flag'")); + assert!(content.contains("'command-1'")); + assert!(content.contains("'command-2'")); assert!(content.contains("SUITE_GROUPED_TESTS_PASSED")); } diff --git a/scripts/axbuild/src/test/case/types.rs b/scripts/axbuild/src/test/case/types.rs index 523f80a165..cadb71657f 100644 --- a/scripts/axbuild/src/test/case/types.rs +++ b/scripts/axbuild/src/test/case/types.rs @@ -18,7 +18,7 @@ pub(super) const CASE_APK_CACHE_DIR_NAME: &str = "apk-cache"; pub(super) const CASE_SH_DIR_NAME: &str = "sh"; pub(super) const CASE_ROOTFS_COPY_NAME: &str = "case-rootfs.img"; pub(super) const GROUPED_RUNNER_SCRIPT_FORMAT_VERSION: &str = - "grouped-runner-starry-init-autoload-v1"; + "grouped-runner-starry-init-autoload-v2-short-command-labels"; pub(super) const PYTHON_PIPELINE_CACHE_VERSION: &str = "python-apk-v1"; pub(super) const RUST_PIPELINE_CACHE_VERSION: &str = "rust-cross-v1"; /// QEMU global snapshot flag -- all disk writes go to a temporary file and are diff --git a/test-suit/starryos/qemu-smp1/system/qemu-aarch64.toml b/test-suit/starryos/qemu-smp1/system/qemu-aarch64.toml index cdbd65443c..0f2f6e1bab 100644 --- a/test-suit/starryos/qemu-smp1/system/qemu-aarch64.toml +++ b/test-suit/starryos/qemu-smp1/system/qemu-aarch64.toml @@ -67,13 +67,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp1/system/qemu-loongarch64.toml b/test-suit/starryos/qemu-smp1/system/qemu-loongarch64.toml index 6e701cdeaf..59851ea694 100644 --- a/test-suit/starryos/qemu-smp1/system/qemu-loongarch64.toml +++ b/test-suit/starryos/qemu-smp1/system/qemu-loongarch64.toml @@ -59,13 +59,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp1/system/qemu-riscv64.toml b/test-suit/starryos/qemu-smp1/system/qemu-riscv64.toml index c4883ba81f..2d10372d93 100644 --- a/test-suit/starryos/qemu-smp1/system/qemu-riscv64.toml +++ b/test-suit/starryos/qemu-smp1/system/qemu-riscv64.toml @@ -67,13 +67,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp1/system/qemu-x86_64.toml b/test-suit/starryos/qemu-smp1/system/qemu-x86_64.toml index 07965511a8..78f91c3841 100644 --- a/test-suit/starryos/qemu-smp1/system/qemu-x86_64.toml +++ b/test-suit/starryos/qemu-smp1/system/qemu-x86_64.toml @@ -67,13 +67,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp4/system/qemu-aarch64.toml b/test-suit/starryos/qemu-smp4/system/qemu-aarch64.toml index fb630c88d8..b5603870bc 100644 --- a/test-suit/starryos/qemu-smp4/system/qemu-aarch64.toml +++ b/test-suit/starryos/qemu-smp4/system/qemu-aarch64.toml @@ -53,13 +53,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp4/system/qemu-loongarch64.toml b/test-suit/starryos/qemu-smp4/system/qemu-loongarch64.toml index fbf10b6bdb..8017c7c2c4 100644 --- a/test-suit/starryos/qemu-smp4/system/qemu-loongarch64.toml +++ b/test-suit/starryos/qemu-smp4/system/qemu-loongarch64.toml @@ -55,13 +55,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp4/system/qemu-riscv64.toml b/test-suit/starryos/qemu-smp4/system/qemu-riscv64.toml index ed1879a447..6bfc417e17 100644 --- a/test-suit/starryos/qemu-smp4/system/qemu-riscv64.toml +++ b/test-suit/starryos/qemu-smp4/system/qemu-riscv64.toml @@ -55,13 +55,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/test-suit/starryos/qemu-smp4/system/qemu-x86_64.toml b/test-suit/starryos/qemu-smp4/system/qemu-x86_64.toml index 294db6fde0..b7b2789021 100644 --- a/test-suit/starryos/qemu-smp4/system/qemu-x86_64.toml +++ b/test-suit/starryos/qemu-smp4/system/qemu-x86_64.toml @@ -51,13 +51,13 @@ for bin in /usr/bin/starry-test-suit/*; do done echo "STARRY_SYSTEM_TEST_TIMING_BEGIN" if [ -s "$timing_file" ]; then - sort -nr "$timing_file" | head -n 20 | while read -r elapsed_s status bin; do + while read -r elapsed_s status bin; do if [ "$status" = passed ]; then printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=passed bin=%s\n' "$elapsed_s" "$bin" else printf 'STARRY_SYSTEM_TEST_TIMING: elapsed_s=%s status=failed bin=%s\n' "$elapsed_s" "$bin" fi - done + done < "$timing_file" fi echo "STARRY_SYSTEM_TEST_TIMING_END" rm -f "$timing_file" diff --git a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs index 98cad97d5d..02a1f4b92a 100644 --- a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs +++ b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs @@ -38,6 +38,12 @@ impl ConsoleIf for TestConsole { 0 } + fn device_id() -> ax_plat::console::ConsoleDeviceIdResult { + Err(ax_plat::console::ConsoleDeviceIdError::NotSpecified) + } + + fn claim_runtime_output() {} + fn irq_num() -> Option { None } From 3ef774795a39a78d1141b5f71b4609d60416b77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Wed, 24 Jun 2026 22:12:55 +0800 Subject: [PATCH 10/16] fix(serial): keep pl011 tx interrupts flowing --- drivers/interface/rdif-serial/src/core.rs | 22 +++++----- drivers/serial/some-serial/src/pl011.rs | 50 +++++++++++++++++++++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index b4f193e436..5d05e48b4a 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -520,7 +520,7 @@ impl SerialIrqHandler { budget: usize, out: &mut SerialIrqOutcome, ) -> usize { - let limit = budget.min(core.raw.tx_load_size().max(1)); + let limit = budget; let mut sent = 0; while sent < limit && core.raw.tx_ready() { let Some(byte) = self.tx.ring.peek_copy() else { @@ -803,21 +803,21 @@ mod tests { } #[test] - fn repeated_tx_kicks_eventually_drain_backlog() { + fn soft_tx_kick_uses_budget_while_hardware_ready() { let mut uart = MockUart::new(); - uart.tx_ready_budget = 3; + uart.tx_ready_budget = TX_KICK_BUDGET + 8; uart.tx_load_size = 1; - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::::split(uart, OwnerId(0)); let mut tx = parts.tx; - tx.submit(b"abc"); + let data = [b'x'; TX_KICK_BUDGET + 8]; + tx.submit(&data); parts.irq.startup(lease(), &Config::new()).unwrap(); - for remaining in [2, 1, 0] { - let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); - assert_eq!(outcome.tx_sent, 1); - assert!(outcome.tx_wakeup); - assert_eq!(tx.chars_in_buffer(), remaining); - } + let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + + assert_eq!(outcome.tx_sent, TX_KICK_BUDGET); + assert!(outcome.tx_wakeup); + assert_eq!(tx.chars_in_buffer(), 8); } #[test] diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index 436230a24c..8e2fbee4ca 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -482,7 +482,7 @@ impl Pl011 { self.registers() .uarticr - .set(active & !(UARTIS::TX::SET.value | UARTIS::RX::SET.value | UARTIS::RT::SET.value)); + .set(active & !(UARTIS::RX::SET.value | UARTIS::RT::SET.value)); IrqSnapshot { claimed: true, @@ -669,7 +669,7 @@ impl RawUart for Pl011 { // 根据ARM文档的建议配置流程: // 1. 禁用UART - let original_enable = self.registers().uartcr.is_set(UARTCR::UARTEN); // 保存原始使能状态 + let original_cr = self.registers().uartcr.extract(); // 保存原始使能状态 self.registers().uartcr.modify(UARTCR::UARTEN::CLEAR); // 禁用UART // 2. 等待当前字符传输完成 @@ -698,8 +698,12 @@ impl RawUart for Pl011 { self.registers().uartlcr_h.modify(UARTLCR_H::FEN::SET); // 6. 恢复UART使能状态 - if original_enable { - self.registers().uartcr.modify(UARTCR::UARTEN::SET); // 重新启用UART + if original_cr.is_set(UARTCR::UARTEN) { + self.registers().uartcr.modify( + UARTCR::UARTEN.val(original_cr.read(UARTCR::UARTEN)) + + UARTCR::TXE.val(original_cr.read(UARTCR::TXE)) + + UARTCR::RXE.val(original_cr.read(UARTCR::RXE)), + ); } Ok(()) @@ -889,6 +893,15 @@ mod tests { } } + fn read_test_reg(regs: &Pl011Registers, offset: usize) -> u32 { + unsafe { + (regs as *const Pl011Registers) + .cast::() + .add(offset / core::mem::size_of::()) + .read_volatile() + } + } + fn owner_lease() -> OwnerLease<'static> { unsafe { OwnerLease::new_unchecked(OwnerId(0)) } } @@ -971,6 +984,35 @@ mod tests { assert_eq!(tx.chars_in_buffer(), 0); } + #[test] + fn tx_irq_snapshot_acknowledges_tx_interrupt() { + let (mut regs, mut uart) = pl011_with_registers(); + + write_test_reg(&mut regs, 0x040, UARTIS::TX::SET.value); + let snapshot = uart.take_irq_snapshot(); + + assert!(snapshot.claimed); + assert!(snapshot.sources.contains(IrqSource::TX_SPACE)); + assert_eq!( + read_test_reg(®s, 0x044) & UARTIS::TX::SET.value, + UARTIS::TX::SET.value + ); + } + + #[test] + fn set_config_preserves_enabled_tx_and_rx_paths() { + let (regs, mut uart) = pl011_with_registers(); + regs.uartcr + .write(UARTCR::UARTEN::SET + UARTCR::TXE::SET + UARTCR::RXE::SET); + + uart.set_config(&Config::new()).unwrap(); + + let cr = regs.uartcr.extract(); + assert!(cr.is_set(UARTCR::UARTEN)); + assert!(cr.is_set(UARTCR::TXE)); + assert!(cr.is_set(UARTCR::RXE)); + } + #[test] fn rx_available_mask_enables_timeout_and_error_interrupts() { let (regs, mut uart) = pl011_with_registers(); From 11e5ff2e0b61198209f39c240e72012574f1f015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 09:14:48 +0800 Subject: [PATCH 11/16] fix(starry): address console selection review --- .../kernel/src/pseudofs/dev/tty/mod.rs | 88 ++++++++----------- platforms/somehal/src/boot_console.rs | 38 ++++---- 2 files changed, 55 insertions(+), 71 deletions(-) diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs index 26a704c1f9..793045d86c 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/mod.rs @@ -56,7 +56,6 @@ pub struct Tty { this: Weak, terminal: Arc, ldisc: Mutex>, - cursor_position_request_match: Mutex, writer: W, is_ptm: bool, } @@ -70,7 +69,6 @@ impl Tty { this: this.clone(), terminal, ldisc, - cursor_position_request_match: Mutex::new(0), writer, is_ptm, }) @@ -116,10 +114,7 @@ impl DeviceOps for Tty { if self.is_ptm { self.writer.write(buf); } else { - let (output, response_count) = { - let mut match_len = self.cursor_position_request_match.lock(); - filter_cursor_position_requests(&mut match_len, buf) - }; + let (output, response_count) = filter_cursor_position_requests(buf); let term = self.terminal.load_termios(); write_output_bytes(&self.writer, term.as_ref(), &output); if response_count > 0 { @@ -258,29 +253,21 @@ impl DeviceOps for Tty { } } -fn filter_cursor_position_requests(match_len: &mut usize, bytes: &[u8]) -> (Vec, usize) { +fn filter_cursor_position_requests(bytes: &[u8]) -> (Vec, usize) { let mut output = Vec::with_capacity(bytes.len()); let mut count = 0; - for &byte in bytes { - loop { - if byte == ANSI_CURSOR_POSITION_REQUEST[*match_len] { - *match_len += 1; - if *match_len == ANSI_CURSOR_POSITION_REQUEST.len() { - *match_len = 0; - count += 1; - } - break; - } - if *match_len > 0 { - output.extend_from_slice(&ANSI_CURSOR_POSITION_REQUEST[..*match_len]); - *match_len = 0; - continue; - } else { - output.push(byte); - break; - } - } + let mut rest = bytes; + + while let Some(pos) = rest + .windows(ANSI_CURSOR_POSITION_REQUEST.len()) + .position(|window| window == ANSI_CURSOR_POSITION_REQUEST) + { + output.extend_from_slice(&rest[..pos]); + count += 1; + rest = &rest[pos + ANSI_CURSOR_POSITION_REQUEST.len()..]; } + + output.extend_from_slice(rest); (output, count) } @@ -331,50 +318,47 @@ mod tests { use super::filter_cursor_position_requests; #[test] - fn cursor_position_request_matcher_spans_writes() { - let mut match_len = 0; - + fn cursor_position_request_matcher_does_not_buffer_partial_writes() { assert_eq!( - filter_cursor_position_requests(&mut match_len, b"\x1b["), - (Vec::new(), 0) + filter_cursor_position_requests(b"\x1b["), + (b"\x1b[".to_vec(), 0) ); - assert_eq!( - filter_cursor_position_requests(&mut match_len, b"6"), - (Vec::new(), 0) - ); - assert_eq!( - filter_cursor_position_requests(&mut match_len, b"n"), - (Vec::new(), 1) - ); - assert_eq!(match_len, 0); + assert_eq!(filter_cursor_position_requests(b"6"), (b"6".to_vec(), 0)); + assert_eq!(filter_cursor_position_requests(b"n"), (b"n".to_vec(), 0)); } #[test] fn cursor_position_request_matcher_recovers_after_partial_mismatch() { - let mut match_len = 0; - assert_eq!( - filter_cursor_position_requests(&mut match_len, b"\x1bX"), + filter_cursor_position_requests(b"\x1bX"), (b"\x1bX".to_vec(), 0) ); - assert_eq!(match_len, 0); + assert_eq!(filter_cursor_position_requests(b"\x1b[6n"), (Vec::new(), 1)); assert_eq!( - filter_cursor_position_requests(&mut match_len, b"\x1b[6n"), - (Vec::new(), 1) - ); - assert_eq!( - filter_cursor_position_requests(&mut match_len, b"\x1b[6n\x1b[6n"), + filter_cursor_position_requests(b"\x1b[6n\x1b[6n"), (Vec::new(), 2) ); } #[test] fn cursor_position_request_filter_preserves_other_output() { - let mut match_len = 0; - assert_eq!( - filter_cursor_position_requests(&mut match_len, b"ab\x1b[6ncd"), + filter_cursor_position_requests(b"ab\x1b[6ncd"), (b"abcd".to_vec(), 1) ); } + + #[test] + fn cursor_position_request_filter_flushes_unmatched_prefix() { + assert_eq!( + filter_cursor_position_requests(b"\x1b[31mred"), + (b"\x1b[31mred".to_vec(), 0) + ); + + assert_eq!( + filter_cursor_position_requests(b"\x1b["), + (b"\x1b[".to_vec(), 0) + ); + assert_eq!(filter_cursor_position_requests(b"A"), (b"A".to_vec(), 0)); + } } diff --git a/platforms/somehal/src/boot_console.rs b/platforms/somehal/src/boot_console.rs index 6273dd69ed..77f0d6b12f 100644 --- a/platforms/somehal/src/boot_console.rs +++ b/platforms/somehal/src/boot_console.rs @@ -34,24 +34,17 @@ fn device_id_from_bootargs_with( serial_device_id: impl Fn(usize) -> Option, ) -> Result { let cmdline = cmdline.ok_or(ConsoleDeviceIdError::NotSpecified)?; - let mut saw_supported_console = false; - let mut saw_hardware_console = false; - let mut last_hardware_device_id = None; + let mut last_spec = None; for spec in console_specs(cmdline) { - saw_supported_console = true; - if let ConsoleSpec::HardwareSerial(index) = spec { - saw_hardware_console = true; - if let Some(device_id) = serial_device_id(index) { - last_hardware_device_id = Some(device_id); - } - } + last_spec = Some(spec); } - match last_hardware_device_id { - Some(device_id) => Ok(device_id), - None if saw_hardware_console => Err(ConsoleDeviceIdError::DeviceNotFound), - None if saw_supported_console => Err(ConsoleDeviceIdError::NoHardwareDevice), + match last_spec { + Some(ConsoleSpec::HardwareSerial(index)) => { + serial_device_id(index).ok_or(ConsoleDeviceIdError::DeviceNotFound) + } + Some(ConsoleSpec::VirtualTty) => Err(ConsoleDeviceIdError::NoHardwareDevice), None => Err(ConsoleDeviceIdError::NotSpecified), } } @@ -186,19 +179,19 @@ mod tests { ); assert_eq!( device_id_from_bootargs(Some("console=ttyS2 console=tty1")), - Err(ConsoleDeviceIdError::DeviceNotFound) + Err(ConsoleDeviceIdError::NoHardwareDevice) ); } #[test] - fn virtual_console_does_not_clear_earlier_serial_console() { + fn later_virtual_console_overrides_earlier_serial_console() { let serial2_device = DeviceId::from(42); assert_eq!( device_id_from_bootargs_with(Some("console=ttyS2,1500000 console=tty1"), |index| { (index == 2).then_some(serial2_device) }), - Ok(serial2_device) + Err(ConsoleDeviceIdError::NoHardwareDevice) ); } @@ -216,7 +209,7 @@ mod tests { } #[test] - fn later_missing_hardware_console_does_not_clear_earlier_available_serial_console() { + fn later_missing_hardware_console_overrides_earlier_available_serial_console() { let serial2_device = DeviceId::from(42); assert_eq!( @@ -224,7 +217,14 @@ mod tests { Some("console=ttyS2,1500000 console=ttyS3,115200 console=tty1"), |index| (index == 2).then_some(serial2_device), ), - Ok(serial2_device) + Err(ConsoleDeviceIdError::NoHardwareDevice) + ); + assert_eq!( + device_id_from_bootargs_with( + Some("console=ttyS2,1500000 console=ttyS3,115200"), + |index| (index == 2).then_some(serial2_device), + ), + Err(ConsoleDeviceIdError::DeviceNotFound) ); } From 8acb0ae4b26121d94e92eb2edfe1ff9a8471afd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 09:49:56 +0800 Subject: [PATCH 12/16] refactor(serial): simplify irq runtime api --- drivers/ax-driver/src/serial/mod.rs | 69 ++----- drivers/ax-driver/src/serial/ns16550.rs | 15 +- drivers/ax-driver/src/serial/pl011.rs | 4 +- drivers/ax-driver/src/serial/rockchip_fiq.rs | 4 +- drivers/ax-driver/src/serial/runtime.rs | 185 +++++++----------- drivers/interface/rdif-serial/src/core.rs | 63 +++--- drivers/interface/rdif-serial/src/raw.rs | 103 ++++++++++ drivers/serial/some-serial/src/ns16550/mod.rs | 4 +- drivers/serial/some-serial/src/pl011.rs | 4 +- .../kernel/src/pseudofs/dev/tty/serial.rs | 26 ++- 10 files changed, 248 insertions(+), 229 deletions(-) diff --git a/drivers/ax-driver/src/serial/mod.rs b/drivers/ax-driver/src/serial/mod.rs index 098b1f5cc4..6ce236099a 100644 --- a/drivers/ax-driver/src/serial/mod.rs +++ b/drivers/ax-driver/src/serial/mod.rs @@ -13,14 +13,14 @@ mod pl011; mod rockchip_fiq; mod runtime; -pub use runtime::{BInterruptSerial, InterruptSerial, KernelSerialPort}; +pub use runtime::SerialPort; use crate::{BindingInfo, binding_info_from_acpi, binding_info_from_fdt}; struct PlatformSerialDevice { name: String, info: SerialDeviceInfo, - interface: Option, + port: Option, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -38,23 +38,15 @@ pub struct SerialDevice { name: String, rdrive_device_id: DeviceId, info: SerialDeviceInfo, - interface: BInterruptSerial, -} - -pub struct SerialRuntimePort { - name: String, - rdrive_device_id: DeviceId, - info: SerialDeviceInfo, - irq_num: usize, - port: BInterruptSerial, + port: SerialPort, } impl PlatformSerialDevice { - fn new(name: String, info: SerialDeviceInfo, interface: BInterruptSerial) -> Self { + fn new(name: String, info: SerialDeviceInfo, port: SerialPort) -> Self { Self { name, info, - interface: Some(interface), + port: Some(port), } } } @@ -95,7 +87,7 @@ impl SerialDevice { } pub fn baudrate(&self) -> u32 { - self.interface.baudrate() + self.port.baudrate() } pub fn irq_num(&self) -> Option { @@ -103,52 +95,15 @@ impl SerialDevice { } pub fn set_config(&self, config: &Config) -> Result<(), ConfigError> { - self.interface.set_config(config) + self.port.set_config(config) } pub fn set_baudrate(&self, baudrate: u32) -> Result<(), ConfigError> { - self.interface.set_config(&Config::new().baudrate(baudrate)) - } - - pub fn into_runtime_port(self) -> Result { - let irq_num = self.info.irq_num.ok_or(AxError::Unsupported)?; - Ok(SerialRuntimePort { - name: self.name, - rdrive_device_id: self.rdrive_device_id, - info: self.info, - irq_num, - port: self.interface, - }) - } -} - -impl SerialRuntimePort { - pub fn name(&self) -> &str { - &self.name - } - - pub fn info(&self) -> &SerialDeviceInfo { - &self.info - } - - pub fn rdrive_device_id(&self) -> DeviceId { - self.rdrive_device_id - } - - pub fn fdt_path(&self) -> &str { - &self.info.fdt_path - } - - pub fn alias_index(&self) -> Option { - self.info.alias_index - } - - pub fn irq_num(&self) -> Option { - Some(self.irq_num) + self.port.set_config(&Config::new().baudrate(baudrate)) } - pub fn port(&self) -> BInterruptSerial { - self.port.clone() + pub fn into_port(self) -> SerialPort { + self.port } } @@ -160,12 +115,12 @@ impl TryFrom> for SerialDevice { let mut dev = base.lock().map_err(|_| AxError::BadState)?; let name = dev.name.clone(); let info = dev.info.clone(); - let interface = dev.interface.take().ok_or(AxError::BadState)?; + let port = dev.port.take().ok_or(AxError::BadState)?; Ok(Self { name, rdrive_device_id, info, - interface, + port, }) } } diff --git a/drivers/ax-driver/src/serial/ns16550.rs b/drivers/ax-driver/src/serial/ns16550.rs index a4c23ad81b..60d25374f4 100644 --- a/drivers/ax-driver/src/serial/ns16550.rs +++ b/drivers/ax-driver/src/serial/ns16550.rs @@ -11,8 +11,7 @@ use rdrive::{ use some_serial::ns16550 as serial_ns16550; use super::{ - BInterruptSerial, KernelSerialPort, PlatformSerialDevice, acpi_serial_device_info, prop_u32, - serial_device_info, + PlatformSerialDevice, SerialPort, acpi_serial_device_info, prop_u32, serial_device_info, }; const ACPI_NS16550_CLOCK: u32 = 1_843_200; @@ -60,21 +59,21 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { let reg_width = prop_u32(node, "reg-io-width").unwrap_or(1) as usize; let reg_shift = prop_u32(node, "reg-shift").map(|shift| 1usize << shift); let ns16550_width = reg_shift.unwrap_or(reg_width); - let mut serial: Option = None; + let mut serial: Option = None; for compatible in node.compatibles() { if compatible == "snps,dw-apb-uart" { let clock_freq = prop_u32(node, "clock-frequency") .unwrap_or(serial_ns16550::dw_apb::SG2002_UART_CLOCK); let raw = serial_ns16550::DwApbUart::new_raw(mmio_base, clock_freq); - serial = Some(KernelSerialPort::new_dyn(raw)); + serial = Some(SerialPort::new(raw)); break; } if matches!(compatible, "ns16550a" | "ns16550") { let clock_freq = prop_u32(node, "clock-frequency").unwrap_or(24_000_000); let raw = serial_ns16550::Ns16550::new_mmio(mmio_base, clock_freq, ns16550_width); - serial = Some(KernelSerialPort::new_dyn(raw)); + serial = Some(SerialPort::new(raw)); break; } } @@ -94,7 +93,7 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { } struct AcpiSerialResource { - serial: BInterruptSerial, + serial: SerialPort, paddr: usize, mapped_base: usize, } @@ -137,7 +136,7 @@ fn acpi_io_serial(info: &AcpiInfo<'_>) -> Result, OnP )) })?; let raw = serial_ns16550::Ns16550::new_port(port, ACPI_NS16550_CLOCK); - let serial = KernelSerialPort::new_dyn(raw); + let serial = SerialPort::new(raw); let mapped_base = serial.base_addr(); Ok(Some(AcpiSerialResource { serial, @@ -168,7 +167,7 @@ fn acpi_mmio_serial(info: &AcpiInfo<'_>) -> Result) -> Result<(), OnProbeError> { let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size as usize)?; let clock_freq = prop_u32(info.node.as_node(), "clock-frequency").unwrap_or(24_000_000); let raw = pl011::Pl011::new(mmio_base, clock_freq); - let serial = KernelSerialPort::new_dyn(raw); + let serial = SerialPort::new(raw); let base = serial.base_addr(); let baudrate = serial.baudrate(); let device_info = serial_device_info(&info, &base_reg, base, baudrate); diff --git a/drivers/ax-driver/src/serial/rockchip_fiq.rs b/drivers/ax-driver/src/serial/rockchip_fiq.rs index 6883e8b3c2..2848b6d927 100644 --- a/drivers/ax-driver/src/serial/rockchip_fiq.rs +++ b/drivers/ax-driver/src/serial/rockchip_fiq.rs @@ -8,7 +8,7 @@ use some_serial::ns16550::rockchip_fiq::{ RockchipFiqSerial, }; -use super::{KernelSerialPort, PlatformSerialDevice, SerialDeviceInfo, prop_u32}; +use super::{PlatformSerialDevice, SerialDeviceInfo, SerialPort, prop_u32}; use crate::BindingInfo; model_register!( @@ -39,7 +39,7 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { } let raw = RockchipFiqSerial::new(mmio_base, fdt_config.config); - let serial = KernelSerialPort::new_dyn(raw); + let serial = SerialPort::new(raw); let base = serial.base_addr(); info!( "Rockchip FIQ debugger UART@{base:#x} registered successfully, serial-id={}, baudrate={}, \ diff --git a/drivers/ax-driver/src/serial/runtime.rs b/drivers/ax-driver/src/serial/runtime.rs index e97408a0e0..407eab04f0 100644 --- a/drivers/ax-driver/src/serial/runtime.rs +++ b/drivers/ax-driver/src/serial/runtime.rs @@ -8,57 +8,25 @@ use rdif_serial::{ SerialIrqHandler, SerialIrqOutcome, SerialSoftWork, TSerialIrqHandler, TxQueue, }; -pub type BInterruptSerial = Arc; - -pub trait InterruptSerial: Send + Sync + 'static { - fn name(&self) -> &str; - fn base_addr(&self) -> usize; - fn owner_cpu(&self) -> usize; - - fn baudrate(&self) -> u32; - fn startup(&self, config: &Config) -> Result; - fn shutdown(&self) -> Result<(), IrqError>; - fn set_config(&self, config: &Config) -> Result<(), ConfigError>; - - fn try_write(&self, bytes: &[u8]) -> SerialWriteResult; - fn write_room(&self) -> usize; - fn chars_in_buffer(&self) -> usize; - fn tx_idle(&self) -> bool; - - fn drain_rx(&self, out: &mut [RxItem]) -> usize; - fn rx_pending(&self) -> bool; - - fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome; - fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome; - - fn counters(&self) -> SerialCounters; -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct SerialWriteResult { - pub accepted: usize, - pub outcome: SerialIrqOutcome, -} - -pub struct KernelSerialPort { +pub struct SerialPort { name: String, base_addr: usize, owner: OwnerId, tx: SpinNoIrq, rx: SpinNoIrq, - irq: Arc>, + irq: Arc, } -impl KernelSerialPort { - pub fn new(raw: T) -> Self { +impl SerialPort { + pub fn new(raw: impl RawUart) -> Self { Self::new_with_owner(raw, 0) } - pub fn new_with_owner(raw: T, owner_cpu: usize) -> Self { + pub fn new_with_owner(raw: impl RawUart, owner_cpu: usize) -> Self { let name = raw.name().into(); let base_addr = raw.base_addr(); let owner = OwnerId(owner_cpu); - let parts = SerialIrqHandler::split(raw, owner); + let parts: rdif_serial::SerialParts = SerialIrqHandler::split(raw, owner); Self { name, base_addr, @@ -69,134 +37,125 @@ impl KernelSerialPort { } } - pub fn new_dyn(raw: T) -> BInterruptSerial { - Arc::new(Self::new(raw)) - } - - fn run_on_owner(&self, op: F) -> Result - where - F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, - { - struct OwnerCall<'a, T: RawUart, F, R> { - port: &'a KernelSerialPort, - op: UnsafeCell>, - result: UnsafeCell>, - } - - unsafe fn thunk(arg: *mut ()) - where - T: RawUart, - F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, - { - let call = unsafe { &*(arg as *const OwnerCall<'_, T, F, R>) }; - let op = unsafe { &mut *call.op.get() } - .take() - .expect("serial owner call entered twice"); - let lease = unsafe { OwnerLease::new_unchecked(call.port.owner) }; - let result = op(&call.port.irq, lease); - unsafe { *call.result.get() = Some(result) }; - } - - let call = OwnerCall { - port: self, - op: UnsafeCell::new(Some(op)), - result: UnsafeCell::new(None), - }; - unsafe { - run_on_cpu_sync( - CpuId(self.owner.0), - thunk::, - (&call as *const OwnerCall<'_, T, F, R> as *mut ()).cast(), - )?; - } - Ok(unsafe { &mut *call.result.get() } - .take() - .expect("serial owner call did not complete")) - } - - fn owner_lease_for_cpu(&self, cpu: CpuId) -> Option> { - (cpu.0 == self.owner.0).then(|| unsafe { OwnerLease::new_unchecked(self.owner) }) - } -} - -impl InterruptSerial for KernelSerialPort { - fn name(&self) -> &str { + pub fn name(&self) -> &str { &self.name } - fn base_addr(&self) -> usize { + pub fn base_addr(&self) -> usize { self.base_addr } - fn owner_cpu(&self) -> usize { + pub fn owner_cpu(&self) -> usize { self.owner.0 } - fn baudrate(&self) -> u32 { + pub fn baudrate(&self) -> u32 { self.run_on_owner(|irq, lease| irq.baudrate(lease)) .unwrap_or(0) } - fn startup(&self, config: &Config) -> Result { + pub fn startup(&self, config: &Config) -> Result { self.run_on_owner(|irq, lease| irq.startup(lease, config)) .map_err(|_| ConfigError::RegisterError)? } - fn shutdown(&self) -> Result<(), IrqError> { + pub fn shutdown(&self) -> Result<(), IrqError> { self.run_on_owner(|irq, lease| irq.shutdown(lease)) } - fn set_config(&self, config: &Config) -> Result<(), ConfigError> { + pub fn set_config(&self, config: &Config) -> Result<(), ConfigError> { self.run_on_owner(|irq, lease| irq.set_config(lease, config)) .map_err(|_| ConfigError::RegisterError)? } - fn try_write(&self, bytes: &[u8]) -> SerialWriteResult { + pub fn submit_tx(&self, bytes: &[u8]) -> (usize, SerialIrqOutcome) { let submit = self.tx.lock().submit(bytes); - let mut outcome = SerialIrqOutcome::default(); - if submit.needs_kick { - outcome = self.service_on_owner(SerialSoftWork::TX_KICK); - } - SerialWriteResult { - accepted: submit.accepted, - outcome, - } + let outcome = if submit.needs_kick { + self.service_on_owner(SerialSoftWork::TX_KICK) + } else { + SerialIrqOutcome::default() + }; + (submit.accepted, outcome) } - fn write_room(&self) -> usize { + pub fn write_room(&self) -> usize { self.tx.lock().write_room() } - fn chars_in_buffer(&self) -> usize { + pub fn chars_in_buffer(&self) -> usize { self.tx.lock().chars_in_buffer() } - fn tx_idle(&self) -> bool { + pub fn tx_idle(&self) -> bool { self.run_on_owner(|irq, lease| irq.tx_idle(lease)) .unwrap_or(false) } - fn drain_rx(&self, out: &mut [RxItem]) -> usize { + pub fn drain_rx(&self, out: &mut [RxItem]) -> usize { self.rx.lock().drain(out) } - fn rx_pending(&self) -> bool { + pub fn rx_pending(&self) -> bool { self.rx.lock().rx_pending() } - fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome { + pub fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome { let Some(lease) = self.owner_lease_for_cpu(cpu) else { return SerialIrqOutcome::default(); }; self.irq.handle(lease) } - fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome { + pub fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome { self.run_on_owner(|irq, lease| irq.service(lease, work)) .unwrap_or_default() } - fn counters(&self) -> SerialCounters { + pub fn counters(&self) -> SerialCounters { self.irq.counters() } + + fn run_on_owner(&self, op: F) -> Result + where + F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, + { + struct OwnerCall<'a, F, R> { + port: &'a SerialPort, + op: UnsafeCell>, + result: UnsafeCell>, + } + + unsafe fn thunk(arg: *mut ()) + where + F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, + { + let call = unsafe { &*(arg as *const OwnerCall<'_, F, R>) }; + let op = unsafe { &mut *call.op.get() } + .take() + .expect("serial owner call entered twice"); + let lease = unsafe { OwnerLease::new_unchecked(call.port.owner) }; + let result = op(&call.port.irq, lease); + unsafe { *call.result.get() = Some(result) }; + } + + let call = OwnerCall { + port: self, + op: UnsafeCell::new(Some(op)), + result: UnsafeCell::new(None), + }; + unsafe { + run_on_cpu_sync( + CpuId(self.owner.0), + thunk::, + (&call as *const OwnerCall<'_, F, R> as *mut ()).cast(), + )?; + } + Ok(unsafe { &mut *call.result.get() } + .take() + .expect("serial owner call did not complete")) + } + + fn owner_lease_for_cpu(&self, cpu: CpuId) -> Option> { + (cpu.0 == self.owner.0).then(|| unsafe { OwnerLease::new_unchecked(self.owner) }) + } } diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index 5d05e48b4a..f0a9133398 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -1,4 +1,4 @@ -use alloc::sync::Arc; +use alloc::{boxed::Box, sync::Arc}; use core::{ cell::{Cell, UnsafeCell}, marker::PhantomData, @@ -265,30 +265,28 @@ pub trait TSerialIrqHandler: Send + Sync + 'static { fn service(&self, lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome; } -pub struct SerialParts< - T: RawUart, - const TX: usize = DEFAULT_TX_CAP, - const RX: usize = DEFAULT_RX_CAP, -> { +type DynRawUart = Box; + +pub struct SerialParts { pub tx: TxQueue, pub rx: RxQueue, - pub irq: Arc>, + pub irq: Arc>, } -pub struct SerialIrqHandler< - T: RawUart, - const TX: usize = DEFAULT_TX_CAP, - const RX: usize = DEFAULT_RX_CAP, -> { +pub struct SerialIrqHandler { owner: OwnerId, - core: Arc>>, + core: Arc>>, tx: Arc>, rx: Arc>, counters: Arc, } -impl SerialIrqHandler { - pub fn split(raw: T, owner: OwnerId) -> SerialParts { +impl SerialIrqHandler { + pub fn split(raw: impl RawUart, owner: OwnerId) -> SerialParts { + Self::split_boxed(Box::new(raw), owner) + } + + pub fn split_boxed(raw: Box, owner: OwnerId) -> SerialParts { let core = Arc::new(OwnerCell::new(CoreInner::new(raw))); let tx = Arc::new(TxState::new()); let rx = Arc::new(RxState::new()); @@ -383,7 +381,7 @@ impl SerialIrqHandler { assert_eq!(lease.owner(), self.owner); } - fn handle_locked(&self, core: &mut CoreInner) -> SerialIrqOutcome { + fn handle_locked(&self, core: &mut CoreInner) -> SerialIrqOutcome { let mut out = SerialIrqOutcome::default(); if core.state != PortState::Running { return out; @@ -439,7 +437,7 @@ impl SerialIrqHandler { fn service_soft_locked( &self, - core: &mut CoreInner, + core: &mut CoreInner, work: SerialSoftWork, ) -> SerialIrqOutcome { let mut out = SerialIrqOutcome::default(); @@ -458,7 +456,7 @@ impl SerialIrqHandler { out } - fn service_rx(&self, core: &mut CoreInner, budget: usize) -> RxService { + fn service_rx(&self, core: &mut CoreInner, budget: usize) -> RxService { let mut result = RxService::default(); for _ in 0..budget { let Some(sample) = core.raw.read_rx() else { @@ -516,7 +514,7 @@ impl SerialIrqHandler { fn service_tx( &self, - core: &mut CoreInner, + core: &mut CoreInner, budget: usize, out: &mut SerialIrqOutcome, ) -> usize { @@ -555,9 +553,7 @@ impl SerialIrqHandler { } } -impl TSerialIrqHandler - for SerialIrqHandler -{ +impl TSerialIrqHandler for SerialIrqHandler { fn owner(&self) -> OwnerId { self.owner } @@ -760,7 +756,7 @@ mod tests { #[test] fn tx_queue_only_submits_software_work() { - let parts = SerialIrqHandler::::split(MockUart::new(), OwnerId(0)); + let parts = SerialIrqHandler::<8, 8>::split(MockUart::new(), OwnerId(0)); let mut tx = parts.tx; let submit = tx.submit(b"abc"); @@ -770,11 +766,20 @@ mod tests { assert_eq!(tx.chars_in_buffer(), 3); } + #[test] + fn split_erases_raw_type_and_keeps_capacity_generics() { + let parts: SerialParts<8, 8> = SerialIrqHandler::split(MockUart::new(), OwnerId(0)); + + assert_eq!(parts.irq.owner(), OwnerId(0)); + assert_eq!(parts.tx.write_room(), 7); + assert!(!parts.rx.rx_pending()); + } + #[test] fn owner_service_flushes_tx_queue() { let mut uart = MockUart::new(); uart.tx_ready_budget = 3; - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; tx.submit(b"abc"); parts.irq.startup(lease(), &Config::new()).unwrap(); @@ -790,7 +795,7 @@ mod tests { fn tx_kick_wakes_again_when_queue_still_has_data() { let mut uart = MockUart::new(); uart.tx_ready_budget = 1; - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; tx.submit(b"abc"); parts.irq.startup(lease(), &Config::new()).unwrap(); @@ -807,7 +812,7 @@ mod tests { let mut uart = MockUart::new(); uart.tx_ready_budget = TX_KICK_BUDGET + 8; uart.tx_load_size = 1; - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<64, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; let data = [b'x'; TX_KICK_BUDGET + 8]; tx.submit(&data); @@ -826,7 +831,7 @@ mod tests { .irq(IrqSource::RX_DATA) .rx_byte(b'A') .rx_byte(b'B'); - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); parts.irq.startup(lease(), &Config::new()).unwrap(); let outcome = parts.irq.handle(lease()); @@ -858,7 +863,7 @@ mod tests { for &byte in burst { uart = uart.rx_byte(byte); } - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<8, 128>::split(uart, OwnerId(0)); parts.irq.startup(lease(), &Config::new()).unwrap(); let outcome = parts.irq.handle(lease()); @@ -887,7 +892,7 @@ mod tests { flag: RxFlag::Parity, overrun: true, }); - let parts = SerialIrqHandler::::split(uart, OwnerId(0)); + let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); parts.irq.startup(lease(), &Config::new()).unwrap(); let outcome = parts.irq.handle(lease()); diff --git a/drivers/interface/rdif-serial/src/raw.rs b/drivers/interface/rdif-serial/src/raw.rs index 5e94411a85..af4077539d 100644 --- a/drivers/interface/rdif-serial/src/raw.rs +++ b/drivers/interface/rdif-serial/src/raw.rs @@ -1,3 +1,4 @@ +use alloc::boxed::Box; use core::{any::Any, num::NonZeroU32}; use crate::{ @@ -94,3 +95,105 @@ pub trait RawUart: Send + Any + 'static { fn ack_modem_status(&mut self) {} fn ack_busy_detect(&mut self) {} } + +impl RawUart for Box { + fn name(&self) -> &'static str { + self.as_ref().name() + } + + fn base_addr(&self) -> usize { + self.as_ref().base_addr() + } + + fn clock_freq(&self) -> Option { + self.as_ref().clock_freq() + } + + fn startup(&mut self, config: &Config) -> Result<(), ConfigError> { + self.as_mut().startup(config) + } + + fn shutdown(&mut self) { + self.as_mut().shutdown(); + } + + fn set_config(&mut self, config: &Config) -> Result<(), ConfigError> { + self.as_mut().set_config(config) + } + + fn baudrate(&self) -> u32 { + self.as_ref().baudrate() + } + + fn data_bits(&self) -> crate::DataBits { + self.as_ref().data_bits() + } + + fn stop_bits(&self) -> crate::StopBits { + self.as_ref().stop_bits() + } + + fn parity(&self) -> crate::Parity { + self.as_ref().parity() + } + + fn enable_loopback(&mut self) { + self.as_mut().enable_loopback(); + } + + fn disable_loopback(&mut self) { + self.as_mut().disable_loopback(); + } + + fn is_loopback_enabled(&self) -> bool { + self.as_ref().is_loopback_enabled() + } + + fn set_irq_mask(&mut self, mask: InterruptMask) { + self.as_mut().set_irq_mask(mask); + } + + fn take_irq_snapshot(&mut self) -> IrqSnapshot { + self.as_mut().take_irq_snapshot() + } + + fn read_rx(&mut self) -> Option { + self.as_mut().read_rx() + } + + fn tx_ready(&mut self) -> bool { + self.as_mut().tx_ready() + } + + fn write_tx(&mut self, byte: u8) { + self.as_mut().write_tx(byte); + } + + fn poll_status(&mut self) -> SerialEvent { + self.as_mut().poll_status() + } + + fn write_byte(&mut self, byte: u8) { + self.as_mut().write_byte(byte); + } + + fn read_byte(&mut self, status: SerialEvent) -> Option> { + self.as_mut().read_byte(status) + } + + fn tx_load_size(&self) -> usize { + self.as_ref().tx_load_size() + } + + fn tx_idle(&mut self) -> bool { + self.as_mut().tx_idle() + } + + fn ack_modem_status(&mut self) { + self.as_mut().ack_modem_status(); + } + + fn ack_busy_detect(&mut self) { + self.as_mut().ack_busy_detect(); + } +} diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index 9428f857b4..dce8865b13 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -821,8 +821,8 @@ mod tests { unsafe { OwnerLease::new_unchecked(OwnerId(0)) } } - fn started_parts(uart: Ns16550) -> SerialParts, 64, 64> { - let parts = SerialIrqHandler::<_, 64, 64>::split(uart, OwnerId(0)); + fn started_parts(uart: Ns16550) -> SerialParts<64, 64> { + let parts = SerialIrqHandler::<64, 64>::split(uart, OwnerId(0)); parts.irq.startup(owner_lease(), &Config::new()).unwrap(); parts } diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index 8e2fbee4ca..a039e14fd3 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -906,8 +906,8 @@ mod tests { unsafe { OwnerLease::new_unchecked(OwnerId(0)) } } - fn started_parts(uart: Pl011) -> SerialParts { - let parts = SerialIrqHandler::<_, 64, 64>::split(uart, OwnerId(0)); + fn started_parts(uart: Pl011) -> SerialParts<64, 64> { + let parts = SerialIrqHandler::<64, 64>::split(uart, OwnerId(0)); parts.irq.startup(owner_lease(), &Config::new()).unwrap(); parts } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index 5134015924..64f7aaa0ab 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -5,7 +5,7 @@ use core::{ }; use ax_driver::serial::{ - self as ax_serial, BInterruptSerial, Config, RxFlag, RxItem, SerialDevice, SerialIrqOutcome, + self as ax_serial, Config, RxFlag, RxItem, SerialDevice, SerialIrqOutcome, SerialPort, SerialSoftWork, }; use ax_errno::{AxError, AxResult}; @@ -73,7 +73,7 @@ struct SerialBackend { tty_name: String, rdrive_device_id: RDriveDeviceId, number: usize, - port: BInterruptSerial, + port: SerialPort, irq_num: usize, irq_handle: SpinNoIrq>, started: AtomicBool, @@ -278,14 +278,13 @@ impl SerialRegistry { fn new_serial_tty(number: usize, serial: SerialDevice) -> AxResult { let tty_name = format!("ttyS{number}"); - let runtime = serial.into_runtime_port()?; - let name = runtime.name().into(); - let info = runtime.info().clone(); - let rdrive_device_id = runtime.rdrive_device_id(); - let Some(irq_num) = runtime.irq_num() else { + let name = serial.name().into(); + let info = serial.info().clone(); + let rdrive_device_id = serial.rdrive_device_id(); + let Some(irq_num) = serial.irq_num() else { return Err(AxError::Unsupported); }; - let port = runtime.port(); + let port = serial.into_port(); let backend = Arc::new(SerialBackend { name, tty_name: tty_name.clone(), @@ -539,9 +538,8 @@ impl TtyWrite for SerialWriter { let _guard = self.backend.output_lock.lock(); let mut written = 0; while written < buf.len() { - let result = self.backend.port.try_write(&buf[written..]); - publish_serial_outcome(&self.backend, result.outcome, false); - let count = result.accepted; + let (count, outcome) = self.backend.port.submit_tx(&buf[written..]); + publish_serial_outcome(&self.backend, outcome, false); if count == 0 { self.backend.tx_notify.wait(); continue; @@ -560,9 +558,9 @@ impl TtyWrite for SerialWriter { let Some(_guard) = self.backend.output_lock.try_lock() else { return 0; }; - let result = self.backend.port.try_write(buf); - publish_serial_outcome(&self.backend, result.outcome, false); - result.accepted + let (count, outcome) = self.backend.port.submit_tx(buf); + publish_serial_outcome(&self.backend, outcome, false); + count } fn flush_echo_before_input(&self) -> bool { From 8f8a5adf30d7489553da36c120944a968781d2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 10:57:19 +0800 Subject: [PATCH 13/16] refactor(serial): own irq handler in boxed callbacks --- components/axklib/src/lib.rs | 8 +- components/irq-framework/src/action.rs | 41 +++- components/irq-framework/src/lib.rs | 6 +- components/irq-framework/src/registry.rs | 23 +- components/irq-framework/src/types.rs | 47 +++- components/irq-framework/tests/std_sim.rs | 86 +++++++ drivers/ax-driver/Cargo.toml | 9 +- drivers/ax-driver/src/serial/mod.rs | 138 +++++++----- drivers/ax-driver/src/serial/ns16550.rs | 44 ++-- drivers/ax-driver/src/serial/pl011.rs | 12 +- drivers/ax-driver/src/serial/rockchip_fiq.rs | 12 +- drivers/ax-driver/src/serial/runtime.rs | 161 -------------- drivers/interface/rdif-serial/src/core.rs | 209 ++++++++++++++---- drivers/interface/rdif-serial/src/lib.rs | 17 +- drivers/serial/some-serial/src/ns16550/mod.rs | 22 +- drivers/serial/some-serial/src/pl011.rs | 8 +- .../kernel/src/pseudofs/dev/tty/serial.rs | 153 ++++++++----- os/arceos/modules/axhal/src/irq.rs | 10 +- platforms/ax-plat/src/irq.rs | 22 +- 19 files changed, 609 insertions(+), 419 deletions(-) delete mode 100644 drivers/ax-driver/src/serial/runtime.rs diff --git a/components/axklib/src/lib.rs b/components/axklib/src/lib.rs index 4080330aff..0f7aeb995e 100644 --- a/components/axklib/src/lib.rs +++ b/components/axklib/src/lib.rs @@ -45,9 +45,9 @@ use core::{ptr::NonNull, time::Duration}; pub use ax_errno::{AxError, AxResult}; pub use ax_memory_addr::{PhysAddr, VirtAddr}; pub use irq_framework::{ - AutoEnable as IrqAutoEnable, CpuId as IrqCpuId, CpuMask as IrqCpuMask, IrqAffinity, IrqContext, - IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, - IrqStatus, RawIrqHandler, ShareMode as IrqShareMode, + AutoEnable as IrqAutoEnable, BoxedIrqHandler, CpuId as IrqCpuId, CpuMask as IrqCpuMask, + IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, + IrqReturn, IrqScope, IrqStatus, RawIrqHandler, ShareMode as IrqShareMode, }; use trait_ffi::*; @@ -195,7 +195,7 @@ pub mod time { /// Convenience re-exports for IRQ operations. pub mod irq { pub use super::{ - IrqAffinity, IrqAutoEnable as AutoEnable, IrqContext, IrqCpuId as CpuId, + BoxedIrqHandler, IrqAffinity, IrqAutoEnable as AutoEnable, IrqContext, IrqCpuId as CpuId, IrqCpuMask as CpuMask, IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqShareMode as ShareMode, IrqStatus, RawIrqHandler, klib::{ diff --git a/components/irq-framework/src/action.rs b/components/irq-framework/src/action.rs index c85f7638bb..167c276ef6 100644 --- a/components/irq-framework/src/action.rs +++ b/components/irq-framework/src/action.rs @@ -1,15 +1,24 @@ -use core::{ - cell::UnsafeCell, - ptr::{self, NonNull}, - sync::atomic::AtomicBool, +use core::{cell::UnsafeCell, ptr, sync::atomic::AtomicBool}; + +use crate::{ + AutoEnable, BoxedIrqHandler, CpuId, CpuMask, IrqContext, IrqExecution, IrqRequest, IrqReturn, + IrqScope, types::IrqHandler, }; -use crate::{AutoEnable, CpuId, CpuMask, IrqExecution, IrqRequest, IrqScope, RawIrqHandler}; +pub(crate) enum ActionHandler { + Raw { + handler: unsafe fn(IrqContext, core::ptr::NonNull<()>) -> IrqReturn, + data: core::ptr::NonNull<()>, + }, + Boxed(UnsafeCell), +} + +unsafe impl Send for ActionHandler {} +unsafe impl Sync for ActionHandler {} pub(crate) struct Action { pub(crate) id: u64, - pub(crate) handler: RawIrqHandler, - pub(crate) data: NonNull<()>, + pub(crate) handler: ActionHandler, pub(crate) scope: IrqScope, pub(crate) execution: IrqExecution, pub(crate) enabled: AtomicBool, @@ -19,17 +28,25 @@ pub(crate) struct Action { pub(crate) next: *mut Action, } -// Raw handler context pointers are owned by the OS adapter. The framework only -// stores and passes them back to the registered handler. +// Raw handler context pointers and boxed callbacks are owned by the registered +// action. Boxed callbacks are only called after the NonReentrant run guard +// succeeds, so the UnsafeCell is not mutably aliased by framework dispatch. unsafe impl Send for Action {} unsafe impl Sync for Action {} impl Action { - pub(crate) fn new(id: u64, request: &IrqRequest) -> Self { + pub(crate) fn new(id: u64, request: &mut IrqRequest) -> Self { + let handler = match request + .handler + .take() + .expect("IRQ handler was already consumed") + { + IrqHandler::Raw { handler, data } => ActionHandler::Raw { handler, data }, + IrqHandler::Boxed(handler) => ActionHandler::Boxed(UnsafeCell::new(handler)), + }; Self { id, - handler: request.handler, - data: request.data, + handler, scope: request.scope, execution: request.execution, enabled: AtomicBool::new(request.auto_enable == AutoEnable::Yes), diff --git a/components/irq-framework/src/lib.rs b/components/irq-framework/src/lib.rs index f41ea5c540..45dc0e1326 100644 --- a/components/irq-framework/src/lib.rs +++ b/components/irq-framework/src/lib.rs @@ -10,7 +10,7 @@ mod types; pub use registry::Registry; pub use types::{ - AutoEnable, CpuId, CpuMask, CpuMaskIter, IrqAffinity, IrqContext, IrqError, IrqExecution, - IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, - RawIrqHandler, ShareMode, + AutoEnable, BoxedIrqHandler, CpuId, CpuMask, CpuMaskIter, IrqAffinity, IrqContext, IrqError, + IrqExecution, IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, + IrqStatus, RawIrqHandler, ShareMode, }; diff --git a/components/irq-framework/src/registry.rs b/components/irq-framework/src/registry.rs index ec03980f14..5f56f7f8b2 100644 --- a/components/irq-framework/src/registry.rs +++ b/components/irq-framework/src/registry.rs @@ -8,7 +8,7 @@ use core::{ use crate::{ CpuId, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, - action::Action, + action::{Action, ActionHandler}, descriptor::{Descriptor, action_matches_cpu, recompute_scope_line_desired}, lock::MetadataLock, }; @@ -48,12 +48,12 @@ impl Registry { } /// Registers an IRQ action. - pub fn request(&self, irq: IrqNumber, request: IrqRequest) -> Result { + pub fn request(&self, irq: IrqNumber, mut request: IrqRequest) -> Result { self.validate_request(&request)?; let snapshot = self.snapshot_and_disable_scope_line(irq, request.scope)?; let id = self.next_id.fetch_add(1, Ordering::Relaxed); - let action = Box::new(Action::new(id, &request)); + let action = Box::new(Action::new(id, &mut request)); let action = Box::into_raw(action); let irq_state = self.lock.lock(&self.ops); let result = self.insert_action_locked(irq, &request, action); @@ -231,7 +231,7 @@ impl Registry { }; outcome.called += 1; - match unsafe { (action.handler)(ctx, action.data) } { + match action.call(ctx) { IrqReturn::Unhandled => {} IrqReturn::Handled => outcome.handled = true, IrqReturn::Wake => { @@ -266,6 +266,9 @@ impl Registry { } fn validate_request(&self, request: &IrqRequest) -> Result<(), IrqError> { + if request.is_boxed() && request.execution == IrqExecution::Concurrent { + return Err(IrqError::Busy); + } if let IrqScope::PerCpu { cpus } = request.scope && cpus.is_empty() { @@ -741,6 +744,18 @@ impl Registry { } } +impl Action { + fn call(&self, ctx: IrqContext) -> IrqReturn { + match &self.handler { + ActionHandler::Raw { handler, data } => unsafe { handler(ctx, *data) }, + ActionHandler::Boxed(handler) => { + let handler = unsafe { &mut *handler.get() }; + handler(ctx) + } + } + } +} + struct LineStateSnapshot { global: bool, percpu: Vec<(CpuId, bool)>, diff --git a/components/irq-framework/src/types.rs b/components/irq-framework/src/types.rs index 32d8426c8f..81910c5b3f 100644 --- a/components/irq-framework/src/types.rs +++ b/components/irq-framework/src/types.rs @@ -1,3 +1,4 @@ +use alloc::boxed::Box; use core::ptr::NonNull; /// A platform IRQ number. @@ -207,6 +208,17 @@ pub struct IrqContext { /// Raw IRQ handler ABI. pub type RawIrqHandler = unsafe fn(ctx: IrqContext, data: NonNull<()>) -> IrqReturn; +/// Boxed IRQ handler ABI. +pub type BoxedIrqHandler = Box IrqReturn + Send + 'static>; + +pub(crate) enum IrqHandler { + Raw { + handler: RawIrqHandler, + data: NonNull<()>, + }, + Boxed(BoxedIrqHandler), +} + /// External capabilities supplied by the OS/platform adapter. pub trait IrqOps { /// Saved local IRQ state. @@ -262,10 +274,8 @@ pub trait IrqOps { } /// Request parameters for an IRQ action. -#[derive(Clone, Copy, Debug)] pub struct IrqRequest { - pub(crate) handler: RawIrqHandler, - pub(crate) data: NonNull<()>, + pub(crate) handler: Option, pub(crate) scope: IrqScope, pub(crate) affinity: IrqAffinity, pub(crate) execution: IrqExecution, @@ -275,10 +285,9 @@ pub struct IrqRequest { impl IrqRequest { /// Creates a new exclusive, global, auto-enabled IRQ request. - pub const fn new(handler: RawIrqHandler, data: NonNull<()>) -> Self { + pub fn new(handler: RawIrqHandler, data: NonNull<()>) -> Self { Self { - handler, - data, + handler: Some(IrqHandler::Raw { handler, data }), scope: IrqScope::Global, affinity: IrqAffinity::Any, execution: IrqExecution::Concurrent, @@ -287,32 +296,48 @@ impl IrqRequest { } } + /// Creates a new exclusive, global, auto-enabled boxed IRQ request. + pub fn new_boxed(handler: BoxedIrqHandler) -> Self { + Self { + handler: Some(IrqHandler::Boxed(handler)), + scope: IrqScope::Global, + affinity: IrqAffinity::Any, + execution: IrqExecution::NonReentrant, + share_mode: ShareMode::Exclusive, + auto_enable: AutoEnable::Yes, + } + } + + pub(crate) fn is_boxed(&self) -> bool { + matches!(self.handler.as_ref(), Some(IrqHandler::Boxed(_))) + } + /// Sets the IRQ scope. - pub const fn scope(mut self, scope: IrqScope) -> Self { + pub fn scope(mut self, scope: IrqScope) -> Self { self.scope = scope; self } /// Sets the IRQ affinity. - pub const fn affinity(mut self, affinity: IrqAffinity) -> Self { + pub fn affinity(mut self, affinity: IrqAffinity) -> Self { self.affinity = affinity; self } /// Sets the action execution contract. - pub const fn execution(mut self, execution: IrqExecution) -> Self { + pub fn execution(mut self, execution: IrqExecution) -> Self { self.execution = execution; self } /// Sets the sharing mode. - pub const fn share_mode(mut self, share_mode: ShareMode) -> Self { + pub fn share_mode(mut self, share_mode: ShareMode) -> Self { self.share_mode = share_mode; self } /// Sets whether the action should be enabled after request. - pub const fn auto_enable(mut self, auto_enable: AutoEnable) -> Self { + pub fn auto_enable(mut self, auto_enable: AutoEnable) -> Self { self.auto_enable = auto_enable; self } diff --git a/components/irq-framework/tests/std_sim.rs b/components/irq-framework/tests/std_sim.rs index c34e3c041c..b671318f54 100644 --- a/components/irq-framework/tests/std_sim.rs +++ b/components/irq-framework/tests/std_sim.rs @@ -358,6 +358,92 @@ fn irq_request_exposes_auto_enable_mode() { .auto_enable_mode(), AutoEnable::No ); + assert_eq!( + IrqRequest::new_boxed(Box::new(|_| IrqReturn::Handled)).auto_enable_mode(), + AutoEnable::Yes + ); +} + +#[test] +fn boxed_callback_persists_captured_state() { + let registry = Registry::new(MockOps::with_cpus(1)); + let calls = Arc::new(AtomicUsize::new(0)); + let callback_calls = calls.clone(); + + registry + .request( + IrqNumber(46), + IrqRequest::new_boxed(Box::new(move |ctx| { + assert_eq!(ctx.irq, IrqNumber(46)); + callback_calls.fetch_add(1, Ordering::SeqCst); + IrqReturn::Wake + })), + ) + .unwrap(); + + let first = registry.dispatch(IrqNumber(46), CpuId(0)); + let second = registry.dispatch(IrqNumber(46), CpuId(0)); + + assert!(first.handled); + assert!(first.wake); + assert_eq!(first.called, 1); + assert!(second.handled); + assert!(second.wake); + assert_eq!(second.called, 1); + assert_eq!(calls.load(Ordering::SeqCst), 2); +} + +#[test] +fn boxed_callback_rejects_concurrent_execution() { + let registry = Registry::new(MockOps::with_cpus(1)); + + let err = registry + .request( + IrqNumber(47), + IrqRequest::new_boxed(Box::new(|_| IrqReturn::Handled)) + .execution(IrqExecution::Concurrent), + ) + .unwrap_err(); + + assert_eq!(err, IrqError::Busy); +} + +#[test] +fn boxed_callback_is_non_reentrant() { + let registry = Arc::new(Registry::new(MockOps::with_cpus(1))); + let entered = Arc::new(Barrier::new(2)); + let release = Arc::new(Barrier::new(2)); + let calls = Arc::new(AtomicUsize::new(0)); + let callback_entered = entered.clone(); + let callback_release = release.clone(); + let callback_calls = calls.clone(); + + registry + .request( + IrqNumber(48), + IrqRequest::new_boxed(Box::new(move |_| { + callback_calls.fetch_add(1, Ordering::SeqCst); + callback_entered.wait(); + callback_release.wait(); + IrqReturn::Handled + })), + ) + .unwrap(); + + let dispatch_registry = registry.clone(); + let dispatch_thread = + thread::spawn(move || dispatch_registry.dispatch(IrqNumber(48), CpuId(0))); + entered.wait(); + + let nested = registry.dispatch(IrqNumber(48), CpuId(0)); + assert!(!nested.handled); + assert_eq!(nested.called, 0); + assert_eq!(calls.load(Ordering::SeqCst), 1); + + release.wait(); + let outcome = dispatch_thread.join().unwrap(); + assert!(outcome.handled); + assert_eq!(outcome.called, 1); } #[test] diff --git a/drivers/ax-driver/Cargo.toml b/drivers/ax-driver/Cargo.toml index 8c2d02b669..79638b7fce 100644 --- a/drivers/ax-driver/Cargo.toml +++ b/drivers/ax-driver/Cargo.toml @@ -54,7 +54,14 @@ usb = ["dep:crab-usb"] rockchip-dwc-xhci = ["usb", "plat-dyn", "rockchip-soc", "rockchip-pm"] xhci-mmio = ["usb", "plat-dyn"] xhci-pci = ["usb", "pci"] -serial = ["plat-dyn", "dep:ax-errno", "dep:ax-kspin", "dep:rdif-serial", "dep:some-serial"] +serial = [ + "plat-dyn", + "dep:ax-errno", + "dep:ax-kernel-guard", + "dep:ax-kspin", + "dep:rdif-serial", + "dep:some-serial", +] sg2002-placeholder = ["plat-dyn"] rknpu = ["plat-dyn", "rockchip-pm", "rockchip-soc", "dep:rockchip-npu"] rga = ["plat-dyn", "dep:rockchip-rga"] diff --git a/drivers/ax-driver/src/serial/mod.rs b/drivers/ax-driver/src/serial/mod.rs index 6ce236099a..29348cb70d 100644 --- a/drivers/ax-driver/src/serial/mod.rs +++ b/drivers/ax-driver/src/serial/mod.rs @@ -1,26 +1,36 @@ use alloc::{string::String, vec::Vec}; +use core::cell::UnsafeCell; use ax_errno::AxError; +use ax_kernel_guard::IrqSave; +use axklib::irq::{CpuId, IrqError, run_on_cpu_sync}; use fdt_edit::{Fdt, RegFixed}; use log::warn; pub use rdif_serial::{ - Config, ConfigError, RxFlag, RxItem, SerialCounters, SerialIrqOutcome, SerialSoftWork, + Config, ConfigError, OwnerId, OwnerLease, RawUart, RxFlag, RxItem, RxQueue, SerialCounters, + SerialIrqHandler, SerialIrqOutcome, SerialParts, SerialPort, SerialSoftWork, TxQueue, }; use rdrive::{Device, DeviceId, DriverGeneric, probe::acpi::AcpiInfo, register::FdtInfo}; mod ns16550; mod pl011; mod rockchip_fiq; -mod runtime; - -pub use runtime::SerialPort; use crate::{BindingInfo, binding_info_from_acpi, binding_info_from_fdt}; +pub type SerialRuntime = SerialParts; + +struct SerialProbeRuntime { + name: &'static str, + base_addr: usize, + baudrate: u32, + runtime: SerialRuntime, +} + struct PlatformSerialDevice { name: String, info: SerialDeviceInfo, - port: Option, + runtime: Option, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -35,18 +45,18 @@ pub struct SerialDeviceInfo { } pub struct SerialDevice { - name: String, - rdrive_device_id: DeviceId, - info: SerialDeviceInfo, - port: SerialPort, + pub name: String, + pub rdrive_device_id: DeviceId, + pub info: SerialDeviceInfo, + pub runtime: SerialRuntime, } impl PlatformSerialDevice { - fn new(name: String, info: SerialDeviceInfo, port: SerialPort) -> Self { + fn new(name: String, info: SerialDeviceInfo, runtime: SerialRuntime) -> Self { Self { name, info, - port: Some(port), + runtime: Some(runtime), } } } @@ -57,53 +67,16 @@ impl DriverGeneric for PlatformSerialDevice { } } -impl SerialDevice { - pub fn name(&self) -> &str { - &self.name - } - - pub fn info(&self) -> &SerialDeviceInfo { - &self.info - } - - pub fn rdrive_device_id(&self) -> DeviceId { - self.rdrive_device_id - } - - pub fn fdt_path(&self) -> &str { - &self.info.fdt_path - } - - pub fn alias_index(&self) -> Option { - self.info.alias_index - } - - pub fn paddr(&self) -> usize { - self.info.paddr - } - - pub fn mapped_base(&self) -> usize { - self.info.mapped_base - } - - pub fn baudrate(&self) -> u32 { - self.port.baudrate() - } - - pub fn irq_num(&self) -> Option { - self.info.irq_num - } - - pub fn set_config(&self, config: &Config) -> Result<(), ConfigError> { - self.port.set_config(config) - } - - pub fn set_baudrate(&self, baudrate: u32) -> Result<(), ConfigError> { - self.port.set_config(&Config::new().baudrate(baudrate)) - } - - pub fn into_port(self) -> SerialPort { - self.port +fn serial_runtime(raw: impl RawUart) -> SerialProbeRuntime { + let name = raw.name(); + let base_addr = raw.base_addr(); + let baudrate = raw.baudrate(); + let runtime = SerialPort::split(raw, OwnerId(0)); + SerialProbeRuntime { + name, + base_addr, + baudrate, + runtime, } } @@ -115,16 +88,61 @@ impl TryFrom> for SerialDevice { let mut dev = base.lock().map_err(|_| AxError::BadState)?; let name = dev.name.clone(); let info = dev.info.clone(); - let port = dev.port.take().ok_or(AxError::BadState)?; + let runtime = dev.runtime.take().ok_or(AxError::BadState)?; Ok(Self { name, rdrive_device_id, info, - port, + runtime, }) } } +pub fn run_on_owner(owner: OwnerId, op: F) -> Result +where + F: FnOnce(OwnerLease<'_>) -> R, +{ + struct OwnerCall { + owner: OwnerId, + op: UnsafeCell>, + result: UnsafeCell>, + } + + unsafe fn thunk(arg: *mut ()) + where + F: FnOnce(OwnerLease<'_>) -> R, + { + let call = unsafe { &*(arg as *const OwnerCall) }; + let op = unsafe { &mut *call.op.get() } + .take() + .expect("serial owner call entered twice"); + let _irq_guard = IrqSave::new(); + let lease = unsafe { OwnerLease::new_unchecked(call.owner) }; + let result = op(lease); + unsafe { *call.result.get() = Some(result) }; + } + + let call = OwnerCall { + owner, + op: UnsafeCell::new(Some(op)), + result: UnsafeCell::new(None), + }; + unsafe { + run_on_cpu_sync( + CpuId(owner.0), + thunk::, + (&call as *const OwnerCall as *mut ()).cast(), + )?; + } + Ok(unsafe { &mut *call.result.get() } + .take() + .expect("serial owner call did not complete")) +} + +pub fn owner_lease_for_cpu(owner: OwnerId, cpu: CpuId) -> Option> { + (cpu.0 == owner.0).then(|| unsafe { OwnerLease::new_unchecked(owner) }) +} + pub fn take_serial_devices() -> Vec { if !rdrive::is_initialized() { warn!("rdrive is not initialized; no serial devices available"); diff --git a/drivers/ax-driver/src/serial/ns16550.rs b/drivers/ax-driver/src/serial/ns16550.rs index 60d25374f4..d824660058 100644 --- a/drivers/ax-driver/src/serial/ns16550.rs +++ b/drivers/ax-driver/src/serial/ns16550.rs @@ -11,7 +11,8 @@ use rdrive::{ use some_serial::ns16550 as serial_ns16550; use super::{ - PlatformSerialDevice, SerialPort, acpi_serial_device_info, prop_u32, serial_device_info, + PlatformSerialDevice, SerialProbeRuntime, acpi_serial_device_info, prop_u32, + serial_device_info, serial_runtime, }; const ACPI_NS16550_CLOCK: u32 = 1_843_200; @@ -59,41 +60,42 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { let reg_width = prop_u32(node, "reg-io-width").unwrap_or(1) as usize; let reg_shift = prop_u32(node, "reg-shift").map(|shift| 1usize << shift); let ns16550_width = reg_shift.unwrap_or(reg_width); - let mut serial: Option = None; + let mut serial: Option = None; for compatible in node.compatibles() { if compatible == "snps,dw-apb-uart" { let clock_freq = prop_u32(node, "clock-frequency") .unwrap_or(serial_ns16550::dw_apb::SG2002_UART_CLOCK); let raw = serial_ns16550::DwApbUart::new_raw(mmio_base, clock_freq); - serial = Some(SerialPort::new(raw)); + serial = Some(serial_runtime(raw)); break; } if matches!(compatible, "ns16550a" | "ns16550") { let clock_freq = prop_u32(node, "clock-frequency").unwrap_or(24_000_000); let raw = serial_ns16550::Ns16550::new_mmio(mmio_base, clock_freq, ns16550_width); - serial = Some(SerialPort::new(raw)); + serial = Some(serial_runtime(raw)); break; } } let serial = serial.ok_or(OnProbeError::NotMatch)?; - let base = serial.base_addr(); - let baudrate = serial.baudrate(); - let device_info = serial_device_info(&info, &base_reg, base, baudrate); + let device_info = serial_device_info(&info, &base_reg, serial.base_addr, serial.baudrate); - info!("NS16550 serial@{base:#x} registered successfully"); + info!( + "NS16550 serial@{:#x} registered successfully", + serial.base_addr + ); plat_dev.register(PlatformSerialDevice::new( - serial.name().into(), + serial.name.into(), device_info, - serial, + serial.runtime, )); Ok(()) } struct AcpiSerialResource { - serial: SerialPort, + serial: SerialProbeRuntime, paddr: usize, mapped_base: usize, } @@ -107,9 +109,13 @@ fn probe_acpi(probe: ProbeAcpi<'_>) -> Result<(), OnProbeError> { } else { acpi_mmio_serial(info)? }; - let baudrate = resource.serial.baudrate(); - let device_info = acpi_serial_device_info(info, resource.paddr, resource.mapped_base, baudrate); - let serial_name = resource.serial.name().into(); + let device_info = acpi_serial_device_info( + info, + resource.paddr, + resource.mapped_base, + resource.serial.baudrate, + ); + let serial_name = resource.serial.name.into(); let plat_dev = probe.into_platform_device(); info!( @@ -119,7 +125,7 @@ fn probe_acpi(probe: ProbeAcpi<'_>) -> Result<(), OnProbeError> { plat_dev.register(PlatformSerialDevice::new( serial_name, device_info, - resource.serial, + resource.serial.runtime, )); Ok(()) } @@ -136,8 +142,8 @@ fn acpi_io_serial(info: &AcpiInfo<'_>) -> Result, OnP )) })?; let raw = serial_ns16550::Ns16550::new_port(port, ACPI_NS16550_CLOCK); - let serial = SerialPort::new(raw); - let mapped_base = serial.base_addr(); + let serial = serial_runtime(raw); + let mapped_base = serial.base_addr; Ok(Some(AcpiSerialResource { serial, paddr: usize::from(port), @@ -167,8 +173,8 @@ fn acpi_mmio_serial(info: &AcpiInfo<'_>) -> Result) -> Result<(), OnProbeError> { let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size as usize)?; let clock_freq = prop_u32(info.node.as_node(), "clock-frequency").unwrap_or(24_000_000); let raw = pl011::Pl011::new(mmio_base, clock_freq); - let serial = SerialPort::new(raw); - let base = serial.base_addr(); - let baudrate = serial.baudrate(); + let serial = serial_runtime(raw); + let base = serial.base_addr; + let baudrate = serial.baudrate; let device_info = serial_device_info(&info, &base_reg, base, baudrate); info!("PL011 serial@{base:#x} registered successfully"); plat_dev.register(PlatformSerialDevice::new( - serial.name().into(), + serial.name.into(), device_info, - serial, + serial.runtime, )); Ok(()) } diff --git a/drivers/ax-driver/src/serial/rockchip_fiq.rs b/drivers/ax-driver/src/serial/rockchip_fiq.rs index 2848b6d927..74bdf5a978 100644 --- a/drivers/ax-driver/src/serial/rockchip_fiq.rs +++ b/drivers/ax-driver/src/serial/rockchip_fiq.rs @@ -8,7 +8,7 @@ use some_serial::ns16550::rockchip_fiq::{ RockchipFiqSerial, }; -use super::{PlatformSerialDevice, SerialDeviceInfo, SerialPort, prop_u32}; +use super::{PlatformSerialDevice, SerialDeviceInfo, prop_u32, serial_runtime}; use crate::BindingInfo; model_register!( @@ -39,8 +39,8 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { } let raw = RockchipFiqSerial::new(mmio_base, fdt_config.config); - let serial = SerialPort::new(raw); - let base = serial.base_addr(); + let serial = serial_runtime(raw); + let base = serial.base_addr; info!( "Rockchip FIQ debugger UART@{base:#x} registered successfully, serial-id={}, baudrate={}, \ irq-mode={}", @@ -59,17 +59,17 @@ fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { ); } plat_dev.register(PlatformSerialDevice::new( - serial.name().into(), + serial.name.into(), SerialDeviceInfo { fdt_path: fdt_config.uart_path, alias_index: Some(fdt_config.config.serial_id as usize), paddr: fdt_config.reg.address as usize, mapped_base: base, - baudrate: serial.baudrate(), + baudrate: serial.baudrate, irq_num: binding_info.irq_num(), binding_info, }, - serial, + serial.runtime, )); Ok(()) } diff --git a/drivers/ax-driver/src/serial/runtime.rs b/drivers/ax-driver/src/serial/runtime.rs deleted file mode 100644 index 407eab04f0..0000000000 --- a/drivers/ax-driver/src/serial/runtime.rs +++ /dev/null @@ -1,161 +0,0 @@ -use alloc::{string::String, sync::Arc}; -use core::cell::UnsafeCell; - -use ax_kspin::SpinNoIrq; -use axklib::irq::{CpuId, IrqError, run_on_cpu_sync}; -use rdif_serial::{ - Config, ConfigError, OwnerId, OwnerLease, RawUart, RxItem, RxQueue, SerialCounters, - SerialIrqHandler, SerialIrqOutcome, SerialSoftWork, TSerialIrqHandler, TxQueue, -}; - -pub struct SerialPort { - name: String, - base_addr: usize, - owner: OwnerId, - tx: SpinNoIrq, - rx: SpinNoIrq, - irq: Arc, -} - -impl SerialPort { - pub fn new(raw: impl RawUart) -> Self { - Self::new_with_owner(raw, 0) - } - - pub fn new_with_owner(raw: impl RawUart, owner_cpu: usize) -> Self { - let name = raw.name().into(); - let base_addr = raw.base_addr(); - let owner = OwnerId(owner_cpu); - let parts: rdif_serial::SerialParts = SerialIrqHandler::split(raw, owner); - Self { - name, - base_addr, - owner, - tx: SpinNoIrq::new(parts.tx), - rx: SpinNoIrq::new(parts.rx), - irq: parts.irq, - } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn base_addr(&self) -> usize { - self.base_addr - } - - pub fn owner_cpu(&self) -> usize { - self.owner.0 - } - - pub fn baudrate(&self) -> u32 { - self.run_on_owner(|irq, lease| irq.baudrate(lease)) - .unwrap_or(0) - } - - pub fn startup(&self, config: &Config) -> Result { - self.run_on_owner(|irq, lease| irq.startup(lease, config)) - .map_err(|_| ConfigError::RegisterError)? - } - - pub fn shutdown(&self) -> Result<(), IrqError> { - self.run_on_owner(|irq, lease| irq.shutdown(lease)) - } - - pub fn set_config(&self, config: &Config) -> Result<(), ConfigError> { - self.run_on_owner(|irq, lease| irq.set_config(lease, config)) - .map_err(|_| ConfigError::RegisterError)? - } - - pub fn submit_tx(&self, bytes: &[u8]) -> (usize, SerialIrqOutcome) { - let submit = self.tx.lock().submit(bytes); - let outcome = if submit.needs_kick { - self.service_on_owner(SerialSoftWork::TX_KICK) - } else { - SerialIrqOutcome::default() - }; - (submit.accepted, outcome) - } - - pub fn write_room(&self) -> usize { - self.tx.lock().write_room() - } - - pub fn chars_in_buffer(&self) -> usize { - self.tx.lock().chars_in_buffer() - } - - pub fn tx_idle(&self) -> bool { - self.run_on_owner(|irq, lease| irq.tx_idle(lease)) - .unwrap_or(false) - } - - pub fn drain_rx(&self, out: &mut [RxItem]) -> usize { - self.rx.lock().drain(out) - } - - pub fn rx_pending(&self) -> bool { - self.rx.lock().rx_pending() - } - - pub fn handle_irq_on_owner(&self, cpu: CpuId) -> SerialIrqOutcome { - let Some(lease) = self.owner_lease_for_cpu(cpu) else { - return SerialIrqOutcome::default(); - }; - self.irq.handle(lease) - } - - pub fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome { - self.run_on_owner(|irq, lease| irq.service(lease, work)) - .unwrap_or_default() - } - - pub fn counters(&self) -> SerialCounters { - self.irq.counters() - } - - fn run_on_owner(&self, op: F) -> Result - where - F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, - { - struct OwnerCall<'a, F, R> { - port: &'a SerialPort, - op: UnsafeCell>, - result: UnsafeCell>, - } - - unsafe fn thunk(arg: *mut ()) - where - F: FnOnce(&SerialIrqHandler, OwnerLease<'_>) -> R, - { - let call = unsafe { &*(arg as *const OwnerCall<'_, F, R>) }; - let op = unsafe { &mut *call.op.get() } - .take() - .expect("serial owner call entered twice"); - let lease = unsafe { OwnerLease::new_unchecked(call.port.owner) }; - let result = op(&call.port.irq, lease); - unsafe { *call.result.get() = Some(result) }; - } - - let call = OwnerCall { - port: self, - op: UnsafeCell::new(Some(op)), - result: UnsafeCell::new(None), - }; - unsafe { - run_on_cpu_sync( - CpuId(self.owner.0), - thunk::, - (&call as *const OwnerCall<'_, F, R> as *mut ()).cast(), - )?; - } - Ok(unsafe { &mut *call.result.get() } - .take() - .expect("serial owner call did not complete")) - } - - fn owner_lease_for_cpu(&self, cpu: CpuId) -> Option> { - (cpu.0 == self.owner.0).then(|| unsafe { OwnerLease::new_unchecked(self.owner) }) - } -} diff --git a/drivers/interface/rdif-serial/src/core.rs b/drivers/interface/rdif-serial/src/core.rs index f0a9133398..05b5955b71 100644 --- a/drivers/interface/rdif-serial/src/core.rs +++ b/drivers/interface/rdif-serial/src/core.rs @@ -259,18 +259,21 @@ bitflags::bitflags! { } } -pub trait TSerialIrqHandler: Send + Sync + 'static { - fn owner(&self) -> OwnerId; - fn handle(&self, lease: OwnerLease<'_>) -> SerialIrqOutcome; - fn service(&self, lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome; -} - type DynRawUart = Box; pub struct SerialParts { + pub port: Arc>, pub tx: TxQueue, pub rx: RxQueue, - pub irq: Arc>, + pub irq: SerialIrqHandler, +} + +pub struct SerialPort { + owner: OwnerId, + core: Arc>>, + tx: Arc>, + rx: Arc>, + counters: Arc, } pub struct SerialIrqHandler { @@ -281,7 +284,7 @@ pub struct SerialIrqHandler, } -impl SerialIrqHandler { +impl SerialPort { pub fn split(raw: impl RawUart, owner: OwnerId) -> SerialParts { Self::split_boxed(Box::new(raw), owner) } @@ -291,14 +294,22 @@ impl SerialIrqHandler { let tx = Arc::new(TxState::new()); let rx = Arc::new(RxState::new()); let counters = Arc::new(SerialCountersAtomic::new()); - let irq = Arc::new(Self { + let port = Arc::new(Self { owner, - core, + core: core.clone(), tx: tx.clone(), rx: rx.clone(), - counters, + counters: counters.clone(), }); + let irq = SerialIrqHandler { + owner, + core: core.clone(), + tx: tx.clone(), + rx: rx.clone(), + counters, + }; SerialParts { + port, tx: TxQueue { state: tx, _single_producer: PhantomData, @@ -311,6 +322,10 @@ impl SerialIrqHandler { } } + pub fn owner(&self) -> OwnerId { + self.owner + } + pub fn startup( &self, mut lease: OwnerLease<'_>, @@ -380,6 +395,22 @@ impl SerialIrqHandler { fn assert_owner(&self, lease: &OwnerLease<'_>) { assert_eq!(lease.owner(), self.owner); } +} + +impl SerialIrqHandler { + pub fn owner(&self) -> OwnerId { + self.owner + } + + pub fn handle(&mut self, mut lease: OwnerLease<'_>) -> SerialIrqOutcome { + self.assert_owner(&lease); + let mut core = unsafe { self.core.access(&mut lease) }; + self.handle_locked(&mut core) + } + + fn assert_owner(&self, lease: &OwnerLease<'_>) { + assert_eq!(lease.owner(), self.owner); + } fn handle_locked(&self, core: &mut CoreInner) -> SerialIrqOutcome { let mut out = SerialIrqOutcome::default(); @@ -435,6 +466,104 @@ impl SerialIrqHandler { out } + fn service_rx(&self, core: &mut CoreInner, budget: usize) -> RxService { + let mut result = RxService::default(); + for _ in 0..budget { + let Some(sample) = core.raw.read_rx() else { + break; + }; + result.consumed += 1; + + match sample.flag { + RxFlag::Normal => {} + RxFlag::Break => { + self.counters.rx_breaks.fetch_add(1, Ordering::Relaxed); + } + RxFlag::Parity => { + self.counters + .rx_parity_errors + .fetch_add(1, Ordering::Relaxed); + } + RxFlag::Framing => { + self.counters + .rx_framing_errors + .fetch_add(1, Ordering::Relaxed); + } + }; + + if let Some(byte) = sample.byte { + self.counters.rx_bytes.fetch_add(1, Ordering::Relaxed); + if self.rx.push_from_owner(RxItem::Byte { + byte, + flag: sample.flag, + }) { + result.published += 1; + } else { + self.counters + .rx_queue_dropped + .fetch_add(1, Ordering::Relaxed); + } + } + + if sample.overrun { + self.rx.overrun.fetch_add(1, Ordering::Relaxed); + self.counters + .rx_fifo_overruns + .fetch_add(1, Ordering::Relaxed); + if self.rx.push_from_owner(RxItem::Overrun) { + result.published += 1; + } else { + self.counters + .rx_queue_dropped + .fetch_add(1, Ordering::Relaxed); + } + } + } + result + } + + fn service_tx( + &self, + core: &mut CoreInner, + budget: usize, + out: &mut SerialIrqOutcome, + ) -> usize { + let limit = budget; + let mut sent = 0; + while sent < limit && core.raw.tx_ready() { + let Some(byte) = self.tx.ring.peek_copy() else { + break; + }; + core.raw.write_tx(byte); + let committed = self.tx.ring.pop(); + debug_assert_eq!(committed, Some(byte)); + self.tx.sent.fetch_add(1, Ordering::Relaxed); + self.counters.tx_bytes.fetch_add(1, Ordering::Relaxed); + sent += 1; + } + + if self.tx.ring.is_empty() { + if core.tx_irq_enabled { + core.irq_mask.remove(InterruptMask::TX_SPACE); + core.raw.set_irq_mask(core.irq_mask); + core.tx_irq_enabled = false; + } + } else if !core.tx_irq_enabled { + core.irq_mask.insert(InterruptMask::TX_SPACE); + core.raw.set_irq_mask(core.irq_mask); + core.tx_irq_enabled = true; + } + + if sent > 0 { + self.tx.blocked.store(false, Ordering::Release); + out.tx_wakeup = true; + } + out.tx_sent += sent; + sent + } +} + +impl SerialPort { fn service_soft_locked( &self, core: &mut CoreInner, @@ -551,20 +680,8 @@ impl SerialIrqHandler { out.tx_sent += sent; sent } -} - -impl TSerialIrqHandler for SerialIrqHandler { - fn owner(&self) -> OwnerId { - self.owner - } - - fn handle(&self, mut lease: OwnerLease<'_>) -> SerialIrqOutcome { - self.assert_owner(&lease); - let mut core = unsafe { self.core.access(&mut lease) }; - self.handle_locked(&mut core) - } - fn service(&self, mut lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome { + pub fn service(&self, mut lease: OwnerLease<'_>, work: SerialSoftWork) -> SerialIrqOutcome { self.assert_owner(&lease); let mut core = unsafe { self.core.access(&mut lease) }; self.service_soft_locked(&mut core, work) @@ -756,7 +873,7 @@ mod tests { #[test] fn tx_queue_only_submits_software_work() { - let parts = SerialIrqHandler::<8, 8>::split(MockUart::new(), OwnerId(0)); + let parts = SerialPort::<8, 8>::split(MockUart::new(), OwnerId(0)); let mut tx = parts.tx; let submit = tx.submit(b"abc"); @@ -768,8 +885,9 @@ mod tests { #[test] fn split_erases_raw_type_and_keeps_capacity_generics() { - let parts: SerialParts<8, 8> = SerialIrqHandler::split(MockUart::new(), OwnerId(0)); + let parts: SerialParts<8, 8> = SerialPort::split(MockUart::new(), OwnerId(0)); + assert_eq!(parts.port.owner(), OwnerId(0)); assert_eq!(parts.irq.owner(), OwnerId(0)); assert_eq!(parts.tx.write_room(), 7); assert!(!parts.rx.rx_pending()); @@ -779,12 +897,12 @@ mod tests { fn owner_service_flushes_tx_queue() { let mut uart = MockUart::new(); uart.tx_ready_budget = 3; - let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); + let parts = SerialPort::<8, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; tx.submit(b"abc"); - parts.irq.startup(lease(), &Config::new()).unwrap(); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + let outcome = parts.port.service(lease(), SerialSoftWork::TX_KICK); assert_eq!(outcome.tx_sent, 3); assert!(outcome.tx_wakeup); @@ -795,12 +913,12 @@ mod tests { fn tx_kick_wakes_again_when_queue_still_has_data() { let mut uart = MockUart::new(); uart.tx_ready_budget = 1; - let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); + let parts = SerialPort::<8, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; tx.submit(b"abc"); - parts.irq.startup(lease(), &Config::new()).unwrap(); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + let outcome = parts.port.service(lease(), SerialSoftWork::TX_KICK); assert_eq!(outcome.tx_sent, 1); assert!(outcome.tx_wakeup); @@ -812,13 +930,13 @@ mod tests { let mut uart = MockUart::new(); uart.tx_ready_budget = TX_KICK_BUDGET + 8; uart.tx_load_size = 1; - let parts = SerialIrqHandler::<64, 8>::split(uart, OwnerId(0)); + let parts = SerialPort::<64, 8>::split(uart, OwnerId(0)); let mut tx = parts.tx; let data = [b'x'; TX_KICK_BUDGET + 8]; tx.submit(&data); - parts.irq.startup(lease(), &Config::new()).unwrap(); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.service(lease(), SerialSoftWork::TX_KICK); + let outcome = parts.port.service(lease(), SerialSoftWork::TX_KICK); assert_eq!(outcome.tx_sent, TX_KICK_BUDGET); assert!(outcome.tx_wakeup); @@ -831,10 +949,11 @@ mod tests { .irq(IrqSource::RX_DATA) .rx_byte(b'A') .rx_byte(b'B'); - let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); - parts.irq.startup(lease(), &Config::new()).unwrap(); + let parts = SerialPort::<8, 8>::split(uart, OwnerId(0)); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.handle(lease()); + let mut irq = parts.irq; + let outcome = irq.handle(lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 2); @@ -863,10 +982,11 @@ mod tests { for &byte in burst { uart = uart.rx_byte(byte); } - let parts = SerialIrqHandler::<8, 128>::split(uart, OwnerId(0)); - parts.irq.startup(lease(), &Config::new()).unwrap(); + let parts = SerialPort::<8, 128>::split(uart, OwnerId(0)); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.handle(lease()); + let mut irq = parts.irq; + let outcome = irq.handle(lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, burst.len()); @@ -892,10 +1012,11 @@ mod tests { flag: RxFlag::Parity, overrun: true, }); - let parts = SerialIrqHandler::<8, 8>::split(uart, OwnerId(0)); - parts.irq.startup(lease(), &Config::new()).unwrap(); + let parts = SerialPort::<8, 8>::split(uart, OwnerId(0)); + parts.port.startup(lease(), &Config::new()).unwrap(); - let outcome = parts.irq.handle(lease()); + let mut irq = parts.irq; + let outcome = irq.handle(lease()); assert!(outcome.claimed); assert_eq!(outcome.rx_pushed, 2); diff --git a/drivers/interface/rdif-serial/src/lib.rs b/drivers/interface/rdif-serial/src/lib.rs index 42ba679572..74d7a43deb 100644 --- a/drivers/interface/rdif-serial/src/lib.rs +++ b/drivers/interface/rdif-serial/src/lib.rs @@ -2,15 +2,16 @@ //! //! The reusable stack is intentionally split by synchronization ownership: //! raw UART drivers expose only register-level operations, `TxQueue` and -//! `RxQueue` own independent lock-free software queues, and `SerialIrqHandler` -//! is the only endpoint allowed to touch runtime UART registers. +//! `RxQueue` own independent lock-free software queues, `SerialPort` owns the +//! task/worker control path, and `SerialIrqHandler` is the IRQ-only endpoint. //! -//! OS glue must route the hardware IRQ and software TX kick to the handler's -//! owner CPU and pass an `OwnerLease`. Task or worker context drains `RxItem`s -//! and enqueues TX bytes, but never polls the shared UART IRQ/status register -//! to rediscover readiness. This keeps the fast path bounded and leaves wakeups, -//! wait queues, poll sets, and line discipline processing to OS-specific layers -//! above this crate. +//! OS glue must route hardware IRQs and control/service calls to the configured +//! owner CPU and pass an `OwnerLease`. Task context drains `RxItem`s and +//! enqueues TX bytes through the queues, while worker context uses `SerialPort` +//! for bounded soft service. Neither TX nor RX queues poll the shared UART +//! IRQ/status register to rediscover readiness. This keeps the fast path bounded +//! and leaves wakeups, wait queues, poll sets, and line discipline processing to +//! OS-specific layers above this crate. #![no_std] diff --git a/drivers/serial/some-serial/src/ns16550/mod.rs b/drivers/serial/some-serial/src/ns16550/mod.rs index dce8865b13..9180069a9d 100644 --- a/drivers/serial/some-serial/src/ns16550/mod.rs +++ b/drivers/serial/some-serial/src/ns16550/mod.rs @@ -698,9 +698,7 @@ mod tests { use core::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; use std::sync::{Mutex, MutexGuard}; - use rdif_serial::{ - OwnerId, OwnerLease, RxItem, SerialIrqHandler, SerialParts, TSerialIrqHandler, - }; + use rdif_serial::{OwnerId, OwnerLease, RxItem, SerialParts, SerialPort}; use super::*; @@ -822,8 +820,8 @@ mod tests { } fn started_parts(uart: Ns16550) -> SerialParts<64, 64> { - let parts = SerialIrqHandler::<64, 64>::split(uart, OwnerId(0)); - parts.irq.startup(owner_lease(), &Config::new()).unwrap(); + let parts = SerialPort::<64, 64>::split(uart, OwnerId(0)); + parts.port.startup(owner_lease(), &Config::new()).unwrap(); parts } @@ -967,7 +965,7 @@ mod tests { let parts = started_parts(uart); let mut tx = parts.tx; let mut rx_queue = parts.rx; - let irq = parts.irq; + let mut irq = parts.irq; assert_eq!(tx.submit(b"ab").accepted, 2); REGS[UART_IIR as usize].store( @@ -1009,7 +1007,7 @@ mod tests { let (_guard, uart) = serial(); let parts = started_parts(uart); let mut tx = parts.tx; - let irq = parts.irq; + let mut irq = parts.irq; assert_eq!(tx.submit(b"x").accepted, 1); REGS[UART_IIR as usize].store( InterruptIdentificationFlags::NO_INTERRUPT_PENDING.bits(), @@ -1063,7 +1061,7 @@ mod tests { fn hard_irq_claims_and_clears_modem_status_interrupt() { let (_guard, uart) = serial(); let parts = started_parts(uart); - let irq = parts.irq; + let mut irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::MODEM_STATUS.bits() @@ -1091,7 +1089,7 @@ mod tests { let (_guard, uart) = serial(); let parts = started_parts(uart); let mut rx_queue = parts.rx; - let irq = parts.irq; + let mut irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVED_DATA_AVAILABLE.bits(), @@ -1120,7 +1118,7 @@ mod tests { let (_guard, uart) = serial(); let parts = started_parts(uart); let mut tx = parts.tx; - let irq = parts.irq; + let mut irq = parts.irq; assert_eq!(tx.submit(b"ab").accepted, 2); assert_eq!(tx.chars_in_buffer(), 2); @@ -1146,7 +1144,7 @@ mod tests { let (_guard, uart) = serial(); let parts = started_parts(uart); let mut rx_queue = parts.rx; - let irq = parts.irq; + let mut irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), @@ -1178,7 +1176,7 @@ mod tests { let (_guard, uart) = serial(); let parts = started_parts(uart); let mut rx_queue = parts.rx; - let irq = parts.irq; + let mut irq = parts.irq; REGS[UART_IIR as usize].store( InterruptIdentificationFlags::RECEIVER_LINE_STATUS.bits(), diff --git a/drivers/serial/some-serial/src/pl011.rs b/drivers/serial/some-serial/src/pl011.rs index a039e14fd3..b6961d38c5 100644 --- a/drivers/serial/some-serial/src/pl011.rs +++ b/drivers/serial/some-serial/src/pl011.rs @@ -866,7 +866,7 @@ mod tests { use core::ptr::NonNull; use std::boxed::Box; - use rdif_serial::{OwnerId, OwnerLease, SerialIrqHandler, SerialParts, TSerialIrqHandler}; + use rdif_serial::{OwnerId, OwnerLease, SerialParts, SerialPort}; use super::*; @@ -907,8 +907,8 @@ mod tests { } fn started_parts(uart: Pl011) -> SerialParts<64, 64> { - let parts = SerialIrqHandler::<64, 64>::split(uart, OwnerId(0)); - parts.irq.startup(owner_lease(), &Config::new()).unwrap(); + let parts = SerialPort::<64, 64>::split(uart, OwnerId(0)); + parts.port.startup(owner_lease(), &Config::new()).unwrap(); parts } @@ -969,7 +969,7 @@ mod tests { let (mut regs, uart) = pl011_with_registers(); let parts = started_parts(uart); let mut tx = parts.tx; - let irq = parts.irq; + let mut irq = parts.irq; write_test_reg(&mut regs, 0x018, UARTFR::TXFF::SET.value); assert_eq!(tx.submit(b"x").accepted, 1); diff --git a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs index 64f7aaa0ab..63e1162451 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/tty/serial.rs @@ -1,18 +1,15 @@ -use alloc::{format, string::String, sync::Arc, vec, vec::Vec}; -use core::{ - ptr::NonNull, - sync::atomic::{AtomicBool, AtomicU32, Ordering}, -}; +use alloc::{boxed::Box, format, string::String, sync::Arc, vec, vec::Vec}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use ax_driver::serial::{ - self as ax_serial, Config, RxFlag, RxItem, SerialDevice, SerialIrqOutcome, SerialPort, - SerialSoftWork, + self as ax_serial, Config, ConfigError, OwnerId, RxFlag, RxItem, RxQueue, SerialDevice, + SerialIrqHandler, SerialIrqOutcome, SerialPort, SerialSoftWork, TxQueue, }; use ax_errno::{AxError, AxResult}; use ax_kspin::SpinNoIrq; use ax_runtime::hal::{ console::{ConsoleDeviceIdError, ConsoleDeviceIdResult}, - irq::{AutoEnable, CpuId, IrqAffinity, IrqExecution, IrqHandle, IrqRequest, ShareMode}, + irq::{AutoEnable, CpuId, IrqAffinity, IrqHandle, IrqRequest, ShareMode}, }; use ax_sync::Mutex; use ax_task::IrqNotify; @@ -73,7 +70,10 @@ struct SerialBackend { tty_name: String, rdrive_device_id: RDriveDeviceId, number: usize, - port: SerialPort, + owner: OwnerId, + port: Arc, + tx: SpinNoIrq, + rx: SpinNoIrq, irq_num: usize, irq_handle: SpinNoIrq>, started: AtomicBool, @@ -222,7 +222,7 @@ impl SerialRegistry { let numbers = assign_tty_numbers( serials .iter() - .map(|serial| serial.alias_index()) + .map(|serial| serial.info.alias_index) .collect::>() .as_slice(), ); @@ -232,8 +232,7 @@ impl SerialRegistry { let Some(number) = number else { warn!( "Skipping serial device {} at {} because ttyS number could not be assigned", - serial.name(), - serial.fdt_path() + serial.name, serial.info.fdt_path ); continue; }; @@ -278,19 +277,29 @@ impl SerialRegistry { fn new_serial_tty(number: usize, serial: SerialDevice) -> AxResult { let tty_name = format!("ttyS{number}"); - let name = serial.name().into(); - let info = serial.info().clone(); - let rdrive_device_id = serial.rdrive_device_id(); - let Some(irq_num) = serial.irq_num() else { + let SerialDevice { + name, + rdrive_device_id, + info, + runtime, + } = serial; + let Some(irq_num) = info.irq_num else { return Err(AxError::Unsupported); }; - let port = serial.into_port(); + let port = runtime.port; + let tx = runtime.tx; + let rx = runtime.rx; + let irq = runtime.irq; + let owner = port.owner(); let backend = Arc::new(SerialBackend { name, tty_name: tty_name.clone(), rdrive_device_id, number, + owner, port, + tx: SpinNoIrq::new(tx), + rx: SpinNoIrq::new(rx), irq_num, irq_handle: SpinNoIrq::new(None), started: AtomicBool::new(false), @@ -302,7 +311,7 @@ fn new_serial_tty(number: usize, serial: SerialDevice) -> AxResult AxResult) -> AxResult<()> { - let data = NonNull::new(Arc::into_raw(self.clone()) as *mut ()).unwrap(); - let request = IrqRequest::new(serial_raw_irq_handler, data) - .share_mode(ShareMode::Shared) - .affinity(IrqAffinity::Fixed(CpuId(self.port.owner_cpu()))) - .execution(IrqExecution::NonReentrant) - .auto_enable(AutoEnable::No); + fn register_irq(self: &Arc, mut irq: SerialIrqHandler) -> AxResult<()> { + let backend = self.clone(); + let request = IrqRequest::new_boxed(Box::new(move |ctx| { + let outcome = backend.handle_irq_on_owner(ctx.cpu, &mut irq); + if !outcome.claimed { + return ax_runtime::hal::irq::IrqReturn::Unhandled; + } + let events = publish_serial_outcome(&backend, outcome, true); + if events.is_empty() { + ax_runtime::hal::irq::IrqReturn::Handled + } else { + ax_runtime::hal::irq::IrqReturn::Wake + } + })) + .share_mode(ShareMode::Shared) + .affinity(IrqAffinity::Fixed(CpuId(self.owner.0))) + .auto_enable(AutoEnable::No); match ax_runtime::hal::irq::request_irq(self.irq_num, request) { Ok(handle) => { *self.irq_handle.lock() = Some(handle); Ok(()) } Err(err) => { - unsafe { - Arc::decrement_strong_count(data.as_ptr() as *const SerialBackend); - } warn!( "Failed to register {} IRQ handler for irq {}: {err:?}", self.tty_name, self.irq_num @@ -370,9 +386,8 @@ impl SerialBackend { return false; }; - if let Err(err) = self - .port - .startup(&Config::new().baudrate(startup_baudrate(self.port.baudrate()))) + if let Err(err) = + self.startup_port(&Config::new().baudrate(startup_baudrate(self.baudrate()))) { warn!( "{} failed to start serial port {}: {:?}", @@ -382,7 +397,7 @@ impl SerialBackend { } if let Err(err) = ax_runtime::hal::irq::enable_irq(handle) { - let _ = self.port.shutdown(); + self.shutdown_port(); warn!( "Failed to enable {} IRQ handler for irq {}: {err:?}", self.tty_name, self.irq_num @@ -393,7 +408,7 @@ impl SerialBackend { self.started.store(true, Ordering::Release); publish_serial_outcome( self, - self.port.service_on_owner(SerialSoftWork::RESERVICE), + self.service_on_owner(SerialSoftWork::RESERVICE), false, ); self.events.publish(SerialEventBits::RX_READY); @@ -407,6 +422,50 @@ impl SerialBackend { Err(AxError::Unsupported) } } + + fn startup_port(&self, config: &Config) -> Result { + ax_serial::run_on_owner(self.owner, |lease| self.port.startup(lease, config)) + .map_err(|_| ConfigError::RegisterError)? + } + + fn shutdown_port(&self) { + let _ = ax_serial::run_on_owner(self.owner, |lease| self.port.shutdown(lease)); + } + + fn set_port_config(&self, config: &Config) -> Result<(), ConfigError> { + ax_serial::run_on_owner(self.owner, |lease| self.port.set_config(lease, config)) + .map_err(|_| ConfigError::RegisterError)? + } + + fn baudrate(&self) -> u32 { + ax_serial::run_on_owner(self.owner, |lease| self.port.baudrate(lease)).unwrap_or(0) + } + + fn service_on_owner(&self, work: SerialSoftWork) -> SerialIrqOutcome { + ax_serial::run_on_owner(self.owner, |lease| self.port.service(lease, work)) + .unwrap_or_default() + } + + fn handle_irq_on_owner(&self, cpu: CpuId, irq: &mut SerialIrqHandler) -> SerialIrqOutcome { + let Some(lease) = ax_serial::owner_lease_for_cpu(self.owner, cpu) else { + return SerialIrqOutcome::default(); + }; + irq.handle(lease) + } + + fn submit_tx(&self, bytes: &[u8]) -> (usize, SerialIrqOutcome) { + let submit = self.tx.lock().submit(bytes); + let outcome = if submit.needs_kick { + self.service_on_owner(SerialSoftWork::TX_KICK) + } else { + SerialIrqOutcome::default() + }; + (submit.accepted, outcome) + } + + fn drain_rx(&self, out: &mut [RxItem]) -> usize { + self.rx.lock().drain(out) + } } fn startup_baudrate(current: u32) -> u32 { @@ -433,7 +492,7 @@ fn spawn_serial_event_worker(backend: Arc) { if pending.contains(SerialEventBits::TX_SPACE) { backend.tx_notify.notify(); unsafe { backend.output_source.wake(IoEvents::OUT) }; - let outcome = backend.port.service_on_owner(SerialSoftWork::TX_KICK); + let outcome = backend.service_on_owner(SerialSoftWork::TX_KICK); publish_serial_outcome(&backend, outcome, false); } } @@ -442,23 +501,6 @@ fn spawn_serial_event_worker(backend: Arc) { ); } -unsafe fn serial_raw_irq_handler( - ctx: ax_runtime::hal::irq::IrqContext, - data: NonNull<()>, -) -> ax_runtime::hal::irq::IrqReturn { - let backend = unsafe { &*(data.as_ptr() as *const SerialBackend) }; - let outcome = backend.port.handle_irq_on_owner(ctx.cpu); - if !outcome.claimed { - return ax_runtime::hal::irq::IrqReturn::Unhandled; - } - let events = publish_serial_outcome(backend, outcome, true); - if events.is_empty() { - ax_runtime::hal::irq::IrqReturn::Handled - } else { - ax_runtime::hal::irq::IrqReturn::Wake - } -} - fn publish_serial_outcome( backend: &SerialBackend, outcome: SerialIrqOutcome, @@ -491,7 +533,7 @@ impl TtyRead for SerialReader { while total < buf.len() { let limit = (buf.len() - total).min(temp.len()); - let read = self.backend.port.drain_rx(&mut temp[..limit]); + let read = self.backend.drain_rx(&mut temp[..limit]); if read == 0 { break; } @@ -538,7 +580,7 @@ impl TtyWrite for SerialWriter { let _guard = self.backend.output_lock.lock(); let mut written = 0; while written < buf.len() { - let (count, outcome) = self.backend.port.submit_tx(&buf[written..]); + let (count, outcome) = self.backend.submit_tx(&buf[written..]); publish_serial_outcome(&self.backend, outcome, false); if count == 0 { self.backend.tx_notify.wait(); @@ -558,7 +600,7 @@ impl TtyWrite for SerialWriter { let Some(_guard) = self.backend.output_lock.try_lock() else { return 0; }; - let (count, outcome) = self.backend.port.submit_tx(buf); + let (count, outcome) = self.backend.submit_tx(buf); publish_serial_outcome(&self.backend, outcome, false); count } @@ -583,8 +625,7 @@ impl TtyWrite for SerialWriter { } if let Err(err) = self .backend - .port - .set_config(&Config::new().baudrate(new_baud)) + .set_port_config(&Config::new().baudrate(new_baud)) { warn!( "{} failed to set baudrate {new_baud} on {}: {:?}", diff --git a/os/arceos/modules/axhal/src/irq.rs b/os/arceos/modules/axhal/src/irq.rs index 9c6faec782..105ca9b1b3 100644 --- a/os/arceos/modules/axhal/src/irq.rs +++ b/os/arceos/modules/axhal/src/irq.rs @@ -4,11 +4,11 @@ pub use ax_config::devices::IPI_IRQ; use ax_cpu::trap::set_irq_handler; pub use ax_plat::irq::{ - AutoEnable, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, - IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, ShareMode, - cpu_online, disable_irq, dispatch_irq, enable_irq, free_irq, handle, irq_status, request_irq, - request_percpu_irq, request_shared_irq, run_on_cpu_sync, set_enable, set_run_on_cpu_sync, - synchronize_irq, + AutoEnable, BoxedIrqHandler, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, + IrqHandle, IrqNumber, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, + ShareMode, cpu_online, disable_irq, dispatch_irq, enable_irq, free_irq, handle, irq_status, + request_boxed_irq, request_boxed_shared_irq, request_irq, request_percpu_irq, + request_shared_irq, run_on_cpu_sync, set_enable, set_run_on_cpu_sync, synchronize_irq, }; #[cfg(feature = "ipi")] pub use ax_plat::irq::{IpiTarget, send_ipi}; diff --git a/platforms/ax-plat/src/irq.rs b/platforms/ax-plat/src/irq.rs index 1555e8d50d..8c55eef57a 100644 --- a/platforms/ax-plat/src/irq.rs +++ b/platforms/ax-plat/src/irq.rs @@ -4,9 +4,9 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use ax_kernel_guard::BaseGuard; pub use irq_framework::{ - AutoEnable, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, IrqHandle, - IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, RawIrqHandler, - Registry, ShareMode, + AutoEnable, BoxedIrqHandler, CpuId, CpuMask, IrqAffinity, IrqContext, IrqError, IrqExecution, + IrqHandle, IrqNumber, IrqOps, IrqOutcome, IrqRequest, IrqReturn, IrqScope, IrqStatus, + RawIrqHandler, Registry, ShareMode, }; use spin::Once; @@ -149,6 +149,22 @@ pub fn request_shared_irq( ) } +/// Requests a boxed IRQ action. +pub fn request_boxed_irq(irq: usize, request: IrqRequest) -> Result { + request_irq(irq, request) +} + +/// Requests a boxed shared IRQ action. +pub fn request_boxed_shared_irq( + irq: usize, + handler: BoxedIrqHandler, +) -> Result { + request_irq( + irq, + IrqRequest::new_boxed(handler).share_mode(ShareMode::Shared), + ) +} + /// Requests a per-CPU IRQ action. pub fn request_percpu_irq( irq: usize, From 430cf29a01f419f6b445fa4f3b33e03dcf9f8d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 11:06:05 +0800 Subject: [PATCH 14/16] docs(driver): record irq endpoint ownership model --- .claude/skills/cross-kernel-driver/SKILL.md | 30 ++++++--- .../cross-kernel-driver/agents/openai.yaml | 2 +- .../references/architecture.md | 66 ++++++++++++++++++- 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/.claude/skills/cross-kernel-driver/SKILL.md b/.claude/skills/cross-kernel-driver/SKILL.md index c5f300e797..fb20dc7a6e 100644 --- a/.claude/skills/cross-kernel-driver/SKILL.md +++ b/.claude/skills/cross-kernel-driver/SKILL.md @@ -1,13 +1,13 @@ --- name: cross-kernel-driver -description: Create, refactor, review, and optimize portable Rust driver crates under `drivers/` by device type in this tgoskits workspace. Use this skill when adding or changing cross-Rust-kernel drivers, separating Driver Core / Capability Boundary / OS Glue / Runtime layers, handling MMIO/iomap with `mmio-api`, handling DMA with `dma-api`, designing IRQ event or queue contracts, or auditing OS API coupling in driver code. +description: Create, refactor, review, and optimize portable Rust driver crates under `drivers/` by device type in this tgoskits workspace. Use this skill when adding or changing cross-Rust-kernel drivers, separating Driver Core / Capability Boundary / OS Glue / Runtime layers, handling MMIO/iomap with `mmio-api`, handling DMA with `dma-api`, designing IRQ callback ownership, control/IRQ/queue endpoint contracts, queue-local completion state, or auditing OS API coupling in driver code. --- # Cross Kernel Driver ## Overview -Use this skill to keep reusable driver crates portable across Rust kernels by separating stable hardware logic from OS API coupling. The target shape is: Driver Core owns registers, descriptors, state machines, queues, and events; Capability Boundary owns MMIO, DMA, IRQ, and queue contracts; OS Glue owns probe, iomap/remap, IRQ registration, and task scheduling; Runtime owns blocking / poll / future / worker integration. +Use this skill to keep reusable driver crates portable across Rust kernels by separating stable hardware logic from OS API coupling. The target shape is: Driver Core owns registers, descriptors, state machines, queues, and events; Capability Boundary owns MMIO, DMA, IRQ, and queue contracts; OS Glue owns probe, iomap/remap, IRQ registration, and task scheduling; Runtime owns blocking / poll / future / worker integration. For IRQ-driven devices, prefer an explicit runtime split into control, IRQ handler, and queue endpoints so each endpoint has one clear owner and synchronization contract. For nontrivial driver design or refactoring, read `references/architecture.md` before editing. @@ -20,10 +20,12 @@ For nontrivial driver design or refactoring, read `references/architecture.md` b 5. For ArceOS/dynamic-platform integration, keep adapters in the existing platform module names such as `platform/axplat-dyn/src/drivers/blk`, even if the reusable crate lives under `drivers/block`. 6. Use small capability traits or API objects instead of a monolithic `KernelHal`. Split MMIO, DMA, IRQ event, queue contract, and wake/poll boundaries. 7. Model queues as independent running units. Prefer APIs such as `submit`, `reclaim`, `poll`, `submit_request`, and `poll_request`. -8. For IRQ-driven devices, keep IRQ endpoints and queue endpoints separate. IRQ handlers should synchronize hardware events into queue-local completion state; queues should advance their own work without locking the IRQ handler or re-reading shared/destructive IRQ status. -9. Make IRQ paths return stable events, normally `handle_irq() -> Event`. OS Glue decides whether to wake a thread, wake a future, schedule a worker, or set a pending flag. -10. When IRQ and task paths share mutable driver state, look for an explicit exclusion protocol: task-side mutation masks the exact interrupt source before taking the lock, while IRQ only touches pre-registered stable state. Document the lifetime/safety contract; otherwise prefer atomics/pending bits plus a deferred worker. -11. Validate the changed crate with formatting and targeted clippy before finishing. +8. For IRQ-driven devices, keep control, IRQ handler, and queue endpoints separate. The control endpoint owns startup/config/service operations; the IRQ endpoint synchronizes hardware events; queue endpoints submit/reclaim work using queue-local state. +9. Move lifetime-sensitive IRQ handler endpoints into the registered IRQ callback when possible. Prefer `FnMut`/boxed callback ownership or an equivalent OS registration token over sharing the IRQ handler through `Arc>`. +10. IRQ handlers should synchronize hardware events into queue-local completion state; queues should advance their own work without locking the IRQ handler or re-reading shared/destructive IRQ status. +11. Make IRQ paths return stable events, normally `handle_irq(&mut self) -> Event` for an IRQ-owned endpoint or `handle_irq() -> Event` for stateless/raw event extractors. OS Glue decides whether to wake a thread, wake a future, schedule a worker, or set a pending flag. +12. When IRQ and task paths share mutable driver state, look for an explicit exclusion protocol: task-side mutation masks the exact interrupt source before taking the lock, while IRQ only touches pre-registered stable state. Document the lifetime/safety contract; otherwise prefer atomics/pending bits plus a deferred worker. +13. Validate the changed crate with formatting and targeted clippy before finishing. ## Dependency Rules @@ -49,7 +51,7 @@ For nontrivial driver design or refactoring, read `references/architecture.md` b ## Interface Shape -Use `&mut self` APIs where exclusive access is the natural contract. Do not require callers to provide an OS lock as part of the portable abstraction. +Use `&mut self` APIs where exclusive access is the natural contract. Do not require callers to provide an OS lock as part of the portable abstraction. If only the IRQ callback should call a handler, make that visible in the type shape: move the handler into the callback and expose `handle(&mut self, ...)` instead of making the handler a clonable shared object. For block-device integration in ArceOS, expose portable block drivers through `rdif_block::Interface` and `rdif_block::IQueue`. Keep queue creation, DMA/wait policy, and IRQ registration in OS glue/runtime layers; the portable boundary should be submit/poll requests plus `handle_irq() -> Event`. @@ -57,7 +59,7 @@ Prefer small interfaces: ```rust pub trait IrqHandle { - fn handle_irq(&self) -> Event; + fn handle_irq(&mut self) -> Event; } pub trait IQueue { @@ -69,6 +71,18 @@ pub trait IQueue { IRQ handlers should identify/clear the interrupt source and extract an `Event`. They should not block, run long slow paths, or hold broad locks. Keep the principle visible during reviews: "interrupts synchronize state; tasks advance flow" (`中断只同步状态,任务才推进流程`). +For runtime designs with richer state, prefer returning split parts: + +```rust +pub struct DeviceParts { + pub control: Arc, + pub irq: IrqHandler, + pub queues: QueueSet, +} +``` + +Register `irq` by moving it into the OS IRQ callback. Let task/worker code hold `control` and queue endpoints, not the IRQ handler itself. + When a driver intentionally shares registries or queue maps between task setup and IRQ completion paths, prefer an xHCI-style exclusion protocol over taking the same spinlock in IRQ: task context masks the same device interrupter/MSI source before mutation; IRQ context does not take that lock and only touches entries whose lifetime was established before interrupts were enabled. This avoids same-lock IRQ reentry deadlocks, but it does not make allocation, blocking, arbitrary wakers, or unrelated OS callbacks safe in hard IRQ. For split queue designs, do not make an IRQ handler lock a queue mutex that task context can hold. If IRQ and queues share one hardware register block, put exclusive register access behind one short, non-blocking core/gate, let the IRQ endpoint be the sole reader/clearer of shared or destructive IRQ status, and fan out results into independent per-queue completion state. Queue `poll` should normally mean "consume synchronized completion state", not "peek the global IRQ/status register again". diff --git a/.claude/skills/cross-kernel-driver/agents/openai.yaml b/.claude/skills/cross-kernel-driver/agents/openai.yaml index da7f61bc5a..d538bf06ad 100644 --- a/.claude/skills/cross-kernel-driver/agents/openai.yaml +++ b/.claude/skills/cross-kernel-driver/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Cross Kernel Driver" short_description: "Create portable driver crates" - default_prompt: "Use $cross-kernel-driver to create or optimize a driver under drivers/ with mmio-api/dma-api capability boundaries." + default_prompt: "Use $cross-kernel-driver to create or optimize a driver under drivers/ with MMIO/DMA capability boundaries and clear control/IRQ/queue endpoint ownership." diff --git a/.claude/skills/cross-kernel-driver/references/architecture.md b/.claude/skills/cross-kernel-driver/references/architecture.md index 12a223b6e4..1710172d95 100644 --- a/.claude/skills/cross-kernel-driver/references/architecture.md +++ b/.claude/skills/cross-kernel-driver/references/architecture.md @@ -128,14 +128,16 @@ Check every DMA path for: Portable IRQ handling should answer "what happened?" OS Glue answers "how should execution continue?" -Use an IRQ handle that extracts a stable event: +Use an IRQ endpoint that extracts a stable event. When the endpoint has mutable runtime state, prefer `&mut self` and let the OS IRQ registration own the endpoint: ```rust pub trait IrqHandle { - fn handle_irq(&self) -> Event; + fn handle_irq(&mut self) -> Event; } ``` +For stateless raw event extractors, `handle_irq(&self)` or a free function can still be appropriate. Do not make a stateful IRQ endpoint clonable merely so registration code can keep a pointer to it. + `Event` should identify: - event kind @@ -152,6 +154,60 @@ The IRQ fast path should: OS Glue converts events into wakeups, future wakers, worker scheduling, or pending polling flags. +### IRQ Callback Ownership Pattern + +If an IRQ handler endpoint is only meaningful inside the registered interrupt callback, encode that in ownership: + +```rust +let mut irq = parts.irq; +request_irq(irq_number, Box::new(move |ctx| { + let event = irq.handle_irq(ctx); + publish_event(event) +})); +``` + +Use the target kernel's equivalent of a boxed `FnMut` callback, registration token, or owned closure. The important property is not the allocation mechanism; it is that the IRQ framework owns the handler for the registration lifetime and calls it non-reentrantly. This gives the handler a single mutation site without `Arc>`, raw pointer lifetime tricks, or public APIs that unrelated task code can call. + +When applying this pattern: + +- Register with a non-reentrant IRQ execution contract if the callback mutates captured state. +- Drop the captured handler when the IRQ action is freed, after in-flight callbacks are synchronized. +- Keep hard-IRQ work small: read/ack status, update queue-local state, and publish a minimal event or pending bit. +- Do not capture OS objects that require allocation, sleeping locks, broad poll-set locks, or file/device-manager callbacks in hard IRQ. Use an IRQ-safe notify or deferred worker bridge instead. +- Keep task-side service/config methods on a separate control endpoint. If they also touch registers, protect them with the same owner CPU, local IRQ exclusion, device interrupt mask, or documented borrow gate that prevents IRQ reentry. + +This ownership model is useful beyond serial ports: block completion queues, network RX/TX interrupt endpoints, input devices, accelerators, and mailbox controllers all benefit when "the IRQ handler" is not a shared runtime object. + +### Control / IRQ / Queue Endpoint Pattern + +For IRQ-driven runtime drivers, split runtime ownership into three endpoint families: + +```text +Control endpoint -> startup, shutdown, config, service/deferred drain +IRQ endpoint -> hard-IRQ event extraction and queue-local state publication +Queue endpoints -> submit, reclaim/read, poll synchronized completion state +``` + +The split keeps each synchronization question local: + +- Control endpoint: owned by task/worker context; may call slow OS services through OS Glue. It can perform deferred drain/service after an IRQ-safe notify. +- IRQ endpoint: owned by IRQ registration; reads and clears shared/destructive IRQ status and writes only pre-allocated completion state. +- Queue endpoint: owned by the runtime user or protected by the OS runtime's queue lock; consumes queue-local permits/completions/errors without borrowing the IRQ endpoint. + +Return these parts explicitly from constructors: + +```rust +pub struct DeviceParts { + pub control: Arc, + pub irq: IrqHandler, + pub queues: Q, +} +``` + +Prefer putting OS-side locks around queue endpoints in OS Glue or the consumer runtime, not in the portable driver core. The portable crate should express what needs exclusive access; the OS chooses `SpinNoIrq`, mutexes, futures, per-CPU routing, or worker serialization. + +Review smell: if task code calls `irq.handle_irq()` directly, if IRQ code locks the same queue mutex as task context, or if queues call a raw `poll_status()` that can clear another queue's event, the endpoint split is not yet enforcing the intended model. + ### IRQ/Task Exclusion Pattern Some drivers need IRQ completion code to consult state that is registered or @@ -185,6 +241,7 @@ For devices with split runtime endpoints, treat the IRQ handle as a state synchr - Give the IRQ handle its own endpoint object, separate from TX/RX queues, completion queues, block queues, network rings, or accelerator engines. - Let the IRQ handle be the only runtime path that reads and clears shared or destructive interrupt/status registers. Queue-side code should not rediscover readiness by peeking the same global register, because that can clear or consume another queue's event. - Fan out IRQ results into queue-local completion state, for example per-queue atomics, bitmaps, counters, or pending lists. The state should name the affected queue or engine and preserve errors separately from readiness. +- Keep TX/RX, submit/completion, or per-queue completion states independent even when hardware reports them in one combined status register. Split combined status immediately in the IRQ endpoint before queue code observes it. - If an IRQ arrives while another context owns the raw register block, record a pending IRQ bit and return quickly. Drain it from a safe context or the next IRQ pass instead of spinning in interrupt context. - Keep raw driver event snapshots close to hardware semantics. Put OS wakeups, task scheduling, and per-queue completion ownership in the adapter/runtime layer above the raw register code. @@ -216,6 +273,7 @@ In an IRQ-driven split design, queue operations consume synchronized queue-local - Queue `reclaim`/`try_read` should consume that queue's own completion or error state. Do not let one queue consume another queue's event because a shared register reported combined status. - If a hardware status register reports multiple queues or directions in one destructive read, split that status immediately in the IRQ/event layer and store independent queue-local state before any queue code runs. - For FIFO-style devices where one readiness interrupt may cover a bounded burst, model the budget explicitly if more than one operation can be performed. Avoid hidden loops that re-read global status from a queue path. +- Queue APIs should make the distinction between "hardware polling" and "consume synchronized state" explicit. A low-level raw `poll_status()` can exist for early boot or polling-only users, but an IRQ-driven queue endpoint should not call it behind the user's back. For a block queue adapter, align portable queue state with `rdif_block::IQueue`: @@ -230,6 +288,7 @@ For a block queue adapter, align portable queue state with `rdif_block::IQueue`: - Do not make OS locks part of the portable Driver Trait. - Do not take a blocking mutex from an IRQ handler when task context can hold the same mutex. Use a non-blocking borrow gate, try-lock with explicit pending state, or a small atomic/interrupt-safe state handoff. - Use internal locks only for short non-IRQ critical sections such as pending flags or small status updates. In IRQ context, prefer atomics, per-queue pending bits, or an explicit deferred drain path. +- Do not hide IRQ endpoint sharing behind `Arc>` or `Arc>` when the endpoint can instead be moved into the IRQ callback. Shared locks make it too easy for task context to call or hold the same state the hard IRQ needs. - If task and IRQ contexts share a lock-protected registry, require the IRQ/task exclusion protocol above: mask the same interrupt source before task-side mutation, keep IRQ lock-free for that registry, and document why the fast path @@ -246,5 +305,8 @@ For a block queue adapter, align portable queue state with `rdif_block::IQueue`: - Is MMIO mapping handled by `mmio-api` or a clear OS Glue boundary? - Is DMA handled through `dma-api` with mask, alignment, direction, lifetime, and address-type clarity? - Does IRQ code return events rather than directly performing OS notification? +- Is a stateful IRQ endpoint owned by the registered callback instead of shared through a public `Arc` or lock? +- Are control, IRQ handler, and queue endpoints separated with clear owners? +- Do queues consume queue-local synchronized state rather than re-reading shared/destructive IRQ registers? - Are queues independent enough to support blocking / poll / future / worker runtimes? - Did validation include `cargo fmt` and targeted `cargo xtask clippy --package `? From e905b110f8fc1d7a12f819191dfe0cab69ee4d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 11:52:19 +0800 Subject: [PATCH 15/16] docs(some-serial): update raw uart doctest --- drivers/serial/some-serial/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/serial/some-serial/src/lib.rs b/drivers/serial/some-serial/src/lib.rs index 3323a1dd91..bd0e060d80 100644 --- a/drivers/serial/some-serial/src/lib.rs +++ b/drivers/serial/some-serial/src/lib.rs @@ -30,7 +30,7 @@ //! ```rust,no_run //! use core::ptr::NonNull; //! -//! use some_serial::{Config, RawUart as _, ns16550::Ns16550}; +//! use some_serial::{Config, RawUart as _, ns16550::Ns16550, pl011::Pl011}; //! //! // 选择合适的驱动 //! #[cfg(target_arch = "aarch64")] @@ -48,10 +48,10 @@ //! //! uart.startup(&config).unwrap(); //! -//! while !uart.tx_ready() { +//! while !uart.poll_status().tx_ready() { //! core::hint::spin_loop(); //! } -//! uart.write_tx(b'h'); +//! uart.write_byte(b'h'); //! ``` #[cfg(test)] From 81e1e1458b64448a24fd4d7bd48fb2c153f20001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Thu, 25 Jun 2026 14:12:57 +0800 Subject: [PATCH 16/16] fix(somehal): keep hardware console selection --- platforms/somehal/src/boot_console.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/platforms/somehal/src/boot_console.rs b/platforms/somehal/src/boot_console.rs index 77f0d6b12f..7c888d3de2 100644 --- a/platforms/somehal/src/boot_console.rs +++ b/platforms/somehal/src/boot_console.rs @@ -34,17 +34,19 @@ fn device_id_from_bootargs_with( serial_device_id: impl Fn(usize) -> Option, ) -> Result { let cmdline = cmdline.ok_or(ConsoleDeviceIdError::NotSpecified)?; - let mut last_spec = None; + let mut has_console_spec = false; + let mut last_hardware_serial = None; for spec in console_specs(cmdline) { - last_spec = Some(spec); + has_console_spec = true; + if let ConsoleSpec::HardwareSerial(index) = spec { + last_hardware_serial = Some(index); + } } - match last_spec { - Some(ConsoleSpec::HardwareSerial(index)) => { - serial_device_id(index).ok_or(ConsoleDeviceIdError::DeviceNotFound) - } - Some(ConsoleSpec::VirtualTty) => Err(ConsoleDeviceIdError::NoHardwareDevice), + match last_hardware_serial { + Some(index) => serial_device_id(index).ok_or(ConsoleDeviceIdError::DeviceNotFound), + None if has_console_spec => Err(ConsoleDeviceIdError::NoHardwareDevice), None => Err(ConsoleDeviceIdError::NotSpecified), } } @@ -179,19 +181,19 @@ mod tests { ); assert_eq!( device_id_from_bootargs(Some("console=ttyS2 console=tty1")), - Err(ConsoleDeviceIdError::NoHardwareDevice) + Err(ConsoleDeviceIdError::DeviceNotFound) ); } #[test] - fn later_virtual_console_overrides_earlier_serial_console() { + fn later_virtual_console_does_not_hide_hardware_device_id() { let serial2_device = DeviceId::from(42); assert_eq!( device_id_from_bootargs_with(Some("console=ttyS2,1500000 console=tty1"), |index| { (index == 2).then_some(serial2_device) }), - Err(ConsoleDeviceIdError::NoHardwareDevice) + Ok(serial2_device) ); } @@ -217,7 +219,7 @@ mod tests { Some("console=ttyS2,1500000 console=ttyS3,115200 console=tty1"), |index| (index == 2).then_some(serial2_device), ), - Err(ConsoleDeviceIdError::NoHardwareDevice) + Err(ConsoleDeviceIdError::DeviceNotFound) ); assert_eq!( device_id_from_bootargs_with(