ch32rv 0.15.1

Flashing and debugging tool for WCH CH32 RISC-V microcontrollers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! en: ch32rv CLI. A thin layer that only composes the library crates (docs/architecture.ja.md §2).
//! The WCH-LinkE-route surface is implemented (flash/verify/read/write/erase/reset/recover, run,
//! dbg + gdb, monitor, target/probe/db/capabilities, arduino); the isp/boot/dap routes still
//! return exit 70 (unimplemented).
//!
//! ja: ch32rv CLI。library crate 群を組み合わせるだけの薄い層(docs/architecture.ja.md §2)。
//! WCH-LinkE 経路の機能は実装済み(flash/verify/read/write/erase/reset/recover、run、dbg+gdb、
//! monitor、target/probe/db/capabilities、arduino)。isp/boot/dap は exit 70(unimplemented)。

mod args;
mod broker;
mod broker_wch;
mod cmd_arduino;
mod cmd_boot;
mod cmd_capabilities;
mod cmd_db;
mod cmd_dbg;
mod cmd_doctor;
mod cmd_flash;
mod cmd_gdb;
mod cmd_monitor;
mod cmd_probe;
mod cmd_recover;
mod cmd_run;
mod cmd_target;
mod cmd_write;
mod config;
mod oep;
mod parse;
mod progress;
mod session;
mod source;

use clap::Parser;

use args::*;
use ch32rv_contract::{self as contract, ErrorKind, ResultEnvelope};

fn main() -> std::process::ExitCode {
    let mut cli = Cli::parse();
    // `--chip` > CH32RV_CHIP (clap reads both) > the config's `[defaults] chip`.
    if cli.chip.is_none() {
        cli.chip = config::default_chip();
    }
    // en: `auto` is "no --chip": detect the target and check it against nothing, spelled as a value
    // so an IDE recipe that always passes `--chip {build.ch32rv_chip}` can ask for it
    // (docs/freeze-decisions.ja.md §1). An empty value stays a usage error.
    // ja: `auto` は「--chip 無し」(検出して何とも照合しない)。常に `--chip {…}` を渡す recipe が値で
    // 頼めるように。空の値は usage の誤りのまま。
    if cli
        .chip
        .as_deref()
        .is_some_and(|c| c.trim().eq_ignore_ascii_case("auto"))
    {
        cli.chip = None;
    }
    // --replay: run against a recorded capture instead of hardware (mutually exclusive with --capture).
    if let Some(path) = cli.replay.as_deref() {
        if cli.capture.is_some() {
            eprintln!("error: --replay and --capture cannot be used together");
            return ErrorKind::Usage.exit_code().into();
        }
        if let Err(e) = ch32rv_usb::replay::start(path) {
            eprintln!("error: --replay: cannot load {}: {e}", path.display());
            return ErrorKind::Usage.exit_code().into();
        }
    }
    // Start USB transaction capture if requested (diagnostic; a failure to open the file only warns).
    if let Some(path) = cli.capture.as_deref()
        && let Err(e) = ch32rv_usb::capture::start(path)
    {
        eprintln!(
            "warning: --capture disabled: cannot write {}: {e}",
            path.display()
        );
    }
    let code = run_command(&cli);
    // After a replay, note if the run diverged from the recording (a protocol change) or ran short.
    if let Some((divergences, underruns)) = ch32rv_usb::replay::summary()
        && (divergences > 0 || underruns > 0)
    {
        eprintln!(
            "warning: replay diverged from the recording ({divergences} write mismatch(es), {underruns} short read(s)) - the code may produce a different protocol than the capture"
        );
    }
    code
}

/// en: A command that opens a WCH-Link itself borrows it from the Link's broker when one runs
/// (a monitor holds the Link through it), and hands it back when it returns. The commands that go
/// through the broker on their own (flash, verify, read, reset, target info, the dmdata / dmseq /
/// fixture-uart monitors) are not among them.
/// ja: WCH-Link を自分で開くコマンドは、その Link のブローカーが動いていれば Link を借り、終わったら
/// 返す。自分でブローカーを通るコマンドは対象外。
fn borrow_link(cli: &Cli) -> Option<broker::Lend> {
    use ch32rv_contract::policy::MonitorSource;
    let direct = match &cli.command {
        Command::Probe(p) => !matches!(p, ProbeCmd::List { .. }),
        Command::Target(t) => !matches!(t, TargetCmd::Info),
        Command::Dbg(_)
        | Command::Erase(_)
        | Command::Recover(_)
        | Command::Capabilities
        | Command::Write(_)
        | Command::Run(_) => true,
        Command::Monitor(m) => {
            m.cmd.is_some() || matches!(m.source, MonitorSource::Sdi | MonitorSource::Rtt)
        }
        _ => false,
    };
    if !direct {
        return None;
    }
    match oep::running_wch_broker(cli, "broker") {
        Some(oep::OepAddr::Wch(t)) => broker::lend(&t).ok(),
        _ => None,
    }
}

