单片机 GUI 设计(十三)- 将文件复制到 NandFlash
单片机 GUI 设计(十三)- 将文件复制到 NandFlash

单片机 GUI 设计(十三)- 将文件复制到 NandFlash

基于 GD32F303ZET6 苹果派开发板

简介

如果硬件系统中有多余的存储介质,例如 Nand Flash,SPI Flash 等,那么我们也可以选择将 SD 卡中的文件复制到这些存储介质,SD 卡仅仅只是用于更新系统。本文以将文件复制到 Nand Flash 为例。

将文件复制到 Nand Flash 时,可以在 Nand Flash 中创建一个文件系统,由 FatFS 负责读写数据;也可以不借助文件系统,直接将文件数据写入到 Nand Flash 中,然后由一张表来记录每个文件在 Nand Flash 中的位置、大小、修改日期等信息。为了适用性更广,本文中用 FatFS 来管理 Nand Flash,即在 Nand Flash 中创建文件系统,然后由 FatFS 负责读写数据。

文件拷贝

简单的文件拷贝驱动如下所示。首先要将 SD 卡中文件的数据加载到内存中,一般是外部 SRAM,然后再调用 f_write 函数将数据写入 Nand Flash 即可。为了能拷贝大文件,CopyFileByName 函数并没有选择一次性加载整个文件到内存中,而是分段读取,依次写入 Nand Flash。

注意:这里并没有在 Nand Flash 中建立文件夹,所有文件默认拷贝到 Nand Flash 的根目录下。(写代码时有点懒,后边不想改了)

static void CopyFileByName(char* file)
{
  FIL*           nandFile;         //NandFlash中的文件
  FRESULT        result;           //文件操作返回变量
  unsigned char* dataBuf;          //数据缓冲区
  unsigned char* newFileName;      //新文件名字
  unsigned int   readNum;          //成功读取的字节数
  unsigned int   writeNum;         //成功写入的字节数
  unsigned int   fileSize;         //文件大小
  unsigned int   byteRemain;       //拷贝剩余数据量
  unsigned int   readAddr;         //读取地址

  //输出当前文件名
  printf("CopyFileByName: Copy file %s\r\n", file);

  //申请内存
  nandFile    = MyMalloc(SRAMIN, sizeof(FIL));
  dataBuf     = MyMalloc(SRAMEX, COPY_BYTE_NUM);
  newFileName = MyMalloc(SRAMIN, _MAX_LFN);
  if((NULL == nandFile) || (NULL == dataBuf) || (NULL == newFileName))
  {
    MyFree(nandFile);
    MyFree(dataBuf);
    MyFree(newFileName);
    printf("CopyFileByName: Fail to malloc\r\n");
    while(1){}
  }

  //生成新的文件名
  newFileName[0] = 0;
  StringAdd((char*)newFileName, "1:/");
  StringAdd((char*)newFileName, GetFileName(file));

  //在NandFlash中创建文件
  result = f_open(nandFile, (const TCHAR*)newFileName, FA_CREATE_ALWAYS | FA_WRITE);
  if(FR_OK != result)
  {
    printf("CopyFileByName: Fail to create %s in Nand Flash\r\n", GetFileName(file));
    while(1){}
  }

  //获取文件大小
  fileSize = ForceGetFileSize(file);
  byteRemain = fileSize;

  //从文件起始地址开始读取
  readAddr = 0;

  //循环读取SD卡文件数据并写入Nand Flash中
  while(0 != byteRemain)
  {
    //从SD卡文件中读取数据
    if(byteRemain >= COPY_BYTE_NUM)
    {
      ForceReadByNameWithCopy(readAddr, dataBuf, COPY_BYTE_NUM, file);
      readAddr = readAddr + COPY_BYTE_NUM;
      byteRemain = byteRemain - COPY_BYTE_NUM;
      readNum = COPY_BYTE_NUM;
    }
    else
    {
      ForceReadByNameWithCopy(readAddr, dataBuf, byteRemain, file);
      readAddr = readAddr + byteRemain;
      readNum = byteRemain;
      byteRemain = 0;
    }
    
    //将数据写入NandFlash
    result = f_write(nandFile, dataBuf, readNum, &writeNum);
    if((FR_OK != result) || (readNum != writeNum))
    {
      printf("CopyFileByName: Fail to write date to nand flash, please retry!!!");
      f_close(nandFile);
      while(1){}
    }
  }

  //关闭保存文件
  f_close(nandFile);

  //释放内存
  MyFree(nandFile);
  MyFree(dataBuf);
  MyFree(newFileName);
}

拷贝整个目录

我们可以根据需要,一个一个文件的拷贝到 Nand Flash,也可以直接一次性拷贝整个文件夹。有了单个文件的拷贝函数,那么拷贝整个文件夹就简单多了,具体如下所示。首先要获取文件夹下文件数量,然后依次调用 CopyFileByName 函数拷贝即可。

注意:CopyFolderByPath 函数并未对子文件夹做处理,所有并不能拷贝子文件夹下的文件。

