3、设备树配置:HDMI节点设备树语法、时钟与复位配置、GPIO与HPD引脚绑定

设备树,说白了就是Linux内核用来描述硬件资源的“说明书”。你想想看,一个SoC要接HDMI,它得知道:I2C总线挂在哪?时钟从哪来?复位信号连哪个GPIO?热插拔检测脚是哪个?这些信息,统统写在设备树里。

我刚开始做HDMI驱动时,总觉得设备树就是填几个寄存器地址。后来踩了坑才明白——设备树配不对,驱动写得再好也白搭。今天咱们就把这块彻底讲透。

3.1 HDMI节点基本结构

一个标准的HDMI设备树节点,长这样:

&hdmi {
    compatible = "rockchip,rk3399-hdmi";
    reg = <0x0 0xff940000 0x0 0x20000>;
    interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru PCLK_HDMI_CTRL>,
             <&cru SCLK_HDMI_CEC>,
             <&cru SCLK_HDMI_HDCP>;
    clock-names = "pclk", "cec", "hdcp";
    resets = <&cru SRST_HDMI_CTRL>;
    reset-names = "hdmi";
    pinctrl-names = "default";
    pinctrl-0 = <&hdmi_i2c_xfer &hdmi_cec>;
    status = "disabled";
};

这里每个字段都有讲究:

属性 说明 我踩过的坑
compatible 驱动匹配字符串,必须和驱动里of_match_table一致 拼写错一个字母,驱动直接不加载
reg 寄存器基地址和长度,64位SoC用两对 地址写错,内核访问非法地址直接panic
interrupts 中断号、类型,GIC_SPI表示共享外设中断 中断号对不上,热插拔检测永远不触发
clocks 时钟句柄列表,顺序要和clock-names对应 顺序搞反,时钟框架报错
resets 复位控制器句柄 漏配复位,HDMI模块处于未知状态
pinctrl 引脚复用配置 I2C引脚没配,EDID读不出来
我的习惯:每次写完设备树,先用dtc反编译检查一遍。dtc -I dtb -O dts -o dump.dts your.dtb,看看实际解析出来的内容和预期是否一致。这招帮我抓出过至少5次低级错误。

3.2 时钟配置:别让HDMI“饿死”

HDMI模块需要多个时钟协同工作。我见过最典型的问题——主时钟频率不对,导致输出画面闪烁或者完全黑屏。

时钟配置的核心是搞清楚每个时钟的用途:

  • pclk:APB总线时钟,用于寄存器访问。频率一般几十MHz。
  • cec时钟:CEC协议时钟,通常32.768kHz或更高。
  • hdcp时钟:HDCP加密引擎时钟,需要和TMDS时钟同步。
  • tmds时钟:TMDS链路时钟,直接决定像素时钟。这个最要命。

实际项目中,我遇到过这么个情况:

/* 错误配置:TMDS时钟源选错了 */
clocks = <&cru PCLK_HDMI_CTRL>,
         <&cru SCLK_HDMI_CEC>,
         <&cru SCLK_HDMI_HDCP>,
         <&cru SCLK_HDMI_TMDS>;  /* 这个时钟源频率不够 */

当时4K@60Hz死活出不来,查了三天,最后发现是TMDS时钟源只能跑到300MHz,而4K@60需要594MHz。换了个时钟源,问题解决。

注意:时钟频率必须满足HDMI规范。比如HDMI 2.0要求TMDS时钟最高600MHz。配时钟前,先查SoC datasheet里PLL能输出的最大频率。我曾经因为没查这个,浪费了一周时间。

3.3 复位配置:上电时序的“起跑线”

复位配置看似简单,其实最容易出问题。HDMI模块的上电时序要求很严格:

  1. 先释放复位信号
  2. 再使能时钟
  3. 等待PLL锁定
  4. 最后初始化寄存器

设备树里复位配置长这样:

resets = <&cru SRST_HDMI_CTRL>,
         <&cru SRST_HDMI_HDCP>;
reset-names = "hdmi", "hdcp";

驱动里获取复位句柄后,调用reset_control_deassert()释放。但有个坑——复位信号必须保持足够长的低电平时间。有些SoC的复位控制器默认只拉低几个时钟周期,根本不够。

我曾经踩过的坑:某款SoC的HDMI模块,复位信号需要至少10us的低电平。但硬件复位控制器只拉了1us。结果就是:10次启动有3次HDMI初始化失败。最后我在驱动里加了手动延时:

reset_control_assert(rst);
udelay(20);  /* 手动保证20us低电平 */
reset_control_deassert(rst);
mdelay(5);   /* 等待PLL稳定 */

从此再没出过问题。

3.4 GPIO与HPD引脚绑定

热插拔检测(Hot Plug Detect)是HDMI用户体验的关键。HPD引脚配置不对,显示器插上去系统没反应。

设备树里HPD的配置方式:

&hdmi {
    hpd-gpios = <&gpio1 RK_PB5 GPIO_ACTIVE_HIGH>;
    /* 或者用中断方式 */
    interrupt-parent = <&gpio1>;
    interrupts = <RK_PB5 IRQ_TYPE_EDGE_BOTH>;
};

两种方式各有优劣:

方式 优点 缺点 适用场景
GPIO轮询 实现简单,不依赖中断控制器 占用CPU,响应延迟 低功耗要求不高的产品
中断触发 实时响应,CPU负载低 需要中断控制器支持,配置复杂 主流方案,推荐使用

我个人强烈建议用中断方式。为什么?因为HPD信号变化时,系统需要立刻响应——比如用户拔掉显示器,驱动要马上关掉HDMI输出,否则可能损坏硬件。

配置中断时要注意:

  • 用IRQ_TYPE_EDGE_BOTH,上升沿和下降沿都要触发
  • 中断号要查GPIO对应的bank和pin,别搞错
  • 中断处理函数里不能做耗时操作,只置标志位
调试技巧:如果HPD不工作,先用cat /sys/kernel/debug/gpio看看GPIO电平对不对。手动短接HPD引脚到地或3.3V,看电平是否变化。这能快速区分是硬件问题还是软件问题。

3.5 完整节点示例与SVG结构图

下面是一个完整的HDMI设备树节点,包含了我们刚才讲的所有内容:

&hdmi {
    compatible = "rockchip,rk3399-hdmi";
    reg = <0x0 0xff940000 0x0 0x20000>;
    interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH>;
    
    clocks = <&cru PCLK_HDMI_CTRL>,
             <&cru SCLK_HDMI_CEC>,
             <&cru SCLK_HDMI_HDCP>,
             <&cru SCLK_HDMI_TMDS>;
    clock-names = "pclk", "cec", "hdcp", "tmds";
    clock-frequency = <0 594000000>;  /* 4K@60Hz TMDS时钟 */
    
    resets = <&cru SRST_HDMI_CTRL>,
             <&cru SRST_HDMI_HDCP>;
    reset-names = "hdmi", "hdcp";
    
    pinctrl-names = "default";
    pinctrl-0 = <&hdmi_i2c_xfer &hdmi_cec &hdmi_hpd>;
    
    hpd-gpios = <&gpio1 RK_PB5 GPIO_ACTIVE_HIGH>;
    interrupt-parent = <&gpio1>;
    interrupts = <RK_PB5 IRQ_TYPE_EDGE_BOTH>;
    
    power-domains = <&power RK3399_PD_HDCP>;
    
    status = "okay";
};

为了让你更直观地理解这些配置之间的关系,我画了张结构图:

HDMI设备树节点配置结构图 HDMI 节点 clocks 时钟配置 pclk: 总线时钟 cec: CEC协议时钟 hdcp: 加密引擎时钟 tmds: TMDS链路时钟 resets 复位配置 hdmi: 主复位 hdcp: HDCP复位 GPIO / HPD hpd-gpios: HPD引脚 interrupts: 中断配置 配置要点总结 • 时钟:确保TMDS时钟频率满足目标分辨率要求 • 复位:保证复位低电平时间足够长(建议>10us) • HPD:优先使用中断方式,配置EDGE_BOTH触发

核心要点:设备树配置是HDMI驱动的“地基”。时钟、复位、GPIO这三块,任何一块出问题,HDMI都跑不起来。我建议你拿到一个新平台时,先对照datasheet把这三项配好,再开始写驱动代码。这样能省掉后面80%的调试时间。