起因

用 Keil 开发 STM32F429 + 外部 SPI Flash(W25QXX)项目时,把图片、字库等资源直接烧进外部 Flash 是刚需(比如 TouchGFX 把图片放在 QSPI 映射区 0x90000000)。但 Keil 默认只认识片内 Flash——点下载时它不知道你那颗 W25Q 该怎么擦、怎么写。解决办法就是给 Keil 做一个“Flash 下载算法”(.FLM 文件)。

需求

  • 制作 STM32F429 外挂 W25QXX(SPI 接口)的下载算法,让 Keil 一键把数据烧进外部 Flash
  • 烧写地址映射到 0x90000000(与 QSPI 内存映射区一致),工程里把资源直接分配到这段地址

技术实现原理

下载算法是什么:一段“借鸡生蛋”的小固件

调试器(ULINK/J-Link/ST-Link)自己不会擦写 W25Q——真正会操作 SPI Flash 的是 STM32 本身。Keil 的机制是:把“怎么擦写”写成一个裸机小工程(即 _Template 模板),编译成 FLM;下载时调试器先通过调试接口把这段代码搬进目标芯片的 RAM,让目标 MCU 自己跑,调试器只按约定的入口函数名调用并传参。这也解释了模板工程的几个特点:

  • 它是纯裸机程序:不能依赖你板子上 App 已经做过的初始化,Init() 里要自己配时钟、配 SPI;
  • 入口函数(Init/UnInit/EraseChip/EraseSector/ProgramPage/BlankCheck/Verify)的函数名和签名是 FlashOS.h 定死的协议,Keil 按名字找,少了就不行;
  • 返回 0 表示成功、非 0 表示失败,调试器据此决定是否中止烧写。

地址换算:0x90000000 与 base_adr

Keil 传给算法的 adr 是映射区地址(0x90000000 起),而 W25QXX 驱动要的是芯片内部偏移(从 0 开始),所以 EraseSector/ProgramPage 里都有 (adr - base_adr) 这一步换算。FlashDev.c 里的 Device Size、Sector Size、Page Size、超时时间则是告诉 Keil 这块“Flash”长什么样:按 4096 字节扇区对齐擦除,超时给 2000/3000ms 防止大数据量烧写被误判失败。

BlankCheck 与 Verify 的取舍

模板里 BlankCheck 返回 1(每次强制擦除)、Verify 直接返回 0(不校验)——最省事的配置,代价是烧写偏慢、且烧完不读回验证。要求高时可以实现真正的查空(读回全 0xFF 就跳过擦除)和逐字校验,速度与可靠性都能提上来。

1、找到Keil的官方Flash下载算法路径进行修改

下载算法的模版在keil安装目录\ARM\Flash下的_Template文件夹,把此文件夹复制出来,进行修改。也可以自行修改此工程名。

1
C:\keil5\ARM\Flash\_Template\NewDevice.uvprojx

2、选择自己的板子的芯片

3、添加SPI和W25QXX驱动

添加W25QXX的驱动代码, 去正点原子的论坛下载STM32F429的寄存器工程,拷贝出HARDWARE文件夹下的SPI和W25QXX驱动,和SYSTEM文件夹。借成熟驱动的目的是少踩 SPI 时序和 W25Q 命令码的坑,算法工程里只管调用。

4、添加相关驱动文件

5、修改FlashDev.c代码

修改FlashDev.c 中外部flash的大小

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include "../FlashOS.H"        // FlashOS Structures

struct FlashDevice const FlashDevice  =  {
   FLASH_DRV_VERS,             // Driver Version, do not modify!
	"STM32F429_W25QXX",   // Device Name 
	EXTSPI,
	0x90000000,// Device Start Address
	0x01000000,// Device Size in Bytes (16M)
	4096,// Programming Page Size
	0,                          // Reserved, must be 0
   0xFF,                       // Initial Content of Erased Memory
	2000,// Program Page Timeout 2000 mSec
	3000,                       // Erase Sector Timeout 3000 mSec
	0x001000,0x000000,// Sector Size  4kB (4 Sectors)
 SECTOR_END
};

6、修改FlashPrg.c代码

