flash 算法:在目标的 RAM 里跑一段代码
主线位置:命令主线第四步「烧录」的后半段。上一页算好了要擦哪些扇区、写哪些页;擦写本身不是调试器做的,而是把芯片厂商写好的一小段代码装进目标的 RAM,让目标自己的 CPU 去操作 flash 控制器。调试器只负责「设寄存器、放行、等它停、读返回值」,用的全是调试主线前半段那些操作。
sequenceDiagram
participant H as probe-rs
participant C as 目标 CPU
participant F as flash 控制器
H->>C: reset_and_halt
H->>C: 写 RAM:断点头加算法代码,栈填 0x56,读回校验
H->>C: PC=Init,R0=flash 起始,R2=1,R9=static_base,SP=stack_top,LR=断点头+1
H->>C: run
C->>F: 解锁、配时钟
C-->>H: 返回到断点头,BKPT,停下
H->>C: 读 R0:0 表示成功
loop 每个扇区
H->>C: PC=EraseSector,R0=扇区地址,run,等停,读 R0
end
H->>C: UnInit,再以 R2=2 调 Init
loop 每页
H->>C: 写页缓冲区,PC=ProgramPage,R0=页地址,R1=页大小,R2=缓冲区
end
H->>C: UnInit
CMSIS 的约定
CMSIS-Pack 的 flash 算法是一组 C 函数,用 AAPCS 调用约定:
| 函数 | 参数(R0–R2) | 返回 R0 |
|---|---|---|
Init | flash 起始地址、时钟、操作(1 擦除、2 编程、3 校验) | 0 成功 |
UnInit | 操作 | 0 成功 |
EraseSector | 扇区地址 | 0 成功 |
ProgramPage | 页地址、字节数、RAM 里的数据地址 | 0 成功 |
EraseChip(可选) | 无 | 0 成功 |
算法是位置无关代码,全局变量通过 R9(static base)寻址。这些入口在 YAML 里是相对偏移(target-gen 与 CMSIS-Pack),probe-rs 装载时再加上实际地址。函数返回时跳到 LR,probe-rs 把 LR 指向自己放在代码前面的一条断点指令,核一返回就停下,调试器就知道调用结束了。
装配:选 RAM、放断点头、算栈和缓冲区
assemble_from_raw_with_data 把 YAML 里的 RawFlashAlgorithm 变成一个带绝对地址的 FlashAlgorithm:
- 断点头:Cortex-M 上是一个字
0xBE00BE00,两条BKPT #0(flash_algorithm.rs:186);Cortex-A / R 用 A32 的BKPT;Armv8-A 用HLT(那里BKPT不进调试态);RISC-V 是两条ebreak;Xtensa 没有头。 - 加载地址:YAML 给了
load_address就用它(减去头的长度),否则放在所选 RAM 区的起点。 - 布局:头、代码、数据,然后是栈。栈默认 512 字节。如果数据区没有单独指定,紧跟在代码后面,栈就只能放到数据区之后。
- 双缓冲:剩下的 RAM 够放两页就分两个页缓冲区,否则一个,一个都放不下就报错。
在模拟目标上用 STM32F103C8 的 stm32f10x_128 算法装配(FlashAlgorithm::assemble_from_raw_with_core 是公开的):
== algo stm32f10x_128 load=0x20000000 code_words=76 init=Some(20000005) uninit=Some(20000037) program_page=0x200000c1 erase_sector=0x20000083 erase_all=Some(20000049)
== static_base=0x2000012c stack_top=0x20000b38 stack_size=0x200 page_buffers=[20000138, 20000538] header=0xbe00be00
| 区域 | 地址 |
|---|---|
| 断点头 | 0x20000000 |
代码(入口 = 0x20000004 + YAML 偏移,最低位是 Thumb 位) | 0x20000004 起 |
| 两个 1 KiB 页缓冲区 | 0x20000138、0x20000538 |
| 栈 | 0x20000938 – 0x20000b38 |
code_words = 76 已经包含断点头;代码末尾的计算(code_start + 76 × 4)因此多算了 4 字节,再按 8 字节对齐到 0x20000138,只是多空出一点 RAM。
load:复位、写入、读回
Flasher::load 在第一次需要算法时执行:
// probe-rs/src/flashing/flasher.rs
// TODO: we probably want a full system reset here to make sure peripherals don't interfere.
tracing::debug!("Reset and halt core {}", self.core_index);
core.reset_and_halt(Duration::from_millis(500))
.map_err(FlashError::ResetAndHalt)?;
// TODO: Possible special preparation of the target such as enabling faster clocks for the flash e.g.
// Load flash algorithm code into target RAM.
tracing::debug!("Downloading algorithm code to {:#010x}", algo.load_address);
core.write(algo.load_address, algo.instructions.as_bytes())
.map_err(FlashError::Core)?;
写完立刻读回逐字比较,不一致就报 FlashAlgorithmNotLoaded;算法要求检查栈溢出时,再把整个栈填成 0x56。每次调用结束后,check_for_stack_overflow 只读栈底那一个字节,不是 0x56 就报 StackOverflowDetected。
第一步就是 reset_and_halt:设 DEMCR 的 VC_CORERESET,让序列做系统复位,等核停在复位向量上(Cortex-M 内核)。FakeProbe 的模拟核不会因为复位而停下,所以上一页的实验在这里得到 Err(ResetAndHalt(Arm(Timeout))):从这一步起,烧录需要一颗会响应复位和 vector catch 的核。
call_function:一次远程函数调用
call_function 依次写这些寄存器:
// probe-rs/src/flashing/flasher.rs
let registers = [
(self.core.program_counter(), Some(registers.pc)),
(regs.argument_register(0), registers.r0),
(regs.argument_register(1), registers.r1),
(regs.argument_register(2), registers.r2),
(regs.argument_register(3), registers.r3),
(
regs.core_register(9),
if init { Some(algo.static_base) } else { None },
),
(
self.core.stack_pointer(),
if init { Some(algo.stack_top) } else { None },
),
(
self.core.return_address(),
// For ARM Cortex-M cores, we have to add 1 to the return address,
// to ensure that we stay in Thumb mode.
if self.instruction_set == InstructionSet::Thumb2 {
Some(algo.load_address + 1)
} else {
Some(algo.load_address)
},
),
];
None 的寄存器不写,保持上一次调用留下的值。R9 和 SP 只在 Init 时设。然后 core.run()。注意 Armv7m::run 一开头会先单步一次,所以算法的第一条指令是单步执行的。
wait_for_completion 循环调 core.status() 直到停下(LockedUp 直接报错),中间没有 sleep:每轮就是一次 DHCSR 读,速度由探针的往返时间决定。超时按操作不同:Init / UnInit 2 秒,擦扇区和编程用 YAML 里的 erase_sector_timeout、program_page_timeout。停下后检查栈,读 R0 作为返回值。
实验:让模拟目标「执行」算法
本卷的模拟 SWD 目标不会执行指令。为了走完整个流程,实验在它上面加了两条规则:写 AIRCR 的 SYSRESETREQ 时,如果 DEMCR 的 VC_CORERESET 置位,就让核停在复位向量上;写 DHCSR 放行(C_DEBUGEN 置位,C_HALT、C_STEP、C_MASKINTS 都为 0)时,记下最近写进 PC 的值和 R0–R3、LR,然后立刻「返回」:R0 = 0,PC = LR,以断点停下。也就是说,算法每次调用都瞬间成功。在 STM32F103C8 上烧上一页那份计划(4 个扇区、4 页):
[sim] Started(Erase)
[sim] Finished(Erase)
[sim] Started(Program)
[sim] Finished(Program)
== commit -> Ok(())
call Init pc=0x20000005 r0=0x08000000 r1=0x00000000 r2=0x00000001 r3=0x00000000 lr=0x20000001
call EraseSector pc=0x20000083 r0=0x08000000 r1=0x00000000 r2=0x00000001 r3=0x00000000 lr=0x20000001
call EraseSector pc=0x20000083 r0=0x08000400 ...
call EraseSector pc=0x20000083 r0=0x08000800 ...
call EraseSector pc=0x20000083 r0=0x08002000 ...
call UnInit pc=0x20000037 r0=0x00000001 r1=0x00000000 r2=0x00000001 r3=0x00000000 lr=0x20000001
call Init pc=0x20000005 r0=0x08000000 r1=0x00000000 r2=0x00000002 r3=0x00000000 lr=0x20000001
call ProgramPage pc=0x200000c1 r0=0x08000000 r1=0x00000400 r2=0x20000138 r3=0x00000000 lr=0x20000001
call ProgramPage pc=0x200000c1 r0=0x08000400 r1=0x00000400 r2=0x20000538 r3=0x00000000 lr=0x20000001
call ProgramPage pc=0x200000c1 r0=0x08000800 r1=0x00000400 r2=0x20000138 r3=0x00000000 lr=0x20000001
call ProgramPage pc=0x200000c1 r0=0x08002000 r1=0x00000400 r2=0x20000538 r3=0x00000000 lr=0x20000001
call UnInit pc=0x20000037 r0=0x00000002 r1=0x00000400 r2=0x20000538 r3=0x00000000 lr=0x20000001
DRW writes into 0x20000000..: 1228
DRW writes into 0xe0000000..: 265
== raw transfers during commit: 2611
读出来的几件事:
- 擦除和编程是两个独立的「会话」:
Init(op=1)…UnInit(1),再Init(op=2)…UnInit(2)。UnInit只设 R0,R1、R2 是上一次ProgramPage留下的。 - 每页一次
ProgramPage,缓冲区在0x20000138和0x20000538之间交替:这是双缓冲,算法写第 n 页时,调试器同时把第 n+1 页写进另一个缓冲区,再wait_for_write_end等上一页结束。加disable_double_buffering后,四次调用都用0x20000138。 - 往 RAM 写了 1228 个字:76 个字的算法、128 个字的栈填充、4 页 × 256 个字的数据。往
0xE000xxxx(DHCSR、DCRSR、DCRDR 等调试寄存器)写了 265 次。整个commit是 2611 次 DP / AP 访问。
真实芯片上的擦写时间、双缓冲带来的加速,要一块板子和一个算法才能测,待实测。
校验、填充和算法里的 RTT
verify打开时,编程后逐页读回比较;算法提供了Verify函数时也可以让目标自己校验。keep_unwritten_bytes的「填充」在擦除之前做:算法有ReadFlash(pc_read)就用它读,否则直接经调试端口读 flash(fill_unwritten)。后一条路径固定用session.core(0),不管这个Flasher属于哪个核。--read-flasher-rtt:如果算法 ELF 里有_SEGGER_RTT(target-gen 会记下它的地址),call_function放行后就在那个确切地址上 attach RTT,wait_for_completion每隔rtt_poll_interval(0.31 起可在算法里配置,默认 20 ms)读一次 up channel,内容作为DiagnosticMessage进度事件发出去(read_rtt)。这是调试自写 flash 算法的主要手段;RTT 本身见 RTT 控制块与通道。- 计划里有 RAM 区的数据时,
commit在 flash 之后才写 RAM(flash 算法会覆盖 RAM);向量表在 RAM 里时,写之前先reset_and_halt让核处于干净状态。
小结
- flash 算法是装进目标 RAM 的一小段位置无关代码;probe-rs 在它前面放一条断点指令,让 LR 指向它,函数一返回核就停下。
- 装配阶段确定加载地址、入口、static base、栈和页缓冲区;RAM 够两页就双缓冲。
load先reset_and_halt,再写入并读回校验、填栈;call_function写 PC、R0–R3、R9、SP、LR 后run,wait_for_completion不 sleep 地轮询状态,最后读 R0。- 模拟目标上的完整调用序列:
Init(1)、每扇区一次EraseSector、UnInit(1)、Init(2)、每页一次ProgramPage(双缓冲交替)、UnInit(2);FakeProbe在第一步reset_and_halt就超时。