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-M | RISC-V | Xtensa |
|---|---|---|---|
| 读核心寄存器 | DCRSR / DCRDR 硬件中转 | 抽象命令 | 执行 WSR DDR, aN,再读 DDR;特殊寄存器先执行 RSR 读进 a3 |
| 读内存 | MEM-AP 旁路,核在跑也行 | 系统总线旁路,或程序缓冲区 | 把地址放进 a3,执行 LDDR32.P a3(读一个字并让 a3 自增),之后每读一次 DDREXEC 就再执行一次 |
| 单步 | DHCSR C_STEP | dcsr.step | ICOUNT:设成 −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 芯片。