* [RFC PATCH net-next v2 01/10] dt-bindings: net: add Airoha EN8811H PHY MCU
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
@ 2026-09-04 19:02 ` Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 02/10] dt-bindings: net: ethernet-controller: add phy-needs-host-firmware Aleksei Sviridkin
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:02 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 | 96 +++++++++++++++++++
1 file changed, 96 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..99a494f6ca1e
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
@@ -0,0 +1,96 @@
+# 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 built around an MD32 core that executes
+ firmware from volatile RAM. The chip answers an MDIO address from
+ power-on, but the PHY function does not exist until firmware has been
+ loaded over MDIO, and it disappears again if that RAM is cleared.
+
+ This node describes the MD32 core. The PHY function answers on a child
+ MDIO bus below it, at the same address the core occupies on the parent
+ bus, and only while firmware is running.
+
+ The reset line is wired to the core, and asserting it clears the
+ firmware RAM. It therefore belongs to this node: a reset described on
+ the PHY node would be pulsed by that node's own lifecycle, whatever
+ the core is doing.
+
+ A board that has firmware in hand while its MDIO bus is enumerated can
+ describe the PHY on its own, as airoha,en8811h.yaml does, and that form
+ stays valid. This binding is for the boards where firmware arrives
+ later, so that there is no PHY to describe until the core has it.
+
+properties:
+ compatible:
+ const: airoha,en8811h-mcu
+
+ reg:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ The GPIO phandle and specifier for the reset signal wired to the
+ MD32 core.
+
+ reset-assert-us:
+ description:
+ Delay after the reset was asserted in microseconds. If this
+ property is missing the delay will be skipped.
+
+ reset-deassert-us:
+ description:
+ Delay after the reset was deasserted in microseconds. If this
+ property is missing the delay will be skipped.
+
+ mdio:
+ $ref: mdio.yaml#
+ unevaluatedProperties: false
+ description:
+ The bus the PHY function answers on while firmware is running, at
+ the same address this node occupies on the parent bus.
+
+required:
+ - compatible
+ - reg
+ - mdio
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ mdio {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-phy-mcu@d {
+ compatible = "airoha,en8811h-mcu";
+ reg = <0xd>;
+ reset-gpios = <&pio 14 GPIO_ACTIVE_LOW>;
+ 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 IRQ_TYPE_LEVEL_LOW>;
+ };
+ };
+ };
+ };
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 02/10] dt-bindings: net: ethernet-controller: add phy-needs-host-firmware
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 01/10] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
@ 2026-09-04 19:02 ` Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 03/10] net: phy: add mdiodev_lock() and mdiodev_unlock() Aleksei Sviridkin
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:02 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
A port can reference a PHY that cannot be identified or driven when
the controller connects to it, because the firmware the chip needs is
not in hand yet. Such a port is dropped at setup and stays unusable for
the whole uptime, even though the PHY works seconds later.
The flag declares that the PHY behind phy-handle is expected to become
usable only after this controller has probed. A consumer that sees it
keeps the port and connects the PHY once its driver binds, instead of
failing the connect. The prefix names the entity, like the other
properties here that name what they describe rather than the
controller; a PCS needing firmware would get pcs-needs-host-firmware
by the same rule.
It sits on the controller here because that is where the waiting
happens.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
.../devicetree/bindings/net/ethernet-controller.yaml | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/Documentation/devicetree/bindings/net/ethernet-controller.yaml b/Documentation/devicetree/bindings/net/ethernet-controller.yaml
index 1bafd687dcb1..ce4a7a6f605f 100644
--- a/Documentation/devicetree/bindings/net/ethernet-controller.yaml
+++ b/Documentation/devicetree/bindings/net/ethernet-controller.yaml
@@ -108,6 +108,13 @@ properties:
$ref: "#/properties/phy-handle"
deprecated: true
+ phy-needs-host-firmware:
+ $ref: /schemas/types.yaml#/definitions/flag
+ description:
+ The PHY referenced by phy-handle runs firmware that the host must
+ load before the PHY can be driven, and so is not usable for as
+ long as that has not happened.
+
ptp-timer:
$ref: /schemas/types.yaml#/definitions/phandle
description:
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 03/10] net: phy: add mdiodev_lock() and mdiodev_unlock()
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 01/10] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 02/10] dt-bindings: net: ethernet-controller: add phy-needs-host-firmware Aleksei Sviridkin
@ 2026-09-04 19:02 ` Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 04/10] net: phy: air: type the buckpbus core on the mdio device Aleksei Sviridkin
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:02 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
The __mdiodev_* accessors require the caller to hold the MDIO bus
lock, but the wrappers that take it, phy_lock_mdio_bus() and
phy_unlock_mdio_bus(), need a phy_device. A driver bound to a plain
MDIO device has none, and must reach into mdiodev->bus->mdio_lock
itself.
They sit in phy.h beside those wrappers because struct mii_bus is
defined here, not in mdio.h where the accessors live.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
include/linux/phy.h | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/include/linux/phy.h b/include/linux/phy.h
index 3d8afe6b7f1c..8c10c8ae21b1 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -2284,6 +2284,16 @@ static inline void phy_unlock_mdio_bus(struct phy_device *phydev)
mutex_unlock(&phydev->mdio.bus->mdio_lock);
}
+static inline void mdiodev_lock(struct mdio_device *mdiodev)
+{
+ mutex_lock(&mdiodev->bus->mdio_lock);
+}
+
+static inline void mdiodev_unlock(struct mdio_device *mdiodev)
+{
+ mutex_unlock(&mdiodev->bus->mdio_lock);
+}
+
void phy_attached_print(struct phy_device *phydev, const char *fmt, ...)
__printf(2, 3);
char *phy_attached_info_irq(struct phy_device *phydev)
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 04/10] net: phy: air: type the buckpbus core on the mdio device
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (2 preceding siblings ...)
2026-09-04 19:02 ` [RFC PATCH net-next v2 03/10] net: phy: add mdiodev_lock() and mdiodev_unlock() Aleksei Sviridkin
@ 2026-09-04 19:02 ` Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 05/10] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:02 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
The buckpbus accessors only need an MDIO device, 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 an mdio_device and keep the exported
phy_device API as page-selecting wrappers that pass &phydev->mdio. The
EN8811H driver already reaches the AN8811HB pbus through an
mdio_device, 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 | 80 ++++++++++++++++++-----------------
1 file changed, 42 insertions(+), 38 deletions(-)
diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
index 5141db19fa5e..e314717843ae 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 mdio_device *mdiodev,
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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_read(mdiodev, 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 = __mdiodev_read(mdiodev, 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 mdio_device *mdiodev,
u32 pbus_address, u32 pbus_data)
{
int ret;
- ret = __phy_write(phydev, AIR_BPBUS_MODE, AIR_BPBUS_MODE_ADDR_FIXED);
+ ret = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 mdio_device *mdiodev,
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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_read(mdiodev, 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 = __mdiodev_read(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, AIR_BPBUS_WR_DATA_LOW,
+ lower_16_bits(pbus_data_new));
if (ret < 0)
return ret;
@@ -144,7 +147,8 @@ 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, pbus_address,
+ pbus_data);
if (ret < 0)
phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
pbus_address, ret);
@@ -163,7 +167,7 @@ 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,
+ ret = __air_buckpbus_reg_write(&phydev->mdio, pbus_address,
pbus_data);
if (ret < 0)
phydev_err(phydev, "%s 0x%08x failed: %d\n", __func__,
@@ -183,8 +187,8 @@ 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, 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] 11+ messages in thread* [RFC PATCH net-next v2 05/10] net: phy: air: move the EN8811H firmware download into the library
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (3 preceding siblings ...)
2026-09-04 19:02 ` [RFC PATCH net-next v2 04/10] net: phy: air: type the buckpbus core on the mdio device Aleksei Sviridkin
@ 2026-09-04 19:02 ` Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 06/10] net: phy: air: skip the download when the MD32 is already running Aleksei Sviridkin
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:02 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 MDIO
device like the buckpbus core. The moved helpers take the bus lock and
save and restore the page register themselves, since phy_select_page()
needs a phy_device.
The status poll now asks for the MMD register through the Clause 22
indirection unconditionally. phy_read_mmd() used to resolve that from
phydev->is_c45, which is false for every device tree describing the
Clause 22 PHYs this library serves, so hardcoding it changes nothing
any board runs today.
The version read at the end is now checked. en8811h_print_fw_version()
threw its result away, so a bus error there left the version at zero
and let probe continue; the load fails instead.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
drivers/net/phy/air_en8811h.c | 139 +-----------------
drivers/net/phy/air_phy_lib.c | 260 ++++++++++++++++++++++++++++++++++
drivers/net/phy/air_phy_lib.h | 24 ++++
3 files changed, 288 insertions(+), 135 deletions(-)
diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
index 38eb18f0e9bb..2f6ed30ba24d 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,9 @@ 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);
}
static int an8811hb_check_crc(struct phy_device *phydev, u32 set1,
@@ -405,7 +318,7 @@ 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, address, fw);
release_firmware(fw);
return ret;
}
@@ -501,54 +414,10 @@ 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, &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 e314717843ae..2f3e5b71abac 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 mdio_device *mdiodev,
u32 pbus_address, u32 *pbus_data)
@@ -198,6 +203,261 @@ 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 mdio_device *mdiodev, u32 address,
+ const struct firmware *fw)
+{
+ unsigned int offset;
+ int ret;
+ u16 val;
+
+ ret = __mdiodev_write(mdiodev, AIR_BPBUS_MODE,
+ AIR_BPBUS_MODE_ADDR_INCR);
+ if (ret < 0)
+ return ret;
+
+ ret = __mdiodev_write(mdiodev, AIR_BPBUS_WR_ADDR_HIGH,
+ upper_16_bits(address));
+ if (ret < 0)
+ return ret;
+
+ ret = __mdiodev_write(mdiodev, 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 = __mdiodev_write(mdiodev, AIR_BPBUS_WR_DATA_HIGH, val);
+ if (ret < 0)
+ return ret;
+
+ val = get_unaligned_le16(&fw->data[offset]);
+ ret = __mdiodev_write(mdiodev, AIR_BPBUS_WR_DATA_LOW, val);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* phy_select_page() needs a phy_device, which does not exist yet. */
+static int __air_mdio_select_page(struct mdio_device *mdiodev, int page)
+{
+ int saved_page, ret;
+
+ saved_page = __mdiodev_read(mdiodev, AIR_EXT_PAGE_ACCESS);
+ if (saved_page < 0)
+ return saved_page;
+
+ if (saved_page != page) {
+ ret = __mdiodev_write(mdiodev, AIR_EXT_PAGE_ACCESS, page);
+ if (ret < 0)
+ return ret;
+ }
+
+ return saved_page;
+}
+
+static int __air_mdio_restore_page(struct mdio_device *mdiodev,
+ int saved_page, int page, int ret)
+{
+ int restore;
+
+ if (saved_page != page) {
+ restore = __mdiodev_write(mdiodev, AIR_EXT_PAGE_ACCESS,
+ saved_page);
+ if (ret >= 0 && restore < 0)
+ ret = restore;
+ }
+
+ return ret;
+}
+
+int air_fw_write_buf(struct mdio_device *mdiodev, u32 address,
+ const struct firmware *fw)
+{
+ int saved_page, ret;
+
+ mdiodev_lock(mdiodev);
+
+ saved_page = __air_mdio_select_page(mdiodev, AIR_PHY_PAGE_EXTENDED_4);
+ if (saved_page < 0) {
+ ret = saved_page;
+ } else {
+ ret = __air_write_buf(mdiodev, address, fw);
+ ret = __air_mdio_restore_page(mdiodev, saved_page,
+ AIR_PHY_PAGE_EXTENDED_4, ret);
+ }
+
+ mdiodev_unlock(mdiodev);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(air_fw_write_buf);
+
+static int air_mdio_buckpbus_reg_read(struct mdio_device *mdiodev,
+ u32 pbus_address, u32 *pbus_data)
+{
+ int saved_page, ret;
+
+ mdiodev_lock(mdiodev);
+
+ saved_page = __air_mdio_select_page(mdiodev, AIR_PHY_PAGE_EXTENDED_4);
+ if (saved_page < 0) {
+ ret = saved_page;
+ } else {
+ ret = __air_buckpbus_reg_read(mdiodev, pbus_address, pbus_data);
+ ret = __air_mdio_restore_page(mdiodev, saved_page,
+ AIR_PHY_PAGE_EXTENDED_4, ret);
+ }
+
+ mdiodev_unlock(mdiodev);
+ return ret;
+}
+
+static int air_mdio_buckpbus_reg_write(struct mdio_device *mdiodev,
+ u32 pbus_address, u32 pbus_data)
+{
+ int saved_page, ret;
+
+ mdiodev_lock(mdiodev);
+
+ saved_page = __air_mdio_select_page(mdiodev, AIR_PHY_PAGE_EXTENDED_4);
+ if (saved_page < 0) {
+ ret = saved_page;
+ } else {
+ ret = __air_buckpbus_reg_write(mdiodev, pbus_address,
+ pbus_data);
+ ret = __air_mdio_restore_page(mdiodev, saved_page,
+ AIR_PHY_PAGE_EXTENDED_4, ret);
+ }
+
+ mdiodev_unlock(mdiodev);
+ return ret;
+}
+
+static int air_mdio_buckpbus_reg_modify(struct mdio_device *mdiodev,
+ u32 pbus_address, u32 mask, u32 set)
+{
+ int saved_page, ret;
+
+ mdiodev_lock(mdiodev);
+
+ saved_page = __air_mdio_select_page(mdiodev, AIR_PHY_PAGE_EXTENDED_4);
+ if (saved_page < 0) {
+ ret = saved_page;
+ } else {
+ ret = __air_buckpbus_reg_modify(mdiodev, pbus_address,
+ mask, set);
+ ret = __air_mdio_restore_page(mdiodev, saved_page,
+ AIR_PHY_PAGE_EXTENDED_4, ret);
+ }
+
+ mdiodev_unlock(mdiodev);
+ return ret;
+}
+
+static int air_mmd_status_read(struct mdio_device *mdiodev)
+{
+ int ret;
+
+ mdiodev_lock(mdiodev);
+ ret = mmd_phy_read(mdiodev->bus, mdiodev->addr, false,
+ MDIO_MMD_VEND1, EN8811H_PHY_FW_STATUS);
+ mdiodev_unlock(mdiodev);
+
+ return ret;
+}
+
+int air_en8811h_wait_mcu_ready(struct mdio_device *mdiodev)
+{
+ int ret, reg_value;
+
+ ret = air_mdio_buckpbus_reg_write(mdiodev, 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, mdiodev);
+ if (reg_value < 0)
+ return reg_value;
+ if (ret) {
+ dev_err(&mdiodev->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 mdio_device *mdiodev, u32 *fw_version)
+{
+ const struct firmware *fw1, *fw2;
+ struct device *dev = &mdiodev->dev;
+ 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(mdiodev, EN8811H_FW_CTRL_1,
+ EN8811H_FW_CTRL_1_START);
+ if (ret < 0)
+ goto air_fw_download_out;
+
+ ret = air_mdio_buckpbus_reg_modify(mdiodev, 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(mdiodev, AIR_FW_ADDR_DM, fw1);
+ if (ret < 0)
+ goto air_fw_download_out;
+
+ ret = air_fw_write_buf(mdiodev, AIR_FW_ADDR_DSP, fw2);
+ if (ret < 0)
+ goto air_fw_download_out;
+
+ ret = air_mdio_buckpbus_reg_modify(mdiodev, EN8811H_FW_CTRL_2,
+ EN8811H_FW_CTRL_2_LOADING, 0);
+ if (ret < 0)
+ goto air_fw_download_out;
+
+ ret = air_en8811h_wait_mcu_ready(mdiodev);
+ if (ret < 0)
+ goto air_fw_download_out;
+
+ ret = air_mdio_buckpbus_reg_read(mdiodev, 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: callers log on their own terms, and a poller
+ * would repeat it on every retry.
+ */
+ return ret;
+}
+EXPORT_SYMBOL_GPL(air_en8811h_fw_download);
+
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..2a4c156d6396 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,11 @@ 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 mdio_device *mdiodev, u32 address,
+ const struct firmware *fw);
+int air_en8811h_wait_mcu_ready(struct mdio_device *mdiodev);
+int air_en8811h_fw_download(struct mdio_device *mdiodev, u32 *fw_version);
+
#endif /* __AIR_PHY_LIB_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 06/10] net: phy: air: skip the download when the MD32 is already running
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (4 preceding siblings ...)
2026-09-04 19:02 ` [RFC PATCH net-next v2 05/10] net: phy: air: move the EN8811H firmware download into the library Aleksei Sviridkin
@ 2026-09-04 19:03 ` Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 07/10] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:03 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 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. Adopting is reported back distinctly from loading,
because nothing on that path touches FW_CTRL_1: the PHY driver must
leave the MCU restart to .config_init() rather than assume probe
already did it.
The image in RAM now wins over the files on disk. Where the PHY node
carries no reset-gpios nothing clears that RAM on unbind, so writing
new firmware files and rebinding keeps the old image running and
reports its version as the current one. A power cycle, or a reset line
on the PHY node for phy_detach() to assert, brings the reload back.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
drivers/net/phy/air_en8811h.c | 4 ++--
drivers/net/phy/air_phy_lib.c | 17 +++++++++++++++++
drivers/net/phy/air_phy_lib.h | 2 ++
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
index 2f6ed30ba24d..51ff90e60c25 100644
--- a/drivers/net/phy/air_en8811h.c
+++ b/drivers/net/phy/air_en8811h.c
@@ -1019,8 +1019,8 @@ static int en8811h_probe(struct phy_device *phydev)
if (ret < 0)
return ret;
- /* mcu has just restarted after firmware load */
- priv->mcu_needs_restart = false;
+ /* Adopted firmware was never restarted here. */
+ priv->mcu_needs_restart = (ret == 1);
/* MDIO_DEVS1/2 empty, so set mmds_present bits here */
phydev->c45_ids.mmds_present |= MDIO_DEVS_PMAPMD | MDIO_DEVS_AN;
diff --git a/drivers/net/phy/air_phy_lib.c b/drivers/net/phy/air_phy_lib.c
index 2f3e5b71abac..fc596942eb65 100644
--- a/drivers/net/phy/air_phy_lib.c
+++ b/drivers/net/phy/air_phy_lib.c
@@ -368,6 +368,12 @@ static int air_mmd_status_read(struct mdio_device *mdiodev)
return ret;
}
+bool air_en8811h_mcu_running(struct mdio_device *mdiodev)
+{
+ return air_mmd_status_read(mdiodev) == EN8811H_PHY_READY;
+}
+EXPORT_SYMBOL_GPL(air_en8811h_mcu_running);
+
int air_en8811h_wait_mcu_ready(struct mdio_device *mdiodev)
{
int ret, reg_value;
@@ -402,6 +408,17 @@ int air_en8811h_fw_download(struct mdio_device *mdiodev, u32 *fw_version)
struct device *dev = &mdiodev->dev;
int ret;
+ if (air_en8811h_mcu_running(mdiodev)) {
+ ret = air_mdio_buckpbus_reg_read(mdiodev, EN8811H_FW_VERSION,
+ fw_version);
+ if (ret < 0)
+ return ret;
+
+ dev_info(dev, "MD32 already running, firmware %08x\n",
+ *fw_version);
+ return 1;
+ }
+
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 2a4c156d6396..6823812c8fd5 100644
--- a/drivers/net/phy/air_phy_lib.h
+++ b/drivers/net/phy/air_phy_lib.h
@@ -59,7 +59,9 @@ struct firmware;
int air_fw_write_buf(struct mdio_device *mdiodev, u32 address,
const struct firmware *fw);
+bool air_en8811h_mcu_running(struct mdio_device *mdiodev);
int air_en8811h_wait_mcu_ready(struct mdio_device *mdiodev);
+/* Returns 1 when it adopted firmware that was already running. */
int air_en8811h_fw_download(struct mdio_device *mdiodev, u32 *fw_version);
#endif /* __AIR_PHY_LIB_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 07/10] net: mdio: add Airoha EN8811H MDIO device driver
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (5 preceding siblings ...)
2026-09-04 19:03 ` [RFC PATCH net-next v2 06/10] net: phy: air: skip the download when the MD32 is already running Aleksei Sviridkin
@ 2026-09-04 19:03 ` Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 08/10] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:03 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 that polls for the files and
downloads through the shared library helper once they can be read; a
chip left running by the bootloader is adopted as-is. Polling rather
than deferred probing because request_firmware_direct() has no
usermode-helper fallback: an unmounted rootfs fails at once and would
keep the deferred-probe list spinning for the whole mount window.
There is no give-up path, since installing the firmware package on a
running system is a normal thing to do. The stall warning is gated on
a per-device flag rather than dev_warn_once(), which is per call site:
a board can carry two of these chips.
The reset line is claimed here rather than on the PHY node, and it is
cycled only while the MD32 does not report a running firmware: that
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.
The chip's MDIO slave implements the Clause 22 MMD indirection and the
firmware data path is pure Clause 22 by design, so the library reaches
the status register the same way. That poll is one register read
behind three address writes, where the indirection costs nothing
measurable, and whether the chip decodes Clause 45 frames before its
firmware runs is unverified on any board.
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 | 160 +++++++++++++++++++++++++
drivers/net/phy/air_phy_lib.h | 8 +-
include/net/phy/air_phy.h | 23 ++++
6 files changed, 204 insertions(+), 6 deletions(-)
create mode 100644 drivers/net/mdio/mdio-airoha-en8811h.c
create mode 100644 include/net/phy/air_phy.h
diff --git a/MAINTAINERS b/MAINTAINERS
index b23fb6f2f4ef..2d2748c44c77 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -750,6 +750,13 @@ S: Supported
F: fs/aio.c
F: include/linux/*aio*.h
+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 ETHERNET 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 d44278f26fab..10cea4235b42 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..595b1b72bc01
--- /dev/null
+++ b/drivers/net/mdio/mdio-airoha-en8811h.c
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Airoha EN8811H MDIO device driver
+ *
+ * The EN8811H is an MD32 microcontroller until firmware is downloaded into
+ * it, and only then an Ethernet PHY.
+ *
+ * Copyright (C) 2026 Aleksei Sviridkin <f@lex.la>
+ */
+
+#include <linux/delay.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 <net/phy/air_phy.h>
+
+#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;
+
+ ret = air_en8811h_fw_download(mcu->mdiodev, &mcu->fw_version);
+ if (ret >= 0) {
+ 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) {
+ 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));
+}
+
+/* The firmware lives in volatile RAM: no reset while the MD32 reports ready. */
+static void en8811h_mcu_reset_if_dormant(struct en8811h_mcu *mcu)
+{
+ struct mdio_device *mdiodev = mcu->mdiodev;
+ struct device *dev = &mdiodev->dev;
+ u32 assert_us = 0, deassert_us = 0;
+ int ret;
+
+ if (air_en8811h_mcu_running(mdiodev)) {
+ dev_dbg(dev, "MD32 already running, adopting it\n");
+ return;
+ }
+
+ if (!mcu->reset_gpio)
+ return;
+
+ device_property_read_u32(dev, "reset-assert-us", &assert_us);
+ device_property_read_u32(dev, "reset-deassert-us", &deassert_us);
+
+ ret = gpiod_direction_output(mcu->reset_gpio, 1);
+ if (ret) {
+ dev_warn(dev, "reset not asserted: %pe\n", ERR_PTR(ret));
+ return;
+ }
+
+ if (assert_us)
+ fsleep(assert_us);
+
+ gpiod_set_value_cansleep(mcu->reset_gpio, 0);
+ if (deassert_us)
+ fsleep(deassert_us);
+}
+
+static int en8811h_mcu_probe(struct mdio_device *mdiodev)
+{
+ struct device *dev = &mdiodev->dev;
+ struct en8811h_mcu *mcu;
+
+ mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
+ if (!mcu)
+ return -ENOMEM;
+
+ mcu->mdiodev = mdiodev;
+ mdiodev_set_drvdata(mdiodev, mcu);
+
+ /* The core claims reset-gpios only for devices flagged as PHYs. */
+ 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");
+
+ en8811h_mcu_reset_if_dormant(mcu);
+
+ mcu->poll_ms = EN8811H_FW_POLL_MIN_MS;
+ INIT_DELAYED_WORK(&mcu->fw_poll, en8811h_mcu_fw_poll);
+ /* Freezable: neither the file lookup nor the download may land on
+ * a suspending bus.
+ */
+ 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_FIRMWARE(EN8811H_MD32_DM);
+MODULE_FIRMWARE(EN8811H_MD32_DSP);
+
+MODULE_DESCRIPTION("Airoha EN8811H MDIO device driver");
+MODULE_AUTHOR("Aleksei Sviridkin <f@lex.la>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/phy/air_phy_lib.h b/drivers/net/phy/air_phy_lib.h
index 6823812c8fd5..a8329223dd4f 100644
--- a/drivers/net/phy/air_phy_lib.h
+++ b/drivers/net/phy/air_phy_lib.h
@@ -10,6 +10,8 @@
#include <linux/phy.h>
+#include <net/phy/air_phy.h>
+
#define AIR_EXT_PAGE_ACCESS 0x1f
#define AIR_PHY_PAGE_STANDARD 0x0000
@@ -29,9 +31,6 @@
#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
@@ -59,9 +58,6 @@ struct firmware;
int air_fw_write_buf(struct mdio_device *mdiodev, u32 address,
const struct firmware *fw);
-bool air_en8811h_mcu_running(struct mdio_device *mdiodev);
int air_en8811h_wait_mcu_ready(struct mdio_device *mdiodev);
-/* Returns 1 when it adopted firmware that was already running. */
-int air_en8811h_fw_download(struct mdio_device *mdiodev, u32 *fw_version);
#endif /* __AIR_PHY_LIB_H */
diff --git a/include/net/phy/air_phy.h b/include/net/phy/air_phy.h
new file mode 100644
index 000000000000..03cb3465ef8d
--- /dev/null
+++ b/include/net/phy/air_phy.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (C) 2026 Airoha Technology Corp.
+ * Copyright (C) 2026 Collabora Ltd.
+ * Louis-Alexis Eyraud <louisalexis.eyraud@collabora.com>
+ * Copyright (C) 2026 Aleksei Sviridkin <f@lex.la>
+ */
+
+#ifndef __NET_PHY_AIR_PHY_H
+#define __NET_PHY_AIR_PHY_H
+
+#include <linux/types.h>
+
+struct mdio_device;
+
+#define EN8811H_MD32_DM "airoha/EthMD32.dm.bin"
+#define EN8811H_MD32_DSP "airoha/EthMD32.DSP.bin"
+
+bool air_en8811h_mcu_running(struct mdio_device *mdiodev);
+/* Returns 1 when it adopted firmware that was already running. */
+int air_en8811h_fw_download(struct mdio_device *mdiodev, u32 *fw_version);
+
+#endif /* __NET_PHY_AIR_PHY_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 08/10] net: mdio: en8811h: add the nested pass-through bus
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (6 preceding siblings ...)
2026-09-04 19:03 ` [RFC PATCH net-next v2 07/10] net: mdio: add Airoha EN8811H MDIO device driver Aleksei Sviridkin
@ 2026-09-04 19:03 ` Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 09/10] net: phylink: wait for PHYs that are known to probe late Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 10/10] net: phylink: report no link modes while a late PHY is missing Aleksei Sviridkin
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:03 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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. The parent's interrupt for that address is carried over,
since a parent that fills irq[] from its own interrupt domain - a
switch, say - would otherwise leave the PHY polling; an interrupts
property on the PHY's node still wins.
Reach the parent through the mdiobus_*_nested() accessors, which take
its lock at MDIO_MUTEX_NESTED, the way 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 | 134 +++++++++++++++++++++++--
1 file changed, 128 insertions(+), 6 deletions(-)
diff --git a/drivers/net/mdio/mdio-airoha-en8811h.c b/drivers/net/mdio/mdio-airoha-en8811h.c
index 595b1b72bc01..222aa4b11b95 100644
--- a/drivers/net/mdio/mdio-airoha-en8811h.c
+++ b/drivers/net/mdio/mdio-airoha-en8811h.c
@@ -13,6 +13,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>
@@ -26,12 +28,104 @@ struct en8811h_mcu {
struct mdio_device *mdiodev;
struct gpio_desc *reset_gpio;
struct delayed_work fw_poll;
+ struct mii_bus *bus;
unsigned int poll_ms;
unsigned int waited_ms;
u32 fw_version;
bool warned;
+ bool fw_running;
};
+static int en8811h_mcu_read(struct mii_bus *bus, int addr, int regnum)
+{
+ struct en8811h_mcu *mcu = bus->priv;
+
+ if (addr != mcu->mdiodev->addr)
+ return -ENODEV;
+
+ return mdiobus_read_nested(mcu->mdiodev->bus, addr, regnum);
+}
+
+static int en8811h_mcu_write(struct mii_bus *bus, int addr, int regnum, u16 val)
+{
+ struct en8811h_mcu *mcu = bus->priv;
+
+ if (addr != mcu->mdiodev->addr)
+ return -ENODEV;
+
+ return mdiobus_write_nested(mcu->mdiodev->bus, addr, regnum, val);
+}
+
+static int en8811h_mcu_read_c45(struct mii_bus *bus, int addr, int devad,
+ int regnum)
+{
+ struct en8811h_mcu *mcu = bus->priv;
+
+ if (addr != mcu->mdiodev->addr)
+ return -ENODEV;
+
+ return mdiobus_c45_read_nested(mcu->mdiodev->bus, addr, devad, regnum);
+}
+
+static int en8811h_mcu_write_c45(struct mii_bus *bus, int addr, int devad,
+ int regnum, u16 val)
+{
+ struct en8811h_mcu *mcu = bus->priv;
+
+ if (addr != mcu->mdiodev->addr)
+ return -ENODEV;
+
+ return mdiobus_c45_write_nested(mcu->mdiodev->bus, addr, devad, regnum,
+ val);
+}
+
+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)
+ return -ENODEV;
+
+ /* Not devm: this is retried, and a devm bus would only be freed at
+ * detach.
+ */
+ bus = mdiobus_alloc();
+ 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;
+ bus->irq[mcu->mdiodev->addr] = parent->irq[mcu->mdiodev->addr];
+
+ 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 = of_mdiobus_register(bus, np);
+ of_node_put(np);
+ if (ret) {
+ mdiobus_free(bus);
+ return ret;
+ }
+
+ mcu->bus = bus;
+ return 0;
+}
+
static void en8811h_mcu_fw_poll(struct work_struct *work)
{
struct en8811h_mcu *mcu = container_of(to_delayed_work(work),
@@ -39,21 +133,35 @@ static void en8811h_mcu_fw_poll(struct work_struct *work)
struct device *dev = &mcu->mdiodev->dev;
int ret;
- ret = air_en8811h_fw_download(mcu->mdiodev, &mcu->fw_version);
- if (ret >= 0) {
+ if (!mcu->fw_running) {
+ ret = air_en8811h_fw_download(mcu->mdiodev, &mcu->fw_version);
+ if (ret < 0)
+ goto retry;
+
dev_dbg(dev, "firmware %08x running after %ums\n",
mcu->fw_version, mcu->waited_ms);
- return;
+ /* Registration is a new phase: its own backoff and warning. */
+ mcu->fw_running = true;
+ mcu->poll_ms = EN8811H_FW_POLL_MIN_MS;
+ mcu->warned = false;
}
+ /* fwnode_mdio defers while the PHY node's interrupt controller is
+ * missing, so a failure here is not necessarily permanent.
+ */
+ ret = en8811h_mcu_bus_register(mcu);
+ if (!ret)
+ return;
+
+retry:
mcu->waited_ms += mcu->poll_ms;
if (!mcu->warned && mcu->waited_ms >= EN8811H_FW_WARN_MS) {
- if (ret == -ENOENT)
+ if (!mcu->fw_running && 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(dev, "PHY not up after %ums: %pe\n",
+ mcu->waited_ms, ERR_PTR(ret));
mcu->warned = true;
}
@@ -99,6 +207,7 @@ static int en8811h_mcu_probe(struct mdio_device *mdiodev)
{
struct device *dev = &mdiodev->dev;
struct en8811h_mcu *mcu;
+ struct device_node *np;
mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
if (!mcu)
@@ -107,6 +216,15 @@ static int en8811h_mcu_probe(struct mdio_device *mdiodev)
mcu->mdiodev = mdiodev;
mdiodev_set_drvdata(mdiodev, mcu);
+ /* Registration needs this only once the firmware runs, but a DT
+ * hole should fail the bind now, not as a work-item error later.
+ */
+ 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 claims reset-gpios only for devices flagged as PHYs. */
mcu->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_ASIS);
if (IS_ERR(mcu->reset_gpio))
@@ -133,6 +251,10 @@ static void en8811h_mcu_remove(struct mdio_device *mdiodev)
struct en8811h_mcu *mcu = mdiodev_get_drvdata(mdiodev);
cancel_delayed_work_sync(&mcu->fw_poll);
+ if (mcu->bus) {
+ mdiobus_unregister(mcu->bus);
+ mdiobus_free(mcu->bus);
+ }
}
static const struct of_device_id en8811h_mcu_of_match[] = {
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [RFC PATCH net-next v2 09/10] net: phylink: wait for PHYs that are known to probe late
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (7 preceding siblings ...)
2026-09-04 19:03 ` [RFC PATCH net-next v2 08/10] net: mdio: en8811h: add the nested pass-through bus Aleksei Sviridkin
@ 2026-09-04 19:03 ` Aleksei Sviridkin
2026-09-04 19:03 ` [RFC PATCH net-next v2 10/10] net: phylink: report no link modes while a late PHY is missing Aleksei Sviridkin
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:03 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 declare that with
phy-needs-host-firmware and poll for the PHY instead of failing. Return
0 rather than -ENODEV, because DSA reads -ENODEV as permission to look
for the PHY on the switch's internal MDIO bus, which is the wrong
device.
Wait for a driver that has bound rather than a device that exists,
because the generic driver would otherwise bind and cannot drive such a
PHY. Deferring the MAC's own probe is not an option: it would take every
port with it, including the one needed to mount the filesystem that
holds the firmware. Keep polling after a failed connect, because a
failed bringup detaches the PHY and whether the next attempt succeeds
depends on which driver binds it, which phylink cannot see; -EBUSY is
the exception and stops the poller, because it means the PHY is already
attached, here or elsewhere, and polling cannot change that, after which
the port keeps the pending state and reports no link modes until it is
reconnected. When the port is running and nothing else is holding the
link down, the MAC is configured before the PHY is started, the order
phylink_start() uses, and the only other place that starts a late PHY
should not use a different one; that also makes the forced configuration
run at a deterministic moment rather than whenever the workqueue reaches
it. rtnl is taken with trylock so the poller never blocks on it, which
keeps it from parking a shared workqueue worker while another thread
holds rtnl. The asynchronous cancel in phylink_disconnect_phy() is then
enough, because the stop flag makes a run that slips through a no-op.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
drivers/net/phy/phylink.c | 179 ++++++++++++++++++++++++++++++++++++--
1 file changed, 172 insertions(+), 7 deletions(-)
diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index 97712c1572ad..bceeac954779 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -73,6 +73,15 @@ struct phylink {
struct phylink_link_state phy_state;
unsigned int phy_ib_mode;
struct work_struct resolve;
+ /* Set before the poller is queued, cleared under rtnl. */
+ struct fwnode_handle *slow_phy_fwnode;
+ u32 slow_phy_flags;
+ struct delayed_work slow_phy_poll;
+ unsigned int slow_phy_poll_ms;
+ unsigned int slow_phy_waited_ms;
+ bool slow_phy_err_logged;
+ /* Read unlocked by the poller; a stale read costs one poll cycle. */
+ bool slow_phy_stop;
unsigned int pcs_neg_mode;
unsigned int pcs_state;
@@ -1829,6 +1838,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 +1878,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 +1962,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);
}
@@ -2219,10 +2235,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;
@@ -2260,7 +2274,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;
@@ -2272,6 +2286,124 @@ int phylink_connect_phy(struct phylink *pl, struct phy_device *phy)
}
EXPORT_SYMBOL_GPL(phylink_connect_phy);
+#define PHYLINK_SLOW_PHY_POLL_MS 1000
+#define PHYLINK_SLOW_PHY_WARN_MS 60000
+#define PHYLINK_SLOW_PHY_POLL_MAX_MS 30000
+
+/* Bound, not drv: drv is published before the driver's probe runs. The
+ * device lock cannot be taken under rtnl, and losing this race costs a
+ * generic-driver attach, not memory safety.
+ */
+static bool phylink_phy_is_usable(struct phy_device *phy_dev)
+{
+ return phy_dev && device_is_bound(&phy_dev->mdio.dev);
+}
+
+static void phylink_slow_phy_backoff(struct phylink *pl)
+{
+ WRITE_ONCE(pl->slow_phy_poll_ms,
+ min_t(unsigned int, pl->slow_phy_poll_ms * 2,
+ PHYLINK_SLOW_PHY_POLL_MAX_MS));
+}
+
+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;
+
+ /* Never block on rtnl: this runs on a shared workqueue. */
+ if (!rtnl_trylock())
+ goto requeue;
+
+ if (READ_ONCE(pl->slow_phy_stop)) {
+ rtnl_unlock();
+ return;
+ }
+
+ /* Under rtnl: phylink_disconnect_phy() puts and clears the node. */
+ 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);
+
+ 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 %pfw (phy-needs-host-firmware)\n",
+ pl->slow_phy_fwnode);
+ /* Past the warn it may never come: stop paying 1 Hz for it. */
+ if (pl->slow_phy_waited_ms >= PHYLINK_SLOW_PHY_WARN_MS)
+ phylink_slow_phy_backoff(pl);
+ rtnl_unlock();
+ goto requeue;
+ }
+
+ if (pl->link_interface == PHY_INTERFACE_MODE_NA) {
+ mutex_lock(&pl->state_mutex);
+ pl->link_interface = phy_dev->interface;
+ pl->link_config.interface = pl->link_interface;
+ mutex_unlock(&pl->state_mutex);
+ }
+
+ 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 {
+ /* Only a major config programs the masks bringup just
+ * narrowed; the resolve's own trigger cannot see it.
+ */
+ mutex_lock(&pl->state_mutex);
+ pl->force_major_config = true;
+ mutex_unlock(&pl->state_mutex);
+ if (!test_bit(PHYLINK_DISABLE_STOPPED,
+ &pl->phylink_disable_state)) {
+ /* MAC first, then the PHY, as phylink_start()
+ * does; the config is skipped while the
+ * resolve is disabled.
+ */
+ phylink_run_resolve(pl);
+ flush_work(&pl->resolve);
+ phy_start(phy_dev);
+ }
+ }
+ }
+ if (ret) {
+ /* The errno does not distinguish permanent from transient. */
+ if (!pl->slow_phy_err_logged) {
+ pl->slow_phy_err_logged = true;
+ phylink_err(pl, "failed to connect late PHY: %pe\n",
+ ERR_PTR(ret));
+ }
+
+ if (ret == -EBUSY) {
+ rtnl_unlock();
+ return;
+ }
+ phylink_slow_phy_backoff(pl);
+ }
+ 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 +2414,8 @@ EXPORT_SYMBOL_GPL(phylink_connect_phy);
* specified by @pl. Actions specified in phylink_connect_phy() will be
* performed.
*
- * Returns 0 on success or a negative errno.
+ * Returns what phylink_fwnode_phy_connect() returns, including 0 for a
+ * deferred connect with no PHY attached yet.
*/
int phylink_of_phy_connect(struct phylink *pl, struct device_node *dn,
u32 flags)
@@ -2300,7 +2433,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 phy-needs-host-firmware 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,
@@ -2322,6 +2461,25 @@ int phylink_fwnode_phy_connect(struct phylink *pl,
}
phy_dev = fwnode_phy_find_device(phy_fwnode);
+ if (fwnode_property_present(fwnode, "phy-needs-host-firmware") &&
+ !phylink_phy_is_usable(phy_dev)) {
+ /* -ENODEV here would also send DSA to the switch's own bus. */
+ if (phy_dev)
+ phy_device_free(phy_dev);
+
+ fwnode_handle_put(pl->slow_phy_fwnode);
+ 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;
+ pl->slow_phy_err_logged = false;
+ WRITE_ONCE(pl->slow_phy_stop, false);
+ /* mod_delayed_work: a cancelled poll may still be pending. */
+ 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)
@@ -2363,6 +2521,13 @@ void phylink_disconnect_phy(struct phylink *pl)
ASSERT_RTNL();
+ /* Async is enough: the stop flag no-ops a run that slips through. */
+ WRITE_ONCE(pl->slow_phy_stop, true);
+ cancel_delayed_work(&pl->slow_phy_poll);
+ /* The next connect re-arms from its own lookup. */
+ fwnode_handle_put(pl->slow_phy_fwnode);
+ pl->slow_phy_fwnode = NULL;
+
mutex_lock(&pl->phydev_mutex);
phy = pl->phydev;
if (phy)
@@ -3741,7 +3906,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] 11+ messages in thread* [RFC PATCH net-next v2 10/10] net: phylink: report no link modes while a late PHY is missing
2026-09-04 19:02 [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes Aleksei Sviridkin
` (8 preceding siblings ...)
2026-09-04 19:03 ` [RFC PATCH net-next v2 09/10] net: phylink: wait for PHYs that are known to probe late Aleksei Sviridkin
@ 2026-09-04 19:03 ` Aleksei Sviridkin
9 siblings, 0 replies; 11+ messages in thread
From: Aleksei Sviridkin @ 2026-09-04 19:03 UTC (permalink / raw)
To: Andrew Lunn, Andrew Lunn, Heiner Kallweit, Russell King,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: Eric Woudstra, netdev, devicetree, linux-kernel
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 until the PHY arrives. Every
path through net/ethtool/ zeroes the reply before the get op, so the
pending path only stamps the unknown speed and duplex.
Setting pause parameters needs the same guard, because it is gated on
the same MAC-derived mask; reading them does not, since it reports the
configured request rather than a capability. The EEE calls need no guard
either: 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 | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index bceeac954779..942d5fe943a1 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -2290,6 +2290,11 @@ EXPORT_SYMBOL_GPL(phylink_connect_phy);
#define PHYLINK_SLOW_PHY_WARN_MS 60000
#define PHYLINK_SLOW_PHY_POLL_MAX_MS 30000
+static bool phylink_slow_phy_pending(struct phylink *pl)
+{
+ return pl->slow_phy_fwnode && !pl->phydev;
+}
+
/* Bound, not drv: drv is published before the driver's probe runs. The
* device lock cannot be taken under rtnl, and losing this race costs a
* generic-driver attach, not memory safety.
@@ -3087,6 +3092,14 @@ int phylink_ethtool_ksettings_get(struct phylink *pl,
ASSERT_RTNL();
+ /* No PHY yet: the port supports nothing, not what the MAC alone can. */
+ if (phylink_slow_phy_pending(pl)) {
+ kset->base.port = pl->link_port;
+ kset->base.speed = SPEED_UNKNOWN;
+ kset->base.duplex = DUPLEX_UNKNOWN;
+ return 0;
+ }
+
if (pl->phydev)
phy_ethtool_ksettings_get(pl->phydev, kset);
else
@@ -3159,6 +3172,10 @@ int phylink_ethtool_ksettings_set(struct phylink *pl,
ASSERT_RTNL();
+ /* Would configure the MAC alone, for a link that cannot come up. */
+ if (phylink_slow_phy_pending(pl))
+ return -EOPNOTSUPP;
+
if (pl->phydev) {
struct ethtool_link_ksettings phy_kset = *kset;
@@ -3373,6 +3390,10 @@ int phylink_ethtool_set_pauseparam(struct phylink *pl,
if (pl->req_link_an_mode == MLO_AN_FIXED)
return -EOPNOTSUPP;
+ /* pl->supported still describes the MAC, so the test below passes. */
+ if (phylink_slow_phy_pending(pl))
+ return -EOPNOTSUPP;
+
if (!phylink_test(pl->supported, Pause) &&
!phylink_test(pl->supported, Asym_Pause))
return -EOPNOTSUPP;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread