Netdev List
 help / color / mirror / Atom feed
* [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes
@ 2026-08-29  5:25 Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 1/9] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
                   ` (8 more replies)
  0 siblings, 9 replies; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

The Airoha EN8811H answers its PHY ID from power-on, but it is an MD32
microcontroller until the host loads firmware into its volatile RAM,
and on systems that keep the firmware files in a filesystem those
files become readable long after the MDIO bus was scanned. Today the
DSA port that names such a PHY is dropped at switch probe and stays
dead for the whole uptime.

This follows the direction Andrew sketched in [1]: describe the chip
as an MDIO device that owns the download and the reset line, publish
the PHY on a child bus only once the firmware runs, and teach phylink
to wait for a PHY that is expected to probe late.

Patches 1-2 add the bindings. Patches 3-5 rework the download into a
library helper typed on a bus and address, shared per [2]: the PHY
driver keeps its behavior through wrappers, and the helper skips the
download when the MD32 already runs firmware - which is what lets the
MDIO device and the PHY driver coexist, whichever runs first. Patch 6
adds the MDIO device driver, patch 7 the pass-through bus, patches
8-9 the phylink half. Patches 8-9 alone carry a board whose chip
answers its ID before firmware: the MDIO layer buys the general case
(chips mute before firmware, quad-PHY packages, reset ownership),
not this board's necessity.

Tested on an MT7981B board (MT7531 switch, EN8811H on a 2500base-x
port), warm boots only - I have no remote way to cut power:

 - download path: U-Boot leaves the MD32 in its bootloader on every
   reboot here, so each boot exercises the MCU driver's pulse-reset
   and download; ~144KB lands in about half a second and the version
   register reads back
 - adopt path: rebinding the MDIO device against a running MD32 takes
   the no-reset branch and registers the bus in ~70ms
 - the PHY driver, probing on the child bus right after, finds the
   firmware running and skips its own download through the same check
 - phylink attaches the PHY ~0.7s later with its interrupt from DT
   (the poll-tick latency tax below), the port reaches forwarding,
   and the interrupt line counts link events across forced
   renegotiations
 - an ifdown/ifup cycle disconnects and reconnects cleanly

The polling costs latency: the attach lands anywhere in
[0, poll interval) after the PHY becomes ready. Three consecutive
boots measured attach timestamps within 47us of each other, which is
the tick phase showing through; mean tax ~500ms, worst case a full
second. The exact event exists - BUS_NOTIFY_BOUND_DRIVER fires at
probe completion - but mdio_bus_type is internal to phylib, so an
event-driven follow-up means phylib owning the notifier behind a
small API. Polling first was the plan agreed in [1]. Is that API a
direction you want?

Two more questions. The property is "slow-to-probe" on the port node,
documented in ethernet-controller.yaml; better-scoped names welcome.
And the MCU driver cycles reset only when the MD32 sits in its
bootloader, since firmware lives in volatile RAM - so on a board
without a reset line, running older firmware is adopted as-is and a
newer file on disk takes effect only after a cold start. If that
trade reads wrong, the alternative is pulsing reset on every probe
and always downloading.

Known and left out: the retry never gives up, because the errno out
of a failed bringup cannot distinguish "still filling in link modes"
from "genuinely incompatible" - the backoff exists since a failed
bringup ends in phy_detach(), which pulses a PHY-node reset line.
Unbinding the MDIO device at runtime while the port is up removes
the child bus under an attached PHY and a later phy_stop() oopses;
that path predates this series (any mdio-mux unbind does the same)
and wants a phy-core fix rather than a workaround here.

This is based on net-next at 91ec20351349. It textually overlaps in
phylink_disconnect_phy() with the pending fix series [3]; a non-RFC
respin will rebase over whichever lands first.

[1] https://lore.kernel.org/netdev/a230d199-5d4d-4637-aff3-e725a37e1da1@lunn.ch/
[2] https://lore.kernel.org/netdev/29f973e4-980d-4198-bbec-452f7421d416@lunn.ch/
[3] https://lore.kernel.org/netdev/20260827211638.63395-1-f@lex.la/

Aleksei Sviridkin (9):
  dt-bindings: net: add Airoha EN8811H PHY MCU
  dt-bindings: net: ethernet-controller: add slow-to-probe
  net: phy: air: type the buckpbus core on the bus and address
  net: phy: air: move the EN8811H firmware download into the library
  net: phy: air: skip the download when the MD32 is already running
  net: mdio: add Airoha EN8811H MDIO device driver
  net: mdio: en8811h: add the nested pass-through bus
  net: phylink: wait for PHYs that are known to probe late
  net: phylink: report no link modes while a late PHY is missing

 .../bindings/net/airoha,en8811h-mcu.yaml      |  83 ++++
 .../bindings/net/ethernet-controller.yaml     |   9 +
 MAINTAINERS                                   |   7 +
 drivers/net/mdio/Kconfig                      |  11 +
 drivers/net/mdio/Makefile                     |   1 +
 drivers/net/mdio/mdio-airoha-en8811h.c        | 300 ++++++++++++++
 drivers/net/phy/air_en8811h.c                 | 148 +------
 drivers/net/phy/air_phy_lib.c                 | 381 ++++++++++++++++--
 drivers/net/phy/air_phy_lib.h                 |  27 ++
 drivers/net/phy/phylink.c                     | 225 ++++++++++-
 10 files changed, 1011 insertions(+), 181 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
 create mode 100644 drivers/net/mdio/mdio-airoha-en8811h.c

-- 
2.53.0


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 1/9] dt-bindings: net: add Airoha EN8811H PHY MCU
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe Aleksei Sviridkin
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

The EN8811H answers its PHY ID from power-on, but a link is only
possible once its MD32 MCU runs firmware that the host loads over
MDIO into volatile RAM. On systems that keep the firmware files in
a filesystem, the files arrive long after the MDIO bus was scanned,
and the PHY node alone cannot express that gap.

Describe the MCU as an MDIO device of its own, with the PHY node on
a child bus underneath it. The device downloads the firmware when
the files appear, or adopts firmware the bootloader left running,
and registers the child bus only then, so the PHY never becomes
visible before the chip can serve it.

The reset line belongs to this node, not to the PHY node: firmware
lives in volatile RAM, so reset may be asserted only while the MCU
is not executing it. On a PHY node the line would be pulsed by
every phy_detach() and wipe the running firmware.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 .../bindings/net/airoha,en8811h-mcu.yaml      | 83 +++++++++++++++++++
 1 file changed, 83 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml

diff --git a/Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml b/Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
new file mode 100644
index 000000000000..b51a14a3b7b5
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/airoha,en8811h-mcu.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Airoha EN8811H PHY MCU
+
+maintainers:
+  - Aleksei Sviridkin <f@lex.la>
+
+description: |
+  The Airoha EN8811H 2.5G PHY is driven by an MD32 MCU that executes
+  firmware loaded over MDIO into volatile RAM. Until that firmware runs,
+  the chip answers its PHY ID but cannot bring up a link, and the
+  firmware files may live on a filesystem that is not yet mounted when
+  the MDIO bus is scanned.
+
+  This node describes the MCU as an MDIO device in its own right. The
+  driver downloads the firmware once the files become available (or
+  detects firmware already left running by the bootloader) and only
+  then registers the child MDIO bus, so the PHY node below never
+  becomes visible before the chip is able to serve it.
+
+  The reset line is owned by this node rather than by the PHY node:
+  the MD32 keeps its firmware only in volatile RAM, so the reset may
+  be asserted only while the MCU is not executing firmware. A PHY-node
+  reset would be pulsed by phy_detach() on every unbind and wipe the
+  running firmware.
+
+properties:
+  compatible:
+    const: airoha,en8811h-mcu
+
+  reg:
+    maxItems: 1
+
+  reset-gpios:
+    maxItems: 1
+
+  reset-assert-us: true
+
+  reset-deassert-us: true
+
+  mdio:
+    $ref: mdio.yaml#
+    unevaluatedProperties: false
+    description:
+      The child bus holding the PHY itself, at the same address the
+      chip answers on the parent bus.
+
+required:
+  - compatible
+  - reg
+  - mdio
+
+additionalProperties: false
+
+examples:
+  - |
+    mdio {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        ethernet-phy-mcu@d {
+            compatible = "airoha,en8811h-mcu";
+            reg = <0xd>;
+            reset-gpios = <&pio 14 1>;
+            reset-assert-us = <10000>;
+            reset-deassert-us = <20000>;
+
+            mdio {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                ethernet-phy@d {
+                    compatible = "ethernet-phy-id03a2.a411";
+                    reg = <0xd>;
+                    interrupts-extended = <&pio 15 8>;
+                };
+            };
+        };
+    };
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 1/9] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-09-04  0:29   ` Andrew Lunn
  2026-08-29  5:25 ` [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address Aleksei Sviridkin
                   ` (6 subsequent siblings)
  8 siblings, 1 reply; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

A port can reference a PHY whose driver is not usable at connect
time because the PHY's firmware, or the module carrying its driver,
lives in a filesystem that is not mounted yet when the controller
probes. Today such a port is dropped at setup and stays unusable
for the whole uptime even though the PHY becomes fully functional
seconds later.

Add a boolean the port node can carry to declare this expected:
the connect keeps the port and attaches the PHY once it becomes
usable instead of failing.

The name and the schema home are an open question of this series:
the property is generic and read from the MAC/port node, so it
lands here rather than in a vendor binding, but a better-scoped
name may exist.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 .../devicetree/bindings/net/ethernet-controller.yaml     | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/Documentation/devicetree/bindings/net/ethernet-controller.yaml b/Documentation/devicetree/bindings/net/ethernet-controller.yaml
index 1bafd687dcb1..ca82672b6cb3 100644
--- a/Documentation/devicetree/bindings/net/ethernet-controller.yaml
+++ b/Documentation/devicetree/bindings/net/ethernet-controller.yaml
@@ -113,6 +113,15 @@ properties:
     description:
       Specifies a reference to a node representing an IEEE 1588 PTP device.
 
