diff --git a/Cargo.toml b/Cargo.toml index 5466dcf..5c24485 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "platforms/axplat-aarch64-phytium-pi", "platforms/axplat-riscv64-qemu-virt", "platforms/axplat-loongarch64-qemu-virt", + "platforms/axplat-arm-qemu-virt", "examples/hello-kernel", "examples/irq-kernel", diff --git a/platforms/axplat-arm-qemu-virt/Cargo.toml b/platforms/axplat-arm-qemu-virt/Cargo.toml new file mode 100644 index 0000000..09cbfd1 --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "axplat-arm-qemu-virt" +version = "0.3.0" +edition = "2024" + +[features] +irq = ["axplat/irq"] +smp = ["axplat/smp"] +rtc = [] +fp-simd = ["axcpu/fp-simd"] +default = [] + +[dependencies] +axconfig-macros = "0.2" +memory_addr = "0.4" +kspin = "0.1" +axcpu = "0.2" +arm_pl011 = "0.1" +lazy_static = { version = "1.5", features = ["spin_no_std"] } +page_table_entry = "0.5" +aarch32-cpu = "0.1" +log = "0.4" +axplat = { workspace = true} +axplat-aarch64-peripherals = { version = "0.3", path = "../axplat-aarch64-peripherals" } \ No newline at end of file diff --git a/platforms/axplat-arm-qemu-virt/axconfig.toml b/platforms/axplat-arm-qemu-virt/axconfig.toml new file mode 100644 index 0000000..fa016c2 --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/axconfig.toml @@ -0,0 +1,86 @@ +# Architecture identifier. +arch = "arm" # str +# Platform identifier. +platform = "arm-qemu-virt" # str +# Platform package. +package = "axplat-arm-qemu-virt" # str + +# +# Platform configs +# +[plat] +# Maximum number of CPUs. For platforms that do not support runtime CPU number +# detection, it's also the number of CPUs to boot. +max-cpu-num = 1 # uint +# Base address of the whole physical memory. +phys-memory-base = 0x4000_0000 # uint +# Size of the whole physical memory. (128M) +phys-memory-size = 0x800_0000 # uint +# Base physical address of the kernel image. +kernel-base-paddr = 0x4001_0000 # uint +# Base virtual address of the kernel image. +kernel-base-vaddr = "0x4001_0000" # uint +# Linear mapping offset, for quick conversions between physical and virtual +# addresses. +phys-virt-offset = "0x0000" # uint +# Offset of bus address and phys address. some boards, the bus address is +# different from the physical address. +phys-bus-offset = 0 # uint +# Kernel address space base. +kernel-aspace-base = "0x0000" # uint +# Kernel address space size. +kernel-aspace-size = "0xffff_f000" # uint +# Stack size on bootstrapping. (256K) +boot-stack-size = 0x40000 # uint + +# PSCI +psci-method = "hvc" # str + +# +# Device specifications +# +[devices] +# MMIO ranges with format (`base_paddr`, `size`). +mmio-ranges = [ + [0x0900_0000, 0x1000], # PL011 UART + [0x0910_0000, 0x1000], # PL031 RTC + [0x0800_0000, 0x2_0000], # GICv2 + [0x0a00_0000, 0x4000], # VirtIO + [0x1000_0000, 0x2eff_0000], # PCI memory ranges (ranges 1: 32-bit MMIO space) +] # [(uint, uint)] +# VirtIO MMIO ranges with format (`base_paddr`, `size`). +virtio-mmio-ranges = [ + +] # [(uint, uint)] +# Base physical address of the PCIe ECAM space. +pci-ecam-base = 0x40_1000_0000 # uint +# End PCI bus number (`bus-range` property in device tree). +pci-bus-end = 0xff # uint +# PCI device memory ranges (`ranges` property in device tree). +pci-ranges = [ + [0x3ef_f0000, 0x1_0000], # PIO space + [0x1000_0000, 0x2eff_0000], # 32-bit MMIO space +] # [(uint, uint)] +# UART Address +uart-paddr = 0x0900_0000 # uint +# UART IRQ number (SPI, 1) +uart-irq = 33 # uint +# Timer interrupt num (PPI, physical timer). +timer-irq = 30 # uint +# IPI interrupt num +ipi-irq = 1 # uint + +# GIC CPU Interface base address +gicc-paddr = 0x0801_0000 # uint +# GIC Distributor base address +gicd-paddr = 0x0800_0000 # uint + +# pl031@9010000 { +# clock-names = "apb_pclk"; +# clocks = <0x8000>; +# interrupts = <0x00 0x02 0x04>; +# reg = <0x00 0x9010000 0x00 0x1000>; +# compatible = "arm,pl031\0arm,primecell"; +# }; +# RTC (PL031) Address +rtc-paddr = 0x901_0000 # uint \ No newline at end of file diff --git a/platforms/axplat-arm-qemu-virt/build.rs b/platforms/axplat-arm-qemu-virt/build.rs new file mode 100644 index 0000000..12b902d --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/build.rs @@ -0,0 +1,6 @@ +fn main() { + println!("cargo:rerun-if-env-changed=AX_CONFIG_PATH"); + if let Ok(config_path) = std::env::var("AX_CONFIG_PATH") { + println!("cargo:rerun-if-changed={config_path}"); + } +} diff --git a/platforms/axplat-arm-qemu-virt/src/boot.rs b/platforms/axplat-arm-qemu-virt/src/boot.rs new file mode 100644 index 0000000..e5d331e --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/boot.rs @@ -0,0 +1,144 @@ +//! Early boot initialization code for ARMv7-A. + +use axplat::mem::pa; +use page_table_entry::{GenericPTE, MappingFlags, arm::A32PTE}; + +use crate::config::plat::BOOT_STACK_SIZE; + +/// Boot stack, 256KB +#[unsafe(link_section = ".bss.stack")] +pub static mut BOOT_STACK: [u8; BOOT_STACK_SIZE] = [0; BOOT_STACK_SIZE]; + +/// Compile-time constants for loading BOOT_STACK_SIZE into ARM registers +/// Split into low and high 16 bits for movw/movt instructions +const BOOT_STACK_SIZE_LOW: u16 = (BOOT_STACK_SIZE & 0xFFFF) as u16; +const BOOT_STACK_SIZE_HIGH: u16 = ((BOOT_STACK_SIZE >> 16) & 0xFFFF) as u16; + +/// ARMv7-A L1 page table (16KB, contains 4096 entries) +/// Must be 16KB aligned for TTBR0 +#[repr(align(16384))] +struct Aligned16K(T); + +impl Aligned16K { + const fn new(inner: T) -> Self { + Self(inner) + } +} + +#[unsafe(link_section = ".data.page_table")] +static mut BOOT_PT_L1: Aligned16K<[A32PTE; 4096]> = Aligned16K::new([A32PTE::empty(); 4096]); + +/// Initialize boot page table. +/// This function is unsafe as it modifies global static variables. +#[unsafe(no_mangle)] +pub unsafe fn init_boot_page_table() { + unsafe { + // Map memory regions using 1MB sections (ARMv7-A max granularity) + // QEMU virt machine memory layout (with -m 128M): + // - 0x00000000..0x08000000: Unmapped/reserved + // - 0x08000000..0x40000000: MMIO devices (Flash, UART, GIC, PCIe, etc.) + // - 0x40000000..0x48000000: 128MB RAM + + // 0x0000_0000..0x0800_0000 (0-128MB): Identity map as device memory for safety + // This prevents accidental access to undefined regions + for i in 0..0x80 { + BOOT_PT_L1.0[i] = A32PTE::new_page( + pa!(i * 0x10_0000), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::DEVICE, + true, + ); + } + + // 0x0800_0000..0x4000_0000 (128MB-1GB): Device memory (MMIO) + // Includes: Flash, PL011 UART, PL031 RTC, GICv2, PCIe, VirtIO devices + for i in 0x80..0x400 { + BOOT_PT_L1.0[i] = A32PTE::new_page( + pa!(i * 0x10_0000), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::DEVICE, + true, + ); + } + + // 0x4000_0000..0x4800_0000 (1GB-1GB+128MB): RAM, RWX + // This is where QEMU loads the kernel and provides working memory + for i in 0x400..0x480 { + BOOT_PT_L1.0[i] = A32PTE::new_page( + pa!(i * 0x10_0000), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::EXECUTE, + true, // 1MB section + ); + } + + // 0x4800_0000..0x1_0000_0000 (1GB+128MB-4GB): Unmapped or device memory + // Map remaining space as device memory to avoid faults + for i in 0x480..0x1000 { + BOOT_PT_L1.0[i] = A32PTE::new_page( + pa!(i * 0x10_0000), + MappingFlags::READ | MappingFlags::WRITE | MappingFlags::DEVICE, + true, + ); + } + } +} + +#[unsafe(naked)] +#[unsafe(no_mangle)] +#[unsafe(link_section = ".text.boot")] +pub unsafe extern "C" fn _start() -> ! { + core::arch::naked_asm!( + " + // Set stack pointer to top of BOOT_STACK + // Use movw/movt to load BOOT_STACK_SIZE (split into 16-bit parts) + ldr sp, ={boot_stack} + movw r0, #{stack_size_low} // Lower 16 bits of BOOT_STACK_SIZE + movt r0, #{stack_size_high} // Upper 16 bits of BOOT_STACK_SIZE + add sp, sp, r0 + + // Initialize page table + bl {init_pt} + + // Enable MMU + ldr r0, ={boot_pt} // Use ldr= pseudo-instruction for full 32-bit address + bl {enable_mmu} + + // Jump to Rust entry + bl {rust_entry} + 1: b 1b", + boot_stack = sym BOOT_STACK, + boot_pt = sym BOOT_PT_L1, + stack_size_low = const BOOT_STACK_SIZE_LOW, + stack_size_high = const BOOT_STACK_SIZE_HIGH, + init_pt = sym init_boot_page_table, + enable_mmu = sym axcpu::init::init_mmu, + rust_entry = sym axplat::call_main, + ) +} + +/// The earliest entry point for the secondary CPUs. +#[cfg(feature = "smp")] +#[unsafe(naked)] +pub(crate) unsafe extern "C" fn _start_secondary() -> ! { + // R0 = stack pointer (passed from primary CPU) + core::arch::naked_asm!( + " + // Save stack pointer from R0 + mov sp, r0 + + // Get CPU ID from MPIDR + mrc p15, 0, r4, c0, c0, 5 // Read MPIDR + and r4, r4, #0xff // Extract CPU ID (Aff0) + + // Enable MMU (page table already initialized by primary CPU) + ldr r0, ={boot_pt} // Use ldr= for full 32-bit address + bl {init_mmu} + + // Call secondary main entry with CPU ID + mov r0, r4 // Pass CPU ID as argument + ldr r1, ={entry} + blx r1 // Use blx (not blr, that's AArch64) + 1: b 1b", + boot_pt = sym BOOT_PT_L1, + init_mmu = sym axcpu::init::init_mmu, + entry = sym axplat::call_secondary_main, + ) +} diff --git a/platforms/axplat-arm-qemu-virt/src/generic_timer.rs b/platforms/axplat-arm-qemu-virt/src/generic_timer.rs new file mode 100644 index 0000000..de9c201 --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/generic_timer.rs @@ -0,0 +1,96 @@ +use core::arch::asm; + +const NANOS_PER_SEC: u64 = 1_000_000_000; + +struct TimeIfImpl; + +/// Read the CNTFRQ register (Counter-timer Frequency register) +#[inline] +fn timer_frequency() -> u64 { + let freq: u32; + unsafe { + // CNTFRQ: c14, c0, 0 + asm!("mrc p15, 0, {}, c14, c0, 0", out(reg) freq); + } + freq as u64 +} + +/// Read the CNTVCT register (Counter-timer Virtual Count register) +#[inline] +fn timer_counter() -> u64 { + let low: u32; + let high: u32; + unsafe { + // CNTVCT: c14 + asm!("mrrc p15, 1, {}, {}, c14", out(reg) low, out(reg) high); + } + ((high as u64) << 32) | (low as u64) +} + +/// Write CNTP_CVAL register (Counter-timer Physical Timer CompareValue register) +#[inline] +fn write_timer_comparevalue(value: u64) { + let low = value as u32; + let high = (value >> 32) as u32; + unsafe { + // CNTP_CVAL: c14, c2 + asm!("mcrr p15, 2, {}, {}, c14", in(reg) low, in(reg) high); + } +} + +/// Write Physical Timer Control register (CNTP_CTL) +#[inline] +fn write_timer_control(control: u32) { + unsafe { + // CNTP_CTL: c14, c2 + asm!("mcr p15, 0, {}, c14, c2, 1", in(reg) control); + } +} + +/// Returns the current clock time in hardware ticks. +#[impl_plat_interface] +impl axplat::time::TimeIf for TimeIfImpl { + fn current_ticks() -> u64 { + timer_counter() + } + + /// Converts hardware ticks to nanoseconds. + fn ticks_to_nanos(ticks: u64) -> u64 { + let freq = timer_frequency(); + ticks * NANOS_PER_SEC / freq + } + + /// Converts nanoseconds to hardware ticks. + fn nanos_to_ticks(nanos: u64) -> u64 { + let freq = timer_frequency(); + nanos * freq / NANOS_PER_SEC + } + + fn epochoffset_nanos() -> u64 { + 0 + } + + /// Set a one-shot timer. + /// + /// A timer interrupt will be triggered at the specified monotonic time deadline (in nanoseconds). + #[cfg(feature = "irq")] + fn set_oneshot_timer(deadline_ns: u64) { + let current_ns = Self::ticks_to_nanos(Self::current_ticks()); + if deadline_ns > current_ns { + let ticks = Self::nanos_to_ticks(deadline_ns - current_ns); + write_timer_comparevalue(Self::current_ticks() + ticks); + write_timer_control(1); // Enable timer + } else { + // Deadline has passed, trigger immediately + write_timer_comparevalue(Self::current_ticks()); + write_timer_control(1); // Enable timer + } + } +} + +/// Enable timer interrupts +#[cfg(feature = "irq")] +pub fn enable_irqs(_irq_num: usize) { + write_timer_comparevalue(timer_counter()); + write_timer_control(1); +} diff --git a/platforms/axplat-arm-qemu-virt/src/init.rs b/platforms/axplat-arm-qemu-virt/src/init.rs new file mode 100644 index 0000000..cf15b5c --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/init.rs @@ -0,0 +1,62 @@ +use axplat::mem::phys_to_virt; +use memory_addr::pa; + +use crate::config::plat::PSCI_METHOD; + +#[cfg(feature = "irq")] +const TIMER_IRQ: usize = crate::config::devices::TIMER_IRQ; + +struct InitIfImpl; + +#[impl_plat_interface] +impl axplat::init::InitIf for InitIfImpl { + /// Initializes the platform at the early stage for the primary core. + /// + /// This function should be called immediately after the kernel has booted, + /// and performed earliest platform configuration and initialization (e.g., + /// early console, clocking). + fn init_early(_cpu_id: usize, _dtb: usize) { + axcpu::init::init_trap(); + axplat_aarch64_peripherals::pl011::init_early(phys_to_virt(pa!( + crate::config::devices::UART_PADDR + ))); + axplat_aarch64_peripherals::psci::init(PSCI_METHOD); + + axplat::console_println!("init_early on QEMU VIRT platform"); + #[cfg(feature = "rtc")] + axplat_aarch64_peripherals::pl031::init_early(phys_to_virt(pa!(RTC_PADDR))); + } + + /// Initializes the platform at the early stage for secondary cores. + #[cfg(feature = "smp")] + fn init_early_secondary(_cpu_id: usize) { + axcpu::init::init_trap(); + } + + /// Initializes the platform at the later stage for the primary core. + /// + /// This function should be called after the kernel has done part of its + /// initialization (e.g, logging, memory management), and finalized the rest of + /// platform configuration and initialization. + fn init_later(_cpu_id: usize, _dtb: usize) { + #[cfg(feature = "irq")] + { + axplat_aarch64_peripherals::gic::init_gic( + phys_to_virt(pa!(crate::config::devices::GICD_PADDR)), + phys_to_virt(pa!(crate::config::devices::GICC_PADDR)), + ); + axplat_aarch64_peripherals::gic::init_gicc(); + crate::generic_timer::enable_irqs(TIMER_IRQ); + } + } + + /// Initializes the platform at the later stage for secondary cores. + #[cfg(feature = "smp")] + fn init_later_secondary(_cpu_id: usize) { + #[cfg(feature = "irq")] + { + axplat_aarch64_peripherals::gic::init_gicc(); + crate::generic_timer::enable_irqs(TIMER_IRQ); + } + } +} diff --git a/platforms/axplat-arm-qemu-virt/src/lib.rs b/platforms/axplat-arm-qemu-virt/src/lib.rs new file mode 100644 index 0000000..75d09f3 --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/lib.rs @@ -0,0 +1,30 @@ +#![no_std] + +#[macro_use] +extern crate axplat; + +pub mod config { + //! Platform configuration module. + //! + //! If the `AX_CONFIG_PATH` environment variable is set, it will load the configuration from the specified path. + //! Otherwise, it will fall back to the `axconfig.toml` file in the current directory and generate the default configuration. + //! + //! If the `PACKAGE` field in the configuration does not match the package name, it will panic with an error message. + axconfig_macros::include_configs!(path_env = "AX_CONFIG_PATH", fallback = "axconfig.toml"); + assert_str_eq!( + PACKAGE, + env!("CARGO_PKG_NAME"), + "`PACKAGE` field in the configuration does not match the Package name. Please check your configuration file." + ); +} + +mod boot; +mod generic_timer; +mod init; +mod mem; +mod power; + +axplat_aarch64_peripherals::console_if_impl!(ConsoleIfImpl); + +#[cfg(feature = "irq")] +axplat_aarch64_peripherals::irq_if_impl!(IrqIfImpl); diff --git a/platforms/axplat-arm-qemu-virt/src/mem.rs b/platforms/axplat-arm-qemu-virt/src/mem.rs new file mode 100644 index 0000000..96fda4f --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/mem.rs @@ -0,0 +1,43 @@ +use axplat::mem::{PhysAddr, RawRange, VirtAddr, pa, va}; + +use crate::config::devices::MMIO_RANGES; +use crate::config::plat::{PHYS_MEMORY_BASE, PHYS_MEMORY_SIZE, PHYS_VIRT_OFFSET}; + +struct MemIfImpl; + +#[impl_plat_interface] +impl axplat::mem::MemIf for MemIfImpl { + /// Returns all physical memory (RAM) ranges on the platform. + /// + /// All memory ranges except reserved ranges (including the kernel loaded + /// range) are free for allocation. + fn phys_ram_ranges() -> &'static [RawRange] { + &[(PHYS_MEMORY_BASE, PHYS_MEMORY_SIZE)] + } + + /// Returns all reserved physical memory ranges on the platform. + /// + /// Reserved memory can be contained in [`phys_ram_ranges`], they are not + /// allocatable but should be mapped to kernel's address space. + /// + /// Note that the ranges returned should not include the range where the + /// kernel is loaded. + fn reserved_phys_ram_ranges() -> &'static [RawRange] { + &[] + } + + /// Returns all device memory (MMIO) ranges on the platform. + fn mmio_ranges() -> &'static [RawRange] { + &MMIO_RANGES + } + + /// Translates a physical address to a virtual address. + fn phys_to_virt(paddr: PhysAddr) -> VirtAddr { + va!(paddr.as_usize() + PHYS_VIRT_OFFSET) + } + + /// Translates a virtual address to a physical address. + fn virt_to_phys(vaddr: VirtAddr) -> PhysAddr { + pa!(vaddr.as_usize() - PHYS_VIRT_OFFSET) + } +} diff --git a/platforms/axplat-arm-qemu-virt/src/power.rs b/platforms/axplat-arm-qemu-virt/src/power.rs new file mode 100644 index 0000000..2443745 --- /dev/null +++ b/platforms/axplat-arm-qemu-virt/src/power.rs @@ -0,0 +1,28 @@ +use axplat::power::PowerIf; + +struct PowerImpl; + +#[impl_plat_interface] +impl PowerIf for PowerImpl { + /// Bootstraps the given CPU core with the given initial stack (in physical + /// address). + /// + /// Where `cpu_id` is the logical CPU ID (0, 1, ..., N-1, N is the number of + /// CPU cores on the platform). + #[cfg(feature = "smp")] + fn cpu_boot(cpu_id: usize, stack_top_paddr: usize) { + use axplat::mem::{va, virt_to_phys}; + let entry_paddr = virt_to_phys(va!(crate::boot::_start_secondary as *const () as usize)); + axplat_aarch64_peripherals::psci::cpu_on(cpu_id, entry_paddr.as_usize(), stack_top_paddr); + } + + /// Shutdown the whole system. + fn system_off() -> ! { + axplat_aarch64_peripherals::psci::system_off() + } + + /// CPU num + fn cpu_num() -> usize { + crate::config::plat::MAX_CPU_NUM + } +}