我们在制作带有屏幕的项目时通常会遇到字库或者图片需要放到片外FLASH的情况,而通常这些文件要比片内flash容量还大,因此需要通过别的手段来把内容放入flash中,其中使用FLM算法是量产项目的最优解法。
一、总体思路
-
将ARM:CMSIS Pack文件夹(通常是Keil_v5\ARM\Pack\ARM\CMSIS\ version \Device_Template_Flash)中的工程复制,取消文件夹的只读属性,重命名项目文件NewDevice.uvprojx以表示新的flash 设备名称,例如MyDevice.uvprojx。
-
打开工程,从工具栏中,使用下拉选择目标来选择处理器架构。
-
打开对话框Project - Options for Target - Output并更改Name of Executable字段的内容以表示设备,例如MyDevice。
-
调整文件FlashPrg中的编程算法。
-
调整文件FlashDev中的设备参数。
-
使用Project - Build Target生成新的 Flash 编程算法。
但我亲自上手后遇到了一些问题,这里大致讲一下
①推荐使用标准库而不是HAL库

这段文本是KEIL给出的模板文件的说明,其中一段表示Flash编程算法使用只读位置无关码(ROPI)和读写位置无关码(RWPI) 而HAL 库默认不是位置无关的,且体积较大。这也解释了我为什么测试算法的时候总是超时。
②制作算法跟编译器没太大关系,使用AC5制作的烧录算法,用AC6编译出来的程序也可以烧录,它并不是一个完整的程序
二、具体操作
测试平台:STM32F407VET6 W25Q128
①首先创建一个能正常读写W25Q128的工程(主要是确保能够正常读写Flash),这里我选择了立创天空星的模板工程二十六、SPI-FLASH应用 | 立创开发板技术文档中心
②将_Template工程复制,取消文件夹的只读属性(对文件夹右键------更多),并创建一个新文件夹来装我们的文件,并把FlashOS.h文件也加进来(这个文件跟模板文件夹一个层级)
③加入更多依赖文件,我们直接在立创提供的网盘文件里搜索就可以
这里board和spi_flash是读写SPI用到的,还要包括
系统初始化文件(system_stm32f4xx.c 和 system_stm32f4xx.h)
芯片头文件(stm32f4xx.h)
标准外设驱动库文件(stm32f4xx_gpio,stm32f4xx_rcc,stm32f4xx_spi)
库配置文件(stm32f4xx_conf.h)
Cortex-M 核心库文件(core_cm开头的文件)

④选择处理器架构,另外这里不要忘了在C/C++加入这一行定义
USE_STDPERIPH_DRIVER,STM32F40_41xxx,


