由于想要读取 MP3 文件,故学习一下 FatFs 文件系统。
文章介绍了 FatFs 的移植,对移植代码进行了分析。
SD卡接口函数还需参照:
STM32关于SDIO的控制,控制SD卡_喜暖知寒的博客-CSDN博客
STM32对SD卡的读、写、擦除操作(SDIO模式)(DMA)_喜暖知寒的博客-CSDN博客
只介绍了 SD 卡的 I/O 接口,并且SD卡的操作代码完全为ST库中提供的示例代码。
🚩 毕竟初学,如有错误还望指正。
目录
绪言
本文撰写时,FatFs 官网提供的最新版本为 FatFs R0.14b 。下载此文件时,还会附带下载FatFs的数据手册网页。
FatFs 官网还提供了应用例程,其中包含 STM32 的例程。但是,提供的 STM32 例程是基于STM32F100的。
主要是分析关于 SD 卡的文件系统。参考:
- [野火]《STM32库开发实战指南》
- [正点原子]《STM32F1开发指南》
野火和正点原子用的都是 R0.11 版本的,肯定是有些差异的。
FatFs简单了解
简单介绍
FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。免费且开源。 FatFs 支持FAT12、FAT16、FAT32 等格式。按照 C89 标准编写,与磁盘I/O层完全分离。可以很容易的移植。并且,FatFs还支持 RTOS 。
![](https://i-blog.csdnimg.cn/blog_migrate/2fffb2c586c1831971a973d03ad51de7.png)
只需会调用 FatFs 提供的接口程序,即可像电脑上编写 C 、Python 等操作文件一样简单了。
使用 FatFs 需要的操作其实就是将底层接口与读写接口连接起来。
官方提供文件目录
主目录下有两个文件夹,为documents、source。
documents | 主要包含提供的帮助文档 |
source | 文件系统代码 |
其中,source文件夹下的文件为系统代码:
名称 | 描述 |
---|---|
00history.txt | 版本更新历史 |
00readme.txt | 介绍source文件下文件功能 |
ff.c | FatFs 模块 |
ff.h | FatFs 和应用模块公用的包含文件 |
diskio.h | FatFs 和磁 盘输入/输出模块共用的包含文件 |
diskio.c | 将现有磁盘输入/输出模块连接到FATF的粘合功能示例 |
ffconf.h | FatFs 模块的配置文件 |
ffunicode.c | 可选的Unicode实用程序函数 |
ffsystem.c | 可选O/S相关函数的示例 |
对于里面的实现方式,目前不想学习,只要求能够移植,写入硬件接口和使用API就足够了。
程序移植问题
FatFs 既不关心使用何种存储设备,也不关心如何实现。唯一的要求是:它是一个以固定大小的块读取/写入的块设备,可以通过定义的磁盘I/O功能访问。
文件系统的结构
![](https://i-blog.csdnimg.cn/blog_migrate/b1a61410552065b5e4ec4b0d945c2e73.png)
这是 FatFs 官方所提供的程序之间的依赖网络。其中虚线框不是必须的,为用户自行编写定义。
在官方提供的文件中,ff.c、ff.h、diskio.h 是不需要操作的。
需要修改的有:
- ffconf.h:通过修改其中的宏定义可裁剪 FatFs 的部分功能。
- diskio.c:底层驱动函数
FatFs 添加到工程文件就不说了,毕竟和创建工程的操作相差不大。
其中,单一硬件存储器和多硬件存储器在软件系统上有些许不同。
要附加具有不同接口的磁盘驱动程序,需要一些粘合功能来实现 FatFs 和驱动程序之间的接口。
底层设备驱动函数
FatFs 移植需要用户支持的函数:
翻译(自己翻译的,有可能有误):
功能 | 条件(定义在ffconf.h) | 备注 |
---|---|---|
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_MAX_SS ! = FF_MIN_SS | |
disk_ioctl (CTRL_TRIM) | FF_USE_TRIM == 1 | |
ff_uni2oem ff_oem2uni ff_wtoupper | FF_USE_LIN ! = 0 | unicode支持函数。将可选模块 ffunicode.c 添加到项目中。支持中文在此设置 |
ff_cre_syncobj ff_del_syncobj ff_req_grant ff_rel_grant | FF_FS_REENTRANT == 1 | 操作系统相关函数。示例代码在 ffsystem.c 中可用。 |
ff_mem_alloc ff_mem_free | FF_USE_LFN == 3 |
其中,关于条件的解释
条件 | 解释 |
---|---|
FF_FS_READONLY | 用来配置是否为只读操作 |
FF_USE_MKFS | 设置是否使能初始化 |
FF_MAX_SS\FF_MIN_SS | 设置缓冲区最大值\最小值 |
FF_USE_TRIM | 定义是否支持 ATA-TRIM |
FF_USE_LFN | 是否支持长文件名(0\1\2\3可选参数) |
FF_FS_REENTRANT | 切换 FatFs 模块本身的重入 |
一般情况下,需要配置的函数就6个:
函数 | 功能 |
---|---|
disk_status | 设备状态获取 |
disk_initialize | 设备初始化 |
disk_read | 扇区读取 |
disk_write | 扇区写入 |
get_fattime | 获取当前时间 |
disk_ioctl(CTRL_SYNC) | 控制设备的其他杂项 |
硬件接口函数配置简介(正点原子)
正点原子的《STM32开发指南》提供了详细的函数解释!很棒!就是不知道 FatFs 的版本问题是否兼容。
disk_initialize(设备初始化)
disk_status(设备状态获取)
disk_read(扇区读取)
disk_write(扇区写入)
disk_ioctl(其他控制)
get_fattime(获取当前时间)
✅ 很简单吧,只要配置了底层硬件接口,其他直接调用 FatFs 提供的 API 函数就可以了。
接口实现代码
❗ 不要忘记定义各种条件!!!磁盘的个数不要忘了配置,最小最大存储空间,格式化等等
因为前面刚刚分析STM32提供的操作SD卡的例程,故此用SD卡为例。
代码基于 FatFs R0.14b 的 diskio.c 文件进行更改,更改处会进行注释。
0️⃣ 物理编号宏定义
FatFs 支持多物理设备,要为每个物理设备定义不同的编号。
/* 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 */
1️⃣ disk_status(设备状态获取)
其中,在diskio.h中定义了设备状态标志,具体如下
/* Disk Status Bits (DSTATUS) */
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */
- STA_NOINIT:设备未初始化
- STA_NODISK:驱动器中无介质
- STA_PROTECT:介质受到写保护
定义了枚举
/* Results of Disk Functions */
/* 磁盘功能结果 */
typedef enum {
RES_OK = 0, /* 0: Successful */
RES_ERROR, /* 1: R/W Error */
RES_WRPRT, /* 2: Write Protected */
RES_NOTRDY, /* 3: Not Ready */
RES_PARERR /* 4: Invalid Parameter */
} DRESULT;
还有关于数据类型的定义,在 ff.h 中定义。
/* 50 */typedef unsigned char BYTE; /* char must be 8-bit */
在 diskio.h 中对 BYTE 进行创建符号
/* 12 *//* Status of Disk Functions */
/* 磁盘功能状态 */
/* 13 */typedef BYTE DSTATUS;
其实要求不高的化,默认状态OK就行了。
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
// 传入的数据为物理编号,返回类型为DSTATUS。
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_status();
// translate the reslut code here
return stat;
case DEV_MMC :
/********************新增上边界***********************/
return 0; //默认准备好了
/********************新增下边界***********************/
/********************删除上边界***********************/
result = MMC_disk_status();
// translate the reslut code here
return stat;
/********************删除上边界***********************/
case DEV_USB :
result = USB_disk_status();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
要求必须读SD卡状态,可用STM32提供的函数
但是对于 SD_SendSDStatus(uint32_t *psdstatus) 返回值的判断需慎重!
SD_Error SD_SendSDStatus(uint32_t *psdstatus) //指针为 SD状态寄存器
/********************新增上边界***********************/
result = SD_Error SD_SendSDStatus(uint32_t *psdstatus)
if(result == SD_OK)
return 0;
else
return STA_NOINIT;
/********************新增下边界***********************/
2️⃣ disk_initialize(设备初始化)
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_initialize();
// translate the reslut code here
return stat;
case DEV_MMC :
/********************新增上边界***********************/
if(SD_Init() == SD_OK) //SD_Init()是SD初始化函数,无错误返回SD_OK
return 0;
else
return STA_NOINIT;
/********************新增下边界***********************/
/********************删除下边界***********************/
result = MMC_disk_initialize();
// translate the reslut code here
return stat;
/********************删除下边界***********************/
case DEV_USB :
result = USB_disk_initialize();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
3️⃣ disk_read(扇区读取)
读取和写就稍微有些复杂
四字节对齐操作
因为分配了数组占用了空间,可以把栈大小改大一点
// startup_stm32f10x_hd.s
Stack_Size EQU 0x00000400
//地址对齐需要引入头文件
#include <string.h>
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
// 读扇区
// @ pdrv 磁盘编号
// @ buff 数据缓冲区地址
// @ sector 扇区地址
// @ count 扇区数
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 */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_read(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
/********************新增上边界***********************/
SD_Error sd_state; //存储SD卡状态
//地址问题,假设传入 51 :110011 & 0011 则为 000011,故地址没对齐
if((DWORD)buff&3) //因为用到了DMA要求存储区为4字节对齐 buff & 0011
{
res = RES_OK;
while(count--)
{
__align(4) DWORD tempbuff[SDCardInfo.CardBlockSize/4]; //__align(4)是mdk语法,四字节对齐
/* //align不能分配变量大小空间,下面正确使用方法
* __align(4) unit8_t tempbuff[512];
* //但是如果分配四字节变量,mdk会自动对齐,所以下面说法也是对的
* DWORD tempbuff[SDCardInfo.CardBlockSize/4];
*/
res = disk_read(DEV_MMC,(BYTE *)tempbuff,sector,1); //自己调用自己
if(res != RES_OK) break;
memcpy(buff,tempbuff,SDCardInfo.CardBlockSize); //引用头文件为了用它
buff += SDCardInfo.CardBlockSize;
}
return res;
}
//这里没判断else,是否还会继续执行??
/* 读取的函数 */
sd_state = SD_ReadMultiBlocks(buff, sector * SDCardInfo.CardBlockSize,
SDCardInfo.CardBlockSize, count) //这里读的是SDIO结构体里对块大小的定义
if(sd_state == SD_OK)
{
/* Check if the Transfer is finished */
sd_state = SD_WaitReadOperation();
while(SD_GetStatus() != SD_TRANSFER_OK);
}
if(sd_state == SD_OK)
res = RES_OK;
else
res = RES_ERROR;
/********************新增下边界***********************/
/********************删除上边界***********************/
// translate the arguments here
result = MMC_disk_read(buff, sector, count);
// translate the reslut code here
return res;
/********************删除下边界***********************/
case DEV_USB :
// translate the arguments here
result = USB_disk_read(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR; //参数无效
}
4️⃣ disk_write(扇区写入)
/*-----------------------------------------------------------------------*/
/* 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 */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
/********************新增上边界***********************/
SD_Error sd_state; //存储SD卡状态
if((DWORD)buff&3) //因为用到了DMA要求存储区为4字节对齐 buff & 0011
{
res = RES_OK;
while(count--)
{
__align(4) DWORD tempbuff[SDCardInfo.CardBlockSize/4]; //__align(4)是mdk语法,四字节对齐
memcpy(tempbuff,buff,SDCardInfo.CardBlockSize); //引用头文件为了用它
res = disk_read(DEV_MMC,(BYTE *)tempbuff,sector,1); //自己调用自己
if(res != RES_OK) break;
buff += SDCardInfo.CardBlockSize;
}
return res;
}
//这里没判断else,是否还会继续执行??
/* 读取的函数 */
sd_state = SD_WriteMultiBlocks((uint8_t)buff, sector * SDCardInfo.CardBlockSize,
SDCardInfo.CardBlockSize, count) //这里读的是SDIO结构体里对块大小的定义
if(sd_state == SD_OK)
{
/* Check if the Transfer is finished */
sd_state = SD_WaitWriteOperation();
while(SD_GetStatus() != SD_TRANSFER_OK);
}
if(sd_state == SD_OK)
res = RES_OK;
else
res = RES_ERROR;
/********************新下上边界***********************/
// translate the arguments here
result = MMC_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_USB :
// translate the arguments here
result = USB_disk_write(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR;
}
#endif
5️⃣ disk_ioctl(其他控制)
command 命令在 diskio.h 文件中有定义!
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// Process of the command for the RAM drive
return res;
case DEV_MMC :
/********************新增上边界***********************/
switch (cmd)
{
case GET_SECTOR_SIZE: //扇区大小
*(WORD *)buff = SDCardInfo.CardBlockSize;
break;
case GET_BLOCK_SIZE: //擦除扇区大小
*(DWORD *)buff = 1;
break;
case GET_SECTOR_COUNT: //扇区数量
*(DWORD *)buff = SDCardInfo.CardCapacity/SDCardInfo.CardBlockSize;
break;
case CTRL_SYNC:
break;
}
/********************新增下边界***********************/
/********************删除上边界***********************/
// Process of the command for the MMC/SD card
return res;
/********************删除下边界***********************/
case DEV_USB :
// Process of the command the USB drive
return res;
}
return RES_PARERR;
}
6️⃣ get_fattime(获取当前时间)
有关时间问题,FatFs 写了一个时间的函数
/* Timestamp */
#if FF_FS_NORTC == 1
#if FF_NORTC_YEAR < 1980 || FF_NORTC_YEAR > 2107 || FF_NORTC_MON < 1 || FF_NORTC_MON > 12 || FF_NORTC_MDAY < 1 || FF_NORTC_MDAY > 31
#error Invalid FF_FS_NORTC settings
#endif
#define GET_FATTIME() ((DWORD)(FF_NORTC_YEAR - 1980) << 25 | (DWORD)FF_NORTC_MON << 21 | (DWORD)FF_NORTC_MDAY << 16)
#else
#define GET_FATTIME() get_fattime()
#endif
自然也是可以写 get_fattime() 给覆盖掉的,由 FF_FS_NORTC 所决定。
例如野火所定义的时间戳函数
__weak DWORD get_fattime(void) {
/* 返回当前时间戳 */
return ((DWORD)(2015 - 1980) << 25) /* Year 2015 */
| ((DWORD)1 << 21) /* Month 1 */
| ((DWORD)1 << 16) /* Mday 1 */
| ((DWORD)0 << 11) /* Hour 0 */
| ((DWORD)0 << 5) /* Min 0 */
| ((DWORD)0 >> 1); /* Sec 0 */
}
总结
今天为了移植系统第一次看野火的视频教程,觉得和正点原子有很多不同
- 想快速使用:用正点原子
- 想仔细学习:用野火
还没有接触到 FatFs 在 FreeRTOS 上的移植,不知道会不会遇到什么困难。
✅ OK,到这里就移植完了!剩下的用户层接口就都是一样的。其实移植更改不多。修改底层接口,修改宏定义以增加、减少、更改功能就够了!
📚 嗯,就到这里吧 ~ 明天再看API相关的。晚安!