Netdev List
 help / color / mirror / Atom feed
From: Vadim Fedorenko <vadim.fedorenko@linux.dev>
To: Sagi Maimon <maimon.sagi@gmail.com>,
	jonathan.lemon@gmail.com, richardcochran@gmail.com,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com
Cc: linux-kernel@vger.kernel.org, netdev@vger.kernel.org
Subject: Re: [PATCH v8] ptp: ocp: add CPLD ISP support for ADVA TimeCard X1
Date: Tue, 4 Aug 2026 12:35:03 +0100	[thread overview]
Message-ID: <65c42538-9607-4dff-adbe-d9d5c9a338d0@linux.dev> (raw)
In-Reply-To: <20260802071132.5078-1-maimon.sagi@gmail.com>

On 02/08/2026 08:11, Sagi Maimon wrote:
> The ADVA TimeCard X1 (PCI device 0x0410) uses a Lattice MachXO3 CPLD
> that is programmed over I2C using in-system programming (ISP).
> 
> The CPLD is connected to a secondary I2C bus shared with the onboard
> MicroBlaze soft CPU.
> 
> Add support for CPLD access and firmware updates on the ADVA TimeCard X1
> board by arbitration of the shared I2C bus, CPLD ISP command handling,
> status polling, and firmware upload operations using the firmware-upload
> subsystem.
> 
> Add the following X1-only user-visible interfaces:
> 
>    /sys/class/timecard/ocpN/cpld_device_id
>          report the 32-bit Lattice MachXO3 CPLD device ID
> 
>    /sys/class/timecard/ocpN/cpld_status
>          report the CPLD status register, including the DONE,
>          BUSY, and FAILED indicators
> 
> Firmware updates are performed through the firmware-upload framework,
> which acquires ownership of the shared I2C bus, erases the CPLD
> configuration flash, programs the image page-by-page, and activates
> the new image using the MachXO3 REFRESH command.
> 
> All CPLD operations are serialized and coordinated with the MicroBlaze
> firmware to ensure exclusive access to the shared I2C bus. The added
> interfaces are available only on ADVA TimeCard X1 boards.
> 
> Signed-off-by: Sagi Maimon <maimon.sagi@gmail.com>
> ---
> 
>   Addressed comments from:
>    - Jakub Kicinski :https://lore.kernel.org/all/20260730130424.77a90fa3@kernel.org/
>    - Vadim Fedorenko :https://lore.kernel.org/all/7075bcdb-da4d-4d2c-bd09-516d9f244277@linux.dev/
>   
>   Changes since v7:
>   - Use %pe instead of %ld + PTR_ERR() (coccicheck)
>   - Add #include <linux/unaligned.h>
>   - Widen adva_x1_i2c_xfer() buffer pointers to void *
>   - Use cpu_to_be32() in adva_x1_cpld_cmd_read()
>   - Use get_unaligned_be32() in adva_x1_cpld_read_status()
>   
>   Documentation/ABI/testing/sysfs-timecard |  34 ++
>   drivers/ptp/ptp_ocp.c                    | 497 ++++++++++++++++++++++-
>   2 files changed, 527 insertions(+), 4 deletions(-)
> 
> diff --git a/Documentation/ABI/testing/sysfs-timecard b/Documentation/ABI/testing/sysfs-timecard
> index 3ae41b7634ac..f7c9955acb0a 100644
> --- a/Documentation/ABI/testing/sysfs-timecard
> +++ b/Documentation/ABI/testing/sysfs-timecard
> @@ -11,6 +11,40 @@ Contact:	Jonathan Lemon <jonathan.lemon@gmail.com>
>   Description:	This directory contains the attributes of the Nth timecard
>   		registered.
>   
> +What:		/sys/class/timecard/ocpN/cpld_device_id
> +Date:		July 2026
> +Contact:	Sagi Maimon <maimon.sagi@gmail.com>
> +Description:	(RO) The 32-bit Lattice device ID of the TAP CPLD, reported as
> +		a hex string, e.g. "0xe12bc043".
> +
> +		Only present on ADVA x1 TAP boards (PCI ID 0xad5a:0x0410).
> +		The Lattice LCMXO3LF-210 reports 0xe12bc043.
> +
> +		The driver acquires the MicroBlaze I2C bus internally before
> +		issuing the READ_IDCODE command; no bus arbitration is required
> +		from userspace.
> +
> +What:		/sys/class/timecard/ocpN/cpld_status
> +Date:		July 2026
> +Contact:	Sagi Maimon <maimon.sagi@gmail.com>
> +Description:	(RO) 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.
> +
> +		The driver acquires the MicroBlaze I2C bus internally; no bus
> +		arbitration is required from userspace.
> +
> +		To program new CPLD firmware use the standard kernel
> +		firmware-upload interface registered at:
> +		/sys/class/firmware/adva-cpld/
> +
>   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 35e911f1ad78..a4a8cab5ed7d 100644
> --- a/drivers/ptp/ptp_ocp.c
> +++ b/drivers/ptp/ptp_ocp.c
> @@ -24,6 +24,11 @@
>   #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>
> +#include <linux/delay.h>
> +#include <linux/firmware.h>

why do you double include headers?

>   
>   #define PCI_DEVICE_ID_META_TIMECARD		0x0400
>   
> @@ -85,6 +90,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 +169,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 +423,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 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)
> @@ -449,6 +462,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;
>   
> @@ -1273,6 +1288,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,
>   		},
>   	},
>   	{ }
> @@ -3197,6 +3213,20 @@ ptp_ocp_adva_board_init(struct ptp_ocp *bp, struct ocp_resource *r)
>   		return err;
>   	ptp_ocp_sma_init(bp);
>   
> +	if (info->has_cpld) {
> +		struct fw_upload *fwl;
> +
> +		fwl = firmware_upload_register(THIS_MODULE, &bp->pdev->dev,
> +					       "adva-cpld",
> +					       &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);
>   }
>   
> @@ -4224,6 +4254,442 @@ static const struct ocp_attr_group art_timecard_groups[] = {
>   	{ },
>   };
>   
> +/*
> + * Internal helpers for the adva_x1 TAP CPLD (Lattice LCMXO3LF-210).
> + *
> + * The CPLD sits at I2C address 0x40 behind a PCA9548 mux (0x74) on
> + * channel 0.  The I2C bus is shared with the MicroBlaze firmware;
> + * cpld_lock + mblaze acquire/release provide mutual exclusion for the
> + * full duration of any CPLD operation.  No raw I2C access is exposed
> + * to userspace; only the high-level attributes below are.
> + */
> +
> +#define ADVA_MUX_ADDR     0x74
> +#define ADVA_CPLD_ADDR    0x40
> +#define ADVA_MUX_CHANNEL  0
> +
> +#define MBLAZE_REQUEST    0x0000ffffU
> +#define MBLAZE_GRANTED    0xffffffffU
> +#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
> +#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
> +
> +/* 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 CPLD bus.
> + *
> + * All buffers are heap-allocated internally to guarantee DMA safety for
> + * the Xilinx I2C controller.  Caller must hold bp->cpld_lock.
> + */
> +static int adva_x1_i2c_xfer(struct ptp_ocp *bp,
> +			    u8 addr, const void *wdata, u8 wlen,
> +			    void *rdata, u8 rlen, bool nostart)
> +{
> +	struct i2c_adapter *adap;
> +	struct i2c_msg msgs[2];
> +	u8 *wbuf = NULL, *rbuf = NULL;
> +	int nmsgs = 0, ret;

as this patch goes through netdev, please respect reverse xmass tree
ordering of variables declaration here and futher in the patch

> +
> +	adap = i2c_get_adapter(READ_ONCE(bp->cpld_i2c_adap_nr));
> +	if (!adap)
> +		return -ENODEV;
> +
> +	if (wlen) {
> +		wbuf = kmemdup(wdata, wlen, GFP_KERNEL);
> +		if (!wbuf) {
> +			ret = -ENOMEM;
> +			goto put;
> +		}
> +		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 put;
> +		}
> +		msgs[nmsgs++] = (struct i2c_msg){
> +			.addr  = addr,
> +			.flags = I2C_M_RD | I2C_M_DMA_SAFE |
> +				 (nostart ? I2C_M_NOSTART : 0),
> +			.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;
> +	}
> +put:
> +	kfree(wbuf);
> +	kfree(rbuf);
> +	i2c_put_adapter(adap);
> +	return ret;
> +}
> +
> +/* Acquire the shared I2C bus from the MicroBlaze firmware. */
> +static int adva_x1_mblaze_acquire(struct ptp_ocp *bp)
> +{
> +	u32 val;
> +	int i;
> +
> +	if (!bp->pps_select)
> +		return -ENODEV;
> +
> +	/* Release any stale grant left by a previous crashed caller. */
> +	iowrite32(0, &bp->pps_select->i2c_bus_ctrl);
> +	val = ioread32(&bp->pps_select->i2c_bus_ctrl);
> +	if (val != 0)
> +		return -EBUSY;
> +
> +	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;
> +	}
> +	return -ETIMEDOUT;
> +}
> +
> +static void adva_x1_mblaze_release(struct ptp_ocp *bp)
> +{
> +	if (bp->pps_select)
> +		iowrite32(0, &bp->pps_select->i2c_bus_ctrl);
> +}
> +
> +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, false);
> +}
> +
> +/* 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, false);
> +}
> +
> +/*
> + * Send a 4-byte command then read data back without an intermediate STOP
> + * (Lattice combined write→repeated-START→read).
> + */
> +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, true);
> +}
> +
> +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;
> +}
> +
> +static int adva_x1_cpld_wait_ready(struct ptp_ocp *bp, unsigned int max_ms)
> +{
> +	u32 status;
> +	unsigned int elapsed = 0;
> +
> +	while (elapsed < max_ms) {
> +		if (adva_x1_cpld_read_status(bp, &status))
> +			return -EIO;
> +		if (status & CPLD_STATUS_FAILED)
> +			return -EIO;
> +		if (!(status & CPLD_STATUS_BUSY))
> +			return 0;
> +		usleep_range(100000, 101000);
> +		elapsed += 100;
> +	}
> +	return -ETIMEDOUT;
> +}
> +
> +/*
> + * cpld_device_id - show the Lattice device ID of the TAP CPLD.
> + *
> + * Returns the 32-bit ID as a hex string, e.g. "0x612bc043\n".
> + * Lattice LCMXO3LF-210 reports 0x612BC043.
> + */
> +static ssize_t
> +cpld_device_id_show(struct device *dev, struct device_attribute *attr,
> +		    char *buf)
> +{
> +	struct ptp_ocp *bp = dev_get_drvdata(dev);
> +	u8 data[4];
> +	u32 id = 0;
> +	int ret;
> +
> +	mutex_lock(&bp->cpld_lock);
> +	ret = adva_x1_mblaze_acquire(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)
> +		id = ((u32)data[0] << 24) | ((u32)data[1] << 16) |
> +		     ((u32)data[2] <<  8) |  (u32)data[3];

why is this code not changed to be32_to_cpu()?

> +	adva_x1_mux_select(bp, -1);
> +release:
> +	adva_x1_mblaze_release(bp);
> +out:
> +	mutex_unlock(&bp->cpld_lock);
> +	return ret ? ret : sysfs_emit(buf, "0x%08x\n", id);
> +}
> +static DEVICE_ATTR_RO(cpld_device_id);
> +
> +/*
> + * 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;
> +
> +	mutex_lock(&bp->cpld_lock);
> +	ret = adva_x1_mblaze_acquire(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_mblaze_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_RO(cpld_status);
> +
> +/*
> + * adva_x1 CPLD firmware-upload callbacks.
> + *
> + * The kernel firmware-upload subsystem (CONFIG_FW_UPLOAD) exposes:
> + *   /sys/class/firmware/adva-cpld/{data,loading,status,error,...}
> + * 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)
> +{
> +	struct ptp_ocp *bp = fwl->dd_handle;
> +	const u8 en_args[2]  = { 0x08, 0x00 };
> +	const u8 era_args[3] = { 0x04, 0x00, 0x00 }; /* cfg sector only */
> +	const u8 zero3[3]    = { 0 };
> +	const u8 dis_args[2] = { 0x00, 0x00 };

this constants can be made static, no need to re-init stack on every
call

> +	enum fw_upload_err ret = FW_UPLOAD_ERR_NONE;
> +
> +	if (!size || size % CPLD_PAGE_SIZE)
> +		return FW_UPLOAD_ERR_INVALID_SIZE;
> +
> +	bp->cpld_cancel		= false;
> +	bp->cpld_in_config_mode	= false;
> +
> +	mutex_lock(&bp->cpld_lock);
> +
> +	if (adva_x1_mblaze_acquire(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;
> +	}
> +
> +	if (adva_x1_cpld_write(bp, CPLD_CMD_EN_CFG_TP, en_args, 2) ||
> +	    adva_x1_cpld_wait_ready(bp, 5000)) {
> +		ret = FW_UPLOAD_ERR_HW_ERROR;
> +		goto err_deselect;
> +	}
> +	bp->cpld_in_config_mode = true;
> +
> +	if (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 = FW_UPLOAD_ERR_HW_ERROR;
> +		goto err_deselect;
> +	}
> +
> +	if (bp->cpld_cancel) {
> +		ret = FW_UPLOAD_ERR_CANCELED;
> +		goto err_deselect;

it looks like a duplicate of check, or do you expect something to change
cpld_cancel during writes?



  reply	other threads:[~2026-08-04 11:35 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-02  7:11 [PATCH v8] ptp: ocp: add CPLD ISP support for ADVA TimeCard X1 Sagi Maimon
2026-08-04 11:35 ` Vadim Fedorenko [this message]
2026-08-05  7:28   ` Sagi Maimon

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=65c42538-9607-4dff-adbe-d9d5c9a338d0@linux.dev \
    --to=vadim.fedorenko@linux.dev \
    --cc=andrew+netdev@lunn.ch \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=jonathan.lemon@gmail.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=maimon.sagi@gmail.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=richardcochran@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox