4. 显示控制器驱动移植与LTDC配置
好,到了这一章,我们终于要跟硬件正面打交道了。
前面几章我们搭好了工程框架,配好了时钟和内存。但说实话,那些都是热身。真正的硬骨头,是让屏幕亮起来。而屏幕亮起来的关键,就是LTDC——LCD-TFT显示控制器。
我个人习惯把LTDC比作一个“快递分拣中心”。它从内存里把图像数据取出来,按照你设定的格式和时序,一包一包地发给屏幕。你配置得好,画面就流畅;配不好,要么花屏,要么干脆黑屏。
4.1 LTDC到底在干什么?
先别急着写代码。我们得搞清楚LTDC的工作流程。
你看这张图,就明白了。
数据从帧缓冲(SDRAM)出发,经过DMA2D加速(可选),送到LTDC。LTDC根据你配的时序参数,把数据转换成RGB信号,通过引脚发给LCD面板。
嗯,这里要注意:LTDC本身不存储图像数据,它只是一个搬运工。所以帧缓冲的地址、像素格式、层配置,一个都不能错。
4.2 时序参数:从数据手册里抠数字
移植LTDC最烦人的一步,就是配时序参数。为什么?因为每个屏幕的时序都不一样。
我记得第一次做这个的时候,拿着一个4.3寸屏的数据手册,翻来覆去找了半小时才找全参数。后来我总结了一个规律:你只需要关注数据手册里的“Timing Characteristics”表格。
以最常见的480x272分辨率屏幕为例,你需要提取以下参数:
| 参数 | 符号 | 典型值 | 说明 |
|---|---|---|---|
| 水平同步宽度 | HSW | 41 | 行同步脉冲宽度(像素时钟数) |
| 水平后沿 | HBP | 2 | 行同步结束到有效数据开始 |
| 水平前沿 | HFP | 2 | 有效数据结束到下一个行同步 |
| 垂直同步宽度 | VSW | 10 | 帧同步脉冲宽度(行数) |
| 垂直后沿 | VBP | 2 | 帧同步结束到有效数据开始 |
| 垂直前沿 | VFP | 2 | 有效数据结束到下一个帧同步 |
4.3 像素时钟的计算
时序参数配好了,像素时钟(Pixel Clock)怎么算?
公式其实很简单:
Pixel Clock = (HSW + HBP + Active Width + HFP) × (VSW + VBP + Active Height + VFP) × 刷新率
举个例子,480x272的屏幕,刷新率60Hz:
一行总像素 = 41 + 2 + 480 + 2 = 525
一帧总行数 = 10 + 2 + 272 + 2 = 286
Pixel Clock = 525 × 286 × 60 ≈ 9.0 MHz
嗯,这里要注意:你配的LTDC时钟源(比如PLLSAI)必须能整除出这个频率。如果除不尽,屏幕会出现水波纹或者闪烁。
4.4 LTDC层配置:图层叠加与颜色格式
LTDC支持最多2个图层(Layer0和Layer1)。你可以把它们叠在一起,实现类似“背景+前景”的效果。
我个人习惯把Layer0作为背景层,Layer1作为UI层。这样TouchGFX只操作Layer1,背景层可以放一个静态图片或者纯色,减少刷新开销。
配置图层时,你需要指定:
- 帧缓冲地址: 必须是32位对齐的地址
- 像素格式: 常见的有RGB565、RGB888、ARGB8888
- 窗口位置: 可以只显示帧缓冲的一部分
- Alpha混合: 图层透明度
看一段实际的初始化代码:
static void LTDC_Layer_Init(void)
{
LTDC_LayerCfgTypeDef layer_cfg;
// Layer0 配置:背景层
layer_cfg.WindowX0 = 0;
layer_cfg.WindowY0 = 0;
layer_cfg.WindowX1 = 480;
layer_cfg.WindowY1 = 272;
layer_cfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
layer_cfg.FBStartAdress = (uint32_t)&background_buffer;
layer_cfg.Alpha = 255;
layer_cfg.Alpha0 = 0;
layer_cfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA;
layer_cfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA;
layer_cfg.ImageWidth = 480;
layer_cfg.ImageHeight = 272;
HAL_LTDC_ConfigLayer(&hltdc, &layer_cfg, 0);
// Layer1 配置:UI层
layer_cfg.FBStartAdress = (uint32_t)ui_buffer;
layer_cfg.Alpha = 255;
HAL_LTDC_ConfigLayer(&hltdc, &layer_cfg, 1);
}
4.5 引脚映射与GPIO复用
LTDC的RGB信号是通过GPIO引脚输出的。不同芯片的引脚映射不一样,你得对照原理图和数据手册来配。
以STM32F429为例,LTDC的引脚分布如下:
| 信号 | 引脚 | 说明 |
|---|---|---|
| LCD_CLK | PG7 | 像素时钟 |
| LCD_HSYNC | PI12 | 行同步 |
| LCD_VSYNC | PI13 | 帧同步 |
| LCD_DE | PK7 | 数据使能 |
| LCD_R[7:0] | PJ0-PJ7 | 红色数据 |
| LCD_G[7:0] | PK0-PK7 | 绿色数据 |
| LCD_B[7:0] | PJ8-PJ15 | 蓝色数据 |
配置GPIO时,记得把复用功能设为AF14(LTDC)。我见过有人配成了AF13,结果屏幕死活不亮,查了半天才发现是复用功能选错了。
4.6 中断与同步:别让屏幕撕裂
屏幕撕裂(Tearing)是什么?就是画面一半是上一帧,一半是当前帧。看起来像撕开了一样。
为什么会这样?因为你在LTDC正在读取帧缓冲的时候,往里面写了新数据。
解决办法有两个:
- 使用垂直同步中断: 在VSYNC中断里切换缓冲
- 使用双缓冲: 一个缓冲被LTDC读取,另一个缓冲被CPU写入,VSYNC时交换
TouchGFX默认使用双缓冲+VSYNC同步。你需要使能LTDC的Line中断或者VSYNC中断:
HAL_NVIC_SetPriority(LTDC_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(LTDC_IRQn);
// 在中断服务函数中
void LTDC_IRQHandler(void)
{
if(__HAL_LTDC_GET_FLAG(&hltdc, LTDC_FLAG_VSYNC))
{
// 通知TouchGFX切换缓冲
touchgfx::HAL::getInstance()->signalVSync();
__HAL_LTDC_CLEAR_FLAG(&hltdc, LTDC_FLAG_VSYNC);
}
}
4.7 调试技巧:黑屏了怎么办?
说实话,LTDC移植第一次就能点亮屏幕的概率,不高。我做了这么多年,也经常遇到黑屏。
如果你遇到黑屏,按这个顺序排查:
- 检查时钟: PLLSAI有没有使能?LTDC时钟有没有输出?用示波器量LCD_CLK引脚
- 检查复位: LTDC外设有没有被复位?有些芯片默认是关闭的
- 检查引脚: GPIO复用功能对不对?有没有被其他外设占用?
- 检查时序: HSW、HBP这些参数有没有配反?我曾经把HSW和HBP写反了,屏幕闪得厉害
- 检查帧缓冲地址: 地址对不对?SDRAM有没有初始化成功?
4.8 本章小结
LTDC移植说白了就是三件事:配时序、配图层、配引脚。时序参数从数据手册里抠,图层配置注意地址对齐,引脚映射别搞错复用功能。
如果你按照这个流程走下来,屏幕应该能亮了。下一章我们会把TouchGFX的渲染引擎挂到LTDC上,让UI真正跑起来。
嗯,先到这。去试试吧,有问题随时翻回来看看。