跳转至

14 fatfs

一、概述

作者:王棚嶙

fatfs库提供了对FAT32文件系统的支持,主要用于读写TF卡、SD卡等外部存储设备。

1.1 存储系统概述

LuatOS的存储系统采用清晰的分层架构设计,其核心目标是为应用层提供统一、简洁的文件操作接口,从而屏蔽底层不同存储介质和文件系统的差异。开发者无需关心数据具体存储在何种硬件上,也无需关心底层文件系统的具体实现,只需调用统一的API即可完成文件操作。

该系统的层次结构与各组件间的协作关系如下图所示,其数据流与依赖关系自上而下贯穿各层:

image.png

1.1.1 物理硬件层

这是数据存储的物理载体,LuatOS支持多种常见的嵌入式存储硬件:

1、SPI/SDIO TF卡:通过高速SPI或SDIO接口连接的块存储设备,特点是容量大、可插拔,适用于存储大量数据(如音频、图片文件);

2、SPI NOR Flash:通过SPI接口访问的NOR型闪存。其特点是读写速度快,通常用于存放固件代码或需要快速读写的关键数据;

3、SPI NAND Flash:通过SPI接口访问的NAND型闪存。其特点是容量大、成本低,适用于存储日志等容量需求较大的数据;

1.1.2 文件系统实现层(fatfs&lf)

文件系统负责管理存储设备的空间,组织文件和目录结构。LuatOS根据硬件特性,适配了两种主流的嵌入式文件系统:

1、FATFS:一个兼容FAT12/FAT16/FAT32标准的通用文件系统。它通常与SD/TF卡配对使用,;

2、LFS(LittleFS):它通常与SPI NOR Flash和SPI NAND Flash配对使用,非常适合在裸Flash设备上运行;

1.1.3 统一API层 (io)

fs库不推荐新客户使用,该库仅对已经使用过的老客户进行保留,其功能已经完全移植到io库中,且更加完善,新客户建议都参考io核心库:

1、fs:提供文件系统的额外操作。不建议使用,请使用io库进行替代;

2、io:提供标准的数据流操作接口,用于文件的打开、读写、关闭等,是处理文件内容的主要手段,包含fs库的所有功能,推荐使用;

1.1.4 应用层(luatOS应用脚本)

客户通过调用io库,fatfs库,lf库以及fs库中的api接口,来实现自己的业务逻辑。

1.1.5 推荐使用

你的需求 推荐使用的库和函数 理由
快速检查一个文件的大小 io.exists(path) 最简洁、最直接,一行代码搞定。
快速查看文件系统的整体使用情况(如剩余空间) io.fsstat(path) 提供通用信息,代码兼容性好。
需要对FAT格式的U盘、SD卡进行深度管理 fatfs 库 (如fatfs.getfree(), fatfs.mount()) 只有它才能提供FAT文件系统的专业控制和详细信息。
要读取、写入文件内容,或进行复杂的文件操作 io 库 (如 io.open, file:read, file:write) 这是进行文件I/O操作的标准和核心方式,不可替代。

对于FAT文件系统需求,使用 fatfs 库

对于文件内容操作需求,使用 io 库

1.2 本文中扇区 (Sector) 的概念

扇区:存储设备,如TF卡、硬盘等外部存储设备中最小的信息管理单位;

1.2.1 物理层面 (最根本的定义)

1、扇区是存储设备(如TF卡、SD卡、硬盘、U盘)能够进行读写操作的最小物理单元;

2、这是由硬件制造商决定的,对于绝大多数现代存储设备,这个大小被固定为 512字节;

3、这意味着,即使您只想在文件中保存1个字节的数据(比如一个字母'A'),存储设备实际上也必须读写整整一个扇区(512字节);

1.2.2 逻辑/软件层面

1、操作系统和文件系统(如FAT32,也就是FatFS操作的对象)在此基础上建立了更高级的管理结构;

