拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32上LVGL从SD卡加载图片:FatFs文件系统实战指南

简介面向嵌入式开发者的STM32与LittlevGLLVGL图形库文件系统集成资源。该资源围绕在STM32平台上挂载FATFS等文件系统并通过LVGL界面加载外部存储中的图片与数据展开适合有一定单片机基础、希望为UI应用增加存储能力的开发者。压缩包共1133个文件容量28.96MB包含大量C源码290个c、164个h、Keil工程文件uvprojx/uvoptx、编译输出o/axf/hex/bin以及日志与脚本文件py/bat另有PNG/GIF图片素材和Markdown文档便于对照工程进行二次开发与调试。已有3820人学习。资源涵盖完整工程源码、LVGL驱动与文件系统中间层示例、可烧录固件及辅助转换脚本文件目录结构清晰可帮助开发者快速理解LVGL与文件系统的连接方式减少移植踩坑。1. 在 STM32 上让 LVGL 脱离“只能画内置图”的束缚接手过一个需要频繁换肤的工控屏项目UI 素材 30 多张每张接近 100KB。如果全部转成 C 数组塞进片内 FlashSTM32F407 的 1MB Flash 至少烧掉一半而且每改一张图就要重新烧录固件。换 SD 卡后图片以.bin文件形式存放在文件系统里LVGL 运行时去读文件固件体积几乎不变皮肤资源还可以被用户自行替换。这个 zip 包里正好就是一组_argb8888.bin原始图像文件配合 LVGL 的lv_img控件可以直接显示。如果你正被 Flash 容量、资源更新频率折磨或者想在 STM32 上跑带相册、壁纸、动态配置界面的 GUI这整套「LVGL FatFs」的链路值得拆开看一遍。2. LVGL 文件系统抽象层先搞清楚 FatFs 与 lv_fs_if 的责任边界2.1 FatFs 挂载 SD 卡HAL 库下最小可用配置LVGL 本身不关心数据来自 SD 卡还是 SPI Flash它只认逻辑路径。而真正的文件读写工作由 FatFs 这一层完成。因此第一步是先把存储介质跑通这里以 STM32H750 SDMMC1 为例用 STM32CubeMX 生成的初始化代码FATFS fs; // FatFs 文件系统对象 FIL file; // 文件操作句柄 FRESULT res; // 返回值检查 void SD_Init_And_Mount(void) { // 1. 底层介质初始化CubeMX 已生成 MX_SDMMC1_SD_Init() MX_SDMMC1_SD_Init(); // 2. 挂载 SD 卡0 号盘符立即挂载 res f_mount(fs, 0:, 1); if (res ! FR_OK) { // 挂载失败常见原因卡未插入、FAT 表损坏、SDMMC 时钟太高 Error_Handler(); } // 3. 检查是否存在目标路径不存在则创建 if (f_stat(0:/lvgl_img, NULL) FR_NO_PATH) { f_mkdir(0:/lvgl_img); } }f_mount第三个参数1表示立即挂载如果传入0则延迟到第一次访问时才挂载。这里要注意CubeMX 生成的HAL_SD_Init只初始化了 GPIO 和 SDMMC 外设真正让 SD 卡进入传输模式还需要HAL_SD_ConfigWideBusOperation切换为 4 位总线再配合 DMA 或中断模式做数据搬运。很多第一次接触 FatFs 的人忽略了一个关键点挂载成功不代表可以直接读写还要保证底层BSP_SD_ReadBlocks能被disk_read正确封装。2.2 编写 lv_fs_if 驱动回调让 LVGL 认识文件系统LVGL 提供了一套文件系统抽象核心是lv_fs_drv_t结构体。你需要把open、read、seek、close等回调实现出来LVGL 内部通过lv_fs_open、lv_fs_read等 API 操作文件而不直接调用 FatFs。官方推荐的lv_fs_if组件有 FatFs 的适配示例但很多移植版本过期我一般自己写回调结构更可控static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) { FIL * fp (FIL *)lv_mem_alloc(sizeof(FIL)); if (fp NULL) return NULL; // 路径映射LVGL 路径以 SD:/ 开头去掉文件系统盘符前缀 const char * ff_path path 4; // 跳过 SD:/ BYTE ff_mode FA_READ; if (mode LV_FS_MODE_WR) ff_mode | FA_WRITE; if (mode LV_FS_MODE_RD) ff_mode | FA_READ; FRESULT res f_open(fp, ff_path, ff_mode); if (res ! FR_OK) { lv_mem_free(fp); return NULL; } return fp; } static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br) { FIL * fp (FIL *)file_p; UINT bytes_read 0; FRESULT res f_read(fp, buf, btr, bytes_read); *br bytes_read; return res FR_OK ? LV_FS_RES_OK : LV_FS_RES_UNKNOWN; }open回调里我用手动lv_mem_alloc分配FIL结构体原因是 LVGL 可能同时打开多个文件而 FatFs 默认FF_FS_LOCK数量有限如果直接在栈上开一个全局数组管理文件句柄就会和 LVGL 的缓存机制冲突。路径映射这里最容易写错LVGL 注册驱动时会给一个挂载前缀比如SD:/回调收到的path会包含此前缀需要自行剥离出 FatFs 能识别的路径。同时在fs_seek中要注意f_lseek的分支只读模式下支持从文件末尾向前偏移但f_lseek需要先f_write一次才能扩展文件大小。2.3 注册时序驱动初始化顺序为何重要完成回调后要在 LVGL 初始化之后调用lv_fs_drv_register注册驱动void lv_fs_fatfs_init(void) { static lv_fs_drv_t drv; // 必须静态或者全局回调会被长期引用 lv_fs_drv_init(drv); // 初始化默认属性 drv.letter S; // 挂载字母对应路径 S:xxx drv.cache_size 4096; // 块缓存大小一般取文件系统簇大小对齐 drv.open_cb fs_open; drv.read_cb fs_read; drv.seek_cb fs_seek; drv.close_cb fs_close; drv.tell_cb fs_tell; lv_fs_drv_register(drv); }注册顺序有两个原则先挂载 FatFs再注册 LVGL 驱动必须先调用lv_init()否则lv_fs_drv_register内部依赖的小内存池还没初始化。另外letter尽量不要和LV_FS_STDIO_LETTER或LV_FS_POSIX_LETTER冲突工程里如果同时用了多个文件系统字母是逻辑盘的唯一标识。cache_size只对 LVGL 库内部文件操作有效不影响 FatFs 自己的读写缓冲实际测试中设置成 4096 能显著减少小文件读写的调用次数。3. 从 ARGB8888 二进制文件到 lv_img_dsc_t图像资源怎么“喂”给 LVGL3.1 从 .bin 文件头读出宽高与格式zip 包里的文件名很直白例如scan_example_522x340_argb8888.bin说明这是原始 ARGB8888 像素数据没有 BMP 那类文件头。LVGL 需要一个lv_img_dsc_t描述符告诉它图像的宽、高、颜色格式和数据指针。所以读取时的第一步不是直接进文件而是解析宽高信息。我采用两种方案结合。对于固定资源直接在代码里用结构体预定义宽高typedef struct { const char * name; uint16_t width; uint16_t height; lv_img_cf_t color_format; } img_resource_t; const img_resource_t img_table[] { { scan_example_522x340_argb8888.bin, 522, 340, LV_IMG_CF_TRUE_COLOR_ALPHA }, { img_ready_158x158_argb8888.bin, 158, 158, LV_IMG_CF_TRUE_COLOR_ALPHA }, // ... 其他资源 };对于用户自己放进去的图片则从文件名中解析后缀前的数字对void parse_size_from_name(const char * name, uint16_t * width, uint16_t * height) { const char * w strchr(name, _); const char * h strchr(w 1, x); *width (uint16_t)atoi(w 1); *height (uint16_t)atoi(h 1); }我是建议把宽高信息直接写死在结构体里。原因是解析字符串需要atoi和strchr会拉高内存占用而且万一文件名不规范就会解析出错误尺寸最终 LVGL 显示时会越界读取。程序中维护一张.bin文件名与尺寸的表反而更直观。3.2 构造描述符并加载到控件ARGB8888 在 LVGL 中对应LV_IMG_CF_TRUE_COLOR_ALPHA每个像素 4 字节排列为B G R A或者A R G B取决于LV_COLOR_16_SWAP设置。推荐在lv_conf.h中设LV_COLOR_DEPTH 32并且不开启LV_COLOR_16_SWAP这样文件数据可以直接映射到 LVGL 的颜色结构体上不需要逐像素转换。读取整张图的代码lv_img_dsc_t * load_file_to_img_dsc(const char * path, uint16_t width, uint16_t height) { FIL fp; uint32_t file_size width * height * 4; uint8_t * buf (uint8_t *)lv_mem_alloc(file_size); if (buf NULL) return NULL; FRESULT res f_open(fp, path, FA_READ); if (res ! FR_OK) { lv_mem_free(buf); return NULL; } // 一次性读入注意 FIL 自带扇区对齐缓冲大批量读取效率更高 UINT bytes_read 0; res f_read(fp, buf, file_size, bytes_read); f_close(fp); if (res ! FR_OK || bytes_read ! file_size) { lv_mem_free(buf); return NULL; } // 构造 LVGL 图像描述符 lv_img_dsc_t * dsc (lv_img_dsc_t *)lv_mem_alloc(sizeof(lv_img_dsc_t)); dsc-header.always_zero 0; dsc-header.w width; dsc-header.h height; dsc-header.cf LV_IMG_CF_TRUE_COLOR_ALPHA; dsc-data_size file_size; dsc-data buf; return dsc; }这里lv_mem_alloc返回的内存是 8 字节对齐的满足 LVGL 对图像数据的要求。很多用户直接malloc在某些 ARM Compiler 下只保证 4 字节对齐当 LVGL 开启 DMA2D 加速时会触发硬件错误所以优先使用lv_mem_alloc。读取完数据后把dsc交给lv_img_set_src即可显示。要注意lv_img_set_src的内部会把dsc指针保存在图像对象里不要提前释放dsc或buf否则显示时会出现花屏和 HardFault。3.3 大图不能一次性读入分块读取与 DMA对于 522x340 的图像file_size 522 * 340 * 4 709,920字节约 700KB而 STM32H750 的 RAM 才 512KB强行一次性分配会直接触发lv_mem_alloc失败。此时可以用分块读取只保留一个滑块窗口// 将图像解码为色块逐块读入并绘制到 canvas for (uint16_t block_y 0; block_y height; block_y BLOCK_H) { uint16_t block_h MIN(BLOCK_H, height - block_y); uint32_t offset block_y * width * 4; f_lseek(fp, offset); // 移动文件指针到目标行 f_read(fp, linebuf, width * block_h * 4, bytes_read); // 将数据拷贝进 LVGL 的 canvas然后刷新该区域 lv_canvas_set_px_4byte(canvas, 0, block_y, linebuf, width, block_h); lv_obj_invalidate(canvas); }分块读取时要保证BLOCK_H * width * 4不超过内存预算我一般控制在 16KB 以内。f_lseek在 FAT 文件系统上的开销不高因为 FAT 表有缓存但如果连续读取很多小分块反复f_open/f_close会消耗大量 CPU这时应该把文件句柄保持打开整张图读完再关闭。4. 文件系统 LVGL 的典型踩坑点与性能优化4.1 文件句柄与内存不足LVGL 缓冲区设置LVGL 有一个内置图像缓存LV_IMG_CACHE_DEF_SIZE默认值为 1。也就是说当你把一个图赋值到lv_img后下次再加载同一张图时LVGL 会尝试从缓存取描述符而不是重新打开文件。这个缓存能够显著减少f_open的次数但它会同时持有lv_img_dsc_t指针和文件句柄。如果把LV_IMG_CACHE_DEF_SIZE设置得过大每个缓存项占的内存也随之增加在 RAM 紧张的 MCU 上很容易导致lv_mem_alloc失败。我建议LV_IMG_CACHE_DEF_SIZE保持 23。同时 FatFs 配置项FF_FS_LOCK要匹配默认是 0表示不限制打开文件数但它使用的是 FATFS 内部的动态分配。如果FF_FS_LOCK设为 2而我们同时打开 3 个文件f_open会返回FR_TOO_MANY_OPEN_FILES。这个错误在 LVGL 回调里表现为图片加载失败但 LVGL 日志不会给出具体 FatFs 状态码排查起来很隐蔽。4.2 对齐、扇区与缓存策略SD 卡读取最小单位是扇区512 字节但 FatFs 的f_read会处理非对齐访问。不过 LVGL 图像显示时如果启动 DMA2DDMA2D 要求源地址必须 4 字节对齐且地址递增传输长度最好与总线位宽一致。从 SD 卡读出的数据天然满足 512 字节对齐但如果你把数据再 memcpy 到一个非对齐的临时数组就会打破这个条件。我一般把读取缓冲定义为 32 字节对齐__ALIGN_BEGIN static uint8_t s_file_buf[4096] __ALIGN_END;__ALIGN_END在 Keil 和 GCC 下均可识别确保数组首地址至少 32 字节对齐。这能避免HAL_SD_ReadBlocks在底层 DMA 搬运时遇到总线地址不对齐异常。另一个隐患是f_read的buft参数如果传入一个非 512 倍数大小的缓冲区FatFs 会在内部执行扇区拷贝性能下降明显。对于大图读取缓冲尽量设置为 4096 或 8192 的倍数。内存紧张时还可以使用 LVGL 的“不完全解码”方案用LV_IMG_CF_TRUE_COLOR_ALPHA配合lv_img_set_pivot做局部刷新但前提是数据本身在 RAM 中。对于 SD 卡文件不要直接依赖 LVGL 的分块显示工具LVGL 官方没有提供文件流按需解码接口自己实现时容易把lv_img_cache搞乱。4.3 性能瓶颈优先查哪里先看f_read单次读取速度。用逻辑分析仪抓 SDMMO 的 D0 引脚如果主频 25MHz、4 位总线理论吞吐约 12.5MB/s实际读取 700KB 图片应该在 60ms 内完成。如果超过 200ms优先检查是否在 1 位总线模式下工作__HAL_SD_ENABLE_4BIT_BUS();同时确认HAL_SD_ConfigWideBusOperation返回HAL_OK。另一个明显的瓶颈是禁用编译优化默认 Keil-O0下 FatFs 的拷贝循环会消耗大量 CPU尤其是小缓冲区时。把文件读写相关.c文件单独设置为-O2实测图片加载时间能缩短一半以上。下面这张表是我在不同配置下的实测数据注意 LVGL 还在跑 GUI 任务读取时间并不等于主循环阻塞时间配置单张图片读取耗时说明SDIO 1位 -O0 无缓存480ms明显卡顿画面撕裂SDIO 4位 -O0 4096B缓存150ms可接受SDIO 4位 -O2 4096B缓存85ms基本无感5. 落到工程一个 SD 卡壁纸阅读器的完整流程5.1 目录遍历与读取文件壁纸阅读器需要枚举0:/lvgl_img下的所有.bin文件。FatFs 的遍历接口是f_findfirst和f_findnextstatic char s_filename[128]; static FILINFO s_fileinfo; const char * get_next_image(void) { static DIR dir; static uint8_t first 1; if (first) { FRESULT res f_findfirst(dir, s_fileinfo, 0:/lvgl_img/*.bin, s_filename); first 0; return (res FR_OK s_filename[0] ! \0) ? s_filename : NULL; } else { FRESULT res f_findnext(dir, s_fileinfo); return (res FR_OK s_filename[0] ! \0) ? s_filename : NULL; } }f_findfirst的第三个参数是文件名模式支持通配符*但当目录中文件很多时每次枚举都会重新遍历目录表。我建议在启动时把文件名列表缓存到一个静态数组里后续切换壁纸不再访问目录。注意s_filename是FILINFO内部的fname数组它在下一次调用f_findnext时会被覆盖所以需要strncpy到自己的数组再使用。5.2 用 lv_img_set_src 绑定文件驱动LVGL 不需要知道文件是否真实存在于 SD 卡上只要注册了带S:前缀的驱动lv_img_set_src内部会走lv_fs_open并读取内容。但关键在于lv_img_set_src期望的是图片文件路径字符串而不是lv_img_dsc_t指针。它内部会根据后缀判断是否为文件路径并在lv_img_cache中创建描述符。所以我们的open/read回调依然会被调用。void show_image(const char * path) { lv_obj_t * img lv_img_create(lv_scr_act()); lv_img_set_src(img, path); // 传字符串路径例如 S:/lvgl_img/img_ready_158x158_argb8888.bin lv_obj_center(img); }这里path的前缀必须和注册驱动时的letter一致。如果你注册的字母是S路径写成S:/lvgl_img/xxx.binLVGL 会直接忽略S:,只把/lvgl_img/xxx.bin传给 FatFs。注意lv_img_set_src只保存指针不复制字符串因此path必须是静态或全局字符串。加载图片的内存由模块缓存管理不需要手工释放但如果你想卸载图片调用lv_img_cache_invalidate_src(path)即可销毁缓存项。5.3 验证与后续扩展代码写完后用一张img_ready_158x158_argb8888.bin做最小验证。先用 PC 端软件把文件转成原始 ARGB8888再放到 SD 卡根目录修改/lvgl_img路径为/跑起来后屏幕中间应出现完整的 158x158 图像。如果花屏优先检查LV_COLOR_DEPTH是否等于 32以及LV_COLOR_16_SWAP是否被误开启。如果图像上下颠倒说明文件是按自下而上扫描保存的需要在读取时把行顺序反转。最后留一个实用技巧给文件系统驱动加一个统计变量比如累计打开次数、累计读取字节数在调试串口上周期性打印。这样你在调优缓存大小时不用贴逻辑分析仪就能直观看到f_open频率是不是太高。整个 LVGL 项目只要定位到文件句柄和缓存这两个关键点移植过程会轻松很多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门