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读不出来 |
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。换了个时钟源,问题解决。
3.3 复位配置:上电时序的“起跑线”
复位配置看似简单,其实最容易出问题。HDMI模块的上电时序要求很严格:
- 先释放复位信号
- 再使能时钟
- 等待PLL锁定
- 最后初始化寄存器
设备树里复位配置长这样:
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,别搞错
- 中断处理函数里不能做耗时操作,只置标志位
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驱动的“地基”。时钟、复位、GPIO这三块,任何一块出问题,HDMI都跑不起来。我建议你拿到一个新平台时,先对照datasheet把这三项配好,再开始写驱动代码。这样能省掉后面80%的调试时间。