单片机 GUI 设计(十)- 文件系统
单片机 GUI 设计(十)- 文件系统

单片机 GUI 设计(十)- 文件系统

基于 GD32F303ZET6 苹果派开发板

简介

对于简单的项目,如果涉及的文件数量不是很多,那么可以简单的将文件按照二进制的形式固化到 Flash 中。对于文件数量很多,或者文件很大的情况下,这个方法往往不适用。例如 24×24 的 GBK 中文字库,它的大小有 1684KB,而苹果派使用的主控,GD32F303ZET6,它的 Flash 只有 512KB,完全存不下。稍微复杂一点的项目,涉及到的图片可能有几十张甚至上百张。

文件系统可以搭配 SD 卡、NandFlash 等存储介质使用。SD 卡很便宜,且存储空间很大,文件系统的引入使得单片机可以轻松获取几十个 G 的储存空间,不再担心储存空间不够。对于单片机下的 GUI 开发,文件系统的使用是必备技能。

文件系统库有很多,在这里我们选择主流、通用的 FatFS。FatFS 是一个完全由 C 语言实现的文件系统库,兼容 Fat32、Fat16 等格式的文件系统,具有文件读写、格式化磁盘等功能。我们日常生活中使用的 U 盘、SD 卡等储存设备大多属于 Fat32 格式,如下所示。使用 FatFS,我们可以很轻松的访问 SD 卡中的文件。因为是用 C 语言写的,FatFS 拥有极高的兼容性,可以很方便的移植到各个平台。当然,读者也可以选用其它的文件系统库。

对于文件系统的移植、SD 卡底层驱动,读者可以参考 GD32F303 进阶版教材,再次不过多介绍。本系列提供的工程中已经做好了移植,读者只需要知道该怎么用就行。

短文件名和长文件名

文件系统的使用中,有个很重要的概念是短文件名和长文件名。短文件名下,文件名最大长度是 7 个字节,后缀最长是 3 个字节,长文件名下则没有限制。对于 FatFS,默认只支持短文件名,但是可以通过修改配置文件实现支持长文件名。

一旦使能了长文件名,因为要做 Unicode 转 GBK,FatFS 会将一个超过 100KB 转换表保存到单片机的 Flash 中,这就导致了编译出来的程序很大,单片机烧写速度明显变慢。当然,后边我们会介绍如何导出这张表到文件中,用文件系统存储这张表。本文中移植好的 FatFS 默认开启了长文件名,读者可视情况关闭使能。

盘符

对于本文中使用的文件系统,SD 卡的盘符是 “0:”,NandFlash 的盘符是 “1:”,USB,即 U 盘的盘符是 “2:” 。如果我们要访问 SD 卡根目录下 BMP 文件夹中的 background.bmp 文件,那么该文件的路径是 “0:/BMP/background.bmp” 。

挂载文件系统示例

FatFS 中,挂载 SD 卡的示例如下,如果需要挂载 NandFlash 或 U 盘,只需要将 FS_VOLUME_SD 修改成 FS_VOLUME_NAND 或 FS_VOLUME_USB即可。注意:FATFS 类型的变量需要设置成静态变量。如果需要卸载文件系统,只需要将第一个参数设为 NULL 即可。另外,本系列提供的源码中并未包含读写 U 盘相关驱动,所以并不能挂载 U 盘。

static FATFS s_structFatFS;
FRESULT result;
result = f_mount(&s_structFatFS, FS_VOLUME_SD, 1);
if(FR_OK != result)
{
    printf("MountSDCard: Mount SD card fail!!!\r\n");
}
else
{
  printf("MountSDCard: Mount SD card success\r\n");
}

读取文件示例

FatFS 中,读取文件示例如下。参数 readPos 即为需要读取的节点与文件开头的偏移量,单位是字节。在 FatFS 中,设置偏移量时尽量 4 字节对齐,否则很容易造成卡死。参数 len 即为需要读取的数据量,单位通常是字节。

unsigned int ReadFile(const char* fileName, unsigned int readPos, void* readBuf, unsigned int len)
{
  static FIL s_structFile
  FRESULT result;
  unsigned int readNum;

  //打开文件
  result = f_open(&s_structFile, (const TCHAR*)fileName, FA_READ);
  if(FR_OK != result)
  {
    printf("ReadFile: fail to open file %s\r\n", fileName);
    return 0;
  }

  //设置文件读取位置
  result = f_lseek(&s_structFile, readPos);
  if(FR_OK != result)
  {
    printf("ReadFile: fail to set pos to file %s\r\n", fileName);
    return 0;
  }

  //读取数据
  result = f_read(&s_structFile, readBuf, len, &readNum);
  if(FR_OK != result)
  {
    printf("ReadFile: fail to read data from file %s\r\n", fileName);
    return 0;
  }

  //关闭文件
  f_close(&s_structFile);

  //返回实际读到的数据量
  return readNum;
} 

