ch32rv 0.9.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
//! en: `run` (docs/cli.ja.md §4.1) - the HIL runner: flash an image (unless `--no-flash`), reset to
//! run, stream the target's runtime output (dmdata or rtt, stdin going back to the target), and
//! end on either a timeout or a semihosting exit whose code is propagated. Self-contained over the
//! Debug Module. Under `--json` the output goes to stderr as `output` events and stdout carries
//! only the result envelope.
//! ja: `run`。HIL 用ランナー。書込→reset 実行→runtime 出力(dmdata / rtt、stdin は target へ)を
//! 流し、timeout か semihosting の exit(コードを伝搬)で終わる。DM 上で自己完結。`--json` では
//! 出力は stderr の `output` event、stdout は envelope のみ。

use std::process::ExitCode;
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};

use ch32rv_contract::policy::{ImageFormat, MonitorSource};
use ch32rv_contract::{ErrorKind, Warning};
use ch32rv_dmi::RegName;
use ch32rv_flash::params_for_family;

use crate::args::{Cli, ExitOn, RunArgs};
use crate::cmd_probe::{fail, select_entry};
use crate::parse;
use crate::session::Session;
use crate::source::{self, DmiSource, Sink};

/// RISC-V semihosting call sequence: `slli x0,x0,0x1f; ebreak; srai x0,x0,7`.
const SEMI_SLLI: u32 = 0x01f0_1013;
const SEMI_EBREAK: u32 = 0x0010_0073;
const SEMI_SRAI: u32 = 0x4070_5013;
/// Semihosting operations we service.
const SYS_WRITEC: u32 = 0x03;
const SYS_WRITE0: u32 = 0x04;
const SYS_EXIT: u32 = 0x18;
const SYS_EXIT_EXTENDED: u32 = 0x20;
/// `ADP_Stopped_ApplicationExit` - a plain SYS_EXIT reason that means "exited normally".
const ADP_APPLICATION_EXIT: u32 = 0x2_0026;

enum ExitMode {
    /// Stream output until this deadline (None = until Ctrl-C), then exit 0.
    Timeout(Option<Duration>),
    /// Service semihosting; propagate the target's SYS_EXIT code (bounded by `cap`).
    Semihosting { cap: Duration },
}

/// Is `(before, at, after)` the RISC-V semihosting `slli/ebreak/srai` sequence?
fn is_semihosting_seq(before: u32, at: u32, after: u32) -> bool {
    before == SEMI_SLLI && at == SEMI_EBREAK && after == SEMI_SRAI
}

pub fn run(cli: &Cli, args: &RunArgs) -> ExitCode {
    const CMD: &str = "run";
    // The run length / safety cap is --duration (transport --timeout stays per-transfer).
    let exit_mode = match args.exit_on {
        Some(ExitOn::Semihosting) => ExitMode::Semihosting {
            cap: Duration::from_secs(cli.duration.unwrap_or(60)),
        },
        None | Some(ExitOn::Timeout) => ExitMode::Timeout(cli.duration.map(Duration::from_secs)),
    };

    // run streams over DMI only (probe-agnostic, no CDC involved): dmdata (default) or rtt.
    let source = args.source.unwrap_or(MonitorSource::Dmdata);
    if matches!(source, MonitorSource::Uart | MonitorSource::Sdi) {
        return fail(
            cli,
            CMD,
            ErrorKind::CapabilityUnsupported,
            "run streams over DMI: --source dmdata or rtt (uart/sdi are `monitor` sources)",
            None,
        );
    }

    let bytes = if args.no_flash {
        Vec::new()
    } else {
        match std::fs::read(&args.elf) {
            Ok(d) => d,
            Err(e) => {
                return fail(
                    cli,
                    CMD,
                    ErrorKind::Usage,
                    format!("read {}: {e}", args.elf.display()),
                    None,
                );
            }
        }
    };

    let entry = match select_entry(cli, CMD) {
        Ok(e) => e,
        Err(c) => return c,
    };
    if entry.mode != ch32rv_contract::ProbeMode::Riscv {
        return fail(
            cli,
            CMD,
            ErrorKind::CapabilityUnsupported,
            "run requires a probe in RISC-V mode",
            None,
        );
    }
    let (speed, mut warnings) = match parse::speed(&cli.speed) {
        Ok(v) => v,
        Err(m) => return fail(cli, CMD, ErrorKind::Usage, m, None),
    };
    let timeout = Duration::from_millis(cli.timeout.map(|s| s * 1000).unwrap_or(3000));
    let mut session = match Session::attach(
        &entry,
        speed,
        timeout,
        Duration::from_secs(cli.lock_timeout),
        cli.chip.as_deref(),
        cli.db.as_deref(),
        &mut warnings,
    ) {
        Ok(s) => s,
        Err(e) => return crate::cmd_probe::session_error(cli, CMD, e),
    };

    // Program the image (unless --no-flash).
    if !args.no_flash {
        let family = session.attach.family_byte;
        let Some(fp) = params_for_family(family) else {
            return fail(
                cli,
                CMD,
                ErrorKind::CapabilityUnsupported,
                format!("flashing family 0x{family:02x} is not supported"),
                None,
            );
        };
        let image = match crate::cmd_flash::parse_image(
            &bytes,
            ImageFormat::Auto,
            &args.elf,
            None,
            ch32rv_flash::CODE_FLASH_START,
        ) {
            Ok(i) => i,
            Err(e) => return fail(cli, CMD, ErrorKind::Usage, e.to_string(), None),
        };
        // en: Erase before programming, as `flash` does. The stub path is a full-region programmer
        // that expects erased flash: writing into programmed flash without an erase is refused by
        // the probe (`0x55`) or - on the CH549 Link, fw 2.12 - simply never answers, so `run`
        // failed with `transfer timed out` at the image's first address on every attempt while the
        // same image flashed fine through `flash` (which erases). A WCH-LinkE tolerated it, which
        // is why the runner worked on the rest of the bench. `run` always programs a whole image
        // from the flash base, so the chip erase `flash --erase auto` would pick is the right one.
        // ja: `flash` と同じく書込前に消去する。stub 経路は消去済み flash を前提にした全 region
        // 書込器で、消去せずに書くと probe が拒否する(`0x55`)か、CH549 Link(fw 2.12)では**応答が
        // 返ってこない** — `run` は image 先頭で必ず `transfer timed out` になっていた(同じ image が
        // 消去を伴う `flash` では通る)。WCH-LinkE が黙認するのでベンチの他機では動いていた。`run` は
        // 常に flash 先頭から image 全体を焼くので、`flash --erase auto` が選ぶ chip erase でよい。
        if let Err(e) = session.link().erase_flash() {
            return fail(
                cli,
                CMD,
                ErrorKind::TransferFailed,
                format!("erase before programming failed: {e}"),
                None,
            );
        }
        // One contiguous region, as the stub path requires - see `Image::program_span`: only the
        // first `write_flash` of a session takes effect, and an ELF's `.data` initialiser arrives
        // as a segment of its own behind `.text`, so anything else loses the tail of the image.
        for seg in image.program_span(fp.data_packet_size as u32).iter() {
            if let Err(e) = session.link().write_flash(seg.addr, &seg.data, &fp, |_| {}) {
                return fail(
                    cli,
                    CMD,
                    ErrorKind::TransferFailed,
                    format!("program failed at {:#010x}: {e}", seg.addr),
                    None,
                );
            }
        }
    }

    // Reset to run the freshly programmed image. For semihosting the ebreak-debug CSR is set
    // *after* the reset (so a core reset cannot clear it) and *before* the source is opened (the
    // rtt scan lets the core run between attempts, and an ebreak must already trap to debug mode).
    // en: Hold the halt request across the reset so the hart comes out of it in debug mode, at its
    // reset vector, instead of running ahead of the host. After the flash step the probe no longer
    // holds the halt request from attach (the stub ran), so a plain reset here lets the image start
    // immediately, and on a slow link the halt lands well inside it - measured on a CH549 WCH-Link:
    // `dpc = 0x2e`, the program's first semihosting call already executed, so `--exit-on
    // semihosting` waited out its cap for an exit it had missed. `halt` then waits and clears the
    // request; the source open / stream resumes the hart when everything is in place.
    // ja: reset をまたいで halt 要求を保持し、hart を reset 直後・reset vector で debug mode に入れる
    // (host より先に走り出させない)。書込後は stub が走った影響で probe 側に attach 由来の halt 要求が
    // 残っておらず、素の reset では image がすぐ走り出す。遅いリンクでは halt がその内側に落ちる —
    // CH549 で実測 `dpc = 0x2e`(最初の semihosting 呼出を実行済み)で、`--exit-on semihosting` は
    // 取り逃がした exit を cap いっぱい待っていた。`halt` が待って要求をクリアし、resume は source を
    // 開いてから stream が行う。
    let _ = session.dm().reset_halt();
    {
        let mut dm = session.dm();
        let _ = dm.halt();
        if matches!(exit_mode, ExitMode::Semihosting { .. }) {
            let _ = dm.enable_ebreak_debug();
        }
    }
    let mut src = match DmiSource::open(&mut session, source, &mut warnings) {
        Ok(s) => s,
        Err(e) => return crate::cmd_monitor::open_error(cli, CMD, e),
    };
    if !cli.json {
        eprintln!(
            "run: {} via {} (Ctrl-C to stop; stdin goes to the target)",
            src.name(),
            entry.dev.serial().unwrap_or("?")
        );
        for w in &warnings {
            eprintln!("warning[{}]: {}", w.code, w.msg);
        }
    }
    let input = source::spawn_reader(std::io::stdin());

    match exit_mode {
        ExitMode::Timeout(dur) => {
            run_stream(cli, CMD, &mut session, &mut src, &input, dur, warnings)
        }
        ExitMode::Semihosting { cap } => {
            run_semihosting(cli, CMD, &mut session, &mut src, &input, cap, warnings)
        }
    }
}

