Rust in Everything
基于 probe-rs 0.32.0

RISC-V 调试模块

主线位置:访问主线和调试主线在 RISC-V 上的对应物。ARM 那边是「DP → MEM-AP → 内存映射的调试寄存器」,RISC-V 这边是「DTM → DMI → 调试模块(DM)的寄存器」;本页按这个对照讲 attach、读寄存器、读内存、断点和单步各走哪条路,不展开 RISC-V 调试规范本身。
flowchart TB
  core["RiscvCore:Riscv32 / Riscv64"] --> ci["RiscvCommunicationInterface"]
  ci --> dmreg["DM 寄存器:dmcontrol、dmstatus、abstractcs、command、data0、progbuf、sbcs"]
  dmreg --> dtm["DtmAccess:schedule_read / schedule_write"]
  dtm --> jtagdtm["JtagDtm:IR 0x11 的 DMI 扫描"]
  dtm --> tunnel["TunneledJtagDtm:借 FPGA 等的隧道"]
  dtm --> memap["MemApDtm:DMI 映射进 ARM 的 MEM-AP"]
  jtagdtm --> probe["JtagAccess:CMSIS-DAP、J-Link、FTDI、ESP USB-JTAG"]

和 ARM 的结构对照

ARM ADI(03 部分)RISC-V Debug Spec
线上的入口SW-DP / JTAG-DPDTM,一般是一个 JTAG TAP:IR 0x10 是 dtmcs,IR 0x11 是 dmi
寄存器总线DP 寄存器 + AP 寄存器,靠 SELECT 换 bankDMI:7 位起的地址空间,每个地址一个 32 位 DM 寄存器
读核心寄存器写 DCRSR、等 S_REGRDY、读 DCRDR往 command(0x17)写一条抽象命令「访问寄存器」,结果在 data0(0x04)
读内存MEM-AP 的 TAR / DRW,核在跑也能读系统总线访问(sbcs 0x38 起)核在跑也能读;否则把 lw 指令放进程序缓冲区(progbuf0 0x20 起)让核执行,要先停核
停核 / 放行DHCSR 的 C_HALTdmcontrol(0x10)的 haltreq / resumereq,看 dmstatus(0x11)的 allhalted
单步DHCSR 的 C_STEP核上 CSR dcsr(0x7b0)的 step 位,然后 resume
硬件断点FPB 比较器触发器模块:tselect / tdata1 / tdata2(0x7a0–0x7a2)
多核每个核一个 AP 或 TARGETSEL一个 DM 管多个 hart,dmcontrol.hartsel 选

最大的区别在第三、四行:ARM 的核心寄存器和内存都在「调试器能直接读写的地址」上,RISC-V 的 DM 只给一组「请求」寄存器,很多事要借核自己执行指令完成。

DTM:把 DMI 访问变成 JTAG 扫描

DtmAccess 是 RISC-V 这侧的「探针」接口,地位相当于 ARM 的 DapAccess:schedule_read / schedule_write 排队,execute 一次送出,read_deferred_result 取结果。也是先攒批再执行,CMSIS-DAP 上可以把很多次 DMI 扫描塞进一个 USB 包。

JTAG 实现 JtagDtm 在 init 里读一次 dtmcs,拿到地址位数 abits 和建议的空闲周期数 idle。之后每次 DMI 访问就是对 IR 0x11 做一次 abits + 34 位的 DR 扫描:34 位里 32 位数据、2 位操作码(读、写、无操作),应答的操作码表示成功、失败或「忙」;忙了就把空闲周期加一再重试。JTAG TAP 状态机和扫描链见位级探针。

attach:探测这个 DM 能做什么

enter_debug_mode 相当于 ARM 那边的 select_dp 加 debug_core_start,但多做了一件事:把 DM 支持的能力逐项问出来,存进 RiscvCommunicationInterfaceState。

  1. 写 dmcontrol.dmactive = 1 激活 DM,读 dmstatus.version:2 是 0.13,3 是 1.0,其他版本直接报 UnsupportedDebugModuleVersion。
  2. 往 hartsel 写全 1 再读回来,数出 hartsel 有几位,再从 1 开始逐个选 hart,直到 anynonexistent 置位,得到 hart 个数。1.0 规范的 stickyunavail 在这一步要顺手写 ackunavail。
  3. 1.0 的 DM 还会置 setkeepalive,请核在被调试时别进低功耗。
  4. 读 abstractcs:程序缓冲区有几个字(progbufsize)、有几个数据寄存器(datacount)。
  5. 往 abstractauto 写全 1 再读回,判断支不支持「读写 data 寄存器自动重发上一条命令」(autoexec,批量读内存用)。
  6. 读 sbcs:sbversion 为 1 时,按 sbaccess8/16/32/64/128 把对应宽度的默认内存访问方式设成系统总线。

第 1 步之后读 confstrptr0..3 拼 128 位配置串指针的那几行,移位量是 8、16、32,而不是 32、64、96(communication_interface.rs:639)。这个值存下来之后在 0.32.0 里没有别处读,所以不影响行为。

寄存器:抽象命令

读写 GPR 和 CSR 都走 execute_abstract_command:先确认 dmcontrol 里没有挂着 halt / resume 请求,写 abstractcs.cmderr = 7 清掉旧错误,写 command,然后轮询 abstractcs.busy,最后按 cmderr 翻译错误。cmderr = 4(HaltResume)说明这条命令要求核处于停止状态:代码会重新查一次核的状态,核在跑就停下来重试一遍,核已经停着才把错误交上去。

有的 DM 不支持用抽象命令读某些 CSR(比如 FU740 的 DM 没有抽象 CSR 访问)。这时 probe-rs 退一步:把 csrr / csrw 指令写进程序缓冲区让核执行,用 s0 中转,执行前后用抽象命令保存、恢复 s0。哪些寄存器号不支持抽象命令,第一次失败后记在 CoreRegisterAbstractCmdSupport 里,下次直接走程序缓冲区。

内存:程序缓冲区还是系统总线

每次读写内存先查 MemoryAccessConfig:按访问宽度有一个默认方式,还可以按地址范围覆盖(调试序列用它处理特殊区域)。没设默认值时的回退规则在 default_method:

// probe-rs/src/architecture/riscv/communication_interface.rs
    pub fn default_method(&self, access: RiscvBusAccess) -> MemoryAccessMethod {
        self.default_method
            .get(&access)
            .copied()
            .unwrap_or(if self.has_program_buffer {
                MemoryAccessMethod::ProgramBuffer
            } else {
                MemoryAccessMethod::AbstractCommand
            })
    }

read_word 按查到的方式分派:

方式怎么做要停核吗
SystemBus写 sbcs(宽度、sbreadonaddr)、写 sbaddress0(RV64 还有 sbaddress1),读 sbdata0,再查 sbcs.sberror / sbbusyerror,忙就整个重来(perform_memory_read_sysbus)不要,和 ARM 的 MEM-AP 一样旁路访问
ProgramBuffer保存 s0,把一条 lw(或 lb / lh)写进 progbuf0,用抽象命令把地址写进 s0 并顺带执行程序缓冲区,再用抽象命令读回寄存器、恢复 s0(perform_memory_read_progbuf);批量读时用 autoexec 流水要,包在 halted_access 里:核在跑就先停、做完再放行
WaitingProgramBuffer同上,每次执行后显式等 abstractcs.busy 清零要
AbstractCommand规范里的「抽象命令访问内存」0.32.0 没实现,返回 NotImplemented("Memory access using abstract commands is not implemented")

所以一颗既没有程序缓冲区、也没有系统总线访问的 RISC-V 芯片,在 0.32.0 上读不了内存。按地址范围选方式时,range_method 在默认方式和所有重叠的覆盖之间取枚举顺序最靠前的一个,也就是最保守的那个。

和 ARM 比,代价在「要停核」这一列:RTT 轮询、DAP 服务器在核运行时读变量,如果走程序缓冲区,每次都会让核停一下再放行。哪些芯片走哪种方式、停核对实时性的影响,待实测。

停核、单步、断点

