use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::space::MemAttrs;
use crate::core::value::Width;
use crate::host::chardev::{CharPort, ports};
use crate::machine::{Machine, catalog};
use super::boot::DTB_OFFSET;
use super::syscon::{Request, signals};
const BOOT_BASE: u64 = 0x1000;
const DRAM: u64 = 0x8000_0000;
const UART: u64 = 0x1000_0000;
const SYSCON: u64 = 0x0010_0000;
const MTIMECMP: u64 = 0x0200_4000;
const MTIME: u64 = 0x0200_bff8;
const RDTIME_SCRATCH: u64 = DRAM + 0x1000;
const PLIC: u64 = 0x0c00_0000;
mod asm {
pub(super) const T0: u32 = 5;
pub(super) const T1: u32 = 6;
pub(super) const T2: u32 = 7;
pub(super) const A0: u32 = 10;
pub(super) const A1: u32 = 11;
pub(super) const T3: u32 = 28;
pub(super) const ZERO: u32 = 0;
pub(super) const CSR_MSTATUS: u32 = 0x300;
pub(super) const CSR_MIE: u32 = 0x304;
pub(super) const CSR_MTVEC: u32 = 0x305;
pub(super) const CSR_TIME: u32 = 0xc01;
fn i_type(opcode: u32, funct3: u32, rd: u32, rs1: u32, imm: i32) -> u32 {
(((imm as u32) & 0xfff) << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | opcode
}
fn s_type(funct3: u32, rs1: u32, rs2: u32, imm: i32) -> u32 {
let imm = imm as u32;
(((imm >> 5) & 0x7f) << 25)
| (rs2 << 20)
| (rs1 << 15)
| (funct3 << 12)
| ((imm & 0x1f) << 7)
| 0b0100011
}
pub(super) fn lui(rd: u32, imm20: u32) -> u32 {
((imm20 & 0xf_ffff) << 12) | (rd << 7) | 0b0110111
}
pub(super) fn auipc(rd: u32, imm20: u32) -> u32 {
((imm20 & 0xf_ffff) << 12) | (rd << 7) | 0b0010111
}
pub(super) fn addi(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0b0010011, 0b000, rd, rs1, imm)
}
pub(super) fn lbu(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0b0000011, 0b100, rd, rs1, imm)
}
pub(super) fn lw(rd: u32, rs1: u32, imm: i32) -> u32 {
i_type(0b0000011, 0b010, rd, rs1, imm)
}
pub(super) fn sb(rs1: u32, rs2: u32, imm: i32) -> u32 {
s_type(0b000, rs1, rs2, imm)
}
pub(super) fn sw(rs1: u32, rs2: u32, imm: i32) -> u32 {
s_type(0b010, rs1, rs2, imm)
}
pub(super) fn sd(rs1: u32, rs2: u32, imm: i32) -> u32 {
s_type(0b011, rs1, rs2, imm)
}
pub(super) fn csrw(csr: u32, rs1: u32) -> u32 {
i_type(0b1110011, 0b001, ZERO, rs1, csr as i32)
}
pub(super) fn csrr(rd: u32, csr: u32) -> u32 {
i_type(0b1110011, 0b010, rd, ZERO, csr as i32)
}
pub(super) fn csrs(csr: u32, rs1: u32) -> u32 {
i_type(0b1110011, 0b010, ZERO, rs1, csr as i32)
}
pub(super) fn wfi() -> u32 {
0x1050_0073
}
pub(super) fn j(imm: i32) -> u32 {
let imm = imm as u32;
((imm >> 20) & 1) << 31
| ((imm >> 1) & 0x3ff) << 21
| ((imm >> 11) & 1) << 20
| ((imm >> 12) & 0xff) << 12
| 0b1101111
}
pub(super) fn li(rd: u32, value: u32) -> [u32; 2] {
assert!(value < 0x8000_0000, "use auipc for {value:#x}");
let hi = (value.wrapping_add(0x800)) >> 12;
let lo = (value & 0xfff) as i32;
let lo = if lo >= 0x800 { lo - 0x1000 } else { lo };
[lui(rd, hi), addi(rd, rd, lo)]
}
}
struct Program {
words: Vec<u32>,
base: u64,
}
impl Program {
fn new(base: u64) -> Program {
Program {
words: Vec::new(),
base,
}
}
fn here(&self) -> u64 {
self.base + self.words.len() as u64 * 4
}
fn push(&mut self, word: u32) -> &mut Program {
self.words.push(word);
self
}
fn push_all(&mut self, words: impl IntoIterator<Item = u32>) -> &mut Program {
self.words.extend(words);
self
}
fn li(&mut self, rd: u32, value: u32) -> &mut Program {
self.push_all(asm::li(rd, value))
}
fn la(&mut self, rd: u32, target: u64) -> &mut Program {
let from = self.here();
let delta = target as i64 - from as i64;
assert!(
(-0x8_0000_0000..0x8_0000_0000).contains(&delta),
"too far for auipc"
);
let hi = ((delta + 0x800) >> 12) as u32;
let lo = (delta & 0xfff) as i32;
let lo = if lo >= 0x800 { lo - 0x1000 } else { lo };
self.push(asm::auipc(rd, hi));
self.push(asm::addi(rd, rd, lo))
}
fn putc(&mut self, byte: u8) -> &mut Program {
self.li(asm::T1, u32::from(byte));
self.push(asm::sb(asm::T0, asm::T1, 0))
}
fn poweroff(&mut self) -> &mut Program {
self.li(asm::T0, SYSCON as u32);
self.li(asm::T1, u32::from(super::syscon::CMD_PASS));
self.push(asm::sw(asm::T0, asm::T1, 0));
self.push(asm::j(0))
}
fn pad_to(&mut self, offset: u64) -> &mut Program {
while self.here() < self.base + offset {
self.push(asm::addi(asm::ZERO, asm::ZERO, 0));
}
assert_eq!(self.here(), self.base + offset, "padded past the target");
self
}
fn bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.words.len() * 4);
for word in &self.words {
out.extend_from_slice(&word.to_le_bytes());
}
out
}
}
struct Board {
machine: Machine,
console: Arc<CharPort>,
power: Arc<super::syscon::Signal>,
}
fn board(tag: &str, firmware: &[u8]) -> Board {
let _ = tag;
let console_name = String::from("console");
let power_name = String::from("power");
let entry = catalog::machine("riscv-virt").expect("this build ships it");
let options = catalog::build_options()
.expect("the catalog agrees with itself")
.with_media("firmware", firmware)
.with_media("flash0", &[][..])
.with_media("flash1", &[][..])
.with_media("initrd", &[][..])
.with_media("disk", &[][..])
.with_param("console", console_name.clone())
.with_param("power", power_name.clone())
.with_param("ram", String::from("8M"));
let registry = catalog::registry().expect("the catalog agrees with itself");
let machine = match crate::machine::build(entry.name, entry.source, ®istry, &options) {
Ok(m) => m,
Err(e) => panic!("riscv-virt does not build: {e}"),
};
Board {
machine,
console: ports::open(&options.realize.hosts, &console_name).expect("the UART opened it"),
power: signals::open(&options.realize.hosts, &power_name).expect("the syscon opened it"),
}
}
impl Board {
fn run(&mut self, quanta: usize) -> Option<Request> {
for _ in 0..quanta {
if let Some(request) = self.power.peek() {
return Some(request);
}
self.machine.run_quantum().expect("the machine advances");
}
self.power.peek()
}
fn output(&self) -> String {
String::from_utf8_lossy(&self.console.drain()).into_owned()
}
fn peek(&self, addr: u64, width: Width) -> u64 {
self.machine
.space("mem")
.expect("the machine has one space")
.read(addr, width, MemAttrs::DEBUG)
.unwrap_or_else(|e| panic!("nothing answers at {addr:#x}: {e}"))
}
fn device_tree(&self) -> Vec<u8> {
let at = BOOT_BASE + DTB_OFFSET;
let space = self.machine.space("mem").expect("one space");
let mut header = [0u8; 8];
space
.read_bytes(at, &mut header, MemAttrs::DEBUG)
.expect("the boot ROM answers");
let total = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize;
assert!(
total > 0 && total < 0x1_0000,
"implausible tree size {total}"
);
let mut dtb = alloc::vec![0u8; total];
space
.read_bytes(at, &mut dtb, MemAttrs::DEBUG)
.expect("the boot ROM answers");
dtb
}
}
#[test]
fn the_boot_rom_hands_over_a_device_tree_that_parses() {
let b = board("dtb", &[]);
let dtb = b.device_tree();
assert_eq!(
u32::from_be_bytes([dtb[0], dtb[1], dtb[2], dtb[3]]),
super::fdt::FDT_MAGIC,
"the blob at the address a1 points at is not a device tree"
);
let tree = super::dt::describe(&dtb).expect("it parses");
for node in [
"cpu@0",
"interrupt-controller",
"memory@80000000",
"soc",
"test@100000",
"clint@2000000",
"plic@c000000",
"serial@10000000",
"virtio_mmio@10001000",
"virtio_mmio@10002000",
"chosen",
"poweroff",
"reboot",
] {
assert!(tree.contains(node), "no `{node}` in:\n{tree}");
}
}
#[test]
fn a_ramdisk_is_staged_in_memory_and_pointed_at_by_the_tree() {
let at = DRAM + 0x10_0000;
let ramdisk = alloc::vec![0x5au8; 4096];
let entry = catalog::machine("riscv-virt").expect("shipped");
let options = catalog::build_options()
.unwrap()
.with_media("firmware", &[][..])
.with_media("flash0", &[][..])
.with_media("flash1", &[][..])
.with_media("initrd", ramdisk.as_slice())
.with_media("disk", &[][..])
.with_param("console", "console")
.with_param("power", "power")
.with_param("ram", "8M")
.with_param("initrd_addr", alloc::format!("{at:#x}"));
let machine = crate::machine::build(
entry.name,
entry.source,
&catalog::registry().unwrap(),
&options,
)
.expect("a board with a ramdisk still builds");
let b = Board {
machine,
console: ports::open(&options.realize.hosts, "console").expect("the UART opened it"),
power: signals::open(&options.realize.hosts, "power").expect("the syscon opened it"),
};
let dtb = b.device_tree();
assert_eq!(
prop_u64(&dtb, "chosen", "linux,initrd-start"),
Some(at),
"{}",
super::dt::describe(&dtb).expect("it parses")
);
assert_eq!(
prop_u64(&dtb, "chosen", "linux,initrd-end"),
Some(at + ramdisk.len() as u64),
"the end is one past the last byte"
);
assert_eq!(b.peek(at, Width::U8), 0x5a, "the first byte of the ramdisk");
assert_eq!(
b.peek(at + ramdisk.len() as u64 - 1, Width::U8),
0x5a,
"the last byte of the ramdisk"
);
let plain = board("no-initrd", &[]).device_tree();
assert_eq!(prop_u64(&plain, "chosen", "linux,initrd-start"), None);
assert_eq!(prop_u64(&plain, "chosen", "linux,initrd-end"), None);
}
#[test]
fn every_address_in_the_tree_came_from_the_memory_map() {
let b = board("dtb-addr", &[]);
let tree = super::dt::describe(&b.device_tree()).expect("it parses");
assert!(tree.contains("serial@10000000"), "{tree}");
let entry = catalog::machine("riscv-virt").expect("shipped");
let moved = entry.source.replace(
"0x10000000 size 0x00000100 = uart",
"0x10004000 size 0x00000100 = uart",
);
assert_ne!(moved, entry.source, "the map statement was not found");
let options = catalog::build_options()
.unwrap()
.with_media("firmware", &[][..])
.with_media("flash0", &[][..])
.with_media("flash1", &[][..])
.with_media("initrd", &[][..])
.with_media("disk", &[][..])
.with_param("console", "console")
.with_param("power", "power")
.with_param("ram", "8M");
let machine = crate::machine::build(
"riscv-virt-moved",
&moved,
&catalog::registry().unwrap(),
&options,
)
.expect("a moved UART is still a machine");
let b2 = Board {
machine,
console: ports::open(&options.realize.hosts, "console").expect("the UART opened it"),
power: signals::open(&options.realize.hosts, "power").expect("the syscon opened it"),
};
let tree = super::dt::describe(&b2.device_tree()).expect("it parses");
assert!(tree.contains("serial@10004000"), "{tree}");
assert!(!tree.contains("serial@10000000"), "{tree}");
}
#[test]
fn the_nor_banks_appear_as_cfi_flash_nodes() {
let b = board("dtb-flash", &[]);
let tree = super::dt::describe(&b.device_tree()).expect("it parses");
assert!(tree.contains("flash@20000000"), "{tree}");
assert!(tree.contains("flash@22000000"), "{tree}");
let dtb = b.device_tree();
assert!(
dtb.windows(10).any(|w| w == b"cfi-flash\0"),
"no `cfi-flash` compatible string in the tree"
);
assert_eq!(prop_u32(&dtb, "flash@22000000", "bank-width"), Some(4));
}
#[test]
fn the_interrupt_numbers_come_out_of_the_wire_graph() {
let b = board("dtb-irq", &[]);
let dtb = b.device_tree();
assert_eq!(interrupts_of(&dtb, "serial@10000000"), Some(10));
assert_eq!(interrupts_of(&dtb, "virtio_mmio@10001000"), Some(1));
assert_eq!(interrupts_of(&dtb, "virtio_mmio@10002000"), Some(2));
}
#[test]
fn the_timebase_is_the_clints_own_clock() {
let b = board("dtb-time", &[]);
let dtb = b.device_tree();
assert_eq!(
prop_u32(&dtb, "cpus", "timebase-frequency"),
Some(10_000_000),
"the tree must report the rate mtime really counts at"
);
}
#[test]
fn the_tree_is_byte_identical_across_builds() {
let a = board("dtb-det-a", &[]).device_tree();
let b = board("dtb-det-b", &[]).device_tree();
assert_eq!(a, b);
}
#[test]
fn a_bare_metal_program_prints_to_the_uart_and_powers_off() {
let mut p = Program::new(DRAM);
p.li(asm::T0, UART as u32);
for byte in b"rsemu\n" {
p.putc(*byte);
}
p.la(asm::T2, DRAM + 0x1000);
p.push(asm::sd(asm::T2, asm::A0, 0));
p.push(asm::sd(asm::T2, asm::A1, 8));
p.poweroff();
let mut b = board("hello", &p.bytes());
assert_eq!(b.run(200), Some(Request::Poweroff), "it never stopped");
assert_eq!(b.output(), "rsemu\n");
assert_eq!(b.peek(DRAM + 0x1000, Width::U64), 0, "a0 is the hart id");
assert_eq!(
b.peek(DRAM + 0x1008, Width::U64),
BOOT_BASE + DTB_OFFSET,
"a1 points at the device tree"
);
}
#[test]
fn a_timer_interrupt_programmed_through_the_clint_reaches_mtvec() {
const HANDLER: u64 = 0x100;
let mut p = Program::new(DRAM);
p.la(asm::T0, DRAM + HANDLER);
p.push(asm::csrw(asm::CSR_MTVEC, asm::T0));
p.li(asm::T0, MTIMECMP as u32);
p.li(asm::T1, 2000);
p.push(asm::sd(asm::T0, asm::T1, 0));
p.li(asm::T0, 1 << 7);
p.push(asm::csrs(asm::CSR_MIE, asm::T0));
p.li(asm::T0, 1 << 3);
p.push(asm::csrs(asm::CSR_MSTATUS, asm::T0));
p.push(asm::wfi());
p.push(asm::j(-4));
p.pad_to(HANDLER);
p.li(asm::T0, UART as u32);
p.putc(b'T');
p.poweroff();
let mut b = board("timer", &p.bytes());
assert_eq!(b.run(400), Some(Request::Poweroff), "the timer never fired");
assert_eq!(b.output(), "T");
}
fn rdtime_loop() -> Program {
let mut p = Program::new(DRAM);
p.la(asm::T2, RDTIME_SCRATCH);
let top = p.here();
p.push(asm::csrr(asm::T0, asm::CSR_TIME));
p.push(asm::sd(asm::T2, asm::T0, 0));
let back = top as i64 - p.here() as i64;
p.push(asm::j(back as i32));
p
}
#[test]
fn rdtime_reads_the_clints_counter() {
let mut b = board("rdtime", &rdtime_loop().bytes());
b.run(200);
let seen = b.peek(RDTIME_SCRATCH, Width::U64);
assert!(seen > 0, "`rdtime` still reads zero after 200 quanta");
let mtime = b.peek(MTIME, Width::U64);
assert!(
seen <= mtime,
"the guest saw {seen}, ahead of the CLINT's own {mtime}"
);
}
#[test]
fn a_hart_with_no_timer_named_reads_zero() {
let entry = catalog::machine("riscv-virt").expect("shipped");
let unwired = entry.source.replace("timer = clint", "");
assert_ne!(unwired, entry.source, "the `timer` property was not found");
let options = catalog::build_options()
.unwrap()
.with_media("firmware", rdtime_loop().bytes().as_slice())
.with_media("flash0", &[][..])
.with_media("flash1", &[][..])
.with_media("initrd", &[][..])
.with_media("disk", &[][..])
.with_param("console", "console")
.with_param("power", "power")
.with_param("ram", "8M");
let machine = crate::machine::build(
"riscv-virt-untimed",
&unwired,
&catalog::registry().unwrap(),
&options,
)
.expect("a board with no timer named is still a board");
let mut b = Board {
machine,
console: ports::open(&options.realize.hosts, "console").expect("the UART opened it"),
power: signals::open(&options.realize.hosts, "power").expect("the syscon opened it"),
};
b.run(200);
assert_eq!(b.peek(RDTIME_SCRATCH, Width::U64), 0);
assert!(
b.peek(MTIME, Width::U64) > 0,
"the CLINT itself must still be counting"
);
}
#[test]
fn naming_a_timer_that_publishes_none_says_so() {
let entry = catalog::machine("riscv-virt").expect("shipped");
let wrong = entry.source.replace("timer = clint", "timer = dram");
let options = catalog::build_options()
.unwrap()
.with_media("firmware", &[][..])
.with_media("flash0", &[][..])
.with_media("flash1", &[][..])
.with_media("initrd", &[][..])
.with_media("disk", &[][..])
.with_param("console", "test.riscv.console.mistimed")
.with_param("power", "test.riscv.power.mistimed")
.with_param("ram", "8M");
let e = crate::machine::build(
"riscv-virt-mistimed",
&wrong,
&catalog::registry().unwrap(),
&options,
)
.expect_err("ram publishes no timebase");
let text = alloc::format!("{e}");
for want in ["cpu0", "dram", "timebase"] {
assert!(text.contains(want), "`{want}` missing from {text}");
}
}
#[test]
fn a_keystroke_crosses_the_uart_the_plic_and_meip() {
const HANDLER: u64 = 0x100;
let mut p = Program::new(DRAM);
p.la(asm::T0, DRAM + HANDLER);
p.push(asm::csrw(asm::CSR_MTVEC, asm::T0));
p.li(asm::T0, (PLIC + 4 * 10) as u32);
p.li(asm::T1, 1);
p.push(asm::sw(asm::T0, asm::T1, 0));
p.li(asm::T0, (PLIC + 0x2000) as u32);
p.li(asm::T1, 1 << 10);
p.push(asm::sw(asm::T0, asm::T1, 0));
p.li(asm::T0, (PLIC + 0x20_0000) as u32);
p.push(asm::sw(asm::T0, asm::ZERO, 0));
p.li(asm::T0, UART as u32);
p.li(asm::T1, 1);
p.push(asm::sb(asm::T0, asm::T1, 1));
p.li(asm::T0, 1 << 11);
p.push(asm::csrs(asm::CSR_MIE, asm::T0));
p.li(asm::T0, 1 << 3);
p.push(asm::csrs(asm::CSR_MSTATUS, asm::T0));
p.push(asm::wfi());
p.push(asm::j(-4));
p.pad_to(HANDLER);
p.li(asm::T0, (PLIC + 0x20_0004) as u32);
p.push(asm::lw(asm::T1, asm::T0, 0)); p.li(asm::T2, UART as u32);
p.push(asm::lbu(asm::T3, asm::T2, 0)); p.push(asm::sb(asm::T2, asm::T3, 0)); p.push(asm::sw(asm::T0, asm::T1, 0)); p.poweroff();
let mut b = board("plic-rx", &p.bytes());
b.console.feed(b"Z");
assert_eq!(b.run(400), Some(Request::Poweroff), "no interrupt arrived");
assert_eq!(b.output(), "Z", "the byte did not come back");
}
#[test]
fn a_guest_can_reboot_itself_through_the_system_controller() {
let mut p = Program::new(DRAM);
p.li(asm::T0, UART as u32);
p.putc(b'.');
p.li(asm::T0, SYSCON as u32);
p.li(asm::T1, u32::from(super::syscon::CMD_RESET));
p.push(asm::sw(asm::T0, asm::T1, 0));
p.push(asm::j(0));
let mut b = board("reboot", &p.bytes());
for _ in 0..200 {
b.machine.run_quantum().expect("the machine advances");
}
let lives = b.output().matches('.').count();
assert!(
lives >= 2,
"the hart came up {lives} time(s); the reset line never pulsed"
);
assert_eq!(b.power.peek(), Some(Request::Reboot), "and it said why");
}
#[test]
fn the_machine_snapshots_and_restores_to_the_same_state_hash() {
let mut p = Program::new(DRAM);
p.li(asm::T0, UART as u32);
p.putc(b'x');
p.poweroff();
let mut b = board("snapshot", &p.bytes());
b.run(50);
let saved = b.machine.save().expect("a machine saves");
let before = b.machine.state_hash().expect("a machine hashes");
b.machine.load(&saved).expect("its own snapshot loads");
assert_eq!(b.machine.state_hash().expect("hashes"), before);
}
#[test]
fn the_virtio_block_device_is_discoverable_and_serves_a_read() {
use crate::core::space::MemAttrs as Attrs;
let b = board("virtio", &[]);
let space = b.machine.space("mem").expect("one space");
let base = 0x1000_1000u64;
let read = |off: u64| {
space
.read(base + off, Width::U32, Attrs::DEFAULT)
.expect("the transport answers")
};
assert_eq!(read(0x000) as u32, super::virtio::mmio::MAGIC);
assert_eq!(read(0x004), u64::from(super::virtio::mmio::VERSION));
assert_eq!(read(0x008), u64::from(super::virtio::DEVICE_ID_BLOCK));
let capacity = space
.read(base + 0x100, Width::U64, Attrs::DEFAULT)
.expect("capacity");
assert_eq!(capacity, 16 * 1024 * 1024 / 512, "16 MiB in sectors");
let rng = 0x1000_2000u64;
assert_eq!(
space.read(rng + 0x008, Width::U32, Attrs::DEFAULT).unwrap(),
u64::from(super::virtio::DEVICE_ID_ENTROPY)
);
}
#[cfg(feature = "std")]
fn with_payload(source: &str, index: usize, slot: &str, addr: u64, len: u64) -> String {
let end = source
.rfind('}')
.expect("a machine description ends with a brace");
let mut out = String::from(&source[..end]);
if addr < DRAM {
let size = len.next_multiple_of(0x10_0000).max(0x10_0000);
out.push_str(&alloc::format!(
"\n object staging{index} \"ram\" {{ size = {size} }}\n map mem {addr:#x} size \
{size:#x} = staging{index}\n"
));
}
out.push_str(&alloc::format!(
"\n object payload{index} \"riscv.loader\" {{\n space = mem\n image = \
\"{slot}\"\n addr = {addr:#x}\n }}\n"
));
out.push_str(&source[end..]);
out
}
#[cfg(feature = "std")]
fn unescape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('\\') => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
#[cfg(feature = "std")]
#[test]
fn firmware_from_the_environment_reaches_its_console() {
let Ok(path) = std::env::var("RSEMU_RISCV_FIRMWARE") else {
eprintln!("skipped: set RSEMU_RISCV_FIRMWARE to a flat binary for 0x80000000");
return;
};
let image = std::fs::read(&path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
let ram = std::env::var("RSEMU_RISCV_RAM").unwrap_or_else(|_| String::from("256M"));
let quanta: usize = std::env::var("RSEMU_RISCV_QUANTA")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2_000_000);
let spec = std::env::var("RSEMU_RISCV_PAYLOAD").unwrap_or_default();
let payloads: Vec<(u64, Vec<u8>)> = spec
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|item| {
let (addr, path) = item
.split_once(':')
.unwrap_or_else(|| panic!("`{item}` is not `addr:path`"));
let addr = u64::from_str_radix(addr.trim().trim_start_matches("0x"), 16)
.unwrap_or_else(|e| panic!("`{addr}` is not a hexadecimal address: {e}"));
let bytes =
std::fs::read(path.trim()).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
eprintln!("payload {addr:#x}: {path} ({} bytes)", bytes.len());
(addr, bytes)
})
.collect();
let bank = |var: &str| -> Vec<u8> {
std::env::var(var).map_or_else(
|_| Vec::new(),
|path| {
let bytes =
std::fs::read(&path).unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
eprintln!("{var}: {path} ({} bytes)", bytes.len());
bytes
},
)
};
let flash0 = bank("RSEMU_RISCV_FLASH0");
let flash1 = bank("RSEMU_RISCV_FLASH1");
let initrd = bank("RSEMU_RISCV_INITRD");
let disk = bank("RSEMU_RISCV_DISK");
let console_name = String::from("test.riscv.console.firmware");
let power_name = String::from("test.riscv.power.firmware");
let entry = catalog::machine("riscv-virt").expect("shipped");
let mut options = catalog::build_options()
.expect("catalog")
.with_media("firmware", image.as_slice())
.with_media("flash0", flash0.as_slice())
.with_media("flash1", flash1.as_slice())
.with_media("initrd", initrd.as_slice())
.with_media("disk", disk.as_slice())
.with_param("console", console_name.clone())
.with_param("power", power_name.clone())
.with_param("ram", ram)
.with_param(
"initrd_addr",
std::env::var("RSEMU_RISCV_INITRD_ADDR").unwrap_or_else(|_| String::from("0x88000000")),
)
.with_param(
"cmdline",
std::env::var("RSEMU_RISCV_BOOTARGS")
.unwrap_or_else(|_| String::from("console=ttyS0 earlycon=sbi")),
);
let mut source = String::from(entry.source);
for (i, (addr, bytes)) in payloads.iter().enumerate() {
let slot = alloc::format!("payload{i}");
source = with_payload(&source, i, &slot, *addr, bytes.len() as u64);
options.realize.media.insert(slot, bytes.as_slice());
}
let machine = crate::machine::build(
entry.name,
&source,
&catalog::registry().expect("catalog"),
&options,
)
.expect("riscv-virt builds");
let mut b = Board {
machine,
console: ports::open(&options.realize.hosts, &console_name).expect("the UART opened it"),
power: signals::open(&options.realize.hosts, &power_name).expect("the syscon opened it"),
};
use std::io::Write as _;
let stop_at = std::env::var("RSEMU_RISCV_STOP_AT").unwrap_or_default();
let script: Vec<(String, String)> = std::env::var("RSEMU_RISCV_INPUT")
.unwrap_or_default()
.split('\n')
.filter(|s| !s.trim().is_empty())
.map(|step| {
let (marker, text) = step
.split_once("=>")
.unwrap_or_else(|| panic!("`{step}` is not `marker=>text`"));
(String::from(marker), unescape(text))
})
.collect();
let mut step = 0usize;
let window = script
.iter()
.map(|(marker, _)| marker.len())
.chain(core::iter::once(stop_at.len()))
.max()
.unwrap_or(0)
.max(1);
let mut printed = 0usize;
let mut seen = String::new();
eprintln!("--- guest console ---");
for _ in 0..quanta {
if b.power.peek().is_some() {
break;
}
b.machine.run_quantum().expect("the machine advances");
let out = b.output();
if out.is_empty() {
continue;
}
printed += out.len();
eprint!("{out}");
let _ = std::io::stderr().flush();
seen.push_str(&out);
if let Some((marker, text)) = script.get(step)
&& seen.contains(marker.as_str())
{
eprintln!("\n(typing `{}`)", text.escape_debug());
let fed = b.console.feed(text.as_bytes());
assert_eq!(
fed,
text.len(),
"the console took {fed} of {} byte(s)",
text.len()
);
step += 1;
seen.clear();
}
if !stop_at.is_empty() && step >= script.len() && seen.contains(&stop_at) {
eprintln!("\n(stopping: the guest printed `{stop_at}`)");
break;
}
if seen.len() > 4 * window {
seen.drain(..seen.len() - 2 * window);
}
}
let tail = b.output();
printed += tail.len();
eprintln!("{tail}\n--------------------- {printed} byte(s)");
if let Ok(out) = std::env::var("RSEMU_RISCV_FLASH1_OUT") {
let len = flash1.len().max(1);
let mut bytes = alloc::vec![0u8; len];
b.machine
.space("mem")
.expect("the board has one space")
.read_bytes(0x2200_0000, &mut bytes, crate::core::space::MemAttrs::DEBUG)
.expect("the variable bank is mapped");
std::fs::write(&out, &bytes).unwrap_or_else(|e| panic!("cannot write {out}: {e}"));
eprintln!("wrote {} byte(s) of flash1 to {out}", bytes.len());
}
assert!(
printed > 0,
"the firmware printed nothing in {quanta} quanta"
);
}
fn interrupts_of(dtb: &[u8], name: &str) -> Option<u32> {
prop_u32(dtb, name, "interrupts")
}
fn prop_u64(dtb: &[u8], name: &str, prop: &str) -> Option<u64> {
let bytes = prop_bytes(dtb, name, prop)?;
let eight: [u8; 8] = bytes.get(..8)?.try_into().ok()?;
Some(u64::from_be_bytes(eight))
}
fn prop_u32(dtb: &[u8], name: &str, prop: &str) -> Option<u32> {
let bytes = prop_bytes(dtb, name, prop)?;
let four: [u8; 4] = bytes.get(..4)?.try_into().ok()?;
Some(u32::from_be_bytes(four))
}
fn prop_bytes<'a>(dtb: &'a [u8], name: &str, prop: &str) -> Option<&'a [u8]> {
let word =
|at: usize| -> u32 { u32::from_be_bytes([dtb[at], dtb[at + 1], dtb[at + 2], dtb[at + 3]]) };
let off_struct = word(8) as usize;
let len_struct = word(36) as usize;
let off_strings = word(12) as usize;
let name_at = |at: usize| -> &str {
let end = dtb[at..].iter().position(|b| *b == 0).unwrap_or(0) + at;
core::str::from_utf8(&dtb[at..end]).unwrap_or("")
};
let mut at = off_struct;
let end = off_struct + len_struct;
let mut inside = false;
let mut depth = 0usize;
while at + 4 <= end {
let token = word(at);
at += 4;
match token {
1 => {
let node = name_at(at);
at += node.len() + 1;
at = at.next_multiple_of(4);
depth += 1;
if node == name {
inside = true;
}
}
2 => {
if inside && depth > 0 {
inside = false;
}
depth = depth.saturating_sub(1);
}
3 => {
let len = word(at) as usize;
let name_off = word(at + 4) as usize;
at += 8;
if inside && name_at(off_strings + name_off) == prop {
return dtb.get(at..at + len);
}
at += len;
at = at.next_multiple_of(4);
}
9 => break,
_ => {}
}
}
None
}