/// Stream the source's output until the deadline (or forever), then exit 0.
fn run_stream(
    cli: &Cli,
    cmd: &str,
    session: &mut Session,
    src: &mut DmiSource,
    input: &Receiver<Vec<u8>>,
    dur: Option<Duration>,
    warnings: Vec<Warning>,
) -> ExitCode {
    let deadline = dur.map(|d| Instant::now() + d);
    // Open leaves the core halted; the sources only move while it runs.
    let _ = session.dm().resume();
    let mut sink = Sink::new(cli, src.name());
    let result = source::stream(session, src, &mut sink, input, deadline);
    sink.finish();
    if let Err(e) = result {
        return fail(
            cli,
            cmd,
            source::dmi_error_kind(&e),
            format!("{} stream failed: {e}", src.name()),
            None,
        );
    }
    if cli.json {
        let mut env = ch32rv_contract::ResultEnvelope::success(cmd);
        env.result = Some(serde_json::json!({ "exit": 0, "reason": "timeout" }));
        env.warnings = warnings;
        crate::print_envelope(&env)
    } else {
        ExitCode::SUCCESS
    }
}

/// Run, servicing semihosting calls, until SYS_EXIT (propagate the code) or the safety cap.
/// The caller has already enabled ebreak-to-debug-mode on the (halted) core.
fn run_semihosting(
    cli: &Cli,
    cmd: &str,
    session: &mut Session,
    src: &mut DmiSource,
    input: &Receiver<Vec<u8>>,
    cap: Duration,
    warnings: Vec<Warning>,
) -> ExitCode {
    let deadline = Instant::now() + cap;
    let mut sink = Sink::new(cli, src.name());
    let mut semi = Sink::new(cli, "semihosting");
    let mut pending = Vec::new();
    let _ = session.dm().resume();
    loop {
        if Instant::now() >= deadline {
            sink.finish();
            semi.finish();
            let msg = "run: timed out waiting for a semihosting exit";
            if cli.json {
                let env =
                    ch32rv_contract::ResultEnvelope::failure(cmd, ErrorKind::TransportTimeout, msg);
                return crate::print_envelope(&env);
            }
            eprintln!("{msg}");
            return ErrorKind::TransportTimeout.exit_code().into();
        }
        // Exchange runtime output / stdin while running (an rtt poll leaves a halted core halted).
        source::drain_input(input, &mut pending);
        if let Ok(b) = src.poll(session, &mut pending) {
            sink.write(&b);
        }
        let mut dm = session.dm();
        match dm.is_halted() {
            Ok(false) => {
                std::thread::sleep(Duration::from_millis(5));
                continue;
            }
            Ok(true) => {}
            Err(_) => {
                std::thread::sleep(Duration::from_millis(5));
                continue;
            }
        }
        // Halted: is it a semihosting call?
        let Ok(dpc) = dm.read_reg(RegName::Pc) else {
            let _ = dm.resume();
            continue;
        };
        let at = dm.read_mem(dpc, 4).ok();
        let before = dm.read_mem(dpc.wrapping_sub(4), 4).ok();
        let after = dm.read_mem(dpc.wrapping_add(4), 4).ok();
        let w = |o: &Option<Vec<u8>>| {
            o.as_ref()
                .filter(|v| v.len() == 4)
                .map(|v| u32::from_le_bytes([v[0], v[1], v[2], v[3]]))
        };
        match (w(&before), w(&at), w(&after)) {
            (Some(b), Some(a), Some(af)) if is_semihosting_seq(b, a, af) => {
                let op = dm.read_reg(RegName::Gpr(10)).unwrap_or(0); // a0
                let arg = dm.read_reg(RegName::Gpr(11)).unwrap_or(0); // a1
                match op {
                    SYS_EXIT | SYS_EXIT_EXTENDED => {
                        let code = semihosting_exit_code(&mut dm, op, arg);
                        sink.finish();
                        semi.finish();
                        if cli.json {
                            let mut env = ch32rv_contract::ResultEnvelope::success(cmd);
                            env.result = Some(serde_json::json!({ "exit": code }));
                            env.warnings = warnings;
                            return crate::print_envelope(&env);
                        }
                        return ExitCode::from(code as u8);
                    }
                    SYS_WRITE0 => {
                        // a1 -> NUL-terminated string.
                        let mut addr = arg;
                        'outer: for _ in 0..1024 {
                            let Ok(chunk) = dm.read_mem(addr, 16) else {
                                break;
                            };
                            for &byte in &chunk {
                                if byte == 0 {
                                    break 'outer;
                                }
                                semi.write(&[byte]);
                            }
                            addr = addr.wrapping_add(16);
                        }
                        // Skip the ebreak: resume from the srai (dpc + 4).
                        let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
                        let _ = dm.resume();
                    }
                    SYS_WRITEC => {
                        if let Ok(c) = dm.read_mem(arg, 1)
                            && let Some(&byte) = c.first()
                        {
                            semi.write(&[byte]);
                        }
                        let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
                        let _ = dm.resume();
                    }
                    _ => {
                        // Unsupported call: skip it and continue.
                        let _ = dm.write_reg(RegName::Pc, dpc.wrapping_add(4));
                        let _ = dm.resume();
                    }
                }
            }
            _ => {
                // A non-semihosting halt (a real breakpoint/trap): the program is not running as
                // expected after program+reset, same class as flash's confirm-run failure (exit 50).
                sink.finish();
                semi.finish();
                let msg = format!("run: target halted at {dpc:#010x} (not a semihosting call)");
                if cli.json {
                    let env = ch32rv_contract::ResultEnvelope::failure(
                        cmd,
                        ErrorKind::NotRunningAfterWrite,
                        msg,
                    );
                    return crate::print_envelope(&env);
                }
                eprintln!("{msg}");
                return ErrorKind::NotRunningAfterWrite.exit_code().into();
            }
        }
    }
}

/// Derive the process exit code from a semihosting SYS_EXIT / SYS_EXIT_EXTENDED call.
fn semihosting_exit_code(
    dm: &mut ch32rv_dmi::DebugModule<'_, ch32rv_wchlink::WchLink>,
    op: u32,
    arg: u32,
) -> u32 {
    if op == SYS_EXIT_EXTENDED {
        // a1 -> [reason(u32), exit_code(u32)].
        if let Ok(block) = dm.read_mem(arg, 8)
            && block.len() == 8
        {
            return u32::from_le_bytes([block[4], block[5], block[6], block[7]]);
        }
        return 1;
    }
    // Plain SYS_EXIT (RV32): a1 is the reason code directly.
    if arg == ADP_APPLICATION_EXIT { 0 } else { 1 }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]
    use super::*;

    #[test]
    fn detects_semihosting_sequence() {
        assert!(is_semihosting_seq(SEMI_SLLI, SEMI_EBREAK, SEMI_SRAI));
        assert!(!is_semihosting_seq(SEMI_SLLI, 0x0000_0013, SEMI_SRAI)); // plain nop, not ebreak
        assert!(!is_semihosting_seq(0, SEMI_EBREAK, 0)); // bare ebreak, no magic brackets
    }
}