ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32N6 FSBL启动链详解:BootROM、签名烧录与三种架构

STM32N6 FSBL启动链详解:BootROM、签名烧录与三种架构

简介:面向ARM架构MCU开发者与嵌入式技术爱好者的STM32N6启动流程技术文档,重点解决新型带NPU芯片引入FSBL概念后,开发者对启动模式选择、镜像签名和工程组织方式不熟悉的问题。内容围绕BootROM执行、Dev boot与Flash boot/Serial boot两种启动模式展开,逐条梳理仅运行FSBL、FSBL装载并运行应用(Load & Run)、以及外置Flash原地执行(XiP)三种典型流程,并说明用ST Signing Tool为二进制镜像添加头部签名的具体命令与Cut版本差异,末尾附IAR配合STM32N6 DK板的完整示例工程搭建与部署指引。资源包为1个PDF文件,约2.07MB,图文对照便于随查随用。已有162人学习,适合希望掌握STM32N6特性、理清BootROM搬运限制与工程框架选型的中高级开发者参考。

1. 一块没有内部 Flash 的 MCU,上电后到底在跑什么

板子焊好、工程编过、调试器里单步跑得好好的,一拔调试器重新上电,串口一声不吭——这是我第一次碰 STM32N6 时的真实场面。原因不在代码,而在这颗芯片的存储器布局:它内部只有一小块 ROM 用来放第一阶段的 BootROM,用户代码必须待在外部的 NOR Flash 里,或者靠 USB、UART 之类的串口灌进去。于是「上电之后谁去把代码搬进来」就成了绕不过去的一环,而扛这件事的角色就是 FSBL(First Stage Boot Loader)。这份应用笔记围绕 STM32N6 的启动模式、三种 FSBL 工程架构、Binary Signing 签名流程和 Flash Programming 烧录操作,把一条完整的启动链摊开讲了一遍。如果你是刚从 F4、H7 那种自带 Flash 的型号迁过来的开发者,第一件要改的习惯就是:程序不再「下载完就能跑」,中间多了一道签名和一段搬运代码。

2. 启动模式与 BootROM:Boot0/Boot1 两个引脚定生死

拿到开发板先别急着点编译,先看清板子上那两个 Boot 跳线帽的位置。STM32N6 用 Boot1 引脚区分两种大方向:一种是 dev boot,上电后 BootROM 直接从内部 RAM 启动,配合调试器往 SRAM 里灌代码,适合开发阶段反复改;另一种是 Flash boot / Serial boot,BootROM 会去外部 Flash 找应用,找不到就什么都不发生。很多人第一次翻车就翻在这里——代码明明编译成功、调试也正常,切到 Flash 启动却黑屏,因为外部 Flash 里压根没有东西,或者有东西但过不了校验。

2.1 Dev boot 与 Flash boot 的引脚组合

把两种模式的差别用表格摆清楚,比在板子上反复试要快得多。

模式Boot1Boot0BootROM 的行为典型用途
Dev boot10从内部 RAM 启动,等待调试器灌入代码日常开发、单步调试
Flash boot00校验并搬运外部 Flash 中的 FSBL量产、脱机运行
Serial boot01通过 USB / U(S)ART 接收镜像救砖、产线首次烧录

开发板(DK 板)和 Nucleo 板的跳线位置不一样,但逻辑是同一套。我在项目里养成的习惯是:只要换了一块新板子,先拿万用表把 Boot 引脚的实际电平量一遍,再对照表格确认模式,不要迷信丝印。

切到 dev boot 之后还有一个容易忽略的动作——某些板子需要重新上电而不是按复位键,因为 Boot 引脚只在复位释放的那一瞬间被采样。如果你按了复位还是进不去 dev 模式,先断电再上电试一次。

2.2 BootROM 干的三件事:验头、搬运、跳转

理解了 BootROM 的行为,后面所有「为什么必须签名」「为什么只能拷 512KB」的疑问都会自动消解。BootROM 固化在片内 ROM 里,改不了,它做的事情只有三步:

