From 70037ae17a13cd3617440291ca6ed070c0417dc5 Mon Sep 17 00:00:00 2001 From: Mihai Ordean Date: Fri, 7 Aug 2026 23:25:13 +0000 Subject: [PATCH 1/3] net: dsa: mxl862xx: derive firmware capabilities from the version The driver adapts to several switch firmware behaviours, and each call site tests the firmware version directly with MXL862XX_FW_VER_MIN(). There are three such sites and all three compare against 1.0.80, but they gate three unrelated things: whether the XPCS command family can drive the SerDes, whether the equalisation and PRBS commands behind the SerDes statistics exist, and whether the firmware installs its own global PCE rules that the driver has to disable. Tying independent behaviours to one open-coded version literal does not survive contact with new firmware. The features move independently: 1.0.83 adds the logical-index PCE rule API, and 1.0.85 drops XPCS_PCS_ENABLE and reshapes the XPCS payloads while leaving every other command's ABI untouched. Each such change currently means either another literal spread across unrelated call sites, or a workaround bolted onto one of them. Introduce a capability mask instead. Derive it once from the cached version in mxl862xx_wait_ready() and give call sites mxl862xx_fw_has(), so a firmware that gains or moves a feature only needs the derivation in mxl862xx_init_fw_caps() updated. Name each capability for the firmware behaviour it describes rather than for the version that introduced it, which also removes the negated test at the global-rule site. No functional change: the derivation reproduces all three original version comparisons exactly, for every firmware version. Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 4 +- drivers/net/dsa/mxl862xx/mxl862xx.c | 28 +++++++++++++- drivers/net/dsa/mxl862xx/mxl862xx.h | 42 ++++++++++++++++++++- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c index f0a2e83c8eeee..28e119dfecf87 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c @@ -540,7 +540,7 @@ void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs, pcs->priv = priv; pcs->port = port; - if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) + if (mxl862xx_fw_has(priv, MXL862XX_CAP_XPCS_API)) pcs->pcs.ops = &mxl862xx_pcs_ops; else pcs->pcs.ops = &mxl862xx_legacy_pcs_ops; @@ -636,7 +636,7 @@ static bool mxl862xx_port_has_serdes_stats(struct dsa_switch *ds, int port) struct mxl862xx_priv *priv = ds->priv; return port >= 9 && port <= 16 && - MXL862XX_FW_VER_MIN(priv, 1, 0, 80); + mxl862xx_fw_has(priv, MXL862XX_CAP_SERDES_STATS); } int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index c2cc259c82cc1..545720ab1b322 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -314,6 +314,30 @@ static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr, return mxl862xx_phy_write_mmd(bus->priv, addr, devadd, regnum, val); } +/** + * mxl862xx_init_fw_caps - Derive the firmware capability mask + * @priv: driver private data + * + * Translates the cached firmware version into the capability bits the + * rest of the driver tests with mxl862xx_fw_has(). Called once from + * mxl862xx_wait_ready() after the version has been read. + * + * Keeping the version comparisons in one place means a firmware that + * gains or moves a feature only needs this function updated, rather + * than every call site that depends on the feature. + */ +static void mxl862xx_init_fw_caps(struct mxl862xx_priv *priv) +{ + u32 caps = 0; + + if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) + caps |= MXL862XX_CAP_XPCS_API | MXL862XX_CAP_SERDES_STATS; + else + caps |= MXL862XX_CAP_FW_GLOBAL_RULES; + + priv->fw_caps = caps; +} + static int mxl862xx_wait_ready(struct dsa_switch *ds) { struct mxl862xx_sys_fw_image_version ver = {}; @@ -348,6 +372,8 @@ static int mxl862xx_wait_ready(struct dsa_switch *ds) priv->fw_version.minor = ver.iv_minor; priv->fw_version.revision = le16_to_cpu(ver.iv_revision); + mxl862xx_init_fw_caps(priv); + return 0; not_ready_yet: @@ -1217,7 +1243,7 @@ static int mxl862xx_setup(struct dsa_switch *ds) if (ret) return ret; - if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) { + if (mxl862xx_fw_has(priv, MXL862XX_CAP_FW_GLOBAL_RULES)) { ret = mxl862xx_disable_fw_global_rules(ds); if (ret) return ret; diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h index 52e538dd4bfa3..133bab0fd182b 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx.h @@ -336,6 +336,30 @@ union mxl862xx_fw_version { #define MXL862XX_FW_VER_MIN(priv, maj, min, rev) \ ((priv)->fw_version.raw >= MXL862XX_FW_VER(maj, min, rev)) +/* Firmware capability bits, see struct mxl862xx_priv::fw_caps. + * + * Each bit describes one behaviour of the switch firmware that the + * driver has to adapt to. Capabilities are derived from the firmware + * version once in mxl862xx_wait_ready() and tested with + * mxl862xx_fw_has(); call sites must not compare versions directly, so + * that a firmware that moves a feature only needs the derivation + * updated. + * + * %MXL862XX_CAP_XPCS_API: the XPCS command family is implemented and + * the SerDes can be driven through it instead + * of the legacy register path + * %MXL862XX_CAP_SERDES_STATS: the equalisation, PRBS and signal-detect + * commands behind the SerDes ethtool statistics + * and self-test are implemented + * %MXL862XX_CAP_FW_GLOBAL_RULES: the firmware installs its own global PCE + * rules at init, which the driver has to + * disable because it manages the flow table + * itself + */ +#define MXL862XX_CAP_XPCS_API BIT(0) +#define MXL862XX_CAP_SERDES_STATS BIT(1) +#define MXL862XX_CAP_FW_GLOBAL_RULES BIT(2) + /* Bit indices for struct mxl862xx_priv::flags */ #define MXL862XX_FLAG_CRC_ERR 0 #define MXL862XX_FLAG_WORK_STOPPED 1 @@ -372,7 +396,10 @@ struct combo_port_mux { * used to unconditionally drop traffic (used to block * flooding) * @fw_version: cached firmware version, populated at probe and - * compared with MXL862XX_FW_VER_MIN() + * used to derive @fw_caps + * @fw_caps: firmware capabilities derived from @fw_version at + * probe, a mask of %MXL862XX_CAP_ bits; test with + * mxl862xx_fw_has() * @serdes_ports: SerDes interfaces incl. sub-interfaces in case of * 10G_QXGMII * @ports: per-port state, indexed by switch port number @@ -413,6 +440,7 @@ struct mxl862xx_priv { enum dsa_tag_protocol tag_proto; u16 drop_meter; union mxl862xx_fw_version fw_version; + u32 fw_caps; struct mxl862xx_pcs serdes_ports[8]; struct mxl862xx_port ports[MXL862XX_MAX_PORTS]; u16 bridges[MXL862XX_MAX_BRIDGES + 1]; @@ -430,4 +458,16 @@ struct mxl862xx_priv { struct combo_port_mux *ds_mux[MXL862XX_MAX_PORTS]; }; +/** + * mxl862xx_fw_has - Test whether the firmware provides a capability + * @priv: driver private data + * @cap: single %MXL862XX_CAP_ bit to test + * + * Return: true if the running firmware provides @cap. + */ +static inline bool mxl862xx_fw_has(const struct mxl862xx_priv *priv, u32 cap) +{ + return !!(priv->fw_caps & cap); +} + #endif /* __MXL862XX_H */ From a74f5c19a325b2f6ad9a124ba9eddc10ad033d2c Mon Sep 17 00:00:00 2001 From: Mihai Ordean Date: Sun, 9 Aug 2026 10:12:04 +0000 Subject: [PATCH 2/3] net: dsa: mxl862xx: use the logical-index PCE rule API where available The per-port protocol trap rules (link-local, IGMP, MLDv1/v2) are written with TFLOW_PCERULEWRITE, where the rule index is a direct offset into a flow-table block the firmware pre-allocated for that CTP at init. A write past the end of that block is refused. Firmware 1.0.85 pre-allocates a smaller block than 1.0.70 did, so the higher trap offsets now fall outside it: mxl862xx mdio-bus:10: switch ready after 2150ms, firmware 1.0.85 (build 85) mxl862xx mdio-bus:10: CMD 0202 returned error -1022 mxl862xx mdio-bus:10: Unable to use tag protocol "mxl862xx-8021q": -EIO mxl862xx mdio-bus:10: probe with driver mxl862xx failed with error -5 The refusals follow the block size rather than the port type -- port 0 accepts offsets 1-4 while port 1 accepts only 1-3 -- which is what a shrunken shared block looks like, not a per-port-type restriction. The failure propagates out of mxl862xx_refresh_cpu_targets(), aborting dsa_switch_setup() and leaving the board with no user ports at all; on a BPI-R4 Pro that is the whole LAN side including the 10G SFP+ port. Firmware 1.0.83 added TFLOW_PCERULELOGICWRITE, which takes the same 466-byte struct mxl862xx_pce_rule but treats the index as logical within the region selected by region/logicalportid, and grows the underlying block on demand. That removes the dependency on a pre-allocated block size the driver never had a way to query. Add MXL862XX_CAP_PCE_LOGIC_IDX, derived at 1.0.83, and route the four trap rule writes through a helper that selects the command from it, keeping the legacy call on firmware below that. The helper logs a rejection instead of propagating it. These rules only add link-local trapping and multicast snooping, and the switch forwards without them, so a firmware that refuses them should not cost the board every user port -- that state is recoverable only over serial. Every rejection is logged rather than only the first, because the affected set is not uniform across ports and offsets. Reproduced on BPI-R4 Pro 8X, MxL86252C, firmware 1.0.85 (build 85), internal GPHYs 0.105. The same board on firmware 1.0.70 (build 70) installs all rules on all ports without error. Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 + drivers/net/dsa/mxl862xx/mxl862xx.c | 62 +++++++++++++++++++++---- drivers/net/dsa/mxl862xx/mxl862xx.h | 6 +++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h index 93da8ab336c9b..015894c8dd218 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h @@ -34,6 +34,7 @@ #define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11) #define MXL862XX_TFLOW_PCERULEWRITE (MXL862XX_TFLOW_MAGIC + 0x2) +#define MXL862XX_TFLOW_PCERULELOGICWRITE (MXL862XX_TFLOW_MAGIC + 0xa) #define MXL862XX_TFLOW_PCERULEALLOC (MXL862XX_TFLOW_MAGIC + 0x4) #define MXL862XX_TFLOW_PCERULEFREE (MXL862XX_TFLOW_MAGIC + 0x5) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index 545720ab1b322..610d2eb8ef111 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -330,6 +330,9 @@ static void mxl862xx_init_fw_caps(struct mxl862xx_priv *priv) { u32 caps = 0; + if (MXL862XX_FW_VER_MIN(priv, 1, 0, 83)) + caps |= MXL862XX_CAP_PCE_LOGIC_IDX; + if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) caps |= MXL862XX_CAP_XPCS_API | MXL862XX_CAP_SERDES_STATS; else @@ -707,18 +710,57 @@ static void mxl862xx_fill_cpu_trap_action(struct dsa_switch *ds, int port, rule->action.fid = priv->cpu_trap_fid; } +/* Install one of the per-CTP protocol trap rules. + * + * Two firmware interfaces exist for this, taking the same 466-byte + * struct mxl862xx_pce_rule. With %MXL862XX_CAP_PCE_LOGIC_IDX the + * rule index is a logical index within the region selected by + * @region and @logicalportid, and the firmware grows the underlying + * block on demand. Without it, the index is a direct offset into a + * block the firmware pre-allocated for the CTP at init, and a write + * past the end of that block is refused. + * + * The older interface is what makes firmware 1.0.85 refuse some of + * these writes with -1022: the pre-allocated block is smaller than it + * was on 1.0.70, so the higher trap offsets fall outside it, and the + * refusal pattern follows the block size rather than the port type + * (port 0 accepts offsets 1-4, port 1 accepts only 1-3). + * + * The rules installed here only add link-local trapping and IGMP/MLD + * snooping and the switch forwards normally without them, so a + * rejection is still logged rather than propagated: failing the write + * would abort mxl862xx_refresh_cpu_targets() and leave the board with + * no user ports at all. + */ +static int mxl862xx_pce_trap_write(struct mxl862xx_priv *priv, + struct mxl862xx_pce_rule *rule) +{ + u16 cmd = mxl862xx_fw_has(priv, MXL862XX_CAP_PCE_LOGIC_IDX) ? + MXL862XX_TFLOW_PCERULELOGICWRITE : + MXL862XX_TFLOW_PCERULEWRITE; + int ret; + + ret = mxl862xx_api_wrap(priv, cmd, rule, sizeof(*rule), false, true); + if (ret) + dev_warn(&priv->mdiodev->dev, + "PCE trap rule rejected (port %u index %u): %pe\n", + rule->logicalportid, + le16_to_cpu(rule->pattern.index), ERR_PTR(ret)); + + return 0; +} + /* Install a PCE rule that traps IEEE 802.1D link-local frames * (01:80:c2:00:00:0x) to the CPU port for a single user port, * preventing the hardware bridge from flooding them to other ports. * The firmware does not install this rule by default because its own * STP module is not used when DSA manages STP. * - * The rule is written into the port's per-CTP flow table at offset 1. - * The firmware already allocates a 44-entry block for every CTP during - * init (8 entries exposed initially, expandable), so no dynamic - * allocation via PCERULEALLOC is needed. Using region=CTP causes the - * firmware to translate the CTP-relative offset into an absolute - * hardware index. + * The rule is written into the port's per-CTP flow table at index 1. + * Setting region=CTP makes the firmware resolve the index against that + * port's block rather than the global table, so no dynamic allocation + * via PCERULEALLOC is needed; see mxl862xx_pce_trap_write() for how the + * index is interpreted on either firmware interface. */ static int mxl862xx_setup_link_local_trap(struct dsa_switch *ds, int port) { @@ -736,7 +778,7 @@ static int mxl862xx_setup_link_local_trap(struct dsa_switch *ds, int port) mxl862xx_fill_cpu_trap_action(ds, port, &rule); - return MXL862XX_API_WRITE(priv, MXL862XX_TFLOW_PCERULEWRITE, rule); + return mxl862xx_pce_trap_write(priv, &rule); } /* Install PCE rules that trap IGMP and MLD frames to the CPU port for @@ -772,7 +814,7 @@ static int mxl862xx_setup_snooping_traps(struct dsa_switch *ds, int port) rule.pattern.protocol = IPPROTO_IGMP; rule.pattern.protocol_enable = 1; - ret = MXL862XX_API_WRITE(priv, MXL862XX_TFLOW_PCERULEWRITE, rule); + ret = mxl862xx_pce_trap_write(priv, &rule); if (ret) return ret; @@ -791,7 +833,7 @@ static int mxl862xx_setup_snooping_traps(struct dsa_switch *ds, int port) rule.pattern.app_data_msb_enable = 1; rule.pattern.app_mask_range_msb_select = 1; /* range mode */ - ret = MXL862XX_API_WRITE(priv, MXL862XX_TFLOW_PCERULEWRITE, rule); + ret = mxl862xx_pce_trap_write(priv, &rule); if (ret) return ret; @@ -808,7 +850,7 @@ static int mxl862xx_setup_snooping_traps(struct dsa_switch *ds, int port) rule.pattern.app_data_msb_enable = 1; /* app_mask_range_msb_select = 0: nibble mask mode (default) */ - return MXL862XX_API_WRITE(priv, MXL862XX_TFLOW_PCERULEWRITE, rule); + return mxl862xx_pce_trap_write(priv, &rule); } static bool mxl862xx_is_lag_master(const struct mxl862xx_priv *priv, int port) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h index 133bab0fd182b..9755555549ed4 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx.h @@ -355,10 +355,16 @@ union mxl862xx_fw_version { * rules at init, which the driver has to * disable because it manages the flow table * itself + * %MXL862XX_CAP_PCE_LOGIC_IDX: PCE rules can be addressed by logical index + * within a region, and the firmware grows the + * underlying block on demand, instead of the + * index being a direct offset into a + * fixed-size pre-allocated block */ #define MXL862XX_CAP_XPCS_API BIT(0) #define MXL862XX_CAP_SERDES_STATS BIT(1) #define MXL862XX_CAP_FW_GLOBAL_RULES BIT(2) +#define MXL862XX_CAP_PCE_LOGIC_IDX BIT(3) /* Bit indices for struct mxl862xx_priv::flags */ #define MXL862XX_FLAG_CRC_ERR 0 From 4411ab90c3f72e02733d164363089ace7eed4b3f Mon Sep 17 00:00:00 2001 From: Mihai Ordean Date: Sat, 8 Aug 2026 15:06:22 +0000 Subject: [PATCH 3/3] net: dsa: mxl862xx: use the v2 XPCS API on firmware >= 1.0.84 Firmware 1.0.84 reshaped the XPCS PCS command family. Auditing the 1.0.85 dispatch table (234 entries) against the driver shows every command the driver sends matches the firmware's declared payload except the XPCS PCS group: PCS_CONFIG grew from 6 to 10 bytes and PCS_GET_STATE from 8 to 12 with packed little-endian mode words replacing the byte fields, PCS_ENABLE (0x1a03) and AN_DISABLE (0x1a06) were removed outright, and 0x1a07 was repurposed. Disassembling the 1.0.85 0x1a07 handler settles its semantics: it takes a packed mode word (port_id in bits 1:0, interface mode in 7:2, duplex in bit 8, USX lane mode and sub-port above) plus a speed in Mbit/s, and acts for SGMII, USXGMII and QSGMII while short-circuiting when inband AN is enabled. That is a per-link-up PCS_LINK_UP notification, not a FORCE_SPEED override: the previous 6-byte layout (u8 port_id, u8 duplex, __le16 speed) would smear the duplex byte across the lane-mode and sub-port fields. Add the v2 structures with layouts matching the firmware's declared sizes (compile-time asserted), a MXL862XX_CAP_XPCS_V2 capability derived at >= 1.0.84, and a v2 phylink_pcs_ops generation gated on it: config and get_state use the packed words, an_restart follows the same mode-word shape, link_up sends PCS_LINK_UP, and there is no pre_config, because the v2 firmware performs the (idempotent) bringup inside PCS_CONFIG and no longer implements PCS_ENABLE. PCS_DISABLE is unchanged, so the existing pcs_disable is reused. This tree drives each XPCS instance as a single lane with sub-port 0. The serdes statistics and self-test commands (0x1a0a-0x1a0d) are a separate range that survived the reshape and are untouched. Keep the CPU port on the legacy path. The firmware guards PCS_LINK_UP, AN_RESTART and SIGNAL_DETECT behind a per-instance check and refuses them, with the Zephyr -EIO (-5), for an instance that has been configured into a fixed-rate mode, which is what these negotiation commands not applying looks like from the host. Tracing the instance through bring-up shows it accepted at probe, accepted after the legacy configuration and still accepted at link up, with the signal detect status bits and the device status bank unchanged throughout, so it is PCS_CONFIG that moves it. PCS_CONFIG is not covered by that guard and reports success, while replacing the configuration the legacy path installs: the port then reports link at 10G and forwards nothing, which takes the switch off the network it is managed over. Nothing is lost by the exclusion, because a fixed link is resolved by phylink from the fixed-link configuration and its pcs_get_state() is never called. Report the configured flow control on the interface modes that carry no AN code word, as the legacy path does for every mode, so that a fixed-rate link does not resolve pause off. The Zephyr -ENOTSUP (-134) that XPCS_PCS_ENABLE draws on the BananaPi 1.0.85 build for the BPI-R4 Pro is the v2 firmware refusing a removed v1 command, not the API being absent, so the capability stays granted and only the ops generation changes. Firmwares 1.0.80 through 1.0.83 keep the existing v1 ops; earlier firmware keeps the legacy register path. All version comparisons remain confined to mxl862xx_init_fw_caps(). The 1.0.84 threshold is taken from the reshape's introduction point; only >= 1.0.85 is directly evidenced on hardware, on a board whose second XPCS instance drives a combo port routed either to an AS21xxx PHY or to an SFP cage. Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx-api.h | 151 +++++++++++ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 + drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 284 +++++++++++++++++++- drivers/net/dsa/mxl862xx/mxl862xx.c | 19 +- drivers/net/dsa/mxl862xx/mxl862xx.h | 5 + 5 files changed, 458 insertions(+), 3 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-api.h b/drivers/net/dsa/mxl862xx/mxl862xx-api.h index d107eade6d706..874d5126f1a4c 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h @@ -2749,6 +2749,157 @@ struct mxl862xx_xpcs_force_speed { __le16 result; } __packed; +/* + * Firmware >= 1.0.84 reshaped the XPCS PCS commands: PCS_CONFIG, + * PCS_GET_STATE and AN_RESTART take packed little-endian mode words + * instead of byte fields, PCS_ENABLE and AN_DISABLE were removed + * (bringup is implicit in PCS_CONFIG, power-down remains PCS_DISABLE) + * and command 0x1a07 became PCS_LINK_UP. The layouts below match the + * payload sizes the 1.0.85 dispatch table declares (10/12/4/6) and the + * field decoding of the disassembled 1.0.85 0x1a07 handler. + */ + +/** + * union mxl862xx_xpcs_an_word - XPCS AN code word, tagged by interface mode + * @cl37: 16-bit base page exchanged over the CL37 hardware AN path. + * Carries the 802.3 CL37 base page for 1000BASE-X/2500BASE-X and + * the Cisco SGMII config word for SGMII/QSGMII. + * @usx: USXGMII 16-bit AN code word, MDIO_USXGMII_* layout + * @cl73: CL73 48-bit base page (10GBASE-KR), per 802.3 Annex 28C + * @cl73.adv1: CL73 SR_AN_ADV1 / SR_AN_LP_ABL1 + * @cl73.adv2: CL73 SR_AN_ADV2 / SR_AN_LP_ABL2 + * @cl73.adv3: CL73 SR_AN_ADV3 / SR_AN_LP_ABL3 + * + * The host picks the member based on the interface field of the + * surrounding struct. + */ +union mxl862xx_xpcs_an_word { + __le16 cl37; + __le16 usx; + struct { + __le16 adv1; + __le16 adv2; + __le16 adv3; + } cl73; +} __packed; + +/* Fields of mxl862xx_xpcs_pcs_cfg_v2.mode */ +#define MXL862XX_XPCS_CFG_PORT_ID GENMASK(1, 0) +#define MXL862XX_XPCS_CFG_INTERFACE GENMASK(7, 2) +#define MXL862XX_XPCS_CFG_NEG_MODE GENMASK(9, 8) +#define MXL862XX_XPCS_CFG_PERMIT_PAUSE BIT(10) +#define MXL862XX_XPCS_CFG_USX_LANE_MODE GENMASK(12, 11) +#define MXL862XX_XPCS_CFG_ROLE BIT(13) +#define MXL862XX_XPCS_CFG_USX_SUBPORT GENMASK(15, 14) + +/* MXL862XX_XPCS_CFG_ROLE values */ +#define MXL862XX_XPCS_ROLE_MAC 0 +#define MXL862XX_XPCS_ROLE_PHY 1 + +/* usx_lane_mode values, all mode words */ +#define MXL862XX_XPCS_USX_SINGLE 0 +#define MXL862XX_XPCS_USX_QUAD 1 + +/** + * struct mxl862xx_xpcs_pcs_cfg_v2 - PCS configuration, firmware >= 1.0.84 + * @mode: packed input parameters, see MXL862XX_XPCS_CFG_*. port_id is + * the XPCS port index; interface is the PCS interface mode + * (enum mxl862xx_xpcs_if_mode); neg_mode is the negotiation mode + * (enum mxl862xx_xpcs_neg_mode); permit_pause allows pause to the + * MAC; usx_lane_mode is the USXGMII lane mode; role is the + * protocol role (MXL862XX_XPCS_ROLE_*); usx_subport selects the + * QSGMII/QUSXGMII sub-port + * @advertising: AN code word the local end transmits; member selected by + * the interface field. Ignored when the local end does not + * transmit an AN word or negotiation is not inband. + * @result: firmware result. >0 means the host must follow with an AN + * restart, 0 means no follow-up, <0 is a Zephyr errno + */ +struct mxl862xx_xpcs_pcs_cfg_v2 { + __le16 mode; + union mxl862xx_xpcs_an_word advertising; + __le16 result; +} __packed; + +static_assert(sizeof(struct mxl862xx_xpcs_pcs_cfg_v2) == 10); + +/* Fields of mxl862xx_xpcs_pcs_state_v2.mode */ +#define MXL862XX_XPCS_ST_PORT_ID GENMASK(1, 0) +#define MXL862XX_XPCS_ST_INTERFACE GENMASK(7, 2) +#define MXL862XX_XPCS_ST_USX_LANE_MODE GENMASK(9, 8) +#define MXL862XX_XPCS_ST_USX_SUBPORT GENMASK(11, 10) +#define MXL862XX_XPCS_ST_LINK BIT(12) +#define MXL862XX_XPCS_ST_AN_COMPLETE BIT(13) +#define MXL862XX_XPCS_ST_DUPLEX BIT(14) +#define MXL862XX_XPCS_ST_PCS_FAULT BIT(15) +#define MXL862XX_XPCS_ST_PAUSE GENMASK(17, 16) +#define MXL862XX_XPCS_ST_LP_EEE_CAP BIT(18) +#define MXL862XX_XPCS_ST_LP_EEE_CS_CAP BIT(19) + +/** + * struct mxl862xx_xpcs_pcs_state_v2 - PCS link state, firmware >= 1.0.84 + * @mode: packed input parameters and firmware status, see + * MXL862XX_XPCS_ST_*. The host writes port_id, interface, + * usx_lane_mode and usx_subport; the firmware fills in link, + * an_complete, duplex, pcs_fault, pause (bit 0 symmetric, bit 1 + * asymmetric), lp_eee_cap and lp_eee_cs_cap + * @speed: resolved speed in Mbit/s (output) + * @lpa: link partner ability word (output); member selected by the + * interface field + */ +struct mxl862xx_xpcs_pcs_state_v2 { + __le32 mode; + __le16 speed; + union mxl862xx_xpcs_an_word lpa; +} __packed; + +static_assert(sizeof(struct mxl862xx_xpcs_pcs_state_v2) == 12); + +/* Fields of mxl862xx_xpcs_an_restart_v2.mode */ +#define MXL862XX_XPCS_ANR_PORT_ID GENMASK(1, 0) +#define MXL862XX_XPCS_ANR_INTERFACE GENMASK(7, 2) +#define MXL862XX_XPCS_ANR_USX_LANE_MODE GENMASK(9, 8) +#define MXL862XX_XPCS_ANR_USX_SUBPORT GENMASK(11, 10) + +/** + * struct mxl862xx_xpcs_an_restart_v2 - AN restart, firmware >= 1.0.84 + * @mode: packed input parameters, see MXL862XX_XPCS_ANR_* + * @result: firmware result. 0 on success, <0 is a Zephyr errno + */ +struct mxl862xx_xpcs_an_restart_v2 { + __le16 mode; + __le16 result; +} __packed; + +static_assert(sizeof(struct mxl862xx_xpcs_an_restart_v2) == 4); + +/* Fields of mxl862xx_xpcs_pcs_link_up.mode */ +#define MXL862XX_XPCS_LU_PORT_ID GENMASK(1, 0) +#define MXL862XX_XPCS_LU_INTERFACE GENMASK(7, 2) +#define MXL862XX_XPCS_LU_DUPLEX BIT(8) +#define MXL862XX_XPCS_LU_USX_LANE_MODE GENMASK(10, 9) +#define MXL862XX_XPCS_LU_USX_SUBPORT GENMASK(12, 11) + +/** + * struct mxl862xx_xpcs_pcs_link_up - PCS link-up, firmware >= 1.0.84 + * @mode: packed input parameters, see MXL862XX_XPCS_LU_*. duplex is the + * duplex mode (enum mxl862xx_xpcs_duplex) + * @speed: resolved speed in Mbit/s + * @result: firmware result. 0 on success, <0 is a Zephyr errno + * + * Replaces FORCE_SPEED on command 0x1a07. Called once per link-up + * event after the host has resolved the line-side speed and duplex. + * The firmware acts on SGMII, USXGMII and QSGMII and succeeds without + * doing anything for the fixed-rate interface modes. + */ +struct mxl862xx_xpcs_pcs_link_up { + __le16 mode; + __le16 speed; + __le16 result; +} __packed; + +static_assert(sizeof(struct mxl862xx_xpcs_pcs_link_up) == 6); + /** * struct mxl862xx_xpcs_loopback_cfg - loopback control * @port_id: XPCS port index diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h index 015894c8dd218..6eb254a19f74e 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h @@ -96,6 +96,8 @@ #define MXL862XX_XPCS_AN_RESTART (MXL862XX_XPCS_MAGIC + 0x5) #define MXL862XX_XPCS_AN_DISABLE (MXL862XX_XPCS_MAGIC + 0x6) #define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7) +/* Same command id as FORCE_SPEED; renamed and reshaped on firmware >= 1.0.84 */ +#define MXL862XX_XPCS_PCS_LINK_UP (MXL862XX_XPCS_MAGIC + 0x7) #define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8) #define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9) #define MXL862XX_XPCS_PRBS_CFG (MXL862XX_XPCS_MAGIC + 0xa) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c index 28e119dfecf87..8fdfb689333c8 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c @@ -7,6 +7,7 @@ * Copyright (C) 2025 Daniel Golle */ +#include #include #include #include @@ -534,13 +535,294 @@ static const struct phylink_pcs_ops mxl862xx_pcs_ops = { .pcs_inband_caps = mxl862xx_pcs_inband_caps, }; +/* Ops for the reshaped XPCS API (MXL862XX_CAP_XPCS_V2, firmware >= + * 1.0.84): PCS_CONFIG/PCS_GET_STATE/AN_RESTART take packed mode words, + * PCS_ENABLE and AN_DISABLE no longer exist (the bringup is idempotent + * and implicit in PCS_CONFIG, so there is no pre_config either) and + * 0x1a07 is PCS_LINK_UP. PCS_DISABLE is unchanged, so the v1 + * pcs_disable is reused. This tree drives each XPCS as a single lane + * with sub-port 0; the quad USXGMII fields stay zero. + */ + +static int mxl862xx_xpcs_errno(int result) +{ + switch (result) { + case -5: /* Zephyr -EIO */ + return -EIO; + case -134: /* Zephyr -ENOTSUP */ + return -EOPNOTSUPP; + default: /* Zephyr -EINVAL and anything unexpected */ + return -EINVAL; + } +} + +static int mxl862xx_pcs_v2_config(struct phylink_pcs *pcs, + unsigned int neg_mode, + phy_interface_t interface, + const unsigned long *advertising, + bool permit_pause_to_mac) +{ + struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs); + struct mxl862xx_priv *priv = mpcs->priv; + struct mxl862xx_xpcs_pcs_cfg_v2 cfg = {}; + int port = mpcs->port; + int if_mode, ret, adv; + + /* Sub-interfaces are set up implicitly by the main interface */ + if (port != 9 && port != 13) + return 0; + + if_mode = mxl862xx_xpcs_if_mode(interface); + if (if_mode < 0) { + dev_err(priv->ds->dev, "unsupported interface: %s\n", + phy_modes(interface)); + return if_mode; + } + + mpcs->if_mode = if_mode; + + cfg.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_CFG_PORT_ID, + mxl862xx_xpcs_port_id(port)) | + FIELD_PREP(MXL862XX_XPCS_CFG_INTERFACE, + if_mode) | + FIELD_PREP(MXL862XX_XPCS_CFG_NEG_MODE, + mxl862xx_xpcs_neg_mode(neg_mode)) | + FIELD_PREP(MXL862XX_XPCS_CFG_USX_LANE_MODE, + MXL862XX_XPCS_USX_SINGLE) | + FIELD_PREP(MXL862XX_XPCS_CFG_ROLE, + MXL862XX_XPCS_ROLE_MAC) | + FIELD_PREP(MXL862XX_XPCS_CFG_PERMIT_PAUSE, + permit_pause_to_mac)); + + if (neg_mode & PHYLINK_PCS_NEG_INBAND) { + adv = phylink_mii_c22_pcs_encode_advertisement(interface, + advertising); + if (adv >= 0) + cfg.advertising.cl37 = cpu_to_le16(adv); + } + + ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PCS_CONFIG, cfg); + if (ret) + return ret; + + ret = (s16)le16_to_cpu(cfg.result); + if (ret < 0) + return mxl862xx_xpcs_errno(ret); + + /* result > 0 means AN restart is needed */ + return ret > 0 ? 1 : 0; +} + +/* These interface modes carry no AN code word, so there is no + * negotiated pause to decode and phylink would otherwise resolve pause + * off. Report the flow control the switch port is configured with, + * which is what the legacy register path reports for every mode. + */ +static void mxl862xx_pcs_read_flow_ctrl(struct mxl862xx_priv *priv, int port, + struct phylink_link_state *state) +{ + struct mxl862xx_port_cfg port_cfg = { + .port_id = port, + }; + + if (MXL862XX_API_READ(priv, MXL862XX_COMMON_PORTCFGGET, port_cfg)) + return; + + state->pause &= ~(MLO_PAUSE_RX | MLO_PAUSE_TX); + switch (port_cfg.flow_ctrl) { + case MXL862XX_FLOW_RXTX: + state->pause |= MLO_PAUSE_TXRX_MASK; + break; + case MXL862XX_FLOW_TX: + state->pause |= MLO_PAUSE_TX; + break; + case MXL862XX_FLOW_RX: + state->pause |= MLO_PAUSE_RX; + break; + case MXL862XX_FLOW_OFF: + default: + break; + } +} + +static void mxl862xx_pcs_v2_get_state(struct phylink_pcs *pcs, + unsigned int neg_mode, + struct phylink_link_state *state) +{ + struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs); + struct mxl862xx_priv *priv = mpcs->priv; + struct mxl862xx_xpcs_pcs_state_v2 st = {}; + int port = mpcs->port; + int if_mode, ret; + u32 mode; + u16 bmsr; + + /* phylink presets state->link = 1 before calling pcs_get_state(); + * make sure a failed firmware read reports link down instead of a + * spurious link up with SPEED_UNKNOWN. + */ + state->link = false; + + if_mode = mxl862xx_xpcs_if_mode(state->interface); + if (if_mode < 0) + return; + + st.mode = cpu_to_le32(FIELD_PREP(MXL862XX_XPCS_ST_PORT_ID, + mxl862xx_xpcs_port_id(port)) | + FIELD_PREP(MXL862XX_XPCS_ST_INTERFACE, + if_mode) | + FIELD_PREP(MXL862XX_XPCS_ST_USX_LANE_MODE, + MXL862XX_XPCS_USX_SINGLE)); + + ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PCS_GET_STATE, st); + if (ret) + return; + + mode = le32_to_cpu(st.mode); + state->link = FIELD_GET(MXL862XX_XPCS_ST_LINK, mode) && + !FIELD_GET(MXL862XX_XPCS_ST_PCS_FAULT, mode); + state->an_complete = FIELD_GET(MXL862XX_XPCS_ST_AN_COMPLETE, mode); + + switch (state->interface) { + case PHY_INTERFACE_MODE_1000BASEX: + case PHY_INTERFACE_MODE_2500BASEX: + case PHY_INTERFACE_MODE_SGMII: + case PHY_INTERFACE_MODE_QSGMII: + bmsr = (state->link ? BMSR_LSTATUS : 0) | + (state->an_complete ? BMSR_ANEGCOMPLETE : 0); + phylink_mii_c22_pcs_decode_state(state, neg_mode, bmsr, + le16_to_cpu(st.lpa.cl37)); + break; + + case PHY_INTERFACE_MODE_USXGMII: + if (state->link) + phylink_decode_usxgmii_word(state, + le16_to_cpu(st.lpa.usx)); + break; + + case PHY_INTERFACE_MODE_10GBASER: + case PHY_INTERFACE_MODE_10GKR: + if (state->link) { + state->speed = SPEED_10000; + state->duplex = DUPLEX_FULL; + mxl862xx_pcs_read_flow_ctrl(priv, port, state); + } + break; + + default: + state->link = false; + break; + } +} + +static void mxl862xx_pcs_v2_an_restart(struct phylink_pcs *pcs) +{ + struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs); + struct mxl862xx_priv *priv = mpcs->priv; + struct mxl862xx_xpcs_an_restart_v2 an = {}; + int port = mpcs->port; + + if (port != 9 && port != 13) + return; + + an.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_ANR_PORT_ID, + mxl862xx_xpcs_port_id(port)) | + FIELD_PREP(MXL862XX_XPCS_ANR_INTERFACE, + mpcs->if_mode) | + FIELD_PREP(MXL862XX_XPCS_ANR_USX_LANE_MODE, + MXL862XX_XPCS_USX_SINGLE)); + + MXL862XX_API_WRITE(priv, MXL862XX_XPCS_AN_RESTART, an); +} + +static void mxl862xx_pcs_v2_link_up(struct phylink_pcs *pcs, + unsigned int neg_mode, + phy_interface_t interface, int speed, + int duplex) +{ + struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs); + struct mxl862xx_priv *priv = mpcs->priv; + struct mxl862xx_xpcs_pcs_link_up lu = {}; + int port = mpcs->port; + int if_mode, dup; + + if (port != 9 && port != 13) + return; + + /* With inband AN the XPCS resolves speed and duplex from the + * partner's AN word itself; skip the firmware round-trip. + */ + if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) + return; + + if_mode = mxl862xx_xpcs_if_mode(interface); + if (if_mode < 0) + return; + + dup = (duplex == DUPLEX_FULL) ? MXL862XX_XPCS_DUPLEX_FULL : + MXL862XX_XPCS_DUPLEX_HALF; + + lu.mode = cpu_to_le16(FIELD_PREP(MXL862XX_XPCS_LU_PORT_ID, + mxl862xx_xpcs_port_id(port)) | + FIELD_PREP(MXL862XX_XPCS_LU_INTERFACE, + if_mode) | + FIELD_PREP(MXL862XX_XPCS_LU_USX_LANE_MODE, + MXL862XX_XPCS_USX_SINGLE) | + FIELD_PREP(MXL862XX_XPCS_LU_DUPLEX, dup)); + lu.speed = cpu_to_le16(speed); + + MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_LINK_UP, lu); +} + +static unsigned int mxl862xx_pcs_v2_inband_caps(struct phylink_pcs *pcs, + phy_interface_t interface) +{ + switch (interface) { + case PHY_INTERFACE_MODE_SGMII: + case PHY_INTERFACE_MODE_QSGMII: + case PHY_INTERFACE_MODE_1000BASEX: + case PHY_INTERFACE_MODE_2500BASEX: + return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE; + case PHY_INTERFACE_MODE_USXGMII: + case PHY_INTERFACE_MODE_10GKR: + return LINK_INBAND_ENABLE; + case PHY_INTERFACE_MODE_10GBASER: + return LINK_INBAND_DISABLE; + default: + return 0; + } +} + +static const struct phylink_pcs_ops mxl862xx_pcs_v2_ops = { + .pcs_disable = mxl862xx_pcs_disable, + .pcs_config = mxl862xx_pcs_v2_config, + .pcs_get_state = mxl862xx_pcs_v2_get_state, + .pcs_an_restart = mxl862xx_pcs_v2_an_restart, + .pcs_link_up = mxl862xx_pcs_v2_link_up, + .pcs_inband_caps = mxl862xx_pcs_v2_inband_caps, +}; + void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs, int port) { pcs->priv = priv; pcs->port = port; - if (mxl862xx_fw_has(priv, MXL862XX_CAP_XPCS_API)) + /* Keep the CPU port on the legacy path. Its link is fixed, so + * phylink resolves it from the fixed-link configuration and never + * calls pcs_get_state() for it, and the firmware refuses the + * negotiation commands for an instance configured into a + * fixed-rate mode, so the XPCS API adds nothing there. It also + * costs: PCS_CONFIG is not covered by that refusal and replaces + * the configuration the legacy path installs, after which the + * port keeps reporting link at 10G and forwards nothing, which + * takes the switch off the network it is managed over. + */ + if (dsa_is_cpu_port(priv->ds, port)) + pcs->pcs.ops = &mxl862xx_legacy_pcs_ops; + else if (mxl862xx_fw_has(priv, MXL862XX_CAP_XPCS_V2)) + pcs->pcs.ops = &mxl862xx_pcs_v2_ops; + else if (mxl862xx_fw_has(priv, MXL862XX_CAP_XPCS_API)) pcs->pcs.ops = &mxl862xx_pcs_ops; else pcs->pcs.ops = &mxl862xx_legacy_pcs_ops; diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index 610d2eb8ef111..5bde382a82902 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -333,10 +333,25 @@ static void mxl862xx_init_fw_caps(struct mxl862xx_priv *priv) if (MXL862XX_FW_VER_MIN(priv, 1, 0, 83)) caps |= MXL862XX_CAP_PCE_LOGIC_IDX; - if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) + if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) { caps |= MXL862XX_CAP_XPCS_API | MXL862XX_CAP_SERDES_STATS; - else + + /* Firmware 1.0.84 reshaped the XPCS PCS commands: the + * PCS_CONFIG and PCS_GET_STATE payloads changed layout + * and size, PCS_ENABLE and AN_DISABLE were removed (the + * bringup became implicit in PCS_CONFIG) and FORCE_SPEED + * became PCS_LINK_UP. The Zephyr -ENOTSUP (-134) that + * XPCS_PCS_ENABLE draws on the BananaPi 1.0.85 build is + * the v2 firmware refusing a removed v1 command, not the + * API being absent: 0x1a03 and 0x1a06 are missing from + * the 1.0.85 dispatch table while the v2 payload sizes + * match it exactly. Select the matching ops generation. + */ + if (MXL862XX_FW_VER_MIN(priv, 1, 0, 84)) + caps |= MXL862XX_CAP_XPCS_V2; + } else { caps |= MXL862XX_CAP_FW_GLOBAL_RULES; + } priv->fw_caps = caps; } diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h index 9755555549ed4..1f154b25bb2d7 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx.h @@ -360,11 +360,16 @@ union mxl862xx_fw_version { * underlying block on demand, instead of the * index being a direct offset into a * fixed-size pre-allocated block + * %MXL862XX_CAP_XPCS_V2: the XPCS PCS commands take the reshaped + * (firmware >= 1.0.84) payloads: packed mode + * words, PCS_ENABLE and AN_DISABLE removed, + * and PCS_LINK_UP in place of FORCE_SPEED */ #define MXL862XX_CAP_XPCS_API BIT(0) #define MXL862XX_CAP_SERDES_STATS BIT(1) #define MXL862XX_CAP_FW_GLOBAL_RULES BIT(2) #define MXL862XX_CAP_PCE_LOGIC_IDX BIT(3) +#define MXL862XX_CAP_XPCS_V2 BIT(4) /* Bit indices for struct mxl862xx_priv::flags */ #define MXL862XX_FLAG_CRC_ERR 0