* [PATCH net-next v11 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 @ 2026-08-11 13:57 Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon 0 siblings, 2 replies; 5+ messages in thread From: Sagi Maimon @ 2026-08-11 13:57 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, as requested in review. v10: https://lore.kernel.org/all/20260805083154.3821-1-maimon.sagi@gmail.com/ Review: https://lore.kernel.org/all/20260810230735.955188-2-kuba@kernel.org/ One review point did not become a code change, so to answer it here: the CPLD paths leaving the PCA9548 with all channels deselected is harmless. The mux sits on the TMC bus, which the EEPROM paths never see - by the time the grant is dropped and the host controller is back on the EEPROM segment, the mux is not reachable from that adapter at all. Deselect-all is also the PCA9548 power-on state, so the MicroBlaze finds it exactly as it would after a cold boot. v11 documents this above the CPLD helpers rather than leaving it to be re-derived. Changes since v10: - Split into two patches, separating the firmware flashing from the bus access and the read-only interfaces (Jakub). - Report the Lattice device ID as the fixed devlink version "cpld.id" and drop the cpld_device_id sysfs attribute (Jakub). The value is cached, so the bus arbitration is paid once per card. - Hold the i2c core adapter lock across the whole grant window and use __i2c_transfer(); while the host controller is routed onto the TMC bus the EEPROMs are not behind that adapter at all, so an unarbitrated transfer from ptp_ocp_read_eeprom(), the nvmem attributes or the at24 sysfs files would be issued onto the wrong bus. - Register the firmware-upload node per card as adva-cpld.N; the fixed name made the second X1 board in a host fail with -EEXIST. - Unregister the upload first on detach, so an in-flight programming cycle is cancelled and flushed while the I2C controller is still up. - Restrict cpld_status to 0400 and take cpld_lock interruptibly, so a reader cannot sit unkillable for the length of an upload or stall device removal. - Fix adva_x1_cpld_wait_ready(): it polled floor(max_ms / 100) times and never re-read after the last sleep, so the 100 ms per-page budget allowed exactly one poll. Use read_poll_timeout() with a wall-clock deadline that also accounts for the I2C transactions. - Cancellation: keep a request that arrives before prepare() runs, annotate the flag with READ_ONCE/WRITE_ONCE, honour it inside the long waits, and report it as CANCELED rather than HW_ERROR. - Set cpld_in_config_mode before EN_CFG_TP and clear it once REFRESH is accepted, so DIS_CFG is neither skipped after a failed wait nor sent to a device that already rebooted. - Retry the post-REFRESH mux reselect; its return was discarded, so a NAK failed an update whose image was already running. - Drop I2C_M_NOSTART: two messages in one transfer already give the repeated-START read the ISC protocol needs, and the flag asks for the opposite on adapters that honour it. - Withdraw the MicroBlaze request when the grant times out, and drop the -EBUSY check that only ever read back its own write. - select FW_LOADER and FW_UPLOAD; without them the documented update path does not exist. - Correct the IDCODE to 0x612bc043 and the part to LCMXO3LF-2100. - Add lockdep_assert_held() to the callbacks and context annotations for the lock handed from prepare() to cleanup(). 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 | 655 ++++++++++++++++++++++- 3 files changed, 677 insertions(+), 4 deletions(-) -- 2.47.0 ^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net-next v11 1/2] ptp: ocp: add TAP CPLD access for ADVA TimeCard X1 2026-08-11 13:57 [PATCH net-next v11 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon @ 2026-08-11 13:57 ` Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon 1 sibling, 0 replies; 5+ messages in thread From: Sagi Maimon @ 2026-08-11 13:57 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 | 19 ++ drivers/ptp/ptp_ocp.c | 344 ++++++++++++++++++++++- 2 files changed, 359 insertions(+), 4 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard index 3ae41b7634ac..26a93cee0b89 100644 --- a/Documentation/ABI/testing/sysfs-timecard +++ b/Documentation/ABI/testing/sysfs-timecard @@ -11,6 +11,25 @@ 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..41a7fde1ae6a 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,12 @@ 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 */ + 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 +461,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 +1284,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 +2197,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 +3224,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 +4252,298 @@ 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 + +#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. + * + * All buffers are heap-allocated internally to guarantee DMA safety for + * the Xilinx I2C controller. Caller must hold the claim taken by + * adva_x1_bus_claim(), hence __i2c_transfer() rather than i2c_transfer(). + */ +static int adva_x1_i2c_xfer(struct ptp_ocp *bp, + u8 addr, const void *wdata, u8 wlen, + void *rdata, u8 rlen) +{ + u8 *wbuf = NULL, *rbuf = NULL; + struct i2c_adapter *adap; + struct i2c_msg msgs[2]; + int nmsgs = 0, ret; + + lockdep_assert_held(&bp->cpld_lock); + + adap = bp->cpld_adap; + if (!adap) + return -ENODEV; + + if (wlen) { + wbuf = kmemdup(wdata, wlen, GFP_KERNEL); + if (!wbuf) { + ret = -ENOMEM; + goto free; + } + msgs[nmsgs++] = (struct i2c_msg){ + .addr = addr, + .flags = I2C_M_DMA_SAFE, + .len = wlen, + .buf = wbuf, + }; + } + if (rlen) { + rbuf = kzalloc(rlen, GFP_KERNEL); + if (!rbuf) { + ret = -ENOMEM; + goto free; + } + 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) { + if (rdata && rlen) + memcpy(rdata, rbuf, rlen); + ret = 0; + } else { + ret = (ret < 0) ? ret : -EIO; + } +free: + kfree(wbuf); + kfree(rbuf); + return ret; +} + +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; + 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. + */ +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; + + 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, 1, 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, &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 reports 0x612BC043. + */ +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 +4592,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 +5223,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 +5400,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 +5550,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 [flat|nested] 5+ messages in thread
* [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1 2026-08-11 13:57 [PATCH net-next v11 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon @ 2026-08-11 13:57 ` Sagi Maimon 2026-08-16 22:19 ` Vadim Fedorenko 1 sibling, 1 reply; 5+ messages in thread From: Sagi Maimon @ 2026-08-11 13:57 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 | 5 + drivers/ptp/Kconfig | 2 + drivers/ptp/ptp_ocp.c | 313 ++++++++++++++++++++++- 3 files changed, 319 insertions(+), 1 deletion(-) diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard index 26a93cee0b89..41eeadd46330 100644 --- a/Documentation/ABI/testing/sysfs-timecard +++ b/Documentation/ABI/testing/sysfs-timecard @@ -30,6 +30,11 @@ 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 Contact: Jonathan Lemon <jonathan.lemon@gmail.com> diff --git a/drivers/ptp/Kconfig b/drivers/ptp/Kconfig index b93640ca08b7..0c2c7dd32e7f 100644 --- a/drivers/ptp/Kconfig +++ b/drivers/ptp/Kconfig @@ -218,6 +218,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 41a7fde1ae6a..4a0bd9b89452 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 @@ -426,6 +428,9 @@ struct ptp_ocp { struct i2c_adapter *cpld_adap; /* claimed adapter; 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) @@ -459,6 +464,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); @@ -3225,6 +3232,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); } @@ -4283,6 +4313,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) @@ -4408,7 +4447,8 @@ static void adva_x1_bus_release(struct ptp_ocp *bp) * 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. + * 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) { @@ -4442,6 +4482,20 @@ static int adva_x1_mux_select(struct ptp_ocp *bp, int ch) return adva_x1_i2c_xfer(bp, ADVA_MUX_ADDR, &val, 1, NULL, 0); } +/* Send 1-byte ISC command + optional arguments. */ +static int adva_x1_cpld_write(struct ptp_ocp *bp, + u8 cmd, const u8 *args, u8 nargs) +{ + u8 buf[1 + 64]; + + if (nargs > 64) + return -EINVAL; + buf[0] = cmd; + if (nargs) + memcpy(&buf[1], args, nargs); + return adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, buf, 1 + 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 @@ -4467,6 +4521,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 @@ -4544,6 +4630,222 @@ 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) +{ + static const u8 era_args[3] = { 0x04, 0x00, 0x00 }; /* cfg sector only */ + static const u8 en_args[2] = { 0x08, 0x00 }; + static const u8 dis_args[2] = { 0x00, 0x00 }; + static const u8 zero3[3] = { 0 }; + 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, en_args, 2) || + 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, era_args, 3) || + 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, zero3, 3)) { + 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, dis_args, 2); + 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 page_args[3 + CPLD_PAGE_SIZE]; + + 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; + + page_args[0] = 0x00; + page_args[1] = 0x00; + page_args[2] = 0x01; + memcpy(&page_args[3], data + offset, CPLD_PAGE_SIZE); + + if (adva_x1_cpld_write(bp, CPLD_CMD_WRITE_PAGE, + page_args, 3 + CPLD_PAGE_SIZE) || + 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) +{ + static const u8 ref_args[2] = { 0x00, 0x00 }; + static const u8 zero3[3] = { 0 }; + 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, zero3, 3) || + 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, ref_args, 2)) + 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) +{ + static const u8 dis_args[2] = { 0x00, 0x00 }; + 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, dis_args, 2); + 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, @@ -5186,6 +5488,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 [flat|nested] 5+ messages in thread
* Re: [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1 2026-08-11 13:57 ` [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon @ 2026-08-16 22:19 ` Vadim Fedorenko 2026-08-18 4:43 ` Sagi Maimon 0 siblings, 1 reply; 5+ messages in thread From: Vadim Fedorenko @ 2026-08-16 22:19 UTC (permalink / raw) To: Sagi Maimon, netdev Cc: richardcochran, kuba, andrew+netdev, davem, edumazet, pabeni, linux-kernel On 11/08/2026 14:57, Sagi Maimon wrote: > 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 | 5 + > drivers/ptp/Kconfig | 2 + > drivers/ptp/ptp_ocp.c | 313 ++++++++++++++++++++++- > 3 files changed, 319 insertions(+), 1 deletion(-) > > diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard > index 26a93cee0b89..41eeadd46330 100644 > --- a/Documentation/ABI/testing/sysfs-timecard > +++ b/Documentation/ABI/testing/sysfs-timecard > @@ -30,6 +30,11 @@ 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 > Contact: Jonathan Lemon <jonathan.lemon@gmail.com> > diff --git a/drivers/ptp/Kconfig b/drivers/ptp/Kconfig > index b93640ca08b7..0c2c7dd32e7f 100644 > --- a/drivers/ptp/Kconfig > +++ b/drivers/ptp/Kconfig > @@ -218,6 +218,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 41a7fde1ae6a..4a0bd9b89452 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 > > @@ -426,6 +428,9 @@ struct ptp_ocp { > struct i2c_adapter *cpld_adap; /* claimed adapter; 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) > @@ -459,6 +464,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); > @@ -3225,6 +3232,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); > } > > @@ -4283,6 +4313,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) > @@ -4408,7 +4447,8 @@ static void adva_x1_bus_release(struct ptp_ocp *bp) > * 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. > + * 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) > { > @@ -4442,6 +4482,20 @@ static int adva_x1_mux_select(struct ptp_ocp *bp, int ch) > return adva_x1_i2c_xfer(bp, ADVA_MUX_ADDR, &val, 1, NULL, 0); > } > > +/* Send 1-byte ISC command + optional arguments. */ > +static int adva_x1_cpld_write(struct ptp_ocp *bp, > + u8 cmd, const u8 *args, u8 nargs) > +{ > + u8 buf[1 + 64]; > + > + if (nargs > 64) > + return -EINVAL; > + buf[0] = cmd; > + if (nargs) > + memcpy(&buf[1], args, nargs); > + return adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, buf, 1 + nargs, NULL, 0); > +} this function looks like not needed. basically, all callers of this function use either static, or stack-allocated buffurs, which are copied to another 65-byte stack allocated buffer, which is later copied to a heap-allocated buffer in adva_x1_i2c_xfer(). I think it's too much of copying for a command interface. It can be simplified to actually copy a command byte to a very last buffer only. The check can also be moved to adva_x1_i2c_xfer(). Even static per-command data can be returned as a pointer via helper with switch-case block. I believe it will make code much simplier and more readable. > + > /* > * Send a 4-byte command then read data back without an intermediate STOP > * (Lattice combined write->repeated-START->read). Two messages in one > @@ -4467,6 +4521,38 @@ static int adva_x1_cpld_read_status(struct ptp_ocp *bp, u32 *status) > return 0; > } > [...] > +static enum fw_upload_err > +adva_cpld_prepare(struct fw_upload *fwl, const u8 *data, u32 size) > +{ > + static const u8 era_args[3] = { 0x04, 0x00, 0x00 }; /* cfg sector only */ > + static const u8 en_args[2] = { 0x08, 0x00 }; > + static const u8 dis_args[2] = { 0x00, 0x00 }; here dis_args are copying another static const buffer from adva_cpld_cleanup() > + static const u8 zero3[3] = { 0 }; this one is redundant, because heap-allocated buffer is zero'ed on alloc this code needs a bit more thinking. > + 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, en_args, 2) || > + 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, era_args, 3) || > + 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, zero3, 3)) { > + 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, dis_args, 2); > + 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 page_args[3 + CPLD_PAGE_SIZE]; > + > + 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; > + > + page_args[0] = 0x00; > + page_args[1] = 0x00; > + page_args[2] = 0x01; > + memcpy(&page_args[3], data + offset, CPLD_PAGE_SIZE); here again, stack allocated -> stack allocated -> heap allocated. quite a lot of allocations and copying for 16 bytes of actual data transfer. > + > + if (adva_x1_cpld_write(bp, CPLD_CMD_WRITE_PAGE, > + page_args, 3 + CPLD_PAGE_SIZE) || > + adva_x1_cpld_wait_ready(bp, 100)) > + return adva_cpld_err(bp); > + > + *written = CPLD_PAGE_SIZE; > + return FW_UPLOAD_ERR_NONE; > +} ^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload for ADVA TimeCard X1 2026-08-16 22:19 ` Vadim Fedorenko @ 2026-08-18 4:43 ` Sagi Maimon 0 siblings, 0 replies; 5+ messages in thread From: Sagi Maimon @ 2026-08-18 4:43 UTC (permalink / raw) To: Vadim Fedorenko Cc: netdev, richardcochran, kuba, andrew+netdev, davem, edumazet, pabeni, linux-kernel On Mon, Aug 17, 2026 at 1:19 AM Vadim Fedorenko <vadim.fedorenko@linux.dev> wrote: > > On 11/08/2026 14:57, Sagi Maimon wrote: > > 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 | 5 + > > drivers/ptp/Kconfig | 2 + > > drivers/ptp/ptp_ocp.c | 313 ++++++++++++++++++++++- > > 3 files changed, 319 insertions(+), 1 deletion(-) > > > > diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard > > index 26a93cee0b89..41eeadd46330 100644 > > --- a/Documentation/ABI/testing/sysfs-timecard > > +++ b/Documentation/ABI/testing/sysfs-timecard > > @@ -30,6 +30,11 @@ 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 > > Contact: Jonathan Lemon <jonathan.lemon@gmail.com> > > diff --git a/drivers/ptp/Kconfig b/drivers/ptp/Kconfig > > index b93640ca08b7..0c2c7dd32e7f 100644 > > --- a/drivers/ptp/Kconfig > > +++ b/drivers/ptp/Kconfig > > @@ -218,6 +218,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 41a7fde1ae6a..4a0bd9b89452 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 > > > > @@ -426,6 +428,9 @@ struct ptp_ocp { > > struct i2c_adapter *cpld_adap; /* claimed adapter; 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) > > @@ -459,6 +464,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); > > @@ -3225,6 +3232,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); > > } > > > > @@ -4283,6 +4313,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) > > @@ -4408,7 +4447,8 @@ static void adva_x1_bus_release(struct ptp_ocp *bp) > > * 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. > > + * 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) > > { > > @@ -4442,6 +4482,20 @@ static int adva_x1_mux_select(struct ptp_ocp *bp, int ch) > > return adva_x1_i2c_xfer(bp, ADVA_MUX_ADDR, &val, 1, NULL, 0); > > } > > > > +/* Send 1-byte ISC command + optional arguments. */ > > +static int adva_x1_cpld_write(struct ptp_ocp *bp, > > + u8 cmd, const u8 *args, u8 nargs) > > +{ > > + u8 buf[1 + 64]; > > + > > + if (nargs > 64) > > + return -EINVAL; > > + buf[0] = cmd; > > + if (nargs) > > + memcpy(&buf[1], args, nargs); > > + return adva_x1_i2c_xfer(bp, ADVA_CPLD_ADDR, buf, 1 + nargs, NULL, 0); > > +} > > this function looks like not needed. basically, all callers of this > function use either static, or stack-allocated buffurs, which are copied > to another 65-byte stack allocated buffer, which is later copied to a > heap-allocated buffer in adva_x1_i2c_xfer(). I think it's too much of > copying for a command interface. It can be simplified to actually copy > a command byte to a very last buffer only. The check can also be moved > to adva_x1_i2c_xfer(). Even static per-command data can be returned as > a pointer via helper with switch-case block. I believe it will make code > much simplier and more readable. > Thanks - v12 restructures the command path as you describe, with one deviation I have called out below: I kept adva_x1_cpld_write() itself. adva_x1_i2c_xfer() now takes the opcode as its own argument and assembles the message in a scratch buffer taken once per bus claim, so there is no intermediate buffer and no per-transfer allocation: static int adva_x1_i2c_xfer(struct ptp_ocp *bp, u8 addr, int cmd, const void *wdata, u8 wlen, void *rdata, u8 rlen) { ... 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; The bound check you asked for is here now, and it has something real to bound: the scratch buffer, rather than the stack array it used to guard. adva_x1_cpld_write() takes just the opcode, and the argument bytes come from a switch-case helper returning a pointer to the static data: 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 }; switch (cmd) { case CPLD_CMD_EN_CFG_TP: *nargs = sizeof(en_cfg_tp); return en_cfg_tp; ... } } 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); } I kept adva_x1_cpld_write() itself, now four lines, because something has to consume the pointer the helper returns and issue the transfer; without it each of the eight call sites needs the lookup, two locals and the full adva_x1_i2c_xfer() argument list. If you meant drop it entirely and have the callers do that, say so and I will. > > + > > /* > > * Send a 4-byte command then read data back without an intermediate STOP > > * (Lattice combined write->repeated-START->read). Two messages in one > > @@ -4467,6 +4521,38 @@ static int adva_x1_cpld_read_status(struct ptp_ocp *bp, u32 *status) > > return 0; > > } > > > > [...] > > > +static enum fw_upload_err > > +adva_cpld_prepare(struct fw_upload *fwl, const u8 *data, u32 size) > > +{ > > + static const u8 era_args[3] = { 0x04, 0x00, 0x00 }; /* cfg sector only */ > > + static const u8 en_args[2] = { 0x08, 0x00 }; > > + static const u8 dis_args[2] = { 0x00, 0x00 }; > > here dis_args are copying another static const buffer from > adva_cpld_cleanup() > Gone - both call sites are now just adva_x1_cpld_write(bp, CPLD_CMD_DIS_CFG). > > + static const u8 zero3[3] = { 0 }; > > this one is redundant, because heap-allocated buffer is zero'ed on alloc > Right in effect, though not for that reason in v11: the write buffer was kmemdup()ed there, which does not zero, so the zero3[] arrays were doing real work. In v12 the all-zero commands (RESET_ADDR, SET_DONE, DIS_CFG, REFRESH) pass NULL and adva_x1_i2c_xfer() zeroes that part of the message, so the arrays are gone. > this code needs a bit more thinking. > Reworked in v12 along the lines above - see the per-command changes in this reply. No functional change: the byte sequences on the wire are unchanged, and I verified CPLD programming on an ADVA TimeCard X1. > > + 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, en_args, 2) || > > + 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, era_args, 3) || > > + 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, zero3, 3)) { > > + 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, dis_args, 2); > > + 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 page_args[3 + CPLD_PAGE_SIZE]; > > + > > + 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; > > + > > + page_args[0] = 0x00; > > + page_args[1] = 0x00; > > + page_args[2] = 0x01; > > + memcpy(&page_args[3], data + offset, CPLD_PAGE_SIZE); > > here again, stack allocated -> stack allocated -> heap allocated. > quite a lot of allocations and copying for 16 bytes of actual data > transfer. > Fixed in v12. The middle stack buffer is gone, and the scratch buffer is allocated once per bus claim rather than per transfer - a claim spans the whole upload, so programming an image is one allocation rather than one per page: /* One scratch buffer per claim, not per transfer. */ bp->cpld_buf = kzalloc(2 * ADVA_CPLD_XFER_MAX, GFP_KERNEL); adva_cpld_write() still stages the three header bytes and the page in a small stack array before the transfer copies it in, so the page data is copied once more than strictly necessary. Removing that too would mean letting the callback write into the transfer's buffer directly, which seemed a worse trade; say the word if you would rather have it. net-next is closed at the moment, so I will post v12 with these changes once it reopens. > > + > > + if (adva_x1_cpld_write(bp, CPLD_CMD_WRITE_PAGE, > > + page_args, 3 + CPLD_PAGE_SIZE) || > > + adva_x1_cpld_wait_ready(bp, 100)) > > + return adva_cpld_err(bp); > > + > > + *written = CPLD_PAGE_SIZE; > > + return FW_UPLOAD_ERR_NONE; > > +} ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-08-18 4:43 UTC | newest] Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-08-11 13:57 [PATCH net-next v11 0/2] ptp: ocp: add TAP CPLD support for ADVA TimeCard X1 Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 1/2] ptp: ocp: add TAP CPLD access " Sagi Maimon 2026-08-11 13:57 ` [PATCH net-next v11 2/2] ptp: ocp: add TAP CPLD firmware upload " Sagi Maimon 2026-08-16 22:19 ` Vadim Fedorenko 2026-08-18 4:43 ` Sagi Maimon
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®