Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/skills/arch-platform-porting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Current Axvisor LoongArch QEMU tests intentionally use the static `ax-hal/loonga
- **CPU runtime**: update `components/axcpu/src/<arch>` 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<rdrive::DeviceId, ConsoleDeviceIdError>` 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.
Expand Down Expand Up @@ -100,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/<linux-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

Expand Down
9 changes: 6 additions & 3 deletions .claude/skills/cross-kernel-driver/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion .claude/skills/cross-kernel-driver/references/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 30 additions & 7 deletions components/axklib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -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.
Expand All @@ -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,
},
};
}
6 changes: 5 additions & 1 deletion components/irq-framework/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CpuMask>,
pub(crate) next: *mut Action,
}
Expand All @@ -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(),
}
Expand Down
15 changes: 14 additions & 1 deletion components/irq-framework/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -38,13 +45,19 @@ impl Descriptor {

if !has_active_actions {
self.share_mode = request.share_mode;
self.affinity = request.affinity;
self.execution = request.execution;
return Ok(());
}

if self.share_mode != ShareMode::Shared || request.share_mode != ShareMode::Shared {
return Err(IrqError::Busy);
}

if self.affinity != request.affinity || self.execution != request.execution {
return Err(IrqError::Busy);
}

Ok(())
}

Expand Down
5 changes: 3 additions & 2 deletions components/irq-framework/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Loading
Loading