STM32H7外部Flash图片LTDC显示实战:从取数机制到DMA2D搬运

发布时间:2026/8/30 14:41:33
STM32H7外部Flash图片LTDC显示实战:从取数机制到DMA2D搬运 做嵌入式显示的时候应该有不少人和我一样踩过同一个坑内部Flash明明不够大但界面上的开机Logo、产品图片却必须放进去最后只能把图片丢到外部Flash里结果LTDC一接上去就黑屏或者闪几下就花了。这次我在STM32H7S78-DK上完整走了一遍“外部Flash图片 LTDC控制器显示”的流程把踩过的坑、查过的资料、试通过的方案都整理出来。文章会从LTDC的取数机制讲起再到CubeMX配置、图像转换、外部Flash烧录、DMA2D搬运最后是常见问题排查整个过程偏实战适合正在调LTDC、RGB屏幕或者想在开机画面/仪表盘项目里直接复用的工程师。1. 先把LTDC的“取数逻辑”理清楚1.1 LTDC不是图片解码器很多刚接触LTDC的人会有一个误解觉得LTDC能直接识别PNG、JPEG这些图片格式。实际不是这样LTDC的完整名称是LCD-TFT显示控制器它做的事情非常机械按照你设置的时序不断从某个内存地址读取像素数据然后打包成RGB信号输出给屏幕。也就是说LTDC根本不关心你存的是图片还是程序它只认地址。你告诉它“从0xC0000000开始每两个字节是一个像素宽度800、高度480”它就老老实实按这个规则去取数。所以图片能不能显示关键在于你准备显示的像素数据是否已经按LTDC要求的像素格式摆在了它能够访问到的内存地址上。这就牵扯出一个很实际的问题外部Flash里的二进制图片数据并不天然等于LTDC需要的帧缓冲。先要把数据从Flash里弄到LTDC能访问的地址空间还要保证数据格式正确才能让屏幕正常显示。1.2 三种显示路径的取舍在STM32H7S78-DK这类板子上常见的做法有三条方案优点缺点适用场景外部Flash直接映射给LTDC代码最少上电就能显示Flash读取带宽有限高分辨率容易闪烁随机读取延迟不稳定低分辨率、静态图、临时调试先搬到内部RAM再显示速度快不影响外部总线RAM容量有限大图放不下小图标、小尺寸图先搬到外部SDRAM再显示容量大、带宽稳定适合LTDC长期刷新多一步SDRAM配置和搬运大尺寸图像、GUI、多图层我最开始图省事直接把LTDC的帧缓冲地址指到了外部Flash的映射地址上结果800x480的RGB565图片根本刷不动画面明显闪烁调试了很久才发现是访问带宽不够。后来老老实实把图片先搬到SDRAM问题立刻消失。1.3 我推荐的最终链路这套方案的完整链路是外部OSPI Flash中的RAW图像数据 → OSPI控制器以Memory-Mapped模式暴露到MCU地址空间 → DMA2D把数据从Flash映射地址搬运到外部SDRAM → LTDC从SDRAM帧缓冲地址持续取像素 → RGB屏幕显示画面简单说就是Flash负责存SDRAM负责跑DMA2D负责搬LTDC负责显示。四者各司其职这是在实际项目中稳定性最高的组合。2. CubeMX里的三大件配置2.1 LTDC时序参数怎么填LTDC的配置核心是时序。屏幕能不能正常点亮、画面会不会偏移都取决于这一组参数。以常见的800x480 RGB屏幕为例典型的参数如下参数数值说明水平同步宽度 Hsync48行同步脉冲宽度水平后廊 HBP88行同步结束到有效数据的延迟水平前廊 HFP40有效数据结束到下一个行同步的间隔垂直同步宽度 Vsync3帧同步脉冲宽度垂直后廊 VBP32场同步结束到有效数据的延迟垂直前廊 VFP13有效数据结束到下一个场同步的间隔Pixel Clock33.3MHz像素时钟由LTDC时钟分频得到这些参数千万别自己编一定要去屏幕数据手册里查不同屏幕差异很大。如果手头没有手册可以先用屏幕厂商提供的初始化代码里的值。CubeMX里配置LTDC时把这些值填到对应字段Pixel Clock根据屏幕支持的刷新率和分辨率计算。假如屏幕面板要求33.3MHz而LTDC输入时钟是100MHz那就用分频器配到33.3MHz。2.2 OSPI Flash的配置要点STM32H7S78-DK板载的外部Flash通常是OSPI NOR Flash也有可能是QSPI。在CubeMX里使能对应接口后有几个关键点需要确认。第一工作模式要选Memory-Mapped。这样外部Flash会被映射到MCU的地址空间比如0x90000000CPU和DMA2D都能直接按地址读取不需要手动发读命令。第二时钟分频要谨慎。OSPI Flash的时钟频率不要一次拉太高先在低频下确认读写正常再逐步提高。我遇到过把分频设得太激进导致读取数据偶尔错位的情况表现就是画面偶尔出现几条彩色斜线。第三如果是多线模式要确认CubeMX里配置的线数和Flash实际支持的模式一致。8线模式配成4线数据完全对不上最容易踩。2.3 SDRAM的配置不能只靠CubeMXSDRAM的初始化在CubeMX里生成的是基本参数实际调试时还需要手动调用一次刷新率设置。因为SDRAM需要周期性刷新如果刷新率不对数据会在几毫秒内丢失。以常见的16位SDRAM为例CubeMX里配置行列地址、CAS延迟、突发长度后HAL库会生成HAL_SDRAM_Init。但很多人漏了下面这一步初始化完成后需要调用HAL_SDRAM_ProgramRefreshRate设置刷新周期。刷新频率和SDRAM颗粒规格有关通常是7.8125us左右具体查颗粒数据手册。另外SDRAM的地址线、数据线如果接错初始化不会报错但写进去的数据会乱序画面就是典型的“像素错位”。用CubeMX生成的引脚映射一般不会错但如果你是手工移植到别的板子一定要逐一核对接线。2.4 别忘了背光和显示使能即使LTDC配置完全正确屏幕也可能不亮原因往往和LTDC本身无关而是背光没有打开。很多开发板会把背光控制引脚接到一个GPIO上需要在代码里拉高。STM32H7S78-DK的BSP包里通常会有BSP_LCD_DisplayOn这类的函数它会处理背光和显示使能时序。如果你是自己写的驱动记得在LTDC初始化完成后至少延几个毫秒再打开背光否则上电瞬间可能出现闪白屏。3. 图像数据准备与外部Flash烧录3.1 为什么直接把PNG/JPEG丢进Flash不行LTDC和DMA2D都不带JPEG/PNG解码功能尤其是JPEG内部是压缩编码LTDC读出来根本不是RGB像素。PNG虽然能转成原始像素但PNG文件本身还带文件头、压缩块LTDC同样不认识。所以最可靠的办法是在PC端把图片转换成裸的RGB565或者ARGB8888数据直接以二进制文件烧录到外部Flash里。这样MCU端不用解码上电后只需要复制数据启动速度最快。如果你必须用JPEG/PNG源图那就要在MCU端加软件解码或硬件JPEG解码器解码后再写入SDRAM但这对启动速度和Flash带宽都有压力。纯静态开机画面RAW数据就够用了。3.2 用Python脚本把图片转成RGB565转换工具很多Image2Lcd、IMG2C都可以但我觉得用Python最灵活尤其是批量转换或者加缩放裁切的时候。下面这个脚本可以把任意图片转成800x480的RGB565二进制文件from PIL import Image W, H 800, 480 img Image.open(logo.png).resize((W, H)).convert(RGB) with open(logo.bin, wb) as f: for y in range(H): for x in range(W): r, g, b img.getpixel((x, y)) # RGB565: R[4:0] G[5:3]? 实际为 R[4:0] G[5:0] B[4:0] pixel ((r 3) 11) | ((g 2) 5) | (b 3) f.write(pixel.to_bytes(2, little))注意这里有个很容易忽略的细节字节序。ARM小端模式下RGB565的单个像素是两个字节低字节在前。如果转换时用了big显示出来的画面会明显偏色红色和蓝色互换绿色也会错乱。图像大小算一下800x480、RGB565一帧是800 * 480 * 2 768000字节也就是750KB。这个大小对于外部Flash来说毫无压力但对内部RAM来说就很紧张了这也从侧面说明为什么不能直接用内部RAM做帧缓冲。3.3 用STM32CubeProgrammer烧录外部Flash把生成的logo.bin烧进外部Flash推荐用STM32CubeProgrammer操作流程比较简单用ST-LINK连接开发板。打开STM32CubeProgrammer选择External memory烧录模式。选择对应的OSPI/QSPI Flash Loader。下载logo.bin烧录地址选择Flash的起始地址也就是Memory-Mapped映射的基地址0x90000000。烧录完成后可以读回来校验确保写入没有错误。实际操作中如果Flash Loader选错型号会烧写失败如果地址选错比如选成了内部Flash地址程序会在启动阶段直接崩溃这点要特别注意。3.4 代码里怎么访问这块Flash数据只要OSPI配置成Memory-Mapped模式外部Flash就相当于一个只读内存区域。比如烧录在Flash起始位置的图片在代码里可以直接这样定义#define IMAGE_FLASH_ADDR 0x90000000UL在STM32CubeIDE的调试模式下你甚至可以直接在Memory窗口输入0x90000000查看烧进去的图像数据是否和PC端的二进制文件一致。这一步验证非常有用能提前排除很多后期显示问题。4. 代码实现从Flash数据到屏幕画面4.1 初始化顺序有讲究很多人在ST开发板上移植LTDC时把初始化顺序写反了结果画面要么不亮、要么闪烁。我建议的顺序是初始化系统时钟确保LTDC、FMC、OSPI的外设时钟都已使能。初始化OSPI并进入Memory-Mapped模式。初始化SDRAM包括刷新率配置。配置MPU把SDRAM帧缓冲地址设为非Cacheable。初始化LTDC配置Layer。把图像从Flash搬运到SDRAM。打开屏幕背光开始显示。顺序的核心逻辑是先保证数据通路可用再让显示控制器工作。如果先打开LTDC再初始化SDRAMLTDC可能已经按错误的地址取数画面恢复不了。4.2 MPU和Cache处理这一步是很多人会踩的深坑。STM32H7系列内核带Cache如果你把SDRAM帧缓冲地址标成CacheableDMA2D写入的数据可能还留在Cache里LTDC读的时候却是从SDRAM读两边看到的不是同一份数据结果就是画面迟迟不更新或者更新不全。最稳妥的方案是在MPU里把帧缓冲所在区域设置为Non-CacheableMPU_Region_InitTypeDef MPU_InitStruct {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress 0xC0000000; // SDRAM起始地址 MPU_InitStruct.Size MPU_REGION_SIZE_1MB; // 覆盖整个帧缓冲 MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_CTRL_PRIVILEGED_DEFAULT);如果你不想用MPU那每次写完帧缓冲后要手动调用SCB_CleanDCache()否则显示也会有问题。但这种方法在多图层、频繁刷新的场景下稍不注意就漏还是MPU一次性设置更省心。4.3 用DMA2D搬运而不是CPU memcpy把数据从Flash搬到SDRAM最简单的是memcpy但这里有两个问题一是750KB的数据用CPU搬会占用大量CPU时间开机动画如果多个几百KB明显感觉卡顿二是CPU读取Memory-Mapped的OSPI Flash如果Flash访问没有做缓冲优化效率很低。DMA2D是STM32内部专门做2D数据传输的DMA控制器可以理解为一个“搬数据专用硬件”。用DMA2D把Flash里的数据搬进SDRAM速度快而且CPU可以继续做别的事。关键代码如下DMA2D_HandleTypeDef hdma2d {0}; hdma2d.Init.Mode DMA2D_M2M; hdma2d.Init.ColorMode DMA2D_RGB565; hdma2d.Init.OutputColorMode DMA2D_OUTPUT_RGB565; hdma2d.Init.LineOffset 0; hdma2d.Init.OutputOffset 0; HAL_DMA2D_Init(hdma2d); HAL_DMA2D_Start(hdma2d, IMAGE_FLASH_ADDR, FRAME_BUFFER_ADDR, 800, 480); HAL_DMA2D_PollForTransfer(hdma2d, 100);注意DMA2D的源地址虽然用的是0x90000000但必须确保OSPI已经进入Memory-Mapped模式。如果OSPI还停留在普通命令模式DMA2D访问这个地址时会直接跑到错误的数据总线上TransferComplete标志不会正常置位。4.4 LTDC的Layer配置LTDC的Layer配置决定了画面在屏幕上的位置、尺寸和像素格式。配置一个全屏的RGB565图层代码如下LTDC_LayerCfgTypeDef layer {0}; layer.WindowX0 0; layer.WindowY0 0; layer.WindowX1 800; layer.WindowY1 480; layer.PixelFormat LTDC_PIXEL_FORMAT_RGB565; layer.FBStartAdress FRAME_BUFFER_ADDR; layer.Alpha 255; layer.Alpha0 0; layer.BlendingFactor1 LTDC_BLENDING_FACTOR1_CA; layer.BlendingFactor2 LTDC_BLENDING_FACTOR2_CA; layer.ImageWidth 800; layer.ImageHeight 480; HAL_LTDC_ConfigLayer(hltdc, layer, 0);这里的FBStartAdress是SDRAM帧缓冲地址不是Flash地址。Layer窗口大小和分辨率不一致时图像会拉伸或只显示一部分这个问题我后面还会细说。完成以上配置后外设初始化顺序正确DMA2D搬运完成屏幕通常就能正常显示图片。如果还不行大概率就是下面这些常见问题。5. 常见问题与排查实录5.1 黑屏从最外层开始查LTDC黑屏是最常见、也最容易让人焦躁的问题。我一般按从外到内的顺序排查屏幕背光是否亮了如果完全不亮先查背光GPIO有没有拉高供电是否正常。有没有显示使能信号很多屏幕还需要一个Display On命令或GPIO只配置LTDC不够。用示波器或逻辑分析仪看LTDC的HSYNC、VSYNC、DE引脚有没有信号。如果这些引脚都没有波形说明LTDC本身没工作重点查时钟和GPIO复用配置。如果信号都有了但还是黑屏检查帧缓冲地址内容。在调试器里看SDRAM地址的数据如果全是0说明DMA2D搬运没成功回过去查OSPI的Memory-Mapped配置。5.2 花屏和斜线大概率是宽高不对齐花屏现象也分好几种。最常见的是图像内容像被撕开一样一块块斜着错开这通常是LTDC的ImageWidth和实际数据宽度不一致导致。比如图像实际是800像素宽你配置成了1280LTDC每行多读了很多数据下一行就从错误的位置开始。另一种情况是图片本身高度、宽度和屏幕不匹配。LTDC的Layer WindowX1/WindowY1是窗口坐标不是缩放比例窗口比图片小就只显示部分窗口比图片大就会用空数据填充。5.3 颜色明显不对检查像素格式和字节序RGB565和ARGB8888如果混用画面颜色会非常奇怪。比如你生成的数据是RGB565但LTDC配置成了ARGB8888那么图像会明显偏色且层次错乱。反过来ARGB8888的数据按RGB565去解析画面会显得“压缩”且颜色完全不对。字节序问题我也提过低字节在前还是高字节在前直接影响颜色通道的读取顺序。调试的时候可以专门生成一张纯红色的图片比如RGB565值为0xF800如果屏幕上显示蓝色那就说明字节序反了。5.4 闪烁或偶发花屏查带宽和Cache如果画面能显示但会闪烁或者刷新几次后出现随机花屏优先怀疑SDRAM带宽不足和Cache一致性问题。SDRAM带宽不足时LTDC在读取帧缓冲的过程中可能被DMA2D等外设抢占画面就会出现瞬时撕裂。这种情况下可以降低LTDC像素时钟试试或者把DMA2D搬运放到画面不刷新的时候。如果图片需要长时间显示搬完数据后可以让DMA2D进入空闲状态减少总线竞争。Cache问题我前面已经强调过帧缓冲区域务必设置为Non-Cacheable。如果你用的是Cacheable模式DMA2D写完数据后一定要进行Cache Clean操作否则画面会“时好时坏”这个坑很隐蔽。5.5 排错速查表现象可能原因检查点完全黑屏背光未开、LTDC未初始化、时钟错误背光GPIO、LTDC引脚波形画面斜线错位ImageWidth/高度配置错误LTDC Layer窗口、ImageWidth偏色严重像素格式不匹配、字节序反了RGB565/ARGB8888、生成脚本字节序闪烁SDRAM带宽不足、Cache不一致像素时钟频率、MPU配置数据搬完了但画面不更新DMA2D没完成、SDRAM刷新配置错误DMA2D标志位、SDRAM刷新率我做过的几个项目里真正把这一整套流程跑通后后面再怎么换图片、换分辨率其实都只是改参数的事。麻烦的不是显示本身而是前面那些看不见的配置细节。最后分享一个个人习惯在跑外部Flash图片之前我会先用LTDC显示一个纯色测试画面比如让帧缓冲里全填0xF800。这样能把“LTDC配置问题”和“Flash读取问题”彻底隔离开。纯色显示正常再引入Flash数据定位问题的范围会小很多。这个习惯帮我省了不少调试时间希望对你有用。