diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08b56d10..0982a1c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -230,6 +230,7 @@ jobs: - test-qemu-v6 - test-qemu-v7r - test-qemu-v7a + - test-qemu-v7a-zynq - test-qemu-v8r - test-qemu-v8r-el2 steps: diff --git a/examples/xilinx-zynq-a9/.cargo/config.toml b/examples/xilinx-zynq-a9/.cargo/config.toml index 62fb41fc..69bebe73 100644 --- a/examples/xilinx-zynq-a9/.cargo/config.toml +++ b/examples/xilinx-zynq-a9/.cargo/config.toml @@ -1,14 +1,14 @@ [target.armv7a-none-eabihf] -runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -kernel" +runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -smp 2 -kernel" [target.thumbv7a-none-eabihf] -runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -kernel" +runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -smp 2 -kernel" [target.armv7a-none-eabi] -runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -kernel" +runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -smp 2 -kernel" [target.thumbv7a-none-eabi] -runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -kernel" +runner = "qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -smp 2 -kernel" [build] target = "armv7a-none-eabihf" diff --git a/examples/xilinx-zynq-a9/README.md b/examples/xilinx-zynq-a9/README.md index 1da002f1..0cf1bc2c 100644 --- a/examples/xilinx-zynq-a9/README.md +++ b/examples/xilinx-zynq-a9/README.md @@ -22,6 +22,17 @@ Zynq-7000, not the real thing. [`.cargo/config.toml`]: ./.cargo/config.toml [`rust-toolchain.toml`]: ./rust-toolchain.toml +## Examples + +- `hello` - prints a message over semihosting and then panics +- `gic` - sets up the memory-mapped GIC (the GICv2 programming model), sends a + Software Generated Interrupt to this core, and handles it +- `smp` - boots the second core and checks that atomics and critical sections + work across both cores + +The Zynq-7000 is a dual-core device, so QEMU is run with `-smp 2`. The +single-core examples simply leave the second core parked. + ## Running Run these examples as follows: @@ -29,7 +40,7 @@ Run these examples as follows: ```console $ cargo run --bin hello Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.80s - Running `qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -kernel target/armv7a-none-eabihf/debug/hello` + Running `qemu-system-arm -machine xilinx-zynq-a9 -cpu cortex-a9 -semihosting -nographic -audio none -smp 2 -kernel target/armv7a-none-eabihf/debug/hello` Hello, this is semihosting! x = 1.000, y = 2.000 PANIC: PanicInfo { message: I am an example panic, @@ -43,6 +54,16 @@ PANIC: PanicInfo { } ``` +The `smp` example prints the following: + +```console +$ cargo run --bin smp +I am core 0 - Mpidr(80000000) +I am core 1 - Mpidr(80000001) +CAS test passed +CS Mutex test passed +``` + ## Debugging You can start a GDB server by adding `-- -s -S` to the end of the `cargo run` @@ -59,7 +80,7 @@ $ arm-none-eabi-gdb target/armv7a-none-eabihf/debug/hello ## Minimum Supported Rust Version (MSRV) These examples are guaranteed to compile on the version of Rust given in the -[`rust-toolchain.toml`] file. These examples are not version controlled and we +[`../../rust-toolchain.toml`] file. These examples are not version controlled and we may change the MSRV at any time. ## Licence diff --git a/examples/xilinx-zynq-a9/memory.x b/examples/xilinx-zynq-a9/memory.x index ff536179..13de51cd 100644 --- a/examples/xilinx-zynq-a9/memory.x +++ b/examples/xilinx-zynq-a9/memory.x @@ -24,3 +24,6 @@ PROVIDE(_abt_stack_size = 16K); PROVIDE(_irq_stack_size = 64); PROVIDE(_fiq_stack_size = 64); PROVIDE(_sys_stack_size = 16K); + +/* This is a dual-core Cortex-A9, so reserve a set of stacks per core. */ +PROVIDE(_num_cores = 2); diff --git a/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabi.out b/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabi.out new file mode 100644 index 00000000..0dff6869 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabi.out @@ -0,0 +1,6 @@ +I am core Mpidr(80000000) +Creating GIC driver @ 0xf8f01000 / 0xf8f00100 +Configure SGI SGI 3 as Group0... +Send SGI to self +- got interrupt SGI 3 +SGI handled diff --git a/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabihf.out b/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabihf.out new file mode 100644 index 00000000..0dff6869 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/gic-armv7a-none-eabihf.out @@ -0,0 +1,6 @@ +I am core Mpidr(80000000) +Creating GIC driver @ 0xf8f01000 / 0xf8f00100 +Configure SGI SGI 3 as Group0... +Send SGI to self +- got interrupt SGI 3 +SGI handled diff --git a/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabi.out b/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabi.out new file mode 100644 index 00000000..0dff6869 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabi.out @@ -0,0 +1,6 @@ +I am core Mpidr(80000000) +Creating GIC driver @ 0xf8f01000 / 0xf8f00100 +Configure SGI SGI 3 as Group0... +Send SGI to self +- got interrupt SGI 3 +SGI handled diff --git a/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabihf.out b/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabihf.out new file mode 100644 index 00000000..0dff6869 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/gic-thumbv7a-none-eabihf.out @@ -0,0 +1,6 @@ +I am core Mpidr(80000000) +Creating GIC driver @ 0xf8f01000 / 0xf8f00100 +Configure SGI SGI 3 as Group0... +Send SGI to self +- got interrupt SGI 3 +SGI handled diff --git a/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabi.out b/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabi.out new file mode 100644 index 00000000..b157d323 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabi.out @@ -0,0 +1,4 @@ +I am core 0 - Mpidr(80000000) +I am core 1 - Mpidr(80000001) +CAS test passed +CS Mutex test passed diff --git a/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabihf.out b/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabihf.out new file mode 100644 index 00000000..b157d323 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/smp-armv7a-none-eabihf.out @@ -0,0 +1,4 @@ +I am core 0 - Mpidr(80000000) +I am core 1 - Mpidr(80000001) +CAS test passed +CS Mutex test passed diff --git a/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabi.out b/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabi.out new file mode 100644 index 00000000..b157d323 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabi.out @@ -0,0 +1,4 @@ +I am core 0 - Mpidr(80000000) +I am core 1 - Mpidr(80000001) +CAS test passed +CS Mutex test passed diff --git a/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabihf.out b/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabihf.out new file mode 100644 index 00000000..b157d323 --- /dev/null +++ b/examples/xilinx-zynq-a9/reference/smp-thumbv7a-none-eabihf.out @@ -0,0 +1,4 @@ +I am core 0 - Mpidr(80000000) +I am core 1 - Mpidr(80000001) +CAS test passed +CS Mutex test passed diff --git a/examples/xilinx-zynq-a9/rust-toolchain.toml b/examples/xilinx-zynq-a9/rust-toolchain.toml deleted file mode 100644 index 08728839..00000000 --- a/examples/xilinx-zynq-a9/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "nightly-2026-06-04" -targets = [] -components = ["rust-src", "clippy", "rustfmt"] diff --git a/examples/xilinx-zynq-a9/src/bin/gic.rs b/examples/xilinx-zynq-a9/src/bin/gic.rs new file mode 100644 index 00000000..7437116d --- /dev/null +++ b/examples/xilinx-zynq-a9/src/bin/gic.rs @@ -0,0 +1,121 @@ +//! # GIC example for the Arm Cortex-A9 on a Xilinx Zynq-7000 +//! +//! Sets up the memory-mapped GIC (the GICv2 programming model), configures a +//! Software Generated Interrupt, sends it to this same core, and handles it in +//! the IRQ handler. This exercises the interrupt controller on a single core, +//! before we add a second core. + +#![no_std] +#![no_main] + +use core::cell::RefCell; +use core::sync::atomic::{AtomicBool, Ordering}; + +use arm_gic::{ + IntId, InterruptGroup, + gicv2::{GicV2, SgiTarget, SgiTargetListFilter}, +}; +use critical_section::Mutex; +use semihosting::println; + +use aarch32_rt::entry; +use xilinx_zynq_a9 as _; + +/// Set once the IRQ handler has run. +static IRQ_FIRED: AtomicBool = AtomicBool::new(false); + +/// Shared interrupt controller driver. +/// +/// The GICv2 CPU interface is memory-mapped, so ack/EOI need `&mut` access to +/// the driver from inside the IRQ handler. We therefore share it behind a +/// critical-section `Mutex`. +static GLOBAL_GIC: Mutex>>> = Mutex::new(RefCell::new(None)); + +/// The Software Generated Interrupt we send to ourselves. +const SGI_INTID: IntId = IntId::sgi(3); + +/// The interrupt group we use. +/// +/// [`GicV2::setup`] only enables Group 0 signalling on the CPU interface (it +/// writes `GICC_CTLR = 0b1`), even when the GIC implements the Security +/// Extensions and assigns every interrupt to Group 1 at the Distributor. So we +/// keep our SGI in Group 0, where it is actually delivered. With `FIQEn` clear, +/// Group 0 interrupts are signalled as IRQs. +const GROUP: InterruptGroup = InterruptGroup::Group0; + +/// The entry-point to the Rust application. +/// +/// It is called by the start-up code in `aarch32-rt`. +#[entry] +fn main() -> ! { + xilinx_zynq_a9::init(); + + println!("I am core {:08x?}", aarch32_cpu::register::Mpidr::read()); + + // SAFETY: this is the only call to `make_gic()`. + let mut gic = unsafe { xilinx_zynq_a9::make_gic() }; + + // `setup()` only enabled Group 1 in the Distributor (this GIC reports the + // Security Extensions), so enable Group 0 too - that is the group our SGI + // and the CPU interface use. + gic.enable_group0(true); + + println!("Configure SGI {:?} as {:?}...", SGI_INTID, GROUP); + gic.set_interrupt_priority(SGI_INTID, 0x31); + gic.set_group(SGI_INTID, GROUP); + gic.enable_interrupt(SGI_INTID, true).unwrap(); + + critical_section::with(|cs| { + GLOBAL_GIC.borrow_ref_mut(cs).replace(gic); + }); + + unsafe { + aarch32_cpu::interrupt::enable(); + } + + println!("Send SGI to self"); + critical_section::with(|cs| { + let mut gic = GLOBAL_GIC.borrow_ref_mut(cs); + let gic = gic.as_mut().unwrap(); + gic.send_sgi( + SGI_INTID, + SgiTarget::List { + target_list_filter: SgiTargetListFilter::ForwardSelfOnly, + target_list: 0, + }, + ); + }); + + // Wait for the IRQ handler to run. + for _ in 0..1_000_000 { + if IRQ_FIRED.load(Ordering::SeqCst) { + break; + } + aarch32_cpu::asm::wfi(); + } + + if IRQ_FIRED.load(Ordering::SeqCst) { + println!("SGI handled"); + } else { + println!("SGI missing?!"); + } + + xilinx_zynq_a9::exit(0); +} + +/// Called when the Arm CPU gets an IRQ. +/// +/// Talks to the GIC to find out which interrupt is pending, handles it, and +/// then tells the GIC it has been handled. +#[aarch32_rt::irq] +fn irq_handler() { + critical_section::with(|cs| { + let mut gic = GLOBAL_GIC.borrow_ref_mut(cs); + let gic = gic.as_mut().unwrap(); + while let Some(intid) = gic.get_and_acknowledge_interrupt(GROUP) { + println!("- got interrupt {:?}", intid); + IRQ_FIRED.store(true, Ordering::SeqCst); + gic.end_interrupt(intid, GROUP); + } + }); +} diff --git a/examples/xilinx-zynq-a9/src/bin/smp.rs b/examples/xilinx-zynq-a9/src/bin/smp.rs new file mode 100644 index 00000000..a0d00c89 --- /dev/null +++ b/examples/xilinx-zynq-a9/src/bin/smp.rs @@ -0,0 +1,130 @@ +//! # Multi-core hello-world for the Arm Cortex-A9 on a Xilinx Zynq-7000 +//! +//! Boots the second core and checks that atomics and critical sections work +//! across both cores. + +#![no_std] +#![no_main] + +use core::cell::RefCell; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use aarch32_rt::entry; +use semihosting::println; +use xilinx_zynq_a9 as _; + +/// Set by Core 1 once it has booted. +static CORE1_BOOTED: AtomicBool = AtomicBool::new(false); + +/// Incremented by both cores using an atomic read-modify-write. +static SHARED_VARIABLE: AtomicU32 = AtomicU32::new(0); + +/// Incremented by both cores from inside a critical section. +static SHARED_VARIABLE_2: critical_section::Mutex> = + critical_section::Mutex::new(RefCell::new(0)); + +/// How long Core 0 waits for Core 1. +const CORE0_WILL_WAIT: usize = 1_000_000; + +/// How many atomic-add loops each core runs. +const CAS_LOOPS: u32 = 1000; + +/// How many critical-section loops each core runs. +const CS_MUTEX_LOOPS: u32 = 1000; + +/// The entry-point to the Rust application. +/// +/// It is called by the start-up code in `aarch32-rt` on Core 0. +#[entry] +fn main() -> ! { + xilinx_zynq_a9::init(); + + println!( + "I am core 0 - {:08x?}", + aarch32_cpu::register::Mpidr::read() + ); + + xilinx_zynq_a9::start_core1(); + + // Wait some time for Core 1 to start. + for counter in 0..=CORE0_WILL_WAIT { + if CORE1_BOOTED.load(Ordering::SeqCst) { + break; + } + if counter == CORE0_WILL_WAIT { + println!("CPU 1 is missing?!"); + xilinx_zynq_a9::exit(0); + } + } + + for _ in 0..CAS_LOOPS { + SHARED_VARIABLE.fetch_add(1, Ordering::Relaxed); + } + + for _ in 0..CS_MUTEX_LOOPS { + critical_section::with(|cs| { + let mut value_ref = SHARED_VARIABLE_2.borrow_ref_mut(cs); + *value_ref += 1; + }) + } + + // Let the other core finish. + for _ in 0..CORE0_WILL_WAIT { + aarch32_cpu::asm::nop(); + } + + let mut code = 0; + let total_a = SHARED_VARIABLE.load(Ordering::Relaxed); + if total_a == CAS_LOOPS * 2 { + println!("CAS test passed"); + } else { + println!("CAS test failed, got {} not 2000", total_a); + code = 1; + } + + let total_b = critical_section::with(|cs| { + let value_ref = SHARED_VARIABLE_2.borrow_ref(cs); + *value_ref + }); + + if total_b == CS_MUTEX_LOOPS * 2 { + println!("CS Mutex test passed"); + } else { + println!("CS Mutex test failed, got {} not 2000", total_b); + code = 1; + } + + xilinx_zynq_a9::exit(code); +} + +/// The entry-point to the Rust application on Core 1. +/// +/// Called by the start-up code once Core 0 has released this core. +#[unsafe(no_mangle)] +pub extern "C" fn kmain_secondary() { + // Each core enables its own MMU before it does any atomics: the exclusive + // monitor used by `fetch_add` and the critical section needs normal, + // shareable memory, which the MMU provides. + xilinx_zynq_a9::init(); + + println!( + "I am core 1 - {:08x?}", + aarch32_cpu::register::Mpidr::read() + ); + CORE1_BOOTED.store(true, Ordering::SeqCst); + + for _ in 0..CAS_LOOPS { + SHARED_VARIABLE.fetch_add(1, Ordering::Relaxed); + } + + for _ in 0..CS_MUTEX_LOOPS { + critical_section::with(|cs| { + let mut value_ref = SHARED_VARIABLE_2.borrow_ref_mut(cs); + *value_ref += 1; + }) + } + + loop { + aarch32_cpu::asm::wfi(); + } +} diff --git a/examples/xilinx-zynq-a9/src/lib.rs b/examples/xilinx-zynq-a9/src/lib.rs index 22f43db3..ffde4ec7 100644 --- a/examples/xilinx-zynq-a9/src/lib.rs +++ b/examples/xilinx-zynq-a9/src/lib.rs @@ -1,13 +1,14 @@ #![no_std] +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; pub mod mmu; -static WANT_PANIC: portable_atomic::AtomicBool = portable_atomic::AtomicBool::new(false); +static WANT_PANIC: AtomicBool = AtomicBool::new(false); /// Track if we're already in the exit routine. /// /// Stops us doing infinite recursion if we panic whilst doing the stack reporting. -static IN_EXIT: portable_atomic::AtomicBool = portable_atomic::AtomicBool::new(false); +static IN_EXIT: AtomicBool = AtomicBool::new(false); /// Called when the application raises an unrecoverable `panic!`. /// @@ -17,7 +18,7 @@ static IN_EXIT: portable_atomic::AtomicBool = portable_atomic::AtomicBool::new(f #[cfg(target_os = "none")] fn panic(info: &core::panic::PanicInfo) -> ! { semihosting::println!("PANIC: {:#?}", info); - if WANT_PANIC.load(portable_atomic::Ordering::Relaxed) { + if WANT_PANIC.load(Ordering::Relaxed) { exit(0); } else { exit(1); @@ -26,20 +27,55 @@ fn panic(info: &core::panic::PanicInfo) -> ! { /// Set the panic function as no longer returning a failure code via semihosting pub fn want_panic() { - WANT_PANIC.store(true, portable_atomic::Ordering::Relaxed); + WANT_PANIC.store(true, Ordering::Relaxed); } /// Init the hardware /// -/// Includes enabling the MMU. +/// Includes enabling the MMU. Each core must call this for itself, because the +/// MMU control registers (`TTBR0`, `SCTLR`) are per-core; they all point at the +/// same shared L1 page table. pub fn init() { mmu::set_mmu(); mmu::enable_mmu_and_cache(); } +static CORE1_RELEASE: AtomicU32 = AtomicU32::new(0); + +/// Release core1 from spin loop +pub fn start_core1() { + CORE1_RELEASE.store(1, Ordering::SeqCst); + unsafe { core::arch::asm!("sev") }; +} + +/// Park function for secondary cores +/// +/// We sleep the cores with a `WFE` and check a register in the FPGA to see if +/// it is time to boot. +#[unsafe(naked)] +#[unsafe(no_mangle)] +pub extern "C" fn _asm_secondary_core_park() { + core::arch::naked_asm!( + r#" + // Address of the release flag + ldr r0, ={release} + 1: + // Wait until Core 0 does a 'sev' + wfe + // Spin until the flag is non-zero + ldr r1, [r0] + cmp r1, 0 + beq 1b + // return to start-up + bx lr + "#, + release = sym CORE1_RELEASE, + ) +} + /// Exit from QEMU with code pub fn exit(code: i32) -> ! { - if !IN_EXIT.swap(true, portable_atomic::Ordering::Relaxed) { + if !IN_EXIT.swap(true, Ordering::Relaxed) { stack_dump(); } semihosting::process::exit(code) @@ -85,3 +121,69 @@ fn stack_dump() { } } } + +/// Represents the hardware we drive in our Zynq-7000 system. +pub struct Board { + /// The Arm Generic Interrupt Controller (memory-mapped / GICv2 model) + pub gic: arm_gic::gicv2::GicV2<'static>, +} + +impl Board { + /// Create a new board structure. + /// + /// Returns `Some(board)` the first time you call it, and `None` thereafter, + /// so you cannot have two copies of the [`Board`] structure. + pub fn new() -> Option { + static TAKEN: AtomicBool = AtomicBool::new(false); + if TAKEN.swap(true, Ordering::SeqCst) { + // they already took the peripherals + return None; + } + Some(Board { + // SAFETY: This is the first and only call to `make_gic()`, as + // guaranteed by the atomic flag check above. + gic: unsafe { make_gic() }, + }) + } +} + +/// The Cortex-A9 MPCore private peripheral base on the Zynq-7000. +/// +/// This is fixed by the SoC (and matches the QEMU `xilinx-zynq-a9` machine). We +/// use a constant rather than reading `CBAR`, because `CBAR` on the Cortex-A9 +/// uses a different encoding to the one `aarch32_cpu::register::ImpCbar` issues. +const PERIPHBASE: usize = 0xF8F0_0000; + +/// Create the Arm GIC driver for the Cortex-A9 MPCore. +/// +/// The Cortex-A9 uses the memory-mapped GIC (the GICv2 programming model). The +/// Distributor sits at `PERIPHBASE + 0x1000` and the CPU interface at +/// `PERIPHBASE + 0x100`. Both regions fall inside the device memory mapped by +/// [`mmu`]. +/// +/// # Safety +/// +/// Only call this function once. +pub unsafe fn make_gic() -> arm_gic::gicv2::GicV2<'static> { + use arm_gic::gicv2::registers::{Gicc, Gicd}; + + /// Offset from PERIPHBASE for the GIC Distributor + const GICD_BASE_OFFSET: usize = 0x0000_1000; + + /// Offset from PERIPHBASE for the GIC CPU interface + const GICC_BASE_OFFSET: usize = 0x0000_0100; + + let gicd_base = (PERIPHBASE + GICD_BASE_OFFSET) as *mut Gicd; + let gicc_base = (PERIPHBASE + GICC_BASE_OFFSET) as *mut Gicc; + semihosting::println!( + "Creating GIC driver @ {:010p} / {:010p}", + gicd_base, + gicc_base + ); + // SAFETY: `gicd_base` and `gicc_base` point at the GIC Distributor and CPU + // interface MMIO regions for this SoC, and this function is only called + // once, so the driver has exclusive ownership. + let mut gic = unsafe { arm_gic::gicv2::GicV2::new(gicd_base, gicc_base) }; + gic.setup(); + gic +}