公司动态
KEIL制作QSPI外部Flash下载算法全解析
正在编写一篇关于KEIL制作QSPI外部Flash下载算法的技术博文将围绕项目标题展开系统性讲解内容涵盖算法原理、工程搭建、代码实现、调试验证与踩坑经验。以下为正式内容1. 为什么需要自己动手做QSPI Flash下载算法先说一个很常见的场景你用STM32或其它MCU做主控代码量越写越大片内Flash装不下于是外挂了QSPI接口的串行Flash打算把字库、音频、固件升级包之类的大块头丢进去。硬件画好了、驱动写好了、程序也能跑可每次要往那颗外部Flash里烧数据就只能靠临时写一个下载工具或者用串口助手一包一包地发折腾得够呛。更难受的是你在KEIL里点了Download按钮它根本不认这颗外挂的Flash——因为它只认识芯片厂提供的内置Flash算法。想要让KEIL像操作片内Flash一样点击一下就能把数据写到QSPI接口的外部Flash你就得给它做一个翻译官也就是本文要讲的Flash下载算法Flash Download Algorithm。它本质上是KEIL调试器与目标Flash之间的中间层——调试器把数据一股脑交出来算法负责按Flash芯片的时序要求完成擦除、写入、校验。做完之后你在KEIL里的Flash Download列表中就会多出一个自定义条目进度条走完数据就进外部Flash了。这件事听起来很专业但真正做起来并不复杂。只要你理解了KEIL算法框架的两个源文件、四五个回调函数以及QSPI外设的基本时序照着做就能搞定。这个应用笔记适合正在做QSPI外部Flash存储方案的嵌入式工程师也能帮那些对下载算法只闻其名、未见其形的同学补上这块知识盲区。2. 下载算法的本质与框架拆解2.1 算法文件编译后的形态FLM文件KEIL的下载算法经过编译后生成一个.flm文件。这个文件会被KEIL的ULINK/J-Link等调试器加载然后下载到目标MCU的RAM中运行。它不是一个独立的应用程序而是一组函数的集合——调试器通过约定的函数指针表来调用这些函数完成对Flash的擦、写、校验。所以你看算法本身跑在MCU的RAM里而不是跑在主机上。这意味着算法代码体积不能太大RAM占用也要尽量小。同时它运行时不能让调试器失去对目标的控制因此算法代码要位置无关Position Independent或者在链接时指定好运行地址。KEIL的工程模板会处理好这些问题我们要做的是理解流程而不是自己从零搭启动代码。2.2 KEIL规定的函数接口一个标准下载算法需要提供至少以下函数函数名功能Init初始化Flash控制器及QSPI外设UnInit反初始化恢复MCU到正常状态EraseChip全片擦除EraseSector按扇区擦除ProgramPage按页编程Verify校验数据KEIL有时不用但建议提供KEIL通过一个叫做FlashDevice的结构体获得Flash的容量、扇区大小、页大小、起始地址等信息通过PrgFunc结构体获得上面这些函数的地址。这套协议非常成熟几乎所有MCU开发工具都遵循同样的逻辑。2.3 必须理解的FlashDevice结构体这是整个算法框架里最核心的数据结构定义在FlashOS.h中。关键字段包括struct FlashDevice { uint16_t Vers; // 版本号 char DevName[128]; // 器件名称会显示在KEIL下拉列表里 uint16_t DevType; // 器件类型ONCHIP/EXT8BIT/EXT16BIT等 uint32_t DevAdr; // Flash起始地址 uint32_t DevSize; // 总容量 uint32_t PageSize; // 页大小 uint32_t EraseReg; // 预留 uint8_t EraseVal; // 擦除后的值一般是0xFF uint32_t Reserved; // 预留 struct { uint32_t Sz; // 扇区大小 uint32_t Adr; // 扇区起始地址 } Sectors[512]; };对于QSPI Flash常用的Sectors填法是把Sz设为扇区大小比如4KBAdr设为Flash起始地址这样一个数组就能覆盖整个扇区表。如果你用的Flash是那种非均匀扇区布局比如W25Q系列4KB/32KB/64KB混合可以用多个Sectors表项分别描述。提示DeviceType对QSPI接口来说通常填EXT8BIT或EXT16BIT。但要注意这里的位宽指的是数据总线位宽QSPI虽然一次能传4位但在算法框架层面KEIL是按字节来读写数据的。新版MDK的算法框架建议直接用EXT8BIT然后在底层QSPI驱动里自行处理多线传输。3. 工程搭建从零开始还是改造现成模板3.1 推荐的做法复制官方模板制作下载算法时最快捷的方式不是新建一个空工程而是从Keil安装目录里复制现成的模板改。你可以在C:\Keil_v5\ARM\Flash\_Template下找到_Template工程这是ARM官方提供的通用算法模板包含了FlashDev.c、FlashPrg.c和FlashOS.h三个核心文件。如果你用的MCU是国产的比如GD32、APM32、AT32或者老版本的ARM芯片比如S3C2440_Template里可能没有你需要的启动文件或芯片头文件。这种情况下我更建议直接找一个同系列MCU的官方算法工程做底子。比如ST官方给STM32H7做的下载算法工程里面有完整的startup_stm32h743xx.s、system_stm32h7xx.c你只需要把QSPI驱动换成自己的即可。3.2 工程配置里的三个关键点把模板复制出来、改好文件后工程配置有几个地方特别容易踩坑第一RAM for Algorithm。在Options for Target - Target - Read/Write Memory Areas里要指定一个足够大的RAM区域。KEIL会把算法代码和数据存放在这里。以STM32H743为例On-chip RAM有512KB你可以把IRAM1起始地址设为0x20000000大小填0x20000实际占用很少但预留充足些没坏处。第二代码运行地址。在Linker选项卡里RW Base要跟上面的RAM起始地址保持一致。如果MHX工程里设置了Execution Region确保算法代码段被放在RAM地址区间内。一旦把Code区域放到Flash地址算法下载时就会被擦掉导致死循环或HardFault。第三编译器优化等级。建议把Optimization设为-O0或-O1。优化等级高了编译器可能把函数内联、变量优化掉导致你在调试算法时无法单步跟踪。算法本身运行在RAM里性能要求不高但可调试性非常重要。3.3 芯片手册和FLM文件的关系你做算法时不太需要理会FLM文件底层的格式但需要理解FLM文件里保存的不仅有代码还有FlashDevice描述信息。KEIL在加载算法时会解析这些描述然后在Flash Download配置列表里按DevName显示。这意味着如果你改了FlashDevice.DevName而没有重新编译生成FLMKEIL里显示的还是旧名字。如果你改了容量没有重新生成FLMKEIL会按旧容量计算下载范围最终写数据时可能会越界或者覆盖到了别的区域。每次修改FlashDev.c一定要重新编译并确认FLM文件的时间戳更新了。4. FlashDev.c修改告诉KEIL这颗QSPI Flash的底细4.1 一个具体实例GD25Q64CSIG为了把过程讲具体我以一颗很常见的QSPI Flash——兆易创新的GD25Q64CSIG为例。它是一颗64Mbit8MB容量的串行NOR Flash支持Standard SPI1-1-1、Dual SPI1-1-2、Quad SPI1-1-4和QPI4-4-4模式。我们使用Quad SPI模式来读写以获得更高的速度。下面是FlashDev.c里修改后的完整FlashDevice结构体你可以直接抄#include FlashOS.H struct FlashDevice const FlashDevice { FLASH_DRV_VERS, // 驱动版本使用模板自带宏 GD25Q64CS QSPI via QUAD, // 显示在KEIL下载列表里的名称 EXT8BIT, // 设备类型 0x90000000, // Flash起始地址按你的映射来 0x00800000, // 容量8MB 4096, // 页大小GD25Q64CS页大小为256字节这里填4KB 0x00, // 擦除值NOR Flash擦除后是0xFF 100, // 编程超时时间单位ms 3000, // 擦除超时时间单位ms { {0x1000, 0x00000000}, // 扇区大小4KB从Flash头部开始 {0x1000, 0x00001000}, // 如果Flash容量大需要多个扇区项 {0x1000, 0x00002000}, // ... 这里可以省略KEIL会自动按最后一个扇区大小扩展 {SECTOR_END} } };4.2 扇区表的自动扩展逻辑很多人不理解如果你只填了三四个扇区项后面几千个扇区KEIL怎么知道实际上KEIL会根据Sectors里第一个扇区项的大小自动推断后续扇区的布局。也就是说只要你给的第一个扇区描述能整除总容量KEIL就会按Sz大小一路延伸到DevSize。比如我上面填的三项都是4KB扇区KEIL就会认为整个8MB都是4KB扇区每4096字节一个扇区。这个做法对GD25Q64CS完全正确因为它的扇区确实都是4KB。但如果你的Flash是W25Q256那种混合扇区就要小心把每种扇区都描述清楚否则擦除时会选错扇区大小导致地址不对。4.3 提前确定好外部Flash的地址映射DevAdr字段十分关键它代表QSPI Flash在MCU地址空间中的映射地址。有两种情况**内存映射模式Memory-Mapped Mode**下MCU把外部Flash映射到一段可寻址地址空间。比如STM32H7把QSPI Flash映射到0x90000000。这时DevAdr填这个地址就可以。非内存映射模式下MCU不能直接寻址QSPI Flash只能通过发送命令来读写。这时KEIL的算法框架不支持因为算法函数是在MCU上跑的它没有办法通过自定义的地址访问来触发读写。所以做下载算法时必须使用内存映射模式或者至少在ProgramPage和EraseSector时切换到非映射模式但在地址上做一次转换。对于STM32系列QSPI接口在内存映射模式下是标准用法起始地址一般是0x90000000。但要注意H7系列的QSPI起始地址固定是0x90000000而F4系列如果用了FMC的NOR/SRAM控制器地址可能是0x60000000。以芯片参考手册为准。5. FlashPrg.c实现QSPI驱动与算法函数的编码细节5.1 顶层函数框架直接套模板模板里的FlashPrg.c已经提供了完整的函数框架我们只需要把里面跟Flash硬件相关的操作替换成自己的QSPI驱动即可。先看一下标准函数长什么样#include FlashOS.H static uint32_t FlashAddress; // 当前操作的目标地址 int Init(uint32_t adr, uint32_t clk, uint32_t fnc) { // 初始化QSPI外设、GPIO、时钟 QSPI_Init(); return 0; } int UnInit(uint32_t fnc) { // 可以关闭QSPI、恢复GPIO状态 QSPI_DeInit(); return 0; } int EraseChip(void) { // 发送全片擦除命令 QSPI_EraseChip(); return 0; } int EraseSector(uint32_t adr) { // 按4KB扇区擦除 QSPI_EraseSector(adr - 0x90000000); return 0; } int ProgramPage(uint32_t adr, uint32_t sz, uint8_t *buf) { // 按页编程256字节一页 QSPI_WritePage(adr - 0x90000000, buf, sz); return 0; } int Verify(uint32_t adr, uint32_t sz, uint8_t *buf) { // 回读校验 QSPI_Read(adr - 0x90000000, buf, sz); return 0; }这里有个小细节adr参数是KEIL传入的目标地址它是MCU视角下的绝对地址。所以如果映射地址是0x90000000QSPI Flash的数据偏移就是adr - 0x90000000。这个减法处理非常容易漏漏掉之后你会在调试时发现数据写到一半就飞了。5.2 QSPI底层驱动实现的几个原则你手头如果有现成的QSPI驱动那很好直接调。否则要写一个精简版。这里我给出编写要点以STM32H7的QSPI外设为例。初始化函数QSPI_Init要做的事使能GPIO时钟配置CLK、BK1_IO0~BK1_IO3为复用功能AF9。配置QSPI时钟分频确保输出时钟在Flash支持的最大频率以内。GD25Q64CSIG最高支持120MHz的Quad Read但为了可靠建议初始化为80MHz以内。配置QSPI为内存映射模式但做编程算法时更推荐先配置为间接模式Indirect Mode因为擦除、编程都需要发命令。设置Flash的基本参数页大小256、扇区大小4096、块大小64KB。读数据用间接模式读一次发出0x6B四线快速读命令先把地址发过去然后一字节一字节读回。KEIL在Verify时调用Verify函数会触发大量读操作所以读函数性能也会影响下载速度。写数据发0x12四线页编程命令在页内写数据。GD25Q64CS的页是256字节一次最多写256字节。如果KEIL传入的sz大于256你必须在ProgramPage里分页处理否则会回绕到页首造成数据错乱。擦除扇区发0x21四线扇区擦除命令。GD25Q64CS的扇区是4KB地址必须是4KB对齐的。模板会在传adr时保证对齐但你还是应该在函数开头做一次对齐检查。5.3 等待忙状态轮询还是中断Flash编程和擦除都需要时间GD25Q64CS擦除一个扇区典型耗时45ms编程一页典型耗时0.4ms。算法在执行这些操作时必须等待Flash内部的写周期完成。最常用的方式是轮询0x05读状态寄存器命令直到BUSY位变为0。void QSPI_WaitBusy(void) { uint8_t status; do { QSPI_Command(0x05); // 发送读状态寄存器命令 QSPI_ReadData(status, 1); } while (status 0x01); // BIT0为BUSY位 }在实际调试中我发现有些MCU的QSPI外设在间接模式下可以直接开启**自动轮询Auto Polling**功能让硬件代替CPU去等忙状态。比如STM32H7的QSPI外设就支持设置SMEN位和BUSY位掩码CPU不用一直去读状态寄存器。能用的建议用起来因为下载算法是运行在RAM里的CPU空转还会占用总线而且一旦连接调试器单步或断点轮询很容易超时。5.4 算法运行在RAM里代码要克制算法代码最终要放到MCU的RAM里运行所以别把整个应用级的QSPI驱动搬过来。我见过有人直接把带RTOS、带DMA、带中断的完整驱动塞进算法工程结果光是编译后的代码体积就有几十KBRAM放不下最后只能用最原始的轮询模式删改。原则很简单只保留Init/UnInit/Read/Write/Erase这五个基本操作不做任何缓存、不做中断、不做超时重试。算法工程里的代码越少调试越容易稳定性反而越高。6. 编译配置细节与FLM生成6.1 链接脚本与RAM分配的坑FLM算法工程的链接脚本并不需要复杂设置但如果你的MCU RAM分成了多段比如H7的DTCM、AXI SRAM那么需要确认算法链接的RAM段是KEIL能访问的。KEIL的Flash算法下载机制会把代码和数据的加载地址全部放在你指定的RAM区域。一般来说选一个可以同时被CPU和调试器正常读写的RAM就好。以STM32H743为例0x20000000起的DTCM RAM是CPU直接访问的调试器也能通过AHB-AP访问。但如果你的算法里QSPI外设要用到DMA比如用MDMA做内存映射模式的预取就要让DMA和CPU对RAM的访问保持一致否则可能出现缓存一致性问题。最简单的做法在算法工程中不要开D-Cache和I-Cache。QSPI Flash下载算法大多数情况下代码量不大不开Cache性能也可接受。6.2 从编译到FLM的检查编译成功后你会得到一个.axf文件KEIL会根据fromelf工具自动生成.flm。整个步骤在User选项卡里配置但模板工程默认已经配好了不用改。如果你改了FLM文件名记得在Options for Target - Output - Name of Executable里同步修改。FLM文件名会直接显示在Flash Download配置的下拉列表里。编译后建议打开生成的FLM文件确认一下大小。正常一个精简的QSPI算法FLM文件在1KB到4KB之间。如果超过了32KB你就要怀疑是不是链接脚本把代码区放到RAM时出了大问题比如把中断向量表也链接进去了。7. 在KEIL Target中配置自定义下载算法7.1 把FLM加进工程下载列表这一步很简单但很容易被忽略。打开Options for Target - Utilities - Settings在弹出的Flash Download对话框里先把Programming Algorithm列表里的默认项删掉然后点击Add找到你的FLM文件加入列表。关键点来了RAM for Algorithm的大小要大于FE算法代码实际占用的RAM。KEIL下载算法时会按照这个配置值在目标RAM里预留空间。如果算法代码实际占用超过你填的RAM大小KEIL会报Error: Flash Download failed - Cortex-M4之类的问题。如果你的MCU内部RAM不大可以改小算法代码逻辑或者把RAM for Algorithm填成实际值。7.2 下载地址和大小参数的正确填法在Flash Download对话框里除了选择算法外你还可以设置Start和Size。这表示KEIL允许下载到外部Flash的地址范围。如果你有一块外部Flash同时用来存代码和数据比如代码从0x90000000开始字库从0x90040000开始那么Start填0x90000000Size填0x00800000KEIL会按需下载。如果芯片同时有内部Flash和外部QSPI Flash你可以同时添加多个算法内部Flash算法保持默认外部Flash算法用你自己做的。KEIL在烧录时会根据目标的地址范围自动调用对应的算法。这个逻辑在FLM文件选中的情况下是自动的不需要额外配置。7.3 实测下载速度的感受我做的QSPI Flash下载算法在STM32H750上实测通过J-Link往GD25Q64CS里写入1MB数据总耗时大概8秒。这其中包括了擦除时间和编程时间。对比直接用SPI接口做下载速度快了接近4倍。如果你选择的Flash支持Quad编程但还跑在Standard SPI模式那下载速度会让人崩溃。8. 调试中的经典问题与排查链路8.1 KEIL提示Erase Failed但芯片明明没问题遇到这种问题先不要怀疑Flash坏了。我排查过好几次最后发现都是算法代码里的擦除命令没有正确发送导致的。一个典型的排查链路是这样的先在算法工程里加一个printf或标志位在EraseSector函数入口和出口打点然后单步跟踪看KEIL是否真的调用了这个函数。如果没有调用说明FLM文件没加载成功大概率是RAM for Algorithm设置太小或者FLM文件本身有问题。如果确实调用了下一步检查发送的地址是否经过偏移转换。我前面提到过adr - 0x90000000这一步漏掉后擦除命令会落在错误地址上。还有一个更隐蔽的坑QSPI Flash的擦除地址必须是4KB对齐的。KEIL算法框架保证传入地址是扇区对齐的但在调试时如果自己手动从0x90000000偏移了一些非对齐地址擦除就会失败。8.2 数据校验失败读写一致性问题的排查如果下载时报Verify failed问题通常出在以下三个环节第一读时序不对。QSPI Flash有几种读模式命令字不同地址位数不同等待周期也不同。如果你在QSPI_Read里用了0x6B四线快速读命令格式是命令0x6B24位地址8个dummy cycles然后连续输出数据。如果你的dummy周期数不匹配读出来的数据就是乱的。第二写缓存问题。有些MCU的QSPI外设在间接模式下有FIFO如果编程函数没有清掉FIFO里的残留数据可能导致写入的数据和实际想要的数据不一致。在发送编程命令前确保FIFO被清空或复位。第三编程函数分页没处理好。GD25Q64CS的页是256字节。如果你在ProgramPage里一次性写的数据跨越了页边界Flash内部会自动回绕到页首把后面的数据覆盖到页首地址去最终数据错乱。解决方案就是每次最多写256字节如果sz大于256就拆分为多页操作。int ProgramPage(uint32_t adr, uint32_t sz, uint8_t *buf) { uint32_t page_off, len; uint32_t flash_adr adr - 0x90000000; while (sz 0) { page_off flash_adr 0xFF; // 页内偏移 len 256 - page_off; // 到页尾的距离 if (len sz) len sz; QSPI_WritePage(flash_adr, buf, len); flash_adr len; buf len; sz - len; } return 0; }8.3 下载时MCU跑飞或进入HardFault这个问题的根源很多最常见的是算法代码运行地址不对。算法是放在RAM里跑的如果你的算法代码里引用了绝对地址的常量比如链接到了Flash地址在RAM里执行时就会跳到错误的地方。排查方法在Init函数入口处打断点单步看能否顺利执行到Init返回。如果进入HardFault查看PC寄存器的值如果落在了0x08xxxxxxFlash地址或0x00000000说明链接脚本有问题。如果PC在RAM地址区间说明是代码本身的问题比如QSPI外设的寄存器地址写错了。还有一个容易被忽略的点堆栈大小。算法运行时用的是KEIL加载算法时配置的栈空间这个栈位于RAM for Algorithm指定的区域内大小是固定的。如果你的QSPI初始化函数里用了大的局部数组很可能把栈顶溢出导致不可预期的行为。建议在算法工程里尽量用全局变量或静态变量替代大局部数组。8.4 下载一半报错查看FLM的RAM占用情况使用新版本MDK的Debug (printf) Viewer可以在算法下载过程中打印调试信息。但我更推荐直接观察Memory窗口。先把RAM地址设为算法工程的RW和ZI区域起始地址单步调试时观察这些内存区域的数据是否被正确初始化。如果发现数据全0或全FF说明算法并没有被正确加载。9. 进阶内存映射模式与代码执行一体的扩展思路做完了基础的下载算法你还可以把QSPI Flash当成XIPExecute In Place来用也就是让MCU直接跑外部Flash里的代码或者至少把频繁只读的数据放在外部Flash里统一管理。这种方案的下载算法是完全一样的但需要额外考虑一些事内存映射模式下的读访问是透明的MCU访问0x90000000地址就是读Flash的内容。但写操作不能通过内存映射直接完成必须切回间接模式发送编程命令。这个切换在算法层是透明的因为KEIL只会在编程和擦除时调用对应算法而内存映射模式下的读操作其实是在调试器做Verify时通过Verify函数间接完成的。如果你希望外部Flash能直接执行代码需要保证Flash的读等待时间足够否则CPU取指时可能因为QSPI时钟太快导致读错误。下载算法本身并不涉及这一部分但你在做启动代码或链接脚本时要有意识。更进一步如果你想把APP从外部Flash启动那么MCU的BootLoader必须先初始化QSPI再跳到0x90000000执行。这里也有一个鸡生蛋问题启动代码通常存放在内部Flash但内部Flash空间有限所以很多人会把BootLoader做得很精简把真正的主程序放到外部Flash。下载算法正好解决了开发环节的痛点——你不需要单独用烧录器去烧外部Flash的代码KEIL一把梭。10. 制作过程中的几点经验总结说实话做QSPI下载算法的门槛不在于代码本身而在于对两个框架的理解和对硬件时序的把控。我前前后后做了快两个星期才彻底调通回头来看有几条经验值得单独拎出来分享。第一善用逻辑分析仪或示波器来观察QSPI时序。当你怀疑算法运行正常但Flash就是不动作时用示波器抓一下CLK和IO线看命令和数据是否按预期发出。有一次我信号线的复用功能配置错了CLK频率高达40MHz但IO0根本没输出查了半天才发现是GPIO复用AF号写错。第二调试算法工程本身时不要把断点打在某个页编程的循环里。因为算法运行在RAM里你断在ProgramPage内部时QSPI Flash可能正在忙调试器再发起访问时就会失败。我把断点放在ProgramPage的入口和出口中间让算法一口气跑完这样成功率最高。第三给算法加一个简单的看门狗定时器。下载算法本身跑在RAM里如果你在调试过程中不小心让MCU跑飞有看门狗能自动复位省得你反复按复位键。但要注意下载算法时调试器已经接管了MCU看门狗一旦超时复位可能会导致调试会话中断。所以我不建议在产品化算法里加看门狗只在调试阶段临时加。第四FLM文件做好之后在别的工程里引用时要把算法文件复制到项目目录而不是一直引用编译输出目录。KEIL在下载时会去找FLM文件路径如果路径中有中文或空格某些版本会加载失败。稳妥起见把FLM放到工程根目录下的FlashAlgo文件夹里路径里不要有中文。如果你也是在用QSPI接口外挂Flash做产品特别是需要往里面烧字库、音频、升级包这类数据花半天时间做一个下载算法能换来以后每次下载都省下的十几分钟。这笔账怎么算都划算。真遇到问题也欢迎在评论区把你的MCU型号、Flash型号和报错信息发出来我看到了会尽量给排查思路。