)
手把手教你用W25Q64 SPI Flash扩展LVGL显示空间附完整代码在嵌入式GUI开发中资源受限是开发者经常面临的挑战。当项目需要显示丰富的图形界面时片上Flash往往捉襟见肘。本文将详细介绍如何通过W25Q64 SPI Flash扩展存储空间结合FatFs文件系统实现图片资源的外部存储与读取为LVGL提供充足的显示资源。1. 硬件准备与环境搭建1.1 硬件选型与连接W25Q64是一款8MB容量的SPI Flash芯片具有以下优势高性价比价格低廉且易于采购低功耗工作电流仅1mA待机0.1μA高速传输支持最高104MHz SPI时钟频率典型硬件连接方式MCU引脚W25Q64引脚备注PA5CLKSPI时钟PA6MISO主入从出PA7MOSI主出从入PA4CS片选3.3VVCC电源GNDGND地线注意确保所有GPIO已配置为SPI功能模式并启用正确的时钟源。1.2 开发环境配置推荐使用以下工具链组合IDESTM32CubeIDE 1.10编译器ARM GCC 10.3-2021.10调试工具J-Link EDU或ST-Link V3关键库文件准备# 获取LVGL官方库 git clone --branch v8.3 https://github.com/lvgl/lvgl.git # 下载最新FatFs wget http://elm-chan.org/fsw/ff/ff.zip2. SPI Flash驱动实现2.1 底层驱动开发首先实现基本的SPI读写函数// SPI发送接收单字节 uint8_t SPI_TransmitReceive(uint8_t data) { while(!(SPI1-SR SPI_SR_TXE)); // 等待发送缓冲区空 SPI1-DR data; while(!(SPI1-SR SPI_SR_RXNE)); // 等待接收完成 return SPI1-DR; } // W25Q64指令发送 void W25Q64_SendCommand(uint8_t cmd) { CS_LOW(); SPI_TransmitReceive(cmd); CS_HIGH(); }2.2 关键功能实现实现Flash的擦除、编程和读取操作#define PAGE_SIZE 256 #define SECTOR_SIZE 4096 // 扇区擦除4KB void W25Q64_EraseSector(uint32_t addr) { W25Q64_WriteEnable(); CS_LOW(); SPI_TransmitReceive(0x20); // Sector Erase指令 SPI_TransmitReceive((addr 16) 0xFF); SPI_TransmitReceive((addr 8) 0xFF); SPI_TransmitReceive(addr 0xFF); CS_HIGH(); W25Q64_WaitForWriteComplete(); } // 页编程256字节 void W25Q64_PageProgram(uint32_t addr, uint8_t *data) { W25Q64_WriteEnable(); CS_LOW(); SPI_TransmitReceive(0x02); // Page Program指令 SPI_TransmitReceive((addr 16) 0xFF); SPI_TransmitReceive((addr 8) 0xFF); SPI_TransmitReceive(addr 0xFF); for(int i0; iPAGE_SIZE; i) { SPI_TransmitReceive(data[i]); } CS_HIGH(); W25Q64_WaitForWriteComplete(); }3. FatFs文件系统移植3.1 diskio.c关键实现修改diskio.c实现底层驱动对接#include ff.h #include diskio.h #include w25q64.h #define FLASH_BASE_ADDR 0x000000 #define FLASH_SIZE (8*1024*1024) DSTATUS disk_status(BYTE pdrv) { return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { uint32_t addr FLASH_BASE_ADDR sector * FF_MIN_SS; W25Q64_ReadData(addr, buff, count * FF_MIN_SS); return RES_OK; } #if !FF_FS_READONLY DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { uint32_t addr FLASH_BASE_ADDR sector * FF_MIN_SS; for(UINT i0; icount; i) { W25Q64_PageProgram(addr i*FF_MIN_SS, buff i*FF_MIN_SS); } return RES_OK; } #endif DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch(cmd) { case GET_SECTOR_SIZE: *(WORD*)buff FF_MIN_SS; break; case GET_SECTOR_COUNT: *(DWORD*)buff FLASH_SIZE / FF_MIN_SS; break; case GET_BLOCK_SIZE: *(DWORD*)buff 1; // 1个扇区为1块 break; default: return RES_PARERR; } return RES_OK; }3.2 ffconf.h配置优化关键配置参数建议#define FF_USE_STRFUNC 2 // 启用字符串功能 #define FF_USE_FIND 1 // 启用文件查找功能 #define FF_USE_MKFS 1 // 启用格式化功能 #define FF_USE_LABEL 1 // 启用卷标功能 #define FF_USE_EXPAND 1 // 启用文件扩展功能 #define FF_CODE_PAGE 936 // 简体中文编码 #define FF_VOLUMES 1 // 单卷配置 #define FF_MAX_SS 512 // 扇区大小 #define FF_MIN_SS 512 #define FF_FS_TINY 1 // 使用精简模式4. LVGL与FatFs集成4.1 文件系统接口配置修改lv_conf.h启用FatFs支持#define LV_USE_FS_FATFS 1 #if LV_USE_FS_FATFS #define LV_FS_FATFS_LETTER 1 // 驱动器号 #define LV_FS_FATFS_CACHE_SIZE 256 // 缓存大小 #endif实现LVGL文件系统接口static void fs_init(void) { static FATFS fs; if(f_mount(fs, 1:, 1) ! FR_OK) { LV_LOG_ERROR(FatFs mount failed!); } } static bool fs_ready(lv_fs_drv_t * drv) { (void)drv; return true; // 总是返回就绪状态 } static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) { (void)drv; FIL * f lv_mem_alloc(sizeof(FIL)); if(f NULL) return NULL; BYTE flags 0; if(mode LV_FS_MODE_WR) flags FA_WRITE | FA_OPEN_ALWAYS; else if(mode LV_FS_MODE_RD) flags FA_READ; else if(mode (LV_FS_MODE_WR | LV_FS_MODE_RD)) flags FA_READ | FA_WRITE | FA_OPEN_ALWAYS; FRESULT res f_open(f, path, flags); if(res ! FR_OK) { lv_mem_free(f); return NULL; } return f; }4.2 图片资源处理与显示使用LVGL官方工具转换图片下载Image Converter工具选择输出格式为Binary RGB565设置输出路径并转换在代码中显示图片lv_obj_t * img lv_img_create(lv_scr_act()); lv_img_set_src(img, 1:/images/background.bin); lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);5. 常见问题排查5.1 挂载失败分析常见错误代码及解决方案错误代码可能原因解决方案FR_NO_FILESYSTEMFlash未格式化调用f_mkfs(1:, 0, 0)格式化FR_DISK_ERRSPI通信异常检查硬件连接和SPI配置FR_NOT_READY驱动未初始化确保W25Q64_Init()已调用FR_INVALID_DRIVE驱动器号错误检查LV_FS_FATFS_LETTER定义5.2 性能优化技巧启用DMA传输配置SPI DMA减少CPU占用// STM32CubeMX中启用SPI TX/RX DMA HAL_SPI_Transmit_DMA(hspi1, data, size);实现缓存机制减少Flash读取次数#define CACHE_SIZE 1024 static uint8_t cache[CACHE_SIZE]; static uint32_t cache_addr 0xFFFFFFFF; void ReadWithCache(uint32_t addr, uint8_t *buf, uint32_t len) { if(addr cache_addr addrlen cache_addrCACHE_SIZE) { memcpy(buf, cache(addr-cache_addr), len); } else { W25Q64_ReadData(addr, cache, CACHE_SIZE); cache_addr addr; memcpy(buf, cache, len); } }使用LVGL缓存配置LV_IMG_CACHE_DEF_SIZE提升图片加载速度6. 完整项目集成6.1 内存管理优化推荐的内存分配策略// 在lv_conf.h中配置 #define LV_MEM_SIZE (48*1024) // 分配48KB给LVGL #define LV_MEM_CUSTOM 1 // 使用自定义内存管理 // 实现自定义内存分配 void * my_malloc(size_t size) { static uint8_t mem_pool[LV_MEM_SIZE]; static size_t mem_used 0; if(mem_used size LV_MEM_SIZE) return NULL; void * ptr mem_pool[mem_used]; mem_used size; return ptr; }6.2 多任务处理建议如果使用RTOS建议任务划分void lvgl_task(void *arg) { while(1) { lv_task_handler(); vTaskDelay(5); } } void gui_task(void *arg) { // 创建UI元素 create_ui(); while(1) { update_ui(); vTaskDelay(20); } } void fs_task(void *arg) { // 处理文件系统操作 while(1) { process_fs_requests(); vTaskDelay(10); } }在实际项目中我们通过这种方式成功将LVGL应用的图片资源从内部Flash迁移到外部SPI Flash释放了超过150KB的片上存储空间使GUI能够支持更丰富的视觉效果。