4. 外部Flash驱动移植:SFDP协议解析、W25Q64/MT25QL等常见Flash初始化、读写擦除接口封装

好,咱们进入第四章。这一章可以说是整个LVGL图片加载项目的基石——外部Flash驱动。你想想看,如果Flash驱动不稳定,读出来的数据都是错的,那后面LVGL再怎么优化也是白搭。

我个人习惯,在移植任何存储设备驱动之前,先搞清楚两件事:协议怎么解析,芯片怎么初始化。咱们今天就围绕这两个核心点展开。

4.1 SFDP协议:让驱动自动适配Flash

先说说SFDP。这玩意儿全称是Serial Flash Discoverable Parameters,说白了就是Flash芯片自带的“身份证”和“说明书”。

以前我们写Flash驱动,得对着数据手册一个个查:页大小多少?扇区多大?支持哪些指令?换一颗Flash就得改代码,烦得很。

SFDP的出现就是为了解决这个问题。芯片出厂时就把这些参数固化在内部寄存器里,驱动通过标准指令读出来,自动配置。我在一个项目里同时用过W25Q64和MT25QL,靠SFDP一套代码搞定,省了不少事。

SFDP的核心价值:

  • 驱动与具体Flash型号解耦
  • 减少手动配置错误
  • 方便后续Flash升级替换

4.2 SFDP数据结构解析

SFDP数据存储在Flash的特定地址区域,通过0x5A指令读取。标准SFDP头结构如下:

// SFDP 头部 (20字节)
typedef struct {
    uint8_t  signature[4];    // 固定为 "SFDP"
    uint8_t  minor_rev;       // 次版本号
    uint8_t  major_rev;       // 主版本号
    uint8_t  num_params;      // 参数头数量
    uint8_t  reserved;        // 保留
    uint32_t length;          // 总长度
} sfdp_header_t;

// 参数头 (8字节)
typedef struct {
    uint8_t  id[2];           // 参数ID
    uint8_t  minor_rev;
    uint8_t  major_rev;
    uint16_t length;          // 该参数表长度
    uint8_t  reserved[2];
} sfdp_param_header_t;

读取流程其实不复杂:先发0x5A指令读20字节头部,验证签名是否为"SFDP"。然后根据参数头数量,逐个读取参数表。

嗯,这里要注意:有些老款Flash不支持SFDP,或者只支持部分参数表。我建议驱动里做个降级处理——如果读不到SFDP,就回退到手动配置模式。

4.3 常见Flash初始化流程

不管用W25Q64还是MT25QL,初始化流程大同小异。我整理了一个通用流程:

  1. 硬件复位:拉低CS,发0x66复位指令,再发0x99确认
  2. 读取ID:发0x9F读3字节厂商ID+设备ID,验证型号
  3. 读取SFDP:发0x5A解析参数表
  4. 配置寄存器:设置状态寄存器,使能四线模式(如果需要)
  5. 退出四线模式:有些Flash默认在四线模式,需要先退出再重新配置

我曾经踩过的坑:MT25QL上电后默认进入四线SPI模式,而W25Q64默认是标准SPI。如果你用同一套初始化代码,MT25QL会直接挂掉。解决办法是在初始化开头先发0xFF复位指令,让Flash回到标准模式。

4.4 读写擦除接口封装

接口封装这块,我习惯分层设计。底层是SPI驱动,中间层是Flash指令封装,上层才是给LVGL用的读写接口。

先看底层SPI接口:

// SPI 传输接口
typedef struct {
    void (*cs_control)(uint8_t level);      // CS控制
    void (*spi_transfer)(uint8_t *tx, uint8_t *rx, uint32_t len);
    void (*spi_dma_transfer)(uint8_t *tx, uint8_t *rx, uint32_t len);
    uint32_t (*get_tick)(void);             // 获取系统tick
} flash_spi_t;

中间层封装Flash指令:

// Flash指令封装
static void flash_write_enable(void);
static void flash_wait_busy(uint32_t timeout_ms);
static void flash_page_program(uint32_t addr, uint8_t *data, uint32_t len);
static void flash_sector_erase(uint32_t addr);
static void flash_read_data(uint32_t addr, uint8_t *buf, uint32_t len);

上层接口:

// 对外接口
int32_t flash_init(void);
int32_t flash_read(uint32_t addr, uint8_t *buf, uint32_t len);
int32_t flash_write(uint32_t addr, uint8_t *data, uint32_t len);
int32_t flash_erase(uint32_t addr, uint32_t len);

我的经验:写接口一定要做地址对齐检查。W25Q64的页大小是256字节,写入地址必须对齐到页边界。我曾经因为没做对齐检查,写入小数据时偶尔会出错,排查了两天才发现。

4.5 性能优化要点

Flash驱动性能直接影响LVGL图片加载速度。这里分享几个优化点:

优化项 说明 效果
四线SPI模式 使用4条数据线传输,速率翻4倍 读取速度从10MB/s提升到40MB/s
DMA传输 CPU不参与数据传输,释放算力 CPU占用率从80%降到5%
缓存对齐 缓冲区32字节对齐,利用Cache 读取速度提升20%
批量读取 一次读多个扇区,减少指令开销 连续读取效率提升30%

RT1052的FlexSPI外设支持四线模式,配合DMA,实测读取速度能到40MB/s以上。这个速度对于LVGL加载图片来说,绰绰有余。

4.6 知识结构总览

下面这张图把本章的核心逻辑串起来了:

外部Flash驱动移植知识结构 SFDP协议解析 硬件复位 读取ID验证 配置寄存器 SPI底层接口 指令封装层 读写擦除接口 LVGL适配层 四线SPI模式 DMA传输 缓存对齐 💡 核心思路: SFDP自动识别 → 通用初始化 → 分层接口封装 → 性能优化。这套架构可以适配市面上90%的SPI Flash。

4.7 避坑指南

最后,我把这些年遇到的坑总结一下:

  • CS引脚控制时序:有些Flash在指令之间需要CS拉高至少50ns,RT1052的GPIO翻转速度很快,建议加个NOP延时
  • 状态寄存器检查:写/擦除操作后一定要轮询BUSY位,不要用固定延时。不同Flash的擦除时间差异很大
  • 四线模式切换:进入四线模式后,后续所有指令都要用四线格式发送,包括读状态寄存器
  • Cache一致性:使用DMA时,注意RT1052的Cache问题。读完后要invalid Cache,写之前要clean Cache