首页 / 资讯中心 / 文章详情

STM32H750外部Flash三件套:Bootloader、App与FLM下载算法联合设计

STM32H750外部Flash三件套:Bootloader、App与FLM下载算法联合设计 ★ FEATURED ARTICLE
简介基于STM32H750VBT6的ZERO开发板完整设计源码涵盖Bootloader、应用程序与下载算法面向具备一定嵌入式基础的开发者解决固件在线升级、硬件初始化及用户应用定制等实际工程问题可适用于学习、项目原型与产品迭代等场景。压缩包共2000个文件约152.7MB以834个c源码与876个h头文件为主辅以188个HTML文档、29个Python脚本、41个txt说明、8个Shell脚本及PDF、Markdown等覆盖驱动、协议栈、界面与构建脚本等开发环节。已有247人学习Bootloader实现注重小内存占用与安全更新机制下载算法包含传输协议、固件校验与容错策略适合深入理解STM32固件架构的开发者参考。文档与图片资源可辅助掌握硬件特性与软件框架Bootloader源码体现了小内存占用、快速启动与安全校验等设计细节下载算法涵盖传输协议、固件校验与异常容错处理开发者可在现有基础上快速二次开发构建稳定可靠的应用产品。1. 基于STM32H750VBT6的ZERO开发板为什么Bootloader、App和下载算法要一起设计STM32H750VBT6是一颗Cortex-M7内核、主频能跑到480MHz的“怪胎芯片”内部SRAM接近1MB可内部Flash偏偏只有128KB正常一个图形界面加协议栈加文件系统就塞满了。ZERO开发板这类以H750为核心的板子普遍的做法是外挂一颗QSPI Flash把应用程序放到外部Flash里XIP执行内部Flash只放Bootloader和引导配置。要把这套架构从“能跑”变成“能在调试器里一键烧录”注定了Bootloader、应用程序和下载算法FLM三套源码要一起设计。这篇笔记就把三者的分工、源码要点和联调坑位一次讲透适合正在H750外部Flash方案里折腾的开发者。2. Bootloader设计先把存储布局摸清再做QSPI驱动与App跳转网上聊STM32H750的帖子绝大多数都在纠结同一件事App到底放哪。ZERO开发板这类板子一般会外挂一颗W25Q系列QSPI Flash把App放进去让CPU直接从0x90000000取指执行。这就引出了Bootloader的第一个职责在跳转前把QSPI Flash配置成memory-mapped模式让App代码真正“可执行”。2.1 存储布局128KB内部Flash留给Bootloader外部Flash才是App的家先看一张我常用的分区表它决定了三套工程各自编到哪个地址段。区域起始地址大小内容Bootloader0x0800000016KB引导代码、升级协议、QSPI驱动App外部Flash0x90000000视Flash型号应用程序固件配置区/备份区0x90000000之后剩余空间升级备份、参数存储内部Flash只有128KBBootloader分16KB已经算宽裕再往上加就挤占App的下载调试空间。16KB能放下QSPI驱动、串口升级协议和一个精简跳转逻辑前提是驱动直接操作寄存器而不是堆HAL库。App的链接地址则必须从0x90000000开始不能再用0x08000000否则Bootloader跳过去后取到的第一Cortex-M7中断向量都是错的。很多初次上手的开发者会把Bootloader和App分到两个MDK工程这没问题但要注意两个工程的编译选项里必须都关掉“Erase Full Chip”否则烧一个把另一个抹了。习惯做法是三个工程boot、app、flashalgo下载算法各自独立维护用同一个头文件把地址宏统一起来。2.2 QSPI Flash驱动从擦除到memory-mapped模式Bootloader里最省时间的一步QSPI驱动是Bootloader里最核心的模块它要做四件事复位Flash、初始化QUADSPI外设时钟和引脚、提供扇区擦除和页编程接口、在跳转前切换成memory-mapped模式。这里最难理解的是“切换模式”擦写Flash时芯片工作在寄存器模式CPU只把QSPI当普通外设访问而跳转前必须切到memory-mapped模式CPU才能直接读0x90000000取指。/* 跳转前调用把QSPI Flash切换成可执行映射 */ void qspi_enter_memory_mapped(void) { /* 复位Flash唤醒发0x66 0x99标准命令序列 */ qspi_send_cmd(0x66); qspi_send_cmd(0x99); /* 配置工作时钟主频480MHz时建议先分频到100MHz以下 */ qspi_cfg_clock(CLK_DIV_4, SAMPLE_EDGE_RISING); /* 清除写保护状态防止映射模式下读异常 */ qspi_write_status(0x00); /* 设置四线读命令0xEB6个dummy周期带1字节前缀 */ qspi_set_read_cmd(QUAD_READ_EB, 6, PREFIX_ENABLE); /* 读取JEDEC ID确认链路正常失败就原地等待 */ if (qspi_read_id() ! W25Q128_JEDEC_ID) { while (1); } /* 进入memory-mapped模式之后CPU可直接从0x90000000取指 */ qspi_set_mode(MEMORY_MAPPED); }这段代码里的CLK_DIV_4和QUAD_READ_EB是两个最值得调的参数。H750跑480MHz时QSPI外设时钟如果直接用1分频很可能采样不稳复位后读ID都是错的一般从4分频开始调确认读回来的数据和Flash丝印一致再往上压。QUAD_READ_EB是W25Q系列的标准四线快速读命令dummy周期要看Flash数据手册W25Q128在四线模式下普遍用6个dummy。要注意的是Bootloader里配置的读命令、dummy周期和引脚复用必须和App里QSPI初始化保持一致否则跳转后App里再初始化一次QSPI时映射模式还没退出操作会乱套。2.3 App跳转关中断、重定向VTOR、重建栈指针一个都不能少Bootloader做完固件校验后最后一步就是跳转。很多人以为跳转就是把函数指针指向App的复位向量实际上Cortex-M7跳转前有四个动作漏一个都会翻车关全局中断、重设VTOR、取出App初始栈指针、清流水线。typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_addr) { app_entry_t app_entry; uint32_t app_sp; /* 1. 关闭全局中断避免跳转过程中外设中断直达异常 */ __disable_irq(); /* 2. 停掉SysTick清掉Pending中断防止跳转后误触发 */ SysTick-CTRL 0; /* 3. 从App向量表头部取初始MSP和复位向量 */ app_sp *(volatile uint32_t *)app_addr; app_entry (app_entry_t)(*(volatile uint32_t *)(app_addr 4)); /* 4. 粗糙校验栈顶是否落在合法RAM范围防止跳到空白Flash */ if ((app_sp 0xFFF00000) ! 0x20000000 (app_sp 0xFFF00000) ! 0x24000000 (app_sp 0xFFF00000) ! 0x30000000) { return; } /* 5. 重定向中断向量表到App地址 */ SCB-VTOR app_addr; __DSB(); __ISB(); /* 6. 重建栈指针然后跳转到App复位向量 */ __set_MSP(app_sp); app_entry(); }这里最容易被忽略的是第4步的栈顶校验。如果外部Flash没烧录任何东西读回来的数据全是0xFF栈指针就会变成0xFFFFFFFF__set_MSP直接触发HardFault。加了RAM范围判断后至少能保证只有“看起来合法”的固件才会被跳转。第5步的__ISB()是Cortex-M7的流水线同步指令不加的话处理器可能还在执行跳转前的旧指令流玄学死机多半从这来。另外跳转前最好把Bootloader用到的外设时钟和中断统一复位一遍否则App里初始化到一半一个残留中断飞过来调试一天都找不到原因。2.4 升级协议给Bootloader加一个能用的串口升级通道有了Bootloader和跳转H750才算具备“bootloader升级”能力。ZERO开发板的Bootloader一般通过串口接收固件协议可以做得极简每包由帧头、目标地址、长度、CRC和数据组成。typedef struct { uint8_t head[2]; /* 固定0xA5 0x5A用于帧同步 */ uint32_t dst_addr; /* 写入地址通常是0x90000000 */ uint32_t length; /* 本包有效数据长度不超过256 */ uint32_t crc; /* 对data区域的CRC32校验值 */ uint8_t data[256]; } boot_packet_t;上电后Bootloader先等一小段“升级窗口”比如500ms内串口收到0xA5 0x5A就进入升级流程收不到就直接校验App并跳转。每收到一包先按dst_addr擦除对应扇区再写页最后整包读回比较。写入期间QSPI必须处于寄存器模式所以每包处理完要退出映射模式、写完后重新进映射模式这个切换开销不小但胜在逻辑简单。实际项目里很多人会用YMODEM替代这套自研协议Bootloader侧只做接收和写Flash上位机用现成工具省掉一版上位机开发工作。3. 应用程序侧的配合分散加载文件、向量表重定向与RAM选择Bootloader跳转过去只是第一步App工程如果不改链接脚本和启动文件跳过去大概率死机。很多人以为App只要把编译地址改成0x90000000就行结果第一行代码都执行不对问题全出在分散加载和向量表上。3.1 分散加载文件让App的代码段落在0x90000000而不是0x08000000App工程里最关键的文件是分散加载文件.sct。它的作用是把代码段、只读数据段定位到外部Flash把读写段和零初始化段定位到内部SRAM。H750的RAM分布和F1/F4不一样选错SRAM区域会直接影响DMA和外设访问。; 外部QSPI Flash起始0x90000000大小8MB LR_IROM1 0x90000000 0x00800000 { ER_IROM1 0x90000000 0x00800000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x24000000 0x00040000 { ; AXI SRAM 512KB .ANY (RW ZI) } }我把RW和ZI段放在0x24000000的AXI SRAM而不是0x20000000的DTCM原因是AXI SRAM挂在系统总线矩阵上可以被DMA、以太网MAC、USB等外设直接访问。DTCM虽然访问延迟低但它和CPU紧耦合DMA访问不到App里一旦用DMA搬运数据目标缓冲在DTCM里就会直接卡死。当然如果App追求极致性能可以把栈和RTOS堆单独放到DTCM用__attribute__((section(dcm)))把热点变量也塞进去代价是这些变量不能被DMA访问得自己在代码里区分。3.2 向量表重定向SCB-VTOR到底要满足什么对齐条件App的向量表跟着代码段一起放到了0x90000000Cortex-M7复位后默认向量表还在0x08000000的Bootloader区域所以App必须在任何中断发生前把SCB-VTOR改成0x90000000。Cortex-M7的VTOR寄存器要求地址128字节对齐0x90000000天然满足但如果有人把App地址错开到0x90000100这种位置中断向量就全乱了。/* 放在Reset_Handler里SystemInit之后、进入main之前执行 */ static void app_vtor_setup(void) { SCB-VTOR 0x90000000; __DSB(); __ISB(); }更保险的做法是在启动文件里直接写汇编在bl SystemInit和b __main之间插入这段LDR R0, 0x90000000 LDR R1, 0xE000ED08 STR R0, [R1] DSB ISB注意H750的Flash启动别名问题和F4不太一样H7复位取向量是从0x08000000物理Flash取的Bootloader就是靠这个机制启动的App侧则必须手动重定向VTOR。很多人把这段代码写在main()第一行理论上只要没有中断发生也能跑但如果你用了HAL库的HAL_Init()里面的PendSV、SysTick异常可能赶在前面触发死机就是一瞬间的事。3.3 RAM分配RW和ZI放哪块SRAMDMA和外设总线怎么考虑H750的SRAM有几个区块ITCM0x00000000、DTCM0x20000000、AXI SRAM0x24000000以及SRAM1/2/3/40x30000000附近。刚上手的人很容易被这几块区域搞晕我习惯按用途分代码执行和栈放ITCM/DTCM数据缓冲和DMA缓冲区放AXI SRAM或SRAM1。RAM区域起始地址特点建议用途ITCM0x00000000紧贴CPU只能取指关键代码段加速DTCM0x20000000紧贴CPU只能数据访问无DMA栈、RTOS堆AXI SRAM0x24000000可DMA访问容量512KBApp RW段、DMA缓冲SRAM1/2/3/40x30000000普通SRAM可DMA数据缓存、网络包缓冲写成这张表的依据不是玄学而是H7总线矩阵的物理限制。App的分散加载文件里把RW段放AXI SRAM能避免90%的DMA问题如果非要省RAM把RW放DTCMDMA传输的目标地址就必须改到别的SRAM块代码里会到处是复制来回的痕迹。4. 下载算法设计让Keil能把App直接烧进外部QSPI FlashBootloader和App都编好了打开Keil一点下载调试器却提示“找不到0x90000000对应的Flash算法”。这就是标题里的“下载算法设计”要解决的事写一个FLM算法工程让MDK/J-Link知道App要写到哪里、怎么擦、怎么编程。4.1 下载算法FLM在Keil烧录流程里管什么事Keil烧录外部Flash的过程可以理解为调试器先把一个Flash编程算法FLM加载到目标板RAM里然后通过算法提供的接口完成初始化、擦除、写页、校验。FLM本质是一个被特殊分散加载定位到RAM运行的ELF文件里面导出几个固定符号Init、UnInit、EraseSector、ProgramPage、FlashDevice。调试器下载时调用的接口顺序是固定的先Init初始化QSPI外设再按地址计算要擦哪些扇区逐个调EraseSector然后按页大小调ProgramPage写数据最后UnInit收尾。FLM里跑的代码就是Bootloader里QSPI驱动的精简版因为它在RAM中运行不能依赖任何在Flash里链接的函数。4.2 FLM工程最小实现PrgFunc、DevDscr与Erase/Program四个接口FLM工程的核心是FlashPrg.c和FlashDev.c两个文件。FlashDev.c里放一个FlashDevice结构体描述Flash的型号、起始地址、容量、扇区大小等静态参数。struct FlashDevice const FlashDevice { FLASH_DRV_VERS, /* 驱动版本号Keil要求固定值 */ W25Q128 0x90000000, /* 调试器里显示的名称 */ EXTSPI, /* 设备类型外部SPI Flash */ 0x90000000, /* 设备起始地址 */ 0x00800000, /* 设备大小 8MB */ 0x1000, /* 扇区大小 4KB */ 0x0100, /* 页大小 256B */ 1000, 1000, /* 擦除/编程超时(ms) */ /* 后续还有保留字段按模板填即可 */ };FlashPrg.c里是实现擦写接口的核心代码。这里最容易踩的坑是地址换算调试器传入的adr是绝对地址也就是0x90000000带偏移而QSPI控制器擦除和编程时用的是相对地址必须先减掉基址。uint32_t Init(uint32_t adr, uint32_t clk, uint32_t fnc) { /* 初始化QSPI引脚时钟发复位命令唤醒Flash */ qspi_lowlevel_init(); qspi_reset_flash(); /* 配置读命令dummy周期和App侧保持一致 */ qspi_set_read_cmd(QUAD_READ_EB, 6); return 1; } uint32_t EraseSector(uint32_t adr) { /* 绝对地址转相对偏移再发扇区擦除命令 */ uint32_t flash_off adr - 0x90000000; return qspi_erase_sector(flash_off) ? 1 : 0; } uint32_t ProgramPage(uint32_t adr, uint32_t sz, uint32_t *buf) { /* 页编程sz不会超过FlashDev里配的256字节 */ uint32_t flash_off adr - 0x90000000; return qspi_page_program(flash_off, buf, sz) ? 1 : 0; } uint32_t UnInit(uint32_t fnc) { /* 烧完切回memory-mapped方便调试器读回校验 */ qspi_exit_prog_mode(); qspi_enter_memory_mapped(); return 1; }注意ProgramPage里的sz由调试器依据FlashDevice里的页大小自动分包FLM里不需要自己处理跨页逻辑只负责把传进来的这一段写进Flash。UnInit里切回memory-mapped模式很关键否则烧录完成后调试器读0x90000000区域做校验时拿到的还是QSPI寄存器状态校验直接失败。4.3 FLM的分散加载为什么算法的代码必须搬到RAM里运行FLM工程的分散加载文件和普通App不一样它不允许把代码放在Flash里。原因很直接擦写外部Flash时Flash本身正在忙CPU不可能同时从0x90000000取指。所以FLM的代码段和数据段要全部放到RAM。LR_LOAD 0x24000000 0x00008000 { ER_LOAD 0x24000000 0x00008000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_DATA 0x20000000 0x00008000 { .ANY (RW ZI) } }我把FLM的代码放在0x24000000AXI SRAM数据放在0x20000000DTCM这样躲开Keil默认算法工作区。很多现成模板会把代码放在0x20000000如果Keil的“RAM for Algorithm”也配成0x20000000加载算法时会互相覆盖表现就是下载算法一加载就HardFault。FLM的栈同样在数据段里分配实际工程里RAM占用很小8KB代码加8KB数据足够。4.4 让下载算法支持J-Link和STM32CubeProgrammer两种工具链MDK里配置FLM的方式很简单在Options for Target的Utilities页选择“Use Debug Driver”然后在Flash Download页勾选外部Flash算法文件把编译好的FLM放到Keil安装目录的ARM/Flash下即可。J-Link在H750上配合MDK使用最常见选对FLM后就能一键下载App到0x90000000。如果平时用STM32CubeProgrammer烧录FLM格式不通用得另外生成.stldr外部加载器。CubeProgrammer的ExternalLoader逻辑和FLM几乎一样也要实现Init、EraseSector、Write几个回调但工程格式是IAR或GCC编译的独立ELF。ZERO开发板这类板子一般会同时提供MDK的FLM和CubeProgrammer的stldr两个工具链用同一份QSPI驱动源码只是外层包装不同。5. 避坑Bootloader、App与下载算法联调中的五个典型翻车现场三套源码放在一起联调是我做H750方案时踩坑最密集的阶段。下面五条是按“现象 → 原因 → 解决”整理的记录每条都来自实际调试过程希望能帮你少走点弯路。5.1 现象烧完App复位直接跑飞连Bootloader都没起来烧录成功但一复位就死用调试器看PC指针跑到了0xFFFFFFFF。原因通常是Bootloader跳转前没有正确退出QSPI的寄存器模式或者跳转地址那行根本没执行到另一个常见原因是App工程里分散加载文件忘了改代码段还在0x08000000Bootloader跳过去取到的不是App程序。解决先确认Bootloader跳转函数里的app_addr实参是0x90000000再用调试器在跳转前打断点检查app_sp读回来的值是否落在合法RAM最后打开Map文件看App的__main入口地址是否在0x90000000之后。5.2 现象QSPI擦写正常但下载速度慢到像在传串口烧录一个100KB的App用了好几分钟。原因多半是FLM里扇区大小配成4KB没问题但每写一页都执行了一次扇区擦除或者ProgramPage里没有关闭写保护状态寄存器导致页编程要等Flash内部超时。解决FlashDevice结构体里扇区大小写4KB、页大小写256B调试器会自动按页分包页编程前不要反复发送写状态寄存器命令W25Q系列只要清一次写保护就能连续写。5.3 现象下载算法一加载进RAM就进HardFaultMDK日志里能看到算法加载地址但紧接着就HardFault。原因基本是FLM的分散加载地址和Keil配置的“RAM for Algorithm”重叠。比如FLM代码放在0x20000000Keil也把算法工作RAM配成0x20000000算法加载到一半被自己的数据覆盖。解决把FLM分散加载的代码段放到0x24000000同时在MDK的Flash Download设置里把“RAM for Algorithm”地址改成0x24000000且大小不超过8KB两者错开。5.4 现象App运行正常但一进中断就死机外设轮询全正常一开中断就HardFault。原因是VTOR设置得不及时中断在向量表重定向之前就发生了CPU还在拿Bootloader的向量表响应App注册的中断跳进一个非法地址。解决把VTOR设置挪到启动文件里SystemInit之后、跳__main之前执行汇编重定向保证任何中断使能之前向量表已经在0x90000000。顺带检查App工程的分散加载里有没有设置RW_IRAM1覆盖了向量表区域Cortex-M7要求VTOR地址128字节对齐0x90000000没问题。5.5 现象J-Link下载报错Flash Timeout但用烧录器又能烧J-Link能连上MCU烧内部Flash正常一烧外部Flash就超时。原因多数是W25Q系列Flash的WP#引脚悬空或被拉低Flash内部状态寄存器写保护没解除擦除命令发给Flash后它直接忽略也有可能是FLM里的QSPI时钟分频配太高Flash实际工作不稳定擦除命令发出后误判超时。解决检查板子上W25Q的WP#引脚是否上拉到3.3V再把FLM里时钟分频降一档比如480MHz下用4分频同时确认FlashDevice结构体里的擦除超时时间不小于1000ms。6. 验证与进阶从“能跳转”到“能可靠升级”的关键一步三套源码跑通之后先别急着加功能把验证链路做扎实。我的习惯是Bootloader里串口打印一行带校验结果的跳转日志App第一行代码也打印一条带运行地址的启动日志两条日志串起来就能确认整个链路没有断点。比如Bootloader侧打印B: boot start, app crc ok, jump0x90000000App侧打印A: app run on qspi flash看到这两行再打开调试器确认PC指针确实停在0x90000000区域这套架构才算真正验收通过。进阶方向上最值得做的是把升级流程做成双备份外部Flash里同时放App主区和App备份区Bootloader先校验主区的CRC失败自动跳到备份区。这个方案能在现场升级断电时保住最后一道防线H750的4KB扇区特性让这种双区方案代价很小。另外可以把Bootloader里的关键异常处理函数放到ITCM里用__attribute__((section(itcm)))把高频中断处理加速到零等待这是H750比M4芯片在实时性上最大的红利。最后说一个我自己交过学费的教训有次发布固件时点错了全片擦除把内部Flash的Bootloader也抹掉了板子从此只能靠外部烧录器救。从那以后我所有H750工程里都保留一个最小的recovery引导放在内部Flash最前段专门负责从外部Flash恢复程序内部Flash其余空间才放正式Bootloader。这套三件套的价值不在“能跑起来”而在于每一次固件升级都有后悔药可吃。ZERO开发板这套方案做扎实了希望帮你在H750的后续项目里少踩几个坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站