3.5 文件系统
3.5.1 完善fatfs库函数
打开fatfs.c文件,修改get_fattime函数如下:
c
DWORD get_fattime(void)
{
/* USER CODE BEGIN get_fattime */
DWORD time;
RTC_TimeTypeDef Time;
RTC_DateTypeDef Date;
extern RTC_HandleTypeDef hrtc;
if (HAL_RTC_GetTime(&hrtc, &Time, RTC_FORMAT_BIN) != HAL_OK
|| HAL_RTC_GetDate(&hrtc, &Date, RTC_FORMAT_BIN) != HAL_OK)
{
return 0;
}
time = ((Date.Year + 20) & 0x7F) << 25; //RTC的起始年份为2000,FATFS的起始年份为1980
time |= (Date.Month & 0x0F) << 21; // 月份占4位
time |= (Date.Date & 0x1F) << 16; // 日占5位
time |= (Time.Hours & 0x1F) << 11; // 时占5位
time |= (Time.Minutes & 0x3F) << 5; // 分占6位
time |= (Time.Seconds >> 1); // 秒右移1位,占5位
return time;
/* USER CODE END get_fattime */
}
当创建文件或修改文件时,fatfs会调用这个函数修改文件属性
关于年的计算,需要说明一下:
我们在RTC中,将0年当做2000年,而FATFS是将0年当做1980年的,相差了20,比如从RTC寄存器中读出年为26表示2026年,再告诉FATFS时,要用2026-1980=46,即rtc.y+2000-1980=fatfs.y
另外注意FATFS的时间里的秒并不是精确的,秒只占5位,范围为0-31,设置的时候将实际秒右移一位即除以2保存,所以文件属性中的时间秒数都是2的倍数。
打开fatfs_platform.c修改BSP_PlatformIsDetected函数如下:
c
uint8_t BSP_PlatformIsDetected(void) {
uint8_t status = SD_PRESENT;
/* Check SD card detect pin */
if(HAL_GPIO_ReadPin(SD_DETECT_GPIO_PORT, SD_DETECT_PIN) != GPIO_PIN_RESET)
{
status = SD_NOT_PRESENT;
}
/* USER CODE BEGIN 1 */
/* user code can be inserted here */
/* USER CODE END 1 */
return status;
}
如果在CubeMX中已经选择了CD引脚,这个函数就会自动生成,不需要修改,如果没有选择,则可以改写为下面这样:
c
uint8_t BSP_PlatformIsDetected(void) {
uint8_t status = SD_PRESENT;
/* Check SD card detect pin */
/* USER CODE BEGIN 1 */
/* user code can be inserted here */
/* USER CODE END 1 */
return status;
}
直接返回SD_PRESENT,表示已插入SD卡。
3.5.2 SDIO初始化
前面在CubeMX中配置SDIO的时候讲过,用低速时钟进行初始化,正常读写卡的时候用高速时钟,这要如何实现呢?
我们查一下main函数,里面有两个与SD卡相关的初始化函数:
c
MX_SDIO_SD_Init();
MX_FATFS_Init();
解释下这两个函数的作用:
MX_SDIO_SD_Init :由CubeMX 自动生成,仅配置SDIO实例的结构体参数,不实际操作寄存器和卡
MX_FATFS_Init:这是 FatFs 文件系统模块的初始化,负责软件层面的准备。它主要完成:
- 注册一个物理磁盘驱动,调用 FATFS_LinkDriver(),将 SD_Driver 等驱动与一个盘符(如 0:)绑定。这样 FatFs 才知道操作哪个物理设备。
- 为文件系统对象分配内存,为全局的 FatFS 等变量分配结构体内存。
总结一下,main函数中的初始化并不真正操作硬件,也就是说,即使没有插卡,初始化也能顺利完成,真正对卡的操作是从挂载文件系统开始的。
调用f_mount挂载时,会调用一次disk_initialize,disk_initialize调用SD_initialize,SD_initialize再调用BSP_SD_Init执行真正的初始化,而BSP_SD_Init的声明为虚函数,先看下原代码中执行了什么:
c
__weak uint8_t BSP_SD_Init(void)
{
uint8_t sd_state = MSD_OK;
/* Check if the SD card is plugged in the slot */
if (BSP_SD_IsDetected() != SD_PRESENT)
{
return MSD_ERROR;
}
/* HAL SD initialization */
sd_state = HAL_SD_Init(&hsd);
/* Configure SD Bus width (4 bits mode selected) */
if (sd_state == MSD_OK)
{
/* Enable wide operation */
if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)
{
sd_state = MSD_ERROR;
}
}
return sd_state;
}
先检查是否插了卡,然后调用HAL_SD_Init配置hsd实例、GPIO、DMA、中断等,最后再设置为4位宽通讯方式,但此时时钟分析仍然是比较低的,所以我们可以重写这个函数,主要内容不变,只是在调用HAL_SD_Init之后,将时钟分频比设置低一些,提高SDIO的时钟频率。
打开init.c文件,添加如下代码:
c
//重写SDIO的初始化函数,原函数为弱函数,在bsp_driver_sd.c中,执行挂载时会调用该函数
uint8_t BSP_SD_Init(void)
{
hsd.Init.BusWide = SDIO_BUS_WIDE_1B; //初始化阶段设置为1bit
if (HAL_SD_Init(&hsd) != MSD_OK) return MSD_ERROR; //用低速率和1bit进行初始化
hsd.Instance->CLKCR = (hsd.Instance->CLKCR & ~SDIO_CLKCR_CLKDIV); //修改分频寄存器,切换到高速时钟
if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK) //设置为4bit模式
{
return MSD_ERROR;
}
return MSD_OK;
}
在调用HAL_SD_Init之前有个设置总线宽度的操作:hsd.Init.BusWide = SDIO_BUS_WIDE_1B,实际上也不需要,因为HAL_SD_Init中会强制设置为1B。也就是说,在CubeMX中配置时,可以直接配置为1B。
3.5.3 挂载文件系统
打开init.c文件,添加如下代码:
c
//文件系统初始化
void File_Init(void)
{
if (SD_CD != 0) Error_Handle(1, "No SD card!"); //检查是否插入了SD卡
PWR_SD3V3 = 1; //使SD卡断电重启
HAL_Delay(200);
PWR_SD3V3 = 0;
HAL_Delay(20);
FRESULT ret = f_mount(&Fatfs, Root, 1); //尝试挂载文件系统
if (ret == FR_NO_FILESYSTEM) //如果没有文件系统就格式化并创建文件系统
{
unsigned char *buf = Sean_Malloc(512); //为格式化操作申请内存
if (buf == NULL)
{
printf("f_mount, Malloc fail!\r\n");
return;
}
ret = f_mkfs(Root, FM_FAT32, 0, buf, 512); //格式化SD卡
Sean_Free(buf); //格式化完成后释放内存
if (ret != FR_OK)
{
printf("Make file system error:%s\r\n", File_Err[ret]);
Error_Handle(3, "No File system!");
}
}
if (ret != FR_OK) //挂载中遇到其它异常
{
printf("Mount error:%s\r\n", File_Err[ret]);
return;
}
}
MSH_INIT_EXPORT(4, File_Init, "File system initialization");
在挂载之前有个重启SD卡电源的操作,是为了防止SDIO接口在单片机不断电复位时死锁或状态异常等导致无法正常初始化,如果硬件上无法控制SD卡的电源,需要整机断电或重新插拔下SD卡才行。
注意 :
执行格式化操作要慎重,应用程序中尽量不要保留这个操作,可以用下面简化的代码:
c
void File_Init(void)
{
if (SD_CD != 0) Error_Handle(2, "No SD card!"); //检查是否插入了SD卡
PWR_SD3V3 = 0; //SD卡上电
HAL_Delay(20);
FRESULT ret = f_mount(&Fatfs, Root, 1); //尝试挂载文件系统
if (ret == FR_NO_FILESYSTEM) //如果没有文件系统就报错
{
printf("No filesystem\r\n");
Error_Handle(3, "No File system!");
}
if (ret != FR_OK) //挂载中遇到其它异常
{
printf("Mount error:%s\r\n", File_Err[ret]);
Error_Handle(4, "Mount error!");
}
}
MSH_INIT_EXPORT(3, File_Init, "File system initialization");
将PWR_SD3V3引脚上电默认电平设置为高电压,程序中只要执行上电即可,不用再次断电。
3.5.4 移植文件操作代码
初始化时使用了两个变量:Fatfs、Root,它们与当前挂载的文件系统是绑定的,在文件操作时也可能会用到,所以这两个变量要定义为全局,在work.h中声明:
c
extern const char *Root; //定义根目录
extern FATFS Fatfs;
将MyFile.c文件添加到当前工程中,里面已实现了常用的文件操作命令:


