FATFS接口与移植详解——嵌入式FATFS[2]

——是时候整理一下一团乱麻的存储介质了<2>

在上一篇文章中我们已经明确了FATFS的基础概念和逻辑组织形式,那么现在就来到了实际调动代码完成功能和完善逻辑访问接口的部分了。非常幸运,FATFS官方提供了使用ANSI C编译的可移植的代码库,所以我们的任务就简化成了分析模块接口并且完成模块在具体工程之中的移植问题。


本文中由于使用大量代码块,为提高阅读体验已做折叠处理,点击可展示详细信息


1. FATFS代码接口分析

前文提到,FATFS本质上是一种中间件,在嵌入式系统中工作时逻辑接口暴露给应用层代码调用,对底层需要接入物理介质驱动代码。FATFS官方提供的代码是一套实现了FAT/exFAT通用文件系统、面向嵌入式设计的可移植平台。编译此模块需要的最低版本是ANSI C也就是C89,并且该模块能够实现对于存储机制读写驱动层的完全隔离。该中间件暴露出的接口主要分为两部分:面向逻辑文件操作的应用层接口(Application Interface)和面向底层介质驱动的介质访问接口(Media Access Interface)。以下将使用若干个表格介绍不同分类下相关的接口函数具体情况,介质访问接口需要用户实现,不在本节讨论。

1.1 文件访问接口 [function:17]

f_open 文件打开函数
/**
  * @brief f_open函数用于打开一个文件
  * @param fp 是一个文件(FIL)类型的指针,函数执行后指向被打开的文件
  * @param path 是一个字符串(const TCHAR*),代表被打开文件的绝对路径+文件名
  * @param mode 是一个字节(BYTE)变量,用于设置打开文件方式的标志位
  * @retval 返回FRESULT类型的执行结果代码
  */
FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode);
标志位标志含义解释
FA_READ代表打开文件后可以读取文件内容
FA_WRITE代表打开文件后可以对文件写入内容,只读文件无法使用此选项
FA_OPEN_EXISING当打开的文件不存在,函数执行失败 [默认选项]
FA_CREATE_NEW创建一个新文件,如果文件存在那么这个动作失败,函数返回FR_EXIST
FA_CREATE_ALWAYS创建一个新文件,如果文件存在,那么久的文件将会被覆盖
FA_OPEN_ALWAYS当打开的文件不存在时创造一个新的文件
FA_OPEN_APPEND打开动作同上,只不过IO指针开始时就定位到问价结尾,附加写入模式
POSIX通用接口FATFS标志位组合
“r”FA_READ
“r+”FA_READ | FA_WRITE
“w”FA_CREATE_ALWAYS | FA_WRITE
“w+”FA_CREATE_ALWAYS | FA_WRITE | FA_READ
“a”FA_OPEN_APPEND | FA_WRITE
“a+”FA_OPEN_APPEND | FA_WRITE | FA_READ
“wx”FA_CREATE_NEW | FA_WRITE
“w+x”FA_CREATE_NEW | FA_WRITE | FA_READ
  • 如果系统处于只读(FF_FS_READONLY==1)模式,只有FA_READFA_OPEN_EXISTING可用
  • 如果打开文件成功,fp会指向一个新创建的文件对象;反之fp会是一个空指针
  • 文件处理完毕后需要使用f_close函数关闭文件
f_close 文件关闭函数
/**
  * @brief f_close函数用于关闭一个已经打开的文件对象,如果文件内容被修改过,那么缓存信息会被写入volume
  * @param fp 一个文件(FIL)类型的指针,代表需要被关闭的文件对象
  * @retval FRESULT类型的执行结果代码
  */
FRESULT f_close(FIL* fp);
  • 无论文件系统在移植过程中如何配置,本函数始终可用
  • 如果FF_FS_LOCK并未使能且文件打开为只读模式,这个动作可以不必执行,但是不推荐这样做
f_read 文件读取函数
/**
  * @breif 本函数用于从一个已经打开的文件对象中读取若干字节
  * @param fp是一个文件(FIL)类型的指针,需要指向一个已经打开的文件对象
  * @param buff应当是一个分配了内存空间的任意类型指针(void*), 用于存放读取出的数据
  * @param btr是一个无符号整数(UINT),代表期望读出的字节数
  * @param br是一个无符号整数指针(UINT*),存放实际上读出的字节数目
  * @retval 返回一个FRESULT作为读取操作的结果代码
  */
FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br);
  • 本函数开始读取的位置是当前文件对象的读写指针所在位置,读取完成后指针将会后移*br个字节
  • 无论如何配置,本函数始终可用
  • 函数执行后如果*br==btr或者说返回值为FR_OK那么说明读取正常;如果*br<btr代表期望读出数据已经大于文件剩余可读字节数目,文件IO指针已经移动到文件末尾
f_gets 字符串读取函数
/**
  * @brief 本函数用于从已经打开的文件中读取指定字符数的字符串
  * @param buff是一个无符号字符串(TCHAR*),用于存放从文件中读出的字符串,必须已经分配内存空间
  * @param len是一个整数(int),规定了需要读出的字符数量
  * @param fp是一个文件(FIL)类型的指针,需要指向一个已经打开的文件对象
  * @retval 如果读取操作成功,返回值就是buff
  */
TCHAR* f_gets(TCHAR* buff, int len,FIL* fp);
  • 读取动作有三种结束条件:读取到了’\n’读取到达了文件结尾buff中已经读取了len-1个字符
  • 读取完成后将会在buff中存储的字符串末尾第len个字符写入‘\0’作为字符串结尾
  • 如果读取不成功,那么返回值将被设置为一个空指针
  • 读取过程中碰到的EOF和错误可以通过f_eoff_error函数分别检测
  • 当系统使用Unicode API(FF_LFN_UNICODE>=1),本函数与f_putc、f_puts、f_printf使用的数据类型将切换为Unicode,使用的字符编码为FF_STRF_ENCODE设置的类型。如果被读取文件的Code Page和系统使用的Code Page不同,那么将进行字符串转换;如果转换失败字符将会丢失。
  • 本函数的底层映射为f_read,当FF_USE_STR_FUNC>=1时函数可用,当设定为2时,文件读取出的数据中字符‘\r’将会被删除
f_write 文件写入函数
/**
  * @brief 本函数用于将缓存中若干字节的数据写入到已经打开的文件之中
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param buff是一个任意类型的指针(void*)并且指向已经分配并且存储需要写入的内容的内存空间
  * @param btw是一个无符号整数(UINT),代表期望写入的字节数
  * @param bw是一个无符号整数指针(UINT*),存放实际上写入的字节数目
  * @retval 返回一个FRESULT作为写入操作的结果代码
  */
FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw);
  • 本山叔开始写入的位置是当前文件对象的读写指针所在位置,写入完成后指针将会后移*bw个字节
  • 当系统配置为可写(FF_FS_READONLY==0)时本函数可用
  • 函数执行后如果*bw==btw或者说返回值为FR_OK那么说明写入正常;如果*bw<btw代表未能完全写入内容,证明磁盘空间已满无法继续写入。当磁盘空间接近满载时这个函数可能执行缓慢。
f_putc 字符写入函数
/**
  * @brief 本函数用于向已经打开的文件中写入一个字符
  * @param chr是一个字符(TCHAR),为待写入内容
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 当写入动作成功,反馈写入文件字符编码的单位数,如果写入失败将会返回一个负数
  */
int f_putc(TCHAR chr, FIL* fp);
  • 关于Unicode与字符集编码相关事宜,查看f_gets函数中蓝色提示,并且注意占据多个字母的Unicode字符无法通过本函数写入
  • 这个函数底层使用f_write实现。当系统可写(FF_FS_READONLY==0)并且FF_USE_STRFUNC(字符串函数可用)时函数可用。当FF_USE_STRFUNC==2时输出的‘\n’将自动替换为”\r\n”
f_puts 字符串写入函数
/**
  * @brief 本函数用于向已经打开的文件中写入一个字符串
  * @param chr是一个字符串(const TCHAR*),为待写入内容
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 当写入动作成功,反馈写入文件字符编码的单位数,如果写入失败将会返回一个负数
  */
int f_puts(const TCHAR* str, FIL* fp);
  • 本函数注意事项全部和f_putc相同,只不过需要将字符方面的结论理解为字符串相关结论
f_printf 格式化字符串写入函数
/**
  * @brief 本函数用于向已经打开的文件中写入一个格式化字符串
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param fmt是一个字符串(const TCHAR*),为格式字符串
  * @param ...为缺省可变参数
  * @retval 当写入动作成功,反馈写入文件字符编码的单位数,如果写入失败将会返回一个负数
  */
int f_printf(FIL* fp, const TCHAR* fmt, ...);
  • 本函数相当于sprintff_write两个函数联合构成,但效率更好,因此语法符合pinrtf的用法
  • 其他注意事项全部和f_puts相同
f_lseek 读写指针移动函数
/**
  * @brief 本函数用于重设读写IO指针在文件中的位置
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param ofs是一个单位数据(FSIZE_T)类型的整数,代表了指针移动的目的坐标
  * @retval 函数返回一个FRESULT状态代码
  */
FRESULT f_lseek(FIL* fp,FSIZE_t ofs);
#define f_rewind(fp) f_lseek((fp), 0);
  • 文件的读写指针指向下一次读写操作的第一个数据,本函数只移动指针而不做读写操作
  • 在具体的使用过程中通常定义宏函数f_rewind用作将指针复位到文件开头
  • FSIZE_t有可能是DWORD(32bit)或者是QWORD(64bit),这依赖于FF_FS_EXFAT的设置,也就是说在exFAT系统中文件长度可能超过2^32,故而exFAT中的FSIZE_t应当使用QWORD
  • 如果在写入模式下移动指针的范围超过了文件的大小,文件大小将会随着指针的移动而扩张,扩展部分的文件数据是完全随机的,这实际上是一种快速分配文件空间的技巧
  • 如果要为文件分配连续的空间,应当使用f_expand更加合理
  • 本函数执行成功后应当检查文件指针的位置保证确实移动成功。如果检测发现移动不成功,那么代表着如下情况:
    • 在只读模式下,指针已经移动到文件末尾,无法向后移动
    • 在可写模式下,整个Volume存储空间已经满载,无法分配新的空间
  • 本函数可用开启快速查找(Fast Seek)功能,这种功能可以在不访问FAT的情况下快速向后长距离查找,本质上使用了RAM中暂存的簇链接映射表(Cluster Link Map Table)
    • 快速查找模式下可以使用f_readf_write但是不能够通过f_write和f_lseek扩展大小
    • 快速查找模式仅仅在FF_USE_FASTSEEK==1时是可用的
    • 使用快速查找模式时必须在执行实际的操作前按照如下步骤预先装载CLMT
      • 首先建立一个DWORD类型的数组用于存储CLMT
      • 将数组的地址存储到fp->cltbl
      • 将数组的大小存储到fp->cltbl[0],也就是数组[0]=sizeof(数组)
      • 执行f_lseek(fp, CREATE_LINK_MAP)建立CLMT
    • 使用快速查找功能时,f_read、f_write和f_lseek不访问FAT信息
    • 快速查找模式下如果函数返回值为FR_NOT_ENOUGH_CORE代表CLMT数组大小不足,这个大小的计算公式为:(文件碎片数量+1)*2,例如一个分段为5的文件就需要12大小的数组
  • 本函数在FF_FS_MINIMIZE<=2可用
f_tell 读写指针查询函数
/**
  * @brief 此函数事实上为宏函数,用于查看当前文件指针位置
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 返回当前文件对象中读写指针的坐标
  */
FSIZE_t f_tell(FIL* fp);
//函数原型定义
#define f_tell(fp) ((fp)->fptr)
  • 无论配置如何,本函数始终可用
f_size 文件大小查询函数
/**
  * @brief 此函数事实上为宏函数,用于查看当前文件大小
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 返回当前文件对象指向文件内容占用的空间(byte)
  */
FSIZE_t f_size(FIL* fp);
//函数原型定义
#define f_size(fp) ((fp)->obj.objsize)
  • 无论配置如何,本函数始终可用
f_eof 判断是否到达文件末尾函数
/**
  * @brief 此函数事实上为宏函数,用于查看读写指针是否到达文件末尾
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 如果到达文件末尾则返回一个非0整数,否则返回0
  */
int f_eof(FIL* fp);
//函数原型定义
#define f_eof(fp) ((int)((fp)->fptr==(fp)->fsize))
  • 无论配置如何,本函数始终可用
f_error 判断文件操作是否发生错误
/**
  * @brief 此函数事实上为宏函数,用于查看历史操作中有无错误信息
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 如果发生错误则返回一个非0整数,否则返回0
  */
int f_error(FIL* fp);
//函数原型定义
#define f_error(fp) ((fp)->err)
  • 无论配置如何,本函数始终可用
f_truncate 文件截断函数
/**
  * @brief 本函数用于将函数在当前指针位置截断
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_truncate(FIL* fp);
  • 本函数将已经打开的文件在当前文件指针处截断,如果文件指针已经到达文件末尾,那么这个动作将毫无用处,不会做出任何改变
  • 系统设置为可写(FF_FS_READONLY==0)并且FF_FS_MINIMIZE==0时本函数可用
f_sync 缓存同步函数
/**
  * @brief 本函数用于将目前暂存在RAM中的改变写入到介质中
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_sync(FIL* fp);
  • 本函数除了关闭文件对象之外和f_close执行完全相同的动作。执行本函数可以将上次sync到现在为止所有的改变写入介质,保证了这部分数据不会因为介质掉电等原因丢失
  • 在f_close之前紧邻调用本函数是没有意义的,因为f_close函数本身就会调用f_sync
  • 当系统设置为可写(FF_FS_READONLY==0)时函数可用
f_forward 文件流转发函数
/**
  * @brief 本函数用于将已经打开的函数中的部分内容输入到使用数据流的设备中
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param func是一个UINT(const BYTE*, UINT)类型的函数指针,用于接受文件的数据流
  * @param btf是一个无符号整数,代表要定向数据流的字节数
  * @param bf是一个无符号整数指针(UINT*),用于粗放实际进行重定向的字节数目
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_forward(FIL* fp, UINT (*func)(const BYTE*, UINT), UINT btf, UINT* bf);
  • 当系统设置FF_USE_FORWARE==1时函数可用
  • 本函数适用于RAM较小的系统,因为在这种应用场景下它极大的节省了内存空间,不需要设置较大的数据缓冲区,在函数内部完成所有过程
  • 如果*bt==btf并且返回FR_OK证明动作正常完成;如果*bf<btf但是没有出现错误证明由于文件读取结束或者在数据传输期间func指向的Stream处于忙碌状态无法完成传输。
  • func的返回值相当于一个bool变量,代表着目前Stream是否可用BYTE*参数代表Stream数据来源指针,而UNIT参数代表数据流大小
f_expand 文件空间分配函数
/**
  * @brief 本函数用于为已打开的文件对象扩展连续的存储空间
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param fsz是一个文件大小类型(FSIZE_t)的变量,代表着需要为文件扩展的自己数目
  * @param opt是一个标志位,0代表准备分配,1代表马上分配
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_expand(FIL* fp, FSIZE_t fsz, BYTE opt);
  • 本函数用于为文件分配一个在物理介质上连续的存储空间,当opt==1时在函数的执行过过程中就会给文件分配存储空间;当opt==0时只是找到一个在介质中连续的数据区域,并且将其设定为下次分配的建议点(最后的Cluster分配从这片区域的顶部开始)
  • 使用本函数前需要保证以下动作提前完成
    • 必须将文件截断,并且保证截断后的文件大小为0,也就是读写指针为0
    • 如果想要保留文件原有的内容则必须预先将内容记录下来,expand后重新写入
  • 本函数分配的空间中存储数据的内容是完全随机的,由于以下原因可能返回FR_DENIED失败
    • 文件大小不为0
    • 文件以只读方式打开
    • FAT Volume中试图分配>=4GB的空间
  • 本函数相比f_lseek分配的是连续的空间,相比于碎片化的存储方式拥有更高效的读写性能
  • 本函数在FF_USE_EXPAND==1并且系统可写(FF_FS_READONLY==0)可用
  • 如果我们要检验是否真的分配了连续的存储空间,我们可以使用以下函数:
/**
  * @brief 本函数用于检测已打开的文件对象是否拥有连续存储空间
  * @param fp是一个文件类型(FIL)的指针,指向一个已经打开的文件对象
  * @param cont代表是否连续,如果是1代表连续,如果是0代表不连续或者大小为0
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_contiguous(FIL* fp, int* cont){
    DWORD clst, clsz, step;
    FSIZE_t fsz;
    FRESULT fr;

    *cont = 0;
    fr = f_rewind(fp);              //将文件指针移动到文件头
    if (fr != FR_OK) return fr;

//确认Cluster占据的byte数目并且写入到clsz
#if FF_MAX_SS == FF_MIN_SS
    clsz = (DWORD)fp->obj.fs->csize * FF_MAX_SS;    
#else
    clsz = (DWORD)fp->obj.fs->csize * fp->obj.fs->ssize;
#endif

//获取文件大小,如果大小为0不做任何检测
    fsz = f_size(fp);
    if (fsz > 0) {
        clst = fp->obj.sclust - 1;  //获取文件目前第一个cluster-1
        while (fsz) {
            //分段检查,如果当前文件大小满足一个Cluster或以上,段长设为cluster大小,否则设定为剩余大小
            step = (fsz >= clsz) ? clsz : (DWORD)fsz;
            fr = f_lseek(fp, f_tell(fp) + step);    //将文件读写指针向前移动一段
            if (fr != FR_OK) return fr;
            if (clst + 1 != fp->clust) break;       //判断当前操作位置cluster序号是否和clst一致
            clst = fp->clust; fsz -= step;          //为下一次循环准备
        }
        if (fsz == 0) *cont = 1;    //通过所有测试后,就说明连续
    }

    return FR_OK;
}

1.2 目录访问接口

f_opendir 目录打开函数
/**
  * @brief 本函数用于打开一个目录
  * @param dp是一个目录(DIR)的指针,函数执行成功后会将其指向一个目录对象
  * @param path是一个字符串(const TCHAR*),代表需要打开的目录的绝对路径
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_opendir(DIR* dp, const TCHAR* path);
  • 本函数打开一个已经存在的目录,并且为dp指向的对象初始化,以备使用
  • 本函数在FF_FS_MINIMIZE<=1可用
  • 以下条目和表格探讨了FATFS Module中使用路径和文件名的规范
    • 路径名必须符合这样的格式:[驱动器号:][/]目录名/文件名
    • FATFS Module支持LFN和8.3格式的SFN。如果使用LFN需要保证FF_USE_LFN>=1
    • 目录的分隔符可以使用“\”或者”/”,注意使用反斜杠在代码中要使用转义字符;连续使用的分隔符例如”//”以及出现在字符串末尾的分隔符将被忽略
    • 与Windows系统不同,FAT Volume中使用的驱动器号使用阿拉伯数字例如”2:/”,如果缺省驱动器号,FAT Volume将自动调用第一个驱动器作为默认选项。
      • 如果FF_STR_VOLUME_ID==1那么驱动器号可以使用该Volume的卷标替代,例如假设驱动器0的卷标为FLASH,那么0:/file.txt和FLASH:/file.txt是相同的
      • 如果使用卷标代替驱动器号时,卷标不存在那么函数返回FR_INVALID_DRIVE
      • 如果FF_STR_VOLUME_ID==2那么可以使用类UNIX的目录格式,FLASH:/file.txt可以被写做”/FLASH/file.txt”,但需要注意的是在卷标层次使用”..”目录是无效的
    • 字符\0和\x1F都被认为是路径字符串的结束符
    • 在LFN中,path字符串中的空格字符是有效的,但是字符串末尾的空格或者”.”会被忽略;而如果Module中使用的是SFN,那么空格将会被视为path字符串的结束标志
    • 如果配置为exFAT文件系统,”..”在官方提供的代码中不起作用,和”.”是相同的
    • 以下表格讨论了FF_FS_RPATH宏定义对于系统的影响。在这个宏定义==0时系统不认为系统存在目录嵌套的层次结构,每个文件或者目录都必须使用绝对路径,点目录”.”和”..”不能使用,这种path将会成为非法路径;反之宏定义==1时允许使用相对路径和点目录,其中当前目录的概念通过f_chdir函数确定,当前驱动器通过f_chdrive函数确定。
Path字符串FF_FS_RPATH==0FF_FS_RPATH==1
file.txt驱动器0根目录下的文件file.txt当前驱动器与当前目录下的文件file.txt
/file.txt驱动器0根目录下的文件file.tst当前驱动器下根目录下的文件file.txt
驱动器0的根目录当前驱动器的当前目录
/驱动器0的根目录当前驱动器的根目录
2:驱动器2的根目录驱动器2的当前目录
2:/驱动器2的根目录驱动器2的根目录
2:file.txt驱动器2根目录下的file.txt驱动器2的当前目录下的file.txt
../file.txt非法字符串当前目录父目录下的文件file.txt
.非法字符串当前目录
..非法字符串当前目录的父目录
dir/..非法字符串当前目录(dir是当前目录的子目录)
/..非法字符串顶层根目录
f_closedir 目录关闭函数
/**
  * @brief 本函数用于关闭一个已经打开一个目录
  * @param dp是一个目录(DIR)的指针,指向一个已经打开的目录
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_closedir(DIR* dp);
  • 本函数在FF_FS_MINIMIZE<=1可用
  • 一般而言,一个已经打开的目录一定需要关闭;如果FF_FS_LOCK选项没有开启,也可以不关闭目录,但是这样的做法并不推荐,这是一种冒险的做法。
f_readdir 目录读取函数
/**
  * @brief 本函数用于读取目录中的文件信息
  * @param dp是一个目录(DIR)的指针,指向一个已经打开的目录
  * @param fno是一个文件信息指针(FILINFO*),存放读出的文件信息,如果指针为空,将目录读写指针回到目录开头
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_readdir(DIR* dp, FILINFO* info);

//定义宏函数用于读写指针回到目录开头
#define f_rewinddir(dp) f_read((dp),0)
  • 本函数用于从打开的目录中读取Directory Entry的信息。在这里也有一个读写指针,目录中的多个项目可以通过多次读取顺序访问——指针会自动迭代叠加
  • 当整个目录都已经读取完毕或者根本没有可读项目时,fno->fname[]会被设置为一个空字符串
  • 空指针传参给fno时,目录读写指针将会回退到目录开头
  • 如果系统开启LFN功能,那么fno中将会把LFN存储在fname[]中,SFN存储在altname[]中。发生以下任何状况无法访问LFN,fname将自动获取为SFN,altname将变成空字符串:
    • 该系统没有LFN(exFAT肯定不是这个原因,exFAT没有LFN和SFN区别)
    • FF_MAX_LFN大小不足LFN的长度(FF_MAX_LFN==255时一定不是这个原因)
    • FF_LFN_BUF大小无法临时存储读出的LFN长度
    • LFN包含当前Code Page中未定义的字符(FF_LFN_UNICODE!=0时一定不是这个原因)
    • 如果读取exFAT时出现问题,因为exFAT不支持SFN,所以fname存放的则是”?”作为文件名表示该对象无法访问,可以满足以上提到的”一定不是”的条件以保证LFN完全支持
  • 无论文件系统是什么,都不会读出”.”和”..”这样就保证了exFAT和FATFS的兼容性
  • 本函数在FF_FS_MINIMIZE<=1可用
f_findfirst 目录首次搜索函数
/**
  * @brief 本函数用于搜索目录中第一个符合条件的对象
  * @param dp是一个目录(DIR)的指针,供函数调用
  * @param fno是一个文件信息指针(FILINFO*),存放搜索结果的文件信息
  * @param path是一个字符串(const TCHAR*),作为被搜索的路径输入到函数
  * @param pattern是一个字符串(const TCHAR*),作为匹配搜索结果的字符串输入到函数
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern);
  • 搜索范围是path指定的目录,搜索只在本目录进行,不做递归动作
  • 如果匹配到成功项目,只搜索第一个,并且将该文件信息存放到fno之中,如果没找到那么将会把fno-fname[]设置为空字符串
  • pattern是搜索匹配字符串,匹配目录中的每一个文件的文件名,可以包含通配符
    • ?表示任意字符,例如???代表长度为3的任意字符串
    • *表示任何字符或者字符串,例如所有jpg文件就是*.jpg,所有四个或者更长的字符串???*
    • FATFS Module中的通配符数量限制为4个以保证Stack Memory的使用(匹配算法本身递归)
    • 使用LFN功能时,FF_USE_FIND==1只搜索fname[],如果FF_USE_FIND==2也匹配altname[]
    • 使用LFN功能并且FF_LFN_UNICODE!=0时,DBCS扩展字符区分大小写
  • 本函数实际上调用了f_opendir和f_readdir这两个函数
  • FF_USE_FIND>=1FF_FS_MINIMIZE<=1时函数可用
f_findnext 目录顺序搜索函数
/**
  * @brief 本函数用于在已经调用过f_findfirst的情况下搜索目录中下一个符合条件的对象
  * @param dp是一个目录(DIR)的指针,必须是上次f_findfirst执行时同一个dp,而且不能更改数据
  * @param fno是一个文件信息指针(FILINFO*),存放搜索结果的文件信息
  * @retval 函数返回一个FRESULT类型的文件操作代码
  */
