简介:面向STM32H750与W25Q128外部Flash的烧录算法工程,适合嵌入式开发者在Keil MDK环境中为外部SPI/QSPI Flash添加编程算法,解决固件无法直接烧写到外部Flash的问题。压缩包共80个文件,以FlashDev.c、FlashPrg.c等C源文件和头文件为核心,同时提供FLM烧录算法文件、Keil工程文件(uvprojx/uvoptx)、启动文件、调试配置文件以及编译中间产物(axf/map/o等),整体仅2.54MB,目录紧凑。这一工程已有近3800人浏览学习,覆盖了SPI接口初始化、扇区擦除、页编程、读取校验等算法关键流程,并保留HAL/LL库适配痕迹与JLink调试配置。读者可将其作为模板,根据实际板卡修改引脚时序、存储布局或Flash型号,快速生成可用于量产烧录的算法工程。 做STM32H750开发的人应该都有这种感觉:芯片性能拉满,480MHz主频加Cortex-M7核心,跑图形界面、语音识别都绰绰有余,但128KB的内部Flash总在关键时候提醒你“我很小”。尤其是用模组星球这类STM32H750开发板做实际项目时,代码加上字库、图片资源、协议栈,体积轻松突破512KB,外挂一片W25Q128几乎是标配。可是W25Q128接上去不代表工具链认它,Keil、JFlash默认都不知道怎么去操作你手里那片SPI Flash,这时候写一个专属的烧录算法工程,就成了整个项目绕不过去的一环。
这篇内容是我实现STM32H750 + W25Q128烧录算法工程的完整记录,从为什么需要烧录算法、算法内部到底做了什么,到Keil工程配置、JFlash适配,以及中间撞到的各种坑,一次讲清楚。适合正在用H750做量产项目、想把代码或资源放到外部Flash里的同学参考。
1. 为什么STM32H750需要外部Flash烧录算法
1.1 128KB内部Flash的现实困境
STM32H750这颗芯片从定位上就很有意思。它有一颗很强的Cortex-M7核心,主频能到480MHz,还带硬件双精度浮点,甚至内部SRAM就有512KB。但ST官方只给了128KB的内置Flash,摆明了让你外接存储。很多没有经验的工程师拿到芯片后发现,GUI界面、日志系统、通信协议栈这些代码一加上去,128KB很快就满了,接下来只能打起外部Flash的主意。
W25Q128是华邦出品的一款8MB SPI NOR Flash,容量足够放代码和资源文件,价格也便宜,所以在H750的板子上特别常见,模组星球的STM32H750开发板手册里也默认搭配了这颗Flash。问题在于,芯片和Flash都接好了,下载工具却不认识这个组合。Keil默认只懂STM32内部Flash的烧录协议,JFlash的默认设备库也不包含某个特定板子上的W25Q128,必须有人把这些下载逻辑写成驱动模块,这就是烧录算法工程的来源。
1.2 烧录算法的本质是什么
烧录算法本质上是一段可以被下载工具加载进目标芯片RAM运行的底层驱动代码。它并不神秘,更像是一份“接口说明书”,告诉下载工具怎么初始化SPI/QSPI、怎么擦除扇区、怎么把数据按页写进去、怎么读回校验。下载工具在界面上显示“Program done”,背后其实是它先把这段算法加载到SRAM里,然后调用算法里的函数,一层层把数据搬运到Flash指定地址。
用生活化的比喻来说,内部Flash下载就像在自家厨房做饭,锅碗瓢盆都是现成的;外部Flash烧录算法则是去野外露营,你得先教会别人怎么搭灶、怎么生火。烧录算法就是那张“搭灶生火说明书”,下载工具严格照做,才能把程序代码安全写进W25Q128。
1.3 什么时候必须自己写这个算法
有人会问,用STM32CubeProgrammer能不能直接往外部Flash写?答案是不能。CubeProgrammer虽然支持外部Flash编程,但前提是你得在工程里加上自定义的烧录算法文件。JFlash也一样,它内置的算法只覆盖常见的评估板,换一块自己设计的PCB板,引脚不同、QSPI配置不同,就必须手动加算法。但凡项目里需要把代码编址到0x90000000的内存映射区启动,或者想把字库、资源文件预烧到Flash里,都需要这个烧录算法工程。
2. 烧录算法工程的整体拆解
2.1 算法文件的核心函数与结构
一个标准的烧录算法,无论最终生成的是Keil的FLM文件还是JLink的ELF文件,内部都要实现一组固定的接口函数。这组函数由下载工具在特定时机调用:
| 函数名 | 作用 | 对应W25Q128操作 |
|---|---|---|
| Init | 初始化硬件,建立通信 | 初始化QSPI外设,读JEDEC ID确认Flash在线 |
| UnInit | 关闭硬件,回到安全状态 | 退出内存映射模式、禁用QSPI |
| EraseChip | 全片擦除 | 发0xC7命令,整片擦除 |
| EraseSector | 扇区/块擦除 | 发0x20命令擦除4KB扇区,或0xD8擦除64KB块 |
| Program | 写入数据 | 发0x02页编程命令,按256字节页写入 |
| Verify | 校验数据 | 读回Flash内容比对,或返回CRC校验结果 |
写算法的时候不需要全部实现,但Init、EraseSector、Program这三个是基础,缺一个下载步骤就跑不通。Keil下载时通常是先调用EraseSector擦除目标区域,然后调用Program逐块写入,最后通过读回比对或CRC来校验。如果算法里没有实现EraseChip,很多下载工具不会报错,只是全片擦除功能不可用,导出生产文件时会受限。
2.2 为什么选择QSPI而不是普通SPI
W25Q128本身支持标准SPI、Dual SPI、Quad SPI以及QPI模式,而STM32H750内部集成了QUADSPI外设,支持内存映射模式,可以把外部Flash直接映射到0x90000000地址空间访问。这就意味着,单片机可以像访问内部Flash一样,通过指针直接读取外部Flash里的代码和数据,性能比普通SPI高一个量级。
烧录算法里如果只用标准SPI,不是不行,但速度差距明显。我实际测过,用标准SPI下载1MB数据大约需要3到4分钟,换成四线Quad模式,同样数据只要20秒左右。产线上如果单片机数量多,这个差距就是效率瓶颈。所以算法里我强烈建议走QUADSPI,并把时钟频率尽量拉高——H750的QUADSPI时钟可以做到系统时钟的四分之一,以480MHz主频算就是120MHz,W25Q128完全能承受这个频率。
2.3 Keil和JFlash两套算法并非一回事
写算法之前先想清楚要服务哪个工具。Keil使用的算法文件后缀是FLM,内部是标准的ARM ELF格式,但链接脚本、加载地址有特定要求。JFlash的算法文件本质上是ELF,还要在JLinkDevices.xml里注册描述信息。两个工具虽然可以共用同一份源码,但编译和注册方式不同,不能直接把Keil生成的FLM扔给JFlash用,也不能让Keil直接加载JLink的ELF。后面我会细说两边怎么处理。
3. Keil下从零创建W25Q128烧录算法
3.1 工程创建与分散加载配置
在Keil里写烧录算法,我推荐自己新建一个工程,不要用默认模板。新建工程后,从包管理器里选择STM32H750VB设备型号,代码用HAL库或LL库都可以,但建议直接复制官方QSPI例程里的驱动,毕竟是验证过的。
编译配置是重点。在Options for Target的Output选项卡里,把输出文件名改成“W25Q128”,这样生成的烧录算法文件会成为W25Q128.FLM。然后在Linker选项卡里取消“Use Memory Layout”,手动指定分散加载文件。我用的典型配置如下:
LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } }地址0x08000000是内部Flash起始地址,这里并不是真的把算法烧进内部Flash,而是给烧录器一个加载地址,最终这段代码会被工具搬运到SRAM中运行。RW段放在0x20000000的SRAM起始区域,这样算法运行时变量不会和应用程序冲突。
3.2 核心实现:QSPI初始化和四个算法函数
初始化函数Init是最关键的一步。QUADSPI不是简单拉起引脚就行,要先配置协议格式、时钟分频、FIFO阈值,然后进入待机状态,再发指令读芯片ID,确认W25Q128响应正常。核心代码大致如下:
int Init(void *args) { QSPI_HandleTypeDef hqspi = {0}; hqspi.Instance = QUADSPI; hqspi.Init.ClockPrescaler = 1; // QSPI时钟 = 内核时钟 / 4 hqspi.Init.FifoThreshold = 4; hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_HALFCLK; hqspi.Init.FlashSize = 23; // 2^23 = 8MB ... return HAL_QSPI_Init(&hqspi) == HAL_OK ? 1 : 0; }页编程函数要注意一个坑:W25Q128的页大小是256字节,如果写入数据跨越页边界,Flash的地址指针不会自动跳到下一页。所以Program函数必须按页边界拆分数据,一次最多写256字节:
int Program(unsigned long addr, unsigned long len, unsigned char *buf) { while (len > 0) { unsigned long page_offset = addr & 0xFF; unsigned long chunk = (page_offset + len > 256) ? 256 - page_offset : len; HAL_QSPI_Transmit(&hqspi, W25Q128_PAGE_PROGRAM, addr, buf, chunk); addr += chunk; buf += chunk; len -= chunk; // 等待W25Q128内部编程完成 while (QSPI_IsBusy()) ; } return 1; }擦除函数同样有讲究。W25Q128支持4KB扇区擦除和64KB块擦除,Keil的下载流程一般调用扇区擦除,也就是0x20命令。擦除前要检查地址是否4KB对齐,不对齐直接返回0,否则会擦掉别的数据。全片擦除函数则可以发0xC7命令,适合产线第一次烧录前清空Flash。
3.3 编译生成FLM并验证
代码写完后勾选“Create HEX File”以外的选项,直接编译,生成的就是W25Q128.FLM。拿到这个文件后,把它复制到Keil安装目录下的“ARM\Flash”文件夹里,然后在Options for Target的Utilities选项卡中点击“Settings”,在Flash Download页面添加算法。添加成功后,界面上应该能看到“W25Q128 External Flash”的条目,勾选它就可以把应用程序直接下载到外部Flash。
我第一次生成FLM时踩过一次坑:编译选项里如果勾了MicroLIB,算法体积可能变大,影响加载速度;如果不小心带上了启动文件和SystemInit代码,会导致算法重定位后冲突。建议在工程里只保留QSPI驱动、算法主体和必要的HAL库文件。
4. JFlash添加W25Q128烧录算法实操
4.1 从FLM变成JLink能识别的算法
JFlash和JLink的算法体系与Keil不同。最简单的方式是,在Keil工程中把编译器切换到ARM Compiler 6,同时把算法源码重新编译成带调试信息的ELF。也可以直接使用JLink安装目录下自带的“JLinkDevices.xml”作为模板,参考其他Flash算法文件的写法添加自己的W25Q128条目。
我实践下来比较顺的流程是:先用Keil编译出FLM,再在JLink提供的编译环境里把同样的源码编译成ELF。注意JLink对算法的加载地址有要求,链接脚本要指定在SRAM地址范围内,并且所有函数必须是位置无关代码,即编译时生成代码要支持重定位,否则算法加载后会跳飞。
4.2 修改JLinkDevices.xml注册算法
JLink通过一个XML文件管理所有外设和Flash算法。打开JLink安装目录下的JLinkDevices.xml,在里面加入类似下面的配置:
<Device> <ChipInfo Vendor="ST" Name="STM32H750VB" Core="JLINK_CORE_CORTEX_M7" WorkRAMAddr="0x20000000" WorkRAMSize="0x10000" /> <FlashBankInfo Name="Internal Flash" BaseAddr="0x08000000" MaxSize="0x20000" Loader="Devices/ST/STM32H750_128K.elf" LoaderType="FLASH_ALGO_TYPE_OPEN" /> <FlashBankInfo Name="W25Q128 QSPI" BaseAddr="0x90000000" MaxSize="0x800000" Loader="Devices/ST/STM32H750_W25Q128.elf" LoaderType="FLASH_ALGO_TYPE_OPEN" /> </Device>其中BaseAddr填0x90000000,MaxSize填0x800000,对应8MB。Loader路径指向刚才生成的ELF文件。XML配置必须写到JLink安装目录下,并且JLink重启后才会加载,这点我经常忘记。
4.3 用JFlash命令行做产线批量烧录
算法在JFlash里注册好之后,不只是开发时能用,产线上也可以做成自动脚本批量烧录。我实际用的脚本很简单:
exec Device = STM32H750VB connect erase loadfile "firmware.hex" verify exit配合JLink的命令行工具,完整烧录加校验大概20秒,在小批量产线上足够用了。这里有个细节:连接速度默认是4MHz,如果你的PCB板走线比较长,建议降到1MHz以下,别小看这个,很多下载中途失败的问题都是SWD时钟太快导致的。
5. 踩坑实录:常见问题与排查技巧
5.1 擦除失败或校验失败
Keil下载到一半卡住,最后报“verify failed”,这是外部Flash烧录最常见的报错。排查顺序很重要,不要一上来就怀疑代码。第一步,用逻辑分析仪看QSPI引脚时序,确认时钟、数据线信号是否正常;第二步,检查W25Q128的状态寄存器,如果BUSY位一直为1,说明擦除命令根本没被正确响应,大概率是SPI模式配置错了;第三步,确认WP引脚和HOLD引脚的状态,这两个引脚在上电时必须拉高,如果悬空或者被意外拉低,Flash会进入保护状态,擦写指令全部无效。
我踩过的一个具体坑是,在擦除函数里用HAL_GetTick()做超时计时,结果算法在RAM里运行的时候系统节拍中断没使能,计时直接失效。后来改成简单的循环计数,每次擦除等待最多100万次循环,可靠得多。
5.2 程序下载到外部Flash后跑不起来
代码烧进W25Q128,地址也映射到了0x90000000,但程序一运行就进HardFault。根据我的调试经验,原因通常有三个:
- 向量表没有偏移。程序从外部Flash启动时,需要把SCB->VTOR设置到0x90000000,如果忘了这步,CPU拿到的是内部Flash里的向量表,执行逻辑完全错乱。
- 启动配置问题。H750有没有正确配置成从外部Flash启动,取决于BOOT0引脚和选项字节,不是所有板子都默认支持外部QSPI启动,模组星球开发板的跳线帽位置也要确认。
- 没有在启动代码里初始化QSPI。如果代码运行依赖外部Flash,而启动阶段QSPI还没被初始化,那么CPU一取指就是空的。
最稳妥的调试方法是分两步走:先把一个最简单的测试程序烧到内部Flash,专门测试QSPI读写、内存映射是否正确;等确认外设完全正常后,再把完整的Bootloader或应用放在外部Flash里启动。这样把问题隔离在单层,不会一会怀疑硬件一会怀疑代码。
5.3 JFlash识别不到自己添加的算法
这个问题主要出现在Windows权限和路径上。JLink安装目录在Program Files下,修改JLinkDevices.xml时如果忘记用管理员权限保存,改动根本写不进去。另外XML里的路径分隔符必须用正斜杠或者双反斜杠,用单个反斜杠会被当成转义字符。还有一个容易忽略的点是,JFlash的版本不能太老,7.88a及以上的版本对自定义ELF算法的兼容性更好。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| Keil下载时卡死在擦除 | QSPI时序不对、WP引脚未上拉 | 先检查时序,再测WP/HOLD电平 |
| 校验失败 | 页编程跨越页边界 | Program函数按256字节分片 |
| 程序跳转HardFault | 向量表偏移没设置 | 设置SCB->VTOR = 0x90000000 |
| JFlash识别不到算法 | XML配置有误、权限不足 | 管理员权限修改,检查路径分隔符 |
| 下载速度很慢 | 使用了标准SPI模式 | 改为Quad模式,拉高QSPI时钟 |
6. 最后补充一点经验
做完这个烧录算法工程之后,我的一个体会是:不要只盯着代码,硬件上的细节更关键。你写的算法再完美,只要W25Q128的WP引脚没上拉,或者SPI走线被旁边的电源干扰,所有下载操作都会变得玄学。建议在PCB设计阶段就把外部Flash的引脚状态考虑进去,至少保证上电默认状态是可擦写的。
如果你后续还想往远程升级方向扩展,这个烧录算法可以直接复用到OTA方案里——把算法里的擦除和编程部分抽出来,放进Bootloader,MCU就能通过串口或网络升级外部Flash里的应用程序。很多量产方案就是这么做的,一套地址映射加一套Flash驱动,能省不少事。
本文还有配套的精品资源,点击获取