STM32H7B0 纯外部 QSPI/OSPI 执行(XIP)实战笔记

/ 0评 / 0

先打个预防针:网上有人说 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 要做什么?

必做清单("不做就跑不起来"的那种)

  1. 时钟初始化(至少把系统拉到一个你希望的频率,后续 OSPI/SDRAM 的时序全靠它)
  2. 必要外设引脚复用:比如 OSPI 的 CS/CLK/IO0..IOx,FMC 的地址/数据线等
  3. 配置 OCTOSPI 为 Memory-mapped 模式(XIP 必备)
  4. FMC/SDRAM 初始化(如果应用要用到显示/大缓存)

官方话术:要从外部执行,程序必须在外部闪存里,且在跳转前把 OCTOSPI 配成 Memory-mapped.就是这么直给.

强烈推荐做("不做容易掉坑里的那种")

视项目选做("做了更优雅,更可维护")

应用(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 系列地址窗口固定:

把 .isr_vector,.text,.rodata 这些段都指到你选的那一块.内存映射窗口的定义见 ST 文档与应用笔记.这些在CMakeList.txt中分别指定就可以,如果是MDK等自行写分散加载,这一点还是GNU这套工具舒服.

注意 MPU/Cache:

很多图形/显示模板(LVGL,H7R/S)给出的 MPU 建议就是对外部 Flash 的前若干 MB 开启 Cache 和可执行权限.

OSPI/QSPI 配置里的几个"变量"

你的 OpenOCD 初始化脚本说明

做三件事:

  1. 开时钟(RCC AHB4/3,CKGA 等)
  2. 配置 GPIO 复用(PB6/CS,PB2/CLK,PD11/12/13/IO,PE2/IO 等)
  3. 初始化 OCTOSPI 到 Memory-mapped,再 flash probe 1,发 JEDEC ID,开 QE 位

这套路完全 OK,但要注意两点:

另外,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;
}

常见坑与排雷

一点性能观感

很多人纠结"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区域

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注