简介:这份面向ARM架构MCU开发者的技术文档,聚焦STM32N6内置NPU芯片的启动体系,适合希望理解BootROM、FSBL与外部存储协同机制的程序员和技术爱好者参考。内容围绕Dev boot与Flash boot/Serial boot两种启动模式,梳理仅运行FSBL、FSBL加载并运行应用、XiP就地执行三类流程,说明BootROM校验、搬运FSBL至内部SRAM及跳转执行等关键环节。文档还介绍用ST Signing Tool为镜像签名的方法,包括不同Cut版本对应的命令与参数含义,并提示BootROM最大拷贝512KB等限制。末尾以IAR与STM32N6 DK板为例,给出从工程创建到部署的示例流程及注意事项,便于建立可复现的开发路径。资源包含1个PDF文件,约2.07MB,已有162人学习,适合作为启动流程查阅与工程实践对照资料。
1. 一个“调试能跑、Flash 启动黑屏”的现场
上周帮人看一块 STM32N6 的板子,现象特别典型:IAR 里点调试,串口打印一切正常;把 Boot 引脚拨到 Flash 启动,上电后毫无反应,连异常电流都没有。代码没改一行,唯一的变化是启动介质。问题不在应用逻辑,而在 FSBL 这一层——这颗带 NPU 的 MCU 上电后先跑片内 BootROM,BootROM 只认带合法 Header 的镜像,你直接丢一个未签名的 bin 进外部 Flash,它连搬运都懒得搬运。这篇就顺着这条链路,把 Boot 模式、三种工程架构、镜像签名和 Flash 烧录拆一遍,适合刚上手 N6、准备把工程从 RAM 调试搬到外部 Flash 的嵌入式开发者。
2. 启动链路拆解:BootROM、Boot 引脚和那块 512K 的搬运窗口
2.1 Boot1/Boot0 决定从哪儿取第一条指令
STM32N6 片内只有一小块 ROM 存 BootROM,容量小到装不下用户程序,所以真正的用户代码必须放在外部 Flash,或者临时放在内部 RAM 里。上电那一刻,芯片靠 BOOT1、BOOT0 两个引脚判断去哪儿找代码。开发板上通常用一个拨码或跳线切换,逻辑是:
| Boot1 | Boot0 | 模式 | BootROM 的行为 |
|---|---|---|---|
| 0 | 0 | Flash boot | 从外部 Flash 读取并校验 FSBL,校验通过才搬运执行 |
| 1 | x | Dev boot | 直接从内部 RAM 的约定地址取指令,不做 Flash 校验 |
| 0 | 1 | Serial boot | 走 USB / U(S)ART 下载通道,用于救砖或量产烧录 |
Flash boot 是最终形态,Dev boot 是开发形态。这两者的差别不只是“从哪儿读代码”,而是校验环节的有无。Dev boot 下 BootROM 不验 Header,你编译完直接下载到 RAM 就能跑;Flash boot 下 BootROM 必须先验证 FSBL 开头那 1024 字节 Header,签名不对就直接停在那儿,串口一声不吭。前面那个黑屏现场,本质就是把开发形态的产物塞进了量产形态的入口。
我一般建议板子到手先做一件事:把两种模式的进入方式写在便签上贴到显示器边。开发板用跳线帽,Nucleo 形态的板子用焊盘或按键组合,各家板子不一致,但共同点是——切模式必须断电重启,热插拔拨码不生效,这点后面还会踩到。
2.2 BootROM 做的三件事:验头、搬运、跳转
Flash boot 模式下,BootROM 的执行顺序可以概括成固定三步:
- 从外部 Flash 的起始地址(N6 上通常是
0x70000000)读出镜像开头的 1024 字节 Header; - 用 Header 里的信息校验后面这段二进制的合法性,包括长度、校验值和 Header 版本;
- 校验通过后,把 FSBL 整段搬到内部 SRAM2 的
0x34180000,然后把 PC 指针指过去。
这里有两个容易被忽略的约束。第一,搬运目标地址是固定的,不是你想放哪儿就放哪儿,所以 FSBL 工程的链接脚本必须把代码段定位到0x34180000,链接错了搬过去也是乱码。第二,搬运窗口上限是 512K 字节,BootROM 只肯搬这么多。这个数字直接决定了后面几种工程架构怎么选:FSBL 加上应用如果超过 512K,单工程方案就走不通了,只能拆成两级,让 FSBL 自己去搬剩下的部分。
签名这一步在文档里被反复强调,原因就在第 2 步。BootROM 不认“没穿衣服”的 bin,它只认带 Header 的 trusted bin。你编译出来的是FSBL.bin,签名之后才是FSBL-trusted.bin,烧进 Flash 的必须是后者。新手最容易混的就是这两个文件名,看着差不多,烧错了就是黑屏。
2.3 地址地图:0x34180000 和 0x70000000 分别是什么
把地址摊开看,整条启动链路就清楚了:
| 地址/区域 | 用途 | 谁在用 |
|---|---|---|
| 片内 ROM | 固化 BootROM | 芯片出厂固化,不可改 |
0x34180000(SRAM2) | FSBL 的运行区,也是 BootROM 的搬运落点 | BootROM 搬运、FSBL 执行 |
| 内部 SRAM 其余区域 | 应用代码、data、栈 | Load & Run 架构下的 APP |
0x70000000(外部 Flash 映射区) | 存放 trusted 镜像;XiP 模式下直接作为代码区 | 烧录工具、XiP 架构 |
这张表解释了为什么烧录命令里地址是0x70000000:那是外部 Flash 在系统地址空间里的映射起点,不是 Flash 芯片内部的偏移。用 External Loader 烧写时,工具会把这段映射地址翻译成实际的 SPI/XSPI 时序写下去。
还有一层:应用在内部 SRAM 里调试时,链接地址和 Flash 启动后的链接地址通常不是一个。调试阶段代码在 SRAM,0x34180000往后排;要搬到外部 Flash 跑 XiP,就得重新链接到0x70000000。很多“烧进去不跑”的案例,根子是编译产物还是调试那一套地址,跟烧录工具没半点关系。
提示:切换架构时,先确认链接脚本里的 ROM 起始地址,再去动烧录参数。顺序反了,你会花半天怀疑 External Loader。
2.4 512K 天花板是怎么卡住工程结构的
512K 这个限制,决定了你什么时候必须拆工程。判断方法很直接:编译完看一眼 map 文件里的代码加只读数据总量。
如果 FSBL 和应用合在一个工程里,总量在 512K 以内,那 BootROM 一次搬完,最简单,也最省事,调试和发布是同一套产物。一旦超了,BootROM 搬一半就断在那儿,现象是程序跑飞或者卡死在某个外设初始化,很难往“镜像太大”上想。这时候只能拆成两级:FSBL 做得很小,只负责把外部 Flash 里的应用搬到 SRAM 或者配置成 XiP,应用本身多大都不影响 BootROM 的搬运量。
我在选型时习惯先按最坏情况估:带 NPU 的模型权重、图形资源、字库,这些只读数据涨得最快。宁可一开始就按两级架构搭,也不要等到超了 512K 再回头改链接脚本和启动流程,那个改动的连带面比想象中大——中断向量表偏移、初始化段拷贝、调试配置全都要跟着动。
3. 三种工程架构:单 FSBL、Load & Run 和 XiP 各自适合什么场面
3.1 架构一:应用程序直接编进 FSBL
这是最省心的形态,也最适合入门。SDK 里那个串口中断回显的例子就属于这一类,路径在Projects\STM32N6570-DK\Examples\UART\UART_HyperTerminal_IT下,整个工程只有一个 FSBL,应用逻辑直接写在里面。
启动链路是:BootROM 验头 → 搬到0x34180000→ 跳过去 → 你的main()开始跑。没有第二级跳转,没有额外的搬运代码,调试的时候下载到 RAM,发布的时候签名烧到 Flash,同一份代码两种用法。
它的边界也很清楚:整个镜像必须能塞进 512K,并且应用和引导逻辑耦合在一起,以后想单独升级应用就得整片重烧。做验证、做 demo、做外设 bring-up,这个架构最合适;产品化阶段如果功能会持续长,早点拆。
调试这一档有个细节值得单独说:虽然工程是 FSBL 工程,但调试阶段不需要签名。因为 Dev boot 下 BootROM 根本不校验 Header,你在 IAR 里点下载,走的是调试器直接写 RAM 的通路,跟 Flash 启动完全是两条路。很多人第一次看到“文档说必须签名”就卡住了,以为不签名连调试都不行,其实签名的必要性只在 Flash 启动时才成立。
3.2 架构二:FSBL 把 APP 整段搬进 SRAM
应用超过 512K,或者你希望引导和应用解耦,就上这一档。FSBL 自己很小,职责变成三件事:找到外部 Flash 里应用的位置、把应用的 code 和 data 完整拷贝到内部 SRAM、跳转过去执行。
跳转这一步的写法在各种 MCU 上大同小异,核心是取向量表里的复位入口地址。示意代码如下:
/* 应用镜像在 SRAM 中的起始地址,需与 APP 工程的链接脚本一致 */ #define APP_BASE_ADDR (0x34000000UL) typedef void (*app_entry_t)(void); void jump_to_app(void) { uint32_t app_sp; /* 应用栈顶,向量表第 0 项 */ uint32_t app_entry; /* 复位入口,向量表第 1 项 */ app_sp = *(volatile uint32_t *)(APP_BASE_ADDR); app_entry = *(volatile uint32_t *)(APP_BASE_ADDR + 4); /* 关全局中断,避免跳转瞬间有中断打进旧向量表 */ __disable_irq(); /* 重定位向量表,Cortex-M 系通过 SCB->VTOR 完成 */ SCB->VTOR = APP_BASE_ADDR; /* 设置栈指针后跳转,顺序不能反 */ __set_MSP(app_sp); ((app_entry_t)app_entry)(); }这段代码里有三个参数值得盯住。APP_BASE_ADDR必须和应用工程链接时的 ROM 起始地址完全一致,差一个字节都会跳飞;app_sp和app_entry必须是应用向量表的前两项,所以应用工程编译时不能把向量表挪走;SCB->VTOR的重定位要在开中断之前完成,否则中断会落到 FSBL 的旧表上。最后__set_MSP必须在函数调用前执行,顺序反了,压栈就把原来的栈写花了。
搬运本身建议走 DMA 或者按字拷贝,别用memcpy处理几 MB 的数据——在没有开 cache 的情况下慢得离谱,而且中途被打断没有恢复机制。拷贝之前记得把目标 SRAM 区域清理干净,尤其是复位后残留的旧数据,不清会偶发校验失败。
这个架构的代价是启动时间变长,而且 SRAM 被应用占满,运行时可用堆栈被压缩。做实时性要求高的场景要算一下搬运耗时,几百 K 的量级在几十毫秒级别,还能接受;上到 MB 就得考虑 XiP 了。
3.3 架构三:XiP 让外部 Flash 直接当代码区
XiP(Execute in Place)的思路是把外部 Flash 配置成内存映射模式,代码不用搬,PC 直接指着 Flash 里的地址取指。FSBL 的任务变成:初始化外部 Flash 控制器、把它切到 XiP 模式、然后跳到0x70000000附近应用的第一条指令。
好处很明显:没有搬运时间,SRAM 全留给数据和堆栈,应用体积只受 Flash 容量限制。对于带 NPU 的 N6 来说,模型权重和一些只读资源放在 Flash 里按需读取,比整段搬进 SRAM 划算得多。
代价在时序上。XiP 下的取指要经过外部 Flash 的读延迟,跑在高主频时必须有 cache 或者预取配合,否则性能掉得厉害。冷启动第一次取指还会有额外的等待周期。所以我把 XiP 用在“代码大、执行热点集中”的场合,热点函数通过链接脚本或手动放置放到 SRAM 里跑,冷代码留在 Flash,两边兼顾。
配置 XiP 的代码在 FSBL 里,具体寄存器操作各家的 External Loader 和 SDK 里都有现成实现,不建议自己从头写。要改的通常只有 Flash 型号相关的时序参数:读命令、 dummy cycle、时钟分频。换 Flash 芯片的时候,这三处对不上,现象是能烧进去但执行到某条指令就取指异常。
3.4 三种架构的对比与选型
| 架构 | 镜像大小上限 | 启动耗时 | SRAM 占用 | 适用场景 |
|---|---|---|---|---|
| 单 FSBL | 512K(BootROM 搬运上限) | 最短 | 全部给应用 | 外设验证、demo、小体积产品 |
| FSBL + Load & Run | 受 SRAM 容量限制 | 中等(含搬运) | 应用独占 | 中等体积、要求确定性执行 |
| FSBL + XiP | 受外部 Flash 容量限制 | 最短(无搬运) | 仅数据与栈 | 大体积代码、模型资源多的产品 |
选型时我按两个问题走:镜像会不会超 512K,超了之后更在意启动时间还是 SRAM 余量。两个都想要,就上 XiP 加热点函数搬 SRAM 的混合方案,这是我在中大项目里最常用的组合。
4. 签名与烧录:1024 字节 Header 和 External Loader 的配合
4.1 Header 里到底装了什么
FSBL 镜像开头的 1024 字节不是代码,是 BootROM 用来判断“这段东西能不能跑”的元数据。它记录了镜像长度、校验信息、Header 版本号等字段,签名工具负责按格式填好并追加到 bin 前面。BootROM 读到这段,先比对版本,再校验内容,全部通过才执行搬运。
版本这个字段是最容易出事的地方。芯片有不同的 Cut 版本,Cut1.1 和 Cut2.0 要求的 Header 版本不一样,前者用 2.1,后者用 2.3。用错了的表现是 BootROM 校验直接失败,连搬运都不开始,串口毫无输出。这一点在拿到新板子时第一件要确认的事——先看芯片的 Rev Code,再决定签名命令里-hv填什么。
4.2 签名命令逐参数拆开看
签名工具是命令行程序,安装 CubeProgrammer 之后在安装目录里能找到,需要把它的路径加进系统 PATH,否则命令行里敲不出来。签名之前先确认这一点,不然第一步就卡住。
Cut1.1 的芯片执行:
# Cut1.1 芯片:Header 版本 2.1 STM32MP_SigningTool_CLI.exe \ -bin FSBL.bin \ -nk \ -of 0x80000000 \ -t fsbl \ -o FSBL-trusted.bin \ -hv 2.1 \ -dump FSBL-trusted.binCut2.0 的芯片只需要改版本号:
# Cut2.0 芯片:Header 版本 2.3,其余参数保持一致 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,编译产物 | 换成你的工程输出路径 |
-nk | no keys,不使用密钥相关选项 | 常规开发保持默认 |
-of | option flags,示例固定0x80000000 | 换存储介质或启动方式时才动,一般不改 |
-t | 输入文件类型,这里是 fsbl | 签应用镜像时按工具支持的类型填 |
-o | 输出文件名 | 建议固定成xxx-trusted.bin,避免和原始 bin 混 |
-hv | Header 版本,对应芯片 Cut 版本 | Cut1.1 填 2.1,Cut2.0 填 2.3 |
-dump | 把生成文件的 Header 打印出来 | 建议每次都带上,多一层确认 |
我最常改的是-bin、-o和-hv三个,前两个是路径,第三个是芯片版本。把它们抽成脚本里的变量,比每次手敲命令可靠得多,尤其是团队里几个人轮流烧板子的时候。
4.3 用 -dump 把 Header 读回来验证
-dump这个参数看着不起眼,实际是省时间的。它会把你刚生成的 trusted bin 的 Header 字段打印到终端,你能直接看到版本号、长度这些值对不对。命令跑完不看输出,等于没验证。
判断标准很简单:版本号要和你填的-hv一致,长度要和原始 bin 加上 1024 字节对得上。如果长度差得离谱,多半是-bin指错了文件,或者上一次的输出被覆盖了。养成签名完扫一眼 dump 的习惯,能挡掉后面一大半“烧进去不启动”的排查时间。
提示:把签名命令和 dump 输出一起存进构建脚本的日志里。出问题时翻日志,比重新跑一遍猜参数快。
4.4 烧录:为什么推荐指定地址写 bin
外部 Flash 的烧写要靠 External Loader 完成,开发板一般自带对应的 loader 程序。如果用的是自己画的板子,Flash 型号不同,需要先做出匹配的 loader,这部分不在启动流程的范围内,但它是烧录能不能成功的前提。
命令行烧录前有三件准备工作:开发板切到 Dev 模式、把 loader 的路径配成环境变量(示例里用DKEL)、把烧录工具的目录加进 PATH。然后执行:
# Linux:把 trusted 镜像写到外部 Flash 映射地址 STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=1 \ -el $DKEL -hardRst \ -w FSBL-trusted.bin 0x70000000REM Windows:参数一致,只是环境变量写法不同 STM32_Programmer_CLI -c port=SWD mode=HOTPLUG ap=1 ^ -el %DKEL% -hardRst ^ -w FSBL-trusted.bin 0x70000000参数含义:-c后面是连接配置,port=SWD走调试口,mode=HOTPLUG表示不断电直接连,ap=1选访问端口;-el指定 External Loader 的路径变量;-hardRst让工具在烧写后做一次硬件复位;-w是写操作,后面跟文件名和目标地址。
地址0x70000000是外部 Flash 的映射起点,这个不能省。强烈建议用 bin 加显式地址的方式烧,而不是直接烧 hex。原因是工程编译出来的 hex 默认地址段往往在内部 RAM,直接烧进去等于把数据写到了错误的介质上,工具不报错,启动就是没反应。这个坑我见过太多次,属于“现象离谱、原因简单”的典型。
图形界面的 CubeProgrammer 流程一样:切 Dev 模式连上、选对 External Loader、烧 hex 时改地址或直接烧 bin 并填0x70000000。烧完把 Boot 拨回 Flash 启动,重新上电。Cut1.1 的芯片在 Flash 启动模式下调试口没有被关掉,可以用 attach to running target 挂上去看现场,这一点对排查启动早期的问题很有用。
5. 避坑排查:五条从调试正常到 Flash 启动失败的现场记录
5.1 现象:调试器里一切正常,切 Flash 启动后串口一个字都不打
原因:烧进 Flash 的是未签名的FSBL.bin,或者签名时-hv和芯片 Cut 版本对不上,BootROM 校验失败直接停住,不会跳到任何用户代码。
解决:确认烧录文件名是xxx-trusted.bin而不是原始 bin;用-dump看 Header 版本;对照芯片 Rev Code 选 2.1 或 2.3。三步都过了还是不启动,再去看是不是烧录地址写错了。
5.2 现象:烧录工具提示成功,重启后依旧跑的是旧程序
原因:直接烧了编译输出的 hex,而 hex 里的地址段指向内部 RAM,数据被写到 RAM 或者被工具重定位到了别处,外部 Flash 里那段还是旧的。
解决:改用 bin 加显式地址0x70000000烧写;如果坚持用 hex,在工具里手动把烧录地址改到外部 Flash 映射区,并且确认链接脚本本身也是按 Flash 地址链接的。判断有没有生效,可以烧一个带版本号打印的镜像,看串口输出对比。
5.3 现象:调试器连不上,或者连上后点运行立刻断开
原因:调试配置里的 Reset 方式选了 Hardware。N6 在 Dev 模式下对复位方式比较敏感,硬件复位会把调试连接一起断掉。
解决:把 Reset 改成 Software,退出调试会话,重新下载再启动调试。改完配置必须重新走一遍下载流程,只改配置不重下是不生效的。这条是我在 N6 上翻车次数最多的一次,改完以为立刻见效,实际得重连。
5.4 现象:External Loader 报找不到,或者烧写过程卡在擦除
原因:环境变量没配、变量名和命令里的不一致,或者板子不在 Dev 模式导致工具连不上目标。自己画的板子还可能是 loader 和实际 Flash 型号不匹配。
解决:先在命令行里 echo 一下环境变量,确认路径展开正确;确认板子已切 Dev 模式并重新上电;自绘板核对 Flash 型号,必要时自己做 loader。烧写卡在擦除这一步,八成是 loader 的时序参数和芯片对不上。
5.5 现象:程序能启动,但跑到某个外设初始化就取指异常
原因:用了 XiP 架构,但外部 Flash 的时序参数(读命令、dummy cycle、时钟分频)还是默认值,和实际焊接的 Flash 不匹配。
解决:换 Flash 型号时同步改 FSBL 里的 XiP 配置;先用最简单的读操作验证 Flash 能正确读回数据,再切 XiP;把热点函数放 SRAM,减少对 Flash 取指时序的依赖,也能降低这类问题的触发概率。
6. 二次开发:换 Flash、改地址和一套能回滚的验证习惯
真正上手改的时候,动的通常不是启动流程本身,而是它周边的三处配置。第一处是链接脚本里的 ROM 起始地址:走 XiP 就链到0x70000000,走 Load & Run 就链到应用在 SRAM 里的落点,两者不能混。第二处是 FSBL 里的搬运长度和源地址,应用体积变了这里必须跟着改,写死一个旧长度是最常见的隐性 bug。第三处是外部 Flash 的时序参数,换料必改。
改动之后怎么验证,我有一套固定顺序,走完基本不会漏:
| 步骤 | 验证内容 | 失败时先看什么 |
|---|---|---|
| Dev 模式下载运行 | 应用逻辑本身是否正确 | 外设初始化和中断向量表 |
| 签名并 dump | Header 版本、长度是否符合预期 | -hv参数、输入 bin 路径 |
| 烧到 Flash 后冷启动 | BootROM 能否校验并跳转 | 烧录地址、trusted 文件名 |
| 反复上下电十次 | 启动是否稳定 | Flash 时序、复位电路 |
| 换一块同型号板 | 是否与个体差异有关 | loader 配置、焊接 |
最后一步是我加进去的,起因是一次只在某块板子上偶发启动失败,查了两天,最后发现是那块板子的 Flash 焊接虚了。从那以后我每次改完启动配置,都强制走一遍“换板复测 + 连续十次冷启动”,不管多赶时间。这套流程枯燥,但它把“偶发”变成了“可复现”,省下来的排查时间远超执行成本。希望帮到你。
本文还有配套的精品资源,点击获取