From 92f63a23b6b1d093ab1ceb95148dd264adeb0aac Mon Sep 17 00:00:00 2001 From: PeterWrighten Date: Wed, 12 Nov 2025 19:58:55 +0900 Subject: [PATCH 1/2] doc(rv32/64): update memory allocator doc --- mdbook/src/internal/memory_allocator.md | 92 +++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/mdbook/src/internal/memory_allocator.md b/mdbook/src/internal/memory_allocator.md index 8448f7da8..8b96a14a6 100644 --- a/mdbook/src/internal/memory_allocator.md +++ b/mdbook/src/internal/memory_allocator.md @@ -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 +} +``` From 00aac1379e7a6764075a3218b8ab16535f407f83 Mon Sep 17 00:00:00 2001 From: PeterWrighten Date: Tue, 16 Jun 2026 22:15:37 +0900 Subject: [PATCH 2/2] doc(rv32/64): add docs of RV32/64 about vm, interrupt controller and pagetable --- mdbook/src/internal/arch/mapper.md | 107 +++++++++++++ mdbook/src/internal/interrupt_controller.md | 131 ++++++++++++++++ mdbook/src/internal/page_table.md | 158 ++++++++++++++++++++ 3 files changed, 396 insertions(+) diff --git a/mdbook/src/internal/arch/mapper.md b/mdbook/src/internal/arch/mapper.md index 3869e78ac..e20064124 100644 --- a/mdbook/src/internal/arch/mapper.md +++ b/mdbook/src/internal/arch/mapper.md @@ -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 +} +``` diff --git a/mdbook/src/internal/interrupt_controller.md b/mdbook/src/internal/interrupt_controller.md index 66c36cc74..0ed75f918 100644 --- a/mdbook/src/internal/interrupt_controller.md +++ b/mdbook/src/internal/interrupt_controller.md @@ -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. @@ -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> { + 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. diff --git a/mdbook/src/internal/page_table.md b/mdbook/src/internal/page_table.md index 36ff1993b..598398e48 100644 --- a/mdbook/src/internal/page_table.md +++ b/mdbook/src/internal/page_table.md @@ -136,3 +136,161 @@ impl crate::paging::PageTable, &'static str> for PageT } } ``` + +## RISC-V 32-bit (RV32) + +For RV32, the `PageTable` structure is defined in +[awkernel_lib/src/arch/rv32/page_table.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv32/page_table.rs). +It implements the **Sv32** translation scheme: 32-bit virtual addresses, 4 KiB pages +and a **2-level** page table. Each page table entry (PTE) holds a physical page number +(`PPN`) and the standard RISC-V flag bits. + +```rust +bitflags! { + /// PTE Flags for RISC-V Sv32 page table + pub struct Flags: u8 { + const V = 1 << 0; // Valid + const R = 1 << 1; // Readable + const W = 1 << 2; // Writable + const X = 1 << 3; // Executable + const U = 1 << 4; // User-accessible + const G = 1 << 5; // Global + const A = 1 << 6; // Accessed + const D = 1 << 7; // Dirty + } +} + +pub struct PageTable { + root_ppn: PhysPageNum, + frames: Vec, +} +``` + +The root page table is allocated from the physical frame allocator on `PageTable::new`, +and every intermediate table allocated while walking the tree is tracked in `frames` +so that it is kept alive for the lifetime of the address space. +The virtual page number is split into two 10-bit indices (`VirtPageNum::indexes`), +one per level of the Sv32 table. + +The `PageTable` structure implements the [`PageTable`:awkernel_lib/src/paging.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/paging.rs) trait as follows. +The generic `Flags` are translated into RISC-V PTE flags: entries are always made valid, +accessed and readable (`V | A | R`); writable mappings also set the dirty bit (`W | D`), +and executable mappings set `X`. + +```rust +impl crate::paging::PageTable for PageTable { + unsafe fn map_to( + &mut self, + virt_addr: VirtAddr, + phy_addr: PhyAddr, + flags: crate::paging::Flags, + _page_allocator: &mut RV32PageAllocator, + ) -> Result<(), &'static str> { + let vpn = VirtPageNum::from(virt_addr); + let ppn = PhysPageNum::from(phy_addr); + + 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; + } + + if self.map(vpn, ppn, rv_flags) { + Ok(()) + } else { + Err("Mapping failed") + } + } +} +``` + +Translation is enabled by writing the `satp` (Supervisor Address Translation and Protection) +register. For Sv32 the `token` method returns the register value with `MODE = 1` (Sv32) in +bit 31 and the root table's PPN in the low bits. + +```rust +pub fn token(&self) -> usize { + (1usize << 31) // MODE = 1 (Sv32 paging mode) + | self.root_ppn.0 // PPN of the root page table +} +``` + +## RISC-V 64-bit (RV64) + +For RV64, the `PageTable` structure is defined in +[awkernel_lib/src/arch/rv64/page_table.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv64/page_table.rs). +It implements the **Sv39** translation scheme: 39-bit virtual addresses, 4 KiB pages +and a **3-level** page table. The PTE layout and `Flags` definition are identical to RV32; +the differences are the number of levels and the `satp` encoding. + +```rust +pub struct PageTable { + root_ppn: PhysPageNum, + frames: Vec, +} +``` + +The virtual page number is split into three 9-bit indices (`VirtPageNum::indexes`), +walked by `find_pte` / `find_pte_create`; the leaf PTE is reached at level index `2`. + +The `map_to` implementation is the same as RV32 — generic `Flags` are mapped to the +RISC-V PTE flags (`V | A | R`, plus `W | D` for writable and `X` for executable pages): + +```rust +impl crate::paging::PageTable for PageTable { + unsafe fn map_to( + &mut self, + virt_addr: VirtAddr, + phy_addr: PhyAddr, + flags: crate::paging::Flags, + _page_allocator: &mut RV64PageAllocator, + ) -> Result<(), &'static str> { + let vpn = VirtPageNum::from(virt_addr); + let ppn = PhysPageNum::from(phy_addr); + + let mut rv_flags = Flags::V | Flags::A; + if flags.write { + rv_flags |= Flags::W | Flags::D; + } + rv_flags |= Flags::R; + if flags.execute { + rv_flags |= Flags::X; + } + + if self.map(vpn, ppn, rv_flags) { + Ok(()) + } else { + Err("Mapping failed") + } + } +} +``` + +For Sv39 the `token` method encodes `MODE = 8` (Sv39) in bits 63-60 and the root PPN in +the low 44 bits: + +```rust +pub fn token(&self) -> usize { + (8usize << 60) // MODE = 8 (Sv39 paging mode) + | self.root_ppn.0 // PPN of the root page table +} +``` + +The kernel address space is built in +[awkernel_lib/src/arch/rv64/vm.rs](https://github.com/tier4/awkernel/blob/main/awkernel_lib/src/arch/rv64/vm.rs): +`new_kernel` maps the kernel sections (`.text`, `.rodata`, `.data`, `.bss`) and an +identity mapping for available RAM, and `activate` installs the table by writing `satp` +and flushing the TLB: + +```rust +pub fn activate(&self) { + let satp = self.page_table.token(); + unsafe { + asm!("csrw satp, {}", in(reg) satp); + asm!("sfence.vma"); + } +} +```