ch32rv_debug/server.rs
1//! en: A gdbstub `Target` for a CH32 RISC-V core reached over a [`DtmAccess`] transport.
2//! Supports registers, memory read/write, halt/continue/single-step, and breakpoints. A `break`
3//! GDB requests as software (Z0) is placed by the cheapest working mechanism, in order: a RAM
4//! `ebreak` memory-patch; else a hardware execute trigger when the core has a free slot (no
5//! wear); else a flash software breakpoint that rewrites the containing flash page (works on
6//! triggerless cores, at the cost of flash wear). Hardware breakpoints use the RISC-V trigger
7//! module (measured: 4 slots on QingKe V4F/CH32V307 and V4C/CH32X035, live-fire confirmed; 0 on
8//! V4B/CH32V203, V2A/CH32V003, V3/CH32V103 - detected dynamically). Flash software breakpoints
9//! need a verified FLASH-controller page profile (256-byte families for now; V003/V103 are a
10//! follow-up). Attach does not modify flash; flash breakpoints are removed and pages restored on
11//! detach (docs/cli.ja.md §4.6).
12//! ja: [`DtmAccess`] 上の CH32 RISC-V core 用 gdbstub Target。register・memory R/W・
13//! halt/continue/step・breakpoint。GDB が software(Z0)で要求した `break` は、動く中で最も安い
14//! 手段の順(RAM `ebreak` patch → 空き HW trigger〔摩耗なし〕→ flash page 書き換えの flash SW
15//! breakpoint〔trigger 無し core でも効くが flash 摩耗あり〕)で張る。hardware breakpoint は
16//! RISC-V trigger module(実測: V4F/V307・V4C/X035 は 4 slot 実発火、V4B/V203・V2A/V003・V3/V103 は
17//! 0。動的検出)。flash SW breakpoint は検証済み FLASH-controller profile が必要(現状 256byte
18//! family、V003/V103 は後続)。attach で flash を書き換えず、flash breakpoint は detach 時に外して
19//! page を復元する。
20
21use ch32rv_dmi::{DebugModule, DmiError, DtmAccess, FlashProgMode, RegName};
22use gdbstub::common::Signal;
23use gdbstub::target::ext::base::singlethread::{
24 SingleThreadBase, SingleThreadResume, SingleThreadResumeOps, SingleThreadSingleStep,
25 SingleThreadSingleStepOps,
26};
27use gdbstub::target::ext::breakpoints::{
28 Breakpoints, BreakpointsOps, HwBreakpoint, HwBreakpointOps, SwBreakpoint, SwBreakpointOps,
29};
30use gdbstub::target::{Target, TargetError, TargetResult};
31
32use crate::arch::{Rv32, Rv32CoreRegs};
33
34/// A software breakpoint: the address and the original bytes we overwrote with `ebreak`.
35struct SwBp {
36 addr: u32,
37 original: Vec<u8>,
38}
39
40/// A hardware breakpoint: the trigger slot it occupies and the address it watches.
41struct HwBp {
42 slot: u32,
43 addr: u32,
44}
45
46/// A flash software breakpoint: an `ebreak` patched into flash-resident code by rewriting its
47/// page. `len` is the patched instruction size (2 for RVC, 4 otherwise).
48struct FlashBp {
49 addr: u32,
50 len: usize,
51}
52
53/// A flash page we manage: its pristine content (before any breakpoint) and what we last
54/// programmed into it, so repeated set/clear that yields identical content skips the rewrite.
55struct FlashPage {
56 page_addr: u32,
57 pristine: Vec<u8>,
58 current: Vec<u8>,
59}
60
61/// en: gdbstub target that OWNS its transport `T` (owning, not borrowing, keeps the type free
62/// of a lifetime so it fits `BlockingEventLoop::Target`). Recover the transport with
63/// [`Ch32Target::into_inner`] to detach cleanly afterwards.
64/// ja: transport `T` を所有する gdbstub target(所有にすることでライフタイムが付かず
65/// `BlockingEventLoop::Target` に収まる)。後始末は [`Ch32Target::into_inner`] で回収する。
66pub struct Ch32Target<T: DtmAccess> {
67 dtm: T,
68 breakpoints: Vec<SwBp>,
69 hw_breakpoints: Vec<HwBp>,
70 hw_trigger_count: u32,
71 /// Number of integer GPRs the core exposes: 16 on RV32E (misa.E, e.g. CH32V003), else 32.
72 /// Reading a non-existent GPR via an abstract command raises cmderr, so we must not touch
73 /// x16..x31 on an RV32E hart.
74 gpr_count: u8,
75 /// Fast-page size for flash software breakpoints, or None when this family's FLASH-controller
76 /// profile is not verified (so a flash breakpoint is refused rather than risked).
77 flash_page_size: Option<u32>,
78 /// The family's fast-program mechanism (meaningful only when `flash_page_size` is Some).
79 flash_prog_mode: FlashProgMode,
80 flash_bps: Vec<FlashBp>,
81 flash_pages: Vec<FlashPage>,
82}
83
84impl<T: DtmAccess> Ch32Target<T> {
85 /// en: Find a free hardware trigger slot (none used twice). Returns None when the core has
86 /// no trigger module or all slots are taken.
87 /// ja: 空いている HW trigger slot を探す。trigger 無し/全 slot 使用中なら None。
88 fn alloc_hw_slot(&self) -> Option<u32> {
89 (0..self.hw_trigger_count).find(|s| !self.hw_breakpoints.iter().any(|b| b.slot == *s))
90 }
91
92 /// en: Wrap a transport and halt the core so GDB attaches to a stopped target. `flash` is the
93 /// FLASH-controller profile for this family (fast-page size + program mode, from
94 /// `ch32rv_flash::flash_controller_profile`), or None to refuse flash software breakpoints.
95 /// ja: transport を包んで halt。`flash` はこの family の FLASH-controller profile(fast page
96 /// サイズ + program mode。None なら flash SW breakpoint を拒否)。
97 pub fn new(dtm: T, flash: Option<(u32, FlashProgMode)>) -> Result<Self, DmiError> {
98 let mut t = Self {
99 dtm,
100 breakpoints: Vec::new(),
101 hw_breakpoints: Vec::new(),
102 hw_trigger_count: 0,
103 gpr_count: 32,
104 flash_page_size: flash.map(|(p, _)| p),
105 flash_prog_mode: flash.map(|(_, m)| m).unwrap_or(FlashProgMode::PgStart),
106 flash_bps: Vec::new(),
107 flash_pages: Vec::new(),
108 };
109 t.dm().halt()?;
110 // Make `ebreak` halt into debug mode so software breakpoints stop the core.
111 let _ = t.dm().enable_ebreak_debug();
112 t.hw_trigger_count = t.dm().hw_trigger_count();
113 // misa.E (bit 4) marks RV32E: only x0..x15 exist. Reading x16.. would raise cmderr.
114 const MISA: u16 = 0x301;
115 const MISA_E: u32 = 1 << 4;
116 if let Ok(misa) = t.dm().read_reg(RegName::Csr(MISA))
117 && misa & MISA_E != 0
118 {
119 t.gpr_count = 16;
120 }
121 Ok(t)
122 }
123
124 /// Number of hardware trigger slots the core exposes (0 on V003/V2A).
125 pub fn hw_trigger_count(&self) -> u32 {
126 self.hw_trigger_count
127 }
128
129 fn dm(&mut self) -> DebugModule<'_, T> {
130 DebugModule::new(&mut self.dtm)
131 }
132
133 /// True if the core is halted right now.
134 pub fn is_halted(&mut self) -> Result<bool, DmiError> {
135 self.dm().is_halted()
136 }
137
138 /// Request a halt (used for Ctrl-C).
139 pub fn halt(&mut self) -> Result<(), DmiError> {
140 self.dm().halt()
141 }
142
143 /// Recover the owned transport (to detach after the session).
144 pub fn into_inner(self) -> T {
145 self.dtm
146 }
147
148 /// True if this core supports flash software breakpoints (verified FLASH-controller profile).
149 pub fn flash_breakpoints_supported(&self) -> bool {
150 self.flash_page_size.is_some()
151 }
152
153 /// en: Restore every managed flash page to its pristine content (removing any `ebreak` still
154 /// patched in). Call before detaching so an interrupted session never leaves a breakpoint
155 /// baked into flash. Best-effort.
156 /// ja: 管理中の flash page をすべて pristine に戻す(残った `ebreak` を消す)。detach 前に
157 /// 呼び、途中終了しても flash に breakpoint が焼き付かないようにする。best-effort。
158 pub fn restore_flash_breakpoints(&mut self) {
159 let Some(page) = self.flash_page_size else {
160 return;
161 };
162 self.flash_bps.clear();
163 let addrs: Vec<u32> = self.flash_pages.iter().map(|p| p.page_addr).collect();
164 for page_addr in addrs {
165 let _ = self.reprogram_flash_page(page, page_addr);
166 }
167 self.flash_pages.clear();
168 }
169
170 /// en: Map a code address into the physical code-flash window. Programs run from the low
171 /// alias (0x0000_0000 mirrors flash), but the FLASH controller must be given the real flash
172 /// address (0x0800_0000+off). Reads work through either mirror; writes must use the physical.
173 /// ja: コード番地を物理 code-flash 窓へ写す。実行は低位 alias(0x0000_0000=flash の鏡)だが、
174 /// FLASH controller には実 flash 番地(0x0800_0000+off)を渡す必要がある。
175 fn flash_phys(addr: u32) -> u32 {
176 const FLASH_BASE: u32 = 0x0800_0000;
177 if addr < FLASH_BASE {
178 FLASH_BASE + addr
179 } else {
180 addr
181 }
182 }
183
184 /// The `ebreak` patch bytes for an instruction of size `len` (2 = RVC c.ebreak, else ebreak).
185 fn ebreak_patch(len: usize) -> &'static [u8] {
186 if len == 2 {
187 &[0x02, 0x90] // c.ebreak (0x9002, little-endian)
188 } else {
189 &[0x73, 0x00, 0x10, 0x00] // ebreak (0x00100073)
190 }
191 }
192
193 /// en: Rewrite `page_addr` to hold its pristine content plus every currently-active flash
194 /// breakpoint in that page. Skips the erase/program when the page already matches (so a
195 /// redundant set/clear round-trip costs no flash wear). Returns Ok(true) if it wrote.
196 /// ja: `page_addr` を「pristine + その page の全 flash breakpoint」の内容に書き直す。既に一致
197 /// なら erase/program を省く(無駄な書き換え=摩耗を避ける)。書いたら Ok(true)。
198 fn reprogram_flash_page(&mut self, page: u32, page_addr: u32) -> Result<bool, DmiError> {
199 let Some(idx) = self
200 .flash_pages
201 .iter()
202 .position(|p| p.page_addr == page_addr)
203 else {
204 return Ok(false);
205 };
206 let mut desired = self.flash_pages[idx].pristine.clone();
207 for bp in &self.flash_bps {
208 if bp.addr & !(page - 1) == page_addr {
209 let off = (bp.addr - page_addr) as usize;
210 let patch = Self::ebreak_patch(bp.len);
211 if off + bp.len <= desired.len() {
212 desired[off..off + bp.len].copy_from_slice(patch);
213 }
214 }
215 }
216 if self.flash_pages[idx].current == desired {
217 return Ok(false); // no net change: skip the flash write
218 }
219 let phys = Self::flash_phys(page_addr);
220 let mode = self.flash_prog_mode;
221 {
222 let mut dm = self.dm();
223 dm.flash_page_erase(phys, mode)?;
224 dm.flash_program_page(phys, &desired, mode)?;
225 }
226 self.flash_pages[idx].current = desired;
227 Ok(true)
228 }
229
230 /// en: Add a flash software breakpoint by rewriting the containing page. Returns Ok(false)
231 /// if this family has no verified flash profile. The hart must be halted.
232 /// ja: 該当 page を書き換えて flash SW breakpoint を張る。未対応 family は Ok(false)。
233 fn add_flash_breakpoint(&mut self, addr: u32, len: usize) -> TargetResult<bool, Self> {
234 let Some(page) = self.flash_page_size else {
235 return Ok(false);
236 };
237 let page_addr = addr & !(page - 1);
238 // Load the pristine page the first time we touch it (it has no breakpoint yet).
239 if !self.flash_pages.iter().any(|p| p.page_addr == page_addr) {
240 let content = self
241 .dm()
242 .read_mem(page_addr, page)
243 .map_err(TargetError::Fatal)?;
244 self.flash_pages.push(FlashPage {
245 page_addr,
246 pristine: content.clone(),
247 current: content,
248 });
249 }
250 self.flash_bps.push(FlashBp { addr, len });
251 self.reprogram_flash_page(page, page_addr)
252 .map_err(TargetError::Fatal)?;
253 // Verify the ebreak actually landed (flash programming can silently fail on protect).
254 let back = self
255 .dm()
256 .read_mem(addr, len as u32)
257 .map_err(TargetError::Fatal)?;
258 if back != Self::ebreak_patch(len) {
259 // Roll back: drop this bp and restore the page.
260 self.flash_bps.pop();
261 let _ = self.reprogram_flash_page(page, page_addr);
262 return Ok(false);
263 }
264 Ok(true)
265 }
266
267 /// en: Remove a flash software breakpoint, restoring the page (dropping the managed page once
268 /// it holds no more breakpoints). Returns Ok(false) if `addr` was not a flash breakpoint.
269 /// ja: flash SW breakpoint を外して page を復元(その page の breakpoint が無くなれば管理解除)。
270 fn remove_flash_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
271 let Some(page) = self.flash_page_size else {
272 return Ok(false);
273 };
274 let Some(pos) = self.flash_bps.iter().position(|b| b.addr == addr) else {
275 return Ok(false);
276 };
277 let page_addr = addr & !(page - 1);
278 self.flash_bps.remove(pos);
279 self.reprogram_flash_page(page, page_addr)
280 .map_err(TargetError::Fatal)?;
281 // If nothing else lives in this page, stop managing it (it is now pristine again).
282 if !self
283 .flash_bps
284 .iter()
285 .any(|b| b.addr & !(page - 1) == page_addr)
286 {
287 self.flash_pages.retain(|p| p.page_addr != page_addr);
288 }
289 Ok(true)
290 }
291}
292
293impl<T: DtmAccess> Target for Ch32Target<T> {
294 type Arch = Rv32;
295 type Error = DmiError;
296
297 #[inline(always)]
298 fn base_ops(&mut self) -> gdbstub::target::ext::base::BaseOps<'_, Self::Arch, Self::Error> {
299 gdbstub::target::ext::base::BaseOps::SingleThread(self)
300 }
301
302 #[inline(always)]
303 fn support_breakpoints(&mut self) -> Option<BreakpointsOps<'_, Self>> {
304 Some(self)
305 }
306}
307
308impl<T: DtmAccess> SingleThreadBase for Ch32Target<T> {
309 fn read_registers(&mut self, regs: &mut Rv32CoreRegs) -> TargetResult<(), Self> {
310 let gpr_count = self.gpr_count;
311 let mut dm = self.dm();
312 regs.x = [0; 32];
313 // On RV32E only x0..x15 exist; leave x16..x31 as zero (touching them raises cmderr).
314 for i in 1..gpr_count {
315 regs.x[i as usize] = dm.read_reg(RegName::Gpr(i)).map_err(TargetError::Fatal)?;
316 }
317 regs.pc = dm.read_reg(RegName::Pc).map_err(TargetError::Fatal)?;
318 Ok(())
319 }
320
321 fn write_registers(&mut self, regs: &Rv32CoreRegs) -> TargetResult<(), Self> {
322 let gpr_count = self.gpr_count;
323 let mut dm = self.dm();
324 for i in 1..gpr_count {
325 dm.write_reg(RegName::Gpr(i), regs.x[i as usize])
326 .map_err(TargetError::Fatal)?;
327 }
328 dm.write_reg(RegName::Pc, regs.pc)
329 .map_err(TargetError::Fatal)?;
330 Ok(())
331 }
332
333 fn read_addrs(&mut self, start: u32, data: &mut [u8]) -> TargetResult<usize, Self> {
334 let bytes = self
335 .dm()
336 .read_mem(start, data.len() as u32)
337 .map_err(TargetError::Fatal)?;
338 let n = bytes.len().min(data.len());
339 data[..n].copy_from_slice(&bytes[..n]);
340 Ok(n)
341 }
342
343 fn write_addrs(&mut self, start: u32, data: &[u8]) -> TargetResult<(), Self> {
344 self.dm()
345 .write_mem(start, data)
346 .map_err(TargetError::Fatal)?;
347 Ok(())
348 }
349
350 #[inline(always)]
351 fn support_resume(&mut self) -> Option<SingleThreadResumeOps<'_, Self>> {
352 Some(self)
353 }
354}
355
356impl<T: DtmAccess> SingleThreadResume for Ch32Target<T> {
357 fn resume(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
358 self.dm().resume()
359 }
360
361 #[inline(always)]
362 fn support_single_step(&mut self) -> Option<SingleThreadSingleStepOps<'_, Self>> {
363 Some(self)
364 }
365}
366
367impl<T: DtmAccess> SingleThreadSingleStep for Ch32Target<T> {
368 fn step(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
369 self.dm().step()
370 }
371}
372
373impl<T: DtmAccess> Breakpoints for Ch32Target<T> {
374 #[inline(always)]
375 fn support_sw_breakpoint(&mut self) -> Option<SwBreakpointOps<'_, Self>> {
376 Some(self)
377 }
378
379 #[inline(always)]
380 fn support_hw_breakpoint(&mut self) -> Option<HwBreakpointOps<'_, Self>> {
381 // Only advertise HW breakpoints when the core actually has trigger slots.
382 if self.hw_trigger_count > 0 {
383 Some(self)
384 } else {
385 None
386 }
387 }
388}
389
390impl<T: DtmAccess> HwBreakpoint for Ch32Target<T> {
391 fn add_hw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
392 let Some(slot) = self.alloc_hw_slot() else {
393 return Ok(false); // out of trigger slots
394 };
395 self.dm()
396 .set_hw_breakpoint(slot, addr)
397 .map_err(TargetError::Fatal)?;
398 self.hw_breakpoints.push(HwBp { slot, addr });
399 Ok(true)
400 }
401
402 fn remove_hw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
403 if let Some(pos) = self.hw_breakpoints.iter().position(|b| b.addr == addr) {
404 let bp = self.hw_breakpoints.remove(pos);
405 self.dm()
406 .clear_hw_breakpoint(bp.slot)
407 .map_err(TargetError::Fatal)?;
408 Ok(true)
409 } else {
410 Ok(false)
411 }
412 }
413}
414
415impl<T: DtmAccess> SwBreakpoint for Ch32Target<T> {
416 /// en: Set a breakpoint GDB asked for as "software" (Z0). We first try a RAM memory-patch
417 /// (`ebreak`); if the write does not stick - the classic case being a flash address, where
418 /// the DM `sw` silently no-ops - we transparently fall back to a hardware execute trigger
419 /// when the core has a free slot. That makes plain `break` work on flash-resident code for
420 /// V4-class cores (which expose triggers) without the risk of rewriting flash. Cores with no
421 /// trigger module (V003/V103) can still set RAM breakpoints; a flash breakpoint there returns
422 /// unsupported (a flash-patch approach is a follow-up).
423 /// ja: GDB が Z0(software)で要求した breakpoint。まず RAM の memory patch(`ebreak`)を
424 /// 試し、書き込みが定着しない(典型は flash 番地。DM の `sw` が無反応)場合は、空き HW slot が
425 /// あれば HW execute trigger へ透過的にフォールバックし(flash 書き換えの危険なし)、trigger が
426 /// 無ければ page 書き換えの flash software breakpoint へフォールバックする。これで trigger を
427 /// 持たない core(V203 等)でも flash 上のコードに通常 `break` が効く(flash 書き換えの摩耗あり)。
428 /// 順序は RAM → HW trigger → flash-patch。どれも不可なら未対応を返す。
429 fn add_sw_breakpoint(&mut self, addr: u32, kind: usize) -> TargetResult<bool, Self> {
430 // kind is the instruction size GDB expects (2 for RVC, 4 otherwise).
431 let (patch, len): (&[u8], usize) = if kind == 2 {
432 (&[0x02, 0x90], 2) // c.ebreak (0x9002, little-endian)
433 } else {
434 (&[0x73, 0x00, 0x10, 0x00], 4) // ebreak (0x00100073)
435 };
436 // Try a RAM memory-patch first. Scope the DM borrow so we can touch other fields after.
437 let (stuck, original) = {
438 let mut dm = self.dm();
439 let original = dm.read_mem(addr, len as u32).map_err(TargetError::Fatal)?;
440 dm.write_mem(addr, patch).map_err(TargetError::Fatal)?;
441 // The patch lands only in writable memory; a flash `sw` silently no-ops.
442 let back = dm.read_mem(addr, len as u32).map_err(TargetError::Fatal)?;
443 let stuck = back == patch;
444 if !stuck {
445 let _ = dm.write_mem(addr, &original); // restore best-effort
446 }
447 (stuck, original)
448 };
449 if stuck {
450 self.breakpoints.push(SwBp { addr, original });
451 return Ok(true);
452 }
453 // Flash (or otherwise unwritable): prefer a free hardware trigger (no wear)...
454 if let Some(slot) = self.alloc_hw_slot() {
455 self.dm()
456 .set_hw_breakpoint(slot, addr)
457 .map_err(TargetError::Fatal)?;
458 self.hw_breakpoints.push(HwBp { slot, addr });
459 return Ok(true);
460 }
461 // ...otherwise fall back to a flash software breakpoint (page rewrite) when supported.
462 self.add_flash_breakpoint(addr, len)
463 }
464
465 fn remove_sw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
466 if let Some(pos) = self.breakpoints.iter().position(|b| b.addr == addr) {
467 let bp = self.breakpoints.remove(pos);
468 self.dm()
469 .write_mem(addr, &bp.original)
470 .map_err(TargetError::Fatal)?;
471 return Ok(true);
472 }
473 // It may have been satisfied by a hardware-trigger fallback (flash address).
474 if let Some(pos) = self.hw_breakpoints.iter().position(|b| b.addr == addr) {
475 let bp = self.hw_breakpoints.remove(pos);
476 self.dm()
477 .clear_hw_breakpoint(bp.slot)
478 .map_err(TargetError::Fatal)?;
479 return Ok(true);
480 }
481 // Or by a flash software breakpoint (page rewrite).
482 self.remove_flash_breakpoint(addr)
483 }
484}