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::ext::target_description_xml_override::{
31 TargetDescriptionXmlOverride, TargetDescriptionXmlOverrideOps,
32};
33use gdbstub::target::{Target, TargetError, TargetResult};
34
35use crate::arch::{Rv32, Rv32CoreRegs};
36
37/// A software breakpoint: the address and the original bytes we overwrote with `ebreak`.
38struct SwBp {
39 addr: u32,
40 original: Vec<u8>,
41}
42
43/// A hardware breakpoint: the trigger slot it occupies and the address it watches.
44struct HwBp {
45 slot: u32,
46 addr: u32,
47}
48
49/// A flash software breakpoint: an `ebreak` patched into flash-resident code by rewriting its
50/// page. `len` is the patched instruction size (2 for RVC, 4 otherwise).
51struct FlashBp {
52 addr: u32,
53 len: usize,
54}
55
56/// A flash page we manage: its pristine content (before any breakpoint) and what we last
57/// programmed into it, so repeated set/clear that yields identical content skips the rewrite.
58struct FlashPage {
59 page_addr: u32,
60 pristine: Vec<u8>,
61 current: Vec<u8>,
62}
63
64/// en: gdbstub target that OWNS its transport `T` (owning, not borrowing, keeps the type free
65/// of a lifetime so it fits `BlockingEventLoop::Target`). Recover the transport with
66/// [`Ch32Target::into_inner`] to detach cleanly afterwards.
67/// ja: transport `T` を所有する gdbstub target(所有にすることでライフタイムが付かず
68/// `BlockingEventLoop::Target` に収まる)。後始末は [`Ch32Target::into_inner`] で回収する。
69pub struct Ch32Target<T: DtmAccess> {
70 dtm: T,
71 breakpoints: Vec<SwBp>,
72 hw_breakpoints: Vec<HwBp>,
73 hw_trigger_count: u32,
74 /// Number of integer GPRs the core exposes: 16 on RV32E (misa.E, e.g. CH32V003), else 32.
75 /// Reading a non-existent GPR via an abstract command raises cmderr, so we must not touch
76 /// x16..x31 on an RV32E hart.
77 gpr_count: u8,
78 /// Fast-page size for flash software breakpoints, or None when this family's FLASH-controller
79 /// profile is not verified (so a flash breakpoint is refused rather than risked).
80 flash_page_size: Option<u32>,
81 /// The family's fast-program mechanism (meaningful only when `flash_page_size` is Some).
82 flash_prog_mode: FlashProgMode,
83 flash_bps: Vec<FlashBp>,
84 flash_pages: Vec<FlashPage>,
85 /// en: DATA0 / DATA1 as the hart left them when it stopped. Register and memory access go
86 /// through DATA0, which is also a DM console's mailbox (dmdata / dmseq): a value left there
87 /// with bit 7 set reads to a dmseq target as its own frame still posted and to the host as an
88 /// invalid word, and the console stops for good (measured on a CH32X035 behind an OEP probe).
89 /// Put back just before the hart runs again.
90 /// ja: hart が止まった時の DATA0 / DATA1。register と memory は DATA0 を通すが、DATA0 は DM の
91 /// console の mailbox でもあり、bit 7 の立った値が残ると dmseq の target は自分のフレームがまだ
92 /// あると読み、host は無効な語と読んで、console が止まったままになる(OEP の probe の X035 で実測)。
93 /// 走らせる直前に戻す。
94 mailbox: Option<(u32, u32)>,
95}
96
97impl<T: DtmAccess> Ch32Target<T> {
98 /// en: Find a free hardware trigger slot (none used twice). Returns None when the core has
99 /// no trigger module or all slots are taken.
100 /// ja: 空いている HW trigger slot を探す。trigger 無し/全 slot 使用中なら None。
101 fn alloc_hw_slot(&self) -> Option<u32> {
102 (0..self.hw_trigger_count).find(|s| !self.hw_breakpoints.iter().any(|b| b.slot == *s))
103 }
104
105 /// en: Wrap a transport and halt the core so GDB attaches to a stopped target. `flash` is the
106 /// FLASH-controller profile for this family (fast-page size + program mode, from
107 /// `ch32rv_flash::flash_controller_profile`), or None to refuse flash software breakpoints.
108 /// ja: transport を包んで halt。`flash` はこの family の FLASH-controller profile(fast page
109 /// サイズ + program mode。None なら flash SW breakpoint を拒否)。
110 pub fn new(dtm: T, flash: Option<(u32, FlashProgMode)>) -> Result<Self, DmiError> {
111 let mut t = Self {
112 dtm,
113 breakpoints: Vec::new(),
114 hw_breakpoints: Vec::new(),
115 hw_trigger_count: 0,
116 gpr_count: 32,
117 flash_page_size: flash.map(|(p, _)| p),
118 flash_prog_mode: flash.map(|(_, m)| m).unwrap_or(FlashProgMode::PgStart),
119 flash_bps: Vec::new(),
120 flash_pages: Vec::new(),
121 mailbox: None,
122 };
123 t.dm().halt()?;
124 t.save_mailbox();
125 // Make `ebreak` halt into debug mode so software breakpoints stop the core.
126 let _ = t.dm().enable_ebreak_debug();
127 t.hw_trigger_count = t.dm().hw_trigger_count();
128 // misa.E (bit 4) marks RV32E: only x0..x15 exist. Reading x16.. would raise cmderr.
129 const MISA: u16 = 0x301;
130 const MISA_E: u32 = 1 << 4;
131 if let Ok(misa) = t.dm().read_reg(RegName::Csr(MISA))
132 && misa & MISA_E != 0
133 {
134 t.gpr_count = 16;
135 }
136 Ok(t)
137 }
138
139 /// Number of hardware trigger slots the core exposes (0 on V003/V2A).
140 pub fn hw_trigger_count(&self) -> u32 {
141 self.hw_trigger_count
142 }
143
144 fn dm(&mut self) -> DebugModule<'_, T> {
145 DebugModule::new(&mut self.dtm)
146 }
147
148 /// True if the core is halted right now.
149 pub fn is_halted(&mut self) -> Result<bool, DmiError> {
150 let halted = self.dm().is_halted()?;
151 if halted {
152 self.save_mailbox();
153 }
154 Ok(halted)
155 }
156
157 /// Request a halt (used for Ctrl-C).
158 pub fn halt(&mut self) -> Result<(), DmiError> {
159 self.dm().halt()?;
160 self.save_mailbox();
161 Ok(())
162 }
163
164 /// en: Keep DATA0 / DATA1 the first time the hart is seen stopped (before any register
165 /// access overwrites them). Best-effort: a failed read keeps nothing to put back.
166 /// ja: 止まった hart を最初に見た時(register に触れる前)に DATA0 / DATA1 を覚える。
167 fn save_mailbox(&mut self) {
168 const DATA0: u8 = 0x04;
169 const DATA1: u8 = 0x05;
170 if self.mailbox.is_none()
171 && let (Ok(d0), Ok(d1)) = (self.dtm.dmi_read(DATA0), self.dtm.dmi_read(DATA1))
172 {
173 self.mailbox = Some((d0, d1));
174 }
175 }
176
177 /// Resume through the probe's run control when it has one ([`DtmAccess::resume_hart`]).
178 fn resume_hart(&mut self) -> Result<(), DmiError> {
179 match self.dtm.resume_hart() {
180 Some(r) => r,
181 None => self.dm().resume(),
182 }
183 }
184
185 /// en: Resume the hart if it is halted, with the console mailbox put back first: the end of a
186 /// session whose transport does not resume on detach.
187 /// ja: 止まっていれば(mailbox を戻してから)走らせる。detach で走らない transport の session の最後。
188 pub fn resume_if_halted(&mut self) -> Result<(), DmiError> {
189 if self.dm().is_halted()? {
190 self.restore_mailbox();
191 self.resume_hart()?;
192 }
193 Ok(())
194 }
195
196 /// Put DATA0 / DATA1 back as the hart left them (right before it runs again).
197 fn restore_mailbox(&mut self) {
198 if let Some((d0, d1)) = self.mailbox.take() {
199 let _ = self.dtm.dmi_write(0x05, d1);
200 let _ = self.dtm.dmi_write(0x04, d0);
201 }
202 }
203
204 /// en: Recover the owned transport (to detach after the session). The console mailbox is put
205 /// back first, since the caller's detach resumes the hart.
206 /// ja: transport を返す(後で detach するため)。detach で hart が走るので、先に mailbox を戻す。
207 pub fn into_inner(mut self) -> T {
208 self.restore_mailbox();
209 self.dtm
210 }
211
212 /// True if this core supports flash software breakpoints (verified FLASH-controller profile).
213 pub fn flash_breakpoints_supported(&self) -> bool {
214 self.flash_page_size.is_some()
215 }
216
217 /// en: Restore every managed flash page to its pristine content (removing any `ebreak` still
218 /// patched in). Call before detaching so an interrupted session never leaves a breakpoint
219 /// baked into flash. The session can end with the hart running (GDB gone mid-`continue`, a
220 /// fatal error), and the page rewrite needs it halted, so it is halted for the rewrite and
221 /// resumed after. Returns the pages it could not restore (they may still hold an `ebreak`).
222 /// ja: 管理中の flash page をすべて pristine に戻す(残った `ebreak` を消す)。detach 前に呼ぶ。
223 /// session は hart が走ったまま終わることがあり(continue 中に GDB が消えた、致命的な error)、
224 /// page の書き直しには止まった hart が要るので、止めて書き直し、走らせ直す。戻せなかった page を返す
225 /// (`ebreak` が残っているかもしれない)。
226 pub fn restore_flash_breakpoints(&mut self) -> Vec<u32> {
227 let Some(page) = self.flash_page_size else {
228 return Vec::new();
229 };
230 self.flash_bps.clear();
231 if self.flash_pages.is_empty() {
232 return Vec::new();
233 }
234 let running = !self.dm().is_halted().unwrap_or(true);
235 if running && self.halt().is_err() {
236 let left = self.flash_pages.iter().map(|p| p.page_addr).collect();
237 self.flash_pages.clear();
238 return left;
239 }
240 let addrs: Vec<u32> = self.flash_pages.iter().map(|p| p.page_addr).collect();
241 let mut left = Vec::new();
242 for page_addr in addrs {
243 if self.reprogram_flash_page(page, page_addr).is_err() {
244 left.push(page_addr);
245 }
246 }
247 self.flash_pages.clear();
248 if running {
249 self.restore_mailbox();
250 let _ = self.resume_hart();
251 }
252 left
253 }
254
255 /// en: Map a code address into the physical code-flash window. Programs run from the low
256 /// alias (0x0000_0000 mirrors flash), but the FLASH controller must be given the real flash
257 /// address (0x0800_0000+off). Reads work through either mirror; writes must use the physical.
258 /// ja: コード番地を物理 code-flash 窓へ写す。実行は低位 alias(0x0000_0000=flash の鏡)だが、
259 /// FLASH controller には実 flash 番地(0x0800_0000+off)を渡す必要がある。
260 fn flash_phys(addr: u32) -> u32 {
261 const FLASH_BASE: u32 = 0x0800_0000;
262 if addr < FLASH_BASE {
263 FLASH_BASE + addr
264 } else {
265 addr
266 }
267 }
268
269 /// The `ebreak` patch bytes for an instruction of size `len` (2 = RVC c.ebreak, else ebreak).
270 fn ebreak_patch(len: usize) -> &'static [u8] {
271 if len == 2 {
272 &[0x02, 0x90] // c.ebreak (0x9002, little-endian)
273 } else {
274 &[0x73, 0x00, 0x10, 0x00] // ebreak (0x00100073)
275 }
276 }
277
278 /// en: Rewrite `page_addr` to hold its pristine content plus every currently-active flash
279 /// breakpoint in that page. Skips the erase/program when the page already matches (so a
280 /// redundant set/clear round-trip costs no flash wear). Returns Ok(true) if it wrote.
281 /// ja: `page_addr` を「pristine + その page の全 flash breakpoint」の内容に書き直す。既に一致
282 /// なら erase/program を省く(無駄な書き換え=摩耗を避ける)。書いたら Ok(true)。
283 fn reprogram_flash_page(&mut self, page: u32, page_addr: u32) -> Result<bool, DmiError> {
284 let Some(idx) = self
285 .flash_pages
286 .iter()
287 .position(|p| p.page_addr == page_addr)
288 else {
289 return Ok(false);
290 };
291 let mut desired = self.flash_pages[idx].pristine.clone();
292 for bp in &self.flash_bps {
293 if bp.addr & !(page - 1) == page_addr {
294 let off = (bp.addr - page_addr) as usize;
295 let patch = Self::ebreak_patch(bp.len);
296 if off + bp.len <= desired.len() {
297 desired[off..off + bp.len].copy_from_slice(patch);
298 }
299 }
300 }
301 if self.flash_pages[idx].current == desired {
302 return Ok(false); // no net change: skip the flash write
303 }
304 let phys = Self::flash_phys(page_addr);
305 let mode = self.flash_prog_mode;
306 {
307 let mut dm = self.dm();
308 dm.flash_page_erase(phys, mode)?;
309 dm.flash_program_page(phys, &desired, mode)?;
310 }
311 self.flash_pages[idx].current = desired;
312 Ok(true)
313 }
314
315 /// en: Add a flash software breakpoint by rewriting the containing page. Returns Ok(false)
316 /// if this family has no verified flash profile. The hart must be halted.
317 /// ja: 該当 page を書き換えて flash SW breakpoint を張る。未対応 family は Ok(false)。
318 fn add_flash_breakpoint(&mut self, addr: u32, len: usize) -> TargetResult<bool, Self> {
319 let Some(page) = self.flash_page_size else {
320 return Ok(false);
321 };
322 let page_addr = addr & !(page - 1);
323 // Load the pristine page the first time we touch it (it has no breakpoint yet).
324 if !self.flash_pages.iter().any(|p| p.page_addr == page_addr) {
325 let content = self
326 .dm()
327 .read_mem(page_addr, page)
328 .map_err(TargetError::Fatal)?;
329 self.flash_pages.push(FlashPage {
330 page_addr,
331 pristine: content.clone(),
332 current: content,
333 });
334 }
335 self.flash_bps.push(FlashBp { addr, len });
336 self.reprogram_flash_page(page, page_addr)
337 .map_err(TargetError::Fatal)?;
338 // Verify the ebreak actually landed (flash programming can silently fail on protect).
339 let back = self
340 .dm()
341 .read_mem(addr, len as u32)
342 .map_err(TargetError::Fatal)?;
343 if back != Self::ebreak_patch(len) {
344 // Roll back: drop this bp and restore the page.
345 self.flash_bps.pop();
346 let _ = self.reprogram_flash_page(page, page_addr);
347 return Ok(false);
348 }
349 Ok(true)
350 }
351
352 /// en: Remove a flash software breakpoint, restoring the page (dropping the managed page once
353 /// it holds no more breakpoints). Returns Ok(false) if `addr` was not a flash breakpoint.
354 /// ja: flash SW breakpoint を外して page を復元(その page の breakpoint が無くなれば管理解除)。
355 fn remove_flash_breakpoint(&mut self, addr: u32) -> TargetResult<bool, Self> {
356 let Some(page) = self.flash_page_size else {
357 return Ok(false);
358 };
359 let Some(pos) = self.flash_bps.iter().position(|b| b.addr == addr) else {
360 return Ok(false);
361 };
362 let page_addr = addr & !(page - 1);
363 self.flash_bps.remove(pos);
364 self.reprogram_flash_page(page, page_addr)
365 .map_err(TargetError::Fatal)?;
366 // If nothing else lives in this page, stop managing it (it is now pristine again).
367 if !self
368 .flash_bps
369 .iter()
370 .any(|b| b.addr & !(page - 1) == page_addr)
371 {
372 self.flash_pages.retain(|p| p.page_addr != page_addr);
373 }
374 Ok(true)
375 }
376}
377
378impl<T: DtmAccess> Target for Ch32Target<T> {
379 type Arch = Rv32;
380 type Error = DmiError;
381
382 #[inline(always)]
383 fn base_ops(&mut self) -> gdbstub::target::ext::base::BaseOps<'_, Self::Arch, Self::Error> {
384 gdbstub::target::ext::base::BaseOps::SingleThread(self)
385 }
386
387 #[inline(always)]
388 fn support_breakpoints(&mut self) -> Option<BreakpointsOps<'_, Self>> {
389 Some(self)
390 }
391
392 fn support_target_description_xml_override(
393 &mut self,
394 ) -> Option<TargetDescriptionXmlOverrideOps<'_, Self>> {
395 // Only an RV32E hart differs from the Arch's default (x0..x31 and pc).
396 (self.gpr_count == 16).then_some(self)
397 }
398}
399
400impl<T: DtmAccess> TargetDescriptionXmlOverride for Ch32Target<T> {
401 fn target_description_xml(
402 &self,
403 _annex: &[u8],
404 offset: u64,
405 length: usize,
406 buf: &mut [u8],
407 ) -> TargetResult<usize, Self> {
408 let xml = crate::arch::RV32E_TARGET_XML.as_bytes();
409 let start = usize::try_from(offset).unwrap_or(usize::MAX).min(xml.len());
410 let n = (xml.len() - start).min(length).min(buf.len());
411 buf[..n].copy_from_slice(&xml[start..start + n]);
412 Ok(n)
413 }
414}
415
416impl<T: DtmAccess> SingleThreadBase for Ch32Target<T> {
417 fn read_registers(&mut self, regs: &mut Rv32CoreRegs) -> TargetResult<(), Self> {
418 let gpr_count = self.gpr_count;
419 let mut dm = self.dm();
420 regs.x = [0; 32];
421 regs.rv32e = gpr_count == 16;
422 // On RV32E only x0..x15 exist; leave x16..x31 as zero (touching them raises cmderr).
423 for i in 1..gpr_count {
424 regs.x[i as usize] = dm.read_reg(RegName::Gpr(i)).map_err(TargetError::Fatal)?;
425 }
426 regs.pc = dm.read_reg(RegName::Pc).map_err(TargetError::Fatal)?;
427 Ok(())
428 }
429
430 fn write_registers(&mut self, regs: &Rv32CoreRegs) -> TargetResult<(), Self> {
431 let gpr_count = self.gpr_count;
432 let mut dm = self.dm();
433 for i in 1..gpr_count {
434 dm.write_reg(RegName::Gpr(i), regs.x[i as usize])
435 .map_err(TargetError::Fatal)?;
436 }
437 dm.write_reg(RegName::Pc, regs.pc)
438 .map_err(TargetError::Fatal)?;
439 Ok(())
440 }
441
442 fn read_addrs(&mut self, start: u32, data: &mut [u8]) -> TargetResult<usize, Self> {
443 let bytes = self
444 .dm()
445 .read_mem(start, data.len() as u32)
446 .map_err(TargetError::Fatal)?;
447 let n = bytes.len().min(data.len());
448 data[..n].copy_from_slice(&bytes[..n]);
449 Ok(n)
450 }
451
452 fn write_addrs(&mut self, start: u32, data: &[u8]) -> TargetResult<(), Self> {
453 self.dm()
454 .write_mem(start, data)
455 .map_err(TargetError::Fatal)?;
456 Ok(())
457 }
458
459 #[inline(always)]
460 fn support_resume(&mut self) -> Option<SingleThreadResumeOps<'_, Self>> {
461 Some(self)
462 }
463}
464
465impl<T: DtmAccess> SingleThreadResume for Ch32Target<T> {
466 fn resume(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
467 self.restore_mailbox();
468 self.resume_hart()
469 }
470
471 #[inline(always)]
472 fn support_single_step(&mut self) -> Option<SingleThreadSingleStepOps<'_, Self>> {
473 Some(self)
474 }
475}
476
477impl<T: DtmAccess> SingleThreadSingleStep for Ch32Target<T> {
478 fn step(&mut self, _signal: Option<Signal>) -> Result<(), Self::Error> {
479 self.dm().step()
480 }
481}
482
483impl<T: DtmAccess> Breakpoints for Ch32Target<T> {
484 #[inline(always)]
485 fn support_sw_breakpoint(&mut self) -> Option<SwBreakpointOps<'_, Self>> {
486 Some(self)
487 }
488
489 #[inline(always)]
490 fn support_hw_breakpoint(&mut self) -> Option<HwBreakpointOps<'_, Self>> {
491 // Only advertise HW breakpoints when the core actually has trigger slots.
492 if self.hw_trigger_count > 0 {
493 Some(self)
494 } else {
495 None
496 }
497 }
498}
499
500impl<T: DtmAccess> HwBreakpoint for Ch32Target<T> {
501 fn add_hw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
502 let Some(slot) = self.alloc_hw_slot() else {
503 return Ok(false); // out of trigger slots
504 };
505 self.dm()
506 .set_hw_breakpoint(slot, addr)
507 .map_err(TargetError::Fatal)?;
508 self.hw_breakpoints.push(HwBp { slot, addr });
509 Ok(true)
510 }
511
512 fn remove_hw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
513 if let Some(pos) = self.hw_breakpoints.iter().position(|b| b.addr == addr) {
514 let bp = self.hw_breakpoints.remove(pos);
515 self.dm()
516 .clear_hw_breakpoint(bp.slot)
517 .map_err(TargetError::Fatal)?;
518 Ok(true)
519 } else {
520 Ok(false)
521 }
522 }
523}
524
525impl<T: DtmAccess> SwBreakpoint for Ch32Target<T> {
526 /// en: Set a breakpoint GDB asked for as "software" (Z0). We first try a RAM memory-patch
527 /// (`ebreak`); if the write does not stick - the classic case being a flash address, where
528 /// the DM `sw` silently no-ops - we transparently fall back to a hardware execute trigger
529 /// when the core has a free slot. That makes plain `break` work on flash-resident code for
530 /// V4-class cores (which expose triggers) without the risk of rewriting flash. Cores with no
531 /// trigger module (V003/V103) can still set RAM breakpoints; a flash breakpoint there returns
532 /// unsupported (a flash-patch approach is a follow-up).
533 /// ja: GDB が Z0(software)で要求した breakpoint。まず RAM の memory patch(`ebreak`)を
534 /// 試し、書き込みが定着しない(典型は flash 番地。DM の `sw` が無反応)場合は、空き HW slot が
535 /// あれば HW execute trigger へ透過的にフォールバックし(flash 書き換えの危険なし)、trigger が
536 /// 無ければ page 書き換えの flash software breakpoint へフォールバックする。これで trigger を
537 /// 持たない core(V203 等)でも flash 上のコードに通常 `break` が効く(flash 書き換えの摩耗あり)。
538 /// 順序は RAM → HW trigger → flash-patch。どれも不可なら未対応を返す。
539 fn add_sw_breakpoint(&mut self, addr: u32, kind: usize) -> TargetResult<bool, Self> {
540 // kind is the instruction size GDB expects (2 for RVC, 4 otherwise).
541 let (patch, len): (&[u8], usize) = if kind == 2 {
542 (&[0x02, 0x90], 2) // c.ebreak (0x9002, little-endian)
543 } else {
544 (&[0x73, 0x00, 0x10, 0x00], 4) // ebreak (0x00100073)
545 };
546 // Try a RAM memory-patch first. Scope the DM borrow so we can touch other fields after.
547 let (stuck, original) = {
548 let mut dm = self.dm();
549 let original = dm.read_mem(addr, len as u32).map_err(TargetError::Fatal)?;
550 dm.write_mem(addr, patch).map_err(TargetError::Fatal)?;
551 // The patch lands only in writable memory; a flash `sw` silently no-ops.
552 let back = dm.read_mem(addr, len as u32).map_err(TargetError::Fatal)?;
553 let stuck = back == patch;
554 if !stuck {
555 let _ = dm.write_mem(addr, &original); // restore best-effort
556 }
557 (stuck, original)
558 };
559 if stuck {
560 self.breakpoints.push(SwBp { addr, original });
561 return Ok(true);
562 }
563 // Flash (or otherwise unwritable): prefer a free hardware trigger (no wear)...
564 if let Some(slot) = self.alloc_hw_slot() {
565 self.dm()
566 .set_hw_breakpoint(slot, addr)
567 .map_err(TargetError::Fatal)?;
568 self.hw_breakpoints.push(HwBp { slot, addr });
569 return Ok(true);
570 }
571 // ...otherwise fall back to a flash software breakpoint (page rewrite) when supported.
572 self.add_flash_breakpoint(addr, len)
573 }
574
575 fn remove_sw_breakpoint(&mut self, addr: u32, _kind: usize) -> TargetResult<bool, Self> {
576 if let Some(pos) = self.breakpoints.iter().position(|b| b.addr == addr) {
577 let bp = self.breakpoints.remove(pos);
578 self.dm()
579 .write_mem(addr, &bp.original)
580 .map_err(TargetError::Fatal)?;
581 return Ok(true);
582 }
583 // It may have been satisfied by a hardware-trigger fallback (flash address).
584 if let Some(pos) = self.hw_breakpoints.iter().position(|b| b.addr == addr) {
585 let bp = self.hw_breakpoints.remove(pos);
586 self.dm()
587 .clear_hw_breakpoint(bp.slot)
588 .map_err(TargetError::Fatal)?;
589 return Ok(true);
590 }
591 // Or by a flash software breakpoint (page rewrite).
592 self.remove_flash_breakpoint(addr)
593 }
594}