目标描述:YAML 到注册表
主线位置:命令主线「attach」之前的一步。
--chip STM32F103C8这个字符串要先在注册表里变成一个Target:有几个核、每个核在哪个 AP 上、内存图、用哪个 flash 算法、选哪个调试序列。本页讲这份数据的格式、它怎么在编译期进入二进制、运行时怎么被查到。
flowchart TB yaml["probe-rs/targets/*.yaml,242 个"] -->|"build.rs:process_targets"| bin["OUT_DIR/targets.bincode"] bin -->|"include_bytes!"| lazy["BUILTIN_TARGETS:LazyLock,首次访问时解码"] plugin["插件:add_builtin_target"] --> lazy lazy -->|"Registry::from_builtin_families,克隆"| reg["Registry"] user["--chip-description-path 或 add_target_family_from_yaml"] --> reg reg -->|"get_target_by_name"| target["Target:核、内存图、flash 算法、调试序列"] elf["固件 ELF 的 .probe-rs.chip 段"] -.->|"省掉 --chip"| reg
数据结构:probe-rs-target
probe-rs-target 只有数据,没有任何硬件代码(源码地图)。一个 YAML 文件是一个 ChipFamily:
| 层级 | 字段 | 说明 |
|---|---|---|
ChipFamily | name、manufacturer(JEP106)、generated_from_pack、pack_file_release、chip_detection | 一个家族,比如「STM32F1 Series」 |
variants: Vec<Chip> | 家族里的型号 | |
flash_algorithms: Vec<RawFlashAlgorithm> | 家族共用的 flash 算法,型号按名字引用 | |
Chip | name、package_variants | 型号名和封装名(STM32F103C8 / STM32F103C8Tx),两者都能用来查 |
cores: Vec<Core> | 每个核的名字、类型(armv7m、riscv、riscv64、xtensa 等)、core_access_options | |
memory_map | !Ram、!Nvm、!Generic 三种区,每个区列出能访问它的核 | |
flash_algorithms: Vec<String> | 引用家族里的算法名 | |
rtt_scan_ranges、jtag、default_binary_format、svd | 可选 | |
RawFlashAlgorithm | instructions(base64)、pc_init / pc_erase_sector / pc_program_page 等偏移、data_section_offset、flash_properties(地址范围、页大小、扇区表、超时) | 一段位置无关代码和它的入口表,flash 算法一章讲怎么用 |
core_access_options 按架构分三种(chip.rs:172):ARM 是 ap(ADIv5 的 !v1 0 或 ADIv6 的 !v2 0x...)、可选的 targetsel、debug_base、cti_base、jtag_tap;RISC-V 是 hart_id、jtag_tap、0.32 新加的 mem_ap;Xtensa 只有 jtag_tap。
RP2040 的两个核挂在同一个 AP 上,靠 SWD 多点(multi-drop)的 targetsel 区分:
# probe-rs/targets/RP2040.yaml
variants:
- name: RP2040
cores:
- name: core0
type: armv6m
core_access_options: !Arm
ap: !v1 0
targetsel: 0x1002927
- name: core1
type: armv6m
core_access_options: !Arm
ap: !v1 0
targetsel: 0x11002927
probe-rs chip info 不显示 targetsel,库里读出来是这样(十进制):
RP2040 core0 Armv6m Arm(ArmCoreAccessOptions { ap: V1(0), targetsel: Some(16787751), debug_base: None, cti_base: None, jtag_tap: None })
RP2040 core1 Armv6m Arm(ArmCoreAccessOptions { ap: V1(0), targetsel: Some(285223207), debug_base: None, cti_base: None, jtag_tap: None })
0x1002927 和 0x11002927 只差最高 4 位的实例号。attach 时 select_dp 用它在 DP 线复位后写 TARGETSEL(DP、AP 与 SELECT)。双核切换在真芯片上的表现待实测。
编译期:YAML 变成 bincode
核心库的 build.rs 在开了 builtin-targets 时调 probe_rs_target::process_targets:遍历 targets/ 目录,每个文件用 yaml_serde 解析成 ChipFamily(解析失败直接让构建 panic),整个 Vec<ChipFamily> 用 bincode 编码写到 OUT_DIR/targets.bincode,再立刻解码一次确认能读回。运行时 registry.rs:165 把这个文件 include_bytes! 进来:
// probe-rs/src/config/registry.rs
static BUILTIN_TARGETS: LazyLock<RwLock<Vec<ChipFamily>>> =
LazyLock::new(|| RwLock::new(builtin_targets()));
#[cfg(feature = "builtin-targets")]
fn builtin_targets() -> Vec<ChipFamily> {
const BUILTIN_TARGETS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/targets.bincode"));
bincode::serde::decode_from_slice(BUILTIN_TARGETS, bincode::config::standard())
.expect("Failed to deserialize builtin targets. This is a bug")
.0
}
ESP 插件的 build.rs 做同样的事,生成自己的 targets.bincode,注册插件时再用 add_builtin_target 追加进这张表(Xtensa 与 ESP 插件)。
注意:环境变量 PROBE_RS_TARGETS_DIR 不是运行时的。它只在 build.rs 里读(还登记了 rerun-if-env-changed),作用是在编译 probe-rs 时把另一个目录里的 YAML 也编进 bincode。要在运行时加目标,命令行用 --chip-description-path <yaml>(环境变量 PROBE_RS_CHIP_DESCRIPTION_PATH),库用 Registry::add_target_family_from_yaml。
实验:只依赖 probe-rs 的程序(没有注册 ESP 插件,debug 构建):
== builtin registry: 248 families, 4379 chips, 832 flash algorithms, 125 generated from packs
first from_builtin_families: 57.640875ms (decodes the embedded bincode), second: 4.370042ms (clone)
- 248 个家族 = 242 个 YAML + 6 个代码里写死的通用目标(
add_generic_targets,Generic ARMv6-M等,没有内存图)。125 个家族标了generated_from_pack: true,是 target-gen 从 CMSIS-Pack 生成的(target-gen 与 CMSIS-Pack)。 - 第一次取注册表时解码 bincode,以后每次
from_builtin_families都把整张表克隆一份。源码 TODO 也承认这一点:「we could handle the registry as an overlay on top of the builtin targets, which would avoid cloning the data」。release 构建下的数字未测。
运行时:按名字查目标
get_target_and_family_by_name 遍历所有家族的所有型号和封装名,用 match_name_prefix 比较:不分大小写,模式里的小写 x 匹配任意字符。完全相等算精确匹配;用户给的名字比型号短算部分匹配。有精确匹配就用它;没有时恰好一个部分匹配就用它并打警告,多个就报「不唯一」。
STM32F103C8 -> STM32F103C8 (1 cores, 3 regions, algos ["stm32f10x_128", "stm32f10x_opt"])
stm32f103c8 -> STM32F103C8 (1 cores, 3 regions, algos ["stm32f10x_128", "stm32f10x_opt"])
STM32F103C8Tx -> STM32F103C8Tx (1 cores, 3 regions, algos ["stm32f10x_128", "stm32f10x_opt"])
STM32F103 -> Err(Found multiple chips matching 'STM32F103', unable to select a single chip. (STM32F103C4, STM32F103C4Tx, ...))
nRF52833_xxAA -> nRF52833_xxAA (1 cores, 4 regions, algos ["nrf52"])
nrf52833_qdaa -> nRF52833_xxAA (1 cores, 4 regions, algos ["nrf52"])
RP2350 -> RP235x (2 cores, 2 regions, algos ["algo"])
Cortex-M3 -> Cortex-M3 (1 cores, 0 regions, algos [])
nrf52833_qdaa、RP2350 都是靠 x 通配匹配上的。STM32F103C8 的 YAML 只列了两个内存区,查出来却有 3 个:Target::new 发现 stm32f10x_opt 算法的地址范围(选项字节,0x1FFFF800 起 16 字节)不在任何内存区里,就「HACK (doubly, even)」地合成一个 is_alias 的 NVM 区,好让全片擦除跳过它;chip info 里那行 NVM: 0x1ffff800..0x1ffff810 (16 B) 就是它。同一个函数还决定调试序列:先问每个 Vendor 认不认识这颗芯片(调试序列与 vendor),都不认识就按第一个核的架构用默认序列。
运行时加一个家族(自己写的最小 YAML,只有一个 Cortex-M3 核、一块 RAM 和一块 NVM):
== add_target_family_from_yaml -> Ok("My Board Family")
MYCHIP_F103 -> MYCHIP_F103 source=External algos=0
add_target_family 先校验(算法引用、核名引用等),再按家族名替换同名的旧家族。没有 flash 算法的目标可以 attach、调试、往 RAM 下载,不能烧 flash。
probe-rs-meta:芯片名写进固件
probe-rs-meta 是固件侧的 no_std 小 crate,不在 workspace 成员里。probe_rs_meta::chip!("rp2040") 展开成一个放进 .probe-rs.chip 段的字节数组,链接脚本 probe-rs.x 把这个段标成 INFO(不占目标的内存)。命令行读 ELF 时由 ElfMetadata::from_object 取出 .probe-rs.version、.probe-rs.chip、.probe-rs.timeout 三个段;有芯片名就可以不传 --chip,两者都给而不一致时报错(一条 probe-rs run)。
小结
- 一个 YAML 是一个
ChipFamily:型号、核与访问方式、内存图、按名字引用的 flash 算法;数据结构在probe-rs-target,与硬件无关。 build.rs把 242 个 YAML 编成 bincode 嵌进库,首次访问时解码;PROBE_RS_TARGETS_DIR是编译期变量,运行时加目标用--chip-description-path或add_target_family_from_yaml。- 芯片名不分大小写,小写
x通配,部分匹配唯一时也接受;Target::new会为不在内存图里的算法合成区域,并选出调试序列。 - 每次取注册表都克隆整张表,源码 TODO 里提了叠加层的改法。
probe-rs-meta把芯片名写进固件的INFO段,probe-rs run可以省掉--chip。