STM32F743VIT6外部Flash烧录全攻略:从GD25Q64C驱动到MDK算法实战
最近在折腾一个基于STM32F743的GUI项目,UI资源文件(尤其是图片)的体积让我有点头疼。芯片内部的Flash虽然不小,但塞进一堆高清图标和背景图之后,空间立马捉襟见肘。手头正好有块闲置的GD25Q64C SPI Flash,容量8MB,放着也是浪费,就琢磨着能不能在程序烧录阶段,直接把UI资源“灌”进去,省去后期单独烧写外部存储的麻烦。
这想法听起来简单,实际操作起来却是个系统工程。你需要让MDK(Keil)认识这块外部Flash,把它当成一个可编程的存储区域,就像对待片内Flash一样。这个过程的核心,就是制作一个MDK能调用的Flash烧录算法文件(.FLM)。对于STM32F743VIT6和GD25Q64C这个组合,市面上没有现成的算法,只能自己动手。今天,我就把从驱动调试到算法生成、再到集成测试的完整流程和踩过的坑,系统地梳理一遍,希望能帮到有同样需求的开发者。
1. 理解MDK烧录算法:它到底是什么?
在开始动手之前,我们得先搞清楚要做的这个东西——MDK Flash烧录算法——究竟扮演什么角色。很多人以为烧录器(J-Link、ST-Link)是直接和Flash芯片对话的,其实不然。对于复杂的存储芯片,尤其是外部SPI Flash,烧录器需要一个“翻译官”和“操作员”。
这个“翻译官”就是Flash算法。它本质上是一段运行在目标芯片RAM中的小程序。当你点击MDK的下载按钮时,烧录器会先把这段算法代码加载到芯片的RAM里,然后通过算法代码中定义的函数(如初始化、擦除、编程),去间接操作外部Flash。你可以把它理解为一个专为烧录场景设计的、极度精简的驱动程序。
为什么非要这么麻烦?因为不同Flash的指令集、时序、页大小、扇区结构千差万别。MDK不可能内置所有型号的驱动。通过算法文件,MDK将具体的硬件操作抽象成几个标准接口,实现了烧录流程的统一。
一个合格的Flash算法文件(.FLM)必须实现以下核心函数接口:
// 定义在 FlashOS.H 中的关键函数原型 int Init (unsigned long adr, unsigned long clk, unsigned long fnc); // 初始化Flash和硬件 int UnInit (unsigned long fnc); // 反初始化 int EraseSector (unsigned long adr); // 擦除指定扇区 int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf); // 编程一页数据 unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf); // 校验数据 int BlankCheck (unsigned long adr, unsigned long sz, unsigned char pat); // 空白检查(可选) int EraseChip (void); // 全片擦除(可选)我们的任务,就是为GD25Q64C实现这些函数,并打包成MDK能识别的.FLM格式。
注意:算法代码在RAM中运行,因此对代码体积和RAM占用非常敏感。必须保持极简,移除任何非必要的库函数和初始化流程。
2. 搭建算法工程与硬件驱动适配
制作算法的第一步,是创建一个独立的Keil工程。这个工程和我们平时的应用工程目的不同:它不生成最终的可执行文件,而是生成一个.axf文件,再通过后处理命令转换成.FLM文件。
2.1 创建与配置算法专用工程
我建议直接从ARM提供的官方模板开始,或者用一个干净的HAL库基础工程改造。关键配置都在工程的Options for Target里:
Target选项卡:
- Read/Only Memory Areas: 这里定义的ROM地址不是Flash的实际物理地址,而是算法代码在RAM中的加载地址。通常设置为芯片RAM的起始地址,例如STM32F743的AXI SRAM地址
0x24000000。 - Read/Write Memory Areas: 同样设置RAM区域,用于算法运行时的堆栈和数据。
- IRAM区域大小需要仔细计算,必须大于算法代码编译后的体积,并留出足够的运行时堆栈空间。后面遇到的很多“内存不足”错误都源于此。
- Read/Only Memory Areas: 这里定义的ROM地址不是Flash的实际物理地址,而是算法代码在RAM中的加载地址。通常设置为芯片RAM的起始地址,例如STM32F743的AXI SRAM地址
Output选项卡:
- 勾选
Create Executable,输出.axf文件。 - 在
Name of Executable一栏,建议使用一个清晰的名称,如GD25Q64C.axf。
- 勾选
User选项卡: 这是生成.FLM文件的关键。我们需要添加编译后的执行命令,将.axf文件复制并重命名。
# 假设生成的目标名称为 GD25Q64C cmd.exe /C copy ".\Objects\GD25Q64C.axf" ".\GD25Q64C.FLM" cmd.exe /C copy ".\GD25Q64C.FLM" "C:\Keil_v5\ARM\Flash\GD25Q64C.FLM"第一条命令将编译产物复制到工程目录并改后缀,第二条命令将其安装到MDK的算法目录。请根据你实际的Keil安装路径和输出目录进行调整。
Linker选项卡:
- 必须使用Scatter File(分散加载文件)。这是控制代码和数据在RAM中如何布局的核心。
- 在Misc controls框中,建议添加
--diag_suppress L6305。这个警告常出现在算法这种特殊加载场景中,屏蔽它可以减少干扰信息。
一个典型的算法工程Scatter File (Target.lin) 内容如下:
; 链接器控制文件 (分散加载) PRG 0 PI ; 编程函数段,位置无关(PI)代码 { PrgCode +0 ; 代码段 { * (+RO) ; 所有只读代码和数据 } PrgData +0 ; 数据段 { * (+RW,+ZI) ; 所有读写和零初始化数据 } } DSCR +0 ; 设备描述段 { DevDscr +0 { FlashDev.o ; 包含Flash设备描述的结构体 } }PI(Position Independent)属性至关重要,它确保代码可以在RAM的任何地址运行,而不依赖绝对地址。
2.2 移植与精简GD25Q64C驱动
现在到了核心环节:让算法工程能够操作GD25Q64C。我们通常会有在应用工程中验证过的SPI驱动和Flash操作库(如SFUD)。直接把这些代码搬过来是不行的,必须进行“外科手术”式的精简。
首先,实现一个极度精简的硬件初始化函数Xmain()。 这个函数替代了标准工程中的main(),只做算法运行所必需的最小初始化:
- 系统时钟初始化(
SystemClock_Config()):必须要有,SPI总线速度依赖于此。 - GPIO初始化(
MX_GPIO_Init()):初始化连接Flash的SPI引脚。 - SPI外设初始化(
MX_SPIx_Init()):初始化与Flash通信的SPI接口。 - 关键点:去除一切冗余。不要初始化用不到的UART、I2C、DMA、中断控制器(如NVIC)。特别是SysTick中断,在算法环境中通常需要屏蔽。
由于算法环境没有操作系统和中断服务,HAL_Delay()这类依赖SysTick中断的函数会失效。我们必须提供一个基于循环等待的简化版本:
// 替代HAL库的Tick获取,使用简单循环计数 uint32_t HAL_GetTick(void) { static uint32_t ticks = 0U; for(uint32_t i = (SystemCoreClock >> 14U); i > 0U; i--) { __NOP(); // 执行一定数量的空操作来消耗时间 } ticks += 1; return ticks; } void HAL_Delay(uint32_t Delay) { uint32_t tickstart = HAL_GetTick(); while((HAL_GetTick() - tickstart) < Delay) { // 空循环等待 } } // 直接返回成功,避免初始化SysTick中断 HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) { return HAL_OK; }这里有个大坑:这个简化的HAL_GetTick()精度很低,循环一次的时间可能比你想象的长。如果你的SPI驱动里有用HAL_Delay()来等待Flash就绪的,可能需要大幅增加延时参数,否则会导致读写超时失败。
其次,修改Flash操作驱动。 如果你使用SFUD这样的库,需要检查其内部是否调用了malloc、printf或依赖文件系统的函数。在算法中,必须替换所有动态内存分配为静态数组,并移除所有文件操作。目标是让驱动只依赖最基本的HAL_SPI_Transmit/Receive函数。
最后,将精简后的驱动文件(.c/.h)添加到算法工程中,并实现下一节要讲的FlashPrg.c中的接口。
3. 实现Flash算法核心接口
所有算法逻辑最终都汇聚在FlashPrg.c这个文件中。我们需要在此文件中,调用上一步准备好的硬件初始化和Flash驱动,填充那6个核心函数。
3.1 设备描述符定义
在FlashDev.c中,我们需要定义一个FlashDevice结构体,告诉MDK关于GD25Q64C的所有物理信息。
struct FlashDevice const FlashDevice = { FLASH_DRV_VERS, // 驱动版本,勿改 "GD25Q64C 8MB @0x90000000", // 在MDK下拉列表中显示的名称 EXTSPI, // 设备类型:外部SPI Flash 0x90000000, // 设备起始地址(映射到内存空间的地址) 0x00800000, // 设备总大小 (8MB = 8*1024*1024) 256, // 编程页大小 (GD25Q64C页编程大小为256字节) 0, // 保留,必须为0 0xFF, // 擦除后的内存内容(通常为0xFF) 100, // 编程页超时(单位:ms) 3000, // 擦除扇区超时(单位:ms) // 扇区结构定义:4KB大小的扇区,共2048个 0x00001000, 0x00000000, // 第一个扇区大小4KB,起始偏移0 SECTOR_END // 结束标志 };- 设备起始地址:这个地址是映射到CPU内存空间的地址。对于通过QSPI或FMC内存映射模式访问的Flash,这就是映射后的首地址。对于普通SPI软件驱动访问,这个地址更像一个“基地址”,算法内部会进行偏移计算。我将其设为
0x90000000,这是一个在F7系列上常见的扩展内存区域地址。 - 设备大小:这里填写芯片的实际容量
0x00800000。如果你只想使用其中一部分(例如只用了2MB存资源),也可以填0x00200000,但务必与后续的分散加载文件配置对应。 - 扇区定义:GD25Q64C支持4KB、32KB、64KB扇区擦除。这里定义最小的擦除单元4KB。MDK在擦除时,会从起始地址开始,按照这个结构依次擦除所有扇区。
3.2 填充FlashPrg.c中的函数
现在,在FlashPrg.c中,将驱动能力与标准接口对接。
1.Init/UnInit函数:Init函数在每次擦除、编程、校验操作前都会被调用。它需要初始化硬件和Flash。一个常见的陷阱是:MDK可能会频繁调用Init和UnInit。如果每次Init都完整初始化SPI,而上次的SPI状态未正确释放,可能导致后续通信失败。
int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { // 在每次初始化前,先尝试反初始化一次,确保SPI处于干净状态 HAL_SPI_DeInit(&hspi6); HAL_GPIO_DeInit(GPIOx, GPIO_PIN_x); // 执行精简的硬件初始化 Xmain(); // 初始化Flash驱动(例如SFUD) if(sfud_init() != SFUD_SUCCESS) { return 1; // 失败 } sfud_dev = sfud_get_device(0); if(sfud_dev == NULL) { return 1; } return 0; // 成功 } int UnInit(unsigned long fnc) { // 这里可以做一些清理工作,如将SPI引脚设为高阻态 // 但通常直接返回成功即可 return 0; }2.EraseSector函数: 参数adr是MDK传递过来的绝对地址(即0x90000000 + 偏移)。我们需要从中减去设备基地址,得到在Flash内部的相对地址,再调用驱动的擦除函数。
int EraseSector(unsigned long adr) { unsigned long relative_addr = adr - 0x90000000; // 计算相对地址 // 调用驱动擦除一个4KB扇区 if(sfud_erase(sfud_dev, relative_addr, 4096) != SFUD_SUCCESS) { return 1; // 擦除失败 } return 0; // 成功 }3.ProgramPage函数: 这是最常用的函数,负责将数据写入Flash。GD25Q64C要求页编程(256字节)不能跨页。MDK会确保传入的sz参数不超过页大小,并且地址是页对齐的。
int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { unsigned long relative_addr = adr - 0x90000000; // 调用驱动页编程函数 if(sfud_write(sfud_dev, relative_addr, sz, buf) != SFUD_SUCCESS) { return 1; // 编程失败 } return 0; // 成功 }4.Verify函数: 校验是确保数据正确写入的关键。算法需要从Flash中读出刚写入的数据,与buf中的原始数据逐字节比较。
unsigned long Verify(unsigned long adr, unsigned long sz, unsigned char *buf) { unsigned long relative_addr = adr - 0x90000000; static uint8_t read_buf[256]; // 使用静态数组,避免动态分配 // 从Flash读取数据 if(sfud_read(sfud_dev, relative_addr, sz, read_buf) != SFUD_SUCCESS) { return (adr); // 读取失败,返回失败地址 } // 逐字节比较 for(unsigned long i = 0; i < sz; i++) { if(read_buf[i] != buf[i]) { return (adr + i); // 返回第一个不匹配的地址 } } // 全部匹配,返回下一个待校验的地址(即adr+sz) return (adr + sz); }5.BlankCheck和EraseChip函数: 这两个是可选的。BlankCheck用于检查某段区域是否已被擦除(全为0xFF)。对于外部Flash,如果访问速度慢,可以直接返回“需要擦除”(返回1),强制MDK执行擦除操作。EraseChip实现全片擦除,如果没实现,MDK会通过循环调用EraseSector来完成。
完成所有函数填充后,编译工程。如果没有错误,在输出目录和Keil的ARM\Flash目录下,你应该能看到生成的GD25Q64C.FLM文件。
4. 集成测试与深度调试技巧
生成.FLM文件只是成功了一半,把它用起来并稳定工作,才是真正的挑战。这一步会遇到各种意想不到的错误。
4.1 在应用工程中配置与使用算法
- 添加算法到工程:打开你的主应用工程,进入
Options for Target -> Debug -> Settings -> Flash Download。在Download Function部分,点击Add,你应该能看到刚刚生成的GD25Q64C 8MB @0x90000000算法,添加它。同时,确保Program、Erase、Verify等选项勾选。 - 配置分散加载文件:这是将数据分配到外部Flash的关键。我们需要修改应用的Scatter File,增加一个加载区域(Load Region),对应外部Flash的地址空间。
LR_IROM1 0x08000000 0x00200000 { ; 内部Flash,2MB ER_IROM1 0x08000000 0x00200000 { ; 执行区域 *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) ; 所有代码和只读数据放这里 } RW_IRAM1 0x20000000 0x00020000 { ; RAM区域... .ANY (+RW +ZI) } } ; 新增:定义外部Flash的加载区域 LR_EROM1 0x90000000 0x00800000 { ; 外部Flash,8MB,地址与算法定义一致 ER_EROM1 0x90000000 0x00800000 { ; 执行区域 *(USE_EXTERNAL_FLASH_SECTION) ; 将所有放在这个段的数据都加载到这里 } }- 将数据分配到外部Flash:在C代码中,通过编译器特性将常量数组、字体、图片等资源放到自定义的段中。
// 定义一个编译器相关的段宏,兼容ARMCC、IAR和GCC #if defined (__CC_ARM) #define EXT_FLASH_SECTION __attribute__((section("USE_EXTERNAL_FLASH_SECTION"), used)) #elif defined (__ICCARM__) #define EXT_FLASH_SECTION __root @ "USE_EXTERNAL_FLASH_SECTION" #elif defined (__GNUC__) #define EXT_FLASH_SECTION __attribute__((section("USE_EXTERNAL_FLASH_SECTION"), used)) #endif // 将一个图片数组分配到外部Flash EXT_FLASH_SECTION const uint8_t gImage_logo[] = { // ... 图片的二进制数据 };现在,当你编译并下载工程时,MDK会先通过内部Flash算法烧录主程序,再通过我们制作的GD25Q64C算法,将gImage_logo的数据烧录到外部Flash的0x90000000地址开始的位置。
4.2 常见错误分析与实战调试
即使一切配置看似正确,下载过程也可能失败。下面是我遇到过的几个典型问题及解决方法:
| 错误信息 | 可能原因 | 排查与解决方法 |
|---|---|---|
| Insufficient RAM for Flash Algorithm! | 算法工作内存(IRAM)设置太小。 | 在算法工程的Target选项里,增大IRAM1或IRAM2的尺寸。算法代码体积和运行时数据都在这里。 |
| Cannot Write to RAM for Flash Algorithms! | 算法代码编译后体积过大,超过了分配给它的RAM空间。 | 1.极致精简代码:移除所有调试打印、非必要的库函数。 2. 检查 Options for Target -> C/C++,将优化等级调到-O2或-O3,显著减少代码体积。3. 确保Scatter File正确,代码段确实被放到了RAM地址。 |
| Flash Timeout. Reset the Target and try it again. | 算法执行超时。这是最复杂的一类错误。 | 1.检查硬件初始化:确保SPI时钟、引脚模式正确。在算法Init中增加一个简单的GPIO翻转,用示波器看是否执行到了。2.检查延时:如前所述,算法环境下的 HAL_Delay不准,尝试将驱动中所有HAL_Delay(1)改为HAL_Delay(10)甚至更长。3.检查驱动状态机:确保每次 Init时,Flash驱动都处于干净的可工作状态。 |
| 下载时MDK软件闪退 | 算法代码存在严重错误,导致MDK在加载或执行算法时崩溃。 | 1. 回到算法工程,屏蔽所有Flash操作代码,只保留最基本的时钟和GPIO初始化,让Init、EraseSector等函数直接返回成功。如果此时下载不再闪退,说明问题在驱动层。2. 逐步添加驱动代码,定位崩溃点。 |
| 校验失败(Verify Failed) | 数据写入后读回不一致。 | 1.先验证驱动本身:在主应用工程中,写一个测试函数,对Flash进行独立的写、读、比较操作,确保驱动在非算法环境下是正常的。 2.检查地址计算:确保 ProgramPage和Verify函数中的地址偏移计算正确无误。3.检查SPI时序:算法环境下的CPU频率和主应用环境可能不同,导致SPI时序差异。 |
高级调试手段:打印日志在算法代码中集成串口打印是终极调试利器。虽然会增加代码体积,但在定位复杂问题时无可替代。
- 在算法工程中初始化一个UART(如USART2)。
- 在
FlashPrg.c的关键函数入口和出口添加printf。 - 编译生成带调试的算法,在下载时,用串口助手观察打印信息,你能看到MDK调用算法的完整流程:
Init(1)->BlankCheck->EraseSector->UnInit(1)->Init(2)->ProgramPage-> ... 这能帮你清晰判断算法卡在哪一步。
折腾完这一整套,当看到MDK顺利地将十几张图片烧进外部Flash,并且上电后GUI能直接从那里流畅加载资源时,那种成就感还是挺足的。整个过程最深的体会就是“精简”和“耐心”——算法代码必须像手术刀一样精确,而调试则需要像侦探一样,根据有限的错误信息,一步步还原出问题的真相。希望这份详细的攻略,能让你在给自己的STM32F743扩展存储空间时,少走些弯路。