MDIO子设备独立不了?MFD重构DSA驱动的设备模型解法

发布时间:2026/10/11 1:12:36
MDIO子设备独立不了?MFD重构DSA驱动的设备模型解法 1. 卡了两个晚上的问题MDIO 子设备为什么“独立不了”1.1 先说清楚 SoC-e 这颗芯片的硬件拓扑我们内部代号叫 SoC-e 的这颗交换芯片本质上是一个带二层转发引擎的千兆交换控制器板级拓扑不算复杂主控 SoC 通过一条 I2C 总线访问芯片的配置寄存器芯片内部则组了一个 MDIO 总线用来管理自己的内置 PHY。它的端口分两类一个是面向 CPU 向上走的 conduit 口另外几个是直接出到面板的普通用户口标准的 DSA 使用场景。第三版驱动的定位很明确把上一版遗留的“寄存器到处散着读、PHY 初始化靠 sleep 硬等”这种实现彻底推翻改成符合 Linux 设备模型的做法。按我的计划I2C 这一层用一个 regmap 管起来DSA 框架负责端口抽象PHY 管理则走标准 MDIO 总线。听上去很顺真正动手时第一步就卡住了MDIO 这条总线的“设备身份”始终没法落定导致 PHY 枚举时好时坏。1.2 最开始的实现MDIO 总线被塞在 DSA 驱动里第一版实现其实是沿用了老代码的习惯在 DSA 驱动的 ops-setup 回调里调用 mdiobus_alloc 和 mdiobus_register 动态注册一条内部 MDIO 总线。说白了就是让交换芯片驱动自己去创建总线、自己去扫描 PHY。static int soce_setup(struct dsa_switch *ds) { struct soce_priv *priv ds-priv; struct mii_bus *bus; bus mdiobus_alloc(); if (!bus) return -ENOMEM; bus-name soce-internal-mdio; bus-read soce_mdio_read; bus-write soce_mdio_write; bus-parent ds-dev-parent; priv-mdio_bus bus; return mdiobus_register(bus); }这段代码在启动阶段能跑通PHY 也能读出来但它有一个很隐蔽的问题MDIO 子设备在设备树里根本没有对应的“身份”。内核的设备模型讲究设备、总线、驱动三者一一对应而这里 MDIO 总线是被临时捏出来的它没有一个独立的 struct device没有自己的 of_node也没有独立的 probe 时机。它的一切生命周期都绑定在 DSA 主驱动的某个回调里。更麻烦的是PHY 子节点里用到的 reset-gpios、interrupt-parent 这类属性本质上应该由“拥有 MDIO 总线的设备”来解析。现在总线是个无主设备of_mdiobus_register 虽然在用但它内部要走到 irq_of_parse_and_map、of_get_named_gpio 这些接口时依赖的是设备节点到 struct device 的关联。这个关联一旦断了就会表现为属性读不到、中断配不上、GPIO 拉不下去。1.3 尝试独立注册后现象更诡异了我一开始觉得既然“无主设备”不行那就给它建一个 platform_device 总可以了吧。于是我在设备树里给 MDIO 单独划了一个节点配上 compatible然后在主驱动里用 of_platform_populate 去手动填充。结果启动日志开始出现一批很拧巴的报错soce-switch 0-002c: mdio bus probe failed, retrying mdio_bus: mdio-soce: PHY 0 not found of_mdiobus_register: invalid phy device node真正让人头疼的是这些报错不是每次都一样。有时候 PHY 能枚举出来但 ethtool 看链路状态是 down有时候 DSA 端口全部注册成功但实际发包全部丢偶尔整个 I2C 设备直接 probe 失败报 -EPROBE_DEFER 却永远不再重试。我去翻代码发现自己陷入了典型的“设备模型时序地狱”MDIO 子设备需要父设备先完成 regmap 初始化而父设备的 probe 又依赖 MDIO 子设备先注册好才能继续处理 DSA 端口。两边互相等谁先谁后全看运气。问题本质很清楚了硬件上这颗芯片是一个“多功能复合设备”软件上我却一直试图把它拆成“一个主设备 一个内部附属总线”。在内核眼里MDIO 总线必须是一个能独立存活、独立 probe、独立管理 PHY 的实体而不是主设备里顺手调用的一个函数。可我当时没想到这种场景的正确解法是 MFD直到邮件列表里那位维护 DSA 多年的老内核看了一眼设备树回了一句这场景不就是 MFD 该干的活吗2. 老内核一句话背后的原理DSA、MDIO 与 MFD 如何协作2.1 DSA 框架的真实工作方式DSA 的全称是 Distributed Switch Architecture它的核心思路是把交换芯片的每个硬件端口包装成一个标准 net_device让上层协议栈完全无感。为了做到这一点DSA 要求驱动提供一个 dsa_switch_ops 结构体里面包含 setup、port_enable、port_disable、phy_read 之类的回调。但很容易被忽略的是DSA 本身不负责管理 PHY 设备。它只是“知道”每个端口该连哪个 PHY真正把 PHY 枚举出来、绑定到端口上是 MDIO 总线和 phylib 的活。也就是说DSA 驱动必须确保“MDIO 总线已经被注册好”然后才能调用 dsa_register_switch。如果你的 MDIO 总线注册时机不受控制DSA 这边再怎么写都是白搭。传统交换芯片驱动里注册 MDIO 总线通常放在 ops-setup 里因为 DSA 框架保证在 setup 回调执行时底层总线已经 ready。这是 DSA 时代遗留的典型做法很多老驱动都这么干。但它默认了一个前提这颗交换芯片的 MDIO 总线是芯片私有的、内部自包含的不需要与设备树中的独立节点交互。而 SoC-e 这颗芯片不一样它的内置 PHY 既挂在私有 MDIO 上又需要从设备树里拿中断、GPIO、复位时序等资源这就逼着你必须给这条 MDIO 总线一个正式身份。2.2 “独立”到底指什么设备模型视角搞内核驱动尤其是遇到这种“子设备注册不上去”的问题先别急着调函数返回值和加睡眠得回到设备模型本身去想Linux 设备模型里最核心的概念就是 device、driver、bus 三者匹配。一个硬件实体要想正常工作必须先有一个 struct device 被注册到某个总线然后总线根据 compatible 或 ID table 找到对应 driver双方配对成功后执行 probe。“MDIO 子设备独立不了”这句话翻译成设备模型的语言就是这条 MDIO 总线没有自己的 struct device因此它不可能被某个 driver probe也不可能拥有一个稳定的设备节点来承接 PHY 子节点。那些 PHY 的 reset GPIO、中断、供电控制全部找不到“挂靠人”。你可以把它类比成公司管理MDIO 总线像一个停车场PHY 是停在里面的车。停车场如果没有独立法人资格、没有自己的营业执照那它就不能单独签合同、不能单独买电、也不能单独雇保安。所有事情都得代办而代办人一旦休假整个停车场就瘫痪。MFD 做的事情就是帮这个停车场注册成独立法人让它能自己签合同、自己运营。2.3 MFD 的拆解逻辑MFD全称 Multi-Function Device它是内核专门为“一颗物理芯片内部包含多个功能单元”这种场景设计的框架。它允许你在软件里把一个物理设备拆成若干个 platform device每个 platform device 有各自独立的 probe 生命周期但共享同一个底层物理访问通道。MFD 的核心思想是“硬件是一个软件拆多个”。父设备负责探测物理芯片、初始化底层通信比如 I2C、SPI、MMIO、创建 regmap、建立中断域子设备各自负责一个功能模块比如一个子设备管 MDIO 总线另一个子设备管 DSA 交换核心将来如果这颗芯片还集成了 GPIO 扩展器或者温度传感器也可以继续加子设备。MFD 和手动调用 of_platform_populate 最大的区别在于生命周期管理。手动填充的 platform_device 是“散养”的父设备退出时没人统一回收而 MFD 通过 mfd_add_devices 和 mfd_remove_devices 把父子设备的生命周期绑在一起。父设备 probe 成功后子设备才被创建父设备 remove 时子设备统一注销不会出现悬空指针。设备树里MFD 的做法就是给每个子功能节点一个独立的 compatible。父设备 probe 时会自动扫描子节点并生成对应的 platform_device每个子设备找到自己的驱动后独立 probe。MDIO 子设备不再依赖 DSA 驱动先跑起来DSA 驱动也不再需要关心 MDIO 总线是谁注册的只要在它的 ops 里通过 mdiobus_get_named 之类的接口找到总线就行。3. 改造实操把 MDIO 子设备从 DSA 驱动里拆出去3.1 设备树重新规划改造第一步不是写代码而是重新组织设备树。旧版本里交换芯片节点下面直接放了一个没有 compatible 的 mdio 子节点它只是给 DSA 驱动内部解析用的内核不会为它创建设备。改造后要把 mdio 子节点和 DSA 核心子节点都变成正式的 platform device 节点。i2c0 { switch2c { compatible soce,switch-v3; reg 0x2c; #address-cells 1; #size-cells 0; mdio-bus0 { compatible soce,switch-v3-mdio; reg 0; #address-cells 1; #size-cells 0; reset-gpios gpio0 5 GPIO_ACTIVE_LOW; phy0: ethernet-phy0 { reg 0; interrupt-parent gpio0; interrupts 11 IRQ_TYPE_EDGE_FALLING; }; phy1: ethernet-phy1 { reg 1; interrupt-parent gpio0; interrupts 12 IRQ_TYPE_EDGE_FALLING; }; }; core1 { compatible soce,switch-v3-core; reg 1; #address-cells 1; #size-cells 0; ports { #address-cells 1; #size-cells 0; port0 { reg 0; label cpu; ethernet mac0; }; port1 { reg 1; label lan1; phy-handle phy0; }; port2 { reg 2; label lan2; phy-handle phy1; }; }; }; }; };注意两个子节点都带了 compatible 和 reg父节点必须有 #address-cells 和 #size-cells。这样 MF 框架对应代码用 devm_of_platform_populate 扫描时才能正确为每个子节点生成 platform_device。3.2 父设备 probe 只做三件事MFD 改造后父设备驱动反而变得很简单。它不再直接处理 PHY 枚举也不再关心 DSA 端口的注册顺序只负责三件事探测物理设备、初始化底层访问通道、把子设备填充出去。static int soce_switch_probe(struct i2c_client *client) { struct device *dev client-dev; struct regmap *regmap; int ret; regmap devm_regmap_init_i2c(client, soce_regmap_config); if (IS_ERR(regmap)) return PTR_ERR(regmap); dev_set_drvdata(dev, regmap); ret devm_of_platform_populate(dev); if (ret) return ret; return 0; }devm_of_platform_populate 这个接口很关键。它扫描当前设备节点下的所有 compatible 子节点逐个生成 platform_device。每个子设备随后独立完成自己的 probe 流程。父设备不再需要知道 MDIO 总线的注册细节也不需要在 DSA 回调里手动 mdiobus_register。共享 regmap 的传递方式我是通过 dev_set_drvdata 放在了父设备上子设备在 probe 时通过 dev_get_drvdata(dev-parent) 拿回来。如果你的子设备有自己的 MMIO 地址段或者有自己的 I2C 从地址那就连共享 regmap 都不需要子设备自己独立初始化即可。3.3 MDIO 子设备驱动怎么写MDIO 子设备驱动的职责非常纯粹注册一个 mii_bus用设备树的 of_node 去扫描 PHY。它的 probe 函数比之前写在 DSA setup 回调里的代码干净很多。static int soce_mdio_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct soce_mdio_priv *priv; struct mii_bus *bus; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; bus devm_mdiobus_alloc_size(dev, sizeof(*priv)); if (!bus) return -ENOMEM; bus-name soce-switch-mdio; bus-read soce_mdio_read; bus-write soce_mdio_write; bus-parent dev; bus-phy_mask 0; snprintf(bus-id, MII_BUS_ID_SIZE, %s, dev_name(dev)); ret of_mdiobus_register(bus, dev-of_node); if (ret) return ret; platform_set_drvdata(pdev, bus); return 0; }关键点在于 bus-parent 被设置成子设备自身的 device而不是整个交换芯片的 device。总线设备的寿命和子设备绑定而不是和父设备绑定。of_mdiobus_register 会解析 dev-of_node 下面的 ethernet-phy 子节点自动把 reset-gpios、interrupt、reg 这些属性落实到位。这里还有一个小细节要注意phy_mask 我们故意设为 0表示不屏蔽任何 PHY 地址。如果你的芯片内部在某个地址上有一个不存在的内部保留 PHY 或者只在特定地址才有 PHY建议通过设备树里的 phys 节点决定枚举范围必要时在驱动里结合产品的实际情况设置 phy_mask 避免误枚举。3.4 DSA 核心驱动怎么收尾DSA 核心子设备同样是一个 platform deviceprobe 里只做 DSA 框架相关的事情。它不需要再关心 MDIO 总线的创建只需要在 ops-setup 里找到这个总线并缓存指针。static int soce_dsa_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct dsa_switch *ds; int ret; ds devm_dsa_register_switch(dev); if (IS_ERR(ds)) return PTR_ERR(ds); ds-priv dev_get_drvdata(dev-parent); platform_set_drvdata(pdev, ds); return 0; }MDIO 子设备完成注册后DSA 驱动在 ops-setup 或 port_enable 回调里直接通过 mdiobus_get_phy 或自己缓存的 bus 指针访问 PHY。dmesg 里不再出现 PHY 枚举失败因为 phylib 已经通过 MDIO 子设备把 PHY 全部扫描好了。在 dsa_switch_ops 里phy_read/phy_write 这两个回调需要特别注意。如果你的芯片内部 PHY 走的是私有 MDIO 总线那这两个回调可以直接从 ds-priv 转到底层的 mii_bus 操作或者直接在回调里调用 mdiobus_read/mdiobus_write。但大部分情况建议用标准 phylib 路径除非有特殊寄存器布局需求。3.5 编译与依赖的几个注意点MFD 改造涉及 Kconfig 调整。父设备驱动需要 select MFD_CORE因为 devm_of_platform_populate 本身由 MFD 核心提供。如果你的内核配置里没有打开 CONFIG_MFD_CORE会发现链接时找不到 devm_of_platform_populate 这个符号。子设备驱动的 Kconfig 项建议各自独立例如 CONFIG_SOCE_SWITCH_MDIO 和 CONFIG_SOCE_SWITCH_CORE。不要图省事全塞在一个选项里否则后续想单独 disable 某个功能模块会很痛苦。编译顺序上父设备驱动和子设备驱动可以编译成同一个模块也可以分开。不过如果是同一个模块一定要注意 module_init 顺序。父设备一定要先注册不然 of_platform_populate 找不到对应的 platform driver 时子设备会进入 deferred probe虽然最终也能起来但 dmesg 会多一堆 -EPROBE_DEFER 的痕迹影响排查其他问题时的心情。4. 实测记录从启动崩溃到 PHY 正常枚举4.1 改造前后 dmesg 对比改造前启动日志经常出现这种组合soce-switch 0-002c: failed to get PHY irq for phy0 mdio_bus: mdio-soce: probe of mdio-soce failed with error -22 soce-switch 0-002c: DSA setup failed: -EINVAL改造后同一块板子上的日志变成soce-switch 0-002c: switch v3 detected, regmap ready soce-mdio 0-002c.mdio-bus0: registered mdio bus soce-mdio 0-002c.mdio-bus0: scanned phy0, phy1 soce-core 0-002c.core1: registered DSA switch顺序很关键MDIO 子设备先是自己 probe 成功注册 mii_bus扫描出 PHY 设备然后 DSA 核心子设备才注册 DSA 框架。两者之间不再有互相等待的循环依赖。启动完成后/sys/class/mdio_bus/ 下能看到类似 mdio-soce0 的目录PHY 的中断、复位 GPIO、供电控制都由这个总线的 struct device 负责。用 ethtool 查看任意用户端口都能读到 PHY ID 和链路状态不再出现“PHY 不存在”的假象。4.2 功能验证端口与 PHY 的关系打通验证工作分三层。第一层是 mii-tool 或 ethtool 看 PHY 状态确认每个用户端口都拿到了对应的 PHY 设备。第二层是打流测试把 CPU 端口和其他用户端口组 VLAN用 iperf3 跑双向流量。第三层是热插拔测试在不同端口反复插拔网线确认链路断开和恢复事件都能正确上报给网络协议栈。这三层我在改造前都验证不过改造后全部通过。其中最有说服力的一个现象是网线断开时dmesg 会立即出现 link down 的 phylib 事件而不再是之前那种需要手动轮询才能感知的迟钝状态。这得益于 MDIO 子设备有了独立的设备节点PHY 中断能正常映射phylib 状态机能及时响应。4.3 三个容易复发的坑第一个坑父设备 probe 里提前访问了 PHY 寄存器。MFD 改造后父设备只该初始化底层访问通道任何 PHY 相关操作都必须放到子设备里。我中途为了方便调试在父设备 probe 里顺手读了一次 PHY ID结果导致子设备还没注册I2C 总线就被占用了。这个问题最后一遍代码审查才抓出来。第二个坑of_mdiobus_register 返回 -ENODEV。原因是设备树里 mdio-bus 子节点没有设置 #address-cells 和 #size-cells导致内核解析 PHY 子节点时找不到地址信息。这个报错很隐蔽因为它是通过 fwnode 解析失败的不会直接告诉你缺少哪个属性。拿着 dmesg 查了半天才发现设备树的问题。第三个坑子设备之间共享 regmap 时没有留意引用计数。devm_regmap_init 创建的 regmap 是父设备管理的子设备通过 dev_get_drvdata 获得指针后理论上只读不写生命周期。但我在 MDIO 子设备驱动里调用了 regmap_read 而父设备已经被 remove 时内核会报 use-after-free。后来改成只通过父设备的一层抽象接口不直接暴露 regmap 引用。5. 避坑清单与我对设备模型的思考5.1 子设备注册异常排查顺序如果你也遇到类似的“子设备独立不了”问题我建议按下面的顺序排查。现象可能原因优先动作PHY 设备无法从设备树解析子节点缺少 #address-cells/#size-cells检查设备树地址编码MDIO 子设备 probe 后没生成 phy_device总线注册成功但 of_node 未关联确认 of_mdiobus_register 传入的 of_node 正确DSA 端口全部注册但 PHY 仍是 unknownMDIO 子设备和 DSA 子设备生命周期错位检查两者 probe 顺序必要时用 device_link 强制约束父设备 remove 后系统崩溃子设备还在引用父设备创建的 regmap/irq使用 devm_mfd_add_devices 或 devm 资源管理统一回收驱动编译报 devm_of_platform_populate 未定义Kconfig 未 select MFD_CORE修改父设备 Kconfig排查的核心思路是先确认设备树里每个子节点是不是都能独立生成 platform_device再确认每个 platform_device 是不是都有 matching driver最后才考虑时序问题。设备模型本身是支持 deferred probe 的如果一个子设备暂时依赖另一个子设备返回 -EPROBE_DEFER 是合法行为但前提是两者都有独立身份。5.2 为什么内核老手只回一句话回头看那位老内核只用一句话就把问题指出来是因为他对设备模型的边界非常敏感。当一个硬件实体的功能单元之间需要真正解耦MFD 就是最标准的答案这不是什么高深技巧而是内核里的约定俗成。新手容易把精力耗在调整调用顺序、加延时、改返回码这些表面功夫上却忽略了一个本质问题你有没有给这个子设备一个“身份证”。我后来的习惯是遇到 probe 顺序问题先停下来问自己三个问题这个子设备有没有自己独立的 struct device它的生命周期归谁管它在设备树里有没有自己的节点如果三个问题里有一个回答不上来那就说明设备模型层面的拆分还没做完。时序问题只是表象身份缺失才是根源。这次改造最值钱的经验不在于 MFD 本身而在于明白了一个道理内核设备模型不是摆设它就是硬件拓扑在软件里的映射。你越早按照它的规则做事后面就越少跟奇怪的时序问题缠斗。现在 SoC-e 这块芯片的 v3 驱动已经在板子上稳定跑了两个多月PHY 枚举、热插拔、VLAN 转发全部正常。假如再往后扩展功能我也不会再把新的硬件模块往老驱动里塞了直接按 MFD 的子设备往上挂就是。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询