mod signal;
use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use anyhow::{Context, bail};
use clap::{Parser, Subcommand};
use rigless::machine::Machine;
use rigless::session::Session;
use rigless::sidecar::{Access, AccessKind, BackendError, SidecarOptions, StopReason, TargetStart};
use rigless::time::VirtualNanos;
const NANOS_PER_MICRO: u64 = 1_000;
const NANOS_PER_MILLI: u64 = 1_000_000;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
const NO_DEADLINE: VirtualNanos = VirtualNanos(i64::MAX as u64);
const UNITS: [(&str, u64); 4] = [
("ns", 1),
("us", NANOS_PER_MICRO),
("ms", NANOS_PER_MILLI),
("s", NANOS_PER_SECOND),
];
#[derive(Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Svd {
#[command(subcommand)]
command: Svd,
},
Run(Run),
}
#[derive(Subcommand)]
enum Svd {
Generate {
soc_directory: PathBuf,
},
}
#[derive(clap::Args)]
struct Run {
#[arg(long)]
machine: String,
#[arg(long, value_parser = parse_virtual_time)]
until: Option<VirtualNanos>,
#[arg(long, value_name = "PORT | MCU=PORT")]
gdb: Vec<String>,
#[arg(long, requires = "gdb")]
wait_for_debugger: bool,
#[arg(long, value_name = "PATH")]
sidecar: Option<PathBuf>,
#[arg(long, value_name = "MCU=PATH")]
firmware: Vec<String>,
image: Option<PathBuf>,
}
fn main() -> anyhow::Result<()> {
match Cli::parse().command {
Command::Svd {
command: Svd::Generate { soc_directory },
} => rigless::svd::write_module(&soc_directory)
.with_context(|| format!("generating {}", soc_directory.display())),
Command::Run(run) => match run_firmware(&run) {
Err(error) if matches!(error.downcast_ref(), Some(BackendError::Cancelled)) => {
Err(signal::die_of_recorded_signal().into())
}
result => result,
},
}
}
fn run_firmware(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::all();
let Some((_name, build_machine)) = machines.iter().find(|(name, _)| *name == run.machine)
else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {}; this build has {}",
run.machine,
names.join(", ")
);
};
let machine = build_machine().with_context(|| format!("building {}", run.machine))?;
let images = images_of(&machine, run)?;
let ports = debugger_ports_of(&machine, run)?;
let labels = labels_of(&machine);
let starts: Vec<TargetStart<'_>> = images
.iter()
.zip(&ports)
.map(|(firmware, &debugger_port)| TargetStart {
firmware,
debugger_port,
wait_for_debugger: run.wait_for_debugger,
})
.collect();
signal::install().context("routing SIGINT and SIGTERM to the run")?;
let options = SidecarOptions {
binary: run.sidecar.clone(),
cancel: &signal::CANCEL,
};
for (label, port) in labels.iter().zip(&ports) {
if let Some(port) = port {
let state = if run.wait_for_debugger {
"waiting for a debugger"
} else {
"serving GDB"
};
writeln!(io::stderr(), "{label}sidecar {state} on :{port}")?;
}
}
let mut session = Session::spawn(machine, &starts, &options)?;
let mut output = BufWriter::new(io::stdout().lock());
let mut write_error = None;
let until = run.until.unwrap_or(NO_DEADLINE);
let stops = session.run(until, &mut |target, access, value| {
if write_error.is_none() {
write_error = write_access(&mut output, &labels[target], access, value).err();
}
})?;
if let Some(error) = write_error {
return Err(error.into());
}
for (target, (label, stop)) in labels.iter().zip(&stops).enumerate() {
writeln!(
output,
"stop {label}t={} n={} reason={}",
stop.at.0,
stop.instruction_count,
stop_reason(stop.reason)
)?;
for (address, program_counter) in session.unmodeled(target) {
writeln!(
output,
"unmodeled {label}at={address:#010x} pc={program_counter:#010x}"
)?;
}
for (instance, interrupt) in session.unrouted(target) {
writeln!(
output,
"unrouted {label}instance={instance} interrupt={interrupt}"
)?;
}
}
output.flush()?;
session.shutdown()?;
Ok(())
}
fn labels_of(machine: &Machine) -> Vec<String> {
if machine.targets.len() == 1 {
return vec![String::new()];
}
machine
.target_names()
.map(|name| format!("target={name} "))
.collect()
}
fn images_of(machine: &Machine, run: &Run) -> anyhow::Result<Vec<PathBuf>> {
let named = by_target(machine, &run.firmware, |value| Ok(PathBuf::from(value)))?;
match (&run.image, machine.targets.len()) {
(Some(image), 1) if run.firmware.is_empty() => Ok(vec![image.clone()]),
(Some(_), 1) => bail!("give {} one firmware image, not two", machine.name),
(Some(_), _) => bail!(
"{} has the MCUs {}, so name each image as MCU=PATH",
machine.name,
names_of(machine)
),
(None, _) => named
.into_iter()
.zip(machine.target_names())
.map(|(image, target)| image.with_context(|| format!("{target} has no firmware image")))
.collect(),
}
}
fn debugger_ports_of(machine: &Machine, run: &Run) -> anyhow::Result<Vec<Option<u16>>> {
if let [only] = &run.gdb[..]
&& machine.targets.len() == 1
&& !only.contains('=')
{
let port = only.parse().context("the debugger port is not a port")?;
return Ok(vec![Some(port)]);
}
by_target(machine, &run.gdb, |value| {
value.parse().context("the debugger port is not a port")
})
}
fn by_target<T>(
machine: &Machine,
values: &[String],
parse: impl Fn(&str) -> anyhow::Result<T>,
) -> anyhow::Result<Vec<Option<T>>> {
let mut by_index: Vec<Option<T>> = machine.targets.iter().map(|_| None).collect();
for value in values {
let (name, rest) = value.split_once('=').with_context(|| {
format!(
"{value} names no MCU; {} has {}",
machine.name,
names_of(machine)
)
})?;
let target = machine.target(name).with_context(|| {
format!(
"{} has no MCU named {name}; it has {}",
machine.name,
names_of(machine)
)
})?;
if by_index[target].is_some() {
bail!("{name} is named twice");
}
by_index[target] = Some(parse(rest)?);
}
Ok(by_index)
}
fn names_of(machine: &Machine) -> String {
machine.target_names().collect::<Vec<_>>().join(", ")
}
fn write_access(
output: &mut impl Write,
label: &str,
access: &Access,
value: u64,
) -> io::Result<()> {
let kind = match access.kind {
AccessKind::Read => "read ",
AccessKind::Write => "write",
};
writeln!(
output,
"{kind} {label}t={} n={} pc={:#010x} at={:#010x} w={} v={value:#010x}",
access.virtual_time.0,
access.instruction_count,
access.program_counter,
access.address,
access.width.bits(),
)
}
fn stop_reason(reason: StopReason) -> &'static str {
match reason {
StopReason::Deadline => "deadline",
StopReason::Idle => "idle",
}
}
fn parse_virtual_time(text: &str) -> Result<VirtualNanos, String> {
let (digits, scale) = UNITS
.iter()
.find_map(|(unit, scale)| text.strip_suffix(unit).map(|digits| (digits, *scale)))
.ok_or_else(|| format!("{text} has no unit; use ns, us, ms, or s"))?;
let count: u64 = digits
.parse()
.map_err(|error| format!("{digits} is not a whole number: {error}"))?;
let nanos = count
.checked_mul(scale)
.ok_or_else(|| format!("{text} does not fit the virtual clock"))?;
if nanos > NO_DEADLINE.0 {
return Err(format!(
"{text} is longer than the virtual clock runs; the most is {} ns",
NO_DEADLINE.0
));
}
Ok(VirtualNanos(nanos))
}
#[cfg(test)]
mod tests {
use super::*;
use rigless::machine::Target;
use rigless::sidecar::{InterruptLine, SidecarCommand};
use rigless::soc::{Cpu, Region, Soc};
const AN_IMAGE: &str = "firmware.elf";
static MEMORY: [Region; 1] = [Region::ram("sram", 0x2000_0000, 0x1000)];
fn target(name: &'static str) -> Target {
Target {
name,
soc: Soc::builder(
Cpu {
name: "a-core",
fpu: false,
interrupt_lines: 1,
},
&MEMORY,
[],
)
.build()
.unwrap(),
sidecar: SidecarCommand {
binary: "a-sidecar",
machine: "a-machine",
cpu: "a-core",
properties: Vec::new(),
icount_shift: 0,
aliases: Vec::new(),
},
interrupt_line: InterruptLine::new,
}
}
fn one_mcu() -> Machine {
Machine {
name: "one",
targets: vec![target("only")],
}
}
fn two_mcus() -> Machine {
Machine {
name: "two",
targets: vec![target("first"), target("second")],
}
}
fn run_of(image: Option<&str>, firmware: &[&str], gdb: &[&str]) -> Run {
Run {
machine: "test".to_owned(),
until: Some(VirtualNanos(1)),
wait_for_debugger: false,
gdb: gdb.iter().map(|value| (*value).to_owned()).collect(),
sidecar: None,
firmware: firmware.iter().map(|value| (*value).to_owned()).collect(),
image: image.map(PathBuf::from),
}
}
#[test]
fn a_machine_with_one_mcu_takes_the_image_on_its_own() {
let images = images_of(&one_mcu(), &run_of(Some(AN_IMAGE), &[], &[])).unwrap();
assert_eq!(images, [PathBuf::from(AN_IMAGE)]);
let ports = debugger_ports_of(&one_mcu(), &run_of(Some(AN_IMAGE), &[], &["3333"])).unwrap();
assert_eq!(ports, [Some(3333)]);
}
#[test]
fn a_machine_with_several_mcus_takes_one_image_each_by_name() {
let run = run_of(None, &["second=two.elf", "first=one.elf"], &["second=3333"]);
assert_eq!(
images_of(&two_mcus(), &run).unwrap(),
[PathBuf::from("one.elf"), PathBuf::from("two.elf")],
"the images come back in the machine's order, not the command line's"
);
assert_eq!(
debugger_ports_of(&two_mcus(), &run).unwrap(),
[None, Some(3333)]
);
}
#[test]
fn an_image_that_names_no_mcu_of_the_machine_is_refused() {
let named = |values: &[&str]| {
images_of(&two_mcus(), &run_of(None, values, &[]))
.unwrap_err()
.to_string()
};
assert!(
named(&["one.elf"]).contains("names no MCU"),
"a bare image names none of several"
);
assert!(named(&["third=one.elf"]).contains("no MCU named third"));
assert!(named(&["first=one.elf"]).contains("second has no firmware image"));
assert!(named(&["first=one.elf", "first=two.elf"]).contains("first is named twice"));
assert!(
images_of(&two_mcus(), &run_of(Some(AN_IMAGE), &[], &[]))
.unwrap_err()
.to_string()
.contains("name each image as MCU=PATH"),
"the positional image belongs to a machine with one MCU"
);
let named_as_well = format!("{}=other.elf", one_mcu().targets[0].name);
assert!(
images_of(&one_mcu(), &run_of(Some(AN_IMAGE), &[&named_as_well], &[]))
.unwrap_err()
.to_string()
.contains("one firmware image, not two")
);
}
#[test]
fn a_duration_parses_with_each_unit_and_refuses_the_rest() {
assert_eq!(parse_virtual_time("48ns"), Ok(VirtualNanos(48)));
assert_eq!(parse_virtual_time("7us"), Ok(VirtualNanos(7_000)));
assert_eq!(parse_virtual_time("2ms"), Ok(VirtualNanos(2_000_000)));
assert_eq!(parse_virtual_time("1s"), Ok(VirtualNanos(NANOS_PER_SECOND)));
assert!(parse_virtual_time("10").is_err());
assert!(parse_virtual_time("1.5ms").is_err());
assert!(parse_virtual_time("99999999999999999999s").is_err());
assert_eq!(
parse_virtual_time(&format!("{}ns", NO_DEADLINE.0)),
Ok(NO_DEADLINE)
);
assert!(
parse_virtual_time(&format!("{}ns", NO_DEADLINE.0 + 1)).is_err(),
"a deadline the engine reads as the past runs nothing, so it is refused here"
);
}
#[test]
fn a_run_without_until_has_no_deadline_and_serves_gdb_without_holding_it() {
let run = Cli::parse_from(["rigless", "run", "--machine", "pixracer", AN_IMAGE]);
let Command::Run(run) = run.command else {
panic!("the run command")
};
assert_eq!(run.until, None, "--until has no default");
assert!(!run.wait_for_debugger);
let held = Cli::parse_from([
"rigless",
"run",
"--machine",
"pixracer",
"--gdb",
"3333",
"--wait-for-debugger",
AN_IMAGE,
]);
let Command::Run(held) = held.command else {
panic!("the run command")
};
assert!(held.wait_for_debugger);
assert!(
Cli::try_parse_from([
"rigless",
"run",
"--machine",
"pixracer",
"--wait-for-debugger",
AN_IMAGE
])
.is_err(),
"waiting needs a port to wait on"
);
}
}