use std::io::Write as _;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use ch32rv_contract::policy::ImageFormat;
use ch32rv_contract::{ErrorKind, Warning};
use ch32rv_dmi::RegName;
use ch32rv_flash::params_for_family;
use crate::args::{Cli, ExitOn, RunArgs};
use crate::cmd_probe::{fail, select_entry};
use crate::parse;
use crate::session::Session;
const SEMI_SLLI: u32 = 0x01f0_1013;
const SEMI_EBREAK: u32 = 0x0010_0073;
const SEMI_SRAI: u32 = 0x4070_5013;
const SYS_WRITEC: u32 = 0x03;
const SYS_WRITE0: u32 = 0x04;
const SYS_EXIT: u32 = 0x18;
const SYS_EXIT_EXTENDED: u32 = 0x20;
const ADP_APPLICATION_EXIT: u32 = 0x2_0026;
enum ExitMode {
Timeout(Option<Duration>),
Semihosting { cap: Duration },
}
fn is_semihosting_seq(before: u32, at: u32, after: u32) -> bool {
before == SEMI_SLLI && at == SEMI_EBREAK && after == SEMI_SRAI
}
pub fn run(cli: &Cli, args: &RunArgs) -> ExitCode {
const CMD: &str = "run";
let exit_mode = match args.exit_on {
Some(ExitOn::Semihosting) => ExitMode::Semihosting {
cap: Duration::from_secs(cli.duration.unwrap_or(60)),
},
None | Some(ExitOn::Timeout) => ExitMode::Timeout(cli.duration.map(Duration::from_secs)),
};
if let Some(src) = args.source
&& !matches!(src, ch32rv_contract::policy::MonitorSource::Dmdata)
{
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
"run currently streams via --source dmdata; uart/sdi/rtt are a follow-up",
None,
);
}
let bytes = if args.no_flash {
Vec::new()
} else {
match std::fs::read(&args.elf) {
Ok(d) => d,
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
format!("read {}: {e}", args.elf.display()),
None,
);
}
}
};
let entry = match select_entry(cli, CMD) {
Ok(e) => e,
Err(c) => return c,
};
if entry.mode != ch32rv_contract::ProbeMode::Riscv {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
"run requires a probe in RISC-V mode",
None,
);
}
let (speed, mut warnings) = match parse::speed(&cli.speed) {
Ok(v) => v,
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
};
let timeout = Duration::from_millis(cli.timeout.map(|s| s * 1000).unwrap_or(3000));
let mut session = match Session::attach(
&entry,
speed,
timeout,
Duration::from_secs(cli.lock_timeout),
cli.chip.as_deref(),
&mut warnings,
) {
Ok(s) => s,
Err(e) => return crate::cmd_probe::session_error(cli, CMD, e),
};
if !args.no_flash {
let family = session.attach.family_byte;
let Some(fp) = params_for_family(family) else {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!("flashing family 0x{family:02x} is not supported"),
None,
);
};
let image = match crate::cmd_flash::parse_image(
&bytes,
ImageFormat::Auto,
&args.elf,
None,
ch32rv_flash::CODE_FLASH_START,
) {
Ok(i) => i,
Err(e) => return fail(cli, CMD, ErrorKind::Usage, e.to_string(), None),
};
for seg in &image.segments {
if let Err(e) = session.link().write_flash(seg.addr, &seg.data, &fp, |_| {}) {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("program failed at {:#010x}: {e}", seg.addr),
None,
);
}
}
}
let _ = session.link().soft_reset();
if !cli.json {
eprintln!(
"run: {} (Ctrl-C to stop)",
entry.dev.serial().unwrap_or("?")
);
}
match exit_mode {
ExitMode::Timeout(dur) => run_stream(cli, CMD, &mut session, dur, warnings),
ExitMode::Semihosting { cap } => run_semihosting(cli, CMD, &mut session, cap, warnings),
}
}
fn run_stream(
cli: &Cli,
cmd: &str,
session: &mut Session,
dur: Option<Duration>,
warnings: Vec<Warning>,
) -> ExitCode {
let deadline = dur.map(|d| Instant::now() + d);
let mut dm = session.dm();
let _ = dm.resume();
let mut out = std::io::stdout().lock();
loop {
if let Some(dl) = deadline
&& Instant::now() >= dl
{
break;
}
match dm.dmdata_poll(&[]) {
Ok(Some(bytes)) if !bytes.is_empty() => {
let _ = out.write_all(&bytes);
let _ = out.flush();
}
_ => std::thread::sleep(Duration::from_millis(20)),
}
}
if cli.json {
let mut env = ch32rv_contract::ResultEnvelope::success(cmd);
env.result = Some(serde_json::json!({ "exit": 0, "reason": "timeout" }));
env.warnings = warnings;
crate::print_envelope(&env)
} else {
ExitCode::SUCCESS
}
}
fn run_semihosting(
cli: &Cli,
cmd: &str,
session: &mut Session,
cap: Duration,
warnings: Vec<Warning>,
) -> ExitCode {
let deadline = Instant::now() + cap;
let mut out = std::io::stdout().lock();
let mut dm = session.dm();
let _ = dm.halt();
let _ = dm.enable_ebreak_debug();
let _ = dm.resume();
loop {
if Instant::now() >= deadline {
let msg = "run: timed out waiting for a semihosting exit";
if cli.json {
let env =
ch32rv_contract::ResultEnvelope::failure(cmd, ErrorKind::TransportTimeout, msg);
return crate::print_envelope(&env);
}
eprintln!("{msg}");
return ErrorKind::TransportTimeout.exit_code().into();
}
if let Ok(Some(b)) = dm.dmdata_poll(&[])
&& !b.is_empty()
{
let _ = out.write_all(&b);
let _ = out.flush();
}
match dm.is_halted() {
Ok(false) => {
std::thread::sleep(Duration::from_millis(5));
continue;
}
Ok(true) => {}
Err(_) => {
std::thread::sleep(Duration::from_millis(5));
continue;
}
}
let Ok(dpc) = dm.read_reg(RegName::Pc) else {
let _ = dm.resume();
continue;
};
let at = dm.read_mem(dpc, 4).ok();
let before = dm.read_mem(dpc.wrapping_sub(4), 4).ok();
let after = dm.read_mem(dpc.wrapping_add(4), 4).ok();
let w = |o: &Option<Vec<u8>>| {
o.as_ref()
.filter(|v| v.len() == 4)
.map(|v| u32::from_le_bytes([v[0], v[1], v[2], v[3]]))
};
match (w(&before), w(&at), w(&after)) {
(Some(b), Some(a), Some(af)) if is_semihosting_seq(b, a, af) => {
let op = dm.read_reg(RegName::Gpr(10)).unwrap_or(0); let arg = dm.read_reg(RegName::Gpr(11)).unwrap_or(0); match op {
SYS_EXIT | SYS_EXIT_EXTENDED => {
let code = semihosting_exit_code(&mut dm, op, arg);
if cli.json {
let mut env = ch32rv_contract::ResultEnvelope::success(cmd);
env.result = Some(serde_json::json!({ "exit": code }));
env.warnings = warnings;
return crate::print_envelope(&env);
}
return ExitCode::from(code as u8);
}
SYS_WRITE0 => {
let mut addr = arg;
'outer: for _ in 0..1024 {
let Ok(chunk) = dm.read_mem(addr, 16) else {
break;
};
for &byte in &chunk {
if byte == 0 {
break 'outer;
}
let _ = out.write_all(&[byte]);
}
addr = addr.wrapping_add(16);
}
let _ = out.flush();
let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
let _ = dm.resume();
}
SYS_WRITEC => {
if let Ok(c) = dm.read_mem(arg, 1)
&& let Some(&byte) = c.first()
{
let _ = out.write_all(&[byte]);
let _ = out.flush();
}
let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
let _ = dm.resume();
}
_ => {
let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
let _ = dm.resume();
}
}
}
_ => {
let msg = format!("run: target halted at {dpc:#010x} (not a semihosting call)");
if cli.json {
let env = ch32rv_contract::ResultEnvelope::failure(
cmd,
ErrorKind::NotRunningAfterWrite,
msg,
);
return crate::print_envelope(&env);
}
eprintln!("{msg}");
return ErrorKind::NotRunningAfterWrite.exit_code().into();
}
}
}
}
fn semihosting_exit_code(
dm: &mut ch32rv_dmi::DebugModule<'_, ch32rv_wchlink::WchLink>,
op: u32,
arg: u32,
) -> u32 {
if op == SYS_EXIT_EXTENDED {
if let Ok(block) = dm.read_mem(arg, 8)
&& block.len() == 8
{
return u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
}
return 1;
}
if arg == ADP_APPLICATION_EXIT { 0 } else { 1 }
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn detects_semihosting_sequence() {
assert!(is_semihosting_seq(SEMI_SLLI, SEMI_EBREAK, SEMI_SRAI));
assert!(!is_semihosting_seq(SEMI_SLLI, 0x0000_0013, SEMI_SRAI)); assert!(!is_semihosting_seq(0, SEMI_EBREAK, 0)); }
}