[PATCH net v3] net: phy: qca83xx: read resolved QCA8337 link status

From: Yongzhao Chen

Date: Tue Oct 06 2026 - 12:06:53 EST


After a QCA8337 SmartSpeed downshift, genphy_read_status() can report
1000 Mbit/s from the advertised modes when the PHY is running at
100 Mbit/s. On a Redmi AX5400 running downstream OpenWrt Linux 6.18.52,
this caused qca8k to program the user-port MAC for 1000 Mbit/s and
traffic failed. An earlier prototype that read the resolved speed
from the PHY restored traffic.

Wrap genphy_read_status() and use at803x_read_specific_status() to
replace its speed and duplex result when autonegotiation is complete
and the link has not stayed up. Resolve pause for the resulting duplex.
QCA8337's register layout matches the helper (data sheet 80-Y0619-3
Rev. D, pages 327-328).

Keep the generic master/slave read order and the early return for a
steady autonegotiated link. This avoids additional reads of the
clear-on-read idle error count in MII_STAT1000. Forced mode continues
to use BMCR. Report the MDI-X state, but clear mdix_ctrl because this
driver does not implement MDI-X configuration.

Fixes: b3591c2a3661 ("net: dsa: qca8k: Switch to PHYLINK instead of PHYLIB")
Suggested-by: Andrew Lunn <andrew@xxxxxxx>
Assisted-by: LLM
Signed-off-by: Yongzhao Chen <yongzhao.derek@xxxxxxxxx>
---
v3:
- Keep genphy_read_status() instead of calling at803x_read_status()
followed by genphy_read_master_slave(). This restores the generic
MII_STAT1000 read frequency and master/slave-before-LPA order,
addressing the idle-error and latched-fault issues raised by Sashiko.
- Report mdix_ctrl as ETH_TP_MDI_INVALID, including after a steady
poll, since QCA8337 has no MDI-X configuration callback.
- Keep the generic BMCR result in forced mode. QCA8337's resolved-bit
behavior in that mode has not been verified.

v2: https://lore.kernel.org/netdev/20261003173210.1235-1-yongzhao.derek@xxxxxxxxx/
v1: https://lore.kernel.org/netdev/20260928220749.857-1-yongzhao.derek@xxxxxxxxx/

Testing: the source-extraction model passes 26 checks covering speed
and duplex at the phylib notification boundary, latch-low transitions,
pause, forced mode, unresolved/reserved status, MDIO errors and the
three reported issues. The baseline fails the downshift checks; v2
reproduces the idle-error loss, one-shot MSFAIL ordering difference and
valid mdix_ctrl report. The MDIO model follows the read-clear and
saturating-counter definitions in Table 5-352, pages 324-325. An arm64
W=1 build of qca83xx.o, at803x.o and qcom-phy-lib.o is clean.

v3 has not been tested on hardware. The hardware result above used
v2's at803x_read_status() plus genphy_read_master_slave() on one board,
with an Intel igb peer, a two-pair cable and default autonegotiation.
Ping changed from 0/20 to 20/20 in each direction at resolved 100 Full.
No idle errors or MSFAIL were injected on hardware; whether MSFAIL can
remain latched while AN complete is set is still unknown.

drivers/net/phy/qcom/qca83xx.c | 37 ++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)

diff --git a/drivers/net/phy/qcom/qca83xx.c b/drivers/net/phy/qcom/qca83xx.c
index bc70ed8efd8..e70da092f42 100644
--- a/drivers/net/phy/qcom/qca83xx.c
+++ b/drivers/net/phy/qcom/qca83xx.c
@@ -92,6 +92,42 @@ static int qca83xx_probe(struct phy_device *phydev)
return 0;
}

+static int qca8337_read_status(struct phy_device *phydev)
+{
+ struct at803x_ss_mask ss_mask = {
+ .speed_mask = AT803X_SS_SPEED_MASK,
+ .speed_shift = __bf_shf(AT803X_SS_SPEED_MASK),
+ };
+ int ret, old_link = phydev->link;
+
+ ret = genphy_read_status(phydev);
+ if (ret)
+ return ret;
+
+ /* genphy_read_status() derives speed and duplex from the
+ * advertisements, which is wrong after a SmartSpeed downshift.
+ * Whenever it has just done so, use what the PHY resolved instead.
+ */
+ if (phydev->autoneg == AUTONEG_ENABLE && phydev->autoneg_complete &&
+ !(old_link && phydev->link)) {
+ phydev->speed = SPEED_UNKNOWN;
+ phydev->duplex = DUPLEX_UNKNOWN;
+ phydev->pause = false;
+ phydev->asym_pause = false;
+
+ ret = at803x_read_specific_status(phydev, ss_mask);
+ if (ret)
+ return ret;
+
+ phy_resolve_aneg_pause(phydev);
+ }
+
+ /* MDI-X configuration is not implemented */
+ phydev->mdix_ctrl = ETH_TP_MDI_INVALID;
+
+ return 0;
+}
+
static int qca83xx_config_init(struct phy_device *phydev)
{
u8 switch_revision;
@@ -220,6 +256,7 @@ static struct phy_driver qca83xx_driver[] = {
.flags = PHY_IS_INTERNAL,
.config_init = qca83xx_config_init,
.soft_reset = genphy_soft_reset,
+ .read_status = qca8337_read_status,
.get_sset_count = qca83xx_get_sset_count,
.get_strings = qca83xx_get_strings,
.get_stats = qca83xx_get_stats,

base-commit: 6dc989ea46b96ce170840174b4a38c4a387fb005
--
2.43.0