diff --git a/.github/workflows/qemu.yaml b/.github/workflows/qemu.yaml new file mode 100644 index 00000000..805f00dd --- /dev/null +++ b/.github/workflows/qemu.yaml @@ -0,0 +1,59 @@ +name: QEMU tests +on: + merge_group: + pull_request: + push: + branches: + - master + +env: + CARGO_TERM_COLOR: always + +jobs: + testexamples: + name: QEMU run + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + toolchain: [stable] + target-qemu: + - target: riscv32i-unknown-none-elf + qemu: riscv32 + - target: riscv32im-unknown-none-elf + qemu: riscv32 + - target: riscv32imc-unknown-none-elf + qemu: riscv32 + - target: riscv32imac-unknown-none-elf + qemu: riscv32 + - target: riscv32imafc-unknown-none-elf + qemu: riscv32 + - target: riscv64imac-unknown-none-elf + qemu: riscv64 + - target: riscv64gc-unknown-none-elf + qemu: riscv64 + example: + - qemu_uart + - qemu_semihosting + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure Rust target ${{ matrix.target-qemu.target }} + run: | + rustup toolchain install ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup target add ${{ matrix.target-qemu.target }} + + - name: Cache Dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install QEMU + run: | + sudo apt update + sudo apt install -y qemu-system-${{ matrix.target-qemu.qemu }} + + - name: Run-pass tests + run: cargo run --package xtask -- qemu --target ${{ matrix.target-qemu.target }} --example ${{ matrix.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/qemu_semihosting.run b/ci/expected/qemu_semihosting.run new file mode 100644 index 00000000..6df362d2 --- /dev/null +++ b/ci/expected/qemu_semihosting.run @@ -0,0 +1 @@ +Hello from semihosting! diff --git a/ci/expected/qemu_uart.run b/ci/expected/qemu_uart.run new file mode 100644 index 00000000..4687b774 --- /dev/null +++ b/ci/expected/qemu_uart.run @@ -0,0 +1 @@ +Hello from UART! diff --git a/riscv-rt/CHANGELOG.md b/riscv-rt/CHANGELOG.md index 1c455572..f4bca19c 100644 --- a/riscv-rt/CHANGELOG.md +++ b/riscv-rt/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added examples for CI tests using semihosting and UART - New `no-mhartid` feature to load 0 to `a0` instead of reading `mhartid`. - New `no-xtvec` feature that removes interrupt stuff. diff --git a/riscv-rt/Cargo.toml b/riscv-rt/Cargo.toml index f81c0191..096784c8 100644 --- a/riscv-rt/Cargo.toml +++ b/riscv-rt/Cargo.toml @@ -32,6 +32,8 @@ defmt = { version = "1.0.1", optional = true } [dev-dependencies] panic-halt = "1.0.0" +riscv-semihosting = { path = "../riscv-semihosting", version = "0.2.1" } +riscv = { path = "../riscv", version = "0.15.0", features = ["critical-section-single-hart"] } [features] pre-init = [] diff --git a/riscv-rt/examples/device_virt.x b/riscv-rt/examples/device_virt.x new file mode 100644 index 00000000..ee4e920a --- /dev/null +++ b/riscv-rt/examples/device_virt.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/qemu_semihosting.rs b/riscv-rt/examples/qemu_semihosting.rs new file mode 100644 index 00000000..52d6eb40 --- /dev/null +++ b/riscv-rt/examples/qemu_semihosting.rs @@ -0,0 +1,22 @@ +//! Semihosting example for QEMU +//! +//! This example uses RISC-V semihosting to print output and cleanly exit QEMU. +//! Run with: `qemu-system-riscv32 -machine virt -nographic -semihosting-config enable=on,target=native -bios none -kernel ` + +#![no_std] +#![no_main] + +extern crate panic_halt; + +use riscv_rt::entry; +use riscv_semihosting::{ + debug::{self, EXIT_SUCCESS}, + hprintln, +}; + +#[entry] +fn main() -> ! { + hprintln!("Hello from semihosting!"); + debug::exit(EXIT_SUCCESS); + loop {} +} diff --git a/riscv-rt/examples/qemu_uart.rs b/riscv-rt/examples/qemu_uart.rs new file mode 100644 index 00000000..81971ea7 --- /dev/null +++ b/riscv-rt/examples/qemu_uart.rs @@ -0,0 +1,61 @@ +//! UART example for QEMU virt machine +//! +//! This example demonstrates direct UART output on QEMU's virt machine. +//! It writes to the NS16550-compatible UART at 0x1000_0000. + +#![no_std] +#![no_main] + +extern crate panic_halt; + +use riscv_rt::entry; +use riscv_semihosting::debug::{self, EXIT_SUCCESS}; + +const UART_BASE: usize = 0x1000_0000; +const UART_THR: usize = UART_BASE; +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 from UART!\n"); + debug::exit(EXIT_SUCCESS); + loop {} +} diff --git a/typos.toml b/typos.toml index 3a088783..c05ff9cf 100644 --- a/typos.toml +++ b/typos.toml @@ -1,3 +1,3 @@ [default] extend-ignore-re = ["[Ss][Ii][Ee]", "[Ss][Xx][Ll]", "[.]?useed[.,:]?", "[Ss][Tt][Ii][Pp]"] -extend-ignore-words-re = ["[Pp]endings", "PENDINGS"] +extend-ignore-words-re = ["[Pp]endings", "PENDINGS", "THR", "THRE"] 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..876dae51 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,163 @@ +use anyhow::{bail, Context}; +use std::{ + fs, + path::PathBuf, + process::{Command, Stdio}, +}; + +fn find_golden_file(target: &str, example: &str) -> Option { + let target_specific: PathBuf = ["ci", "expected", target, &format!("{}.run", example)] + .iter() + .collect(); + if target_specific.exists() { + return Some(target_specific); + } + + let generic: PathBuf = ["ci", "expected", &format!("{}.run", example)] + .iter() + .collect(); + if generic.exists() { + return Some(generic); + } + + None +} + +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.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", + "-semihosting-config", + "enable=on,target=native", + ]; + 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 child = Command::new(qemu) + .args(&qemu_args) + .arg("-kernel") + .arg(&kernel_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("running qemu")?; + 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.starts_with("QEMU ") && !l.contains("monitor")) + .collect::>() + .join("\n"); + let stdout = if stdout.is_empty() { + String::new() + } else { + format!("{}\n", stdout.trim()) + }; + + let expected_path = match find_golden_file(&target, &example) { + Some(p) => p, + None => { + let target_path: PathBuf = ["ci", "expected", &target, &format!("{}.run", example)] + .iter() + .collect(); + let generic_path: PathBuf = ["ci", "expected", &format!("{}.run", example)] + .iter() + .collect(); + bail!( + "golden file not found. Expected one of:\n - {}\n - {}", + target_path.display(), + generic_path.display() + ); + } + }; + 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 + } +}