4. LCD控制器驱动移植(IMX6ULL):从芯片手册到DMA配置

LCD驱动移植,说白了就是让CPU和屏幕能对上话。IMX6ULL的ELCDIF控制器,我前前后后调过不下十块板子,每次踩的坑都不太一样。今天咱们就从芯片手册开始,一步步把整个流程走通。

核心要点:ELCDIF(Enhanced LCD Interface)是IMX6ULL内置的显示控制器,支持RGB接口和ITU-R BT.656接口。我们主要用RGB接口接TFT屏。

4.1 芯片手册ELCDIF章节精读

拿到芯片手册,别急着翻代码。我习惯先看Chapter 34: Enhanced LCD Interface (ELCDIF)。重点看这几个寄存器组:

寄存器组 偏移地址 作用
CTRL 0x00 控制寄存器,使能、复位、数据格式选择
CTRL1 0x04 额外控制,RGB顺序、DMA请求触发
TRANSFER_COUNT 0x20 设置分辨率(宽×高)
CUR_BUF / NEXT_BUF 0x30 / 0x34 显存地址寄存器,DMA搬运目标
VDCTRL0 ~ VDCTRL4 0x40 ~ 0x50 时序控制,VSYNC/HSYNC/DE/像素时钟
PIXEL_TRANSFER 0x60 像素传输配置,颜色深度

嗯,这里要注意:CTRL寄存器的[28:24]位是LCDIF_MASTER,必须设置为0x7才能让控制器工作在master模式。我曾经在这卡了两天,手册写得不明显,后来翻reference manual的勘误表才找到。

4.2 设备树中LCD节点配置

设备树是Linux下描述硬件的标准方式。IMX6ULL的LCD节点通常在imx6ull.dtsi里已经定义了骨架,我们需要在板级dts里填充具体参数。

我贴一个我实际项目里用过的配置:

&lcdif {
    pinctrl-names = "default";
    pinctrl-0 = &pinctrl_lcdif_dat
                &pinctrl_lcdif_ctrl;
    display = &display0;
    status = "okay";

    display0: display {
        bits-per-pixel = <32>;
        bus-width = <24>;

        display-timings {
            native-mode = <&timing0>;
            timing0: timing0 {
                clock-frequency = <33000000>;
                hactive = <800>;
                vactive = <480>;
                hback-porch = <46>;
                hfront-porch = <210>;
                hsync-len = <1>;
                vback-porch = <23>;
                vfront-porch = <22>;
                vsync-len = <1>;
                de-active = <1>;
                pixelclk-active = <0>;
            };
        };
    };
};

个人经验:pixelclk-active这个参数特别容易搞反。0表示数据在时钟下降沿采样,1表示上升沿。我建议先用示波器看下屏的时序要求,或者直接问屏厂FAE。曾经有个项目,就因为这里设反了,画面一直有雪花点。

4.3 时钟与复位初始化

时钟配置是LCD驱动的命脉。IMX6ULL的LCD时钟来自PLL5(Video PLL),经过CCM(Clock Controller Module)分频后供给ELCDIF。

我个人习惯在arch/arm/mach-imx/clk-imx6ul.c里确认时钟树:

/* 时钟路径:PLL5 -> lcdif_pre_sel -> lcdif_pred -> lcdif_podf -> lcdif_sel */
static const char *lcdif_pre_sel[] = { "pll5_video_div", "pll2_pfd2_396m", ... };

复位这块,ELCDIF有软复位和硬复位两种:

  • 硬复位:通过CCM的CCGRx寄存器关闭再打开LCD时钟
  • 软复位:设置CTRL寄存器的[1]位(SW_RST)为1,等待清零

我建议驱动初始化时先做一次硬复位,再做一次软复位。为什么?因为有些屏在uboot阶段已经被初始化过,寄存器状态不确定。双重复位能保证ELCDIF回到干净状态。

注意:软复位后必须等待至少5个时钟周期再配置其他寄存器。我见过有人直接连续写寄存器,结果CTRL1的值死活写不进去。加个udelay(10)就解决了。

4.4 显存分配与DMA配置

显存分配是LCD驱动里最容易出问题的地方。IMX6ULL的ELCDIF使用DMA方式从内存搬运像素数据到LCD接口。

分配显存时要注意:

  1. 连续物理内存:DMA需要物理地址连续,不能用vmalloc
  2. 对齐要求:起始地址至少16字节对齐,我习惯用64字节对齐
  3. 大小计算:width × height × (bpp / 8),比如800×480×32bpp = 1,536,000字节

代码实现上,我推荐用dma_alloc_coherent:

/* 分配显存 */
fbdev->screen_base = dma_alloc_coherent(&pdev->dev,
                                         SZ_2M,  /* 多分配一些,避免越界 */
                                         &fbdev->screen_phys,
                                         GFP_KERNEL);
if (!fbdev->screen_base) {
    dev_err(&pdev->dev, "Failed to allocate framebuffer memory\n");
    return -ENOMEM;
}

/* 配置DMA地址 */
writel(fbdev->screen_phys, lcdif_base + CUR_BUF);
writel(fbdev->screen_phys, lcdif_base + NEXT_BUF);

DMA配置的核心是CTRL1寄存器的[19:16]位,用于设置DMA请求触发条件。我一般设为0x0(VSYNC触发),这样每帧开始才搬运数据,避免撕裂。

ELCDIF DMA数据流与显存管理 CPU / 应用层 显存 (Framebuffer) ELCDIF DMA LCD 屏幕 mmap / write DMA搬运 RGB数据 时序控制器 VSYNC触发 关键说明: 1. 显存由 dma_alloc_coherent 分配,保证物理连续 2. DMA在VSYNC信号到来时开始搬运下一帧数据,避免画面撕裂

4.5 避坑指南与调试技巧

最后分享几个我实际项目中踩过的坑:

坑1:显存地址写错

我曾经把CUR_BUF和NEXT_BUF都写成了同一个地址,结果画面一直闪烁。后来才发现,ELCDIF内部有双缓冲机制,两个地址必须指向不同的显存区域,或者都指向同一个区域(单缓冲模式)。

调试技巧:如果屏幕不亮,先检查这几个点:

  • 时钟频率对不对?用示波器量LCD_CLK引脚
  • 复位信号有没有拉高?量LCD_RESET引脚
  • 设备树里status是不是"okay"?我犯过写成了"ok"的低级错误
  • 显存里有没有数据?用devmem2读一下物理地址