起因

GD32 项目里要往 SD 卡记数据(日志、参数文件),单片机直接读写扇区只有“块”的概念,没有文件和目录,拔下来插电脑也读不回来——这层“文件”的抽象就是文件系统要做的事。FatFS 是为小型嵌入式写的 FAT 实现:纯 C、体积极小、不挑硬件,是单片机上事实标准的选择。这篇记录在 GD32 + FreeRTOS 上移植它的过程。

需求

  • FatFS 在 GD32 工程里跑通 f_open / f_read / f_write
  • FreeRTOS 多任务环境下的并发访问不破坏数据(线程安全)

技术实现原理

FatFS 的分层:文件系统与硬件之间只隔 6 个函数

上层 API(f_open、f_write)把“文件”翻译成“扇区读写 + FAT 表/目录项维护”,它完全不知道底层是什么存储器件;diskio.c 里的 6 个函数(状态、初始化、读、写、控制命令、取时间)是唯一的硬件边界。移植 FatFS = 把这 6 个函数接到 SD 卡驱动上,其余源码原样编译——这就是它“随处可移植”的结构来源,也是文档反复强调这 6 个函数的原因。

注意读写的单位是“块”:disk_read / disk_write 的单位是 sector(通常 512 字节),不是字节。FatFS 内部按簇/扇区管理空间,驱动层必须按块对齐地访问设备,字节级的偏移由上层换算好了再传下来。

无系统 / 带系统的取舍(线程安全的根因)

  • 不上 RTOS:只添加 ff.c 和 diskio.c,_FS_REENTRANT 保持 0,没有并发就没有竞争;
  • 上 FreeRTOS:多个任务同时对同一卷 f_write,FAT 表和目录项的更新是“读-改-写”过程,交错执行就会互相覆盖——这就是要打开 _FS_REENTRANT 的根因。打开后 FatFs 用互斥量保护同一卷的访问,代价是四个同步函数(ff_req_grant / ff_rel_grant / ff_cre_syncobj / ff_del_syncobj)必须自己接到 OS 的 API 上,syscall.c 就是这四个函数的模板。

官方注释还划了边界:不同卷之间的访问天然安全;f_mount / f_mkfs 这类卷管理函数无论开不开重入都不受保护,多任务调用要自己在应用层加锁。

ffconf.h 与 syscall.c 怎么配合

_SYNC_t 定义锁对象的类型,FreeRTOS 下用 SemaphoreHandle_t;ffconf.h 开了 _FS_REENTRANT 后,syscall.c 里被注释掉的 xSemaphoreCreateMutex 等实现按提示解开,接到 FreeRTOS 的信号量 API。

下载地址

http://elm-chan.org/fsw/ff/00index_e.html

解压后

1
2
doc
src

文档

在doc中有相关介绍文档,提示我们需要补全这六个函数

1
2
3
4
5
6
disk_status
disk_initialize
disk_read
disk_write
disk_ioctl
get_fattime

在src中有readme文件

里面介绍了每个文件的内容

系统

非常重要的问题,在系统中使用需要考虑线程安全问题,在syscall.c中给出了相关的解决方式

使用系统

必须添加syscall.c

不使用系统

只需要添加ff.c和diskio.c

修改文件

diskio.c diskio中的文件进行补全

disk_status,可直接返回0

disk_initialize,写入初始化,如果已经初始化过设备可以直接返回0

disk_read,读函数,需要注意,读指的是读一个block,即512个字节,在设备中需要区分

disk_write,写函数,同上

disk_ioctl,命令相关,该函数需要注意,在配置文件中不一样的配置需要不一样的内容,具体可查看http://elm-chan.org/fsw/ff/doc/dioctl.html

get_fattime,该函数可直接返回0,可添加该函数,如果_FS_NORTC=1不必提供

syscall.c 这里使用了FreeRTOS系统,需要进行修改

在ffconf.h中,需要使能_FS_REENTRANT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define _FS_REENTRANT	0
#define _FS_TIMEOUT		1000
#define	_SYNC_t			HANDLE
/* The _FS_REENTRANT option switches the re-entrancy (thread safe) of the FatFs
/  module itself. Note that regardless of this option, file access to different
/  volume is always re-entrant and volume control functions, f_mount(), f_mkfs()
/  and f_fdisk() function, are always not re-entrant. Only file/directory access
/  to the same volume is under control of this feature.
/
/   0: Disable re-entrancy. _FS_TIMEOUT and _SYNC_t have no effect.
/   1: Enable re-entrancy. Also user provided synchronization handlers,
/      ff_req_grant(), ff_rel_grant(), ff_del_syncobj() and ff_cre_syncobj()
/      function, must be added to the project. Samples are available in
/      option/syscall.c.
/
/  The _FS_TIMEOUT defines timeout period in unit of time tick.
/  The _SYNC_t defines O/S dependent sync object type. e.g. HANDLE, ID, OS_EVENT*,
/  SemaphoreHandle_t and etc.. A header file for O/S definitions needs to be
/  included somewhere in the scope of ff.c. */

按照提示修改

1
#define _SYNC_t SemaphoreHandle_t

并且在添加头文件

1
2
3
#include "FreeRTOS.h"
#include "queue.h"
#include "semphr.h"

接下来修改函数部分

按照提示取消FreeRTOS屏蔽(把注释解开)

1
2
// *sobj = xSemaphoreCreateMutex(); /* FreeRTOS */
// ret = (int)(*sobj != NULL);

踩坑记录与注意事项

  • 链接报 undefined reference to disk_xxx:六个函数一个都不能少,哪怕 get_fattime 只返回 0 也要有实现(或设 _FS_NORTC=1 让它不需要);
  • disk_read/disk_write 的返回值别写反:DRESULT 里 RES_OK 是 0,与“非 0 为真”的直觉相反,返回值错了上层全部失败;
  • disk_ioctl 的 CTRL_SYNC 别空着:它是缓存落盘的钩子,不实现的话写数据停在设备缓存里,掉电丢文件甚至损坏文件系统;
  • 多任务不开 _FS_REENTRANT:偶发文件内容错乱、FAT 表损坏,极难复现定位——根因就是 FAT 表读-改-写竞争,一开始就开;
  • 改了 ffconf.h 要全量重编:配置宏影响所有源文件的编译结果,增量编译会带着旧配置的对象文件混链接;
  • 长文件名/中文路径:默认 _USE_LFN 关闭,8.3 短名够用就别开;要开就得同时规划它的工作内存(静态/栈/堆三选一)。