按照模版添加驱动代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include "../FlashOS.H"        // FlashOS Structures

#include "sys.h"
#include "delay.h"
#include "w25qxx.h"
#include "spi1.h"
#include "usart.h"

#define PAGE_SIZE            4096

uint8_t aux_buf[PAGE_SIZE];
uint32_t base_adr;
 
int Init (unsigned long adr, unsigned long clk, unsigned long fnc) {

  /* Add your Code */
     Stm32_Clock_Init(360,25,2,8);//设置时钟,180Mhz
     delay_init(180);            //初始化延时函数 
     //uart_init(90,115200);     //初始化串口波特率为115200
     W25QXX_Init();              //W25QXX初始化
     return (0);                                  // Finished without Errors
}

int UnInit (unsigned long fnc) {

  /* Add your Code */
  return (0);                                  // Finished without Errors
}

int EraseChip (void) {

  /* Add your Code */
	W25QXX_Erase_Chip();
	return (0);                                  // Finished without Errors
}

int EraseSector (unsigned long adr) {

  /* Add your Code */
	W25QXX_Erase_Sector((adr-base_adr)/4096);
	return (0);                                  // Finished without Errors
}

int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) {

  /* Add your Code */
	W25QXX_Write_NoCheck(buf,adr-base_adr,sz);
	return (0);                                  // Finished without Errors
}

int BlankCheck (unsigned long adr, unsigned long sz, unsigned char pat) {

    return (1);                                        /* Always Force Erase */
}
unsigned long Verify      (unsigned long adr,   // Verify Function
                           unsigned long sz,
                           unsigned char *buf)
{
        return 0;//直接返回0,表示成功                             
}

7、编译后将xxx.FLM拷贝到C:\keil5\ARM\Flash目录

工程编译完成后在输出目录下把STM32F429_W25QXX.FLM拷贝到Keil安装目录C:\keil5\ARM\Flash中, 之后在工程的 Debug 设置 → Flash Download 里 Add 这条算法,并把编程地址范围设为 0x90000000 起的那段即可调用。

代码下载

Github地址:https://gitee.com/Aladdin-Wang/hellotouchGFX.git

代码说明

  • FlashDev.c:这块外部 Flash 的“地图”——起始地址 0x90000000、16M 容量、4096 字节扇区、擦后初值 0xFF、编程/擦除超时。Keil 的下载对话框按这份表来安排擦写;
  • Init():时钟、SPI、W25Q 的初始化全在这里做。算法是被搬进 RAM 运行的独立小固件,不能假设外设已被你的 App 配置过;
  • EraseSector()/ProgramPage():核心各一行——(adr-base_adr) 把映射地址换算成 W25Q 内部偏移(擦除再除以 4096 得扇区号),然后交给驱动;
  • BlankCheck 返回 1、Verify 返回 0:强制擦除 + 跳过校验的最简配置,见上文原理节的分析。

踩坑记录与注意事项

  • base_adr 只声明没赋值:全局变量初值为 0,(adr-base_adr) 实际等于没减。使用时要让 Keil 下载设置里的算法起始地址与 FlashDev.c 的 0x90000000 一致,并在 Init 里给 base_adr 赋 0x90000000(或直接用宏写死),否则擦写偏移整体错位,现象是烧完数据全在错误地址上;
  • 算法工程必须链接在 RAM 里:模板已配好分散加载,别动;它运行时目标芯片的 App 还没跑,一切外设初始化都要 Init 里自己做;
  • 扇区号换算:EraseSector 里 (adr-base_adr)/4096 依赖 FlashDev.c 声明的 Sector Size 与 W25Q 实际扇区一致(W25Q 系列为 4K),换成 256 字节页的 Flash 要相应调整;
  • 超时别给太小:W25Q 整片擦除几十秒,如果要用 EraseChip,注意 Keil 侧的 Erase Timeout 与驱动里的等待循环要配套;
  • Verify 返回 0 等于放弃校验:烧写不读回验证,出现“图片偶发花屏/数据错”时先想到这一点,必要时实现真正的读回比较;
  • FLM 更新后要重新添加:同名覆盖后建议重启 µVision,老版本对 FLM 缓存偶尔不刷新。