Rust in Everything
基于 probe-rs 0.32.0

Xtensa 与 ESP 插件

主线位置:访问主线和调试主线在 Xtensa 上的对应物,以及命令主线第一步「注册插件」的另一半。ESP32、ESP32-S2、ESP32-S3 用 Xtensa 核;本页讲 probe-rs 怎么经 JTAG 和 Xtensa 的调试模块打交道,再讲为什么所有 ESP 芯片(包括 RISC-V 核的 ESP32-C 系列)的支持都在一个单独的插件里。
sequenceDiagram
  participant H as probe-rs
  participant X as XDM
  participant C as Xtensa 核
  H->>X: NAR 选 DDR,NDR 写地址
  H->>X: NAR 选 DIR0EXEC,NDR 写 WSR a3 之类的指令
  X->>C: 在调试模式下执行这条指令
  C-->>X: 结果写进 DDR
  H->>X: 读 DDR
  Note over H,C: 读写寄存器、内存都是这样:借核执行指令

XDM:一切都靠执行指令

Xtensa 的调试模块叫 XDM,经 JTAG 访问。xdm.rs 开头列出了要用到的几个 Nexus 寄存器:

// probe-rs/src/architecture/xtensa/xdm.rs
const NARADR_OCDID: u8 = 0x40;
const NARADR_DCRSET: u8 = 0x43;
const NARADR_DCRCLR: u8 = 0x42;
const NARADR_DSR: u8 = 0x44;
const NARADR_DDR: u8 = 0x45;
const NARADR_DDREXEC: u8 = 0x46;
// DIR0 that also executes when written
const NARADR_DIR0EXEC: u8 = 0x47;
// Assume we only support 16-24b instructions for now
const NARADR_DIR0: u8 = 0x48;

访问方式是一对 JTAG 指令:先用 NAR 选寄存器号,再用 NDR 读写 32 位数据,相当于 ARM 的「写 SELECT 再访问」压成一次。最关键的是 DIR0EXEC:往里写一条指令的编码,核就在调试模式下执行它;DDR 是核和调试器之间的数据中转寄存器。

和 ARM、RISC-V 比:

操作ARM Cortex-MRISC-VXtensa
读核心寄存器DCRSR / DCRDR 硬件中转抽象命令执行 WSR DDR, aN,再读 DDR;特殊寄存器先执行 RSR 读进 a3
读内存MEM-AP 旁路,核在跑也行系统总线旁路,或程序缓冲区把地址放进 a3,执行 LDDR32.P a3(读一个字并让 a3 自增),之后每读一次 DDREXEC 就再执行一次
单步DHCSR C_STEPdcsr.stepICOUNT:设成 −2,执行一条后溢出触发调试异常

