From nobody Sat Jul 25 00:15:11 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 80700472F9C; Tue, 21 Jul 2026 23:58:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678327; cv=none; b=A3BfNSST4bxlBdcno0fCTH4LPy+ZIIHKrGQYDi0YkRJp4vCRhhxbK8//6DOdfcCaj4hPTUrwr/jt/oPytNzkJkrAhyOOQyYKtfaxpSF7bXcdQxNY2cTLrM3uYCpvDkbPYVbJ2eQ5x7p2iOLqaVM2HXt147Q9TGfedl8Ex/yXJq0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678327; c=relaxed/simple; bh=fGKmP1kw06r9IBjtGa6JSP7OLwIr80h91ZbPRACGYsE=; h=Date:From:To:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=shguiRhPFCS+y2F4C0JJ5DKbCDALAR7RWAwVIx0ZboLrbWeONaLK/XjlPXgLBycJMENqNiYobartHyE9llgHg0VWV1JYEJm5GO8xXStDI5pYZNpbtaKezS0PInZ/2g0bzZtyhA5Sh/HoFKGfGlXhCKJUk/5i4Df4vDUtQA8pAJ4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wmKMD-000000008EH-1D5M; Tue, 21 Jul 2026 23:58:41 +0000 Date: Wed, 22 Jul 2026 00:58:38 +0100 From: Daniel Golle To: Daniel Golle , Andrew Lunn , Vladimir Oltean , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , linux-kernel@vger.kernel.org, netdev@vger.kernel.org Subject: [PATCH net-next v4 1/4] net: dsa: wire flash_update devlink callback to drivers Message-ID: <52db7048c2836a36182839916578636d1560f6b5.1784665017.git.daniel@makrotopia.org> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a devlink_flash_update callback to dsa_switch_ops so that DSA drivers can support devlink dev flash without open-coding the devlink plumbing. Unlike the other trampolines in net/dsa/devlink.c, the flash_update op is only installed for switches whose driver implements the callback: the devlink core rejects flash requests up front when the op is absent, before fetching the firmware file from userspace, and an unconditionally present trampoline would defeat that early check and let unsupported requests block on request_firmware() only to fail with -EOPNOTSUPP afterwards. Signed-off-by: Daniel Golle --- v4: only install the flash_update op for drivers implementing the callback so the devlink core keeps rejecting unsupported flash requests before fetching the firmware file v3: no changes v2: align continuation lines with the open parenthesis include/net/dsa.h | 3 +++ net/dsa/devlink.c | 50 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/include/net/dsa.h b/include/net/dsa.h index 8c16ef23cc10..c9e19348de61 100644 --- a/include/net/dsa.h +++ b/include/net/dsa.h @@ -1170,6 +1170,9 @@ struct dsa_switch_ops { int (*devlink_info_get)(struct dsa_switch *ds, struct devlink_info_req *req, struct netlink_ext_ack *extack); + int (*devlink_flash_update)(struct dsa_switch *ds, + struct devlink_flash_update_params *params, + struct netlink_ext_ack *extack); int (*devlink_sb_pool_get)(struct dsa_switch *ds, unsigned int sb_index, u16 pool_index, struct devlink_sb_pool_info *pool_info); diff --git a/net/dsa/devlink.c b/net/dsa/devlink.c index ed342f345692..d6022267a839 100644 --- a/net/dsa/devlink.c +++ b/net/dsa/devlink.c @@ -20,6 +20,15 @@ static int dsa_devlink_info_get(struct devlink *dl, return -EOPNOTSUPP; } =20 +static int dsa_devlink_flash_update(struct devlink *dl, + struct devlink_flash_update_params *params, + struct netlink_ext_ack *extack) +{ + struct dsa_switch *ds =3D dsa_devlink_to_ds(dl); + + return ds->ops->devlink_flash_update(ds, params, extack); +} + static int dsa_devlink_sb_pool_get(struct devlink *dl, unsigned int sb_index, u16 pool_index, struct devlink_sb_pool_info *pool_info) @@ -167,18 +176,31 @@ dsa_devlink_sb_occ_tc_port_bind_get(struct devlink_po= rt *dlp, p_max); } =20 -static const struct devlink_ops dsa_devlink_ops =3D { - .info_get =3D dsa_devlink_info_get, - .sb_pool_get =3D dsa_devlink_sb_pool_get, - .sb_pool_set =3D dsa_devlink_sb_pool_set, - .sb_port_pool_get =3D dsa_devlink_sb_port_pool_get, - .sb_port_pool_set =3D dsa_devlink_sb_port_pool_set, - .sb_tc_pool_bind_get =3D dsa_devlink_sb_tc_pool_bind_get, - .sb_tc_pool_bind_set =3D dsa_devlink_sb_tc_pool_bind_set, - .sb_occ_snapshot =3D dsa_devlink_sb_occ_snapshot, - .sb_occ_max_clear =3D dsa_devlink_sb_occ_max_clear, - .sb_occ_port_pool_get =3D dsa_devlink_sb_occ_port_pool_get, +/* The devlink core rejects flash requests up front when the flash_update + * op is absent, before fetching the firmware file from userspace. Only + * install the op for switches whose driver implements it, so that + * unsupported requests keep failing early. + */ +#define DSA_DEVLINK_OPS \ + .info_get =3D dsa_devlink_info_get, \ + .sb_pool_get =3D dsa_devlink_sb_pool_get, \ + .sb_pool_set =3D dsa_devlink_sb_pool_set, \ + .sb_port_pool_get =3D dsa_devlink_sb_port_pool_get, \ + .sb_port_pool_set =3D dsa_devlink_sb_port_pool_set, \ + .sb_tc_pool_bind_get =3D dsa_devlink_sb_tc_pool_bind_get, \ + .sb_tc_pool_bind_set =3D dsa_devlink_sb_tc_pool_bind_set, \ + .sb_occ_snapshot =3D dsa_devlink_sb_occ_snapshot, \ + .sb_occ_max_clear =3D dsa_devlink_sb_occ_max_clear, \ + .sb_occ_port_pool_get =3D dsa_devlink_sb_occ_port_pool_get, \ .sb_occ_tc_port_bind_get =3D dsa_devlink_sb_occ_tc_port_bind_get, + +static const struct devlink_ops dsa_devlink_ops =3D { + DSA_DEVLINK_OPS +}; + +static const struct devlink_ops dsa_devlink_flash_ops =3D { + DSA_DEVLINK_OPS + .flash_update =3D dsa_devlink_flash_update, }; =20 int dsa_devlink_param_get(struct devlink *dl, u32 id, @@ -378,12 +400,16 @@ void dsa_switch_devlink_unregister(struct dsa_switch = *ds) int dsa_switch_devlink_alloc(struct dsa_switch *ds) { struct dsa_devlink_priv *dl_priv; + const struct devlink_ops *ops; struct devlink *dl; =20 + ops =3D ds->ops->devlink_flash_update ? &dsa_devlink_flash_ops + : &dsa_devlink_ops; + /* Add the switch to devlink before calling setup, so that setup can * add dpipe tables */ - dl =3D devlink_alloc(&dsa_devlink_ops, sizeof(*dl_priv), ds->dev); + dl =3D devlink_alloc(ops, sizeof(*dl_priv), ds->dev); if (!dl) return -ENOMEM; =20 --=20 2.55.0 From nobody Sat Jul 25 00:15:11 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D5E58476054; Tue, 21 Jul 2026 23:58:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678336; cv=none; b=R6GEqLWtjXtoL8nY45ix7GHdUuTcPBG1IKO1iU6KVRy7IuepcJmrlO6rkgA9ddznnK/Z8oAvzi8r1wNxAat2CP0vP+r050BFulqjM+ReKUA2rrS2pbF9iYdxkMAZKv+Gn1We/6ttVIhfP4Df2l882GxZizK+gwVW4h7CG1Dib+k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678336; c=relaxed/simple; bh=hGP3X4Vdqt2WRZf9Vlef5kHl6Q4E28ipST2yoCDcqTQ=; h=Date:From:To:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=g6XOw67AgfA/SVwKP/TXV0BFB7irkU/Yx3sSDXat44avjp2BhD1UWGYuEasBlLxuoTwn/I7sZFdOzgn1E8MtfiRPc/3Wof16jXavqYv55OwrRNGl6TrXn3eLK0oyXtm65T0wyg5uE7EeplrXvqtBZ4FBHX1dYEZNRRY51WTCaaE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wmKMM-000000008Eb-4BMv; Tue, 21 Jul 2026 23:58:51 +0000 Date: Wed, 22 Jul 2026 00:58:48 +0100 From: Daniel Golle To: Daniel Golle , Andrew Lunn , Vladimir Oltean , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , linux-kernel@vger.kernel.org, netdev@vger.kernel.org Subject: [PATCH net-next v4 2/4] net: dsa: mxl862xx: add SMDIO clause-22 register access Message-ID: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add mxl862xx_smdio_read() and mxl862xx_smdio_write() for clause-22 SMDIO register access. MCUboot rescue mode only exposes clause-22 registers; the existing clause-45 MMD interface is unavailable during firmware transfer. The MDIO bus lock is held per-transaction (not across polls) so that SB PDI polling during flash erase does not starve other non-switch users of the same MDIO bus, such as separate PHYs providing WAN or management interfaces. Unlike mxl862xx_api_wrap(), which takes the bus lock with MDIO_MUTEX_NESTED because it can be entered from the accessors of the switch-internal MDIO bus while that bus's lock of the same lock class is already held, the SMDIO helpers take it with a plain mutex_lock(). They are only called from probe and devlink flash contexts where no other MDIO bus lock can be held. Signed-off-by: Daniel Golle --- v4: no changes v3: explain the plain mutex_lock() vs MDIO_MUTEX_NESTED choice in the commit message v2: clarify in the commit message that the per-transaction bus locking is about unrelated non-switch devices on the same MDIO bus (Andrew Lunn) drivers/net/dsa/mxl862xx/mxl862xx-host.c | 35 ++++++++++++++++++++++++ drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 ++ 2 files changed, 37 insertions(+) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl= 862xx/mxl862xx-host.c index 4acd216f7cc0..6e582caea1fa 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c @@ -495,6 +495,41 @@ int mxl862xx_reset(struct mxl862xx_priv *priv) return ret; } =20 +#define MXL862XX_SMDIO_ADDR_REG 0x1f +#define MXL862XX_SMDIO_PAGE_MASK 0xfff0 +#define MXL862XX_SMDIO_OFF_MASK 0x000f + +int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr) +{ + struct mii_bus *bus =3D priv->mdiodev->bus; + int phy =3D priv->mdiodev->addr; + int ret; + + mutex_lock(&bus->mdio_lock); + ret =3D __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG, + addr & MXL862XX_SMDIO_PAGE_MASK); + if (ret >=3D 0) + ret =3D __mdiobus_read(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK); + mutex_unlock(&bus->mdio_lock); + return ret; +} + +int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val) +{ + struct mii_bus *bus =3D priv->mdiodev->bus; + int phy =3D priv->mdiodev->addr; + int ret; + + mutex_lock(&bus->mdio_lock); + ret =3D __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG, + addr & MXL862XX_SMDIO_PAGE_MASK); + if (ret >=3D 0) + ret =3D __mdiobus_write(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK, + val); + mutex_unlock(&bus->mdio_lock); + return ret; +} + void mxl862xx_host_init(struct mxl862xx_priv *priv) { INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.h b/drivers/net/dsa/mxl= 862xx/mxl862xx-host.h index 66d6ae198aff..4e054c6e4c0e 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h @@ -18,5 +18,7 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd= , void *data, u16 size, mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true) =20 int mxl862xx_reset(struct mxl862xx_priv *priv); +int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr); +int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val); =20 #endif /* __MXL862XX_HOST_H */ --=20 2.55.0 From nobody Sat Jul 25 00:15:11 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9AC5A327BFB; Wed, 22 Jul 2026 00:00:48 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678453; cv=none; b=PzCG/ROuB6aOMKMa8gLd7hZE631ih09BGUHpGc92jUk1oFQydu+6p16NzzKQyphSt0tTK9cPmh+SNm+jY3LYRN3Cz8FBYuBnB2UX3vfMAVxGQbFV168grl+jrmqx/j65Wi9h868F0/BE/lqqa9GhSg5vYS5O4xvT5jKmFe5mYHQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678453; c=relaxed/simple; bh=R/Qp+TQnSz0vWymEMOM8Qv1v7JCXKccTzut6R0e+kkI=; h=Date:From:To:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=Jge4heifhlDTNChhI8RY4405BJtX36pqfT3noyfYBNt7+Wchg9eUgJO5AIfAqLQN+5DQkaKHNU8qBSEvo0Ro7rTa4rm4DXmxigub36GKHRnMAQJK3k4XOY4pxzQ1KBQnhxJLbLLbux+ncb+ucDqpdacXRCZlX34sPyT/wJZfwtU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wmKOA-000000000Go-3hX5; Wed, 22 Jul 2026 00:00:43 +0000 Date: Wed, 22 Jul 2026 01:00:40 +0100 From: Daniel Golle To: Daniel Golle , Andrew Lunn , Vladimir Oltean , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , linux-kernel@vger.kernel.org, netdev@vger.kernel.org Subject: [PATCH net-next v4 3/4] net: dsa: mxl862xx: add devlink flash_update and info_get Message-ID: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Implement runtime firmware upgrade via "devlink dev flash" and version reporting via "devlink dev info": devlink dev info mdio_bus// devlink dev flash mdio_bus// file The "asic.id" fixed version reports a per-model chip name taken from the OF match data, e.g. "MaxLinear MxL86252", following the mv88e6xxx example. Userspace such as fwupd should build firmware matching identifiers from this value. The firmware image is validated first, including both payload CRCs, so a malformed file is rejected without disturbing the running switch. The driver then sends SYS_MISC_FW_UPDATE to enter MCUboot rescue mode and transfers the signed image over the SB PDI bulk-transfer protocol (clause-22 SMDIO). Every SMDIO write during the transfer is checked; in particular a failed address write during the half-bank switch would otherwise place the second half of the payload at the wrong flash offset without any status poll noticing. Once the transfer has finished and the switch has rebooted, the driver launches device_reprobe() for a clean remove()+probe() cycle. Before the transfer begins the driver closes all user and conduit interfaces and additionally marks the user ports not-present via netif_device_detach() so that userspace cannot bring them back up during the flash and reprobe cycle, which takes just under a minute. The conduit belongs to the MAC driver and is only closed. Closing the ports also stops phylib from polling the switch-internal PHYs, whose firmware-relayed MDIO access is unavailable while the switch is in MCUboot mode. The bridge defers the STP state changes triggered by closing its ports, so the driver flushes the switchdev deferred queue while still holding the rtnl lock; the DISABLED transitions thus reach the firmware while it is still running instead of failing against the host block later. Progress is reported through devlink status notifications. While the update runs, firmware API commands from any other path are rejected. The blocking flags are read and written under the MDIO bus lock so that a command which had already passed the check cannot reach the bus once the switch has rebooted into MCUboot. The block is never lifted again; SYS_MISC_FW_VERSION is exempt from it so the new firmware version can be queried after a successful transfer, and the version query is only issued from the flash path itself. The periodic stats poll and the CRC error handler are stopped before the transfer starts. After the transfer, API read commands fail with -ENODEV while write commands return fake success: the scheduled reprobe tears the driver down through remove(), whose teardown writes must not stall on the absent firmware, but read paths such as an FDB dump must not consume buffers the firmware never filled -- port_fdb_dump() would loop forever on such fabricated data. port_mdb_del() maps -ENODEV from its lookup to successful deletion: the firmware holding the entry is gone, and reporting failure would only make the DSA core keep the software entry and report leftover host MDB addresses when the tree is torn down for the reprobe. Once the FW_UPDATE command has been sent the switch is in MCUboot mode and normal operation can only be restored by a reprobe, so the reprobe kernel thread and the module and device references it holds are set up before the switch is disturbed and the thread is woken regardless of transfer outcome. The reprobe runs in a kernel thread rather than a work item because device_reprobe() triggers remove() which frees the devm-managed priv, and a work item's final module_put() would still execute module code afterwards, racing against module unload. The thread instead exits through module_put_and_kthread_exit(), which drops the reference from core-kernel code without returning to module text. Signed-off-by: Daniel Golle --- v4: - run the deferred reprobe from a kernel thread ending in module_put_and_kthread_exit() instead of a work item whose final module_put() raced against module unload - fail API read commands with -ENODEV after the update instead of faking success with an unfilled buffer, which sent port_fdb_dump() into an endless loop - keep block_host set across the post-update version query by exempting SYS_MISC_FW_VERSION instead of briefly lifting the block, and write the blocking flags under the MDIO bus lock - check the return value of every SB PDI control write; a failed address write during the half-bank switch could place the second half of the payload at the wrong flash offset undetected - report SMDIO write failures through one shared error path instead of per-site messages - initialise the progress notification deadline from jiffies so notifications are not suppressed on 32-bit shortly after boot - flush the switchdev deferred queue after closing the ports so the bridge's deferred STP DISABLED transitions reach the firmware while it is still running instead of failing with -EBUSY against the host block - treat -ENODEV as successful deletion in port_mdb_del() so the post-update teardown does not leave leftover host MDB entries behind for the DSA core to report v3: - validate the image, including both CRCs, before closing any ports so a malformed file no longer triggers a flash and reprobe cycle - reject images whose declared payload sizes overflow when summed (check_add_overflow) or sum up to zero; the latter used to erase the flash without writing anything back - allocate the reprobe work item and take the module and device references before disturbing the switch instead of silently skipping the reprobe when the allocation fails afterwards - check block_host/skip_teardown under the MDIO bus lock to close the window where a command already past the check could reach the bus after the switch rebooted into MCUboot - prevent the stats poll work from being re-armed and cancel the CRC error work before the transfer - check the return value of SB PDI data word writes; control writes are verified by the subsequent status polls - report a per-model chip name from the OF match data as "asic.id" instead of the devicetree compatible string whose comma is awkward for userspace consumers (Andrew Lunn) - commit message: the conduit is only closed, not detached v2: - factor out SB PDI slice flush and devlink status notification helpers, resolving checkpatch issues - use kzalloc_obj() (Manuel Ebner) - add kernel-doc for the new mxl862xx_priv members - trim comments and state the actual duration of a flash and reprobe cycle, just under a minute (Manuel Ebner) - reword commit message: split up run-on sentence, explain the dynamically allocated reprobe work item (Manuel Ebner), mention that closing the ports stops phylib polling (Andrew Lunn) drivers/net/dsa/mxl862xx/Makefile | 2 +- drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 + drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 497 +++++++++++++++++++++++ drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 15 + drivers/net/dsa/mxl862xx/mxl862xx-host.c | 12 + drivers/net/dsa/mxl862xx/mxl862xx.c | 12 +- drivers/net/dsa/mxl862xx/mxl862xx.h | 6 + 7 files changed, 542 insertions(+), 3 deletions(-) create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h diff --git a/drivers/net/dsa/mxl862xx/Makefile b/drivers/net/dsa/mxl862xx/M= akefile index a7be0e6669df..bccac0d0f703 100644 --- a/drivers/net/dsa/mxl862xx/Makefile +++ b/drivers/net/dsa/mxl862xx/Makefile @@ -1,3 +1,3 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_NET_DSA_MXL862) +=3D mxl862xx_dsa.o -mxl862xx_dsa-y :=3D mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o +mxl862xx_dsa-y :=3D mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o mxl862xx= -fw.o diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl8= 62xx/mxl862xx-cmd.h index c87a955c13c4..e2aa2934e9e1 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h @@ -70,6 +70,7 @@ #define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1) #define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2) =20 +#define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1) #define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2) =20 #define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl86= 2xx/mxl862xx-fw.c new file mode 100644 index 000000000000..4cd8231c7eca --- /dev/null +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Firmware flash and devlink support for MaxLinear MxL862xx + * + * Copyright (C) 2025 Daniel Golle + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mxl862xx.h" +#include "mxl862xx-api.h" +#include "mxl862xx-cmd.h" +#include "mxl862xx-fw.h" +#include "mxl862xx-host.h" + +/* SB PDI registers (clause-22 SMDIO address space) */ +#define MXL862XX_SB_PDI_CTRL 0xe100 +#define MXL862XX_SB_PDI_ADDR 0xe101 +#define MXL862XX_SB_PDI_DATA 0xe102 +#define MXL862XX_SB_PDI_STAT 0xe103 + +/* SB PDI CTRL modes */ +#define MXL862XX_SB_PDI_CTRL_RST 0x00 +#define MXL862XX_SB_PDI_CTRL_WR 0x02 + +/* SB PDI handshake magic */ +#define MXL862XX_SB_PDI_READY 0xc55c +#define MXL862XX_SB_PDI_START 0xf48f +#define MXL862XX_SB_PDI_END 0x3cc3 + +/* Firmware transfer geometry */ +#define MXL862XX_FW_HDR_SIZE 20 +#define MXL862XX_FW_BANK_HALF 16384 /* words per half-bank */ +#define MXL862XX_FW_BANK_SLICE 32760 /* words per full slice */ +#define MXL862XX_FW_SB1_ADDR 0x7800 /* SB1 word address */ + +/* Timeouts (generous upper bounds) */ +#define MXL862XX_FW_READY_TIMEOUT_MS 30000 +#define MXL862XX_FW_ACK_TIMEOUT_MS 5000 +#define MXL862XX_FW_ERASE_TIMEOUT_MS 300000 +#define MXL862XX_FW_WRITE_TIMEOUT_MS 120000 +#define MXL862XX_FW_REBOOT_DELAY_MS 5000 +#define MXL862XX_FW_REPROBE_DELAY_MS 500 + +static int mxl862xx_sb_pdi_reset(struct mxl862xx_priv *priv) +{ + int ret; + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_RST); + if (ret < 0) + return ret; + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR, + MXL862XX_SB_PDI_CTRL_RST); + if (ret < 0) + return ret; + + return mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, + MXL862XX_SB_PDI_CTRL_RST); +} + +static int mxl862xx_sb_pdi_poll_stat(struct mxl862xx_priv *priv, u16 expec= ted, + unsigned long timeout_ms) +{ + unsigned long timeout =3D jiffies + msecs_to_jiffies(timeout_ms); + int ret; + + do { + ret =3D mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT); + if (ret < 0) + return ret; + if ((u16)ret =3D=3D expected) + return 0; + usleep_range(10000, 11000); + } while (time_before(jiffies, timeout)); + + return -ETIMEDOUT; +} + +static int mxl862xx_sb_pdi_flush_slice(struct mxl862xx_priv *priv, + u32 data_written) +{ + int ret; + + ret =3D mxl862xx_sb_pdi_reset(priv); + if (ret < 0) + return ret; + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, data_written); + if (ret < 0) + return ret; + + return mxl862xx_sb_pdi_poll_stat(priv, 0, + MXL862XX_FW_WRITE_TIMEOUT_MS); +} + +static void mxl862xx_flash_notify(struct devlink *dl, const char *status, + u32 done, u32 total) +{ + devlink_flash_update_status_notify(dl, status, NULL, done, total); +} + +/* device_reprobe() -> remove() frees priv while the thread runs, so + * the thread must not dereference priv; it drops its module reference + * without returning to module code afterwards. + */ +static int mxl862xx_reprobe_thread_fn(void *data) +{ + struct device *dev =3D data; + + msleep(MXL862XX_FW_REPROBE_DELAY_MS); + if (device_reprobe(dev)) + dev_err(dev, "reprobe failed\n"); + put_device(dev); + module_put_and_kthread_exit(0); +} + +/* MCUboot firmware image header */ +struct mxl862xx_fw_hdr { + __le32 image_type; + __le32 image_size_1; + __le32 image_checksum_1; + __le32 image_size_2; + __le32 image_checksum_2; +} __packed; + +static int mxl862xx_flash_validate(struct mxl862xx_priv *priv, + const struct firmware *fw, + u32 *payload_size) +{ + const struct mxl862xx_fw_hdr *hdr; + u32 size1, size2, total; + const u8 *payload; + u32 crc; + + if (fw->size < MXL862XX_FW_HDR_SIZE) + return -EINVAL; + + hdr =3D (const struct mxl862xx_fw_hdr *)fw->data; + payload =3D fw->data + MXL862XX_FW_HDR_SIZE; + size1 =3D le32_to_cpu(hdr->image_size_1); + size2 =3D le32_to_cpu(hdr->image_size_2); + + if (check_add_overflow(size1, size2, &total) || + total > fw->size - MXL862XX_FW_HDR_SIZE) { + dev_err(&priv->mdiodev->dev, + "flash: firmware file too small for declared size\n"); + return -EINVAL; + } + + if (!total) { + dev_err(&priv->mdiodev->dev, + "flash: firmware file with empty payload\n"); + return -EINVAL; + } + + if (size1) { + crc =3D ~crc32_le(~0U, payload, size1); + if (crc !=3D le32_to_cpu(hdr->image_checksum_1)) { + dev_err(&priv->mdiodev->dev, + "flash: image 1 CRC mismatch (got %08x, expected %08x)\n", + crc, le32_to_cpu(hdr->image_checksum_1)); + return -EINVAL; + } + } + + if (size2) { + crc =3D ~crc32_le(~0U, payload + size1, size2); + if (crc !=3D le32_to_cpu(hdr->image_checksum_2)) { + dev_err(&priv->mdiodev->dev, + "flash: image 2 CRC mismatch (got %08x, expected %08x)\n", + crc, le32_to_cpu(hdr->image_checksum_2)); + return -EINVAL; + } + } + + *payload_size =3D total; + + return 0; +} + +static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv, + const struct firmware *fw, + u32 payload_size, struct devlink *dl) +{ + const u8 *payload =3D fw->data + MXL862XX_FW_HDR_SIZE; + u32 word_idx =3D 0, data_written =3D 0, idx =3D 0; + unsigned long next_notify =3D jiffies - 1; + u16 word, fdata; + int ret, i; + + /* Step 1: reboot the firmware into MCUboot rescue mode */ + ret =3D mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0, + false, false); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: FW_UPDATE command failed: %pe\n", + ERR_PTR(ret)); + return ret; + } + + /* Failures from here on must go through end_magic so MCUboot + * reboots instead of waiting forever. + */ + + /* Step 2: wait for bootloader ready */ + mxl862xx_flash_notify(dl, "Waiting for bootloader", 0, 0); + ret =3D mxl862xx_sb_pdi_reset(priv); + if (ret < 0) + goto write_err; + + ret =3D mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_READY, + MXL862XX_FW_READY_TIMEOUT_MS); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: bootloader not ready: %pe\n", ERR_PTR(ret)); + goto end_magic; + } + + /* Step 3: start handshake */ + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, + MXL862XX_SB_PDI_START); + if (ret < 0) + goto write_err; + + ret =3D mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_START + 1, + MXL862XX_FW_ACK_TIMEOUT_MS); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: start handshake failed: %pe\n", ERR_PTR(ret)); + goto end_magic; + } + + /* Step 4: transfer image header */ + mxl862xx_flash_notify(dl, "Erasing flash", 0, 0); + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_WR); + if (ret < 0) + goto write_err; + + for (i =3D 0; i < MXL862XX_FW_HDR_SIZE / 2; i++) { + word =3D fw->data[i * 2] | + ((u16)fw->data[i * 2 + 1] << 8); + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, word); + if (ret < 0) + goto write_err; + } + + ret =3D mxl862xx_sb_pdi_reset(priv); + if (ret < 0) + goto write_err; + + /* the byte count in STAT triggers the erase */ + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, + MXL862XX_FW_HDR_SIZE); + if (ret < 0) + goto write_err; + + /* ACK is byte count + 1 */ + ret =3D mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_FW_HDR_SIZE + 1, + MXL862XX_FW_ACK_TIMEOUT_MS); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: header ACK failed: %pe\n", ERR_PTR(ret)); + goto end_magic; + } + + /* Step 5: wait for erase to complete */ + ret =3D mxl862xx_sb_pdi_poll_stat(priv, 0, + MXL862XX_FW_ERASE_TIMEOUT_MS); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: erase timeout: %pe\n", ERR_PTR(ret)); + goto end_magic; + } + + /* Step 6: transfer payload */ + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_WR); + if (ret < 0) + goto write_err; + + while (idx < payload_size) { + if (idx + 1 < payload_size) { + fdata =3D payload[idx] | + ((u16)payload[idx + 1] << 8); + idx +=3D 2; + data_written +=3D 2; + } else { + fdata =3D payload[idx]; + idx++; + data_written++; + } + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, fdata); + if (ret < 0) + goto write_err; + word_idx++; + + if (idx >=3D payload_size) { + ret =3D mxl862xx_sb_pdi_flush_slice(priv, data_written); + break; + } + + /* Half-bank boundary: switch to SB1 address */ + if (word_idx =3D=3D MXL862XX_FW_BANK_HALF) { + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_RST); + if (ret < 0) + goto write_err; + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR, + MXL862XX_FW_SB1_ADDR); + if (ret < 0) + goto write_err; + + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_WR); + if (ret < 0) + goto write_err; + } else if (word_idx >=3D MXL862XX_FW_BANK_SLICE) { + ret =3D mxl862xx_sb_pdi_flush_slice(priv, data_written); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: write timeout at %u/%u: %pe\n", + idx, payload_size, ERR_PTR(ret)); + goto end_magic; + } + word_idx =3D 0; + data_written =3D 0; + ret =3D mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL, + MXL862XX_SB_PDI_CTRL_WR); + if (ret < 0) + goto write_err; + + if (time_after(jiffies, next_notify)) { + mxl862xx_flash_notify(dl, "Flashing", idx, + payload_size); + next_notify =3D jiffies + msecs_to_jiffies(500); + } + } + } + + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: final write timeout: %pe\n", ERR_PTR(ret)); + goto end_magic; + } + + mxl862xx_flash_notify(dl, "Flashing", payload_size, payload_size); + goto end_magic; + +write_err: + dev_err(&priv->mdiodev->dev, "flash: SMDIO write failed: %pe\n", + ERR_PTR(ret)); +end_magic: + /* reboot MCUboot even after a failed transfer */ + mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, + MXL862XX_SB_PDI_END); + msleep(MXL862XX_FW_REBOOT_DELAY_MS); + + return ret; +} + +int mxl862xx_devlink_info_get(struct dsa_switch *ds, + struct devlink_info_req *req, + struct netlink_ext_ack *extack) +{ + struct mxl862xx_priv *priv =3D ds->priv; + const char *model; + char ver_str[32]; + int ret; + + model =3D device_get_match_data(ds->dev); + if (model) { + ret =3D devlink_info_version_fixed_put(req, + DEVLINK_INFO_VERSION_GENERIC_ASIC_ID, + model); + if (ret) + return ret; + } + + snprintf(ver_str, sizeof(ver_str), "%u.%u.%u", + priv->fw_version.major, priv->fw_version.minor, + priv->fw_version.revision); + + return devlink_info_version_running_put(req, "fw", ver_str); +} + +int mxl862xx_devlink_flash_update(struct dsa_switch *ds, + struct devlink_flash_update_params *params, + struct netlink_ext_ack *extack) +{ + struct mxl862xx_sys_fw_image_version ver =3D {}; + struct mxl862xx_priv *priv =3D ds->priv; + struct task_struct *reprobe; + struct dsa_port *dp; + u32 payload_size; + int ret, i; + + if (params->component) { + NL_SET_ERR_MSG_MOD(extack, "component is not supported"); + return -EOPNOTSUPP; + } + + ret =3D mxl862xx_flash_validate(priv, params->fw, &payload_size); + if (ret) { + NL_SET_ERR_MSG_MOD(extack, "firmware image validation failed"); + return ret; + } + + /* Everything needed to launch the reprobe which restores + * normal operation must be in place before the switch is + * disturbed. + */ + if (!try_module_get(THIS_MODULE)) + return -ENODEV; + + reprobe =3D kthread_create(mxl862xx_reprobe_thread_fn, ds->dev, + "mxl862xx-reprobe"); + if (IS_ERR(reprobe)) { + module_put(THIS_MODULE); + return PTR_ERR(reprobe); + } + + get_device(ds->dev); + + dev_info(ds->dev, "flash: running firmware %u.%u.%u\n", + priv->fw_version.major, priv->fw_version.minor, + priv->fw_version.revision); + + /* Close ports while the firmware is still alive so the DSA + * core's MDB/FDB tracking is drained, and detach user ports + * so userspace cannot reopen them during the flash. The + * conduit belongs to the MAC driver and is only closed. + */ + rtnl_lock(); + dsa_switch_for_each_user_port(dp, ds) { + if (dp->user) { + dev_close(dp->user); + netif_device_detach(dp->user); + } + } + dsa_switch_for_each_cpu_port(dp, ds) + dev_close(dp->conduit); + /* The bridge defers the STP state changes triggered by closing + * the ports; let them reach the firmware while it is still alive. + */ + switchdev_deferred_process(); + rtnl_unlock(); + + mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED); + priv->block_host =3D true; + mutex_unlock(&priv->mdiodev->bus->mdio_lock); + + set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags); + cancel_delayed_work_sync(&priv->stats_work); + cancel_work_sync(&priv->crc_err_work); + for (i =3D 0; i < ds->num_ports; i++) + cancel_work_sync(&priv->ports[i].host_flood_work); + + ret =3D mxl862xx_flash_firmware(priv, params->fw, payload_size, + ds->devlink); + if (ret) + NL_SET_ERR_MSG_MOD(extack, "firmware transfer failed"); + + if (!ret) { + /* SYS_MISC_FW_VERSION is exempt from block_host, so the + * new version can be queried with the block still up. + */ + memset(&ver, 0, sizeof(ver)); + if (!MXL862XX_API_READ_QUIET(priv, SYS_MISC_FW_VERSION, ver) && + ver.iv_major) + dev_info(ds->dev, "flash: new firmware %u.%u.%u\n", + ver.iv_major, ver.iv_minor, + le16_to_cpu(ver.iv_revision)); + } + + mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED); + priv->skip_teardown =3D true; + mutex_unlock(&priv->mdiodev->bus->mdio_lock); + + wake_up_process(reprobe); + + return ret; +} diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl86= 2xx/mxl862xx-fw.h new file mode 100644 index 000000000000..a1b60fbacebf --- /dev/null +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef __MXL862XX_FW_H +#define __MXL862XX_FW_H + +#include + +int mxl862xx_devlink_info_get(struct dsa_switch *ds, + struct devlink_info_req *req, + struct netlink_ext_ack *extack); +int mxl862xx_devlink_flash_update(struct dsa_switch *ds, + struct devlink_flash_update_params *params, + struct netlink_ext_ack *extack); + +#endif /* __MXL862XX_FW_H */ diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl= 862xx/mxl862xx-host.c index 6e582caea1fa..2b69eac24b31 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c @@ -15,6 +15,7 @@ #include #include #include "mxl862xx.h" +#include "mxl862xx-cmd.h" #include "mxl862xx-host.h" =20 #define CTRL_BUSY_MASK BIT(15) @@ -340,6 +341,17 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 = cmd, void *_data, =20 mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED); =20 + if (priv->skip_teardown) { + ret =3D read ? -ENODEV : 0; + goto out; + } + + if (priv->block_host && cmd !=3D SYS_MISC_FW_UPDATE && + cmd !=3D SYS_MISC_FW_VERSION) { + ret =3D -EBUSY; + goto out; + } + max =3D (size + 1) / 2; =20 ret =3D mxl862xx_busy_wait(priv); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx= /mxl862xx.c index 45d237b3a40f..bf0f2264a931 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -21,6 +21,7 @@ #include "mxl862xx.h" #include "mxl862xx-api.h" #include "mxl862xx-cmd.h" +#include "mxl862xx-fw.h" #include "mxl862xx-host.h" #include "mxl862xx-phylink.h" =20 @@ -1572,6 +1573,11 @@ static int mxl862xx_port_mdb_del(struct dsa_switch *= ds, int port, ether_addr_copy(qparam.mac, mdb->addr); =20 ret =3D MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam); + /* -ENODEV: the firmware and its MAC table are gone, nothing left + * to delete + */ + if (ret =3D=3D -ENODEV) + return 0; if (ret) return ret; =20 @@ -2086,6 +2092,8 @@ static const struct dsa_switch_ops mxl862xx_switch_op= s =3D { .get_pause_stats =3D mxl862xx_get_pause_stats, .get_rmon_stats =3D mxl862xx_get_rmon_stats, .get_stats64 =3D mxl862xx_get_stats64, + .devlink_info_get =3D mxl862xx_devlink_info_get, + .devlink_flash_update =3D mxl862xx_devlink_flash_update, }; =20 static int mxl862xx_probe(struct mdio_device *mdiodev) @@ -2191,8 +2199,8 @@ static void mxl862xx_shutdown(struct mdio_device *mdi= odev) } =20 static const struct of_device_id mxl862xx_of_match[] =3D { - { .compatible =3D "maxlinear,mxl86282" }, - { .compatible =3D "maxlinear,mxl86252" }, + { .compatible =3D "maxlinear,mxl86282", .data =3D "MaxLinear MxL86282" }, + { .compatible =3D "maxlinear,mxl86252", .data =3D "MaxLinear MxL86252" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mxl862xx_of_match); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx= /mxl862xx.h index 432a5f3f2e08..64d91ad51936 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx.h @@ -319,6 +319,10 @@ struct mxl862xx_fw_version { * @evlan_ingress_size: per-port ingress Extended VLAN block size * @evlan_egress_size: per-port egress Extended VLAN block size * @vf_block_size: per-port VLAN Filter block size + * @block_host: reject firmware API commands (except FW_UPDATE) + * during a firmware flash + * @skip_teardown: discard firmware API commands during the teardown + * triggered by the post-flash reprobe * @stats_work: periodic work item that polls RMON hardware counte= rs * and accumulates them into 64-bit per-port stats */ @@ -337,6 +341,8 @@ struct mxl862xx_priv { u16 evlan_ingress_size; u16 evlan_egress_size; u16 vf_block_size; + bool block_host; + bool skip_teardown; struct delayed_work stats_work; }; =20 --=20 2.55.0 From nobody Sat Jul 25 00:15:11 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A13C92F532F; Wed, 22 Jul 2026 00:01:35 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678498; cv=none; b=a2KYRNmHq9bRthA2lx7/7dXguFKcuugJaSNbhxI6iE2EQukwhymcxbr4vrHHtMrpwnrAcMr7RYZwdHoH7cbUwQSMjhfGulmjM6FFCd7by6Iw//I0BbdTSOm3y66mhdY1eEkRESXEnvlv24c20MsOe0FSovjNWzh6sfIbbabSzsc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784678498; c=relaxed/simple; bh=tSqeWAZIS+l75+mPxOypqCyuyYqRs8RDFWKXADvnFxY=; h=Date:From:To:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=usdNFJv4qkVlHPBnMZEk0BttWxFlGYcR3LFIv5nXMTFGcJTgBfxBF89tcEjX9RUwaVOzd08nlRXNAUvNbEuXmHfGywuBaV/r2BFdpGvkPk6yk+PZjkr76b6804Kkfia/5JCriL9Gv+iQYcs+Pxe9uUbt260U1gRxeUWrPKuBlGA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wmKOw-000000000dK-0PS5; Wed, 22 Jul 2026 00:01:30 +0000 Date: Wed, 22 Jul 2026 01:01:27 +0100 From: Daniel Golle To: Daniel Golle , Andrew Lunn , Vladimir Oltean , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , linux-kernel@vger.kernel.org, netdev@vger.kernel.org Subject: [PATCH net-next v4 4/4] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Message-ID: <67171f553478c82d8299712879adedf35986016d.1784665017.git.daniel@makrotopia.org> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" When the application firmware image is broken (e.g. after an interrupted flash update) or the sticky rescue flag is set, the switch stays in MCUboot which only exposes the clause-22 SMDIO interface used for firmware download. The clause-45 MMD API never becomes ready, probe fails with -ETIMEDOUT and the user is left without any way to install a working firmware image other than physical access to the UART console of the switch. Detect this state during setup by reading the SB PDI STAT register via SMDIO: MCUboot signals readiness for a firmware download with the ready magic. Probe this register first to avoid pointlessly waiting for the MMD API on a switch sitting in rescue mode, and probe it again after a ready timeout to catch firmware which was still alive enough to accept the reset command but then fell into rescue mode due to a broken image on flash. An SMDIO bus error during detection logs its own diagnostic, distinct from the MMD ready timeout on a live bus, and counts as not in rescue mode. In rescue mode, complete the switch registration without any user interfaces so that devlink is available: user ports fail port_setup with -ENODEV which makes the DSA core re-register them as unused ports, while shared and unused ports succeed as their setup must not fail for the tree to register. The CPU port works without firmware access since it uses a fixed link and the phylink MAC ops do not touch the hardware; mac_select_pcs returns no PCS in rescue mode. Firmware API commands fail fast with -ENODEV instead of running into the MDIO poll timeout, and the port enable/disable and STP callbacks invoked by the DSA core during registration and teardown become no-ops. devlink dev info reports the running firmware version as the canonical null version "0.0.0" in rescue mode. An operational switch can never report this version since the major version of any released firmware is non-zero. Version-comparing update tools like fwupd thus treat every available release as an upgrade and offer to install it, so the switch can be recovered through the regular update flow. devlink flash update skips the FW_UPDATE command in rescue mode as MCUboot is already waiting for a download, and proceeds directly with the SB PDI handshake. After a successful transfer the usual reprobe cycle restores normal operation; after a failed one rescue mode is detected again and the user can simply retry. Signed-off-by: Daniel Golle --- v4: - log a distinct diagnostic when rescue mode detection fails on an SMDIO bus error instead of silently treating it as "not in rescue mode" - clear the rescue_mode flag under the MDIO bus lock, following the flag write locking in the previous patch v3: - report the canonical null version "0.0.0" instead of "mcuboot-rescue" so that version-comparing update tools like fwupd offer any available release as an upgrade for recovery - check the rescue_mode flag under the MDIO bus lock, following the block_host/skip_teardown change in the previous patch v2: new patch, allowing recovery from a failed or interrupted update without having to use a special recovery OS image (Andrew Lunn) drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 58 +++++++++++++++++---- drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 3 ++ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 5 ++ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 2 + drivers/net/dsa/mxl862xx/mxl862xx.c | 38 ++++++++++++-- drivers/net/dsa/mxl862xx/mxl862xx.h | 3 ++ 6 files changed, 96 insertions(+), 13 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl86= 2xx/mxl862xx-fw.c index 4cd8231c7eca..a3f941580852 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c @@ -111,6 +111,29 @@ static void mxl862xx_flash_notify(struct devlink *dl, = const char *status, devlink_flash_update_status_notify(dl, status, NULL, done, total); } =20 +/** + * mxl862xx_rescue_mode_detect - check whether the switch sits in MCUboot + * @priv: driver private data + * + * MCUboot signals readiness for a firmware download with the SB PDI + * ready magic; only the clause-22 SMDIO interface works in this state. + * + * Return: true if MCUboot is waiting for a firmware download. + */ +bool mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv) +{ + int ret; + + ret =3D mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT); + if (ret < 0) { + dev_err(&priv->mdiodev->dev, + "rescue mode detection failed: %pe\n", ERR_PTR(ret)); + return false; + } + + return ret =3D=3D MXL862XX_SB_PDI_READY; +} + /* device_reprobe() -> remove() frees priv while the thread runs, so * the thread must not dereference priv; it drops its module reference * without returning to module code afterwards. @@ -201,13 +224,15 @@ static int mxl862xx_flash_firmware(struct mxl862xx_pr= iv *priv, int ret, i; =20 /* Step 1: reboot the firmware into MCUboot rescue mode */ - ret =3D mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0, - false, false); - if (ret) { - dev_err(&priv->mdiodev->dev, - "flash: FW_UPDATE command failed: %pe\n", - ERR_PTR(ret)); - return ret; + if (!priv->rescue_mode) { + ret =3D mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0, + false, false); + if (ret) { + dev_err(&priv->mdiodev->dev, + "flash: FW_UPDATE command failed: %pe\n", + ERR_PTR(ret)); + return ret; + } } =20 /* Failures from here on must go through end_magic so MCUboot @@ -391,6 +416,10 @@ int mxl862xx_devlink_info_get(struct dsa_switch *ds, return ret; } =20 + /* canonical null version: no operational firmware on the switch */ + if (priv->rescue_mode) + return devlink_info_version_running_put(req, "fw", "0.0.0"); + snprintf(ver_str, sizeof(ver_str), "%u.%u.%u", priv->fw_version.major, priv->fw_version.minor, priv->fw_version.revision); @@ -436,9 +465,13 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *d= s, =20 get_device(ds->dev); =20 - dev_info(ds->dev, "flash: running firmware %u.%u.%u\n", - priv->fw_version.major, priv->fw_version.minor, - priv->fw_version.revision); + if (priv->rescue_mode) + dev_info(ds->dev, + "flash: recovering switch from MCUboot rescue mode\n"); + else + dev_info(ds->dev, "flash: running firmware %u.%u.%u\n", + priv->fw_version.major, priv->fw_version.minor, + priv->fw_version.revision); =20 /* Close ports while the firmware is still alive so the DSA * core's MDB/FDB tracking is drained, and detach user ports @@ -476,6 +509,11 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *d= s, NL_SET_ERR_MSG_MOD(extack, "firmware transfer failed"); =20 if (!ret) { + mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, + MDIO_MUTEX_NESTED); + priv->rescue_mode =3D false; + mutex_unlock(&priv->mdiodev->bus->mdio_lock); + /* SYS_MISC_FW_VERSION is exempt from block_host, so the * new version can be queried with the block still up. */ diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl86= 2xx/mxl862xx-fw.h index a1b60fbacebf..57c2000cfee5 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h @@ -5,6 +5,9 @@ =20 #include =20 +struct mxl862xx_priv; + +bool mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv); int mxl862xx_devlink_info_get(struct dsa_switch *ds, struct devlink_info_req *req, struct netlink_ext_ack *extack); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl= 862xx/mxl862xx-host.c index 2b69eac24b31..4c61820f05ee 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c @@ -346,6 +346,11 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 = cmd, void *_data, goto out; } =20 + if (priv->rescue_mode) { + ret =3D -ENODEV; + goto out; + } + if (priv->block_host && cmd !=3D SYS_MISC_FW_UPDATE && cmd !=3D SYS_MISC_FW_VERSION) { ret =3D -EBUSY; diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c b/drivers/net/dsa/= mxl862xx/mxl862xx-phylink.c index b689652aa9b9..a5b6940b552e 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c @@ -406,6 +406,8 @@ mxl862xx_phylink_mac_select_pcs(struct phylink_config *= config, =20 switch (port) { case 9 ... 16: + if (priv->rescue_mode) + return NULL; if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 84)) { dev_warn_once(dp->ds->dev, "SerDes PCS unsupported on old firmware.\n"); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx= /mxl862xx.c index bf0f2264a931..7a2ea580e6f0 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -629,9 +629,22 @@ static int mxl862xx_setup(struct dsa_switch *ds) if (ret) return ret; =20 - ret =3D mxl862xx_wait_ready(ds); - if (ret) - return ret; + priv->rescue_mode =3D mxl862xx_rescue_mode_detect(priv); + if (!priv->rescue_mode) { + ret =3D mxl862xx_wait_ready(ds); + if (ret) { + /* the reset may only now have triggered rescue mode */ + priv->rescue_mode =3D mxl862xx_rescue_mode_detect(priv); + if (!priv->rescue_mode) + return ret; + } + } + + if (priv->rescue_mode) { + dev_warn(ds->dev, + "switch stuck in MCUboot rescue mode, use devlink to flash new firmwar= e\n"); + return 0; + } =20 mutex_init(&priv->serdes_lock); for (i =3D 0; i < ARRAY_SIZE(priv->serdes_ports); i++) @@ -716,11 +729,21 @@ static int mxl862xx_port_state(struct dsa_switch *ds,= int port, bool enable) static int mxl862xx_port_enable(struct dsa_switch *ds, int port, struct phy_device *phydev) { + struct mxl862xx_priv *priv =3D ds->priv; + + if (priv->rescue_mode) + return 0; + return mxl862xx_port_state(ds, port, true); } =20 static void mxl862xx_port_disable(struct dsa_switch *ds, int port) { + struct mxl862xx_priv *priv =3D ds->priv; + + if (priv->rescue_mode) + return; + if (mxl862xx_port_state(ds, port, false)) dev_err(ds->dev, "failed to disable port %d\n", port); } @@ -1338,6 +1361,12 @@ static int mxl862xx_port_setup(struct dsa_switch *ds= , int port) bool is_cpu_port =3D dsa_port_is_cpu(dp); int ret; =20 + /* DSA reinits failed user ports as unused; shared ports must + * succeed for the tree to register. + */ + if (priv->rescue_mode) + return dsa_port_is_user(dp) ? -ENODEV : 0; + ret =3D mxl862xx_port_state(ds, port, false); if (ret) return ret; @@ -1634,6 +1663,9 @@ static void mxl862xx_port_stp_state_set(struct dsa_sw= itch *ds, int port, struct mxl862xx_priv *priv =3D ds->priv; int ret; =20 + if (priv->rescue_mode) + return; + switch (state) { case BR_STATE_DISABLED: param.port_state =3D cpu_to_le32(MXL862XX_STP_PORT_STATE_DISABLE); diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx= /mxl862xx.h index 64d91ad51936..5d21296561e3 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx.h @@ -323,6 +323,8 @@ struct mxl862xx_fw_version { * during a firmware flash * @skip_teardown: discard firmware API commands during the teardown * triggered by the post-flash reprobe + * @rescue_mode: switch is stuck in MCUboot; firmware API commands + * fail fast, only clause-22 SMDIO works * @stats_work: periodic work item that polls RMON hardware counte= rs * and accumulates them into 64-bit per-port stats */ @@ -343,6 +345,7 @@ struct mxl862xx_priv { u16 vf_block_size; bool block_host; bool skip_teardown; + bool rescue_mode; struct delayed_work stats_work; }; =20 --=20 2.55.0