基于 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);
}
实验结果
实验结果如下所示。
-文件系统-实验结果-20230304-1024x768.jpg)
源码
本章节中的源码请参考《单片机 GUI 设计(零)- 大纲》