先展示最终实现的功能效果如下:

1.目的与意义
为什么选用SD卡?
使用Nor-flash(W25Q系列)进行图片的存取,需要先把图片通过对应软件批量处理为二进制bin文件,再通过SPI等通讯方式将 bin文件烧写进Nor-flash才能进行使用,使用时还要记住每张图片的首地址和对应字节数,MCU才能准确的读出来并显示,所以在要更换图片或者读取显示图片上会显得十分繁琐麻烦;相对来说,SD(TF)卡虽然价格较贵,但通过SD(TF)卡和读卡器直接连接电脑可以将SD(TF)卡虚拟为U盘,直接往里面拷贝图片即可,更换图片就显得方便简单;
为什么要使用FATFS文件系统?
MCU要跟SD卡之间进行通讯,可以使用SPI和SDIO通讯方式,不移植FATFS文件系统的话也可以像Nor-flash一样通过对地址及扇区字节进行读取,但是此方法较麻烦,为了让MCU可以直接对SD卡内的各类文件格式进行读取识别,所以需要一个相同的文件系统,又由于fatfs系统在现阶段最广泛兼容,且STM32CUBEMX支持移植,所以就选用了该文件系统;
为什么使用Tinyjpeg解码库?
STM32F4系列具有较大的flash和ram,所以可以直接移植LVGL或Emwin图形库对图片格式进行解码,同时STM32F4及以上系列的MCU,STM32CubeMX也已经支持Tinyjpeg解码库的直接移植:![图片[1] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/1037a80cf07149dd94f7c22efba3811f.png)
所以证明Tinyjpeg解码库还是挺受欢迎的;
而STM32H系列价格昂贵,但具有JEPG硬件解码,所以不需要软件解码库;
STM32F1系列作为STM32家族中的廉价产品,其外设及内存肯定也较少,即没有硬件JPEG解码,flash和ram又较小,所以在使用显示屏显示图片时,移植Tinyjpeg库就是比较好的选择了,通过软件多写一点,就能节省MCU的价格,相信大部分人还是愿意做的。
2.使用STM32CubeMX建立工程
这里先给出我使用的TF卡的硬件原理图:
这里我设计成了只要TF卡插入卡槽,LED灯就会被点亮,同时这个CD脚也是后面配置FATFS文件系统要用到的,所以才在这里给出原理图。
通过STM32CubeMX我们要完成创建对SD卡的SDIO通讯,FATFS文件系统的移植,Tinyjpeg则只需要拷贝几个C文件和h文件即可:
1.使能SYS的serial Wire,选择晶振及配置时钟树,这些创建基本工程也都要进行配置,这里我就不具体给出设置参数了,我这边使用的是外部晶振配置为72MHz时钟:

2.配置SD卡的SDIO通讯方式(因为MCU又要从SD卡读取文件,又要将数据发送到TFTLCD进行显示,所以SDIO这里使用DMA方式,减少对MCU线程的占用):
这里配置了SDIO的基本参数,开启4线通讯(对应4个IO口才会使能),然后使能硬件流(看过很多博主都说使能了硬件流之后SD卡初始化成功概率高很多,我自己测试也确实是),最后设置工作频率1Mhz(计算方式为:SDIO的时钟频率/(SDIOCLK clock divide factor+2),通过时钟树可以看到SDIO的时钟频率为36MHz ,如果SD卡通讯失败率很高,则可以再调小频率进行尝试,若改小后效果仍然很差,需检查硬件布线是否存在较大线长差异或线路干扰等);

接下来开启SDIO的DMA通道及中断使能:
由于是从SD卡读取数据到MCU,所以方向选择外设到内存;
![图片[2] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/5337aacd3d604bee847cc5bafe404545.png)

这里设置好后要到中断优先级NVIC里面,将DMA的中断优先级改低,一般DMA的中断优先级都调到比其他重要中断低,防止大规模传输数据时打断其他重要中断:
![图片[3] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/da7ad791210343e2abcba17298773001.png)
这样SDIO就配置完成了。
3.移植FATFS文件系统:

