自学内容网 自学内容网

【GD32】从零开始学GD32单片机 | 基于SD卡的FatFs文件系统移植(GD32F470ZGT6)

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;
else
return 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;
else
return 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;
else
return 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;
else
return 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;
else
return 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,可用空间也是正常的。

进到里面,可以看到刚才测试例程创建的文件夹,里面文件的内容也是正确的。

 


原文地址:https://blog.csdn.net/JackieCoo/article/details/140400112

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!