2、外部存储加载机制:LVGL文件系统接口、外部存储设备类型与挂载流程

做嵌入式UI,尤其是用LVGL加载外部图片时,文件系统这块是绕不过去的坎。我见过不少开发者,LVGL界面画得挺漂亮,一到加载SD卡图片就崩,最后发现是文件系统接口没接对。说白了,LVGL本身不直接操作硬件,它通过一层抽象接口来读写文件。这层接口,就是我们要聊的LVGL文件系统。

2.1 LVGL的文件系统抽象层

LVGL的文件系统接口设计得很轻量。它定义了一组回调函数,你只需要实现这些回调,就能让LVGL读写任何存储介质。核心结构体是 lv_fs_drv_t,里面包含了open、close、read、write、seek、tell等操作。

核心要点:LVGL不关心底层是SD卡还是Flash,它只认 lv_fs_drv_t 里的函数指针。你挂载什么驱动,它就访问什么设备。

我个人习惯把文件系统驱动分为三层:

  • 物理层: 实际的存储芯片或卡槽,比如SD卡SPI接口、Flash的QSPI接口。
  • 驱动层: 操作物理层的底层驱动,比如FatFS、LittleFS、SPIFFS。
  • 适配层: 把驱动层的读写接口包装成 lv_fs_drv_t 要求的格式。

你想想看,如果直接让LVGL去操作SD卡的SPI寄存器,那代码得多乱?有了这层抽象,换存储设备就像换插件一样简单。

2.2 常见外部存储设备类型

实际项目中,我遇到过三种主流的外部存储设备。它们的特性差异很大,选型时一定要想清楚。

设备类型 接口方式 典型容量 读写速度 适用场景
SD卡 SPI / SDIO 2GB ~ 32GB 中等(SPI慢,SDIO快) 大容量图片存储、日志记录
NOR Flash SPI / QSPI 8MB ~ 256MB 较快(XIP模式更快) 固件存储、字库、小图片
U盘(USB Host) USB OTG 8GB ~ 128GB 快 数据交换、现场升级

我的经验: 如果项目只需要存几十张图片,NOR Flash + LittleFS 是最稳的组合。SD卡虽然便宜,但SPI模式下的随机读取速度很慢,加载大图时容易卡顿。我曾经在一个产品里用SPI SD卡加载800x480的JPEG,结果每张图要等2秒,后来换成QSPI Flash才解决。

2.3 文件系统挂载流程

挂载流程听起来高大上,其实就三步:初始化硬件 → 注册文件系统 → 挂载到LVGL。我画了一张流程图,帮你理清关系。

LVGL外部存储挂载流程图 1. 硬件初始化 2. 底层文件系统挂载 3. 注册 lv_fs_drv_t 驱动 4. LVGL 文件操作 5. lv_img 加载图片 注意:步骤2和步骤3的顺序不能颠倒,必须先挂载底层文件系统,再注册LVGL驱动

下面我拆开每一步,结合代码给你讲清楚。

2.3.1 硬件初始化

这一步没什么好说的,就是初始化SPI或SDIO外设,配置GPIO引脚。以STM32 + SD卡SPI模式为例:

// SPI初始化
void sd_spi_init(void) {
    // 配置SPI引脚:SCK、MISO、MOSI、CS
    // 设置SPI时钟、极性、相位
    // 使能SPI外设
    HAL_SPI_Init(&hspi1);
    
    // 配置CS引脚为推挽输出,拉高
    HAL_GPIO_WritePin(SD_CS_GPIO_Port, SD_CS_Pin, GPIO_PIN_SET);
}

注意: SD卡的SPI模式初始化有严格的时序要求。上电后必须等待至少74个时钟周期,才能发送CMD0进入SPI模式。我见过有人在这里翻车,卡死活初始化不过,最后发现是上电后没等够时间。

2.3.2 底层文件系统挂载

这里以FatFS为例。FatFS是嵌入式领域最常用的文件系统,支持FAT12/16/32。挂载代码很简单:

FATFS fs;          // 文件系统对象
FRESULT res;

// 挂载SD卡到盘符"0:"
res = f_mount(&fs, "0:", 1);
if (res != FR_OK) {
    // 挂载失败,可能是未格式化或硬件错误
    printf("FatFS mount failed: %d\n", res);
}

