use std::process::ExitCode;
use std::time::{Duration, Instant};
use ch32rv_contract::{ErrorKind, ResultEnvelope};
use ch32rv_dmi::{ResetMode, TargetAccess};
use ch32rv_oep::registry::core as oep_core;
use ch32rv_oep::session::{OepError, Probe, random_session_id};
use ch32rv_oep::target::{AttachOptions, OepDtm, WireKind, attach, detach};
use ch32rv_usb::Selector;
use crate::args::{Cli, FlashArgs};
use crate::cmd_probe::fail;
use crate::parse;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OepAddr {
Serial(String),
Probe {
path: String,
port: Option<String>,
target: Option<String>,
whole: bool,
},
Tcp(String),
Net {
addr: String,
unit: String,
port: Option<String>,
target: Option<String>,
whole: bool,
},
Wch(crate::broker::BrokerTarget),
}
impl OepAddr {
pub(crate) fn port(&self) -> Option<&str> {
match self {
OepAddr::Probe { port, .. } | OepAddr::Net { port, .. } => {
port.as_deref().filter(|p| *p != "any")
}
_ => None,
}
}
pub(crate) fn whole_probe(&self) -> bool {
matches!(
self,
OepAddr::Probe { whole: true, .. } | OepAddr::Net { whole: true, .. }
)
}
pub(crate) fn target(&self) -> Option<&str> {
match self {
OepAddr::Probe { target, .. } | OepAddr::Net { target, .. } => target.as_deref(),
_ => None,
}
}
}
const BROWSE_WAIT: Duration = Duration::from_millis(1500);
pub(crate) fn net_target(a: &OepAddr) -> Option<crate::broker::BrokerTarget> {
match a {
OepAddr::Tcp(t) if crate::broker::is_broker_endpoint(t) => None,
OepAddr::Tcp(t) => {
let unit = unit_at(t);
if unit.is_none() {
eprintln!(
"warning[oep-unit-unknown]: {t} did not say its unit id; its broker is keyed by the address"
);
}
Some(crate::broker::BrokerTarget::Net {
addr: t.clone(),
unit,
by_unit: false,
})
}
OepAddr::Net { addr, unit, .. } => Some(crate::broker::BrokerTarget::Net {
addr: addr.clone(),
unit: Some(unit.clone()),
by_unit: true,
}),
_ => None,
}
}
pub(crate) fn unit_at(addr: &str) -> Option<String> {
let memo = ch32rv_usb::runtime_dir().join(format!(
"{}.unit",
ch32rv_usb::sanitize_key(&format!("oep-tcp-{addr}"))
));
if let Some(u) = std::fs::read_to_string(&memo)
.ok()
.map(|t| t.trim().to_owned())
.filter(|u| !u.is_empty())
{
let t = crate::broker::BrokerTarget::Net {
addr: addr.to_owned(),
unit: Some(u.clone()),
by_unit: false,
};
if crate::broker::existing_link_for(&t).is_some() {
return Some(u);
}
}
let u = read_unit(addr)?;
let _ = std::fs::create_dir_all(ch32rv_usb::runtime_dir());
let _ = std::fs::write(&memo, format!("{u}\n"));
Some(u)
}
fn read_unit(addr: &str) -> Option<String> {
use std::net::ToSocketAddrs;
let sa = addr.to_socket_addrs().ok()?.next()?;
let s = std::net::TcpStream::connect_timeout(&sa, Duration::from_secs(2)).ok()?;
let _ = s.set_nodelay(true);
let mut link = ch32rv_oep::link::Link::new(Box::new(s), ch32rv_oep::link::Framing::Length);
link.set_timeout(Duration::from_secs(2));
let mut p = Probe::connect(link).ok()?;
p.describe(ch32rv_oep::registry::core::FN)
.ok()?
.into_iter()
.find(|t| t.tag == ch32rv_oep::registry::core::tlvs::describe::UNIT_ID)
.map(|t| String::from_utf8_lossy(&t.value).to_ascii_lowercase())
}
pub(crate) fn open_net(addr: &str, unit: &str) -> Result<Probe, String> {
let link = ch32rv_oep::link::open_tcp(addr).map_err(|e| format!("{addr}: {e}"))?;
let mut p = Probe::connect(link).map_err(|e| format!("{addr}: {e}"))?;
let said = p
.describe(ch32rv_oep::registry::core::FN)
.ok()
.and_then(|t| {
t.into_iter()
.find(|t| t.tag == ch32rv_oep::registry::core::tlvs::describe::UNIT_ID)
})
.map(|t| String::from_utf8_lossy(&t.value).into_owned());
match said {
Some(u) if u.eq_ignore_ascii_case(unit) => Ok(p),
Some(u) => Err(format!(
"{addr} announced unit {unit} but describes itself as {u}: not used"
)),
None => Err(format!("{addr} does not say its unit_id: not used")),
}
}
pub(crate) fn is_oep_device(dev: &ch32rv_usb::UsbDeviceInfo) -> bool {
use ch32rv_oep::registry::usb;
dev.vid() == usb::PROJECT_VID && dev.pid() == usb::PROJECT_PID
}
pub(crate) fn oep_devices() -> Vec<ch32rv_usb::UsbDeviceInfo> {
ch32rv_usb::enumerate()
.unwrap_or_default()
.into_iter()
.filter(is_oep_device)
.collect()
}
pub(crate) fn probe_id(dev: &ch32rv_usb::UsbDeviceInfo) -> String {
dev.serial()
.map(str::to_owned)
.unwrap_or_else(|| dev.topology())
}
pub(crate) fn oep_port(dev: &ch32rv_usb::UsbDeviceInfo) -> Option<String> {
dev.serial_ports().into_iter().next()
}
fn listed_on_usb(p: &mut Probe) -> bool {
let Ok(tlvs) = p.describe(oep_core::FN) else {
return false;
};
let Some(unit) = tlvs
.iter()
.find(|t| t.tag == oep_core::tlvs::describe::UNIT_ID)
.map(|t| String::from_utf8_lossy(&t.value).into_owned())
else {
return false;
};
let serials: Vec<String> = oep_devices()
.iter()
.filter_map(|d| d.serial().map(str::to_owned))
.collect();
unit_among(&unit, &serials)
}
fn unit_among(unit_id: &str, serials: &[String]) -> bool {
!unit_id.is_empty() && serials.iter().any(|s| s.eq_ignore_ascii_case(unit_id))
}
fn port_of_oep_device(path: &str) -> bool {
let sel = Selector::Port(path.to_owned());
oep_devices()
.iter()
.enumerate()
.any(|(i, d)| sel.matches(d, i))
}
struct VendorBulk(ch32rv_usb::BulkPipe);
impl ch32rv_oep::link::ByteStream for VendorBulk {
fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> {
self.0
.write(data, Duration::from_secs(1))
.map_err(std::io::Error::other)
}
fn read_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> std::io::Result<usize> {
self.0.read(buf, timeout).map_err(std::io::Error::other)
}
}
struct OepHid {
dev: hidapi::HidDevice,
shape: ch32rv_oep::hid::ReportShape,
rx: std::collections::VecDeque<u8>,
}
impl OepHid {
fn open(dev: &ch32rv_usb::UsbDeviceInfo) -> Option<OepHid> {
let api = hidapi::HidApi::new().ok()?;
for info in api.device_list() {
if info.vendor_id() != dev.vid()
|| info.product_id() != dev.pid()
|| info.serial_number() != dev.serial()
|| info.usage_page() != ch32rv_oep::registry::usb::HID_USAGE_PAGE
|| info.usage() != u16::from(ch32rv_oep::registry::usb::HID_USAGE)
{
continue;
}
let Ok(h) = info.open_device(&api) else {
continue;
};
let mut desc = [0u8; 4096];
let Ok(n) = h.get_report_descriptor(&mut desc) else {
continue;
};
if let Some(shape) = ch32rv_oep::hid::vendor_report(&desc[..n]) {
return Some(OepHid {
dev: h,
shape,
rx: std::collections::VecDeque::new(),
});
}
}
None
}
}
impl ch32rv_oep::link::ByteStream for OepHid {
fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> {
for r in self.shape.pack(data) {
self.dev.write(&r).map_err(std::io::Error::other)?;
}
Ok(())
}
fn read_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> std::io::Result<usize> {
if self.rx.is_empty() {
let mut r = vec![0u8; 1 + self.shape.input];
let ms = i32::try_from(timeout.as_millis().max(1)).unwrap_or(i32::MAX);
let n = self
.dev
.read_timeout(&mut r, ms)
.map_err(std::io::Error::other)?;
if let Some(d) = self.shape.unpack(&r[..n]) {
self.rx.extend(d);
}
}
let n = buf.len().min(self.rx.len());
for (d, s) in buf.iter_mut().zip(self.rx.drain(..n)) {
*d = s;
}
Ok(n)
}
}
pub(crate) fn connect_upstream(
path: &str,
timeout: Option<Duration>,
) -> Result<(Probe, &'static str), String> {
connect_ordered(path, timeout, false)
}
pub(crate) fn connect_for_listing(
path: &str,
timeout: Duration,
) -> Result<(Probe, &'static str), String> {
connect_ordered(path, Some(timeout), true)
}
fn connect_ordered(
path: &str,
timeout: Option<Duration>,
hid_first: bool,
) -> Result<(Probe, &'static str), String> {
let start = std::env::var("CH32RV_OEP_TRANSPORT").unwrap_or_default();
let sel = Selector::Port(path.to_owned());
let dev = oep_devices()
.into_iter()
.enumerate()
.find(|(i, d)| sel.matches(d, *i))
.map(|(_, d)| d);
let try_link = |stream: Box<dyn ch32rv_oep::link::ByteStream>| {
let mut link = ch32rv_oep::link::Link::new(stream, ch32rv_oep::link::Framing::Length);
if let Some(t) = timeout {
link.set_timeout(t);
}
Probe::connect(link).ok()
};
if let Some(d) = &dev {
if hid_first
&& matches!(start.as_str(), "" | "hid")
&& let Some(p) = OepHid::open(d).and_then(|h| try_link(Box::new(h)))
{
return Ok((p, "hid"));
}
if !matches!(start.as_str(), "hid" | "serial")
&& let Some(p) = d
.open_vendor_bulk(
ch32rv_oep::registry::usb::VENDOR_BULK_SUBCLASS,
ch32rv_oep::registry::usb::VENDOR_BULK_PROTOCOL,
)
.ok()
.flatten()
.and_then(|pipe| try_link(Box::new(VendorBulk(pipe))))
{
return Ok((p, "vendor-bulk"));
}
if !hid_first
&& start != "serial"
&& let Some(p) = OepHid::open(d).and_then(|h| try_link(Box::new(h)))
{
return Ok((p, "hid"));
}
}
let mut link = ch32rv_oep::link::open_serial(path).map_err(|e| e.to_string())?;
if let Some(t) = timeout {
link.set_timeout(t);
}
Probe::connect(link)
.map(|p| (p, "serial"))
.map_err(|e| e.to_string())
}
pub(crate) fn resolve_oep_url(url: &str) -> Result<OepAddr, String> {
let rest = url.strip_prefix("oep://").unwrap_or(url);
let segs: Vec<&str> = rest.split('/').collect();
let ok = |s: &str| {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
};
if segs.len() > 3 || !segs.iter().all(|s| ok(s)) {
return Err(format!(
"`{url}` is not oep://<probe>[/<port>[/<target>]] (a-z 0-9 - . only)"
));
}
let id = segs[0];
let whole = segs.len() == 1;
let port = segs.get(1).map(|s| (*s).to_owned());
let target = segs
.get(2)
.filter(|t| **t != "any")
.map(|s| (*s).to_ascii_lowercase());
let devs = oep_devices();
let dev = if id == "any" {
if devs.len() > 1 {
return Err(format!(
"oep://any matches {} OEP probes: name one by its unit id",
devs.len()
));
}
devs.into_iter().next()
} else {
devs.into_iter()
.find(|d| probe_id(d).eq_ignore_ascii_case(id))
};
let Some(dev) = dev else {
let f = crate::mdns::find_unit(id, BROWSE_WAIT).ok_or_else(|| {
format!(
"no OEP probe {id} is connected over USB or announced on the local network \
(mDNS: name it tcp:<host>:<port> where mDNS does not reach)"
)
})?;
let addr = f
.endpoint()
.ok_or_else(|| format!("OEP probe {id} announces no reachable endpoint"))?;
return Ok(OepAddr::Net {
addr,
unit: id.to_owned(),
port,
target,
whole,
});
};
let path = oep_port(&dev).ok_or_else(|| format!("OEP probe {id} has no serial port"))?;
Ok(OepAddr::Probe {
path,
port,
target,
whole,
})
}
pub(crate) fn tcp_addr(name: &str) -> Result<OepAddr, (String, Option<&'static str>)> {
let name = name.strip_prefix("//").unwrap_or(name);
if name.contains(':') {
return Ok(OepAddr::Tcp(name.to_owned()));
}
let Some(f) = crate::mdns::find_named(name, BROWSE_WAIT) else {
return Err((
format!("no OEP probe {name} is announced on the local network (DNS-SD _oep._tcp)"),
Some("mDNS stays on the local link: name it tcp:<host>:<port> where it does not reach"),
));
};
match (f.endpoint(), f.unit_id) {
(Some(addr), Some(unit)) => Ok(OepAddr::Net {
addr,
unit,
port: None,
target: None,
whole: false,
}),
(Some(addr), None) => Ok(OepAddr::Tcp(addr)),
(None, _) => Err((
format!("OEP probe {name} announces no reachable endpoint"),
None,
)),
}
}
pub(crate) fn addr(cli: &Cli, cmd: &str) -> Result<Option<OepAddr>, ExitCode> {
let sel = match crate::cmd_probe::parse_selector(cli, cmd)? {
Some(Selector::Tcp(a)) => {
Some(Selector::Tcp(a.strip_prefix("//").unwrap_or(&a).to_owned()))
}
Some(Selector::Port(p)) if p.starts_with("tcp:") => {
let rest = &p["tcp:".len()..];
Some(Selector::Tcp(
rest.strip_prefix("//").unwrap_or(rest).to_owned(),
))
}
other => other,
};
match sel {
Some(Selector::Tcp(name)) => tcp_addr(&name)
.map(Some)
.map_err(|(m, hint)| fail(cli, cmd, ErrorKind::DeviceNotFound, m, hint)),
Some(Selector::Port(p)) if p.starts_with("oep://") => resolve_oep_url(&p)
.map(Some)
.map_err(|m| fail(cli, cmd, ErrorKind::DeviceNotFound, m, None)),
Some(Selector::Port(p)) if !p.starts_with("wchlink://") && !p.starts_with("hid://") => {
let sel = Selector::Port(p.clone());
let owned = crate::cmd_probe::wch_devices()
.unwrap_or_default()
.iter()
.enumerate()
.any(|(i, e)| sel.matches(&e.dev, i));
if owned {
return Ok(running_wch_broker(cli, cmd));
}
Ok(Some(OepAddr::Serial(p)))
}
_ => Ok(running_wch_broker(cli, cmd)),
}
}
pub(crate) fn running_wch_broker(cli: &Cli, cmd: &str) -> Option<OepAddr> {
let sel = crate::cmd_probe::parse_selector(cli, cmd).ok()?;
let entries = crate::cmd_probe::wch_devices().ok()?;
let i = ch32rv_usb::resolve(sel.as_ref(), entries.iter().map(|e| &e.dev)).ok()?;
let t = crate::broker::BrokerTarget::wch(&entries[i]);
crate::broker::existing_link_for(&t).map(|_| OepAddr::Wch(t))
}
fn oep_fail(cli: &Cli, cmd: &str, e: OepError) -> ExitCode {
let (kind, hint) = match &e {
OepError::Locked { .. } => (
ErrorKind::DeviceBusy,
Some("another host holds the probe; wait for it, or retry with --force-lock"),
),
OepError::Link(ch32rv_oep::link::LinkError::NotOep(_)) => (
ErrorKind::DeviceNotFound,
Some("this port does not answer OEP v1 (not an OEP probe, or its firmware is too old)"),
),
OepError::Link(_) => (ErrorKind::TransferFailed, None),
OepError::NoInterface(_) => (ErrorKind::CapabilityUnsupported, None),
_ => (ErrorKind::TransferFailed, None),
};
fail(cli, cmd, kind, e.to_string(), hint)
}
fn connect(cli: &Cli, cmd: &str, a: &OepAddr) -> Result<Probe, ExitCode> {
let link = match a {
OepAddr::Serial(p) | OepAddr::Probe { path: p, .. } => crate::broker::client_link(p)
.map_err(|m| {
fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
m,
Some("the probe's broker could not start or be reached"),
)
})?,
OepAddr::Tcp(_) | OepAddr::Net { .. } => match net_target(a) {
Some(t) => crate::broker::client_link_for(&t).map_err(|m| {
fail(
cli,
cmd,
ErrorKind::DeviceOpenFailed,
m,
Some("the probe's broker could not start or be reached"),
)
})?,
None => {
let OepAddr::Tcp(t) = a else {
return Err(fail(cli, cmd, ErrorKind::Usage, "no tcp address", None));
};
ch32rv_oep::link::open_tcp(t)
.map_err(|e| fail(cli, cmd, ErrorKind::DeviceOpenFailed, e.to_string(), None))?
}
},
OepAddr::Wch(t) => crate::broker::client_link_for(t)
.map_err(|m| fail(cli, cmd, ErrorKind::DeviceOpenFailed, m, None))?,
};
Probe::connect(link).map_err(|e| oep_fail(cli, cmd, e))
}
pub(crate) fn single_serial(p: &mut Probe) -> bool {
use ch32rv_oep::registry::core::enums::transport_kind as k;
let Ok(tlvs) = p.describe(oep_core::FN) else {
return false;
};
let kinds: Vec<u8> = tlvs
.iter()
.filter(|t| t.tag == oep_core::tlvs::describe::TRANSPORT)
.filter_map(|t| t.value.get(1).copied())
.collect();
matches!(kinds.as_slice(), [one] if [k::UART_BRIDGE, k::USB_CDC, k::USB_SERIAL_JTAG].contains(one))
}
pub(crate) fn open_with_lock_rule(
p: &mut Probe,
serial: bool,
owner: &str,
lease_ms: u32,
) -> Result<u32, OepError> {
let sid = random_session_id();
let deadline = Instant::now() + Duration::from_secs(5);
let mut force = false;
loop {
match p.open(sid, lease_ms, force, Some(owner)) {
Ok(_) => return Ok(sid),
Err(OepError::Locked { .. }) if serial && !force => force = true,
Err(OepError::Locked { remaining_ms, .. }) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(u64::from(
remaining_ms.clamp(50, 1000),
)));
}
Err(e) => return Err(e),
}
}
}
fn open_session(cli: &Cli, cmd: &str, p: &mut Probe, serial: bool) -> Result<(), ExitCode> {
let owner = format!("ch32rv {cmd} pid {}", std::process::id());
open_with_lock_rule(p, serial, &owner, 3000)
.map(|_| ())
.map_err(|e| oep_fail(cli, cmd, e))
}
fn pick_wire(p: &mut Probe, chip: Option<&str>) -> Result<WireKind, OepError> {
let has = |p: &mut Probe, k: WireKind| p.interface(k.interface()).is_ok();
let (rv, sw) = (has(p, WireKind::Rvswd), has(p, WireKind::Swio));
let one_wire = chip.is_some_and(|c| {
let u = c.to_ascii_uppercase();
u.starts_with("CH32V00") || u.starts_with("CH32M007") || u.starts_with("CH641")
});
match (rv, sw) {
(true, false) => Ok(WireKind::Rvswd),
(false, true) => Ok(WireKind::Swio),
(true, true) if one_wire => Ok(WireKind::Swio),
(true, true) => Ok(WireKind::Rvswd),
(false, false) => Err(OepError::NoInterface(
"oep.wire.rvswd / oep.wire.swio".into(),
)),
}
}
#[derive(Debug, Clone)]
struct Place {
wire: WireKind,
pins: Option<(u16, u16)>,
max_speed: Option<u32>,
idle_clock: Option<u8>,
mechanism: Option<ch32rv_oep::stream::Mechanism>,
}
impl Place {
fn options(&self, halt: bool, max_speed_hz: Option<u32>) -> AttachOptions {
let max_speed_hz = match (self.max_speed, max_speed_hz) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
};
AttachOptions {
halt,
max_speed_hz,
pins: self.pins,
idle_clock: self.idle_clock,
reset: None,
}
}
}
struct PlaceError {
kind: ErrorKind,
msg: String,
}
impl From<String> for PlaceError {
fn from(msg: String) -> Self {
PlaceError {
kind: ErrorKind::TargetAmbiguous,
msg,
}
}
}
impl From<PlaceError> for String {
fn from(e: PlaceError) -> String {
e.msg
}
}
fn choose_place(p: &mut Probe, a: &OepAddr, chip: Option<&str>) -> Result<Place, PlaceError> {
use ch32rv_oep::config::{find_line, firmware_labels, labels, ports_named};
let labels = labels(p).map_err(|e| e.to_string())?;
let firmware = firmware_labels(p).map_err(|e| e.to_string())?;
let ports = ports_named(&labels);
if a.whole_probe() {
return Err(PlaceError {
kind: ErrorKind::Usage,
msg: whole_probe_msg(&ports),
});
}
let port = match a.port() {
Some(name) => {
let name = name.to_ascii_lowercase();
if !ports.contains(&name) {
return Err(PlaceError {
kind: ErrorKind::DeviceNotFound,
msg: if ports.is_empty() {
format!(
"the probe names no ports (no `{name}.swdio`-style labels): use oep://<probe> alone"
)
} else {
format!(
"the probe has no port `{name}` (its ports: {})",
ports.join(", ")
)
},
});
}
Some(name)
}
None => match ports.as_slice() {
[] => None,
[one] => Some(one.clone()),
many => {
return Err(PlaceError::from(format!(
"the probe has {} ports ({}): name one, oep://<probe>/<port>",
many.len(),
many.join(", ")
)));
}
},
};
let line = |name: &str| find_line(&labels, &firmware, port.as_deref(), name);
let has = |p: &mut Probe, k: WireKind| p.interface(k.interface()).is_ok();
let wire = match (line("swio"), line("swclk")) {
(Some(_), None) if has(p, WireKind::Swio) => WireKind::Swio,
(None, Some(_)) if has(p, WireKind::Rvswd) => WireKind::Rvswd,
_ => pick_wire(p, chip).map_err(|e| e.to_string())?,
};
let pins = match wire {
WireKind::Rvswd => line("swdio").zip(line("swclk")),
WireKind::Swio => line("swio").or_else(|| line("swdio")).map(|d| (d, 0xFFFF)),
};
let named = a
.target()
.and_then(|t| t.split_once('-'))
.is_some_and(|(f, _)| idles_low(f));
let idle_clock = (wire == WireKind::Rvswd
&& (named
|| chip.is_some_and(|c| {
ch32rv_target::Db::builtin()
.families_for_chip_name(c)
.iter()
.any(|f| idles_low(f))
})))
.then_some(ch32rv_oep::registry::wire_rvswd::enums::idle_clock::LOW);
Ok(Place {
wire,
pins,
max_speed: None,
idle_clock,
mechanism: None,
})
}
fn idles_low(family: &str) -> bool {
family.eq_ignore_ascii_case("CH32L103")
}
fn attach_at(
p: &mut Probe,
wire: WireKind,
mut options: AttachOptions,
) -> Result<ch32rv_oep::target::Attached, ch32rv_oep::session::OepError> {
let at = attach(p, wire, options)?;
let family =
at.wch_chip_id.and_then(
|id| match ch32rv_target::Db::builtin().resolve_by_chip_id(id) {
ch32rv_target::Resolution::Sku(s) => Some(s.family.clone()),
ch32rv_target::Resolution::Family(f, _) => Some(f),
ch32rv_target::Resolution::Unknown => None,
},
);
let low = ch32rv_oep::registry::wire_rvswd::enums::idle_clock::LOW;
if wire != WireKind::Rvswd
|| options.idle_clock.is_some()
|| at.existing
|| at.idle_clock == Some(low)
|| !family.is_some_and(|f| idles_low(&f))
{
return Ok(at);
}
detach(p, wire, at.connection)?;
options.idle_clock = Some(low);
attach(p, wire, options)
}
fn whole_probe_msg(ports: &[String]) -> String {
if ports.is_empty() {
"oep://<probe> names the probe itself, not a port: its one unnamed port is \
oep://<probe>/any"
.to_owned()
} else {
format!(
"oep://<probe> names the probe itself, not a port: name one, oep://<probe>/<port> \
(its ports: {}), or write oep://<probe>/any",
ports.join(", ")
)
}
}
fn family(cli: &Cli, cmd: &str, chip_id: Option<u32>) -> Result<String, ExitCode> {
let db = ch32rv_target::Db::builtin();
let detected = chip_id.and_then(|id| match db.resolve_by_chip_id(id) {
ch32rv_target::Resolution::Sku(s) => Some(s.family.clone()),
ch32rv_target::Resolution::Family(f, _) => Some(f),
ch32rv_target::Resolution::Unknown => None,
});
let requested = cli
.chip
.as_deref()
.map(|c| (c, db.families_for_chip_name(c)));
match (detected, requested) {
(Some(d), Some((c, fams))) if !fams.iter().any(|f| f.eq_ignore_ascii_case(&d)) => {
Err(fail(
cli,
cmd,
ErrorKind::TargetAmbiguous,
format!(
"--chip {c} conflicts with the detected {d} (chip id 0x{:08x})",
chip_id.unwrap_or(0)
),
Some("pass the correct --chip, or omit it to use auto-detection"),
))
}
(Some(d), _) => Ok(d),
(None, Some((_, fams))) if fams.len() == 1 => Ok(fams[0].clone()),
(None, Some((c, _))) => Err(fail(
cli,
cmd,
ErrorKind::TargetNotInDb,
format!("the probe read no chip id and --chip {c} does not name one family"),
None,
)),
(None, None) => Err(match chip_id {
Some(id) => fail(
cli,
cmd,
ErrorKind::TargetNotInDb,
format!("chip id 0x{id:08x} is not in the target DB"),
Some("the part is not in this build's DB (`ch32rv db list`)"),
),
None => fail(
cli,
cmd,
ErrorKind::TargetNoResponse,
"the probe read no chip id at attach",
Some("check the wiring and power, or pass --chip to name the target"),
),
}),
}
}
pub(crate) fn flash(cli: &Cli, args: &FlashArgs, bytes: &[u8], a: &OepAddr) -> ExitCode {
const CMD: &str = "flash";
if let OepAddr::Serial(p) = a
&& port_of_oep_device(p)
{
return fail(
cli,
CMD,
ErrorKind::Usage,
format!("{p} is an OEP probe's serial port; flash it by its oep:// address instead"),
Some("use port:oep://<probe>[/<port>] (`ch32rv arduino discovery` lists them)"),
);
}
let mut p = match connect(cli, CMD, a) {
Ok(p) => p,
Err(c) => return c,
};
if matches!(a, OepAddr::Serial(_)) && listed_on_usb(&mut p) {
return fail(
cli,
CMD,
ErrorKind::Usage,
"this serial port belongs to an OEP probe that is also on USB as an OEP device; flash it by its oep:// address instead",
Some("use port:oep://<probe>[/<port>] (`ch32rv arduino discovery` lists them)"),
);
}
let serial = matches!(a, OepAddr::Serial(_)) && single_serial(&mut p);
if let Err(c) = open_session(cli, CMD, &mut p, serial) {
return c;
}
let r = flash_in_session(cli, args, bytes, &mut p, a);
let _ = p.end();
r
}
fn flash_in_session(
cli: &Cli,
args: &FlashArgs,
bytes: &[u8],
p: &mut Probe,
a: &OepAddr,
) -> ExitCode {
const CMD: &str = "flash";
let place = match choose_place(p, a, cli.chip.as_deref()) {
Ok(v) => v,
Err(e) => {
let hint = (e.kind == ErrorKind::TargetAmbiguous)
.then_some("name the port with oep://<probe>/<port>");
return fail(cli, CMD, e.kind, e.msg, hint);
}
};
let max_speed_hz = match parse::speed(&cli.speed) {
Ok((s, _)) => Some(match s {
ch32rv_wchlink::Speed::Low => 400_000,
ch32rv_wchlink::Speed::Medium => 4_000_000,
ch32rv_wchlink::Speed::High => 6_000_000,
}),
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
};
let wire = place.wire;
let at = match attach_at(p, wire, place.options(true, max_speed_hz)) {
Ok(a) => a,
Err(e) => return oep_fail(cli, CMD, e),
};
let log_hint = match a {
OepAddr::Serial(path) | OepAddr::Probe { path, .. } => Some(format!(
"the probe's broker keeps a log: {}",
crate::broker::log_path(&crate::broker::BrokerTarget::Serial(path.clone()).key())
.display()
)),
_ => None,
};
let r = flash_attached(cli, args, bytes, p, at.connection, at.wch_chip_id, log_hint);
let _ = detach(p, wire, at.connection);
r
}
fn flash_attached(
cli: &Cli,
args: &FlashArgs,
bytes: &[u8],
p: &mut Probe,
connection: u32,
chip_id: Option<u32>,
log_hint: Option<String>,
) -> ExitCode {
const CMD: &str = "flash";
let family = match family(cli, CMD, chip_id) {
Ok(f) => f,
Err(c) => return c,
};
let Some(plan) = ch32rv_flash::loader::plan_for_family(&family) else {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!("the device DB has no loader plan for {family}"),
None,
);
};
let bin_offset = match &args.at {
Some(s) => match parse::u32_addr(s) {
Ok(a) => Some(a),
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
},
None => None,
};
let image = match crate::cmd_flash::parse_image(
bytes,
args.format,
&args.file,
bin_offset,
ch32rv_flash::CODE_FLASH_START,
) {
Ok(i) => i,
Err(e) => return fail(cli, CMD, ErrorKind::Usage, e.to_string(), None),
};
let mut t = match OepDtm::new(p, connection) {
Ok(t) => t,
Err(e) => return oep_fail(cli, CMD, e),
};
if let Err(e) = t.reset(ResetMode::HaltAtReset) {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("reset-halt: {e}"),
None,
);
}
let started = Instant::now();
let report = match ch32rv_flash::loader::program(&mut t, plan, &image.segments, &mut |_, _| {})
{
Ok(r) => r,
Err(e) => {
let kind = crate::cmd_flash::loader_error_kind(&e);
let hint = log_hint.filter(|_| kind != ErrorKind::VerifyMismatch);
return fail(cli, CMD, kind, e.to_string(), hint.as_deref());
}
};
let secs = started.elapsed().as_secs_f64();
let running = (args.reset == ch32rv_contract::policy::ResetPolicy::Run
&& args.confirm_run.is_some())
.then_some(true);
if args.reset == ch32rv_contract::policy::ResetPolicy::Run {
let mode = if args.confirm_run.is_some() {
ResetMode::RunVerified
} else {
ResetMode::Run
};
match t.reset(mode) {
Ok(_) => {}
Err(e @ ch32rv_dmi::DmiError::NotReached(_)) => {
return fail(
cli,
CMD,
ErrorKind::NotRunningAfterWrite,
format!("target not running after reset: {e}"),
None,
);
}
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("reset: {e}"),
None,
);
}
}
}
let total = image.total_len();
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({
"flash": {
"bytes": total,
"programmer": "oep-loader",
"family": family,
"chip_id": chip_id.map(|c| format!("0x{c:08x}")),
"pages": report.pages,
"rewritten": report.rewritten,
"restarted_runs": report.restarted_runs,
"verified": true,
"skipped": false,
"scope": "pages",
"running": running,
"seconds": secs,
}
}));
crate::print_envelope(&env)
} else {
println!(
"flashed {total} bytes to {family} over OEP in {secs:.2} s: {} page(s), {} rewritten, {} run(s) re-issued, verified",
report.pages, report.rewritten, report.restarted_runs
);
ExitCode::SUCCESS
}
}
fn check_family_text(chip_id: Option<u32>, chip: Option<&str>) -> Result<Option<String>, String> {
let db = ch32rv_target::Db::builtin();
let detected = chip_id.and_then(|id| match db.resolve_by_chip_id(id) {
ch32rv_target::Resolution::Sku(s) => Some(s.family.clone()),
ch32rv_target::Resolution::Family(f, _) => Some(f),
ch32rv_target::Resolution::Unknown => None,
});
if let (Some(d), Some(c)) = (&detected, chip) {
let fams = db.families_for_chip_name(c);
if !fams.iter().any(|f| f.eq_ignore_ascii_case(d)) {
return Err(format!(
"--chip {c} conflicts with the detected {d} (chip id 0x{:08x})",
chip_id.unwrap_or(0)
));
}
}
if let (Some(id), Some(c)) = (chip_id, chip)
&& let Some(other) = db.sku_conflict(c, id)
{
return Err(format!(
"--chip {c} conflicts with the detected {other} (chip id 0x{id:08x})"
));
}
Ok(detected)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StreamWanted {
Console(ch32rv_oep::stream::Mechanism),
ConsoleDefault,
FixtureUart(u32),
Rtt,
}
impl crate::source::RttTarget for OepDtm<'_> {
fn is_halted(&mut self) -> Result<bool, ch32rv_dmi::DmiError> {
ch32rv_dmi::DebugModule::new(self).is_halted()
}
fn halt(&mut self) -> Result<(), ch32rv_dmi::DmiError> {
TargetAccess::halt(self)
}
fn resume(&mut self) -> Result<(), ch32rv_dmi::DmiError> {
for _ in 0..8 {
match TargetAccess::resume_once(self) {
Ok(()) => return Ok(()),
Err(ch32rv_dmi::DmiError::OperationFailed(_)) => {
if !ch32rv_dmi::DebugModule::new(self).is_halted()? {
return Ok(());
}
}
Err(e) => return Err(e),
}
}
Err(ch32rv_dmi::DmiError::OperationFailed(
"hart did not resume".to_owned(),
))
}
fn read(&mut self, addr: u32, len: u32) -> Result<Vec<u8>, ch32rv_dmi::DmiError> {
let start = addr & !3;
let end = addr.saturating_add(len).saturating_add(3) & !3;
let words = self.read_words(start, ((end - start) / 4) as usize)?;
let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
let from = (addr - start) as usize;
Ok(bytes
.get(from..from + len as usize)
.map(<[u8]>::to_vec)
.unwrap_or_default())
}
fn write(&mut self, addr: u32, data: &[u8]) -> Result<(), ch32rv_dmi::DmiError> {
if data.is_empty() {
return Ok(());
}
let start = addr & !3;
let end = addr.saturating_add(data.len() as u32).saturating_add(3) & !3;
let mut bytes = if start == addr && end - start == data.len() as u32 {
vec![0; data.len()]
} else {
self.read(start, end - start)?
};
let from = (addr - start) as usize;
if let Some(dst) = bytes.get_mut(from..from + data.len()) {
dst.copy_from_slice(data);
}
let words: Vec<u32> = bytes
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes(*c))
.collect();
self.write_words(start, &words)
}
}
pub(crate) struct ConsoleSession {
probe: Probe,
stream: Backing,
attached: Option<(WireKind, u32)>,
max_read: u16,
relink: Option<(Place, ch32rv_oep::stream::Mechanism)>,
quiet_since: Instant,
source_name: &'static str,
}
const RELINK_CHECK: Duration = Duration::from_secs(1);
enum Backing {
Stream(ch32rv_oep::stream::PosStream),
Rtt {
connection: u32,
parts: (u16, usize, u32, ch32rv_oep::target::Offered),
channels: crate::source::RttChannels,
input: Vec<u8>,
},
}
impl ConsoleSession {
pub(crate) fn open(
a: &OepAddr,
wanted: StreamWanted,
chip: Option<&str>,
) -> Result<Self, String> {
let link = match a {
OepAddr::Serial(p) | OepAddr::Probe { path: p, .. } => crate::broker::client_link(p)?,
OepAddr::Tcp(_) | OepAddr::Net { .. } => match net_target(a) {
Some(t) => crate::broker::client_link_for(&t)?,
None => match a {
OepAddr::Tcp(t) => ch32rv_oep::link::open_tcp(t).map_err(|e| e.to_string())?,
_ => return Err("no tcp address".into()),
},
},
OepAddr::Wch(t) => crate::broker::client_link_for(t)?,
};
let mut probe = Probe::connect(link).map_err(|e| e.to_string())?;
if a.whole_probe() {
let labels = ch32rv_oep::config::labels(&mut probe).unwrap_or_default();
return Err(whole_probe_msg(&ch32rv_oep::config::ports_named(&labels)));
}
let owner = format!("ch32rv monitor pid {}", std::process::id());
open_with_lock_rule(&mut probe, false, &owner, 3000).map_err(|e| e.to_string())?;
let mut relink = None;
let mut source_name = match wanted {
StreamWanted::FixtureUart(_) => "fixture-uart",
StreamWanted::Rtt => "rtt",
StreamWanted::Console(m) => m.name(),
StreamWanted::ConsoleDefault => "dmseq",
};
let (stream, attached) = match wanted {
StreamWanted::FixtureUart(baud) => {
let pins = uart_pins(&mut probe, a);
let (s, _) = ch32rv_oep::stream::PosStream::open_uart(&mut probe, baud, pins).map_err(
|e| match e {
OepError::Rejected { reason, .. }
if reason == ch32rv_oep::registry::reject_reasons::UNAVAILABLE =>
{
"the probe's fixture UART has no pins: label its channels `<port>.rx` and `<port>.tx`".to_owned()
}
OepError::NoInterface(_) => "this OEP probe has no fixture UART".to_owned(),
e => format!("fixture UART: {e}"),
},
)?;
let mut s = s;
s.start_at_last_reset(&mut probe)
.map_err(|e| e.to_string())?;
(Backing::Stream(s), None)
}
StreamWanted::Rtt => {
let place = choose_place(&mut probe, a, chip)?;
let wire = place.wire;
let at = attach_at(&mut probe, wire, place.options(false, None))
.map_err(|e| e.to_string())?;
check_family_text(at.wch_chip_id, chip)?;
let mut t = OepDtm::new(&mut probe, at.connection).map_err(|e| e.to_string())?;
let mut warnings = Vec::new();
let found = crate::source::rtt_find(
&mut t,
crate::source::rtt_scan_len(at.wch_chip_id),
&mut warnings,
);
let resumed = crate::source::RttTarget::resume(&mut t);
let channels = found.map_err(|e| e.to_string())?;
resumed.map_err(|e| format!("resume after the RTT scan: {e}"))?;
for w in warnings {
eprintln!("warning[{}]: {}", w.code, w.msg);
}
let parts = t.parts();
(
Backing::Rtt {
connection: at.connection,
parts,
channels,
input: Vec::new(),
},
Some((wire, at.connection)),
)
}
StreamWanted::Console(_) | StreamWanted::ConsoleDefault => {
let place = choose_place(&mut probe, a, chip)?;
let mech = match wanted {
StreamWanted::Console(m) => m,
_ => place
.mechanism
.unwrap_or(ch32rv_oep::stream::Mechanism::Dmseq),
};
source_name = mech.name();
let wire = place.wire;
let at = attach_at(&mut probe, wire, place.options(false, None))
.map_err(|e| e.to_string())?;
check_family_text(at.wch_chip_id, chip)?;
let mut s =
ch32rv_oep::stream::PosStream::open_console(&mut probe, at.connection, mech)
.map_err(|e| e.to_string())?;
s.start_at_last_reset(&mut probe)
.map_err(|e| e.to_string())?;
relink = Some((place, mech));
(Backing::Stream(s), Some((wire, at.connection)))
}
};
let max_read = probe.limits().max_frame.saturating_sub(14).clamp(16, 1000);
Ok(ConsoleSession {
probe,
stream,
attached,
max_read,
relink,
quiet_since: Instant::now(),
source_name,
})
}
pub(crate) fn poll(&mut self) -> Result<Vec<u8>, String> {
match &mut self.stream {
Backing::Stream(s) => {
let data = match s.poll(&mut self.probe, self.max_read) {
Err(OepError::Rejected { reason, .. })
if reason == ch32rv_oep::registry::reject_reasons::NO_RESOURCE =>
{
self.quiet_since = Instant::now();
self.relink()?;
return Ok(Vec::new());
}
r => r.map(|c| c.data).map_err(|e| e.to_string())?,
};
if !data.is_empty() {
self.quiet_since = Instant::now();
} else if self.quiet_since.elapsed() >= RELINK_CHECK {
self.quiet_since = Instant::now();
self.relink_if_closed()?;
}
Ok(data)
}
Backing::Rtt {
connection,
parts,
channels,
input,
} => {
let mut t = OepDtm::from_parts(&mut self.probe, *connection, *parts);
crate::source::rtt_poll(&mut t, *channels, input).map_err(|e| e.to_string())
}
}
}
fn relink_if_closed(&mut self) -> Result<(), String> {
let (Backing::Stream(s), Some((place, mech))) = (&mut self.stream, &self.relink) else {
return Ok(());
};
let open = match s.is_open(&mut self.probe) {
Err(OepError::Rejected { reason, .. })
if reason == ch32rv_oep::registry::reject_reasons::UNKNOWN_OPERATION
|| reason == ch32rv_oep::registry::reject_reasons::UNSUPPORTED =>
{
None
}
r => r.map_err(|e| e.to_string())?,
};
let _ = (place, mech);
if open != Some(false) {
return Ok(());
}
self.relink()
}
fn relink(&mut self) -> Result<(), String> {
let (Backing::Stream(s), Some((place, mech))) = (&mut self.stream, &self.relink) else {
return Ok(());
};
let Ok(at) = attach_at(&mut self.probe, place.wire, place.options(false, None)) else {
return Ok(());
};
s.reopen_console(&mut self.probe, at.connection, *mech)
.map_err(|e| e.to_string())?;
self.attached = Some((place.wire, at.connection));
eprintln!("monitor: the target's debug line was lost; attached again");
Ok(())
}
pub(crate) fn write(&mut self, data: &[u8]) -> Result<usize, String> {
match &mut self.stream {
Backing::Stream(s) => s.write(&mut self.probe, data).map_err(|e| e.to_string()),
Backing::Rtt { input, .. } => {
input.extend_from_slice(data);
Ok(data.len())
}
}
}
pub(crate) fn set_baud(&mut self, baud: u32) -> Result<(), String> {
match &self.stream {
Backing::Stream(s) => s
.configure_baud(&mut self.probe, baud)
.map(|_| ())
.map_err(|e| e.to_string()),
Backing::Rtt { .. } => Ok(()),
}
}
pub(crate) fn source_name(&self) -> &'static str {
self.source_name
}
pub(crate) fn is_rtt(&self) -> bool {
matches!(self.stream, Backing::Rtt { .. })
}
}
impl Drop for ConsoleSession {
fn drop(&mut self) {
if let Some((wire, conn)) = self.attached {
let _ = detach(&mut self.probe, wire, conn);
}
let _ = self.probe.end();
}
}
pub(crate) struct Attached<'a> {
pub(crate) t: OepDtm<'a>,
pub(crate) chip_id: Option<u32>,
pub(crate) family: Option<String>,
pub(crate) existing: bool,
pub(crate) halted_at: Option<u32>,
}
pub(crate) fn with_attached(
cli: &Cli,
cmd: &str,
a: &OepAddr,
halt: bool,
f: impl FnOnce(&mut Attached<'_>) -> ExitCode,
) -> ExitCode {
with_attached_reset(cli, cmd, a, halt, false, f)
}
fn uart_pins(p: &mut Probe, a: &OepAddr) -> Option<(u16, u16)> {
use ch32rv_oep::config::{find_line, firmware_labels, labels, ports_named};
let labels = labels(p).ok()?;
let firmware = firmware_labels(p).unwrap_or_default();
let ports = ports_named(&labels);
let port = match a.port() {
Some(port) => Some(port.to_ascii_lowercase()),
None if ports.len() <= 1 => ports.first().cloned(),
None => return None,
};
let line = |name| find_line(&labels, &firmware, port.as_deref(), name);
line("rx").zip(line("tx"))
}
const NRST_HOLD_MS: u16 = 20;
fn nrst_line(p: &mut Probe, a: &OepAddr) -> Result<u16, String> {
use ch32rv_oep::config::{find_line, firmware_labels, labels, ports_named};
let labels = labels(p).map_err(|e| e.to_string())?;
let firmware = firmware_labels(p).map_err(|e| e.to_string())?;
let ports = ports_named(&labels);
let port = match a.port() {
Some(port) => Some(port.to_ascii_lowercase()),
None if ports.len() <= 1 => ports.first().cloned(),
None => {
return Err(format!(
"name the port (`oep://<probe>/<port>`) to find its NRST line (ports: {})",
ports.join(", ")
));
}
};
find_line(&labels, &firmware, port.as_deref(), "nrst").ok_or_else(|| match &port {
Some(s) => format!("no NRST line for port `{s}`: set a label `{s}.nrst` on its channel"),
None => "no NRST line: set a label `nrst` on its channel".to_owned(),
})
}
pub(crate) fn with_attached_reset(
cli: &Cli,
cmd: &str,
a: &OepAddr,
halt: bool,
under_reset: bool,
f: impl FnOnce(&mut Attached<'_>) -> ExitCode,
) -> ExitCode {
let mut p = match connect(cli, cmd, a) {
Ok(p) => p,
Err(c) => return c,
};
let serial = matches!(a, OepAddr::Serial(_)) && single_serial(&mut p);
if let Err(c) = open_session(cli, cmd, &mut p, serial) {
return c;
}
let r = (|| {
let place = match choose_place(&mut p, a, cli.chip.as_deref()) {
Ok(v) => v,
Err(e) => return fail(cli, cmd, e.kind, e.msg, None),
};
let wire = place.wire;
let mut options = place.options(halt, None);
if under_reset {
match nrst_line(&mut p, a) {
Ok(ch) => options.reset = Some((ch, NRST_HOLD_MS)),
Err(m) => {
return fail(
cli,
cmd,
ErrorKind::CapabilityUnsupported,
m,
Some("oep-spec host-development-guide §8.1 names the lines"),
);
}
}
}
let at = match attach_at(&mut p, wire, options) {
Ok(a) => a,
Err(e) => return oep_fail(cli, cmd, e),
};
let family = match check_family_text(at.wch_chip_id, cli.chip.as_deref()) {
Ok(f) => f,
Err(m) => {
let _ = detach(&mut p, wire, at.connection);
return fail(cli, cmd, ErrorKind::TargetAmbiguous, m, None);
}
};
let r = match OepDtm::new(&mut p, at.connection) {
Ok(t) => {
let mut x = Attached {
t,
chip_id: at.wch_chip_id,
family,
existing: at.existing,
halted_at: at.halted,
};
match a.target().map(|want| (want, target_name(&mut x))) {
Some((want, Some(name))) if !name.eq_ignore_ascii_case(want) => fail(
cli,
cmd,
ErrorKind::TargetAmbiguous,
format!("the target here is {name}, not {want}"),
None,
),
Some((want, None)) => fail(
cli,
cmd,
ErrorKind::TargetNoResponse,
format!("cannot read the target's unique id to check that it is {want}"),
None,
),
_ => f(&mut x),
}
}
Err(e) => oep_fail(cli, cmd, e),
};
let _ = detach(&mut p, wire, at.connection);
r
})();
let _ = p.end();
r
}
pub(crate) fn gdb(cli: &Cli, args: &crate::args::GdbArgs, a: &OepAddr) -> ExitCode {
const CMD: &str = "gdb";
with_attached(cli, CMD, a, false, |x| {
let profile = x
.family
.as_deref()
.and_then(ch32rv_flash::flash_controller_profile_for);
if matches!(a, OepAddr::Wch(_))
&& !x.existing
&& profile.is_some_and(|p| p.attach_corrupts_regs)
{
let _ = x.t.reset(ResetMode::Run);
std::thread::sleep(Duration::from_millis(50));
eprintln!(
"gdb: reset after attach (this core's attach corrupts a register; the target restarted)"
);
}
let stream = match crate::cmd_gdb::listen(cli, args) {
Ok(s) => s,
Err(c) => return c,
};
let flash = profile
.filter(|p| p.gdb_breakpoints)
.map(|p| (p.page_size, p.mode));
let mut target = match ch32rv_debug::Ch32Target::new(&mut x.t, flash) {
Ok(t) => t,
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::AttachFailed,
format!("halt for gdb failed: {e}"),
None,
);
}
};
let code = crate::cmd_gdb::run_session(cli, &mut target, stream);
let _ = target.resume_if_halted();
code
})
}
fn sku_of(chip_id: Option<u32>) -> Option<ch32rv_target::SkuRecord> {
match ch32rv_target::Db::builtin().resolve_by_chip_id(chip_id?) {
ch32rv_target::Resolution::Sku(s) => Some(s.clone()),
_ => None,
}
}
pub(crate) fn reset(cli: &Cli, args: &crate::args::ResetArgs, a: &OepAddr) -> ExitCode {
const CMD: &str = "reset";
if args.dm {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
"--dm is a WCH-Link operation; an OEP probe resets the target",
None,
);
}
with_attached(cli, CMD, a, false, |x| {
let mode = if args.halt {
ResetMode::HaltAtReset
} else if args.confirm_run.is_some() {
ResetMode::RunVerified
} else {
ResetMode::Run
};
let (ok, pc) = match x.t.reset(mode) {
Ok(r) => (true, r.pc),
Err(ch32rv_dmi::DmiError::NotReached(_)) if mode == ResetMode::RunVerified => {
(false, 0)
}
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("reset: {e}"),
None,
);
}
};
let running = (mode == ResetMode::RunVerified).then_some(ok);
if cli.json {
let mut env = if running == Some(false) {
ResultEnvelope::failure(
CMD,
ErrorKind::NotRunningAfterWrite,
"target not running after reset",
)
} else {
ResultEnvelope::success(CMD)
};
env.result = Some(serde_json::json!({
"mode": if args.halt { "halt" } else { "run" },
"running": running,
"pc": format!("0x{pc:08x}"),
}));
crate::print_envelope(&env)
} else if running == Some(false) {
eprintln!("ch32rv: error[not-running-after-write]: target not running after reset");
ErrorKind::NotRunningAfterWrite.exit_code().into()
} else {
println!(
"{}",
if args.halt {
"reset and halted"
} else {
"reset, running"
}
);
ExitCode::SUCCESS
}
})
}
fn resume_after(t: &mut OepDtm<'_>) {
let _ = ch32rv_dmi::resume_ch32(t, |t| {
ch32rv_dmi::DebugModule::new(t).read_reg(ch32rv_dmi::RegName::Pc)
});
}
pub(crate) fn verify(
cli: &Cli,
args: &crate::args::VerifyArgs,
bytes: &[u8],
a: &OepAddr,
) -> ExitCode {
const CMD: &str = "verify";
let bin_offset = match &args.at {
Some(s) => match parse::u32_addr(s) {
Ok(a) => Some(a),
Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
},
None => None,
};
let image = match crate::cmd_flash::parse_image(
bytes,
args.format,
&args.file,
bin_offset,
ch32rv_flash::CODE_FLASH_START,
) {
Ok(i) => i,
Err(e) => return fail(cli, CMD, ErrorKind::Usage, e.to_string(), None),
};
with_attached(cli, CMD, a, true, |x| {
let mut first_bad = None;
for seg in &image.segments {
let (lo, hi) = (
seg.addr & !3,
(seg.addr + seg.data.len() as u32).div_ceil(4) * 4,
);
let got = match x.t.read_words(lo, ((hi - lo) / 4) as usize) {
Ok(w) => w.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>(),
Err(e) => {
resume_after(&mut x.t);
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("read: {e}"),
None,
);
}
};
let off = (seg.addr - lo) as usize;
if let Some(i) = (0..seg.data.len()).find(|&i| got[off + i] != seg.data[i]) {
first_bad = Some(seg.addr + i as u32);
break;
}
}
resume_after(&mut x.t);
match first_bad {
Some(at) => fail(
cli,
CMD,
ErrorKind::VerifyMismatch,
format!("mismatch at {at:#010x}"),
None,
),
None if cli.json => {
let mut env = ResultEnvelope::success(CMD);
env.result =
Some(serde_json::json!({ "bytes": image.total_len(), "verified": true }));
crate::print_envelope(&env)
}
None => {
println!("verify: OK ({} bytes match)", image.total_len());
ExitCode::SUCCESS
}
}
})
}
pub(crate) fn read(cli: &Cli, args: &crate::args::ReadArgs, a: &OepAddr) -> ExitCode {
const CMD: &str = "read";
with_attached(cli, CMD, a, true, |x| {
let sku = sku_of(x.chip_id);
let (flash, sram) = sku
.as_ref()
.map_or((0, 0), |s| (s.flash_bytes, s.sram_bytes));
let option_base = x
.family
.as_deref()
.and_then(ch32rv_target::option_bytes_layout)
.map(|l| l.base);
let (start, len) = match crate::cmd_dbg::resolve_range(args, flash, sram, option_base) {
Ok(v) => v,
Err(m) => {
resume_after(&mut x.t);
return fail(cli, CMD, ErrorKind::Usage, m, None);
}
};
let lo = start & !3;
let words = (start + len - lo).div_ceil(4) as usize;
let data = match x.t.read_words(lo, words) {
Ok(w) => {
let b: Vec<u8> = w.iter().flat_map(|w| w.to_le_bytes()).collect();
b[(start - lo) as usize..(start - lo + len) as usize].to_vec()
}
Err(e) => {
resume_after(&mut x.t);
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("read: {e}"),
None,
);
}
};
resume_after(&mut x.t);
if args.blank_check {
let blank = data.iter().all(|&b| b == 0xff);
if cli.json {
let mut env = if blank {
ResultEnvelope::success(CMD)
} else {
ResultEnvelope::failure(CMD, ErrorKind::BlankCheckFailed, "region is not blank")
};
env.result = Some(serde_json::json!({
"addr": format!("0x{start:08x}"), "len": len, "blank": blank,
}));
return crate::print_envelope(&env);
}
println!(
"blank check 0x{start:08x}+{len}: {}",
if blank { "BLANK" } else { "NOT BLANK" }
);
return if blank {
ExitCode::SUCCESS
} else {
ErrorKind::BlankCheckFailed.exit_code().into()
};
}
crate::cmd_dbg::output_data(cli, CMD, args, start, &data, Vec::new())
})
}
pub(crate) fn write(cli: &Cli, addr: u32, bytes: &[u8], a: &OepAddr) -> ExitCode {
const CMD: &str = "write";
if addr < 0x2000_0000 {
return fail(
cli,
CMD,
ErrorKind::CapabilityUnsupported,
format!(
"0x{addr:08x} is flash / system / option space; an OEP probe writes it only through `flash` or `erase`"
),
None,
);
}
if !addr.is_multiple_of(4) || !bytes.len().is_multiple_of(4) {
return fail(
cli,
CMD,
ErrorKind::Usage,
"an OEP probe writes whole words: the address and length must be multiples of 4",
None,
);
}
let words: Vec<u32> = bytes
.as_chunks::<4>()
.0
.iter()
.map(|c| u32::from_le_bytes(*c))
.collect();
let len = bytes.len();
let check = addr < 0x4000_0000;
with_attached(cli, CMD, a, true, |x| {
let result = x.t.write_words(addr, &words).and_then(|()| {
if check {
x.t.read_words(addr, words.len())
} else {
Ok(words.clone())
}
});
resume_after(&mut x.t);
match result {
Ok(back) if back == words => {}
Ok(_) => {
return fail(
cli,
CMD,
ErrorKind::VerifyMismatch,
format!("write to 0x{addr:08x} did not verify (readback differs)"),
None,
);
}
Err(e) => {
return fail(
cli,
CMD,
ErrorKind::TransferFailed,
format!("write failed: {e}"),
None,
);
}
}
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.result = Some(serde_json::json!({
"addr": format!("0x{addr:08x}"),
"len": len,
"region": "memory",
"verified": check.then_some(true),
}));
crate::print_envelope(&env)
} else {
println!(
"wrote {len} byte(s) to 0x{addr:08x} (memory) - {}",
if check {
"verified"
} else {
"not read back (register)"
}
);
ExitCode::SUCCESS
}
})
}
fn read_uid(x: &mut Attached<'_>) -> Option<String> {
const ESIG_UID: u32 = 0x1FFF_F7E8;
let words = match x.t.read_words(ESIG_UID, 2) {
Ok(w) => w,
Err(_) => {
let was_running = x.halted_at.is_none();
x.t.halt().ok()?;
let w = x.t.read_words(ESIG_UID, 2);
if was_running {
resume_after(&mut x.t);
}
w.ok()?
}
};
let [w0, w1] = words[..] else {
return None;
};
if (w0 == 0 && w1 == 0) || (w0 == u32::MAX && w1 == u32::MAX) {
return None;
}
Some(format!("{w0:08x}{w1:08x}"))
}
fn target_name(x: &mut Attached<'_>) -> Option<String> {
let family = x.family.clone()?.to_ascii_lowercase();
Some(format!("{family}-{}", read_uid(x)?))
}
pub(crate) fn target_info(cli: &Cli, a: &OepAddr) -> ExitCode {
const CMD: &str = "target.info";
with_attached(cli, CMD, a, false, |x| {
let sku = sku_of(x.chip_id);
let uid = read_uid(x);
if cli.json {
let mut env = ResultEnvelope::success(CMD);
env.target = Some(ch32rv_contract::envelope::TargetReport {
sku: sku.as_ref().map(|s| s.sku.clone()),
family: x.family.clone(),
chip_id: x.chip_id.map(|c| format!("0x{c:08x}")),
uid: uid.clone(),
verified: sku.as_ref().map(|s| s.verified),
provisional: sku.as_ref().map(|s| s.provisional),
protected: None,
flash_bytes: sku.as_ref().map(|s| s.flash_bytes),
});
env.result = Some(serde_json::json!({
"probe": { "kind": "oep" },
"target": {
"chip_id": x.chip_id.map(|c| format!("0x{c:08x}")),
"family": x.family,
"sku": sku.as_ref().map(|s| s.sku.clone()),
"uid": uid,
"flash_bytes": sku.as_ref().map(|s| s.flash_bytes),
"sram_bytes": sku.as_ref().map(|s| s.sram_bytes),
},
}));
crate::print_envelope(&env)
} else {
println!("probe: OEP");
match x.chip_id {
Some(c) => println!("chip_id: 0x{c:08x}"),
None => println!("chip_id: (the probe read none)"),
}
println!("family: {}", x.family.as_deref().unwrap_or("unknown"));
println!("uid: {}", uid.as_deref().unwrap_or("-"));
if let Some(s) = &sku {
println!("sku: {}", s.sku);
println!("flash: {} KiB", s.flash_bytes / 1024);
println!("sram: {} KiB", s.sram_bytes / 1024);
}
ExitCode::SUCCESS
}
})
}
#[cfg(test)]
mod tests {
#[test]
fn a_unit_id_matches_a_serial_case_aside() {
let serials = vec!["30EDA0E31108".to_owned(), "9489dd2ae0953650".to_owned()];
assert!(super::unit_among("30eda0e31108", &serials));
assert!(super::unit_among("9489DD2AE0953650", &serials));
assert!(!super::unit_among("0070070d9394", &serials));
assert!(!super::unit_among("", &serials));
}
}