⑤修改配置文件 FlashDev.c(这块是对flash基本信息做配置)
cpp
#include "FlashOS.H" // FlashOS Structures
struct FlashDevice const FlashDevice = {
FLASH_DRV_VERS,
"STM32F407_W25Q128", /* 外部 Flash 标识 */
EXTSPI,
0x00000000, /* 无内存映射,起始地址为 0 */
0x01000000, /* 16MB */
256,
0,
0xFF,
300, /* 页编程 */
5000, /* 5s 扇区擦除(兼容 64KB 块擦除) */
0x001000, 0x000000, /* 4KB 扇区 */
SECTOR_END
};
| 参数 | 值 | 说明 |
|---|---|---|
FLASH_DRV_VERS |
驱动版本 | 固定值,不要修改 |
| Device Name | "STM32F407_W25Q128" | 设备描述名称 |
| Device Type | EXTSPI |
设备类型(ONCHIP/EXTSPI) |
| Start Address | 0x00000000 |
Flash起始地址 |
| Device Size | 0x01000000 |
设备总大小(16MB) |
| Page Size | 256 |
编程页大小(可用4096字节,更快) |
| Reserved | 0 |
保留字段 |
| Erased Value | 0xFF |
擦除后内存的初始值 |
| Program Timeout | 300 |
页编程超时时间(ms) |
| Erase Timeout | 5000 |
扇区擦除超时时间(ms) |
| Sector Table | 0x001000, 0x000000 |
扇区配置:大小+地址对 |
⑥修改配置文件 FlashPrg.c
在这个 C 文件里,需要我们包含一些必需的 Flash 编程函数:Init, UnInit, EraseSector, 和 ProgramPage. 然后除了这些必需函数,因为我们这里并非XIP(直接映射地址),是直接加了一块FLASH,必须实现BlankCheck 和 Verify 功能,至于 EraseChip最好也加上不差这一个了。
这 7 个函数的功能,分别是:
Init 必需函数 初始化,并准备设备以进行 Flash 编程。
UnInit 必需函数 完成某个 Flash 编程步骤后,取消微控制器的初始化。
EraseSector 必需函数 删除指定扇区的 Flash 内存内容。
ProgramPage 必需函数 将应用程序写入到 Flash 内存中。
BlankCheck 可选/必需函数 检查并比较数据模式。
EraseChip 可选/必需函数 删除整个 Flash 内存的内容。
Verify 可选函数 将 Flash 内存内容与程序代码进行比较。
这里根据嘉立创提供的驱动函数进行包装
cpp
#include "FlashOS.H" // FlashOS Structures
#include "spi_flash.h"
#include "board.h"
/*
Mandatory Flash Programming Functions (Called by FlashOS):
int Init (unsigned long adr, // Initialize Flash
unsigned long clk,
unsigned long fnc);
int UnInit (unsigned long fnc); // De-initialize Flash
int EraseSector (unsigned long adr); // Erase Sector Function
int ProgramPage (unsigned long adr, // Program Page Function
unsigned long sz,
unsigned char *buf);
Optional Flash Programming Functions (Called by FlashOS):
int BlankCheck (unsigned long adr, // Blank Check
unsigned long sz,
unsigned char pat);
int EraseChip (void); // Erase complete Device
unsigned long Verify (unsigned long adr, // Verify Function
unsigned long sz,
unsigned char *buf);
*/
/*
* Initialize Flash Programming Functions
* Parameter: adr: Device Base Address
* clk: Clock Frequency (Hz)
* fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
* Return Value: 0 - OK, 1 - Failed
*/
int Init (unsigned long adr, unsigned long clk, unsigned long fnc) {
/* 初始化SPI接口 */
bsp_spi_init();
/* 可选:读取ID验证芯片连接(调试用,不影响功能) */
// uint16_t id = W25Q128_readID();
return (0); // Finished without Errors
}
/*
* De-Initialize Flash Programming Functions
* Parameter: fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
* Return Value: 0 - OK, 1 - Failed
*/
int UnInit (unsigned long fnc) {
/* 确保片选拉高,SPI进入空闲状态 */
W25QXX_CS_ON(1);
return (0); // Finished without Errors
}
/*
* Erase complete Flash Memory
* Return Value: 0 - OK, 1 - Failed
*/
int EraseChip (void) {
/* 发送写使能指令 */
W25Q128_write_enable();
/* 等待设备就绪 */
W25Q128_wait_busy();
/* 发送整片擦除指令 (0xC7 或 0x60) */
W25QXX_CS_ON(0);
spi_read_write_byte(0xC7); // Chip Erase指令
W25QXX_CS_ON(1);
/* 等待擦除完成(整片擦除可能需要20-100秒) */
W25Q128_wait_busy();
return (0); // Finished without Errors
}
/*
* Erase Sector in Flash Memory
* Parameter: adr: Sector Address (absolute address)
* Return Value: 0 - OK, 1 - Failed
*
* Note: W25Q128 sector size is 4KB (4096 bytes)
*/
int EraseSector (unsigned long adr) {
/* 将绝对地址转换为扇区号(W25Q128共有4096个扇区,每个4KB) */
uint32_t sector_num = adr / 4096;
/* 调用已有的扇区擦除函数 */
W25Q128_erase_sector(sector_num);
return (0); // Finished without Errors
}
/*
* Program Page in Flash Memory
* Parameter: adr: Page Start Address (must be page-aligned, 256 bytes alignment)
* sz: Page Size (max 256 bytes, function handles larger sizes by looping)
* buf: Page Data
* Return Value: 0 - OK, 1 - Failed
*
* Note: W25Q128 page size is 256 bytes. Writing can only be done within one page
* (cannot cross page boundary in a single operation).
*/
int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) {
uint32_t i;
uint32_t page_size;
/* W25Q128一页为256字节,如果数据超过一页需要分页写入 */
while (sz > 0) {
/* 计算本次写入的字节数(不超过256字节,且不超过当前页剩余空间) */
page_size = 256 - (adr % 256); // 当前页剩余空间
if (page_size > sz) {
page_size = sz;
}
/* 写使能 */
W25Q128_write_enable();
/* 等待设备就绪 */
W25Q128_wait_busy();
/* 发送页编程指令 */
W25QXX_CS_ON(0);
spi_read_write_byte(0x02); // Page Program指令
/* 发送24位地址(高->中->低) */
spi_read_write_byte((uint8_t)(adr >> 16));
spi_read_write_byte((uint8_t)(adr >> 8));
spi_read_write_byte((uint8_t)(adr));
/* 发送数据 */
for (i = 0; i < page_size; i++) {
spi_read_write_byte(buf[i]);
}
/* 拉高片选,开始编程 */
W25QXX_CS_ON(1);
/* 等待编程完成 */
W25Q128_wait_busy();
/* 更新指针和计数器 */
sz -= page_size;
buf += page_size;
adr += page_size;
}
return (0); // Finished without Errors
}
/*
* Blank Check Flash Memory
* Parameter: adr: Block Start Address
* sz: Block Size
* pat: Block Pattern (通常未使用,默认为0xFF)
* Return Value: 0 - OK (Blank), 1 - Failed (Not Blank)
*/
int BlankCheck (unsigned long adr, unsigned long sz, unsigned char pat) {
uint8_t buffer[256]; // 使用缓冲区批量读取,提高效率
uint32_t i, chunk;
(void)pat; // 参数未使用,避免编译警告(W25Q128擦除后固定为0xFF)
while (sz > 0) {
/* 计算本次读取的块大小 */
chunk = (sz > 256) ? 256 : sz;
/* 读取数据到缓冲区 */
W25Q128_read(buffer, adr, chunk);
/* 检查是否为空白(0xFF) */
for (i = 0; i < chunk; i++) {
if (buffer[i] != 0xFF) {
return (1); // 发现非空白数据,返回失败
}
}
/* 更新地址和剩余大小 */
adr += chunk;
sz -= chunk;
}
return (0); // 所有数据都是0xFF,空白检查通过
}
/*
* Verify Flash Memory
* Parameter: adr: Start Address
* sz: Size
* buf: Data to Compare
* Return Value: (adr + sz) - OK (Verified), 其他值 - 第一个不匹配位置的地址
*/
unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf) {
uint8_t buffer[256]; // 使用缓冲区批量读取
uint32_t i, chunk;
uint32_t offset = 0; // 当前校验的偏移量
while (sz > 0) {
/* 计算本次读取的块大小 */
chunk = (sz > 256) ? 256 : sz;
/* 读取Flash数据到缓冲区 */
W25Q128_read(buffer, adr + offset, chunk);
/* 逐字节比较 */
for (i = 0; i < chunk; i++) {
if (buffer[i] != buf[offset + i]) {
/* 返回第一个不匹配字节的地址 */
return (adr + offset + i);
}
}
/* 更新偏移量和剩余大小 */
offset += chunk;
sz -= chunk;
}
/* 全部校验通过,返回结束地址(表示成功) */
return (adr + offset);
}
最后编译并将生成的flm文件拷贝到...\Keil_v5\ARM\Flash目录,即可被MDK识别到
三、测试算法
这里给出大致思路
①在download界面加入算法

②在linker界面编辑SCT文件,告诉编译器什么地方放什么文件
cpp
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000 { ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x0001C000 { ; RW data
.ANY (+RW +ZI)
}
RW_IRAM2 0x2001C000 0x00004000 {
.ANY (+RW +ZI)
}
}
; -----------------------------------------------------------------------------
; External SPI Flash (W25Q128) region
; NOTE:
; - This region is for "assets/data" only, NOT for code execution.
; - Keil will program this address range using the W25Q128 .FLM algorithm
; (0x00000000 - 0x00FFFFFF, 16MB) when you add it in Flash Download settings.
; - Only sections explicitly placed into ".extflash" will go here.
; -----------------------------------------------------------------------------
LR_EXTFLASH 0x00000000 0x01000000 {
ER_EXTFLASH 0x00000000 0x01000000 {
*(.extflash*)
}
}
③放入一段数组
cpp
#include <stdint.h>
/*
* 这个文件里的数据会被链接到外部 SPI Flash (W25Q128):
* - 依赖 Scatter 文件把 ".extflash" 段映射到 0x00000000
* - 下载时 Keil 会用 W25Q128 的 FLM 把该段写入外部 Flash
*/
/* AC6可能会对未引用的段做裁剪,这里用 used 强制保留该符号到最终镜像中 */
__attribute__((section(".extflash"), used))
const uint8_t g_extflash_test_pattern[256] = {
/* 0x00..0xFF 递增,方便读回校验 */
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
};
④在STM32的片内flash加入读取测试函数
cpp
/* 只取地址,避免被链接器裁剪(切勿去访问该地址指向的数据) */
s_keep_extflash_asset_addr = (uintptr_t)&g_extflash_test_pattern[0];
/* 使用 FLM + SPI 验证外部 W25Q128 中的测试数据是否写入成功
数据模式:0x00 ~ 0xFF 递增,共 256 字节,对应 extflash_assets.c 中的 g_extflash_test_pattern */
printf("\r\n[EXTFLASH] 开始验证 W25Q128 中的测试数据...\r\n");
uint8_t ext_read_buf[256];
memset(ext_read_buf, 0, sizeof(ext_read_buf));
/* 直接使用 w25flash 驱动,从外部 Flash 芯片偏移 0x00000000 读取 256 字节 */
uint16_t ext_id = Flash_ReadID();
printf("[EXTFLASH] Flash_ReadID = 0x%04X\r\n", ext_id);
Flash_FastReadBytes(0x00000000u, ext_read_buf, (uint16_t)sizeof(ext_read_buf));
printf("[EXTFLASH] 前16字节: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", ext_read_buf[i]);
}
printf("\r\n");
int ext_err_cnt = 0;
for (int i = 0; i < 256; i++) {
uint8_t expected = (uint8_t)i; /* 对应 g_extflash_test_pattern 的模式 */
if (ext_read_buf[i] != expected) {
ext_err_cnt++;
if (ext_err_cnt <= 5) { /* 只打印前几个错误,避免刷屏 */
printf("[EXTFLASH] 数据错误 index=%d, 期望=0x%02X, 实际=0x%02X\r\n",
i, expected, ext_read_buf[i]);
}
}
}
if (ext_err_cnt == 0) {
printf("[EXTFLASH] 验证成功:0x00000000 处 256 字节数据与模式 0x00~0xFF 完全一致。\r\n");
} else {
printf("[EXTFLASH] 验证失败:共有 %d 个字节不匹配,请检查 FLM 配置或外部 Flash 连接。\r\n", ext_err_cnt);
}
⑤连接串口,显示结果