如果用的是LittleFS(适合Flash),挂载方式略有不同:

lfs_t lfs;
lfs_file_t file;

// 挂载LittleFS
int err = lfs_mount(&lfs, &cfg);
if (err) {
    // 首次使用需要格式化
    lfs_format(&lfs, &cfg);
    lfs_mount(&lfs, &cfg);
}

2.3.3 注册LVGL文件系统驱动

这是最关键的一步。你需要实现 lv_fs_drv_t 里的回调函数,然后把驱动注册到LVGL。我直接给你一个完整的注册示例:

// 定义文件系统驱动结构体
static lv_fs_drv_t fs_drv;

// 回调函数实现
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode) {
    FIL file;
    BYTE flags = 0;
    
    if (mode == LV_FS_MODE_RD) flags = FA_READ;
    else if (mode == LV_FS_MODE_WR) flags = FA_WRITE | FA_CREATE_ALWAYS;
    else if (mode == (LV_FS_MODE_RD | LV_FS_MODE_WR)) flags = FA_READ | FA_WRITE;
    
    // 注意:path前面要加上盘符
    char full_path[64];
    sprintf(full_path, "0:%s", path);
    
    if (f_open(&file, full_path, flags) == FR_OK) {
        // 返回文件指针,注意要动态分配或使用静态变量
        FIL * pFile = malloc(sizeof(FIL));
        memcpy(pFile, &file, sizeof(FIL));
        return pFile;
    }
    return NULL;
}

static lv_fs_res_t fs_close(lv_fs_drv_t * drv, void * file_p) {
    f_close((FIL *)file_p);
    free(file_p);
    return LV_FS_RES_OK;
}

static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br) {
    UINT bytes_read;
    f_read((FIL *)file_p, buf, btr, &bytes_read);
    *br = bytes_read;
    return LV_FS_RES_OK;
}

// 注册驱动
void lv_fs_drv_register(void) {
    lv_fs_drv_init(&fs_drv);
    
    fs_drv.letter = 'S';          // 盘符,LVGL用'S:'访问
    fs_drv.open_cb = fs_open;
    fs_drv.close_cb = fs_close;
    fs_drv.read_cb = fs_read;
    fs_drv.write_cb = fs_write;   // 类似实现
    fs_drv.seek_cb = fs_seek;
    fs_drv.tell_cb = fs_tell;
    
    lv_fs_drv_register(&fs_drv);
}

避坑指南: 我曾经在注册驱动时忘了设置 cache_size 字段,结果LVGL读取大文件时频繁触发回调,性能极差。建议把 cache_size 设为512字节(一个扇区大小),能显著提升读取速度。

2.3.4 在LVGL中加载外部图片

驱动注册完成后,加载图片就变得非常简单。你只需要在文件路径前加上盘符:

// 加载SD卡上的图片
lv_img_dsc_t img_dsc;
lv_fs_file_t file;

// 方式一:直接使用文件路径
lv_img_set_src(img_obj, "S:/images/bg.png");

// 方式二:从文件读取到内存
lv_fs_open(&file, "S:/images/logo.png", LV_FS_MODE_RD);
uint8_t * buf = malloc(1024 * 1024);  // 根据实际大小分配
lv_fs_read(&file, buf, file_size, &bytes_read);
// 解码并显示...
lv_fs_close(&file);

2.4 多设备同时挂载

有些项目需要同时使用SD卡和Flash。LVGL支持同时注册多个驱动,只要盘符不同就行。我做过一个产品,SD卡存用户图片,Flash存系统字库和图标:

盘符 设备 文件系统 用途
S: SD卡 FatFS 用户图片、日志
F: NOR Flash LittleFS 字库、图标、配置文件

注册两个驱动时,注意回调函数不能共用全局变量。每个驱动实例需要独立的上下文。我习惯把文件指针放在驱动结构体的 user_data 字段里,避免冲突。

重要提醒: 多设备挂载时,LVGL的 lv_fs_drv_t 结构体必须定义为多个实例,不能重复使用同一个。每个实例的 letter 字段必须唯一。否则LVGL内部查找驱动时会混乱,导致文件操作失败。