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