第一步,读外部 Flash 起始位置那段固定长度的 Header,也就是镜像头部 1024 字节的元信息,做完整性和合法性校验。校验不过就直接放弃,表现就是「上电毫无反应」,连个错误码都不会给你。

第二步,校验通过后把 FSBL 程序搬到内部 SRAM2 的 0x34180000 地址。注意这个地址是固定的,你的 FSBL 工程链接地址必须和它对得上,否则搬过去之后 PC 跳过去执行的就是一堆错位的指令。

第三步,把 PC 指针指到 0x34180000,FSBL 开始跑。这时候才轮到你写的代码接管系统。

提示:BootROM 的搬运能力有上限,最大 512KB。这个数字后面会反复出现,是决定架构选型的关键约束。

因为第一步的存在,任何要放进外部 Flash 的镜像都必须先经过签名工具处理,把 Header 补齐、把头信息算对。调试阶段之所以可以不签名,是因为 dev boot 走的是另一条路径,BootROM 根本没去读外部 Flash,也就没有校验这回事。这一点很容易让人产生错觉,以为「我的工程不需要签名」,等切到 Flash 启动才发现少了关键一步。

3. 三种启动架构拆解:FSBL 单体、Load&Run 与 XiP 怎么选

FSBL 不是一个必须存在的独立工程,它更像一个「可以装东西的壳」。根据应用代码放在哪里、什么时候搬,官方给出了三种典型架构。选错架构的代价不小:要么被 512KB 卡死,要么白搬一遍代码浪费启动时间,要么在外部 Flash 上取指慢到影响实时性。

3.1 只有 FSBL:最省事,但有 512KB 的隐形天花板

最简单的一种,把应用程序直接编进 FSBL 工程里,编译出一个 bin,签名后烧到外部 Flash。上电后 BootROM 校验并搬运,FSBL 跑起来就是你的应用,中间没有第二次跳转。

这种架构的好处是链路最短、调试最直观,示例工程Projects\STM32N6570-DK\Examples\UART\UART_HyperTerminal_IT就是这种形式,只有一个 FSBL 工程,适合拿来验证工具链、跑通第一个串口打印。

代价是那个 512KB 的上限。BootROM 一次只能搬这么多,工程一旦超了,多出来的部分根本不会被搬进 SRAM,运行起来就是随机崩。所以这种架构只适合功能裁剪得很干净的固件,或者纯粹作为 bring-up 阶段的验证工程。我在实际项目里通常把它当作「地基」——先用它确认签名、烧录、启动这条链没问题,再往上叠真正的应用。

3.2 FSBL + Load&Run:把 code 和 data 都搬进 SRAM

当应用超出 512KB,或者需要把启动逻辑和应用逻辑解耦,就要拆成两个工程。FSBL 保持精简,只负责搬运和跳转;应用单独编译,链接到内部 SRAM 的运行地址;FSBL 运行时把应用的完整内容——包括代码段和数据段——从外部 Flash 拷到内部 SRAM,然后跳过去执行。

搬运逻辑常见写法是这样:

/* FSBL 主体:把外部 Flash 中的 APP 镜像整体搬到内部 SRAM,再跳转执行 */ #define APP_FLASH_ADDR 0x70000000U /* APP 在外部 Flash 中的存放基地址 */ #define APP_RUN_ADDR 0x34000000U /* APP 链接时约定的运行地址(内部 SRAM) */ typedef void (*app_entry_t)(void); extern uint8_t _app_img_start[]; /* 镜像在 Flash 中的起始,由链接脚本或符号表给出 */ extern uint8_t _app_img_end[]; /* 镜像在 Flash 中的结束 */ static void copy_app_to_sram(void) { uint32_t size = (uint32_t)(_app_img_end - _app_img_start); uint8_t *src = (uint8_t *)APP_FLASH_ADDR; uint8_t *dst = (uint8_t *)APP_RUN_ADDR; /* 按字拷贝比按字节快,注意源地址和目的地址都要求 4 字节对齐 */ for (uint32_t i = 0; i < size; i += 4U) { *(uint32_t *)(dst + i) = *(uint32_t *)(src + i); } } static void jump_to_app(uint32_t entry) { app_entry_t app = (app_entry_t)entry; __disable_irq(); /* 跳转前关总中断,避免 FSBL 的中断打进 APP 的向量表 */ SysTick->CTRL = 0U; /* 关掉 SysTick,防止 APP 重新配置时打架 */ SCB->VTOR = entry; /* 向量表基址指向 APP 的向量表 */ __set_MSP(*(uint32_t *)entry); /* 向量表第一个字就是 APP 的初始主栈指针 */ app(); }

这段代码有两个关键点值得说开。第一,APP_RUN_ADDR必须和 APP 工程自己的链接地址完全一致,FSBL 拷到哪里、APP 认为自己在哪里,两边一旦对不上,全局变量和函数指针立刻全废。第二,__set_MSP那一步是很多人的盲区——跳转前必须把主栈指针换成 APP 的初始值,否则 APP 用的还是 FSBL 的栈空间,深度一上来就踩穿。

至于镜像大小怎么算,取决于你用 IAR、Keil 还是 GCC。IAR 下可以在.icf里导出一个__image_size之类的符号,GCC 下就是在.ld里定义_app_img_start/​_end。不管哪种,思路都是让链接器把「镜像有多大」这件事变成一个编译期常量,而不是运行时去猜。

3.3 FSBL + XiP:让外部 Flash 直接变成取指空间

第三种是 XiP(Execute in Place)。FSBL 跑起来之后不搬应用,而是把外部 Flash 的控制器配置成可以直接取指的模式,然后把 PC 跳到外部 Flash 中应用的第一条指令。代码原地执行,SRAM 完整地留给数据和堆栈。

好处很直接:启动快,省掉了整段拷贝时间;SRAM 占用小,对大内存需求的应用特别友好。代价是取指延迟。外部 Flash 再快也有访问周期,遇到频繁跳转的代码或者高实时性中断,性能表现和片内 SRAM 不是一个量级。所以选 XiP 之前,最好先评估一下应用的时序敏感程度,别等跑起来发现中断响应超标再回头改架构。

配置环节常见的做法是在 FSBL 里初始化外部存储控制器(XSPI 一类),设置好命令序列和时序参数,再切换内存映射模式。这部分参数不同板子、不同 Flash 型号都不一样,必须照着自己硬件的手册来。

3.4 三种方案怎么选

把三种架构放在一起对比,选型会清晰很多:

架构应用存放位置512KB 限制启动耗时SRAM 占用适用场景
仅 FSBL与应用同工程受限于 BootROM最短小bring-up 验证、极小固件
FSBL + Load&Run外部 Flash,运行时搬入 SRAM不受限中等(含拷贝)大大多数功能完整的应用
FSBL + XiP外部 Flash,原地执行不受限短小大内存需求、代码以外设驱动为主

我的默认选择是 Load&Run:性能可控、调试链路清晰、SRAM 够用。只有在对启动时间有硬要求、或者 SRAM 实在吃紧的时候,才会切到 XiP。

4. 签名与烧录实操:从 bin 到外部 Flash 0x70000000

前面讲的是原理,这一章全是手上的活儿。从工程编译出 bin,到它躺在外部 Flash 的 0x70000000 地址上被 BootROM 认出来,中间要过签名和烧录两道关,任何一道出错的表现都差不多——上电没反应。所以每一步做完都要有可验证的输出。

4.1 Binary Signing:给 bin 补一个 BootROM 认得的头

签名工具是随烧录软件一起装的命令行程序,用之前先做两件准备工作:把可执行文件的目录加进系统 PATH,确保命令行能直接找到它;再确认手里的芯片是哪个 Cut 版本,因为这决定 Header Version 传 2.1 还是 2.3。

# Cut1.1 版本芯片的签名命令 STM32MP_SigningTool_CLI.exe -bin FSBL.bin -nk -of 0x80000000 -t fsbl \ -o FSBL-trusted.bin -hv 2.1 -dump FSBL-trusted.bin # Cut2.0 版本芯片的签名命令,只差 header version STM32MP_SigningTool_CLI.exe -bin FSBL.bin -nk -of 0x80000000 -t fsbl \ -o FSBL-trusted.bin -hv 2.3 -dump FSBL-trusted.bin

参数逐个说清楚:-bin指原始 bin 文件,也就是 IDE 编译出来的那个;-nk表示 no keys,本次不使用密钥相关选项,走的是最简路径;-of是 option flags,这里固定传 0x80000000;-t fsbl告诉工具这个 bin 的类型是 fsbl,工具会按 FSBL 的格式去构造 Header;-o是输出文件名;-hv是 header version,前面提到的 Cut 版本差异就体现在这个值上;-dump会把生成文件的 Header 信息打印出来,方便肉眼看一遍。

-dump这个参数不要省。它是整条链路上唯一能让你「看到」Header 的手段,尤其当你改过链接地址、换过芯片版本之后,扫一眼 dump 出来的字段比盲猜快得多。我一般会把 dump 结果存成文本一起放进版本库,出了问题能第一时间对比是不是签名环节变了。

4.2 Flash Programming:External Loader 与命令行烧录

烧进外部 Flash 靠的是 External Loader,这是给烧录软件用的一小段算法程序,负责告诉工具「怎么擦、怎么写这颗外部 Flash」。开发板和 Nucleo 板自带现成的 Loader,自绘板子就得自己开发一个,那是另一个话题。

命令行烧录之前先做准备:开发板切到 dev 模式;把 External Loader 的路径设成环境变量(示例里用的名字是DKEL);再把烧录工具的目录加进 PATH。

# Linux 下的写法 STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=1 -el $DKEL \ -hardRst -w FSBL-trusted.bin 0x70000000 # Windows 下环境变量换成 %DKEL% 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选访问端口。-el指定 External Loader。-hardRst让工具在写完之后拉一次硬件复位。-w后面跟文件名和写入地址,0x70000000就是外部 Flash 的映射基地址。

注意:烧 bin 一定要显式给地址。工程编译出来的 hex 默认链接地址通常是内部 RAM,直接烧 hex 会写错地方,这是最容易出错的一步。

4.3 图形化烧录:CubeProgrammer 里的两个下拉框

不喜欢命令行的,用图形化工具一样能完成。操作路径是:先把 Boot1 切到 dev 模式再连接,然后做三件事——选对 External Loader(这一步选错,后面的擦写全是错的)、下载 hex 文件、或者下载 bin 文件并手动填 0x70000000 这个地址。

hex 和 bin 的差别在这里体现得很明显:hex 自带地址信息,理论上可以不带地址直接烧,但前提是它的链接地址就是外部 Flash 的地址;bin 是纯二进制,地址必须由你给。所以如果你不打算改工程链接脚本,又想把程序放进外部 Flash,走 bin 加显式地址这条路最稳。

烧完之后把 Boot 引脚切回 Flash 启动位置,重新上电。如果一切正常,串口应该有输出了,行为和调试阶段一致。另外在较早的 Cut 版本上,Flash 启动模式并不会关闭调试口,可以用 attach to running target 的方式挂上去继续看变量和断点——这个特性在排查「烧录成功但行为不对」时特别有用,等于给了你一次在线对账的机会。

5. 避坑排查:外部 Flash 启动失败的 5 种典型现场

这一章全是踩出来的。现象、原因、解决三件套,照着对号入座,能省掉大半天。

现象一:调试器里一切正常,切到 Flash 启动后串口毫无输出,芯片像没上电。原因:镜像没有签名,或者 Header Version 和芯片 Cut 版本不匹配,BootROM 校验直接失败并静默放弃。 解决:用-dump把生成的 trusted bin 的 Header 打出来,确认版本字段;再对照芯片丝印或工具读出的 Cut 版本,把-hv改成 2.1 或 2.3 重新签一次。别指望这时候有错误提示,BootROM 在这一步不会告诉你原因。