+  slow-to-probe:
+    $ref: /schemas/types.yaml#/definitions/flag
+    description:
+      The PHY referenced by phy-handle may not exist, or may not have a
+      usable driver, when this controller connects to it, because the
+      driver or firmware arrives later (for example from a filesystem
+      mounted after the controller probed). Instead of failing, keep
+      the port and connect the PHY once it becomes usable.
+
   rx-fifo-depth:
     $ref: /schemas/types.yaml#/definitions/uint32
     description:
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 1/9] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-09-04  0:48   ` Andrew Lunn
  2026-08-29  5:25 ` [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
                   ` (5 subsequent siblings)
  8 siblings, 1 reply; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

The buckpbus accessors only need an MDIO bus and an address, but they
take a phy_device, which ties them to a probed PHY. An upcoming MDIO
device driver needs the same register access before any phy_device
exists, since it runs precisely to make the PHY presentable.

Retype the internal helpers onto (mii_bus, addr) and keep the exported
phy_device API as page-selecting wrappers around them. The file
already carries an mdio_device-typed accessor for the AN8811HB pbus,
so this follows an existing direction rather than opening a new one.

No functional change.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/phy/air_phy_lib.c | 85 +++++++++++++++++++----------------
 1 file changed, 46 insertions(+), 39 deletions(-)

diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
index 5141db19fa5e..e0fca5f285d2 100644
--- a/drivers/net/phy/air_phy_lib.c
+++ b/drivers/net/phy/air_phy_lib.c
@@ -14,31 +14,32 @@
 
 #include "air_phy_lib.h"
 
-static int __air_buckpbus_reg_read(struct phy_device *phydev,
+static int __air_buckpbus_reg_read(struct mii_bus *bus, int addr,
 				   u32 pbus_address, u32 *pbus_data)
 {
 	int pbus_data_low, pbus_data_high;
 	int ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_FIXED);
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_MODE,
+			      AIR_BPBUS_MODE_ADDR_FIXED);
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_RD_ADDR_HIGH,
-			  upper_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_RD_ADDR_HIGH,
+			      upper_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_RD_ADDR_LOW,
-			  lower_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_RD_ADDR_LOW,
+			      lower_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	pbus_data_high = __phy_read(phydev, AIR_BPBUS_RD_DATA_HIGH);
+	pbus_data_high = __mdiobus_read(bus, addr, AIR_BPBUS_RD_DATA_HIGH);
 	if (pbus_data_high < 0)
 		return pbus_data_high;
 
-	pbus_data_low = __phy_read(phydev, AIR_BPBUS_RD_DATA_LOW);
+	pbus_data_low = __mdiobus_read(bus, addr, AIR_BPBUS_RD_DATA_LOW);
 	if (pbus_data_low < 0)
 		return pbus_data_low;
 
@@ -46,64 +47,66 @@ static int __air_buckpbus_reg_read(struct phy_device *phydev,
 	return 0;
 }
 
-static int __air_buckpbus_reg_write(struct phy_device *phydev,
+static int __air_buckpbus_reg_write(struct mii_bus *bus, int addr,
 				    u32 pbus_address, u32 pbus_data)
 {
 	int ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_FIXED);
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_MODE,
+			      AIR_BPBUS_MODE_ADDR_FIXED);
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_HIGH,
-			  upper_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_HIGH,
+			      upper_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_LOW,
-			  lower_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_LOW,
+			      lower_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_HIGH,
-			  upper_16_bits(pbus_data));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_HIGH,
+			      upper_16_bits(pbus_data));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_LOW,
-			  lower_16_bits(pbus_data));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_LOW,
+			      lower_16_bits(pbus_data));
 	if (ret < 0)
 		return ret;
 
 	return 0;
 }
 
-static int __air_buckpbus_reg_modify(struct phy_device *phydev,
+static int __air_buckpbus_reg_modify(struct mii_bus *bus, int addr,
 				     u32 pbus_address, u32 mask, u32 set)
 {
 	int pbus_data_low, pbus_data_high;
 	u32 pbus_data_old, pbus_data_new;
 	int ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_FIXED);
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_MODE,
+			      AIR_BPBUS_MODE_ADDR_FIXED);
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_RD_ADDR_HIGH,
-			  upper_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_RD_ADDR_HIGH,
+			      upper_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_RD_ADDR_LOW,
-			  lower_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_RD_ADDR_LOW,
+			      lower_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	pbus_data_high = __phy_read(phydev, AIR_BPBUS_RD_DATA_HIGH);
+	pbus_data_high = __mdiobus_read(bus, addr, AIR_BPBUS_RD_DATA_HIGH);
 	if (pbus_data_high < 0)
 		return pbus_data_high;
 
-	pbus_data_low = __phy_read(phydev, AIR_BPBUS_RD_DATA_LOW);
+	pbus_data_low = __mdiobus_read(bus, addr, AIR_BPBUS_RD_DATA_LOW);
 	if (pbus_data_low < 0)
 		return pbus_data_low;
 
@@ -112,23 +115,23 @@ static int __air_buckpbus_reg_modify(struct phy_device *phydev,
 	if (pbus_data_new == pbus_data_old)
 		return 0;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_HIGH,
-			  upper_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_HIGH,
+			      upper_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_LOW,
-			  lower_16_bits(pbus_address));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_LOW,
+			      lower_16_bits(pbus_address));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_HIGH,
-			  upper_16_bits(pbus_data_new));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_HIGH,
+			      upper_16_bits(pbus_data_new));
 	if (ret < 0)
 		return ret;
 
-	ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_LOW,
-			  lower_16_bits(pbus_data_new));
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_LOW,
+			      lower_16_bits(pbus_data_new));
 	if (ret < 0)
 		return ret;
 
@@ -144,7 +147,9 @@ int air_phy_buckpbus_reg_read(struct phy_device *phydev, u32 pbus_address,
 	saved_page = phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
 
 	if (saved_page >= 0) {
-		ret = __air_buckpbus_reg_read(phydev, pbus_address, pbus_data);
+		ret = __air_buckpbus_reg_read(phydev->mdio.bus,
+					      phydev->mdio.addr,
+					      pbus_address, pbus_data);
 		if (ret < 0)
 			phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
 				   pbus_address, ret);
@@ -163,8 +168,9 @@ int air_phy_buckpbus_reg_write(struct phy_device *phydev, u32 pbus_address,
 	saved_page = phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
 
 	if (saved_page >= 0) {
-		ret = __air_buckpbus_reg_write(phydev, pbus_address,
-					       pbus_data);
+		ret = __air_buckpbus_reg_write(phydev->mdio.bus,
+					       phydev->mdio.addr,
+					       pbus_address, pbus_data);
 		if (ret < 0)
 			phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
 				   pbus_address, ret);
@@ -183,8 +189,9 @@ int air_phy_buckpbus_reg_modify(struct phy_device *phydev, u32 pbus_address,
 	saved_page = phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
 
 	if (saved_page >= 0) {
-		ret = __air_buckpbus_reg_modify(phydev, pbus_address, mask,
-						set);
+		ret = __air_buckpbus_reg_modify(phydev->mdio.bus,
+						phydev->mdio.addr,
+						pbus_address, mask, set);
 		if (ret < 0)
 			phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
 				   pbus_address, ret);
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (2 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-09-04  1:16   ` Andrew Lunn
  2026-08-29  5:25 ` [RFC PATCH net-next 5/9] net: phy: air: skip the download when the MD32 is already running Aleksei Sviridkin
                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

The EN8811H firmware loader is welded to the PHY driver, but the next
patch adds an MDIO device driver that must perform the same download
before any phy_device exists.

Move the download engine into the shared library, typed on the bus
and address like the buckpbus core: the paged transfer, the FW_CTRL
sequencing, the MCU-ready wait and the version readout. The bus-level
helpers take the MDIO bus lock themselves and save and restore the
page register directly, since phy_select_page() needs a phy_device.
The MMD status poll goes through mmd_phy_read(), which already
handles both C22 indirection and C45.

The PHY driver keeps thin wrappers with its old behavior, including
the AN8811HB path, which retains its own CRC-checked loader and only
shares the paged buffer write.

No functional change.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/phy/air_en8811h.c | 143 +-----------------
 drivers/net/phy/air_phy_lib.c | 268 ++++++++++++++++++++++++++++++++++
 drivers/net/phy/air_phy_lib.h |  26 ++++
 3 files changed, 302 insertions(+), 135 deletions(-)

diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
index edd49c193e47..fdc64362a565 100644
--- a/drivers/net/phy/air_en8811h.c
+++ b/drivers/net/phy/air_en8811h.c
@@ -20,21 +20,15 @@
 #include <linux/bitfield.h>
 #include <linux/property.h>
 #include <linux/wordpart.h>
-#include <linux/unaligned.h>
 
 #include "air_phy_lib.h"
 
 #define EN8811H_PHY_ID		0x03a2a411
 #define AN8811HB_PHY_ID		0xc0ff04a0
 
-#define EN8811H_MD32_DM		"airoha/EthMD32.dm.bin"
-#define EN8811H_MD32_DSP	"airoha/EthMD32.DSP.bin"
 #define AN8811HB_MD32_DM	"airoha/an8811hb/EthMD32_CRC.DM.bin"
 #define AN8811HB_MD32_DSP	"airoha/an8811hb/EthMD32_CRC.DSP.bin"
 
-#define AIR_FW_ADDR_DM	0x00000000
-#define AIR_FW_ADDR_DSP	0x00100000
-
 /* MII Registers */
 #define AIR_AUX_CTRL_STATUS		0x1d
 #define   AIR_AUX_CTRL_STATUS_SPEED_MASK	GENMASK(4, 2)
@@ -44,8 +38,6 @@
 #define   AIR_AUX_CTRL_STATUS_SPEED_2500	0xc
 
 /* Registers on MDIO_MMD_VEND1 */
-#define EN8811H_PHY_FW_STATUS		0x8009
-#define   EN8811H_PHY_READY			0x02
 
 #define AIR_PHY_MCU_CMD_0		0x800b
 #define AIR_PHY_MCU_CMD_1		0x800c
@@ -108,8 +100,6 @@
 #define EN8811H_2P5G_LPA		0x3b30
 #define   EN8811H_2P5G_LPA_2P5G			BIT(0)
 
-#define EN8811H_FW_VERSION		0x3b3c
-
 #define EN8811H_POLARITY		0xca0f8
 #define   EN8811H_POLARITY_TX_NORMAL		BIT(0)
 #define   EN8811H_POLARITY_RX_REVERSE		BIT(1)
@@ -122,12 +112,6 @@
 #define EN8811H_CLK_CGM			0xcf958
 #define   EN8811H_CLK_CGM_CKO			BIT(26)
 
-#define EN8811H_FW_CTRL_1		0x0f0018
-#define   EN8811H_FW_CTRL_1_START		0x0
-#define   EN8811H_FW_CTRL_1_FINISH		0x1
-#define EN8811H_FW_CTRL_2		0x800000
-#define EN8811H_FW_CTRL_2_LOADING		BIT(11)
-
 #define AN8811HB_CRC_PM_SET1		0xf020c
 #define AN8811HB_CRC_PM_MON2		0xf0218
 #define AN8811HB_CRC_PM_MON3		0xf021c
@@ -270,80 +254,10 @@ static int __air_pbus_reg_write(struct mdio_device *mdiodev,
 			       upper_16_bits(pbus_data));
 }
 
-static int __air_write_buf(struct phy_device *phydev, u32 address,
-			   const struct firmware *fw)
-{
-	unsigned int offset;
-	int ret;
-	u16 val;
-
-	ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_INCR);
-	if (ret < 0)
-		return ret;
-
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_HIGH,
-			  upper_16_bits(address));
-	if (ret < 0)
-		return ret;
-
-	ret = __phy_write(phydev, AIR_BPBUS_WR_ADDR_LOW,
-			  lower_16_bits(address));
-	if (ret < 0)
-		return ret;
-
-	for (offset = 0; offset < fw->size; offset += 4) {
-		val = get_unaligned_le16(&fw->data[offset + 2]);
-		ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_HIGH, val);
-		if (ret < 0)
-			return ret;
-
-		val = get_unaligned_le16(&fw->data[offset]);
-		ret = __phy_write(phydev, AIR_BPBUS_WR_DATA_LOW, val);
-		if (ret < 0)
-			return ret;
-	}
-
-	return 0;
-}
-
-static int air_write_buf(struct phy_device *phydev, u32 address,
-			 const struct firmware *fw)
-{
-	int saved_page;
-	int ret = 0;
-
-	saved_page = phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
-
-	if (saved_page >= 0) {
-		ret = __air_write_buf(phydev, address, fw);
-		if (ret < 0)
-			phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
-				   address, ret);
-	}
-
-	return phy_restore_page(phydev, saved_page, ret);
-}
-
 static int en8811h_wait_mcu_ready(struct phy_device *phydev)
 {
-	int ret, reg_value;
-
-	ret = air_phy_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
-					 EN8811H_FW_CTRL_1_FINISH);
-	if (ret)
-		return ret;
-
-	/* Because of mdio-lock, may have to wait for multiple loads */
-	ret = phy_read_mmd_poll_timeout(phydev, MDIO_MMD_VEND1,
-					EN8811H_PHY_FW_STATUS, reg_value,
-					reg_value == EN8811H_PHY_READY,
-					20000, 7500000, true);
-	if (ret) {
-		phydev_err(phydev, "MCU not ready: 0x%x\n", reg_value);
-		return -ENODEV;
-	}
-
-	return 0;
+	return air_en8811h_wait_mcu_ready(phydev->mdio.bus, phydev->mdio.addr,
+					  phydev->is_c45, &phydev->mdio.dev);
 }
 
 static int an8811hb_check_crc(struct phy_device *phydev, u32 set1,
@@ -405,7 +319,8 @@ static int an8811hb_load_file(struct phy_device *phydev, const char *name,
 	if (ret < 0)
 		return ret;
 
-	ret = air_write_buf(phydev, address,  fw);
+	ret = air_fw_write_buf(phydev->mdio.bus, phydev->mdio.addr, address,
+			       fw);
 	release_firmware(fw);
 	return ret;
 }
@@ -501,54 +416,12 @@ static int an8811hb_load_firmware(struct phy_device *phydev)
 
 static int en8811h_load_firmware(struct phy_device *phydev)
 {
-	struct device *dev = &phydev->mdio.dev;
-	const struct firmware *fw1, *fw2;
+	struct en8811h_priv *priv = phydev->priv;
 	int ret;
 
-	ret = request_firmware_direct(&fw1, EN8811H_MD32_DM, dev);
-	if (ret < 0)
-		return ret;
-
-	ret = request_firmware_direct(&fw2, EN8811H_MD32_DSP, dev);
-	if (ret < 0)
-		goto en8811h_load_firmware_rel1;
-
-	ret = air_phy_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
-					 EN8811H_FW_CTRL_1_START);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	ret = air_phy_buckpbus_reg_modify(phydev, EN8811H_FW_CTRL_2,
-					  EN8811H_FW_CTRL_2_LOADING,
-					  EN8811H_FW_CTRL_2_LOADING);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	ret = air_write_buf(phydev, AIR_FW_ADDR_DM,  fw1);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	ret = air_write_buf(phydev, AIR_FW_ADDR_DSP, fw2);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	ret = air_phy_buckpbus_reg_modify(phydev, EN8811H_FW_CTRL_2,
-					  EN8811H_FW_CTRL_2_LOADING, 0);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	ret = en8811h_wait_mcu_ready(phydev);
-	if (ret < 0)
-		goto en8811h_load_firmware_out;
-
-	en8811h_print_fw_version(phydev);
-
-en8811h_load_firmware_out:
-	release_firmware(fw2);
-
-en8811h_load_firmware_rel1:
-	release_firmware(fw1);
-
+	ret = air_en8811h_fw_download(phydev->mdio.bus, phydev->mdio.addr,
+				      phydev->is_c45, &phydev->mdio.dev,
+				      &priv->firmware_version);
 	if (ret < 0)
 		phydev_err(phydev, "Load firmware failed: %d\n", ret);
 
diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
index e0fca5f285d2..1ed5c69d7073 100644
--- a/drivers/net/phy/air_phy_lib.c
+++ b/drivers/net/phy/air_phy_lib.c
@@ -8,11 +8,16 @@
  */
 
 #include <linux/export.h>
+#include <linux/firmware.h>
+#include <linux/iopoll.h>
+#include <linux/mdio.h>
 #include <linux/module.h>
 #include <linux/phy.h>
+#include <linux/unaligned.h>
 #include <linux/wordpart.h>
 
 #include "air_phy_lib.h"
+#include "phylib.h"
 
 static int __air_buckpbus_reg_read(struct mii_bus *bus, int addr,
 				   u32 pbus_address, u32 *pbus_data)
@@ -201,6 +206,269 @@ int air_phy_buckpbus_reg_modify(struct phy_device *phydev, u32 pbus_address,
 }
 EXPORT_SYMBOL_GPL(air_phy_buckpbus_reg_modify);
 
+static int __air_write_buf(struct mii_bus *bus, int addr, u32 address,
+			   const struct firmware *fw)
+{
+	unsigned int offset;
+	int ret;
+	u16 val;
+
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_MODE,
+			      AIR_BPBUS_MODE_ADDR_INCR);
+	if (ret < 0)
+		return ret;
+
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_HIGH,
+			      upper_16_bits(address));
+	if (ret < 0)
+		return ret;
+
+	ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_ADDR_LOW,
+			      lower_16_bits(address));
+	if (ret < 0)
+		return ret;
+
+	for (offset = 0; offset < fw->size; offset += 4) {
+		val = get_unaligned_le16(&fw->data[offset + 2]);
+		ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_HIGH, val);
+		if (ret < 0)
+			return ret;
+
+		val = get_unaligned_le16(&fw->data[offset]);
+		ret = __mdiobus_write(bus, addr, AIR_BPBUS_WR_DATA_LOW, val);
+		if (ret < 0)
+			return ret;
+	}
+
+	return 0;
+}
+
+/* The phy_select_page() path is not usable here: these run before any
+ * phy_device exists. Callers hold the bus lock across select/op/restore.
+ */
+static int air_mdio_select_page(struct mii_bus *bus, int addr, int page)
+{
+	int saved_page, ret;
+
+	saved_page = __mdiobus_read(bus, addr, AIR_EXT_PAGE_ACCESS);
+	if (saved_page < 0)
+		return saved_page;
+
+	if (saved_page != page) {
+		ret = __mdiobus_write(bus, addr, AIR_EXT_PAGE_ACCESS, page);
+		if (ret < 0)
+			return ret;
+	}
+
+	return saved_page;
+}
+
+static int air_mdio_restore_page(struct mii_bus *bus, int addr,
+				 int saved_page, int page, int ret)
+{
+	int restore;
+
+	if (saved_page != page) {
+		restore = __mdiobus_write(bus, addr, AIR_EXT_PAGE_ACCESS,
+					  saved_page);
+		if (ret >= 0 && restore < 0)
+			ret = restore;
+	}
+
+	return ret;
+}
+
+int air_fw_write_buf(struct mii_bus *bus, int addr, u32 address,
+		     const struct firmware *fw)
+{
+	int saved_page, ret;
+
+	mutex_lock(&bus->mdio_lock);
+
+	saved_page = air_mdio_select_page(bus, addr, AIR_PHY_PAGE_EXTENDED_4);
+	if (saved_page < 0) {
+		ret = saved_page;
+	} else {
+		ret = __air_write_buf(bus, addr, address, fw);
+		ret = air_mdio_restore_page(bus, addr, saved_page,
+					    AIR_PHY_PAGE_EXTENDED_4, ret);
+	}
+
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(air_fw_write_buf);
+
+static int air_mdio_buckpbus_reg_read(struct mii_bus *bus, int addr,
+				      u32 pbus_address, u32 *pbus_data)
+{
+	int saved_page, ret;
+
+	mutex_lock(&bus->mdio_lock);
+
+	saved_page = air_mdio_select_page(bus, addr, AIR_PHY_PAGE_EXTENDED_4);
+	if (saved_page < 0) {
+		ret = saved_page;
+	} else {
+		ret = __air_buckpbus_reg_read(bus, addr, pbus_address,
+					      pbus_data);
+		ret = air_mdio_restore_page(bus, addr, saved_page,
+					    AIR_PHY_PAGE_EXTENDED_4, ret);
+	}
+
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+
+static int air_mdio_buckpbus_reg_write(struct mii_bus *bus, int addr,
+				       u32 pbus_address, u32 pbus_data)
+{
+	int saved_page, ret;
+
+	mutex_lock(&bus->mdio_lock);
+
+	saved_page = air_mdio_select_page(bus, addr, AIR_PHY_PAGE_EXTENDED_4);
+	if (saved_page < 0) {
+		ret = saved_page;
+	} else {
+		ret = __air_buckpbus_reg_write(bus, addr, pbus_address,
+					       pbus_data);
+		ret = air_mdio_restore_page(bus, addr, saved_page,
+					    AIR_PHY_PAGE_EXTENDED_4, ret);
+	}
+
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+
+static int air_mdio_buckpbus_reg_modify(struct mii_bus *bus, int addr,
+					u32 pbus_address, u32 mask, u32 set)
+{
+	int saved_page, ret;
+
+	mutex_lock(&bus->mdio_lock);
+
+	saved_page = air_mdio_select_page(bus, addr, AIR_PHY_PAGE_EXTENDED_4);
+	if (saved_page < 0) {
+		ret = saved_page;
+	} else {
+		ret = __air_buckpbus_reg_modify(bus, addr, pbus_address,
+						mask, set);
+		ret = air_mdio_restore_page(bus, addr, saved_page,
+					    AIR_PHY_PAGE_EXTENDED_4, ret);
+	}
+
+	mutex_unlock(&bus->mdio_lock);
+	return ret;
+}
+
+static int air_mmd_status_read(struct mii_bus *bus, int addr, bool is_c45)
+{
+	int ret;
+
+	mutex_lock(&bus->mdio_lock);
+	ret = mmd_phy_read(bus, addr, is_c45, MDIO_MMD_VEND1,
+			   EN8811H_PHY_FW_STATUS);
+	mutex_unlock(&bus->mdio_lock);
+
+	return ret;
+}
+
+int air_en8811h_wait_mcu_ready(struct mii_bus *bus, int addr, bool is_c45,
+			       struct device *dev)
+{
+	int ret, reg_value;
+
+	ret = air_mdio_buckpbus_reg_write(bus, addr, EN8811H_FW_CTRL_1,
+					  EN8811H_FW_CTRL_1_FINISH);
+	if (ret)
+		return ret;
+
+	/* Because of mdio-lock, may have to wait for multiple loads. A read
+	 * error ends the poll at once, like phy_read_mmd_poll_timeout()
+	 * would: the bus is not going to heal within the timeout.
+	 */
+	ret = read_poll_timeout(air_mmd_status_read, reg_value,
+				reg_value < 0 ||
+				reg_value == EN8811H_PHY_READY,
+				20000, 7500000, true, bus, addr, is_c45);
+	if (reg_value < 0)
+		return reg_value;
+	if (ret) {
+		dev_err(dev, "MCU not ready: 0x%x\n", reg_value);
+		return -ENODEV;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(air_en8811h_wait_mcu_ready);
+
+int air_en8811h_fw_download(struct mii_bus *bus, int addr, bool is_c45,
+			    struct device *dev, u32 *fw_version)
+{
+	const struct firmware *fw1, *fw2;
+	int ret;
+
+	ret = request_firmware_direct(&fw1, EN8811H_MD32_DM, dev);
+	if (ret < 0)
+		return ret;
+
+	ret = request_firmware_direct(&fw2, EN8811H_MD32_DSP, dev);
+	if (ret < 0)
+		goto air_fw_download_rel1;
+
+	ret = air_mdio_buckpbus_reg_write(bus, addr, EN8811H_FW_CTRL_1,
+					  EN8811H_FW_CTRL_1_START);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_mdio_buckpbus_reg_modify(bus, addr, EN8811H_FW_CTRL_2,
+					   EN8811H_FW_CTRL_2_LOADING,
+					   EN8811H_FW_CTRL_2_LOADING);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_fw_write_buf(bus, addr, AIR_FW_ADDR_DM, fw1);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_fw_write_buf(bus, addr, AIR_FW_ADDR_DSP, fw2);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_mdio_buckpbus_reg_modify(bus, addr, EN8811H_FW_CTRL_2,
+					   EN8811H_FW_CTRL_2_LOADING, 0);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_en8811h_wait_mcu_ready(bus, addr, is_c45, dev);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	ret = air_mdio_buckpbus_reg_read(bus, addr, EN8811H_FW_VERSION,
+					 fw_version);
+	if (ret < 0)
+		goto air_fw_download_out;
+
+	dev_info(dev, "MD32 firmware version: %08x\n", *fw_version);
+
+air_fw_download_out:
+	release_firmware(fw2);
+
+air_fw_download_rel1:
+	release_firmware(fw1);
+
+	/* No error print here: the callers retry or log on their own terms,
+	 * and a poller retrying a half-installed firmware package would turn
+	 * a print at this level into a permanent drumbeat.
+	 */
+	return ret;
+}
+EXPORT_SYMBOL_GPL(air_en8811h_fw_download);
+
+MODULE_FIRMWARE(EN8811H_MD32_DM);
+MODULE_FIRMWARE(EN8811H_MD32_DSP);
+
 int air_phy_read_page(struct phy_device *phydev)
 {
 	return __phy_read(phydev, AIR_EXT_PAGE_ACCESS);
diff --git a/drivers/net/phy/air_phy_lib.h b/drivers/net/phy/air_phy_lib.h
index 01bb32e7c7c9..6b11dbeaea9b 100644
--- a/drivers/net/phy/air_phy_lib.h
+++ b/drivers/net/phy/air_phy_lib.h
@@ -29,6 +29,23 @@
 #define AIR_BPBUS_RD_DATA_HIGH		0x17
 #define AIR_BPBUS_RD_DATA_LOW		0x18
 
+#define EN8811H_MD32_DM			"airoha/EthMD32.dm.bin"
+#define EN8811H_MD32_DSP		"airoha/EthMD32.DSP.bin"
+
+#define AIR_FW_ADDR_DM			0x00000000
+#define AIR_FW_ADDR_DSP			0x00100000
+
+#define EN8811H_FW_CTRL_1		0x0f0018
+#define   EN8811H_FW_CTRL_1_START		0x0
+#define   EN8811H_FW_CTRL_1_FINISH		0x1
+#define EN8811H_FW_CTRL_2		0x800000
+#define   EN8811H_FW_CTRL_2_LOADING		BIT(11)
+
+#define EN8811H_PHY_FW_STATUS		0x8009
+#define   EN8811H_PHY_READY			0x02
+
+#define EN8811H_FW_VERSION		0x3b3c
+
 int air_phy_buckpbus_reg_modify(struct phy_device *phydev, u32 pbus_address,
 				u32 mask, u32 set);
 int air_phy_buckpbus_reg_read(struct phy_device *phydev, u32 pbus_address,
@@ -38,4 +55,13 @@ int air_phy_buckpbus_reg_write(struct phy_device *phydev, u32 pbus_address,
 int air_phy_read_page(struct phy_device *phydev);
 int air_phy_write_page(struct phy_device *phydev, int page);
 
+struct firmware;
+
+int air_fw_write_buf(struct mii_bus *bus, int addr, u32 address,
+		     const struct firmware *fw);
+int air_en8811h_wait_mcu_ready(struct mii_bus *bus, int addr, bool is_c45,
+			       struct device *dev);
+int air_en8811h_fw_download(struct mii_bus *bus, int addr, bool is_c45,
+			    struct device *dev, u32 *fw_version);
+
 #endif /* __AIR_PHY_LIB_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 5/9] net: phy: air: skip the download when the MD32 is already running
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (3 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

The download is unconditional, so a chip whose firmware was loaded by
something else - a bootloader, an earlier bind of the PHY driver, or
an MDIO device serving the chip - is reprogrammed with what it is
already running, at 144KB per probe.

Read the status register the loader already polls for readiness and
skip the download when it reports ready, only picking up the running
firmware's version. The wait that follows is what makes this safe: a
chip that was not in fact running fails there instead of coming up
misprogrammed.

Living in the shared helper, the check covers every caller, and it is
what lets the PHY driver and the coming MDIO device driver coexist:
whichever runs second finds the firmware already up and leaves it
alone.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/phy/air_en8811h.c |  5 ++++-
 drivers/net/phy/air_phy_lib.c | 28 ++++++++++++++++++++++++++++
 drivers/net/phy/air_phy_lib.h |  1 +
 3 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
index fdc64362a565..fcc6e373edd6 100644
--- a/drivers/net/phy/air_en8811h.c
+++ b/drivers/net/phy/air_en8811h.c
@@ -1030,7 +1030,10 @@ static int en8811h_probe(struct phy_device *phydev)
 	if (ret < 0)
 		return ret;
 
-	/* mcu has just restarted after firmware load */
+	/* Freshly downloaded firmware has just started; firmware adopted
+	 * from the bootloader is already past its own start. Neither needs
+	 * the restart a later resume would.
+	 */
 	priv->mcu_needs_restart = false;
 
 	/* MDIO_DEVS1/2 empty, so set mmds_present bits here */
diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
index 1ed5c69d7073..c1187f357f4c 100644
--- a/drivers/net/phy/air_phy_lib.c
+++ b/drivers/net/phy/air_phy_lib.c
@@ -374,6 +374,12 @@ static int air_mmd_status_read(struct mii_bus *bus, int addr, bool is_c45)
 	return ret;
 }
 
+bool air_en8811h_mcu_running(struct mii_bus *bus, int addr, bool is_c45)
+{
+	return air_mmd_status_read(bus, addr, is_c45) == EN8811H_PHY_READY;
+}
+EXPORT_SYMBOL_GPL(air_en8811h_mcu_running);
+
 int air_en8811h_wait_mcu_ready(struct mii_bus *bus, int addr, bool is_c45,
 			       struct device *dev)
 {
@@ -409,6 +415,28 @@ int air_en8811h_fw_download(struct mii_bus *bus, int addr, bool is_c45,
 	const struct firmware *fw1, *fw2;
 	int ret;
 
+	if (air_en8811h_mcu_running(bus, addr, is_c45)) {
+		/* Loaded by a bootloader, an earlier bind, or another
+		 * device serving the chip. The wait below is what makes
+		 * trusting the status register safe: a chip that was not
+		 * in fact running fails there instead of coming up
+		 * misprogrammed.
+		 */
+		ret = air_en8811h_wait_mcu_ready(bus, addr, is_c45, dev);
+		if (ret < 0)
+			return ret;
+
+		ret = air_mdio_buckpbus_reg_read(bus, addr,
+						 EN8811H_FW_VERSION,
+						 fw_version);
+		if (ret < 0)
+			return ret;
+
+		dev_info(dev, "MD32 already running, firmware %08x\n",
+			 *fw_version);
+		return 0;
+	}
+
 	ret = request_firmware_direct(&fw1, EN8811H_MD32_DM, dev);
 	if (ret < 0)
 		return ret;
diff --git a/drivers/net/phy/air_phy_lib.h b/drivers/net/phy/air_phy_lib.h
index 6b11dbeaea9b..8d9f24da1271 100644
--- a/drivers/net/phy/air_phy_lib.h
+++ b/drivers/net/phy/air_phy_lib.h
@@ -59,6 +59,7 @@ struct firmware;
 
 int air_fw_write_buf(struct mii_bus *bus, int addr, u32 address,
 		     const struct firmware *fw);
+bool air_en8811h_mcu_running(struct mii_bus *bus, int addr, bool is_c45);
 int air_en8811h_wait_mcu_ready(struct mii_bus *bus, int addr, bool is_c45,
 			       struct device *dev);
 int air_en8811h_fw_download(struct mii_bus *bus, int addr, bool is_c45,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (4 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 5/9] net: phy: air: skip the download when the MD32 is already running Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-09-04  1:36   ` Andrew Lunn
  2026-08-29  5:25 ` [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

Until its firmware has been downloaded the EN8811H is not an Ethernet
PHY, it is an MD32 microcontroller waiting in its bootloader. On
systems that keep the firmware files in a filesystem, the files become
readable long after the MDIO bus was scanned, and the PHY driver's
probe-time download then cannot work at boot.

Describe the chip as an MDIO device. The driver polls for the
firmware files with backoff and downloads through the shared library
helper once they can be read; a chip whose firmware was left running
by the bootloader is adopted as-is through the helper's running
check. There is no give-up path: installing the firmware package on a
running system is a normal thing to do, and a driver that had stopped
looking would turn that into a needless reboot.

The reset line is claimed here rather than on the PHY node, and it is
cycled only when the MD32 sits in its bootloader: the firmware lives
in volatile RAM, so an assert on a running chip - such as the one
phy_detach() performs on a PHY-node reset - would wipe it.

Polling is used rather than deferred probing because
request_firmware_direct() has no usermode-helper fallback: an
unmounted rootfs fails immediately and would keep the deferred-probe
list spinning for the whole mount window.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 MAINTAINERS                            |   7 +
 drivers/net/mdio/Kconfig               |  11 ++
 drivers/net/mdio/Makefile              |   1 +
 drivers/net/mdio/mdio-airoha-en8811h.c | 172 +++++++++++++++++++++++++
 4 files changed, 191 insertions(+)
 create mode 100644 drivers/net/mdio/mdio-airoha-en8811h.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 460cb7268845..21d39049e692 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -759,6 +759,13 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/net/airoha,en7581-eth.yaml
 F:	drivers/net/ethernet/airoha/
 
+AIROHA EN8811H MCU MDIO DRIVER
+M:	Aleksei Sviridkin <f@lex.la>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
+F:	drivers/net/mdio/mdio-airoha-en8811h.c
+
 AIROHA PCIE PHY DRIVER
 M:	Lorenzo Bianconi <lorenzo@kernel.org>
 L:	linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
diff --git a/drivers/net/mdio/Kconfig b/drivers/net/mdio/Kconfig
index a05229838cb4..d46bebd05cc8 100644
--- a/drivers/net/mdio/Kconfig
+++ b/drivers/net/mdio/Kconfig
@@ -29,6 +29,17 @@ config MDIO_AIROHA
 	  This module provides a driver for the MDIO busses found in the
 	  Airoha AN7583 SoC's.
 
+config MDIO_AIROHA_EN8811H
+	tristate "Airoha EN8811H MDIO device support"
+	depends on OF_MDIO
+	select AIR_NET_PHYLIB
+	help
+	  This module provides a driver for the Airoha EN8811H, which is an
+	  MD32 microcontroller until firmware is downloaded into it and only
+	  becomes an Ethernet PHY afterwards. The driver downloads that
+	  firmware once it becomes readable, or adopts firmware a bootloader
+	  left running, before letting the PHY be probed.
+
 config MDIO_SUN4I
 	tristate "Allwinner sun4i MDIO interface support"
 	depends on ARCH_SUNXI || COMPILE_TEST
diff --git a/drivers/net/mdio/Makefile b/drivers/net/mdio/Makefile
index 048586746026..06d096675dac 100644
--- a/drivers/net/mdio/Makefile
+++ b/drivers/net/mdio/Makefile
@@ -6,6 +6,7 @@ obj-$(CONFIG_FWNODE_MDIO)	+= fwnode_mdio.o
 obj-$(CONFIG_OF_MDIO)		+= of_mdio.o
 
 obj-$(CONFIG_MDIO_AIROHA)		+= mdio-airoha.o
+obj-$(CONFIG_MDIO_AIROHA_EN8811H)	+= mdio-airoha-en8811h.o
 obj-$(CONFIG_MDIO_ASPEED)		+= mdio-aspeed.o
 obj-$(CONFIG_MDIO_BCM_IPROC)		+= mdio-bcm-iproc.o
 obj-$(CONFIG_MDIO_BCM_UNIMAC)		+= mdio-bcm-unimac.o
diff --git a/drivers/net/mdio/mdio-airoha-en8811h.c b/drivers/net/mdio/mdio-airoha-en8811h.c
new file mode 100644
index 000000000000..e16211da3d70
--- /dev/null
+++ b/drivers/net/mdio/mdio-airoha-en8811h.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Airoha EN8811H MDIO device driver
+ *
+ * Until its firmware has been downloaded the EN8811H is not an Ethernet PHY,
+ * it is an MD32 microcontroller waiting in its bootloader. Describing it as a
+ * plain MDIO device lets the firmware be downloaded as soon as the files can
+ * be read - in practice, once the filesystem holding them has been mounted -
+ * and lets the PHY appear only after the chip is able to act as one.
+ *
+ * Copyright (C) 2026 Aleksei Sviridkin <f@lex.la>
+ */
+
+#include <linux/delay.h>
+#include <linux/firmware.h>
+#include <linux/gpio/consumer.h>
+#include <linux/mdio.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/property.h>
+#include <linux/workqueue.h>
+
+#include "../phy/air_phy_lib.h"
+
+/*
+ * Poll rather than defer probing: request_firmware_direct() has no usermode
+ * helper fallback, so a rootfs that is not mounted yet fails immediately and
+ * would keep the deferred-probe list spinning for the whole mount window.
+ *
+ * Never give up. The firmware can arrive arbitrarily late and still be worth
+ * waiting for - installing the firmware package on a running system is a
+ * normal thing to do - and a driver that had stopped looking would turn that
+ * into a needless reboot. Back off to a slow poll instead, and leave a single
+ * breadcrumb for the system that simply does not have the files.
+ */
+#define EN8811H_FW_POLL_MIN_MS	1000
+#define EN8811H_FW_POLL_MAX_MS	30000
+#define EN8811H_FW_WARN_MS	60000
+
+struct en8811h_mcu {
+	struct mdio_device *mdiodev;
+	struct gpio_desc *reset_gpio;
+	struct delayed_work fw_poll;
+	unsigned int poll_ms;
+	unsigned int waited_ms;
+	u32 fw_version;
+	bool warned;
+};
+
+static void en8811h_mcu_fw_poll(struct work_struct *work)
+{
+	struct en8811h_mcu *mcu = container_of(to_delayed_work(work),
+					       struct en8811h_mcu, fw_poll);
+	struct device *dev = &mcu->mdiodev->dev;
+	int ret;
+
+	/* The chip enumerates as a C22 PHY; MMD access is indirect */
+	ret = air_en8811h_fw_download(mcu->mdiodev->bus, mcu->mdiodev->addr,
+				      false, dev, &mcu->fw_version);
+	if (!ret) {
+		dev_dbg(dev, "firmware %08x running after %ums\n",
+			mcu->fw_version, mcu->waited_ms);
+		return;
+	}
+
+	mcu->waited_ms += mcu->poll_ms;
+	if (!mcu->warned && mcu->waited_ms >= EN8811H_FW_WARN_MS) {
+		/* Missing files resolve by themselves once installed; a bus
+		 * or register error will not, and deserves its own message.
+		 */
+		if (ret == -ENOENT)
+			dev_warn(dev, "still waiting for %s and %s\n",
+				 EN8811H_MD32_DM, EN8811H_MD32_DSP);
+		else
+			dev_warn(dev, "firmware download keeps failing: %pe\n",
+				 ERR_PTR(ret));
+		mcu->warned = true;
+	}
+
+	mcu->poll_ms = min(mcu->poll_ms * 2, EN8811H_FW_POLL_MAX_MS);
+	queue_delayed_work(system_freezable_wq, &mcu->fw_poll,
+			   msecs_to_jiffies(mcu->poll_ms));
+}
+
+static int en8811h_mcu_probe(struct mdio_device *mdiodev)
+{
+	struct device *dev = &mdiodev->dev;
+	struct en8811h_mcu *mcu;
+	u32 deassert_us = 0;
+
+	mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
+	if (!mcu)
+		return -ENOMEM;
+
+	mcu->mdiodev = mdiodev;
+	mdiodev_set_drvdata(mdiodev, mcu);
+
+	/*
+	 * The core only claims reset-gpios for devices flagged as PHYs
+	 * (mdiobus_register_device()), so claim it here. Owning it at this
+	 * level is the point: phy_detach() asserts the reset of the PHY it
+	 * detaches, which would wipe firmware the MD32 holds in RAM.
+	 */
+	mcu->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_ASIS);
+	if (IS_ERR(mcu->reset_gpio))
+		return dev_err_probe(dev, PTR_ERR(mcu->reset_gpio),
+				     "failed to get reset GPIO\n");
+
+	if (mcu->reset_gpio)
+		gpiod_set_consumer_name(mcu->reset_gpio, "EN8811H reset");
+
+	/*
+	 * Firmware left running by the bootloader, or by a previous bind,
+	 * lives in volatile RAM: the reset line must not be touched then.
+	 * Only a chip still in its bootloader gets the clean reset cycle.
+	 */
+	if (air_en8811h_mcu_running(mdiodev->bus, mdiodev->addr, false)) {
+		dev_dbg(dev, "MD32 already running, adopting it\n");
+	} else if (mcu->reset_gpio) {
+		u32 assert_us = 0;
+
+		device_property_read_u32(dev, "reset-assert-us", &assert_us);
+		device_property_read_u32(dev, "reset-deassert-us",
+					 &deassert_us);
+
+		gpiod_direction_output(mcu->reset_gpio, 1);
+		if (assert_us)
+			fsleep(assert_us);
+
+		gpiod_set_value_cansleep(mcu->reset_gpio, 0);
+		if (deassert_us)
+			fsleep(deassert_us);
+	}
+
+	mcu->poll_ms = EN8811H_FW_POLL_MIN_MS;
+	INIT_DELAYED_WORK(&mcu->fw_poll, en8811h_mcu_fw_poll);
+	/* Freezable, so neither the file lookup nor the ~144KB MDIO
+	 * download can land on a bus that is suspending. The download is
+	 * long for a bound worker, but it runs once per firmware arrival.
+	 */
+	queue_delayed_work(system_freezable_wq, &mcu->fw_poll, 0);
+
+	return 0;
+}
+
+static void en8811h_mcu_remove(struct mdio_device *mdiodev)
+{
+	struct en8811h_mcu *mcu = mdiodev_get_drvdata(mdiodev);
+
+	cancel_delayed_work_sync(&mcu->fw_poll);
+}
+
+static const struct of_device_id en8811h_mcu_of_match[] = {
+	{ .compatible = "airoha,en8811h-mcu" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, en8811h_mcu_of_match);
+
+static struct mdio_driver en8811h_mcu_driver = {
+	.probe = en8811h_mcu_probe,
+	.remove = en8811h_mcu_remove,
+	.mdiodrv.driver = {
+		.name = "airoha-en8811h-mcu",
+		.of_match_table = en8811h_mcu_of_match,
+	},
+};
+
+mdio_module_driver(en8811h_mcu_driver);
+
+MODULE_DESCRIPTION("Airoha EN8811H MDIO device driver");
+MODULE_AUTHOR("Aleksei Sviridkin <f@lex.la>");
+MODULE_LICENSE("GPL");
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (5 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-09-04  1:43   ` Andrew Lunn
  2026-08-29  5:25 ` [RFC PATCH net-next 8/9] net: phylink: wait for PHYs that are known to probe late Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 9/9] net: phylink: report no link modes while a late PHY is missing Aleksei Sviridkin
  8 siblings, 1 reply; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

Registering the bus is what publishes the PHY, so it must happen only
once the MD32 is running its firmware. Put the PHY on a bus of its
own rather than on the parent so that the device tree can describe it
normally, interrupts included, and so that the MCU keeps ownership of the
reset line the PHY must not touch.

Only the address the MD32 answers on is passed through; every other
address returns -ENODEV, so scanning this bus cannot produce anything but
this chip's PHY. phy_mask would express the same thing but is not usable
here: of_mdiobus_register() overwrites it before walking the children.

Take the parent lock with MDIO_MUTEX_NESTED and use the __mdiobus
accessors, which is how the DSA drivers reach through a child bus into
their parent.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/mdio/mdio-airoha-en8811h.c | 128 +++++++++++++++++++++++++
 1 file changed, 128 insertions(+)

diff --git a/drivers/net/mdio/mdio-airoha-en8811h.c b/drivers/net/mdio/mdio-airoha-en8811h.c
index e16211da3d70..b28e116289e6 100644
--- a/drivers/net/mdio/mdio-airoha-en8811h.c
+++ b/drivers/net/mdio/mdio-airoha-en8811h.c
@@ -17,6 +17,8 @@
 #include <linux/mdio.h>
 #include <linux/module.h>
 #include <linux/of.h>
+#include <linux/of_mdio.h>
+#include <linux/phy.h>
 #include <linux/property.h>
 #include <linux/workqueue.h>
 
@@ -47,6 +49,112 @@ struct en8811h_mcu {
 	bool warned;
 };
 
+static int en8811h_mcu_read(struct mii_bus *bus, int addr, int regnum)
+{
+	struct en8811h_mcu *mcu = bus->priv;
+	struct mii_bus *parent = mcu->mdiodev->bus;
+	int ret;
+
+	if (addr != mcu->mdiodev->addr)
+		return -ENODEV;
+
+	mutex_lock_nested(&parent->mdio_lock, MDIO_MUTEX_NESTED);
+	ret = __mdiobus_read(parent, addr, regnum);
+	mutex_unlock(&parent->mdio_lock);
+
+	return ret;
+}
+
+static int en8811h_mcu_write(struct mii_bus *bus, int addr, int regnum, u16 val)
+{
+	struct en8811h_mcu *mcu = bus->priv;
+	struct mii_bus *parent = mcu->mdiodev->bus;
+	int ret;
+
+	if (addr != mcu->mdiodev->addr)
+		return -ENODEV;
+
+	mutex_lock_nested(&parent->mdio_lock, MDIO_MUTEX_NESTED);
+	ret = __mdiobus_write(parent, addr, regnum, val);
+	mutex_unlock(&parent->mdio_lock);
+
+	return ret;
+}
+
+static int en8811h_mcu_read_c45(struct mii_bus *bus, int addr, int devad,
+				int regnum)
+{
+	struct en8811h_mcu *mcu = bus->priv;
+	struct mii_bus *parent = mcu->mdiodev->bus;
+	int ret;
+
+	if (addr != mcu->mdiodev->addr)
+		return -ENODEV;
+
+	mutex_lock_nested(&parent->mdio_lock, MDIO_MUTEX_NESTED);
+	ret = __mdiobus_c45_read(parent, addr, devad, regnum);
+	mutex_unlock(&parent->mdio_lock);
+
+	return ret;
+}
+
+static int en8811h_mcu_write_c45(struct mii_bus *bus, int addr, int devad,
+				 int regnum, u16 val)
+{
+	struct en8811h_mcu *mcu = bus->priv;
+	struct mii_bus *parent = mcu->mdiodev->bus;
+	int ret;
+
+	if (addr != mcu->mdiodev->addr)
+		return -ENODEV;
+
+	mutex_lock_nested(&parent->mdio_lock, MDIO_MUTEX_NESTED);
+	ret = __mdiobus_c45_write(parent, addr, devad, regnum, val);
+	mutex_unlock(&parent->mdio_lock);
+
+	return ret;
+}
+
+static int en8811h_mcu_bus_register(struct en8811h_mcu *mcu)
+{
+	struct device *dev = &mcu->mdiodev->dev;
+	struct mii_bus *parent = mcu->mdiodev->bus;
+	struct device_node *np;
+	struct mii_bus *bus;
+	int ret;
+
+	np = of_get_child_by_name(dev->of_node, "mdio");
+	if (!np) {
+		dev_err(dev, "no mdio node describing the PHY\n");
+		return -ENODEV;
+	}
+
+	bus = devm_mdiobus_alloc(dev);
+	if (!bus) {
+		of_node_put(np);
+		return -ENOMEM;
+	}
+
+	bus->name = "airoha-en8811h";
+	snprintf(bus->id, MII_BUS_ID_SIZE, "%s", dev_name(dev));
+	bus->priv = mcu;
+	bus->parent = dev;
+
+	if (parent->read) {
+		bus->read = en8811h_mcu_read;
+		bus->write = en8811h_mcu_write;
+	}
+	if (parent->read_c45) {
+		bus->read_c45 = en8811h_mcu_read_c45;
+		bus->write_c45 = en8811h_mcu_write_c45;
+	}
+
+	ret = devm_of_mdiobus_register(dev, bus, np);
+	of_node_put(np);
+
+	return ret;
+}
+
 static void en8811h_mcu_fw_poll(struct work_struct *work)
 {
 	struct en8811h_mcu *mcu = container_of(to_delayed_work(work),
@@ -60,6 +168,15 @@ static void en8811h_mcu_fw_poll(struct work_struct *work)
 	if (!ret) {
 		dev_dbg(dev, "firmware %08x running after %ums\n",
 			mcu->fw_version, mcu->waited_ms);
+
+		/* Unlike a missing firmware file, this does not resolve by
+		 * itself, so unlike the poll there is no retry: surface it
+		 * once and stop.
+		 */
+		ret = en8811h_mcu_bus_register(mcu);
+		if (ret)
+			dev_err(dev, "failed to register the PHY's bus: %pe\n",
+				ERR_PTR(ret));
 		return;
 	}
 
@@ -86,6 +203,7 @@ static int en8811h_mcu_probe(struct mdio_device *mdiodev)
 {
 	struct device *dev = &mdiodev->dev;
 	struct en8811h_mcu *mcu;
+	struct device_node *np;
 	u32 deassert_us = 0;
 
 	mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
@@ -95,6 +213,16 @@ static int en8811h_mcu_probe(struct mdio_device *mdiodev)
 	mcu->mdiodev = mdiodev;
 	mdiodev_set_drvdata(mdiodev, mcu);
 
+	/* The bus registration only needs this once the firmware runs, but
+	 * a DT hole should fail the bind now, not as a work-item error a
+	 * second after probe already returned success.
+	 */
+	np = of_get_child_by_name(dev->of_node, "mdio");
+	if (!np)
+		return dev_err_probe(dev, -ENODEV,
+				     "no mdio node describing the PHY\n");
+	of_node_put(np);
+
 	/*
 	 * The core only claims reset-gpios for devices flagged as PHYs
 	 * (mdiobus_register_device()), so claim it here. Owning it at this
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 8/9] net: phylink: wait for PHYs that are known to probe late
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (6 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  2026-08-29  5:25 ` [RFC PATCH net-next 9/9] net: phylink: report no link modes while a late PHY is missing Aleksei Sviridkin
  8 siblings, 0 replies; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

A PHY whose driver or firmware lives on a filesystem mounted after the
MAC probes cannot be connected when the port is set up, and the port is
lost for the rest of the uptime. Let a port say so with slow-to-probe,
and poll for the PHY instead of failing.

Returning 0 rather than -ENODEV matters beyond the error itself: DSA
reads -ENODEV as permission to go looking for the PHY on the switch's
internal MDIO bus, which is the wrong device.

Wait for a PHY whose own driver has finished probing, not merely for one
that exists. A PHY can be registered long before its driver is, because
the driver arrives when userspace gets around to loading it; connecting
in between would bind the generic driver, which cannot drive such a PHY
and leaves it worse off than not connecting at all.
phylink_sfp_connect_phy() already refuses a PHY with no driver for the
same reason. Binding alone is not enough either: phy_probe() publishes
phydev->drv before it calls the driver's probe, so a PHY caught inside
that window is validated against a supported mask that is still empty.
Note that this makes slow-to-probe a request for a specific driver: a
PHY meant to run on the generic driver must not sit behind the property,
because the wait would then never end.

A PHY arriving this late has missed phylink_start(), which starts the PHY
it finds already attached, so start it here when the port is up.

Keep polling after a failed connect, because the errno does not say
whether the failure is permanent: phylink_validate_phy() returns -EINVAL
both for a PHY that cannot speak the port's interface and for one whose
link modes are not in place yet, and only the PHY's driver binding again
changes that answer. Giving up would lose the port for good. Back the
interval off and report the first failure only, since a retry that
reaches bringup detaches the PHY again and phy_detach() asserts its
reset line.

The poller runs from a workqueue and needs rtnl, while
phylink_disconnect_phy() cancels it with rtnl already held, so it takes
rtnl with trylock and requeues on failure. For the same reason the
synchronous cancel belongs in phylink_destroy(), which documents that
rtnl must not be held.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/phy/phylink.c | 202 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 196 insertions(+), 6 deletions(-)

diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index 5b8e956902fb..eda7b61ba5d1 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -73,6 +73,24 @@ struct phylink {
 	struct phylink_link_state phy_state;
 	unsigned int phy_ib_mode;
 	struct work_struct resolve;
+	/* Set while waiting for a PHY that is expected to probe late.
+	 * Written and read under rtnl only: phylink_disconnect_phy() puts
+	 * and clears it there, which is what makes the poller's dereference
+	 * safe.
+	 */
+	struct fwnode_handle *slow_phy_fwnode;
+	u32 slow_phy_flags;
+	struct delayed_work slow_phy_poll;
+	/* Interval before the next poll. Set when the poller is armed and
+	 * thereafter owned by it, since a delayed work never runs twice at
+	 * once.
+	 */
+	unsigned int slow_phy_poll_ms;
+	unsigned int slow_phy_waited_ms;
+	/* Written under rtnl, read unlocked by the poller; a stale read only
+	 * costs an extra poll cycle.
+	 */
+	bool slow_phy_stop;
 	unsigned int pcs_neg_mode;
 	unsigned int pcs_state;
 
@@ -1829,6 +1847,8 @@ int phylink_set_fixed_link(struct phylink *pl,
 }
 EXPORT_SYMBOL_GPL(phylink_set_fixed_link);
 
+static void phylink_slow_phy_poll(struct work_struct *work);
+
 /**
  * phylink_create() - create a phylink instance
  * @config: a pointer to the target &struct phylink_config
@@ -1867,6 +1887,7 @@ struct phylink *phylink_create(struct phylink_config *config,
 	mutex_init(&pl->phydev_mutex);
 	mutex_init(&pl->state_mutex);
 	INIT_WORK(&pl->resolve, phylink_resolve);
+	INIT_DELAYED_WORK(&pl->slow_phy_poll, phylink_slow_phy_poll);
 
 	pl->config = config;
 	if (config->type == PHYLINK_NETDEV) {
@@ -1950,6 +1971,10 @@ void phylink_destroy(struct phylink *pl)
 	if (pl->link_gpio)
 		gpiod_put(pl->link_gpio);
 
+	WRITE_ONCE(pl->slow_phy_stop, true);
+	cancel_delayed_work_sync(&pl->slow_phy_poll);
+	fwnode_handle_put(pl->slow_phy_fwnode);
+
 	cancel_work_sync(&pl->resolve);
 	kfree(pl);
 }
@@ -2201,10 +2226,8 @@ static int phylink_bringup_phy(struct phylink *pl, struct phy_device *phy,
 }
 
 static int phylink_attach_phy(struct phylink *pl, struct phy_device *phy,
-			      phy_interface_t interface)
+			      phy_interface_t interface, u32 flags)
 {
-	u32 flags = 0;
-
 	if (WARN_ON(pl->cfg_link_an_mode == MLO_AN_FIXED))
 		return -EINVAL;
 
@@ -2242,7 +2265,7 @@ int phylink_connect_phy(struct phylink *pl, struct phy_device *phy)
 		pl->link_config.interface = pl->link_interface;
 	}
 
-	ret = phylink_attach_phy(pl, phy, pl->link_interface);
+	ret = phylink_attach_phy(pl, phy, pl->link_interface, 0);
 	if (ret < 0)
 		return ret;
 
@@ -2254,6 +2277,129 @@ int phylink_connect_phy(struct phylink *pl, struct phy_device *phy)
 }
 EXPORT_SYMBOL_GPL(phylink_connect_phy);
 
+/* How often to re-check whether a late-probing PHY has turned up. */
+#define PHYLINK_SLOW_PHY_POLL_MS	1000
+#define PHYLINK_SLOW_PHY_WARN_MS	60000
+
+/* Ceiling that interval backs off to once a connect attempt has failed. */
+#define PHYLINK_SLOW_PHY_POLL_MAX_MS	30000
+
+/* A phy-handle has resolved only once it names a device with its own driver
+ * bound. A device that is absent, or present with the generic driver bound to
+ * it, is equally unusable: this is the state slow-to-probe waits out, and both
+ * the arming site and the poller must agree on it.
+ *
+ * Bound is not probed. phy_probe() assigns phydev->drv before it calls the
+ * driver's own probe(), and only fills phydev->supported from get_features()
+ * once that has returned; a device connected in between is validated against
+ * an empty link-mode mask. PHY_DOWN is the documented state of a device whose
+ * probe has not completed, and phy_probe() leaves it in PHY_READY.
+ */
+static bool phylink_phy_is_usable(struct phy_device *phy_dev)
+{
+	return phy_dev && phy_dev->drv && phy_dev->state != PHY_DOWN &&
+	       !phy_driver_is_genphy(phy_dev);
+}
+
+static void phylink_slow_phy_poll(struct work_struct *work)
+{
+	struct phylink *pl = container_of(to_delayed_work(work), struct phylink,
+					  slow_phy_poll);
+	struct phy_device *phy_dev;
+	int ret;
+
+	if (READ_ONCE(pl->slow_phy_stop))
+		return;
+
+	/* phylink_disconnect_phy() cancels this work while holding rtnl, so
+	 * blocking on rtnl here would deadlock against it. Requeue instead and
+	 * let the canceller finish.
+	 */
+	if (!rtnl_trylock())
+		goto requeue;
+
+	if (READ_ONCE(pl->slow_phy_stop)) {
+		rtnl_unlock();
+		return;
+	}
+
+	/* Do not "optimize" the lookup out from under rtnl: rtnl is what
+	 * makes phylink_disconnect_phy()'s put-and-clear of slow_phy_fwnode
+	 * safe against this dereference. The uncontended trylock above is
+	 * cheaper than the use-after-free.
+	 */
+	phy_dev = fwnode_phy_find_device(pl->slow_phy_fwnode);
+	if (!phylink_phy_is_usable(phy_dev)) {
+		if (phy_dev)
+			phy_device_free(phy_dev);
+
+		/* A PHY that never turns up would otherwise wait in complete
+		 * silence: one breadcrumb after a minute, like the connect
+		 * failure below gets, and like a missing firmware file gets
+		 * from the MDIO side.
+		 */
+		pl->slow_phy_waited_ms += pl->slow_phy_poll_ms;
+		if (pl->slow_phy_waited_ms >= PHYLINK_SLOW_PHY_WARN_MS &&
+		    pl->slow_phy_waited_ms - pl->slow_phy_poll_ms <
+		    PHYLINK_SLOW_PHY_WARN_MS)
+			phylink_warn(pl,
+				     "still waiting for the slow-to-probe PHY (%pfw)\n",
+				     pl->slow_phy_fwnode);
+		rtnl_unlock();
+		goto requeue;
+	}
+
+	if (pl->link_interface == PHY_INTERFACE_MODE_NA) {
+		pl->link_interface = phy_dev->interface;
+		pl->link_config.interface = pl->link_interface;
+	}
+
+	ret = phylink_attach_phy(pl, phy_dev, pl->link_interface,
+				 pl->slow_phy_flags);
+	phy_device_free(phy_dev);
+	if (!ret) {
+		ret = phylink_bringup_phy(pl, phy_dev,
+					  pl->link_config.interface);
+		if (ret)
+			phy_detach(phy_dev);
+		else if (!test_bit(PHYLINK_DISABLE_STOPPED,
+				   &pl->phylink_disable_state))
+			/* phylink_start() starts the PHY it finds attached,
+			 * and ran long before this one turned up.
+			 */
+			phy_start(phy_dev);
+	}
+	if (ret) {
+		/* Whether this is permanent cannot be read off the errno: the
+		 * same -EINVAL comes out of phylink_validate_phy() for a PHY
+		 * that is still filling in its link modes and for one that
+		 * genuinely cannot speak this interface. What decides it is
+		 * the PHY's supported mask, and only the driver binding again
+		 * changes that, so keep polling - giving up here loses the
+		 * port for the lifetime of this phylink. Back off instead: a
+		 * failed bringup ends in phy_detach(), which asserts the PHY's
+		 * reset line. Report the first failure only, so a PHY that
+		 * will never fit costs one line rather than a stream of them.
+		 */
+		if (pl->slow_phy_poll_ms == PHYLINK_SLOW_PHY_POLL_MS)
+			phylink_err(pl, "failed to connect late PHY: %pe\n",
+				    ERR_PTR(ret));
+
+		WRITE_ONCE(pl->slow_phy_poll_ms,
+			   min_t(unsigned int, pl->slow_phy_poll_ms * 2,
+				 PHYLINK_SLOW_PHY_POLL_MAX_MS));
+	}
+	rtnl_unlock();
+
+	if (!ret)
+		return;
+
+requeue:
+	queue_delayed_work(system_freezable_power_efficient_wq,
+			   &pl->slow_phy_poll,
+			   msecs_to_jiffies(READ_ONCE(pl->slow_phy_poll_ms)));
+}
+
 /**
  * phylink_of_phy_connect() - connect the PHY specified in the DT mode.
  * @pl: a pointer to a &struct phylink returned from phylink_create()
@@ -2282,7 +2428,13 @@ EXPORT_SYMBOL_GPL(phylink_of_phy_connect);
  * Connect the phy specified @fwnode to the phylink instance specified
  * by @pl.
  *
- * Returns 0 on success or a negative errno.
+ * If the port node carries the slow-to-probe property and the PHY is not
+ * usable yet, 0 is returned with no PHY connected: a poller connects it
+ * once its driver has probed. Until then the MAC runs without a PHY and
+ * ethtool reports no link modes.
+ *
+ * Returns 0 on success - the PHY connected, or the deferred connect
+ * armed - or a negative errno.
  */
 int phylink_fwnode_phy_connect(struct phylink *pl,
 			       const struct fwnode_handle *fwnode,
@@ -2304,6 +2456,32 @@ int phylink_fwnode_phy_connect(struct phylink *pl,
 	}
 
 	phy_dev = fwnode_phy_find_device(phy_fwnode);
+	if (!phylink_phy_is_usable(phy_dev) &&
+	    fwnode_property_present(fwnode, "slow-to-probe")) {
+		/* The PHY is known to appear late, so keep the node and poll
+		 * for it rather than failing the port for good. Returning an
+		 * error here would also send DSA off to look for the PHY on
+		 * the switch's own MDIO bus.
+		 */
+		if (phy_dev)
+			phy_device_free(phy_dev);
+
+		pl->slow_phy_fwnode = phy_fwnode;
+		pl->slow_phy_flags = flags;
+		WRITE_ONCE(pl->slow_phy_poll_ms, PHYLINK_SLOW_PHY_POLL_MS);
+		pl->slow_phy_waited_ms = 0;
+		WRITE_ONCE(pl->slow_phy_stop, false);
+		/* mod_ rather than queue_: a poll that a disconnect cancelled
+		 * mid-backoff may still be pending with seconds left on it.
+		 */
+		/* Freezable: attaching and starting a PHY has no place in
+		 * the middle of a system suspend transition.
+		 */
+		mod_delayed_work(system_freezable_power_efficient_wq,
+				 &pl->slow_phy_poll, 0);
+		return 0;
+	}
+
 	/* We're done with the phy_node handle */
 	fwnode_handle_put(phy_fwnode);
 	if (!phy_dev)
@@ -2345,6 +2523,18 @@ void phylink_disconnect_phy(struct phylink *pl)
 
 	ASSERT_RTNL();
 
+	/* Cannot cancel synchronously: the poller takes rtnl, which is held. */
+	WRITE_ONCE(pl->slow_phy_stop, true);
+	cancel_delayed_work(&pl->slow_phy_poll);
+	/* The next connect re-arms from its own fwnode lookup; holding on to
+	 * this one would leak a reference per connect cycle and keep the
+	 * pending state - and its empty-ksettings window - alive on an
+	 * interface whose PHY is long since usable. The poller cannot race
+	 * this: it reads the node only under rtnl, after the stop check.
+	 */
+	fwnode_handle_put(pl->slow_phy_fwnode);
+	pl->slow_phy_fwnode = NULL;
+
 	mutex_lock(&pl->phydev_mutex);
 	phy = pl->phydev;
 	if (phy) {
@@ -3730,7 +3920,7 @@ static int phylink_sfp_config_phy(struct phylink *pl, struct phy_device *phy)
 	/* Attach the PHY so that the PHY is present when we do the major
 	 * configuration step.
 	 */
-	ret = phylink_attach_phy(pl, phy, config.interface);
+	ret = phylink_attach_phy(pl, phy, config.interface, 0);
 	if (ret < 0)
 		return ret;
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* [RFC PATCH net-next 9/9] net: phylink: report no link modes while a late PHY is missing
  2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
                   ` (7 preceding siblings ...)
  2026-08-29  5:25 ` [RFC PATCH net-next 8/9] net: phylink: wait for PHYs that are known to probe late Aleksei Sviridkin
@ 2026-08-29  5:25 ` Aleksei Sviridkin
  8 siblings, 0 replies; 15+ messages in thread
From: Aleksei Sviridkin @ 2026-08-29  5:25 UTC (permalink / raw)
  To: netdev
  Cc: andrew, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel, Aleksei Sviridkin

A port waiting for its PHY reported the MAC's full set of link modes and
accepted settings for them, which describes a link that cannot come up.
Report an empty set and refuse to configure, matching what an empty SFP
cage already does.

The EEE calls need no such guard: they already return -EOPNOTSUPP when no
PHY is attached.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
 drivers/net/phy/phylink.c | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index eda7b61ba5d1..6fbd1fa0be78 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -2284,6 +2284,12 @@ EXPORT_SYMBOL_GPL(phylink_connect_phy);
 /* Ceiling that interval backs off to once a connect attempt has failed. */
 #define PHYLINK_SLOW_PHY_POLL_MAX_MS	30000
 
+/* True while a PHY declared slow-to-probe has not been connected yet. */
+static bool phylink_slow_phy_pending(struct phylink *pl)
+{
+	return pl->slow_phy_fwnode && !pl->phydev;
+}
+
 /* A phy-handle has resolved only once it names a device with its own driver
  * bound. A device that is absent, or present with the generic driver bound to
  * it, is equally unusable: this is the state slow-to-probe waits out, and both
@@ -3106,6 +3112,17 @@ int phylink_ethtool_ksettings_get(struct phylink *pl,
 	else
 		kset->base.port = pl->link_port;
 
+	/* Until the PHY arrives the port can do nothing, so report no link
+	 * modes at all rather than the MAC's own capabilities, which is what
+	 * an empty SFP cage reports.
+	 */
+	if (phylink_slow_phy_pending(pl)) {
+		linkmode_zero(kset->link_modes.supported);
+		kset->base.speed = SPEED_UNKNOWN;
+		kset->base.duplex = DUPLEX_UNKNOWN;
+		return 0;
+	}
+
 	linkmode_copy(kset->link_modes.supported, pl->supported);
 
 	switch (pl->act_link_an_mode) {
@@ -3173,6 +3190,12 @@ int phylink_ethtool_ksettings_set(struct phylink *pl,
 
 	ASSERT_RTNL();
 
+	/* Without a PHY and without an SFP bus this would configure the MAC
+	 * on its own, for a link that cannot come up yet.
+	 */
+	if (phylink_slow_phy_pending(pl))
+		return -EINVAL;
+
 	if (pl->phydev) {
 		struct ethtool_link_ksettings phy_kset = *kset;
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe
  2026-08-29  5:25 ` [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe Aleksei Sviridkin
@ 2026-09-04  0:29   ` Andrew Lunn
  0 siblings, 0 replies; 15+ messages in thread
From: Andrew Lunn @ 2026-09-04  0:29 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: netdev, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel

On Sat, Aug 29, 2026 at 05:25:39AM +0000, Aleksei Sviridkin wrote:
> A port can reference a PHY whose driver is not usable at connect
> time because the PHY's firmware, or the module carrying its driver,
> lives in a filesystem that is not mounted yet when the controller
> probes. Today such a port is dropped at setup and stays unusable
> for the whole uptime even though the PHY becomes fully functional
> seconds later.
> 
> Add a boolean the port node can carry to declare this expected:
> the connect keeps the port and attaches the PHY once it becomes
> usable instead of failing.

That sentence does not parse for me. I have problems at the :

> +  slow-to-probe:

Maybe this should include what entity is slow to probe, since this is
in the MAC node, but it is not the MAC which is slower. We might also
get the situation that the PCS needs firmware and is also slow to
probe?

	Andrew

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address
  2026-08-29  5:25 ` [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address Aleksei Sviridkin
@ 2026-09-04  0:48   ` Andrew Lunn
  0 siblings, 0 replies; 15+ messages in thread
From: Andrew Lunn @ 2026-09-04  0:48 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: netdev, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel

On Sat, Aug 29, 2026 at 05:25:40AM +0000, Aleksei Sviridkin wrote:
> The buckpbus accessors only need an MDIO bus and an address, but they
> take a phy_device, which ties them to a probed PHY. An upcoming MDIO
> device driver needs the same register access before any phy_device
> exists, since it runs precisely to make the PHY presentable.
> 
> Retype the internal helpers onto (mii_bus, addr) and keep the exported
> phy_device API as page-selecting wrappers around them. The file
> already carries an mdio_device-typed accessor for the AN8811HB pbus,
> so this follows an existing direction rather than opening a new one.
> 
> No functional change.
> 
> Assisted-by: LLM
> Signed-off-by: Aleksei Sviridkin <f@lex.la>
> ---
>  drivers/net/phy/air_phy_lib.c | 85 +++++++++++++++++++----------------
>  1 file changed, 46 insertions(+), 39 deletions(-)
> 
> diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
> index 5141db19fa5e..e0fca5f285d2 100644
> --- a/drivers/net/phy/air_phy_lib.c
> +++ b/drivers/net/phy/air_phy_lib.c
> @@ -14,31 +14,32 @@
>  
>  #include "air_phy_lib.h"
>  
> -static int __air_buckpbus_reg_read(struct phy_device *phydev,
> +static int __air_buckpbus_reg_read(struct mii_bus *bus, int addr,
>  				   u32 pbus_address, u32 *pbus_data)
>  {
>  	int pbus_data_low, pbus_data_high;
>  	int ret;
>  
> -	ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_FIXED);
> +	ret = __mdiobus_write(bus, addr, AIR_BPBUS_MODE,
> +			      AIR_BPBUS_MODE_ADDR_FIXED);

We have mdiodev_read() and mdiodev_write:

static inline int mdiodev_read(struct mdio_device *mdiodev, u32 regnum)
{
	return mdiobus_read(mdiodev->bus, mdiodev->addr, regnum);
}

static inline int mdiodev_write(struct mdio_device *mdiodev, u32 regnum,
				u16 val)
{
	return mdiobus_write(mdiodev->bus, mdiodev->addr, regnum, val);
}

A PHY is a superset of an mdiodev....

struct phy_device {
	struct mdio_device mdio;

	/* Information about the PHY type */
	/* And management functions */
	const struct phy_driver *drv;

So it might look better to add __mdiodev_read()/__mdiodev_write(), and
have the PHY driver pass &phydev->mdio, and the firmware download
driver can directly use its mdiodev.

	Andrew

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library
  2026-08-29  5:25 ` [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
@ 2026-09-04  1:16   ` Andrew Lunn
  0 siblings, 0 replies; 15+ messages in thread
From: Andrew Lunn @ 2026-09-04  1:16 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: netdev, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel

>  static int an8811hb_check_crc(struct phy_device *phydev, u32 set1,
> @@ -405,7 +319,8 @@ static int an8811hb_load_file(struct phy_device *phydev, const char *name,
>  	if (ret < 0)
>  		return ret;
>  
> -	ret = air_write_buf(phydev, address,  fw);
> +	ret = air_fw_write_buf(phydev->mdio.bus, phydev->mdio.addr, address,
> +			       fw);

This can probably become:

     air_fw_write_buf(&phydev->mdio, address, fw);

> +int air_fw_write_buf(struct mii_bus *bus, int addr, u32 address,
> +		     const struct firmware *fw)
> +{
> +	int saved_page, ret;
> +
> +	mutex_lock(&bus->mdio_lock);

If you are adding __mdiodev_read(), it makes sense to also add
mdiodev_lock()/mdiodev_unlock().

> +static int air_mmd_status_read(struct mii_bus *bus, int addr, bool is_c45)
> +{
> +	int ret;
> +
> +	mutex_lock(&bus->mdio_lock);
> +	ret = mmd_phy_read(bus, addr, is_c45, MDIO_MMD_VEND1,
> +			   EN8811H_PHY_FW_STATUS);
> +	mutex_unlock(&bus->mdio_lock);
> +
> +	return ret;
> +}

Is C45 over C22 required here? At least when downloading the actual
firmware data, you don't want to be doing C45 over C22 if you can
avoid it. So maybe mdiodev_c45_read()?

      Andrew

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver
  2026-08-29  5:25 ` [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
@ 2026-09-04  1:36   ` Andrew Lunn
  0 siblings, 0 replies; 15+ messages in thread
From: Andrew Lunn @ 2026-09-04  1:36 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: netdev, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel

> +
> +#include "../phy/air_phy_lib.h"


We try to avoid such paths. Please use include/net/phy/

> +static void en8811h_mcu_fw_poll(struct work_struct *work)
> +{
> +	struct en8811h_mcu *mcu = container_of(to_delayed_work(work),
> +					       struct en8811h_mcu, fw_poll);
> +	struct device *dev = &mcu->mdiodev->dev;
> +	int ret;
> +
> +	/* The chip enumerates as a C22 PHY; MMD access is indirect */

This is an mdiodev. It is not enumerated. It is explicitly listed as a
device on the bus with a compatible.

C45 over C22 is a PHY concept, not an mdio device concept. In this
case, this mdio device is special and does implement C45 over C22. But
it is up to use to this driver to decided if you want to use it. I
would probably look as bus->read_c45, and if it is not NULL use
direct.

I wounder if there are any boards using this devices which are not
capable of direct?

> +	mcu->waited_ms += mcu->poll_ms;
> +	if (!mcu->warned && mcu->waited_ms >= EN8811H_FW_WARN_MS) {
> +		/* Missing files resolve by themselves once installed; a bus
> +		 * or register error will not, and deserves its own message.
> +		 */
> +		if (ret == -ENOENT)
> +			dev_warn(dev, "still waiting for %s and %s\n",
> +				 EN8811H_MD32_DM, EN8811H_MD32_DSP);
> +		else
> +			dev_warn(dev, "firmware download keeps failing: %pe\n",
> +				 ERR_PTR(ret));

dev_warn_once().

	Andrew

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus
  2026-08-29  5:25 ` [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
@ 2026-09-04  1:43   ` Andrew Lunn
  0 siblings, 0 replies; 15+ messages in thread
From: Andrew Lunn @ 2026-09-04  1:43 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: netdev, hkallweit1, linux, davem, edumazet, kuba, pabeni, robh,
	krzk+dt, conor+dt, devicetree, linux-kernel

> +	bus->name = "airoha-en8811h";
> +	snprintf(bus->id, MII_BUS_ID_SIZE, "%s", dev_name(dev));
> +	bus->priv = mcu;
> +	bus->parent = dev;

Maybe set bus->phy_mask.

Also, copy parent->irq[addr] to bus->irq[addr], so that interrupts
work.

	Andrew

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-09-04  1:43 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-29  5:25 [RFC PATCH net-next 0/9] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
2026-08-29  5:25 ` [RFC PATCH net-next 1/9] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
2026-08-29  5:25 ` [RFC PATCH net-next 2/9] dt-bindings: net: ethernet-controller: add slow-to-probe Aleksei Sviridkin
2026-09-04  0:29   ` Andrew Lunn
2026-08-29  5:25 ` [RFC PATCH net-next 3/9] net: phy: air: type the buckpbus core on the bus and address Aleksei Sviridkin
2026-09-04  0:48   ` Andrew Lunn
2026-08-29  5:25 ` [RFC PATCH net-next 4/9] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
2026-09-04  1:16   ` Andrew Lunn
2026-08-29  5:25 ` [RFC PATCH net-next 5/9] net: phy: air: skip the download when the MD32 is already running Aleksei Sviridkin
2026-08-29  5:25 ` [RFC PATCH net-next 6/9] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
2026-09-04  1:36   ` Andrew Lunn
2026-08-29  5:25 ` [RFC PATCH net-next 7/9] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
2026-09-04  1:43   ` Andrew Lunn
2026-08-29  5:25 ` [RFC PATCH net-next 8/9] net: phylink: wait for PHYs that are known to probe late Aleksei Sviridkin
2026-08-29  5:25 ` [RFC PATCH net-next 9/9] net: phylink: report no link modes while a late PHY is missing Aleksei Sviridkin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox