Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
107 changes: 107 additions & 0 deletions mdbook/src/internal/arch/mapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,110 @@ To handle page tables, the `OffsetPageTable` structure defined in the [`x86_64`]

For AArch64, the `AArch64` structure implements the `Mapper` trait in [awkernel_lib/src/arch/aarch64/paging.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/aarch64/paging.rs).
To handle page tables, the `PageTable` structure defined in the [`awkernel_lib/src/arch/aarch64/page_table.rs`](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/aarch64/page_table.rs) is used.

## RISC-V 32-bit (RV32)

For RV32, the `RV32` structure implements the `Mapper` trait in
[awkernel_lib/src/arch/rv32/paging.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv32/paging.rs).
Each operation reads the **currently active** page table from the `satp` register via
`get_page_table` (see [Page Table](../page_table.md)), then delegates to the Sv32
`PageTable`. The generic `Flags` are translated into RISC-V PTE flags — entries are always
valid, accessed and readable (`V | A | R`), writable pages also set `W | D`, and executable
pages set `X`.

```rust
impl crate::paging::Mapper for super::RV32 {
unsafe fn map(
vm_addr: VirtAddr,
phy_addr: PhyAddr,
flags: crate::paging::Flags,
) -> Result<(), MapError> {
// Check address alignment
if !vm_addr.aligned() || !phy_addr.aligned() {
return Err(MapError::AddressNotAligned);
}

// Get current page table
if let Some(mut page_table) = get_page_table(vm_addr) {
let vpn = VirtPageNum::from(vm_addr);
let ppn = phy_addr.floor();

// Convert common flags to RV32 flags
let mut rv_flags = Flags::V | Flags::A; // Always valid and accessed
if flags.write {
rv_flags |= Flags::W | Flags::D; // Writable and dirty
}
rv_flags |= Flags::R; // Always readable
if flags.execute {
rv_flags |= Flags::X;
}

// Try to map the page
if page_table.map(vpn, ppn, rv_flags) {
Ok(())
} else {
Err(MapError::AlreadyMapped)
}
} else {
Err(MapError::InvalidPageTable)
}
}

// unmap / vm_to_phy omitted
}
```

`vm_to_phy` walks the active page table for the page's virtual page number, and, if the
leaf PTE is valid, reconstructs the physical address from the entry's PPN plus the page
offset.

## RISC-V 64-bit (RV64)

For RV64, the `RV64` structure implements the `Mapper` trait in
[awkernel_lib/src/arch/rv64/paging.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv64/paging.rs).
The structure mirrors RV32 but uses the Sv39 `PageTable`. Two differences are worth noting:
`map` first checks `vm_to_phy` to reject an already-mapped address, and both `map` and
`unmap` explicitly align the virtual and physical addresses down to the 4 KiB page
boundary before walking the table.

```rust
impl crate::paging::Mapper for super::RV64 {
unsafe fn map(
vm_addr: VirtAddr,
phy_addr: PhyAddr,
flags: crate::paging::Flags,
) -> Result<(), MapError> {
// Check if already mapped
if Self::vm_to_phy(vm_addr).is_some() {
return Err(MapError::AlreadyMapped);
}

let vm_addr_aligned = vm_addr.as_usize() & !(PAGESIZE - 1);
let phy_addr_aligned = phy_addr.as_usize() & !(PAGESIZE - 1);

if let Some(mut page_table) = get_page_table(VirtAddr::from_usize(vm_addr_aligned)) {
let vpn = VirtPageNum::from(VirtAddr::from_usize(vm_addr_aligned));
let ppn = PhysPageNum::from(PhyAddr::from_usize(phy_addr_aligned));

let mut rv_flags = Flags::V | Flags::A;
rv_flags |= Flags::R; // Always readable
if flags.write {
rv_flags |= Flags::W | Flags::D;
}
if flags.execute {
rv_flags |= Flags::X;
}

if page_table.map(vpn, ppn, rv_flags) {
Ok(())
} else {
Err(MapError::AlreadyMapped)
}
} else {
Err(MapError::InvalidPageTable)
}
}

// unmap / vm_to_phy omitted
}
```
131 changes: 131 additions & 0 deletions mdbook/src/internal/interrupt_controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,48 @@ pub extern "C" fn curr_el_spx_irq_el1(_ctx: *mut Context, _sp: usize, _esr: usiz
}
```

## RISC-V 64-bit (RV64)

For RV64, traps are taken in machine mode by a low-level handler installed into `mtvec`
during boot in [kernel/src/arch/rv64/boot.S](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv64/boot.S).
The handler reads `mcause` to distinguish exceptions from interrupts and dispatches on the
interrupt code: a machine timer interrupt (code 7) and a machine software interrupt / IPI
(code 3) are handled, while other causes return without action.

```asm
early_trap_handler:
csrr t0, mcause
blt t0, zero, handle_interrupt # MSB set => interrupt
j unhandled_trap

handle_interrupt:
# ... mask off the interrupt code ...
li t1, 7 # M-mode timer interrupt
beq t0, t1, handle_timer_interrupt
li t1, 3 # M-mode software interrupt (IPI)
beq t0, t1, handle_software_interrupt
```

After saving the clobbered registers, the timer path calls `riscv_handle_timer` and the
IPI path calls `riscv_handle_ipi`. Both are defined in
[kernel/src/arch/rv64/kernel_main.rs](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv64/kernel_main.rs)
and forward to the architecture-independent `handle_irqs`:

```rust
/// M-mode software interrupt (IPI) handler called from assembly.
pub extern "C" fn riscv_handle_ipi() {
awkernel_lib::interrupt::handle_irqs(true);
}

/// M-mode timer interrupt handler called from assembly.
pub extern "C" fn riscv_handle_timer() {
awkernel_lib::interrupt::handle_irqs(true);
}
```

The software-interrupt (`MSIP`) bit is cleared in assembly before returning, and the timer
is re-armed by the registered timer handler.

# Handling Preemption

A preemption request can be sent by an inter process interrupt (IPI) to the target CPU.
Expand Down Expand Up @@ -164,3 +206,92 @@ BCM2835's (Raspberry Pi 3) interrupt controller, GICv2 and GICv3 are supported f
- [BCM2835's interrupt controller](https://github.com/tier4/awkernel/tree/main/awkernel_drivers/src/interrupt_controller/bcm2835.rs)
- [GICv2](https://github.com/tier4/awkernel/tree/main/awkernel_drivers/src/interrupt_controller/gicv2.rs)
- [GICv3](https://github.com/tier4/awkernel/tree/main/awkernel_drivers/src/interrupt_controller/gicv3.rs)

## RISC-V 64-bit (RV64)

For RV64, external interrupts are managed by the **PLIC** (Platform-Level Interrupt
Controller), and inter-processor interrupts (IPIs) are delivered through the
**CLINT/ACLINT** software-interrupt (`MSIP`) registers. Both are implemented by the
`RiscvPlic` structure in
[kernel/src/arch/rv64/interrupt_controller.rs](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv64/interrupt_controller.rs).

```rust
/// RISC-V PLIC (Platform-Level Interrupt Controller) implementation.
/// Combined with CLINT/ACLINT for IPI support.
pub struct RiscvPlic {
base_address: usize,
max_priority: u32,
num_sources: u16,
}

// CLINT/ACLINT base address for IPIs.
const ACLINT_BASE: usize = 0x0200_0000;
const MSIP_OFFSET: usize = 0x0000; // Machine Software Interrupt Pending
```

The PLIC register layout is computed relative to `base_address`:

| register | address | purpose |
|----------|---------|---------|
| priority | `base + source * 4` | per-source interrupt priority (0-7) |
| enable | `base + 0x2000 + context * 0x80` | per-context enable bitmap |
| threshold | `base + 0x200000 + context * 0x1000` | per-context priority threshold |
| claim/complete | `base + 0x200004 + context * 0x1000` | claim a pending IRQ / signal completion |

The PLIC *context* for the running hart is computed as `hartid * 2 + 1` (the
supervisor-mode context), where the hart ID is read from the `mhartid` CSR.

`RiscvPlic` implements the [`InterruptController`](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/interrupt.rs) trait:

- `enable_irq` sets the source priority to `1` and sets its bit in the enable register.
- `disable_irq` clears the source's bit in the enable register.
- `pending_irqs` reads the claim register; a non-zero value is the claimed IRQ, which is
immediately written back to the claim register to signal completion.
- `send_ipi` writes `1` to the target hart's `MSIP` register; the broadcast variants do so
for every hart (skipping the sender for `send_ipi_broadcast_without_self`).
- `init_non_primary` sets the context threshold to `0` (accept all priorities) and enables
machine software interrupts (`mie.MSIE`) so the core can receive IPIs.

```rust
impl InterruptController for RiscvPlic {
fn enable_irq(&mut self, irq: u16) {
self.set_priority(irq, 1);
let context = self.get_supervisor_context();
self.enable_interrupt(context, irq);
}

fn pending_irqs(&self) -> Box<dyn Iterator<Item = u16>> {
let context = self.get_supervisor_context();
let claim_reg = self.claim_reg(context);
let mut pending = alloc::vec::Vec::new();
unsafe {
let claimed = read_volatile(claim_reg);
if claimed != 0 {
pending.push(claimed as u16);
write_volatile(claim_reg, claimed); // Complete the interrupt.
}
}
Box::new(pending.into_iter())
}

// ... send_ipi / init_non_primary / irq_range omitted ...
}
```

The controller is instantiated and registered during boot in
[kernel/src/arch/rv64/kernel_main.rs](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv64/kernel_main.rs),
with the PLIC mapped at `0x0c00_0000` and 128 interrupt sources:

```rust
const PLIC_BASE: usize = 0x0c000000;
const NUM_SOURCES: u16 = 128;

let plic = Box::new(RiscvPlic::new(PLIC_BASE, NUM_SOURCES));
awkernel_lib::interrupt::register_interrupt_controller(plic);
```

## RISC-V 32-bit (RV32)

For RV32, a PLIC-based `InterruptController` is not yet implemented;
[awkernel_lib/src/arch/rv32/interrupt.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv32/interrupt.rs)
currently provides only the low-level enable/disable of interrupts via the `sstatus` CSR.
92 changes: 92 additions & 0 deletions mdbook/src/internal/memory_allocator.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,95 @@ unsafe fn primary_cpu(device_tree_base: usize) {
// omitted
}
```

## RISC-V 32-bit (RV32)

For RISC-V 32-bit, the primary and backup allocators are initialized
in `primary_hart` function defined in
[kernel/src/arch/rv32/kernel_main.rs](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv32/kernel_main.rs) as follows.

**Unlike x86_64 and AArch64, RISC-V initializes heap allocators BEFORE setting up virtual memory.**
This is because the page table allocation itself requires heap memory.
The initialization order is:
1. Initialize heap allocators (lines 89-92)
2. Initialize page allocator (line 123)
3. Initialize and activate virtual memory (lines 126-129)

```rust
unsafe fn primary_hart(hartid: usize) {
// omitted

// setup the VM
let backup_start = HEAP_START;
let backup_size = BACKUP_HEAP_SIZE;
let primary_start = HEAP_START + BACKUP_HEAP_SIZE;
let primary_size = HEAP_SIZE;

// enable heap allocator
heap::init_primary(primary_start, primary_size);
heap::init_backup(backup_start, backup_size);
heap::TALLOC.use_primary_then_backup(); // use backup allocator

// omitted

// Initialize memory management (page allocator)
awkernel_lib::arch::rv32::init_page_allocator();

// Initialize virtual memory system
awkernel_lib::arch::rv32::init_kernel_space();

// Activate virtual memory (enable MMU and page tables)
awkernel_lib::arch::rv32::activate_kernel_space();

// omitted
}
```

## RISC-V 64-bit (RV64)

For RISC-V 64-bit, the primary and backup allocators are initialized
in `init_heap_allocation` function defined in
[kernel/src/arch/rv64/kernel_main.rs](https://github.com/tier4/awkernel/blob/main/kernel/src/arch/rv64/kernel_main.rs) as follows.

**Like RV32, RV64 also initializes heap allocators BEFORE setting up virtual memory,**
as stated in the comment "Setup heap allocation FIRST - page tables need this!"
The initialization order in `primary_hart` function is:
1. Initialize UART for early debugging (line 241)
2. Setup heap allocation (line 244, calling `init_heap_allocation()`)
3. Initialize memory management including page allocator and virtual memory (line 247, calling `init_memory_management()`)
4. Initialize architecture-specific features, console, timer, and interrupts

RV64 has additional unique features:
- Uses the `ekernel` symbol to determine heap start address dynamically
- Implements dynamic heap sizing with `get_heap_size()` based on available memory
- Has fallback logic for minimal heap configuration (64MB) when memory is limited

```rust
unsafe fn init_heap_allocation() {
// omitted

extern "C" {
fn ekernel();
}

let heap_start = (ekernel as usize + 0xfff) & !0xfff; // Align to 4K
let backup_size = BACKUP_HEAP_SIZE;
let total_heap_size = awkernel_lib::arch::rv64::get_heap_size();

if total_heap_size <= backup_size {
// Use minimal heap if not enough memory
let primary_size = 64 * 1024 * 1024; // 64MB minimum
heap::init_primary(heap_start + backup_size, primary_size);
heap::init_backup(heap_start, backup_size);
} else {
// Use dynamic calculation
let primary_size = total_heap_size - backup_size;
heap::init_primary(heap_start + backup_size, primary_size);
heap::init_backup(heap_start, backup_size);
}

heap::TALLOC.use_primary_then_backup();

// omitted
}
```
Loading
Loading