diff --git a/.github/workflows/ci/expected/riscv32imac-unknown-none-elf/hello.run b/.github/workflows/ci/expected/riscv32imac-unknown-none-elf/hello.run new file mode 100644 index 00000000..37fa813f --- /dev/null +++ b/.github/workflows/ci/expected/riscv32imac-unknown-none-elf/hello.run @@ -0,0 +1 @@ +QEMU 10.1.2 monitor - type 'help' for more information diff --git a/.github/workflows/qemu.yaml b/.github/workflows/qemu.yaml new file mode 100644 index 00000000..d21ef87a --- /dev/null +++ b/.github/workflows/qemu.yaml @@ -0,0 +1,66 @@ +name: QEMU tests +on: + merge_group: + pull_request: + push: + branches: + - master + +env: + CARGO_TERM_COLOR: always + +jobs: + # Verify the example output with run-pass tests + testexamples: + name: QEMU run + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + input: + - toolchain: stable + target: riscv32imac-unknown-none-elf + qemu-system: riscv32 + example: hello + + - toolchain: nightly + target: riscv32imac-unknown-none-elf + qemu-system: riscv32 + example: hello + + - toolchain: stable + target: riscv64gc-unknown-none-elf + qemu-system: riscv64 + example: hello + + - toolchain: nightly + target: riscv64gc-unknown-none-elf + qemu-system: riscv64 + example: hello + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure Rust target ${{ matrix.input.target }} + run: | + rustup toolchain install ${{ matrix.input.toolchain }} + rustup default ${{ matrix.input.toolchain }} + rustup target add ${{ matrix.input.target }} + + - name: Cache Dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install QEMU + run: | + sudo apt update + sudo apt install -y qemu-system-${{ matrix.input.qemu-system }} + + - name: Check which QEMU is used + run: | + which qemu-system-${{ matrix.input.qemu-system }} + + - name: Run-pass tests + run: cargo run --package xtask -- qemu --target ${{ matrix.input.target }} --example ${{ matrix.input.example }} + + diff --git a/Cargo.toml b/Cargo.toml index c8391343..65b34667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "riscv-types", "tests-build", "tests-trybuild", + "xtask", ] default-members = [ diff --git a/ci/expected/riscv32imac-unknown-none-elf/hello.run b/ci/expected/riscv32imac-unknown-none-elf/hello.run new file mode 100644 index 00000000..e69de29b diff --git a/ci/expected/riscv64gc-unknown-none-elf/hello.run b/ci/expected/riscv64gc-unknown-none-elf/hello.run new file mode 100644 index 00000000..e69de29b diff --git a/riscv-rt/Cargo.toml b/riscv-rt/Cargo.toml index 7afdbc76..a1402323 100644 --- a/riscv-rt/Cargo.toml +++ b/riscv-rt/Cargo.toml @@ -32,6 +32,7 @@ defmt = { version = "1.0.1", optional = true } [dev-dependencies] panic-halt = "1.0.0" +riscv-semihosting = { path = "../riscv-semihosting", version = "0.2.1" } [features] pre-init = [] diff --git a/riscv-rt/examples/device_s_mode.x b/riscv-rt/examples/device_s_mode.x new file mode 100644 index 00000000..494882f6 --- /dev/null +++ b/riscv-rt/examples/device_s_mode.x @@ -0,0 +1,14 @@ +MEMORY +{ + RAM : ORIGIN = 0x80200000, LENGTH = 16K + FLASH : ORIGIN = 0x20000000, LENGTH = 4M +} + +REGION_ALIAS("REGION_TEXT", FLASH); +REGION_ALIAS("REGION_RODATA", FLASH); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); + +INCLUDE link.x diff --git a/riscv-rt/examples/device_virt_m.x b/riscv-rt/examples/device_virt_m.x new file mode 100644 index 00000000..ee4e920a --- /dev/null +++ b/riscv-rt/examples/device_virt_m.x @@ -0,0 +1,11 @@ +MEMORY +{ + RAM : ORIGIN = 0x80000000, LENGTH = 16M +} +REGION_ALIAS("REGION_TEXT", RAM); +REGION_ALIAS("REGION_RODATA", RAM); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); +INCLUDE link.x diff --git a/riscv-rt/examples/device_virt_s.x b/riscv-rt/examples/device_virt_s.x new file mode 100644 index 00000000..2f9a9585 --- /dev/null +++ b/riscv-rt/examples/device_virt_s.x @@ -0,0 +1,11 @@ +MEMORY +{ + RAM : ORIGIN = 0x80200000, LENGTH = 16M +} +REGION_ALIAS("REGION_TEXT", RAM); +REGION_ALIAS("REGION_RODATA", RAM); +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); +INCLUDE link.x diff --git a/riscv-rt/examples/hello.rs b/riscv-rt/examples/hello.rs new file mode 100644 index 00000000..aa9a2977 --- /dev/null +++ b/riscv-rt/examples/hello.rs @@ -0,0 +1,45 @@ +#![no_std] +#![no_main] +extern crate panic_halt; +use riscv_rt::entry; +const UART_BASE: usize = 0x1000_0000; +const UART_THR: usize = UART_BASE + 0; +const UART_IER: usize = UART_BASE + 1; +const UART_FCR: usize = UART_BASE + 2; +const UART_LCR: usize = UART_BASE + 3; +const UART_LSR: usize = UART_BASE + 5; +const LCR_DLAB: u8 = 1 << 7; +const LCR_8N1: u8 = 0x03; +const LSR_THRE: u8 = 1 << 5; +unsafe fn uart_write_reg(off: usize, v: u8) { + (off as *mut u8).write_volatile(v); +} +unsafe fn uart_read_reg(off: usize) -> u8 { + (off as *const u8).read_volatile() +} +fn uart_init() { + unsafe { + uart_write_reg(UART_LCR, LCR_DLAB); + uart_write_reg(UART_THR, 0x01); + uart_write_reg(UART_IER, 0x00); + uart_write_reg(UART_LCR, LCR_8N1); + uart_write_reg(UART_FCR, 0x07); + } +} +fn uart_write_byte(b: u8) { + unsafe { + while (uart_read_reg(UART_LSR) & LSR_THRE) == 0 {} + uart_write_reg(UART_THR, b); + } +} +fn uart_write_str(s: &str) { + for &b in s.as_bytes() { + uart_write_byte(b); + } +} +#[entry] +fn main() -> ! { + uart_init(); + uart_write_str("HELLO_QEMU\n"); + loop {} +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..13979c34 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..c681b722 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,139 @@ +use anyhow::{bail, Context}; +use std::{ + fs, + path::PathBuf, + process::{Command, Stdio}, + thread, + time::Duration, +}; + +fn main() -> anyhow::Result<()> { + let mut args = std::env::args().skip(1).collect::>(); + if args.is_empty() || args[0] != "qemu" { + bail!("usage: cargo run -p xtask -- qemu --target --example "); + } + args.remove(0); + let mut target = None; + let mut example = None; + let mut features: Option = None; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--target" => { + target = Some(args.get(i + 1).context("missing target")?.clone()); + i += 2; + } + "--example" => { + example = Some(args.get(i + 1).context("missing example")?.clone()); + i += 2; + } + "--features" => { + features = Some(args.get(i + 1).context("missing features")?.clone()); + i += 2; + } + _ => { + bail!("unknown arg {}", args[i]); + } + } + } + let target = target.context("--target required")?; + let example = example.context("--example required")?; + let mut rustflags = "-C link-arg=-Triscv-rt/examples/device_virt_m.x".to_string(); + if let Some(f) = &features { + if f.contains("s-mode") { + rustflags = "-C link-arg=-Triscv-rt/examples/device_virt_s.x".into(); + } + } + + let mut cmd = Command::new("cargo"); + cmd.env("RUSTFLAGS", rustflags).args([ + "build", + "--package", + "riscv-rt", + "--release", + "--target", + &target, + "--example", + &example, + ]); + cmd.apply_features(features.as_deref()); + let status = cmd.status()?; + if !status.success() { + bail!("build failed"); + } + + let qemu = if target.starts_with("riscv32") { + "qemu-system-riscv32" + } else { + "qemu-system-riscv64" + }; + let mut qemu_args = vec![ + "-machine", + "virt", + "-nographic", + "-serial", + "stdio", + "-monitor", + "none", + ]; + if !features.as_deref().unwrap_or("").contains("s-mode") { + qemu_args.push("-bios"); + qemu_args.push("none"); + } + let kernel_path = format!("target/{}/release/examples/{}", target, example); + let mut child = Command::new(qemu) + .args(&qemu_args) + .arg("-kernel") + .arg(&kernel_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("running qemu")?; + thread::sleep(Duration::from_secs(2)); + let _ = child.kill(); + let output = child.wait_with_output()?; + let raw_stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + let stdout = raw_stdout + .lines() + .filter(|l| !l.contains("QEMU") && !l.contains("monitor")) + .collect::>() + .join("\n"); + let stdout = if stdout.is_empty() { + String::new() + } else { + format!("{}\n", stdout.trim()) + }; + + let expected_path: PathBuf = ["ci", "expected", &target, &format!("{}.run", example)] + .iter() + .collect(); + if !expected_path.exists() { + fs::create_dir_all(expected_path.parent().unwrap())?; + fs::write(&expected_path, stdout.as_bytes())?; + bail!("expected output created; re-run CI"); + } + let expected = fs::read_to_string(&expected_path)?; + if expected != stdout { + bail!( + "output mismatch\nexpected: {}\nactual: {}", + expected, + stdout + ); + } + if !stdout.is_empty() { + println!("{}", stdout.trim_end()); + } + Ok(()) +} + +trait CmdExt { + fn apply_features(&mut self, f: Option<&str>) -> &mut Self; +} +impl CmdExt for std::process::Command { + fn apply_features(&mut self, f: Option<&str>) -> &mut Self { + if let Some(feat) = f { + self.arg("--features").arg(feat); + } + self + } +}