4. 设备树基础:显示相关的设备树节点编写

设备树,说白了就是一块“硬件描述板”。你告诉内核:我有哪些外设、它们挂在哪里、怎么配置。显示驱动尤其依赖它——LCD的时序、引脚、复位顺序,全得靠设备树说清楚。

我刚开始接触设备树时,总觉得它就是个配置文件。后来调一块MIPI屏,死活点不亮,最后发现是pinctrl里一个引脚功能没配对。嗯,从那以后我再也不敢小看设备树了。

4.1 显示设备树节点的基本骨架

一个典型的LCD设备树节点,长这样:

&lcd_panel: lcd-panel {
    compatible = "xx公司,xx型号", "simple-panel";
    reg = <0x0 0x0 0x0>;
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&lcd_pins_active>;
    pinctrl-1 = <&lcd_pins_sleep>;
    backlight = <&backlight>;
    enable-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
    reset-gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;
    port {
        lcd_in: endpoint {
            remote-endpoint = <&display_controller_out>;
        };
    };
};

这个节点里,compatible、reg、pinctrl 是三个最关键的属性。我们一个一个拆开讲。

4.2 compatible:驱动匹配的“身份证”

compatible 是内核用来找驱动的。内核遍历设备树节点时,拿这个字符串去匹配驱动里的 of_match_table。

格式上,一般写成 "厂商,型号",比如 "innolux,at070tn92"。我习惯再加一个 fallback 字符串,比如 "simple-panel"。这样如果内核没有专门驱动,还能 fallback 到通用面板驱动。

我的习惯:compatible 字符串不要乱写。去内核源码的 Documentation/devicetree/bindings/ 目录下查一下,看官方怎么命名的。我曾经见过有人把 "compatible" 写成 "compatiable"——少了个 i,驱动死活匹配不上,查了两天。

4.3 reg:告诉内核“你占哪块地址”

reg 描述设备在总线上的地址空间。对于显示控制器,它通常是 MMIO 地址段。

格式是 reg = <address length>。比如:

reg = <0x01e00000 0x1000>;

这表示显示控制器的寄存器基地址是 0x01e00000,映射长度 4KB。

如果显示控制器有多个地址段,可以写多组:

reg = <0x01e00000 0x1000>,
      <0x01f00000 0x100>;

驱动里通过 of_iomap() 或 platform_get_resource() 来获取这些地址。

注意:reg 的地址必须和芯片手册、硬件原理图一致。我曾经调一块 i.MX6 的板子,原理图上 LCDIF 基地址是 0x021B8000,设备树里写成了 0x021B800——少了个 0,内核直接 oops。这种错,查起来特别痛苦。

4.4 pinctrl:引脚功能的“开关”

pinctrl 负责配置引脚复用功能。显示接口通常需要多个引脚——数据线、时钟、同步信号、使能等。每个引脚可能有多种功能(GPIO、LCD 模式、PWM 等),pinctrl 就是告诉 SoC:这些引脚现在要当显示接口用。

设备树里通过 pinctrl-names 和 pinctrl-X 来引用:

pinctrl-names = "default", "sleep";
pinctrl-0 = <&lcd_pins_active>;
pinctrl-1 = <&lcd_pins_sleep>;

default 状态是正常工作时的引脚配置,sleep 状态是休眠时的配置(通常把引脚设为高阻或下拉,省电)。

引脚节点本身定义在 SoC 的 pinctrl 控制器下:

&iomuxc {
    lcd_pins_active: lcd-pins-active {
        fsl,pins = <
            MX6UL_PAD_LCD_DATA00__LCDIF_DATA00  0x79
            MX6UL_PAD_LCD_DATA01__LCDIF_DATA01  0x79
            MX6UL_PAD_LCD_CLK__LCDIF_CLK        0x79
            MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE  0x79
            MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC    0x79
            MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC    0x79
        >;
    };
};

每个引脚后面那个 0x79 是配置值——包括驱动强度、上下拉、速度等。这个值怎么算?看芯片手册的 IOMUX 章节。

