3、设备树配置实战:显示控制器设备树节点详解、时钟与复位配置、LVDS/HDMI输出接口配置

设备树配置,说白了就是告诉内核:「你的硬件长什么样,怎么叫醒它干活」。R-Car 的显示驱动移植,设备树写不对,后面全是白搭。我刚开始接触 R-Car 时,就吃过这个亏——时钟配错一个 parent,屏幕死活不亮,查了三天才发现是设备树里一个 clock-names 写错了。

这一章,我们直接动手。我会把 DU(Display Unit)的节点掰开揉碎,时钟复位怎么配,LVDS 和 HDMI 的 output 端口怎么接,全部过一遍。

3.1 显示控制器(DU)设备树节点详解

R-Car 的显示控制器在设备树里叫 du。不同 SoC 的 DU 实例数量不一样,比如 R-Car H3 有 3 个 DU 通道,M3 有 2 个。但节点结构是统一的。

先看一个典型的 DU 节点骨架:

&du {
    clocks = <&cpg CPG_MOD 724>,
             <&cpg CPG_MOD 723>,
             <&cpg CPG_MOD 722>,
             <&cpg CPG_MOD 721>,
             <&cpg CPG_MOD 727>;
    clock-names = "du.0", "du.1", "du.2", "du.3", "lvds.0";
    resets = <&cpg 724>, <&cpg 723>, <&cpg 722>, <&cpg 721>;
    reset-names = "du.0", "du.1", "du.2", "du.3";

    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        port@0 {
            reg = <0>;
            du_out_rgb: endpoint {
                remote-endpoint = <&rgb_connector>;
            };
        };

        port@1 {
            reg = <1>;
            du_out_lvds0: endpoint {
                remote-endpoint = <&lvds0_in>;
            };
        };

        port@2 {
            reg = <2>;
            du_out_hdmi: endpoint {
                remote-endpoint = <&hdmi_in>;
            };
        };
    };
};

这里有几个关键点,我一个个说。

3.1.1 clocks 与 clock-names

DU 的时钟源不止一个。每个 DU 通道有自己的时钟门控,LVDS 也有独立的 PLL 时钟。你看上面的例子,5 个 clock 分别对应 4 个 DU 通道 + 1 个 LVDS。

clock-names 对应模块 CPG 模块 ID(H3 为例)
du.0 DU 通道 0 724
du.1 DU 通道 1 723
du.2 DU 通道 2 722
du.3 DU 通道 3 721
lvds.0 LVDS PLL 727
注意: 不同 R-Car 系列的 CPG 模块 ID 不一样。比如 R-Car M3 的 DU 只有 2 个通道,clock 数量就少。千万别直接抄 H3 的配置,一定要查对应 SoC 的 datasheet 或参考设备树。

3.1.2 resets 与 reset-names

复位配置和时钟是一一对应的。每个 DU 通道在使能时钟前,必须先释放复位。顺序不能乱——先复位,再时钟,否则硬件会挂。

我遇到过一个问题:复位配了,但 reset-names 写成了 "du0" 而不是 "du.0",少了个点。内核驱动匹配不到,复位一直没释放,DU 模块直接罢工。这种坑,你想想看,查 log 都查不出来,因为驱动根本没报错,只是默默不干活。

3.1.3 ports 输出端口

每个 port 对应一个 DU 通道的物理输出。port@0 通常是 RGB 并行接口,port@1 是 LVDS,port@2 是 HDMI。但这不是绝对的,具体要看 SoC 的 pinmux 和 board 设计。

每个 port 里有一个 endpoint,通过 remote-endpoint 连接到下游的显示接口节点。这个连接关系,就是整个显示链路的数据流走向。

3.2 时钟与复位配置实战

时钟配置是设备树里最容易出错的地方。我建议你按照这个步骤来检查:

  1. 确认 SoC 的 DU 通道数量 —— 查 datasheet 或参考设备树。
  2. 找到对应的 CPG 模块 ID —— 在 clk-rcar-gen3.c 或类似文件中可以找到。
  3. 按顺序列出 clocks —— 先 DU 通道,再 LVDS PLL。
  4. 配齐 resets —— 数量和顺序与 clocks 保持一致。
  5. 检查 clock-names 和 reset-names —— 名字必须和驱动里定义的字符串完全一致。
小技巧: 如果你不确定 clock-names 怎么写,直接去内核源码里搜 du_probe 函数,看它调用了 devm_clk_get 时传的参数是什么。驱动怎么写,设备树就怎么配。

举个例子,R-Car H3 的 DU 驱动里,clock 获取代码大概是这样的:

du->clocks[i] = devm_clk_get(&pdev->dev, clock_names[i]);

而 clock_names 数组定义在 rcar_du_of_clks 里:

static const char *const rcar_du_of_clks[H3_NUM_DU_CHANNELS + 1] = {
    "du.0", "du.1", "du.2", "du.3", "lvds.0"
};

