点击↑深色口袋物联,选择关注公众号,获取更多内容,不迷路
最近在优化eDp显示驱动,修改了设备树,重构了bridge驱动代码,发现不论怎么调用devm_drm_of_get_bridge ,抑或drm_of_find_panel_or_bridge,系统一直返回 -ENODEV,一直说找不到设备。设备树的链路肯定没问题呀,compatible 都对了,系统启动后,查看节点也都生成了,面板驱动也调用了,为什么说找不到呢?
驱动代码是:drm_of_find_panel_or_bridge(dev->of_node, 0, 0, &panel, &panel_bridge)
设备树是:
panel_edp{
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0{
panel_in_edp0: endpoint {
remote-endpoint = <&bridge_out_edp0>;
};
};
};
};
bridge@5c {
compatible = "ite,brideg";
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
dual-lvds-odd-pixels;
lcd_in0: endpoint {
remote-endpoint = <&oldi_out0>;
};
};
port@1 {
dual-lvds-even-pixels;
lcd_in1: endpoint {
remote-endpoint = <&oldi_out1>;
};
};
port@2{
bridge_out_edp0: endpoint {
remote-endpoint = <&panel_in_edp0>;
};
};
};
};有人知道上面的代码有什么错吗?如果可以找到2处,恭喜你,你已经知道我要说什么了!
这其实是 DRM 驱动开发中最经典的“坑”——设备树图论模型与代码参数对不上。很多初学者对着 port 和 endpoint 这俩整型参数瞎蒙,觉得是 0 就行,结果就是“一顿操作猛如虎,一看日志二百五”。
本篇就来梳理下这两个参数的含义,以及它们和设备树节点之间那些错综复杂的“亲戚关系”。
问题背景:
在 DRM Bridge 驱动中,我们通常需要获取下一级设备(比如 Panel 或者下一个 Bridge)的句柄。常用的两个 API 是 drm_of_find_panel_or_bridge 和 devm_drm_of_get_bridge。
直觉写法:
很多开发者看到函数原型里有 int port 和 int endpoint,直觉反应就是:“我只有一个输出口,那肯定是 0 和 0 ”吧;抑或是0,代码会自动查找?
现象:
ports 节点,且把输出放在了 port@1。代码传 (0, 0) -> 失败。port 节点,但代码传 (1, 0) -> 失败。核心问题:
这两个参数不是瞎猜的,它们是 Open Firmware (OF) Graph 模型里的“坐标轴”。port 是纵坐标(哪个接口),endpoint 是横坐标(哪根线)。
Linux 设备树使用 OF Graph(Bindings for Graph)来描述硬件拓扑连接。这套机制把连接抽象成了“节点”和“边”。
原理剖析:
ports 容器:如果一个设备有多个接口(比如一个 Bridge 有 1 个输入、1 个输出),通常会把所有接口包装在一个 ports 节点里。port 节点 :代表设备的一个物理接口port@0 通常约定为输入接口。port@1 通常约定为输出接口。port 参数,对应的是 reg 属性的值,或者是该节点在父节点下的索引顺序。endpoint 节点:代表接口上的一个连接端点endpoint 参数,对应的是 endpoint 节点的 reg 值或索引。对比图示:直觉 vs 真相
【直觉写法:瞎蒙参数】
驱动代码:find_xxx(dev, 0, 0); // 只要第一个
|
v
设备树:
bridge {
ports {
port@0 { ... }; // 输入
port@1 { ... }; // 输出
}
};
|
v
结果:内核遍历 port@0 (输入),发现没连 panel -> 返回失败!
【正确做法:按图索骥】
驱动代码:find_xxx(dev, 1, 0); // 找第 1 号 port (输出)
|
v
设备树:
bridge {
ports {
port@0 { reg = <0>; ... }; // 输入
port@1 { reg = <1>; endpoint { ... }; }; // 输出
}
};
|
v
结果:内核定位到 port@1,找到 endpoint -> 成功!要搞定这两个参数,必须遵循以下逻辑:
关键设计要点:
ports 还是 port:port(无 ports 包装),则该节点默认为索引 0。ports 包装,则必须看 port 节点的 reg 属性,或者其在 ports 下的顺序。endpoint 参数几乎总是 0。内核工作流程:
[驱动调用] find_panel_or_bridge(node, port=1, ep=0)
|
v
[内核 OF 解析]
|
+-> 1. 在 node 下找 "ports" 或 "port"
|
+-> 2. 定位第 "1" 个 port (检查 reg 属性)
| (假设找到了 port@1)
|
+-> 3. 在 port@1 下找第 "0" 个 endpoint
| (假设找到了 endpoint@0)
|
+-> 4. 读取 endpoint@0 的 "remote-endpoint" 属性
|
+-> 5. 跳转到远端设备节点
|
v
[返回结果] 找到 Panel 或 Bridge 设备我们通过三个实战场景,来演示参数该怎么填。
特点:Bridge 有一个输入(port@0),一个输出(port@1)。我们要在驱动里找输出端连接的 Panel。
设备树代码:
/* Bridge 设备节点 */
hdmi-bridge {
compatible = "ti,tfp410";
// ... 其他属性
ports { // 容器
#address-cells = <1>;
#size-cells = <0>;
port@0 { // 输入口
reg = <0>;
bridge_in: endpoint {
remote-endpoint = <&dpi_out>; // 连接 SoC
};
};
port@1 { // 输出口
reg = <1>;
bridge_out: endpoint {
remote-endpoint = <&panel_in>; // 连接 Panel
};
};
};
};驱动代码:
/*
* 场景:Bridge 驱动 Probe
* 目标:找到下一个 Bridge 或 Panel
*/
static int my_bridge_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct drm_bridge *next_bridge;
// 解析参数:
// 1. dev->of_node: 指向当前 hdmi-bridge 节点
// 2. port: 1 (我们要找输出端,对应 port@1 的 reg=<1>)
// 3. endpoint: 0 (通常只有一个 endpoint)
next_bridge = devm_drm_of_get_bridge(dev, dev->of_node, 1, 0);
if (IS_ERR(next_bridge))
return PTR_ERR(next_bridge); // 如果找不到,这里会报错
// 成功获取到下一级 bridge (panel bridge),可以进行 attach 操作
return 0;
}为什么这样设计?reg = <1> 明确定义了编号,代码必须填 1。如果设备树里没写 reg 属性,内核会按节点顺序索引,第一个节点是 0,第二个是 1。但强烈建议显式写上 reg,避免歧义。
特点:SoC 通常有多个显示输出(比如一个 DSI,一个 DPI)。驱动需要指定从哪个口输出。
设备树代码:
/* SoC 显示控制器 */
dss {
compatible = "vendor,dss";
// ...
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 { // DSI 输出
reg = <0>;
dsi_out: endpoint { ... };
};
port@1 { // DPI 输出
reg = <1>;
dpi_out: endpoint {
remote-endpoint = <&bridge_in>; // 连接 Bridge
};
};
};
};驱动代码:
/*
* 场景:SoC Encoder/Encoder Driver
* 目标:根据硬件配置,连接到 DPI 接口 (port@1)
*/
static int dss_bind(struct device *dev)
{
// 我们想连 DPI,对应 port@1
// 参数:port=1, endpoint=0
struct drm_bridge *bridge = devm_drm_of_get_bridge(dev, dev->of_node, 1, 0);
if (IS_ERR(bridge)) {
dev_err(dev, "No bridge found for DPI output\n");
return PTR_ERR(bridge);
}
// Attach bridge...
return 0;
}特点:有些简单设备树没写 ports,也没写 reg。
设备树代码:
simple-panel {
compatible = "simple-panel";
port { // 只有一个 port,没有 ports 包装,也没有 reg
panel_in: endpoint {
remote-endpoint = <&bridge_out>;
};
};
};驱动代码(在 Bridge 驱动里找它):
/*
* 场景:Bridge 下游是一个简单的 panel
* 设备树里只有 port 节点
*/
static int bridge_probe(struct platform_device *pdev)
{
// 注意:
// 虽然设备树没写 reg,但内核默认这个 port 索引为 0
// 所以 port 参数填 0
struct drm_bridge *panel_bridge = devm_drm_of_get_bridge(dev, dev->of_node, 0, 0);
// ...
}避坑指南:
如果驱动代码写了 port=1,而设备树里只有一个 port 节点,内核会找不着,直接返回 -ENODEV。这就对应了开篇那个哥们的错误。如果设备树结构变了,驱动参数必须跟着变。
如果 devm_drm_of_get_bridge 还是失败,不要只盯着代码看,试试下面几招。
1. 检查设备树最终状态
有时候设备树 .dtsi 包含关系复杂,不知道最后编译出来是啥样。
/sys/firmware/devicetree/base/ 目录结构。ls /sys/firmware/devicetree/base/soc/bridge@50/ports/port@0、port@1 到底存在不存在,名字是不是对。2. 确认 remote-endpoint 双向绑定
OF Graph 要求连接必须是双向的。
endpoint { remote-endpoint = <&B>; };endpoint { remote-endpoint = <&A>; };phandle 指针指错了,内核也会解析失败。3. 使用 ftrace 跟踪
如果实在找不到原因,可以开启 ftrace 查看 of_graph_parse_endpoint 的调用过程。
伪代码调试补丁:
// 在驱动 probe 里加点料
int port = 1; // 假设是输出
int ep = 0;
// 打印要查找的路径
dev_info(dev, "Looking for port %d, endpoint %d\n", port, ep);
// 尝试手动解析看看有没有
struct device_node *remote_np;
remote_np = of_graph_get_remote_node(dev->of_node, port, ep);
if (!remote_np) {
dev_err(dev, "DT Error: remote node not found! Check port/reg index.\n");
return -ENODEV;
} else {
dev_info(dev, "Found remote node: %pOF\n", remote_np);
of_node_put(remote_np);
}最后,我们把排查思路串起来:
[问题] devm_drm_of_get_bridge 返回失败
|
v
[第一步:查设备树结构]
|
+---> 有 ports {} 吗?
| |--> YES: port 参数看子节点的 reg 值
| |--> NO: port 参数通常为 0
|
v
[第二步:确认方向]
|
+---> 当前节点是什么设备?
| |--> Bridge: 找输出端 (通常是 port 1)
| |--> SoC: 找对应接口 (DSI=0, DPI=1 等)
|
v
[第三步:检查 Endpoint]
|
+---> endpoint 参数通常为 0
| (除非有多链路连接)
|
v
[第四步:检查 remote-endpoint]
|
+---> 指针是否双向指向正确?
|
v
[结果] 参数修正,驱动加载成功OF Graph 的 port 和 endpoint 参数,本质上是硬件拓扑在软件层面的映射。
reg 值或索引顺序。差一个数就是天涯陌路。ports { port@0... port@1... } 是最标准的写法,虽然写起来繁琐,但逻辑最清晰,容错率最高。看到这里,知道文章最开始处的2处错误了吗,答案留言处见!