1.前言
????????FATFS的教程与历程屡见不鲜,其覆盖的平台从8051内核单片机到Cortex-M7内核高性能单片机甚至更高性能的应用处理器。然而大多数STM32的教程和例程均绕开了使用D-Cache回写模式和DMA配合的这种模式。例如正点原子和野火的历程均是将D-Cache设置为透写模式。这样很大程度上消弱了带有D-Cache的处理器的性能,尤其是在连接SDRAM时,使用D-Cache透写模式频繁写入SDRAM将会有很长的延迟,而处理器需要不断等待数据写入完成。而使用D-Cache回写模式无需频繁写入SDRAM,且在Cache更新时是以Cache行为单位写入SDRAM,可以充分发挥SDRAM猝发式读写的优势。
2.相关要点
????????DMA和Cache同时使用时将会发生Cache和其对应位置的内存中的数据不一致的情况。由于Cache中存储的内容完全取决于CPU的主动访存。
? ? ? ? 在只有CPU和Cache的系统中,由于只有CPU具有读写内存和Cache的能力,故最终输出的结果是完全由程序决定的。而当系统中加入DMA后,DMA可以更改内存数据而不同步到Cache,DMA也可以输出没有从Cache中最新状态更新的存放于内存中的老数据(Cache在回写模式下)。于是产生了数据不一致的问题。
? ? ? ? 解决该问题需要合理地使用 清除Cache 和 无效化Cache 两个功能来同步Cache和内存中的数据。而实际使用中这里这两个功能并不是简单的调用,不注意地址对齐的细节和顺序会有丢失数据的风险。例如本贴解决问题的关键点就在于,官方的sd_diskio.c文件中地址对齐的细节没有处理好,同步DMA接收到内存中的数据到Cache时需要无效化Cache,无效化Cache时将相邻地址的写入Cache还没有同步到内存的数据丢失。
3.关键问题详细介绍
?3.1Cache与内存数据不一致的问题? ??
????????在只有CPU和Cache的系统中,CPU发出主动访存请求时,如果在Cache中命中就直接读写Cache,如果没有命中,则需要访问内存,并将访问地址与Cache行大小的反码取余得到访问地址所在Cache行的地址,并将整个Cache行调入Cache。例如访问地址为0X1314,Cache行大小为32字节(0X001F个字节,取反为0XFFE0),则0X1314所在的Cache行地址为 0X1314&0XFFE0?= 0X1300 。?????
? ? ? ? 当系统中出现DMA时,DMA虽然只有读写内存的权利,但是其读写操作和CPU的读写操作是异步的,且不通过直接的方式相互通知,这就会导致:
- ????????DMA输入数据后,如果输入的地址在Cache命中,CPU只会读取Cache中的数据,而不理会内存中的数据。
- ? ? ? ? DMA输出数据时,由于CPU在命中Cache时直接读写Cache,所以CPU输出的结果可能存放于Cache中而不在内存中,DMA如果直接输出内存中的数据将会导致将未处理的旧数据输出。
?3.2同步Cache与内存数据操作导致数据丢失问题
? ? ? ? Cache同步是按行同步的,如果同步的内容的地址不是与Cache行的地址对齐,将会在无效化Cache操作时,丢失与同步内容地址相邻的但存放于Cache中的数据。该问题比较绕,如下图所示。
? ? ? ? ?对比同步前后情况可以明显地发现,同步时无效化Cache的操作虽然将DMA接收后的数据同步到了Cache中,但是也同时将CPU修改前的旧数据同步到了Cache中,导致CPU修改后的新数据丢失。
4.实践步骤
- 使用CUBEMX配置SDIO和FATFS
- 设置Cache和DMA
- 更改sd_diskio.c文件
- 编写测试程序
5.配置SDIO,FATFS和DMA
?5.1SDIO配置
? ? ? ? STM32H7系列和STM32F7系列中称作SDMMC,本帖中所介绍内容与SDIO和SDMMC的细节无关,故本贴中SDIO和SDMMC名称可以互换,即连接SD卡的接口。其基本参数配置下图所示。
其中尤其注意“SDMMC clock divide factor" SDMMC分频设置,这个参数需要与时钟树中SDMMC外设的输入时钟相关,最终目的是分频后不要超过25MHZ,例如本帖中,时钟树为SDMMC配置的时钟为90MHZ。四分配后为22.5MHZ,符合要求。
?
?5.2FATFS配置
? ? ? ? 首先选择SD_Card选项,然后下面会出现一个选择列表。选择列表中,其余保持默认,将红色框中选择设置即可。这个设置可以使得文件系统可以识别中文名称的文件。同时,由于存储在了系统栈中,所以需要加大系统栈的大小设置如图所示。
?????????实测表示,堆大小可以不设置,保持默认,但是栈大小需要加大。
? ? ? ? 最后配置FATFS底层读写使用DMA,如图所示。这个选项在使用FreeRTOS时,必须选择DMA。
?
?5.3Cache配置
? ? ? ? 在”系统内核“设置大类中选择”CORTEX_M7"内核设置,设置ICache和DCache都使能,使能后,DCache默认设置为回写模式。?
?5.4修改sd_diskio.c文件
首先增加宏定义:
#define ENABLE_SCRATCH_BUFFER 1
#define ENABLE_SD_DMA_CACHE_MAINTENANCE 1
????????否则,因为开了Cache所以如果不添加这些宏定义在读写时条件编译不会编译Cache同步数据的代码,功能直接无法实现。其次,注意第二个红色框中定义的scartch数组,这个数组时为了应对读写时使用的缓冲区没有进行地址对齐处理时的补救方案。由于SDIO的自带DMA是字为单位传输的,所以使用DMA缓冲区应当4字节对齐。由于Cortex-M7的D-Cache一行是32字节,所以使用Cache时缓冲区应当32字节对齐。
? ? ? ? 这个补救方案原本是特殊情况使用的,然而就在FATFS文件系统初始化后,挂载磁盘时在读取SD卡的第一扇区、文件系统第一扇区等基本信息使用的缓冲区被定义在类型名为FATFS的结构体中,该结构体定义如下
/* File system object structure (FATFS) */
typedef struct {
BYTE fs_type; /* File system type (0:N/A) */
BYTE drv; /* Physical drive number */
BYTE n_fats; /* Number of FATs (1 or 2) */
BYTE wflag; /* win[] flag (b0:dirty) */
BYTE fsi_flag; /* FSINFO flags (b7:disabled, b0:dirty) */
WORD id; /* File system mount ID */
WORD n_rootdir; /* Number of root directory entries (FAT12/16) */
WORD csize; /* Cluster size [sectors] */
#if _MAX_SS != _MIN_SS
WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */
#endif
#if _USE_LFN != 0
WCHAR* lfnbuf; /* LFN working buffer */
#endif
#if _FS_EXFAT
BYTE* dirbuf; /* Directory entry block scratchpad buffer */
#endif
#if _FS_REENTRANT
_SYNC_t sobj; /* Identifier of sync object */
#endif
#if !_FS_READONLY
DWORD last_clst; /* Last allocated cluster */
DWORD free_clst; /* Number of free clusters */
#endif
#if _FS_RPATH != 0
DWORD cdir; /* Current directory start cluster (0:root) */
#if _FS_EXFAT
DWORD cdc_scl; /* Containing directory start cluster (invalid when cdir is 0) */
DWORD cdc_size; /* b31-b8:Size of containing directory, b7-b0: Chain status */
DWORD cdc_ofs; /* Offset in the containing directory (invalid when cdir is 0) */
#endif
#endif
DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */
DWORD fsize; /* Size of an FAT [sectors] */
DWORD volbase; /* Volume base sector */
DWORD fatbase; /* FAT base sector */
DWORD dirbase; /* Root directory base sector/cluster */
DWORD database; /* Data base sector */
DWORD winsect; /* Current sector appearing in the win[] */
BYTE win[_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */
} FATFS;
? ? ? ? 该缓冲区正是该结构体位于最后的数组win[_MAS_SS],即使定义结构体变量时使用32位字节对齐也不能保证这个数组的首地址32字节对齐,故需要使用之前介绍的那种方法规避这个问题。
?5.5编写测试代码及测试
#include <string.h>
char filename[] = "STM32H743_SDMMC_TEST.txt";
char wtext[] = "SUPER IDIL 的笑容都没你的甜,八月的阳光都没你耀眼爱上,爱上105度的你,还有纯净的蒸馏水";
char rtext[100];
void Fatfs_RW_test(void)
{
uint32_t fre_clust, fre_sect=0, tot_sect=0;
uint32_t write_count;
uint32_t read_count;
uint32_t br;
FATFS *fs1;
retSD = f_mount(&SDFatFS, (TCHAR const *)SDPath, 1);
SCB_CleanDCache();
if(retSD){
printf("mount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("mount success!!! \r\n");
}
retSD = f_getfree((const TCHAR*)SDPath, (DWORD*)&fre_clust, &fs1);
if(retSD==0){
tot_sect = (SDFatFS.n_fatent-2)*fs1->csize;
fre_sect = fre_clust*fs1->csize;
printf("total:%dMB, free:%dMB\r\n", tot_sect>>11, fre_sect>>11);
}
FIL fil;
FILINFO finfo;
DIR fdir;
retSD = f_open(&fil, filename, FA_CREATE_ALWAYS|FA_WRITE);
if(retSD){
printf("open error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("open file success!!!\r\n");
}
memset(rtext, 0X00, sizeof(rtext));
retSD = f_write(&fil, wtext, sizeof(wtext), (void *)&write_count);
if(retSD){
printf("write error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("write file success!!!\r\n");
}
f_close(&fil);
if(retSD){
printf("mount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("close success!!!\r\n");
}
retSD = f_open(&fil, "SD卡测试文档.txt", FA_READ);
if(retSD){
printf("open error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("open file success!!!\r\n");
}
memset(rtext, 0X00, sizeof(rtext));
retSD = f_read(&fil, rtext, 50, &br);
if(retSD){
printf("read error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("read file %s success!!!\r\n", "SD卡测试.txt");
printf("%s\r\n", rtext);
}
f_close(&fil);
if(retSD){
printf("mount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("close success!!!\r\n");
}
retSD = f_open(&fil, "SD_TEST_TEXT.txt", FA_READ);
if(retSD){
printf("open error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("open file success!!!\r\n");
}
memset(rtext, 0X00, sizeof(rtext));
retSD = f_read(&fil, rtext, 50, &br);
if(retSD){
printf("read error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("read file %s success!!!\r\n", "SD_TEST_TEXT.txt");
printf("%s\r\n", rtext);
}
f_close(&fil);
if(retSD){
printf("mount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("close success!!!\r\n");
}
retSD = f_open(&fil, filename, FA_READ);
if(retSD){
printf("open error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("open file success!!!\r\n");
}
memset(rtext, 0X00, sizeof(rtext));
retSD = f_read(&fil, rtext, sizeof(rtext), &br);
if(retSD){
printf("read error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("read file %s success!!!\r\n", filename);
printf("%s\r\n", rtext);
}
f_close(&fil);
if(retSD){
printf("mount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("close success!!!\r\n");
}
retSD = f_mount(NULL, (TCHAR const *)SDPath, 1);
if(retSD){
printf("UNmount error: %d,%s\r\n", retSD,FR_Table[retSD]);
} else{
printf("UNmount success!!! \r\n");
}
}
????????测试代码的内容分别为:1挂载磁盘 2选择SD卡的总大小和剩余大小?3创建文件STM32H743_SDMMC_TEST.txt,若已经存在则覆盖? ?4写入STM32H743_SDMMC_TEST.txt文件内容 5关闭STM32H743_SDMMC_TEST.txt文件 6英文名称文件读取测试? 7中文文件名文件读写测试? 8读取STM32H743_SDMMC_TEST.txt内容? 9卸载磁盘
? ? ? ? 然而在测试时,发现发生了错误,如下图
在挂载磁盘后,打开文件时会报错,错误代号为2。
????????不过在后续的查证中发现,导致该错误的原因根本不是文件系统出问题了,正是因为D-Cache同步时出了错误,导致关键信息丢失。
? ? ? ? 经过仔细琢磨sd_disk.c文件的SD_read函数,发现该函数编写时有问题。该函数代码如下:
/* USER CODE BEGIN beforeReadSection */
/* can be used to modify previous code / undefine following code / add new code */
/* USER CODE END beforeReadSection */
/**
* @brief Reads Sector(s)
* @param lun : not used
* @param *buff: Data buffer to store read data
* @param sector: Sector address (LBA)
* @param count: Number of sectors to read (1..128)
* @retval DRESULT: Operation result
*/
DRESULT SD_read(BYTE lun, BYTE *buff, DWORD sector, UINT count)
{
DRESULT res = RES_ERROR;
uint32_t timeout;
#if defined(ENABLE_SCRATCH_BUFFER)
uint8_t ret;
#endif
#if (ENABLE_SD_DMA_CACHE_MAINTENANCE == 1)
uint32_t alignedAddr;
#endif
/*
* ensure the SDCard is ready for a new operation
*/
if (SD_CheckStatusWithTimeout(SD_TIMEOUT) < 0)
{
return res;
}
#if defined(ENABLE_SCRATCH_BUFFER)
if (!((uint32_t)buff & 0x1f))
{
#endif
if(BSP_SD_ReadBlocks_DMA((uint32_t*)buff,
(uint32_t) (sector),
count) == MSD_OK)
{
ReadStatus = 0;
/* Wait that the reading process is completed or a timeout occurs */
timeout = HAL_GetTick();
while((ReadStatus == 0) && ((HAL_GetTick() - timeout) < SD_TIMEOUT))
{
}
/* incase of a timeout return error */
if (ReadStatus == 0)
{
res = RES_ERROR;
}
else
{
ReadStatus = 0;
timeout = HAL_GetTick();
while((HAL_GetTick() - timeout) < SD_TIMEOUT)
{
if (BSP_SD_GetCardState() == SD_TRANSFER_OK)
{
res = RES_OK;
#if (ENABLE_SD_DMA_CACHE_MAINTENANCE == 1)
/*
the SCB_InvalidateDCache_by_Addr() requires a 32-Byte aligned address,
adjust the address and the D-Cache size to invalidate accordingly.
*/
alignedAddr = (uint32_t)buff & ~0x1F;
SCB_InvalidateDCache_by_Addr((uint32_t*)alignedAddr, count*BLOCKSIZE + ((uint32_t)buff - alignedAddr));
#endif
break;
}
}
}
}
#if defined(ENABLE_SCRATCH_BUFFER)
}
else
{
/* Slow path, fetch each sector a part and memcpy to destination buffer */
int i;
for (i = 0; i < count; i++) {
ret = BSP_SD_ReadBlocks_DMA((uint32_t*)scratch, (uint32_t)sector++, 1);
if (ret == MSD_OK) {
/* wait until the read is successful or a timeout occurs */
timeout = HAL_GetTick();
while((ReadStatus == 0) && ((HAL_GetTick() - timeout) < SD_TIMEOUT))
{
}
if (ReadStatus == 0)
{
res = RES_ERROR;
break;
}
ReadStatus = 0;
#if (ENABLE_SD_DMA_CACHE_MAINTENANCE == 1)
/*
*
* invalidate the scratch buffer before the next read to get the actual data instead of the cached one
*/
SCB_InvalidateDCache_by_Addr((uint32_t*)scratch, BLOCKSIZE);
#endif
memcpy(buff, scratch, BLOCKSIZE);
buff += BLOCKSIZE;
}
else
{
break;
}
}
if ((i == count) && (ret == MSD_OK))
res = RES_OK;
}
#endif
return res;
}
? ? ? ? 该代码主要由两个分支构成,一个分支是buff缓冲区地址4字节对齐的情况,另一种情况是buff缓冲区不是4字节对齐的情况。然而在此处其实忘记考虑使用D-Cache时需要32字节对齐的情况,故如果实际当中buff不是32字节对齐存放的,而只满足4字节对齐存放时,问题就出现了。例如下图情况:
? ? ? ? 此图是上文提到过的FATFS结构体在运行时的一个实例,该实例中数组win的地址为0X24000414该地址是四字节对齐的,但是不是32字节对齐的,于是在同步D-Cache数据时紧挨着win数组的几个变量数值会发生错误。实际测试发现从volbase到winsect的数值都是错的,这就是导致打开文件时报错的直接原因。而其根本原因知识同步D-Cache时没有考虑好字节对齐的问题。
?5.6问题解决
????????
? ? ? ? 直接将判断是否4字节对齐的语句改为判断是否32字节对齐。因为如果没有4字节对齐,那么必定没有32字节对齐。
?5.7改正后测试结果
????????从串口打印的结果可以看出,可以正常写入文件并能够正常读出。其次也可以正常打开中文文件名的文件,但是由于该文件使用Unicode编码,而串口助手以GB2312编码解码,所以不能正常显示。
? ? ? ? 最后,是将SD卡连接到电脑上后看到由单片机写入的文件
?6.后续
? ? ? ? 接下来,在FreeRTOS操作系统上实现这些功能!
?
?
?
?
|