Rust in Everything
基于 probe-rs 0.32.0

从镜像到烧录计划

主线位置:命令主线的第四步「烧录」的前半段。cli::flash 先发 flash/build,服务端读镜像、建 FlashLoader;再发 flash/flash,服务端 commit。本页讲 commit 动手擦写之前的部分:数据从哪来、怎么按 flash 的扇区和页切好;擦写本身在下一页。

flowchart TB
  file["fw.elf、bin、hex、uf2、idf"] --> fmt["image_format:LOADERS 表"]
  fmt --> loader["ImageLoader::load:ELF 取 PT_LOAD 段的 p_paddr"]
  loader --> add["FlashLoader::add_data:检查内存图"]
  add --> builder["FlashBuilder:BTreeMap,地址到字节"]
  builder --> plan["commit:prepare_plan"]
  plan --> group["按 NVM 区找 flash 算法,同一算法合成一个 Flasher"]
  group --> layout["build_sectors_and_pages:扇区、页、填充"]
  layout --> dry{"dry_run"}
  dry -->|是| ok["返回 Ok"]
  dry -->|否| init["initialize:发 FlashLayoutReady"]
  init --> prog["擦除、编程、校验:下一页"]

入口与选项

库的入口是 download.rs 里的三个函数:build_loader 打开文件、建一个空的 FlashLoader(内存图来自目标描述)、调 load_image;download_file_with_options 等于 build_loader 再 commit。命令行把这两步拆成两个 RPC 调用,中间可以检查数据(一条 probe-rs run)。

DownloadOptions 的字段基本一一对应命令行参数:

字段含义
keep_unwritten_bytes扇区只能整块擦,没写到的字节会丢;打开后先把它们读出来,编程时一起写回
dry_run「prepares everything for flashing, but does not write anything」,下文看它到底停在哪
do_chip_erase / skip_erase整片擦除代替逐扇区擦除 / 跳过擦除(芯片已经擦过)
preverify / verify编程前读回比较、跳过没变的部分 / 编程后读回校验
disable_double_buffering关掉双缓冲
preferred_algos一个区有多个算法可用时指定用哪个
progress进度回调,下文的实验就靠它拿到计划

镜像格式

镜像格式是源码地图里四张全局表之一,LOADERS 在 builtin-formats 打开时放 ELF、BIN、Intel HEX、UF2 四种,ESP 插件再追加 idf。image_format 按名字找第一个认这个名字的工厂。每种格式实现 ImageLoader::load,把数据一块块 add_data 进 FlashLoader。

load_image 在调格式之前还做一件事:如果调用方说明了镜像的指令集,就检查它和目标的核是否兼容(比如把 RISC-V 固件烧进 Cortex-M 会在这里报 IncompatibleImage)。

ELF:按加载地址取段

extract_from_elf_inner 只看 program header:

