简介:这份PDF文档面向从事ARM架构MCU开发的程序员与嵌入式技术爱好者,聚焦STM32N6这颗ST首款带NPU的MCU,系统讲解其启动机制与FSBL(First Stage Boot Loader)的基本流程,帮助开发者理解片内BootROM、外部Flash加载与Dev boot/Flash boot等启动模式的差异。文档详细拆解三种典型启动架构:仅运行FSBL、FSBL装载并运行应用(Load & Run)、以及在外置存储中直接执行代码(XiP),并说明BootROM对FSBL镜像512KB的搬运限制。同时给出使用ST Signing Tool对二进制镜像签名、添加Header的完整命令与参数含义,确保镜像安全合法运行。文末以IAR搭配STM32N6 DK板为例,提供从工程创建到部署的示例教程与注意事项。资源为1个PDF文件,压缩包约2.07MB,已有159人学习,适合希望快速掌握STM32N6新启动体系的开发者参考。
1. STM32N6 的 FSBL 到底在启动链里干了什么
第一次拿到 STM32N6 的板子,很多人会卡在一个反直觉的地方:代码明明烧进去了,上电却没有任何反应,调试器也连不上。原因往往不是硬件坏了,而是这颗芯片的启动方式和传统 STM32 完全不是一回事。STM32N6 内部没有可反复擦写的用户 Flash,程序必须放在外部存储介质里,上电后由芯片内部固化的 BootROM 先跑起来,再由它去加载一段用户自己写的引导程序,这段程序就是 FSBL(First Stage Boot Loader,一级引导加载程序)。FSBL 跑通之后,才会把真正的应用固件搬到内部 RAM 里执行。所以 FSBL 不是可选项,它是整条启动链里绕不过去的第一环。这篇笔记面向正在评估或已经上手 STM32N6 的嵌入式工程师,把 FSBL 的基本流程、关键配置和常见翻车点讲清楚,让你能自己搭出一个最小可跑的引导程序。
2. 启动链拆解:BootROM、FSBL 和应用固件怎么接力
2.1 为什么 STM32N6 必须要有 FSBL
传统 STM32 系列(比如 F4、H7)内部有 Flash,上电后 CPU 直接从 Flash 取指令,整个启动过程对用户是透明的。STM32N6 换了一条路:它把用户代码的存放位置挪到了片外,常见的是外部串行 Flash 或者通过外部存储接口挂载的存储介质。芯片内部只保留了一块 BootROM,这段代码是出厂固化、不可修改的,它负责最基础的硬件初始化和启动介质探测。
BootROM 做完自己的事之后,会去约定的位置读取一段头部信息,判断这段数据是不是合法的引导镜像。如果合法,就把它加载到内部 RAM 的指定地址并跳转过去执行。这段被加载的代码就是 FSBL。FSBL 拿到控制权之后,才有机会做更复杂的初始化——比如配置外部存储控制器、初始化 DDR、把应用固件从外部存储搬到内部 RAM 或者外部 DDR 里,最后跳转到应用入口。
这个设计带来的直接好处是灵活:应用固件可以很大,放在便宜的外部存储里;启动介质可以换,不用改芯片。代价就是多了一层引导,FSBL 写不对,后面全都跑不起来。
2.2 从复位向量到应用入口的完整时序
把启动过程按时间线拆开,大致是这么几步:
第一步,芯片上电或复位,CPU 从 BootROM 的复位向量开始执行。BootROM 会读取启动模式引脚(BOOT0、BOOT1 等)的状态,决定从哪个介质启动。STM32N6 支持多种启动介质,常见的是外部串行 Flash 和 SD 卡之类。
第二步,BootROM 按照选定的介质,去固定偏移位置读取引导头部。这个头部里包含了一些关键字段:镜像的加载地址、入口地址、校验信息、以及可选的签名信息。如果启用了安全启动,BootROM 还会验证签名,验证不过就直接停在 BootROM 里,不会跳转。
第三步,BootROM 把 FSBL 镜像搬运到头部里声明的加载地址。这个地址通常在内部 SRAM 范围内,因为此时 DDR 还没初始化,不能用。
第四步,BootROM 跳转到 FSBL 的入口地址,控制权交给用户代码。FSBL 开始执行,此时能用的资源只有内部 SRAM 和最基本的时钟。
第五步,FSBL 初始化外部存储控制器和 DDR(如果应用需要跑在 DDR 里),然后把应用固件从外部存储读到目标地址,做必要的校验,最后跳转到应用入口。
第六步,应用固件开始执行,FSBL 的使命结束。有些设计里 FSBL 跳转后不会返回,有些会保留一些运行时服务,这取决于具体需求。
整个链条里,FSBL 是唯一一段你能完全控制的代码,也是调试时最应该先跑通的部分。
2.3 最小 FSBL 工程需要哪些文件
一个能跑起来的最小 FSBL 工程,通常包含这几类文件:
| 文件类型 | 作用 | 常见命名 |
|---|---|---|
| 启动汇编 | 设置栈指针、跳转到 C 入口 | startup_xxx.s |
| 链接脚本 | 定义代码和数据的存放地址 | xxx_flash.ld 或 xxx_sram.ld |
| 时钟配置 | 初始化系统时钟和外设时钟 | system_xxx.c |
| 存储驱动 | 读写外部 Flash 或 SD 卡 | xxx_flash.c / sdmmc.c |
| 头部生成 | 生成 BootROM 能识别的引导头部 | 由工具或脚本生成 |
| 主逻辑 | 搬运应用固件并跳转 | main.c |
链接脚本是最容易出问题的地方。FSBL 自己被加载到哪个地址、栈放在哪里、堆放在哪里,都必须和 BootROM 读取的头部信息一致。如果链接脚本里写的加载地址和头部里声明的不一样,BootROM 搬过去之后跳转就会跑到错误的地址,表现为直接死机或者 HardFault。
3. 手把手搭一个最小 FSBL:从链接脚本到跳转应用
3.1 链接脚本里必须对齐的三个地址
链接脚本决定了 FSBL 镜像的内存布局。对于 STM32N6 的 FSBL,有三个地址必须和头部信息严格对齐:
第一个是加载地址(LOADADDR),也就是 BootROM 把 FSBL 镜像搬到内部 RAM 的哪个位置。这个地址必须在内部 SRAM 范围内,而且要避开 BootROM 自己使用的区域。常见做法是放在 SRAM 的起始位置往后偏移一段。
第二个是入口地址(ENTRY),也就是 BootROM 跳转过来执行的第一条指令地址。通常就是加载地址加上复位向量偏移。
第三个是栈顶地址(_estack),FSBL 运行时的栈指针初始值。栈必须放在可读写且不被其他数据占用的区域。
下面是一个简化后的链接脚本片段,展示这三个地址怎么定义:
/* 链接脚本片段:定义 FSBL 的内存布局 */ MEMORY { /* 内部 SRAM 区域,FSBL 加载和运行的地方 */ RAM (xrw) : ORIGIN = 0x34000000, LENGTH = 256K } /* 栈顶设在 RAM 区域末尾,向下增长 */ _estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { /* 代码段从 RAM 起始地址开始放置 */ .text : { . = ALIGN(4); KEEP(*(.isr_vector)) /* 中断向量表必须在最前面 */ *(.text) *(.text*) . = ALIGN(4); _etext = .; } > RAM /* 只读数据段紧随代码段之后 */ .rodata : { . = ALIGN(4); *(.rodata) *(.rodata*) . = ALIGN(4); } > RAM /* 已初始化数据段,运行时从加载地址复制到运行地址 */ .data : { . = ALIGN(4); _sdata = .; *(.data) *(.data*) . = ALIGN(4); _edata = .; } > RAM /* 未初始化数据段,启动时清零 */ .bss : { . = ALIGN(4); _sbss = .; *(.bss) *(.bss*) . = ALIGN(4); _ebss = .; } > RAM }这段脚本里,ORIGIN(RAM)就是加载地址的基准,_estack是栈顶。中断向量表放在最前面,因为 BootROM 跳转过来后,CPU 取的第一条指令就是向量表里的复位向量。如果向量表位置不对,第一条指令就取错了,直接跑飞。
参数说明:ORIGIN和LENGTH要根据具体芯片型号的内部 SRAM 大小来填,不能照抄。ALIGN(4)保证四字节对齐,ARM 架构下不对齐访问可能触发异常。KEEP防止链接器把向量表优化掉。
3.2 时钟和外部存储的初始化顺序
FSBL 跑起来之后,第一件事是配置系统时钟。STM32N6 的时钟树比传统系列复杂,有多个 PLL 和分频器。FSBL 阶段不需要把所有时钟都开到最高,够用就行,因为此时还在内部 SRAM 里跑,功耗和发热都不是主要矛盾。
时钟配置的一般顺序是:先启动内部高速时钟(HSI)作为临时时钟源,配置 PLL 倍频到目标频率,等待 PLL 锁定,然后切换系统时钟源到 PLL,最后配置外设总线时钟分频。这个顺序不能乱,PLL 没锁定就切过去,系统会跑飞。
时钟配好之后,才能初始化外部存储控制器。如果应用固件放在外部串行 Flash 里,FSBL 需要通过 QSPI 或类似接口去读。初始化流程通常是:配置引脚复用、配置控制器时钟分频、发送复位命令、读取 Flash ID 确认通信正常、配置读写模式。
/* 外部 Flash 初始化简化流程 */ void ext_flash_init(void) { /* 1. 配置 QSPI 引脚复用 */ gpio_config_qspi(); /* 2. 使能 QSPI 控制器时钟 */ rcc_enable_qspi_clk(); /* 3. 复位 QSPI 控制器 */ qspi_reset(); /* 4. 配置 QSPI 基本参数:时钟分频、采样模式 */ qspi_config_t cfg; cfg.clock_prescaler = 4; /* 分频系数,先设大一点保证稳定 */ cfg.sample_shift = QSPI_SAMPLE_SHIFT_NONE; cfg.flash_size = 24; /* 2^24 = 16MB,按实际容量填 */ qspi_init(&cfg); /* 5. 读取 Flash ID,确认通信正常 */ uint8_t id[3]; qspi_read_id(id, sizeof(id)); if (id[0] == 0xFF) { /* 读不到 ID,说明通信失败,需要检查引脚和时钟 */ error_handler(); } /* 6. 配置 Flash 为四线快速读取模式 */ qspi_enable_quad_mode(); }逻辑说明:先配引脚再开时钟,是因为引脚复用配置本身需要 GPIO 控制器时钟。复位控制器是为了清掉可能存在的残留状态。读 ID 是最简单的通信验证手段,ID 读不到,后面读数据肯定也不行。分频系数一开始设大一点(数值大表示分频多、时钟慢),通信稳定后再逐步提高速度。
参数说明:clock_prescaler的具体取值要看 QSPI 控制器时钟源频率和目标 Flash 支持的最高频率。flash_size填的是地址位宽,不是字节数,16MB 对应 24 位地址。sample_shift在高速通信时才需要调整,低速下用默认值即可。
3.3 把应用固件搬到 RAM 并跳转
外部存储能正常读之后,就可以把应用固件搬进来了。应用固件通常以二进制格式存放在外部 Flash 的某个固定偏移处,FSBL 需要知道三个信息:源地址(在外部 Flash 里的偏移)、目标地址(搬到内部 RAM 或 DDR 的哪里)、固件大小。
搬运过程就是循环读取再写入:
/* 从外部 Flash 搬运应用到目标地址 */ void load_application(uint32_t src_offset, uint32_t dst_addr, uint32_t size) { uint32_t remaining = size; uint32_t offset = src_offset; uint8_t *dst = (uint8_t *)dst_addr; while (remaining > 0) { uint32_t chunk = (remaining > 256) ? 256 : remaining; /* 从外部 Flash 读取一块数据到临时缓冲区 */ qspi_read(offset, temp_buf, chunk); /* 写入目标地址 */ memcpy(dst, temp_buf, chunk); offset += chunk; dst += chunk; remaining -= chunk; } }搬运完成后,还需要做一次校验,确认搬过来的数据和源数据一致。最简单的做法是算 CRC 或者校验和,和头部里记录的预期值比对。校验不过就不要跳转,否则应用跑起来也是错的,反而更难排查。
跳转本身很简单,把应用入口地址转成函数指针调用即可:
/* 跳转到应用入口 */ void jump_to_application(uint32_t app_entry) { /* 关闭中断,避免跳转过程中被中断打断 */ __disable_irq(); /* 设置主栈指针为应用的栈顶 */ __set_MSP(*(uint32_t *)app_entry); /* 取复位向量并跳转 */ void (*app_reset_handler)(void) = (void (*)(void))(*(uint32_t *)(app_entry + 4)); app_reset_handler(); }逻辑说明:跳转前关中断是必须的,因为 FSBL 的中断向量表和应用的中断向量表不一样,跳转后如果来了中断,会跑到错误的处理函数里。设置主栈指针是因为应用有自己的栈空间,不设置的话应用用的还是 FSBL 的栈,可能越界。取复位向量时偏移 4 个字节,是因为向量表第一个字是栈顶地址,第二个字才是复位向量。
参数说明:app_entry是应用固件在目标地址的起始位置,这个地址必须和应用的链接脚本一致。如果应用是位置无关代码,可以搬到任意地址;如果是固定地址链接的,就必须搬到链接时指定的地址。
4. 避坑指南:FSBL 调试中最容易翻车的五个地方
4.1 上电后完全无反应,调试器也连不上
现象:板子上电后没有任何输出,调试器无法连接芯片。
原因:最常见的是 BootROM 根本没找到合法的引导头部,或者头部校验失败,芯片停在 BootROM 里不跳转。另一种可能是启动模式引脚状态不对,BootROM 去错了介质找镜像。
解决:先用示波器或者逻辑分析仪看启动介质上的通信波形,确认 BootROM 有没有去读。如果没有读,检查启动模式引脚。如果有读但读到的数据不对,检查头部生成工具的参数是否和链接脚本一致。头部里的加载地址、入口地址、校验值,任何一个不对都会导致 BootROM 拒绝跳转。
4.2 跳转到应用后立刻 HardFault
现象:FSBL 能跑,串口有输出,但跳转后应用没有任何反应,调试器显示 HardFault。
原因:通常是栈指针没设置对,或者中断向量表偏移没配置。应用如果依赖自己的向量表,需要在跳转前设置 SCB 的 VTOR 寄存器指向应用的向量表。另外,应用如果用了浮点单元,FSBL 跳转前没有使能 FPU,应用一执行浮点指令就异常。
解决:在跳转函数里加上 VTOR 设置和 FPU 使能。VTOR 设置成应用的向量表地址,FPU 通过 SCB 的 CPACR 寄存器使能。这两个操作在跳转前完成,顺序是先设 VTOR 再使能 FPU,最后关中断跳转。
4.3 外部 Flash 读出来的数据全是 0xFF
现象:FSBL 初始化外部 Flash 后,读 ID 正常,但读数据全是 0xFF。
原因:0xFF 通常表示读到了空白区域或者通信时序不对。可能是读取命令用错了,比如 Flash 支持四线读取但代码里用的是单线命令;也可能是地址偏移算错了,读到了没有数据的区域。
解决:先用单线模式、低速时钟读一小段已知有数据的位置,确认基本通信没问题。然后逐步切换到四线模式、提高时钟。每次只改一个变量,改完验证一次。地址偏移要对照烧录工具的输出确认,烧录时写到了哪个偏移,读取时就从哪个偏移读。
4.4 FSBL 本身能跑但应用固件搬运后校验失败
现象:搬运过程没有报错,但校验和对不上,应用跳转后行为异常。
原因:可能是搬运过程中源地址和目标地址有重叠,导致数据被覆盖。也可能是外部 Flash 读取时没有处理等待周期,高速读取下数据还没准备好就被读走了。
解决:检查源地址和目标地址的范围,确保没有重叠。如果有重叠,要么改变目标地址,要么从后往前搬运。读取等待周期根据 Flash 手册里的时序参数来设,时钟越快等待周期越多。不确定的时候先把时钟降下来,稳定后再逐步提高。
4.5 改了链接脚本后 FSBL 就跑不起来了
现象:调整了链接脚本里的地址或大小,重新编译烧录后 FSBL 不工作。
原因:链接脚本改了之后,头部生成工具用的地址没有同步更新,BootROM 按照旧的头部信息去搬,搬到了错误的位置。或者栈顶地址改到了不可写的区域,一压栈就异常。
解决:链接脚本和头部生成参数必须联动修改,改完一个立刻改另一个。栈顶地址要确保落在可读写 RAM 范围内,并且留出足够的栈空间。改完之后用调试器单步跟一下复位后的前几条指令,确认栈指针和入口地址都对。
5. 进阶技巧:用双镜像和 CRC 校验让 FSBL 更可靠
5.1 双镜像备份的基本思路
产品化场景下,FSBL 本身也可能需要升级。如果只有一份 FSBL,升级过程中断电就会变砖。常见做法是在外部 Flash 里存两份 FSBL 镜像,BootROM 先读第一份,校验不过再读第二份。STM32N6 的 BootROM 支持一定程度的回退机制,具体行为要看参考手册里的启动章节。
实现双镜像的关键是头部里要有一个字段标识镜像序号或者有效性,BootROM 根据这个字段决定加载哪一份。FSBL 自己在运行后也可以检查另一份镜像的版本,决定要不要触发更新。
5.2 给应用固件加 CRC 校验的代码模板
搬运完应用固件后做一次 CRC 校验,能挡掉大部分搬运错误。下面是一个简单的 CRC32 实现:
/* CRC32 查表法,用于校验搬运后的应用固件 */ static uint32_t crc32_table[256]; void crc32_init(void) { for (uint32_t i = 0; i < 256; i++) { uint32_t crc = i; for (int j = 0; j < 8; j++) { crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1)); } crc32_table[i] = crc; } } uint32_t crc32_calc(const uint8_t *data, uint32_t len) { uint32_t crc = 0xFFFFFFFF; for (uint32_t i = 0; i < len; i++) { crc = (crc >> 8) ^ crc32_table[(crc ^ data[i]) & 0xFF]; } return crc ^ 0xFFFFFFFF; }逻辑说明:初始化时预计算 256 项查找表,之后每次计算只需要查表和异或,速度比逐位计算快很多。计算时初始值取0xFFFFFFFF,最后再取反,这是 CRC32 的标准做法。校验时把计算结果和头部里记录的预期值比对,不一致就报错。
参数说明:多项式0xEDB88320是 CRC32 常用的反射多项式,和 zlib、gzip 用的是一套。如果头部生成工具用的是别的多项式,这里要对应修改。
5.3 一个我踩过的坑:校验通过但应用还是跑飞
早期调试时遇到过一次,CRC 校验通过了,但应用跳转后还是跑飞。查了很久才发现,CRC 校验的是搬运后的数据,但搬运过程中如果目标地址的某一段被其他数据覆盖了,CRC 算出来可能碰巧还是对的。后来改成在搬运前先擦除目标区域,搬运后再校验,问题就消失了。
这个经历让我养成了一个习惯:FSBL 里所有涉及内存写入的操作,写之前先确保目标区域是干净的。擦除、清零、校验,三步一个都不能少。看起来多花了一点时间,但省下来的调试时间远不止这一点。
希望帮到你。
本文还有配套的精品资源,点击获取