fn run_command(cli: &Cli) -> std::process::ExitCode {
    // en: `--dry-run` is global, but only these commands honour it. Any other one refuses it before
    // touching a device, rather than doing the real thing (`erase --all --dry-run --yes` erased).
    // ja: `--dry-run` は global だが、効くのは下の 2 つだけ。ほかは device に触れる前に断る(以前は
    // 無視して実行し、`erase --all --dry-run --yes` が本当に消した)。
    if cli.dry_run
        && !matches!(
            cli.command,
            Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Update { .. }))
                | Command::Boot(BootCmd::Hid(HidBootCmd::Flash { .. }))
        )
    {
        let name = canonical_name(&cli.command);
        return cmd_probe::fail(
            cli,
            name,
            ch32rv_contract::ErrorKind::Usage,
            format!("`{name}` does not support --dry-run yet; nothing was done"),
            Some("--dry-run works with `probe firmware update` and `boot hid flash`"),
        );
    }
    // en: Global flags that are accepted but not implemented are refused, not ignored
    // (docs/freeze-decisions.ja.md §9): a run that ignored them would do something else.
    // ja: 受け付けるが未実装の global は、無視せずに断る。
    if cli.core != 0 || cli.connect_under_reset {
        let what = if cli.core != 0 {
            format!(
                "--core {}: only core 0 is supported (a second core, H41x, is not implemented)",
                cli.core
            )
        } else {
            "--connect-under-reset is not implemented yet".to_owned()
        };
        return cmd_probe::fail(
            cli,
            canonical_name(&cli.command),
            ch32rv_contract::ErrorKind::CapabilityUnsupported,
            what,
            None,
        );
    }
    // An empty `--chip` (or CH32RV_CHIP) names nothing: a usage error, not "every family".
    if cli.chip.as_deref().is_some_and(|c| c.trim().is_empty()) {
        return cmd_probe::fail(
            cli,
            canonical_name(&cli.command),
            ch32rv_contract::ErrorKind::Usage,
            "--chip is empty",
            Some(
                "leave --chip out to detect the target, or name a family / SKU (`ch32rv db list`)",
            ),
        );
    }
    // A name the DB does not know stops here, before any probe is opened (it would stop after the
    // attach anyway, docs/freeze-decisions.ja.md §1): an IDE's board for a series ch32rv has no
    // name for fails without touching the bench. A broken `--db` overlay is left to the command.
    if let (Some(c), Ok(db)) = (cli.chip.as_deref(), cmd_db::db_for(cli))
        && db.families_for_chip_name(c).is_empty()
    {
        return cmd_probe::fail(
            cli,
            canonical_name(&cli.command),
            ch32rv_contract::ErrorKind::TargetNotInDb,
            format!("--chip {c} is not in the target DB"),
            Some(
                "name a family, series or SKU from `ch32rv db list`, or leave --chip out to detect the target",
            ),
        );
    }
    let _lend = borrow_link(cli);
    match &cli.command {
        Command::Version => cmd_version(cli),
        Command::Probe(ProbeCmd::List { watch }) => cmd_probe::list(cli, *watch),
        Command::Probe(ProbeCmd::Info) => cmd_probe::info(cli),
        Command::Probe(ProbeCmd::Mode(ModeCmd::Get)) => cmd_probe::mode_get(cli),
        Command::Probe(ProbeCmd::Mode(ModeCmd::Set { mode })) => cmd_probe::mode_set(cli, *mode),
        Command::Probe(ProbeCmd::Power(p)) => cmd_probe::power(cli, p),
        Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Info)) => cmd_probe::firmware_info(cli),
        Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Check { min })) => {
            cmd_probe::firmware_check(cli, min.as_deref())
        }
        Command::Probe(ProbeCmd::Firmware(FirmwareCmd::ExitIap)) => {
            cmd_probe::firmware_exit_iap(cli)
        }
        Command::Probe(ProbeCmd::Firmware(FirmwareCmd::Update { image })) => {
            cmd_probe::firmware_update(cli, image)
        }
        Command::Target(TargetCmd::Info) => cmd_target::info(cli),
        Command::Target(TargetCmd::Opt(OptionCmd::Get)) => cmd_target::option_get(cli),
        Command::Target(TargetCmd::Opt(OptionCmd::WriteRaw { hex })) => {
            cmd_target::option_write_raw(cli, hex)
        }
        Command::Target(TargetCmd::Opt(OptionCmd::Set { kv })) => cmd_target::option_set(cli, kv),
        Command::Target(TargetCmd::Opt(OptionCmd::Reset)) => cmd_target::option_reset(cli),
        Command::Target(TargetCmd::Protect { state }) => cmd_target::protect(cli, *state),
        Command::Dbg(DbgCmd::Regs) => cmd_dbg::regs(cli),
        Command::Dbg(DbgCmd::Halt { reset }) => cmd_dbg::halt(cli, *reset),
        Command::Dbg(DbgCmd::Resume) => cmd_dbg::resume(cli),
        Command::Dbg(DbgCmd::Step { n }) => cmd_dbg::step(cli, *n),
        Command::Dbg(DbgCmd::Reg(sub)) => cmd_dbg::reg(cli, sub),
        Command::Dbg(DbgCmd::Dmi(sub)) => cmd_dbg::dmi(cli, sub),
        Command::Read(args) => cmd_dbg::read(cli, args),
        Command::Flash(args) => cmd_flash::flash(cli, args),
        Command::Verify(args) => cmd_flash::verify(cli, args),
        Command::Erase(args) => cmd_flash::erase(cli, args),
        Command::Reset(args) => cmd_flash::reset(cli, args),
        Command::Recover(args) => cmd_flash::recover(cli, args),
        Command::Doctor(args) => cmd_doctor::doctor(cli, args),
        Command::Monitor(args) => cmd_monitor::monitor(cli, args),
        Command::Gdb(args) => cmd_gdb::gdb(cli, args),
        Command::Db(DbCmd::List {
            family,
            verified_only,
        }) => cmd_db::list(cli, family.as_deref(), *verified_only),
        Command::Db(DbCmd::Info { sku }) => cmd_db::info(cli, sku),
        Command::Capabilities => cmd_capabilities::capabilities(cli),
        Command::Write(args) => cmd_write::write(cli, args),
        Command::Arduino(ArduinoCmd::Discovery) => cmd_arduino::discovery(cli),
        Command::Arduino(ArduinoCmd::Monitor { protocol }) => cmd_arduino::monitor(cli, protocol),
        Command::Run(args) => cmd_run::run(cli, args),
        Command::Broker(BrokerCmd::Serve) => broker::serve(cli),
        Command::Broker(BrokerCmd::Endpoint) => broker::endpoint(cli),
        Command::Boot(BootCmd::Hid(HidBootCmd::Flash { file, usb_id })) => {
            cmd_boot::hid_flash(cli, file, usb_id.as_deref())
        }
        Command::Complete(a) => cmd_complete(a.shell),
        other => unimplemented_cmd(cli, canonical_name(other)),
    }
}