static void CopyFolderByPath(char* path)
{
  unsigned int fileNum;      //目录下文件数量
  unsigned int fileCnt;      //文件计数
  char*        name;         //文件名,不含路径
  char*        nameWithPath; //文件名,含路径

  //文件名缓冲区内存申请
  name         = MyMalloc(SRAMIN, _MAX_LFN);
  nameWithPath = MyMalloc(SRAMIN, _MAX_LFN);
  if((NULL == name) || (NULL == nameWithPath))
  {
    MyFree(name);
    MyFree(nameWithPath);
    printf("CopyFolderByPath: fail to malloc\r\n");
    while(1){}
  }
  
  //获取该目录下文件数量
  fileNum = GetFileNumInPath(path);

  //文件计数清零
  fileCnt = 0;

  //循环拷贝所有文件
  while(fileCnt < fileNum)
  {
    //获取指定序号下的文件名
    GetFileNameWithNode(path, fileCnt, name);

    //序号加一
    fileCnt++;

    //将路径添加到文件名中
    nameWithPath[0] = 0;
    StringAdd(nameWithPath, path);
    StringAdd(nameWithPath, "/");
    StringAdd(nameWithPath, name);

    //跳过旧文件
    if(0 == IsNewFile(nameWithPath))
    {
      continue;
    }

    //拷贝文件
    CopyFileByName(nameWithPath);
  }

  //释放内存
  MyFree(name);
  MyFree(nameWithPath);
}

文件的遍历可参考如下代码,知道了如何遍历文件夹,也就知道了如何统计文件夹下文件数量,以及获取指定序号的文件名了。

注意:使能了长文件名后,如果文件的名字过短,那么该文件的文件名有可能只存储在 fileInfo.fname,而不是 fileInfo.lfname 中。

void PrintAllFileNameInPath(char* path)
{
  FRESULT result;        //文件操作返回变量
  DIR     direct;        //路径
  FILINFO fileInfo;      //文件信息
  int     fileCnt;       //文件计数
  
  //文件计数清零
  fileCnt = 0;

  //长文件名内存申请
  #if (1 == _USE_LFN)
    fileInfo.lfname = MyMalloc(SRAMIN, _MAX_LFN);
    fileInfo.lfsize = _MAX_LFN;
    if(NULL == fileInfo.lfname)
    {
      printf("PrintAllFileNameInPath: fail to malloc\r\n");
      while(1){}
    }
  #endif

  //打开特定路径
  while(1)
  {
    result = f_opendir(&direct, path);
    if(result != FR_OK)
    {
      printf("PrintAllFileNameInPath: fail to open path %s\r\n", path);
      printf("PrintAllFileNameInPath: retry...\r\n");
      DelayNms(500);
      ForceMountByName(path);
    }
    else
    {
      break;
    }
  }

  printf("PrintAllFileNameInPath: Start print all file name in %s\r\n", path);

  //查询目录下所有文件
  while(1)
  {
    result = f_readdir(&direct, &fileInfo);
    if(result != FR_OK)
    {
      break;
    }

    //长文件名
    #if _USE_LFN
      if(0 == (AM_DIR & fileInfo.fattrib))
      {
        if(0 != fileInfo.lfname[0])
        {
          fileCnt++;
          printf("%s\r\n", fileInfo.lfname);
        }
        else if(0 != fileInfo.fname[0])
        {
          fileCnt++;
          printf("%s\r\n", fileInfo.fname);
        }
        else
        {
          break;
        }
      }

    //短文件名
    #else
      if(0 == (AM_DIR & fileInfo.fattrib))
      {
        if(0 == fileInfo.fname[0])
        {
          break;
        }
        else
        {
          fileCnt++;
          printf("%s\r\n", fileInfo.fname);
        }
      }

    #endif
  }

  //关闭目录
  result = f_closedir(&direct);
  if(result != FR_OK)
  {
    printf("PrintAllFileNameInPath: Fail to close path (%d)\r\n", result);
  }

  //长文件名内存释放
  #if (1 == _USE_LFN)
    MyFree(fileInfo.lfname);
  #endif

  printf("PrintAllFileNameInPath: file num %d\r\n", fileCnt);
}

文件信息表

为了避免文件重复拷贝,只拷贝那些更新过的文件,我们可以在建立一张文件信息表,里边储存着系统版本号、文件的修改日期等信息,存储在 Nand Flash 中。拷贝文件时,首先检索文件信息表,如果该文件有被更新过,那么就复制该文件;如果该文件没被更新,即 Nand Flash 中的文件就是最新版本,那么就可以选择不复制该文件。如此一来,插入 SD 卡时,就可以避免每次开机都复制文件。

文件信息表的文件头信息以及单个文件的信息如下所示,具体实现在此不过多描述,读者可以参考本系列提供的源码。

//文件头信息
typedef __packed struct
{
  char         flag[4];      //文件标志,固定为 "INFO"
  char         version[256]; //版本信息
  unsigned int fileNum;      //已经记录的文件的数量
  unsigned int fileSize;     //整个文件的大小,字节
  unsigned int dataSize;     //数据域的大小,字节
  unsigned int offSet;       //文件开头到第一个文件信息的偏移量
}StructFileInfoHead;

//单个文件的信息
typedef struct 
{
  char         name[256]; //SD 卡中的文件名,含路径
  unsigned int sdDate;    //SD 卡中的修改日期
}StructFileInfo;

总结

对于简单的应用,文件数量不多,读者完全可以只拷贝指定文件,而不用拷贝整个文件夹。同样的 Nand Flash 中也没必要必须使用文件系统,直接读写速度会更快一些,但不方便管理。

实验结果

实验结果如下所示。

源码

本章节中的源码请参考《单片机 GUI 设计(零)- 大纲