简介:《LAT1491 STM32N6的FSBL基本流程介绍》面向从事ARM架构MCU开发的工程师及嵌入式技术爱好者,尤其是希望掌握STM32N6这颗带NPU芯片启动机制与FSBL用法的中高级开发者。内容围绕片内BootROM上电后的执行路径展开,讲清Dev boot与Flash boot/Serial boot两种模式的引脚选择,并分别梳理仅加载FSBL、FSBL搬运并运行应用(Load & Run)、外部Flash就地执行(XiP)三种启动流程,同时说明BootROM最大搬运512KB带来的工程拆分约束。文档还给出使用ST Signing Tool为FSBL.bin添加Header并签名的具体命令与参数含义,区分Cut1.1与Cut2.0的header版本,最后以IAR加STM32N6 DK板的UART示例为主线,串起从开发模式进入、工程创建到烧录部署的完整步骤与注意事项。资源包为1个PDF文件,大小约2.07MB,结构紧凑便于随查随用,已有162人学习。
1. 从 0x34180000 说起:为什么一颗 MCU 要有两段 Boot
给一块 STM32N6 板子上电,PC 指针拿到的第一条指令既不在你写的工程里,也不在外部 Flash 里,而在片内一小块出厂就固化死的 ROM 中。更反直觉的是:你编译出的 FSBL 二进制哪怕一个字节都没错,只要开头那 1024 字节没被签名工具处理过,芯片就当作没这回事,安静地停在原地。这颗带 NPU 的 MCU 把「启动」拆成了两段——BootROM 只负责校验和搬运,FSBL 才负责真正的板级初始化与加载。搞懂这条两段式链路,你才能解释清楚那些「程序明明下载进去了却不跑」「仿真器一连上就正常、断电就死」的玄学现场,也才知道 512KB 这个数字为什么会突然变成架构分水岭。
2. BootROM 的信任边界:1024 字节 Header 里到底装了什么
BootROM 是整条链路上唯一不可修改的一环。它躺在片内 ROM 里,出厂即定版,没有补丁,没有后悔药。正因为改不了,它必须极度保守:只认一种它能验证的二进制格式,只做它能保证的几件事。这就决定了 FSBL 不能是一个裸的 bin,而必须是一个「带信封的包裹」。
2.1 把二进制当包裹看:Header 的字段设计与存在理由
FSBL 工程编译出来的FSBL.bin是最原始的机器码,从头到尾没有任何元信息。BootROM 拿到它,无法回答三个关键问题:这段数据有多长?它该被放到哪里?它有没有被篡改或传坏?所以原厂的做法是在 bin 前面拼一段固定 1024 字节的 Header,让 BootROM 先读这 1KB,再决定后面怎么办。
常见的实现思路是,这类 Header 至少包含以下几类字段:
- 魔数与格式标识:让 BootROM 一眼判断「这是不是我能认的东西」,避免把随机数据当程序跑。
- Header 版本号:对应芯片的不同 Cut 版本。Cut1.1 用
-hv 2.1,Cut2.0 用-hv 2.3,版本号填错,BootROM 直接拒绝。 - 载荷长度与搬运目标地址:告诉 BootROM 后面有多少字节、要搬到哪个地址。STM32N6 上这个落点就是内部 SRAM2 的
0x34180000。 - 完整性校验值:对载荷做哈希,搬运前后各算一次,对不上就不跳。
- 选项标志位:签名命令里
-of 0x80000000写的就是这 32 位掩码,控制 BootROM 在搬运阶段的若干行为开关,具体位定义要看对应芯片的参考手册。 - 认证相关信息:本文场景用
-nk(no keys)走的是不带密钥的签名路径,Header 里对应位置的填充方式与带密钥时不同。
这 1024 字节的固定长度本身就是一种设计取舍:变长 Header 更省空间,但 BootROM 就得先读长度字段再二次读取,多一次不确定性;固定 1KB 意味着 BootROM 可以一次性把整段 Header 读进内部小缓冲,解析逻辑简单到几乎不可能出错。ROM 里的代码,简单比聪明重要。
2.2 校验通过之后:搬运、设栈、跳转的时序
Header 验证通过,BootROM 才开始干活。这条时序链在文档里被压缩成一句话「校验、搬运、跳转」,但每一步都有它必须存在的理由。用一段伪代码把 BootROM 侧的逻辑还原出来,会直观得多:
/* BootROM 侧启动逻辑的等价伪代码,非真实源码 */ int bootrom_boot(void) { fsbl_header_t hdr; /* 1. 从启动介质读前 1024 字节 Header 到内部缓冲 */ if (read_header(&hdr, BOOT_MEDIA_BASE) != OK) return ERR_NO_HEADER; /* 2. 格式与版本校验:Cut 版本不匹配直接停 */ if (hdr.magic != FSBL_MAGIC) return ERR_BAD_MAGIC; if (!header_version_supported(hdr.version)) /* 2.1 / 2.3 */ return ERR_BAD_VERSION; /* 3. 完整性校验:对 payload 做哈希比对 */ if (hash(payload_of(&hdr), hdr.length) != hdr.checksum) return ERR_BAD_HASH; /* 4. 搬运:落到 SRAM2 固定地址 0x34180000 */ memcpy((void *)SRAM2_FSBL_BASE, payload_of(&hdr), hdr.length); /* 5. 取向量表前两项:初始 MSP 与 Reset_Handler */ __set_MSP(*(uint32_t *)SRAM2_FSBL_BASE); void (*entry)(void) = (void (*)(void))*(uint32_t *)(SRAM2_FSBL_BASE + 4); /* 6. 跳转,BootROM 从此退场 */ entry(); return ERR_UNREACHABLE; }几个参数值得单独拎出来说。SRAM2_FSBL_BASE固定为0x34180000,意味着 FSBL 工程的链接脚本必须把代码段起点对齐到这个地址,否则搬过去之后所有绝对地址跳转全部错位,现象是跳转瞬间进 HardFault。hdr.length决定了搬运长度,链接脚本里配的 ROM 区域大小和它是一体两面的事。第 5 步读向量表这个动作,是 ARM 架构的通用约定:向量表前两个字永远是初始栈指针和复位入口,BootROM 不需要知道你的 C 代码怎么写,只需要按约定取这两个值。第 6 步之后 BootROM 就彻底放手了,中断向量表、时钟、外设,全部归 FSBL 管。
注意一个容易忽略的细节:BootROM 只负责把 FSBL 搬到 SRAM 并跳过去,它不初始化任何外设时钟,也不配置外部 Flash 控制器。所以 FSBL 一进来就跑在一个「几乎裸机」的环境里,它要做的第一件事往往是把时钟树拉到可用频率,否则后面访问外部 Flash 会慢到不可接受。
3. FSBL 之后的三种落地形态:Load & Run 与 XiP 到底差在哪
FSBL 跑起来之后,应用代码到底放哪儿执行,是 STM32N6 启动设计里最需要想清楚的一件事。文档给了三种形态,看起来只是配置差异,实际上是三种完全不同的运行时模型,直接影响启动时间、RAM 占用和调试手段。
3.1 单工程形态:APP 直接长在 FSBL 里
最简单的一种:应用程序和 FSBL 就是同一个工程,编译出一个 bin,签一次名,烧一次。BootROM 搬完,跳过去,你的main()就在 SRAM 里跑起来了。文档里那个串口 demo 走的就是这条路。
它的优点是链路上没有任何中间环节,出问题的地方少,适合开发早期验证板子和工具链。代价是受 512KB 限制约束——这个数字后面单独讲。另一个隐性代价是:这种形态下「FSBL」这个名字其实名不副实,它没有在 loader 任何东西,只是恰好被 BootROM 当作第一段代码搬进去了而已。
3.2 Load & Run:把 code 和 data 一起搬进 SRAM
第二种形态才是教科书意义上的两段式启动。BootROM 搬 FSBL 并跳转;FSBL 醒来后,把外部 Flash 里的应用程序完整内容——注意是完整的,代码段和数据段都算——拷贝到内部 SRAM,然后跳转到 APP 的入口。
/* FSBL 中 Load & Run 的关键动作,示意写法 */ typedef struct { uint32_t load_addr; /* APP 在 SRAM 中的落点 */ uint32_t entry_addr; /* APP 入口,通常是 load_addr + 4 取值 */ uint32_t size; /* 需要搬运的字节数 */ } app_image_t; int fsbl_load_and_run(const app_image_t *img) { /* 1. 配置外部 Flash 控制器,使其可被随机读 */ ext_flash_init(); /* 2. 整段搬运:code + rodata + data 一次到位 */ ext_flash_read(img->load_addr, (void *)img->load_addr, img->size); /* 3. 数据段此时已在 SRAM,无需再做 copy-down */ /* 4. 关中断、清 Cache、设 MSP,再跳 */ __disable_irq(); __set_MSP(*(uint32_t *)img->load_addr); ((void (*)(void))(*(uint32_t *)(img->load_addr + 4)))(); return 0; }参数size的取值不是拍脑袋的,它对应 APP 工程链接脚本里 ROM 区实际占用的字节数,多搬是浪费启动时间,少搬是随机崩溃。第 3 步那句注释是这条路径最大的好处:因为 data 段是被「搬」过去的而不是「拷贝初始化」的,传统启动文件里那段.data从 Flash 拷到 RAM 的代码可以直接省掉,链接脚本也可以写得更简单。代价是 SRAM 被吃满——APP 有多大,SRAM 就得留多大,而且启动时间是搬运时间,外部 Flash 读得慢,启动就慢。
3.3 XiP:让取指发生在外部 Flash 的映射地址上
第三种形态把搬运这一步整个砍掉。FSBL 醒来后不做拷贝,只做一件事:把外部 Flash 控制器配成内存映射模式(memory-mapped / XiP 模式),让外部 Flash 的一段地址空间直接映射到 CPU 的取指总线上,然后把 PC 指向 APP 在外部 Flash 里的第一条指令。之后每一次取指,硬件自动去外部 Flash 读,软件层面完全感知不到。
这条路省掉了 SRAM 占用,APP 想做多大做多大,也不再有搬运耗时。但它对硬件配置极其敏感:读时序参数、等待周期、地址映射窗口,任何一项配错,现象都是取指返回全 0 或者全 0xFF,CPU 跑飞。这条路上没有中间态,要么配置完全正确,要么直接不启动。
| 形态 | 代码执行位置 | SRAM 占用 | 启动耗时 | 受 512KB 限制 | 典型用途 |
|---|---|---|---|---|---|
| 单工程 FSBL | 内部 SRAM2 | 全部 | 最短 | 是 | 开发验证、小应用 |
| Load & Run | 内部 SRAM | 全部 | 搬运耗时 | 否 | 中小应用、需快速执行 |
| XiP | 外部 Flash 映射区 | 仅数据 | 几乎为零 | 否 | 大应用、成本敏感方案 |
三种形态在文档里是并列的三张图,但在工程决策上是有先后的:先看 APP 体积是否越过 512KB,越过了就必须离开单工程形态;再看对启动时间和 SRAM 余量的要求,在 Load & Run 和 XiP 之间选。选 XiP 之前,最好先确认外部 Flash 的读带宽撑得住你的实时性要求,否则省下的 SRAM 会以执行效率的形式还回去。
4. 512KB 的硬上限:BootROM 为什么不肯多搬一点
文档里那句话写得很轻:「BootROM 最大只能拷贝 512K 字节」。但它其实是整个 STM32N6 启动架构的分水岭,直接决定了你什么时候必须引入第二个工程、什么时候必须写 External Loader。理解这个数字从哪来,比记住它有用。
4.1 搬运能力被 ROM 面积锁死
BootROM 是掩膜在硅片上的,它的代码和数据缓冲都要占面积,而 ROM 面积直接换算成芯片成本。芯片厂商在这里的取舍很明确:BootROM 只保证「能把第一段代码搬进 SRAM 并跳过去」,不承诺做一个通用的加载器。512KB 就是它对单次搬运量的承诺上限。
这条限制的推论很直接:当你的工程编译出来超过 512KB,单工程形态就不可行了,必须切到 Load & Run 或 XiP,让 FSBL 保持精瘦(远小于 512KB),把大块头应用交给 FSBL 去加载。文档里「是不是所有 APP 都塞进 FSBL 里只烧一个工程」这个提问,答案就是被这条限制否掉的。
这里有个容易混淆的点:512KB 限制约束的是BootROM 单次搬运的载荷长度,不是你的应用体积上限,也不是 SRAM 容量。切到两段式之后,应用想多大都行,只是加载它的责任转移到了 FSBL 身上。
4.2 链接地址和烧录地址:两个完全不同的概念
在这套架构里,一个 bin 文件会同时牵涉两个地址,混淆它们是新手最常见的翻车点。
链接地址(load address)由工程的链接脚本决定,它告诉编译器和链接器「这段代码认为自己运行时位于哪里」。Dev boot 阶段从 RAM 调试,链接地址就是 SRAM 地址;Load & Run 形态下同样是 SRAM 地址;而 XiP 形态下,链接地址必须改成外部 Flash 的映射基址,否则所有绝对跳转都会跳到错误位置。
烧录地址则是你告诉烧录工具「把这个文件写到介质的哪个偏移」。文档里那条烧录命令的末尾0x70000000就是这个东西——外部 Flash 在地址空间里的映射基址。
# 烧录 FSBL 到外部 Flash,注意末尾的地址参数 STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=1 \ -el $DKEL -hardRst \ -w FSBL-trusted.bin 0x70000000逐项拆开:-c port=SWD指定用 SWD 调试口连接;mode=HOTPLUG ap=1表示在目标已上电运行的状态下挂接,不执行复位后 halt,ap=1选访问端口;-el $DKEL指定 External Loader,也就是一段跑在 SRAM 里、专门负责「怎么跟这颗外部 Flash 说话」的驱动小程序;-hardRst让工具在写完后拉一次硬件复位;-w后面跟文件名和目标地址。
正因如此,文档里那句提示非常关键:建议烧 bin 并显式指定地址,而不是直接烧 hex。因为工程编译出来的 hex 里,段的默认地址通常是 SRAM(那是你调试时的链接地址),烧录工具会老老实实按 hex 里的地址写,结果就是写进了一片根本不该写的区域,或者直接报地址越界。这是这条链路上最高频的错误来源之一。
5. 从 Dev boot 到 Flash boot:一次启动失败该怎么排查
启动链路一长,故障点就多。Dev boot 模式跑得好好的工程,切到 Flash boot 就死,是这块板子上最典型的场景。把踩过的坑按「现象 → 原因 → 解决」整理出来,比读十页手册管用。
5.1 五条血泪踩坑记录
现象:Dev 模式下调试一切正常,切到 Flash boot 后完全无输出,串口一片死寂。原因:烧进去的是未签名的FSBL.bin。BootROM 校验 Header 失败,根本不会搬运,更不会跳转。 解决:确认烧录的是签名后的产物。签名命令必须跑一遍,输出文件名通常带-trusted后缀以示区分,烧的就是这个文件。
现象:签名跑过了,切 Flash boot 依旧不启动。原因:Header 版本号和芯片 Cut 版本对不上。Cut1.1 要用-hv 2.1,Cut2.0 要用-hv 2.3,填错时 BootROM 的版本检查直接拒绝。 解决:先确认板子上芯片的实际 Cut 版本,再选对应命令。签名时加-dump参数把 Header 打出来看一眼版本字段,比反复试快得多。
现象:CubeProgrammer 里烧 hex 成功,重启不跑;改成烧 bin 就正常。原因:hex 里记录的地址是 RAM 地址(调试时的链接地址),不是外部 Flash 的0x70000000。 解决:不要烧 hex,烧 bin 并显式带地址参数。如果非要用 hex,得先把工程链接地址改成外部 Flash 映射基址再编译。
现象:烧录命令报错「找不到 External Loader」。原因:环境变量DKEL没设置,或者STM32_Programmer_CLI所在目录不在 PATH 里。 解决:把 External Loader 的路径配成环境变量并在命令里用%DKEL%(Windows)或$DKEL(Linux)引用;把 Programmer CLI 目录加进 PATH。自绘板没有现成的 External Loader,需要自己按 Flash 型号写一个,这一步绕不过去。
现象:IAR 下点下载后程序不跑,或者一进调试就复位循环。原因:Reset 方式设成了 Hardware,而 Dev boot 模式下 BootROM 已经跳过一次,硬件复位会把状态打回去。 解决:把 Reset 方式改成 Software,退出调试,重新下载再进调试。改完这个选项后如果不退出重进,设置不生效——这一条我踩过不止一次。
5.2 attach 与 Reset 方式:调试链路上的两个开关
文档里提到一个很有价值的细节:在 Cut1.1 版本的芯片上,Flash 启动模式并不会关闭调试口,所以可以用 "attach to running target" 的方式连上去调试一个已经在跑的镜像。这个能力在排查「启动到一半挂了」的问题时非常好用——你不需要让程序在复位后停下来,而是让它自然跑起来,再挂上去看它卡在哪。
它的前提是芯片的调试口在 Flash boot 模式下仍然使能。这一点在不同 Cut 版本上行为可能不同,所以如果你的板子挂不上,先别怀疑代码,先确认这颗 Cut 的调试口策略。另外要注意 attach 上去之后不要随手点复位,否则等于把系统打回起点,之前跑到的状态全丢。
提示:签名与烧录是两个独立环节,Dev boot 调试阶段完全不需要签名,只有走外部 Flash 启动才需要。把这两件事分开,能省掉大量「到底哪一步错了」的排查时间。
6. 把「能不能启动」变成可验证的工程习惯
这套两段式启动的设计取舍其实很清楚:把「信任」和「加载」彻底分离。BootROM 只做最小可信集的校验与搬运,代码量小到可以掩膜固化、几乎不可能有 bug;所有和板子相关的复杂度——时钟、外部 Flash 时序、应用加载策略——全部下推到 FSBL,让它可以随板子改、随应用改。代价是多了一个必须被正确签名和放置的中间产物,以及一条更长的故障链。
对比一下两种极端做法会更清楚。假如把加载逻辑全部塞进 ROM,芯片确实能做到「烧进去就跑」,但外部 Flash 型号千千万,ROM 里不可能预置所有时序;假如完全不要 BootROM,让 CPU 直接从外部 Flash 取第一条指令,那启动的第一毫秒就在跑未经任何校验的代码,安全和可靠性都无从谈起。两段式是这个矛盾下的中间解,而 512KB 就是它愿意承担的复杂度上限。
想验证自己是否真的理解了这条链路,有个很朴素的办法:拿一份签名后的 bin,用签名工具的-dump把 Header 打出来,逐字段对照你的链接脚本——载荷长度对不对、目标地址是不是0x34180000、版本号是不是当前 Cut 对应的那个。三个字段全对上,说明你对「包裹」的理解是实的;对不上,Flash boot 一定会有症状,只是早晚。
进阶一点的方向是 External Loader。文档明确说了自绘板需要自己开发,它本质上是一段跑在 SRAM 里的 Flash 驱动,需要实现擦除、写入、校验几个回调。写完它,你才算真正掌握了「从 PC 到外部 Flash」这最后一段路。
从那以后我每次改链接脚本或者换 Flash 型号,都会强制走一遍「签名 → dump Header → 核对三个字段 → 烧 bin 带地址」这个流程,哪怕只是改了一个段大小。这条链路上的坑,几乎全部来自某个本该对上的数字悄悄错位,而它们从来不会在 Dev 模式下暴露。希望帮到你。
本文还有配套的精品资源,点击获取