- 嵌入式
- 硬件开发
- 开发工具
- 调试器
【免费下载链接】stlink
Open source STM32 MCU programming toolset
本文围绕 stlink 开源 STM32 编程工具集(仓库根目录)中的核心性能机制——Flashloader(闪存加载器)展开。Flashloader 是一段被 stlink 加载到 STM32 SRAM 中执行的微型汇编程序,负责把"擦除—写数据—等待 FLASH 不忙"的循环从主机端搬到芯片端,从而大幅缩短烧写时间。读完本文,你将掌握 Flashloader 的两阶段烧写原理、st-flash 与 flashloader 之间的寄存器调用约定、各 STM32 系列的汇编实现差异、构建流程以及一套可落地的调试技巧。
什么是 Flashloader:为什么 STM32 烧写需要它
STM32 片上 FLASH 的写入是以字节 / 半字 / 字 / 双字为粒度进行的(具体粒度因系列而异)。如果整个过程完全由主机端的stlink逐单位写数据、逐单位等待 FLASH 完成,主机与芯片之间的一次次往返通信会累积成难以忍受的漫长烧写时间。
Flashloader 的引入把烧写过程拆成两个阶段(见 doc/flashloaders.md):
- 第一阶段(加载阶段):
stlink把编译好的 flashloader 二进制和待烧写数据一并写入 SRAM。此阶段不做 busy 检查,纯数据搬运,速度快。 - 第二阶段(运行阶段):
stlink启动(kick-start)flashloader,由它在芯片内部把 SRAM 中的数据写入 FLASH。此阶段 flashloader 自身执行"写单位数据 → 检查 FLASH 是否忙 → 继续"的循环。
这样,"写—查忙"的循环完全由 STM32 芯片自己完成,省去的是 stlink 与 STM32 之间大量的等待往返,通信开销被显著压缩。
由于 SRAM 容量通常远小于 FLASH,stlink一次只烧写一页(若 SRAM 不足则不足一页),整个烧写过程可能包含多次启动 flashloader。每次启动都重复"加载 loader + 数据 → 运行 → 等待 halt"的周期。
烧写全流程:从 st-flash 到 FLASH
原文档给出的完整流程如下(实现位于 src/stlink-lib/flash_loader.c 与 src/stlink-lib/common_legacy.c):
st-flash调用stlink_flash_loader_init(flash_loader.c),把对应芯片的 flashloader 二进制加载到 SRAM;st-flash调用stlink_erase_flash_page(位于common_legacy.c),擦除对应 FLASH 页;st-flash调用stlink_flash_loader_run(flash_loader.c),在此函数内完成下列动作:- 将一整页的数据缓冲写入 SRAM,紧跟 flashloader 之后;
- 把缓冲区起始地址(SRAM 内)写入寄存器
r0; - 把目标起始地址(FLASH 内、页对齐)写入寄存器
r1; - 把缓冲区大小写入寄存器
r2; - 把 flashloader 的起始地址(当前为
0x20000000)写入r15(即pc); - 之后启动 flashloader,等待内核停住(halt,由 flashloader 触发的断点所致),并确认
r2已归零来判定烧写完成;
- flashloader 端的行为本质上是一个带 busy 检查的
memcpy:- 从 SRAM 拷贝单个单位数据到 FLASH;
- (大多数设备)等待 FLASH 不再忙碌;
- 完成后触发断点(breakpoint)使内核停下。
从源码看stlink_flash_loader_run的寄存器约定
结合 flash_loader.c 的实现,实际的寄存器设定比文档描述多一个r3:
| 寄存器 | 内容 | 说明 |
|---|---|---|
r0 | fl->buf_addr | 数据缓冲区起始地址(SRAM 内,紧贴 loader 之后) |
r1 | target | 目标 FLASH 地址(页对齐) |
r2 | padded_size | 待写入字节数(可能按对齐规则补齐) |
r3 | flash_base | FLASH 寄存器基址偏移(仅 F1_XL 双 bank 场景使用,对其他芯片无害) |
r15 | fl->loader_addr | flashloader 起始地址,即pc,从这里开始执行 |
代码中还包含几个容易被忽略的细节:
- 对齐补齐:对 WB0/H5,
pad_modulo取 16(H5 以 128 位 quad-word 为单位编程),其余设备默认按 1 处理;size若不对齐会被补齐为padded_size(flash_loader.c)。 - 数据写入 SRAM:
write_buffer_to_sram(common_legacy.c)把数据写到fl->buf_addr,并用0xFF填充对齐产生的空隙。 - IWDG 喂狗:若
fl->iwdg_kr非零,启动 loader 前会向独立看门狗键寄存器写入0xAAAA(STM32F0_WDG_KR_KEY_RELOAD),避免 loader 运行期间看门狗复位(flash_loader.c)。该键寄存器地址在 stlink_flash_loader_init 中按STM32_FLASH_TYPE_H7(0x58004800)与其他设备(0x40003000)分别设置。 - 超时与轮询:启动后主机以
time_ms() + 500为截止时间,每usleep(10000)检查一次stlink_is_core_halted(内部通过stlink_status更新core_stat,见 common_legacy.c),直到内核 halt 或超时(flash_loader.c)。 - 成功判定:halt 后读取
r2,只有r2 <= 0且不小于-7才视为成功;r2为正说明还有字节未写完,负得太多说明因末块未对齐写出了多余字节(flash_loader.c)。 - 失败诊断:出错时强制进入 debug 状态,转储
DHCSR/DFSR/CFSR/HFSR与R0~R7寄存器,便于定位 loader 停在了哪条路径上(flash_loader.c)。
flash_loader_t结构体定义于 inc/stlink.h,包含loader_addr(loader 的 SRAM 地址)、buf_addr(缓冲区 SRAM 地址)、rcc_dma_bkp(RCC DMA 使能状态备份)与iwdg_kr(看门狗键寄存器地址)四个字段。
编写 Flashloader 的约束清单
对于想要修改或新写 flashloader 的开发者,原文档列出了必须满足的约束:
- 只能使用thumb-1(如 STM32F0 等)或thumb-1 + thumb-2(如 STM32F1 等)指令,不得使用 ARM 指令;
- 不能使用栈,因为栈区可能覆盖缓冲区数据;
- 对大多数设备,写完单个单位数据后必须等待 FLASH 不再忙碌;
- 部分设备还需在烧写过程中检查是否发生错误;
- 必须遵守单次拷贝的单位大小;
- 烧写完成后必须触发断点(breakpoint)让内核停下;
- 一次成功运行的结束标志是halt 时
r2已归零; - flashloader 至少要能在
0x20000000(SRAM 基地址)运行。
补充的调用约定细节记录在仓库内的 flashloaders/cleanroom.md:所有参数通过寄存器传递,r0为拷贝源基址、r1为拷贝目标基址、r2为拷贝字节数、r3为 FLASH 寄存器偏移(用于支持双 bank)。寄存器r3~r12可自由使用,而r13(sp)、r14(lr)与r15(pc)有特殊用途,禁止任何栈操作。汇编文件开头必须带.syntax unified编译指令,代码段放在.text。
对需要等待 FLASH 不忙的设备,检查FLASH_SR的BUSY 位;对需要检错的设备,检查FLASH_SR中(X)ERR位族(X可为任意错误状态)。各设备的FLASH_SR相关偏移与单次拷贝粒度可在 ST 官方参考手册或仓库的 cleanroom 文档中找到,下面一节给出仓库内可验证的汇总。
各系列 Flashloader 的差异:寄存器基址、状态位偏移与拷贝粒度
仓库 flashloaders/ 目录存放着各系列的汇编源文件,下表汇总了 cleanroom.md 中记录的关键参数:
| 汇编源文件 | 适用系列 | flash_base | FLASH_SR偏移 | 单次拷贝单位 | 特殊要求 |
|---|---|---|---|---|---|
stm32f0.s | F0(及编译为 F1/F3 的stm32vl) | 0x40022000 | 0x0c(12) | 半字(2 字节) | 每次拷贝前读FLASH_CR置 PG 位写回;等待 BUSY;检查错误位0x04;退出前清 PG 位 |
stm32f4.s | F2/F4 高电压 | 0x40023c00 | 0x0e(14) | 字(4 字节) | 等待 BUSY 复位 |
stm32f4lv.s | F2/F4 低电压 | 0x40023c00 | 0x0e(14) | 字节(1 字节) | 每次拷贝一个字节 |
stm32f7.s | F7 高电压 | 0x40023c00 | 0x0e(14) | 字(4 字节) | 与 F4 类似,但每次拷贝后、查忙前需执行dsb sy内存屏障 |
stm32f7lv.s | F7 低电压 | 0x40023c00 | 0x0e(14) | 字节(1 字节) | 同 F7,每次拷贝一字节 |
stm32lx.s | L0/L1 | — | — | 字(4 字节) | 无需等待写完成 |
stm32l4.s | L4 | 0x40022000 | 0x12(18,即FLASH_BSY) | 双字(8 字节) | 允许使用多个寄存器;读FLASH_BSY半字等待 BUSY |
stm32h5.s | H5 | 0x40022000 | — | 128 位 quad-word(16 字节) | 每次编程一个 128 位 quad-word,等待 NSSR BSY |
stm32wb0.s | WB0/WL3 等 | 0x40001000(FLASH 外设基址) | — | 16 字节块 | 通过 FLASH 外设寄存器写,目标地址须 16 字节对齐、长度须为 16 的倍数;出错提前退出,r2保存未写完数量 |
源码级示例:stm32f0.s
以 flashloaders/stm32f0.s 为例,其主循环可以逐条对应约束:
loop: # 拷贝 2 字节(半字) ldrh r4, [r0] strh r4, [r1] adds r0, r0, #0x2 adds r1, r1, #0x2 wait: # 读 FLASH_SR,等待 BUSY 位复位 ldr r4, [r5] tst r4, r7 # r7 = 0x01 bne wait # 检查 PGERR 或 WRPRTERR(掩码 0x14)是否置位 ldr r7, =0x14 tst r4, r7 bne exit subs r2, r2, #0x2 # 剩余计数减 2 bgt loop exit: bkpt # 触发断点,让内核停下对应的.word数据段给出flash_base = 0x40022000、FLASH_SR偏移0x0c。stm32f7.s则在每次str之后多了一条dsb sy(flashloaders/stm32f7.s),以满足 Cortex-M7 的内存顺序要求。
电压相关的 loader 选择
F2/F4 与 F7 系列存在高电压(32 位写)与低电压(8 位写)两个变体:stm32f4.s/stm32f4lv.s、stm32f7.s/stm32f7lv.s。选择逻辑位于 stlink_flash_loader_write_to_sram 调用的loader_v_dependent_assignment(flash_loader.c):
- STLINK V1 无法读取目标电压,默认使用 32 位写的高压 loader;
- 其余版本通过
stlink_target_voltage读取电压,> 2700 mV 时选用 32 位 loader,否则选用 8 位 loader;读电压失败则直接报错返回。
如何构建 Flashloader:从汇编到字节数组
flashloader 不是独立的可执行程序,而是被嵌入 stlink 主机程序里的字节数组。构建链路由 flashloaders/Makefile 定义:
- 用arm-none-eabi-gcc(而非系统 gcc)编译汇编源文件:Cortex-M0(ARMv6-M,
stm32f0.s、stm32wb0.s、stm32lx.s)用-mcpu=Cortex-M0,其余 ARMv7-M 用-mcpu=Cortex-M3,均带-ffreestanding -nostdlib且链接脚本为linker.ld; objcopy -O binary把目标文件转成裸二进制.bin;xxd -i -c 4把二进制转换成 C 头文件(如stm32f4.h)。
因此,修改 flashloader 的正确姿势是编辑 flashloaders/ 下的.s汇编文件,而不是直接改字节数组。这一点在 flash_loader.c 的注释中有明确警告:
!!! DO NOT MODIFY FLASH LOADERS DIRECTLY !!! Edit assembly files in the '/flashloaders' instead. The sizes of binary flash loaders must be aligned by 4 (it's written by stlink_write_mem32)
即:编译出的二进制长度必须4 字节对齐,因为它是通过stlink_write_mem32写入 SRAM 的。编译产物以loader_code_stm32vl、loader_code_stm32f0、loader_code_stm32wb0、loader_code_stm32lx、loader_code_stm32f4、loader_code_stm32f4_lv、loader_code_stm32h5、loader_code_stm32l4、loader_code_stm32f7、loader_code_stm32f7_lv等数组形式内嵌于 flash_loader.c。
链接脚本 flashloaders/linker.ld 把代码段放在0x20000000(64K RAM),与文档要求的"至少能在0x20000000运行"一致。
芯片匹配逻辑
stlink_flash_loader_write_to_sram依据chip_id/core_id/flash_type选择 loader:
- L1/L0 系列 →
loader_code_stm32lx(armv6-m 编译,同时兼容 L0 的 Cortex-M0 与 L1 的 Cortex-M3); - F1/F3/VL 系列 →
loader_code_stm32vl(同一份汇编、thumb-2 编译); - F0 系列 →
loader_code_stm32f0; - WB0 →
loader_code_stm32wb0;H5 →loader_code_stm32h5;L4 系列 →loader_code_stm32l4; - F2/F4 与 F7 按上文电压规则选择;
- 无法识别的
core_id/chip_id直接ELOG报错并中止。
FLASH_SR 状态位与错误检查
对需要等待的设备,核心是轮询FLASH_SR的BUSY 位(bit 0)。对需要检错的设备(如 F0 的 PGERR/WRPRTERR),源码层面有对应的wait_flash_busy、check_flash_error、clear_flash_error等辅助函数(flash_loader.c与common_flash.c),在stlink_flashloader_write的每轮写入后调用,失败时返回-1并记录错误地址。L0/L1 的软写回退路径还会在每次写字后自行轮询FLASH_SR的 bit 0(见 flash_loader.c)。
不同系列的FLASH_SR相关偏移与单次拷贝粒度可在 ST 官方参考手册、其他开源项目头文件以及仓库内的 flashloaders/cleanroom.md 中找到依据。
调试技巧:断点、gdb/lldb 与超时
当某个 flashloader 损坏,或需要为新型号编写 flashloader 时,原文档给出了如下调试套路:
- 把
WAIT_ROUNDS宏改大,争取在 st-flash 等待内核 halt 期间手动杀掉它; - 运行
st-flash,待 flashloader 加载进 SRAM 后将其杀掉; - 启动
st-util与gdb/lldb; - 在 SRAM 基地址处设断点;
- 跳到基地址开始单步调试。
该套路之所以有效,是因为闪存解锁、擦除、加载 flashloader 到 SRAM 等大部分准备工作已自动完成,省去了手工搭建调试环境的时间。
需要说明的是:当前仓库源码中已不再存在WAIT_ROUNDS宏,等待内核 halt 的超时逻辑实现为time_ms() + 500的截止时间配合usleep(10000)轮询(flash_loader.c),代码注释中解释了此前"10000 次 × 10 µs"方案在类 Unix 系统上因 tick 舍入实际要等约 20 秒、而调整为按与 tick 同量级睡眠的原因。若要在调试时争取更多手动干预时间,可相应调大timeout计算中的500(ms)或轮询睡眠间隔;同时建议打开-v详细输出观察DLOG("Running flash loader...")之类的进度信息。
调试过程中一旦触发错误路径,stlink_flash_loader_run会把 PC、R0~R7 与 MCU 状态寄存器(DHCSR/DFSR/CFSR/HFSR)一并打印出来(flash_loader.c),这些信息可用于反推 loader 是在等待忙、检错还是计数循环中挂掉的。
小结
Flashloader 是 stlink 烧写性能的关键支点:它以"加载到 SRAM + 芯片内自循环烧写"的两阶段架构,把最耗时的写—查忙周期从主机端通信中剥离出来。本文从 doc/flashloaders.md 出发,结合 flash_loader.c、flashloaders/ 目录下的汇编源码与构建脚本,完整还原了寄存器调用约定、各系列实现差异、约束清单与调试方法。后续若想深入了解擦除流程、软写回退路径或芯片参数加载,可继续阅读 doc/backend.md、doc/flashloaders.md 与 src/stlink-lib/common_flash.c。
- 嵌入式
- 硬件开发
- 开发工具
- 调试器
【免费下载链接】stlink
Open source STM32 MCU programming toolset
相关推荐
CANN ops-transformer InterleaveRope 算子深度解析:旋转位置编码原理、约束与 aclnn 两段式调用实战
CANN ops transformer InterleaveRope 算子深度解析:旋转位置编码原理、约束与 aclnn 两段式调用实战 Interleave
算子库人工智能大模型深度学习CANNAscendBambuStudio FilamentGroup 回归测试方案深度解析:两层架构、评分模型与约束验证实战
BambuStudio FilamentGroup 回归测试方案深度解析:两层架构、评分模型与约束验证实战 本指南基于 tests/filament_group
嵌入式开发工具构建工具操作系统CANN ops-nn 算子 aclnnMaxUnpool2d 深度解析:两段式 API 原理、参数约束与完整调用实战
CANN ops nn 算子 aclnnMaxUnpool2d 深度解析:两段式 API 原理、参数约束与完整调用实战 导读 aclnnMaxUnpool2d
人工智能算子库深度学习CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考