简介:面向STM32F7系列嵌入式开发者的LTDC RGB屏驱动工程包,围绕STM32F767的高性能Cortex-M7平台,解决从GPIO、时序到多层显示与帧缓冲管理的完整驱动落地问题。内容适合已具备一定HAL库基础、希望掌握RGB液晶屏底层显示控制的开发者,也可作为嵌入式图形界面项目的参考工程。压缩包共182个文件,以92个h头文件和81个c源文件为主体,另含Keil工程文件、hex烧录文件、批处理脚本及少量配置与说明文件,整体约1.12MB,便于直接导入编译和按模块查阅。已有1693人学习下载,说明其在LTDC驱动学习场景中具备一定参考价值。工程覆盖LTDC架构配置、LCD时序参数设置、层属性与颜色格式管理、帧缓冲区组织,以及DMA与中断配合等关键环节,读者可据此梳理初始化流程、对照硬件接口设计并排查显示异常,积累可复用的RGB屏驱动经验。
1. 800×480 RGB 屏点不亮时,先别换屏:LTDC 的时序账本比排线更容易错
一块 800×480 的 RGB 屏接到 STM32F767 上,背光亮了却满屏白,或者图像整体右移几十像素,很多人第一反应是屏坏了、排线松了。血泪经验是:LTDC 本身不复杂,难的是把屏幕手册里的 8 个时序参数、PLLSAI 像素时钟和 SDRAM 显存带宽对上账。这个标题讲的是用 F767 的 LTDC 外设直接驱动并行 RGB 屏,不经过 SPI/I2C 转接,适合想做中尺寸彩屏、GUI 或多图层显示的开发者。先跑通彩条,再谈 UI。
2. STM32F767 的 LTDC 数据通路:PLLSAI 像素时钟、FMC 显存和图层取数
LTDC 不是独立时钟源,它吃的是 PLLSAI 的 R 分频输出;显存也不是随便找个数组就能放。F767 内部 SRAM 对 800×480 的 RGB565 单缓冲还能勉强塞下,一旦上双缓冲或者 1024×600,就必须外挂 SDRAM。这一章先把数据通路拆开:像素时钟从哪来、一帧显存放哪、图层怎么从显存取数,最后用彩条把最小通路打通。
2.1 从 PLLSAI 到像素时钟:先算总宽高,再选 R 分频
屏幕手册给的往往不是像素时钟,而是水平同步宽度、水平后肩、水平前肩、垂直同步宽度、垂直后肩、垂直前肩和有效分辨率。像素时钟的算法是:总宽等于有效宽加三个水平消隐参数,总高等于有效高加三个垂直消隐参数,再乘刷新率。常见 800×480 屏在 60 Hz 下算出来约 30~33 MHz,这个值要落进 PLLSAI 的 R 分频,不能随手拿系统主频去分。
def ltdc_pixel_clock(h_active, h_sync, h_bp, h_fp, v_active, v_sync, v_bp, v_fp, fps): # 总宽高包含消隐区,像素时钟按整帧扫描面积计算 h_total = h_active + h_sync + h_bp + h_fp v_total = v_active + v_sync + v_bp + v_fp return h_total * v_total * fps # 示例:800×480 屏,同步宽度 48/3,后肩 88/32,前肩 40/13 f = ltdc_pixel_clock(800, 48, 88, 40, 480, 3, 32, 13, 60) print(f) # 约 30.9 MHz这段脚本只做一件事:把屏参换算成像素时钟。参数说明里,h_sync 是水平同步脉冲宽度,h_bp 是水平后肩,h_fp 是水平前肩;垂直方向同理。算出来的值不要顶到 LTDC 上限,我一般会留 10% 左右余量,再用 PLLSAI 的 N、R 去凑,凑不到就微调前肩,别动同步宽度。
2.2 显存为什么必须外挂 SDRAM:一帧 RGB565 的字节预算
800×480 的 RGB565 一帧是 800×480×2 字节,等于 768000 字节,约 750 KiB。单缓冲还能在内部 SRAM 里挤一挤,双缓冲就是 1.5 MiB,再叠加 GUI 的临时缓冲和 DMA2D 的目标区,内部 SRAM 很快见底。常见做法是把帧缓冲放到 FMC 外扩的 16 位 SDRAM 上,地址从 FMC Bank1 的 0xC0000000 起映射,LTDC 的 FBStartAdress 直接写这个地址。
这里有个容易忽略的点:SDRAM 不是“能读写就行”,LTDC 是实时取数,DMA2D 和 CPU 也会抢同一块总线。显存地址要按 32 字节对齐,双缓冲两个地址之间不要跨到别的 Bank,否则切换时容易出现半帧旧数据。我一般会把第一缓冲放在 0xC0000000,第二缓冲放在 0xC00C0000 之后,中间留一点空隙,方便用逻辑分析仪抓地址。
2.3 用彩条测试打通 LTDC 最小通路
不要一上来就贴图片。先让 LTDC 输出纯色,再输出彩条,最后才接 GUI。纯色能验证背光、像素时钟和图层使能;彩条能验证 RGB 引脚顺序和像素格式。下面这段 C 代码用 CPU 直接写显存,不依赖 DMA2D,适合第一次点屏时排除外设干扰。
// 假设 fb 指向 SDRAM 中的帧缓冲,格式 RGB565 void lcd_draw_color_bars(uint16_t *fb, uint32_t w, uint32_t h) { // 8 条彩条:白、黄、青、绿、品红、红、蓝、黑 const uint16_t bars[8] = { 0xFFFF, 0xFFE0, 0x07FF, 0x07E0, 0xF81F, 0xF800, 0x001F, 0x0000 }; for (uint32_t y = 0; y < h; y++) { for (uint32_t x = 0; x < w; x++) { fb[y * w + x] = bars[(x * 8) / w]; } } }逻辑说明:按横坐标把屏幕切成 8 段,每段填一个 RGB565 颜色值。参数说明:fb 必须是 LTDC 当前图层配置的起始地址,w 和 h 要和 ImageWidth、ImageHeight 一致;如果屏幕颜色顺序是 BGR,红蓝会互换,这时先别改代码,回头查 RGB 引脚映射。彩条正常后再换图片,能省掉很多“到底哪一层错了”的排查时间。
3. 把屏幕手册的 8 个时序参数写进 LTDC 寄存器:HSYNC/HBP/HFP 与 VSYNC/VBP/VFP
屏幕手册的时序表通常画成一行波形,真正要写进 LTDC 的是寄存器字段,而且很多字段是“值减 1”。这一章把手册参数翻译成 LTDC 初始化结构体,再讲 CubeMX 生成骨架后必须手改的地方,最后落到图层窗口和帧缓冲地址。先把单缓冲跑稳,再考虑双缓冲,不然撕裂和花屏会混在一起,排查起来像黑匣子。
3.1 从手册抓参数:同步宽度、前后肩和有效区
拿到屏参先别急着填代码,先把 8 个数抄到一张表里:HSYNC 宽度、HBP、HFP、VSYNC 宽度、VBP、VFP、有效宽、有效高。LTDC 的 HorizontalSync 填 HSYNC 宽度减 1,VerticalSync 填 VSYNC 宽度减 1;AccumulatedHBP 填 HSYNC 加 HBP 再减 1;AccumulatedVBP 填 VSYNC 加 VBP 再减 1;AccumulatedActiveW 填 HSYNC 加 HBP 加有效宽再减 1;TotalWidth 填 HSYNC 加 HBP 加有效宽加 HFP 再减 1。垂直方向同理。
| 参数 | 屏幕手册符号 | LTDC 字段 | 800×480 示例 |
|---|---|---|---|
| 水平同步宽度 | HSYNC | HorizontalSync | 48 - 1 |
| 水平后肩 | HBP | AccumulatedHBP | 48 + 88 - 1 |
| 水平前肩 | HFP | TotalWidth 反推 | 40 |
| 垂直同步宽度 | VSYNC | VerticalSync | 3 - 1 |
| 垂直后肩 | VBP | AccumulatedVBP | 3 + 32 - 1 |
| 垂直前肩 | VFP | TotalHeigh 反推 | 13 |
| 有效分辨率 | Width/Height | AccumulatedActiveW/H | 800/480 |
| 像素时钟 | Clock | PLLSAI R | 约 30.9 MHz |
这张表建议打印出来,改一版参数就用铅笔标一版。屏幕手册的极性也要看:HSPolarity、VSPolarity、DEPolarity 常见是低有效,但不同屏不一样。极性填错不一定全黑,有时表现为图像整体偏移或边缘抖动,很容易误判成时序算错。
3.2 CubeMX 生成 LTDC 初始化骨架后要改哪几处
CubeMX 能生成 LTDC 的初始化骨架,但屏幕参数、显存地址和图层格式还是要手改。下面这段代码是常见的初始化结构体,重点看累加值是不是按“减 1”填的,FBStartAdress 是不是指向 SDRAM。
LTDC_HandleTypeDef hltdc; LTDC_LayerCfgTypeDef layer; hltdc.Instance = LTDC; hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL; hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL; hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL; hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC; hltdc.Init.HorizontalSync = 48 - 1; hltdc.Init.VerticalSync = 3 - 1; hltdc.Init.AccumulatedHBP = 48 + 88 - 1; hltdc.Init.AccumulatedVBP = 3 + 32 - 1; hltdc.Init.AccumulatedActiveW = 48 + 88 + 800 - 1; hltdc.Init.AccumulatedActiveH = 3 + 32 + 480 - 1; hltdc.Init.TotalWidth = 48 + 88 + 800 + 40 - 1; hltdc.Init.TotalHeigh = 3 + 32 + 480 + 13 - 1; HAL_LTDC_Init(&hltdc); layer.WindowX0 = 0; layer.WindowX1 = 800; layer.WindowY0 = 0; layer.WindowY1 = 480; layer.PixelFormat = LTDC_PIXEL_FORMAT_RGB565; layer.Alpha = 255; layer.Alpha0 = 0; layer.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA; layer.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA; layer.FBStartAdress = 0xC0000000; layer.ImageWidth = 800; layer.ImageHeight = 480; HAL_LTDC_ConfigLayer(&hltdc, &layer, 0);逻辑说明:先初始化 LTDC 全局时序,再配置第 0 层窗口和帧缓冲。参数说明:HorizontalSync 等字段填的是寄存器值,不是手册里的脉冲宽度;FBStartAdress 是 SDRAM 地址,必须和 FMC 初始化后的映射一致;PixelFormat 要和显存里的数据格式一致,RGB565 每像素 2 字节,别把 RGB888 的 3 字节数据塞进 RGB565 图层。
注意:LTDC 的累加字段大多是“值减 1”,把手册脉冲宽度直接写进去,轻则图像偏移,重则整屏不亮。
3.3 图层窗口与帧缓冲地址:单缓冲先跑通,再上双缓冲
图层窗口的 WindowX1 和 WindowY1 是右下角坐标,不是宽度和高度。800×480 全屏就是 X0=0、X1=800、Y0=0、Y1=480。ImageWidth 和 ImageHeight 是帧缓冲里的图像尺寸,通常和窗口一致;如果窗口只显示图像的一部分,这两个值可以大于窗口,靠 WindowX0/Y0 做偏移。单缓冲时 FBStartAdress 固定,改完显存后等一次垂直消隐再让 LTDC 重载,画面才不会撕。
双缓冲的做法是准备两个地址,在垂直消隐期间调用 HAL_LTDC_SetAddress 切换,或者直接改 LTDC_LxCFBAR 再触发重载。我一般会先让单缓冲跑一天,确认没有偶发花屏,再上双缓冲。单缓冲都花屏,双缓冲只会把问题藏得更深。切换地址时不要在屏幕扫描到一半时改,否则上半屏旧图、下半屏新图,看起来就是撕裂。
4. SDRAM 显存与 LTDC 抢带宽:800×480 也会花屏的根因和参数
很多人以为 800×480 分辨率不高,SDRAM 随便配就能跑。实际 LTDC 是实时取数,像素时钟 30 MHz 左右时,每秒钟要从显存读走几十兆字节,DMA2D 和 CPU 再一挤,SDRAM 的刷新和行列切换就会变成瓶颈。这一章先算带宽,再看 FMC SDRAM 的关键参数,最后用 DMA2D 把 CPU 从显存搬运里解放出来。
4.1 算清 LTDC 实时带宽:像素时钟 × 每像素字节数
带宽的粗算方法是像素时钟乘每像素字节数。800×480 RGB565 在 60 Hz 下,有效像素带宽约 800×480×2×60,等于 46.08 MB/s;但 LTDC 扫描时还要读消隐区,实际取数按总宽高算,接近 56 MB/s。双缓冲不会让 LTDC 读两倍,但 CPU 和 DMA2D 写另一块缓冲会占总线,所以显存带宽要按“读一帧加写一帧”留余量。
| 分辨率 | 像素格式 | 每帧字节 | 60 Hz 有效带宽 | 双缓冲占用 |
|---|---|---|---|---|
| 800×480 | RGB565 | 768000 | 约 46.1 MB/s | 约 1.5 MiB |
| 800×480 | RGB888 | 1152000 | 约 69.1 MB/s | 约 2.3 MiB |
| 1024×600 | RGB565 | 1228800 | 约 73.7 MB/s | 约 2.4 MiB |
| 1024×600 | RGB888 | 1843200 | 约 110.6 MB/s | 约 3.5 MiB |
这张表不是让你死记,而是选型时快速判断:16 位 SDRAM、100 MHz 左右的时钟,跑 800×480 RGB565 单缓冲比较稳;上 1024×600 或 RGB888,就要看 SDRAM 的时序余量和 FMC 的突发配置。别等花屏了才回头算,先算再画板。
4.2 FMC SDRAM 初始化:刷新计数、CAS 延迟和突发长度怎么定
FMC 初始化 SDRAM 时,刷新计数按“SDRAM 时钟频率 × 刷新周期 ÷ 行数 - 20”估算,具体用器件手册的 tRFC 和行数代进去。CAS 延迟常见填 2 或 3,要跟 SDRAM 颗粒支持的最快值一致;突发长度常见 1、2、4、8,LTDC 连续取数时突发长度太短会增加命令开销,太长又可能被高优先级访问打断。我一般先用保守参数点亮,再用示波器看 FMC 的读写波形,逐步收紧。
还有一个常被忽略的参数是 FMC 的读等待和写等待。SDRAM 不是 SRAM,行列地址复用,切换 Bank 有额外周期。LTDC 取数模式比较规律,DMA2D 写显存则是大块连续写,两者交错时如果写等待太短,读数据就会偶发错位。表现不是一直花屏,而是刷新几次后某一行颜色不对,像显存里被写进了脏数据。
4.3 用 DMA2D 填充和拷贝把 CPU 从显存搬运里解放出来
CPU 循环写显存不仅慢,还会让 D-Cache 和 SDRAM 之间产生大量写回。DMA2D 做纯色填充、矩形拷贝和 Alpha 混合更合适。下面这段代码用 DMA2D 的寄存器到存储器模式填一块 RGB565 区域,适合清屏和画背景。
DMA2D_HandleTypeDef hdma2d; void lcd_fill_rgb565(uint32_t fb_addr, uint16_t color, uint32_t w, uint32_t h) { hdma2d.Instance = DMA2D; hdma2d.Init.Mode = DMA2D_R2M; hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = 0; HAL_DMA2D_Init(&hdma2d); // color 是 RGB565 打包值,DMA2D 直接写入 SDRAM HAL_DMA2D_Start(&hdma2d, (uint32_t)color, fb_addr, w, h); HAL_DMA2D_PollForTransfer(&hdma2d, 100); }逻辑说明:DMA2D 工作在 R2M 模式,把前景色填到目标地址,不用 CPU 逐像素写。参数说明:fb_addr 是显存起始地址,w 和 h 是要填充的区域大小,color 要按 RGB565 打包,比如红色是 0xF800。轮询超时给 100 ms 是保守值,实际填充一帧远小于这个时间;如果超时,先查 DMA2D 时钟使能和目标地址是否在可写内存区。
注意:CPU 写过显存后如果马上让 LTDC 显示,D-Cache 可能还没写回 SDRAM。要么把显存区配成 non-cacheable,要么在切换前做一次 Cache 清理。
5. RGB 屏调试避坑:从全白到偶发花屏的 5 条排查路径
RGB 屏的故障现象很会骗人:全白可能是背光,也可能是 LTDC 没使能;颜色偏红可能是引脚顺序,也可能是像素格式;偶发花屏可能是 SDRAM 带宽,也可能是 Cache 一致性。这一章按现象、原因、解决三步走,把最容易翻车的 5 个点拆开。每条都先别改代码,先量信号或看寄存器,避免越改越乱。
5.1 全白或全黑:先量背光使能,再查 LTDC 是否真的在刷
现象:上电后背光亮,屏幕全白;或者背光不亮,屏幕全黑。原因:全白常见于 LTDC 没输出、图层没使能、帧缓冲全 0xFF;全黑常见于背光使能脚没拉高、背光升压没工作、LTDC 像素时钟没出来。解决:先用万用表量背光使能脚和升压输出,再用示波器量 LTDC 的像素时钟和 HSYNC/VSYNC。如果时钟没有,回头查 PLLSAI 的 R 分频和 LTDC 时钟使能;如果时钟有但屏全白,查图层使能和 FBStartAdress 是否指向有效显存。
我遇到过一种情况:背光使能脚接在 GPIO 上,初始化顺序把 GPIO 复用成了别的功能,背光一闪就灭,看起来像 LTDC 输出黑屏。后来把背光使能放在 LTDC 初始化之后、开显示之前,问题消失。电源和使能顺序不要嫌麻烦,先固定下来,后面排查会少很多变量。
5.2 图像偏移或抖动:时序参数和像素时钟余量
现象:图像整体右移或下移几十像素,边缘有一条黑边或彩边;有时图像会左右抖动。原因:HSYNC/HBP/HFP 或 VSYNC/VBP/VFP 填错,或者像素时钟和屏幕手册不匹配。解决:拿手册的时序表逐项对寄存器,重点看累加值是不是“同步宽度加后肩减 1”;像素时钟不要顶上限,先降 10% 看抖动是否消失。如果降频后正常,说明 SDRAM 带宽或 PLLSAI 分频余量不够,不要硬撑。
还有一种情况是极性填反。HSPolarity 和 VSPolarity 填错时,图像不一定全黑,可能整体偏移到消隐区,看起来像“屏幕分辨率不对”。我一般会把四种极性组合列出来,每次只改一个,改完复位看现象,别一次改多个参数。
5.3 颜色偏红偏蓝:RGB 引脚顺序和像素格式
现象:彩条测试时红色和蓝色互换,或者颜色整体发紫、发绿。原因:RGB 数据线顺序接反,或者 LTDC 图层像素格式和显存数据格式不一致。解决:先看硬件原理图的 RGB 引脚映射,R0~R7、G0~G7、B0~B7 有没有交叉;再看 layer.PixelFormat 是 RGB565 还是 RGB888,显存里写的字节序是不是和 LTDC 一致。RGB565 下红色是 0xF800,蓝色是 0x001F,用纯色填充能最快确认。
如果只有某几种颜色不对,比如白色正常、红色偏暗,可能是低位数据线没接好。RGB565 的 R 和 B 各 5 位,G 有 6 位,低位悬空时颜色会阶梯状失真。这类问题用彩条不一定看得出来,要用渐变图或纯色逐通道测试。
5.4 刷新撕裂:双缓冲切换时机和显存对齐
现象:快速刷新画面时,屏幕上半部分是新图,下半部分是旧图,像被刀切过。原因:在 LTDC 扫描过程中改了帧缓冲地址,或者双缓冲切换没有等垂直消隐。解决:用 HAL_LTDC_SetAddress 切换后,等 LTDC 的寄存器重载完成再释放旧缓冲;或者用 LTDC 的垂直消隐中断做切换。显存地址要按 32 字节对齐,两个缓冲不要重叠。
单缓冲下也可能撕裂,因为 CPU 写显存时 LTDC 正在读同一块区域。这种场景要么用双缓冲,要么在垂直消隐期间集中写。不要指望提高 CPU 主频解决,LTDC 的读取是实时的,CPU 再快也会和扫描撞车。
5.5 偶发花屏:SDRAM 带宽、Cache 一致性和中断优先级
现象:画面偶尔闪一下,或者某一行颜色错乱,复位后又正常。原因:SDRAM 带宽不够、FMC 时序太紧、D-Cache 没清理、DMA2D 和 LTDC 抢总线。解决:先把 LTDC 和 DMA2D 的突发访问错开,DMA2D 填充大块显存时不要让 CPU 同时大量写 SDRAM;再把显存区配成 non-cacheable,或者在 CPU 写完后调用 SCB_CleanDCache_by_Addr;最后看中断优先级,LTDC 的行中断不要被长任务堵住。
这类问题最难查,因为每次现象不一样。我一般会先降像素时钟,如果花屏消失,基本就是带宽或时序余量问题;如果降频还花,再查 Cache 和电源。别一上来就换屏,先让系统跑最保守的参数,再逐项放开。
6. 把 LTDC 用顺手的三个进阶动作:双缓冲校验、DMA2D 混合和帧率实测
第一个动作是双缓冲校验。准备两块显存,画不同颜色,在垂直消隐中断里切换地址,肉眼看有没有撕裂;再用逻辑分析仪抓 LTDC 的 VSYNC 和显存写使能,确认切换落在消隐区。第二个动作是 DMA2D 混合,把前景图层做成 ARGB8888,背景用 RGB565,用 DMA2D 做 Alpha 混合后再交给 LTDC 显示,适合做半透明菜单。第三个动作是帧率实测,不要靠感觉,用 GPIO 翻转加示波器最直接。
// 在 LTDC 行中断里翻转测试 GPIO,示波器上相邻上升沿就是一帧 void HAL_LTDC_LineEventCallback(LTDC_HandleTypeDef *hltdc) { // 只使能第 0 行中断,避免每行都翻转 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); }这段回调的关键是只使能一行中断,否则每行都翻转,示波器上看到的是行频而不是帧频。参数说明:GPIOA_PIN5 只是示例测试脚,换成板上空闲引脚即可;中断里不要做耗时操作,翻转完就退出。测到帧率后,再和屏幕手册的刷新率对比,差太多就回头查像素时钟和消隐参数。
我一般会把屏幕手册的时序页打印出来贴在工位,改一版参数就用铅笔标一版;先量背光和像素时钟,再动代码。LTDC 调通一次之后,后面换屏就是换一张时序表的事,别怕麻烦。希望帮到你。
本文还有配套的精品资源,点击获取