4. 显示控制器硬件抽象:CRTC与显示时序、扫描输出原理、硬件光标支持

好,我们继续往下走。这一章要聊的,是显示驱动里最核心、也最容易被忽视的部分——CRTC。说白了,CRTC就是显示控制器的“大脑”,它负责把内存里的像素数据,变成显示器能识别的信号。我当年第一次调CRTC时序的时候,差点被折腾疯掉。嗯,咱们慢慢说。

4.1 CRTC是什么?为什么需要它?

CRTC,全称是Cathode Ray Tube Controller。名字挺古老,但现代LCD屏也离不开它。它的核心任务就两个:

  • 生成显示时序:告诉显示器什么时候该扫哪一行、什么时候该换行、什么时候该刷新。
  • 控制帧缓冲读取:从显存里按顺序把像素数据搬出来,送给显示接口。

你想想看,如果没有CRTC,CPU就得自己盯着时钟,一行一行地往外送数据。那CPU啥也别干了,光伺候显示器就够呛。所以CRTC本质上是一个硬件状态机,它解放了CPU。

重要概念:CRTC不是“显示控制器”的全部,它只是其中的时序生成单元。显示控制器还包括FIFO、颜色查找表、硬件光标等模块。

4.2 显示时序:那些让你头疼的参数

调显示驱动,最烦的就是配时序参数。我见过不少新手,拿着数据手册一顿猛填,结果屏幕要么黑屏,要么画面撕裂。为什么会这样?因为时序参数之间是环环相扣的。

一个完整的显示行周期,包含以下几个阶段:

  1. HSYNC(水平同步):告诉显示器,我要开始新一行了。
  2. HBP(水平后沿):同步信号结束后,等待一段时间,让显示器准备好接收数据。
  3. 显示区域:真正传输像素数据的时间段。
  4. 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的显示控制器为例:

  1. 获取显示参数:从EDID或者设备树中读取时序参数。
  2. 配置像素时钟:设置PLL,生成目标像素时钟。比如1080p@60Hz需要148.5MHz。
  3. 配置CRTC寄存器:写入HTotal、VTotal、HSYNC、VSYNC等参数。
  4. 配置帧缓冲地址:告诉CRTC,帧缓冲在内存的哪个位置。
  5. 使能CRTC:启动扫描输出。
  6. 配置硬件光标:加载光标图案,设置初始位置。
// 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和硬件光标,是显示驱动里最底层的硬件抽象。理解了它们,你就能看懂大部分显示控制器的数据手册了。下一章,咱们聊聊帧缓冲的具体实现,包括多缓冲和页面翻转。到时候见。