FRESULT f_findnext(DIR* dp, FILINFO* fno);
  • 为了保证读写指针的移动连续性,本函数调用前必须先调用f_findfirst,同样用于f_findfirst的dp指针需要传参到本函数中,并且过程中dp不能更改!
  • 本函数搜索结果和其他注意事项全部和f_findfirst相同

1.3 文件与目录管理接口

f_stat 信息查询函数
/**
  * @brief 本函数用于查询指定的文件或者文件夹的信息
  * @param path是一个字符串(const TCHAR*),代表需要查询的目录和文件的路径
  * @param fno是一个文件信息(FILINFO)指针,指向一个空白结构体,函数执行后存放查询到的信息
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_stat(const TCHAR* path, FILINFO* fno);
  • 如果该文件或者目录存在,返回FR_OK,fno存储目标信息;如果文件或者目录不存在,fno作为空指针被返回,但是要判定文件真的不存在,应当检测返回值是否为FR_NO_FILE
  • 这个文件信息实际上来自于介质上Entry中的元数据,所以如果文件或者目录已被修改那么在执行此函数前需要f_sync同步或者f_close(f_closedir)关闭文件
  • FF_FS_MINIMIZE==0时该函数可用
f_unlink 删除函数
/**
  * @brief 本函数用于删除文件或者子目录
  * @param path是一个字符串(const TCHAR*),代表需要删除的目录和文件的路径
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_unlink(const TCHAR* path);
  • 当需要被删除的对象无论是文件还是目录触碰到以下条件是,动作将被拒绝,返回FR_DENIED
    • 文件或者目录不能含有只读属性(AM_RDO)
    • 如果删除一个目录,那么该目录不能是当前目录,且目录必须为空,该动作不能够递归
    • 文件或者目录不能处于打开状态,否则严重时可能造成FAT Volume崩溃。但是在正常状况下FATFS不提供判断接口,要保证没有冲突问题需要启动文件锁定功能
  • 当系统可写(FF_FS_READONLY==0)并且FF_FS_MINIMIZE==0时函数可用
f_rename 重命名函数
/**
  * @brief 本函数用于重命名文件或者目录
  * @param old_name是一个字符串(const TCHAR*),代表旧的文件或者目录的路径
  * @param new_name是一个字符串(const TCHAR*),代表新的文件或者目录的路径
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_rename(const TCHAR* old_name, const TCHAR* new_name);
  • old_name指向的文件或者目录必须存在,否则函数执行不成功;new_name指向的位置不能存在任何文件或者目录,否则返回FR_EXIST不成功,并且FAT Volume有可能崩溃,这样的危险操作可以通过打开文件锁定功能避免
  • 系统可写(FF_FS_READONLY==0)并且FF_FS_MINIMIZE==0时函数可用
f_chmod 属性修改函数
/**
  * @brief 本函数用于更改目录或者文件的属性
  * @param path是一个字符串(const TCHAR*),代表需要更改属性的文件或者目录的路径
  * @param attr是一个标志位组合(BYTE)
  * @param mask是一个标为组合,代表了哪些bit代表的标志位需要更改,剩余的标志位不变
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_chmod(const TCHAR* path, BYTE attr, BYTE mask);
  • 标志位主要包括(只读)AM_RDO,(归档)AM_ARC,(系统)AM_SYS,(隐藏)AM_HID
  • mask主要用于标定哪些标志位需要被更改:
    • 如果在mask中出现attr中也出现,标志位将被设定为1
    • 如果在mask中出现attr中没出现,标志位将被设定为0
    • 例如f_chmod(“/etc/file.txt”,AM_RDO,AM_RDO|AM_HID)表示将文件设定为只读但不隐藏
  • 当系统可写(FF_FS_READONLY==0)并且FF_USE_CHMOD==1时该函数可用
f_utime 时间戳修改函数
/**
  * @brief 本函数用于改变文件或者目录的时间戳
  * @param path是一个字符串(const TCHAR*),代表需要更改属性的文件或者目录的路径
  * @param fno是一个文件信息类型(FILINFO)的指针,fno内只应当含有最后修改的时间日期,也就是fdate和ftime
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_utime(const TCHAR* path, const FILINFO* fno);
  • 系统可写(FF_FS_READONLY==0)并且FF_USE_CHMOD==1时该函数可用
  • fno内不应当有其他的参数,仅应当有关于时间戳的内容,以下是一个封装的例子
/**
  * @brief 本函数二次封装f_utime转换为设置年份+月份+日期+小时+分钟+秒钟
  * @param path是一个字符串(const TCHAR*),代表需要更改属性的文件或者目录的路径
  * @param year,整数int,年份
  * @param month,整数int,月份
  * @param day,整数int,日期
  * @param hour,整数int,小时
  * @param min,整数int,分钟
  * @param sec,整数int,秒钟
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT setDateTime(const TCHAR* path, int year, int month, int day, int hour, int min, int sec){
    FILINFO fno;
    //具体的转换公式查看FATFS详解中RTC部分的字段表
    fno.fdate = (WORD)((year-1980) << 9 | month << 5 | day);
    fno.ftime = (WORD)(hour << 11 | min << 5 | sec >> 1);
    return f_utime(path, &fno);
}
f_mkdir 目录创建函数
/**
  * @brief 本函数用于创建一个新的子目录
  * @param path是一个字符串(const TCHAR*),代表新创造的目录的路径
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_mkdir(const TCHAR* path);
  • 当系统可写(FF_FS_READONLY==0)并且FF_FS_MINIMIZE==0时函数可用
f_chdir 目录跳转函数
/**
  * @brief 本函数用于变更当前目录
  * @param path是一个字符串(const TCHAR*),代表跳转道的目标目录
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_chdir(const TCHAR* path);
  • 本函数在FF_STR_VOLUME_ID<2时不能直接变更驱动器,只能在当前驱动器内变更目录;当宏定义FF_STR_VOLUME_ID==2时使用类Unix样式的path可以直接变更驱动器
  • 注意:“当前目录”保存在每个文件系统的对象之中,而当前驱动器号码作为静态变量存在。故而改变这个部分的变量也会影响其他函数操作文件系统的动作
  • FF_FS_PATH>=1时本函数可用
f_chdrive 驱动器跳转函数
/**
  * @brief 本函数用于变更当前驱动器
  * @param path是一个字符串(const TCHAR*),代表需要跳转到的驱动器号
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_chdrive(const TCHAR* path);
  • 驱动器号默认设置为0,path可以是驱动器号也可以是驱动器卷标(需要卷标功能支持)
  • 当使用类Unix的文件路径时,不需要使用本函数,f_chdir即可完成驱动器切换
  • 注意事项参考f_chdir的注意事项
  • FF_FS_PATH>=1时本函数可用
f_getcwd 当前目录获取函数
/**
  * @brief 本函数用于查询当前目录
  * @param buff是一个字符串(const TCHAR*),指向用于接收函数结果的缓冲区
  * @param len是一个无符号整数(UINT),代表了buff的大小,以TCHAR为单位
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_getcwd(TCHAR* buff, UINT len);
  • 本函数返回当前操作点所处的目录,类似于Linux系统中的pwd指令
  • FF_VOLUMES>=2时,返回字符串中会包含有驱动器前缀,格式依赖于FF_STR_VOLUME_ID,也就是说当宏定义允许使用卷标时,前缀中的驱动器号会被设置为卷标
  • 在官方提供的FATFS Module之中,此函数无法在exFAT使用,始终返回根目录路径

1.4 系统配置与管理接口

f_getfree 可用空间计算函数
/**
  * @brief 本函数用于统计Volume上的空余Cluster数量
  * @param path是一个字符串(const TCHAR*),表示需要进行统计的逻辑驱动器,空字符串则代表默认驱动器
  * @param nclst是一个双字(DWORD)类型的指针,用于存放读出的空闲Cluster数量
  * @param fatfs是一个文件系统的二次指针(fatfs**),用于存放相关的文件系统对象
FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs);
  • 由于FATFS对象中的csize成员指示了每Cluster中的Sector数量,因此可以用此信息来二次计算以Sector为单位的可用空间
  • 如果FAT32 Volume上的FSInfo结构没有正常同步,此函数可能统计不正常。为了避免这种错误的发生,可以使能FF_FS_NOFSINFO选项来强制完整扫描FAT,但是可能花费很长时间。
  • 当系统可写(FF_FS_READONLY==0)并且FF_FS_MINIMIZE==0时函数可用
f_mount 文件系统挂载函数
/**
  * @brief 本函数用于挂载已存在的FATFS,fs为空则表示解挂
  * @param fs是一个文件系统类型(FATFS)的指针,为待挂载文件系统
  * @param path是一个字符串(const TCHAR*),为挂载的驱动器号
  * @param opt是一个标志变量(BYTE),0表示等待第一次动作时挂载,1表示强制挂载并且检测是否READY
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt);

FRESULT f_unmount(const TCHAR* path);
//定义宏函数用于解挂文件系统
#define f_unmount(path) fmount(0,(path),0)
  • 本函数始终可用
  • 除了一些Volume管理函数例如f_mkfs, f_fdisk, f_setcp之外,其他动作都需要先挂载系统
  • f_mount挂载/解挂文件系统的过程包括四个部分:
    • 确认path指定的逻辑驱动器存在
    • 如果Volume已有挂载工作区,那么清除旧数据并且注销工作区
    • 如果fs不为空,清除动作完成后将会重新挂载
    • 如果opt==1那么立即强制执行挂载动作
  • 如果opt==0,无论物理介质如何,此函数始终执行成功只清除旧数据,新的挂载动作等待下一次文件动作时延迟执行。
  • 挂载动作可以分解为:初始化物理驱动器->查找其中的FAT Volume->初始化工作区。在以下条件任意满足时执行上述三个动作:
    • 文件系统对象(FATFS)尚未初始化,并且通过f_mount初始化
    • 物理驱动器尚未初始化,原因是物理介质被移除或者系统重启
  • 如果opt==1并且动作失败则返回FR_NOT_READY;这代表着FATFS已经成功注册,但是物理接口仍未处于READY状态,后续的文件操作将会不断尝试挂载过程
  • FATFS Module没有异步检测(例如中断)物理介质更改——但是每次更嘎后需要执行f_mount保证文件系统数据正常和物理介质的存储安全
f_getlabel 查询卷标函数
/**
  * @brief 本函数用于查询目标Volume的卷标和序列号
  * @param path是一个字符串(const TCHAR*),指向目标驱动器,如果path为空则设定为默认驱动器
  * @param label是一个字符串(const TCHAR*),用于存放查询到的卷标,如果不需要可以设置为空指针
  * @param vsn是一个双字节指针(DWORD*),用于存放查询到的序列号,如果不需要可以设置为空指针
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_getlabel(const TCHAR* path, TCHAR* label, DWORD* vsn);
  • FF_USE_LABEL==1时函数可用
  • 如果当前Volume没有卷标,一个空字符串将会存储到label之中。这个字符串的大小如下表:
系统配置FF_FS_EXFAT==0FF_FS_EXFAT==1
FF_USE_LFN==012错误配置
FF_LFN_UNICODE==01223
FF_LFN_UNICODE==1,31212
FF_LFN_UNICODE==23434
f_setlabel 设置卷标函数
/**
  * @brief 本函数用于设置默认Volume的卷标
  * @param label是一个字符串(const TCHAR*),用于存放需要设置的卷标
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_setlabel(const TCHAR* label);
  • 设置卷标使用一个非空字符串;清除卷标使用空字符串
  • 当字符串具有驱动器前缀时,指定驱动器的卷标会设定为字符串内容;如果字符串没有指定驱动器号,那么使用默认驱动器设置卷标
  • 卷标的设置格式要求为:
    • 在使用OME Code Page转换的FAT Volume上,最长可达11Byte
    • FAT Volume上可以使用SFN字符,不包括”.”,并且做强制大小转换
    • 在exFAT Volume上,最长可达11Byte
    • exFAT Volume上可以用LFN字符,包括”.”,不做强制大小转换
    • 卷标字符串中可以使用空格,但是不能够出现在末尾,末尾空格会被忽略
    • 无论何种状况,开头不允许使用0xE5字符
f_setcp 转换码表函数
/**
  * @brief 本函数用于设置系统使用的码表
  * @param cp是一个16bit的代号
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_setcp(WORD cp);
  • 本函数在FF_CODE_PAGE==0时可用
  • FF_STRF_ENCODE==0并且FF_LFN_UNICODE>=1时所有字符串中的字符转换都会被影响,而文件操作中的路径字符串总是受影响
  • 本函数应当在系统初始化后未作出任何文件动作时使用且后续不应当更改
  • 码表(Code Page)和cp对应代码如下所示:
CP码表字符语言cp码表字符语言
0无扩展437U.S.
720Arabic737Greek
771KBL775Baltic
850Latin1852Latin2
855Cyrillic857Turkish
860Portuguese861Icelandic
862Hebrew863CanadianFrench
864Arabic2865Nordic
866Russian869Greek2
932Japanese936Chinese_Sp
949Korean950Chinese_Td
f_fdisk 介质分区函数
/**
  * @brief 本函数用于格式化物理驱动器
  * @param pdrv是一个字节长度(BYTE)的标记,代表物理驱动器号码
  * @param ptbl是一个LBA_t类型的数组,记录了各个分区的大小
  * @param work是一个指针(void*)指向函数所用的工作区
  * @retval 返回一个FRESULT类型结果代码
  */
FRESULT f_fdisk(BYTE pdrv, const LBA_t ptbl[], void* mark);
  • work指向的工作区大小起码要超过FF_MAX_SS也就是一个Sector的大小。当给定一个空指针并且FF_USE_LFN==3的时候,函数将会直接申请一个内存区块用于分区过程中使用
  • 本函数在系统可写(FF_FS_READONLY==0)并且(FF_USE_MKFS==1)(FF_MULTI_PATITION==1)三个条件共同满足时才可用
  • 本函数创建的分区可以是MBR也可以是GPT,当LBA_t设置为64bit并且物理驱动器存储空间的大小>=FF_MIN_GPT时才可以使用GPT分区表。前者最多能够创建4个主分区,后者则能有10+
  • ptbl[]是分区大小的列表,如果元素<=100则理解为本分区占据存储空间相对于物理介质的百分比,反之>100则代表分区使用的Sector数量。分区表以0结束,如果分区过程中空间不足或者超过MBR能够容纳的分区数量,那么直接截断最后一个分区
f_mkfs 格式化函数
/**
  * @brief 本函数用于格式化一个逻辑驱动器
  * @param path是一个字符串(const TCHAR*),代表需要格式化的驱动器
  * @param opt是一个格式化参数(MKFS_PARM)类型的指针,写入格式化选项,空指针使用默认参数
  * @param work是一个指针(void*),指向工作区缓存
  * @param len是一个无符号整数(UINT),代表工作区缓存大小
  * @retval 返回一个FRESULT类型结果代码
  */