/// `complete <shell>`: print a shell completion script to stdout (redirect it into your shell's
/// completion dir). Generated from the clap command tree, so it always matches the CLI.
fn cmd_complete(shell: Shell) -> std::process::ExitCode {
    use clap::CommandFactory;
    let target = match shell {
        Shell::Bash => clap_complete::Shell::Bash,
        Shell::Zsh => clap_complete::Shell::Zsh,
        Shell::Fish => clap_complete::Shell::Fish,
        Shell::Powershell => clap_complete::Shell::PowerShell,
    };
    let mut cmd = Cli::command();
    clap_complete::generate(target, &mut cmd, "ch32rv", &mut std::io::stdout());
    std::process::ExitCode::SUCCESS
}

fn cmd_version(cli: &Cli) -> std::process::ExitCode {
    let git_rev = env!("CH32RV_GIT_REV");
    let stub_digest = ch32rv_flash::stub::stub_digest();
    // en: Embedded device-DB provenance (source rev + fingerprint) - docs/architecture.ja.md §3.
    // ja: 埋め込み device DB の来歴(source rev + 指紋)。architecture.ja.md §3 の再現性契約。
    let db = ch32rv_target::provenance();
    let mut env = ResultEnvelope::success("version");
    env.result = Some(serde_json::json!({
        "version": env!("CARGO_PKG_VERSION"),
        "git_rev": git_rev,
        "target_db": {
            "source": "ch32-device-data",
            "source_rev": db.source_rev,
            "digest": db.digest,
        },
        "flash_stub_digest": stub_digest,
        "build": {
            "os": std::env::consts::OS,
            "arch": std::env::consts::ARCH,
        },
    }));
    if cli.json {
        print_envelope(&env)
    } else {
        println!("ch32rv {} ({git_rev})", env!("CARGO_PKG_VERSION"));
        println!("contract:   {}", contract::CONTRACT_VERSION);
        println!(
            "target db:  ch32-device-data@{} ({})",
            db.source_rev.as_deref().unwrap_or("unknown"),
            db.digest
        );
        println!("flash stub: {stub_digest}");
        println!(
            "build:      {}-{}",
            std::env::consts::ARCH,
            std::env::consts::OS
        );
        std::process::ExitCode::SUCCESS
    }
}

pub(crate) fn unimplemented_cmd(cli: &Cli, cmd: &str) -> std::process::ExitCode {
    if cli.json {
        let mut env = ResultEnvelope::failure(
            cmd,
            ErrorKind::Unimplemented,
            format!("`{cmd}` is reserved and not implemented yet"),
        );
        if let Some(e) = env.error.as_mut() {
            e.hint = Some("the name is reserved for a later version (docs/cli.ja.md)".to_owned());
        }
        let code = ErrorKind::Unimplemented.exit_code();
        let _ = print_envelope(&env);
        code.into()
    } else {
        eprintln!("ch32rv: error[unimplemented]: `{cmd}` is reserved and not implemented yet");
        ErrorKind::Unimplemented.exit_code().into()
    }
}

pub(crate) fn print_envelope(env: &ResultEnvelope) -> std::process::ExitCode {
    match serde_json::to_string(env) {
        Ok(s) => {
            println!("{s}");
            if env.ok {
                std::process::ExitCode::SUCCESS
            } else {
                env.error
                    .as_ref()
                    .map(|e| std::process::ExitCode::from(e.code))
                    .unwrap_or(contract::ExitCode::Internal.into())
            }
        }
        Err(e) => {
            eprintln!("ch32rv: internal error: failed to serialize result: {e}");
            contract::ExitCode::Internal.into()
        }
    }
}