所以 Xtensa 上几乎每次访问都会改动核里的某个寄存器(通常是 a3)。schedule_read_register_uncached 在借用 a3 之前先 ensure_register_saved 把原值存进 RegisterCache,再把它标成「脏」;resume_core 放行之前一律调 restore_registers 把脏寄存器写回。漏掉一个就会让固件在恢复运行后读到一个被调试器改过的寄存器:0.32 的 CHANGELOG 里正好有一条「Xtensa: Fixed a bug where instruction stepping trashed a CPU register」(#3803)。

内存:快路径和慢路径

LDDR32.P / SDDR32.P 只在部分内存上能用,能不能用、支不支持非对齐访问,由芯片的序列在连接时登记进 MemoryRegionProperties。ESP32-S3 的序列登记了内部 DRAM、DROM、IROM、IRAM 四段为 fast_memory_access,外部存储和外设地址用默认值,也就是全 false。读写时按范围选 FastMemoryAccess(LDDR32.P 流水)或 SlowMemoryAccess(每个字单独执行 load / store)。

读内存同样要停核。fast_halted_access 的做法是乐观的:在同一批里排「读状态、停核、再读状态」,假设已经停住就接着排内存操作,一起执行;事后发现没停住,再走慢的 halted_access 重做一遍。核原本在跑的话,做完再放行。和 RISC-V 的程序缓冲区一样,运行中读内存会让核停一下。

单步:ICOUNT

step 不用专门的单步位,而是借 Xtensa 的指令计数调试功能:

// probe-rs/src/architecture/xtensa/communication_interface.rs
    pub fn step(&mut self, by: u32, intlevel: u32) -> Result<(), XtensaError> {
        // Instructions executed below icountlevel increment the ICOUNT register.
        self.schedule_write_register(ICountLevel(intlevel + 1))?;

        // An exception is generated at the beginning of an instruction that would overflow ICOUNT.
        self.schedule_write_register(ICount(-((1 + by) as i32) as u32))?;

        self.resume_core()?;
        // TODO: instructions like WAITI should be emulated as they are not single steppable.
        // ...

ICOUNT 设为 −(1 + n),执行 n 条指令后再执行下一条时溢出、进调试模式。源码 TODO 里说的 WAITI(等中断)没法单步,目前的办法是 100 ms 超时后强制停核。

为什么 ESP 是一个插件

0.32 之前 ESP 支持写在核心库里;0.32 整体搬进 probe-rs-espressif(#3813)。源码地图已经列过它往四张全局表里各放了什么,这里补上每一样是干什么的:

放进的表东西作用
目标12 个家族的 ChipFamily插件自己的 build.rs 也调 probe_rs_target::process_targets,把 targets/*.yaml 编成 bincode 嵌进插件
厂商Espressif按芯片名选序列:ESP32、S2、S3 返回 DebugSequence::Xtensa,C2 / C3 / C5 / C6 / C61、H2、P4、S31 返回 DebugSequence::Riscv;按 JTAG IDCODE 加 ROM 里 0x4000_1000 处的魔数识别具体型号
探针驱动EspUsbJtagFactory新一点的 ESP 芯片自带 USB-JTAG 桥。它只实现 RawJtagIo(逐位移位),JTAG 事务由 probe-rs 的 polyfill 拼,属于位级探针那一类
镜像格式IdfLoaderFactory(idf / esp-idf / espidf)ESP 芯片从外部 flash 启动,要二级 bootloader、分区表和 IDF 格式的应用镜像;插件依赖 espflash 4.5 生成这些,再交给 FlashLoader

序列里另一件 ESP 特有的事是看门狗:ESP32-S3 的序列在 on_connect 和 on_halt 里都调 disable_wdts,否则核停在断点上时看门狗会把芯片复位。

拆出去的好处是核心库里不再有任何 ESP 专用代码,espflash 这样的依赖也不会进到只调 STM32 的用户那里;代价是用库的人得记得注册。不注册时,内置目标里根本没有 ESP 芯片。本卷的实验程序只依赖 probe-rs,没有注册插件:

== builtin registry: 248 families, 4379 chips, 832 flash algorithms, 125 generated from packs
   esp32c3         -> Err(The requested chip 'esp32c3' was not found in the list of known targets.)

本机的命令行在 main 第一行注册过插件,同一颗芯片就能找到:

$ probe-rs chip info esp32s3
esp32s3
Cores (2):
    - cpu0 (Xtensa)
    - cpu1 (Xtensa)
NVM: 0x00000000..0x04000000 (64.0 MiB)
NVM: 0x3c000000..0x3e000000 (32.0 MiB)
RAM: 0x3fc88000..0x3fcf0000 (416.0 KiB)
...

命令行 chip list 的 260 个家族里,有 12 个来自插件(probe-rs 解决什么问题);库里内置 248 个家族,其中 6 个是 Generic ARMv6-M 这类不带内存图的通用目标。常见坑把「库用户忘了注册插件」作为一个坑再讲。

本机没有 ESP 板子,XDM 的实际访问序列、快慢两种内存路径的速度、看门狗关闭的效果都待实测。

小结

  • Xtensa 的 XDM 用 NAR / NDR 一对 JTAG 指令访问 Nexus 寄存器;读寄存器、读内存都要往 DIR0EXEC 写指令让核执行,结果经 DDR 中转。
  • 被借用的 a3 等寄存器先存进 RegisterCache,放行前一律还原;运行中读内存要先停核,fast_halted_access 乐观地把停核和访问排进同一批。
  • 单步靠 ICOUNT 溢出;WAITI 不能单步,靠超时强停。
  • ESP 的目标、序列(含关看门狗)、USB-JTAG 驱动、IDF 镜像格式全在 probe-rs-espressif 里,一次 register_plugin 放进四张全局表;库用户不调它就找不到任何 ESP 芯片。

💬 关联讨论

发起讨论
加载中...