FRESUTLT f_mkfs(const TCHAR* path, const MKFS_PARM* opt, void* work, UINT len);
  • 系统可写(FF_FS_READONLY==0)并且FF_USE_MKFS==1时函数可用
  • 结构体(MKFS_PARM)类型参数opt中各个成员的设置说明:
    • [BYTE]opt->fmt:指定FAT类型的标志字段,可以设置为FM_FAT,FM_FAT32,FM_EXFAT三种或者是他们的位或运算,也就是FM_ANY。如果系统的exFAT功能没开启那么exFAT的选项不生效。如果有多种选择可用,则根据空间大小和下面的au_size进行选择,默认值为组合选项FM_ANY。如果设置了标志位FM_SFD,那么指定在SFD分区格式的驱动器上格式化。
    • [BYTE]opt->n_fat:制定了FAT表的个数可以是1或者2,默认值(0)和其他的值都会被设置为1个FAT表。如果格式化类型是exFAT,这个选项无效。
    • [UINT]opt->n_align:以Sector为单位指定Data Aera的对齐值,此成员有效值为2的整数幂,如果给定0(默认值)或者任何无效值,那么将调用disk_ioctl确认这个值。
    • [DWORD]opt->au_size:以Byte为单位指定文件分配单元(Cluster)的大小,对于普通的FATFS有效值为1~128个Sector之间2的整数幂;对于exFAT则最大可以到达16MB。如果给定0,也就是默认值,或者其他任何无效值,系统将根据Volume的大小自动选择。
    • [UINT]opt->n_root:指定根目录的Entry数量,有效值最大为32768,并且需要与Sector中的字节数/32对齐。默认值为512,但是当格式为FAT32或者exFAT,这个字段无效。
  • 当待格式化的逻辑驱动器与物理驱动器相统一(FF_MULTI_PARTITION==0或者VolToPart[].pt==0)并且没有设置FM_SPD标志的时候,分区将会占满整个驱动器并且创建文件系统
  • 当指定了标志FM_SPD,将在不进行任何分区的情况下创建文件系统。
  • 当待格式化的逻辑驱动器与特定分区相关联(FF_MULTI_PARTITION==1并且VolToPart[].pt>0)的时候无论FM_SPD是否标记,都在指定分区中创建文件系统。
  • 如果在分区中创建文件系统,那么需要先使用f_fdisk或者其他方式先进行分区。如果执行此函数时驱动本身不存在,那么将返回FR_MKFS_ABORTED
  • 目前标准的分区格式有三种:MBR(f_disk)格式,GPT格式(需要LBA64bit),SFD。最后一种其实是一种无分区格式,FAT Volume直接写入到LBA0并且扩张到整个驱动器。

1.5 数据结构体与标志定义

FRESULT 文件返回代码

大多数的API接口函数返回值类型都是FRESULT,事实上这个类型是一个枚举体变量,以FR_OK=0开始并且依次递增。具体的错误代码和相关错误原因如下所示:

  • FR_OK(0)
    • 代表动作执行正常
  • FR_DISK_ERR
    • 底层函数:disk_read, disk_write, disk_ioctl等函数发生了无法恢复的硬件错误
    • 只要这个错误出现,正在打开的文件对象就被废弃,除了f_close之外的动作全部拒止
  • FR_INT_ERR
    • 断言错误,在内部流程中检测到异常状况,可能出现了以下错误:
      • 工作区(当前FIL/DIR/FILINFO/FATFS变量存储区域)由于爆栈等原因损坏
      • 当前文件系统中FAT表的结构发生了错误
      • 官方提供的FATFS Module代码自己的BUG问题
      • 底层驱动函数没有正确的暴露接口
    • 只要这个错误出现,正在打开的文件对象就被废弃,除了f_close之外的动作全部拒止
  • FR_NOT_READY
    • 底层驱动中的disk_initialize函数报告了物理介质无法正常工作状态的错误,可能因为:
      • 驱动器中没有存储介质
      • 底层驱动代码没有正确的暴露接口
      • 硬件配置错误
      • 存储介质本身产生了物理上的破损
  • FR_NO_FILE
    • 在指定目录中找不到指定文件
  • FR_NO_PATH
    • 按照给定的路径搜索不到对应的目录
  • FR_INVALID_NAME
    • 给出中的path字符串不符合FATFS的命名规范,可能因为:
      • 文件名中出现了非法字符
      • 在使用SFN的情况下,文件名不符合8.3格式
      • 在使用LFN的情况下,FF_MAX_LFN的长度不足以容纳文件名
      • 在字符串的字符编码过程中出现了错误
  • FR_DENIED
    • 用户通过接口提交的动作由于以下原因之一或者多个原因的组合被拒止:
      • f_open动作时设置了可写,但是系统或者文件设置为只读
      • f_unlink动作试图删除只读的文件或者目录
      • f_unlink动作试图删除一个不为空的目录
      • f_read动作试图读取一个没有标记读取(FA_READ)打开方式的文件
      • f_write/f_truncate/f_expand动作试图更改一个不带有可写(FA_WRITE)打开方式的文件
      • f_open/f_mkdir动作在创建新的文件或者目录时发现根目录已满或者介质已满
      • f_expand动作无法在当前卷中给文件分配一个连续空间
  • FR_EXIST
    • 命名冲突,在当前目录中出现了与操作对象同名的其他对象
  • FR_INVALID_OBJECT
    • 文件对象或者目录对象是无效取值或者给出的是一个空指针,可能因为:
      • 对象已经被关闭或者该结构体已经遭受了不可接受的数据污染
      • 对象已经失效,Volume上打开的对象在Volume的挂载过程中会被报废
      • 由于物理介质的移除,介质无法进入等待操作的READY状态
  • FR_WRITE_PROTECTED
    • 写入操作与带有写保护属性的介质产生了逻辑冲突
  • FR_INVALID_DRIVE
    • 无效的驱动器号在PATH中出现,或者是PATH中给出了空指针
  • FR_NOT_ENABLED
    • 逻辑驱动器的工作空间尚未通过f_mount函数挂载注册
  • FR_NO_FILESYSTEM
    • 驱动器中无法找到有效的FAT格式的Volume,可能是因为:
      • 驱动器上原有的FAT文件系统已经损坏
      • 底层驱动代码没有正确的暴露接口
      • 分区设置错误
  • FR_MKFS_ABORTED
    • f_mkfs在执行格式化动作之前被取消了,可能是因为:
      • 根据给定的条件,不可能创建合适的FAT格式Volume
      • 当前Volume的尺寸太小,FM_SFD选项至少要求128个Sector
      • 逻辑驱动器对应的分区在介质中不存在
  • FR_TIMEOUT
    • 在多线程-线程安全中,函数因为超时而动作失败
  • FR_LOCKED
    • 在开启了文件锁定功能的系统中,动作被锁定机制拒止
  • FR_NOT_ENOUGH_CORE
    • 内存耗尽:无法为LFN分配缓冲内存/或者给定的内存无法满足空间需求
  • FR_TOO_MANY_OPEN_FILES
    • 在开启了文件锁定功能的系统中,当前打开文件的数量已经达到设定的最大值
  • FR_INVALID_PARAMETER
    • 给定的参数无效,或者给定参数与Volumede真实状况发生了冲突
FIL 文件对象结构体
//FIL 是一个对于文件类型结构体的定义
typedef struct{
    FFOBJID obj; //文件对象的识别信息
    BYTE flag; //文件的状态标志位
    BYTE err;  //文件的错误标志位
    FSIZE_t fptr; //文件当前的读写指针坐标
                  //FSIZE_t根据文件系统可能是32bit或者64bit的无符号整数
    DWORD clust; //当前文件指针fptr所处的Cluster编号
                 //当fptr正处于Cluster的边界时,clust落后当前cluster一个编号,当fptr==0,此字段无效
#if !FF_FS_READONLY
    LBA_t sect; //当前文件指针fptr所处的Sector编号,如果fptr在Cluster边界上,此字段wuxiao
                //LBA_t根据文件系统可能是32bit或者64bit无符号整数,与FSIZE_t相同
    BYTE* dir_ptr; //指向该文件的Directory Entry
#endif
#if FF_USE_FASTSEEK
    DWORD* ctbl; //指向快速查找时的簇链接映射表
#endif
#if !FF_FS_TINY
    BYTE buff[FF_MAX_SS];
#endif
} FIL;
  • FIL结构对象保存着一个已经打开的文件的“状态”,通过f_open创建并且在f_close中销毁
  • 除了簇缓冲表(簇链接映射表)之外,不建议应用侧程序改动任何子成员,否则可能引起FAT崩溃
  • 在系统可写时,定义sect成员和dir_ptr成员
  • 在快速查找模式下,定义簇链接映射表(Cluster Link Map Table)
  • 非小系统模式下,该结构体定义一个Sector大小的缓冲空间,会占用内存
