MDIO子设备注册失败?用MFD重构DSA交换机驱动 电子邮件的最后几行总是一样的。我盯着那个 v3 补丁看了三天总觉得自己离“能提交”只差一步结果 review 邮件里只有一句针对 MDIO 子设备的意见“这个东西不该自己注册把它做成 MFD。”发信人是 Andrew化名内核社区习惯直接叫名字。当时我第一反应是不服——我为这个 MDIO 子设备折腾了 v2 和 v3 两个版本从总线创建时机到设备树 node 归属都试过凭什么一句“改成 MFD”就能解决事实证明Andrew 说得对而且当我真正把结构改成 MFD 之后原本纠缠了我几周的“子设备独立不了”问题直接消失了。这篇就聊聊我在这次 DSA 交换机 v3 补丁改造里踩过的坑、Andrew 那句话背后的设计逻辑以及从 v3 改到 v4 的完整实操过程。如果你也在写 DSA 驱动、MDIO 子设备或者嵌入式多功能设备驱动这篇应该对你有用。1. v3这个补丁到底卡在了哪里1.1 “独立不了”说的是哪一步先说清楚项目背景。我们做的是某个 SoC 平台的以太网交换方案CPU 主控通过 MDIO 总线下挂一颗交换芯片交换芯片本身由 DSADistributed Switch Architecture框架管理。这颗交换芯片内部有若干端口其中一部分端口直接复用芯片内置的 PHY另外还接了一个独立的多功能 PHY 设备需要把它挂到交换芯片提供的内部 MDIO 总线上并要求它在系统里以一个独立的 MDIO 子设备存在有自己的驱动、自己的设备树节点、自己的生命周期。问题就出在最后一步“独立的 MDIO 子设备”上。按常规思路MDIO 子设备要么挂在某个mii_bus下面让总线扫描自动创建要么在驱动里手动调用mdio_device_register注册。但在这个 DSA 场景里两条路都走不通。v2 时我们尝试让 MDIO 总线扫描自动发现它。结果不行因为交换芯片内部 MDIO 总线是由 DSA 框架在dsa_register_switch之后动态创建的外部子设备不能保证在这条总线创建之前或之后以正确的顺序出现。v3 改成在 DSA 驱动 probe 里手动创建也就是拿到交换芯片的设备树节点后我们自己分配一个mdio_device填好地址和 bus 指针然后调用mdio_device_register。这一步表面上注册成功了但内核设备模型马上会出问题要么 probe 跑不进来自定义的 MDIO 驱动要么of_node被重复使用要么系统 suspend/resume 时顺序错乱。我当时把它归咎于“MDIO 总线初始化时机太晚”“probe defer 在 MDIO 子系统里不完善”试图用更晚的注册时机去绕。Andrew 看完 v3 直接指出这不是时机问题是归属问题这个子设备根本不应该在总线上“独立注册”而应该作为父设备的一个 MFDMulti-Function Device子功能存在。1.2 在DSA驱动里直接注册MDIO设备为什么不行为了说清楚“为什么不行”我把 v3 里的相关逻辑简化出来。DSA 驱动 probe 流程大概是这样的static int foo_switch_probe(struct mdio_device *mdiodev) { struct dsa_switch *ds; int ret; ds devm_kzalloc(mdiodev-dev, sizeof(*ds), GFP_KERNEL); ds-ops foo_switch_ops; ds-dev mdiodev-dev; /* 一些初始化 */ ret dsa_register_switch(ds); if (ret) return ret; /* v3 里我在这里尝试注册外部 MDIO 子设备 */ ret register_foo_ext_mdio_device(ds); if (ret) return ret; return 0; }register_foo_ext_mdio_device做的事情看起来也不复杂找到内部 MDIO 总线的struct mii_bus *get_phy_device得到phy_device或者直接创建一个mdio_device然后phy_device_register/mdio_device_register。从逻辑上这时候 DSA 内部 MDIO 总线已经存在应该能注册成功。但真实跑起来会发现这么几个现象第一of_node冲突。外部 PHY 在设备树里的节点如果仍然是 MDIO 总线节点的子节点那么在of_mdiobus_register扫描时内核已经为它创建过一个phy_device并把这个设备的dev.of_node指向了这个 DT 节点。等你再手动register一次第二次注册的设备又想去拿同一个 DT 节点内核就会报dev has a node but it has already been claimed模块加载直接返回-EEXIST。第二就算 DT 节点的问题能绕开mdio_device_register产生的设备投放到mdio_bus_type上它的驱动匹配也未必能发生。MDIO 子系统的 bus 匹配其实很原始不像 platform bus 那样有成熟的-EPROBE_DEFER重试机制。如果对应的 MDIO driver 还没加载或者总线注册还没完全结束这个设备会一直处于“已经注册但没有 driver”的状态并且内核不会像 platform bus 那样每注册一个新 driver 就回过来对老设备重新匹配。第三更致命的是 remove 路径。当前 MDIO 子设备是我们在 DSA switch probe 里“手工注册”的它没有挂在父设备的devm资源链上。如果交换芯片驱动因为某种原因 unbind内部 MDIO 总线被释放子设备仍然悬空存在着。等它再被 remove 时访问的总线指针已经是释放过后的空指针直接 oops。这些现象合在一起已经不单纯是“注册时机”问题而是“这个设备在设备模型里的挂靠点选错了”。2. 拆开DSA和MDIO的关系才能明白谁该管这个子设备2.1 DSA对内部MDIO总线的“所有权”要理解为什么子设备不能独立得先搞清楚 DSA 框架对自己创建的 MDIO 总线有很强的“所有权意识”。DSA 框架管理的是分布式交换机它对上层网络协议栈暴露的是一个个 netdev 端口但底层的交换芯片尤其是带内部 PHY 的芯片通常会用一条内部 MDIO 总线来访问这些 PHY。这条总线不是由普通mdiobus_register创建的而是由 DSA 在dsa_register_switch时通过dsa_tree_setup、dsa_switch_setup这一系列流程动态构建的。总线的物理访问方式最终落到dsa_switch_ops里的phy_read/phy_write回调读写请求会被翻译成交换芯片寄存器操作。也就是说这整条 MDIO 总线本质上是 DSA 框架为了满足自身对端口 PHY 管理需求而创建的“私有总线”。它上面的设备应该都是 DSA 发现并管理的。如果此时外部某个子设备想“自力更生”注册到这条总线上DSA 既不知道该设备的存在也没有义务处理它的生命周期就会出现资源归属不明确的问题。打个比方DSA 是房东内部 MDIO 总线是房东自己修的楼PHY 是楼里的住户。房东修楼的时候已经决定好要让哪些住户搬进来。现在外部设备想直接挤进这栋楼房东不知道它它自己也不跟房东签租赁协议。结果就是楼在时它能住楼拆时它还在里面。2.2 设备树、of_node与重复注册内核设备模型里有一个很严的规矩一个 device_node 只能被一个struct device引用。当我们把一个 DT 节点分配给dev-of_node时这个节点就属于这个设备了。如果另一个struct device也要引用同一个节点内核在对节点的kref管理上会产生混乱。MDIO 子系统的of_mdiobus_register会在注册总线时遍历总线的所有子节点为每个子节点创建对应的 PHY 设备。因此只要外部 PHY 的 DT 节点被写在了内部 MDIO 总线节点下面无论你在 DSA 驱动里怎么手动注册都注定会撞车。有一种想法是把外部 PHY 节点不放在 MDIO 总线下面而是放在交换芯片自己的设备节点下这样 DSA 驱动手动注册时就不会撞。但是问题又来了MDIO 子设备在设备模型里必须挂在某个 bus 上如果设备树节点指向交换芯片父节点你对它做mdio_device_register时还是得找到内部 MDIO 总线然后把dev-of_node从父节点换到外部 PHY 节点。这个“从父级节点下找一个代表子设备的 DT 节点”的操作没有标准 API纯靠of_parse_phandle或者字符串拼索引又脆又不规范。我当时用的是一个很丑的方案在交换芯片 DT 节点下面写ext_phy-handle mdio_phy;然后 DSA 驱动里of_parse_phandle拿到 PHY 节点再去注册。能跑但设备模型看这个设备是“没有 DT 节点归属的孤儿设备”它和 DT 里的实际节点没有建立正式的dev.of_node关系后面devm资源和of_find_device_by_node都找不到它。Andrew 的意思正是别再用这些 hack把外部 PHY 当作交换芯片这个多功能设备的一个 cell让 MFD 框架去帮你建立正确的of_node映射和生命周期关系。2.3 生命周期remove路径才是最容易翻车的地方很多人在开发时只盯着 probe 能不能成功直到验证 suspend/resume 或者热插拔时才崩溃。这也是我们当时栽得最狠的地方。DSA 驱动本身是一套很重的状态机dsa_register_switch之后会注册多个网络设备涉及到 DSA tree 的 teardown 顺序。DSA 框架在 remove 时会按顺序释放端口、释放 MDIO 总线、释放交换芯片私有数据。如果你的子设备是在 DSA probe 里手动mdio_device_register的那么它的注册顺序在 DSA 内部状态之前remove 顺序却在 DSA 之后。简单说父设备先拆子设备后拆子设备调phy_detach/mdio_device_remove时内部 MDIO 总线可能已经不存在。就算总线的指针还在总线上关联的 PHY 驱动也可能已经被phy_device_remove处理过一遍二次操作直接 double free。MFD 框架解决这个问题的思路是完全不同的MFD 子设备的生命周期挂在父设备上父设备 remove 时devm_mfd_add_devices注册出来的子平台设备会先于父设备完成移除。而且子设备作为 platform device弃用 MDIO 子系统的 remove 顺序统一归 platform bus 管。这套生命周期链是现成的不需要自己协调。3. MFD介入改变了什么从“设备找总线”到“父设备管子设备”3.1 MFD cell让子设备不再和总线抢地址MFD 的全称是 Multi-Function Device很多 Linux 驱动里都见过它电源管理芯片、音频编解码芯片、触摸屏控制器这些芯片内部往往同时集成了好几类功能MFD 框架就把父设备拆成多个子平台设备。它的关键抽象是struct mfd_cell。一个父设备可以声明一个mfd_cell数组每个 cell 描述一个子功能。devm_mfd_add_devices会遍历这个数组为每个 cell 创建对应的platform_device并把这些 platform_device 的dev.parent指向父设备。这里有个容易误解的地方MFD 框架创建出来的子设备本质上都是 platform 总线上的设备而不是 MDIO 总线上的设备。所以在我们的场景里外部 PHY 的 cell 并不是直接变成phy_device而是先变成一个 platform device再由这个 platform device 的 driver 在 probe 里去操作内部 MDIO 总线、拿到或注册phy_device。也就是说MFD 是在中间加了一层“代言人”。这层“代言人”的价值在于它把子设备的创建权从 DSA 框架手里解放出来。DSA 不需要知道外部 PHY 的具体注册细节只需要在成功建立内部 MDIO 总线后调用一次devm_mfd_add_devices。至于 PHY 用什么地址、注册到哪个 bus、用哪个 driver都是子 platform driver 的事情。子设备不再需要和内部 MDIO 总线抢“谁先发现我”的问题。3.2 为什么platform bus会救场我一开始对“改成 MFD”很抵触觉得这不过是从 MDIO 注册换成了 platform 注册问题还在。但实际改完才发现platform bus 恰好补上了 MDIO 子系统欠的债。其一-EPROBE_DEFER重试机制。MDIO 子系统里设备在总线上注册后如果没有匹配的 driver就永远停在“无驱动”状态。而 platform bus 对返回-EPROBE_DEFER的 probe 结果有完整重试机制每当有新的 driver 注册或新的设备出现内核都会重新对处于 defer 状态的设备做一次匹配。子 platform driver 如果发现父设备还没有准备好提供mii_bus就可以理直气壮返回-EPROBE_DEFER等 DSA 那边把总线准备好之后再重试。其二生命周期的天然绑定。平台设备以父设备为 parentdevm_mfd_add_devices的清理在父设备devm链上remove 顺序有保障。其三of_node映射比 MDIO 子系统成熟。platform_device 可以通过mfd_cell的of_compatible字段把一个 DT 节点正确挂到子设备的dev.of_node上。设备模型对 platform bus 的 DT 匹配支持非常完善platform_driver的of_match_table直接就能匹配到 cell 对应的节点。这样外部 PHY 的 DT 节点能名正言顺地挂在交换芯片子节点下而不再和 MDIO 总线扫描抢同一个节点。4. v3到v4一次真实的改造过程附代码骨架4.1 原来的失败实现先给你们看一段 v3 里让我头疼很久的注册逻辑示意代码细节已经简化static int register_ext_phy(struct dsa_switch *ds) { struct device_node *np ds-dev-of_node; struct device_node *mdio_np, *phy_np; struct mii_bus *bus; struct phy_device *phydev; u32 addr; int ret; mdio_np of_get_child_by_name(np, mdio); if (!mdio_np) return -ENODEV; /* 内部 MDIO 总线挂在 ds 私有数据里 */ bus ds-priv-internal_mii_bus; phy_np of_parse_phandle(np, ext-phy-handle, 0); if (!phy_np) return -ENODEV; ret of_property_read_u32(phy_np, reg, addr); of_node_put(phy_np); if (ret) return ret; phydev get_phy_device(bus, addr, false); if (IS_ERR(phydev)) return PTR_ERR(phydev); ret phy_device_register(phydev); if (ret) phy_device_free(phydev); return ret; }第一眼看上去没有任何问题总线存在、地址合法、设备树节点也能拿得到。但跑起来要么of_node被抢要么get_phy_device返回的设备在一个 PCI 风格的 probe 时序里出现从未 probe 的情况。最典型的是设备树里mdio子节点下如果还写了一个ethernet-phy1c节点of_mdiobus_register在注册内部 MDIO 总线时已经把地址0x1c扫描并创建了设备你再get_phy_devicephy_device_register同一个地址就重复了。这个方案从一开始就把子设备放在了“跟总线底层争夺设备和地址”的位置不管怎么改注册时机都是治标不治本。4.2 MFD后的实现骨架改成 MFD 后我们把外部 PHY 看成交换芯片的一个子功能。父设备在成功注册 DSA switch 之后只是添加 cell不直接碰 PHY#include linux/mfd/core.h static const struct mfd_cell foo_switch_cells[] { { .name foo-ext-phy, .of_compatible vendor,foo-ext-phy, .platform_data NULL, .pdata_size 0, .id PLATFORM_DEVID_AUTO, }, }; static int foo_switch_probe(struct mdio_device *mdiodev) { struct dsa_switch *ds; int ret; ds devm_kzalloc(mdiodev-dev, sizeof(*ds), GFP_KERNEL); ds-ops foo_switch_ops; ds-dev mdiodev-dev; ret dsa_register_switch(ds); if (ret) return ret; /* * 此时内部 MDIO 总线已经由 DSA 建立 * 外部 PHY 交给 MFD 生成 platform 子设备。 */ ret devm_mfd_add_devices(mdiodev-dev, PLATFORM_DEVID_NONE, foo_switch_cells, ARRAY_SIZE(foo_switch_cells), NULL, 0, NULL); if (ret) dev_err(mdiodev-dev, failed to add mfd devices: %d\n, ret); return ret; }然后新增一个 platform driver它的任务只有一个从父设备拿到内部 MDIO 总线然后注册外部 PHY。static const struct of_device_id foo_ext_phy_of_match[] { { .compatible vendor,foo-ext-phy }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, foo_ext_phy_of_match); static int foo_ext_phy_probe(struct platform_device *pdev) { struct foo_switch_priv *priv dev_get_drvdata(pdev-dev.parent); struct mii_bus *bus priv-internal_mii_bus; struct phy_device *phydev; struct device_node *np pdev-dev.of_node; u32 addr; int ret; ret of_property_read_u32(np, reg, addr); if (ret) return ret; phydev get_phy_device(bus, addr, false); if (IS_ERR(phydev)) return PTR_ERR(phydev); ret phy_device_register(phydev); if (ret) { phy_device_free(phydev); return ret; } /* * 这里你还可以做更多保存 phydev 到 priv、 * 注册中断、触发 link 上报等。 */ return 0; } static struct platform_driver foo_ext_phy_driver { .probe foo_ext_phy_probe, .driver { .name foo-ext-phy, .of_match_table foo_ext_phy_of_match, }, }; module_platform_driver(foo_ext_phy_driver);区别在哪里关键不是“用 platform driver 替代 MDIO driver”而是子设备的创建顺序不再依赖 MDIO 总线的自动扫描也不是 DSA 驱动手动插进去的而是由 MFD 框架在 DSA 总线建立妥当之后、父设备 devm 资源链上自然而然生成的。子设备已经在 platform 总线上等着了MDIO 总线准备好时它就能被 probe没准备好时 platform 总线会持续-EPROBE_DEFER重试直到 DSA 把内部 MDIO 总线准备好为止。外部 PHY 最终在 MDIO subsystem 里仍然是一个phy_device但负责创建它的“任务”已经在正确的时序里执行了。还有一点devm_mfd_add_devices用的设备是mdiodev-dev也就是 DSA 父设备的 device。这样 MFD 子设备的 remove 必然发生在父设备 remove 路径的devm阶段不会出现在父设备拆完总线之后再访问总线的局面。4.3 改造过程中踩到的两个新坑MFD 也不是银弹改完 v4 之后我又踩了两个新坑。第一个坑是外部 PHY 的 DT 节点位置。如果节点仍然放在 MDIO 总线子节点下of_mdiobus_register扫描时依然会先创建phy_device然后 MFD 子 platform device 拿同样的of_compatible去匹配时of_node已经被占用了。最后的解决方法很直接把外部 PHY 的 DT 节点从内部 MDIO 总线节点下面移到交换芯片父节点下面让 MFD 的of_compatible去解析它。也就是说节点不再是“MDIO 总线上的一个 PHY”而是“交换芯片上的一个子功能”。第二个坑是mfd_cell里的platform_data。v4 第一版我忘了通过platform_data或dev_get_drvdata传递内部 MDIO 总线指针子平台 driver 在 probe 里直接尝试用of_parse_phandle去解析“mdio 节点”并调用of_mdiobus_find_bus结果在总线的mii_bus还没有与任意 DTmdio节点正式关联时拿不到。更稳定的做法是直接用父设备私有数据里的internal_mii_bus因为 DSA switch 驱动在 probe 时已经明确持有这个指针。这也说明了一个经验MFD 只是把“谁创建子设备”的职责分清楚了“子设备如何拿到父设备资源”还是要靠 platform driver 自己设计好。5. 这类review意见背后的“一句顶万句”经验5.1 什么时候该想到MFD而不是写probeAndrew 建议用 MFD不是心血来潮而是这个场景有几个非常明确的特征。第一个特征是父设备内部集成了多个功能并且这些功能需要共享父设备的资源。交换芯片除了 DSA 暴露的网络端口之外还集成了外部 PHY 管理、可能还有温度传感器、寄存器访问接口等这是典型的多功能设备结构。DSA 只是这个多功能设备的一个“功能视图”。第二个特征是子设备无法靠某个总线机制天然发现。外部 PHY 虽然在 MDIO 地址空间里但它依赖父设备先创建内部 MDIO 总线并且父设备对这条总线有所有权所以单纯把它作为总线上的设备就会陷入“谁先注册谁”的僵局。MFD 的定位就是处理这种“设备在父设备生命周期内才存在”的附属功能。第三个特征是子设备需要与父设备同生共死。如果子设备离开父设备之后没有任何存在的意义那它就不应该独立。一个永远不会单独被 insert/remove 的子设备强行独立注册只会给自己找麻烦。反向的提示也有。如果你的子设备本来就是挂在一条外部可见的、由系统固件或者别的驱动注册的 MDIO 总线上那它应该走正常的 MDIO 总线扫描流程不需要 MFD。MFD 适合解决“父子关系强绑定”的问题不适合解决“外设需要被独立驱动管理”的问题。后者应该老老实实做 bus 设备。5.2 把“注册不好”说成“设计归属错了”这次 review 让我印象很深的一点是 Andrew 并没有帮我调“注册时机”也没有让我加-EPROBE_DEFER而是直接否定了“子设备要独立注册”这个前提。他说的是“别把它做成独立 MDIO 设备做成 MFD device。”这句话从表面看只是换了个框架实际上是把设备模型里的“所有权归属”重新划定了。在我们的 v3 方案里外部 PHY 这个struct device的 parent 是内部 MDIO 总线的mii_bus而它真正的资源提供者却是交换芯片父设备。这种 parent 和资源提供者不一致的情况在设备模型里就是各种诡异 bug 的温床。改成 MFD 之后struct device的 parent 变成交换芯片父设备它需要的mii_bus只是 probe 时从 parent 那里顺手拿来的资源而不是它赖以生存的“挂靠点”。所以以后再遇到“设备注册不上”的问题我的建议是先别急着加各种延迟注册、手动创建、of_node hack先问自己一个问题这个设备的 parent 到底应该是谁它的生命周期应该跟谁对齐如果答案跟当前代码不一致那不管注册方式多努力最终都会翻车。把设备放到正确的“归属”下面注册只是水到渠成的事。这句经验比任何一句代码模板都值钱。