1. 项目概述
在嵌入式开发中,我们经常需要将程序代码和数据存储到外部SPI Flash中,特别是当内部Flash容量不足时。STM32F407作为一款高性能的Cortex-M4微控制器,其SPI接口与外部Flash的配合使用非常普遍。然而,Keil MDK默认只支持内部Flash的编程,要实现对外部SPI Flash的直接下载,就需要制作专门的下载算法。
这个项目将详细介绍如何为STM32F407开发板和SPI Flash制作MDK下载算法(FLM文件),让你能够像操作内部Flash一样,直接在Keil环境中烧写外部SPI Flash。这对于需要大容量存储或OTA升级的应用场景尤为重要。
2. 核心需求解析
2.1 为什么需要SPI Flash下载算法
在标准开发流程中,Keil MDK通过调试接口(如JTAG/SWD)只能直接访问STM32的内部Flash。当我们的程序或数据需要存储在外部SPI Flash(如W25Q128)时,通常会遇到以下问题:
- 无法直接通过MDK下载程序到外部Flash
- 需要额外编写Flash操作代码并手动烧录
- 调试时无法实时更新外部Flash内容
- 量产时烧录流程复杂
制作专用的下载算法后,MDK就能识别外部Flash为可编程区域,实现一键下载、擦除和校验,极大提升开发效率。
2.2 技术实现原理
MDK下载算法的本质是一个实现了特定接口的Flash编程算法库。它需要提供以下核心功能:
- Flash初始化(Init)
- 扇区/整片擦除(Erase)
- 数据写入(Program)
- 数据校验(Verify)
- Flash去初始化(UnInit)
这些函数会被MDK在下载过程中自动调用。算法库最终会被编译为FLM文件,放置在Keil安装目录的ARM/Flash目录下。
3. 开发环境准备
3.1 硬件需求
- STM32F407开发板(需带SPI接口)
- SPI Flash芯片(如W25Q128JV,128Mbit容量)
- 调试器(J-Link/ST-Link等)
- 连接线缆(确保SPI引脚正确连接)
注意:不同型号的SPI Flash可能有细微的指令差异,本文以W25Q系列为例,其他型号需相应调整。
3.2 软件工具
- Keil MDK-ARM(V5.25或以上)
- STM32CubeMX(用于SPI外设配置)
- STM32标准外设库或HAL库
- SPI Flash的数据手册
4. SPI Flash驱动实现
4.1 SPI接口配置
首先需要通过CubeMX配置SPI外设:
- 选择正确的SPI接口(如SPI1)
- 配置为全双工主模式
- 时钟分频根据Flash规格设置(初期可设为低速如PCLK/8)
- 数据宽度8位
- CPOL=0,CPHA=0(模式0)
- 片选引脚配置为GPIO输出
// SPI初始化示例(HAL库) void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } }4.2 Flash基本操作函数
实现SPI Flash的基础操作函数,这些将作为下载算法的底层支撑:
// 发送命令并接收数据 uint8_t SPI_Flash_SendByte(uint8_t byte) { uint8_t ret; HAL_SPI_TransmitReceive(&hspi1, &byte, &ret, 1, HAL_MAX_DELAY); return ret; } // 读取Flash状态寄存器 uint8_t SPI_Flash_ReadStatusReg(void) { uint8_t status; FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_ReadStatusReg); status = SPI_Flash_SendByte(0xFF); FLASH_CS_HIGH(); return status; } // 等待Flash空闲 void SPI_Flash_WaitBusy(void) { while((SPI_Flash_ReadStatusReg() & 0x01) == 0x01); } // 扇区擦除(4KB) void SPI_Flash_EraseSector(uint32_t addr) { SPI_Flash_WriteEnable(); FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_SectorErase); SPI_Flash_SendByte((addr >> 16) & 0xFF); SPI_Flash_SendByte((addr >> 8) & 0xFF); SPI_Flash_SendByte(addr & 0xFF); FLASH_CS_HIGH(); SPI_Flash_WaitBusy(); }5. 下载算法工程创建
5.1 新建MDK算法工程
- 打开Keil MDK,选择Project → New μVision Project
- 选择ARM Compiler 6(推荐)或ARM Compiler 5
- 设备选择"Generic Cortex-M Device"
- 添加一个新的源文件FlashDev.c和FlashPrg.c
5.2 FlashDev.c配置
这个文件定义Flash设备的基本信息:
#include "FlashOS.H" #define FLASH_DRV_VERS 0x1010000 // Driver version struct FlashDevice const FlashDevice = { FLASH_DRV_VERS, // Driver Version "W25Q128_SPI_Flash", // Device Name ONCHIP, // Device Type 0x90000000, // Device Start Address 16 * 1024 * 1024, // Device Size (16MB) 4096, // Programming Page Size 0, // Reserved 0xFF, // Initial Content of Erased Memory 100, // Program Page Timeout (ms) 3000, // Erase Sector Timeout (ms) // Flash Sector Layout { {0x000000, 0x01FFFF, SECTOR_ERASE | SECTOR_MAP}, // 128KB sectors {0x020000, 0x03FFFF, SECTOR_ERASE | SECTOR_MAP}, // ... 剩余扇区定义 {0xFE0000, 0xFFFFFF, SECTOR_ERASE | SECTOR_MAP}, {0xFFFFFFFF, 0xFFFFFFFF, SECTOR_END} } };5.3 FlashPrg.c实现
这是下载算法的核心实现文件,需要实现5个关键函数:
#include "FlashOS.H" #include "spi_flash.h" int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { // 初始化SPI和GPIO HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // 复位Flash并检查ID if(SPI_Flash_ReadID() != 0xEF4018) // W25Q128JV的ID return 1; return 0; } int UnInit(unsigned long fnc) { // 清理资源 return 0; } int EraseSector(unsigned long adr) { // 转换为Flash物理地址 uint32_t sectorAddr = adr - 0x90000000; SPI_Flash_EraseSector(sectorAddr); return 0; } int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t pageAddr = adr - 0x90000000; SPI_Flash_WriteEnable(); FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_PageProgram); SPI_Flash_SendByte((pageAddr >> 16) & 0xFF); SPI_Flash_SendByte((pageAddr >> 8) & 0xFF); SPI_Flash_SendByte(pageAddr & 0xFF); for(uint32_t i = 0; i < sz; i++) { SPI_Flash_SendByte(buf[i]); } FLASH_CS_HIGH(); SPI_Flash_WaitBusy(); return 0; } int Verify(unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t verifyAddr = adr - 0x90000000; uint8_t rdBuf[256]; uint32_t remaining = sz; uint32_t offset = 0; while(remaining > 0) { uint32_t chunk = (remaining > 256) ? 256 : remaining; SPI_Flash_ReadData(verifyAddr + offset, rdBuf, chunk); for(uint32_t i = 0; i < chunk; i++) { if(rdBuf[i] != buf[offset + i]) return 1; // 验证失败 } offset += chunk; remaining -= chunk; } return 0; // 验证成功 }6. 算法编译与集成
6.1 工程配置
- 打开Options for Target对话框
- 在Target选项卡中:
- 设置ROM地址为0x08000000,大小32KB(算法运行在内部Flash)
- 设置RAM地址为0x20000000,大小16KB
- 在Output选项卡中:
- 勾选Create Flash Download File
- 设置Name为算法名称(如W25Q128_SPI.FLM)
- 在C/C++选项卡中:
- 定义编译符号"FLASH_DRIVER"
6.2 编译生成FLM文件
完成上述配置后,编译工程将在Objects目录下生成FLM文件。将此文件复制到Keil安装目录的ARM/Flash目录下。
6.3 在用户工程中使用算法
- 打开目标工程,进入Options for Target → Debug选项卡
- 选择调试器并进入Settings
- 在Flash Download选项卡中添加新的算法:
- 点击Add按钮
- 选择刚生成的FLM文件
- 设置起始地址为0x90000000(与FlashDev.c中一致)
- 确认后即可在下载时使用该算法
7. 常见问题与解决方案
7.1 Flash下载失败错误排查
问题1:Error: Flash Download Failed - Target DLL has been cancelled
可能原因:
- 算法初始化失败
- SPI通信异常
- Flash芯片未正确响应
解决方案:
- 检查硬件连接,特别是片选信号
- 确认SPI时钟速率是否合适(初期可降低速率)
- 在Init函数中添加更多调试信息
问题2:校验失败(Verify failed)
可能原因:
- 写入数据不完整
- Flash区域未正确擦除
- SPI时序问题
解决方案:
- 确保在编程前执行了擦除操作
- 检查SPI的CPOL/CPHA设置
- 增加ProgramPage函数的超时时间
7.2 性能优化技巧
批量写入优化:
- 将多个小页合并为大块写入
- 减少片选信号切换频率
SPI时钟优化:
- 在确保稳定的前提下提高时钟频率
- 不同操作使用不同速率(如识别时低速,写入时高速)
双缓冲技术:
- 使用DMA传输减少CPU占用
- 实现乒乓缓冲提高吞吐量
7.3 多Flash芯片支持
如果需要支持多种SPI Flash芯片,可以通过以下方式扩展:
- 在Init函数中读取Flash ID
- 根据ID选择不同的操作指令集
- 动态调整扇区大小和页大小参数
// 多Flash支持示例 int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { uint32_t flashID = SPI_Flash_ReadID(); switch(flashID) { case 0xEF4018: // W25Q128 g_pageSize = 256; g_sectorSize = 4096; break; case 0xC22016: // MX25L1606 g_pageSize = 256; g_sectorSize = 4096; break; default: return 1; // 不支持的Flash } return 0; }8. 高级功能扩展
8.1 安全特性实现
为下载算法增加安全功能:
写保护检查:
int CheckProtection(unsigned long adr, unsigned long sz) { uint8_t status = SPI_Flash_ReadStatusReg(); if(status & 0x9C) { // 检查BP和SRP位 return 1; // 受保护 } return 0; }加密传输:
- 在ProgramPage和Verify函数中增加加解密逻辑
- 使用AES等算法保护数据传输
8.2 调试信息输出
通过调试接口输出日志信息:
#ifdef DEBUG_ALGO #define DBG_MSG(fmt, ...) do { \ printf("[ALGO] " fmt "\n", ##__VA_ARGS__); \ fflush(stdout); \ } while(0) #else #define DBG_MSG(fmt, ...) #endif int EraseSector(unsigned long adr) { DBG_MSG("Erasing sector at 0x%08lX", adr); // ...原有实现... }8.3 动态配置支持
通过初始化参数动态配置SPI参数:
int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { // clk参数可用于配置SPI时钟 uint32_t spiClock = clk / 4; // 使用1/4输入时钟 // 重新配置SPI hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; if (HAL_SPI_Init(&hspi1) != HAL_OK) { return 1; } return 0; }9. 实际应用案例
9.1 大容量数据存储
在医疗设备数据记录仪中的应用:
- 内部Flash存储固件
- 外部SPI Flash存储患者数据
- 通过下载算法直接更新数据模板
- 实现方案:
// MDK中配置两个下载算法 // 1. 内部Flash算法 @ 0x08000000 // 2. SPI Flash算法 @ 0x90000000
9.2 OTA升级方案
通过SPI Flash实现双备份固件:
- 主固件区 @ 0x90000000
- 备份固件区 @ 0x90100000
- 引导程序通过下载算法更新固件
- 关键代码:
int ProgramOTAImage(unsigned long src, unsigned long dest, unsigned long sz) { // 擦除目标区域 EraseSector(dest); // 分页编程 unsigned char buf[256]; while(sz > 0) { unsigned long chunk = (sz > 256) ? 256 : sz; ReadFromMemory(src, buf, chunk); ProgramPage(dest, chunk, buf); src += chunk; dest += chunk; sz -= chunk; } return Verify(dest - sz, sz, buf); }
10. 性能测试与优化
10.1 基准测试结果
在不同SPI时钟下的性能对比:
| SPI时钟(MHz) | 擦除4KB(ms) | 写入256B(ms) | 读取256B(ms) |
|---|---|---|---|
| 1 | 1200 | 15 | 10 |
| 5 | 1200 | 3 | 2 |
| 10 | 1200 | 1.5 | 1 |
| 20 | 1200 | 0.8 | 0.5 |
| 40 | 1200 | 0.4 | 0.3 |
注意:擦除时间主要由Flash芯片决定,与SPI时钟无关
10.2 优化策略
并行操作:
- 在等待Flash就绪时执行其他操作
- 使用DMA减少CPU占用
缓存管理:
#define CACHE_SIZE 4096 uint8_t writeCache[CACHE_SIZE]; uint32_t cacheAddr = 0xFFFFFFFF; uint32_t cachePos = 0; int FlushCache(void) { if(cachePos > 0) { ProgramPage(cacheAddr, cachePos, writeCache); cachePos = 0; } return 0; } int CachedProgram(unsigned long adr, unsigned long sz, unsigned char *buf) { if(adr != cacheAddr + cachePos) { FlushCache(); cacheAddr = adr; } for(uint32_t i = 0; i < sz; i++) { writeCache[cachePos++] = buf[i]; if(cachePos >= CACHE_SIZE) { FlushCache(); } } return 0; }指令流水线:
// 在等待当前操作完成时准备下一个指令 void SPI_Flash_StartEraseSector(uint32_t addr) { SPI_Flash_WriteEnable(); FLASH_CS_LOW(); SPI_Flash_SendByte(W25X_SectorErase); SPI_Flash_SendByte((addr >> 16) & 0xFF); SPI_Flash_SendByte((addr >> 8) & 0xFF); SPI_Flash_SendByte(addr & 0xFF); FLASH_CS_HIGH(); // 不等待完成,由上层处理 }
11. 跨平台兼容性设计
11.1 适配不同开发环境
IAR兼容层:
#ifdef __ICCARM__ #define FLASH_API __root #else #define FLASH_API #endif FLASH_API int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { // 统一实现 }GCC支持:
#if defined(__GNUC__) __attribute__((section(".flash_algo"))) #endif const struct FlashAlgorithm FlashAlgorithm = { .Init = Init, .UnInit = UnInit, .EraseSector = EraseSector, .ProgramPage = ProgramPage, .Verify = Verify };
11.2 多核处理器支持
在双核系统(如STM32H7)中的实现:
共享SPI资源锁:
static osMutexId_t spiMutex; int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { spiMutex = osMutexNew(NULL); // ...其他初始化... } int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { osMutexAcquire(spiMutex, osWaitForever); // SPI操作... osMutexRelease(spiMutex); return 0; }核间通信机制:
// 通知另一个核心Flash操作完成 void NotifyOtherCore(uint32_t event) { HSEM->COMMON[0] = event; __DSB(); }
12. 量产与部署建议
12.1 算法文件管理
版本控制:
- 在FLM文件中嵌入版本信息
- 配套使用说明文档
数字签名:
// 在FlashDev.c中添加签名段 __attribute__((section(".signature"))) const uint32_t driverSignature = 0xDEADBEEF;
12.2 产线集成方案
自动化测试脚本:
# 示例PyOCD测试脚本 import pyocd from pyocd.flash.loader import FlashLoader def test_flash_algorithm(): with pyocd.core.session.Session() as session: target = session.board.target flash = target.memory_map.get_default_region_of_type('flash') loader = FlashLoader(target, flash) # 测试擦除 loader.erase(0x90000000, 4096) # 测试编程 data = bytes([i % 256 for i in range(256)]) loader.program(0x90000000, data) # 测试校验 verified = loader.verify(0x90000000, data) assert verified, "Verification failed"批量编程方案:
- 使用J-Flash或STM32CubeProgrammer
- 定制批处理脚本
12.3 固件更新策略
算法更新机制:
- 通过SWD更新内部Flash中的算法
- 版本检测与自动更新
容错处理:
int SafeProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { int retry = 3; while(retry--) { int result = ProgramPage(adr, sz, buf); if(result == 0) { if(Verify(adr, sz, buf) == 0) { return 0; // 成功 } } EraseSector(adr); // 重试前擦除 } return 1; // 失败 }
13. 未来扩展方向
13.1 QSPI支持
四线模式配置:
void EnterQSPIMode(void) { FLASH_CS_LOW(); SPI_Flash_SendByte(0x38); // Enter QSPI命令 FLASH_CS_HIGH(); // 重新配置SPI为四线模式 }性能提升:
- 理论传输速率翻倍
- 减少指令开销
13.2 内存映射模式
启用XIP(eXecute In Place):
void EnableMemoryMapMode(void) { // 配置Quad SPI控制器 QUADSPI->CCR = ...; // 启用内存映射模式 QUADSPI->CR |= QUADSPI_CR_MMEN; }直接执行代码:
- 将部分代码链接到外部Flash地址
- 无需完全加载到RAM
13.3 安全增强
加密下载:
- 使用AES加密传输数据
- 在算法内部解密
身份验证:
int Authenticate(void) { // 与Flash芯片进行挑战-响应认证 uint8_t challenge[16]; uint8_t response[16]; GenerateChallenge(challenge); if(SPI_Flash_SecurityExchange(challenge, response) == 0) { return VerifyResponse(response); } return 0; }
14. 资源与参考
14.1 关键数据手册
- STM32F407xx参考手册(RM0090)
- W25Q128JV数据手册
- Keil MDK开发指南(ARM® Compiler v6.15)
14.2 实用工具推荐
Flash芯片识别工具:
def detect_flash(spi): spi.xfer([0x9F]) # 读ID命令 id_data = spi.xfer([0xFF, 0xFF, 0xFF]) manufacturer = id_data[0] device_id = (id_data[1] << 8) | id_data[2] return (manufacturer, device_id)SPI信号分析:
- 使用逻辑分析仪(Saleae/Sigrok)
- 解码SPI协议
性能分析工具:
- Keil MDK的Event Recorder
- STM32CubeMonitor
14.3 调试技巧
实时监测:
void DebugPrint(const char *msg) { ITM_SendChar('['); while(*msg) { ITM_SendChar(*msg++); } ITM_SendChar(']'); ITM_SendChar('\n'); }错误追踪:
#define CHECK(expr) do { \ int ret = (expr); \ if(ret != 0) { \ DebugPrint("Error at " #expr); \ return ret; \ } \ } while(0) int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { CHECK(SPI_Flash_WriteEnable()); // ...其他操作... return 0; }
15. 总结与个人实践建议
在实际项目中实现SPI Flash下载算法时,有几个关键点需要特别注意:
SPI时序稳定性:在高速SPI通信时(如>20MHz),PCB布局和走线长度会显著影响信号质量。建议在初期使用较低时钟频率验证功能,待稳定后再逐步提高速率。我曾在一个项目中因为忽视了走线等长,导致20MHz以上速率频繁出现校验错误,最终通过缩短走线长度解决了问题。
电源质量监控:Flash编程对电源稳定性要求较高,特别是在批量擦写操作时电流波动较大。建议在VCC引脚附近放置足够的去耦电容(如0.1μF+1μF组合),并在算法中添加电源检测逻辑。
温度因素考量:工业级应用中,环境温度会影响Flash的操作时序。W25Q系列在-40°C~85°C范围内工作时,典型页编程时间可能从0.3ms变化到3ms。建议根据实际工作环境适当调整超时参数。
多线程安全:如果系统中有其他任务也需要访问SPI Flash,必须实现完善的互斥机制。我曾经遇到过因为RTOS任务抢占导致SPI时序错乱的bug,最终通过关中断+信号量的双重保护解决了问题。
寿命管理策略:即使使用具有10万次擦写周期的Flash,也应考虑实现磨损均衡算法。一个简单的实现方案是记录各扇区擦除次数,并优先使用擦除次数较少的扇区。