4. 硬件抽象层(HAL)基础
显示控制器,说白了就是 MCU 和屏幕之间的「翻译官」。你想想看,MCU 里跑的是 RGB 像素数据,屏幕要的是特定时序的电压信号,中间没有驱动层,数据根本过不去。
我最早接触 TouchGFX 时,以为驱动就是填几个寄存器。结果第一次点亮屏幕,花屏了整整三天。后来才明白——显示控制器的适配,核心就三件事:时序对、引脚对、数据格式对。今天咱们就把 LTDC、DSI 和 SPI 这三种最常见的接口,一个一个拆开讲。
核心要点:无论哪种接口,驱动适配的最终目标都是让 TouchGFX 的 FrameBuffer 能正确写入像素数据,并且屏幕能按刷新率稳定显示。
4.1 LTDC 驱动:直接控制 RGB 并行接口
LTDC 是 STM32 上最常见的显示控制器。它直接输出 RGB 数据和同步信号,不需要额外的显示芯片。我个人习惯把 LTDC 的适配分成三步走:时钟配置 → 时序参数 → 引脚映射。
4.1.1 时钟树配置
LTDC 的像素时钟(PCLK)决定了刷新率。公式很简单:
PCLK = H_Total × V_Total × Refresh_Rate
举个例子,480×272 的屏,H_Total=525,V_Total=286,60Hz 刷新率:
PCLK = 525 × 286 × 60 ≈ 9.0 MHz
嗯,这里要注意——PCLK 不能超过 LTDC 时钟源的上限。我在项目中遇到过有人把 PCLK 配到 30MHz,结果屏幕直接黑掉,因为时钟源分频没算对。
经验之谈:用 CubeMX 生成 LTDC 初始化代码时,记得手动检查 PLLSAI 的配置。CubeMX 有时会算出非整数分频,导致 PCLK 偏差超过 5%,屏幕就会闪烁。
4.1.2 时序参数配置
每个 RGB 屏的数据手册都会给一组时序参数。你需要把它们填进 LTDC 的寄存器。我整理了一个对照表:
| 时序参数 | LTDC 寄存器 | 说明 |
|---|---|---|
| H_Sync | LxCFG1.HSPW | 行同步脉冲宽度 |
| H_Back_Porch | LxCFG1.HBP | 行同步后肩 |
| H_Front_Porch | LxCFG1.HFP | 行同步前肩 |
| V_Sync | LxCFG1.VSPW | 帧同步脉冲宽度 |
| V_Back_Porch | LxCFG1.VBP | 帧同步后肩 |
| V_Front_Porch | LxCFG1.VFP | 帧同步前肩 |
我曾经踩过一个坑:某款屏的 H_Back_Porch 是 46 个 PCLK,但我配成了 43。结果屏幕左边有 3 个像素的偏移,整个画面往右挪了一截。后来用逻辑分析仪抓波形才找到问题。
4.1.3 引脚映射与复用
LTDC 需要大量 GPIO,24 位 RGB 加上同步信号,轻松超过 30 个引脚。我的建议是:
- 优先使用 AF 复用功能,不要手动操作 GPIO 寄存器
- 检查引脚冲突,LTDC 引脚经常和 FMC、SDIO 共用
- 注意信号完整性,RGB 线尽量等长,CLK 线单独包地
警告:LTDC 的 DE(数据使能)信号必须正确连接。如果 DE 悬空或接错,屏幕会显示全白或全黑。我见过有人把 DE 接到了 VSYNC 上,折腾了两天才发现。
4.2 DSI 驱动:串行差分的高带宽方案
DSI 是 MIPI 联盟的标准,用差分信号传输像素数据。它比 LTDC 少用很多引脚——4 Lane 的 DSI 只需要 10 根线,却能驱动 1080p 的屏幕。说白了,这是高端嵌入式设备的首选。
4.2.1 DSI 的两种工作模式
DSI 有两种模式,你得根据屏幕类型选:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 视频模式 | 实时传输像素流,无需帧缓冲 | 大屏、高刷新率 |
| 命令模式 | 通过 DCS 命令写入显存 | 小屏、低功耗 |
我建议:如果 MCU 内部有足够大的 SRAM 或 SDRAM 做帧缓冲,优先用视频模式。命令模式虽然省电,但每次刷屏都要发命令,TouchGFX 的动画效果会卡顿。
4.2.2 DSI 初始化流程
DSI 的初始化比 LTDC 复杂,因为它涉及 PHY 层和协议层。我的标准流程是:
- 配置 DSI 时钟:设置 PLL 输出频率,确保满足 Lane 速率
- 初始化 DSI 主机:设置 Lane 数、极性、Escape 时钟
- 启动 DSI PHY:等待 PHY 锁定,检查状态寄存器
- 发送 DCS 命令:初始化屏幕(Sleep Out、Display On 等)
- 配置视频模式时序:与 LTDC 类似,但参数通过 DSI 封装
关键代码片段:DSI 启动 PHY 的等待循环,很多人会漏掉超时处理。
// 等待 DSI PHY 锁定
uint32_t timeout = 1000000;
while (!(DSI->ISR & DSI_ISR_PLLLS)) {
if (--timeout == 0) {
// 错误处理:PHY 未锁定
break;
}
}
4.2.3 与 TouchGFX 的集成
TouchGFX 通过 TouchGFXHAL 类与显示驱动交互。对于 DSI,你需要重写 flushFrameBuffer() 方法:
void TouchGFXHAL::flushFrameBuffer(const Rect& rect) {
// 计算要刷新的像素区域
uint16_t* fb = getFrameBuffer();
uint32_t offset = rect.y * LCD_WIDTH + rect.x;
// 通过 DSI 写入像素数据
DSI->DCWR = (uint32_t)&fb[offset];
DSI->DCCR = rect.width * rect.height * 2; // RGB565 格式
}
嗯,这里要注意——DSI 的写入地址必须 4 字节对齐。我遇到过因为地址没对齐,导致 DMA 传输卡死的情况。
4.3 SPI 驱动:小尺寸屏的经典方案
SPI 接口的屏幕成本低、引脚少,在 3.5 寸以下的市场里非常常见。但它的缺点也很明显——速度慢。SPI 时钟一般跑到 40~80MHz,刷 320×240 的屏,全屏刷新大概要 10ms,做动画会吃力。
4.3.1 SPI 驱动架构
SPI 屏通常带一个内部控制器(比如 ILI9341、ST7789),MCU 通过 SPI 命令操作它的显存。TouchGFX 的适配思路是:
- 使用 SPI DMA:不要用轮询方式发数据,否则 CPU 占用率会飙到 90%
- 实现局部刷新:只更新变化区域,减少 SPI 传输量
- 利用屏幕的窗口命令:设置 CASET/RASET 限定刷新区域
我的习惯:SPI 驱动里加一个 spi_dma_transfer_done 回调,用信号量通知 TouchGFX 可以继续刷下一帧。这样能避免帧撕裂。
4.3.2 代码实现示例
下面是一个简化的 SPI 刷屏函数,配合 TouchGFX 的 flushFrameBuffer:
void LCD_IO_WriteMultipleData(uint16_t* data, uint32_t count) {
// 设置屏幕窗口(假设只刷新脏矩形)
LCD_IO_WriteCmd(0x2A); // CASET
LCD_IO_WriteData(rect.x >> 8);
LCD_IO_WriteData(rect.x & 0xFF);
LCD_IO_WriteData((rect.x + rect.width - 1) >> 8);
LCD_IO_WriteData((rect.x + rect.width - 1) & 0xFF);
LCD_IO_WriteCmd(0x2B); // RASET
// ... 类似设置 y 范围
// 写像素数据
LCD_IO_WriteCmd(0x2C); // RAMWR
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)data, count * 2);
}
避坑指南:SPI 屏的 DCX(数据/命令选择)引脚必须在发送每个字节前设置正确。我曾经在 DMA 传输中忘了切换 DCX,结果屏幕把像素数据当成了命令,画面直接花掉。
4.4 三种接口的对比与选型建议
最后,我根据自己的项目经验,给出一份选型参考:
| 维度 | LTDC | DSI | SPI |
|---|---|---|---|
| 引脚数 | 30+ | 6~12 | 4~5 |
| 最大分辨率 | 1024×768 | 1920×1080 | 480×320 |
| 帧率(典型) | 60fps | 60fps | 30~40fps |
| 硬件图层 | 支持 | 支持 | 不支持 |
| 驱动复杂度 | 中 | 高 | 低 |
| 成本 | 中 | 高 | 低 |
我个人建议:如果你的项目引脚充裕、分辨率在 800×480 以下,LTDC 是最稳的选择。DSI 适合高端产品,但调试起来确实费时间。SPI 嘛,做小尺寸原型验证最快,但别指望它跑复杂的动画。
好了,这一章的内容就到这儿。三种接口的驱动适配,说白了就是「时序配好、数据传对、中断管住」。你动手写代码时,记得先用逻辑分析仪抓一下波形,确认时序对不对——这一步能省掉你后面至少一半的调试时间。