现象二:烧录工具报地址越界或者写进去后程序跑飞,PC 停在一个奇怪的位置。原因:烧的是 hex,而 hex 的链接地址是内部 RAM,工具老老实实按 hex 里的地址写,写到了 RAM 空间或者干脆没落到外部 Flash。 解决:改成烧 bin,并显式指定 0x70000000。如果坚持用 hex,就必须改工程的链接配置,让输出地址直接指向外部 Flash。

现象三:一进调试就断不下来,或者程序跑几条指令就进 HardFault。原因:调试器的 Reset 方式不是 Software。芯片的启动流程决定了复位策略必须和 ROM 的行为配合,默认的硬件复位方式在这里不适用。 解决:在调试配置里把 Reset 改成 Software,退出调试,重新下载,再重新进入调试会话。改完记得保存配置,不然下次开工程又回到默认值。

现象四:工程越做越大,某次加完一个模块后设备再也起不来,连串口都不打印。原因:撞上了 BootROM 的 512KB 搬运上限。超出的部分根本不会被搬到 SRAM2,代码段只搬了一半,执行到缺口自然就崩。 解决:看一眼编译输出的 bin 大小。超了就必须换架构,按第 3 章的思路拆成 FSBL + Load&Run,让 FSBL 保持精简,应用自己搬自己。

现象五:命令行烧录提示找不到 External Loader,或者擦除阶段直接失败。原因:DKEL环境变量没设、设错路径,或者环境变量改了但当前终端没重启,读到的还是旧值。 解决:先在终端里 echo 一下变量确认值,再确认路径指向的是.stldr文件本身而不是它所在的目录。改完环境变量务必重开终端。

6. 二次开发改造:把示例引成自己产品的启动链

示例工程能跑通只是起点,真到产品里,你要动的是链接地址、向量表和启动时序这三处。我的做法是先把「仅 FSBL」这一版当作地基,确认签名、烧录、上电启动这条链完全跑通,再往上叠 Load&Run,一层一层加,哪一层出问题就退回上一层,不给自己留复合故障。

拆成双工程之后,第一件事是改 APP 的链接脚本,让它认为自己运行在 SRAM 的某个固定地址:

/* APP 的链接脚本片段:运行地址指向内部 SRAM,和 FSBL 里的 APP_RUN_ADDR 保持一致 */ MEMORY { SRAM_APP (rwx) : ORIGIN = 0x34000000, LENGTH = 2M } SECTIONS { .text : { *(.text*) *(.rodata*) } > SRAM_APP .data : { *(.data*) } > SRAM_APP .bss : { *(.bss*) *(COMMON) } > SRAM_APP }

ORIGIN这个值必须和 FSBL 里的APP_RUN_ADDR一字不差。我见过有人两边各改各的,一个写 0x34000000 一个写 0x34080000,编译全过,运行起来全局变量全是垃圾——这种错最难查,因为它没有任何编译期告警。

第二件事是向量表。APP 启动后要第一时间把SCB->VTOR设成自己的基址,否则中断触发时会跳到 FSBL 的向量表里:

/* APP 的 SystemInit 里补一句,确保中断向量指向自己 */ SCB->VTOR = (uint32_t)0x34000000U; __DSB(); __ISB();

第三件事是验证。别急着上板反复试,先做静态对账:-dump一份 Header、确认 bin 大小、确认链接地址、确认APP_RUN_ADDR三处一致。这四样对上了,再上板。上板之后用 attach 挂上去看 PC 是不是落在 APP 的范围里,比盯着串口等打印高效得多。

最后一个习惯:每完成一次可启动的镜像,我都留一份「黄金 bin」单独存着,附上签名参数和烧录命令。后面无论怎么改,只要新镜像起不来,先烧黄金 bin 确认硬件和链路没问题,再回头查代码。从那以后我每次改链接地址或者换芯片 Cut 版本,都强制走一遍「签名 - dump - 烧录 - 上电 - 串口」这条最小闭环,绝不跳步。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进