halt 有一条快路径:在同一批里写 haltreq、读 dmstatus、清 haltreq,只有读回来的 allhalted 为假才退回慢路径轮询。停下之后确保 dcsr 的 ebreakm 等位已打开,这样固件里的 ebreak 会进调试模式而不是触发普通异常(0.32 修过「所有核都已停时没有重新打开」的问题,#4095)。

step 和 ARM 的思路相同、手段不同:

  • 停在软件断点(ebreak / c.ebreak)或 semihosting 上时不真的单步,直接把 dpc(CSR 0x7b1)加 2 或 4 跳过这条指令,就像 ARM 停在 BKPT 上手动把 PC 加 2(Cortex-M 内核)。
  • 停在硬件断点上时先关断点。
  • 置 dcsr.step、清 stepie(单步时不响应中断,相当于 ARM 的 C_MASKINTS)、置 stopcount,resume,等停下;超时就强制 halt。最后无论成败都把 dcsr 改回去。

硬件断点用触发器模块。determine_number_of_hardware_breakpoints 从 tselect = 0 往上试:写不进去、读回不等、tinfo 说「不存在」或 tdata1 的类型为 0 就停止计数。set_hw_breakpoint 只接受类型 2(0.13 的 mcontrol)和 6(1.0 的 mcontrol6),两者低 16 位兼容,于是用同一个 Mcontrol 值:action = 1(进调试模式)、execute、M 和 U 模式都匹配,写 tdata2 = 地址。

semihosting 的识别规则也不同:ARM 看 PC 处是不是 BKPT 0xAB,RISC-V 要看 PC−4 起的三条指令是不是 slli x0,x0,0x1f; ebreak; srai x0,x0,7(check_for_semihosting)。之后的解码两种架构共用,见 defmt 与 semihosting。

0.32:Debug Spec 1.0 与 RV64

  • 1.0 规范(#4039):DebugModuleVersion::Version1_0、stickyunavail / ackunavail、setkeepalive、mcontrol6 触发器,都在上面几段里出现过。
  • RV64(#3910):RiscvCore<'state, X: XlenMode> 用一个类型参数区分宽度,Riscv32 和 Riscv64 是两个别名(mod.rs:277)。XlenMode 是一个 sealed trait,两个实现提供寄存器表(registers64.rs 由 32 位寄存器表加宽得到,寄存器号不变)、tdata1 的打包方式和断点地址校验;系统总线访问在 RV64 上多写一个 sbaddress1。目标描述里写 type: riscv64,0.32.0 的内置目标里只有 SiFive_FU740.yaml 和 Nuclei_EvalSoC.yaml 用到。

本机 probe-rs chip info 看几颗 RISC-V 芯片:

$ probe-rs chip info FU740-C000
FU740-C000
Cores (1):
    - e51 (Riscv64)
RAM: 0x01000000..0x01002000 (8.0 KiB)
RAM: 0x08000000..0x08200000 (2.0 MiB)
NVM: 0x20000000..0x22000000 (32.0 MiB)
$ probe-rs chip info RP235x_riscv
RP235x_riscv
Cores (2):
    - rv0 (Riscv)
    - rv1 (Riscv)
NVM: 0x10000000..0x14000000 (64.0 MiB)
RAM: 0x20000000..0x20082000 (520.0 KiB)

RP2350:DMI 挂在 MEM-AP 后面

RP2350 有两个 Cortex-M33 和两个 Hazard3 RISC-V 核,调试端口是 ARM 的 SW-DP。它的 RISC-V 调试模块不在 JTAG TAP 上,而是作为一段地址挂在一个 ADIv6 的 MEM-AP 后面。目标描述这样写:

# probe-rs/targets/RP235x.yaml
- name: RP235x_riscv
  cores:
  - name: rv0
    type: riscv
    core_access_options: !Riscv
      hart_id: 0
      mem_ap: !v2 0xa000

0.32 新加的 MemApDtm 只有 162 行:它拿着一个 ARM 的 ArmMemoryInterface,把第 n 号 DMI 寄存器的读写换成对 MEM-AP 地址 n * 4 的 read_word_32 / write_word_32。Session 在建 ARM 接口时检查目标里有没有带 mem_ap 的 RISC-V 核,有就构造 ArchitectureInterface::ArmWithRiscv(session.rs:382),之后 session.core(i) 对 RISC-V 核走这条路。于是同一根 SWD 线、同一套 DP、AP 与 SELECT 代码,下面接的是 RISC-V 的调试模块。这条路径在真芯片上的表现待实测。

小结

  • RISC-V 的调试器只能「请求」:经 DTM 读写 DM 寄存器,寄存器靠抽象命令,内存靠系统总线或让核执行程序缓冲区里的指令。
  • attach 时把 DM 的版本、hart 数、程序缓冲区大小、autoexec、系统总线宽度逐项问出来,之后按能力选访问方式;抽象命令访问内存在 0.32.0 没实现。
  • 程序缓冲区访问要先停核,这是和 ARM MEM-AP 最大的不同。
  • 单步用 dcsr.step,停在 ebreak 上时手动推进 dpc;断点用触发器,兼容 0.13 和 1.0 两种类型。
  • 0.32 加了 Debug Spec 1.0、RV64(XlenMode 类型参数),以及让 RP2350 的 RISC-V 核经 ARM MEM-AP 调试的 MemApDtm。

💬 关联讨论

发起讨论
加载中...