写入文件

FatFS 不仅可以读取文件数据,也能创建,并写入文件。FatFS 创建的文件在电脑端也能被读取。参数 len 即为需要写入的数据量,以字节为单位。注意:写入数据后,必须要调用 f_close 函数关闭文件,该文件才会被真正保存到文件系统。

unsigned int WriteFile(const char* fileName, void* writeBuf, unsigned int len)
{
  static FIL s_structFile
  FRESULT result;
  unsigned int writeNum;

  //创建文件,如果该文件已经存在,那么就覆盖该文件
  result = f_open(&s_structFile, (const TCHAR*)fileName, FA_CREATE_ALWAYS | FA_WRITE);
  if(FR_OK != result)
  {
    printf("WriteFile: fail to create file %s\r\n", fileName);
    return 0;
  }

  //写入数据
  result = f_write(&s_structFile, writeBuf, len, &writeNum);
  if(FR_OK != result)
  {
    printf("WriteFile: fail to write data to file %s\r\n", fileName);
    return 0;
  }

  //保存
  f_close(&s_structFile);

  //返回实际写入的数据量
  return writeNum;
} 

完善 BMP 驱动

对于 JPEG 和 PNG 图片,因为它们本身就是压缩过的,文件比较小,所以显示文件系统中的 JPEG 和 PNG 图片时,可以直接将整个文件读入内存中,然后调用以前写的绘制函数绘制图片。对于 ARM 架构的芯片,Flash 和 SRAM 的地址是统一分配的,没有地址重叠的部分,这一点与 51 单片机区别很大。

BMP,即位图文件,是未经压缩过的。假设屏幕尺寸为 800×480,现在要用一张 BMP 图片来做为背景图片。如果该图片是 ARGB8888 格式的,那么总共占据的内存空间大小为 800x480x4=1536000B,即 1.5MB 左右。然而苹果派开发板外拓的 SRAM 才 1MB,再加上内部 SRAM 的 64KB,也存不下一整张图片。为了显示这张位图,我们只能是修改 BMP 驱动,按行读入 BMP 图片数据并显示。因为一次只需要读一行,所以内存是够用的。

当然,做为背景图片,因为本身就不需要透明度信息,所以我们也可以使用 RGB888 格式的 BMP 图片,这样的话背景图片只占据 800x480x3=1152000B,也就是 1.1MB 存储空间,一下子就缩小了四分之一。如果 LCD 驱动使用的是 RGB565 像素点格式,那么背景图片还可以进一步缩小,直接使用 RGB565 格式,这样只需要占据 800x480x2=768000B,即 768KB 内存空间,直接将整张背景图片读入外拓 SRAM 也不是不行。图片占据的存储空间越小,FatFS 读取文件的速度就越快,刷图的速度也随之加快。

为了驱动的通用性,我们还是需要修改一下 BMP 的底层驱动,添加一个显示文件系统中的 BMP 图片函数,如下所示。DrawBMPInFatFS 函数按行读入 BMP 图片的像素点信息,然后更新到屏幕显示。

ForceReadByNameWithCopy 函数用于强制从文件系统中读取数据。因为 SD 卡不是焊死在电路板上的,数据传输过程中随时有可能会脱落,ForceReadByNameWithCopy 函数会在读取失败后,重新挂载文件系统,然后再打开文件,再次尝试读取,直至最终读取成功。

