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
65 changes: 65 additions & 0 deletions .github/workflows/qemu.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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:
# UART example (riscv32)
- toolchain: stable
target: riscv32imac-unknown-none-elf
qemu-system: riscv32
example: hello

# Semihosting example (riscv32)
- toolchain: stable
target: riscv32imac-unknown-none-elf
qemu-system: riscv32
example: qemu_semihosting

# UART example (riscv64)
- toolchain: stable
target: riscv64gc-unknown-none-elf
qemu-system: riscv64
example: hello

# Semihosting example (riscv64)
- toolchain: stable
target: riscv64gc-unknown-none-elf
qemu-system: riscv64
example: qemu_semihosting

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: Run-pass tests
run: cargo run --package xtask -- qemu --target ${{ matrix.input.target }} --example ${{ matrix.input.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/riscv32imac-unknown-none-elf/hello.run
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from UART!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from semihosting!
1 change: 1 addition & 0 deletions ci/expected/riscv64gc-unknown-none-elf/hello.run
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from UART!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from semihosting!
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 riscv-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
14 changes: 14 additions & 0 deletions riscv-rt/examples/device_s_mode.x
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions riscv-rt/examples/device_virt_m.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
11 changes: 11 additions & 0 deletions riscv-rt/examples/device_virt_s.x
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions riscv-rt/examples/hello.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! 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;

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");
loop {}
}
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 {}
}
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"
137 changes: 137 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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::<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_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"];
// Use semihosting for semihosting examples, serial for others
if example.contains("semihosting") {
qemu_args.extend(["-semihosting-config", "enable=on,target=native"]);
} else {
qemu_args.extend(["-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(3));
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.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: 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
}
}