DIR 目录对象结构体
//DIR 是一个对于目录类型结构体的定义
typedef struct{
    FFOBJID obj; //文件对象的识别信息
    DWORD dptr; //目录读写指针位置
    DWORD clust; //当前dptr所处的cluster编号
    LBA_t sect; //当前dptr所处的sector编号
    BYTE* dir; //指向当前SFN Entry的目录条目指针
    BYTE* fn; //指向存放SFN缓冲区{file[8],ext[3],status[1]}的指针
#if FF_USE_LFN
    DWORD blk_ofs; //存放LFN条目的存储空间偏移值
    WCHAR* lfn; //指向LFN缓冲区的指针
#endif
#if FF_USE_FIND
    const TCHAR* pat; //指向存放搜索文件使用的匹配字符串
#endif
} DIR;
  • f_opendir, f_readdir, f_findfirst, f_findindex四个函数需要调用本结构体
  • 应用侧程序不应当直接修改结构体之中的任何成员,否则f_readdir函数可能无法正常动作
  • 如果开启了LFN功能,那么定义blk_ofs和lfn用于LFN缓冲区工作
  • 如果开启了查找功能,那么结构体中存放模式匹配字符串
FATFS 文件系统对象结构体
//FATFS 是FAT文件系统对象结构体的定义
typedef struct{
    BYTE fs_type; //代表文件系统类型{0,FS_FAT12,FS_FAT16,FS_FAT32,FS_EXFAT}
    BYTE pdrv; //代表Volume存放的物理介质的类型
    BYTE n_fats; //代表当前文件系统中FAT表的数目{1,2}
    BYTE wflag; //文件系统工作状态标志位
    BYTE fsi_flag; //FSInfo相关标志位
    WORD id; //卷挂载ID
    WORD n_rootdir; //对FAT12或者FAT16来说,根目录Entry的数量
    WORD csize; //每个Cluster含有的Sector的数量
#if FF_MAX_SS != FF_MIN_SS
    WORD ssize; //每个Sector含有Byte的数量{512,1024,2048,4096}
#endif
#if FF_FS_EXFAT
    BYTE* dirbuf; //exFAT系统中用于存放目录Entry Set的临时缓冲区
#endif
#if FF_FS_REENTRANT
    FF_SYNC_t sobj; //可重入系统中的同步对象结构体
#endif
#if !FF_FS_READONLY
    DWORD last_clust; //FSInfo中最后一个已分配的Cluster编号
    DWORD free_clust; //FSInfo中空余的Cluster数量
#endif
#if FF_FS_RPATH
    DWORD cdir; //当前目录占用的Cluster数量
#if FF_FS_EXFAT
    DWORD cdc_scl; //当前目录起始Cluster编号
    DWORD csc_size; //bit[31:8]代表目录的大小 bit[7:0]代表Cluster Chain状态
    DWORD cdc_ofs; //当前目录偏移的Cluster数量
#endif
#endif
    DWORD n_fatent; //FAT Entry的数量
    DWORD fsize; //FAT表占用的Sector数目
    LBA_t volbase; //卷对应LBA的基地址
    LBA_t fatbase; //FAT表对应LBA的地址
    LBA_t dirbase; //根目录对应LBA的地址
    LBA_t datbase; //数据区对应LBA的地址
    LBA_t winsect; //在win[]中出现的Sector的LBA地址
    BYTE win[FF_MAX+SS]; //磁盘对目录和FAT表以及文件的访问缓冲区
} FATFS;
  • 结构体中提到的win[]是用于存储FATFS系统的数据缓冲区。操作文件系统时缓存数据,减少对于存储设备的读写次数,从而提高性能
  • 结构体保存各个逻辑驱动器的动态工作区,应用侧创建,通过f_mount进行挂载或者注销
  • 应用测程序不应该修改结构体中任何成员,否则FAT Volume和有可能损坏崩溃
FILINFO 文件信息结构体
//FILINFO 是一个对于文件信息类型结构体的定义
typedef strcut{
    FSIZE_t fsize; //文件大小
    WORD fdate; //最后修改日期
    WORD ftime; //最后修改时间
    BYTE fattrib; //文件参数
#if FF_USE_LFN
    TCHAR altname[FF_SFN_BUF+1] //LFN模式下的SFN文件名
    TCHAR fname[FF_LFN_BUF+1] //LFN模式下的主要文件名(LFN文件名)
#else
    TCHAR fname[12+1]; //SFN模式下的文件名
#endif 
} FILINFO;
  • fsize代表文件占用的字节数,根据文件系统是FAT或者exFAT这个变量大小为DWORD(32bit)或者为QWORD(64bit),如果该文件信息指向的是一个目录那么这个字段不必关心
  • fdate和ftime参照前文提到的Directory Entry字段解释表
  • fattrib:即文件Entry中记录的文件属性。AM_RDO代表只读,AM_HID代表隐藏,AM_SYS代表系统文件,AM_DIR代表当前结构体指向的文件是一个目录,AM_ARCHIVE,代表归档——一般来说代表这个文件是新建或者正处于修改中的状态。

2. FATFS模块移植方法

2.1 FATFS模块调用关系与移植条件

FATFS模块本体是数个代码源文件,这些源文件包括了FATFS中的数据结构、动作函数实现、底层接口定义和通过宏定义配置的开关。开发者在嵌入式系统中使用本模块时需要满足以下支持条件:

  • 编译或者说交叉编译环境需要兼容至少ANSI C(C89)标准,如果使用exFAT需要支持C99标准
  • 对于整数类型的数据变量大小,必须满足:
    • char类型的长度为单字节也就是8bit
    • int类型的长度需要保证为双字节(16bit)或者四字节(32bit),推荐后者
    • int类型变种short和long需要保证长度为双字节(16bit)和四字节(32bit)
  • GNU C代码支持:
    • C89标准下编译需要string.h
    • C99标准下编译需要string.h和stdint.h
    • 如果系统尚有余力,可以提供stdarg.hmath.h的支持

FATFS模块在ff.h之中定义了文件系统运行过程中需要用到的整数类的数据类型,官方做出的定义其实是按照Win32 API(windef.h)照抄的,在一般的嵌入式系统上不是什么问题,但是为了保证不产生冲突与系统兼容性,建议检查以下定义能否在目标平台中使用:

  • BYTE:8bit无符号整数
  • DWORD:32bit无符号整数
  • UINT:usigned int
  • FSIZE_t:DWORD/QWORD选择
  • LBA_t:DWORD/QWORD选择
  • WORD:16bit无符号整数
  • QWORD:64bit无符号整数
  • WCHAR:16bit双字节字符
  • TCHAR:char/WCHAR/DWORD选择

FATFS模块实际上相当小巧简单(只是因为要使用在FLASH和RAM以KB计算的嵌入式系统上看起来像是一个巨大的模块),必须使用的文件实际上只有5个,可选用的文件有2个。文件之间互相调用和包含的方式如下所示,蓝色部分代表FATFS的核心代码:

  • ffconf.h:本header不含有任何功能性函数和数据定义,由各种宏定义开关组成,是整个模块的配置文件,具体的配置开关和对应的功能将在后续部分展示。
  • ff.h & ff.c:这一对header和source文件实现了1.1-1.5部分提到的所有文件系统可调用逻辑接口,是模块的核心代码,实现了FATFS的核心功能。
  • diskio.h:这个header定义了模块向底层调用的磁盘操作函数的接口格式,我们需要按照这个文件的说明和指示根据我们的物理驱动严格的编写接口函数。
  • diskio.c:这个source在途中被定义为mmc.c,实际上是一个和diskio.h配套的source,是唯一一个需要我们自己编写的文件,需要实现diskio.h中的所有函数原型。
  • ff_unicode.c:这个source在图中没有提到,如果系统不使用LFN和Unicode可以无视。本souce中实现了FATFS标准中对于LFN和Unicode的大部分内容。
  • ff_system.c:这个source在途中没有提到,如果系统不考虑对接OS兼容性和可重入性可以无视,本source中实现了FATFS在多线程环境下的操作函数。

可以看到除了在下一节中要着重说明的配置文件之外,移植过程最重要的步骤就是在diskio.c这个文件中实现模块必须的MAI(Memory Access Interface)函数,下表中展示了我们在不同的文件系统配置情况下需要实现的函数:

函数功能对应系统配置条件补充说明信息
disk_status
disk_initialize
disk_read
始终需要基础IO函数,最小实现
disk_write
get_fattime
diskioctl(CTRL_SYNC)
系统可写
FF_FS_READONLY==0
基础IO函数,最小实现
diskioctl(GET_SECTOR_COUNT)
diskioctl(GET_BLOCK_SIZE)
系统有格式化功能
FF_USE_MKFS==1
基础IO函数,最小实现
disk_ioctl(GET_SECTOR_SIZE)介质Sector大小有歧义
FF_MAX_SS!=FF_MIN_SS
基础IO函数,最小实现
disk_ioctl(CTRL_TRIM)系统开启擦除功能
FF_USE_TRIM==1
基础IO函数,最小实现
ff_uni2oem
ff_oem2uni
ff_wtoupper
系统开启LFN功能
FF_USE_LFN!=0
Unicode支持部分
ff_mutex_create
ff_mutex_delete
ff_mutex_take
ff_mutex_give
系统有多线程可重入需求
FF_FS_REENTRANT==1
OS级别功能需求
ff_mem_alloc
ff_mem_free
系统开启LFN的全部功能
FF_USE_LFN==3
OS级别功能需求

2.2 FATFS模块配置宏开关

基础函数功能开关
  • FF_FS_READONLY:配置系统是否为只读模式,以下函数在只读模式将不可用:
    • f_write f_sync f_unlink f_mkdir f_chmod f_rename f_truncate f_getfree
  • FF_FS_MINIMIZE:配置系统的最小化压缩程度,以下是最小化压缩等级对应功能删减:
    • Level[0]:所有的API函数都可以使用,完全不压缩功能
    • Level[1]:f_stat f_getfree f_unlink f_mkdir f_chmod f_utime f_truncate f_name被禁用
    • Level[2]:在1的基础上f_opendir f_readdir f_closedir将被禁用
    • Level[3]:在2的基础上f_lseek将被禁用
  • FF_USE_FIND:系统是否有查找功能(f_findfisrt f_findnext),要求系统压缩等级在2以下
  • FF_USE_MKFS:配置系统是否有格式化功能(f_mkfs)
  • FF_USE_FASTSEEK:配置系统是否为f_lseek f_read f_write开启快速寻址功能
  • FF_USE_EXPAND:配置系统是否有分配连续空间功能(f_expand)
  • FF_USE_CHMOD:配置系统是否能修改文件属性(f_chmod f_utime),要求系统可写
  • FF_USE_LABEL:是否开启卷标功能(f_getlabel f_setlabel)
  • FF_USE_FORWARD:是否开启数据流转发功能(f_forward)
  • FF_USE_STRFUNC:是否启用字符串功能(f_gets f_putc f_puts f_printf),如果打开这个功能那么模块在编译时就会使用stdarg.h,具体选项级别为:
    • Level[0]:不开启字符串处理功能
    • Level[1]:开启字符串功能,但是不带有\n与\r\n之间的转换
    • Level[2]:开启字符串功能,同时带有\n与\r\n之间的转换
  • FF_PRINT_LLI:开启此选项后在f_printf中可以输出long long类型的整数,需要C99标准编译
  • FF_PRINT_FLOAT:开启此选项后在f_printf中可以使用浮点数等输出功能,需要C99标准编译,并且在编译时使用math.h,具体选项级别为:
    • Level[0]:不接受任何浮点数参数
    • Level[1]:接受%f %e %E三种浮点处理参数
    • Level[2]:在2的基础上将小数点”.”转写为”,”
  • FF_STRF_ENCODE:当LFN使用Unicode(FF_LFN_UNICODE>=1)时开启本选项将会在字符串处理时加入编码为Unicode的步骤,具体选项指代如下:
    • Level[0]:字符编码为当前Code Page中的ANSI/OEM字符
    • Level[1]:字符按照UTF-16LE编码
    • Level[2]:字符按照UTF-16BE编码
    • Level[3]:字符按照UTF-8编码
路径与命名字符相关设置
  • FF_CODE_PAGE:取值与f_setcp中提到的码表相同,用于设置Code Page
  • FF_USE_LFN:系统是否开启LFN功能,需要在编译时包含ffunicode.c,使用此选项需要注意堆栈内存的数据安全,使用堆分配方式时会调用ff_memalloc ff_memfree,具体选项如下:
    • Level[0]:不开启LFN,只使用8.3的SFN功能
    • Level[1]:开启LFN功能,但使用全局静态存储,线程不安全
    • Level[2]:开启LFN功能,工作区存储在栈(STACK)内存中
    • Level[3]:开启LFN功能,工作区存储在堆(HEAP)内存之中
  • FF_MAX_LFN:定义了LFN的最大长度,对应一个buffer,有效值范围为12~255,单位为UTF16大小的字符,面向不同的文件系统有不同的buffer大小:
    • 传统FATFS:(FF_MAX_LFN+1) * 2字节
    • exFAT系统:(FF_MAX_LFN+44) / 15 * 32字节
  • FF_LFN_UNICODE:用于设置字符串编码,本字段优先度高于FF_CODE_PAGE,具体如下:
    • Level[0]:TCHAR为char类型,只使用CodePage中的ANSI/OEM字符
    • Level[1]:TCHAR为WCHAR类型,使用Unicode:UTF-16编码字符
    • Level[2]:TCHAR为char类型,使用Unicode:UTF-8编码字符
    • Level[3]:TCHAR为DWORD类型,使用Unicode:UTF-32编码字符
  • FF_LFN_BUF&FF_SFN_BUF:定义了FILINFO中读取文件名的buffer的大小,需要满足所用的编码方式对应的最大文件名的大小,标准如下:
    • ANSI/OEM-SBCS:LFN最大为255字符 SFN最大为12字符
    • ANSI/OEM-DBCS:LFN最大为510字符 SFN最大为12字符
    • Unicode-UTF16/32:LFN最大为255字符 SFN最大为12字符
    • Unicode-UTF8:LFN最大为765字符 SFN最大为34字符
  • FF_FS_RPATH:定义了文件系统中相对路径功能是否开启,具体如下:
    • Level[0]:不准许使用相对路径
    • Level[1]:准许使用相对路径,但是没有递归查询,支持f_chdir f_chdrive
    • Level[2]:准许使用相对路径,并且支持递归查询,相比于1还支持f_getcwd
分区与卷管理设置
  • FF_VOLUMES:制定了逻辑驱动器的数目最大为10
  • FF_STR_VOLUME_ID:用于切换卷标字符串的使用方式,主要改变文件名中对于驱动器号的修饰方法,无论如何设置,原生的数字驱动器号始终可用,具体如下:
    • Level[0]:仅使用数字驱动器号
    • Level[1]:支持<string>:/格式的卷标前缀
    • Level[2]:相比于1,增加了对于/<string>/类型的类Unix写法的支持
  • FF_VOLUME_STRS:本字段较为特殊,应当宏定义为一个字符串列表,代表着每个驱动器在修饰前缀时使用的字符串,列表长度应当为FF_VOLUMES。如果FF_STR_VOLUME_ID>=1并且此选项未定义,那么用户必须自定义一个字符串列表。
  • FF_MULTI_PARTITION:用于设置是否使用启用多个物理分区的功能。如果不开启,每个逻辑驱动器号仅与同一物理驱动器号绑定;开启收驱动器号将绑定到一个需要用户定义的分区表VolToPart之中列出的分区。如果开启多分区功能,f_fdisk函数将可用。
  • FF_MIN_SS&FF_MAX_SS:定义介质上最小Sector大小和最大Sector大小,一般而言这两个数值应当相同。如果二者不相同,将会开启可变Sector功能,用户也必须实现disk_ioctl函数中关于获取扇区大小(GET_SECTOR_SIZE)的功能。
  • FF_LBA64:用于开启GPT分区功能和LBA-64bit功能,仅在exFAT功能开启时有意义
  • FF_MIN_GPT:当上一个选项开启时,这个字段定义了一个大小,当f_mkfs和f_fdisk执行时发现扇区数量大于这个字段就会自动创建一个GPT分区
  • FF_TRIM:用于设置是否开启ATA_TRIM功能,也就是标记哪些数据已经被删除。如果开启这个功能必须在disk_ioctl函数中支持CTRL_TRIM功能。
系统级策略设置
  • FF_FS_TINY:标记文件系统是否为超微型系统。超微型系统将会下调FIL等结构体中各个缓冲区的大小,并且将所有的Secotr缓冲区使用共享的存储空间,而不是独立设置
  • FF_FS_EXFAT:设定了是否使用EXFAT文件系统,如果开启了EXFAT文件系统那么产生以下需求:
    • 设定FF_LFN_UNICODE>=1并且FF_MAX_LFN==255以使用全部功能
    • 必须使用C99标准进行编译,因为exFAT需要使用大量的64bit整型变量
  • FF_FS_NORTC:设定了系统是否无需RTC功能。如果没有禁用RTC那么系统在文件改变时将会自动生成时间戳,因此必须实现get_fattime功能;反之则使用一套通过宏定义预先设置好的时间刻度常数值:FF_NORTC_MON,FF_NORTC_YEAR,FF_NORTC_MDAY
  • FF_FS_NOFSINFO:针对于FAT32系统设置是否通过FSInfo判断可用空间bit[0]=1时f_getfree会在系统挂在后强制进行一次FAT扫描bit[1]=0时使用最后一次分配的cluster序号作为新分配空间的起点,具体的执行方式如下所示:
    • bit[0]=0 使用FSInfo中记录的空闲容量信息
    • bit[0]=1 不信任FSInfo信息,转而对FAT做完全扫描,耗时
    • bit[1]=0 使用FSInfo记录的最后一个clust而编号作为新分配的cluster
    • bit[1]=1 不信任FSInfo信息记录的Cluster状态,强制查询FAT
  • FF_FS_LOCK文件锁定功能,主要涉及多个文件同时开启和对文件操作的非法判定条件。当系统只读时,这个选项必须关闭。具体决策如下:
    • =0 不开启文件锁定功能,相关工作必须由应用侧代码完成
    • >0 开启文件锁定功能,非法操作会导致函数返回FR_LOCKED
  • FF_FS_REENTRANT:用于切换FATFS模块的可重入性(多线程安全)。对于不同Volume上的文件和目录始终具有线程安全性,同一个Volume中的文件和目录才具有线程安全风险。对Volume的管理动作f_mount f_mkfs f_fdisk始终是线程不安全的。启动此功能需要实现接口:ff_mutex_take, ff_mutex_give, ff_mutex_create, ff_mutex_delete
  • FF_FS_TIMEOUT:当系统考虑多线程问题时,本字段作为操作等待的最长时间
FATFS模块功能配置总结表

2.3 FATFS模块移植若干事项

在FATFS模块移植的过程中代码和FAT格式本身会引起一些使用方面的限制如下所示。并且上一小节中提到的不同配置会消耗不同的内存占用量和FLASH占用量,映射表也如下所示,注意这里列出的可能占用空间是不统计面向物理存储介质的底层驱动的[V表示使用逻辑启动器数量|F表示打开文件数量]

  • 模块支持的文件系统:FAT12,FAT16,FAT32(revision[0.0]),exFAT(revision[1.0])
  • 注意RAM对于打开文件数量的限制
  • 支持最多10个逻辑驱动器(Volume)
  • 支持Sector尺寸:512Byte,1024Byte,2048Byte,4096Byte
  • Volume最小尺寸:128个Sector[本条目存疑]
  • 32bit-LBA的最大Volume尺寸:2^32-1个Sector
  • 64bit-LBA的最大Volume尺寸:近乎无限,在嵌入式系统中几乎可以不考虑
  • Cluster最大尺寸:FAT中最大为128个Sector,exFAT中最大为16MB
