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