做嵌入式GUI开发,特别是LVGL玩到一定阶段,图片资源往哪放基本是绕不开的问题。内部Flash就那么几MB,UI素材稍微上点量就爆了,尤其是现在动不动1024x600、RGB565的大背景图,一张图超过1MB,内部Flash根本塞不下。所以LVGL配合外部Flash存放图片资源几乎是量产产品的标配方案,我自己在STM32F429 + W25Q64 + LVGL 8.3这套组合上把整套UI切图全部搬到了外部Flash,从启动加载到页面切换都做过一轮调优,这篇就把整个方案的选型思路、核心实现和踩过的坑完整整理出来。
这篇文章适合正在做LVGL+外部存储图片方案的人看,无论是刚把LVGL跑起来、想知道怎么把图片移到外部Flash,还是已经能显示但切图卡顿、花屏、偶发死机不知道怎么排查,里面都会有你用得上的内容。我会从最根上的数据链路讲起,再到实际代码怎么组织,最后是性能和稳定性优化,尽量把“为什么这么做”也讲清楚,而不是只丢给你一段能跑的代码。
1. 项目整体设计与思路拆解
1.1 为什么图片必须放外部Flash:容量与读取速度的权衡
先算一笔最直观的账。一块320x240的RGB565图片,单个像素16bit也就是2字节,体积是3202402 = 150KB。一块1024x600的RGB565图片是1.17MB,光这一张图就能吃掉F407这种512KB内部Flash的四分之一还多。一个稍微像样的产品UI,开机logo、背景图、按钮图标、状态栏图标、弹窗背景,林林总总加一起轻松超过5MB。把图片放内部Flash,在成本上完全不现实。
但放外部Flash不是没有代价的。内部Flash读取路径短,CPU可以直接按地址访问,速度几乎不损耗。外部SPI Flash走的是SPI协议,要发命令、等等待周期、读数据,吞吐量再快也有物理瓶颈,更别说还有文件系统的解析开销、动态内存分配这些额外成本。所以外部Flash方案的核心问题从来不是“能不能读出来”,而是“怎么读才能不拖累LVGL的绘制性能”。
我做这个项目时定的原则是:
- 能用外部Flash绝对不用内部Flash,UI素材再大也不心疼;
- 能用bin裸数据绝不用PNG/JPEG,解码开销在MCU上是致命的;
- 能走内存映射绝不走文件系统读取,少一层封装就少一分延迟;
- 必须有缓存层,不能让LVGL渲染时直接跟Flash打交道。
1.2 一张图片从文件到屏幕的完整链路
要优化,先得知道一张图片在LVGL里是怎么走完生命周期的。这里我先讲典型流程,后面代码部分再展开。当你在代码里调用lv_img_set_src把一张外部图片赋值给lv_img控件后,LVGL内部会做这样几件事:
- 根据src字符串判断图片来源,是内置符号、C数组、还是文件路径;
- 如果是文件路径,调用lv_fs_open打开文件,然后按需读取图片头信息(宽度、高度、颜色格式),这里就涉及外部存储的读取;
- LVGL核心的lv_timer_handler周期任务会检查显示缓冲区,把所有待绘制控件(包括lv_img)按区域合并、排序,生成脏矩形渲染列表;
- 渲染时把图片像素数据填入显示缓冲区(display buffer),这个阶段如果是PNG就要解码,如果是bin裸数据就是纯内存拷贝;
- 显示缓冲区填满后,调用flush_cb回调,由显示驱动把这块内存通过SPI或者RGB并口刷到LCD面板上;
- flush完成后必须调用lv_disp_flush_ready告知LVGL缓冲区已释放,才能继续下一帧渲染。
这个链路里每一步都可能成为卡顿点。文件解析慢、解码慢、缓冲区太小导致频繁flush、DMA没有开启导致CPU空转……任何一个环节出问题,表现出来就是切图卡、滚动掉帧。所以优化外部Flash图片加载,本质上是把这条链路上每一步的“浪费”都挤掉。
1.3 主流实现路线怎么选:文件系统、裸数据回调、内存映射
我梳理了一下,现阶段大家做LVGL外部Flash图片资源,基本跑不出这三条路线:
| 路线 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 文件系统 + bin | Flash烧录FatFS/LittleFS,LVGL通过lv_fs_drv_t访问 | 管理灵活、路径清晰、支持后续OTA替换文件 | 有FAT表解析开销,读取链路长 | 资源经常更新、需要通过SD卡或网络升级UI |
| 裸数据回调 | 不用文件系统,按固定偏移读取Flash,把数据填进lv_img_dsc_t | 读取路径短,RAM占用少,稳定性高 | 资源地址硬编码,增删素材要维护偏移表 | 出厂烧录后资源不变、追求极致稳定 |
| 内存映射(XIP) | MCU QSPI/OSPI外设把外部Flash映射到地址空间,LVGL直接按指针访问 | 对LVGL不可见,读取速度最接近内部Flash | 需要MCU硬件支持,且对Flash型号有要求 | STM32F7/H7、NXP RT系列等带QSPI的芯片 |
我的建议是,如果你的MCU带QSPI外设且支持内存映射,优先考虑第三条路线,这种方案代码最简单、性能最好,后面我会详细说。如果MCU没有这个外设,那就走“裸数据回调”或“文件系统+bin”,根据自己的升级需求来定。我这次项目用的是裸数据回调为主、文件系统作为补充的混合方案:大背景图和常驻图标走固定偏移裸读,仅在有OTA上传新素材时才挂载文件系统覆盖读取。
2. 资源准备与格式转换
2.1 图片格式选型:别再用PNG做背景图了
我在不少群和论坛里看到有人问“LVGL怎么显示PNG”,然后真有人把整张PNG背景图丢给LVGL去解码显示,结果在MCU上跑起来就是灾难。这里必须把格式选型摆到台面上说清楚。
PNG的优点是体积小、支持透明通道,适合图标和装饰性小图。但代价是解码需要足够大的RAM临时缓冲以及可观的CPU算力。一张1024x600的PNG背景,解码过程中动辄需要分配数MB的临时内存,很多MCU的RAM总共才256KB,直接撑爆。LVGL官方提供了PNG解码器(lv_png)和JPEG解码器(lv_jpeg),但它们的定位是“偶尔用一下的小图”,不是让你拿来做整屏背景的。
对于MCU场景,我的选型经验是:
- 大背景图、全屏图:必须转成RGB565裸数据(bin),与屏幕色深完全一致,LVGL拿到就能直接拷贝进缓冲;
- 小图标(含透明通道):优先转成LVGL支持的C数组或bin,格式用ARGB8888;如果Flash空间紧张,可以压成索引色,但绘制时CPU开销会上去,数量多的话反而得不偿失;
- PNG/JPEG:仅在“图片无法预先转好、必须运行时从文件系统读取”的场景使用,而且要限制分辨率,300x300以内还能接受,再大就不建议了。
2.2 用脚本批量生成LVGL可用的bin文件
图片转格式我强烈建议用脚本批量处理,别一张一张用GUI工具点。实测下来,ImageMagick这个命令行工具最顺手,跨平台,几行命令就能处理整个目录的素材。
以大背景图为例,我有这么个处理管线:
# 把源图缩放到目标尺寸,强制RGB565输出 magick input.jpg -resize 1024x600! -type TrueColor -depth 8 rgb565:output.bin # 小图标保持透明通道 magick icon.png -alpha copy -define quantum:format=floating-point rgba:icon.bin这里有个很关键的坑:RGB565的字节序问题。RGB565一个像素占两个字节,LVGL在STM32这种小端MCU上,默认是低字节在前(实际要看LV_COLOR_16BIT_SWAP宏),而ImageMagick输出的是高位在前。我刚开始直接批量生成后烧进去,屏幕上一片“雪花”,排查半天才发现是字节序反了。最简单粗暴的办法:先导出一张纯色图片在屏幕上显示,验证字节序方向,如果颜色不对就交换高低字节,或者让转换脚本对每个16bit做一次swap。LVGL官方在线图片转换工具(imgconv)输出的C数组会默认处理这个字节序,所以用它是比较省心的,只是批量处理效率低。
2.3 固定偏移表还是文件系统:Flash资源管理方案对比
素材转完之后,怎么烧进Flash、怎么让固件找到它,这是两个方案分叉的地方。
固定偏移表方式是我这次主力在用的。我在Flash的开头放一个资源索引结构,每一行记录图片ID、起始地址、数据长度、宽度、高度、颜色格式。程序里调一个接口,传入图片ID就能拿到对应的偏移和大小,然后从Flash读取。这个方案的优点是:
- 读取路径最短,不需要解析FAT表,也没有目录层级;
- RAM占用几乎为零,不需要挂载文件系统;
- 不会因为文件系统碎片或掉电损坏导致资源丢失;
- LVGL侧只需要一个极轻量的“read_at(offset, len, buf)”函数。
文件系统方式(FatFS/LittleFS)适合产品需要在线更新UI资源的场景。比如你想在设备端接收一个升级包,解压后覆盖旧的图片文件,用文件系统天然支持。但代价是系统复杂度上去了:文件系统要占Flash空间存FAT表、要占RAM做文件句柄和缓冲,读取一张图片要先open再seek再read,链路长,出错概率也高。FatFS在SPI Flash上还有磨损均衡问题,如果频繁写操作,需要叠加上限均衡层,复杂度进一步上升。
所以我的结论是:产品不需要在线升级素材就老老实实用固定偏移表,需要再考虑文件系统,不要为了“将来可能需要”提前买单。后面代码部分我会重点讲怎么用固定偏移表方式接入LVGL,文件系统方式会简单带过,但思路是通的。
3. 核心代码实现:把外部Flash接入LVGL
3.1 轻量级Flash读取封装
不管走哪条路线,第一步都是把Flash的读取能力封装好。以我用的W25Q64为例,底层是标准SPI协议。我这里的做法是直接按“读取地址 + 读取长度 + 目标缓冲”的方式设计接口,不暴露Flash页、扇区这些概念,因为图片读取是纯只读场景,不需要擦写均衡:
// flash_img.h #ifndef FLASH_IMG_H #define FLASH_IMG_H #include <stdint.h> #include <stddef.h> typedef struct { uint32_t addr; uint32_t size; uint16_t width; uint16_t height; uint8_t cf; // LVGL color format } img_resource_t; int img_resource_lookup(uint16_t img_id, img_resource_t *res); int img_resource_read(const img_resource_t *res, uint32_t offset, void *buf, uint32_t len); #endif实现层面,底层用HAL库的SPI接口,开DMA传输:
int img_resource_read(const img_resource_t *res, uint32_t offset, void *buf, uint32_t len) { if (offset + len > res->size) { return -1; } uint32_t flash_addr = FLASH_BASE_OFFSET + res->addr + offset; uint8_t cmd[4] = {0x03, (uint8_t)(flash_addr >> 16), (uint8_t)(flash_addr >> 8), (uint8_t)(flash_addr)}; // 拉低CS HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi2, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive_DMA(&hspi2, (uint8_t *)buf, len); // 这里必须等DMA完成才能拉高CS while (HAL_SPI_GetState(&hspi2) != HAL_SPI_STATE_READY) { // 超时保护 } HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); return (int)len; }这里有个很容易踩的坑:DMA传输期间CS必须保持低电平,如果SPI是半双工共用的,还要保证没有其他任务在同时使用同一个SPI外设。在FreeRTOS + LVGL的环境里,我会用互斥量保护整个Flash访问函数,否则一旦另一个任务切进来操作SPI,读取内容直接错乱。
3.2 固定偏移表方式接入LVGL图片控件
资源索引表本身是一段烧写在Flash前部的数据,结构体数组。为了在运行时能定位到它,我把它放在一个固定地址偏移处,固件里用绝对偏移读取:
#define RESOURCE_INDEX_ADDR 0x00001000 const img_resource_t *get_resource_table(uint32_t *count) { // 从内部Flash或RAM保存一份只读表,避免每次都读外部Flash static img_resource_t table[64]; static int loaded = 0; if (!loaded) { // 读Flash前部的资源表 read_flash(RESOURCE_INDEX_ADDR, table, sizeof(table)); loaded = 1; *count = sizeof(table) / sizeof(img_resource_t); } return table; }索引表放外部Flash前部的好处是,固件升级时如果资源表变了,可以连资源区一起整体更新。这里我踩过一个坑:不要把索引表放在固件bin文件的同一区域内烧录,否则每次固件升级会覆盖索引表,导致运行时资源位置全乱。
拿到索引之后,怎么把一张图片显示出来?如果MCU支持内存映射(比如STM32F769的QSPI),直接把data指针指向映射地址就行:
// 用内存映射方式显示,LVGL直接读Flash数据 lv_img_dsc_t bg_img_dsc = { .header.always_zero = 0, .header.w = 1024, .header.h = 600, .header.cf = LV_IMG_CF_TRUE_COLOR, .data_size = 1024 * 600 * 2, .data = (const uint8_t *)(0x90000000UL + RESOURCE_OFFSET), }; lv_obj_t *bg = lv_img_create(lv_scr_act()); lv_img_set_src(bg, &bg_img_dsc);但很多MCU不支持QSPI内存映射,那就要退一步:先按需把整张图片读进RAM缓冲,再把缓冲指针交给LVGL。这种方式适合页面切换时一次性加载的大图,代码也清晰:
static uint8_t s_bg_buf[1024 * 600 * 2]; // 池子,预分配 void show_background(lv_obj_t *parent, uint16_t img_id) { img_resource_t res; lv_img_dsc_t dsc; img_resource_lookup(img_id, &res); img_resource_read(&res, 0, s_bg_buf, res.size); memset(&dsc, 0, sizeof(dsc)); dsc.header.always_zero = 0; dsc.header.w = res.width; dsc.header.h = res.height; dsc.header.cf = res.cf; dsc.data_size = res.size; dsc.data = s_bg_buf; lv_obj_t *img = lv_img_create(parent); lv_img_set_src(img, &dsc); }注意缓存池s_bg_buf要静态分配,不要用malloc,否则长期运行会有碎片问题。
3.3 文件系统方式接入LVGL
如果你的方案定了文件系统,LVGL这边要注册一个自定义文件系统驱动。LVGL 8.3里实现lv_fs_drv_t,把open/read/seek/close这些回调接上:
static void *fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) { FIL *fp = lv_mem_alloc(sizeof(FIL)); if (f_open(fp, path, FA_READ) != FR_OK) { lv_mem_free(fp); return NULL; } return fp; }注册的时候指定盘符,比如"F:":
lv_fs_drv_t fs_drv; lv_fs_drv_init(&fs_drv); fs_drv.letter = 'F'; fs_drv.open_cb = fs_open; fs_drv.read_cb = fs_read; fs_drv.close_cb = fs_close; fs_drv.seek_cb = fs_seek; lv_fs_drv_register(&fs_drv);之后图片资源路径就是"F:/ui/bg.bin",LVGL内部会自动调文件系统接口去读。这条链路比裸数据读取多了开文件、路径解析等操作,但对维护来说方便很多,如果UI素材经常调整,这个成本是值得的。
3.4 LVGL关键配置项调整
不管你走哪条路线,lv_conf.h里的几个宏直接影响外部Flash图片的加载体验:
- LV_COLOR_DEPTH:建议16(RGB565),与屏幕一致,不要用32。32位色深在拷贝到显示缓冲时带宽翻倍,Flash读取压力也翻倍;
- LV_COLOR_16BIT_SWAP:按你屏幕的字节序设置,这个错了就是花屏;
- LV_MEM_SIZE:LVGL自己管理的堆。外部Flash方案下,PNG解码、文件句柄、动态创建的控件都从这里分配,建议至少给8KB~16KB,如果跑文件系统+解码,建议32KB以上;
- LV_DISP_DEF_REFR_PERIOD:LVGL定时器多久刷新一次渲染,默认30ms。如果你感觉界面操作响应慢,可以调到16ms或者10ms,但会提高CPU占用,需要实测折中;
- LV_IMG_CACHE_DEF_SIZE:默认0,即不缓存解码后的图片。如果用的是PNG之类带解码格式,这个务必开启,建议4~8张的容量。bin裸数据没有解码过程,缓存意义不大,但文件系统场景下缓存能避免频繁open/close。
3.5 FreeRTOS环境下的集成注意点
LVGL官方推荐把lv_timer_handler放在独立任务里,周期调用。外部Flash读取如果直接在UI任务里同步进行,整帧渲染会被拖住。我这边用FreeRTOS,结构是这样:
- LVGL任务:优先级中,周期4~5ms调用一次lv_timer_handler;
- 显示驱动任务:负责LCD刷新,DMA中断里做同步;
- Flash读取任务:低优先级,负责预读图片到缓存池,通过消息队列通知LVGL任务“图片已就绪”。
关键点是SPI Flash读取要用互斥量保护,避免和LCD的SPI冲突。如果Flash和LCD共用同一个SPI外设(很多低成本方案都这么干),那问题更明显,必须确认LCD刷新DMA和Flash读取不能同时占用总线,否则数据会被打乱。
4. 性能优化:让页面切换不再卡顿
4.1 先定位瓶颈:是Flash读得慢,还是LVGL画得慢?
优化之前先做测量,不要凭感觉。我的经验是做一个基准测试:
- 隐藏掉图片,只绘制纯色块,测一次UI刷新周期耗时;
- 用内部Flash数组方式加载同一张图片,测一次页面切换耗时;
- 用外部Flash方式加载同一张图片,测一次页面切换耗时。
如果步骤1很快、步骤2快、步骤3慢,说明问题在Flash读取链路。如果步骤2本身就慢,那问题在LVGL渲染配置上,比如缓冲太小、未开DMA、刷新周期太长等。
实测下来,W25Q64这种SPI Flash在80MHz时钟、开DMA的情况下,读一张150KB的RGB565图大约需要15~25ms(片选、指令开销、SPI时序损失都算上)。对一个页面切换来说,这个数字如果叠加在渲染流程里,用户能明显感觉到卡顿。所以优化的核心目标就是“让Flash读取时间与渲染时间重叠”,而不是减少Flash读取时间本身——后者物理极限摆在那。
4.2 DMA、双缓冲与display buffer协同
显示驱动这边,flush_cb里我们要把显示缓冲区的数据交给LCD控制器。如果LCD是SPI接口,flush_cb里发SPI写命令+数据,也是DMA传输。优化点在于:DMA传输期间CPU不能去等,要立刻返回让LVGL继续渲染下一块数据。
这就引出双缓冲或部分刷新机制。LVGL配置里可以设置两个display buffer(lv_disp_draw_buf_init传两个buf),当LVGL渲染到buf1时,DMA正在把buf0刷给屏幕。两边并行,CPU利用率上去,整体帧率几乎能翻倍。
和外部Flash图片结合时,我的做法是:页面切换瞬间,Flash读取任务先把新页面最耗时的背景图读入RAM缓存,LVGL渲染时只用缓存指针,不直接触发Flash读取。这样渲染路径完全不跟Flash打交道,切图自然快。
4.3 预加载与RAM缓存池设计
缓存池是我这套方案收益最大的部分。原理很简单:页面切换的耗时大头通常是背景大图,只要把“下一屏”的背景图提前读进RAM,切换时就只是换指针。
具体实现上,我维护了一个固定大小的缓存池,存放两张最大背景图的RAM空间(比如2 * 1024 * 600 * 2 = 2.4MB,对大多数MCU太奢侈,所以实际产品背景图通常不超过480x272,那池子就是2480272*2 = 512KB,还可接受)。用一个简单的状态机:
- 页面A显示时,后台任务预读页面B的背景图到空闲缓冲;
- 用户点击跳转页面B,UI任务把bg_img_dsc.data改为已就绪的缓冲地址;
- 页面B显示时,后台任务按算法腾出一块缓冲,预读页面C或返回A的图;
- 如果预读未完成用户就点了跳转,则放弃预读,直接同步读取(并接受这一次的延迟)。
这个方案对任何MCU都适用,只是池子大小按RAM余量调整。如果你的RAM只够缓存一张半图,就不要预读,改成同步读,保证用户看到的画面一致。
4.4 减少图片数据量的三个实用手段
除了缓存,从源头减少数据量也能直接提升加载速度。
第一,色深能降就降。UI里能接受RGB565就绝对不搞ARGB8888,数据量直接砍一半。只有必须带透明通道的图标才用ARGB8888。
第二,小图标能合图就合图。把十几个小图标横向拼成一张Atlased大图,LVGL 9.x里可以直接用lv_image的crop功能显示子区域;8.x的话可以自己在解码层处按区域读取,或者干脆拆成独立小文件。合图的好处是文件句柄少、读取次数少,文件系统场景下尤其明显。
第三,用LVGL内置的压缩格式。比如LV_IMG_CF_RAW,可以在转换工具里选择压缩选项,但代价是绘制时CPU要解压。MCU主频如果只有几十上百MHz,压缩带来的CPU开销可能超过Flash读取的节省,反而更慢。这个要实测,别只看Flash占用。
4.5 一个容易忽略的点:Cache一致性
如果你用的是Cortex-M7这类带D-Cache的MCU,DMA读外部Flash后,CPU读RAM缓冲必须是经过DMA写回的。这时候不做Cache维护,就会出现“DMA明明把数据读好了,但CPU读到的还是旧的缓存内容”这种诡异问题,表现为偶发花屏、复位后第一次显示异常。
解决方法是刷新(invalidate)对应RAM区域的Cache:
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);一定要在DMA完成、但CPU访问该缓冲之前调用。在H7/NXP RT系列上,这个坑基本必踩,提前加进去能省很多调试时间。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
我在这个项目里遇到的坑,按出现频率排个序,做成速查表:
| 症状 | 可能原因 | 解决方向 |
|---|---|---|
| 图片花屏 | RGB565字节序反了;LV_COLOR_16BIT_SWAP配置错误;图片宽高与dsc不一致 | 用纯色测试图验证字节序,逐一排除 |
| 白屏/黑屏无显示 | 文件路径错误;资源索引表读错;data_size为0 | 用调试器打印src路径和dsc.data指针,确认非空 |
| 图片显示一半后卡死 | DMA传输未完成就触发lv_disp_flush_ready;SPI被其他任务打断 | 严格在DMA完成回调里调用lv_disp_flush_ready,SPI加互斥 |
| 页面切换明显卡顿 | 未开DMA;Flash时钟太低;同步读取大图;LV_DISP_DEF_REFR_PERIOD太长 | 开DMA、提时钟、走缓存预读、缩短刷新周期 |
| 偶发花屏/旧图 | Cache一致性未处理(M7芯片);LVGL内存被踩 | 加Cache invalidate,查lv_mem监控,看是否有溢出 |
| 硬错误/内存爆 | LV_MEM_SIZE太小;图片数据直接malloc大块 | 调大LV_MEM_SIZE,大图缓冲用静态数组 |
| 某些图标显示成方块 | 颜色格式不匹配,比如把ARGB8888的bin按RGB565显示 | 检查dsc.header.cf和转换工具的格式 |
5.2 一个真实调试案例:花屏花得毫无规律
我印象最深的一次花屏排障。主题是“切到二级页面,背景图偶发花屏,不是每次都花,复位后第一次百分百花,继续切几次又好了”。当时怀疑过Flash时序、DMA传输、SPI线上干扰,折腾了半天,直到我意识到芯片是Cortex-M7内核,才反应过来Cache一致性问题。DMA把Flash数据读进RAM缓冲区后没有invalidate Cache,CPU从Cache里读到了旧数据,所以显示出来的图就是残缺的。加上一行SCB_InvalidateDCache_by_Addr之后,问题彻底消失。这个坑没有任何调试器能帮你查出来,只能靠经验。所以我强烈建议:M7及以上带Cache的MCU,DMA方案里第一优先把Cache维护写好。
还有一个案例是关于文件路径的。当时我在一个文件系统方案里,把图片路径字符串定义成一个局部变量,LVGL内部是异步读取文件的,函数返回后局部变量已经销毁,LVGL再去访问就是野指针,表现为“经常显示一下然后系统崩了”。排查很久才发现是生命周期问题。用LVGL文件系统时,路径字符串必须保证在整个图片生命周期内都有效,建议用静态数组或全局const字符串。
5.3 一条本文没有展开的扩展:OTA资源更新
最后再分享一个方向。如果你产品需要在现场更新UI素材,光靠固定偏移表肯定不行,那就需要在文件系统方案里实现资源区覆盖。我做过的做法是:外部Flash规划两个资源分区,OTA升级包先写入备用分区,校验通过后再把备用分区整体拷贝到主分区。虽然看上去多一倍的Flash空间开销,但换来的是“升级过程中断电也不怕,大不了回滚版本”的可靠性。LVGL侧完全无感,切换主备只需要改一个启动参数指向不同偏移。
踩过几次偏方之后,我现在做外部Flash图片加载方案的第一原则是:不炫技,不引入不必要的复杂度。能用缓存解决性能就用缓存,能用固定偏移表解决资源定位就坚决不挂文件系统,能用DMA并行解决等待就绝不让CPU空转。MCU资源有限,方案每多一层封装就多一层出错的机会,所有“优化”都必须以可稳定量产为底线。
这个内容后续还可以这样扩展:把资源索引表改成带CRC校验的版本管理结构,启动时校验资源和固件的版本匹配性;或者在显示驱动层面做局部脏矩形和外部Flash图片读取的联动,进一步减少无效读取。核心思路就一条,整个链路里每一毫秒都值得抠,但前提是你能说清楚这一毫秒到底消耗在哪里。