看到没?名字必须一模一样。多一个空格都不行。

3.3 LVDS 输出接口配置

LVDS 在 R-Car 上通常用于连接 LCD 屏。配置 LVDS 接口,需要两个部分:DU 端的输出端口,和 LVDS 编码器节点。

先看 LVDS 编码器节点:

&lvds0 {
    clocks = <&cpg CPG_MOD 727>;
    clock-names = "fck";

    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        port@0 {
            reg = <0>;
            lvds0_in: endpoint {
                remote-endpoint = <&du_out_lvds0>;
            };
        };

        port@1 {
            reg = <1>;
            lvds0_out: endpoint {
                remote-endpoint = <&panel_in>;
            };
        };
    };
};

这里有两个 port:

  • port@0 —— 输入口,连接 DU 的 LVDS 输出。
  • port@1 —— 输出口,连接实际的 LCD 屏。

我曾经犯过一个错:把 port 的 reg 写反了。port@0 配成了输出,port@1 配成了输入。结果内核在解析 endpoint 时,发现数据流方向不对,直接报错 EPROBE_DEFER,驱动加载失败。查了半天才发现是 reg 顺序搞反了。

关键点: LVDS 的时钟 "fck" 必须和 DU 的 "lvds.0" 是同一个时钟源。如果两个节点用的时钟不同步,屏幕会出现闪烁或花屏。

3.4 HDMI 输出接口配置

HDMI 配置比 LVDS 复杂一些,因为涉及 HDMI 编码器(如 ADV7511 或 SiI9022)的 I2C 配置。R-Car 的 DU 输出的是并行 RGB 或数字视频信号,需要外接 HDMI 编码器转成 HDMI 协议。

设备树里,HDMI 编码器通常是一个 I2C 设备节点:

&i2c2 {
    hdmi-encoder@39 {
        compatible = "adi,adv7511";
        reg = <0x39>;
        interrupt-parent = <&gpio1>;
        interrupts = <15 IRQ_TYPE_LEVEL_LOW>;

        clocks = <&cpg CPG_MOD 722>;
        clock-names = "pixel";

        ports {
            #address-cells = <1>;
            #size-cells = <0>;

            port@0 {
                reg = <0>;
                hdmi_in: endpoint {
                    remote-endpoint = <&du_out_hdmi>;
                };
            };

            port@1 {
                reg = <1>;
                hdmi_out: endpoint {
                    remote-endpoint = <&hdmi_connector>;
                };
            };
        };
    };
};

这里要注意几个点:

  • compatible —— 必须和驱动匹配。ADV7511 用 "adi,adv7511",SiI9022 用 "sil,sii9022"。
  • reg —— I2C 地址,查编码器 datasheet。
  • pixel clock —— HDMI 编码器需要像素时钟,通常来自 DU 的某个通道时钟。
注意: HDMI 编码器的像素时钟频率必须和 DU 输出的分辨率匹配。比如 1080p60 需要 148.5MHz 的像素时钟。如果时钟频率不对,HDMI 输出会没有信号,或者画面撕裂。

3.5 完整的数据流链路

把上面所有节点串起来,整个显示链路是这样的:

R-Car 显示数据流链路 DU 显示控制器 LVDS 编码器 HDMI 编码器 LCD 屏 HDMI 连接器 port@1 port@2 LVDS 信号 HDMI 信号 💡 每个箭头对应设备树中的 remote-endpoint 连接。配错一个 endpoint,整条链路就断了。

从 DU 出来,数据流向 LVDS 或 HDMI 编码器,再到达最终的显示设备。每个环节的 endpoint 必须一一对应,少一个或者配错一个,屏幕都不会亮。

3.6 常见问题与排查方法

设备树配完了,屏幕不亮怎么办?我总结了几种常见情况:

现象 可能原因 排查方法
内核启动 log 无 DU 相关输出 设备节点未使能,或 compatible 不匹配 检查 status = "okay" 是否添加
屏幕全黑,无背光 时钟或复位配置错误 检查 /sys/kernel/debug/clk 下时钟是否使能
屏幕有背光但无画面 endpoint 连接错误 检查 remote-endpoint 的 phandle 是否正确
画面花屏或闪烁 像素时钟频率不匹配 用示波器测时钟频率,或调整 DU 的时钟 parent
HDMI 无信号 HDMI 编码器 I2C 通信失败 用 i2cdetect 检查编码器地址是否响应
我的习惯: 每次改完设备树,先 make dtbs 编译,然后用 dtc -I dtb -O dts 反编译确认生成的 DTS 内容。有时候语法错误不会报错,但生成的 DTB 是坏的。反编译看一眼,心里踏实。

嗯,设备树配置这块,说白了就是细心活。时钟、复位、端口、endpoint,每个字段都得对上。我刚开始做 R-Car 移植时,光设备树就改了十几版。但只要你把这一章的内容吃透了,后面调显示输出就会顺很多。