use std::process::ExitCode;
use std::time::{Duration, Instant};
use ch32rv_contract::policy::{EraseMode, ImageFormat};
use ch32rv_contract::{ErrorKind, ResultEnvelope, Warning};
use ch32rv_isp::{Identity, IspConfig, IspDevice, IspError, OptionFields};
use ch32rv_usb::UsbDeviceInfo;
use ch32rv_dmi::{DmiError, ResetMode, TargetAccess};
use crate::args::{
Cli, IspArgs, IspCmd, IspConfigCmd, IspEntry, IspReset, IspTransport, IspVerify, IspVia,
};
use crate::cmd_probe::fail;
const CODE_FLASH_START: u32 = 0x0800_0000;
pub fn isp(cli: &Cli, args: &IspArgs) -> ExitCode {
match &args.cmd {
IspCmd::List => list(cli),
IspCmd::Info => info(cli, args),
IspCmd::Flash {
file,
erase,
verify,
reset,
} => flash(cli, args, file, *erase, *verify, *reset),
IspCmd::Verify { file } => verify_only(cli, args, file),
IspCmd::Erase => erase_only(cli, args),
IspCmd::Reset => reset(cli, args),
IspCmd::Enter { via: IspVia::Probe } => enter(cli, args),
IspCmd::Config(IspConfigCmd::Get) => config_get(cli, args),
IspCmd::Config(IspConfigCmd::Set { kv }) => config_set(cli, args, kv),
}
}
struct Opened {
dev: IspDevice,
usb: Option<String>,
topology: Option<String>,
at: String,
id: Identity,
cfg: IspConfig,
}
fn open(cli: &Cli, cmd: &str, args: &IspArgs) -> Result<Opened, ExitCode> {
open_with(cli, cmd, args, args.entry)
}
fn open_with(cli: &Cli, cmd: &str, args: &IspArgs, entry: IspEntry) -> Result<Opened, ExitCode> {
if args.transport == IspTransport::Uart {
return open_uart(cli, cmd, args, entry);
}
let mut app = None;
let mut want = match args.device.as_deref() {
None => None,
Some(d) => match position(d) {
Some(Ok(t)) => Some(t.to_owned()),
Some(Err(())) => {
return Err(fail(
cli,
cmd,
ErrorKind::Usage,
format!("--device `{d}`: no position"),
Some("--device topology:<bus>-<ports> or ch32rv-isp://<bus>-<ports>"),
));
}
None => match find_usb_device(d) {
Ok(a) => {
let t = a.dev.topology();
app = Some(a);
Some(t)
}
Err(m) if m.app_gone() => None,
Err(m) => return Err(m.fail(cli, cmd)),
},
},
};
let present = |want: &Option<String>| {
ch32rv_isp::list().is_ok_and(|v| {
v.iter()
.any(|d| want.as_deref().is_none_or(|t| d.topology() == t))
})
};
let mut wait = Duration::from_secs(args.wait);
let mut entered = false;
if entry != IspEntry::None && !ch32rv_usb::replay::active() && !present(&want) {
let via = match entry {
IspEntry::Probe => Via::Probe,
_ => match enter_via_app(cli, cmd, args, entry, app)? {
Some(at) => {
want.get_or_insert(at);
Via::App
}
None if entry == IspEntry::Auto && cli.probe.is_some() => Via::Probe,
None => Via::Nothing,
},
};
if via == Via::Probe {
enter_via_probe(cli, cmd)?;
}
if via != Via::Nothing {
entered = true;
wait = wait.max(ENTER_WAIT);
}
}
let deadline = Instant::now() + wait;
let mut told = false;
let dev = loop {
let found = match ch32rv_isp::list() {
Ok(v) => v,
Err(e) => {
return Err(fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
e.to_string(),
None,
));
}
};
let mut found: Vec<UsbDeviceInfo> = found
.into_iter()
.filter(|d| want.as_deref().is_none_or(|t| d.topology() == t))
.collect();
if found.len() > 1 {
let at: Vec<serde_json::Value> = found
.iter()
.map(|d| serde_json::json!(format!("topology:{}", d.topology())))
.collect();
return Err(crate::cmd_probe::fail_with_candidates(
cli,
cmd,
ErrorKind::DeviceAmbiguous,
format!("{} ISP devices are connected", found.len()),
Some(
"pick one with --device ch32rv-isp://<bus>-<ports> (the IDE's ch32rv-isp:// port)",
),
at,
));
}
if let Some(d) = found.pop() {
break d;
}
if ch32rv_usb::replay::active() || Instant::now() >= deadline {
return Err(fail(
cli,
cmd,
ErrorKind::DeviceNotFound,
match (&want, entered) {
(_, true) => format!(
"the chip was sent to its bootloader, but no ISP device appeared{} within {} s",
want.as_deref()
.map(|t| format!(" at topology:{t}"))
.unwrap_or_default(),
wait.as_secs()
),
(Some(t), false) => format!("no ISP device at topology:{t}"),
(None, false) => "no ISP device (1a86:55e0 / 4348:55e0) was found".to_owned(),
},
Some(if entered {
"is the chip's own USB (D+/D-) connected to this host? the bootloader returns to the application after ~10 s unattended"
} else {
ENTRY_HINT
}),
));
}
if !told && !entered {
told = true;
eprintln!(
"waiting up to {} s for an ISP device (1a86:55e0) ...",
wait.as_secs()
);
eprintln!(" {ENTRY_HINT}");
}
std::thread::sleep(Duration::from_millis(200));
};
if told {
eprintln!("ISP device found at topology:{}", dev.topology());
}
let usb = dev.usb_id();
let topology = dev.topology();
let mut isp = IspDevice::open(&dev).map_err(|e| isp_fail(cli, cmd, e))?;
if let Some(s) = cli.timeout {
isp.set_timeout(Duration::from_secs(s));
}
let id = isp.identify().map_err(|e| isp_fail(cli, cmd, e))?;
let cfg = isp.read_config().map_err(|e| isp_fail(cli, cmd, e))?;
Ok(Opened {
dev: isp,
usb: Some(usb),
at: format!("topology:{topology}"),
topology: Some(topology),
id,
cfg,
})
}
fn open_uart(cli: &Cli, cmd: &str, args: &IspArgs, entry: IspEntry) -> Result<Opened, ExitCode> {
let Some(port) = args.port.as_deref() else {
return Err(fail(
cli,
cmd,
ErrorKind::Usage,
"--transport uart needs --port <serial port>".to_owned(),
Some("the serial port wired to the chip's bootloader USART (e.g. /dev/ttyUSB0)"),
));
};
let baud = args.baud.unwrap_or(ch32rv_isp::UART_BAUD);
let mut wait = Duration::from_secs(args.wait);
match entry {
IspEntry::None => {}
IspEntry::Probe => {
enter_via_probe(cli, cmd)?;
wait = wait.max(ENTER_WAIT);
}
_ => {
let via_app = enter_via_app(cli, cmd, args, entry, None)?.is_some();
if !via_app && entry == IspEntry::Auto && cli.probe.is_some() {
enter_via_probe(cli, cmd)?;
}
wait = wait.max(ENTER_WAIT);
}
}
let deadline = Instant::now() + wait;
let mut isp = IspDevice::open_serial(port, baud).map_err(|e| isp_fail(cli, cmd, e))?;
isp.set_timeout(Duration::from_millis(500));
let id = loop {
match isp.identify() {
Ok(id) => break id,
Err(e @ ch32rv_isp::IspError::NotFactoryIsp(_)) => return Err(isp_fail(cli, cmd, e)),
Err(e) if Instant::now() >= deadline => {
return Err(fail(
cli,
cmd,
ErrorKind::DeviceNotFound,
format!("no ISP bootloader answered on {port} at {baud} bps ({e})"),
Some(ENTRY_HINT),
));
}
Err(_) => std::thread::sleep(Duration::from_millis(200)),
}
};
isp.set_timeout(
cli.timeout
.map_or(Duration::from_secs(5), Duration::from_secs),
);
let cfg = isp.read_config().map_err(|e| isp_fail(cli, cmd, e))?;
Ok(Opened {
dev: isp,
usb: None,
topology: None,
at: format!("uart:{port}"),
id,
cfg,
})
}
const ENTRY_HINT: &str = "put the chip in its bootloader: CH32X03x - plug in USB with the BOOT/DOWNLOAD key (PC17 = D+ high) held, or `--entry probe` over a debug probe; BOOT0 parts - BOOT0 high at reset";
fn isp_fail(cli: &Cli, cmd: &str, e: IspError) -> ExitCode {
let (kind, hint) = match &e {
IspError::Usb(ch32rv_usb::UsbError::Open(_)) => (
ErrorKind::DeviceOpenFailed,
Some("check permissions for the device (udev rule for 1a86:55e0 / 4348:55e0)"),
),
IspError::Serial(_) => (ErrorKind::DeviceOpenFailed, None),
IspError::Usb(_)
| IspError::ShortWrite { .. }
| IspError::UnexpectedReply { .. }
| IspError::SerialTimeout
| IspError::Checksum { .. } => (ErrorKind::TransferFailed, None),
IspError::NotFactoryIsp(_) => (
ErrorKind::CapabilityUnsupported,
Some("a WCH-Link in IAP mode uses the same USB ID: see `ch32rv probe firmware`"),
),
IspError::KeyMismatch { .. } | IspError::Status { .. } | IspError::NoKey => {
(ErrorKind::TransferFailed, None)
}
IspError::VerifyMismatch(_) => (ErrorKind::VerifyMismatch, None),
IspError::ImageTooLarge { .. } => (ErrorKind::Usage, None),
};
fail(cli, cmd, kind, e.to_string(), hint)
}
fn hex(b: &[u8]) -> String {
b.iter().map(|b| format!("{b:02x}")).collect()
}
fn series(id: Identity) -> String {
ch32rv_isp::series_name(id.device_type).map_or_else(
|| format!("unknown (0x{:02x})", id.device_type),
str::to_owned,
)
}
fn describe(o: &Opened) -> serde_json::Value {
serde_json::json!({
"usb": o.usb,
"topology": o.topology,
"at": o.at,
"series": ch32rv_isp::series_name(o.id.device_type),
"chip_id": format!("0x{:02x}", o.id.chip_id),
"device_type": format!("0x{:02x}", o.id.device_type),
"btver": o.cfg.btver_text(),
"uid": hex(&o.cfg.uid),
"read_protected": o.cfg.read_protected(),
"config": hex(&o.cfg.config),
"flash_bytes": ch32rv_isp::flash_size(o.id),
})
}
fn list(cli: &Cli) -> ExitCode {
const CMD: &str = "isp.list";
let devs = match ch32rv_isp::list() {
Ok(v) => v,
Err(e) => return fail(cli, CMD, ErrorKind::DeviceOpenFailed, e.to_string(), None),
};
let mut rows = Vec::new();
for d in &devs {
let answer = IspDevice::open(d).and_then(|mut i| {
i.set_timeout(Duration::from_secs(1));
i.identify()
});
rows.push(match answer {
Ok(id) => serde_json::json!({
"usb": d.usb_id(), "topology": d.topology(), "kind": "factory-isp",
"series": ch32rv_isp::series_name(id.device_type),
"chip_id": format!("0x{:02x}", id.chip_id),
"device_type": format!("0x{:02x}", id.device_type),
}),
Err(e) => serde_json::json!({
"usb": d.usb_id(), "topology": d.topology(), "kind": "unknown",
"error": e.to_string(),
}),
});
}
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({ "devices": rows }));
return crate::print_envelope(&env);
}
if rows.is_empty() {
println!("no ISP device");
}
for r in &rows {
let s = |k: &str| r[k].as_str().unwrap_or("-").to_owned();
if s("kind") == "factory-isp" {
println!(
"topology:{:<14} {} {} chip 0x{} type {}",
s("topology"),
s("usb"),
s("series"),
s("chip_id").trim_start_matches("0x"),
s("device_type")
);
} else {
println!(
"topology:{:<14} {} not a factory bootloader ({})",
s("topology"),
s("usb"),
s("error")
);
}
}
ExitCode::SUCCESS
}
fn info(cli: &Cli, args: &IspArgs) -> ExitCode {
const CMD: &str = "isp.info";
let o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(describe(&o));
return crate::print_envelope(&env);
}
match &o.usb {
Some(usb) => println!("usb: {usb} at {}", o.at),
None => println!("port: {}", o.at),
}
println!(
"chip: {} (chip 0x{:02x}, type 0x{:02x})",
series(o.id),
o.id.chip_id,
o.id.device_type
);
if let Some(n) = ch32rv_isp::flash_size(o.id) {
println!("flash: {} KiB", n / 1024);
}
println!("btver: {}", o.cfg.btver_text());
println!("uid: {}", hex(&o.cfg.uid));
println!(
"protect: {}",
if o.cfg.read_protected() {
"read-protected"
} else {
"off"
}
);
println!("config: {}", hex(&o.cfg.config));
ExitCode::SUCCESS
}
fn load_image(cli: &Cli, cmd: &str, file: &std::path::Path) -> Result<Vec<u8>, ExitCode> {
let bytes = std::fs::read(file).map_err(|e| {
fail(
cli,
cmd,
ErrorKind::Usage,
format!("read {}: {e}", file.display()),
None,
)
})?;
let image =
crate::cmd_flash::parse_image(&bytes, ImageFormat::Auto, file, None, CODE_FLASH_START)
.map_err(|e| fail(cli, cmd, ErrorKind::Usage, e.to_string(), None))?;
let mut flat = Vec::new();
for s in &image.segments {
let base = if s.addr >= CODE_FLASH_START {
s.addr - CODE_FLASH_START
} else {
s.addr
};
if base >= 0x0010_0000 {
return Err(fail(
cli,
cmd,
ErrorKind::Usage,
format!(
"segment at 0x{:08x} is outside code flash; the ISP route writes code flash only",
s.addr
),
None,
));
}
let end = base as usize + s.data.len();
if flat.len() < end {
flat.resize(end, 0xff);
}
flat[base as usize..end].copy_from_slice(&s.data);
}
if flat.is_empty() {
return Err(fail(cli, cmd, ErrorKind::Usage, "image is empty", None));
}
Ok(flat)
}
fn check_size(cli: &Cli, cmd: &str, o: &Opened, image: &[u8]) -> Result<(), ExitCode> {
match ch32rv_isp::flash_size(o.id) {
Some(limit) if image.len() > limit => Err(isp_fail(
cli,
cmd,
IspError::ImageTooLarge {
actual: image.len(),
limit,
},
)),
_ => Ok(()),
}
}
fn progress(cli: &Cli, phase: &'static str, total: usize) -> impl FnMut(usize) {
let show =
!cli.json && cli.progress.is_none() && std::io::IsTerminal::is_terminal(&std::io::stderr());
let mut last = 0usize;
move |done| {
if show && (done - last >= 4096 || done == total) {
last = done;
eprint!("\r{phase}: {done}/{total} bytes");
if done == total {
eprintln!();
}
}
}
}
fn flash(
cli: &Cli,
args: &IspArgs,
file: &std::path::Path,
erase: EraseMode,
verify: IspVerify,
reset: IspReset,
) -> ExitCode {
const CMD: &str = "isp.flash";
let image = match load_image(cli, CMD, file) {
Ok(i) => i,
Err(c) => return c,
};
let store = std::fs::read(file)
.ok()
.and_then(|b| ch32rv_flash::store_region(&b, CODE_FLASH_START));
let mut o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
if let Err(c) = check_size(cli, CMD, &o, &image) {
return c;
}
let cfg = o.cfg.clone();
if let Err(e) = o.dev.set_key(&cfg) {
return isp_fail(cli, CMD, e);
}
let erased = match isp_erase_plan(erase, image.len(), store, ch32rv_isp::flash_size(o.id)) {
Ok(p) => p,
Err(m) => {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
m,
Some("use --erase sector or keep-store, or erase first with `isp erase`"),
);
}
};
if let Some(n) = erased
&& let Err(e) = o.dev.erase_exact(n)
{
return isp_fail(cli, CMD, e);
}
if let Err(e) = o.dev.program(&image, progress(cli, "program", image.len())) {
return isp_fail(cli, CMD, e);
}
let verified = verify == IspVerify::On;
if verified && let Err(e) = o.dev.verify(&image, progress(cli, "verify", image.len())) {
return isp_fail(cli, CMD, e);
}
let running = reset == IspReset::Run;
let summary = describe(&o);
if let Err(e) = o.dev.end(running) {
return isp_fail(cli, CMD, e);
}
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({
"flash": {
"bytes": image.len(),
"programmer": "isp",
"family": summary["series"],
"usb": summary["usb"],
"chip_id": summary["chip_id"],
"erased_sectors": erased,
"verified": verified.then_some(true),
"running": running,
"skipped": false,
"scope": erase.as_str(),
},
"device": summary,
}));
if erase == EraseMode::KeepStore
&& let Some(r) = env.result.as_mut()
{
r["flash"]["store"] = crate::cmd_flash::store_json(store, "untouched");
}
crate::print_envelope(&env)
} else {
println!(
"flashed {} byte(s) through the ISP bootloader ({} at {}){}{}",
image.len(),
series(o.id),
o.at,
if verified { ", verified" } else { "" },
if running {
", application started"
} else {
", left in the bootloader"
}
);
ExitCode::SUCCESS
}
}
fn isp_erase_plan(
erase: EraseMode,
image_len: usize,
store: Option<ch32rv_flash::StoreRegion>,
flash_size: Option<usize>,
) -> Result<Option<u32>, String> {
let covered = ch32rv_isp::sectors_for(image_len);
Ok(match erase {
EraseMode::None => None,
EraseMode::Chip => match flash_size {
Some(size) => Some(ch32rv_isp::sectors_for(size)),
None => {
return Err(
"the code flash size of this chip is not known, so --erase chip cannot be sized"
.to_owned(),
);
}
},
EraseMode::Sector => Some(ch32rv_isp::erase_count(covered)),
EraseMode::KeepStore => {
let n = ch32rv_isp::erase_count(covered);
let reaches = store.is_some_and(|s| {
let store_off = u64::from(s.addr.saturating_sub(CODE_FLASH_START));
u64::from(n) * ch32rv_isp::SECTOR as u64 > store_off
});
Some(if reaches { covered } else { n })
}
})
}
fn verify_only(cli: &Cli, args: &IspArgs, file: &std::path::Path) -> ExitCode {
const CMD: &str = "isp.verify";
let image = match load_image(cli, CMD, file) {
Ok(i) => i,
Err(c) => return c,
};
let mut o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
if let Err(c) = check_size(cli, CMD, &o, &image) {
return c;
}
let cfg = o.cfg.clone();
if let Err(e) = o.dev.set_key(&cfg) {
return isp_fail(cli, CMD, e);
}
if let Err(e) = o.dev.verify(&image, progress(cli, "verify", image.len())) {
return isp_fail(cli, CMD, e);
}
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({ "bytes": image.len(), "verified": true }));
crate::print_envelope(&env)
} else {
println!("verified {} byte(s): match", image.len());
ExitCode::SUCCESS
}
}
fn erase_only(cli: &Cli, args: &IspArgs) -> ExitCode {
const CMD: &str = "isp.erase";
let mut o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
let Some(size) = ch32rv_isp::flash_size(o.id) else {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"code flash size of {} is not known; nothing was erased",
series(o.id)
),
None,
);
};
if let Err(m) = crate::cmd_probe::confirm_destructive(cli, "erase all of code flash?") {
return fail(cli, CMD, ErrorKind::Usage, m, None);
}
let sectors = ch32rv_isp::sectors_for(size);
if let Err(e) = o.dev.erase(sectors) {
return isp_fail(cli, CMD, e);
}
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({ "erased_sectors": sectors }));
crate::print_envelope(&env)
} else {
println!("erased {sectors} sector(s) (all of code flash)");
ExitCode::SUCCESS
}
}
fn reset(cli: &Cli, args: &IspArgs) -> ExitCode {
const CMD: &str = "isp.reset";
let o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
if let Err(e) = o.dev.end(true) {
return isp_fail(cli, CMD, e);
}
if cli.json {
crate::print_envelope(&ResultEnvelope::success(CMD))
} else {
println!("left the bootloader; application started");
ExitCode::SUCCESS
}
}
const ENTER_WAIT: Duration = Duration::from_secs(5);
const FLASH_KEYR: u32 = 0x4002_2004;
const FLASH_STATR: u32 = 0x4002_200c;
const FLASH_BOOT_MODEKEYR: u32 = 0x4002_2028;
const FLASH_KEY1: u32 = 0x4567_0123;
const FLASH_KEY2: u32 = 0xcdef_89ab;
const FLASH_OBR: u32 = 0x4002_201c;
const OBR_RDPRT: u32 = 1 << 1;
fn protected(cli: &Cli, cmd: &str) -> ExitCode {
fail(
cli,
cmd,
ErrorKind::TargetProtected,
"the chip is read-protected, and its factory bootloader does not run while it is",
Some(
"clear the protection over the debug wire: `ch32rv recover --method unprotect` (erases code flash)",
),
)
}
const STATR_BOOT_MODE: u32 = 1 << 14;
fn boot_mode_family(family: &str) -> bool {
matches!(
family.to_ascii_uppercase().as_str(),
"CH32X035" | "CH32X033" | "CH32H417"
)
}
fn set_boot_mode<T: TargetAccess + ?Sized>(t: &mut T) -> Result<(), DmiError> {
t.reset(ResetMode::HaltAtReset)?;
t.write_words(FLASH_KEYR, &[FLASH_KEY1])?;
t.write_words(FLASH_KEYR, &[FLASH_KEY2])?;
t.write_words(FLASH_BOOT_MODEKEYR, &[FLASH_KEY1])?;
t.write_words(FLASH_BOOT_MODEKEYR, &[FLASH_KEY2])?;
let statr = t.read_words(FLASH_STATR, 1)?[0];
t.write_words(FLASH_STATR, &[statr | STATR_BOOT_MODE])?;
let statr = t.read_words(FLASH_STATR, 1)?[0];
if statr & STATR_BOOT_MODE == 0 {
return Err(DmiError::OperationFailed(format!(
"BOOT_MODE did not stick (FLASH_STATR = 0x{statr:08x})"
)));
}
t.reset(ResetMode::Run).map(|_| ())
}
fn enter_via_probe(cli: &Cli, cmd: &str) -> Result<(), ExitCode> {
let a = match crate::oep::addr(cli, cmd) {
Ok(Some(a)) => a,
Ok(None) => return enter_via_wchlink(cli, cmd),
Err(c) => return Err(c),
};
let mut outcome = Ok(());
let code = crate::oep::with_attached(cli, cmd, &a, true, |x| {
let family = x.family.clone().unwrap_or_default();
if !boot_mode_family(&family) {
return fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
format!(
"{} has no software-selected boot mode ch32rv knows; nothing was changed",
if family.is_empty() {
"this chip"
} else {
&family
}
),
Some(ENTRY_HINT),
);
}
match x.t.read_words(FLASH_OBR, 1) {
Ok(v) if v[0] & OBR_RDPRT != 0 => {
outcome = Err(());
return protected(cli, cmd);
}
_ => {}
}
if let Err(e) = set_boot_mode(&mut x.t) {
outcome = Err(());
return fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("entering the bootloader: {e}"),
None,
);
}
ExitCode::SUCCESS
});
if code != ExitCode::SUCCESS || outcome.is_err() {
return Err(code);
}
if !cli.json {
eprintln!("reset into the bootloader over the debug wire");
}
Ok(())
}
fn enter_via_wchlink(cli: &Cli, cmd: &str) -> Result<(), ExitCode> {
let mut session = crate::cmd_probe::attach(cli, cmd)?;
let family = crate::cmd_target::db_family_of(&mut session);
if !boot_mode_family(&family) {
return Err(fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
format!(
"{family} has no software-selected boot mode ch32rv knows; nothing was changed"
),
Some(ENTRY_HINT),
));
}
let step = |r: Result<(), DmiError>| {
r.map_err(|e| {
fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("entering the bootloader: {e}"),
None,
)
})
};
{
let mut dm = session.dm();
if dm.read_mem32(FLASH_OBR).is_ok_and(|v| v & OBR_RDPRT != 0) {
return Err(protected(cli, cmd));
}
step(dm.reset_halt())?;
for (addr, key) in [
(FLASH_KEYR, FLASH_KEY1),
(FLASH_KEYR, FLASH_KEY2),
(FLASH_BOOT_MODEKEYR, FLASH_KEY1),
(FLASH_BOOT_MODEKEYR, FLASH_KEY2),
] {
step(dm.write_mem32(addr, key))?;
}
let statr = dm.read_mem32(FLASH_STATR);
step(statr.and_then(|v| dm.write_mem32(FLASH_STATR, v | STATR_BOOT_MODE)))?;
let statr = dm.read_mem32(FLASH_STATR);
step(statr.and_then(|v| {
if v & STATR_BOOT_MODE == 0 {
Err(DmiError::OperationFailed(format!(
"BOOT_MODE did not stick (FLASH_STATR = 0x{v:08x})"
)))
} else {
Ok(())
}
}))?;
}
crate::cmd_flash::soft_reset_and_run(&mut session).map_err(|m| {
fail(
cli,
cmd,
ErrorKind::TransferFailed,
format!("entering the bootloader: {m}"),
None,
)
})?;
if !cli.json {
eprintln!("reset into the bootloader over the debug wire");
}
Ok(())
}
fn enter(cli: &Cli, args: &IspArgs) -> ExitCode {
const CMD: &str = "isp.enter";
let o = match open_with(cli, CMD, args, IspEntry::Probe) {
Ok(o) => o,
Err(c) => return c,
};
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(describe(&o));
crate::print_envelope(&env)
} else {
println!(
"in the bootloader: {} (chip 0x{:02x}) at {}, btver {}",
series(o.id),
o.id.chip_id,
o.at,
o.cfg.btver_text()
);
ExitCode::SUCCESS
}
}
#[derive(PartialEq, Eq)]
enum Via {
Probe,
App,
Nothing,
}
fn position(d: &str) -> Option<Result<&str, ()>> {
d.strip_prefix(ISP_SCHEME)
.or_else(|| d.strip_prefix("topology:"))
.map(|t| if t.is_empty() { Err(()) } else { Ok(t) })
}
pub(crate) struct AppDevice {
pub(crate) dev: UsbDeviceInfo,
pub(crate) port: Option<String>,
}
fn ide_address(s: &str) -> Option<&str> {
let com = s.len() > 3
&& s[..3].eq_ignore_ascii_case("com")
&& s[3..].chars().all(|c| c.is_ascii_digit());
let url = ch32rv_usb::selector::ADDRESS_SCHEMES
.iter()
.any(|scheme| s.starts_with(scheme));
(url || s.starts_with('/') || s.starts_with("\\\\.\\") || com).then_some(s)
}
pub(crate) fn usb_device(cli: &Cli, cmd: &str, sel: &str) -> Result<AppDevice, ExitCode> {
find_usb_device(sel).map_err(|m| m.fail(cli, cmd))
}
struct Miss {
kind: ErrorKind,
msg: String,
hint: Option<String>,
candidates: Vec<serde_json::Value>,
}
impl Miss {
fn new(kind: ErrorKind, msg: String, hint: Option<String>) -> Self {
Miss {
kind,
msg,
hint,
candidates: Vec::new(),
}
}
fn fail(self, cli: &Cli, cmd: &str) -> ExitCode {
crate::cmd_probe::fail_with_candidates(
cli,
cmd,
self.kind,
self.msg,
self.hint.as_deref(),
self.candidates,
)
}
fn app_gone(&self) -> bool {
matches!(
self.kind,
ErrorKind::DeviceNotFound | ErrorKind::DeviceAmbiguous
)
}
}
fn find_usb_device(sel: &str) -> Result<AppDevice, Miss> {
if let Some(instead) = ch32rv_usb::selector::removed(sel) {
return Err(Miss::new(
ErrorKind::Usage,
format!("--device `{sel}`: this form is gone; write {instead}"),
None,
));
}
for (scheme, what) in [
(crate::cmd_boot::HID_SCHEME, "boot hid flash"),
(ISP_SCHEME, "isp"),
] {
if sel.starts_with(scheme) {
return Err(Miss::new(
ErrorKind::Usage,
format!("--device `{sel}` names a bootloader this command does not write"),
Some(format!("`{what} --device {sel}` writes it")),
));
}
}
let usb_sel;
let sel = match ide_address(sel).and_then(|a| a.strip_prefix(APP_SCHEME)) {
Some(id) => {
let dev = find_app(id).map_err(|(kind, m)| Miss::new(kind, m, None))?;
usb_sel = format!("topology:{}", dev.topology());
usb_sel.as_str()
}
None => sel,
};
let all = ch32rv_usb::enumerate()
.map_err(|e| Miss::new(ErrorKind::DeviceOpenFailed, e.to_string(), None))?;
let canon = |p: &str| {
std::fs::canonicalize(p)
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_else(|_| p.to_owned())
};
let (mut found, port): (Vec<UsbDeviceInfo>, Option<String>) =
if let Some(path) = ide_address(sel) {
let want = canon(path);
let mut found: Vec<UsbDeviceInfo> = all
.into_iter()
.filter(|d| d.serial_ports().iter().any(|p| canon(p) == want))
.collect();
let tops: Vec<String> = found.iter().map(UsbDeviceInfo::topology).collect();
found.retain(|d| {
let t = d.topology();
!tops
.iter()
.any(|o| o.len() > t.len() && o.starts_with(&format!("{t}.")))
});
(found, Some(path.to_owned()))
} else if let Some(t) = sel.strip_prefix("topology:") {
(
all.into_iter().filter(|d| d.topology() == t).collect(),
None,
)
} else {
let mut parts = sel.splitn(3, ':');
let num = |s: Option<&str>| {
s.and_then(|v| u16::from_str_radix(v.trim_start_matches("0x"), 16).ok())
};
let (Some(vid), Some(pid)) = (num(parts.next()), num(parts.next())) else {
return Err(Miss::new(
ErrorKind::Usage,
format!("invalid --device `{sel}`"),
Some(
"use the serial port's path, topology:<bus>-<ports> or VID:PID[:SERIAL]"
.to_owned(),
),
));
};
let serial = parts.next().filter(|s| !s.is_empty());
let found = all
.into_iter()
.filter(|d| {
d.vid() == vid && d.pid() == pid && serial.is_none_or(|s| d.serial() == Some(s))
})
.collect();
(found, None)
};
match found.len() {
0 => Err(Miss::new(
ErrorKind::DeviceNotFound,
format!("no USB device matches --device `{sel}`"),
None,
)),
1 => {
let dev = found.remove(0);
let port = port.or_else(|| dev.serial_ports().into_iter().next());
Ok(AppDevice { dev, port })
}
n => Err(Miss {
candidates: found
.iter()
.map(|d| serde_json::json!(format!("topology:{}", d.topology())))
.collect(),
..Miss::new(
ErrorKind::DeviceAmbiguous,
format!("{n} USB devices match --device `{sel}`"),
Some("name it by position: --device topology:<bus>-<ports>".to_owned()),
)
}),
}
}
fn enter_via_app(
cli: &Cli,
cmd: &str,
args: &IspArgs,
entry: IspEntry,
resolved: Option<AppDevice>,
) -> Result<Option<String>, ExitCode> {
if let Some(t) = args
.device
.as_deref()
.and_then(|d| d.strip_prefix(ISP_SCHEME))
{
return Ok(Some(t.to_owned()));
}
let app = match (resolved, args.device.as_deref()) {
(Some(a), _) => Some(a),
(None, Some(d)) if d.starts_with("topology:") => {
let t = &d["topology:".len()..];
let all = ch32rv_usb::enumerate()
.map_err(|e| fail(cli, cmd, ErrorKind::DeviceOpenFailed, e.to_string(), None))?;
match all.into_iter().find(|dev| dev.topology() == t) {
Some(dev) => {
let port = dev.serial_ports().into_iter().next();
Some(AppDevice { dev, port })
}
None if entry == IspEntry::Auto => None,
None => {
return Err(fail(
cli,
cmd,
ErrorKind::DeviceNotFound,
format!("no USB device at topology:{t}"),
None,
));
}
}
}
(None, Some(sel)) => Some(find_usb_device(sel).map_err(|mut m| {
if m.app_gone() {
m.hint = Some(format!(
"the application is not connected and no ISP device is up; pick the board's port as it is now (ch32rv-isp://… once the chip is in its bootloader){}",
m.hint.map(|h| format!(" ({h})")).unwrap_or_default()
));
}
m.fail(cli, cmd)
})?),
(None, None) if entry == IspEntry::DfuDetach => {
let all = ch32rv_usb::enumerate()
.map_err(|e| fail(cli, cmd, ErrorKind::DeviceOpenFailed, e.to_string(), None))?;
let mut dfu: Vec<UsbDeviceInfo> = all.into_iter().filter(is_ch32rv_app).collect();
match dfu.len() {
0 => {
return Err(fail(
cli,
cmd,
ErrorKind::DeviceNotFound,
format!(
"no USB device offers a DFU run-time interface named `{APP_ENTRY_NAME}`"
),
Some("name the application with --device"),
));
}
1 => Some(AppDevice {
dev: dfu.remove(0),
port: None,
}),
n => {
return Err(crate::cmd_probe::fail_with_candidates(
cli,
cmd,
ErrorKind::DeviceAmbiguous,
format!(
"{n} USB devices offer a DFU run-time interface named `{APP_ENTRY_NAME}`"
),
Some("name the application with --device topology:<bus>-<ports>"),
dfu.iter()
.map(|d| serde_json::json!(format!("topology:{}", d.topology())))
.collect(),
));
}
}
}
(None, None) if entry == IspEntry::Touch1200 => {
return Err(fail(
cli,
cmd,
ErrorKind::Usage,
"--entry touch1200 needs the application's serial port",
Some("--device <serial port>"),
));
}
(None, None) => None,
};
let Some(app) = app else {
return Ok(None);
};
let at = app.dev.topology();
let dfu = app.dev.dfu_runtime_interface();
let use_dfu = match entry {
IspEntry::DfuDetach => true,
IspEntry::Auto => dfu.is_some(),
_ => false,
};
if use_dfu {
let Some(intf) = dfu else {
return Err(fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
format!("the device at topology:{at} has no DFU run-time interface"),
Some("use --entry touch1200 for an application with a serial port"),
));
};
if let Err(e) = app.dev.dfu_detach(intf, DETACH_TIMEOUT_MS) {
crate::warn(
"dfu-detach",
format!("{e} (waiting for the bootloader anyway)"),
);
}
if !cli.json {
eprintln!("sent DFU_DETACH to topology:{at} (interface {intf})");
}
return Ok(Some(at));
}
let Some(port) = app.port else {
return match entry {
IspEntry::Auto => Ok(None),
_ => Err(fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
format!("the device at topology:{at} has no serial port to touch"),
None,
)),
};
};
touch1200(&port).map_err(|e| {
fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
format!("1200 bps touch on {port}: {e}"),
None,
)
})?;
if !cli.json {
eprintln!("1200 bps touch on {port} (topology:{at})");
}
Ok(Some(at))
}
const DETACH_TIMEOUT_MS: u16 = 1000;
fn touch1200(port: &str) -> Result<(), serialport::Error> {
let mut p = serialport::new(port, 1200)
.timeout(Duration::from_millis(100))
.open()?;
let _ = p.write_data_terminal_ready(false);
drop(p);
Ok(())
}
fn option_layout(id: Identity) -> (Option<&'static str>, crate::optbytes::Layout) {
let db = match id.device_type {
0x14 | 0x15 => Some("CH32V103"),
0x17 => Some("CH32V307"),
0x18 | 0x19 => Some("CH32V20x"),
0x23 => Some("CH32X035"),
0x24 => Some("CH32L103"),
_ => None,
};
let label = series(id);
let layout = match db {
Some(f) => crate::optbytes::Layout {
label,
..crate::optbytes::Layout::of(f)
},
None => crate::optbytes::Layout {
semantics: matches!(id.device_type, 0x20 | 0x21 | 0x25)
.then(|| ch32rv_target::option_semantics("CH32V006"))
.flatten(),
user_fields: Vec::new(),
label,
},
};
(db, layout)
}
fn options_result(o: &Opened, cfg: &[u8; 12]) -> (serde_json::Value, Vec<Warning>) {
let (db, layout) = option_layout(o.id);
let warnings = layout
.interim_warning()
.map(|msg| Warning {
code: "option-decode-interim".to_owned(),
msg,
})
.into_iter()
.collect();
let v = crate::optbytes::result(
ch32rv_isp::series_name(o.id.device_type),
db,
&hex(cfg),
&layout,
OptionFields::from_config(cfg),
);
(v, warnings)
}
fn config_get(cli: &Cli, args: &IspArgs) -> ExitCode {
const CMD: &str = "isp.config.get";
let o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
let (v, warnings) = options_result(&o, &o.cfg.config);
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(v);
env.warnings = warnings;
crate::print_envelope(&env)
} else {
crate::optbytes::print(&v);
for w in &warnings {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
ExitCode::SUCCESS
}
}
fn config_set(cli: &Cli, args: &IspArgs, kv: &[String]) -> ExitCode {
const CMD: &str = "isp.config.set";
let mut o = match open(cli, CMD, args) {
Ok(o) => o,
Err(c) => return c,
};
let before = OptionFields::from_config(&o.cfg.config);
let mut want = before;
let (_, layout) = option_layout(o.id);
let rdp_changed = match crate::optbytes::apply(&layout, &mut want, kv, 0x00) {
Ok(a) => a.read_protect.is_some(),
Err(m) => {
return fail(
cli,
CMD,
ErrorKind::Usage,
m,
Some(&format!("keys for {}: {}", layout.label, layout.keys())),
);
}
};
if rdp_changed {
let what = if want.rdpr == ch32rv_isp::RDPR_OFF {
"turn read protection off? the chip erases all of code flash"
} else {
"turn read protection on? the debug port can no longer read flash, the factory bootloader stops working (measured on CH32X035), and only `recover --method unprotect` over a debug probe turns it off again, erasing code flash"
};
if let Err(m) = crate::cmd_probe::confirm_destructive(cli, what) {
return fail(cli, CMD, ErrorKind::Usage, m, None);
}
}
if want != before
&& let Err(e) = o.dev.write_config(&want.to_config())
{
return isp_fail(cli, CMD, e);
}
let after_cfg = match o.dev.read_config() {
Ok(c) => c.config,
Err(e) => return isp_fail(cli, CMD, e),
};
let after = OptionFields::from_config(&after_cfg);
if after != want {
return fail(
cli,
CMD,
ErrorKind::VerifyMismatch,
format!(
"the option bytes read back as {} after writing {}",
hex(&after.to_config()),
hex(&want.to_config())
),
Some("some option changes take effect only after a reset; check with `isp config get`"),
);
}
let (mut v, warnings) = options_result(&o, &after_cfg);
if cli.json {
v["changed"] = serde_json::json!(want != before);
let mut env = ResultEnvelope::success(CMD);
env.result = Some(v);
env.warnings = warnings;
crate::print_envelope(&env)
} else {
if want == before {
println!("no change");
}
crate::optbytes::print(&v);
for w in &warnings {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
ExitCode::SUCCESS
}
}
pub(crate) const APP_ENTRY_NAME: &str = "CH32RV";
pub(crate) const APP_SCHEME: &str = "ch32rv-app://";
pub(crate) const ISP_SCHEME: &str = "ch32rv-isp://";
pub(crate) fn app_id(d: &UsbDeviceInfo, apps: &[UsbDeviceInfo]) -> String {
match d.serial().filter(|s| !s.is_empty()) {
Some(s) if apps.iter().filter(|o| o.serial() == Some(s)).count() == 1 => s.to_owned(),
_ => format!("usb-{}", d.topology()),
}
}
pub(crate) fn find_app(id: &str) -> Result<UsbDeviceInfo, (ErrorKind, String)> {
let apps: Vec<UsbDeviceInfo> = ch32rv_usb::enumerate()
.map_err(|e| (ErrorKind::DeviceOpenFailed, e.to_string()))?
.into_iter()
.filter(is_ch32rv_app)
.collect();
let mut hits: Vec<&UsbDeviceInfo> = apps
.iter()
.filter(|d| match id.strip_prefix("usb-") {
Some(t) => d.topology() == t,
None => d.serial() == Some(id),
})
.collect();
match hits.len() {
0 => Err((
ErrorKind::DeviceNotFound,
format!("no CH32RV application {APP_SCHEME}{id} is connected"),
)),
1 => {
let found = hits.remove(0).topology();
apps.into_iter().find(|d| d.topology() == found).ok_or((
ErrorKind::DeviceNotFound,
format!("{APP_SCHEME}{id} went away"),
))
}
n => Err((
ErrorKind::DeviceAmbiguous,
format!("{n} CH32RV applications have the serial {id}; pick the port by position"),
)),
}
}
pub(crate) fn is_ch32rv_app(d: &UsbDeviceInfo) -> bool {
d.dfu_runtime_interface().is_some_and(|n| {
d.interface_names()
.iter()
.any(|(i, name)| *i == n && name == APP_ENTRY_NAME)
})
}
#[cfg(test)]
mod tests {
use super::*;
use ch32rv_flash::StoreRegion;
#[test]
fn isp_erase_plan_per_mode() {
let k = 1024;
assert_eq!(isp_erase_plan(EraseMode::None, 4 * k, None, None), Ok(None));
assert_eq!(
isp_erase_plan(EraseMode::Sector, 4 * k, None, None),
Ok(Some(8))
);
assert_eq!(
isp_erase_plan(EraseMode::Chip, 4 * k, None, Some(62 * k)),
Ok(Some(62))
);
assert!(isp_erase_plan(EraseMode::Chip, 4 * k, None, None).is_err());
assert_eq!(
isp_erase_plan(EraseMode::KeepStore, 4 * k, None, None),
Ok(Some(8))
);
let high = StoreRegion {
addr: CODE_FLASH_START + 61 * k as u32,
len: k as u32,
};
assert_eq!(
isp_erase_plan(EraseMode::KeepStore, 4 * k, Some(high), None),
Ok(Some(8))
);
let low = StoreRegion {
addr: CODE_FLASH_START + 7 * k as u32,
len: k as u32,
};
assert_eq!(
isp_erase_plan(EraseMode::KeepStore, 4 * k, Some(low), None),
Ok(Some(4))
);
}
}