先打个预防针:网上有人说 H7B0 有 2 MB 片上 Flash,这事别信.H7B0 Value-line 只有 128 KB 内部 Flash,官方页面和手册都这么写,做产品就按手册来,别和量产质量过不去.
思路很简单:用小 Bootloader 放在片内 128 KB,只干"上电后最必要的配置"(时钟,引脚,OSPI 映射,SDRAM 等),再跳转到片外 QSPI/OSPI 的应用程序去跑(XIP = Execute-In-Place).这套路对 H7A3/H7B3/H7B0 系列都通用--要想从外部闪存执行代码,必须先把 OCTOSPI 配成 Memory-mapped 模式再跳过去.
顺路也把一个常见误区掰正:H7 家族普遍不支持直接从外部 QSPI 作为复位启动源,所以"开机直接从外部启动"的幻想基本要靠你自己这段内置 Bootloader 实现.
Bootloader 要做什么?
必做清单("不做就跑不起来"的那种)
- 时钟初始化(至少把系统拉到一个你希望的频率,后续 OSPI/SDRAM 的时序全靠它)
- 必要外设引脚复用:比如 OSPI 的 CS/CLK/IO0..IOx,FMC 的地址/数据线等
- 配置 OCTOSPI 为 Memory-mapped 模式(XIP 必备)
- FMC/SDRAM 初始化(如果应用要用到显示/大缓存)
官方话术:要从外部执行,程序必须在外部闪存里,且在跳转前把 OCTOSPI 配成 Memory-mapped.就是这么直给.
强烈推荐做("不做容易掉坑里的那种")
- 应用镜像校验:CRC/哈希/签名皆可,校验不过就别跳.
- 升级逻辑(DFU/U 口/网口):包含加解密与回滚策略(A/B 分区或"上一版备份").
- 安全检查:固件版本/签名/设备绑定校验.
视项目选做("做了更优雅,更可维护")
- 看门狗门面活:进 App 前先关/喂/重配,看门狗策略统一.
- 崩溃转储 & 启动次数计数:异常复位 N 次自动进安全模式/升级模式.
- GPIO 组合键/按住某键进升级:现场救砖必备.
- 硬件版本/料号自检:不同板子装不同 App 或不同时序表.
- Boot-time 诊断串口日志:只在 Bootloader 打印,App 尽量干净.
应用(App)这边怎么配?
Startup 里要改什么?
不要再改系统时钟,不要再重复初始化 OCTOSPI/FMC.所以把复位后那两句去掉:
原本:
Reset_Handler:
ldr sp, =_estack
bl ExitRun0Mode
bl SystemInit
改成:
Reset_Handler:
ldr sp, =_estack
备注:
ExitRun0Mode
是 ST 模板里和供电/运行级别相关的收尾动作,多数项目里放在 Bootloader 处理更合适.App 阶段通常不需要它.真正必须的是:App 一上来就把 VTOR 指到自己向量表,这样中断别跑回 Bootloader.VTOR = Vector Table Offset Register,Cortex-M7 用它来把中断向量搬家.
App 里的"跳板代码"怎么写?
Bootloader 跳 App 的函数可以这样稍微"硬核"一点,避免常见尾椎骨折(中断残留,SysTick 残留,VTOR 未设等):
typedef void (*pFunction)(void);
__attribute__((noreturn))
static void jump_to_app(uint32_t app_base)
{
__disable_irq();
/* 可选:停掉 SysTick,清掉所有中断使能/挂起 */
SysTick->CTRL = 0;
SysTick->VAL = 0;
for (uint32_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFF;
NVIC->ICPR[i] = 0xFFFFFFFF;
}
/* 设置向量表到 App 起始地址 */
SCB->VTOR = app_base;
__DSB(); __ISB();
/* 读取 MSP 和复位向量 */
uint32_t sp = *(__IO uint32_t*) (app_base + 0);
uint32_t pc = *(__IO uint32_t*) (app_base + 4);
__set_MSP(sp);
((pFunction)pc)();
for (;;);
}
把中断清理和 VTOR 都做了,现场问题会更少.VTOR 的工作原理与注意事项,ARM 的手册里写得很清楚.
链接脚本 / 内存布局要点
Bootloader 的 LD 脚本照常放在 FLASH (rx) = 0x0800_0000, LENGTH = 128K
.
App 的代码段与向量表要放到 OSPI 窗口,H7 系列地址窗口固定:
- OCTOSPI1:0x9000_0000 ~ 0x9FFF_FFFF
- OCTOSPI2:0x7000_0000 ~ 0x7FFF_FFFF
把 .isr_vector
,.text
,.rodata
这些段都指到你选的那一块.内存映射窗口的定义见 ST 文档与应用笔记.这些在CMakeList.txt中分别指定就可以,如果是MDK等自行写分散加载,这一点还是GNU这套工具舒服.
注意 MPU/Cache:
- 读(执行)阶段:把 OSPI 区域标成 Normal,Cacheable(开启 I-Cache/D-Cache 带来回填),可执行,这样 XIP 性能不会难看.
- 写/擦阶段:别在 Memory-mapped 模式下"边写边执行",切回 Indirect 模式,或把目标区域设置为 Device/Strongly-ordered 并从 SRAM 执行写入.
很多图形/显示模板(LVGL,H7R/S)给出的 MPU 建议就是对外部 Flash 的前若干 MB 开启 Cache 和可执行权限.
OSPI/QSPI 配置里的几个"变量"
- 1-1-4 vs 1-4-4:
1-1-4(指令/地址单线,数据四线)和 1-4-4(指令/地址/数据全四线)的差距主要体现在地址阶段是否是四线传输,理论上 1-4-4 更快;但如果 I-Cache/D-Cache + 预取 配好了,XIP 下热点命中率高时差距会被抹平不少.你要真卡性能,先把 Dummy Cycles 调准到闪存数据手册的值,然后再考虑从 1-1-4 升到 1-4-4. - Dummy Cycles:
连不上/跑着跑着 HardFault,十有八九是 Dummy 配错.每家 NOR Flash 的"高频下推荐 Dummy"不同,严格按器件手册来. - SIOO(Send-Instruction-Only-Once)/Wrap 模式:
打开之后能减少指令开销,优化 Cache Line 回填,H7 新版文档有提及.
你的 OpenOCD 初始化脚本说明
做三件事:
- 开时钟(RCC AHB4/3,CKGA 等)
- 配置 GPIO 复用(PB6/CS,PB2/CLK,PD11/12/13/IO,PE2/IO 等)
- 初始化 OCTOSPI 到 Memory-mapped,再
flash probe 1
,发 JEDEC ID,开 QE 位
这套路完全 OK,但要注意两点:
- 厂商命令细节:
0x06
(WREN),0x01
(写状态寄存器)这些顺序,写哪些位与具体 Flash 型号强相关,别复制粘贴就上车,查自己芯片手册;AN4760/AN5050 里也有通用流程可参考. - 调速:初期
adapter speed 1000
慢点稳,确认稳定再拉高.OSPI 的最终时钟上限由片上分频和外部闪存规格共同决定.
另外,Memory-mapped 模式下"写操作"不靠谱(状态轮询/忙信号问题),编程/擦除建议用 Indirect 模式,这点官方/社区都反复强调过.
App 区跳转前的健壮性检查
static bool app_vector_seems_valid(uint32_t base)
{
uint32_t sp = *(uint32_t *)(base + 0);
uint32_t pc = *(uint32_t *)(base + 4);
/* 简单检查:SP 是否落在合法 SRAM,PC 是否落在 OSPI 映射窗口 */
bool sp_ok = (sp >= 0x20000000UL && sp < 0x38000000UL);
bool pc_ok = (pc >= 0x90000000UL && pc < 0xA0000000UL); /* OCTOSPI1 窗口 */
return sp_ok && pc_ok;
}
常见坑与排雷
- 断点/单步体验奇怪:XIP 下调试器断点有时不好使,属正常现象,社区里也有人踩过.必要时把关键函数搬到 ITCM 或 SRAM.
- MPU/Cache 和"推测访问":M7 的取指/数据访问可能会"探路",在 OSPI 未就绪前最好把
0x9000_0000
这块标成强顺序/不可执行,等配置完成再改属性;这样能避免因为推测访问导致的 HardFault. - 边执行边写外部 Flash:别这么玩.切回 Indirect,从 SRAM 里执行编程例程.
- 启动文件里把 SystemInit 全砍了:可以,但要确保 SystemCoreClock 等全局频率变量在 App 里被正确设置/覆盖,别让中间件以为还是 64 MHz.
- 地址窗口别搞错:毕竟片上可能有2组OSPI.
一点性能观感
很多人纠结"1-1-4 和 1-4-4 性能差距到底多大".经验上,先把 Cache/MPU/Dummy 调通,命中率起来后差距就没想象中夸张;如果你的应用是大函数顺序执行,那 1-4-4 会舒服些;如果是跳来跳去的小函数+高命中,差距就小了.真要压榨极限,把热点函数放到 ITCM 或 SRAM 跑,外部 Flash 留给冷代码/资源.参考应用笔记对 Cache/回填/Wrap 的描述,机制就是为 XIP 提速准备的.
附:OpenOCD 片段
source [find interface/cmsis-dap.cfg]
transport select swd
set CHIPNAME stm32h7b0xx
if {![info exists OCTOSPI1]} {
set OCTOSPI1 1
set OCTOSPI2 0
}
source [find target/stm32h7x.cfg]
proc octospi_init { octo } {
# 1) 时钟开门:GPIO / OCTOSPI / CKGA
mww 0x580244E0 0x000007FF
mww 0x580244D4 0x01E95031
mww 0x580244B0 0x00002000
sleep 1
# 2) IOM 配置:OCTOSPI1 选 Port1
mww 0x5200B404 0x00010101
mww 0x5200B408 0x00000000
# 3) 引脚复用:PB6 NCS(AF10), PB2 CLK(AF9), PD11/12/13 IO0/1/3(AF9), PE2 IO2(AF9)
mww 0x58020400 0x00002020
mww 0x58020408 0x00003030
mww 0x5802040C 0x00000000
mww 0x58020420 0x0A000900
mww 0x58020C00 0x0A800000
mww 0x58020C08 0x0FC00000
mww 0x58020C0C 0x00000000
mww 0x58020C24 0x00999000
mww 0x58021000 0x00000020
mww 0x58021008 0x00000030
mww 0x5802100C 0x00000000
mww 0x58021020 0x00000900
# 4) OCTOSPI 基本寄存器:根据你的芯片型号再细化(指令/地址/数据线宽,Dummy 等)
mww 0x52005130 0x00001000
mww 0x52005000 0x30401f01
mww 0x52005008 0x00160709
mww 0x5200500C 0x0000000f
mww 0x52005108 0x40000008
mww 0x52005100 0x01002101
mww 0x52005110 0x0000000B
sleep 1
# 5) 试探闪存:JEDEC-ID,开写使能,写状态(比如开 QE 位)-- 按器件手册改
flash probe 1
stmqspi cmd 1 3 0x9f
stmqspi cmd 1 0 0x06
stmqspi cmd 1 0 0x01 0x00 0x02
sleep 1
}
$_CHIPNAME.cpu0 configure -event reset-init {
# 复位后先把系统拉到 ~64MHz,保证外设和 OSPI 能稳定工作
mmw 0x52002000 0x00000004 0x0000000B
mmw 0x58024400 0x00000001 0x00000018
mww 0x58024418 0x00000040
mww 0x5802441C 0x00000440
mww 0x58024420 0x00000040
mww 0x58024428 0x00404040
mww 0x5802442C 0x01ff0ccc
mww 0x58024430 0x01010207
mww 0x58024438 0x01010207
mww 0x58024440 0x01010207
mmw 0x58024400 0x01000000 0
sleep 1
mmw 0x58024410 0x00000003 0
sleep 1
adapter speed 1000
octospi_init 0
}
reset_config none separate
最终给一个下载的测试工程
程序执行地址是0x90000000区域