将ymodem.c文件添加到当前工程中,里面已实现了发送和接收文件的命令:

有了这两个文件,就可以使用配套的工具软件进行文件操作了:

3.5.5 其它
FIL的内存占用
先看下FIL的类型定义:
c
typedef struct {
_FDID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */
BYTE flag; /* File status flags */
BYTE err; /* Abort flag (error code) */
FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */
DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */
DWORD sect; /* Sector number appearing in buf[] (0:invalid) */
#if !_FS_READONLY
DWORD dir_sect; /* Sector number containing the directory entry */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] */
#endif
#if _USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !_FS_TINY
BYTE buf[_MAX_SS]; /* File private data read/write window */
#endif
} FIL;
在非TINY模式,即标准模式下,一个FIL变量占用内存达552字节,若要支持exFAT,还要增加20字节达到572字节。
DIR相对较少,但也达到56字节。
这块内存的作用 :
用于缓存一个正在读写的扇区的数据,因为磁盘就像flash一样,写入之前需要先擦除(仅能由1写为0,不可由0写为1),fatfs会在读写文件时,先查询要操作哪个扇区,然后将整个扇区数据读出来放在buf中,然后调用f_write时,实际修改了buf中的数据。
而且读写文件还需要用到数据缓存,因此操作文件需要消耗大量的内存,所以在编程时,需要仔细考虑自己的程序架构。
如果只有一个任务需要操作文件,则在任务函数中定义文件是没有问题的;
如果多个任务都要操作文件,但不会同时操作,则仅定义一个全局的文件变量无疑会大大节约内存,避免在每个任务栈中都分配这块内存;
如果只是偶尔需要操作一次文件,也可以使用用动态内存,如下示例代码:
c
void File_GetMD5(const char * FilePath, char * strMD5)
{
#define BUFFER_SIZE 512
FIL *fil;
FRESULT res;
unsigned char *buffer;
fil = Sean_Malloc(sizeof(FIL) + BUFFER_SIZE); //申请一块内存
......
buffer = (unsigned char *)(fil + 1); //设置数据缓存区位置在FIL之后
res = f_open(fil, FilePath, FA_READ); //打开文件
......
res = f_read(fil, buffer, BUFFER_SIZE, &bytes_read); //读取文件
......
f_close(fil); //关闭文件
Sean_Free(fil); //释放申请的内存
}
落盘机制
落盘机制是指fatfs采用什么样的机制执行实际的写入磁盘的操作,我们调用f_write写文件时,数据会先写入到文件的扇区缓存中,如果缓存区写满则执行一次写卡操作,这个写卡操作只是将整个buf的数据发送到SD卡缓存,并不立即写入磁盘,然后再读取下一个要操作的扇区,继续执行;
当调用f_sync或f_close时,会触发写入磁盘的命令,fatfs才会通知SD卡执行落盘。
实际上SD卡内部也有一定的缓存,目的是合并和优化写入操作,提高存储器寿命。
再来看看调用f_sync后SD卡会干什么:
- 更新文件目录项(大小、时间戳),这通常需要重写目录所在的扇区(512字节);
- 如果文件大小变化,还可能需要更新FAT表,又是重写一个或多个扇区(每个512字节);
- 数据区重写一个扇区的数据,执行"读-改-写",SD卡的内部闪存必须擦除后再写入,且最小擦除单元是块(Block),通常为几MB:
-读出包含该扇区的整个物理块(比如4MB);
-在缓存中修改这512字节;
-擦除这个块
-把修改后的整个块重新写入。
你每次调用 f_sync 修改一个512字节的扇区,都可能触发SD卡内部几MB的物理擦写循环。 这会让实际写入闪存的字节数,是你用户数据的几千倍。
所以为了平衡数据安全与设备寿命,你必须综合考虑在何时写入、何时同步或关闭文件,原则上可以考虑:
- 关键的数据保存,无法接受数据丢失,那就随时调用f_sync,牺牲寿命,但可以考虑使用高耐久性(High Endurance)的专用SD卡,它们使用SLC或pSLC模式,能承受更高的擦写循环。
- 不太重要的记录,如日志,尽量降低保存频次
另外还有个更好的方法,利用单片机内部掉电不丢失的SRAM,大小为4K,先将要写入的数据保存到这块内存中,然后在数据量大时,或单片机上电时,或周期性地执行保存,对于日志来说,可以极大地降低写卡频率,大幅提升SD卡寿命。
完结
回看第一篇:FreeRTOS项目程序框架介绍(一)