Rust in Everything
基于 probe-rs-target 0.32.0

目标描述: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:

层级字段说明
ChipFamilyname、manufacturer(JEP106)、generated_from_pack、pack_file_release、chip_detection一个家族,比如「STM32F1 Series」
variants: Vec<Chip>家族里的型号
flash_algorithms: Vec<RawFlashAlgorithm>家族共用的 flash 算法,型号按名字引用
Chipname、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可选
RawFlashAlgorithminstructions(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。

💬 关联讨论

发起讨论
加载中...