void DrawBMPInFatFS(const char* img, int x, int y)
{
  StructBmpFileHeader* fileHeader; //文件头
  StructBmpInfoHeader* infoHeader; //信息头
  int x0, y0, x1, y1, width, height;
  unsigned int a, r, g, b;
  unsigned short color, backgroung;
  unsigned char* fileHeadBuf;
  unsigned char* readBuf;
  unsigned int readSize;
  unsigned int lineCnt;
  unsigned int readCnt;
  
  //统计文件头和信息头的大小总和
  readSize = sizeof(StructBmpFileHeader) + sizeof(StructBmpInfoHeader);
  
  //为读取缓冲区申请动态内存
  fileHeadBuf = MyMalloc(SRAMIN, readSize);
  if(NULL == fileHeadBuf)
  {
    printf("DrawBMPInFatFS: Fail to malloc for BMP file head\r\n");
    while(1){}
  }
  
  //读取文件头和信息头
  ForceReadByNameWithCopy(0, fileHeadBuf, readSize, (void*)img);

  //获取图片宽度和高度
  infoHeader = (StructBmpInfoHeader*)(fileHeadBuf + 14);
  width = infoHeader->width;
  height = infoHeader->height;

  //计算起点和终点
  x0 = x;
  y0 = y;
  x1 = x0 + width - 1;
  y1 = y0 + height - 1;

  //获取信息头
  fileHeader = (StructBmpFileHeader*)fileHeadBuf;
  
  //16 位位图解码
  if(16 == infoHeader->colorSize)
  {
    //计算一行的数据量
    readSize = 2 * width;
    while(0 != (readSize % 4)){readSize++;}
    
    //为读取缓冲区申请动态内存
    readBuf = MyMalloc(SRAMIN, readSize);
    if(NULL == readBuf)
    {
      printf("DrawBMPInFatFS: Fail to malloc for read buf\r\n");
      while(1){}
    }
    
    //行计数清零
    lineCnt = 0;
    
    //按行显示
    for(y = y1; y >= y0; y--)
    {
      //读取一整行数据
      ForceReadByNameWithCopy(fileHeader->offBits + lineCnt * readSize, readBuf, readSize, (void*)img);
      
      //读取计数清零
      readCnt = 0;
      
      //一个一个像素点显示
      for(x = x0; x <= x1; x++)
      {
        //获取像素点数据
        color = (readBuf[readCnt + 1] << 8) | readBuf[readCnt + 0];
        
        //更新计数
        readCnt = readCnt + 2;

        //没有透明度信息,直接覆盖显示
        LCDFastDrawPoint(x, y, color);
      }
      
      //行计数加一
      lineCnt++;
    }
  }
  
  //24 位位图解码
  else if(24 == infoHeader->colorSize)
  {
    //计算一行的数据量
    readSize = 3 * width;
    while(0 != (readSize % 4)){readSize++;}
    
    //为读取缓冲区申请动态内存
    readBuf = MyMalloc(SRAMIN, readSize);
    if(NULL == readBuf)
    {
      printf("DrawBMPInFatFS: Fail to malloc for read buf\r\n");
      while(1){}
    }
    
    //行计数清零
    lineCnt = 0;
    
    //按行显示
    for(y = y1; y >= y0; y--)
    {
      //读取一整行数据
      ForceReadByNameWithCopy(fileHeader->offBits + lineCnt * readSize, readBuf, readSize, (void*)img);
      
      //读取计数清零
      readCnt = 0;
      
      //一个一个像素点显示
      for(x = x0; x <= x1; x++)
      {
        //获取像素点数据,右移是为了方便后续转成 RGB565 格式
        b = readBuf[readCnt++] >> 3; //蓝色
        g = readBuf[readCnt++] >> 2; //绿色
        r = readBuf[readCnt++] >> 3; //红色

        //RGB888 转 RGB565
        color = ((r << 11) | (g << 5) | (b << 0));

        //没有透明度信息,直接覆盖显示
        LCDFastDrawPoint(x, y, color);
      }
      
      //行计数加一
      lineCnt++;
    }
  }

  //32 位位图解码
  else if(32 == infoHeader->colorSize)
  {
    //计算一行的数据量
    readSize = 4 * width;

    //为读取缓冲区申请动态内存
    readBuf = MyMalloc(SRAMIN, readSize);
    if(NULL == readBuf)
    {
      printf("DrawBMPInFatFS: Fail to malloc for read buf\r\n");
      while(1){}
    }
    
    //行计数清零
    lineCnt = 0;
    
    //按行显示
    for(y = y1; y >= y0; y--)
    {
      //读取一整行数据
      ForceReadByNameWithCopy(fileHeader->offBits + lineCnt * readSize, readBuf, readSize, (void*)img);
      
      //读取计数清零
      readCnt = 0;
      
      //一个一个像素点显示
      for(x = x0; x <= x1; x++)
      {
        //获取像素点数据
        b = readBuf[readCnt++]; //蓝色
        g = readBuf[readCnt++]; //绿色
        r = readBuf[readCnt++]; //红色
        a = readBuf[readCnt++]; //透明度

        //读取背景颜色
        backgroung = LCDReadPoint(x, y);

        //透明度叠加
        color = CalcAlphaRGB565(backgroung, r, g, b, a);

        //显示
        LCDFastDrawPoint(x, y, color);
      }
      
      //行计数加一
      lineCnt++;
    }
  }
  
  //释放内存
  MyFree(fileHeadBuf);
  MyFree(readBuf);
}

实验结果

实验结果如下所示。

源码

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