Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions platforms/axplat-arm-qemu-virt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
86 changes: 86 additions & 0 deletions platforms/axplat-arm-qemu-virt/axconfig.toml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions platforms/axplat-arm-qemu-virt/build.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
144 changes: 144 additions & 0 deletions platforms/axplat-arm-qemu-virt/src/boot.rs
Original file line number Diff line number Diff line change
@@ -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>(T);

impl<T> Aligned16K<T> {
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,
)
}
96 changes: 96 additions & 0 deletions platforms/axplat-arm-qemu-virt/src/generic_timer.rs
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use helpful register access functions from the aarch32_cpu crate? This crate also provides generic timer abstraction like El1PhysicalTimer .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/rust-embedded/aarch32/blob/885d39aa7cd4680d232d94db2e748cba0240dd85/aarch32-cpu/src/lib.rs#L32
This register is also available in aarch32_cpu::register::armv8r::Cntfrq, but it's only compiled for ARMv8-R targets. Since Generic Timer exists in ARMv7-A and the register encoding is identical, we use inline assembly here.

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);
}
Loading
Loading