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; 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}