Skip to content
Merged
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
59 changes: 59 additions & 0 deletions .github/workflows/qemu.yaml
Original file line number Diff line number Diff line change
@@ -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 }}

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"riscv-types",
"tests-build",
"tests-trybuild",
"xtask",
]

default-members = [
Expand Down
1 change: 1 addition & 0 deletions ci/expected/qemu_semihosting.run
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from semihosting!
1 change: 1 addition & 0 deletions ci/expected/qemu_uart.run
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from UART!
1 change: 1 addition & 0 deletions riscv-rt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions riscv-rt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
11 changes: 11 additions & 0 deletions riscv-rt/examples/device_virt.x
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions riscv-rt/examples/qemu_semihosting.rs
Original file line number Diff line number Diff line change
@@ -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 <binary>`

#![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 {}
}
61 changes: 61 additions & 0 deletions riscv-rt/examples/qemu_uart.rs
Original file line number Diff line number Diff line change
@@ -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 {}
}
2 changes: 1 addition & 1 deletion typos.toml
Original file line number Diff line number Diff line change
@@ -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"]
7 changes: 7 additions & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
163 changes: 163 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> {
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::<Vec<_>>();
if args.is_empty() || args[0] != "qemu" {
bail!("usage: cargo run -p xtask -- qemu --target <triple> --example <name>");
}
args.remove(0);
let mut target = None;
let mut example = None;
let mut features: Option<String> = 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::<Vec<_>>()
.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
}
}