use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
use rigless::time::VirtualNanos;
#[cfg(not(emulator_absent))]
use anyhow::bail;
#[cfg(not(emulator_absent))]
use rigless::chip::uart::Host;
#[cfg(not(emulator_absent))]
use rigless::machine::Board;
#[cfg(not(emulator_absent))]
use std::io::{self, Write};
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_name = "MCU")]
target: Option<String>,
#[arg(long, value_parser = parse_virtual_time)]
until: Option<VirtualNanos>,
#[arg(long, value_name = "PORT")]
gdb: Option<u16>,
#[arg(long, requires = "gdb")]
wait_for_debugger: bool,
#[arg(long, value_name = "PATH")]
coverage: Option<PathBuf>,
#[arg(long, value_parser = parse_virtual_time, value_name = "PERIOD")]
actuators: Option<VirtualNanos>,
#[arg(long, conflicts_with_all = ["until", "actuators", "coverage"])]
link: bool,
#[arg(long, value_name = "WIRE=PORT")]
host: Vec<String>,
image: 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) => run_firmware(&run),
}
}
#[cfg(not(emulator_absent))]
fn attach_hosts(machine: &mut dyn Board, name: &str, ports: &[String]) -> anyhow::Result<()> {
let mut chosen: Vec<(&str, u16)> = Vec::with_capacity(ports.len());
for override_ in ports {
let (wire, port) = override_
.split_once('=')
.with_context(|| format!("{override_} is not WIRE=PORT"))?;
let port = port
.parse()
.with_context(|| format!("{port} is not a port"))?;
chosen.push((wire, port));
}
for wire in machine.hosts() {
let port = chosen
.iter()
.find(|(named, _)| *named == wire.name)
.map_or(wire.port, |(_, port)| *port);
let host = Host::listen(port)
.with_context(|| format!("listening for {} on :{port}", wire.name))?;
let port = host.port()?;
if !machine.attach_host(wire.name, Box::new(host)) {
bail!("{name} names no wire called {}", wire.name);
}
writeln!(io::stderr(), "{} on :{port}", wire.name)?;
}
Ok(())
}
#[cfg(not(emulator_absent))]
fn target_index(machine: &dyn Board, named: Option<&str>) -> anyhow::Result<usize> {
let names = machine.target_names();
match (named, &names[..]) {
(Some(name), _) => machine.target(name).with_context(|| {
format!(
"{} has no MCU named {name}; it has {}",
machine.name(),
names.join(", ")
)
}),
(None, [_only]) => Ok(0),
(None, _) => bail!(
"{} has the MCUs {}, so name the one to run with --target",
machine.name(),
names.join(", ")
),
}
}
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(emulator_absent)]
fn run_firmware(_run: &Run) -> anyhow::Result<()> {
Err(rigless::emulator::Error::NoEmulator.into())
}
#[cfg(not(emulator_absent))]
fn run_firmware(run: &Run) -> anyhow::Result<()> {
if run.link {
linked::run(run)
} else {
standalone::run(run)
}
}
#[cfg(not(emulator_absent))]
mod standalone {
use std::fs;
use std::io::{self, BufWriter, Write};
use anyhow::{Context, bail};
use rigless::coverage::Coverage;
use rigless::edge::Actuator;
use rigless::emulator::{Access, AccessKind, Driver, Finished, Start, Stop, StopReason};
use rigless::machine::Board;
use rigless::session::Session;
use rigless::time::VirtualNanos;
use super::{NO_DEADLINE, Run, attach_hosts, target_index};
pub(super) fn run(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::all();
let Some((name, build)) = 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 mut board = build().with_context(|| format!("building {name}"))?;
let target = target_index(&*board, run.target.as_deref())?;
attach_hosts(&mut *board, name, &run.host)?;
if let Some(port) = run.gdb {
let state = if run.wait_for_debugger {
"waiting for a debugger"
} else {
"serving GDB"
};
writeln!(io::stderr(), "{state} on :{port}")?;
}
let coverage = Coverage::of(board.mcu(target).expect("the index was checked"));
let until = run.until.unwrap_or(NO_DEADLINE);
let first = run.actuators.map_or(until, |period| period.min(until));
let session = Session::new(board, target, first)?;
let start = Start {
firmware: &run.image,
debugger_port: run.gdb,
wait_for_debugger: run.wait_for_debugger,
};
let driver = Printing {
output: BufWriter::new(io::stdout()),
coverage,
until,
period: run.actuators,
actuators: Vec::new(),
failed: None,
};
let Finished {
mut session,
mut driver,
stop,
} = rigless::emulator::run(session, &start, driver)?;
if let Some(error) = driver.failed.take() {
return Err(error.into());
}
if let Some(error) = session.take_error() {
return Err(error.into());
}
let output = &mut driver.output;
if let Some(stop) = stop {
writeln!(
output,
"stop t={} n={} reason={}",
stop.at.0,
stop.instruction_count,
stop_reason(stop.reason)
)?;
}
for reset in session.resets() {
writeln!(output, "reset t={}", reset.0)?;
}
for (address, program_counter) in session.unmodeled() {
writeln!(
output,
"unmodeled at={address:#010x} pc={program_counter:#010x}"
)?;
}
for (address, seen) in session.unbacked() {
let wrote = seen
.written
.map_or_else(String::new, |value| format!(" wrote={value:#010x}"));
writeln!(
output,
"unbacked instance={} register={} at={address:#010x} pc={:#010x} reads={} \
writes={}{wrote}",
seen.instance, seen.register, seen.program_counter, seen.reads, seen.writes
)?;
}
for (instance, interrupt) in session.unrouted() {
writeln!(output, "unrouted instance={instance} interrupt={interrupt}")?;
}
if let Some(path) = &run.coverage {
fs::write(path, driver.coverage.to_string())
.with_context(|| format!("writing {}", path.display()))?;
}
output.flush()?;
Ok(())
}
struct Printing<W: Write + Send> {
output: BufWriter<W>,
coverage: Coverage,
until: VirtualNanos,
period: Option<VirtualNanos>,
actuators: Vec<Actuator>,
failed: Option<io::Error>,
}
impl<W: Write + Send> Driver<dyn Board> for Printing<W> {
fn on_access(&mut self, access: &Access, value: u64) {
self.coverage.saw(access);
if self.failed.is_none() {
self.failed = write_access(&mut self.output, access, value).err();
}
}
fn on_stop(
&mut self,
session: &mut Session<dyn Board>,
stop: Stop,
) -> Option<VirtualNanos> {
let period = self.period?;
let read = session.board_mut().actuators();
if read != self.actuators {
for actuator in &read {
if self.failed.is_none() {
self.failed = write_actuator(&mut self.output, stop.at, actuator).err();
}
}
self.actuators = read;
}
if stop.at >= self.until {
return None;
}
Some(VirtualNanos(
stop.at.0.saturating_add(period.0).min(self.until.0),
))
}
}
fn write_actuator(
output: &mut impl Write,
at: VirtualNanos,
actuator: &Actuator,
) -> io::Result<()> {
match (actuator.pulse, actuator.command) {
(Some(pulse), Some(command)) => writeln!(
output,
"actuator t={} name={} width={} period={} command={command}",
at.0, actuator.name, pulse.width.0, pulse.period.0
),
_ => writeln!(output, "actuator t={} name={} none", at.0, actuator.name),
}
}
fn write_access(output: &mut impl Write, access: &Access, value: u64) -> io::Result<()> {
let kind = match access.kind {
AccessKind::Read => "read ",
AccessKind::Write => "write",
};
writeln!(
output,
"{kind} 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",
StopReason::Reset => "reset",
}
}
}
#[cfg(not(emulator_absent))]
mod linked {
use std::io::{Stdin, Stdout};
use anyhow::{Context, bail};
use rigless::edge::Coupled;
use rigless::emulator::{Driver, Finished, Start, Stop};
use rigless::link::{Frame, LinkError, Output, Report, Served};
use rigless::session::Session;
use rigless::time::VirtualNanos;
use super::{Run, attach_hosts, target_index};
pub(super) fn run(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::coupled();
let Some((name, build)) = machines.iter().find(|(name, _)| *name == run.machine) else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {} has an edge a plant couples to; this build has {}",
run.machine,
names.join(", ")
);
};
let mut board = build().with_context(|| format!("building {name}"))?;
let target = target_index(&*board, run.target.as_deref())?;
attach_hosts(&mut *board, name, &run.host)?;
let mut served = Served::stdio();
let Frame::Advance { until, signals } = served.receive()? else {
served.report(Report::default())?;
return Ok(());
};
if let Some(bytes) = signals {
board.sense_wire(&bytes)?;
}
let first = until;
let session = Session::new(board, target, first)?;
let start = Start {
firmware: &run.image,
debugger_port: run.gdb,
wait_for_debugger: run.wait_for_debugger,
};
let driver = Linked {
served,
failed: None,
};
let Finished {
mut session,
mut driver,
..
} = rigless::emulator::run(session, &start, driver)?;
if let Some(error) = driver.failed.take() {
return Err(error.into());
}
if let Some(error) = session.take_error() {
return Err(error.into());
}
driver.served.report(Report {
resets: session.resets().to_vec(),
unmodeled: count(session.unmodeled().len()),
refused: count(session.refused().len()),
unbacked: count(session.unbacked().len()),
unrouted: count(session.unrouted().len()),
})?;
Ok(())
}
fn count(entries: usize) -> u64 {
u64::try_from(entries).expect("a count fits")
}
struct Linked {
served: Served<Stdin, Stdout>,
failed: Option<LinkError>,
}
impl Driver<dyn Coupled> for Linked {
fn on_stop(
&mut self,
session: &mut Session<dyn Coupled>,
stop: Stop,
) -> Option<VirtualNanos> {
let outputs = session
.board_mut()
.actuators()
.into_iter()
.map(|actuator| Output {
pulse: actuator.pulse,
command: actuator.command,
})
.collect();
if let Err(error) = self.served.reached(stop, outputs) {
self.failed = Some(error);
return None;
}
match self.served.receive() {
Ok(Frame::Advance { until, signals }) => {
if let Some(bytes) = signals
&& let Err(error) = session.board_mut().sense_wire(&bytes)
{
self.failed = Some(error.into());
return None;
}
Some(until)
}
Ok(_) => None,
Err(error) => {
self.failed = Some(error);
None
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_duration_parses_with_each_unit_and_refuses_the_rest() {
assert_eq!(parse_virtual_time("5ns"), Ok(VirtualNanos(5)));
assert_eq!(parse_virtual_time("5us"), Ok(VirtualNanos(5_000)));
assert_eq!(parse_virtual_time("5ms"), Ok(VirtualNanos(5_000_000)));
assert_eq!(parse_virtual_time("5s"), Ok(VirtualNanos(5_000_000_000)));
assert!(parse_virtual_time("5").is_err());
assert!(parse_virtual_time("fives").is_err());
assert!(parse_virtual_time("99999999999999999999s").is_err());
assert!(
parse_virtual_time("9223372036854775808ns").is_err(),
"past the emulator's signed deadline"
);
}
}