这里也没啥需要进行配置修改的,由于大家都是中国人,难免会用到中文给文件夹起名,所以这里将CODE_PAGE修改为simplified Chinese即可,同时为了避免长文件名出错,所以也使能了USE_LFN使用栈的方式。由于这里我们只用了一个外部存储器(TF卡),所以VOLUMES默认为1即可,操作块(MAX_SS及MIN_SS)为512字节也是默认即可。
然后配置其设备检测IO口(即上面原理图跟CD脚相连的MCU的IO口,低电平触发,所以配置为上拉输入即可):
![图片[4] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/cf24b93800254f7fa848dd38d629e26d.png)
至此SDIO(DMA)跟FATFS文件系统也配置完成。
接下来就是Keil生成工程,这里把堆栈可申请空间都稍微调大至4KB,确保FATFS和Tinyjpeg操作时有足够的空间。
3.对工程进行修改,并测试MCU跟SD卡正常通讯及挂载FATFS系统
打开KEIL工程的main.c文件,找到SDIO初始化的位置将其数据总线改为1位,这里仅是做初始化用(初始化用1位数据总线,400KHz以下频率),初始化完成后程序会切换到4位数据总线:
![图片[5] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/497343f5f5714b769d2676427e48d7a9.png)
![图片[6] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/dc83477ce8054bb39d0c6b0c6f2adfba.png)
接下来编写SD卡的测试函数:
1.配置uart对接printf函数:

#include "stdio.h"
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
PUTCHAR_PROTOTYPE //重定义usart1,之后使用printf()函数将自动通过串口1输出
{
HAL_UART_Transmit(&huart1 , (uint8_t *)&ch, 1, 0xFFFF); //改变&huart1为&huart2可以选择串口2
return ch;
}
然后勾选USE MicroLIB库:

2.读取SD卡的信息:
void SDCard_ShowInfo(void)
{
//SD卡信息结构体变量
HAL_SD_CardInfoTypeDef cardInfo;
HAL_StatusTypeDef res = HAL_SD_GetCardInfo(&hsd, &cardInfo);
if(res!=HAL_OK)
{
printf("HAL_SD_GetCardInfo() error
");
return;
}
printf("
*** HAL_SD_GetCardInfo() info ***
");
printf("Card Type= %d
", cardInfo.CardType);
printf("Card Version= %d
", cardInfo.CardVersion);
printf("Card Class= %d
", cardInfo.Class);
printf("Relative Card Address= %d
", cardInfo.RelCardAdd);
printf("Block Count= %d
", cardInfo.BlockNbr);
printf("Block Size(Bytes)= %d
", cardInfo.BlockSize);
printf("LogiBlockCount= %d
", cardInfo.LogBlockNbr);
printf("LogiBlockSize(Bytes)= %d
", cardInfo.LogBlockSize);
printf("SD Card Capacity(MB)= %d
", cardInfo.BlockNbr>>1>>10);
}
如果SD卡初始化成功且被正确读取,通过串口可以看到以下信息:

3.挂载FATFS文件系统并测试其创建、打开、读取、关闭功能等是否正常:
void SD_mount_text(void)
{
uint32_t byteswritten; /* File write counts */
uint32_t bytesread; /* File read counts */
uint8_t wtext[] = "This is STM32 working with FatFs"; /* File write buffer */
uint8_t rtext[100]; /* File read buffers */
char filename[] = "STM32cube.txt";
char SensorBuff[100];
printf("********* STM32CubeMX FatFs Example *********
");
if(f_mount(&SDFatFS,SDPath,1) == FR_OK){
printf("f_mount sucess!!!
");
if(f_open(&SDFile,filename,FA_CREATE_ALWAYS|FA_WRITE) == FR_OK){
printf("f_open file sucess!!!
");
if(f_write(&SDFile,wtext,sizeof(wtext),&byteswritten) == FR_OK){
printf("f_write file sucess!!!
");
printf("f_write Data : %s
",wtext);
if(f_close(&SDFile) == FR_OK)
printf("f_close sucess!!!
");
else
printf("f_close error : %d
",retSD);
}
else
printf("f_write file error
");
}
else
printf("f_open file error
");
}else
printf("f_mount error : %d
",retSD);
retSD = f_open(&SDFile, filename, FA_READ);
if(retSD)
printf("f_open file error : %d
",retSD);
else
printf("f_open file sucess!!!
");
retSD = f_read(&SDFile, rtext, sizeof(rtext), (UINT*)&bytesread);
if(retSD)
printf("f_read error!!! %d
",retSD);
else{
printf("f_read sucess!!!
");
printf("f_read Data : %s
",rtext);
}
retSD = f_close(&SDFile);
if(retSD)
printf("f_close error!!! %d
",retSD);
else
printf("f_close sucess!!!
");
if(bytesread == byteswritten)
printf("FatFs is working well!!!
");
if(f_open(&SDFile,(const char*)"Sensor.csv",FA_CREATE_ALWAYS|FA_WRITE) == FR_OK){
printf("Sensor.csv was opened/created!!!
");
sprintf(SensorBuff, "Item,Temp,Humi,Light
");
f_write(&SDFile,SensorBuff,strlen(SensorBuff),&byteswritten);
for(int i = 0; i < 10; i++){
sprintf(SensorBuff, "%d,%d,%d,%d
",i + 1, i + 20, i + 30, i + 40);
f_write(&SDFile,SensorBuff,strlen(SensorBuff),&byteswritten);
f_sync(&SDFile);
}
f_close(&SDFile);
}
}
如果FATFS被成功挂载,且通讯数据稳定,可以看到串口输出以下信息:

总的main函数初始化如下:

4.移植Tinyjpeg解码库
总共需要这5个文件(后面我会给出链接提供下载),在工程里面新建一个组,然后将两个C文件添加进去,文件路径也进行包含:
![图片[7] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/32128a4ce51f4ecfb9cb903b306a4a20.png)
![图片[8] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/04706cf50fb74c8db1076cf68722c0fa.png)
这里面我也添加了很多串口输出作为测试,可以在遇到问题时定位到问题出现在哪一步:

比如上图程序,串口接收到success1代表成功打开文件,接收到success2代表成功读取了文件信息,同时可以输出文件的长宽像素来判断是否正确读取到了图片的数据信息,接收到success3代表成功将文件数据处理完成并发出;

在解码库的输出回调函数里面,由于该JPEG解码库我设置了按16X16像素矩阵去扫描,所以每次输出回调函数可以输出16X16个像素点的RGB值,可以通过串口查看其RGB值是否正确。
void Debug_PrintRGB565(uint16_t* rgb565_buf, JRECT* rect, int width, int height) {
printf("
----- RGB565 Block @(%d,%d)-(%d,%d) -----
",
rect->left, rect->top, rect->right, rect->bottom);
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
uint16_t val = rgb565_buf[y * width + x];
// ´ÓRGB565ÌáÈ¡¸÷·ÖÁ¿
uint8_t r = (val >> 11) & 0x1F;
uint8_t g = (val >> 5) & 0x3F;
uint8_t b = val & 0x1F;
// ת»»Îª8λ±ê×¼RGBÖµ
r = (r << 3) | (r >> 2);
g = (g << 2) | (g >> 4);
b = (b << 3) | (b >> 2);
printf("[%d,%d,%d] ", r, g, b);
}
printf("
");
}
}
Tinyjpeg解码库移植容易遇到的问题:
我在移植时候遇到三个问题:
1.分配给JPEG的运行内存不够;2.RGB颜色不对,3.Tinyjpeg解码库的输出坐标位置与我的画点函数坐标位置不匹配;
第一个问题的现象是串口会报错preprare error:JDR_MEM1,通过对返回值进行查看,其原因为Insufficient memory pool for the image,这时候加大uint16_t work[1024*y]里面y的值即可,一般有4Kb就够用了;
第二个问题的现象就是显示屏显示的颜色与实际图片对不上,其原因是Tinyjpeg经过一系列解码后到达其输出回调函数的RGB888顺序为B、G、R,而我采用的ILI9341显示屏驱动设置的是RGB565格式(在命令为0x3A后面对应写数据0x55为RGB565,写数据0x66为RGB888);这里我们一开始调试的时候可以先导入3张纯色的红、绿、蓝JPG图片到SD卡里面,再通过实际显示出来的RGB值判断是什么颜色反了,然后我在文件中定义了一个结构体分别存储了8位的R、G、B的值,根据显示屏的显示与JPG图片的不同,对这3个值进行位置更换即可:
typedef struct __attribute__((packed)) {
uint8_t r; //
uint8_t g; //
uint8_t b; //
} BGR888_Pixel;
在输出回调函数 UINT STM32_tjd_output()中就会进行对应的切换。
第三个问题的现象如图:
![图片[9] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/b1c8f104615c4373b4272ff440bbed36.png)
这是显示一张纯红色的JPG图出来的实际显示效果,看起来颜色是对了,但是屏幕产生撕裂感,产生白色条纹,一般这种都会认为是申请内存不够导致的画点异常,但是我经过一天的调整各种内存,包括调整ILI9341的初始化都无法完成。最后通过调整这两个函数的坐标匹配解决了,然后就导入一张不纯色的图片进行测试:

![图片[10] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/1aa60ae7e3284238833323f029aeeb85.png)
//
![图片[11] - SD卡+FATFS+Tinyjpeg图片解码显示 (STM32F103VET6通过CubeMX快速建立工程) - 宋马](https://pic.songma.com/blogimg/20250602/69c7350622b04c27a0169ae6630da3c6.png)
就快成功了,这里可以看出来就是自上而下扫描变成自下而上扫描了,所以再翻转一下Y轴扫描即可,正确显示JPG图片:

5.结语
经过测试整屏刷新大概也需要1秒一张图,大概的原因应该是较多时间耗在JPEG软解码的过程中了,如果各位有更好更快的刷屏显示方式也欢迎互相交流学习。
感谢阅读!



















暂无评论内容