关键点:pinctrl 配置不对,屏幕可能花屏、闪屏、甚至完全不亮。我遇到过一块屏,数据线驱动强度设小了,显示出来颜色偏淡,调大两级就好了。

4.5 显示时序参数:让屏幕“对齐”

除了上面三个属性,显示节点还需要时序参数。它们通常在 display-timings 子节点里:

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

这些参数直接对应 LCD 数据手册里的时序图。我一般会拿手册对照着填,一个像素时钟周期都不能差。

参数 含义 常见错误
clock-frequency 像素时钟频率(Hz) 单位写错,写成 MHz 导致频率过高
hactive / vactive 有效显示区域(宽×高) 和分辨率对不上,画面偏移
hfront-porch / hback-porch 行消隐前后肩 值太小,画面右边出现黑边
hsync-len / vsync-len 同步信号脉宽 太短,同步不稳定导致闪屏
de-active 数据使能极性 极性反了,画面全黑
pixelclk-active 像素时钟采样沿 沿选错,画面出现噪点
避坑:clock-frequency 的单位是 Hz,不是 kHz 也不是 MHz。我见过有人把 33MHz 写成 33,结果内核以为只有 33Hz,屏幕刷新率低到肉眼可见闪烁。

4.6 知识结构图:显示设备树节点全景

下面这张图把显示设备树节点的核心结构画出来了,方便你对照着写:

显示设备树节点核心结构 lcd-panel 节点 compatible reg pinctrl 驱动匹配字符串 MMIO 地址段 引脚复用配置 backlight / enable-gpios reset-gpios display-timings 子节点 编写要点 • compatible 必须与驱动匹配,建议加 fallback 字符串 • reg 地址与芯片手册严格对应,长度不要多写或少写 • pinctrl 配置驱动强度、上下拉,影响信号质量 • display-timings 参数与 LCD 数据手册逐项核对

4.7 实战:写一个完整的显示设备树节点

假设我们有一块 7 寸屏,分辨率 1024×600,接口是 RGB 并行,挂在 i.MX6ULL 上。完整的节点大概这样:

&lcdif: lcdif@021b8000 {
    compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
    reg = <0x021b8000 0x4000>;
    interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
             <&clks IMX6UL_CLK_LCDIF_APB>;
    clock-names = "pix", "axi";
    pinctrl-names = "default";
    pinctrl-0 = <&lcdif_pins>;
    status = "okay";

    display = &<display0>;
    display0: display {
        bits-per-pixel = <32>;
        bus-width = <24>;
        display-timings {
            native-mode = <&timing0>;
            timing0: timing0 {
                clock-frequency = <51000000>;
                hactive = <1024>;
                vactive = <600>;
                hfront-porch = <160>;
                hback-porch = <140>;
                hsync-len = <20>;
                vfront-porch = <12>;
                vback-porch = <20>;
                vsync-len = <3>;
                de-active = <1>;
                pixelclk-active = <0>;
            };
        };
    };
};

写完之后,用 dtc 编译成 dtb,再反编译回来检查一遍。我每次都会做这个步骤——有时候手误写错一个括号,编译能过,但内核解析时直接跳过整个节点。

检查技巧:用 dtc -I dts -O dtb -o test.dtb test.dts 编译,再用 dtc -I dtb -O dts test.dtb 反编译。对比反编译结果和原文件,看属性值有没有被截断或改变。

4.8 常见错误与排查方法

我整理了几个高频问题,你写设备树时大概率会遇到:

  • compatible 不匹配:驱动没 probe。去 /sys/firmware/devicetree/base/ 下查看节点是否存在,cat compatible 看字符串对不对。
  • reg 地址冲突:内核启动时打印 "BUG: Bad page map"。用 /proc/iomem 查看地址占用情况。
  • pinctrl 配置无效:引脚功能不对,屏幕不亮。用 cat /sys/kernel/debug/gpio 查看引脚状态,确认是否被正确复用。
  • 时序参数错误:画面偏移或花屏。用示波器量像素时钟和同步信号,对比设备树里的值。