2、文件系统会将多个扇区组合成一个“簇” (Cluster) 或“块” (Block) 来进行文件管理,以提高效率;

1.2.3 如何计算总容量

既然1个扇区 = 512字节,那么:

总容量计算:

总容量 (字节) = total_sectors * 512

总容量 (KB) = total_sectors * 512 / 1024 = total_sectors / 2

二、核心示例

1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;

2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;

3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/tf_card;

--挂载失败默认格式化,
-- 如无需格式化应改为fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000, nil, 1, false),
-- 一般是在测试硬件是否有问题的时候把格式化取消掉
mount_ok, mount_err = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
if mount_ok then
    log.info("fatfs.mount", "挂载成功", mount_err)
else
    log.error("fatfs.mount", "挂载失败", mount_err)
    goto resource_cleanup
end

-- ########## 获取SD卡的可用空间信息并打印。 ########## 
data, err = fatfs.getfree("/sd")
if data then
    --打印SD卡的可用空间信息
    log.info("fatfs", "getfree", json.encode(data))
else
    --打印错误信息
    log.info("fatfs", "getfree", "err", err)
    goto resource_cleanup
end

-- 列出所有挂载点,如不需要,可注释掉。
data = io.lsmount()
log.info("fs", "lsmount", json.encode(data))
::resource_cleanup::

log.info("结束", "开始执行关闭操作...")  
-- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
if mount_ok then
    if fatfs.unmount("/sd") then
        log.info("文件系统", "卸载成功")
    else
        log.error("文件系统", "卸载失败")
    end
end

三、常量详解

核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;

3.1 fatfs.SPI(SPI模式)

常量含义: SPI总线模式,通过SPI接口挂载存储设备(TF卡)
数据类型: number
取值范围: 暂无;
注意事项:1. 需预先初始化SPI接口(spi.setup)
        2. 需提供有效的SPI设备对象或ID
示例代码:-- 创建SPI设备对象;
        local spi_device = spi.deviceSetup(1, 20, 0, 0, 8, 24000000) 
        -- 挂载TF卡;
        fatfs.mount(fatfs.SPI, "/sd", spi_device)

3.2 fatfs.SDIO(SDIO模式)

常量含义: SDIO模式,用于通过SDIO接口挂载存储设备
数据类型: number
取值范围: 暂无;
注意事项:1. 无需预先初始化接口
        2. 仅需提供挂载点路径
示例代码:-- 使用SDIO模式挂载TF卡(不需要提供SPI相关参数)
        local mount_ok, err = fatfs.mount(fatfs.SDIO, "/sd", 24 * 1000 * 1000)

四、函数详解

4.1 fatfs.mount(mode, mount_point, spiid_or_spidevice, spi_cs, spi_speed, power_pin, power_on_delay, auto_format)

功能

挂载FAT文件系统;

注意事项

1、挂载前需确保SPI接口已正确初始化,SDIO模式不需要初始化; 2、若挂载失败,可以尝试打开调试信息查找原因;

参数

mode

参数含义:fatfs模式
数据类型:number
取值范围:fatfs.SPI(SPI模式)fatfs.SDIO(SDIO模式)
是否必选:是;
注意事项:需根据硬件连接方式选择对应模式;
参数示例:fatfs.SPI

mount_point

参数含义:文件系统的挂载点;
数据类型:string
取值范围:有效路径字符串;
是否必选:是;
注意事项:暂无;
参数示例:"/sd"

spiid_or_spidevice

参数含义:SPI的ID或者是SPI设备对象
数据类型:number或userdata
取值范围:有效的SPI ID或设备对象
是否必选:是;
注意事项:若当前这个参数传的是spi device,后续spi_cs和spi_speed参数可不传,如传后续参数会自动忽略;
参数示例:-- 1. 初始化SPI总线并获取一个配置好的SPI设备对象
         -- spi.setup 返回的 spi_device 已经包含了CS引脚(20)和速度(24MHz)的信息!
         local spi_device = spi.deviceSetup(1, 20, 0, 0, 8, 24000000)
         -- 2. 挂载文件系统:直接把这个配置好的“参数”交给fatfs 
         -- 注意:这里不需要再传 pin_cs 和 spi_speed 参数了!
         fatfs.mount(fatfs.SPI, "/sd", spi_device)

spi_cs

参数含义:片选脚的GPIO号
数据类型:number
取值范围:有效的GPIO编号
是否必选:SPI模式并且spiid_or_spidevice参数为SPI ID时,必选;
         其余情况下,此参数无意义;
注意事项:SPI模式并且spiid_or_spidevice参数为SPI ID时,必选;
         其余情况下,此参数无意义;
参数示例:20

spi_speed

参数含义:设置的SPI最高速度tf卡初始化时会到达400kHz,之后会恢复到设置速度;
         1tf卡初始化时达到400kHz速率,是为了尽量兼容不同品牌的卡,采用一个最
         保守、最通用的低速来进行第一次通信;
         2、初始化后恢复设置速度,如24MHz,是为了极大程度的提高后续的读写能力;
数据类型:number
取值范围:速率与主控芯片的spi_clk引脚或sdio_clk引脚的最大速率有关
         Air8000Air780EH系列模块通过spi挂载tf卡,最高可支持25MHz
         有效取值范围400 kHz - 25 MHz,低于400kHz可能无法初始化

         Air8101系列或Air6101系列通过sdio挂载tf卡,最高可支持80MHz
         有效取值范围400 kHz - 80 MHz
         可根据要传输的数据大小,使用的主控硬件设备,选择的tf卡种类综合判断该参数传入的大小
是否必选:否;
注意事项:默认10MHz,单位Hz
参数示例:24000000 (24MHz)

power_pin

参数含义:TF卡电源控制脚
数据类型:number
取值范围:有效的GPIO编号
是否必选:否;
注意事项:如果没有,或者是内置电源控制方式,可不传;
参数示例:140

power_on_delay

参数含义:TF卡电源复位过程时间
         当在 fatfs.mount 函数中指定了 power_pin 参数时,完整的电源复位流程才会被触发
         其目的是通过完全断电再上电的方式,让TF卡硬件恢复到绝对的初始状态
         复位过程:
                 1、断电(强制TF卡完全停止工作);
                 2、放电等待(电压完全跌落到零,确保一次彻底的硬复位);
                 3、上电(驱动将 power_pin 恢复为高电平,即有效供电电平,重新为TF卡供电);
                 4、上电延时等待power_on_delay(目的是为了留出时间让电压稳定,并完成自检,时间同步,寄存器初始化等内部操作);
                 5、开始通信 (在power_on_delay时间结束后,驱动才认为TF卡已经准备就绪,随后开始发送最初的SPI命令进行初始化);

数据类型:number
取值范围:≥0的整数;
是否必选:否;
注意事项:单位ms,默认值是1
参数示例:10

auto_format

参数含义:挂载失败是否尝试格式化;
数据类型:boolean
取值范围:true或false
是否必选:否;
注意事项:默认是true,即自动格式化;
参数示例:true

返回值

local success, err = fatfs.mount(mode, mount_point, spiid_or_spidevice, spi_cs, spi_speed, power_pin, power_on_delay, auto_format)

有两个返回值success和err

success

含义说明:挂载是否成功;
数据类型:boolean
取值范围:true或false
注意事项:成功返回true,失败返回false
返回示例:true

err

含义说明:失败的原因;
数据类型:string或者number
取值范围:错误描述字符串;
注意事项:仅当success为false时有效
返回示例:3

错误返回码对照表

返回码 符号名称 含义说明 常见原因及排查建议
1 FR_DISK_ERR 底层磁盘I/O层发生硬件错误 - SPI通信错误(检查接线) - 卡响应超时(降低SPI速度尝试) - 硬件故障(更换TF卡或模块)
2 FR_INT_ERR 断言失败(内部错误) - 系统资源不足 - 无法创建互斥锁(ff_mutex_create返回0) - FatFS库内部错误
3 FR_NOT_READY 物理驱动器不工作 - 卡未正确插入(重新插拔TF卡) - 硬件连接问题(检查电路) - 电源问题(检查供电引脚和电压)
13 FR_NO_FILESYSTEM 找不到有效的FAT卷 - SD卡未格式化(使用工具格式化) - 文件系统损坏(尝试修复或格式化) - 卡类型不受支持(更换TF卡)
14 FR_MKFS_ABORTED f_mkfs函数因某些问题中止 - 格式化过程被中断 - 卡写保护(检查写保护开关) - 存储介质问题
15 FR_TIMEOUT 无法在定义的时间内获得卷控制 - 卡响应慢(增加超时时间) - 硬件性能问题 - 通信干扰
17 FR_NOT_ENOUGH_CORE LFN工作缓冲区无法分配或给定缓冲区大小不足 - 系统内存不足 - 需要增加LFN工作缓冲区大小 - 优化内存使用
19 FR_INVALID_PARAMETER 给定参数无效 - 挂载点参数错误 - SPI参数配置错误 - 函数调用参数不正确

错误返回值对照表

返回值 含义 常见原因 排查建议
"out of memory when malloc FATFS" 系统无法为FATFS文件系统对象分配内存 1. 系统可用内存不足 2. 内存碎片严重 3. FATFS对象大小超过可用块 1. 检查内存使用情况 2. 优化程序释放未用内存 3. 减少同时挂载的文件系统数量
"out of memory when malloc luat_fatfs_spi_t" 无法为SPI模式分配专用结构体内存 1. SPI模式专用内存池耗尽 2. 多次重复挂载未卸载 3. SPI参数配置过大 1. 卸载无用挂载点(fatfs.unmount()) 2. 检查SPI缓冲区大小设置 3. 重启设备释放残留内存
"out of memory when malloc luat_fatfs_sdio_t" 无法为SDIO模式分配专用结构体内存 1. SDIO控制器实例超限 2. DMA缓冲区配置过大 3. 硬件不支持多SDIO设备 1. 确认硬件SDIO支持数量 2. 减小DMA缓冲区大小 3. 检查是否存在冲突外设
"fatfs_mode error" 文件系统模式参数无效 1. 错误模式值传入 2. 常量名拼写错误 3. SDK版本不兼容 1. 检查mode参数值(fatfs.SPI等) 2. 验证SDK版本支持的模式 3. 使用常量而非硬编码数值

示例

-- 使用SPI模式挂载TF卡
local spiId = 2
-- 设置并启用SPI
local result = spi.setup(spiId, 255, 0, 0, 8, 400000)
local TF_CS = 8
gpio.setup(TF_CS, 1)
-- 挂载sd卡
local success, err = fatfs.mount(fatfs.SPI, "/sd", spiId, TF_CS, 24000000)
if success then
   log.info("fatfs.mount", "挂载成功")
else
   log.error("fatfs.mount", "挂载失败", err)
end

4.2 fatfs.unmount(mount_point)

功能

取消挂载fatfs文件系统;

注意事项

1、必须与fatfs.mount的挂载点参数一致;

2、卸载前应确保所有文件操作已完成;

参数

mount_point

    参数含义:文件系统的挂载点;
    数据类型:string
    取值范围:有效的挂载点路径;
    是否必选:是;
    注意事项:必须与fatfs.mount传入的值一致
    参数示例:"/sd"

返回值

local result = fatfs.unmount(mount_point)

有一个返回值 result

result

含义说明:操作结果;
数据类型:number
取值范围:成功返回0,且取消挂载必然会成功;
注意事项:取消挂载必然会成功;
返回示例:0

示例

