* [PATCH net-next v12 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1
@ 2026-09-02 4:42 Sagi Maimon
2026-09-02 4:42 ` [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon
2026-09-02 4:42 ` [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon
0 siblings, 2 replies; 5+ messages in thread
From: Sagi Maimon @ 2026-09-02 4:42 UTC (permalink / raw)
To: netdev
Cc: vadim.fedorenko, richardcochran, kuba, andrew+netdev, davem,
edumazet, pabeni, linux-kernel, Sagi Maimon
The ADVA TimeCard X1 (PCI device 0x0410) carries a Lattice MachXO3 CPLD
on the TMC I2C bus. This series adds access to it and an update path
through the kernel firmware-upload subsystem.
The card has two I2C controllers. Linux registers only the one at
0x00150000, normally routed to the config EEPROMs; the one at 0x00120000
belongs to the on-card MicroBlaze and is routed to the TMC or M.2 bus as
its firmware selects. A request/grant handshake re-routes the host
controller onto the TMC bus, where the PCA9548 and the CPLD behind its
channel 0 live. Everything here is built on that handshake.
Patch 1 adds the arbitration and the read-only interfaces on top of it.
Patch 2 adds the flashing.
v11: https://lore.kernel.org/netdev/20260811135720.109580-1-maimon.sagi@gmail.com/T/#u
Changes since v11 - all from Vadim's review of the ISC command path:
- adva_x1_i2c_xfer() takes the opcode as its own argument and assembles
the message in a scratch buffer taken once per bus claim, so the
opcode and its arguments are copied exactly once and a firmware upload
costs one allocation instead of one per 16-byte page. The bound check
lives here now, against the scratch size. Patch 1.
- adva_x1_cpld_write() takes just the opcode; the per-command argument
bytes come from adva_x1_cpld_args(), a switch-case helper returning a
pointer to the static data. This drops the seven per-caller argument
arrays, including the dis_args duplicated between prepare() and
cleanup(). Patch 2.
- Commands whose arguments are all zero (RESET_ADDR, SET_DONE, DIS_CFG,
REFRESH) pass NULL and the transfer sends zeros, so the zero3 arrays
are gone.
- Comment fix: the LCMXO3LF-2100 IDCODE is 0xe12bc043 (0x612bc043 is the
4300-LUT part). Patch 1.
No functional change: the byte sequences on the wire are unchanged, and
CPLD programming was verified on an ADVA TimeCard X1.
Sagi Maimon (2):
ptp: ocp: add TAP CPLD access for ADVA TimeCard X1
ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1
Documentation/ABI/testing/sysfs-timecard | 24 +
drivers/ptp/Kconfig | 2 +
drivers/ptp/ptp_ocp.c | 693 ++++++++++++++++++++++-
3 files changed, 715 insertions(+), 4 deletions(-)
base-commit: c29b37ed7a4d9856ed758a82282456d69cee2ed1
--
2.47.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access for ADVA TimeCard X1
2026-09-02 4:42 [PATCH net-next v12 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon
@ 2026-09-02 4:42 ` Sagi Maimon
2026-09-04 7:46 ` netdev-bot+sashiko
2026-09-02 4:42 ` [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon
1 sibling, 1 reply; 5+ messages in thread
From: Sagi Maimon @ 2026-09-02 4:42 UTC (permalink / raw)
To: netdev
Cc: vadim.fedorenko, richardcochran, kuba, andrew+netdev, davem,
edumazet, pabeni, linux-kernel, Sagi Maimon
The ADVA TimeCard X1 (PCI device 0x0410) carries a Lattice MachXO3 CPLD
on the TMC I2C bus. The card has two I2C controllers: Linux registers
only the one at 0x00150000, normally routed to the config EEPROMs, while
the one at 0x00120000 belongs to the on-card MicroBlaze and is routed to
either the TMC or the M.2 bus as its firmware selects.
A request/grant handshake re-routes the host controller from the EEPROM
segment onto the TMC bus, leaving the MicroBlaze with M.2 only. The
PCA9548 at 0x74 and the CPLD at 0x40 behind its channel 0 are reachable
from the host only while that grant is held.
Add the arbitration and the read-only interfaces built on it. Because
the handshake changes what the host adapter is wired to, an operation
takes the i2c core adapter lock for the whole grant window and uses
__i2c_transfer() internally; without that, a concurrent transfer from
ptp_ocp_read_eeprom(), from the nvmem attributes or from the at24 sysfs
files would be issued onto the TMC bus instead of to the EEPROM.
The Lattice device ID is a fixed property of the part, so report it as
the fixed devlink version "cpld.id" rather than as a sysfs attribute,
and cache it so the arbitration is paid once per card.
Add one X1-only attribute:
/sys/class/timecard/ocpN/cpld_status
report the CPLD status register, including the DONE, BUSY and
FAILED indicators
A read arbitrates for the shared bus and reprograms the on-card mux, so
it is root-only and takes cpld_lock interruptibly.
Signed-off-by: Sagi Maimon <maimon.sagi@gmail.com>
---
Documentation/ABI/testing/sysfs-timecard | 20 ++
drivers/ptp/ptp_ocp.c | 362 ++++++++++++++++++++++-
2 files changed, 378 insertions(+), 4 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard
index 3ae41b7634ac..b384e837c5e6 100644
--- a/Documentation/ABI/testing/sysfs-timecard
+++ b/Documentation/ABI/testing/sysfs-timecard
@@ -11,6 +11,26 @@ Contact: Jonathan Lemon <jonathan.lemon@gmail.com>
Description: This directory contains the attributes of the Nth timecard
registered.
+What: /sys/class/timecard/ocpN/cpld_status
+Date: July 2026
+Contact: Sagi Maimon <maimon.sagi@gmail.com>
+Description: (RO, root only) The status register of the TAP CPLD, in
+ human-readable form:
+
+ done=<0|1> busy=<0|1> failed=<0|1>
+
+ Only present on ADVA x1 TAP boards (PCI ID 0xad5a:0x0410).
+
+ done=1 indicates the configuration flash was successfully
+ programmed and is active. busy=1 means an internal operation
+ is in progress. failed=1 means the last ISC operation failed.
+
+ A read arbitrates for the shared I2C bus and reprograms the
+ on-card mux, so it is restricted to root. The Lattice device
+ ID of the CPLD is reported as the fixed "cpld.id" version by
+ devlink dev info.
+
+
What: /sys/class/timecard/ocpN/available_clock_sources
Date: September 2021
Contact: Jonathan Lemon <jonathan.lemon@gmail.com>
diff --git a/drivers/ptp/ptp_ocp.c b/drivers/ptp/ptp_ocp.c
index 3d26ec1f7b9e..0c5c3ec8de5f 100644
--- a/drivers/ptp/ptp_ocp.c
+++ b/drivers/ptp/ptp_ocp.c
@@ -24,6 +24,8 @@
#include <linux/nvmem-consumer.h>
#include <linux/crc16.h>
#include <linux/dpll.h>
+#include <linux/unaligned.h>
+#include <linux/delay.h>
#define PCI_DEVICE_ID_META_TIMECARD 0x0400
@@ -85,6 +87,7 @@ struct ptp_ocp_adva_info {
u8 signals_nr;
u8 freq_in_nr;
const struct ocp_attr_group *attr_groups;
+ bool has_cpld; /* x1: supports CPLD firmware upload */
};
#define OCP_CTRL_ENABLE BIT(0)
@@ -163,7 +166,8 @@ struct gpio_reg {
u32 gpio1;
u32 __pad0;
u32 gpio2;
- u32 __pad1;
+ /* adva_x1: I2C bus ownership register; reserved on other variants */
+ u32 i2c_bus_ctrl;
};
struct irig_master_reg {
@@ -416,6 +420,13 @@ struct ptp_ocp {
dpll_tracker tracker;
int signals_nr;
int freq_in_nr;
+ /* adva_x1 CPLD I2C (internal use only) */
+ struct mutex cpld_lock; /* serialises CPLD operations */
+ int cpld_i2c_adap_nr; /* I2C adapter nr; -1 if absent */
+ struct i2c_adapter *cpld_adap; /* claimed adapter; valid under cpld_lock */
+ u8 *cpld_buf; /* DMA-safe scratch; valid under cpld_lock */
+ u32 cpld_id; /* cached Lattice device ID; 0 if unread */
+ bool has_cpld; /* x1 TAP CPLD present */
};
#define OCP_REQ_TIMESTAMP BIT(0)
@@ -451,6 +462,7 @@ static int ptp_ocp_adva_board_init(struct ptp_ocp *bp, struct ocp_resource *r);
static const struct ocp_sma_op ocp_adva_sma_op;
static const struct ocp_sma_op ocp_adva_x1_sma_op;
+static int adva_x1_cpld_device_id(struct ptp_ocp *bp, u32 *id);
static const struct ocp_attr_group fb_timecard_groups[];
@@ -1273,6 +1285,7 @@ static struct ocp_resource ocp_adva_x1_resource[] = {
.signals_nr = 4,
.freq_in_nr = 4,
.attr_groups = adva_timecard_x1_groups,
+ .has_cpld = true,
},
},
{ }
@@ -2185,6 +2198,19 @@ ptp_ocp_devlink_info_get(struct devlink *devlink, struct devlink_info_req *req,
if (err)
return err;
+ if (bp->has_cpld) {
+ u32 id;
+
+ err = adva_x1_cpld_device_id(bp, &id);
+ if (err)
+ return err;
+
+ sprintf(buf, "0x%08x", id);
+ err = devlink_info_version_fixed_put(req, "cpld.id", buf);
+ if (err)
+ return err;
+ }
+
return 0;
}
@@ -3199,6 +3225,7 @@ ptp_ocp_adva_board_init(struct ptp_ocp *bp, struct ocp_resource *r)
return err;
ptp_ocp_sma_init(bp);
+ bp->has_cpld = info->has_cpld;
return ptp_ocp_init_clock(bp, &info->servo);
}
@@ -4226,6 +4253,315 @@ static const struct ocp_attr_group art_timecard_groups[] = {
{ },
};
+/*
+ * Internal helpers for the adva_x1 TAP CPLD (Lattice LCMXO3LF-2100).
+ *
+ * The card has two I2C controllers; Linux registers only 0x00150000.
+ * The i2c_bus_ctrl handshake re-routes what each one is wired to:
+ *
+ * grant 0: 0x150000 -> EEPROMs 0x120000 -> TMC or M.2, MB's choice
+ * grant 1: 0x150000 -> TMC bus 0x120000 -> M.2
+ *
+ * The PCA9548 at 0x74 and the CPLD at 0x40 behind its channel 0 sit on
+ * the TMC bus, so they are reachable only while the grant is held. For
+ * that same window the EEPROMs are not behind the adapter at all, so a
+ * CPLD operation holds cpld_lock and the i2c adapter lock across it to
+ * keep the EEPROM and nvmem paths off the controller.
+ *
+ * No raw I2C access is exposed to userspace, only the attributes below.
+ */
+
+#define ADVA_MUX_ADDR 0x74
+#define ADVA_CPLD_ADDR 0x40
+#define ADVA_MUX_CHANNEL 0
+
+/* Longest command or reply; bounds each half of the per-claim scratch. */
+#define ADVA_CPLD_XFER_MAX 32
+
+#define MBLAZE_REQUEST 0x0000aaaaU
+#define MBLAZE_GRANTED 0x5555aaaaU
+#define MBLAZE_RELEASE 0x55550000U
+#define MBLAZE_RETRIES 200
+#define MBLAZE_RETRY_US 10000
+
+/* Lattice LCMXO3LF ISC command codes */
+#define CPLD_CMD_READ_ID 0xE0000000UL
+#define CPLD_CMD_READ_STATUS 0x3C000000UL
+
+/* Status register bit positions (Lattice LCMXO3LF datasheet) */
+#define CPLD_STATUS_DONE BIT(8)
+#define CPLD_STATUS_BUSY BIT(12)
+#define CPLD_STATUS_FAILED BIT(13)
+
+/*
+ * adva_x1_i2c_xfer() - issue a single I2C transaction on the TMC bus.
+ *
+ * Writes @cmd when it is not negative, followed by @wlen bytes of @wdata,
+ * then reads @rlen bytes if asked. A NULL @wdata sends zeros.
+ *
+ * The message is assembled in the scratch buffer taken by
+ * adva_x1_bus_claim(), which the Xilinx controller needs for DMA safety:
+ * an opcode and its arguments are copied exactly once.
+ *
+ * Caller must hold that claim, hence __i2c_transfer() over i2c_transfer().
+ */
+static int adva_x1_i2c_xfer(struct ptp_ocp *bp, u8 addr, int cmd,
+ const void *wdata, u8 wlen,
+ void *rdata, u8 rlen)
+{
+ u8 *wbuf, *rbuf;
+ struct i2c_adapter *adap;
+ struct i2c_msg msgs[2];
+ int nmsgs = 0, ret;
+ u16 hdr = cmd >= 0;
+
+ lockdep_assert_held(&bp->cpld_lock);
+
+ adap = bp->cpld_adap;
+ if (!adap || !bp->cpld_buf)
+ return -ENODEV;
+
+ if (hdr + wlen > ADVA_CPLD_XFER_MAX || rlen > ADVA_CPLD_XFER_MAX)
+ return -EINVAL;
+
+ wbuf = bp->cpld_buf;
+ rbuf = bp->cpld_buf + ADVA_CPLD_XFER_MAX;
+
+ if (hdr + wlen) {
+ if (hdr)
+ wbuf[0] = cmd;
+ if (wdata)
+ memcpy(wbuf + hdr, wdata, wlen);
+ else
+ memset(wbuf + hdr, 0, wlen);
+ msgs[nmsgs++] = (struct i2c_msg){
+ .addr = addr,
+ .flags = I2C_M_DMA_SAFE,
+ .len = hdr + wlen,
+ .buf = wbuf,
+ };
+ }
+ if (rlen) {
+ msgs[nmsgs++] = (struct i2c_msg){
+ .addr = addr,
+ .flags = I2C_M_RD | I2C_M_DMA_SAFE,
+ .len = rlen,
+ .buf = rbuf,
+ };
+ }
+
+ ret = __i2c_transfer(adap, msgs, nmsgs);
+ if (ret != nmsgs)
+ return (ret < 0) ? ret : -EIO;
+
+ if (rdata && rlen)
+ memcpy(rdata, rbuf, rlen);
+
+ return 0;
+}
+
+static void adva_x1_mblaze_release(struct ptp_ocp *bp)
+{
+ if (bp->pps_select)
+ iowrite32(MBLAZE_RELEASE, &bp->pps_select->i2c_bus_ctrl);
+}
+
+/* Acquire the shared I2C bus from the MicroBlaze firmware. Returns with no
+ * request outstanding on failure, so the firmware is never left granting a
+ * segment to a host that has given up waiting for it.
+ */
+static int adva_x1_mblaze_acquire(struct ptp_ocp *bp)
+{
+ u32 val;
+ int i;
+
+ if (!bp->pps_select)
+ return -ENODEV;
+
+ /* Drop a request left by a caller that died mid-sequence; cpld_lock
+ * keeps live ones out. The read back only flushes the posted write.
+ */
+ iowrite32(0, &bp->pps_select->i2c_bus_ctrl);
+ ioread32(&bp->pps_select->i2c_bus_ctrl);
+
+ iowrite32(MBLAZE_REQUEST, &bp->pps_select->i2c_bus_ctrl);
+ for (i = 0; i < MBLAZE_RETRIES; i++) {
+ usleep_range(MBLAZE_RETRY_US, MBLAZE_RETRY_US + 1000);
+ val = ioread32(&bp->pps_select->i2c_bus_ctrl);
+ if (val == MBLAZE_GRANTED)
+ return 0;
+ }
+
+ adva_x1_mblaze_release(bp);
+ return -ETIMEDOUT;
+}
+
+/* Route the host controller back to the EEPROMs and release the adapter.
+ * Safe after a failed claim: it also clears a request that was never granted.
+ */
+static void adva_x1_bus_release(struct ptp_ocp *bp)
+{
+ struct i2c_adapter *adap = bp->cpld_adap;
+
+ if (!adap)
+ return;
+
+ adva_x1_mblaze_release(bp);
+ bp->cpld_adap = NULL;
+ kfree(bp->cpld_buf);
+ bp->cpld_buf = NULL;
+ i2c_unlock_bus(adap, I2C_LOCK_ROOT_ADAPTER);
+ i2c_put_adapter(adap);
+}
+
+/*
+ * Claim the TMC bus for a CPLD operation. Holding the adapter lock over
+ * the handshake keeps ptp_ocp_read_eeprom(), the nvmem attributes and the
+ * at24 sysfs files off the controller while it is routed away from the
+ * EEPROMs. A firmware upload holds it across the whole prepare/write/poll
+ * sequence, so an EEPROM read blocks for as long as programming takes.
+ */
+static int adva_x1_bus_claim(struct ptp_ocp *bp)
+{
+ struct i2c_adapter *adap;
+ int ret;
+
+ lockdep_assert_held(&bp->cpld_lock);
+
+ adap = i2c_get_adapter(READ_ONCE(bp->cpld_i2c_adap_nr));
+ if (!adap)
+ return -ENODEV;
+
+ /* One scratch buffer per claim, not per transfer. */
+ bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL);
+ if (!bp->cpld_buf) {
+ i2c_put_adapter(adap);
+ return -ENOMEM;
+ }
+
+ i2c_lock_bus(adap, I2C_LOCK_ROOT_ADAPTER);
+ bp->cpld_adap = adap;
+
+ ret = adva_x1_mblaze_acquire(bp);
+ if (ret)
+ adva_x1_bus_release(bp);
+
+ return ret;
+}
+
+/* Select a mux channel, or deselect all with ch < 0 - the power-on state.
+ * The mux is on the TMC bus, so what it is left set to never affects the
+ * EEPROM paths.
+ */
+static int adva_x1_mux_select(struct ptp_ocp *bp, int ch)
+{
+ u8 val = (ch >= 0) ? BIT(ch) : 0;
+
+ return adva_x1_i2c_xfer(bp, ADVA_MUX_ADDR, val, NULL, 0, NULL, 0);
+}
+
+/*
+ * Send a 4-byte command then read data back without an intermediate STOP
+ * (Lattice combined write->repeated-START->read). Two messages in one
+ * transfer is exactly that, so no protocol-mangling flag is needed.
+ */
+static int adva_x1_cpld_cmd_read(struct ptp_ocp *bp,
+ u32 cmd_be, u8 *out, u8 out_len)
+{
+ __be32 cmd = cpu_to_be32(cmd_be);
+
+ return adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, -1, &cmd, 4, out, out_len);
+}
+
+static int adva_x1_cpld_read_status(struct ptp_ocp *bp, u32 *status)
+{
+ u8 buf[4];
+ int ret;
+
+ ret = adva_x1_cpld_cmd_read(bp, CPLD_CMD_READ_STATUS, buf, 4);
+ if (ret)
+ return ret;
+ *status = get_unaligned_be32(buf);
+ return 0;
+}
+
+/*
+ * Read the Lattice device ID of the TAP CPLD. It is a fixed property of
+ * the part, so cache it and pay the bus arbitration only once. The
+ * LCMXO3LF-2100 IDCODE is 0xe12bc043.
+ */
+static int adva_x1_cpld_device_id(struct ptp_ocp *bp, u32 *id)
+{
+ u8 data[4];
+ int ret;
+
+ if (bp->cpld_id) {
+ *id = bp->cpld_id;
+ return 0;
+ }
+
+ /* A CPLD operation can hold cpld_lock a long time; stay killable. */
+ ret = mutex_lock_interruptible(&bp->cpld_lock);
+ if (ret)
+ return ret;
+
+ ret = adva_x1_bus_claim(bp);
+ if (ret)
+ goto out;
+ ret = adva_x1_mux_select(bp, ADVA_MUX_CHANNEL);
+ if (ret)
+ goto release;
+ ret = adva_x1_cpld_cmd_read(bp, CPLD_CMD_READ_ID, data, 4);
+ if (!ret)
+ bp->cpld_id = get_unaligned_be32(data);
+ adva_x1_mux_select(bp, -1);
+release:
+ adva_x1_bus_release(bp);
+out:
+ mutex_unlock(&bp->cpld_lock);
+ if (!ret)
+ *id = bp->cpld_id;
+
+ return ret;
+}
+
+/*
+ * cpld_status - show the status register of the TAP CPLD.
+ *
+ * Returns a human-readable string: "done=<0|1> busy=<0|1> failed=<0|1>\n"
+ */
+static ssize_t
+cpld_status_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct ptp_ocp *bp = dev_get_drvdata(dev);
+ u32 st = 0;
+ int ret;
+
+ /* A CPLD operation can hold cpld_lock a long time; stay killable. */
+ ret = mutex_lock_interruptible(&bp->cpld_lock);
+ if (ret)
+ return ret;
+
+ ret = adva_x1_bus_claim(bp);
+ if (ret)
+ goto out;
+ ret = adva_x1_mux_select(bp, ADVA_MUX_CHANNEL);
+ if (ret)
+ goto release;
+ ret = adva_x1_cpld_read_status(bp, &st);
+ adva_x1_mux_select(bp, -1);
+release:
+ adva_x1_bus_release(bp);
+out:
+ mutex_unlock(&bp->cpld_lock);
+ return ret ? ret : sysfs_emit(buf, "done=%u busy=%u failed=%u\n",
+ !!(st & CPLD_STATUS_DONE),
+ !!(st & CPLD_STATUS_BUSY),
+ !!(st & CPLD_STATUS_FAILED));
+}
+static DEVICE_ATTR_ADMIN_RO(cpld_status);
+
static struct attribute *adva_timecard_attrs[] = {
&dev_attr_serialnum.attr,
&dev_attr_gnss_sync.attr,
@@ -4274,6 +4610,7 @@ static struct attribute *adva_timecard_x1_attrs[] = {
&dev_attr_ts_window_adjust.attr,
&dev_attr_utc_tai_offset.attr,
&dev_attr_tod_correction.attr,
+ &dev_attr_cpld_status.attr,
NULL,
};
@@ -4904,6 +5241,7 @@ ptp_ocp_detach(struct ptp_ocp *bp)
clk_hw_unregister_fixed_rate(bp->i2c_clk);
if (bp->n_irqs)
pci_free_irq_vectors(bp->pdev);
+ mutex_destroy(&bp->cpld_lock);
device_unregister(&bp->dev);
}
@@ -5080,6 +5418,17 @@ ptp_ocp_probe(struct pci_dev *pdev, const struct pci_device_id *id)
if (err)
goto out_disable;
+ /* Must be before the first error path that calls ptp_ocp_detach(),
+ * so mutex_destroy() always runs on an initialised mutex.
+ * Must also be before ptp_ocp_register_resources(): the I2C bus
+ * notifier (ptp_ocp_i2c_notifier_call) fires when the adapter
+ * registers and stores the adapter number in cpld_i2c_adap_nr; the
+ * -1 sentinel below must already be written so that a notifier
+ * firing during registration is never overwritten by this init.
+ */
+ mutex_init(&bp->cpld_lock);
+ bp->cpld_i2c_adap_nr = -1;
+
INIT_DELAYED_WORK(&bp->sync_work, ptp_ocp_sync_work);
/* compat mode.
@@ -5219,11 +5568,16 @@ ptp_ocp_i2c_notifier_call(struct notifier_block *nb,
found:
bp = dev_get_drvdata(dev);
- if (add)
+ if (add) {
ptp_ocp_symlink(bp, child, "i2c");
- else
+ /* Cache adapter nr; used by the CPLD status/id/upload paths
+ * for reference-counted unbind-safe adapter access.
+ */
+ WRITE_ONCE(bp->cpld_i2c_adap_nr, i2c_verify_adapter(child)->nr);
+ } else {
+ WRITE_ONCE(bp->cpld_i2c_adap_nr, -1); /* invalidate before free */
sysfs_remove_link(&bp->dev.kobj, "i2c");
-
+ }
return 0;
}
--
2.47.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1
2026-09-02 4:42 [PATCH net-next v12 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon
2026-09-02 4:42 ` [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon
@ 2026-09-02 4:42 ` Sagi Maimon
2026-09-04 7:46 ` netdev-bot+sashiko
1 sibling, 1 reply; 5+ messages in thread
From: Sagi Maimon @ 2026-09-02 4:42 UTC (permalink / raw)
To: netdev
Cc: vadim.fedorenko, richardcochran, kuba, andrew+netdev, davem,
edumazet, pabeni, linux-kernel, Sagi Maimon
The Lattice MachXO3 CPLD on the ADVA TimeCard X1 is programmed over I2C
using in-system programming (ISP). Build on the TMC bus arbitration
added previously and expose the update path through the kernel
firmware-upload subsystem.
The framework acquires the bus, erases the configuration flash, programs
the image page-by-page and activates it with the MachXO3 REFRESH
command. The upload node is registered per card as adva-cpld.N, using
the same index as the owning ocpN device, so a host with more than one
X1 board gets one node each:
/sys/class/firmware/adva-cpld.N/
The whole prepare/write/poll_complete/cleanup sequence runs under
cpld_lock and the i2c adapter lock, so an EEPROM read blocks for as long
as programming takes; the alternative is reading the TMC bus instead.
The upload is unregistered first on detach, which cancels and flushes an
in-flight programming cycle while the I2C controller is still up.
Select FW_LOADER and FW_UPLOAD, as the documented update path does not
exist without them.
Signed-off-by: Sagi Maimon <maimon.sagi@gmail.com>
---
Documentation/ABI/testing/sysfs-timecard | 4 +
drivers/ptp/Kconfig | 2 +
drivers/ptp/ptp_ocp.c | 335 ++++++++++++++++++++++-
3 files changed, 339 insertions(+), 2 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard
index b384e837c5e6..41eeadd46330 100644
--- a/Documentation/ABI/testing/sysfs-timecard
+++ b/Documentation/ABI/testing/sysfs-timecard
@@ -30,6 +30,10 @@ Description: (RO, root only) The status register of the TAP CPLD, in
ID of the CPLD is reported as the fixed "cpld.id" version by
devlink dev info.
+ To program new CPLD firmware use the standard kernel
+ firmware-upload interface, registered per card at:
+ /sys/class/firmware/adva-cpld.N/
+ where N is the index of this ocpN device.
What: /sys/class/timecard/ocpN/available_clock_sources
Date: September 2021
diff --git a/drivers/ptp/Kconfig b/drivers/ptp/Kconfig
index feb50f8cc406..24f047c37e4c 100644
--- a/drivers/ptp/Kconfig
+++ b/drivers/ptp/Kconfig
@@ -219,6 +219,8 @@ config PTP_1588_CLOCK_OCP
select NET_DEVLINK
select CRC16
select DPLL
+ select FW_LOADER
+ select FW_UPLOAD
help
This driver adds support for an OpenCompute time card.
diff --git a/drivers/ptp/ptp_ocp.c b/drivers/ptp/ptp_ocp.c
index 0c5c3ec8de5f..86d1c1a311cc 100644
--- a/drivers/ptp/ptp_ocp.c
+++ b/drivers/ptp/ptp_ocp.c
@@ -20,12 +20,14 @@
#include <linux/spi/altera.h>
#include <net/devlink.h>
#include <linux/i2c.h>
+#include <linux/iopoll.h>
#include <linux/mtd/mtd.h>
#include <linux/nvmem-consumer.h>
#include <linux/crc16.h>
#include <linux/dpll.h>
#include <linux/unaligned.h>
#include <linux/delay.h>
+#include <linux/firmware.h>
#define PCI_DEVICE_ID_META_TIMECARD 0x0400
@@ -427,6 +429,9 @@ struct ptp_ocp {
u8 *cpld_buf; /* DMA-safe scratch; valid under cpld_lock */
u32 cpld_id; /* cached Lattice device ID; 0 if unread */
bool has_cpld; /* x1 TAP CPLD present */
+ struct fw_upload *cpld_fw_upload; /* firmware upload handle; NULL if absent */
+ bool cpld_cancel; /* cancellation requested */
+ bool cpld_in_config_mode; /* EN_CFG_TP issued but not yet REFRESH'd */
};
#define OCP_REQ_TIMESTAMP BIT(0)
@@ -460,6 +465,8 @@ static int ptp_ocp_art_board_init(struct ptp_ocp *bp, struct ocp_resource *r);
static int ptp_ocp_adva_board_init(struct ptp_ocp *bp, struct ocp_resource *r);
+static const struct fw_upload_ops adva_cpld_upload_ops;
+
static const struct ocp_sma_op ocp_adva_sma_op;
static const struct ocp_sma_op ocp_adva_x1_sma_op;
static int adva_x1_cpld_device_id(struct ptp_ocp *bp, u32 *id);
@@ -3226,6 +3233,29 @@ ptp_ocp_adva_board_init(struct ptp_ocp *bp, struct ocp_resource *r)
ptp_ocp_sma_init(bp);
bp->has_cpld = info->has_cpld;
+ if (bp->has_cpld) {
+ struct fw_upload *fwl;
+ const char *name;
+
+ /* One instance per card, numbered like the ocpN device.
+ * firmware_upload_register() keeps the pointer rather than
+ * copying the string, so it has to outlive the registration.
+ */
+ name = devm_kasprintf(&bp->pdev->dev, GFP_KERNEL,
+ "adva-cpld.%d", bp->id);
+ if (!name)
+ return -ENOMEM;
+
+ fwl = firmware_upload_register(THIS_MODULE, &bp->pdev->dev,
+ name, &adva_cpld_upload_ops, bp);
+ if (IS_ERR(fwl))
+ dev_warn(&bp->pdev->dev,
+ "CPLD firmware upload unavailable: %pe\n",
+ fwl);
+ else
+ bp->cpld_fw_upload = fwl;
+ }
+
return ptp_ocp_init_clock(bp, &info->servo);
}
@@ -4287,6 +4317,15 @@ static const struct ocp_attr_group art_timecard_groups[] = {
/* Lattice LCMXO3LF ISC command codes */
#define CPLD_CMD_READ_ID 0xE0000000UL
#define CPLD_CMD_READ_STATUS 0x3C000000UL
+#define CPLD_CMD_EN_CFG_TP 0x74 /* enable config, transparent mode */
+#define CPLD_CMD_DIS_CFG 0x26
+#define CPLD_CMD_ERASE 0x0E
+#define CPLD_CMD_RESET_ADDR 0x46
+#define CPLD_CMD_WRITE_PAGE 0x70
+#define CPLD_CMD_SET_DONE 0x5E
+#define CPLD_CMD_REFRESH 0x79
+#define CPLD_PAGE_SIZE 16
+#define CPLD_POLL_US 10000 /* status poll interval while busy */
/* Status register bit positions (Lattice LCMXO3LF datasheet) */
#define CPLD_STATUS_DONE BIT(8)
@@ -4301,7 +4340,8 @@ static const struct ocp_attr_group art_timecard_groups[] = {
*
* The message is assembled in the scratch buffer taken by
* adva_x1_bus_claim(), which the Xilinx controller needs for DMA safety:
- * an opcode and its arguments are copied exactly once.
+ * an opcode and its arguments are copied exactly once, and a firmware
+ * upload costs one allocation rather than one per page.
*
* Caller must hold that claim, hence __i2c_transfer() over i2c_transfer().
*/
@@ -4432,7 +4472,10 @@ static int adva_x1_bus_claim(struct ptp_ocp *bp)
if (!adap)
return -ENODEV;
- /* One scratch buffer per claim, not per transfer. */
+ /* One scratch buffer per claim rather than per transfer: a firmware
+ * upload holds the claim for the whole image, so this is a single
+ * allocation instead of one for each 16-byte page.
+ */
bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL);
if (!bp->cpld_buf) {
i2c_put_adapter(adap);
@@ -4460,6 +4503,47 @@ static int adva_x1_mux_select(struct ptp_ocp *bp, int ch)
return adva_x1_i2c_xfer(bp, ADVA_MUX_ADDR, val, NULL, 0, NULL, 0);
}
+/*
+ * Argument bytes that follow an ISC opcode. Returns NULL with @nargs set
+ * when the arguments are all zero: adva_x1_i2c_xfer() zeroes the buffer.
+ */
+static const u8 *adva_x1_cpld_args(u8 cmd, u8 *nargs)
+{
+ static const u8 en_cfg_tp[] = { 0x08, 0x00 };
+ static const u8 erase_cfg[] = { 0x04, 0x00, 0x00 }; /* cfg sector only */
+
+ switch (cmd) {
+ case CPLD_CMD_EN_CFG_TP:
+ *nargs = sizeof(en_cfg_tp);
+ return en_cfg_tp;
+ case CPLD_CMD_ERASE:
+ *nargs = sizeof(erase_cfg);
+ return erase_cfg;
+ case CPLD_CMD_RESET_ADDR:
+ case CPLD_CMD_SET_DONE:
+ *nargs = 3;
+ return NULL;
+ case CPLD_CMD_DIS_CFG:
+ case CPLD_CMD_REFRESH:
+ *nargs = 2;
+ return NULL;
+ default:
+ *nargs = 0;
+ return NULL;
+ }
+}
+
+/* Send an ISC command with the fixed arguments that belong to it. */
+static int adva_x1_cpld_write(struct ptp_ocp *bp, u8 cmd)
+{
+ const u8 *args;
+ u8 nargs;
+
+ args = adva_x1_cpld_args(cmd, &nargs);
+
+ return adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, cmd, args, nargs, NULL, 0);
+}
+
/*
* Send a 4-byte command then read data back without an intermediate STOP
* (Lattice combined write->repeated-START->read). Two messages in one
@@ -4485,6 +4569,38 @@ static int adva_x1_cpld_read_status(struct ptp_ocp *bp, u32 *status)
return 0;
}
+/* Poll the status register until the CPLD goes idle, or @max_ms elapses.
+ * The deadline is on wall time, so the I2C transactions count against it,
+ * and the status is read once more after it expires before giving up.
+ */
+static int adva_x1_cpld_wait_ready(struct ptp_ocp *bp, unsigned int max_ms)
+{
+ u32 status = 0;
+ int err, ret;
+
+ ret = read_poll_timeout(adva_x1_cpld_read_status, err,
+ err || READ_ONCE(bp->cpld_cancel) ||
+ (status & CPLD_STATUS_FAILED) ||
+ !(status & CPLD_STATUS_BUSY),
+ CPLD_POLL_US, max_ms * USEC_PER_MSEC, false,
+ bp, &status);
+ if (ret)
+ return ret;
+ if (READ_ONCE(bp->cpld_cancel))
+ return -ECANCELED;
+ if (err || (status & CPLD_STATUS_FAILED))
+ return -EIO;
+
+ return 0;
+}
+
+/* A step aborted by cancel() must be reported as such, not as a HW error. */
+static enum fw_upload_err adva_cpld_err(struct ptp_ocp *bp)
+{
+ return READ_ONCE(bp->cpld_cancel) ? FW_UPLOAD_ERR_CANCELED
+ : FW_UPLOAD_ERR_HW_ERROR;
+}
+
/*
* Read the Lattice device ID of the TAP CPLD. It is a fixed property of
* the part, so cache it and pay the bus arbitration only once. The
@@ -4562,6 +4678,212 @@ cpld_status_show(struct device *dev, struct device_attribute *attr,
}
static DEVICE_ATTR_ADMIN_RO(cpld_status);
+/*
+ * adva_x1 CPLD firmware-upload callbacks.
+ *
+ * The kernel firmware-upload subsystem (CONFIG_FW_UPLOAD) exposes:
+ * /sys/class/firmware/adva-cpld.N/{data,loading,status,error,...}
+ * where N is the index of the owning ocpN device.
+ * Userspace writes the raw binary page data directly — no /lib/firmware/
+ * staging file is needed.
+ *
+ * Callback sequence driven by the framework:
+ * prepare() - validate size, acquire bus, enable config, erase flash
+ * write() - program one 16-byte page per call
+ * poll_complete()- set DONE, REFRESH, wait for CPLD to reboot
+ * cancel() - set flag; checked at the start of each callback
+ * cleanup() - release bus resources (called on success or failure)
+ */
+static enum fw_upload_err
+adva_cpld_prepare(struct fw_upload *fwl, const u8 *data, u32 size)
+{
+ enum fw_upload_err ret = FW_UPLOAD_ERR_NONE;
+ struct ptp_ocp *bp = fwl->dd_handle;
+
+ /* Do not clear cpld_cancel here: fw_upload_start() queues the work
+ * before this runs, so a cancel may already have arrived. It is
+ * cleared once the upload is over, on every exit below and in
+ * cleanup().
+ */
+ if (!size || size % CPLD_PAGE_SIZE) {
+ WRITE_ONCE(bp->cpld_cancel, false);
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+ }
+
+ bp->cpld_in_config_mode = false;
+
+ mutex_lock(&bp->cpld_lock);
+
+ if (adva_x1_bus_claim(bp)) {
+ ret = FW_UPLOAD_ERR_TIMEOUT;
+ goto err_unlock;
+ }
+
+ if (adva_x1_mux_select(bp, ADVA_MUX_CHANNEL)) {
+ ret = FW_UPLOAD_ERR_HW_ERROR;
+ goto err_release;
+ }
+
+ /* Set before issuing EN_CFG_TP, not after it completes: the CPLD may
+ * have entered configuration mode even if the write reports an error
+ * or the wait below times out, and err_deselect only sends DIS_CFG
+ * when this is set. A DIS_CFG to a device that never entered the
+ * mode is harmless; leaving it enabled is not.
+ */
+ bp->cpld_in_config_mode = true;
+
+ if (adva_x1_cpld_write(bp, CPLD_CMD_EN_CFG_TP) ||
+ adva_x1_cpld_wait_ready(bp, 5000)) {
+ ret = adva_cpld_err(bp);
+ goto err_deselect;
+ }
+
+ if (READ_ONCE(bp->cpld_cancel)) {
+ ret = FW_UPLOAD_ERR_CANCELED;
+ goto err_deselect;
+ }
+
+ if (adva_x1_cpld_write(bp, CPLD_CMD_ERASE) ||
+ adva_x1_cpld_wait_ready(bp, 15000)) {
+ ret = adva_cpld_err(bp);
+ goto err_deselect;
+ }
+
+ if (READ_ONCE(bp->cpld_cancel)) {
+ ret = FW_UPLOAD_ERR_CANCELED;
+ goto err_deselect;
+ }
+
+ if (adva_x1_cpld_write(bp, CPLD_CMD_RESET_ADDR)) {
+ ret = FW_UPLOAD_ERR_HW_ERROR;
+ goto err_deselect;
+ }
+
+ /* cleanup() unlocks everything. fw_upload_main() only pairs it with
+ * a prepare() that succeeded, so the error paths below unlock here
+ * instead; hand the context to cleanup() for sparse's benefit.
+ */
+ __release(&bp->cpld_lock);
+ return FW_UPLOAD_ERR_NONE;
+
+err_deselect:
+ if (bp->cpld_in_config_mode) {
+ adva_x1_cpld_write(bp, CPLD_CMD_DIS_CFG);
+ bp->cpld_in_config_mode = false;
+ }
+ adva_x1_mux_select(bp, -1);
+err_release:
+ adva_x1_bus_release(bp);
+err_unlock:
+ WRITE_ONCE(bp->cpld_cancel, false);
+ mutex_unlock(&bp->cpld_lock);
+ return ret;
+}
+
+static enum fw_upload_err
+adva_cpld_write(struct fw_upload *fwl, const u8 *data,
+ u32 offset, u32 size, u32 *written)
+{
+ struct ptp_ocp *bp = fwl->dd_handle;
+ u8 args[3 + CPLD_PAGE_SIZE] = { 0x00, 0x00, 0x01 };
+
+ lockdep_assert_held(&bp->cpld_lock);
+
+ if (READ_ONCE(bp->cpld_cancel))
+ return FW_UPLOAD_ERR_CANCELED;
+
+ if (size < CPLD_PAGE_SIZE)
+ return FW_UPLOAD_ERR_INVALID_SIZE;
+
+ memcpy(&args[3], data + offset, CPLD_PAGE_SIZE);
+
+ if (adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, CPLD_CMD_WRITE_PAGE,
+ args, sizeof(args), NULL, 0) ||
+ adva_x1_cpld_wait_ready(bp, 100))
+ return adva_cpld_err(bp);
+
+ *written = CPLD_PAGE_SIZE;
+ return FW_UPLOAD_ERR_NONE;
+}
+
+static enum fw_upload_err
+adva_cpld_poll_complete(struct fw_upload *fwl)
+{
+ struct ptp_ocp *bp = fwl->dd_handle;
+ int err;
+ u32 st;
+
+ lockdep_assert_held(&bp->cpld_lock);
+
+ if (READ_ONCE(bp->cpld_cancel))
+ return FW_UPLOAD_ERR_CANCELED;
+
+ if (adva_x1_cpld_write(bp, CPLD_CMD_SET_DONE) ||
+ adva_x1_cpld_wait_ready(bp, 1000))
+ return adva_cpld_err(bp);
+
+ if (adva_x1_cpld_read_status(bp, &st) || !(st & CPLD_STATUS_DONE))
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ if (adva_x1_cpld_write(bp, CPLD_CMD_REFRESH))
+ return FW_UPLOAD_ERR_HW_ERROR;
+
+ /* REFRESH reboots the CPLD out of configuration mode, so cleanup()
+ * must not send DIS_CFG afterwards even if the checks below fail.
+ */
+ bp->cpld_in_config_mode = false;
+
+ /* The new image is already running at this point, so a segment that
+ * is not back yet must not be reported as a failed update: retry the
+ * reselect instead of sampling the mux once at a fixed delay.
+ */
+ msleep(1500);
+ if (read_poll_timeout(adva_x1_mux_select, err, !err, CPLD_POLL_US,
+ 3000 * USEC_PER_MSEC, false,
+ bp, ADVA_MUX_CHANNEL))
+ return FW_UPLOAD_ERR_TIMEOUT;
+
+ if (adva_x1_cpld_wait_ready(bp, 3000))
+ return READ_ONCE(bp->cpld_cancel) ? FW_UPLOAD_ERR_CANCELED
+ : FW_UPLOAD_ERR_TIMEOUT;
+
+ return FW_UPLOAD_ERR_NONE;
+}
+
+static void
+adva_cpld_cancel(struct fw_upload *fwl)
+{
+ struct ptp_ocp *bp = fwl->dd_handle;
+
+ WRITE_ONCE(bp->cpld_cancel, true);
+}
+
+static void
+adva_cpld_cleanup(struct fw_upload *fwl)
+{
+ struct ptp_ocp *bp = fwl->dd_handle;
+
+ __acquire(&bp->cpld_lock); /* held since prepare() returned ok */
+ lockdep_assert_held(&bp->cpld_lock);
+
+ if (bp->cpld_in_config_mode) {
+ adva_x1_cpld_write(bp, CPLD_CMD_DIS_CFG);
+ bp->cpld_in_config_mode = false;
+ }
+ adva_x1_mux_select(bp, -1);
+ adva_x1_bus_release(bp);
+ WRITE_ONCE(bp->cpld_cancel, false);
+ mutex_unlock(&bp->cpld_lock);
+}
+
+static const struct fw_upload_ops adva_cpld_upload_ops = {
+ .prepare = adva_cpld_prepare,
+ .write = adva_cpld_write,
+ .poll_complete = adva_cpld_poll_complete,
+ .cancel = adva_cpld_cancel,
+ .cleanup = adva_cpld_cleanup,
+};
+
static struct attribute *adva_timecard_attrs[] = {
&dev_attr_serialnum.attr,
&dev_attr_gnss_sync.attr,
@@ -5204,6 +5526,15 @@ ptp_ocp_detach(struct ptp_ocp *bp)
{
int i;
+ /* Must come first: cancels and flushes an in-flight upload while the
+ * I2C controller is still up, and drops cpld_lock so a cpld_status
+ * reader cannot stall ptp_ocp_attr_group_del() below.
+ */
+ if (bp->cpld_fw_upload) {
+ firmware_upload_unregister(bp->cpld_fw_upload);
+ bp->cpld_fw_upload = NULL;
+ }
+
ptp_ocp_debugfs_remove_device(bp);
ptp_ocp_detach_sysfs(bp);
ptp_ocp_attr_group_del(bp);
--
2.47.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access for ADVA TimeCard X1
2026-09-02 4:42 ` [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon
@ 2026-09-04 7:46 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-09-04 7:46 UTC (permalink / raw)
To: maimon.sagi
Cc: netdev, vadim.fedorenko, richardcochran, kuba, andrew+netdev,
davem, edumazet, pabeni, linux-kernel
Thank you for your contribution! Sashiko AI review found 9 potential
issue(s) to consider:
Critical: 0 · High: 2 · Medium: 5 · Low: 2
- [High] The CPLD bus-arbitration/mux-reprogramming operation that the
patch deliberately restricts to root via…
- [High] `ptp_ocp_devlink_info_get()` propagates any failure of the new
`adva_x1_cpld_device_id()` read out of the `->info_get()` callback…
- [Medium] `adva_x1_i2c_xfer()` marks both messages `I2C_M_DMA_SAFE`
(`.flags = I2C_M_DMA_SAFE` for the write and `I2C_M_RD |…
- [Medium] The new `cpld.id` reporting block is appended at the end of
`ptp_ocp_devlink_info_get()`, i.e. after the pre-existing `if…
- [Medium] `adva_x1_bus_claim()` resolves the I2C adapter from a cached
adapter *number* (`i2c_get_adapter(READ_ONCE(bp->cpld_i2c_adap_nr))`)…
- [Medium] `ptp_ocp_detach()` now calls `mutex_destroy(&bp->cpld_lock)`,
but on the probe error paths detach runs while the devlink instance is…
- [Medium] Acquiring the shared I2C segment is a polled firmware
handshake (`iowrite32(MBLAZE_REQUEST)` then up to 200 x 10 ms polling…
- [Low] Several new comments in this patch document a CPLD firmware-
upload path that does not exist in the tree at this commit: the struct…
- [Low] The patch introduces the driver-specific devlink version name
`cpld.id` without the driver-specific devlink documentation that…
--
ptp: ocp: add TAP CPLD access for ADVA TimeCard X1
This adds a MicroBlaze request/grant handshake that re-routes the host I2C
controller of the ADVA TimeCard X1 from the config EEPROMs onto the TMC bus,
plus read-only accessors for the Lattice CPLD behind the PCA9548 there. The
CPLD device ID is exported as the fixed devlink version cpld.id and the status
register as a new root-only cpld_status sysfs attribute.
A few questions below.
> diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard
> index 3ae41b7634ac9..b384e837c5e64 100644
> --- a/Documentation/ABI/testing/sysfs-timecard
> +++ b/Documentation/ABI/testing/sysfs-timecard
> @@ -11,6 +11,26 @@ Contact: Jonathan Lemon <jonathan.lemon@gmail.com>
> Description: This directory contains the attributes of the Nth timecard
> registered.
>
> +What: /sys/class/timecard/ocpN/cpld_status
> +Date: July 2026
> +Contact: Sagi Maimon <maimon.sagi@gmail.com>
> +Description: (RO, root only) The status register of the TAP CPLD, in
> + human-readable form:
> +
> + done=<0|1> busy=<0|1> failed=<0|1>
> +
> + Only present on ADVA x1 TAP boards (PCI ID 0xad5a:0x0410).
> +
> + done=1 indicates the configuration flash was successfully
> + programmed and is active. busy=1 means an internal operation
> + is in progress. failed=1 means the last ISC operation failed.
> +
> + A read arbitrates for the shared I2C bus and reprograms the
> + on-card mux, so it is restricted to root. The Lattice device
> + ID of the CPLD is reported as the fixed "cpld.id" version by
> + devlink dev info.
> +
> +
> What: /sys/class/timecard/ocpN/available_clock_sources
> Date: September 2021
> Contact: Jonathan Lemon <jonathan.lemon@gmail.com>
> diff --git a/drivers/ptp/ptp_ocp.c b/drivers/ptp/ptp_ocp.c
> index 3d26ec1f7b9ec..0c5c3ec8de5fa 100644
> --- a/drivers/ptp/ptp_ocp.c
> +++ b/drivers/ptp/ptp_ocp.c
[ ... ]
> @@ -85,6 +87,7 @@ struct ptp_ocp_adva_info {
> u8 signals_nr;
> u8 freq_in_nr;
> const struct ocp_attr_group *attr_groups;
> + bool has_cpld; /* x1: supports CPLD firmware upload */
> };
[Severity: Low]
This isn't a bug, but the comment describes a firmware upload path that does
not exist yet at this commit. Here has_cpld only gates the devlink cpld.id
version and the cpld_status attribute.
Two other new comments have the same forward reference. In
adva_x1_bus_claim():
* EEPROMs. A firmware upload holds it across the whole prepare/write/poll
* sequence, so an EEPROM read blocks for as long as programming takes.
and in ptp_ocp_i2c_notifier_call():
/* Cache adapter nr; used by the CPLD status/id/upload paths
The follow-up patch in the series that adds firmware_upload_register() and
the prepare/write/poll_complete ops makes all three accurate, so this is only
about patch 1 reading on its own. At this commit the longest hold of the
adapter lock is the ~2s MBLAZE_RETRIES timeout, not a programming cycle.
[ ... ]
> @@ -416,6 +420,13 @@ struct ptp_ocp {
> dpll_tracker tracker;
> int signals_nr;
> int freq_in_nr;
> + /* adva_x1 CPLD I2C (internal use only) */
> + struct mutex cpld_lock; /* serialises CPLD operations */
> + int cpld_i2c_adap_nr; /* I2C adapter nr; -1 if absent */
> + struct i2c_adapter *cpld_adap; /* claimed adapter; valid under cpld_lock */
> + u8 *cpld_buf; /* DMA-safe scratch; valid under cpld_lock */
> + u32 cpld_id; /* cached Lattice device ID; 0 if unread */
> + bool has_cpld; /* x1 TAP CPLD present */
> };
[ ... ]
> @@ -2185,6 +2198,19 @@ ptp_ocp_devlink_info_get(struct devlink *devlink, struct devlink_info_req *req,
> if (err)
> return err;
>
> + if (bp->has_cpld) {
> + u32 id;
> +
> + err = adva_x1_cpld_device_id(bp, &id);
[Severity: High]
The same hardware operation is root-only through sysfs:
static DEVICE_ATTR_ADMIN_RO(cpld_status);
and the new ABI text says "A read arbitrates for the shared I2C bus and
reprograms the on-card mux, so it is restricted to root."
Is that restriction bypassed here? DEVLINK_CMD_INFO_GET is registered in
net/devlink/netlink_gen.c with only .flags = GENL_CMD_CAP_DO and
GENL_CMD_CAP_DUMP, without GENL_ADMIN_PERM, so any user can walk:
devlink dev info
devlink_nl_info_get_doit()
ptp_ocp_devlink_info_get()
adva_x1_cpld_device_id()
adva_x1_bus_claim()
i2c_lock_bus(adap, I2C_LOCK_ROOT_ADAPTER)
adva_x1_mblaze_acquire()
adva_x1_mblaze_acquire() then sleeps in a 200 x usleep_range(10000, 11000)
loop while the adapter lock is held and the controller is routed away from the
EEPROMs.
Since bp->cpld_id is assigned only on a successful read, a card where the
handshake does not complete repeats the whole ~2s arbitration on every call:
if (bp->cpld_id) {
*id = bp->cpld_id;
return 0;
}
Can an unprivileged loop over devlink dev info keep the EEPROM, nvmem and at24
paths off the controller indefinitely this way, and should the devlink path
either use a value cached at probe time or be gated the way cpld_status is?
> + if (err)
> + return err;
> +
[Severity: High]
Should a failure of this optional version string abort the entire info reply?
devlink discards the message when ->info_get() returns non-zero, so the fw
version, serial number and board id that were already put above are lost.
For the dump form, devlink_nl_info_get_dump_one() tolerates exactly one errno:
if (err == -EOPNOTSUPP)
err = 0;
return err;
so -ENODEV, -ENOMEM, -ETIMEDOUT or -ERESTARTSYS coming out of the new path
aborts devlink dev info for every devlink instance on the host, not just this
card.
-ENODEV is reachable in an ordinary configuration rather than a race:
bp->cpld_i2c_adap_nr keeps the -1 sentinel written in ptp_ocp_probe() whenever
no i2c adapter device ever appears (xiic-i2c not built or not bound, or the
i2c resource skipped), and adva_x1_bus_claim() starts with
adap = i2c_get_adapter(READ_ONCE(bp->cpld_i2c_adap_nr));
if (!adap)
return -ENODEV;
Would skipping the cpld.id put on error be better here, matching how the
function already degrades for missing EEPROM data?
> + sprintf(buf, "0x%08x", id);
> + err = devlink_info_version_fixed_put(req, "cpld.id", buf);
> + if (err)
> + return err;
> + }
> +
> return 0;
> }
[Severity: Medium]
This block is appended after the pre-existing early return earlier in the same
function:
if (!bp->has_eeprom_data) {
ptp_ocp_read_eeprom(bp);
if (!bp->has_eeprom_data)
return 0;
}
The CPLD id is read over the TMC I2C bus and has no dependency on bp->serial
or bp->board_id, yet ptp_ocp_read_eeprom() takes its fail: path when
ptp_ocp_nvmem_device_get() returns an error pointer (at24/nvmem provider not
bound) or nvmem_device_read() returns a short count, leaving has_eeprom_data
false.
On such a card devlink dev info succeeds but never reports cpld.id, while the
new ABI entry states:
The Lattice device
ID of the CPLD is reported as the fixed "cpld.id" version by
devlink dev info.
Should the cpld.id put happen before the EEPROM early return?
[Severity: Low]
cpld.id is a driver-specific devlink version name.
Documentation/networking/devlink/devlink-info.rst says that driver-specific
names are a last resort and "should be documented in the driver-specific
file", but there is no ptp_ocp file under Documentation/networking/devlink/
and no entry for one in its index. The only description of cpld.id is the
prose inside the sysfs-timecard entry for cpld_status. Could a devlink
driver documentation file be added for it?
[ ... ]
> @@ -4226,6 +4253,315 @@ static const struct ocp_attr_group art_timecard_groups[] = {
> { },
> };
>
> +/*
> + * Internal helpers for the adva_x1 TAP CPLD (Lattice LCMXO3LF-2100).
[ ... ]
> +/*
> + * adva_x1_i2c_xfer() - issue a single I2C transaction on the TMC bus.
> + *
> + * Writes @cmd when it is not negative, followed by @wlen bytes of @wdata,
> + * then reads @rlen bytes if asked. A NULL @wdata sends zeros.
> + *
> + * The message is assembled in the scratch buffer taken by
> + * adva_x1_bus_claim(), which the Xilinx controller needs for DMA safety:
> + * an opcode and its arguments are copied exactly once.
> + *
> + * Caller must hold that claim, hence __i2c_transfer() over i2c_transfer().
> + */
> +static int adva_x1_i2c_xfer(struct ptp_ocp *bp, u8 addr, int cmd,
> + const void *wdata, u8 wlen,
> + void *rdata, u8 rlen)
> +{
[ ... ]
> + wbuf = bp->cpld_buf;
> + rbuf = bp->cpld_buf + ADVA_CPLD_XFER_MAX;
> +
> + if (hdr + wlen) {
> + if (hdr)
> + wbuf[0] = cmd;
> + if (wdata)
> + memcpy(wbuf + hdr, wdata, wlen);
> + else
> + memset(wbuf + hdr, 0, wlen);
> + msgs[nmsgs++] = (struct i2c_msg){
> + .addr = addr,
> + .flags = I2C_M_DMA_SAFE,
> + .len = hdr + wlen,
> + .buf = wbuf,
> + };
> + }
> + if (rlen) {
> + msgs[nmsgs++] = (struct i2c_msg){
> + .addr = addr,
> + .flags = I2C_M_RD | I2C_M_DMA_SAFE,
> + .len = rlen,
> + .buf = rbuf,
> + };
> + }
[Severity: Medium]
Do these two buffers really satisfy the I2C_M_DMA_SAFE contract? Both are
halves of a single 64-byte allocation made in adva_x1_bus_claim():
bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL);
with rbuf = bp->cpld_buf + 32. Setting I2C_M_DMA_SAFE tells the core the
buffer can be handed straight to the adapter's DMA mapping:
drivers/i2c/i2c-core-base.c:i2c_get_dma_safe_msg_buf() {
if (msg->flags & I2C_M_DMA_SAFE)
return msg->buf;
...
}
so no bounce buffer is allocated and the interior pointer is used as is.
kmalloc only guarantees alignment for the start of the allocation, so
cpld_buf + 32 is merely 32-byte aligned and shares a cacheline with the write
half on any architecture with a 64-byte or larger cacheline. That puts the
DMA_TO_DEVICE and DMA_FROM_DEVICE buffers of the same transfer in one line.
The justification in the comment above also does not seem to hold:
drivers/i2c/busses/i2c-xiic.c has no dma_map_single(), no
i2c_get_dma_safe_msg_buf() and no dmaengine use at all, so nothing behind this
card needs the flag. Would dropping I2C_M_DMA_SAFE (letting the core bounce
if a future adapter does DMA), or using two separate allocations, be the
better option?
[ ... ]
> +static void adva_x1_mblaze_release(struct ptp_ocp *bp)
> +{
> + if (bp->pps_select)
> + iowrite32(MBLAZE_RELEASE, &bp->pps_select->i2c_bus_ctrl);
> +}
[Severity: Medium]
Acquiring the segment is a polled handshake, but releasing it is a single
fire-and-forget write with no wait for the MicroBlaze to route controller
0x00150000 back to the EEPROM segment. adva_x1_bus_release() unlocks the
adapter immediately after:
adva_x1_mblaze_release(bp);
bp->cpld_adap = NULL;
kfree(bp->cpld_buf);
bp->cpld_buf = NULL;
i2c_unlock_bus(adap, I2C_LOCK_ROOT_ADAPTER);
Can the first EEPROM/nvmem/at24 transfer that was queued behind the CPLD
operation be issued while routing is still on the TMC bus? That is the window
the commit message says the adapter lock closes:
an operation
takes the i2c core adapter lock for the whole grant window and uses
__i2c_transfer() internally; without that, a concurrent transfer from
ptp_ocp_read_eeprom(), from the nvmem attributes or from the at24 sysfs
files would be issued onto the TMC bus instead of to the EEPROM.
Depending on hardware latency the result would be either a NAK surfacing as
"could not read eeprom", or a reply from an unrelated TMC device that gets
cached as serial number / board id.
There is a related question about the register values. MBLAZE_RELEASE
(0x55550000) leaves the 0x5555 half that MBLAZE_GRANTED (0x5555aaaa) encodes
as the grant, and the next acquire writes 0 over that firmware-owned field
and discards the read-back:
iowrite32(0, &bp->pps_select->i2c_bus_ctrl);
ioread32(&bp->pps_select->i2c_bus_ctrl);
If that half is not host-writable, can the exact-match test val ==
MBLAZE_GRANTED be satisfied on the first poll by a stale grant? Would polling
with a bounded timeout until the grant pattern is gone, before unlocking the
adapter, be more robust?
> +
> +/* Acquire the shared I2C bus from the MicroBlaze firmware. Returns with no
> + * request outstanding on failure, so the firmware is never left granting a
> + * segment to a host that has given up waiting for it.
> + */
> +static int adva_x1_mblaze_acquire(struct ptp_ocp *bp)
> +{
[ ... ]
> + iowrite32(MBLAZE_REQUEST, &bp->pps_select->i2c_bus_ctrl);
> + for (i = 0; i < MBLAZE_RETRIES; i++) {
> + usleep_range(MBLAZE_RETRY_US, MBLAZE_RETRY_US + 1000);
> + val = ioread32(&bp->pps_select->i2c_bus_ctrl);
> + if (val == MBLAZE_GRANTED)
> + return 0;
> + }
> +
> + adva_x1_mblaze_release(bp);
> + return -ETIMEDOUT;
> +}
[ ... ]
> +static int adva_x1_bus_claim(struct ptp_ocp *bp)
> +{
> + struct i2c_adapter *adap;
> + int ret;
> +
> + lockdep_assert_held(&bp->cpld_lock);
> +
> + adap = i2c_get_adapter(READ_ONCE(bp->cpld_i2c_adap_nr));
> + if (!adap)
> + return -ENODEV;
[Severity: Medium]
Is the adapter identity guaranteed here? The number is snapshotted first and
the reference is taken afterwards inside i2c_get_adapter(), which blocks on
the i2c core_lock that i2c_del_adapter() and i2c_add_adapter() hold while
releasing and allocating adapter numbers. i2c_del_adapter() frees the idr
slot before device_del() fires the notifier that resets the cached number:
drivers/i2c/i2c-core-base.c:i2c_del_adapter() {
found = idr_find(&i2c_adapter_idr, adap->nr);
if (found == adap)
idr_replace(&i2c_adapter_idr, NULL, adap->nr);
...
}
and only later does the driver see:
WRITE_ONCE(bp->cpld_i2c_adap_nr, -1); /* invalidate before free */
So if the card's xiic-i2c adapter is unbound after the snapshot and the number
is recycled by an unrelated adapter, can this claim return that foreign
adapter? adva_x1_mux_select() and adva_x1_cpld_cmd_read() would then write a
PCA9548 channel select to 0x74 and Lattice ISC opcodes to 0x40 on somebody
else's bus while holding its root adapter lock.
Nothing re-validates ownership (for example adap->dev.parent ==
&bp->i2c_ctrl->dev), so the notifier comment describing this as
"reference-counted unbind-safe adapter access" does not seem to match. Would
storing a refcounted adapter pointer from the notifier, or checking the
parent after i2c_get_adapter(), close this?
> +
> + /* One scratch buffer per claim, not per transfer. */
> + bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL);
> + if (!bp->cpld_buf) {
> + i2c_put_adapter(adap);
> + return -ENOMEM;
> + }
> +
> + i2c_lock_bus(adap, I2C_LOCK_ROOT_ADAPTER);
> + bp->cpld_adap = adap;
> +
> + ret = adva_x1_mblaze_acquire(bp);
> + if (ret)
> + adva_x1_bus_release(bp);
> +
> + return ret;
> +}
[ ... ]
> +static DEVICE_ATTR_ADMIN_RO(cpld_status);
> +
> static struct attribute *adva_timecard_attrs[] = {
[ ... ]
> @@ -4904,6 +5241,7 @@ ptp_ocp_detach(struct ptp_ocp *bp)
> clk_hw_unregister_fixed_rate(bp->i2c_clk);
> if (bp->n_irqs)
> pci_free_irq_vectors(bp->pdev);
> + mutex_destroy(&bp->cpld_lock);
> device_unregister(&bp->dev);
> }
[Severity: Medium]
Is cpld_lock still reachable when it is destroyed here? On the probe error
paths ptp_ocp_detach() runs while the devlink instance is still registered:
ptp_ocp_probe() calls devlink_register(devlink) and the later failures of
dpll_device_get(), dpll_device_register() and dpll_pin_register() fall through
to
out:
ptp_ocp_detach(bp);
out_disable:
pci_disable_device(pdev);
out_free:
devlink_free(devlink);
with no devlink_unregister() in between. The remove path does the right
ordering, calling devlink_unregister() before ptp_ocp_detach().
Since this patch makes ptp_ocp_devlink_info_get() take cpld_lock and then
touch bp->pps_select MMIO and claim the i2c adapter, and
DEVLINK_CMD_INFO_GET needs no privilege, can a concurrent devlink dev info in
that window lock a mutex that mutex_destroy() has already reset, and drive a
device whose i2c controller and IRQ vectors detach has released?
The comment added at the mutex_init() site reasons only about destroying an
initialised mutex:
/* Must be before the first error path that calls ptp_ocp_detach(),
* so mutex_destroy() always runs on an initialised mutex.
Would unregistering devlink on the probe error path before detach, or moving
mutex_destroy() after all users are drained, be the fix?
> @@ -5219,11 +5568,16 @@ ptp_ocp_i2c_notifier_call(struct notifier_block *nb,
>
> found:
> bp = dev_get_drvdata(dev);
> - if (add)
> + if (add) {
> ptp_ocp_symlink(bp, child, "i2c");
> - else
> + /* Cache adapter nr; used by the CPLD status/id/upload paths
> + * for reference-counted unbind-safe adapter access.
> + */
> + WRITE_ONCE(bp->cpld_i2c_adap_nr, i2c_verify_adapter(child)->nr);
> + } else {
> + WRITE_ONCE(bp->cpld_i2c_adap_nr, -1); /* invalidate before free */
> sysfs_remove_link(&bp->dev.kobj, "i2c");
> -
> + }
> return 0;
> }
Thanks.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260902044232.21188-1-maimon.sagi%40gmail.com
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1
2026-09-02 4:42 ` [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon
@ 2026-09-04 7:46 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-09-04 7:46 UTC (permalink / raw)
To: maimon.sagi
Cc: netdev, vadim.fedorenko, richardcochran, kuba, andrew+netdev,
davem, edumazet, pabeni, linux-kernel
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 3 · Low: 1 (1)
- [Medium] The upload callbacks emit firmware-upload error categories
that contradict the meanings documented for enum fw_upload_err in…
- [Medium] bp->cpld_cancel is cleared by the driver in
adva_cpld_cleanup() (ptp_ocp.c:4875) and on adva_cpld_prepare()'s…
- [Medium] Programming success is declared without validating the full
MachXO3 sysCONFIG status: the driver defines only DONE/BUSY/FAILED…
- [Low] The new callback block comment states 'cancel() - set flag;
checked at the start of each callback', but adva_cpld_prepare() does…
Pre-existing issues:
- [Low] adva_x1_bus_claim() makes a single 2 * ADVA_CPLD_XFER_MAX
(64-byte) kzalloc and adva_x1_i2c_xfer() splits it into wbuf at offset…
--
ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1
This adds an in-system programming path for the Lattice MachXO3 TAP CPLD on
the ADVA TimeCard X1, driven through the kernel firmware-upload subsystem
and registered per card as adva-cpld.N. The prepare/write/poll_complete/
cleanup sequence runs under cpld_lock while holding the i2c adapter lock.
A few questions about the new upload callbacks below.
> diff --git a/drivers/ptp/ptp_ocp.c b/drivers/ptp/ptp_ocp.c
> index 0c5c3ec8de5fa..86d1c1a311cc5 100644
> --- a/drivers/ptp/ptp_ocp.c
> +++ b/drivers/ptp/ptp_ocp.c
[ ... ]
> @@ -4287,6 +4317,15 @@ static const struct ocp_attr_group art_timecard_groups[] = {
> /* Lattice LCMXO3LF ISC command codes */
> #define CPLD_CMD_READ_ID 0xE0000000UL
> #define CPLD_CMD_READ_STATUS 0x3C000000UL
> +#define CPLD_CMD_EN_CFG_TP 0x74 /* enable config, transparent mode */
> +#define CPLD_CMD_DIS_CFG 0x26
> +#define CPLD_CMD_ERASE 0x0E
> +#define CPLD_CMD_RESET_ADDR 0x46
> +#define CPLD_CMD_WRITE_PAGE 0x70
> +#define CPLD_CMD_SET_DONE 0x5E
> +#define CPLD_CMD_REFRESH 0x79
> +#define CPLD_PAGE_SIZE 16
> +#define CPLD_POLL_US 10000 /* status poll interval while busy */
>
> /* Status register bit positions (Lattice LCMXO3LF datasheet) */
> #define CPLD_STATUS_DONE BIT(8)
[ ... ]
> @@ -4432,7 +4472,10 @@ static int adva_x1_bus_claim(struct ptp_ocp *bp)
> if (!adap)
> return -ENODEV;
>
> - /* One scratch buffer per claim, not per transfer. */
> + /* One scratch buffer per claim rather than per transfer: a firmware
> + * upload holds the claim for the whole image, so this is a single
> + * allocation instead of one for each 16-byte page.
> + */
> bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL);
> if (!bp->cpld_buf) {
> i2c_put_adapter(adap);
[Severity: Low]
This isn't a bug introduced by this patch, but this patch makes the single
scratch allocation part of the documented rationale, so it seems worth
raising. adva_x1_i2c_xfer() splits this one 64-byte allocation into two
halves:
wbuf = bp->cpld_buf;
rbuf = bp->cpld_buf + ADVA_CPLD_XFER_MAX;
and then marks both messages I2C_M_DMA_SAFE in the same __i2c_transfer().
Is the second half really DMA-safe in the sense that flag asserts? With a
64-byte cacheline both halves share one line, so for the combined
write -> repeated-START -> read used by adva_x1_cpld_cmd_read() the
TO_DEVICE mapping of wbuf and the FROM_DEVICE mapping of rbuf would cover
the same line on a non-coherent master, and a writeback of the dirty
transmit half could clobber the status bytes the programming state machine
polls.
No adapter that can bind here honours the flag today (the xiic-i2c
controller instantiated by ocp_adva_x1_resource does not use DMA), so this
looks inert on this hardware. Would separate allocations, or aligning each
half to ARCH_DMA_MINALIGN, be preferable given the flag's stated contract?
> @@ -4485,6 +4569,38 @@ static int adva_x1_cpld_read_status(struct ptp_ocp *bp, u32 *status)
> return 0;
> }
>
> +/* Poll the status register until the CPLD goes idle, or @max_ms elapses.
> + * The deadline is on wall time, so the I2C transactions count against it,
> + * and the status is read once more after it expires before giving up.
> + */
> +static int adva_x1_cpld_wait_ready(struct ptp_ocp *bp, unsigned int max_ms)
> +{
> + u32 status = 0;
> + int err, ret;
> +
> + ret = read_poll_timeout(adva_x1_cpld_read_status, err,
> + err || READ_ONCE(bp->cpld_cancel) ||
> + (status & CPLD_STATUS_FAILED) ||
> + !(status & CPLD_STATUS_BUSY),
> + CPLD_POLL_US, max_ms * USEC_PER_MSEC, false,
> + bp, &status);
> + if (ret)
> + return ret;
> + if (READ_ONCE(bp->cpld_cancel))
> + return -ECANCELED;
> + if (err || (status & CPLD_STATUS_FAILED))
> + return -EIO;
> +
> + return 0;
> +}
> +
> +/* A step aborted by cancel() must be reported as such, not as a HW error. */
> +static enum fw_upload_err adva_cpld_err(struct ptp_ocp *bp)
> +{
> + return READ_ONCE(bp->cpld_cancel) ? FW_UPLOAD_ERR_CANCELED
> + : FW_UPLOAD_ERR_HW_ERROR;
> +}
> +
[Severity: Medium]
Are the firmware-upload error categories chosen here the ones userspace
expects? The values map straight onto the strings in fw_upload_err_str[]
in drivers/base/firmware_loader/sysfs_upload.c, which are what shows up in
/sys/class/firmware/adva-cpld.N/error.
adva_cpld_err() reports any transport failure as FW_UPLOAD_ERR_HW_ERROR, so
a failed __i2c_transfer() in adva_x1_i2c_xfer() never surfaces as
FW_UPLOAD_ERR_RW_ERROR, which include/linux/firmware.h documents as "read or
write to HW failed".
In adva_cpld_prepare(), every adva_x1_bus_claim() failure becomes a timeout:
if (adva_x1_bus_claim(bp)) {
ret = FW_UPLOAD_ERR_TIMEOUT;
but adva_x1_bus_claim() also returns -ENODEV immediately when there is no
adapter (cpld_i2c_adap_nr is -1), and -ENOMEM when the scratch kzalloc
fails. Should those be reported as "timeout on handshake with HW"?
And in the opposite direction, the last check in adva_cpld_poll_complete():
if (adva_x1_cpld_wait_ready(bp, 3000))
return READ_ONCE(bp->cpld_cancel) ? FW_UPLOAD_ERR_CANCELED
: FW_UPLOAD_ERR_TIMEOUT;
reports a timeout even when wait_ready() returned -EIO because
CPLD_STATUS_FAILED was asserted or the status read itself failed.
> @@ -4562,6 +4678,212 @@ cpld_status_show(struct device *dev, struct device_attribute *attr,
> }
> static DEVICE_ATTR_ADMIN_RO(cpld_status);
>
> +/*
> + * adva_x1 CPLD firmware-upload callbacks.
> + *
> + * The kernel firmware-upload subsystem (CONFIG_FW_UPLOAD) exposes:
> + * /sys/class/firmware/adva-cpld.N/{data,loading,status,error,...}
> + * where N is the index of the owning ocpN device.
> + * Userspace writes the raw binary page data directly — no /lib/firmware/
> + * staging file is needed.
> + *
> + * Callback sequence driven by the framework:
> + * prepare() - validate size, acquire bus, enable config, erase flash
> + * write() - program one 16-byte page per call
> + * poll_complete()- set DONE, REFRESH, wait for CPLD to reboot
> + * cancel() - set flag; checked at the start of each callback
> + * cleanup() - release bus resources (called on success or failure)
> + */
> +static enum fw_upload_err
> +adva_cpld_prepare(struct fw_upload *fwl, const u8 *data, u32 size)
> +{
> + enum fw_upload_err ret = FW_UPLOAD_ERR_NONE;
> + struct ptp_ocp *bp = fwl->dd_handle;
> +
> + /* Do not clear cpld_cancel here: fw_upload_start() queues the work
> + * before this runs, so a cancel may already have arrived. It is
> + * cleared once the upload is over, on every exit below and in
> + * cleanup().
> + */
> + if (!size || size % CPLD_PAGE_SIZE) {
> + WRITE_ONCE(bp->cpld_cancel, false);
> + return FW_UPLOAD_ERR_INVALID_SIZE;
> + }
[Severity: Low]
The block comment above says cancel() sets a flag "checked at the start of
each callback", but does adva_cpld_prepare() actually check it at entry?
For an invalid size the pending cancellation is cleared here and the result
is reported as invalid-file-size rather than user-abort.
For a valid image, the bus is claimed, the mux is switched and
CPLD_CMD_EN_CFG_TP is issued before the first effective test of
bp->cpld_cancel inside adva_x1_cpld_wait_ready(), so an already-cancelled
upload still cycles the CPLD into configuration mode and back out via
DIS_CFG. Would an explicit check before the size validation match the
comment?
> +
> + bp->cpld_in_config_mode = false;
> +
> + mutex_lock(&bp->cpld_lock);
> +
> + if (adva_x1_bus_claim(bp)) {
> + ret = FW_UPLOAD_ERR_TIMEOUT;
> + goto err_unlock;
> + }
> +
[ ... ]
> +err_deselect:
> + if (bp->cpld_in_config_mode) {
> + adva_x1_cpld_write(bp, CPLD_CMD_DIS_CFG);
> + bp->cpld_in_config_mode = false;
> + }
> + adva_x1_mux_select(bp, -1);
> +err_release:
> + adva_x1_bus_release(bp);
> +err_unlock:
> + WRITE_ONCE(bp->cpld_cancel, false);
> + mutex_unlock(&bp->cpld_lock);
> + return ret;
> +}
[ ... ]
> +static enum fw_upload_err
> +adva_cpld_poll_complete(struct fw_upload *fwl)
> +{
> + struct ptp_ocp *bp = fwl->dd_handle;
> + int err;
> + u32 st;
> +
> + lockdep_assert_held(&bp->cpld_lock);
> +
> + if (READ_ONCE(bp->cpld_cancel))
> + return FW_UPLOAD_ERR_CANCELED;
> +
> + if (adva_x1_cpld_write(bp, CPLD_CMD_SET_DONE) ||
> + adva_x1_cpld_wait_ready(bp, 1000))
> + return adva_cpld_err(bp);
> +
> + if (adva_x1_cpld_read_status(bp, &st) || !(st & CPLD_STATUS_DONE))
> + return FW_UPLOAD_ERR_HW_ERROR;
> +
> + if (adva_x1_cpld_write(bp, CPLD_CMD_REFRESH))
> + return FW_UPLOAD_ERR_HW_ERROR;
[Severity: Medium]
Is the post-REFRESH state checked strictly enough to call the update
successful?
DONE is sampled only before REFRESH. After REFRESH the only remaining
check is adva_x1_cpld_wait_ready(bp, 3000), which returns 0 as soon as a
status read succeeds with BUSY and FAILED clear, so an image that reboots
with DONE clear is still reported as FW_UPLOAD_ERR_NONE.
The status bit definitions added here also cover only DONE, BUSY and
FAILED. drivers/fpga/lattice-sysconfig.h defines a three-bit error field
for the same family:
#define SYSCONFIG_STATUS_ERR GENMASK(25, 23)
and lattice-sysconfig.c requires that field to be zero, DONE set and BUSY
clear before declaring programming complete. Should this driver re-read
status after REFRESH and apply the same three conditions, so a nonzero
command or CRC error code with FAILED clear is not treated as success?
> +
> + /* REFRESH reboots the CPLD out of configuration mode, so cleanup()
> + * must not send DIS_CFG afterwards even if the checks below fail.
> + */
> + bp->cpld_in_config_mode = false;
[ ... ]
> + if (adva_x1_cpld_wait_ready(bp, 3000))
> + return READ_ONCE(bp->cpld_cancel) ? FW_UPLOAD_ERR_CANCELED
> + : FW_UPLOAD_ERR_TIMEOUT;
> +
> + return FW_UPLOAD_ERR_NONE;
> +}
> +
> +static void
> +adva_cpld_cancel(struct fw_upload *fwl)
> +{
> + struct ptp_ocp *bp = fwl->dd_handle;
> +
> + WRITE_ONCE(bp->cpld_cancel, true);
> +}
> +
> +static void
> +adva_cpld_cleanup(struct fw_upload *fwl)
> +{
> + struct ptp_ocp *bp = fwl->dd_handle;
> +
> + __acquire(&bp->cpld_lock); /* held since prepare() returned ok */
> + lockdep_assert_held(&bp->cpld_lock);
> +
> + if (bp->cpld_in_config_mode) {
> + adva_x1_cpld_write(bp, CPLD_CMD_DIS_CFG);
> + bp->cpld_in_config_mode = false;
> + }
> + adva_x1_mux_select(bp, -1);
> + adva_x1_bus_release(bp);
> + WRITE_ONCE(bp->cpld_cancel, false);
> + mutex_unlock(&bp->cpld_lock);
> +}
[Severity: Medium]
Can bp->cpld_cancel survive past the upload that owns it and abort the next
one?
cleanup() clears the flag, but the core still reports the operation as
active at that point. In fw_upload_main() (sysfs_upload.c):
done:
if (fwlp->ops->cleanup)
fwlp->ops->cleanup(fwl);
...
fw_upload_prog_complete(fwlp);
progress only becomes FW_UPLOAD_PROG_IDLE in fw_upload_prog_complete(),
while cancel_store() calls ops->cancel() for any non-IDLE progress. So:
worker other task
adva_cpld_cleanup()
WRITE_ONCE(cpld_cancel, false)
mutex_unlock(&bp->cpld_lock)
echo 1 > cancel
progress != IDLE
adva_cpld_cancel()
WRITE_ONCE(cpld_cancel, true)
fw_upload_prog_complete()
Nothing clears it again, because adva_cpld_prepare() deliberately does not
reset the flag on entry. The next upload then hits it on the first
iteration of adva_x1_cpld_wait_ready():
err || READ_ONCE(bp->cpld_cancel) || ...
which returns -ECANCELED, and prepare() reports user-abort for an upload
nobody cancelled, after EN_CFG_TP has already put the CPLD into
configuration mode.
The err_unlock path in adva_cpld_prepare() clears the flag with the same
timing. Since the flag's lifetime is managed entirely outside fwlp->lock,
the only lock that orders cancel delivery, READ_ONCE/WRITE_ONCE cannot
close this. Would tying the state to an upload generation, or resetting it
under the same serialization, work better here?
> +
> +static const struct fw_upload_ops adva_cpld_upload_ops = {
> + .prepare = adva_cpld_prepare,
> + .write = adva_cpld_write,
> + .poll_complete = adva_cpld_poll_complete,
> + .cancel = adva_cpld_cancel,
> + .cleanup = adva_cpld_cleanup,
> +};
> +
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260902044232.21188-1-maimon.sagi%40gmail.com
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-04 7:46 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-02 4:42 [PATCH net-next v12 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon
2026-09-02 4:42 ` [PATCH net-next v12 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon
2026-09-04 7:46 ` netdev-bot+sashiko
2026-09-02 4:42 ` [PATCH net-next v12 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon
2026-09-04 7:46 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox