STM32F407与FreeRTOS结合外扩SRAM实现JPEG图片LCD显示

发布时间:2026/9/8 5:20:57
STM32F407与FreeRTOS结合外扩SRAM实现JPEG图片LCD显示 简介面向嵌入式开发者的正点原子探索者STM32开发板工程资源以FreeRTOS实时操作系统为核心整合SRAM运行内存、JPEG图像解码与LCD液晶显示演示如何在STM32平台上完成多任务调度、大容量图片解码与实时显示。压缩包共1449个文件整体约35.61MB包含683个C源码、327个头文件、编译生成的o/d/lst/s文件、IAR与STM32CubeMX工程配置icf/ioc、以及hex/bin/elf等可直接烧录的镜像文件目录结构清晰方便对照学习。资源特别涉及FreeRTOS Heap_5内存管理策略帮助理解嵌入式动态内存分配与防碎片化实现。目前已有872人学习下载适合正在学习STM32FreeRTOS或从事物联网显示终端的开发者参考。通过该工程可快速掌握JPEG解码显示的关键流程、LCD驱动方式并基于完整源码进行二次开发。 把“正点原子探索者 FreeRTOS SRAM JPEG LCD显示图片”这一串关键词放到一起其实是嵌入式学习路上非常典型的一道坎。很多人都想把一张JPEG图片显示到LCD上但一跑起来就遇到黑屏、花屏、HardFault甚至卡死问题基本出在内存规划和任务设计上。这篇就把我实际调通的思路和踩过的坑完整拆一遍。1. 这个项目为什么绕不开外扩SRAM1.1 内存账本768KB的帧缓冲从哪来探索者开发板用的是STM32F407ZGT6这颗芯片内部SRAM共192KB分成两块128KB的主SRAM0x20000000起始和64KB的CCM RAM0x10000000起始。光看数字好像不小但你算算一张LCD全屏图片要占多少内存。探索者的LCD接口最高支持800x480分辨率用RGB565格式存储的话每个像素2字节800 x 480 x 2 768000 字节 750KB一帧全屏图片就要750KB。就算你把分辨率降到480x272480 x 272 x 2 261120 字节 255KB直接把内部192KB全部塞进去都不够更别说你的系统还要跑FreeRTOS任务栈、消息队列、解码工作区全都要内存。所以在F407上做JPEG显示外扩SRAM不是可选项是刚需。1.2 软件解码是硬约束F407没有硬件JPEG单元这里要先说明一个容易混淆的点。STM32F4系列里F429、F469这些型号带了硬件JPEG编解码器DJPEGF407没有。探索者F407做JPEG解码只能用软件解码库——正点原子官方例程用的是TjpgDec。TjpgDec是一个专为嵌入式设计的轻量级JPEG解码器作者是日本一位工程师。它的特点是纯C实现不依赖任何硬件外设RAM占用可以做到非常低支持分块输出解码过程通过回调函数驱动底层输入流支持输出RGB565、RGB888、灰度图等格式因为没有硬件解码所有的霍夫曼解码、反量化、IDCT变换、颜色空间转换全部靠CPU算也就是全部靠软件跑。一颗168MHz的Cortex-M4跑一张800x480的JPEG图解码时间通常在1到3秒之间具体看图片压缩率和内容复杂程度。这个速度决定了后面FreeRTOS的任务设计思路解码是一个CPU密集型的耗时操作不能阻塞整个系统必须放到独立任务里跑还要考虑显示任务在解码期间的调度。2. 硬件链路FSMC接口与1MB SRAM的配合细节2.1 探索者板载IS62WV51216的接线逻辑探索者开发板上有一颗IS62WV51216容量1M x 16bit正好1MB。它挂在F407的FSMC接口上使用的Bank1的第1个片选区NE1映射地址从0x60000000开始。连线关系大概是FSMC_NE1 → SRAM片选CSFSMC_A[18:0] → SRAM地址线A[18:0]19根地址线寻址2^19 512K再乘以16bit位宽正好1MBFSMC_D[15:0] → SRAM数据线FSMC_NOE → SRAM读使能OEFSMC_NWE → SRAM写使能WE要注意FSMC的Bank1支持NOR Flash、PSRAM、SRAM等设备每个Bank又分4个片选区每个片选区对应一段64MB的空间。探索者的SRAM用NE1所以访问地址就是0x60000000到0x60FFFFFF这段。数据手册里FSMC Bank1的地址映射是这样的片选区对应引脚地址范围Bank1 NE1FSMC_NE10x60000000 - 0x6FFFFFFFBank1 NE2FSMC_NE20x64000000 - 0x6FFFFFFFBank1 NE3FSMC_NE30x68000000 - 0x6FFFFFFFBank1 NE4FSMC_NE40x6C000000 - 0x6FFFFFFF在正点原子的例程里操作外部SRAM已经是宏定义好的比如Bank1_SRAM3_ADDR这类地址宏然后用FSMC初始化结构体配置时序参数。2.2 FSMC时序配置以SRAM读周期为准FSMC挂SRAM其实比挂TFT-LCD简单因为SRAM是标准的异步读写时序。关键在FSMC_BCR和FSMC_BTR这两个寄存器FSMC_BCR总线配置寄存器控制存储器类型、位宽、突发模式等FSMC_BTR总线时序寄存器控制读时序FSMC_BWTR总线写时序寄存器控制写时序核心时序参数有四个ADDSET地址建立时间单位HCLK周期ADDHOLD地址保持时间DATAST数据建立时间BUSTURN总线恢复时间对于IS62WV51216这种标准SRAM典型读周期在55ns级别。F407的HCLK如果跑168MHz一个HCLK周期约5.95ns。工程上常见的一组配置如下SRAM_InitStructure.FSMC_AddressSetupTime 0x02; // 地址建立时间 SRAM_InitStructure.FSMC_AddressHoldTime 0x00; // 地址保持时间 SRAM_InitStructure.FSMC_DataSetupTime 0x05; // 数据建立时间 SRAM_InitStructure.FSMC_BusTurnAroundDuration 0x00; // 总线恢复时间 SRAM_InitStructure.FSMC_DataLatency 0; // 不用于SRAM SRAM_InitStructure.FSMC_AccessMode FSMC_AccessMode_A;我实测的感受是ADDSET给2、DATAST给5这个组合比较稳读写都没问题。这里有个容易被忽略的点——写时序和读时序可以分开配置BWTR但如果你没有单独配置写时序寄存器FSMC会按读时序来执行写操作。SRAM的写周期通常比读周期短所以只要读时序配置合理写大概率也没问题。如果配置得过快比如DATAST给1甚至0SRAM的数据线还没稳定FSMC就采样了读出来就是错数据表现就是花屏、颜色错乱、某些区域随机跳变。反过来配置过慢画面能用但刷新速度明显变慢。2.3 字节对齐与MPU没有Cache也不能忽视的细节探索者F407是Cortex-M4内核没有L1 Cache这一点和F429/H7不同。所以网上很多讲外部SRAM的Cache一致性问题的文章在F407上其实用不上。但有两个问题依然要处理。第一个问题是字节序。外部SRAM是16位宽FSMC访问时最小单位是16位。如果你通过uint8_t指针去逐字节写外部SRAMFSMC会自动处理字节选通信号NBL0/NBL1看起来没问题。但如果你用uint32_t批量写就要确保数据是对齐的否则会产生总线错误。最稳妥的做法是所有要放到外部SRAM的缓冲区起始地址按4字节对齐。第二个问题是MPU。虽然F407没有Cache但代码里如果开了MPU外部SRAM区域的MPU配置会影响访问行为。常见的配置是把外部SRAM设为Normal、Non-cacheable、Bufferable或者直接Write-through。如果配置成Strongly-ordered类型虽然也能用但会让编译器生成的优化代码在访问该区域时降低效率。MPU_Region_InitTypeDef MPU_InitStruct; MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0x68000000; // 外部SRAM基地址 MPU_InitStruct.Size MPU_REGION_SIZE_1MB; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number MPU_REGION_NUMBER2; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_ENABLE;我一开始没有单独给外部SRAM配置MPU区域跑小图没问题跑大图时偶尔出现随机错位的情况。后来排查发现FSMC的总线仲裁本身没问题是MPU的Region0默认配置覆盖了全部地址空间属性不合适。单独分配一个Region给外部SRAM区域后问题消失。3. 解码层策略TjpgDec与缓冲区三段式设计3.1 解码器选型对比嵌入式场景JPEG解码常见方案有TjpgDec和libjpeg两大阵营。libjpeg是桌面和服务器领域的事实标准功能全、优化好但代码体积大内存占用高依赖文件I/O层对MCU来说太臃肿。TjpgDec则专门为MCU设计整个库只需要两个源文件tjpgd.c和tjpgd.h通过回调函数机制把输入流抽象化你可以从SD卡读JPEG数据流也可以从内存数组读灵活性极高。从实际体验看TjpgDec在168MHz的F407上表现可以接受库本身的API也非常简单JDEC jdec; JRESULT rc; rc jd_prepare(jdec, in_func, work, sizeof(work), (void*)file); rc jd_decomp(jdec, out_func, 0);核心API就两个jd_prepare负责解析JPEG头、准备解码工作区jd_decomp执行实际解码解码结果通过输出回调函数逐块返回。3.2 三段缓冲区设计现在重点来了外部SRAM的1MB怎么分配直接决定项目成败。我把外部SRAM分成三段用途大小说明JPEG文件输入缓冲100-200KB存放从SD卡读出的压缩图片原始数据TjpgDec工作区约30KB解码过程中的中间数据jd_prepare需要解码输出缓冲RGB565帧缓冲750KB存放解码后的整帧图像数据这种划分有个关键前提TjpgDec的jd_decomp支持分块输出你可以设置JD_SZBUF让输出回调每次返回一小块数据。理论上不需要一整块750KB的帧缓冲可以一边解码一边往LCD刷。但这样做的代价是解码和显示深度耦合如果中间有其他高优先级任务打断LCD刷新时序就不连续可能出现撕裂。我选择的是整图解码到SRAM的帧缓冲然后再从帧缓冲刷新到LCD。虽然解码期间不能同时显示但换来了代码逻辑简单、显示阶段可以独立优化、后续如果要加特效缩放、旋转、淡入淡出也方便。实际分配的时候要注意一个细节TjpgDec的工作区大小可以通过JD_WORK_SIZE宏设置这个值不是越大越好也不一定越小越省。它必须能满足解码器内存需求的上限如果给太小jd_prepare会返回JD_MEMERR。我实测800x480的图工作区给4KB也跑得起来但解码速度明显慢给30KB时速度和稳定性最佳。原理是工作区还要存放霍夫曼表、量化表、位流缓冲等中间数据太小了频繁换入换出反而拖慢速度。3.3 缩放也是一种提速方案TjpgDec支持在解码时直接缩放jd_decomp的输出回调会得到缩放到1/2、1/4、1/8尺寸的画面。这在大图显示场景里非常实用。就拿800x480的原始JPEG来说如果你只是想全屏显示但LCD分辨率只有480x272直接解码800x480再缩小既费时又费内存。改成在jd_decomp设置缩放比例1/2解码输出的就是400x240内存占用直接降到原来的四分之一解码速度也快很多。不过程序里要注意jd_decomp的缩放参数是JRESULT jd_decomp(JDEC *jd, JD_FUNC out_func, UINT out_x, UINT out_y)这个函数里没有直接的缩放参数——实际上TjpgDec的缩放是在jd_prepare阶段通过JD_SCALE宏控制的不同版本API有差异。建议直接看正点原子例程里怎么调的不同版本的函数签名略有不同。4. FreeRTOS任务设计双缓冲流水线与同步机制4.1 任务拆分与职责边界有了外部SRAM的大帧缓冲接下来就是FreeRTOS的任务设计。我采用的是典型的生产者-消费者模式解码任务从SD卡读JPEG文件调用TjpgDec解码到SRAM帧缓冲显示任务从SRAM帧缓冲读取RGB565数据通过FSMC写入LCD GRAM如果用户在显示过程中按下按键切换图片还需要一个输入检测任务或者直接在显示任务里轮询按键。任务拆分的原则是CPU密集型的解码任务是生产者FSMC刷屏是消费者两者通过队列或信号量同步。解码期间其他任务照常运行系统不会卡死。4.2 双缓冲机制这里必须用双缓冲。如果只用单缓冲解码任务往缓冲A写入新图片时显示任务还在从缓冲A刷屏两者会互相踩数据画面就会出现半张旧图半张新图的撕裂现象。双缓冲的思路是解码任务解码到缓冲B解码完成后通知显示任务“缓冲B已就绪”显示任务开始从缓冲B刷屏与此同时解码任务可以开始解码下一张图到缓冲A两个缓冲交替使用互不干扰。// 帧缓冲句柄 #define FRAME_BUF_SIZE (800 * 480 * 2) uint8_t frame_buf[2][FRAME_BUF_SIZE] __attribute__((section(.ARM.__at_0x68000000))); // 空闲缓冲队列记录哪些缓冲可以用 QueueHandle_t xFreeBufQueue; // 就绪缓冲队列记录哪些缓冲已解码完成待显示 QueueHandle_t xReadyBufQueue; // 解码任务 void vJpegDecodeTask(void *pvParameters) { uint8_t *pFreeBuf; uint8_t *pReadyBuf; while (1) { // 等一个空闲缓冲 xQueueReceive(xFreeBufQueue, pFreeBuf, portMAX_DELAY); // 从SD卡读取并解码JPEG到pFreeBuf jpeg_decode_to_buf(pFreeBuf); // 解码完成交给显示任务 xQueueSend(xReadyBufQueue, pFreeBuf, portMAX_DELAY); } } // 显示任务 void vLcdDisplayTask(void *pvParameters) { uint8_t *pFrameBuf; while (1) { // 等解码任务完成 xQueueReceive(xReadyBufQueue, pFrameBuf, portMAX_DELAY); // 刷新到LCD lcd_show_frame(pFrameBuf); // 刷完释放缓冲回到空闲队列 xQueueSend(xFreeBufQueue, pFrameBuf, portMAX_DELAY); } }两个队列的角色很清楚xFreeBufQueue初始时放入两个缓冲的地址表示两个缓冲都可写。解码任务取出一个写入显示任务刷完后又归还一个。xReadyBufQueue只存放解码完成待显示的缓冲。这样还有一个额外好处SD卡读取和JPEG解码被隔离在解码任务里即使SD卡某个扇区读取慢也不会拖累LCD刷新。4.3 优先级设计为什么显示任务优先级更高优先级分配我这样定的任务优先级说明显示任务3高保证刷屏流畅解码任务2中CPU密集但可被抢占空闲任务或输入检测1低显示任务刷屏是个长时间操作刷一屏800x480的时间大概在50ms到100ms量级取决于FSMC时序和LCD控制器。如果解码任务优先级更高解码任务可能持续占着CPU显示任务迟迟得不到调度画面就卡顿。反过来显示任务只在有就绪缓冲时才被唤醒平时阻塞在队列上不会浪费CPU。即使解码任务优先级低它也能在显示任务阻塞期间占满CPU跑解码。一个典型的运行时间线解码任务解码中占用CPU约1.5秒→ 解码完成发送队列 → 显示任务被唤醒开始刷屏占用CPU约80ms→ 刷完发队列继续阻塞 → 解码任务接管CPU解码下一张这样系统整体看起来就是LCD连续显示上一张图后台静默解码下一张整个过程用户无感知卡顿。5. 实测踩坑记录与调优经验5.1 花屏排查链路从纯色填充到逐段定位花屏是这个项目最常碰到的问题。我总结了一套排查链路按顺序走基本能锁定问题根源。第一步先用纯色填充验证外部SRAM读写。程序里对SRAM整段写入、读出、比对比如写0xAA55再读回来。如果这一步都错说明FSMC时序配置或硬件连接有问题跟JPEG、FreeRTOS都没关系。第二步用小尺寸JPEG测解码链路。比如一张32x32的小图解码后直接显示到LCD左上角。如果小图正常、大图花屏问题大概率出在内存越界或缓冲区不足。第三步检查缓冲区是否越界。注意frame_buf数组如果用__attribute__((at()))或section指定到外部SRAM要确认起始地址和结束地址在SRAM范围内不能超过0x60FFFFFF。我之前踩过一次缓冲区定义了两个帧缓冲共1.5MB超过了1MB物理SRAM结果第二部分被写到不存在的地址FSMC读回全F图片下半屏全是雪花噪点。第四步排查字节对齐和memcpy问题。从SD卡读JPEG数据时如果f_read到外部SRAM缓冲区而缓冲区地址不是4字节对齐FATFS在部分配置下会出现字节错位导致JPEG解码器直接返回格式错误。5.2 HardFault与JD_MEMERR的隐藏含义项目中最容易让人心态崩的是不定期HardFault和jd_prepare返回JD_MEMERR。JD_MEMERR字面意思是内存错误但实际触发原因有两个一是工作区真的给太小二是JPEG本身的尺寸、色彩空间超出解码器预期。比如某些相机拍出的JPEG可能是YUV422格式的TjpgDec对YUV444、YUV422、YUV420、灰度图都支持但不同版本的支持程度有细微差别。碰到JD_MEMERR时先用排除法换一张标准测试图比如Photoshop导出的小尺寸YCbCr444 JPEG验证解码器本身没问题再检查自己的缓冲区分配。HardFault则更麻烦。我遇到的一种典型场景是在解码任务里调用FatFS读文件时任务栈不够大。FatFS的f_read内部会频繁调用memcpy和文件系统内部函数如果任务栈只有512字节很容易压栈溢出。排查方法有两个在FreeRTOSConfig.h里开启configCHECK_FOR_STACK_OVERFLOW并实现vApplicationStackOverflowHook硬件上直接看故障发生时的PC指针位置正点原子例程的写法里解码任务栈通常给512到1024字word也就是2KB到4KB。我建议至少1024字稳妥一点给1536字。5.3 提升刷新速度的两个土办法解码之后刷屏速度是另一个体验瓶颈。FSMC写一块800x480的RGB565数据如果是一个像素一个像素循环写时间非常可观。两个改进思路见效最明显。第一个是循环展开和32位宽写。ILI9806这类LCD控制器支持16位/8位并口但FSMC每次访问最小是16位。如果lcd_show_frame里用uint32_t指针一次写两个像素直接把写屏时间砍半。注意LCD控制器的RS引脚接的是FSMC的A1还是A6决定你写GRAM的地址偏移这个在正点原子的LCD驱动里已有处理不要自己乱改。第二个是尽量利用正点原子例程里的LCD_Fast_DrawPoint和颜色填充函数。例程其实已经做了很多优化比如刷色块时会用FSMC连续写地址而不是每次重新计算坐标。在做图片显示功能时直接复用这些底层函数比自己重新实现快得多。至于DMA刷数据到LCDF407没有LTDC和DMA2D所以DMA只能通过FSMC写LCD的GRAM这个方式可实现但需要注意FSMC的AHB总线仲裁。正点原子探索者的例程默认是CPU直接写实际使用中稳定性最好我在项目中就保持了这种方式。5.4 从单张显示到连续切换的细节最后补充一个从单张显示到连续切换图片时容易忽略的点SD卡的文件打开和关闭逻辑。每显示一张图片就f_open一次如果图片文件比较大比如800KB的JPEGf_open要扫描FAT表时间不可忽视。更麻烦的是FATFS默认配置下f_read是按扇区读取的如果JPEG文件不连续存储SD卡碎片化读取速度会明显变慢。我在解码任务里做了一个小优化启动时把要显示的图片列表扫描一次记录文件名和起始扇区显示时直接用f_lseek跳过不必要的数据或者干脆在空闲时把整张JPEG读入SRAM的输入缓冲解码时直接读内存不走SD卡。这里还要注意FatFS的互斥问题。如果多个任务同时访问SD卡一个显示图片一个记录日志同一时刻只能有一个任务调用f_open、f_read这些函数否则FATFS内部状态会错乱。正确的是把SD卡访问全部收敛到解码任务里其他任务要读SD卡数据通过队列向解码任务发请求。6. 一些实测环境参数方便你对照最后列一下我调通时的关键参数不同批次板子可能略有差异但可作为参照项目参数主频168MHz外部SRAM型号IS62WV512161MB16位FSMC时序ADDSET2, DATAST5, AccessMode ALCD分辨率800x480 RGB565输出帧缓冲750KB外部SRAMTjpgDec工作区30KB外部SRAMJPEG输入缓冲200KB外部SRAM解码任务栈1536 word6KB内部SRAM显示任务栈512 word2KB内部SRAM解码任务优先级2显示任务优先级3实际测试中一张800x480、约200KB的JPEG图从SD卡读取到完整显示耗时大约1.8到2.5秒。其中解码占大头刷屏占80ms左右这个速度用于图片浏览器、开机Logo、菜单界面展示完全够用。如果想让解码更快可以尝试把TjpgDec的JD_FASTDECODE宏打开它会用查表法加速部分运算代价是代码体积增大。另外把编译器优化级别从-O0改成-O2解码速度提升非常明显我从2.2秒压到1.5秒左右。注意开优化后要重新验证一下SD卡读取和JPEG解码的稳定性个别编译器优化等级会影响位域操作的时序行为不过实测GCC和MDK的-O2都没有问题。关于外扩SRAM的驱动稳定性我再多说一句。不要在运行中频繁调整FSMC时序配置初始化一次后就别动了。FSMC时钟和AHB时钟关联运行中调整FSMC_BTR可能造成总线事务断裂轻则数据错乱重则直接硬件错误。我调试初期为了测试不同时序参数在程序里做了按键动态调整结果偶尔触发HardFault后来改成编译时宏开关再没出现过类似问题。本文还有配套的精品资源点击获取