STM32通过SDIO驱动并移植FatFs文件系统详细步骤说明
对于sd卡sdio卡tf卡和mmc卡的介绍就不说了,对于sdio的介绍和fatfs的介绍不做说明,默认读者已经掌握这些基础知识
本说明采用STM32F407ZGT6单片机,最新版本的Fatfs的文件,对于之前的Fatfs文件系统也会介绍
一. 架构说明
最底层是各类存储设备,FATFS支持SD卡,TF卡以及FLASH和各类存储器,SDIO是和存储设备直接进行交互的接口,FATFS是建立在SDIO接口之上的管理协议,最上层的应用层是根据我们自己的需求去编写的。
SDIO说明
TF卡与SDIO接口硬件连接
TF卡与SDIO硬件的连接只需要连接6根线即可,将TF接在STM32的SDIO硬件接口上。
SDIO移植说明
关于SDIO的初始化和各类驱动函数都是为了给FATFS文件系统做基础,因为FATFS文件系统的接口需要初始化,读TF卡信息以及读指定块数据和写指定块数据函数,并且需要有返回值,入参出参的格式也都是固定的,所以要和FATFS文件系统格式保持一致
1. 初始化函数
建立一个空白的STM32工程,然后将SDIO的库函数添加进来,然后首先初始化和使能SDIO的时钟,设置时钟沿,设置旁路时钟,设置节能模式,设置数据宽度为1位,设置硬件流控制,最后设置时钟分频,这是STM32种SDIO的配置结构体。使能GPIO的时钟之后我们配置所有的GPIO的输出引脚,全部设置为复用推挽输出,到此整个SDIO初始化就完成了,但是此初始化需要返回一个变量,用于判断是否初始化成功的参数,这样做的目的是为后面移植FATFS文件系统做准备。
/*
Name:uint8_t Sd_init(void)
----------------------
Des: SDIO初始化函数
Ref:
Paras:
Author: zx
Date: 2023年11月09日
*/
uint8_t SdInit(void)
{
GPIO_InitTypeDef gpio_init_struct = {0};
/*初始化GPIO*/
__HAL_RCC_SDIO_CLK_ENABLE();
SD_D0_GPIO_CLK_ENABLE();
SD_D1_GPIO_CLK_ENABLE();
SD_D2_GPIO_CLK_ENABLE();
SD_D3_GPIO_CLK_ENABLE();
SD_SCK_GPIO_CLK_ENABLE();
SD_CMD_GPIO_CLK_ENABLE();
/* 配置D0引脚 */
gpio_init_struct.Pin = SD_D0_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_D0_GPIO_AF;
HAL_GPIO_Init(SD_D0_GPIO_PORT, &gpio_init_struct);
/* 配置D1引脚 */
gpio_init_struct.Pin = SD_D1_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_D1_GPIO_AF;
HAL_GPIO_Init(SD_D1_GPIO_PORT, &gpio_init_struct);
/* 配置D2引脚 */
gpio_init_struct.Pin = SD_D2_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_D2_GPIO_AF;
HAL_GPIO_Init(SD_D2_GPIO_PORT, &gpio_init_struct);
/* 配置D3引脚 */
gpio_init_struct.Pin = SD_D3_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_D3_GPIO_AF;
HAL_GPIO_Init(SD_D3_GPIO_PORT, &gpio_init_struct);
/* 配置SCK引脚 */
gpio_init_struct.Pin = SD_SCK_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_SCK_GPIO_AF;
HAL_GPIO_Init(SD_SCK_GPIO_PORT, &gpio_init_struct);
/* 配置CMD引脚 */
gpio_init_struct.Pin = SD_CMD_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = SD_CMD_GPIO_AF;
HAL_GPIO_Init(SD_CMD_GPIO_PORT, &gpio_init_struct);
/* 配置SD */
sd_handle.Instance = SDIO;
sd_handle.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
sd_handle.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
sd_handle.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
sd_handle.Init.BusWide = SDIO_BUS_WIDE_1B;
sd_handle.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
sd_handle.Init.ClockDiv = 1;
if (HAL_SD_Init(&sd_handle) != HAL_OK)
{
return 1;
}
/* 获取SD信息 */
HAL_SD_GetCardInfo(&sd_handle, &sd_card_info);
/* 配置4bit总线宽度 */
if (HAL_SD_ConfigWideBusOperation(&sd_handle, SDIO_BUS_WIDE_4B) != HAL_OK)
{
return 2;
}
return 0;
}
注意,一开始配置的是单线数据传输,在后面我们将单线数据传输改为了四线数据传输,这样可提高速度,至于为啥不一开始就配置成四线的,这个问题我也不太清楚
2. 读TF卡信息函数
在这个函数内,我们直接调用HAL库的函数即可获取TF卡的容量大小,剩余空间以及卡类型的信息,
/*
Name:uint8_t SdGetCardInfo
----------------------
Des: 读取SD卡信息
Ref:
Paras: info:SD卡信息
Author: zx
Date: 2023年11月09日
*/
uint8_t SdGetCardInfo(HAL_SD_CardInfoTypeDef *info)
{
if (HAL_SD_GetCardInfo(&sd_handle, info) != HAL_OK)
{
return 1;
}
return 0;
}
3. 读SD卡指定数量的块数据
此函数可以读取TF卡种指定块的数据,也是用于给文件系统做基础。
/*
Name:uint8_t SdGetCardInfo
----------------------
Des: 读SD卡指定数量的块数据
Ref:
Paras: buf: 数据保存的起始地址;addr: 块地址;count: 块数量;
Author: zx
Date: 2023年11月09日
*/
uint8_t SdReadDisk(uint8_t *u8pbuf, uint32_t u32addr, uint32_t u32count)
{
uint32_t u32timeout = SD_DATATIMEOUT;
if (HAL_SD_ReadBlocks(&sd_handle, u8pbuf, u32addr, u32count, SD_DATATIMEOUT) != HAL_OK)
{
return 1;
}
while ((HAL_SD_GetCardState(&sd_handle) != HAL_SD_CARD_TRANSFER) && (--u32timeout != 0));
if (u32timeout == 0)
{
return 1;
}
return 0;
}
4. 写SD卡指定数量的块数据
/*
Name:uint8_t SdWriteDisk
----------------------
Des: 写SD卡指定数量的块数据
Ref:
Paras: buf: 数据保存的起始地址;addr: 块地址;count: 块数量;
Author: zx
Date: 2023年11月09日
*/
uint8_t SdWriteDisk(uint8_t *u8pbuf, uint32_t u32addr, uint32_t u32count)
{
uint32_t u32timeout = SD_DATATIMEOUT;
if (HAL_SD_WriteBlocks(&sd_handle, u8pbuf, u32addr, u32count, SD_DATATIMEOUT) != HAL_OK)
{
return 1;
}
while ((HAL_SD_GetCardState(&sd_handle) != HAL_SD_CARD_TRANSFER) && (--u32timeout != 0));
if (u32timeout == 0)
{
return 1;
}
return 0;
}
到此,FATFS文件系统需要的函数已经全部编写完成,能保证SDIO和TF正常通讯的基础上可跳过下面两个函数,下面两个函数是我写的专门用来测试SDIO与TF卡通讯是否正常的函数,是修改正点原子的程序。在FATFS文件系统中用不到下面的函数,仅仅测试使用
5. 打印TF卡信息函数,可在初始化之后调用
打印TF的信息,可用来检查TF卡和SDIO连接正常与否,若正常,则可以打印TF的信息。若打印都失败,就要检查硬件和TF卡是否格式化为FAT32格式,必须保证这一步没有任何问题方能进行下一步。
/*
Name:void ShowSdInfo(void)
----------------------
Des: 将SD卡信息通过串口打印出来
Ref:
Paras:
Author: zx
Date: 2023年11月10日
*/
void ShowSdInfo(void)
{
HAL_SD_CardCIDTypeDef sd_card_cid = {0};
HAL_SD_GetCardCID(&sd_handle, &sd_card_cid);
USART1_printf("Card Type: %s\r\n", (sd_card_info.CardType == CARD_SDSC) ? ((sd_card_info.CardVersion == CARD_V1_X) ? ("SDSC V1") :
((sd_card_info.CardVersion == CARD_V1_X) ? ("SDSC V2") :
(""))) :
((sd_card_info.CardType == CARD_SDHC_SDXC) ? ("SDHC") :
((sd_card_info.CardType == CARD_SECURED) ? ("SECURE") :
(""))));
USART1_printf("Card ManufacturerID:%d\r\n", sd_card_cid.ManufacturerID);
USART1_printf("Card RCA:%d\r\n", sd_card_info.RelCardAdd);
USART1_printf("LogBlockNbr:%d \r\n", sd_card_info.LogBlockNbr);
USART1_printf("LogBlockSize:%d \r\n", sd_card_info.LogBlockSize);
USART1_printf("Card Capacity:%d MB\r\n", (uint32_t)(((uint64_t)sd_card_info.LogBlockNbr * sd_card_info.LogBlockSize) >> 20));
USART1_printf("Card BlockSize:%d\r\n\r\n", sd_card_info.BlockSize);
}
6. 读取SD卡第0扇区的数据
/*
Name:void SdReadTest(void)
----------------------
Des: 读取SD卡第0扇区的数据
Ref:
Paras:
Author: zx
Date: 2023年11月10日
*/
void SdReadTest(void)
{
uint8_t *buf;
uint16_t index;
if (buf == NULL)
{
return;
}
/* 读取并打印SD卡第0个块的数据 */
if (SdReadDisk(buf, 0, 1) == 0)
{
USART1_printf("Block 0 Data:\r\n");
for (index=0; index<sd_card_info.BlockSize; index++)
{
USART1_printf("%02X ", buf[index]);
}
USART1_printf("\r\nData End\r\n");
}
else
{
USART1_printf("SD read Failure!\r\n");
}
}
到此,全部的驱动就已经移植完成了,若初始化SDIO之后可以在串口助手中看到正常显示的TF卡信息和读到的第0扇区的所有数据,那么就成功一半了
最后贴上在c文件中声明的句柄和结构体,这些可以根据自己的嵌入式软件编码规范修改
/* SD句柄 */
SD_HandleTypeDef sd_handle = {0};
/* SD信息 */
HAL_SD_CardInfoTypeDef sd_card_info = {0};
FATFS文件系统移植
版本区别
没啥区别,就是早期的版本有interger.h头文件,是定义数据类型的,现在把这个头文件封装在了ff.c里,现在的版本没有了这个头文件,移植方法和操作都是一样的。
移植步骤
- 把source中的文件除了两个txt说明文件的其他文件都拷到自己创建的工程文件里,在工程中建立一个fatfs的文件夹,放进去,然后就是把头文件都添加进去,把fatfs的路径进行设置。
将文件都添加进来之后在diskio.c中移植接口,整个FATFS只修改两个文件,一个是diskio.c和ffcon.h,diskio是接口程序,ffcon.h是头文件,和FreeRtos的config文件是一样的,用来配置各种功能和裁剪。言归正传,移植接口。
/*-----------------------------------------------------------------------*/
/* Low level disk I/O module SKELETON for FatFs (C)ChaN, 2019 */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be */
/* attached to the FatFs via a glue function rather than modifying it. */
/* This is an example of glue functions to attach various exsisting */
/* storage control modules to the FatFs module with a defined API. */
/*-----------------------------------------------------------------------*/
#include "ff.h" /* Obtains integer types */
#include "diskio.h" /* Declarations of disk functions */
#include "app_sd_card.h"
/* Definitions of physical drive number for each drive */
#define DEV_RAM 0 /* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB 2 /* Example: Map USB MSD to physical drive 2 */
/************************************************************************/
/*手动新增*/
#define SD_CARD 0 /* SD卡,卷标为0 */
#define EX_FLASH 1 /* 外部spi flash,卷标为1 */
/************************************************************************/
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
return RES_OK;
}
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
switch (pdrv)
{
case SD_CARD :
{
return SdInit();
}
}
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
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 */
)
{
switch (pdrv)
{
case SD_CARD :
{
return SdReadDisk(buff, sector, count);
}
}
return RES_PARERR;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
#if FF_FS_READONLY == 0
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 */
)
{
switch (pdrv)
{
case DEV_RAM :
{
return SdWriteDisk((uint8_t *)buff, sector, count);
}
}
return RES_PARERR;
}
#endif
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT result;
switch (pdrv) {
case SD_CARD :
switch (cmd)
{
case CTRL_SYNC:
result = RES_OK;
break;
case GET_SECTOR_SIZE:
*(DWORD *)buff = 512;
result = RES_OK;
break;
case GET_BLOCK_SIZE:
*(WORD *)buff = sd_card_info.LogBlockSize;
result = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD *)buff = sd_card_info.LogBlockNbr;
result = RES_OK;
break;
default:
result = RES_PARERR;
break;
}
return result;
}
return RES_PARERR;
}
将SDIO写的函数添加到diskio.c中的函数中,初始化,读TF卡信息以及读写块的函数添加进来。
/*手动新增*/
#define SD_CARD 0 /* SD卡,卷标为0 */
#define EX_FLASH 1 /* 外部spi flash,卷标为1 */
这两个宏定义是我们为SD卡定义的识别代号,也就是卷标。
然后我门需要定义一个结构体,FATFS *fs[FF_VOLUMES]; 这个结构体是定义一个磁盘所需要的信息,我们读写都会要操作这个结构体,另外需要给磁盘定义一个工作区,保存FATFS文件管理系统的参数,这个工作区是必需的,否则FATFS文件系统会运行不起来,这个结构体也是必需的,因为它相当于一个指针,指向这个磁盘。如果同时挂载两个磁盘,那么就需要定义两个工作区。
/*创建一个SD卡工作区*/
FATFS SDWorkBuf[1];
/*创建一个文件注册区,FATFS是枚举类型*/
/* 逻辑磁盘工作区(在调用任何FATFS相关函数之前,必须先给fs申请内存) */
FATFS *fs[FF_VOLUMES];
然后我们还需要定义一个文件结构体一个文件夹结构体,这两个结构体是定义的实体,在操作文件或者文件夹的时候会直接操作这个结构体,这两个结构体指向TF卡中的文件和文件夹。
/*定义一个文件结构体*/
FIL fil;
/*定义一个文件夹结构体*/
DIR dir;
还需要定义一个文件缓冲区,这个缓冲区用于从TF卡中读取数据,将数据暂存在这个缓存中,另外还需要一个u32的变量,用于计数,这个是文件系统API函数要填的参数,以上的定义都是必需的。
/*定义一个u32的变量,用于计数*/
uint32_t bw = 0;
/*定义一个数据缓冲区*/
uint8_t bbuff[200];
移植了所有的文件,定义了所有的参数和结构体之后就可以进行编译,遇到啥问题改啥问题即可,至此,FATFS文件系统的移植就结束了
FATFS文件系统的操作
第一步,首先需要给每个TF卡申请内存区域,为什么说是每个,是因为FATFS文件系统支持10个磁盘,所以写一个通用的函数用于申请内存,这步等于是将一个结构体指针指向给TF卡创建的工作区结构体,我们操作这个指针就是在操作这个TF卡的数据。
/*
Name:uint8_t ApplyForMemory(void)
----------------------
Des: 为SD卡申请内存
Ref:
Paras:
Author: zx
Date: 2023年11月10日
*/
uint8_t ApplyForMemory(void)
{
uint8_t u8LoopData = 0;
uint8_t u8Res = 0;
/*给所有连接得SD卡申请内存区域,FF_VOLUMES是挂载SD卡个数*/
for (u8LoopData = 0; u8LoopData < FF_VOLUMES; u8LoopData++)
{
fs[u8LoopData] = SDWorkBuf; /* 为磁盘i工作区申请内存 */
if (!fs[u8LoopData])
{
break;
}
}
if (u8LoopData == FF_VOLUMES && u8Res == 0)
{
return 0; /* 申请有一个失败,即失败. */
}
else
{
return 1;
}
}
第二步,就是需要挂载TF卡,挂载TF卡的意思就是初始化外设以及各个参数,并且将TF卡注册到FATFS文件系统中,挂载成功之后才可以进行各项操作。
/* 挂载SD卡 */
res = f_mount(fs[0], "0:", 1);
if(res == FR_OK)
{
USART1_printf("挂载成功\r\n");
}
else
{
USART1_printf("挂载失败\r\n",res);
}
当显示TF卡挂载成功之后就可以创建文件夹和创建文件,当然,读写文件夹也是没有问题的,下面简单写几个函数用于打开文件和读文件,首先我们在打开一个草滩小王子的文件夹,然后打开这个文件夹,打开一个名为测试.txt的文档,并且读出里面的数据并打印出来,FA_READ函数是读,如果打开文件夹是要写,参数就改为FA_WRITE,这个参数还有很多选项,大家可查阅FATFS的IAP技术手册。
res = f_opendir(&dir,"草滩小王子");
if(res == FR_OK)
{
USART1_printf("打开文件成功\r\n",sizeof(SDWorkBuf));
}
else
{
USART1_printf("打开文件失败\r\n");
}
res = f_open(&fil, "0:/草滩小王子/测试.txt", FA_READ);
if(res == FR_OK)
{
USART1_printf("打开文件成功\r\n");
}
else
{
USART1_printf("打开文件失败\r\n");
}
f_read(&fil,bbuff,64,&bw);
f_close(&fil);
Usart1_SendBuf(bbuff,sizeof(bbuff));
下面是创建一个名为草滩小王子的文件夹例子
f_chdir("/");
res = f_mkdir("草滩小王子");
if(res == FR_OK)
{
USART1_printf("创建文件夹成功\r\n");
}
else
{
USART1_printf("创建文件夹失败:%d\r\n");
}
下面是打开一个在草滩小王子文件夹下的测试.txt的文件,并且给文件中写入数据的例子
res = f_open(&fil, "0:/草滩小王子/测试.txt", FA_WRITE );
if(res == FR_OK)
{
USART1_printf("打开文件成功\r\n");
}
else
{
USART1_printf("打开文件失败\r\n");
}
/*这个函数忘记啥了,有空了查一查*/
f_lseek(&fil, f_size(&fil));
/*给文件中写入数据*/
f_printf(&fil, "我是小绵羊!\r");
f_printf(&fil, "我是小绵羊!\r");
f_printf(&fil, "我是小绵羊!\r");
f_printf(&fil, "我是小绵羊!\r");
f_printf(&fil, "我是小绵羊!\r");
/*此函数用于向文件中写入数据,f_printf也可以实现此功能*/
f_write(&fil, "我是小绵羊!\r",100,&bw);
/*关闭文件*/
f_close(&fil);
总结:SDIO是专门用于操作TF、SD卡的接口,速度比SPI快,配置方便,并且支持单线数据传输和四线数据传输,在我们的程序中,就用了四线并口传输,速度嘎嘎猛。如果要使用SPI也可以,将diskio.c中将SDIO的驱动更换成SPI的驱动即可,其他都一样。FATFS文件系统操作方便,支持裸奔和操作系统。是管理大容量存储器的比较好的方案,之前还用过南京沁恒微的CH378文件管理芯片,对比FATFS文件管理系统,效率和操作方法明显不如FATFS文件管理系统,所以推荐使用SDIO+FATFS文件系统的方案。