// probe-rs/src/flashing/loader.rs
        for segment in elf_header.program_headers(elf_header.endian()?, elf_data)? {
            // Get the physical address of the segment. The data will be programmed to that location.
            let p_paddr: u64 = segment.p_paddr(endian).into();
            // ...
            if !segment_data.is_empty() && segment.p_type(endian) == PT_LOAD {
                // ... 找出落在这个段里的各个 section,跳过 skip_sections 里列的

数据写到 p_paddr(加载地址 LMA),不是 p_vaddr(运行地址 VMA)。对 Cortex-M 固件来说,.data 的运行地址在 RAM,初值存在 flash 里 .rodata 后面,启动代码再把它拷到 RAM;按 LMA 取正好把初值烧进 flash。filesz 为 0 的段(.bss)没有数据,被跳过。FakeProbe 一章列过 nRF52833 夹具的段,按这个规则,loader 得到两块:

== loader data 0x00000000 len=33300
== loader data 0x00008218 len=112

第一块是向量表、.text、.rodata 三个相邻段合在一起(0x8214 字节),第二块是 .data 的 112 字节初值。合并发生在 FlashBuilder::add_data:数据按起始地址存在一个 BTreeMap 里,新块和左右邻居重叠就报 DataOverlaps,正好接上左邻居就直接追加。在这之前,FlashLoader::add_data 先检查整段地址都落在目标的 NVM 或 RAM 区里,否则报 NoSuitableNvm。所以 RAM 里的数据也可以「烧」:commit 最后会直接写进 RAM。

commit:先做计划

commit 的第一步是 prepare_plan:遍历内存图里的每个 NVM 区,跳过没有数据的;为有数据的区找 flash 算法(考虑 preferred_algos、算法的地址范围和它能在哪个核上跑);同一个算法、同一个核的区合进同一个 Flasher,免得一个算法装载两次,也保证整片擦除每个算法只做一次。Flasher::new 在这里就把算法「装配」好(选 RAM、算栈和缓冲区,见下一页),但还不碰硬件。

每加一个区,build_sectors_and_pages 就算出这个区的 FlashLayout:

  • 扇区:遍历算法的扇区表,扇区或它第一页里有数据的才列入。只擦需要的扇区。
  • 页:遍历所有页,有数据的才列入;keep_unwritten_bytes 时,所在扇区有数据的空页也列入(扇区要整块擦,整块都得写回)。每页先填满擦除值(通常 0xFF),再把数据拷进去。
  • 填充:页里数据块之间、首尾空出来的部分,各记一个 FlashFill。只有 keep_unwritten_bytes 时才真的去芯片上把这些字节读回来填进页缓冲区。

然后:

// probe-rs/src/flashing/loader.rs
        if options.dry_run {
            tracing::info!("Skipping programming, dry run!");

            options.progress.failed_filling();
            options.progress.failed_erasing();
            options.progress.failed_programming();

            return Ok(());
        }

        self.initialize(&mut algos, session, &mut options)?;

dry_run 在算完计划后返回,并给进度回调发三个「失败」事件,让界面上的进度条收起来;计划本身(FlashLayoutReady)是在 initialize 里才发出的,所以 dry run 拿不到它。initialize 只做计算:汇总各段大小、加进度条、发 FlashLayoutReady,不碰硬件。ProgressEvent 的文档说 FlashLayoutReady 是第一个事件,代码里却先发 AddProgressBar,实验输出里能看到这个顺序。

实验一:FakeProbe 上的 nRF52833

用 FakeProbe::with_mocked_core_and_binary 加载 nRF52833 夹具的 ELF,attach nRF52833_xxAA(用 expect_operation 应付 CTRL-AP 的那次读),build_loader 后分别以 dry_run = true 和 false 调 commit,进度回调把事件打印出来:

== raw algo nrf52 page_size=0x1000 sectors=[SectorDescription { size: 4096, address: 0 }]
[fake] Failed(Fill)
[fake] Failed(Erase)
[fake] Failed(Program)
== commit dry_run=true -> Ok(())
[fake] AddProgressBar { operation: Erase, total: Some(36864) }
[fake] AddProgressBar { operation: Program, total: Some(36864) }
[fake] FlashLayoutReady
-- phase 0: 9 sectors, 9 pages, 2 fills, 2 data blocks
   sector 0x00000000 size 0x1000
   ...
   sector 0x00008000 size 0x1000
   page   0x00000000 size 0x1000 (0xff bytes in buffer: 55)
   ...
   page   0x00008000 size 0x1000 (0xff bytes in buffer: 3467)
   fill   0x00008214 size 0x4 -> page #8
   fill   0x00008288 size 0xd78 -> page #8
   data   0x00000000 size 0x8214
   data   0x00008218 size 0x70
[fake] Started(Erase)
[fake] Failed(Erase)
== commit dry_run=false -> Err(ResetAndHalt(Arm(Timeout)))

nRF52 的页和扇区都是 4 KiB,0x8288 字节的数据要擦写 9 个扇区、36 KiB。两个填充都在最后一页:0x8214..0x8218 是两块数据之间的 4 字节空隙,0x8288 之后是页尾。计划在 FakeProbe 上完整算出来了,开始擦除时第一件事是把 flash 算法装进 RAM,装之前要 reset_and_halt,FakeProbe 的模拟核在复位后不会停下,于是超时;为什么在这一步、换成能「执行」算法的模拟目标会怎样,见下一页。

实验二:页和扇区不一样大

FakeProbe 上 nRF52 的页、扇区一样大,看不出两者的区别。换本卷的模拟 SWD 目标,直接往 FlashLoader 里 add_data 两块数据:0x08000000 起 3000 字节,另一块 16 字节放在较远处。STM32F103C8 的算法页和扇区都是 1 KiB;STM32F407VG 的算法页是 1 KiB,前 4 个扇区是 16 KiB:

### STM32F103C8,第二块在 0x08002100
-- phase 0: 4 sectors, 4 pages, 3 fills, 2 data blocks
   sector 0x08000000 size 0x400
   sector 0x08000400 size 0x400
   sector 0x08000800 size 0x400
   sector 0x08002000 size 0x400
   page   0x08000000 size 0x400 (0xff bytes in buffer: 4)
   page   0x08000400 size 0x400 (0xff bytes in buffer: 4)
   page   0x08000800 size 0x400 (0xff bytes in buffer: 75)
   page   0x08002000 size 0x400 (0xff bytes in buffer: 1008)
   fill   0x08000bb8 size 0x48 -> page #2
   fill   0x08002000 size 0x100 -> page #3
   fill   0x08002110 size 0x2f0 -> page #3
### STM32F407VG,第二块在 0x08004100
[sim] AddProgressBar { operation: Erase, total: Some(32768) }
[sim] AddProgressBar { operation: Program, total: Some(4096) }
-- phase 0: 2 sectors, 4 pages, 3 fills, 2 data blocks
   sector 0x08000000 size 0x4000
   sector 0x08004000 size 0x4000
   page   0x08000000 size 0x400 ...
   page   0x08000400 size 0x400 ...
   page   0x08000800 size 0x400 ...
   page   0x08004000 size 0x400 ...

### STM32F407VG,同上加 keep_unwritten_bytes
[sim] AddProgressBar { operation: Fill, total: Some(29752) }
[sim] AddProgressBar { operation: Erase, total: Some(32768) }
[sim] AddProgressBar { operation: Program, total: Some(32768) }
-- phase 0: 2 sectors, 32 pages, 31 fills, 2 data blocks

F407 上只写了 4 KiB,却要擦 32 KiB:两个 16 KiB 的扇区。默认情况下扇区里其余 28 KiB 被擦掉、不再写回;打开 keep_unwritten_bytes 后,计划变成 32 页、31 个填充,要先从芯片读回 29752 字节,再整整写满两个扇区。这就是该选项文档里说的「不直观」:不打开时,同一扇区里你没碰的数据也没了。

小结

  • 镜像格式是一张全局表,内置 ELF、BIN、HEX、UF2,ESP 插件加 IDF;ELF 只看 PT_LOAD 段,按 p_paddr 取数据,.data 的初值因此进 flash。
  • FlashBuilder 用 BTreeMap 存数据块,拒绝重叠、合并相邻;FlashLoader::add_data 要求数据落在 NVM 或 RAM 区里。
  • commit 先按算法分组、为每个区算 FlashLayout:只擦有数据的扇区,只写有数据的页,空隙记成填充;keep_unwritten_bytes 会把整个扇区读回再写满。
  • dry_run 在算完计划后返回,进度回调收到三个失败事件,拿不到 FlashLayoutReady;真烧录在 initialize 发出计划后,从装载 flash 算法开始碰硬件,FakeProbe 在其中的 reset_and_halt 超时。

💬 关联讨论

发起讨论
加载中...