【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)!