简介:本资源是面向嵌入式开发初学者与中级工程师的STM32F429 RGB屏驱动实战项目,聚焦4.3寸800×480分辨率LCD的LTDC底层寄存器级驱动实现,解决图形显示中时序配置、帧缓冲管理、多图层叠加及DMA2D加速等核心难点,适用于工业HMI、智能仪表等需自主可控显示方案的场景。压缩包共42个文件,含23个头文件(定义寄存器地址、结构体及宏)、14个C源文件(覆盖系统时钟、LTDC初始化、LCD底层驱动、SDRAM显存分配、LED/OLED/KEY/TPAD等外设协同逻辑),以及Keil工程配置文件(uvprojx/uvoptx)、启动脚本(s)、调试配置(ini)和可执行hex文件,整体仅238KB,轻量易集成。已有1091人学习下载,提供完整可编译运行的寄存器库工程,含清晰分层的HARDWARE模块(LCD/SDRAM/LED/TIMER等)与USER主逻辑,无需HAL库依赖,便于深入理解LTDC硬件机制与内存带宽优化实践。 做F429显示方案的时候,我一开始也会想:不就是把屏幕点亮嘛,SPI屏、FSMC屏都写过,LTDC能有多难。真把一块4.3寸800x480的RGB屏接到LTDC上,才发现这套东西和以前那些“控制器在屏端”的方案完全不是一个思路。如果你手头正准备用STM32F429跑LTDC,并且想用寄存器库(不依赖HAL)把这套驱动彻底搞明白,这篇应该能帮你省掉不少弯路。
这篇文章不是贴一段能跑的代码就完事,我会把LTDC的时序怎么算、寄存器怎么填、帧缓冲为什么必须放SDRAM、画面撕裂到底怎么压住这些关键问题逐个拆开讲。适合两类人看:一是刚接触RGB屏、想搞懂LTDC底层机制的;二是已经在HAL库里调通了,但想用寄存器库重写或者排查疑难问题的。
1. LTDC和以往显示方案的本质区别:屏端没有控制器了
1.1 从FSMC“写寄存器”到LTDC“刷像素”
之前用FSMC驱动TFTLCD,比如常见的ILI9341或者NT35510,MCU的工作本质是“往屏的控制寄存器里写命令和数据”。屏内部有一颗控制芯片,负责把写入的GRAM数据按行列扫描刷新到液晶面板上。FSMC只是提供了一组类似外部存储器的读写时序,让CPU写GRAM的速度更快而已。这种方案很成熟,但问题在于:控制芯片能力有限,分辨率做高之后,内部GRAM不够、刷新率上不去,成本也压在屏端。
LTDC是F429内部集成的一个液晶控制器,它把原来在屏端控制芯片的工作搬到了MCU里。MCU通过LTDC直接对外输出像素时钟(DOTCLK)、行同步(HSYNC)、帧同步(VSYNC)、数据使能(DE)以及RGB数据线。也就是说,RGB屏本身几乎没有“智能”,它只是一块负责把数据线上的电压转换成液晶偏转的矩阵。你给它对的时钟、对的同步信号、对的像素数据,它才会稳定地显示出画面。
这个区别直接影响了你的设计思路:以前FSMC驱动,通常不需要什么显存,屏内部有GRAM;现在LTDC方案,所有像素数据必须由MCU侧的显存(FrameBuffer)持续提供给LTDC,MCU相当于同时在当显卡和显存。理解不了这一点,后面很多问题都会卡住。
1.2 为什么是LTDC而不是更快的FSMC+DMA
有些项目为了省事,会用FSMC+DMA去驱动RGB接口屏,思路是先配置好GPIO,用FSMC的写时序把RGB像素数据“打”到屏的数据线上。这种方式在低分辨率、低刷新率下能跑,但并不是为RGB屏设计的。RGB屏需要连续、实时的像素时钟和行场同步信号,FSMC的时序本质上是被动的写操作,靠DMA硬撑,很容易出现刷新不完整、闪烁、撕裂。LTDC的优势在于它内部有专门的时序生成器,像素时钟、同步信号、消隐区都是硬件自动产生的,MCU只需要保证显存里的数据是正确且够新的。
4.3寸800x480的RGB屏,在不含消隐的情况下,像素数就是800*480=384000个。如果按60Hz刷新,每秒至少要送2300万个像素。假如用RGB565格式,每个像素2字节,那么每秒显存读取量约46MB。纯靠CPU或者FSMC+DMA搬运这个数据量,一方面CPU被完全占满,另一方面时序抖动会导致画面肉眼可见地闪。LTDC的DMA通道会自己按行同步从显存搬数据,CPU几乎不用干预,这才是它存在的价值。
2. RGB屏的时序拆解:800x480不是真的只有800x480
2.1 接口上到底需要接哪些信号线
4.3寸RGB屏通常引脚很多,核心信号包括:
- R0-R7、G0-G7、B0-B7:24根数据线,对应RGB888;如果只接高6位或者用RGB565格式,可以只接16根,高两位接GND或电源。
- DOTCLK(像素时钟):每个上升沿或下降沿送出一个像素。
- HSYNC(行同步):每扫完一行,发出一个脉冲表示新一行开始。
- VSYNC(帧同步):每扫完一帧,发出一个脉冲表示新一帧开始。
- DE(数据使能):高电平期间,数据线上的RGB信号是有效像素;低电平期间是消隐区。
- 背光控制:通常是LED_A/LED_K,或者一个单独的背光使能脚。
很多人第一次接RGB屏容易犯一个错误:觉得DE信号没用,直接接高电平或者不管。实际上,在LTDC配置Dither、消隐等场景下,DE的状态直接影响屏端对有效像素的解析。驱动配置时务必保证DE正常连接。
2.2 一行的时间要拆成四段
LTDC产生HSYNC信号的时候,并不是连续把800个有效像素送出去就完事。一行的完整时间段包含四部分:HSYNC脉冲宽度(HSAW)、水平后肩(HBP)、有效显示区(AAW)、水平前肩(HFP)。
画一条时间轴来理解:
同一行数据,先发一个同步脉冲,告诉屏幕“新行来了”,然后等待一段后肩时间,这段时间数据线上不送有效像素,再然后送入真正的800个像素,最后再等待一段前肩时间,再进入下一行。这个消隐机制是LCD行业沿用CRT时代的规范,目的是给面板的信号处理留出稳定时间。
寄存器体现为:
- LTDC_SSCR:设置行同步宽度和场同步宽度。
- LTDC_AWCR:设置有效显示区域的宽和高。
- LTDC_BPCR:设置水平后肩和垂直后肩,但要注意这里的值和“后肩”不完全等价,它是后肩加同步宽度减1。
- LTDC_WCR:设置水平前肩、垂直前肩,也是前肩加同步宽度加后肩然后减1的关系。
新手容易陷进去死记公式,我建议直接看屏的规格书,里面有完整的时序图和建议值。以常见的4.3寸800x480屏为例,典型参数大致是:HSAW=40、HBP=88、HFP=40、VSAW=10、VBP=32、VFP=13。具体每块屏不一样,务必以你手上屏的规格书为准。
2.3 像素时钟(DOTCLK)怎么算
像素时钟决定了LTDC向屏发送像素的速度。表面上看,800x480@60Hz只需要80048060=23.04MHz,但实际上要把消隐期的时间也算进去。用上面那组典型参数估算:
水平总周期 = HSAW + HBP + 有效宽度 + HFP = 40 + 88 + 800 + 40 = 968 垂直总周期 = VSAW + VBP + 有效高度 + VFP = 10 + 32 + 480 + 13 = 535
像素时钟 = 水平总周期 * 垂直总周期 * 帧率 = 968 * 535 * 60 ≈ 31.08MHz
取整后常用33.3MHz左右。F429内部PLLSAI专门为LTDC提供时钟,公式是:
DOTCLK = PLLSAI_VCO / PLLSAI_R PLLSAI_VCO = HSE / PLLSAI_M * PLLSAI_N
假设板载HSE晶振是25MHz,要得到33.33MHz,可以配置M=25、N=200、R=6:
25MHz / 25 * 200 = 200MHz 200MHz / 6 = 33.33MHz
这个值非常合适。需要注意PLLSAI的R分频在F429上只能取2、4、6、8,不是连续可调的,所以算时钟的时候要按这个约束来凑M/N/R,而不是随意改R。如果实在凑不到恰好值,取一个接近且不超过屏规格书标称最大值的频率也可以,因为屏对DOTCLK有一定容忍范围。
3. 寄存器级LTDC初始化:从RCC到Layer的完整链路
3.1 时钟使能:GPIO、LTDC、DMA2D一个都不能少
我建议把初始化拆成五步:开时钟、配GPIO复用、配LTDC全局时序、配Layer、启动显示。
第一步先看RCC寄存器。在STM32F429上,GPIO时钟在RCC_AHB1ENR里使能,LTDC时钟在RCC_APB2ENR里使能,DMA2D时钟也在RCC_AHB1ENR里使能。这里最常见的坑是:GPIO和LTDC的时钟都开了,但忘了开DMA2D时钟,后面调用DMA2D刷图时寄存器写进去完全没反应。寄存器库驱动里,这三者必须明确分开初始化,不像HAL库会自动判断。
3.2 GPIO复用配置:不是所有引脚都叫AF14
LTDC的引脚复用大多数是AF14,但并非全部。拿F429来说,部分数据引脚是AF13或者其他AF编号。比如:
- PA4(LTDC_VSYNC)是AF14
- PC6(LTDC_HSYNC)是AF14
- PE4(LTDC_B0)是AF14
- PF10(LTDC_DE)是AF14
- PH9(LTDC_R3)可能是AF14或AF9,不同封装版本有差异
所以最稳妥的办法是翻开对应型号的数据手册,查AF映射表,而不是照抄别人工程里的AF值。尤其是你自己画的板子,引脚分配如果和开发板不一致,复用功能号一错,屏幕就完全不出信号。
寄存器配置时,先把对应GPIO口的MODER设为复用模式,然后在AFRL或者AFRH里填入正确的AF编号。例如GPIOG的PIN6作为LTDC_R7,如果AF是14,需要配置GPIOG->AFRH的AFRH_MODE6为1111。注意每根引脚都设置完后,还要确认GPIO速度等级可以设高点,比如OSPEEDR设为50MHz或100MHz,避免信号边沿太缓影响高速下的时序。
3.3 LTDC全局寄存器:GCR、SSCR、BPCR、AWCR、WCR的顺序
这里有个经验规律:先配置LTDC的同步参数,再配置显示有效区域,然后配置后台颜色,最后使能LTDC。顺序反了容易出奇怪现象。
首先是LTDC_GCR。这个寄存器有几个关键位:
- LTDCEN:LTDC总使能,初始化最后才置1。
- PCKEN:像素时钟输出使能,必须为1,否则屏收不到DOTCLK。
- DEPOL:DE极性,一般0表示高有效。
- PCPOL:像素时钟极性,需要配合屏的规格书,有的屏要求DOTCLK下降沿采样,有的上升沿采样。如果方向反了,会看到画面偏移或者轻微花影。
- HSPOL和VSPOL:行场同步极性,多数屏低有效,配置成0。
然后是时序相关寄存器:
- LTDC_SSCR:写入HSAW和VSAW,注意要减1,比如HSAW=40,寄存器写39。
- LTDC_BPCR:写入AHBP和AVBP,计算方式是水平后肩+水平同步宽度-1,垂直同理。
- LTDC_AWCR:写入AAW和AAH,代表有效显示区域的宽高,也都要减1,所以800x480对应799和479。
- LTDC_WCR:写入AAHBP和AAVBP,计算方式是水平前沿+水平同步宽度+水平后肩整体再减1,表示一行总周期-1。
这几个寄存器的关系,一句话总结就是:SSCR定义脉冲,BPCR定义后肩边界,AWCR定义有效区,WCR定义总周期。只要这四者的边界对齐,LTDC的时序生成器就能稳定工作。如果画面出现整体偏移或者边缘有一条竖线,通常就是这些值加一减一弄错了。
接着配置LTDC_BCCR,也就是后台颜色寄存器。它决定在没有图层覆盖区域显示的颜色,例如RGB值为0x000000就是黑色。调试阶段我习惯把后台颜色设成一个亮色,比如红色0xFF0000,如果屏幕能正常显示纯红色背景,说明LTDC的同步和像素时钟已经通了,问题大概率出在图层或者显存上,这个技巧能帮你快速定位。
3.4 图层(Layer)配置:窗口位置、像素格式、显存地址
LTDC在F429上有两个图层,Layer0和Layer1。驱动初始化一般先把Layer0配置为主图层。
关键寄存器如下:
- LTDC_LxCR:图层控制寄存器,置1使能图层。
- LTDC_LxWPR:图层窗口位置寄存器,定义窗口左上角和右下角的坐标,单位是像素。比如全屏显示就设左上角(0,0),右下角(799,479)。注意寄存器里存的是实际坐标,不是宽高。
- LTDC_LxPFCR:像素格式寄存器。RGB565对应0x02,RGB888对应0x03,ARGB8888对应0x00。这里必须和你使用的FrameBuffer里的数据格式严格一致。
- LTDC_LxCACR:恒定Alpha值,范围0-255,255表示完全不透明。
- LTDC_LxDCCR:默认颜色,在图层未覆盖区域显示的颜色。
- LTDC_LxBFCR:混合因子配置,决定图层和后台颜色如何混合。最简单粗暴的配置是恒定Alpha、完全不透明。
- LTDC_LxCFBAR:帧缓冲基地址寄存器,写入FrameBuffer的首地址。
- LTDC_LxCFBLR:帧缓冲行长度寄存器,包含CFBLL和CFBP两个字段。
- LTDC_LxCFBLNR:帧缓冲行数寄存器,写入窗口高度。
这里再提一个最容易出问题的寄存器:LTDC_LxCFBLR。CFBLL字段表示一行的总字节数,计算公式是“有效显示宽度 * 每像素字节数 + 3”。为什么加3?因为硬件要求CFBLL按32字节对齐,实际寄存器写入的最小步长是4字节,+3能保证非对齐宽度也能覆盖完整。CFBP字段表示行距Pitch,是下一行起始地址相对当前行偏移的字节数。全屏刷新时,CFBP通常等于CFBLL;如果只是从一张大图上取一块区域显示,CFBP就要填大图的整行字节数。
3.5 手工触发重载:为什么写了寄存器画面不更新
LTDC的很多图层寄存器都是影子寄存器,也就是说,你写入寄存器后,在当前帧没有结束之前不会真正生效。要让配置生效,必须写LTDC_SRCR寄存器触发重载。这个寄存器有两个位:
- IMR:立即重载,立即生效。
- VBR:垂直消隐期重载,等当前帧扫描到垂直消隐区时才生效。
初始化流程结束后,我习惯同时置位IMR和VBR,确保配置尽快生效。后续运行时如果只改少量寄存器,比如移动窗口位置,推荐只用VBR,避免在屏正在扫描到一半时切换显存地址,否则容易在画面中间出现一条撕裂横线。
图层配置的完整顺序大致是:先关图层(LxCR清零)-> 配置窗口坐标和像素格式 -> 配置FrameBuffer地址、行长度和行数 -> 配置常量Alpha和混合因子 -> 使能图层 -> 触发重载。调试初期不要搞花活,老老实实按这个顺序走。
4. 帧缓冲和DMA2D:显存放哪,决定了你的板子能不能跑起来
4.1 内部256KB SRAM根本放不下一整帧RGB565
算一笔账:800x480分辨率,RGB565格式,每个像素2字节:
800 * 480 * 2 = 768000字节 = 750KB
F429内部SRAM总共约256KB(192KB+64KB),放不下整帧。如果强行把分辨率降到320x240,RGB565,那就是3202402=150KB,勉强放下,但留给堆栈和全局变量的空间就非常紧张了,容易莫名死机。
所以,做4.3寸800x480RGB屏,外部SDRAM几乎是必须的。常见的做法是用FMC接口外挂一颗16位SDRAM,比如W9825G6KH或者IS42S16400J,容量至少4MB以上。显存大一点还有个好处:可以做多缓冲,比如双缓冲或者三缓冲,这对消除撕裂非常有用。
如果你的板子上没有SDRAM,那就只能退而求其次,用一个内部SRAM放不下就做局部刷新。但RGB屏的LTDC是按整个有效区域持续刷新的,局部刷新只能靠改变Layer窗口位置实现,显示逻辑会非常别扭。
4.2 显存布局和LTDC_LxCFBLR的对应关系
显存地址不是随便给个数组名就完事。LTDC读取FrameBuffer时,会按你配置的行长和行数,逐行从基地址开始读取。如果显存基地址是0xD0008000,那么:
- 第0行起始地址 = 基地址 + 0
- 第1行起始地址 = 基地址 + CFBP
- 第n行起始地址 = 基地址 + n * CFBP
CFBP如果配得比CFBLL大,比如CFBLL填800*2+3=1603,CFBP填2048,那每一行末尾会跳过448字节的间隔。这在做图像裁剪显示时很有用,比如从一张1024宽的大图里截取中间800像素显示。如果只是全屏刷新,CFBP直接等于CFBLL即可,千万别把CFBP设置成0,否则所有行都会从同一个地址读,画面就会变成一条条重复的纹理。
还需要注意显存地址的对齐。FrameBuffer基地址建议32字节对齐,也就是地址的低5位为0。虽然LTDC硬件没有强制所有地址都对到这个程度,但不对齐的情况下,和DMA2D配合时容易出问题。
4.3 DMA2D刷图:把CPU从逐像素搬运里解放出来
F429的DMA2D是一个图形专用DMA,支持纯内存到内存拷贝、带格式转换的内存到内存拷贝、混合模式等。它的核心价值在于:对FrameBuffer做填充或图像拷贝,CPU只负责下发几条指令,大块数据搬运全部由硬件完成。
最简单的使用场景是清屏,也就是把一个颜色值填充到整个FrameBuffer。可以配DMA2D为寄存器到内存模式(R2M),把颜色值放到输出寄存器,然后设置输出地址为FrameBuffer基地址,输出偏移设为0,一次性写完整个区域。这样清一帧750KB的RGB565数据,DMA2D在几十微秒内就能完成,而CPU逐点写至少需要几毫秒,差别非常明显。
另一个常见场景是图片显示,比如把一张ARGB8888位图转换成RGB565格式输出到FrameBuffer。配置DMA2D为M2M_PFC模式:前景地址指向原始ARGB8888图片,前景像素格式配成ARGB8888,输出地址指向FrameBuffer对应位置,输出像素格式配成RGB565,然后启动DMA2D。转换由硬件完成,速度比CPU逐像素转换快很多。需要注意原始图片的每一行字节数可能不是4字节对齐,这个可以在前景偏移寄存器里配置,保证图片边界不对齐时也能正确读取。
我第一次用DMA2D也踩过一次坑:发完启动指令后立刻去读FrameBuffer,发现数据还没写完。DMA2D是异步的,开启传输后必须等标志位(比如TCIF)置位,或者用中断告知传输完成,再去做后续操作。否则你读到的是半个旧数据半个新数据的混合体,显示出来就是撕裂的瞬间。
5. RGB屏上显示中文:从字模到自绘UI的最小实现
5.1 字模是怎么取出来的
RGB屏上要显示中文,最直接的方法是用点阵字模。常见的是16x16点阵,一个汉字占16行,每行16像素,按单色编码时每行2字节,总共32字节。更大字号就是24x24或者32x32。
取模工具我用过不少,PCtoLCD2002比较直观。它支持横向取模、纵向取模、字节正序、字节反序等多种选项。最常用的模式是横向取模,字节高位在前:从左到右每8个像素凑一个字节,字节的高位对应左边的像素。这样写显示函数时,检查bit7到bit0的顺序最自然。
取出来的字模数据结构,一般是一个常量数组。比如汉字“电”的16x16字模,可以用const unsigned char font_table[] = {0x00, 0x00, ...}的形式。如果你做了GB2312编码查表,可以通过汉字编码索引到字模数组的偏移地址。对于项目里只用到固定几个汉字的情况,也可以直接把所有汉字的字模拼进一个大数组,自己维护一个映射表。
5.2 一个能跑的16x16汉字显示函数
假设FrameBuffer是RGB565格式,我们配置LTDC窗口为全屏800x480,显存首地址为buf_base。要显示一个汉字,核心思路是:先计算汉字在屏上的起始坐标,然后逐行逐像素读取字模数据,如果该位为1,就把FrameBuffer对应位置的像素写成前景色;如果为0,可以写背景色,或者保持不动形成透明效果。
一个简单的画点函数如下:
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x >= 800 || y >= 480) return; *(volatile uint16_t *)(buf_base + y * 800 + x) = color; }这里直接用y*800加上x算出RGB565在显存中的偏移,除以2的运算编译器会优化,不用担心效率。屏宽我写死了800,做通用封装时应当把这个值作为参数或者宏定义,避免改屏之后到处找。
画汉字的函数:
void LCD_ShowChar16(uint16_t x, uint16_t y, uint16_t color, uint8_t *font_data) { uint8_t byte; for (uint8_t row = 0; row < 16; row++) { byte = font_data[row * 2]; for (uint8_t col = 0; col < 8; col++) { if (byte & (0x80 >> col)) { LCD_DrawPoint(x + col, y + row, color); } } byte = font_data[row * 2 + 1]; for (uint8_t col = 0; col < 8; col++) { if (byte & (0x80 >> col)) { LCD_DrawPoint(x + 8 + col, y + row, color); } } } }这里有两个容易弄错的地方。第一,如果你在取模工具里选择了“纵向取模”,那么上面的读取顺序就不对,需要改成逐列读取。第二,如果取模时选了“字节反序”,判断位就应该是byte & (1 << col)。所以拿到字模数组之后,必须先确认取模设置,不要想当然。我曾经因为拿错了字模方向,显示出来的汉字整体镜像,排查了半天才发现是取模方向的问题。
5.3 提升显示效果:透明背景和反色
直接跳过背景像素,就能实现透明文字效果。在屏幕上叠加菜单标题时,透明文字看起来比带底色块的方式舒服很多。反色显示则适合做菜单选中高亮:读一下目标位置的当前像素颜色,取反后再写回去。RGB565的取反不是简单位翻转所有16位,那样颜色会变得非常奇怪,正确做法是把RGB分量分别取反,再重新组装。
函数可以这样:
uint16_t LCD_GetPoint(uint16_t x, uint16_t y) { return *(volatile uint16_t *)(buf_base + y * 800 + x); } void LCD_InvertRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { for (uint16_t y = y0; y <= y1; y++) { for (uint16_t x = x0; x <= x1; x++) { uint16_t c = LCD_GetPoint(x, y); uint16_t r = 31 - ((c >> 11) & 0x1F); uint16_t g = 63 - ((c >> 5) & 0x3F); uint16_t b = 31 - (c & 0x1F); LCD_DrawPoint(x, y, (r << 11) | (g << 5) | b); } } }如果觉得手动取反太慢,也可以借助LTDC的混合功能做半透明菜单条,但那就涉及Layer1和Layer0的混合系数,逻辑更复杂。调试初期先把手动绘制函数跑通,再考虑硬件混合更稳妥。
6. 白屏、花屏、撕裂:LTDC调试踩坑记录
6.1 白屏排查链路:从背光到LTDC总使能
白屏的第一反应不要直接去查LTDC,先确认背光有没有亮。很多RGB屏的背光需要MCU给一个高电平或者PWM信号,如果背光没开,屏幕就是灰白色或者黑色,什么都看不见。背光亮了再往LTDC方向排查。
背光正常但白屏,按优先级检查以下几处:
- RCC_APB2ENR里的LTDC时钟是否打开,没有时钟,所有LTDC寄存器写入无效。
- GPIO复用是否配好,尤其是DOTCLK引脚,如果DOTCLK没输出,屏就不知道何时采样数据。
- LTDC_GCR里的PCKEN是否置1,这个位控制像素时钟实际输出。
- LTDC_GCR里的LTDCEN是否置1,总使能没开,LTDC不工作。
- LTDC_SRCR是否触发了重载,影子寄存器不重载,配置不会生效。
- Layer是否使能,LxCR=0时图层全部不用,屏幕显示后台颜色。
我把这个顺序写死成自己的排查流程,遇到白屏就是从下往上一条条过,而不是东查一下西查一下。
6.2 花屏和颜色异常:极性和行场参数
花屏的表现有很多种。如果是整屏均匀地出现彩色噪点,优先怀疑像素时钟采样极性反了。试试把GCR里的PCPOL位翻转,很多屏是上升沿采样,如果配成了下降沿,就会出现颜色乱跳的现象。如果是画面整体偏移,出现黑色竖条或者斜纹,那就检查SSCR、BPCR、AWCR、WCR这几个寄存器的加减一关系,以及行场同步极性HSPOL、VSPOL。
颜色偏色则要检查LTDC_LxPFCR里的像素格式和实际写入FrameBuffer的数据格式。一个常见误解:LTDC配置成RGB888模式,但FrameBuffer里却放的是RGB565数据,这样LTDC读数据时会按3字节一个像素解析,显示出来颜色会完全错乱且画面有纵向拉伸。反过来,配置成RGB565但FrameBuffer里是ARGB8888数据,颜色会偏且周围出现奇怪的边界。确保这两者一致,比其他任何调试都有效。
还有一个容易忽略的点:SDRAM的初始化时序如果不对,FrameBuffer里的数据读取会不稳定,表现为某些区域不定期花屏。这种花屏不是固定位置的,扫描几次有时正常有时坏,多半是SDRAM时序配置偏紧或者刷新率不够。可以尝试把SDRAM的刷新周期调长一点,或者降低LTDC像素时钟,看看现象是否减轻。
6.3 画面撕裂:为什么用VBR重载而不是IMR
撕裂现象是:画面更新过程中,屏上半部分已经是新一帧内容,下半部分还是旧一帧内容,中间有一条明显的横线。
根因是:Layer的FrameBuffer地址在屏扫描到一半时被切换了。LTDC正在从旧显存地址读行数据,你突然把地址改成了新显存地址,那屏就会在一帧内拼出两个不同帧的内容。
解法有几个层次:
- 最基础的:所有影响图层配置的寄存器更新都用VBR(垂直消隐重载),让LTDC在扫描完一帧后的消隐期才切换显存地址。
- 更稳妥的:用双缓冲。一帧显示期间,你往另一块FrameBuffer里绘制内容,等垂直消隐到临时切换显存地址,这样彻底避免在绘制中间被LTDC读到不完整数据。
- 进阶的:利用LTDC行中断LIPCR,设置一个在消隐区前或者消隐期内的行号,触发中断后执行显存地址切换。F429有LTDC_LIPCR、LTDC_LISR等寄存器,可按行触发中断。
我实际使用中,双缓冲配合VBR重载效果最好。程序里维护两个显存地址,一个当前显示,一个正在绘制;VSYNC中断到来时,交换两个地址并触发VBR重载。这个方案虽然占用的SDRAM内存翻倍,但省心,画面干净利落。
6.4 关于FSMC+DMA驱动RGB屏的一个补充
热词里有人提到FSMC+DMA驱动LCD的同步问题。我必须说,如果你只是想把一块RGB屏点亮,FSMC+DMA确实是可以跑的,但那是用非标准的方式去模拟RGB传输,任何时序毛刺都会直接反映成画面异常。LTDC方案下,这些同步问题由硬件时序生成器解决,你反而只需要关心显存地址的切换时机。这两者的调试思路完全不同,如果你发现FSMC+DMA方案同步问题怎么都调不好,考虑转LTDC可能是更高效的选择。
结尾聊点个人体会
这套LTDC驱动前前后后我移植过好几个项目,最深的感受是:寄存器库驱动真正难的不是某个寄存器不会配,而是它和以前“写命令控制屏”的思维模式完全不一样。一旦接受“MCU即显卡,显存即天下”这个设定,所有寄存器之间的关系就都顺了。
调试时建议把手上的屏的规格书完整读一遍,尤其是时序参数表,不要靠猜。遇到白屏就按第6节的链路逐条查,遇到撕裂就上双缓冲加VBR,这两招能解决绝大部分疑难问题。另外,寄存器库驱动有个天然优势:不受HAL库版本升级影响,代码贴到工程里就能用,出问题也好跟踪到底层。如果你正在做F429的RGB屏项目,希望这篇能帮你少走几天的弯路。
本文还有配套的精品资源,点击获取