/// en: Canonical command name (`cmd` in the JSON envelope; docs/contract/result.schema.json).
/// ja: command の正規名(JSON の `cmd`。docs/contract/result.schema.json)。
fn canonical_name(cmd: &Command) -> &'static str {
    match cmd {
        Command::Flash(_) => "flash",
        Command::Verify(_) => "verify",
        Command::Read(_) => "read",
        Command::Write(_) => "write",
        Command::Erase(_) => "erase",
        Command::Reset(_) => "reset",
        Command::Run(_) => "run",
        Command::Recover(_) => "recover",
        Command::Probe(p) => match p {
            ProbeCmd::List { .. } => "probe.list",
            ProbeCmd::Info => "probe.info",
            ProbeCmd::Power(_) => "probe.power",
            ProbeCmd::Mode(ModeCmd::Get) => "probe.mode.get",
            ProbeCmd::Mode(ModeCmd::Set { .. }) => "probe.mode.set",
            ProbeCmd::Firmware(FirmwareCmd::Info) => "probe.firmware.info",
            ProbeCmd::Firmware(FirmwareCmd::Check { .. }) => "probe.firmware.check",
            ProbeCmd::Firmware(FirmwareCmd::ExitIap) => "probe.firmware.exit-iap",
            ProbeCmd::Firmware(FirmwareCmd::Update { .. }) => "probe.firmware.update",
            ProbeCmd::Vendor { .. } => "probe.vendor",
        },
        Command::Target(t) => match t {
            TargetCmd::Info => "target.info",
            TargetCmd::Opt(OptionCmd::Get) => "target.option.get",
            TargetCmd::Opt(OptionCmd::Set { .. }) => "target.option.set",
            TargetCmd::Opt(OptionCmd::Reset) => "target.option.reset",
            TargetCmd::Opt(OptionCmd::WriteRaw { .. }) => "target.option.write-raw",
            TargetCmd::Protect { .. } => "target.protect",
        },
        Command::Dbg(d) => match d {
            DbgCmd::Halt { .. } => "dbg.halt",
            DbgCmd::Resume => "dbg.resume",
            DbgCmd::Step { .. } => "dbg.step",
            DbgCmd::Regs => "dbg.regs",
            DbgCmd::Reg(_) => "dbg.reg",
            DbgCmd::Dmi(_) => "dbg.dmi",
        },
        Command::Monitor(m) => match &m.cmd {
            Some(MonitorCmd::List) => "monitor.list",
            Some(MonitorCmd::Sdi { .. }) => "monitor.sdi",
            None => "monitor",
        },
        Command::Gdb(_) => "gdb",
        Command::Dap(_) => "dap",
        Command::Isp(i) => match &i.cmd {
            IspCmd::List => "isp.list",
            IspCmd::Info => "isp.info",
            IspCmd::Enter { .. } => "isp.enter",
            IspCmd::Flash { .. } => "isp.flash",
            IspCmd::Verify { .. } => "isp.verify",
            IspCmd::Erase => "isp.erase",
            IspCmd::Eeprom(EepromCmd::Read { .. }) => "isp.eeprom.read",
            IspCmd::Eeprom(EepromCmd::Write { .. }) => "isp.eeprom.write",
            IspCmd::Eeprom(EepromCmd::Erase) => "isp.eeprom.erase",
            IspCmd::Config(IspConfigCmd::Get) => "isp.config.get",
            IspCmd::Config(IspConfigCmd::Set { .. }) => "isp.config.set",
            IspCmd::Config(IspConfigCmd::Reset) => "isp.config.reset",
            IspCmd::Reset => "isp.reset",
        },
        Command::Boot(b) => match b {
            BootCmd::Enter { .. } => "boot.enter",
            BootCmd::Dfu(DfuCmd::Flash { .. }) => "boot.dfu.flash",
            BootCmd::Dfu(DfuCmd::Info) => "boot.dfu.info",
            BootCmd::Uf2(Uf2Cmd::Flash { .. }) => "boot.uf2.flash",
            BootCmd::Uart(UartBootCmd::Flash { .. }) => "boot.uart.flash",
            BootCmd::Uart(UartBootCmd::Info { .. }) => "boot.uart.info",
            BootCmd::Hid(HidBootCmd::Flash { .. }) => "boot.hid.flash",
        },
        Command::Db(d) => match d {
            DbCmd::List { .. } => "db.list",
            DbCmd::Info { .. } => "db.info",
        },
        Command::Capabilities => "capabilities",
        Command::Doctor(_) => "doctor",
        Command::Version => "version",
        Command::Complete(_) => "complete",
        Command::Broker(b) => match b {
            BrokerCmd::Serve => "broker.serve",
            BrokerCmd::Endpoint => "broker.endpoint",
        },
        Command::Arduino(a) => match a {
            ArduinoCmd::Discovery => "arduino.discovery",
            ArduinoCmd::Monitor { .. } => "arduino.monitor",
        },
    }
}