local result = fatfs.unmount("/sd")
-- 通过返回值是否等于0,判断是否卸载成功;
if result == 0 then
   log.info("fatfs.unmount", "卸载成功")
else
   log.error("fatfs.unmount", "卸载失败", result)
end

4.3 fatfs.getfree(mount_point)

功能

获取文件系统可用空间信息;

注意事项

1、当前扇区大小固定在512字节;

2、需要跟fatfs.mount传入的挂载点一致;

参数

mount_point

参数含义:文件系统挂载点;
数据类型:string
取值范围:有效的挂载点路径;
是否必选:是;
注意事项:必须与fatfs.mount传入的值一致
参数示例:"/sd"

返回值

local data, err = fatfs.getfree(mount_point)

有两个返回值data和err

data

含义说明:空间信息表,表的格式为{total_sectors, total_kb, free_sectors, free_kb},说明如下;
         {
            -- 含义说明:总扇区数量;
            -- 数据类型:number;
            -- 取值范围:必须>0;
            -- 注意事项:当前扇区大小固定在512字节;
            -- 返回示例:244264960
            total_sectors = 244264960,

            -- 含义说明:总存储容量;
            -- 数据类型:number;
            -- 取值范围:必须>0;
            -- 注意事项:单位是千字节(KB),计算方式为total_sectors * 512 / 1024;
            -- 返回示例:122132480
            total_kb = 122132480,

            -- 含义说明:空闲扇区数量;
            -- 数据类型:number;
            -- 取值范围:0 ≤ free_sectors ≤ total_sectors;
            -- 注意事项:表示当前可用的扇区数量;
            -- 返回示例:244262144
            free_sectors = 244262144,



            -- 含义说明:可用存储容量;
            -- 数据类型:number;
            -- 取值范围:0 ≤ free_kb ≤ total_kb;
            -- 注意事项:单位是千字节(KB),计算方式为free_sectors * 512 / 1024;
            -- 返回示例:122131072
            free_kb = 122131072, 
          }
数据类型:table
取值范围:成功时返回包含空间信息的table,失败时返回nil
注意事项:包含total_sectors, total_kb, free_sectors, free_kb 字段;
返回示例:local data, err = fatfs.getfree("/sd")
           if data then
             --打印SD卡的可用空间信息
             --fatfs getfree {"free_sectors":244262144,"total_kb":122132480,"free_kb":122131072,"total_sectors":244264960}
             log.info("fatfs", "getfree", json.encode(data))

err

含义说明:底层错误码;
数据类型:number
取值范围:系统错误码;
注意事项:仅当data为nil时有效
返回示例:3

错误返回码对照表

返回码 符号名称 含义说明 常见原因 排查建议
0 FR_OK 函数执行成功 操作正常完成 -
1 FR_DISK_ERR 底层磁盘I/O错误 SPI通信错误 卡响应超时 硬件故障 检查接线连接 降低SPI通信速度 更换TF卡测试
2 FR_INT_ERR 断言失败(内部错误) 文件系统结构损坏 FAT表损坏 内部逻辑错误 格式化TF卡 使用磁盘修复工具 检查文件系统完整性
3 FR_NOT_READY 物理驱动器未准备好 卡未正确插入 硬件连接问题 电源问题 重新插拔TF卡 检查电路连接 验证电源电压稳定性
4 FR_NO_FILE 找不到文件 文件不存在 路径错误 文件名大小写问题 确认文件路径正确 检查文件名大小写 验证文件是否被删除
5 FR_NO_PATH 找不到路径 目录路径不存在 路径格式错误 检查路径格式 确认目录存在 使用绝对路径
6 FR_INVALID_NAME 路径名格式无效 文件名含非法字符 路径长度超限 避免使用特殊字符 缩短文件路径长度 使用合规命名
7 FR_DENIED 访问被拒绝 文件只读属性 磁盘空间不足 权限限制 检查文件属性 清理磁盘空间 确认访问权限
8 FR_EXIST 文件已存在 尝试创建已存在的文件 重命名目标已存在 先检查文件是否存在 选择不同的文件名 删除已存在的文件
9 FR_INVALID_OBJECT 文件对象无效 文件句柄已关闭 对象被删除或移动 重新打开文件 检查文件状态 验证对象有效性
10 FR_WRITE_PROTECTED 物理写保护 TF卡写保护开关开启 文件系统只读挂载 关闭写保护开关 重新以读写模式挂载 检查硬件写保护状态
11 FR_INVALID_DRIVE 逻辑驱动器号无效 驱动器号超出范围 驱动器未初始化 检查驱动器编号 确认驱动器已初始化 验证配置参数
12 FR_NOT_ENABLED 卷无工作区 驱动器未挂载 工作区未分配 先挂载文件系统 分配工作区内存 检查初始化流程
13 FR_NO_FILESYSTEM 无有效FAT卷 SD卡未格式化 文件系统损坏 卡类型不支持 格式化TF卡为FAT32 使用磁盘修复工具 更换兼容的存储卡
14 FR_MKFS_ABORTED 格式化中止 格式化过程被中断 卡写保护 存储介质问题 解除写保护 检查存储介质健康状态 重新执行格式化
15 FR_TIMEOUT 操作超时 卡响应缓慢 硬件性能问题 通信干扰 增加超时时间设置 检查硬件连接质量 降低通信频率
16 FR_LOCKED 文件被锁定 文件被其他进程锁定 共享冲突 关闭其他占用进程 等待资源释放 检查文件锁状态
17 FR_NOT_ENOUGH_CORE 内存不足 系统内存不足 LFN缓冲区太小 增加系统内存 扩大LFN工作缓冲区 优化内存使用
18 FR_TOO_MANY_OPEN_FILES 打开文件数超限 同时打开文件过多 FF_FS_LOCK设置太小 关闭不再使用的文件 增加FF_FS_LOCK配置值 优化文件打开策略
19 FR_INVALID_PARAMETER 参数无效 函数参数错误 参数超出范围 检查API调用参数 验证参数取值范围 查阅函数文档

示例

local data, err = fatfs.getfree("/sd")
if data then
    --打印SD卡的可用空间信息
    -- fatfs getfree {"free_sectors":244262144,"total_kb":122132480,"free_kb":122131072,"total_sectors":244264960}
    log.info("fatfs", "getfree", json.encode(data))
else
    --打印错误信息
    log.info("fatfs", "getfree", "err", err)
    goto resource_cleanup
end

4.4 fatfs.debug(value)

功能

设置fatfs调试模式;

注意事项

建议仅在排查问题时开启,增加调试日志;

参数

value

参数含义:设置fatfs调试模式的开关
数据类型:number
取值范围:10
是否必选:是;
注意事项:1时表示开,0时表示关;
参数示例:1

返回值

示例

-- 若挂载失败,可以尝试打开调试信息,查找原因
fatfs.debug(1)

4.5 fatfs.config(crc_check, write_to)

功能

配置fatfs特殊参数;

注意事项

1、大部分卡无需配置;

2、部分不能正常读写的卡,经过配置后可能能读写成功(暂未有实际案例);

参数

crc_check

参数含义:读取时是否跳过CRC检查
数据类型:number
取值范围:01
是否必选:否;
注意事项:1跳过不检查CRC0不跳过检查CRC,默认0。跳过CRC检查的主要风险是无法保障数据的正确性,不应跳出过crc校验,除非你测试确认是由于校验失败导致的问题;
参数示例:1

write_to

参数含义:单次写入超时时间;
数据类型:number
取值范围:≥0的整数;
是否必选:否;
注意事项:单位ms,默认100ms
参数示例:200

返回值

示例

-- 配置跳过CRC检查,写入超时200ms
fatfs.config(1, 200)

五、产品支持说明

支持 LuatOS 开发的所有产品都支持fatfs核心库。