* [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update
@ 2026-07-27 2:42 Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 1/5] net: dsa: wire flash_update devlink callback to drivers Daniel Golle
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:42 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
The firmware of MxL862xx managed Ethernet switches can be updated
in-system via the same MDIO bus which is also used to manage the
switch. Wire up the devlink flash_update operation for DSA drivers
and implement firmware update and version reporting in the mxl862xx
driver.
Changes since v5 [5]:
- run the post-flash reprobe from a kthread that drops the module
reference with module_put_and_kthread_exit() from core code, fixing
a use-after-free where a work item's trailing module_put() could
return into module text a racing rmmod had freed; a workqueue kickoff
spawns the kthread off the devlink caller where kthread_create() can
return -EINTR
- send END on every flash failure from the ready handshake onward so an
aborted transfer lets MCUboot reboot instead of leaving it waiting
- after the background drain finalises an interrupted download, reprobe
and let the probe-time detection re-classify the switch, so a valid
image a last-moment interruption left bootable comes up as running
firmware; rescue_drain() no longer inspects or reports the outcome
- re-read the SB PDI status register once more after a poll timeout
expires, so a preempted poll cannot report a spurious -ETIMEDOUT
- bail out of the periodic stats poll when the flash teardown has set
WORK_STOPPED, closing a get_stats64() re-arm race
- allocate the reprobe kickoff before disturbing the switch, so an
-ENOMEM cannot leave it flashed but never reprobed
- omit asic.id/asic.rev when the CHIP ID read returned 0, instead of
publishing a bogus "0000" for fwupd to match firmware against
- treat the flashless-download loop (STAT 0xc33c) as an unsupported
configuration and fail probe with -ENODEV instead of advertising it
as flashable
Changes since v4 [4]:
- report the numeric chip part number and version read from the
static CHIP ID registers as the "asic.id" and "asic.rev" fixed
versions instead of a model-name string, which does not belong in
a devlink version identifier (Jakub Kicinski)
- report the running firmware version as the "stored" version too,
since the switch boots it from its own flash, so userspace can
distinguish a flash-backed part from a flashless one by the
presence of "stored" without a future API change
- run the post-flash reprobe from a self-contained work item again
instead of the v4 kernel thread, which tripped the hung-task
watchdog while parked across the flash and returned -EINTR from
kthread_create() when the devlink command was interrupted
- re-read the new firmware version through the reprobe's fresh probe
and drop the SYS_MISC_FW_VERSION exemption from the host block
- raise the firmware command poll timeout so the FW_UPDATE command
that reboots into MCUboot is not cut short
- detect the switch state from the value MCUboot publishes in the SB
PDI STAT register (loader ready, wedged download, or running
firmware), confirming a live console loader with a register-read
challenge, instead of trusting a bare SMDIO scratch write
- fail probe with -ENODEV over SB PDI when the switch does not respond
at all (absent, unpowered, or misdescribed in the device tree)
instead of letting the clause-45 API flood the log with CRC errors
- drain a wedged interrupted download back to a clean ready state
from a background work item so the multi-minute recovery never
holds the devlink instance lock, reporting no firmware version and
refusing flash with -EBUSY until it completes
- report the rescue-mode null firmware version "0.0.0" as both the
running and stored version, matching the running/stored reporting
above
- split the devlink documentation into its own patch and add
Documentation/networking/devlink/mxl862xx.rst describing the info
versions and the flash update behaviour (Jakub Kicinski)
- include example "devlink dev info" outputs in the commit messages
of patches 3 and 4 (Jakub Kicinski)
Changes since v3 [3]:
- only install the flash_update devlink op for switches whose
driver implements it, so the devlink core rejects unsupported
requests before fetching the firmware file from userspace
- run the deferred reprobe from a kernel thread which ends in
module_put_and_kthread_exit() instead of a work item that
dropped its module reference while still executing module code
- fail firmware API read commands with -ENODEV after the update
has finished instead of faking success with an unfilled buffer,
which could send port_fdb_dump() into an endless loop
- keep the host block in place across the post-update version
query by exempting SYS_MISC_FW_VERSION from block_host instead
of briefly lifting the block, and write all blocking flags under
the MDIO bus lock
- check the return value of all SB PDI control writes; a failed
address write during the half-bank switch could otherwise place
the second half of the payload at the wrong flash offset
- initialise the progress notification deadline from jiffies so
notifications are not suppressed on 32-bit systems shortly
after boot
- log a distinct diagnostic when rescue mode detection fails on an
SMDIO bus error instead of silently treating it as not being in
rescue mode
- 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 against
the host block with "failed to set STP state" errors
- treat -ENODEV as successful deletion in port_mdb_del() so the
post-update teardown no longer leaves host MDB entries behind
for the DSA core to report when the tree is torn down
Changes since v2 [2]:
- validate the firmware image, including both CRCs, before taking
down any ports, so that a malformed file is rejected without
disturbing the running switch and without the needless flash and
reprobe cycle it previously triggered
- reject images whose declared payload sizes overflow when summed
(check_add_overflow) or sum up to zero; the latter previously
erased the flash without writing anything back
- allocate the reprobe work item and take the module and device
references before starting the update, so scheduling the reprobe
can no longer fail after the switch has been pushed into MCUboot
- prevent the stats poll work from being re-armed and cancel the
CRC error work before starting the transfer
- check the host-blocking flags in mxl862xx_api_wrap() under the
MDIO bus lock to close the race window where an API command
which had already passed the check could reach the bus after the
switch rebooted into MCUboot
- check the return value of SB PDI data word writes so a failed
MDIO transaction aborts the transfer instead of being noticed
only through a corrupted image
- report a per-model chip name (e.g. "MaxLinear MxL86252") as the
devlink "asic.id" fixed version instead of the devicetree
compatible string, whose comma is awkward for userspace
consumers such as fwupd (see discussion on v2 patch 3)
- report the canonical null version "0.0.0" instead of
"mcuboot-rescue" as the running firmware version in rescue mode,
so that version-comparing update tools like fwupd treat every
available release as an upgrade and offer it for recovery
Changes since RFC [1]:
- detect a switch stuck in MCUboot rescue mode at probe, register
the switch without any ports and report "mcuboot-rescue" as the
running firmware version, so devlink flash can recover from a
failed or interrupted update (Andrew Lunn)
- clarify in the commit message of patch 2 that the per-transaction
MDIO bus locking is about other, non-switch devices on the same
MDIO bus (Andrew Lunn)
- mention in the commit message of patch 3 that closing the ports
also stops phylib from polling the switch-internal PHYs during
the transfer (Andrew Lunn)
- split up run-on sentence and explain the dynamically allocated
reprobe work item instead of just pointing at iwlwifi in the
commit message of patch 3 (Manuel Ebner)
- use kzalloc_obj() (Manuel Ebner)
- state the actual duration of a complete flash and reprobe cycle
(just under a minute) in comments and the commit message, and
clarify that the timeout values are generous upper bounds
(Manuel Ebner)
[1] https://lore.kernel.org/all/ak0J-HgzMRea53om@makrotopia.org/
[2] https://lore.kernel.org/all/cover.1783988826.git.daniel@makrotopia.org/
[3] https://lore.kernel.org/all/cover.1784513694.git.daniel@makrotopia.org/
[4] https://lore.kernel.org/all/cover.1784665017.git.daniel@makrotopia.org/
[5] https://lore.kernel.org/all/cover.1784945329.git.daniel@makrotopia.org/
Daniel Golle (5):
net: dsa: wire flash_update devlink callback to drivers
net: dsa: mxl862xx: add SMDIO clause-22 register access
net: dsa: mxl862xx: add devlink flash_update and info_get
net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
net: dsa: mxl862xx: document devlink flash and info support
Documentation/networking/devlink/index.rst | 1 +
Documentation/networking/devlink/mxl862xx.rst | 63 ++
MAINTAINERS | 1 +
drivers/net/dsa/mxl862xx/Makefile | 2 +-
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 10 +
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 949 ++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 20 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 54 +
drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 2 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 134 ++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 26 +
include/net/dsa.h | 3 +
net/dsa/devlink.c | 50 +-
15 files changed, 1298 insertions(+), 21 deletions(-)
create mode 100644 Documentation/networking/devlink/mxl862xx.rst
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h
base-commit: 04026c998c24ac47eb76886b9790c5710b603eb4
prerequisite-patch-id: 0000000000000000000000000000000000000000
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH net-next v6 1/5] net: dsa: wire flash_update devlink callback to drivers
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
@ 2026-07-27 2:42 ` Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 2/5] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:42 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
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 <daniel@makrotopia.org>
---
v6: no changes
v5: no changes
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;
}
+static int dsa_devlink_flash_update(struct devlink *dl,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack)
+{
+ struct dsa_switch *ds = 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_port *dlp,
p_max);
}
-static const struct devlink_ops dsa_devlink_ops = {
- .info_get = dsa_devlink_info_get,
- .sb_pool_get = dsa_devlink_sb_pool_get,
- .sb_pool_set = dsa_devlink_sb_pool_set,
- .sb_port_pool_get = dsa_devlink_sb_port_pool_get,
- .sb_port_pool_set = dsa_devlink_sb_port_pool_set,
- .sb_tc_pool_bind_get = dsa_devlink_sb_tc_pool_bind_get,
- .sb_tc_pool_bind_set = dsa_devlink_sb_tc_pool_bind_set,
- .sb_occ_snapshot = dsa_devlink_sb_occ_snapshot,
- .sb_occ_max_clear = dsa_devlink_sb_occ_max_clear,
- .sb_occ_port_pool_get = 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 = dsa_devlink_info_get, \
+ .sb_pool_get = dsa_devlink_sb_pool_get, \
+ .sb_pool_set = dsa_devlink_sb_pool_set, \
+ .sb_port_pool_get = dsa_devlink_sb_port_pool_get, \
+ .sb_port_pool_set = dsa_devlink_sb_port_pool_set, \
+ .sb_tc_pool_bind_get = dsa_devlink_sb_tc_pool_bind_get, \
+ .sb_tc_pool_bind_set = dsa_devlink_sb_tc_pool_bind_set, \
+ .sb_occ_snapshot = dsa_devlink_sb_occ_snapshot, \
+ .sb_occ_max_clear = dsa_devlink_sb_occ_max_clear, \
+ .sb_occ_port_pool_get = dsa_devlink_sb_occ_port_pool_get, \
.sb_occ_tc_port_bind_get = dsa_devlink_sb_occ_tc_port_bind_get,
+
+static const struct devlink_ops dsa_devlink_ops = {
+ DSA_DEVLINK_OPS
+};
+
+static const struct devlink_ops dsa_devlink_flash_ops = {
+ DSA_DEVLINK_OPS
+ .flash_update = dsa_devlink_flash_update,
};
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;
+ ops = 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 = devlink_alloc(&dsa_devlink_ops, sizeof(*dl_priv), ds->dev);
+ dl = devlink_alloc(ops, sizeof(*dl_priv), ds->dev);
if (!dl)
return -ENOMEM;
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net-next v6 2/5] net: dsa: mxl862xx: add SMDIO clause-22 register access
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 1/5] net: dsa: wire flash_update devlink callback to drivers Daniel Golle
@ 2026-07-27 2:42 ` Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 3/5] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:42 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
Add mxl862xx_smdio_read() and mxl862xx_smdio_write() for clause-22
SMDIO register access. The switch's MCUboot bootloader exposes only
clause-22 registers; the clause-45 MMD interface needs the running
firmware and is unavailable while the switch is in MCUboot. 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 <daniel@makrotopia.org>
---
v6: no changes
v5: no changes
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/mxl862xx/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;
}
+#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 = priv->mdiodev->bus;
+ int phy = priv->mdiodev->addr;
+ int ret;
+
+ mutex_lock(&bus->mdio_lock);
+ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+ addr & MXL862XX_SMDIO_PAGE_MASK);
+ if (ret >= 0)
+ ret = __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 = priv->mdiodev->bus;
+ int phy = priv->mdiodev->addr;
+ int ret;
+
+ mutex_lock(&bus->mdio_lock);
+ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+ addr & MXL862XX_SMDIO_PAGE_MASK);
+ if (ret >= 0)
+ ret = __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/mxl862xx/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)
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);
#endif /* __MXL862XX_HOST_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net-next v6 3/5] net: dsa: mxl862xx: add devlink flash_update and info_get
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 1/5] net: dsa: wire flash_update devlink callback to drivers Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 2/5] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
@ 2026-07-27 2:42 ` Daniel Golle
2026-07-27 2:43 ` [PATCH net-next v6 4/5] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
2026-07-27 2:43 ` [PATCH net-next v6 5/5] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle
4 siblings, 0 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:42 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
Implement runtime firmware upgrade via "devlink dev flash" and version
reporting via "devlink dev info":
$ devlink dev info mdio_bus/mdio-bus:10
mdio_bus/mdio-bus:10:
driver mxl862xx
versions:
fixed:
asic.id 8628
asic.rev 0
running:
fw 1.0.70
stored:
fw 1.0.70
The "asic.id" and "asic.rev" fixed versions carry the numeric chip part
number and revision from the static CHIP ID registers (SYS_MISC_REG_RD),
which userspace such as fwupd matches firmware against; they are omitted
if the read fails or the part is unfused, so no bogus "0000" is
published. The switch boots its firmware from its own flash, so "fw" is
reported as both the running and the stored version; a flashless part
would omit "stored", distinguishing the two without an API change.
$ devlink dev flash mdio_bus/mdio-bus:10 file mxl862xx-fw.bin
The image, including both payload CRCs, is validated first, so a
malformed file is rejected without disturbing the running switch. The
driver then sends SYS_MISC_FW_UPDATE, which reboots the switch into its
MCUboot bootloader, and transfers the signed image over the SB PDI
protocol (clause-22 SMDIO), checking every write: a failed address write
at the half-bank boundary would otherwise misplace half the payload
unnoticed. A successful transfer reboots the switch into the new
firmware. The whole cycle takes just under a minute.
For its duration the driver closes all user and conduit interfaces and
marks the user ports not-present with netif_device_detach() so userspace
cannot reopen them; the conduit belongs to the MAC driver and is only
closed. This also stops phylib polling the switch-internal PHYs,
unreachable in MCUboot. The bridge's deferred STP DISABLED transitions
are flushed under rtnl so they reach the firmware while it still runs;
the stats poll and CRC error handler are stopped; and firmware API
commands from other paths are blocked under the MDIO bus lock so none
reaches the bus once the switch has rebooted. Progress is reported
through devlink status notifications.
The switch leaves MCUboot on its own by booting the new image, but the
driver has no in-place path back, so it reinitialises with a full
device_reprobe() scheduled regardless of the transfer outcome -- after a
failure the switch is still in MCUboot and probe re-detects it. During
the teardown its API reads return -ENODEV and writes fake success, so it
neither stalls on the absent firmware nor consumes buffers it never
filled. The reprobe holds module and device references taken before the
switch was disturbed and runs from a kthread so it can drop the module
reference with module_put_and_kthread_exit(), from core code -- a work
item's trailing module_put() could return into module text a racing
rmmod had freed. A workqueue kickoff spawns the kthread, off the devlink
caller where kthread_create() can return -EINTR.
The closed user ports and conduit are not returned to their pre-flash
administrative state across the reprobe; userspace brings them back up,
and restoring it in-driver would need DSA-core support that does not
yet exist.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v6:
- confirm the new firmware is running with mxl862xx_wait_ready() and
lift the host block before reporting success, so devlink dev flash
completes only once the update has taken effect instead of relying
on the later reprobe to pick up the new version
- run the post-flash reprobe from a kthread that drops the module
reference with module_put_and_kthread_exit(), spawned from a
workqueue kickoff, closing a use-after-free where a work item's
trailing module_put() could return into module text a racing rmmod
had already freed
- jump to the end_magic teardown on every flash failure from the
ready handshake onward, so an aborted transfer sends END and lets
MCUboot reboot instead of leaving the loader waiting
- poll the SB PDI status register with read_poll_timeout(), which
evaluates the condition once more after the deadline, so a preempted
poll cannot report a spurious -ETIMEDOUT
- bail out of the periodic stats poll when the flash teardown has set
WORK_STOPPED, closing a get_stats64() re-arm race
- allocate the reprobe kickoff before disturbing the switch, so an
-ENOMEM cannot leave it flashed but never reprobed with block_host
and skip_teardown stuck set
- omit asic.id/asic.rev when the CHIP ID read returned 0, instead of
publishing a bogus "0000" for fwupd to match firmware against
v5:
- report the numeric chip part number and version read from the
static CHIP ID registers as "asic.id" and "asic.rev" instead of a
model-name string, which does not belong in a devlink version
identifier (Jakub Kicinski)
- report the running firmware version as the "stored" version too,
since the switch boots it from its own flash, so userspace can
tell a flash-backed part from a flashless one by the presence of
"stored" without a future API change
- run the post-flash reprobe from a self-contained work item again
instead of the v4 kernel thread, which tripped the hung-task
watchdog while parked across the flash and returned -EINTR from
kthread_create() when the devlink command was interrupted
- re-read the new firmware version through the reprobe's fresh probe
and drop the SYS_MISC_FW_VERSION exemption from the host block
- raise the firmware command poll timeout so the FW_UPDATE command
that reboots into MCUboot is not cut short
- move the devlink documentation into its own patch
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-api.h | 10 +
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 624 +++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 17 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 11 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 67 ++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 14 +
8 files changed, 745 insertions(+), 2 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/Makefile
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) += mxl862xx_dsa.o
-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
+mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o mxl862xx-fw.o
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-api.h b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
index a180a5decffc..6f771895984c 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1224,6 +1224,16 @@ struct mxl862xx_sys_fw_image_version {
__le32 iv_build_num;
} __packed;
+/**
+ * struct mxl862xx_sys_reg_rw - System register read/write
+ * @addr: 32-bit register address
+ * @val: register value
+ */
+struct mxl862xx_sys_reg_rw {
+ __le32 addr;
+ __le32 val;
+} __packed;
+
/**
* enum mxl862xx_port_type - Port Type
* @MXL862XX_LOGICAL_PORT: Logical Port
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
index c87a955c13c4..a865425aa61e 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -70,7 +70,9 @@
#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2)
+#define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1)
#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+#define SYS_MISC_REG_RD (SYS_MISC_MAGIC + 0x8)
#define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1)
#define MXL862XX_XPCS_PCS_GET_STATE (MXL862XX_XPCS_MAGIC + 0x2)
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
new file mode 100644
index 000000000000..86b069586d69
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -0,0 +1,624 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Firmware flash and devlink support for MaxLinear MxL862xx
+ *
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ *
+ * SB PDI - firmware download interface over clause-22 SMDIO
+ * =========================================================
+ *
+ * The MxL862xx MCUboot loader accepts a firmware image through four "SB PDI"
+ * registers in the switch SMDIO register space. It runs whenever no WSP
+ * firmware is active: the normal firmware update enters it deliberately - the
+ * SYS_MISC_FW_UPDATE API command sets a sticky rescue bit and reboots into
+ * MCUboot - and the loader also stays here when the stored WSP firmware fails
+ * its boot-time integrity check. This driver drives the loader's 0xc55c
+ * "console" download path.
+ *
+ * SMDIO register access (mxl862xx_smdio_read/write):
+ * MII reg 0x1f := <sb_pdi_reg> ; address/page latch
+ * MII reg 0x00 := / => <u16 data> ; data window
+ *
+ * SB PDI registers (host name/addr -> MCU mailbox):
+ * CTRL 0xe100 -> 0xc0938400 mode: RST=0x00 RD=0x01 WR=0x02
+ * ADDR 0xe101 -> 0xc0938404 SB target word address (SB1 bank = 0x7800)
+ * DATA 0xe102 -> 0xc0938408 16-bit data / reply word
+ * STAT 0xe103 -> 0xc093840c handshake: a magic (below) or a byte count
+ *
+ * STAT magics:
+ * READY 0xc55c loader idle in the console loop (this driver)
+ * START 0xf48f host -> begin download session
+ * ACK 0xf490 loader -> START acknowledged (START + 1)
+ * END 0x3cc3 host -> end of transfer / finalise
+ *
+ * Console flash path (STAT=0xc55c) - mxl862xx_flash_firmware():
+ *
+ * host loader
+ * ---- ------
+ * reset (CTRL=ADDR=DATA=0)
+ * read STAT ............................ 0xc55c (READY, idle)
+ * STAT := START(0xf48f) -------------->
+ * <-------------- STAT = 0xf490 (ACK)
+ * CTRL := WR
+ * DATA := hdr[0..9] (20-byte header: type,size1,crc1,size2,crc2)
+ * reset; STAT := 20 (header len) -----> parse hdr; r_remain=size1+size2;
+ * ERASE target region(s)
+ * <-------------- STAT=21 (len+1), then STAT=0
+ * (erased)
+ * -- payload, streamed in slices: --
+ * CTRL := WR
+ * DATA := word x N ...
+ * at word 16384: reset; ADDR:=0x7800; CTRL:=WR (half-bank -> SB1)
+ * at word 32760: flush slice:
+ * reset; STAT := <bytes_this_slice> ---> r_remain -= bytes; program
+ * <------------------- STAT=0 (ready for next slice)
+ * ... repeat until the whole payload is sent ...
+ * STAT := END(0x3cc3) ---------------------> finalise
+ *
+ * The r_remain == 0 rule (critical):
+ * Every host STAT write in the payload phase is a byte count; the loader
+ * does r_remain -= count and stays in the receive loop while r_remain != 0.
+ * It leaves the loop, validates, and - if it was in rescue - clears its
+ * rescue-enable bit so boot_go boots the new image, ONLY when r_remain hits
+ * EXACTLY 0. A count larger than r_remain underflows the 32-bit counter and
+ * wedges the loader until a power cycle. Hence:
+ * - never send a slice/chunk count larger than what is outstanding;
+ */
+
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/iopoll.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/overflow.h>
+#include <linux/rtnetlink.h>
+#include <linux/workqueue.h>
+#include <net/dsa.h>
+#include <net/switchdev.h>
+
+#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 (published/consumed via STAT) */
+#define MXL862XX_SB_PDI_READY 0xc55c /* loader idle, console loop */
+#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 3000
+#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 = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_RST);
+ if (ret < 0)
+ return ret;
+
+ ret = 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 expected,
+ unsigned long timeout_ms)
+{
+ int ret, val;
+
+ ret = read_poll_timeout(mxl862xx_smdio_read, val,
+ val < 0 || (u16)val == expected,
+ 10000, timeout_ms * 1000, false,
+ priv, MXL862XX_SB_PDI_STAT);
+ if (val < 0)
+ return val;
+ return ret;
+}
+
+static int mxl862xx_sb_pdi_flush_slice(struct mxl862xx_priv *priv,
+ u32 data_written)
+{
+ int ret;
+
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ return ret;
+
+ ret = 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);
+}
+
+/* Post-flash reprobe. device_reprobe() -> remove() frees priv, so this runs
+ * detached from priv, on only the held device reference. It runs as a kthread
+ * so the module reference can be dropped with module_put_and_kthread_exit(),
+ * from core-kernel code -- a work item's trailing module_put() could return
+ * into module text a racing rmmod has already freed.
+ */
+static int mxl862xx_reprobe_thread_fn(void *data)
+{
+ struct device *dev = 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);
+}
+
+struct mxl862xx_reprobe_kickoff {
+ struct work_struct work;
+ struct device *dev;
+};
+
+/* Spawn the reprobe kthread from workqueue context, where kthread_create()
+ * cannot return -EINTR as it can from the devlink caller's context.
+ */
+static void mxl862xx_reprobe_kickoff_fn(struct work_struct *work)
+{
+ struct mxl862xx_reprobe_kickoff *ko =
+ container_of(work, struct mxl862xx_reprobe_kickoff, work);
+ struct device *dev = ko->dev;
+ struct task_struct *task;
+
+ kfree(ko);
+ task = kthread_run(mxl862xx_reprobe_thread_fn, dev, "mxl862xx-reprobe");
+ if (IS_ERR(task)) {
+ dev_err(dev,
+ "reprobe kthread failed (%pe); reload the driver to restore operation\n",
+ task);
+ put_device(dev);
+ module_put(THIS_MODULE);
+ }
+}
+
+/* Allocate the reprobe kickoff up front, before the switch is disturbed, so an
+ * allocation failure aborts cleanly. The caller holds a module and a device
+ * reference; once the kickoff is scheduled the reprobe kthread releases both.
+ * Returns NULL on -ENOMEM.
+ */
+static struct mxl862xx_reprobe_kickoff *mxl862xx_reprobe_alloc(struct device *dev)
+{
+ struct mxl862xx_reprobe_kickoff *ko;
+
+ ko = kzalloc_obj(*ko);
+ if (!ko)
+ return NULL;
+ ko->dev = dev;
+ INIT_WORK(&ko->work, mxl862xx_reprobe_kickoff_fn);
+ return ko;
+}
+
+/* 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 = (const struct mxl862xx_fw_hdr *)fw->data;
+ payload = fw->data + MXL862XX_FW_HDR_SIZE;
+ size1 = le32_to_cpu(hdr->image_size_1);
+ size2 = 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 = ~crc32_le(~0U, payload, size1);
+ if (crc != 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 = ~crc32_le(~0U, payload + size1, size2);
+ if (crc != 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 = 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 = fw->data + MXL862XX_FW_HDR_SIZE;
+ u32 word_idx = 0, data_written = 0, idx = 0;
+ unsigned long next_notify = jiffies - 1;
+ u16 word, fdata;
+ int ret, i;
+
+ /* Step 1: reboot the firmware into MCUboot rescue mode */
+ ret = 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;
+ }
+
+ /* Step 2: wait for bootloader ready */
+ mxl862xx_flash_notify(dl, "Waiting for bootloader", 0, 0);
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ goto write_err;
+
+ /* Failures from here on must go through end_magic so MCUboot
+ * reboots instead of waiting forever.
+ */
+ ret = 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 = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_START);
+ if (ret < 0)
+ goto write_err;
+
+ ret = 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 = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ if (ret < 0)
+ goto write_err;
+
+ for (i = 0; i < MXL862XX_FW_HDR_SIZE / 2; i++) {
+ word = fw->data[i * 2] |
+ ((u16)fw->data[i * 2 + 1] << 8);
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, word);
+ if (ret < 0)
+ goto write_err;
+ }
+
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ goto write_err;
+
+ /* the byte count in STAT triggers the erase */
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_FW_HDR_SIZE);
+ if (ret < 0)
+ goto write_err;
+
+ /* ACK is byte count + 1 */
+ ret = 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 = 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 = 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 = payload[idx] |
+ ((u16)payload[idx + 1] << 8);
+ idx += 2;
+ data_written += 2;
+ } else {
+ fdata = payload[idx];
+ idx++;
+ data_written++;
+ }
+
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, fdata);
+ if (ret < 0)
+ goto write_err;
+ word_idx++;
+
+ if (idx >= payload_size) {
+ ret = mxl862xx_sb_pdi_flush_slice(priv, data_written);
+ break;
+ }
+
+ /* Half-bank boundary: switch to SB1 address */
+ if (word_idx == MXL862XX_FW_BANK_HALF) {
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_RST);
+ if (ret < 0)
+ goto write_err;
+
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+ MXL862XX_FW_SB1_ADDR);
+ if (ret < 0)
+ goto write_err;
+
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ if (ret < 0)
+ goto write_err;
+ } else if (word_idx >= MXL862XX_FW_BANK_SLICE) {
+ ret = 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 = 0;
+ data_written = 0;
+ ret = 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 = 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 = ds->priv;
+ char buf[16];
+ int ret;
+
+ /* A 0 part number means the CHIP ID read failed or the part is
+ * unfused; omit it rather than publish a bogus "0000" that fwupd
+ * would match firmware against -- it then falls back to the driver
+ * name.
+ */
+ if (priv->asic_id) {
+ snprintf(buf, sizeof(buf), "%04X", priv->asic_id);
+ ret = devlink_info_version_fixed_put(req,
+ DEVLINK_INFO_VERSION_GENERIC_ASIC_ID,
+ buf);
+ if (ret)
+ return ret;
+
+ snprintf(buf, sizeof(buf), "%u", priv->asic_rev);
+ ret = devlink_info_version_fixed_put(req,
+ DEVLINK_INFO_VERSION_GENERIC_ASIC_REV,
+ buf);
+ if (ret)
+ return ret;
+ }
+
+ snprintf(buf, sizeof(buf), "%u.%u.%u",
+ priv->fw_version.major, priv->fw_version.minor,
+ priv->fw_version.revision);
+
+ ret = devlink_info_version_running_put(req, "fw", buf);
+ if (ret)
+ return ret;
+
+ /* boots this image from its own flash: stored == running */
+ return devlink_info_version_stored_put(req, "fw", buf);
+}
+
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_reprobe_kickoff *ko;
+ struct mxl862xx_priv *priv = ds->priv;
+ 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 = mxl862xx_flash_validate(priv, params->fw, &payload_size);
+ if (ret) {
+ NL_SET_ERR_MSG_MOD(extack, "firmware image validation failed");
+ return ret;
+ }
+
+ /* The references the reprobe work needs to restore normal operation
+ * must be held before the switch is disturbed; the work itself is
+ * scheduled only once the flash is done (see below).
+ */
+ if (!try_module_get(THIS_MODULE))
+ return -ENODEV;
+
+ get_device(ds->dev);
+
+ /* Allocate the reprobe kickoff before disturbing the switch, so an
+ * -ENOMEM here cannot strand it flashed but never reprobed.
+ */
+ ko = mxl862xx_reprobe_alloc(ds->dev);
+ if (!ko) {
+ put_device(ds->dev);
+ module_put(THIS_MODULE);
+ return -ENOMEM;
+ }
+
+ 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 = 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 = 0; i < ds->num_ports; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+
+ ret = mxl862xx_flash_firmware(priv, params->fw, payload_size,
+ ds->devlink);
+ if (ret)
+ NL_SET_ERR_MSG_MOD(extack, "firmware transfer failed");
+
+ if (!ret) {
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock,
+ MDIO_MUTEX_NESTED);
+ priv->block_host = false;
+ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+ /* Refresh the cached versions so the flash update only
+ * completes once the new firmware is confirmed running and
+ * devlink dev info reports it. Must happen before setting
+ * skip_teardown, which discards all firmware API reads.
+ */
+ ret = mxl862xx_wait_ready(ds);
+ if (ret)
+ NL_SET_ERR_MSG_MOD(extack,
+ "new firmware did not become ready");
+ }
+
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+ priv->skip_teardown = true;
+ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+ /* Kick off the reprobe last; the kickoff was allocated up front and
+ * its module and device references are already held.
+ */
+ schedule_work(&ko->work);
+
+ return ret;
+}
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
new file mode 100644
index 000000000000..e96db19b2888
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_FW_H
+#define __MXL862XX_FW_H
+
+#include <net/dsa.h>
+
+struct mxl862xx_priv;
+
+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/mxl862xx/mxl862xx-host.c
index 6e582caea1fa..66b388eed0ce 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -15,6 +15,7 @@
#include <linux/unaligned.h>
#include <net/dsa.h>
#include "mxl862xx.h"
+#include "mxl862xx-cmd.h"
#include "mxl862xx-host.h"
#define CTRL_BUSY_MASK BIT(15)
@@ -340,6 +341,16 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+ if (priv->skip_teardown) {
+ ret = read ? -ENODEV : 0;
+ goto out;
+ }
+
+ if (priv->block_host && cmd != SYS_MISC_FW_UPDATE) {
+ ret = -EBUSY;
+ goto out;
+ }
+
max = (size + 1) / 2;
ret = mxl862xx_busy_wait(priv);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
index 45d237b3a40f..aa922e88be74 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"
@@ -71,6 +72,13 @@ static const struct ethtool_rmon_hist_range mxl862xx_rmon_ranges[] = {
#define MXL862XX_READY_TIMEOUT_MS 10000
#define MXL862XX_READY_POLL_MS 100
+/* Chip ID registers, read via SYS_MISC_REG_RD */
+#define MXL862XX_CHIPID_L 0xc0d28884
+#define MXL862XX_CHIPID_M 0xc0d28888
+#define MXL862XX_CHIPID_L_PNUML GENMASK(15, 12)
+#define MXL862XX_CHIPID_M_PNUMM GENMASK(11, 0)
+#define MXL862XX_CHIPID_M_VERSION GENMASK(14, 12)
+
#define MXL862XX_TCM_INST_SEL 0xe00
#define MXL862XX_TCM_CBS 0xe12
#define MXL862XX_TCM_EBS 0xe13
@@ -222,7 +230,46 @@ 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);
}
-static int mxl862xx_wait_ready(struct dsa_switch *ds)
+/* Read the static chip part number and version from the CHIP ID
+ * registers. Only possible with a running firmware, so the values are
+ * cached at setup and left zero when the switch is in rescue mode.
+ */
+static int mxl862xx_read_chip_id(struct mxl862xx_priv *priv)
+{
+ struct mxl862xx_sys_reg_rw reg = {};
+ u16 chipid_l, chipid_m;
+ int ret;
+
+ reg.addr = cpu_to_le32(MXL862XX_CHIPID_L);
+ ret = MXL862XX_API_READ(priv, SYS_MISC_REG_RD, reg);
+ if (ret)
+ return ret;
+ chipid_l = le32_to_cpu(reg.val);
+
+ reg.addr = cpu_to_le32(MXL862XX_CHIPID_M);
+ ret = MXL862XX_API_READ(priv, SYS_MISC_REG_RD, reg);
+ if (ret)
+ return ret;
+ chipid_m = le32_to_cpu(reg.val);
+
+ priv->asic_id = FIELD_GET(MXL862XX_CHIPID_L_PNUML, chipid_l) |
+ FIELD_GET(MXL862XX_CHIPID_M_PNUMM, chipid_m) << 4;
+ priv->asic_rev = FIELD_GET(MXL862XX_CHIPID_M_VERSION, chipid_m);
+
+ return 0;
+}
+
+/**
+ * mxl862xx_wait_ready - wait for the switch firmware to become operational
+ * @ds: DSA switch instance
+ *
+ * Poll the firmware until it reports its version and accepts
+ * configuration commands, then cache the firmware version and chip ID.
+ * Takes at least two seconds.
+ *
+ * Return: 0 on success or a negative error code.
+ */
+int mxl862xx_wait_ready(struct dsa_switch *ds)
{
struct mxl862xx_sys_fw_image_version ver = {};
unsigned long start = jiffies, timeout;
@@ -254,6 +301,11 @@ static int mxl862xx_wait_ready(struct dsa_switch *ds)
priv->fw_version.major = ver.iv_major;
priv->fw_version.minor = ver.iv_minor;
priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
+
+ ret = mxl862xx_read_chip_id(priv);
+ if (ret)
+ dev_warn(ds->dev, "failed to read chip ID: %pe\n",
+ ERR_PTR(ret));
return 0;
not_ready_yet:
@@ -1572,6 +1624,11 @@ static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port,
ether_addr_copy(qparam.mac, mdb->addr);
ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+ /* -ENODEV: the firmware and its MAC table are gone, nothing left
+ * to delete
+ */
+ if (ret == -ENODEV)
+ return 0;
if (ret)
return ret;
@@ -2015,6 +2072,12 @@ static void mxl862xx_stats_work_fn(struct work_struct *work)
struct dsa_switch *ds = priv->ds;
struct dsa_port *dp;
+ /* A get_stats64() re-arm can race the flash teardown's WORK_STOPPED
+ * set and cancel; bail here so a stray poll never runs during a flash.
+ */
+ if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+ return;
+
dsa_switch_for_each_available_port(dp, ds)
mxl862xx_stats_poll(ds, dp->index);
@@ -2086,6 +2149,8 @@ static const struct dsa_switch_ops mxl862xx_switch_ops = {
.get_pause_stats = mxl862xx_get_pause_stats,
.get_rmon_stats = mxl862xx_get_rmon_stats,
.get_stats64 = mxl862xx_get_stats64,
+ .devlink_info_get = mxl862xx_devlink_info_get,
+ .devlink_flash_update = mxl862xx_devlink_flash_update,
};
static int mxl862xx_probe(struct mdio_device *mdiodev)
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h
index 432a5f3f2e08..66989280c59d 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -303,6 +303,10 @@ struct mxl862xx_fw_version {
* flooding)
* @fw_version: cached firmware version, populated at probe and
* compared with MXL862XX_FW_VER_MIN()
+ * @asic_id: chip part number read from the CHIP ID registers,
+ * reported as the devlink "asic.id" fixed version
+ * @asic_rev: chip version read from the CHIP ID registers,
+ * reported as the devlink "asic.rev" fixed version
* @serdes_ports: SerDes interfaces incl. sub-interfaces in case of
* 10G_QXGMII or QSGMII
* @serdes_refcount: per-XPCS count of sub-ports enabled by phylink;
@@ -319,6 +323,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 counters
* and accumulates them into 64-bit per-port stats
*/
@@ -329,6 +337,8 @@ struct mxl862xx_priv {
unsigned long flags;
u16 drop_meter;
struct mxl862xx_fw_version fw_version;
+ u16 asic_id;
+ u8 asic_rev;
struct mxl862xx_pcs serdes_ports[8];
int serdes_refcount[2];
struct mutex serdes_lock;
@@ -337,7 +347,11 @@ 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;
};
+int mxl862xx_wait_ready(struct dsa_switch *ds);
+
#endif /* __MXL862XX_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net-next v6 4/5] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
` (2 preceding siblings ...)
2026-07-27 2:42 ` [PATCH net-next v6 3/5] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
@ 2026-07-27 2:43 ` Daniel Golle
2026-07-27 2:43 ` [PATCH net-next v6 5/5] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle
4 siblings, 0 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:43 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
A broken or interrupted firmware image, or the sticky rescue bit, keeps
the switch in its MCUboot bootloader, which exposes only the clause-22
SMDIO download interface. The clause-45 MMD API never comes up, so probe
would fail with -ETIMEDOUT and the only way back would be the switch's
UART console, or a power cycle or out-of-band reset (not currently
implemented).
Probe for the loader over SB PDI at setup, before any clause-45 access,
since the C45 API floods the log with CRC errors when no firmware
answers. The same probe tells three cases apart without touching C45: a
running firmware (probe continues normally), a switch in MCUboot (enter
rescue mode), and a switch that does not answer at all -- absent,
unpowered, or misdescribed in the device tree (wrong address or bus, or
a reset GPIO with inverted polarity) -- which fails probe cleanly with
-ENODEV instead of a CRC-error storm.
In rescue mode the switch registers without user interfaces so devlink
stays available: user ports fail port_setup with -ENODEV (the DSA core
re-registers them as unused) while shared and CPU ports succeed, and the
CPU port works on its fixed link with mac_select_pcs returning no PCS.
Firmware API commands fail fast with -ENODEV and the port and STP
callbacks become no-ops.
An interrupted download can leave the loader wedged mid-payload. A
background work item off the devlink flash path drains it back to a
clean ready state by feeding the outstanding byte count, which can take
minutes; until then devlink dev info reports no version and devlink dev
flash returns -EBUSY. The drain only finalises the transfer and then
reprobes, letting the probe-time detection re-classify the switch -- a
valid image that a last-moment interruption left bootable comes up as
running firmware, with no second-guessing in the drain path.
The CHIP ID registers need a running firmware, so no asic.id/asic.rev is
reported in rescue mode. Once the loader is ready, devlink dev info
reports the null firmware version "0.0.0" as both running and stored:
$ devlink dev info mdio_bus/mdio-bus:10
mdio_bus/mdio-bus:10:
driver mxl862xx
versions:
running:
fw 0.0.0
stored:
fw 0.0.0
An operational switch never reports 0.0.0 (a released firmware's major
is non-zero), so version-comparing tools like fwupd offer every release
as an upgrade, recovering the switch through the regular flash flow,
matched on the driver name. The flash skips the FW_UPDATE command since
MCUboot is already waiting. A successful flash reboots the switch into
the new firmware and the reprobe then brings the driver up against it
normally; after a failed one the switch is still in MCUboot, rescue mode
is detected again, and the user can retry.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v6:
- after the background drain finalises the interrupted transfer,
reprobe and let the probe-time detection re-classify the switch, so
a valid image a last-moment interruption left bootable is picked up
as running firmware; rescue_drain() no longer inspects or reports
the outcome (its stale kernel-doc claiming a "return 1" case is gone)
- poll the drain status register with read_poll_timeout() as well,
which evaluates the condition once more after the deadline, matching
the poll fix in the previous patch
- treat the flashless-download loop (STAT 0xc33c) as an unsupported
configuration and fail probe with -ENODEV, rather than advertising it
as flashable when the console flash path cannot drive it
v5:
- detect the switch state from the value MCUboot publishes in the SB
PDI STAT register (loader ready, wedged download, or running
firmware), confirming a live console loader with a register-read
challenge, instead of trusting a bare SMDIO scratch write
- fail probe with -ENODEV over SB PDI when the switch does not respond
at all -- absent, unpowered, or misdescribed in the device tree --
instead of letting the clause-45 API flood the log with CRC errors
- drain a wedged interrupted download back to a clean ready state from
a background work item so the multi-minute recovery never holds the
devlink instance lock, and refuse devlink dev info and flash until it
is ready
- report the null firmware version as the stored version too, matching
the running/stored reporting of the previous patch
- do not report asic.id/asic.rev in rescue mode as the CHIP ID
registers are unreadable without firmware; recovery tools match on
the driver name and the "0.0.0" version instead (follows the numeric
asic.id change in the previous patch)
- move the devlink documentation into its own patch
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 | 345 +++++++++++++++++++-
drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 3 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 8 +
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 2 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 67 +++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 12 +
6 files changed, 420 insertions(+), 17 deletions(-)
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
index 86b069586d69..54b3554534c2 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -27,9 +27,11 @@
*
* STAT magics:
* READY 0xc55c loader idle in the console loop (this driver)
+ * DL_RDY 0xc33c loader idle in the flashless loop
* START 0xf48f host -> begin download session
* ACK 0xf490 loader -> START acknowledged (START + 1)
* END 0x3cc3 host -> end of transfer / finalise
+ * RDREG 0xe2c0 host -> register-read command (| index), see below
*
* Console flash path (STAT=0xc55c) - mxl862xx_flash_firmware():
*
@@ -63,6 +65,36 @@
* EXACTLY 0. A count larger than r_remain underflows the 32-bit counter and
* wedges the loader until a power cycle. Hence:
* - never send a slice/chunk count larger than what is outstanding;
+ * - interrupted-download recovery feeds 1 byte at a time (see below).
+ *
+ * Interrupted-flash recovery (mxl862xx_rescue_drain):
+ * A host that dies mid-payload leaves the loader spinning in the slice loop
+ * holding STAT=0 (no magic). Feed single 1-byte chunks (one DATA word +
+ * STAT=1) until r_remain reaches 0, then STAT=END; the loader finalises the
+ * (now corrupt) image and re-arms READY for a clean reflash.
+ *
+ * Register-read challenge (non-destructive liveness proof):
+ * DATA := 0x7c23 (marker); STAT := 0xe2c0|idx
+ * -> loader returns a runtime word in DATA and re-arms STAT=0xc55c.
+ * The reply source is loader BSS, not a chip id; used only to prove a live
+ * mailbox in mxl862xx_rescue_mode_detect().
+ *
+ * The other STAT ready magic, 0xc33c, marks the loader's flashless
+ * chip-to-chip download mode (MxL86281S 16-port tier); this driver does not
+ * use it.
+ *
+ * Rescue lifecycle (devlink): probe runs mxl862xx_rescue_mode_detect(); a wedged
+ * loader is drained back to READY by a background self-heal (rescue_heal_work) so
+ * the multi-minute recovery never holds the devlink lock. devlink dev info
+ * exposes the fw version (the "flashable" signal) only once at READY;
+ * flash_update returns -EBUSY until then, and reprobes to WSP firmware on success.
+ *
+ * Notes:
+ * - Chip id/revision (0xc0d28884/88) are NOT reachable on this channel; they
+ * need the clause-45 MMD firmware mailbox, which is dead under MCUboot.
+ * Rescue identity is by SB PDI behaviour only (mxl862xx_rescue_mode_detect).
+ * - The SMDIO PHY address and the 0xe1xx offsets are OTP-configurable; derive
+ * them from the DT binding, do not assume fixed values.
*/
#include <linux/crc32.h>
@@ -96,8 +128,15 @@
/* SB PDI handshake magic (published/consumed via STAT) */
#define MXL862XX_SB_PDI_READY 0xc55c /* loader idle, console loop */
+#define MXL862XX_SB_PDI_DL_READY 0xc33c /* loader idle, flashless loop */
#define MXL862XX_SB_PDI_START 0xf48f
#define MXL862XX_SB_PDI_END 0x3cc3
+#define MXL862XX_SB_PDI_RDREG 0xe2c0 /* register-read cmd (| index) */
+#define MXL862XX_SB_PDI_RDREG_MARK 0x7c23 /* marker placed in DATA for RDREG */
+
+/* Behavioural presence probe: two distinct 16-bit latches on ADDR/DATA. */
+#define MXL862XX_SB_PDI_PROBE_A 0x5a5a
+#define MXL862XX_SB_PDI_PROBE_D 0xa5a5
/* Firmware transfer geometry */
#define MXL862XX_FW_HDR_SIZE 20
@@ -112,6 +151,7 @@
#define MXL862XX_FW_WRITE_TIMEOUT_MS 120000
#define MXL862XX_FW_REBOOT_DELAY_MS 5000
#define MXL862XX_FW_REPROBE_DELAY_MS 500
+#define MXL862XX_RESCUE_READY_TIMEOUT_MS 1000
static int mxl862xx_sb_pdi_reset(struct mxl862xx_priv *priv)
{
@@ -228,6 +268,260 @@ static struct mxl862xx_reprobe_kickoff *mxl862xx_reprobe_alloc(struct device *de
return ko;
}
+/* Byte-count of each chunk fed to the loader during drain. It MUST be 1: the
+ * loader only lets us observe "counter == 0", never "counter < step", so any
+ * step > 1 can subtract past zero, underflow the 32-bit counter and wedge the
+ * loader for ~2^32 more bytes (a state only a power cycle clears). Stepping by
+ * 1 walks the counter through every value and is guaranteed to land on zero
+ * whatever its (possibly odd) start. A 1-byte chunk is a path the loader
+ * already handles: the normal transfer ends with a single trailing byte for
+ * odd-sized images (see Step 6).
+ */
+#define MXL862XX_DRAIN_CHUNK_BYTES 1
+
+/* Poll STAT while draining a stuck download: 0 means "feed the next chunk",
+ * READY means the loader left the receive loop and re-armed its command loop,
+ * anything else is a transient (the count being consumed) - BUSY past the
+ * window means the counter has hit zero and the loader is finalising.
+ */
+enum { MXL862XX_DRAIN_FEED, MXL862XX_DRAIN_READY, MXL862XX_DRAIN_BUSY };
+static int mxl862xx_sb_pdi_poll_drain(struct mxl862xx_priv *priv,
+ unsigned long timeout_ms)
+{
+ int val;
+
+ read_poll_timeout(mxl862xx_smdio_read, val,
+ val < 0 || (u16)val == MXL862XX_SB_PDI_READY ||
+ (u16)val == 0,
+ 50, timeout_ms * 1000, false,
+ priv, MXL862XX_SB_PDI_STAT);
+ if (val < 0)
+ return val;
+ if ((u16)val == MXL862XX_SB_PDI_READY)
+ return MXL862XX_DRAIN_READY;
+ if ((u16)val == 0)
+ return MXL862XX_DRAIN_FEED;
+ return MXL862XX_DRAIN_BUSY;
+}
+
+/* Recover a switch whose SB PDI download was interrupted mid-transfer - the
+ * host died after MCUboot began erasing flash, whether it aborted mid erase or
+ * mid image-write, both end up in the same place: the payload receive loop.
+ * There the loader publishes STAT=0, waits for the host to write a byte-count
+ * to STAT, DMAs that many bytes and subtracts the count from a remaining-bytes
+ * counter, leaving the loop only when the counter reaches exactly zero. The
+ * image size died with the host, so we feed single-byte chunks (see
+ * MXL862XX_DRAIN_CHUNK_BYTES) to walk the counter to zero without underflow,
+ * then send END. The loader validates the (now corrupt) image and re-arms
+ * READY, or boots a valid image that happened to survive in flash. This only
+ * finalises the transfer; the caller's reprobe classifies whichever state
+ * results. Returns 0 once finalised, <0 on error. Does NOT recover a counter
+ * already underflowed by an earlier oversized-chunk attempt - that needs a
+ * power cycle.
+ */
+static int mxl862xx_rescue_drain(struct mxl862xx_priv *priv)
+{
+ struct device *dev = &priv->mdiodev->dev;
+ /* Bound: twice the loader's 16 MiB image cap, one byte per chunk. */
+ u32 max_chunks = 2u * (16u << 20) / MXL862XX_DRAIN_CHUNK_BYTES;
+ u32 chunk = 0;
+ int ret;
+
+ dev_warn(dev, "flash: draining interrupted download\n");
+
+ while (chunk < max_chunks) {
+ /* Teardown can interrupt this minutes-long drain. */
+ if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+ return -ECANCELED;
+
+ ret = mxl862xx_sb_pdi_poll_drain(priv, 2000);
+ if (ret < 0)
+ return ret;
+ if (ret == MXL862XX_DRAIN_READY)
+ return 0;
+ if (ret == MXL862XX_DRAIN_BUSY)
+ break;
+
+ /* Feed one zero byte; reset cleared the write latch. */
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_DRAIN_CHUNK_BYTES);
+ if (ret < 0)
+ return ret;
+ chunk++;
+ cond_resched();
+ }
+
+ if (chunk >= max_chunks) {
+ dev_err(dev,
+ "flash: interrupted download did not drain after %u chunks\n",
+ chunk);
+ return -ETIMEDOUT;
+ }
+
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_END);
+ if (ret < 0)
+ return ret;
+
+ /* Let the loader validate and re-arm READY, or boot a surviving image;
+ * the caller's reprobe then classifies whichever state results.
+ */
+ msleep(MXL862XX_FW_REBOOT_DELAY_MS);
+ return 0;
+}
+
+/* Background self-heal: drain a wedged download off the devlink flash path, so
+ * the minutes-long recovery never holds the devlink lock. Scheduled from probe;
+ * reprobes on success so the probe-time detection re-classifies the switch.
+ */
+void mxl862xx_rescue_heal_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_priv *priv =
+ container_of(work, struct mxl862xx_priv, rescue_heal_work);
+ struct device *dev = &priv->mdiodev->dev;
+ struct mxl862xx_reprobe_kickoff *ko;
+ int ret;
+
+ dev_info(dev, "recovering interrupted download in background\n");
+ ret = mxl862xx_rescue_drain(priv);
+ if (test_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags))
+ return;
+ if (ret)
+ return;
+
+ /* The interrupted transfer is finalised; reprobe so the probe-time
+ * detection brings the driver up -- flashable in rescue mode if the
+ * loader is at READY, or normally if a valid image booted. The refs
+ * are released by the reprobe once it completes.
+ */
+ if (!try_module_get(THIS_MODULE))
+ return;
+ get_device(dev);
+ ko = mxl862xx_reprobe_alloc(dev);
+ if (!ko) {
+ put_device(dev);
+ module_put(THIS_MODULE);
+ return;
+ }
+ schedule_work(&ko->work);
+}
+
+/* Detect MCUboot rescue mode over clause-22 SMDIO alone, so the caller can rule
+ * the loader out before any C45 API request (which spews CRC errors when no WSP
+ * firmware answers). A scratch write to ADDR/DATA must latch or the chip is
+ * absent (-ENODEV); STAT then classifies the state, poked destructively only
+ * when 0, the one value a running firmware never holds:
+ *
+ * - 0xc33c: flashless loop, ready.
+ * - 0xc55c: console loop, if the register-read challenge is serviced.
+ * - other non-zero: running firmware, left unpoked.
+ * - 0: wedged receive loop, if a 1-byte slice-advance drains back to 0.
+ *
+ * Return: MXL862XX_IN_RESCUE, MXL862XX_NOT_RESCUE, or negative (-ENODEV/SMDIO).
+ */
+int mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv)
+{
+ int stat, dat, ret, rb, a, d;
+
+ /* rescue_ready gates flashing; a wedged loader needs the drain first. */
+ priv->rescue_ready = false;
+
+ /* Presence: a live chip latches the scratch write, an absent one floats. */
+ a = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+ MXL862XX_SB_PDI_PROBE_A);
+ if (a < 0)
+ return a;
+ d = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
+ MXL862XX_SB_PDI_PROBE_D);
+ if (d < 0)
+ return d;
+ a = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_ADDR);
+ if (a < 0)
+ return a;
+ d = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_DATA);
+ if (d < 0)
+ return d;
+ if ((u16)a != MXL862XX_SB_PDI_PROBE_A ||
+ (u16)d != MXL862XX_SB_PDI_PROBE_D)
+ return -ENODEV;
+
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ return ret;
+
+ stat = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT);
+ if (stat < 0)
+ return stat;
+
+ /* Flashless-download loop (MxL86281S tier): this driver does not
+ * support it -- the console flash path expects READY. Treat it as an
+ * unusable configuration, like any other unsupported state.
+ */
+ if ((u16)stat == MXL862XX_SB_PDI_DL_READY)
+ return -ENODEV;
+
+ /* Console loop at READY: confirm the live mailbox with the register-read
+ * challenge (consumes the marker from DATA and re-arms READY).
+ */
+ if ((u16)stat == MXL862XX_SB_PDI_READY) {
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
+ MXL862XX_SB_PDI_RDREG_MARK);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_RDREG);
+ if (ret < 0)
+ return ret;
+ rb = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_READY,
+ MXL862XX_RESCUE_READY_TIMEOUT_MS);
+ dat = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_DATA);
+ if (dat < 0)
+ return dat;
+ mxl862xx_sb_pdi_reset(priv);
+ if (!rb && (u16)dat != MXL862XX_SB_PDI_RDREG_MARK) {
+ priv->rescue_ready = true;
+ return MXL862XX_IN_RESCUE;
+ }
+ return -ENODEV;
+ }
+
+ /* Any other non-zero value is a running firmware, not a loader. */
+ if (stat)
+ return MXL862XX_NOT_RESCUE;
+
+ /* STAT == 0: a wedged receive loop consumes a 1-byte slice-advance back
+ * to 0 (feed one DATA word first, like a drain chunk).
+ */
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_sb_pdi_reset(priv);
+ if (ret < 0)
+ return ret;
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT, 1);
+ if (ret < 0)
+ return ret;
+ rb = mxl862xx_sb_pdi_poll_stat(priv, 0, MXL862XX_RESCUE_READY_TIMEOUT_MS);
+ if (!rb)
+ return MXL862XX_IN_RESCUE;
+
+ return -ENODEV;
+}
+
/* MCUboot firmware image header */
struct mxl862xx_fw_hdr {
__le32 image_type;
@@ -303,13 +597,15 @@ static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv,
int ret, i;
/* Step 1: reboot the firmware into MCUboot rescue mode */
- ret = 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 = 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;
+ }
}
/* Step 2: wait for bootloader ready */
@@ -482,6 +778,23 @@ int mxl862xx_devlink_info_get(struct dsa_switch *ds,
char buf[16];
int ret;
+ /* No chip-id/revision in MCUboot (needs the firmware MMD mailbox). The
+ * fw version doubles as the "ready to flash" signal: report it only
+ * once the loader is at a clean READY, nothing while still draining.
+ */
+ if (priv->rescue_mode) {
+ if (!READ_ONCE(priv->rescue_ready))
+ return 0;
+
+ snprintf(buf, sizeof(buf), "%u.%u.%u",
+ priv->fw_version.major, priv->fw_version.minor,
+ priv->fw_version.revision);
+ ret = devlink_info_version_running_put(req, "fw", buf);
+ if (ret)
+ return ret;
+ return devlink_info_version_stored_put(req, "fw", buf);
+ }
+
/* A 0 part number means the CHIP ID read failed or the part is
* unfused; omit it rather than publish a bogus "0000" that fwupd
* would match firmware against -- it then falls back to the driver
@@ -536,6 +849,13 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
return ret;
}
+ /* Refuse to flash while the background self-heal is still draining. */
+ if (priv->rescue_mode && !READ_ONCE(priv->rescue_ready)) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "switch is recovering an interrupted download, retry shortly");
+ return -EBUSY;
+ }
+
/* The references the reprobe work needs to restore normal operation
* must be held before the switch is disturbed; the work itself is
* scheduled only once the flash is done (see below).
@@ -555,9 +875,13 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
return -ENOMEM;
}
- 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: flashing switch via 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);
/* Close ports while the firmware is still alive so the DSA
* core's MDB/FDB tracking is drained, and detach user ports
@@ -598,6 +922,7 @@ int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
mutex_lock_nested(&priv->mdiodev->bus->mdio_lock,
MDIO_MUTEX_NESTED);
priv->block_host = false;
+ priv->rescue_mode = false;
mutex_unlock(&priv->mdiodev->bus->mdio_lock);
/* Refresh the cached versions so the flash update only
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
index e96db19b2888..7cd87c7ad871 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -6,7 +6,10 @@
#include <net/dsa.h>
struct mxl862xx_priv;
+struct work_struct;
+int mxl862xx_rescue_mode_detect(struct mxl862xx_priv *priv);
+void mxl862xx_rescue_heal_work_fn(struct work_struct *work);
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/mxl862xx/mxl862xx-host.c
index 66b388eed0ce..2dbd074c0fe2 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -16,6 +16,7 @@
#include <net/dsa.h>
#include "mxl862xx.h"
#include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
#include "mxl862xx-host.h"
#define CTRL_BUSY_MASK BIT(15)
@@ -346,6 +347,11 @@ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
goto out;
}
+ if (priv->rescue_mode) {
+ ret = -ENODEV;
+ goto out;
+ }
+
if (priv->block_host && cmd != SYS_MISC_FW_UPDATE) {
ret = -EBUSY;
goto out;
@@ -544,9 +550,11 @@ int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val)
void mxl862xx_host_init(struct mxl862xx_priv *priv)
{
INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
+ INIT_WORK(&priv->rescue_heal_work, mxl862xx_rescue_heal_work_fn);
}
void mxl862xx_host_shutdown(struct mxl862xx_priv *priv)
{
cancel_work_sync(&priv->crc_err_work);
+ cancel_work_sync(&priv->rescue_heal_work);
}
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,
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 aa922e88be74..8d21747cdf15 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -674,15 +674,49 @@ static int mxl862xx_setup(struct dsa_switch *ds)
int n_user_ports = 0, max_vlans;
int ingress_finals, vid_rules;
struct dsa_port *dp;
- int ret, i;
+ int ret, i, rescue;
- ret = mxl862xx_reset(priv);
- if (ret)
- return ret;
+ /* Detect the loader over SB PDI first: it needs no firmware, unlike the
+ * C45 API (mxl862xx_reset/wait_ready) which spews CRC errors when none
+ * answers. Touch C45 only once rescue is ruled out.
+ */
+ rescue = mxl862xx_rescue_mode_detect(priv);
+ if (rescue < 0)
+ return rescue;
- ret = mxl862xx_wait_ready(ds);
- if (ret)
- return ret;
+ if (rescue == MXL862XX_NOT_RESCUE) {
+ ret = mxl862xx_reset(priv);
+ if (ret)
+ return ret;
+
+ ret = mxl862xx_wait_ready(ds);
+ if (ret) {
+ /* the reset may only now have triggered rescue mode */
+ rescue = mxl862xx_rescue_mode_detect(priv);
+ if (rescue < 0)
+ return rescue;
+ if (rescue == MXL862XX_NOT_RESCUE)
+ return ret;
+ }
+ }
+
+ priv->rescue_mode = rescue;
+
+ if (priv->rescue_mode) {
+ if (priv->rescue_ready) {
+ dev_warn(ds->dev,
+ "switch in MCUboot rescue mode, use devlink to flash new firmware\n");
+ } else {
+ /* Drain the wedged download in the background so it
+ * never holds the devlink lock; info and flash become
+ * available once ready.
+ */
+ dev_warn(ds->dev,
+ "switch in MCUboot with an interrupted download, recovering in background\n");
+ queue_work(system_long_wq, &priv->rescue_heal_work);
+ }
+ return 0;
+ }
mutex_init(&priv->serdes_lock);
for (i = 0; i < ARRAY_SIZE(priv->serdes_ports); i++)
@@ -767,11 +801,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 = ds->priv;
+
+ if (priv->rescue_mode)
+ return 0;
+
return mxl862xx_port_state(ds, port, true);
}
static void mxl862xx_port_disable(struct dsa_switch *ds, int port)
{
+ struct mxl862xx_priv *priv = 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);
}
@@ -1389,6 +1433,12 @@ static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
bool is_cpu_port = dsa_port_is_cpu(dp);
int ret;
+ /* 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 = mxl862xx_port_state(ds, port, false);
if (ret)
return ret;
@@ -1685,6 +1735,9 @@ static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
struct mxl862xx_priv *priv = ds->priv;
int ret;
+ if (priv->rescue_mode)
+ return;
+
switch (state) {
case BR_STATE_DISABLED:
param.port_state = 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 66989280c59d..129985f2bbf3 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -14,6 +14,10 @@ struct mxl862xx_priv;
#define MXL862XX_FIRST_SERDES_PORT 9
#define MXL862XX_SERDES_SLOTS 4
+/* mxl862xx_rescue_mode_detect() return codes (negative values are errors) */
+#define MXL862XX_NOT_RESCUE 0
+#define MXL862XX_IN_RESCUE 1
+
#define MXL862XX_DEFAULT_BRIDGE 0
#define MXL862XX_MAX_BRIDGES 48
#define MXL862XX_MAX_BRIDGE_PORTS 128
@@ -327,6 +331,11 @@ 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 in MCUboot; firmware API commands fail fast,
+ * only clause-22 SMDIO works
+ * @rescue_ready: (rescue_mode) loader is at a clean READY and will accept
+ * a flash; false while rescue_heal_work is draining
+ * @rescue_heal_work: background self-heal draining a wedged download to READY
* @stats_work: periodic work item that polls RMON hardware counters
* and accumulates them into 64-bit per-port stats
*/
@@ -334,6 +343,7 @@ struct mxl862xx_priv {
struct dsa_switch *ds;
struct mdio_device *mdiodev;
struct work_struct crc_err_work;
+ struct work_struct rescue_heal_work;
unsigned long flags;
u16 drop_meter;
struct mxl862xx_fw_version fw_version;
@@ -349,6 +359,8 @@ struct mxl862xx_priv {
u16 vf_block_size;
bool block_host;
bool skip_teardown;
+ bool rescue_mode;
+ bool rescue_ready;
struct delayed_work stats_work;
};
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH net-next v6 5/5] net: dsa: mxl862xx: document devlink flash and info support
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
` (3 preceding siblings ...)
2026-07-27 2:43 ` [PATCH net-next v6 4/5] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
@ 2026-07-27 2:43 ` Daniel Golle
4 siblings, 0 replies; 6+ messages in thread
From: Daniel Golle @ 2026-07-27 2:43 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Daniel Golle, Andrew Lunn, Vladimir Oltean, netdev, linux-doc,
linux-kernel
Describe the devlink info versions and the flash update behaviour,
including the MCUboot rescue mode recovery, in a dedicated file under
Documentation/networking/devlink/ and link it from the index. Add the
new file to the driver's MAINTAINERS entry.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v6: no changes
v5: new patch, splitting the devlink documentation out of the flash
update and rescue mode recovery patches so each keeps to code
(Jakub Kicinski asked for the documentation)
Documentation/networking/devlink/index.rst | 1 +
Documentation/networking/devlink/mxl862xx.rst | 63 +++++++++++++++++++
MAINTAINERS | 1 +
3 files changed, 65 insertions(+)
create mode 100644 Documentation/networking/devlink/mxl862xx.rst
diff --git a/Documentation/networking/devlink/index.rst b/Documentation/networking/devlink/index.rst
index 4745148fecf4..058999d0dc56 100644
--- a/Documentation/networking/devlink/index.rst
+++ b/Documentation/networking/devlink/index.rst
@@ -94,6 +94,7 @@ parameters, info versions, and other features it supports.
mlx5
mlxsw
mv88e6xxx
+ mxl862xx
netdevsim
nfp
octeontx2
diff --git a/Documentation/networking/devlink/mxl862xx.rst b/Documentation/networking/devlink/mxl862xx.rst
new file mode 100644
index 000000000000..b3e9f7d4d496
--- /dev/null
+++ b/Documentation/networking/devlink/mxl862xx.rst
@@ -0,0 +1,63 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+========================
+mxl862xx devlink support
+========================
+
+This document describes the devlink features implemented by the
+``mxl862xx`` device driver.
+
+Info versions
+=============
+
+The ``mxl862xx`` driver reports the following versions
+
+.. list-table:: devlink info versions implemented
+ :widths: 5 5 5 85
+
+ * - Name
+ - Type
+ - Example
+ - Description
+ * - ``asic.id``
+ - fixed
+ - 8628
+ - The chip part number read from the CHIP ID registers. Not
+ reported for a switch sitting in MCUboot rescue mode as the
+ registers are only accessible with a running firmware.
+ * - ``asic.rev``
+ - fixed
+ - 0
+ - The chip version read from the CHIP ID registers. Not reported
+ in MCUboot rescue mode either.
+ * - ``fw``
+ - running, stored
+ - 1.0.70
+ - Version of the firmware running on the switch, reported as both
+ running and stored since the switch boots it from its own flash.
+ In MCUboot rescue mode nothing is reported while an interrupted
+ download is still being recovered in the background; once the
+ loader is ready to accept a new image the version is reported (as
+ both running and stored), which is the signal that a flash will be
+ accepted. It reads "0.0.0" when the switch came up straight into
+ MCUboot without ever running firmware.
+
+Flash update
+============
+
+The ``mxl862xx`` driver implements support for ``devlink dev flash``.
+The signed firmware image is transferred to the switch over the same
+MDIO bus which is also used to manage the switch, then verified and
+installed by the MCUboot bootloader running on the switch. All ports
+of the switch are closed for the duration of the update and the driver
+reprobes the switch after it has rebooted into the new firmware. A
+complete flash and reprobe cycle takes about one minute.
+
+A switch stuck in MCUboot rescue mode, e.g. after an interrupted
+update, is registered without user ports. If the previous download was
+interrupted mid-transfer the loader is wedged; the driver drains it
+back to a clean ready state in the background, which can easily take
+more than 10 minutes. During that recovery ``devlink dev info`` reports
+no firmware version and ``devlink dev flash`` returns ``-EBUSY``.
+Once the loader is ready the firmware version appears and flashing a
+firmware image through the regular update flow recovers the switch.
diff --git a/MAINTAINERS b/MAINTAINERS
index 60cff00953dc..3a55ed2bad9c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16107,6 +16107,7 @@ M: Daniel Golle <daniel@makrotopia.org>
L: netdev@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/net/dsa/maxlinear,mxl862xx.yaml
+F: Documentation/networking/devlink/mxl862xx.rst
F: drivers/net/dsa/mxl862xx/
F: net/dsa/tag_mxl862xx.c
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-27 2:43 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27 2:42 [PATCH net-next v6 0/5] net: dsa: mxl862xx: support firmware update Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 1/5] net: dsa: wire flash_update devlink callback to drivers Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 2/5] net: dsa: mxl862xx: add SMDIO clause-22 register access Daniel Golle
2026-07-27 2:42 ` [PATCH net-next v6 3/5] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
2026-07-27 2:43 ` [PATCH net-next v6 4/5] net: dsa: mxl862xx: recover switch stuck in MCUboot rescue mode Daniel Golle
2026-07-27 2:43 ` [PATCH net-next v6 5/5] net: dsa: mxl862xx: document devlink flash and info support Daniel Golle
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.