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,初始化流程大同小异。我整理了一个通用流程:
- 硬件复位:拉低CS,发0x66复位指令,再发0x99确认
- 读取ID:发0x9F读3字节厂商ID+设备ID,验证型号
- 读取SFDP:发0x5A解析参数表
- 配置寄存器:设置状态寄存器,使能四线模式(如果需要)
- 退出四线模式:有些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 知识结构总览
下面这张图把本章的核心逻辑串起来了:
4.7 避坑指南
最后,我把这些年遇到的坑总结一下:
- CS引脚控制时序:有些Flash在指令之间需要CS拉高至少50ns,RT1052的GPIO翻转速度很快,建议加个NOP延时
- 状态寄存器检查:写/擦除操作后一定要轮询BUSY位,不要用固定延时。不同Flash的擦除时间差异很大
- 四线模式切换:进入四线模式后,后续所有指令都要用四线格式发送,包括读状态寄存器
- Cache一致性:使用DMA时,注意RT1052的Cache问题。读完后要invalid Cache,写之前要clean Cache