* [RFC PATCH net-next v2 00/10] net: survive a PHY whose firmware arrives after the MAC probes
@ 2026-09-04 19:02 Aleksei Sviridkin
2026-09-04 19:02 ` [RFC PATCH net-next v2 01/10] dt-bindings: net: add Airoha EN8811H PHY MCU Aleksei Sviridkin
` (9 more replies)
0 siblings, 10 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 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. Patch 3 adds mdiodev_lock() and
mdiodev_unlock(), the names Andrew asked for in [2], for code that
holds the bus lock across a burst without a phy_device. Patches 4-6
move the download into a library helper typed on the MDIO device and
teach it to skip a download when the MD32 already runs firmware, which
is what lets the two drivers coexist whichever runs first. Patch 7 adds
the MDIO device driver, patch 8 the pass-through bus, patches 9-10 the
phylink half. Patches 9-10 alone carry this board, whose chip answers
its ID before firmware; the MDIO layer buys the general case - chips
mute before firmware, quad-PHY packages, reset ownership.
Tested on an MT7981B board (MT7531 switch, EN8811H on a 2500base-x
port), warm boots only - I have no remote way to cut power. The board
runs OpenWrt, so what booted is these patches backported onto its 6.18
tree rather than the mailed text byte-for-byte. Two hunks needed
adapting to that tree: one keeps a local in-band guard upstream has
since dropped, and one moves the delayed work's initialisation ahead of
a setup step that can return early there. What the board showed:
- download and handover, normal boot path, no unbind: U-Boot leaves
the MD32 in its bootloader on every reboot here, so each boot runs
the MCU driver's pulse-reset and download; ~144KB lands in about
half a second, the MCU driver reports the firmware it loaded at
6.33 s, and the PHY driver finds that firmware already running at
6.39 s and skips its own download through the shared check
- phylink attaches the PHY at 7.10 s through the child bus with
irq=15 from DT rather than PHY_POLL; the port reaches forwarding and
the link comes up at 20.1 s, and the interrupt line counts link
events across forced renegotiations
- the case the series exists for, a PHY arriving while the port is
already running: attach at 67.44 s, carrier at 71.93 s, so 4.5 s,
and the PHY's interrupt fires without any port bounce. This needs
the net fix [4]; without it the same path left the port dead
- an ifdown/ifup cycle disconnects and reconnects cleanly
- the stopped-port path, reached deterministically by booting with the
firmware out of reach and putting the port down while the PHY cannot
exist: the PHY attaches to the stopped port, sits there attached and
carrier-less, and the later up starts it, with the link three
seconds behind
- the wait itself: one warning at 65 s naming the property and the
missing PHY, then a 29.19 s gap between the PHY becoming usable and
the poller noticing it - the ceiling doing its job, where the
initial one-second interval would have attached within a second
Not exercised here, and I would rather say so. The irq[] copy on the
pass-through bus is a no-op on this board: the PHY sits on the SoC bus
and its interrupt comes from its own DT node. The -EBUSY exit where the
poller gives up was reasoned about, not reached - it needs two nodes
pointing at one PHY, which this board cannot express. The retry after a
failed connect cannot be reached here at all: the EN8811H reports
RATE_MATCH_PAUSE, so phylink_validate_phy() never intersects the port's
line-rate modes with the PHY's copper ones and no MAC-supported
interface makes it return -EINVAL. That is worth knowing beyond this
board - the retry branch fires for PHYs that do not rate-adapt, which is
not the case that motivated the series.
No in-tree device tree sets phy-needs-host-firmware yet. The board I
tested is supported out of tree, in OpenWrt; the in-tree
mt7986a-bananapi-bpi-r3-mini carries the same chip and would be the
first candidate, but I have no such board to test the conversion on.
Andrew asked whether any board with this chip sits on a bus without
direct Clause 45. I found none. The two in-tree device trees that name
it (mt7981b-openwrt-one, mt7986a-bananapi-bpi-r3-mini) put it on
mtk_eth_soc, and in OpenWrt it hangs off one of four controllers:
mtk_eth_soc, the MT7530 DSA bus on EN7581, mdio-airoha on AN7583, or
the SiFlower xgmac. All four set read_c45, and none of those boards
reaches the chip through an mdio-mux or a bitbanged bus.
The status poll keeps the Clause 22 indirection. It went through
phy_read_mmd(), which resolved the mode from phydev->is_c45 - false for
these Clause 22 PHYs - so the constant in the moved helper records what
the code already did rather than choosing it. The chip does answer
Clause 45 before its firmware, though. With the MD32 never programmed,
read on the parent bus while the child bus does not yet exist, the
PMA/PMD identifier comes back as 0x03a2, matching the Clause 22 pair
0x03a2/0xa411 at the same address, and the vendor status reads 0x0000
rather than the 0xffff a silent chip would float to. So the indirection
is a preference, not a necessity.
The flag sits on the MAC/port node because that is where the waiting
happens: phylink keeps the port and polls until the PHY turns up. The
PHY node could carry it instead. phylink resolves phy-handle to a
fwnode before it needs the device, so reading the property from the
referenced node is a one-token change, and a PCS node could carry its
own the same way. Which node do you want it on?
One behaviour change worth naming here: once the MCU driver adopts a
running MD32, the image in RAM wins over the files on disk, so writing
new firmware and rebinding keeps the old image running until a power
cycle. Every in-tree board with this chip keeps reset-gpios on the PHY
node, and phy_detach() asserts that line, so the mitigation is there.
Known and left out: system sleep. A power-cutting suspend wipes the MD32
and nothing re-downloads on resume; the obvious fix, queueing the
download from .resume, cannot run before the PHY child resumes
(freezable workqueues thaw after dpm_resume_end()), so it needs a
synchronous re-download from the firmware cache and S2R testing this
board cannot give. The poller never gives up either, because the errno
out of a failed bringup cannot distinguish "still filling in link modes"
from "genuinely incompatible": a site with the property and no PHY polls
at the 30 s ceiling for the uptime, after one warning in the first
minute. The backoff exists because a failed bringup ends in
phy_detach(), which pulses a PHY-node reset line. Unbinding the MDIO
device at runtime removes the child bus under an attached PHY, and
either the next open or the next close then dereferences the stale
phydev, in phy_start() or phy_stop(); that path predates this series
(any mdio-mux unbind does the same) and wants a phy-core fix rather than
a workaround here. Adoption itself does not depend on it and is
exercised on the ordinary boot path.
The attach lands anywhere in [0, poll interval) after the PHY becomes
ready, ~0.5 s mean at the 1 s interval. The exact edge exists -
BUS_NOTIFY_BOUND_DRIVER fires from driver_bound() after phy_probe() has
set PHY_READY - so an event-driven follow-up is a bus notifier plus a
one-shot work item. Polling first was the plan agreed in [1]; say if
you want the notifier in this series instead.
Why not -EPROBE_DEFER and fw_devlink: the phy-handle supplier link is
satisfied as soon as the PHY device exists, and here it does not exist
at all until the MCU driver has loaded the firmware and registered the
child bus. Nothing in the device core waits for a device that has not
been created. Cost in struct phylink: six fields and a delayed_work.
The flag is a request for a dedicated PHY driver - a PHY meant to run
on the generic driver must not carry it, or the wait never ends.
The base matters for patch 6: it makes the MCU restart on the adoption
path, and 03b4702fc5e3 with 3498acda6b68 moved the LED GPIO enable to
the end of config_init, after that restart. On an older base the
restart would have cleared the buckpbus-mapped GPIO state with nothing
to re-enable it.
This is based on net-next 6ebcf5074cff with patch 1 of the pending pair
[3] applied - the series needs a late bringup failure to leave
pl->phydev clear, or every retry hits -EBUSY; format-patch lists it as a
prerequisite. It also wants [4], a fix now on the list for net: a forced
major configuration can run over an uninitialised link_state, and the
poller in patch 9 reaches it on a port that is already up when the PHY
arrives, because the attach reports the not-yet-started PHY as down and
the resolve then takes the link-failed branch. Patch 5 moves the
firmware write loop that the pending size check [5] guards; whichever
lands first, the other rebases trivially.
[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/20260902080511.2211261-1-f@lex.la/
[4] https://lore.kernel.org/netdev/20260904185540.2844261-1-f@lex.la/
[5] https://lore.kernel.org/netdev/20260903123650.23855-1-f@lex.la/
Changes since v1 [6]:
- Andrew Lunn: the property names the entity instead of describing the
MAC node it sits in, and states the hardware fact rather than an
event in the driver model: phy-needs-host-firmware (2)
- Andrew Lunn: that binding's commit message is rewritten; the open
question it states is placement now, not naming (2)
- Andrew Lunn: both binding descriptions are hardware only - no
driver, no kernel symbols, no filesystems (1, 2)
- Andrew Lunn: mdiodev_lock()/mdiodev_unlock() beside
phy_lock_mdio_bus(), so the library stops open-coding the bus
mutex (3, new)
- Andrew Lunn: the buckpbus core and the firmware loader are typed on
struct mdio_device, using the existing __mdiodev_read/write (4, 5,
6); the MMD status poll stays on the Clause 22 indirection, since
the data path was already Clause 22 - see above (5)
- Andrew Lunn: the symbols the MDIO driver consumes moved to
include/net/phy/air_phy.h instead of reaching into ../phy/ (7)
- Andrew Lunn asked for dev_warn_once(); kept a per-device flag,
because dev_warn_once() is per call site and
mt7986a-bananapi-bpi-r3-mini carries two of these chips (7)
- Andrew Lunn: the comment claiming the chip enumerates as a C22 PHY
is gone (7)
- Andrew Lunn: the parent bus's interrupt is copied onto the
pass-through bus so a PHY behind it is not forced to poll (8);
phy_mask stays unset because of_mdiobus_register() overwrites it
- the readiness test is device_is_bound() rather than phydev->drv,
which phy_probe() publishes before the driver's probe has filled in
the supported mask (9)
- the poller configures the MAC before it starts the PHY, the order
phylink_start() uses (9)
- a late attach forces a major reconfiguration, so a port started with
no phy-mode leaves PHY_INTERFACE_MODE_NA; the not-yet-usable poll
backs off after the first warning, ceiling 30 s (9)
- while the PHY is pending, ksettings_get reports SPEED_UNKNOWN and
DUPLEX_UNKNOWN with no link modes, relying on the ethtool core's
zeroing for the masks, and ksettings_set and set_pauseparam return
-EOPNOTSUPP - with the port up, set_pauseparam would program the PCS
from an advertisement seeded by the MAC mask (10)
- the download reports adoption distinctly from loading, and adopting
a running MD32 no longer writes FW_CTRL_1: that write was an
unpaired FINISH into a chip that had just reported ready, and the
poll after it asked a question already answered (6)
- a failed pass-through bus registration is retried, since fwnode_mdio
returns -EPROBE_DEFER for an interrupt controller that has not
probed yet; the download is not re-run (7)
- MODULE_FIRMWARE for the EN8811H files moved off the shared library,
which AN8801-only systems also load, onto the MDIO driver (7)
- the netdev AI review of v1 raised four points, all answered: the
dangling pl->phydev after a late bringup failure goes to the
prerequisite [3] rather than here (9), the suspend hole is stated
above as known and left out (7), the naming question is answered by
the rename (2), and the uninitialised masks rest on the ethtool
core's zeroing, argued in the patch (10)
- commit message of the phylink wait patch shortened (9)
- rebased onto net-next 6ebcf5074cff
[6] https://lore.kernel.org/r/20260829052546.1152446-1-f@lex.la/
Aleksei Sviridkin (10):
dt-bindings: net: add Airoha EN8811H PHY MCU
dt-bindings: net: ethernet-controller: add phy-needs-host-firmware
net: phy: add mdiodev_lock() and mdiodev_unlock()
net: phy: air: type the buckpbus core on the mdio device
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 | 96 +++++
.../bindings/net/ethernet-controller.yaml | 7 +
MAINTAINERS | 7 +
drivers/net/mdio/Kconfig | 11 +
drivers/net/mdio/Makefile | 1 +
drivers/net/mdio/mdio-airoha-en8811h.c | 282 ++++++++++++++
drivers/net/phy/air_en8811h.c | 143 +------
drivers/net/phy/air_phy_lib.c | 357 ++++++++++++++++--
drivers/net/phy/air_phy_lib.h | 22 ++
drivers/net/phy/phylink.c | 200 +++++++++-
include/linux/phy.h | 10 +
include/net/phy/air_phy.h | 23 ++
12 files changed, 977 insertions(+), 182 deletions(-)
create mode 100644 Documentation/devicetree/bindings/net/airoha,en8811h-mcu.yaml
create mode 100644 drivers/net/mdio/mdio-airoha-en8811h.c
create mode 100644 include/net/phy/air_phy.h
base-commit: 6ebcf5074cff0402730c6981d2397139fee6322d
prerequisite-patch-id: 293623f600b825505376e5c5bf72df2ac1f58e1e
--
2.53.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [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
end of thread, other threads:[~2026-09-04 19:03 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [RFC PATCH net-next v2 03/10] net: phy: add mdiodev_lock() and mdiodev_unlock() 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
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 ` [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 ` [RFC PATCH net-next v2 07/10] net: mdio: add Airoha EN8811H MDIO device driver 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
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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox