1. 简介
FatFs是一个专门为微处理器设计的通用文件系统,像8051、AVR、PIC、ARM架构的微处理器都能兼容该文件系统。
FatFs文件系统最大的一个优点是它是DOS和Windows兼容的,这意味着你只需要再移植一个USB驱动就可以实现在电脑中访问单片机的储存结构,做一个小U盘或者实现文件拖拽升级这样的骚操作。
当然除了上面的优点,它还同时支持长文件名、多文件系统分区、线程安全等功能,同时开发者可以根据需要对FatFs进行裁切,使其满足嵌入式系统的要求。
更多的资料可以查看FatFs的官网:FatFs - Generic FAT Filesystem Module
2. 移植
2.1 准备
移植前先在官网下载源码:点击下载
目前FatFs的最新版本是R0.15
对于想在其他单片机中移植的同学可以在官网下载一份移植例程,里面提供了许多典型单片机的移植例程:点击下载
本次移植涉及到SDIO外设和RTC外设,没有学习的同学可以前往对应的文章提前学习一下。
2.2 文件结构
FatFs的文件还是比较简洁的。
diskio.c和diskio.h:与存储介质相关的函数接口,由用户进行移植。
ff.c和ff.h:FatFs的核心代码,无需修改。
ffconf.h:FatFs的配置文件,用户可根据实际需要修改并配置文件系统的功能。
ffsystem.c:与嵌入式系统相关的函数接口,如获取时间、互斥锁、内存管理等函数,由用户进行移植。
ffunicode.c:与文本编码相关的函数,如果我们需要文件系统支持除英文外的语言,那么FatFs就会调用里面的函数进行处理,用户无需修改。
可以看到我们只需要修改两个文件的内容即可完成移植。
2.3 配置项
FatFs有丰富的配置选项供用户选择,用户需要移植什么函数取决于我们的配置,因此这里先介绍一下常用的配置项。
FF_FS_READONLY:文件系统只读,默认为0,如果置1那么FatFs会关闭所有能修改文件的函数,大大减少FatFs的体积。
FF_FS_MINIMIZE:文件系统最小化,这个配置也可以减少FatFs的体积,它主要是通过关闭一些不常用的函数实现的;默认为0,即所有基础函数全开;置1的时候会关闭f_stat、f_getfree、f_unlink、f_mkdir、f_truncate和f_rename;置2的时候在上面的基础上再关闭f_opendir、f_readdir、f_closedir函数;置3的时候在上面的基础上再关闭f_lseek函数。
FF_USE_MKFS:文件系统格式化,默认为0,如果需要支持格式化可以置1开启这个功能。
FF_CODE_PAGE:文件系统语言,通过这个可以修改文件系统支持的语言,默认为437(英语),简体中文对应936,繁体中文对应950,语言全开就置0。
FF_USE_LFN:长文件名支持,默认为0,即不支持;这个配置一个有3种选项,区别在于文件名的储存方式;置1时,文件名储存在内存的BSS段中,此时是线程不安全的;置2时,文件名储存在栈中;置3时,文件名储存在堆中,这种方式是最推荐的。
FF_MAX_LFN:文件名长度,这个是和上面的配置对应的,如果没有使能长文件名支持,那么可以忽略该配置,文件名的长度最大可以设置为255字节。
FF_VOLUMES:储存介质数量,默认为1,如果单片机挂载了多于1种储存介质并且都有挂载文件系统,那么可以根据需要设置。
FF_MULTI_PARTITION:多分区支持,默认为0,如果文件系统需要支持多分区可以开启该配置,开启后需要用户创建分区表。
FF_MIN_SS和FF_MAX_SS:最小最大扇区大小,默认都为512,一般的存储介质扇区大小都是512字节,如果有不同可以根据储存芯片参数修改。
FF_FS_NORTC:时间戳支持,默认为0,即支持时间戳。
FF_FS_LOCK:文件锁支持,默认为0,它可以控制文件系统同时可以开启多少文件,一般建议设置成1,即同时只能开启一个文件。
FF_FS_REENTRANT:可重入支持,默认为0,单片机中有操作系统的话建议开启,它可以防止操作系统对文件系统进行异常操作。
2.4 需要移植的函数
官方列出了一个表供开发者参考。
函数 | 移植条件 | 备注 |
---|---|---|
disk_status disk_initialize disk_read | 总是需要 | |
disk_write get_fattime disk_ioctl (CTRL_SYNC) | FF_FS_READONLY == 0 | |
disk_ioctl (GET_SECTOR_COUNT) disk_ioctl (GET_BLOCK_SIZE) | FF_USE_MKFS == 1 | |
disk_ioctl (GET_SECTOR_SIZE) | FF_MIN_SS != FF_MAX_SS | |
disk_ioctl (CTRL_TRIM) | FF_USE_TRIM == 1 | |
ff_uni2oem ff_oem2uni ff_wtoupper | FF_USE_LFN != 0 | 用户无需移植 |
ff_mutex_create ff_mutex_delete ff_mutex_take ff_mutex_give | FF_FS_REENTRANT == 1 | |
ff_mem_alloc ff_mem_free | FF_USE_LFN == 3 |
2.5 开始移植
2.5.1 disk_initialize函数
这个函数负责存储介质的底层初始化,它有一个参数pdrv,指示物理硬盘号。
DSTATUS disk_initialize (BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{if (pdrv) return STA_NODISK;sd_error_enum status = SD_OK;uint32_t cardstate = 0;nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);nvic_irq_enable(SDIO_IRQn, 0, 0);uint8_t retry = 5;while (retry--) {// 初始化SD卡if ((status = sd_init()) != SD_OK) {LOG(TAG, "sdcard init failed");Stat = STA_NOINIT;continue;}// 获取SD卡信息if(SD_OK != (status = sd_card_information_get(&sd_cardinfo))) {LOG(TAG, "get sdcard info failed");Stat = STA_NOINIT;continue;}// 片选SD卡if(SD_OK != (status = sd_card_select_deselect(sd_cardinfo.card_rca))) {LOG(TAG, "select card failed");Stat = STA_NOINIT;continue;}// 获取SD卡状态if (SD_OK != (status = sd_cardstatus_get(&cardstate))) {LOG(TAG, "get card status failed");Stat = STA_NOINIT;continue;} else if(cardstate & 0x02000000) {LOG(TAG, "the card is locked!");Stat = STA_PROTECT;continue;}// 设置4bit总线模式if(SD_OK != (status = sd_bus_mode_config(SDIO_BUSMODE_4BIT))) {LOG(TAG, "set bus mode failed");Stat = STA_NOINIT;continue;}// 设置DMA传输模式if(SD_OK != (status = sd_transfer_mode_config(SD_DMA_MODE))) {LOG(TAG, "set dma mode failed");Stat = STA_NOINIT;continue;}}if (retry) {Stat = FR_OK;printf("sdcard block count: %d\r\n", (sd_cardinfo.card_csd.c_size + 1) * 1024);printf("sdcard block size: %d\r\n", sd_cardinfo.card_blocksize);}return Stat;
}
因为例程中只有一个硬盘,所以pdrv一直都会是0,如果是非0我们就认为是操作异常。
接下来的初始化操作就是跟SDIO那篇文章的基本一致 ,初始化的过程最多重试5次,因为很多时候SD卡一次初始化不一定可以。
2.5.2 disk_write函数
这个函数有4个参数,pdrv是硬盘号,buff是指向要写入数据的指针,sector是扇区号,count是要写入的扇区数。
DRESULT disk_write (BYTE pdrv, /* Physical drive nmuber to identify the drive */const BYTE *buff, /* Data to be written */LBA_t sector, /* Start sector in LBA */UINT count /* Number of sectors to write */
)
{if (pdrv || !count) return RES_PARERR; /* Check parameter */if (Stat & STA_NOINIT) return RES_NOTRDY; /* Check drive status */if (Stat & STA_PROTECT) return RES_WRPRT; /* Check write protect */if (count == 1) {if (SD_OK == sd_block_write((uint32_t*)buff, sector * sd_cardinfo.card_blocksize, sd_cardinfo.card_blocksize))return RES_OK;elsereturn RES_ERROR;} else {if (SD_OK == sd_multiblocks_write((uint32_t*)buff, sector * sd_cardinfo.card_blocksize, sd_cardinfo.card_blocksize, count))return RES_OK;elsereturn RES_ERROR;}return RES_ERROR;
}
当只需要写一个扇区时调单块写函数,如果多于一个扇区那么调多块写函数,可以提高效率。
另外要注意的是,驱动库函数的第二个参数是写入的地址,因此要将扇区号×扇区大小得出写入的地址。
2.5.3 disk_read函数
这个就跟写的函数差不多了,直接看代码。
DRESULT disk_read (BYTE pdrv, /* Physical drive nmuber to identify the drive */BYTE *buff, /* Data buffer to store read data */LBA_t sector, /* Start sector in LBA */UINT count /* Number of sectors to read */
)
{if (pdrv || !count) return RES_PARERR; /* Check parameter */if (Stat & STA_NOINIT) return RES_NOTRDY; /* Check if drive is ready */if (count == 1) {if (SD_OK == sd_block_read((uint32_t*)buff, sector * sd_cardinfo.card_blocksize, sd_cardinfo.card_blocksize))return RES_OK;elsereturn RES_ERROR;} else {if (SD_OK == sd_multiblocks_read((uint32_t*)buff, sector * sd_cardinfo.card_blocksize, sd_cardinfo.card_blocksize, count))return RES_OK;elsereturn RES_ERROR;}return RES_ERROR;
}
2.5.4 disk_ioctl函数
这个函数主要是用来获取底层IO的信息返回上层的, 有3个参数,pdrv是硬盘号,cmd是命令,不同的命令会返回不同的数据,buff是数据缓冲区,用来存放要接收或发送的数据。
不同的存储介质和文件系统配置需要支持不同的命令,像我这里就支持了GET_SECTOR_COUNT(获取扇区数)、GET_BLOCK_SIZE(获取块大小)、MMC_GET_TYPE(获取卡类型)、MMC_GET_CSD(获取CSD寄存器)、MMC_GET_CID(获取CID寄存器)、MMC_GET_SDSTAT(获取卡状态)这几个命令。
DRESULT disk_ioctl (BYTE pdrv, /* Physical drive nmuber (0..) */BYTE cmd, /* Control code */void *buff /* Buffer to send/receive control data */
)
{if (pdrv) return RES_PARERR; /* Check parameter */if (Stat & STA_NOINIT) return RES_NOTRDY; /* Check if drive is ready */switch (cmd){case GET_SECTOR_COUNT:*(LBA_t*)buff = (sd_cardinfo.card_csd.c_size + 1) * 1024;break;case GET_BLOCK_SIZE:*(DWORD*)buff = sd_cardinfo.card_blocksize;break;case MMC_GET_TYPE:*(BYTE*)buff = sd_cardinfo.card_type;break;case MMC_GET_CSD:memcpy(buff, &sd_cardinfo.card_csd, sizeof(sd_csd_struct));break;case MMC_GET_CID:memcpy(buff, &sd_cardinfo.card_cid, sizeof(sd_cid_struct));break;case MMC_GET_SDSTAT:if (SD_OK == sd_cardstatus_get((uint32_t*)buff))return RES_OK;elsereturn RES_ERROR;default:return RES_OK;}return RES_OK;
}
2.5.5 get_fattime函数
这个函数是用来获取时间戳的。
DWORD get_fattime (void)
{return time(NULL) - 8 * 3600;
}
因为我们直接调用time.h头文件里面的time函数获取时间戳,但是要注意的是time函数返回的时间戳是日期回归线的时间,像北京时间东8区的话,要减去8小时才行。time函数的话我进行了重定义,像下面这样。
time_t time(time_t *t)
{struct tm time_struct = {0};rtc_get_time(&time_struct);time_struct.tm_mon--;time_struct.tm_year -= 1900;time_struct.tm_wday--;time_struct.tm_yday--;if (t){*t = mktime(&time_struct);return *t;}return mktime(&time_struct);
}
rtc_get_time函数在RTC外设那篇文章有讲解。mktime函数也是time.h头文件自带的,这里不用重定义就可以用的,传入struct tm结构体它可以返回对应的时间戳,但要仔细看这个结构体每个成员的说明,是要做一丢丢转换的。
2.5.6 ff_memalloc函数和ff_memfree函数
这两个就是内存申请和释放的函数,沿用作者原始的代码即可,不用修改;如果单片机中移植了操作系统的话才可能需要修改。
void* ff_memalloc ( /* Returns pointer to the allocated memory block (null if not enough core) */UINT msize /* Number of bytes to allocate */
)
{return malloc((size_t)msize); /* Allocate a new memory block */
}void ff_memfree (void* mblock /* Pointer to the memory block to free (no effect if null) */
)
{free(mblock); /* Free the memory block */
}
2.6 测试
移植好上面的函数就可以来使用FatFs了,main函数里面简单写一个测试代码。
FATFS fs;
FIL file;
DIR dir;
FRESULT res;struct tm rtc_conf = {.tm_year = 2024,.tm_mon = 1,.tm_mday = 1,.tm_wday = RTC_MONDAY,.tm_hour = 0,.tm_min = 0,.tm_sec = 0
};/*!\brief main function\param[in] none\param[out] none\retval none
*/
int main(void)
{debug_init();printf("fatfs demo\r\n");/* 初始化RTC */rtc_config(&rtc_conf);// 格式化SD卡if (FR_OK != (res = f_mkfs("", NULL, NULL, 1024))) {printf("mkfs failed, err: %d\r\n", res);goto __err;} else {printf("mkfs ok\r\n");}// 挂载SD卡if (FR_OK != (res = f_mount(&fs, "", 0))) {printf("mount sdcard failed, err: %d\r\n", res);goto __err;} else {printf("mount sdcard ok\r\n");}/* 查看容量 */DWORD space = 0;FATFS *pfs;if (FR_OK != (res = f_getfree("", &space, &pfs))) {printf("get free space failed, err: %d\r\n", res);goto __err;} else {printf("free space: %d KB\r\n", space * pfs->csize / 2);}// 创建文件夹if (FR_OK != (res = f_mkdir("dir"))) {printf("create dir failed, err: %d\r\n", res);goto __err;} else {printf("create dir\r\n");}/* 写文件 */if (FR_OK != (res = f_open(&file, "0:dir/test.txt", FA_CREATE_NEW | FA_WRITE))) {printf("open file failed, err: %d\r\n", res);goto __err;} else {printf("open file ok\r\n");}UINT bw = 0;char str[] = "This a test text";if (FR_OK != (res = f_write(&file, str, sizeof(str), &bw) || bw != sizeof(str))) {printf("write file failed, err: %d\r\n", res);goto __err;} else {printf("write text \"%s\" to file\r\n", str);}f_close(&file);/* 读文件 */if (FR_OK != (res = f_open(&file, "0:dir/test.txt", FA_READ))) {printf("open file failed, err: %d\r\n", res);goto __err;} else {printf("open file ok\r\n");}UINT br = 0;char text[64] = {0};if (FR_OK != (res = f_read(&file, text, sizeof(str), &br) || br != sizeof(str))) {printf("read file failed, err: %d\r\n", res);goto __err;} else {printf("read text \"%s\" from file\r\n", text);}f_close(&file);// 遍历文件夹if (FR_OK != (res = f_opendir(&dir, "dir"))) {printf("open dir filed, err: %d\r\n", res);goto __err;} else {printf("open dir ok\r\n");}while (1) {FILINFO fno = {0};if (FR_OK != f_readdir(&dir, &fno) || fno.fname[0] == 0) {break;}if (fno.fattrib & AM_DIR) {printf("<DIR> %s\r\n", fno.fname);} else {printf("%10u %s\r\n", fno.fsize, fno.fname);}}f_closedir(&dir);/* 卸载SD卡 */if (FR_OK != (res = f_mount(0, "", 0))) {printf("unmount sdcard failed\r\n");goto __err;} else {printf("unmount sdcard ok\r\n");}__err:while(1) {}
}
先初始化RTC外设。然后调f_mkfs格式化SD卡,它有4个参数,path是硬盘号,这里填空字符串,这样它就会选择默认的硬盘;opt是格式化的选项,像文件系统类型、数据对齐等等,这里给空指针,这样它就会选择默认的配置去格式化;work是工作缓冲区,用来存放格式化过程中的数据,这里给空指针,让它去申请堆区的内存;len是工作缓冲区的大小,我这里给了1024个字节,这里给得越大那么格式化的速度会越快,SD卡容量比较大的话建议给大点,可以缩减格式化时间。
然后调f_mount函数挂载SD卡,它有3个参数;fs是文件系统结构体;path是硬盘路径,这里我们传一个空字符串,就是指定默认的硬盘;opt是挂载选项,0的话是稍后挂载,就是有文件操作时才去挂载硬盘,1的话是立即挂载。
之后调了f_getfree获取文件系统的容量,看看移植有没有问题,它有3个参数;path是硬盘路径,这里同样传空字符串指定默认硬盘;nclst是空余的簇数量,簇其实就是块,结合块的大小就可以算出剩余容量;fatfs是文件系统指针,这个函数会返回指向这个硬盘的文件系统指针。
f_mkdir创建一个文件夹,用法跟libc一样。
f_open打开一个文件,用法也是跟libc一样,这里路径要最好加硬盘号。
f_write和f_read用法类似,它们最后一个参数是实际写入和读出的字节数,可以通过这个值判断函数执行是否成功。
每次读跟写建议都调f_close关一下文件;如果一定要在文件打开的时候又写又读,那么写文件后记得f_sync一下,因为f_write并不是每次都会立刻写入文件的;读的时候调f_lseek设置文件指针,因为读的时候是会偏移指针的。
最后我们遍历一下文件夹,具体操作也是跟libc差不多的。先f_opendir打开文件夹,用f_readdir可以按顺序读取文件夹内的文件和文件夹,一般是按文件名顺序,它会返回一个FILINFO结构体,里面有文件的一些信息如大小、日期等等,如果文件夹内的文件读完了,那么结构体内的文件名会为空,通过这个我们可以判断文件夹遍历完没有。最后记得f_closedir关闭文件夹。
如果硬盘不再用了,可以卸载掉,同样调f_mount函数,所有参数均为空,这样就可以卸载默认硬盘。
下面是整一个测试例程的输出。
前面说过,FatFs是兼容Windows系统的,因此我们把SD卡拔出插入电脑,可以看到电脑是成功识别的。文件系统为FAT32,可用空间也是正常的。
进到里面,可以看到刚才测试例程创建的文件夹,里面文件的内容也是正确的。