Skip to main content

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}