4. 显示控制器硬件抽象:CRTC与显示时序、扫描输出原理、硬件光标支持
好,我们继续往下走。这一章要聊的,是显示驱动里最核心、也最容易被忽视的部分——CRTC。说白了,CRTC就是显示控制器的“大脑”,它负责把内存里的像素数据,变成显示器能识别的信号。我当年第一次调CRTC时序的时候,差点被折腾疯掉。嗯,咱们慢慢说。
4.1 CRTC是什么?为什么需要它?
CRTC,全称是Cathode Ray Tube Controller。名字挺古老,但现代LCD屏也离不开它。它的核心任务就两个:
- 生成显示时序:告诉显示器什么时候该扫哪一行、什么时候该换行、什么时候该刷新。
- 控制帧缓冲读取:从显存里按顺序把像素数据搬出来,送给显示接口。
你想想看,如果没有CRTC,CPU就得自己盯着时钟,一行一行地往外送数据。那CPU啥也别干了,光伺候显示器就够呛。所以CRTC本质上是一个硬件状态机,它解放了CPU。
重要概念:CRTC不是“显示控制器”的全部,它只是其中的时序生成单元。显示控制器还包括FIFO、颜色查找表、硬件光标等模块。
4.2 显示时序:那些让你头疼的参数
调显示驱动,最烦的就是配时序参数。我见过不少新手,拿着数据手册一顿猛填,结果屏幕要么黑屏,要么画面撕裂。为什么会这样?因为时序参数之间是环环相扣的。
一个完整的显示行周期,包含以下几个阶段:
- HSYNC(水平同步):告诉显示器,我要开始新一行了。
- HBP(水平后沿):同步信号结束后,等待一段时间,让显示器准备好接收数据。
- 显示区域:真正传输像素数据的时间段。
- HFP(水平前沿):一行数据传完了,再等一会儿,准备下一行。
垂直方向也一样,有VSYNC、VBP、显示行数、VFP。这些参数合起来,就构成了一个完整的帧周期。
| 参数 | 说明 | 常见范围(1080p@60Hz) |
|---|---|---|
| HTotal | 水平总像素(显示+消隐) | 2200 |
| VTotal | 垂直总行数(显示+消隐) | 1125 |
| Pixel Clock | 像素时钟频率 | 148.5 MHz |
| HSYNC Width | 水平同步脉冲宽度 | 44 |
| VSYNC Width | 垂直同步脉冲宽度 | 5 |
个人经验:我建议你拿到一块新屏,先查它的EDID数据。EDID里通常包含了标准时序。如果EDID没有,那就去VESA标准里找。千万别自己瞎猜参数,我曾经因为HBP填错了一个像素,导致屏幕右边出现一条黑边,查了两天才发现。
4.3 扫描输出原理:从帧缓冲到像素流
CRTC是怎么把帧缓冲里的数据变成像素流的?这个过程,我习惯把它分成三步:
第一步:地址生成
CRTC内部有一个地址计数器。每过一个像素时钟,地址就加1。当一行结束时,地址会加上一个“行跨度”值,跳到下一行的起始位置。这个行跨度,通常等于帧缓冲的宽度,但有时为了内存对齐,会比显示宽度大一些。
第二步:数据读取
地址生成后,CRTC通过DMA或者直接内存访问,从显存里把像素数据读到内部的FIFO里。FIFO的作用是缓冲,防止因为内存带宽波动导致画面断流。
第三步:数据格式化
读出来的原始像素数据,可能是RGB565、RGB888、ARGB8888等格式。CRTC会根据配置,把数据转换成显示接口需要的格式。比如LVDS接口可能需要6位或8位颜色深度。
// 伪代码:CRTC扫描一行
void crtc_scan_line(struct crtc *crtc, int line) {
uint32_t *fb_addr = crtc->fb_base + line * crtc->stride;
for (int x = 0; x < crtc->hactive; x++) {
uint32_t pixel = fb_addr[x];
// 将像素送入FIFO
fifo_push(crtc->fifo, pixel);
}
}
注意:帧缓冲的stride(行跨度)和显示宽度不一定相等。比如显示宽度是1920,但为了64字节对齐,stride可能是1920 + 8 = 1928。如果你忘了这个,画面会出现斜纹或者错位。我踩过这个坑,真的。
4.4 硬件光标支持:不占CPU的鼠标指针
硬件光标,也叫硬件游标。它的原理很简单:在CRTC扫描输出的时候,如果当前扫描位置正好落在光标区域内,就用光标图案覆盖掉原来的像素数据。整个过程不涉及CPU,也不修改帧缓冲内容。
硬件光标通常有以下几个寄存器:
- 光标位置寄存器:设置光标在屏幕上的X、Y坐标。
- 光标图案寄存器:存放光标位图,通常是64x64或32x32像素。
- 光标颜色寄存器:定义光标的前景色和背景色。
- 光标使能寄存器:开启或关闭光标显示。
为什么需要硬件光标?因为软件光标需要CPU在每一帧都去修改帧缓冲里的像素,而且还要处理鼠标移动时的擦除和重绘。这在低端嵌入式系统上,会消耗大量CPU时间。硬件光标则完全免费,零开销。
关键点:硬件光标和帧缓冲是独立的。光标图案存储在CRTC内部的SRAM里,不占用显存。所以即使你切换帧缓冲,光标依然存在。
4.5 实战中的CRTC配置流程
好了,理论说完了。咱们看看实际项目中怎么配CRTC。我以QNX下操作i.MX8的显示控制器为例:
- 获取显示参数:从EDID或者设备树中读取时序参数。
- 配置像素时钟:设置PLL,生成目标像素时钟。比如1080p@60Hz需要148.5MHz。
- 配置CRTC寄存器:写入HTotal、VTotal、HSYNC、VSYNC等参数。
- 配置帧缓冲地址:告诉CRTC,帧缓冲在内存的哪个位置。
- 使能CRTC:启动扫描输出。
- 配置硬件光标:加载光标图案,设置初始位置。
// QNX下配置CRTC的简化示例
void crtc_init(struct crtc_dev *dev) {
// 1. 设置像素时钟
dev->set_pixel_clock(dev, 148500000); // 148.5 MHz
// 2. 配置水平时序
dev->write_reg(CRTC_HTOTAL, 2200);
dev->write_reg(CRTC_HACTIVE, 1920);
dev->write_reg(CRTC_HSYNC, 44);
dev->write_reg(CRTC_HBP, 148);
dev->write_reg(CRTC_HFP, 88);
// 3. 配置垂直时序
dev->write_reg(CRTC_VTOTAL, 1125);
dev->write_reg(CRTC_VACTIVE, 1080);
dev->write_reg(CRTC_VSYNC, 5);
dev->write_reg(CRTC_VBP, 36);
dev->write_reg(CRTC_VFP, 4);
// 4. 设置帧缓冲地址
dev->write_reg(CRTC_FB_ADDR, dev->fb_phys);
// 5. 使能CRTC
dev->write_reg(CRTC_ENABLE, 1);
}
避坑指南:我曾经在配置CRTC时,忘了设置帧缓冲的stride。结果画面显示出来是斜的,每一行都偏移了几个像素。后来发现,CRTC默认stride等于HACTIVE,但我的帧缓冲因为对齐要求,stride比HACTIVE大了8个字节。所以,一定要显式设置stride,不要依赖默认值。
4.6 硬件光标的QNX实现要点
在QNX下实现硬件光标,通常需要写一个简单的驱动,暴露给上层一个接口。上层应用通过ioctl来设置光标位置、图案和可见性。
我习惯这样设计:
- 光标图案:用2位色深,00表示透明,01表示前景色,10表示背景色,11表示反转色。
- 光标热区:定义光标点击点的偏移量。比如箭头光标的尖点通常在左上角,偏移量就是(0, 0)。
- 光标位置:支持硬件自动更新,还是需要软件轮询?我个人建议用硬件自动更新,省事。
// 硬件光标控制结构体
struct hwcursor {
uint16_t x, y; // 光标位置
uint8_t pattern[64]; // 64x64 2bpp 光标图案
uint16_t hot_x, hot_y; // 热区偏移
uint8_t enable; // 使能标志
};
嗯,这一章的内容就到这里。CRTC和硬件光标,是显示驱动里最底层的硬件抽象。理解了它们,你就能看懂大部分显示控制器的数据手册了。下一章,咱们聊聊帧缓冲的具体实现,包括多缓冲和页面翻转。到时候见。