运行平台ARM7-32bitARM7-ThumbCM3-Thumb2AVRH8/300HPIC24RL78V850ESSH-2ARX600IA-32
编译器GCCGCCGCCGCCCH38C30CC78K0RCA850SHCRXCMSC
.text空间(Def,读写)10.4K6.7K6.1K12.5K11.0K11.4K13.0K8.9K9.2K6.5K8.9K
.text空间(Min,读写)7.0K4.7K4.2K8.5K7.6K7.9K9.5K6.3K6.4K4.7K6.4K
.text空间(Def,只读)4.9K3.2K2.7K6.1K5.2K5.4K6.5K4.3K4.2K3.2K4.3K
.text空间(Min,只读)3.7K2.5K2.1K4.4K4.0K4.2K5.1K3.4K3.3K2.5K3.5K
.bss静态区4V+24V+24V+22V+24V+22V+22V+24V+24V+24V+24V+2
工作区(普通系统)564V+552F564V+552F564V+552F560V+546F560V+546F560V+546F560V+546F564V+552F564V+552F564V+552F564V+552F
工作区(小系统)564V+40F564V+40F564V+40F560V+34F560V+34F560V+34F560V+34F564V+40F564V+40F564V+40F564V+40F

FATFS对于标准规定中的LFN功能拥有较为完备的功能扩展,LFN和SFN在API级别上是通用的。默认情况下该功能禁用,要启用LFN需要设置FF_USE_LFN宏定义,并且使用ff_unicode.c模块。如果需要配合OS功能实现可重入性,那么要求FF_USE_LFN>=2。并且启动LFN功能模块根据使用的Code Page不同会增加不同的代码大小,具体如下:

  • 所有SBCS:增加3.3KB
  • 日文(932):增加62KB
  • 韩文(949):增加140K
  • 简体中文(936):增加177K
  • 繁体中文(950):增加111K
  • 包含所有Code Page:增加486K

在使用操作系统时——主要是考虑多线程的操作系统,需要考虑线程安全问题也就是所谓的可重入性问题。不同Volume的文件始终可重入,打开FF_FS_REENTRANT选项将能够保证同一Volume内的文件可重入。但是需要注意f_mount,f_fdisk,f_mkfs这样Volume级别的操作函数始终是不可重入的。FATFS模块不支持针对于同一文件的同时读写冲突,当文件的打开方法全部为只读模式时才准许同时多次进行打开。相关操作可以通过FF_FS_LOCK选项启用文件锁定控制功能保证安全

针对于Volume管理来说,每个逻辑驱动器都和同一驱动器号中的物理驱动相关联——物理驱动器上的FAT Volume在挂在过程中就会自动配置,驱动程序读取Boot Sector并写检查是否符合FAT格式。如果启用多分区功能FF_MULTI_PARTITION==1的情况下,每个单独的逻辑启动器都和VolToPart[]全局数组中指定的分区或者物理介质相关联举例如下:

PARITION VolToPart[FF_VOLUMES] = {
    {0, 1}, //代表Volume 0:/对应到物理介质0的第一个分区
    {0, 2}, //代表Volume 1:/对应到物理介质0的第二个分区
    {0, 3}, //代表Volume 2:/对应到物理介质0的第三个分区
    {1, 0}, //代表Volume 3:/对应到物理介质1的分区[通常是一个可移动介质,分区自动识别]
};
  • 如果一个物理介质中的挂载了多个Volume,必须在移出介质前卸载所有的Volume
  • 对VolToPart[]进行任何更改前应当卸载相应的Volume
  • MBR格式的物理介质上最多可以容纳四个逻辑分区,不建议分配更多分区

3. FATFS用户实现接口部分

相关变量类型与宏定义
//定义介质操作状态
typedef BYTE DSTATUS;
#define STA_NOINIT  0x01 //未能初始化介质
#define STA_NODISK  0x02 //介质已经移除
#define STA_PROTECT 0x04 //介质写入保护

//定义读写控制操作需求
#define CTRL_SYNC        0 //将缓存中的内容写入介质
#define GET_SECTOR_COUNT 1 //获取介质中的Sector数目
#define GET_SECTOR_SIZE  2 //获取介质中的Sector占用Byte数目
#define GET_BLOCK_SIZE   3 //获取介质擦除时的擦除单位大小
#define CTRL_TRIM        4 //设置介质中相关Sectors为空闲状态
//以下选项在FATFS模块中不提供开启接口,需要用户手动配置
#define CTRL_POWER       5 //获取或者设置介质供电状态
#define CTRL_LOCK        6 //为介质移除动作设置锁定或者解锁
#define CTRL_EJECT       7 //弹出介质
#define CTRL_FORMAT      8 //在介质上进行物理格式化

//定义介质操作结果枚举体
typedef enum{
    RES_OK = 0, //介质操作成功
    RES_ERROR,  //介质读写操作
    RES_WRPT,   //介质具有写入保护
    RES_NOTRDY, //介质当前处于不可操作状态
    RES_PARERR  //传入的介质操作参数错误
} DRESULT;
disk_initialize 介质初始化函数
/**
  * @brief 该函数用于初始化介质
  * @param pdrv是一个字节变量(BYTE),代表物理驱动器号
  * @retval DSTATUS介质状态代码
  */
DSTATUS disk_initialize(BYTE pdrv);
  • 函数开始执行前应当设置返回值为STA_NOINIT标志位,当介质初始化成功并且可以进行通用读写操作时清除返回值中的STA_NOINIT标志位
  • 此函数应当且仅由FATFS模块调用,应用侧程序不应调用此函数,需要重新初始化存储介质应当通过FATFS模块调用f_mount执行动作
disk_status 介质状态查询函数
/**
  * @brief 该函数用于查询当前驱动器状态
  * @param pdrv是一个字节变量(BYTE),代表物理驱动器号
  * @retval DSTATUS介质状态代码
  */
DSTATUS disk_status(BYTE pdrv);
  • STA_NODISK 状态代码表示驱动器中没有物理介质
  • STA_PROTECT 当STA_NODISK没有设置标志位时,此功能无效。此字段代表介质写保护
  • STA_NOINIT 状态代码代表介质没有初始化成功或者当前不可操作。前文提到的initialize函数执行成功时将其清除,但是必须异步统计(例如中断)任何介质状态更改并且将其放置到标志中,否则文件系统的挂载动作无法执行。如果系统介质并不支持检测,那么需要手动执行f_mount
disk_read 介质数据读取函数
/**
  * @brief 本函数用于从介质中读出一定sector的内容
  * @param pdrv是一个字节变量(BYTE),代表物理驱动器号
  * @param buff是一个字节指针(BYTE*),指向读出数据存放的缓冲区
  * @param sector是一个LBA编号(LBA_t),指向开始读取的sector编号
  * @param count是一个无符号整数(UINT),代表需要读取的sector数目
  * @retval DRESULT介质操作状态代码
  */
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count);
disk_write 介质数据写入函数
/**
  * @brief 本函数用于向介质中写入一定sector的内容
  * @param pdrv是一个字节变量(BYTE),代表物理驱动器号
  * @param buff是一个字节指针(const BYTE*),指向写入数据的缓冲区
  * @param sector是一个LBA编号(LBA_t),指向开始写入的sector编号
  * @param count是一个无符号整数(UINT),代表需要写入的sector数目
  * @retval DRESULT介质操作状态代码
  */
DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count);
  • 当系统配置为只读(FF_FS_READONLY==1),本函数不需要实现
  • 应用测程序不得调用此函数,否则Volume上的FAT结构可能会损坏
disk_ioctl 介质读写控制函数
/**
  * @brief 本函数用于对介质进行读写控制和信息查询
  * @param pdrv是一个字节变量(BYTE),代表物理驱动器号
  * @param cmd是一个字节变量(BYTE),代表读写控制类型代码
  * @param buff是一个指针(void*),指向参数或者数据的存储空间
  * @retval DRESULT介质操作状态代码
  */
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff);
  • 系统只读(FF_FS_READONLY==1)并且Sector大小确定(FF_MAX_SS==FF_MIN_SS)本函数无需实现
  • cmd主要操作(不包括对于ATA,扩展控制,MMC,SDC的代码类型)如下:
控制命令控制命令描述信息
CTRL_SYNC确认当前介质是否已经完成了写入操作。主要是在进行缓存同步动作是需要判断当前所有的缓冲提交动作(Buffer Commit)——也就是写入是否已经完全完成。
GET_SECTOR_COUNT获取当前介质中可用的Sector数目和(最大允许的LBA地址+1)的取值,计算完成后写入到buff指向的LBA_t类型的变量中。这种操作在系统执行卷管理动作f_mkfs和f_fdisk中被下发底层实现。
GET_SECTOR_SIZE获取当前介质中的Sector大小,写入到buff指向的WORD类型的变量之中,当Sector大小不确定时(FF_MAX_SS>FF_MIN_SS)需要执行操作。
GET_BLOCK_SIZE获取当前介质中擦除时单位擦除单元包括的Sector数量,计算完成后写入到buff指向的DWORD类型的变量中。当f_mkfs被调用时需要执行这个动作。
CTRL_TRIM通知介质驱动某一Sector中的数据不再有效,可以进行擦除。相关Sector的编号在buff指向的LBA_t数组{Start,End}中指定
get_fattime 时间戳获取函数
/**
  * @brief 本函数用于返回时间戳
  * @retval 编码后的时间戳
  */
DWORD get_fattime(void);

//以下是一个可能的实现
DWORD get_fattime(void){
    time_t t;
    struct tm *stm;
    
    t = time(0);
    stm = localtime(&t);

    return (DWORD)(stm->tm_year - 80) << 25|
           (DWORD)(stm->tm_mon + 1) << 21|
           (DWORD)stm->tm_mday << 16|
           (DWORD)stm->tm_hour << 11|
           (DWORD)stm->tm_min << 5|
           (DWORD)stm->tm_sec >> 1;
}
  • 本函数在系统只读(FF_FS_READONLY==1)或者不适用RTC(FF_FS_NORTC==1)不需要实现

4. 相关资源与链接



You May Also Like

About the Author: Fenice

本人及开发团队主要兴趣领域为:自动控制理论、网站开发、移动端开发、嵌入式系统、机器人相关项目、电力电子技术、电动机控制。以及,兼任北京海淀区北下关街道反卷委员会常务委员长并且获得“全年度中国最佳懒狗”称号。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注