* [PATCH net-next v17 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get
2026-09-23 2:33 [PATCH net-next v17 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
@ 2026-09-23 2:35 ` Daniel Golle
0 siblings, 0 replies; 2+ messages in thread
From: Daniel Golle @ 2026-09-23 2:35 UTC (permalink / raw)
To: Jiri Pirko, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan,
Randy Dunlap, Daniel Golle, Greg Kroah-Hartman, Rafael J. Wysocki,
Danilo Krummrich, Andrew Lunn, Vladimir Oltean, Russell King,
netdev, linux-doc, linux-kernel, driver-core
Implement "devlink dev flash" for firmware upgrade and "devlink dev
info" for version reporting. info reports the chip part number and
revision as the asic.id and asic.rev fixed versions, and the firmware
version as both the running and the stored version, since the switch
boots its firmware from its own flash; a zero part number or an all-zero
version is omitted.
flash validates the image and its payload CRCs, reboots the switch into
its MCUboot loader with SYS_MISC_FW_UPDATE, transfers the image over the
SB PDI clause-22 SMDIO protocol, and reboots into the new firmware once
the loader has verified it. For the duration the driver closes and
detaches the user ports so userspace cannot reopen them, closes the
conduit, and blocks firmware API access under the MDIO bus lock so
nothing reaches the bus after the switch has left for MCUboot. A blocked
write reports success, so a bridge tearing its ports down over a flash
does not see port_vlan_del() fail and leak its VLAN group.
MCUboot offers no in-place path back, so the driver reinitialises with a
deferred re-probe scheduled through device_schedule_reprobe() from the
previous patch, which runs in the driver core off the devlink caller's
locks and without holding a module or device reference. ->shutdown() and
.remove() take the devlink instance lock up front, since neither goes
through devlink and dsa_unregister_switch() frees the user netdevs
before devlink_unregister() would: this waits out a transfer in flight
rather than cutting it in half, and refuses a flash that starts once
teardown has begun.
Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
v17:
- wait out a flash in flight in .remove() too, not only ->shutdown():
dsa_unregister_switch() frees the user netdevs before it reaches the
devlink instance lock, so an unbind racing a flash could touch a
freed netdev; .remove() now takes that lock up front like
->shutdown(), and both announce the wait with dev_info() (found by
Sashiko AI review)
- report success for a firmware read blocked by a flash (-EBUSY) as
well as the teardown -ENODEV in port_mdb_add() and port_mdb_del(), so
an MDB change racing a flash does not fail and leave the entry linked
(found by Sashiko AI review)
- clear the SB PDI ADDR and DATA latches with 0 rather than the CTRL
reset mode value, which only happened to be 0 (found by Sashiko AI
review)
- log that an unbind and rebind is needed when the post-flash re-probe
cannot be scheduled although the new firmware is already running
(found by Sashiko AI review)
- kernel-doc: describe the block_host and skip_teardown policy as it
is, a host read failing while a write reports success (found by
Sashiko AI review)
- commit message: scheduling the re-probe can also fail with -EINVAL
when the device is being unbound, not only -ENOMEM (found by Sashiko
AI review)
v16:
- report success for a firmware write blocked by a running flash,
instead of -EBUSY. Tearing a bridge down over a flash made
port_vlan_del() fail, which leaves the VLAN on the bridge's list:
__vlan_group_free() then warns and frees the group with the entries
still linked
- wait for a flash in flight in ->shutdown() and refuse one requested
after it, so a reboot no longer cuts the image in half
- state a fact in the -EBUSY extack of a pending reprobe, dropping the
retry advice
- the comment on mxl862xx_read_chip_id() no longer describes a
rescue-mode cache that only the next patch creates
- select CRC32, which nothing else selects for the crc32_le() calls
validating the image checksums (found by Sashiko AI review)
- Andrew's Reviewed-by is dropped for the ->shutdown() change
v15:
- treat the closing END write as advisory: the loader has left the
receive loop and verified the image by then, so a failed write no
longer reports a completed flash as a transfer failure (found by
Sashiko AI review)
- admit only the flash task's own firmware reads past block_host,
instead of every read from any context, which let ethtool and FDB
queries reach the freshly booted firmware and left an mdb_add() half
executed (found by Sashiko AI review)
v14: no changes, picked up Andrew's v13 Reviewed-by
v13: stop the stats poll with disable_delayed_work_sync() in the flash
path, so a racing get_stats64() re-arm is a no-op, and drop the
early return in the work function with it; the v12 reordering of
remove() is gone as well, since the race it addressed predates
this series and needs a fix of its own (found by Sashiko AI review)
v12:
- cancel the stats poll after dsa_unregister_switch() in .remove(): a
get_stats64() past its lockless WORK_STOPPED check could re-arm the
work after the cancel and then run against the devres-freed priv
(found by Sashiko AI review)
- keep block_host set across the post-flash readiness poll and let
only the flash path's own firmware reads through, so a concurrent
bridge or STP write cannot reach the freshly booted firmware with
stale pre-flash resource IDs while rtnl is dropped (found by Sashiko
AI review)
v11:
- schedule the post-flash re-probe with device_schedule_reprobe()
instead of a driver-owned work item; the module and device references
and the drvdata bound-check go away with it, and with them both
findings of the v10 AI review -- the work function no longer ends in
module text behind a module_put(), and the bound-check and detach now
run under one __device_driver_lock() hold in the core where
->shutdown() cannot interleave between them
- the dsa_switch allocation returns to devres; keeping it out only
defused the check-vs-detach window that the core helper now closes
outright
- scheduling the re-probe is now the one step that can fail after the
switch was flashed, since the helper allocates its own work item; an
-ENOMEM there is returned as-is, since unbind and rebind reinitialises
the driver
v10:
- do not reprobe a device that has been shut down or unbound. The work
cannot be cancelled from teardown: .shutdown() runs under
device_lock(), which device_reprobe() takes as well, so a lock shared
between the two would deadlock, and cancel_delayed_work_sync() would
hang on the nested remove() the work triggers itself. It checks
drvdata under device_lock() instead, which both .shutdown() and the
driver core clear (found by Sashiko AI review of patch 4, but this is
where the reprobe comes from)
- allocate the dsa_switch outside devres and free it in .remove(), so
losing the remaining race with .shutdown() cannot let devres free it
while the DSA tree still holds dsa_ports pointing at it
- lower the per-slice write timeout from 120 s to 60 s, so this path
and the drain added in patch 4 agree on how long the same loader step
may take (found by Sashiko AI review)
v9: no changes
v8:
- refuse a second devlink dev flash while the previous one's reprobe is
still pending: the firmware API is short-circuited by then, so the
FW_UPDATE command faked success and the raw SB PDI writes ran against
a switch the driver no longer tracks (found by Sashiko AI review)
- omit the firmware version from devlink dev info while the cached copy
is all-zero, so a failed transfer no longer publishes 0.0.0 as both
running and stored, and clear asic_rev along with asic_id (found by
Sashiko AI review)
- only translate -ENODEV into success in port_mdb_del() during the
post-flash teardown; outside it, a genuine bus error was reported to
switchdev as a successful deletion (found by Sashiko AI review)
- evaluate the image verification verdict the loader publishes after
the last slice, instead of polling for a value it had already
published, so a rejected image is no longer reported as a write
timeout
- rename the end_magic label to no_end, which is what it does, and
correct the protocol comment: END is optional as the loader finalises
on a 2 s timeout, the status register keeps the byte count while a
chunk is programmed, and the half-bank switch does not clear DATA
(found by Sashiko AI review)
v7:
- reprobe from a single delayed work item instead of a kthread spawned
by a workqueue kickoff; the kthread existed only to drop the module
reference from core code, but its creation-failure path did the racy
module_put() from module text anyway and could strand the driver
bound with skip_teardown set. The collapsed form matches
iwl_trans_reprobe_wk(), and a failed reprobe now leaves the device
unbound like a failed probe
- only signal END on a successful transfer; a failure leaves the loader
mid-payload, where a STAT write is a byte count and END (0x3cc3) is
read as one, risking a receive-counter underflow, so return the error
and let the reprobe recover
- add cond_resched() to the payload loop so a long transfer over a
bit-banged MDIO bus under CONFIG_PREEMPT_NONE does not trip the
soft-lockup detector
- drop the cached firmware version and chip id on a failed flash so
devlink dev info stops reporting the pre-flash version until the
reprobe re-reads it
- report the firmware version under DEVLINK_INFO_VERSION_GENERIC_FW
instead of a bare "fw" string
- correct the SB PDI header comment's SMDIO register map (page in MII
reg 0x1f, register from the low nibble) and expand the note on why
closing the shared conduit is safe
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/Kconfig | 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 | 680 +++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 18 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 20 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 77 ++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 25 +
9 files changed, 833 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/Kconfig b/drivers/net/dsa/mxl862xx/Kconfig
index e51a67a3cf9b..8bb524fe28d1 100644
--- a/drivers/net/dsa/mxl862xx/Kconfig
+++ b/drivers/net/dsa/mxl862xx/Kconfig
@@ -3,6 +3,7 @@ config NET_DSA_MXL862
tristate "MaxLinear MxL862xx"
depends on NET_DSA
select CRC16
+ select CRC32
select NET_DSA_TAG_MXL_862XX
help
This enables support for the MaxLinear MxL862xx switch family.
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..7a506d110c8d
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -0,0 +1,680 @@
+// 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> & 0xfff0) ; page latch
+ * MII reg (<sb_pdi_reg> & 0x000f) := / => <u16 data>
+ * so CTRL/ADDR/DATA/STAT (0xe100..0xe103) are MII regs 0/1/2/3 of page
+ * 0xe100, not all reg 0x00.
+ *
+ * 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 -> finalise now (optional, see below)
+ *
+ * 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: CTRL:=RST; 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=0 image verified
+ * (STAT=1: image rejected)
+ * STAT := END(0x3cc3) ---------------------> finalise and boot
+ *
+ * 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 ONLY when r_remain hits EXACTLY 0, and a count larger
+ * than r_remain underflows the 32-bit counter and wedges the loader until a
+ * power cycle. Having left it, the loader verifies the image, publishes the
+ * verdict in STAT (0 good, 1 rejected) and waits 2 s for END before
+ * finalising regardless -- clearing its rescue-enable bit so boot_go boots
+ * the new image -- so END only saves that wait. Hence:
+ * - never send a slice/chunk count larger than what is outstanding;
+ * - a STAT write is a command only once the loader has left the loop;
+ * - the loader leaves the count in STAT while it programs the chunk, so
+ * a lingering count does not distinguish "busy" from "verdict".
+ */
+
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/iopoll.h>
+#include <linux/netdevice.h>
+#include <linux/overflow.h>
+#include <linux/rtnetlink.h>
+#include <linux/sched.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
+
+/* Image verification verdict published in STAT once the receive loop ends */
+#define MXL862XX_SB_PDI_VERIFY_OK 0
+#define MXL862XX_SB_PDI_VERIFY_BAD 1
+
+/* 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 60000
+#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;
+
+ /* CTRL selects RST mode; ADDR and DATA are cleared to 0. */
+ 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, 0x0000);
+ if (ret < 0)
+ return ret;
+
+ return mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, 0x0000);
+}
+
+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);
+}
+
+/* Flush the last slice, which ends the receive loop: the loader verifies the
+ * image and replaces the count in STAT with its verdict, so wait for the count
+ * to go rather than for a fixed value.
+ */
+static int mxl862xx_sb_pdi_flush_last(struct mxl862xx_priv *priv,
+ u32 data_written)
+{
+ int ret, val;
+
+ 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;
+
+ ret = read_poll_timeout(mxl862xx_smdio_read, val,
+ val < 0 || (u16)val != (u16)data_written,
+ 10000, MXL862XX_FW_WRITE_TIMEOUT_MS * 1000,
+ false, priv, MXL862XX_SB_PDI_STAT);
+ if (val < 0)
+ return val;
+
+ if (!ret && (u16)val == MXL862XX_SB_PDI_VERIFY_OK)
+ return 0;
+
+ /* A final count of 1 is indistinguishable from the reject verdict, so
+ * a timeout still holding it lands here too.
+ */
+ if ((u16)val == MXL862XX_SB_PDI_VERIFY_BAD) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: loader rejected the image\n");
+ return -EBADMSG;
+ }
+
+ return ret ? ret : -EPROTO;
+}
+
+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);
+}
+
+/* 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 end up at no_end, which returns the error
+ * without signalling END -- see there.
+ */
+ 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 no_end;
+ }
+
+ /* 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 no_end;
+ }
+
+ /* 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 no_end;
+ }
+
+ /* 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 no_end;
+ }
+
+ /* 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) {
+ cond_resched();
+ 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_last(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 no_end;
+ }
+ 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 slice failed: %pe\n", ERR_PTR(ret));
+ goto no_end;
+ }
+
+ mxl862xx_flash_notify(dl, "Flashing", payload_size, payload_size);
+
+ /* Success: the loader has left the receive loop at r_remain == 0 and
+ * verified the image, so END(0x3cc3) is a finalise/boot request rather
+ * than a byte count. Signal it here -- and only here -- to boot the new
+ * image without waiting out the loader's 2 s END timeout.
+ */
+ ret = mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_END);
+ if (ret < 0)
+ dev_warn(&priv->mdiodev->dev,
+ "flash: END signalling failed, waiting the loader out: %pe\n",
+ ERR_PTR(ret));
+
+ msleep(MXL862XX_FW_REBOOT_DELAY_MS);
+ return 0;
+
+write_err:
+ dev_err(&priv->mdiodev->dev, "flash: SMDIO write failed: %pe\n",
+ ERR_PTR(ret));
+no_end:
+ /* A failure leaves the loader mid transfer; do not signal END (a STAT
+ * write is a byte count then, and END would be misread as one, risking
+ * a receive-counter underflow). Return the error; the caller reprobes.
+ */
+ 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;
+ }
+
+ /* An all-zero version is the cache a failed flash left behind, not a
+ * released firmware; omit it like the part number above.
+ */
+ if (!priv->fw_version.major && !priv->fw_version.minor &&
+ !priv->fw_version.revision)
+ 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,
+ DEVLINK_INFO_VERSION_GENERIC_FW, buf);
+ if (ret)
+ return ret;
+
+ /* boots this image from its own flash: stored == running */
+ return devlink_info_version_stored_put(req,
+ DEVLINK_INFO_VERSION_GENERIC_FW, buf);
+}
+
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u32 payload_size;
+ int ret, err, i;
+
+ if (params->component) {
+ NL_SET_ERR_MSG_MOD(extack, "component is not supported");
+ return -EOPNOTSUPP;
+ }
+
+ /* Written under the instance lock this call is holding. */
+ if (priv->shutting_down) {
+ NL_SET_ERR_MSG_MOD(extack, "device is shutting down");
+ return -ENODEV;
+ }
+
+ /* A previous flash is still waiting for its reprobe: the firmware API
+ * is short-circuited, so the raw SB PDI writes below would run against
+ * a switch this driver no longer tracks.
+ */
+ if (priv->skip_teardown) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "a previous flash awaits its reprobe");
+ return -EBUSY;
+ }
+
+ ret = mxl862xx_flash_validate(priv, params->fw, &payload_size);
+ if (ret) {
+ NL_SET_ERR_MSG_MOD(extack, "firmware image validation failed");
+ return ret;
+ }
+
+ 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 is only closed,
+ * not detached: it belongs to the MAC driver. This driver binds a
+ * single switch with a direct host link and no cascade ports, so the
+ * conduit serves only this switch, and flashing it reboots the switch,
+ * which takes the tree down regardless.
+ */
+ 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);
+ disable_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);
+ /* Keep block_host set so host writes stay blocked, but let the
+ * readiness poll below read the freshly booted firmware.
+ */
+ priv->flash_owner = current;
+ 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");
+ }
+
+ if (ret) {
+ /* The switch is in MCUboot with erased or partly written flash;
+ * drop the cached identity so devlink dev info stops reporting
+ * the pre-flash version until the reprobe re-reads the truth.
+ */
+ memset(&priv->fw_version, 0, sizeof(priv->fw_version));
+ priv->asic_id = 0;
+ priv->asic_rev = 0;
+ }
+
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+ priv->flash_owner = NULL;
+ priv->block_host = false;
+ priv->skip_teardown = true;
+ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
+
+ /* Reinitialise through a deferred re-probe: remove() runs with
+ * skip_teardown set, then a fresh probe() starts against whatever
+ * the switch now runs. The core skips the re-probe if the device
+ * is unbound or shut down before it fires.
+ */
+ err = device_schedule_reprobe(ds->dev, MXL862XX_FW_REPROBE_DELAY_MS);
+ if (!ret && err)
+ dev_err(ds->dev,
+ "flash: new firmware is running but re-probe could not be scheduled (%pe); unbind and rebind to reinitialise\n",
+ ERR_PTR(err));
+
+ return ret ? ret : err;
+}
+
+/* The devlink core holds the instance lock across a flash, so taking it here
+ * waits for a transfer in flight instead of cutting the image in half, and
+ * bars one that has not started yet.
+ */
+void mxl862xx_flash_shutdown(struct dsa_switch *ds)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+
+ if (!ds->devlink)
+ return;
+
+ /* A flash holds the instance lock for its whole run. Announce the
+ * wait so the delay is not mistaken for a hang.
+ */
+ if (!devl_trylock(ds->devlink)) {
+ dev_info(ds->dev,
+ "firmware update in progress, waiting for it to finish\n");
+ devl_lock(ds->devlink);
+ }
+ priv->shutting_down = true;
+ devl_unlock(ds->devlink);
+}
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.h b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
new file mode 100644
index 000000000000..15ed3a46bcfe
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -0,0 +1,18 @@
+/* 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);
+void mxl862xx_flash_shutdown(struct dsa_switch *ds);
+
+#endif /* __MXL862XX_FW_H */
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-host.c b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
index 11759fa6069b..4b3956a518cf 100644
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -12,9 +12,11 @@
#include <linux/crc16.h>
#include <linux/iopoll.h>
#include <linux/limits.h>
+#include <linux/sched.h>
#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 +342,24 @@ 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;
+ }
+
+ /* During the post-flash readiness poll block_host stays set, but the
+ * flash path's own firmware version reads must reach the new image;
+ * host writes stay blocked so stale resource IDs cannot corrupt it.
+ * A blocked write reports success: the reprobe discards the switch
+ * configuration anyway, and a bridge tearing down over a flash must
+ * not see port_vlan_del() fail, which leaks its VLAN group.
+ */
+ if (priv->block_host && cmd != SYS_MISC_FW_UPDATE &&
+ !(read && priv->flash_owner == current)) {
+ ret = read ? -EBUSY : 0;
+ 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 e05ad52cd297..33a7cdb8edd3 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,45 @@ 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)
+/* The CHIP ID registers are only readable through the firmware mailbox, so
+ * the values are cached here and stay zero while no firmware answers.
+ */
+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 +300,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:
@@ -1547,6 +1598,13 @@ static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
+ /* A flash blocks the API (-EBUSY) and its teardown drops the MAC
+ * table (-ENODEV); there is then nothing to program, and the reprobe
+ * rebuilds the configuration. See mxl862xx_port_mdb_del().
+ */
+ if ((ret == -EBUSY && priv->block_host) ||
+ (ret == -ENODEV && priv->skip_teardown))
+ return 0;
if (ret)
return ret;
@@ -1584,6 +1642,13 @@ 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);
+ /* A flash blocks the API (-EBUSY) and its teardown drops the MAC
+ * table (-ENODEV); a delete then has nothing to do. Outside these,
+ * both are bus errors and must be reported.
+ */
+ if ((ret == -EBUSY && priv->block_host) ||
+ (ret == -ENODEV && priv->skip_teardown))
+ return 0;
if (ret)
return ret;
@@ -2097,6 +2162,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)
@@ -2161,6 +2228,12 @@ static void mxl862xx_remove(struct mdio_device *mdiodev)
priv = ds->priv;
+ /* Wait out a flash in flight and bar a new one before the DSA core
+ * frees the user netdevs; the devlink instance lock does not cover
+ * that free, which dsa_unregister_switch() reaches first.
+ */
+ mxl862xx_flash_shutdown(ds);
+
set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
dsa_unregister_switch(ds);
@@ -2187,6 +2260,8 @@ static void mxl862xx_shutdown(struct mdio_device *mdiodev)
priv = ds->priv;
+ mxl862xx_flash_shutdown(ds);
+
dsa_switch_shutdown(ds);
set_bit(MXL862XX_FLAG_WORK_STOPPED, &priv->flags);
diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.h b/drivers/net/dsa/mxl862xx/mxl862xx.h
index 432a5f3f2e08..054d0d35d3a5 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,19 @@ 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: during a firmware flash, a host firmware read fails
+ * with -EBUSY and a write reports success without
+ * touching the bus, so a teardown racing the flash
+ * does not fail; FW_UPDATE and the flash owner's own
+ * reads still reach the bus
+ * @flash_owner: task running the post-flash readiness poll; only its
+ * own firmware reads pass block_host
+ * @skip_teardown: during the post-flash reprobe teardown, a host
+ * firmware read fails with -ENODEV and a write reports
+ * success without touching the bus
+ * @shutting_down: set under the devlink instance lock once ->shutdown()
+ * or .remove() has begun, so no flash starts while the
+ * switch is going away
* @stats_work: periodic work item that polls RMON hardware counters
* and accumulates them into 64-bit per-port stats
*/
@@ -329,6 +346,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 +356,13 @@ struct mxl862xx_priv {
u16 evlan_ingress_size;
u16 evlan_egress_size;
u16 vf_block_size;
+ struct task_struct *flash_owner;
+ bool block_host;
+ bool skip_teardown;
+ bool shutting_down;
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] 2+ messages in thread
* Re: [PATCH net-next v17 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get
@ 2026-09-25 5:34 netdev-bot+sashiko
0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-09-25 5:34 UTC (permalink / raw)
To: daniel
Cc: jiri, davem, edumazet, kuba, pabeni, horms, corbet, skhan,
rdunlap, gregkh, rafael, dakr, andrew, olteanv, linux, netdev,
linux-doc, linux-kernel, driver-core
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 4 · Low: 2
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] After a failed transfer in mxl862xx_devlink_flash_update()
(drivers/net/dsa/mxl862xx/mxl862xx-fw.c), the driver sets…
- [Medium] mxl862xx_port_mdb_add() and mxl862xx_port_mdb_del()
(drivers/net/dsa/mxl862xx/mxl862xx.c) decide whether a failed…
- [Medium] In mxl862xx_devlink_flash_update()
(drivers/net/dsa/mxl862xx/mxl862xx-fw.c), the…
- [Medium] After writing STAT := 20 to hand over the header,
mxl862xx_flash_firmware() waits for the header ACK with…
- [Low] The commit message says the post-flash re-probe scheduled through
device_schedule_reprobe() 'runs in the driver core off the devlink…
- [Low] The kernel-doc no longer matches how the cached identity fields
are handled at runtime.
--
net: dsa: mxl862xx: add devlink flash_update and info_get
This patch adds devlink info_get and flash_update support to the mxl862xx
DSA driver. A flash reboots the switch into its MCUboot loader, streams the
image over the SB PDI clause-22 SMDIO protocol, and then schedules a
deferred re-probe through device_schedule_reprobe() to reinitialise the
driver. Firmware API access is blocked while the flash runs, and
->shutdown() and .remove() take the devlink instance lock so they wait for
a flash that is already in progress.
> MCUboot offers no in-place path back, so the driver reinitialises with a
> deferred re-probe scheduled through device_schedule_reprobe() from the
> previous patch, which runs in the driver core off the devlink caller's
> locks and without holding a module or device reference. ->shutdown() and
[Severity: Low]
Is the part about the device reference accurate? device_schedule_reprobe()
takes one:
drivers/base/dd.c:device_schedule_reprobe() {
...
rp->dev = get_device(dev);
rp->drv = drv;
...
}
device_reprobe_work_fn() drops it with put_device(dev) after the attach, so
only the module reference is avoided. Could this sentence be reworded?
> diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-fw.c b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
> new file mode 100644
> index 0000000000000..7a506d110c8da
> --- /dev/null
> +++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
> @@ -0,0 +1,680 @@
[ ... ]
> + /* 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 no_end;
> + }
[Severity: Medium]
Can this poll miss the header ACK? The protocol description at the top of
this file says STAT only holds 21 until the erase finishes:
* <-------------- STAT=21 (len+1), then STAT=0
* (erased)
mxl862xx_sb_pdi_poll_stat() only accepts an exact match, and its reads come
2.5 to 10 ms apart, each read being two MDIO transactions. STAT can go from
21 to 0 between two samples, for example with a short erase, preemption, or
a wait on mdio_lock. The poll then sees 0 until the 5 s ACK timeout runs
out, logs "header ACK failed" and jumps to no_end. By then the loader has
accepted the header, erased the flash and is waiting in its receive loop.
mxl862xx_sb_pdi_flush_last() in the same file avoids this by waiting for
STAT to change away from the value the host wrote. Could the header ACK
poll accept either 21 or 0 in the same way?
At this commit, a missed ACK leaves the switch in MCUboot with its flash
erased. The poll is unchanged later in the series, where a missed ACK leads
to the rescue-mode drain. That drain feeds one byte at a time and can take
tens of minutes.
[ ... ]
> +write_err:
> + dev_err(&priv->mdiodev->dev, "flash: SMDIO write failed: %pe\n",
> + ERR_PTR(ret));
> +no_end:
> + /* A failure leaves the loader mid transfer; do not signal END (a STAT
> + * write is a byte count then, and END would be misread as one, risking
> + * a receive-counter underflow). Return the error; the caller reprobes.
> + */
> + return ret;
> +}
[Severity: Medium]
Does the reprobe recover the device at this point in the series? After a
failed transfer the switch is still in MCUboot, but at this commit
mxl862xx_setup() goes straight to the clause-45 firmware API:
ret = mxl862xx_reset(priv);
if (ret)
return ret;
ret = mxl862xx_wait_ready(ds);
MCUboot does not answer that API, so the re-probe fails and
device_reprobe_work_fn() logs "re-probe failed, device left unbound". Once
the device is unbound, no devlink instance is left to retry the flash from.
The loader also stays resident across a power cycle, because the flash is
erased or half written.
The next patch in the series, "net: dsa: mxl862xx: recover switch stuck in
MCUboot rescue mode", fixes this by calling mxl862xx_rescue_mode_detect()
at the top of mxl862xx_setup(). So the gap only affects bisection within
the series. Could the patches be reordered, or the "the caller reprobes"
comment and the commit message be adjusted until the rescue-mode patch
lands?
[ ... ]
> + mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
> + priv->flash_owner = NULL;
> + priv->block_host = false;
> + priv->skip_teardown = true;
> + mutex_unlock(&priv->mdiodev->bus->mdio_lock);
> +
> + /* Reinitialise through a deferred re-probe: remove() runs with
> + * skip_teardown set, then a fresh probe() starts against whatever
> + * the switch now runs. The core skips the re-probe if the device
> + * is unbound or shut down before it fires.
> + */
> + err = device_schedule_reprobe(ds->dev, MXL862XX_FW_REPROBE_DELAY_MS);
> + if (!ret && err)
> + dev_err(ds->dev,
> + "flash: new firmware is running but re-probe could not be scheduled (%pe); unbind and rebind to reinitialise\n",
> + ERR_PTR(err));
> +
> + return ret ? ret : err;
> +}
[Severity: Medium]
What happens if the transfer fails and device_schedule_reprobe() also
fails, for example with -ENOMEM from kzalloc_obj() or kstrdup()?
The scheduling error is only logged when ret is 0, and the return
statement drops it otherwise. By this point the user ports are closed and
detached, the conduit is closed, stats_work is disabled and skip_teardown
is set. Only the reprobe undoes any of that.
The device then stays bound, but every firmware read fails with -ENODEV
and every write is dropped. Nothing in the log says that an unbind and
rebind is needed.
A retry of devlink dev flash then hits the early check in
mxl862xx_devlink_flash_update():
if (priv->skip_teardown) {
NL_SET_ERR_MSG_MOD(extack,
"a previous flash awaits its reprobe");
return -EBUSY;
}
That check fails even though no reprobe is pending. The path is still the
same at the end of the series, where the rescue-mode reprobe would
otherwise recover the switch.
Should the scheduling failure also be logged, and possibly returned, when
the transfer failed?
[ ... ]
> diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c
> index e05ad52cd297e..33a7cdb8edd31 100644
> --- a/drivers/net/dsa/mxl862xx/mxl862xx.c
> +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
[ ... ]
> @@ -222,7 +230,45 @@ static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr,
[ ... ]
> +/**
> + * 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)
> {
[ ... ]
> @@ -254,6 +300,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;
[Severity: Low]
This isn't a bug, but the kernel-doc here and in struct mxl862xx_priv no
longer quite matches the code.
The mxl862xx_wait_ready() kernel-doc says it will "cache the firmware
version and chip ID". However, a failed mxl862xx_read_chip_id() is only
logged and the function still returns 0. If SYS_MISC_REG_RD fails after a
successful flash, asic_id and asic_rev keep their pre-flash values, and
mxl862xx_devlink_flash_update() treats the 0 return as a complete refresh.
The @fw_version comment in mxl862xx.h still says:
* @fw_version: cached firmware version, populated at probe and
* compared with MXL862XX_FW_VER_MIN()
mxl862xx_devlink_flash_update() now also rewrites fw_version at runtime,
through mxl862xx_wait_ready() after a successful flash and with memset()
after a failed one. So MXL862XX_FW_VER_MIN() checks can see 0.0.0 until
the reprobe.
Could these comments be updated?
[ ... ]
> @@ -1547,6 +1598,13 @@ static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
> qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
>
> ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
> + /* A flash blocks the API (-EBUSY) and its teardown drops the MAC
> + * table (-ENODEV); there is then nothing to program, and the reprobe
> + * rebuilds the configuration. See mxl862xx_port_mdb_del().
> + */
> + if ((ret == -EBUSY && priv->block_host) ||
> + (ret == -ENODEV && priv->skip_teardown))
> + return 0;
> if (ret)
> return ret;
[Severity: Medium]
Can this check race with the end of a flash? mxl862xx_api_wrap() returns
-EBUSY while holding mdio_lock with block_host set. Here, though,
block_host and skip_teardown are read again after that lock has been
released. mxl862xx_devlink_flash_update() changes both flags in a single
critical section:
priv->flash_owner = NULL;
priv->block_host = false;
priv->skip_teardown = true;
If that runs between api_wrap() returning and this check, neither
condition matches:
CPU1 (MDB add/del) CPU2 (flash task)
mxl862xx_api_wrap()
block_host set -> -EBUSY
mutex_unlock(mdio_lock)
mutex_lock(mdio_lock)
block_host = false
skip_teardown = true
mutex_unlock(mdio_lock)
ret == -EBUSY && block_host -> false
ret == -ENODEV && ... -> false
return -EBUSY
The flash task holds only the devlink lock at this point, so an MDB change
from a bridge teardown can run at the same time.
For a CPU port, dsa_port_do_mdb_del() in net/dsa/switch.c then keeps the
entry on dp->mdbs:
err = ds->ops->port_mdb_del(ds, port, mdb, db);
trace_dsa_mdb_del_hw(dp, mdb->addr, mdb->vid, &db, err);
if (err) {
refcount_set(&a->refcount, 1);
goto out;
}
The entry stays until dsa_switch_release_ports() in the reprobe cleans it
up. In the add direction, port_mdb_add() reports a failure that should not
happen. The v17 changelog and the comment above both say this case is
handled.
The change from block_host to skip_teardown only goes one way. Would it
work to treat either errno as success whenever block_host or skip_teardown
is set? Another option is for mxl862xx_api_wrap() to return a distinct
result for a command blocked by a flash. The same check in
mxl862xx_port_mdb_del() has the same window.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1790130482.git.daniel%40makrotopia.org
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-09-25 5:34 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-25 5:34 [PATCH net-next v17 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get netdev-bot+sashiko
-- strict thread matches above, loose matches on Subject: below --
2026-09-23 2:33 [PATCH net-next v17 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
2026-09-23 2:35 ` [PATCH net-next v17 4/6] net: dsa: mxl862xx: add devlink flash_update and info_get Daniel Golle
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox