LVGL 从外部 SPI Flash 加载 BMP 图片
问题背景
在资源受限的嵌入式项目中,片上 RAM 往往非常紧张。如果把所有 UI 图片都编译进固件或常驻 RAM,很快就会超出可用空间。一个常见的做法是将图片以文件形式写入外部 SPI Flash(如 W25Q 系列),运行时再按需读取到内存中交给 LVGL 渲染。
本文记录一个从 SPI Flash 读取 BMP 图片并填充 lv_img_dsc_t 的实现思路,代码基于 LVGL v8 的 API 风格。
适用场景
- 主控 RAM 有限(例如 256 KB 级别),无法将全部图片常驻内存。
- 外部 SPI Flash 容量充足,可以存放多张 BMP 图片。
- 图片在运行时才需要显示,不需要启动时全部加载。
- 使用 LVGL v8 的
lv_img_dsc_t接口(v9 已改为lv_image_dsc_t,字段名有变化)。
原理概述
- SPI Flash 分块读取:SPI NOR Flash 的页编程单位通常为 256 B,扇区擦除单位为 4 KB。读取操作本身没有对齐限制,代码中按 4096 B 分块是软件层面的选择,目的是减少 SPI 事务次数并便于 DMA 对齐(具体扇区/页大小取决于芯片手册)。
- BMP 文件结构:标准 BMP 文件前 14 字节是
BITMAPFILEHEADER,之后是BITMAPINFOHEADER(40 字节)。LVGL 的lv_img_dsc_t需要从中提取宽度、高度、位深、颜色格式等信息。 - LVGL 图片描述符:
lv_img_dsc_t包含header(含宽高、颜色格式等位域)和data(像素数据指针)。填充完毕后调用lv_img_set_src()即可显示。
实现步骤
1. 分配读取缓冲区
uint8_t *pbuf = lv_mem_alloc(file_size);这里一次性分配与图片等大的缓冲区,将整张图读入 RAM。对于较大的图片(例如 1024×768×24-bit ≈ 2.25 MB),需要确认 lv_mem_alloc 的堆空间足够,否则返回 NULL。
2. 分块从 SPI Flash 读取
int secTime = file_size / 4096; // 完整 4 KB 块数
int secOff = file_size % 4096; // 剩余不足 4 KB 的字节数
for (int i = 0; i < secTime; i++) {
SPI_Flash_Read(&pbuf[i * 4096], start_addr + i * 4096, 4096);
}
if (secOff > 0) {
SPI_Flash_Read(&pbuf[secTime * 4096],
start_addr + secTime * 4096, secOff);
}SPI_Flash_Read 是底层驱动封装,具体实现取决于所用 Flash 芯片和 SPI 控制器。4096 这个值对应常见 SPI NOR Flash 的扇区大小,如果芯片扇区大小不同,需要相应调整。
3. 解析 BMP 头并填充 lv_img_dsc_t
原文中这一步被截断,可见的逻辑是:
pbmp = (uint8_t *)pbuf + 4; // 跳过 4 字节自定义偏移(可能是文件魔数或对齐填充)
header = (uint32_t)pbuf[3] << 24 | ...; // 拼接 32 位字段完整的解析通常需要:
- 校验 BMP 魔数(
0x4D42,即 "BM")。 - 读取
BITMAPINFOHEADER中的biWidth、biHeight、biBitCount、biCompression。 - 根据位深和压缩方式确定 LVGL v8 的
lv_img_cf_t(如LV_IMG_CF_RGB565、LV_IMG_CF_RGB888、LV_IMG_CF_TRUE_COLOR等)。 - 计算像素数据在文件中的偏移(
bfOffBits),将image->data指向该地址。 - 设置
image->header.w、image->header.h、image->header.cf等字段。
注意:原文中
pbmp = pbuf + 4跳过了 4 字节,这并非标准 BMP 布局。如果 Flash 中存储的是原始 BMP 文件,通常不需要额外偏移;如果是自定义封装格式,需要确保写入端和读取端一致。
4. 显示图片
lv_img_set_src(img_obj, image);显示完成后,如果图片不再需要,应调用 lv_mem_free(pbuf) 释放缓冲区,避免 RAM 泄漏。
风险与边界
- 内存分配失败:
lv_mem_alloc在堆空间不足时返回 NULL。原文未做判空,直接解引用会触发总线错误(BusFault)或 HardFault(具体取决于内核故障处理配置)。生产代码中必须检查返回值。 - file_size 为 0 或极小值:当
file_size < 14(BMP 文件头最小长度)时,解析 BMP 头会越界读取。应在函数入口做最小长度校验。 - SPI Flash 读取可靠性:SPI 总线在高频或长线缆下可能出现位错误。关键场景可考虑加 CRC 校验或读取后校验 BMP 头魔数。
- 4096 分块大小是平台相关的:不同 Flash 芯片的扇区大小不同(常见 4 KB,也有 8 KB 等),硬编码 4096 在换芯片后可能不正确。建议用宏或从驱动层获取。
- 并发与线程安全:如果 LVGL 运行在独立任务中,而 Flash 读取在另一个任务执行,需要确保
lv_mem_alloc/lv_mem_free的线程安全(LVGL v8 的lv_mem本身不提供互斥,应用层需加锁),以及 SPI 总线不被两个任务同时访问。 - 代码不完整:原文在 BMP 头解析处截断,无法确认完整的
lv_img_dsc_t填充逻辑和内存释放路径。以上分析基于可见部分推断。
验证建议
- 在 PC 端用相同 BMP 文件,写一个最小 C 程序解析头字段,确认宽高、位深、像素偏移与预期一致。
- 在目标板上用
dbg_printf打印解析出的w、h、cf,与图片实际尺寸对比。 - 用逻辑分析仪或示波器抓 SPI 波形,确认读取地址和长度正确。
- 压力测试:连续加载/卸载多张图片,观察 RAM 使用是否稳定(无泄漏)。
- 边界测试:传入
file_size = 0、file_size = 13、file_size = 4096、file_size = 4097等值,确认不会崩溃。
小结
从外部 SPI Flash 加载图片是嵌入式 UI 中节省 RAM 的常用手段。核心流程是:分块读取 → 解析 BMP 头 → 填充 lv_img_dsc_t → 交给 LVGL 显示 → 用完后释放内存。实现时重点关注内存分配失败处理、BMP 头校验、分块大小与芯片扇区匹配、以及多线程环境下的资源保护。
本文最初于 2022/4/6 发布在 CSDN。
相关文章
评论
正在读取评论…