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 到通用面板驱动。
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() 来获取这些地址。
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 章节。
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 | 像素时钟采样沿 | 沿选错,画面出现噪点 |
4.6 知识结构图:显示设备树节点全景
下面这张图把显示设备树节点的核心结构画出来了,方便你对照着写:
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查看引脚状态,确认是否被正确复用。 - 时序参数错误:画面偏移或花屏。用示波器量像素时钟和同步信号,对比设备树里的值。