FreeRTOS项目程序框架介绍(五)

接上篇:FreeRTOS项目程序框架介绍(四)

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 文件系统模块的初始化,负责软件层面的准备。它主要完成:

  1. 注册一个物理磁盘驱动,调用 FATFS_LinkDriver(),将 SD_Driver 等驱动与一个盘符(如 0:)绑定。这样 FatFs 才知道操作哪个物理设备。
  2. 为文件系统对象分配内存,为全局的 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卡会干什么:

  1. 更新文件目录项(大小、时间戳),这通常需要重写目录所在的扇区(512字节);
  2. 如果文件大小变化,还可能需要更新FAT表,又是重写一个或多个扇区(每个512字节);
  3. 数据区重写一个扇区的数据,执行"读-改-写",SD卡的内部闪存必须擦除后再写入,且最小擦除单元是块(Block),通常为几MB:
    -读出包含该扇区的整个物理块(比如4MB);
    -在缓存中修改这512字节;
    -擦除这个块
    -把修改后的整个块重新写入。

你每次调用 f_sync 修改一个512字节的扇区,都可能触发SD卡内部几MB的物理擦写循环。 这会让实际写入闪存的字节数,是你用户数据的几千倍。

所以为了平衡数据安全与设备寿命,你必须综合考虑在何时写入、何时同步或关闭文件,原则上可以考虑:

  1. 关键的数据保存,无法接受数据丢失,那就随时调用f_sync,牺牲寿命,但可以考虑使用高耐久性(High Endurance)的专用SD卡,它们使用SLC或pSLC模式,能承受更高的擦写循环。
  2. 不太重要的记录,如日志,尽量降低保存频次

另外还有个更好的方法,利用单片机内部掉电不丢失的SRAM,大小为4K,先将要写入的数据保存到这块内存中,然后在数据量大时,或单片机上电时,或周期性地执行保存,对于日志来说,可以极大地降低写卡频率,大幅提升SD卡寿命。

完结

回看第一篇:FreeRTOS项目程序框架介绍(一)

相关推荐
searchforAI1 小时前
Ai好记 vs Get笔记:AI音视频笔记工具深度测评对比
人工智能·笔记·学习·ai·音视频·语音识别
嵌入式小站2 小时前
STM32 零基础可移植教程 15:ADC 多通道扫描,读取三路 PWM 的平均电压
stm32·单片机·嵌入式硬件
噜噜噜阿鲁~2 小时前
python学习笔记 | 11.5、面向对象高级编程-使用枚举类
笔记·python·学习
hai3152475432 小时前
# FiveOS V5.0 交付(终极合成器版 · 物理合规修正)
人工智能·stm32·单片机·嵌入式硬件·神经网络
GLDbalala3 小时前
GPU PRO 5 - 2.5 TressFX: Advanced Real-Time Hair Rendering 笔记
笔记
憧憬成为java架构高手的小白3 小时前
数据库期末复习笔记
数据库·笔记·oracle
嵌入式ZYXC3 小时前
第6章:通信接口的硬件特性——为什么你的UART乱码、I2C死锁、SPI干扰大?
stm32·单片机·嵌入式硬件·物联网·智能硬件
05候补工程师3 小时前
【408数据结构】核心考点:图(Graph)精炼笔记与算法直觉
数据结构·经验分享·笔记·考研·算法·图论
fffzd3 小时前
STM32:串口--轮询模式
stm32·单片机·嵌入式硬件·串口·hal库·轮询模式