【嵌入式】由于 FatFs 太坑,我开发了 Phat 用于读写 FAT12/16/32 文件系统
# Phat:又一个 FAT12/16/32 轮子立项原因是我被垃圾的 FatFs 坑了。这玩意儿代码不清晰,行为不明确,ROM 占的极大(它存了大量的代码页转换表),UTF-8、UTF-16 的支持比较暧昧(估计作者都不知道这两个可以直接相互转换),另外它的版本管理也非常混乱,缺乏 LRU 缓存等。这都是其次的了,最主要是:它用不了。因为有野指针,挂载后会把 FatFs 结构体后半部分给 `mem_set()` 成零,导致无法枚举目录。问了其他的朋友,他们有完全相同的遭遇:就是用不了。那,没办法了。大家都想着有人造一个至少能用的。我于是出手了。
## 概述
Phat 是一个专为嵌入式系统和跨平台开发设计的 FAT 文件系统 API。
* 支持 MBR、GPT 分区表
* 支持无分区表(使整个磁盘为一个分区)
* 支持 FAT12/16/32 的基本实现。
* 遍历目录
* 打开文件(可创建或只读)
* 读取文件
* 写入文件
* 文件寻址
* 删除文件
* 创建目录
* 删除目录
* 重命名
* 移动
* 将磁盘初始化为 MBR/GPT
* 在 MBR/GPT 磁盘上创建分区
* MakeFS: 将分区格式化为 FAT12/16/32
* 在 Windows 上通过虚拟磁盘功能进行调试。
* 支持多分区。
* 不使用动态内存分配(Windows 调试代码除外)。
* 文件名和目录采用 UTF-16 编码。
* 默认代码页为 437(OEM 美国)。
* 包含 LRU(最近最少使用)扇区缓存。
* 对任何路径长度没有限制(仅限制文件名/目录名长度 ≤ 255)。
## 代码仓库
(https://github.com/0xAA55/Phat)
(https://gitee.com/a5k3rn3l/phat)
两个仓库是同步的,如果你不方便访问 GitHub,就从 Gitee 拉取代码吧。
## 用法
首先,你需要查看 `BSP_phat.c` 和 `BSP_phat.h` 文件。这些文件提供了底层驱动实现,使 PHAT API 能够访问存储设备。
你需要实现以下几个函数,注意:扇区大小被固定为 512
```C
typedef int PhatBool_t;
PhatBool_t BSP_OpenDevice(void *userdata);
PhatBool_t BSP_CloseDevice(void *userdata);
PhatBool_t BSP_ReadSector(void *buffer, LBA_t LBA, size_t num_blocks, void *userdata);
PhatBool_t BSP_WriteSector(void *buffer, LBA_t LBA, size_t num_blocks, void *userdata);
LBA_t BSP_GetDeviceCapacity(void *userdata);
```
为 STM32H750 微控制器提供了默认实现,在使用 GCC 或 ARMCC 编译时可通过 SDMMC1 进行读写操作。
如果定义了 `_WIN32`,默认实现会使用 `CreateFileW()` 打开 `test.vhd`。
虚拟硬盘会被自动创建,测试代码退出后会自动被挂载到系统,也就是你会发现你多了一个硬盘。查看这个硬盘可以观察文件系统是否正常。
你只需要 `BSP_phat.c`、`BSP_phat.h`、`phat.c` 和 `phat.h` 这几个文件。将它们添加到您的项目中即可。
## API
```C
/**
* @brief 初始化 Phat 文件系统实例
* @param phat 指向 Phat_t 结构体的指针,用于存储文件系统状态
* @return PhatState 状态码
* - PhatState_OK: 初始化成功
* - PhatState_InvalidParameter: phat 参数为 NULL
* - PhatState_DriverError: 底层驱动初始化失败
*
* 此函数会初始化底层磁盘驱动,并设置默认的日期时间。
* 在使用任何其他 Phat API 之前必须先调用此函数。
*/
PhatState Phat_Init(Phat_p phat);
/**
* @brief 反初始化 Phat 文件系统实例
* @param phat 指向已初始化的 Phat_t 结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 反初始化成功
* - PhatState_InvalidParameter: phat 参数为 NULL
*
* 此函数会卸载文件系统(如果已挂载),关闭底层磁盘驱动,
* 并清理所有缓存数据。调用后 phat 结构体内容将被清零。
*/
PhatState Phat_DeInit(Phat_p phat);
/**
* @brief 挂载文件系统分区
* @param phat 指向已初始化的 Phat_t 结构体的指针
* @param partition_index 分区索引号(0-based)
* - 0: 第一个分区
* - 1: 第二个分区,以此类推
* @param write_enable 是否启用写操作
* - 0: 只读模式
* - 1: 读写模式
* @return PhatState 状态码
* - PhatState_OK: 挂载成功
* - PhatState_InvalidParameter: phat 参数为 NULL
* - PhatState_NoMBR: 未找到有效的分区表
* - PhatState_FSNotFat: 分区不是 FAT 文件系统
* - PhatState_PartitionIndexOutOfBound: 分区索引超出范围
* - PhatState_NeedBigLBA: 需要 64 位 LBA 支持(定义宏 PHAT_BIGLBA 然后重新编译 Phat)
*
* 此函数会读取分区表,定位指定的 FAT 分区,并加载文件系统元数据。
* 如果分区索引为 0 且磁盘没有分区表,则整个磁盘被视为一个分区。
*/
PhatState Phat_Mount(Phat_p phat, int partition_index, PhatBool_t write_enable);
/**
* @brief 刷新所有缓存数据到磁盘
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 刷新成功
* - PhatState_InvalidParameter: phat 参数为 NULL
* - PhatState_WriteFail: 写入磁盘失败
*
* 此函数会将所有脏缓存页写入磁盘,确保数据持久化。
* 在安全移除存储设备前应调用此函数。
*/
PhatState Phat_FlushCache(Phat_p phat);
/**
* @brief 卸载文件系统
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 卸载成功
* - PhatState_InvalidParameter: phat 参数为 NULL
*
* 此函数会刷新缓存并卸载文件系统,但保持磁盘驱动打开状态。
* 调用后可以重新挂载其他分区。
*/
PhatState Phat_Unmount(Phat_p phat);
/**
* @brief 设置当前日期时间
* @param phat 指向 Phat_t 结构体的指针
* @param cur_date 指向日期结构体的指针(可为 NULL)
* @param cur_time 指向时间结构体的指针(可为 NULL)
*
* 此函数设置的文件创建/修改时间将用于后续的文件操作。
* 如果任一参数为 NULL,则对应的时间字段保持不变。
*/
void Phat_SetCurDateTime(Phat_p phat, const Phat_Date_p cur_date, const Phat_Time_p cur_time);
/**
* @brief 打开根目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param dir_info 指向目录信息结构体的指针,用于存储目录状态
*
* 此函数初始化目录遍历状态,准备从根目录开始遍历。
* 使用完毕后应调用 Phat_CloseDir() 清理资源。
*/
void Phat_OpenRootDir(Phat_p phat, Phat_DirInfo_p dir_info);
/**
* @brief 改变当前目录
* @param dir_info 指向已打开的目录信息结构体的指针
* @param dirname 要切换到的目录名(相对路径)
* @return PhatState 状态码
* - PhatState_OK: 切换成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_DirectoryNotFound: 目录不存在
* - PhatState_NotADirectory: 指定路径不是目录
*
* 支持相对路径和绝对路径,路径分隔符可以是 '/' 或 '\'。
*/
PhatState Phat_ChDir(Phat_DirInfo_p dir_info, const WChar_p dirname);
/**
* @brief 打开指定路径的目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 目录路径(绝对或相对路径)
* @param dir_info 指向目录信息结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 打开成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_DirectoryNotFound: 目录不存在
* - PhatState_NotADirectory: 指定路径不是目录
*
* 路径会被自动规范化,支持多层目录路径。
*/
PhatState Phat_OpenDir(Phat_p phat, const WChar_p path, Phat_DirInfo_p dir_info);
/**
* @brief 读取目录中的下一个条目
* @param dir_info 指向已打开的目录信息结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 成功读取到条目
* - PhatState_EndOfDirectory: 已到达目录末尾
* - PhatState_InvalidParameter: dir_info 参数为 NULL
* - PhatState_ReadFail: 读取磁盘失败
*
* 成功调用后,dir_info 结构体中将包含当前条目的详细信息,
* 包括文件名、属性、大小、时间戳等。
*/
PhatState Phat_NextDirItem(Phat_DirInfo_p dir_info);
/**
* @brief 关闭目录
* @param dir_info 指向目录信息结构体的指针
*
* 清理目录遍历状态,释放相关资源。
* 对于已关闭的目录再次调用此函数是安全的。
*/
void Phat_CloseDir(Phat_DirInfo_p dir_info);
/**
* @brief 打开文件
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 文件路径
* @param readonly 是否以只读方式打开
* - 0: 读写模式(如文件不存在则创建)
* - 1: 只读模式(文件必须存在)
* @param file_info 指向文件信息结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 打开成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_FileNotFound: 文件不存在(只读模式)
* - PhatState_IsADirectory: 指定路径是目录
* - PhatState_ReadOnly: 文件系统为只读模式
* - PhatState_NameTooLong: 文件名过长
*
* 在读写模式下,如果文件不存在会自动创建。
* 使用完毕后必须调用 Phat_CloseFile() 关闭文件。
*/
PhatState Phat_OpenFile(Phat_p phat, const WChar_p path, PhatBool_t readonly, Phat_FileInfo_p file_info);
/**
* @brief 读取文件数据
* @param file_info 指向已打开的文件信息结构体的指针
* @param buffer 存储读取数据的缓冲区
* @param bytes_to_read 要读取的字节数
* @param bytes_read 实际读取的字节数(可为 NULL)
* @return PhatState 状态码
* - PhatState_OK: 读取成功(可能未读完)
* - PhatState_EndOfFile: 已到达文件末尾
* - PhatState_InvalidParameter: file_info 或 buffer 为 NULL
* - PhatState_ReadFail: 读取磁盘失败
*
* 从当前文件指针位置开始读取,读取后文件指针会相应移动。
* 如果 bytes_read 不为 NULL,会返回实际读取的字节数。
*/
PhatState Phat_ReadFile(Phat_FileInfo_p file_info, void *buffer, size_t bytes_to_read, size_t *bytes_read);
/**
* @brief 写入文件数据
* @param file_info 指向已打开的文件信息结构体的指针
* @param buffer 要写入的数据缓冲区
* @param bytes_to_write 要写入的字节数
* @param bytes_written 实际写入的字节数(可为 NULL)
* @return PhatState 状态码
* - PhatState_OK: 写入成功
* - PhatState_InvalidParameter: file_info 或 buffer 为 NULL
* - PhatState_ReadOnly: 文件为只读模式
* - PhatState_WriteFail: 写入磁盘失败
* - PhatState_NotEnoughSpace: 磁盘空间不足
*
* 从当前文件指针位置开始写入,写入后文件指针会相应移动。
* 写入操作可能不会立即持久化到磁盘,除非调用刷新函数。
*/
PhatState Phat_WriteFile(Phat_FileInfo_p file_info, const void *buffer, size_t bytes_to_write, size_t *bytes_written);
/**
* @brief 关闭文件
* @param file_info 指向文件信息结构体的指针
* @return PhatState 状态码
* - PhatState_OK: 关闭成功
* - PhatState_InvalidParameter: file_info 为 NULL
*
* 关闭文件并更新文件的元数据(如修改时间、文件大小等)。
* 对于已关闭的文件再次调用此函数是安全的。
*/
PhatState Phat_CloseFile(Phat_FileInfo_p file_info);
/**
* @brief 设置文件指针位置
* @param file_info 指向文件信息结构体的指针
* @param position 新的文件指针位置(从文件开头计算)
* @return PhatState 状态码
* - PhatState_OK: 设置成功
* - PhatState_EndOfFile: 位置超出文件末尾(设置到末尾)
* - PhatState_InvalidParameter: file_info 为 NULL
*
* 设置下次读写操作的起始位置。
* 如果位置超出文件大小,下次写入文件时,会自动从文件末尾往后追加空数据 0x00 直到设置的位置。
*/
PhatState Phat_SeekFile(Phat_FileInfo_p file_info, FileSize_t position);
/**
* @brief 获取当前文件指针位置
* @param file_info 指向文件信息结构体的指针
* @param position 存储当前位置的指针
*/
void Phat_GetFilePointer(Phat_FileInfo_p file_info, FileSize_t *position);
/**
* @brief 获取文件大小
* @param file_info 指向文件信息结构体的指针
* @param size 存储文件大小的指针
*/
void Phat_GetFileSize(Phat_FileInfo_p file_info, FileSize_t *size);
/**
* @brief 检查是否到达文件末尾
* @param file_info 指向文件信息结构体的指针
* @return PhatBool_t
* - 0: 未到达文件末尾
* - 1: 已到达文件末尾
*/
PhatBool_t Phat_IsEOF(Phat_FileInfo_p file_info);
/**
* @brief 创建目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 要创建的目录路径
* @return PhatState 状态码
* - PhatState_OK: 创建成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_DirectoryAlreadyExists: 目录已存在
* - PhatState_ReadOnly: 文件系统为只读模式
* - PhatState_NameTooLong: 目录名过长
* - PhatState_BadFileName: 目录名包含非法字符
*
* 支持创建多级目录,如果父目录不存在会自动创建。
*/
PhatState Phat_CreateDirectory(Phat_p phat, const WChar_p path);
/**
* @brief 删除目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 要删除的目录路径
* @return PhatState 状态码
* - PhatState_OK: 删除成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_DirectoryNotFound: 目录不存在
* - PhatState_NotADirectory: 指定路径不是目录
* - PhatState_DirectoryNotEmpty: 目录不为空
* - PhatState_ReadOnly: 文件系统为只读模式
*
* 只能删除空目录,删除前需确保目录内没有文件或子目录。
*/
PhatState Phat_RemoveDirectory(Phat_p phat, const WChar_p path);
/**
* @brief 删除文件
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 要删除的文件路径
* @return PhatState 状态码
* - PhatState_OK: 删除成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_FileNotFound: 文件不存在
* - PhatState_IsADirectory: 指定路径是目录
* - PhatState_ReadOnly: 文件系统为只读模式
*
* 删除文件并释放其占用的磁盘空间。
*/
PhatState Phat_DeleteFile(Phat_p phat, const WChar_p path);
/**
* @brief 重命名文件或目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param path 原路径
* @param new_name 新名称(不是完整路径,只是名称部分)
* @return PhatState 状态码
* - PhatState_OK: 重命名成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_FileNotFound: 原路径不存在
* - PhatState_FileAlreadyExists: 新名称已存在
* - PhatState_ReadOnly: 文件系统为只读模式
* - PhatState_BadFileName: 新名称包含非法字符
*
* 新名称必须在同一目录内,不能跨目录重命名。
*/
PhatState Phat_Rename(Phat_p phat, const WChar_p path, const WChar_p new_name);
/**
* @brief 移动文件或目录
* @param phat 指向已挂载的 Phat_t 结构体的指针
* @param oldpath 原路径
* @param newpath 新路径(完整路径)
* @return PhatState 状态码
* - PhatState_OK: 移动成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_FileNotFound: 原路径不存在
* - PhatState_FileAlreadyExists: 目标路径已存在
* - PhatState_ReadOnly: 文件系统为只读模式
* - PhatState_InvalidParameter: 试图将目录移动到其子目录
*
* 支持跨目录移动,但不能将目录移动到其自身的子目录中。
*/
PhatState Phat_Move(Phat_p phat, const WChar_p oldpath, const WChar_p newpath);
/**
* @brief 初始化磁盘为 MBR 分区表
* @param phat 指向 Phat_t 结构体的指针
* @param force 是否强制初始化
* - 0: 如果已有有效分区表则跳过
* - 1: 强制覆盖现有分区表
* @param flush 是否立即刷新到磁盘
* - 0: 仅修改缓存
* - 1: 立即写入磁盘
* @return PhatState 状态码
* - PhatState_OK: 初始化成功
* - PhatState_InvalidParameter: phat 为 NULL
* - PhatState_DiskAlreadyInitialized: 磁盘已初始化且 force=0
* - PhatState_WriteFail: 写入磁盘失败
*
* 此函数会写入标准的 MBR 引导代码和空的分区表。
*/
PhatState Phat_InitializeMBR(Phat_p phat, PhatBool_t force, PhatBool_t flush);
/**
* @brief 初始化磁盘为 GPT 分区表
* @param phat 指向 Phat_t 结构体的指针
* @param force 是否强制初始化
* @param flush 是否立即刷新到磁盘
* @return PhatState 状态码
* - PhatState_OK: 初始化成功
* - PhatState_InvalidParameter: phat 为 NULL
* - PhatState_DiskAlreadyInitialized: 磁盘已初始化且 force=0
* - PhatState_WriteFail: 写入磁盘失败
*
* 此函数会创建保护性 MBR 和完整的 GPT 分区表结构。
*/
PhatState Phat_InitializeGPT(Phat_p phat, PhatBool_t force, PhatBool_t flush);
/**
* @brief 获取可用的首尾 LBA 地址
* @param phat 指向 Phat_t 结构体的指针
* @param first 存储首个可用 LBA 的指针
* @param last 存储最后一个可用 LBA 的指针
* @return PhatState 状态码
* - PhatState_OK: 获取成功
* - PhatState_InvalidParameter: 参数为 NULL
* - PhatState_NoMBR: 未找到有效的分区表
*
* 根据分区表类型(MBR/GPT)计算可用的磁盘空间范围。
*/
PhatState Phat_GetFirstAndLastUsableLBA(Phat_p phat, LBA_p first, LBA_p last);
/**
* @brief 创建分区
* @param phat 指向 Phat_t 结构体的指针
* @param partition_start 分区起始 LBA
* @param partition_size_in_sectors 分区大小(扇区数)
* @param bootable 是否可引导
* - 0: 非引导分区
* - 1: 引导分区
* @param flush 是否立即刷新到磁盘
* @return PhatState 状态码
* - PhatState_OK: 创建成功
* - PhatState_InvalidParameter: phat 为 NULL
* - PhatState_NoMBR: 磁盘未初始化
* - PhatState_PartitionLBAIsIllegal: LBA 范围非法
* - PhatState_PartitionOverlapped: 与现有分区重叠
* - PhatState_NoFreePartitions: 没有空闲的分区表项
*
* 分区会自动对齐到合适的边界,避免性能问题。
*/
PhatState Phat_CreatePartition(Phat_p phat, LBA_t partition_start, LBA_t partition_size_in_sectors, PhatBool_t bootable, PhatBool_t flush);
/**
* @brief 格式化分区并挂载
* @param phat 指向 Phat_t 结构体的指针
* @param partition_index 分区索引
* @param FAT_bits FAT 类型位数
* - 0: 自动选择
* - 12: FAT12
* - 16: FAT16
* - 32: FAT32
* @param root_dir_entry_count 根目录条目数(FAT12/16)
* - 0: 使用默认值
* - 其他: 自定义条目数
* @param volume_ID 卷序列号
* @param volume_lable 卷标(可为 NULL)
* @param flush 是否立即刷新到磁盘
* @return PhatState 状态码
* - PhatState_OK: 格式化并挂载成功
* - PhatState_FSIsSubOptimal: 格式化成功但簇大小较大
* - PhatState_InvalidParameter: phat 为 NULL
* - PhatState_CannotMakeFS: 无法创建文件系统(参数错误)
* - PhatState_PartitionTooSmall: 分区太小
*
* 此函数会执行完整的分区格式化流程,并在成功后自动挂载。
*/
PhatState Phat_MakeFS_And_Mount(Phat_p phat, int partition_index, int FAT_bits, uint16_t root_dir_entry_count, uint32_t volume_ID, const char *volume_lable, PhatBool_t flush);
``` 感谢大佬分享~~
页:
[1]