800×480 的 RGB 屏挂到 STM32 上,LTDC 时钟使能了、图层开了、显存地址也填了,程序下载进去之后屏幕一片纯黑——背光明明亮着,就是不出图。这个现象我前后遇到过四五次,每次的根因都不一样:有一次是 SDRAM 刷新计数器沿用了模板里的默认值,有一次是 D-Cache 没配 MPU 导致写进缓存的像素根本没落到 SDRAM,还有一次纯粹是显存首地址没按字对齐。黑屏只是表象,真正要拆的是 LTDC 取数链路和 SDRAM 刷新、时序之间的协同关系,这两者任何一头没对齐,屏幕就是黑的,或者花得没法看。下面我把这套排查逻辑完整摊开讲,从带宽账本算起,一直到寄存器级别的参数落地,适合刚上手 RGB 屏驱动、也适合调过一阵但一直没彻底搞明白的同行对照着看。
1. 黑屏先别急着改代码:把现象拆成三层
1.1 三种“黑”对应三条完全不同的链路
同样是黑屏,示波器一夹就能分出三类。第一类是像素时钟 PCLK 根本没有输出,这属于 LTDC 的时钟域问题,源头在 PLL 配置或者 LTDC 使能位没置起来,跟 SDRAM 一点关系都没有。第二类是 PCLK 有、HSYNC/VSYNC/DE 都有波形,但屏幕全黑,这时候 LTDC 已经在正常扫屏了,问题出在它读到的像素数据上——要么显存里全是 0,要么读回来的数据是错的,这一类才是 SDRAM 出问题的典型表现。第三类是屏幕能亮但只有背光,或者偶尔闪一下白,一般是屏本身的复位、背光使能、部分带 SPI 初始化序列的屏没走完上电流程。
我一般的做法是先用示波器或者逻辑分析仪夹住 PCLK 和 DE 两根线,确认时序信号是否真的在跑。这一步花不了五分钟,但能直接砍掉一半的排查范围。很多人一上来就怀疑 SDRAM,结果折腾半天发现是 PLLSAI 的分频系数算错了,PCLK 压根没出来。分清楚这三类之后,“SDRAM 刷新与时序协同”这个话题才有意义——它只对第二类黑屏负责。
1.2 为什么嫌疑名单里 SDRAM 排第一
在没有外部 SRAM、也没有内部显存足够大的情况下,RGB 屏的帧缓冲基本只能放在外部 SDRAM 里。这就意味着 LTDC 每一行有效像素期间都要从 SDRAM 连续搬运数据,而 SDRAM 同时又得定期做刷新、还要应付 CPU 和 DMA2D 的写入请求。三方抢同一个存储控制器,任何一方的参数没配好,表现到屏幕上就是黑屏、花屏或者撕裂。
更麻烦的是 SDRAM 的问题有很强的隐蔽性。写进去的数据读出来不对,不一定是颗粒坏了,很可能是刷新计数设得太离谱、时序参数比手册要求还紧、或者 SDCLK 相位没调对导致采样点落在数据跳变沿上。这些问题的共同点是:代码能编译、能下载、不报错,但屏幕就是不给你面子。所以我习惯把 SDRAM 单独拎出来先做裸读写验证,验证通过了再往上叠 LTDC,这样出了问题能立刻定位到是哪一层。
2. LTDC 与 SDRAM 的账本:带宽与时序到底谁卡谁
2.1 像素时钟不是拍脑袋定的
像素时钟由屏幕的时序参数反推出来,公式很直白:PCLK = (HSW + HBP + Width + HFP) × (VSW + VBP + Height + VFP) × 刷新率。以一块 800×480 的屏为例,手册给的典型值是 HSW=48、HBP=88、HFP=40、VSW=3、VBP=32、VFP=13,那么一行的总像素数是 48+88+800+40=976,一帧的总行数是 3+32+480+13=528,按 60Hz 算就是 976×528×60≈30.9MHz。
实际工程里更常用 33MHz 这个整数,因为它能被 PLL 更容易地分频出来,代价是刷新率略高于 60Hz,屏幕一般都能吃下。这里有个容易忽略的点:PCLK 的上限不只受屏幕限制,还受 LTDC 外设本身和 SDRAM 带宽限制。PCLK 越高,单位时间从 SDRAM 拉的数据越多,如果 SDRAM 那边供不上,就会出现 FIFO 欠载,表现为行内突然出现一段黑或者整帧闪。
计算的时候还要注意别把 HSW 和 HBP 搞混。HSW 是同步脉冲宽度,HBP 是同步脉冲结束到有效像素开始之间的后沿,HFP 是有效像素结束到下一个同步脉冲之间的前沿。有些屏手册把 HBP 和 HFP 标成“Back Porch”和“Front Porch”,也有标成“BP”和“FP”的,看的时候要对着时序图确认一遍,别拿错数字。
2.2 显存带宽的粗算与留余量
带宽这块我习惯算两个数:平均值和峰值。平均值决定 SDRAM 长期负载,峰值决定会不会瞬间欠载。800×480、RGB565、60Hz 的平均带宽是 800×480×2×60≈46.08MB/s,峰值则是 PCLK × 每像素字节数,也就是 33MHz×2=66MB/s。
再看 SDRAM 这一侧。假设用的是一颗 16 位数据宽度、SDCLK 跑 90MHz 的颗粒,理论峰值带宽是 90M×2=180MB/s。但理论值不能直接用,SDRAM 每次读写都要经历激活、行地址选通、列地址选通、预充电这一套流程,加上定期刷新要占时间,实际有效带宽通常只有峰值的六到七成。按 65% 折算,可用带宽大概 117MB/s。
这样一对比,单层 RGB565 的 46MB/s 平均负载看起来绰绰有余。但如果再加一层 UI、再叠一个 DMA2D 做局部刷新、CPU 还要往显存里写文字,负载很容易冲到 80MB/s 以上,余量就变薄了。我的经验是:平均负载不要超过 SDRAM 可用带宽的 50%,超过这个线,屏幕在某些极端画面下就会开始出现偶发黑条。真压不住就得往上加 SDCLK、换 32 位位宽,或者干脆降低分辨率。
2.3 刷新命令是怎么“偷走”带宽的
SDRAM 靠电容存数据,电容会漏电,所以必须周期性刷新。刷新是一个占用总线时间的操作,在刷新期间 SDRAM 不能响应正常的读写请求。FMC 外设内部会把刷新请求和读写请求做仲裁,读写优先,但刷新请求积压到一定程度就必须插进去执行。
以 13 位行地址、8192 行、tREF=64ms 的颗粒为例,平均每个刷新周期是 64ms/8192=7.8125μs。每次刷新大约占用 7 个 SDCLK 周期,90MHz 下就是 78ns 左右。算下来刷新占用总时间的比例是 78ns/7.8125μs≈1%。这个比例很低,正常情况下不会成为瓶颈。
真正的问题出在配置错误上。如果刷新计数器被人改成了默认的 0 或者接近 0,刷新频率会高得离谱,总线被刷新命令占满,LTDC 取数严重不足,屏幕直接黑掉或者大面积花屏。反过来,如果计数设得太大,刷新不足,电容漏电导致数据翻转,读回来的像素是乱码,屏幕上是雪花点。这两种错误方向相反,但最终都指向黑屏,所以光看现象是分不出方向的,必须回到寄存器里核对数值。
3. 参数落地:从屏手册到寄存器
3.1 屏幕时序六件套的读取与换算
拿到一块屏,第一件事是翻手册里的时序表,把 HSW、HBP、HFP、VSW、VBP、VFP 这六个数抄下来。抄的时候要特别注意单位,有的手册给的是像素数,有的给的是时间(ns 或者 μs),如果是时间还得用 PCLK 换算回像素数。换算公式是:像素数 = 时间 × PCLK,然后向上取整。
抄完之后立刻做一次自检:把这六个数代进总行数、总列数公式,算出来的刷新率应该在 55Hz 到 75Hz 之间。如果算出来是 200Hz 或者 20Hz,说明某个参数抄错了单位或者看错了列。这一步能挡住很大一部分低级错误,我在新手代码里见过太多次因为 HBP 抄成了 HSW 导致的图像左移。
LTDC 寄存器这边有个细节必须记住:所有累积值都要减一。硬件内部是从 0 开始计数的,而手册给的是个数,所以写寄存器的时候要统一减 1。具体对应关系是:HorizontalSync 写 HSW-1,AccumulatedHBP 写 HSW+HBP-1,AccumulatedActiveW 写 HSW+HBP+Width-1,TotalWidth 写 HSW+HBP+Width+HFP-1。垂直方向同理。这个减一如果漏掉,图像会整体偏移一个像素,边界上会出现一条细黑线,看起来像黑屏的一部分,很容易误判。
3.2 LTDC 侧配置代码与易错点
配置代码本身不复杂,难点在于每个字段的含义要和上面的公式一一对应:
hltdc.Init.HorizontalSync = 48 - 1; // HSW hltdc.Init.VerticalSync = 3 - 1; // VSW hltdc.Init.AccumulatedHBP = 48 + 88 - 1; // HSW + HBP hltdc.Init.AccumulatedVBP = 3 + 32 - 1; // VSW + VBP 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; // 一帧总长 hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL; hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL; hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL; hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;极性这四项是最容易被忽略的黑屏元凶。HSPolarity、VSPolarity、DEPolarity 决定同步信号和有效区标志是高有效还是低有效,PCPolarity 决定像素数据在时钟上升沿还是下降沿采样。不同屏厂的要求不一样,配反了轻则图像偏移,重则整屏黑掉。我的做法是:手册上写“Active Low”就选_AL,写“Active High”就选_AH,不确定的时候两个都试一遍,出图的那个留下,五分钟的事,比翻手册猜语义快得多。
图层配置里还有一个坑是ImageWidth和ImageHeight,这两个值必须等于实际分辨率,不能填成整个屏幕加消隐区的大小,也不能填成显存里分配的更大尺寸。填错了 LTDC 会按错误的步长取数,画面会斜切。显存首地址FBStartAdress必须是 4 字节对齐,最好直接按 32 字节对齐,配合 Cache 行大小用起来更省心。
3.3 SDRAM 侧初始化、时序与刷新计数
SDRAM 的初始化序列是有固定顺序的,顺序错了颗粒根本不上电。大致流程是:先开时钟使能命令,延时一小段时间;然后发预充电所有 Bank 命令;接着连续发 8 次自动刷新命令;再发加载模式寄存器命令,把突发长度、CAS 延迟、写突发模式配进去;最后单独配一次刷新率。这几步的间隔和参数在颗粒手册的“Initialization”章节都写得很清楚,照抄就行,但最后那次刷新率计算要自己动手。
刷新计数的公式是:COUNT = (tREF / 行数) × SDCLK频率 - 20。以前面说的 8192 行、tREF=64ms、SDCLK=90MHz 为例,64ms/8192=7.8125μs,乘以 90MHz 得 703,减 20 得 683。所以:
HAL_SDRAM_ProgramRefreshRate(&hsdram1, 683);减 20 是 FMC 内部预留的余量,让刷新命令不至于卡在读写中间。这个值很多模板里写成 1386 或者 924,那是因为对应的是不同行数和不同时钟的颗粒,直接照抄模板必踩坑。刷新计数必须按自己板子上的颗粒手册和实际 SDCLK 重新算。
时序参数这块,FMC 要求按 SDCLK 周期数填写,所以要把颗粒手册里的纳秒值除以时钟周期再向上取整:
| 参数 | 含义 | 手册典型值 | 90MHz 下取值 |
|---|---|---|---|
| TMRD | 模式寄存器设置到命令 | 2 tCK | 2 |
| TXSR | 退出自刷新时间 | 67ns | 7 |
| TRAS | 行激活到预充电 | 42ns | 4 |
| TRC | 行周期 | 60ns | 6 |
| TWR | 写恢复时间 | 15ns | 2 |
| TRP | 预充电时间 | 18ns | 2 |
| TRCD | 行到列延迟 | 18ns | 2 |
取整一律向上,宁可给多一个周期也不能少,少了就是时序违规,读出来的数据会随机错位。另外ReadPipeDelay和ReadBurst这两项要跟着硬件设计走:走线短、信号质量好的板子可以把读管道延迟设成 0,走线长或者有转接板的建议设成 1 甚至 2,用相位补偿换稳定性。SDCLK 相位那一项在 FMC 里是SDClockPeriod,取值 2 或 3 对应不同的时钟输出相位,读数据异常时这是第一个要调的地方。
3.4 显存布局、对齐与 Cache/MPU
显存地址建议直接放在 SDRAM 的起始段,并且和 LTDC 的其他缓冲区(比如双层时的第二层)保持足够的间距,避免越界覆盖。如果用的是带 D-Cache 的核,这里有个必须处理的问题:CPU 往显存写像素时,数据可能只落在 Cache 里没写回 SDRAM,LTDC 从 SDRAM 读到的还是旧值,表现就是屏幕一直黑着或者显示的是上一帧内容。
解决办法有两条。第一条是把显存区域配置成 Write-through 或者 Non-cacheable 的 MPU 区域,让写操作直接穿透到 SDRAM:
MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_32MB; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;第二条是在每次改完显存内容后手动做一次SCB_CleanDCache_by_Addr,只清显存那一段。第一条更省心,代价是 CPU 写显存的速度会慢一点,但对 LTDC 这种实时取数的场景来说,稳定性远比那点写入速度重要。我现在的习惯是显存区域一律走 Non-cacheable,其他地方照常开 Cache,这样既不影响运算性能,也不会出现“程序里明明画了图、屏幕上啥也没有”的诡异现象。
4. 分阶段点亮:把黑屏逼到某个确定环节
4.1 第一阶段:SDRAM 裸读写验证
在碰 LTDC 之前,先写一段最朴素的 SDRAM 测试:往整个显存区域按 32 位字写入递增模式,再读回来逐个比对,统计错误个数和首个错误地址。这一步能一次性排除掉颗粒焊接、地址线错位、时序参数错误、刷新计数错误这几类问题。
我一般会测三遍:第一遍写 0xAA55AA55、第二遍写 0x55555555、第三遍写地址本身。三遍都过,说明数据线和地址线都没问题。如果只在某些地址段出错,多半是行地址或者 Bank 地址线的问题;如果错误是随机的、每次位置都不一样,基本可以确定是刷新不足或者时序太紧。这时候把刷新计数调小一点、把 TRCD 和 TRP 各加一个周期,通常就能收敛。
这一步还有一个容易被跳过的地方:上电后要等 SDRAM 稳定再访问。有些板子的电源爬升比较慢,初始化序列发太早会导致模式寄存器没写进去,后面所有读写都是错的。加个几毫秒的延时,成本几乎为零。
4.2 第二阶段:屏幕信号线验证
SDRAM 过了之后,先别急着开 LTDC 出图。把 LTDC 配好但先往显存里填一个纯色,然后用示波器确认 PCLK、HSYNC、VSYNC、DE 四根线的波形都正常,频率和计算值对得上。这一步是分水岭:波形对了说明 LTDC 的时钟和时序没问题,接下来黑屏只可能是数据通路或者显存内容的问题;波形不对就回去查 PLL 和极性配置。
有些屏还带一根独立的复位脚和背光使能脚,这两根线如果没拉对,LTDC 输出再标准屏幕也不会亮。带 SPI 初始化序列的 IPS 屏更麻烦,得先通过 SPI 把上电序列、伽马参数、像素格式发完,屏才会接受 RGB 信号。这类屏黑屏时,先量一下 SPI 有没有真的发出去,别一头扎进 LTDC 里。
4.3 第三阶段:LTDC 纯色输出
纯色测试是判断“黑屏到底是不是 SDRAM 数据问题”的最快手段。往显存首地址开始填满 0xF800(红色,RGB565),如果屏幕变成整屏红色,说明 LTDC 取数链路、显存地址、像素格式全部正确,剩下的问题只可能出在“你写的数据没到显存里”或者“写进去的数据被覆盖了”。如果填了红色还是黑的,那就往前查:显存地址对不对、图层使能位有没有置、LTDC 全局使能位LTDCEN有没有置、SRCR寄存器里的立即重载位有没有触发。
这里有个细节:改完 LTDC 寄存器后要等一个垂直消隐期新配置才生效,改完立刻读状态寄存器可能还是旧值。性子急的可以直接置LTDC_SRCR的VBR位强制重载,或者置IMR位立即生效,但立即生效在某些情况下会造成画面瞬闪,调试阶段用没问题,量产代码里还是老老实实等垂直消隐。
4.4 第四阶段:加图、加压、加并发
纯色过了之后,开始往显存里写真实图像。这一步最容易暴露带宽问题:静态图正常、一动起来就撕裂,或者画面切换时出现横向黑条,基本可以判定是 SDRAM 带宽余量不够或者刷新和读请求打架。
我的验证方法是写一个简单的压力测试:CPU 持续往显存的另一半区域写数据,同时 LTDC 正常扫屏,观察屏幕有没有异常。如果只在 CPU 写入时出现花屏,说明总线仲裁偏向 CPU,可以考虑把 CPU 写显存的操作改成 DMA2D 搬运,让 DMA 控制器以更规整的突发方式访问,减少对 LTDC 读请求的打断。
另一个要盯的点是 SDRAM 的写操作效率。FMC 的写是单次写,不像读那样能组突发,所以频繁的小块写会明显拖低带宽。用 DMA2D 做矩形填充和图像拷贝,能把写操作聚合成连续访问,实测下来比 CPU 逐像素写快不少,对屏幕稳定性的帮助也很直接。
5. 踩过的坑与排障速查表
5.1 八个高频坑位
第一个坑:刷新计数照抄模板。模板里的值对应的是另一颗颗粒、另一个时钟频率,直接抄过来要么刷新过密拖垮带宽,要么刷新不足数据翻转。必须按自己的颗粒手册重算。
第二个坑:显存没做 Cache 处理。CPU 写进去了,LTDC 读不到,屏幕黑着但调试器看显存内容是对的。配 MPU 或者手动清 Cache,二选一。
第三个坑:显存地址没对齐。有些编译器把数组放在奇数地址上,LTDC 取数直接错位,画面斜切甚至全黑。加__attribute__((aligned(32)))或者用链接脚本固定段地址。
第四个坑:SDRAM 时序取整方向错了。手册给 18ns,90MHz 下是 1.62 个周期,必须向上取 2,取 1 就是违规。所有时序参数一律向上取整。
第五个坑:SDCLK 相位没调。波形看着有,但数据采样点落在跳变沿上,读回来的像素是乱的。表现为花屏而非纯黑,调相位或者加读管道延迟。
第六个坑:LTDC 极性配反。同步信号极性错了,屏幕识别不到有效区,整屏黑。四个极性挨个试,最快。
第七个坑:图层混合系数配错。单层显示时如果混合系数配成了需要背景层参与的模式,而背景层又没配置,输出可能是全黑。单层场景直接把 Alpha 设 255、混合系数设成 PAxCA 就行。
第八个坑:背光和复位没处理。屏的背光使能脚悬空或者极性反了,LTDC 输出再标准也看不见。硬件上电第一件事就是量这两根线的电平。
5.2 排障速查表
| 现象 | 优先怀疑 | 验证手段 | 处理方向 |
|---|---|---|---|
| 全黑,PCLK 无输出 | PLL 分频或 LTDC 未使能 | 示波器测 PCLK 脚 | 检查 PLLSAI 分频与 LTDCEN |
| 全黑,PCLK 正常,显存有数据 | Cache 未回写 | 清 DCache 后观察 | 配 MPU 为 Non-cacheable |
| 全黑,显存内容全 0 | 应用层没写显存 | 调试器读显存首地址 | 检查写显存代码路径 |
| 全黑,显存地址异常 | 地址未对齐 | 打印指针值 | 强制 32 字节对齐 |
| 花屏、雪花点 | 刷新不足或时序太紧 | 裸读写测试统计错误率 | 重算 COUNT,放宽 TRCD/TRP |
| 图像整体偏移 | HBP/VBP 算错或漏减一 | 对照手册重算累积值 | 修正寄存器值 |
| 图像上下翻滚 | VSW 太小或极性反 | 改 VSPOL 试 | 加大 VSW 或翻转极性 |
| 颜色错乱 | 像素格式或 PCPOL 反 | 填纯红纯绿对比 | 改 RGB/BGR 或翻转 PCPOL |
| 偶发横向黑条 | 带宽不足或 FIFO 欠载 | 降帧率看是否消失 | 提 SDCLK、降分辨率、减层数 |
| 画面撕裂 | 显存被边写边读 | 双缓冲切换测试 | 上双缓冲或加垂直同步等待 |
5.3 一个我自己常用的定位顺序
遇到黑屏,我现在的固定动作是:先测 PCLK 有没有,有就填纯色看屏幕变不变,变了说明通路 OK 去查应用层,不变就翻寄存器看LTDC_GCR的使能位和图层使能位;如果使能位都对但画面还是黑的,把显存首地址往前挪 32 字节再试一次,很多时候就是对齐问题;最后才去怀疑 SDRAM 刷新和时序。这个顺序的好处是每一步都能给出明确的“是/否”结论,不会在几个可能的根因之间来回打转。
SDRAM 那边我还有一个习惯:把刷新计数和几个关键时序参数做成宏定义,调试的时候可以现场改、现场烧,不用每次翻数据手册重算。等你把一块板子调通之后,这些值就是这块板子的“指纹”,换个颗粒、换个 SDCLK 就必须重新来过,千万别觉得调通一次就一劳永逸。
最后分享一个小技巧:如果手头没有逻辑分析仪,可以用一个空闲 GPIO 在 LTDC 的行中断里翻转,配合示波器看翻转频率是不是等于刷新率。这个方法虽然粗糙,但判断 LTDC 有没有真的在扫屏足够用了,比盯着黑屏瞎猜强得多。