Devicetree
 help / color / mirror / Atom feed
From: Santhosh Kumar K <s-k6@ti.com>
To: Miquel Raynal <miquel.raynal@bootlin.com>
Cc: <broonie@kernel.org>, <robh@kernel.org>, <krzk+dt@kernel.org>,
	<conor+dt@kernel.org>, <richard@nod.at>, <vigneshr@ti.com>,
	<pratyush@kernel.org>, <mwalle@kernel.org>,
	<takahiro.kuwano@infineon.com>, <linux-spi@vger.kernel.org>,
	<devicetree@vger.kernel.org>, <linux-kernel@vger.kernel.org>,
	<linux-mtd@lists.infradead.org>, <praneeth@ti.com>,
	<u-kumar1@ti.com>, <a-dutta@ti.com>, <s-k6@ti.com>
Subject: Re: [PATCH v4 14/16] mtd: spinand: negotiate optimal PHY operating point before dirmap creation
Date: Thu, 16 Jul 2026 11:32:17 +0530	[thread overview]
Message-ID: <1ab838c8-ffd8-488f-b22b-6548723829ef@ti.com> (raw)
In-Reply-To: <87pl15pgz3.fsf@bootlin.com>

Hello Miquel,

Apologies for the delayed response.

On 02/07/26 20:38, Miquel Raynal wrote:
> On 18/06/2026 at 13:07:23 +0530, Santhosh Kumar K <s-k6@ti.com> wrote:
> 
>> Dirmap descriptors encode the op template including the operating
>> frequency at creation time, so PHY tuning must complete before dirmaps
>> are created to ensure the validated frequency is embedded in the
>> descriptors from the start.
> 
> Good idea.
> 
>> Move dirmap creation from spinand_init() to spinand_probe(), after a
>> new spinand_configure_phy()
> 
> Fine with the idea, but in the core I would like to avoid TI specific
> naming. We can call this helper spinand_optimize_controller(); for
> instance.
> 
> Please, do not use "PHY" in the SPI NAND core, because I find it too TI
> specific and we need generic enough naming so that we can include other
> controller's optimizations in this logic, if ever needed.

Got it!

> 
>> call that negotiates the best available PHY
>> operating point. spinand_configure_phy() tries the pre-selected variant
>> first. If the controller signals that PHY tuning is not applicable for
>> that op, spinand_try_phy_ranked() iterates remaining variants in
>> performance order — DTR variants first, then SDR variants after
>> switching the bus interface if needed. On full failure the device falls
>> back to the best available non-PHY mode.
> 
> Makes sense.
> 
>> Add spinand_reset_max_ops() to copy op templates with max_freq zeroed
> 
> spinand_reset_max_freq_ops()?

...

> 
>> before each execute_tuning call, enforcing the invariant that a non-zero
>> max_freq only results from a successful tuning.
>>
>> PHY failure is non-fatal; the device operates at the conservative base
>> rate.
>>
>> Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
>> ---
>>   drivers/mtd/nand/spi/core.c | 214 ++++++++++++++++++++++++++++++++++--
>>   include/linux/mtd/spinand.h |  11 ++
>>   2 files changed, 216 insertions(+), 9 deletions(-)
>>
>> diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
>> index b678d0534297..5dcfaabaf2cc 100644
>> --- a/drivers/mtd/nand/spi/core.c
>> +++ b/drivers/mtd/nand/spi/core.c
>> @@ -1280,10 +1280,16 @@ static int spinand_create_dirmap(struct spinand_device *spinand,
>>   	/* The plane number is passed in MSB just above the column address */
>>   	info.offset = plane << fls(nand->memorg.pagesize);
>>   
>> +	/*
>> +	 * Propagate the validated PHY frequency into the dirmap op templates
>> +	 * at construction time.
>> +	 */
>> +
>>   	/* Write descriptor */
>>   	info.length = nanddev_page_size(nand) + nanddev_per_page_oobsize(nand);
>>   	info.primary_op_tmpl = *spinand->op_templates->update_cache;
>>   	info.primary_op_tmpl.data.ecc = enable_ecc;
>> +	info.primary_op_tmpl.max_freq = spinand->max_write_op.max_freq;
>>   	desc = devm_spi_mem_dirmap_create(&spinand->spimem->spi->dev,
>>   					  spinand->spimem, &info);
>>   	if (IS_ERR(desc))
>> @@ -1294,9 +1300,11 @@ static int spinand_create_dirmap(struct spinand_device *spinand,
>>   	/* Read descriptor */
>>   	info.primary_op_tmpl = *spinand->op_templates->read_cache;
>>   	info.primary_op_tmpl.data.ecc = enable_ecc;
>> +	info.primary_op_tmpl.max_freq = spinand->max_read_op.max_freq;
>>   	if (secondary_op) {
>>   		info.secondary_op_tmpl = *spinand->op_templates->cont_read_cache;
>>   		info.secondary_op_tmpl.data.ecc = enable_ecc;
>> +		info.secondary_op_tmpl.max_freq = spinand->max_read_op.max_freq;
>>   	}
>>   	desc = spinand_create_rdesc(spinand, &info);
>>   	if (IS_ERR(desc))
>> @@ -1744,6 +1752,17 @@ int spinand_match_and_init(struct spinand_device *spinand,
>>   				spinand->cont_read_possible = false;
>>   		}
>>   
>> +		/*
>> +		 * Save the full read variant list (ODTR and SSDR ops) for PHY
>> +		 * tuning iteration. Only saved when all ODTR templates are
>> +		 * valid so spinand_configure_phy() knows ranked fallback is
>> +		 * available.
>> +		 */
>> +		if (spinand->odtr_op_templates.read_cache &&
>> +		    spinand->odtr_op_templates.write_cache &&
>> +		    spinand->odtr_op_templates.update_cache)
>> +			spinand->phy_read_variants = info->op_variants.read_cache;
>> +
>>   		return 0;
>>   	}
>>   
>> @@ -1922,7 +1941,6 @@ static int spinand_mtd_suspend(struct mtd_info *mtd)
>>   
>>   static int spinand_init(struct spinand_device *spinand)
>>   {
>> -	struct device *dev = &spinand->spimem->spi->dev;
>>   	struct mtd_info *mtd = spinand_to_mtd(spinand);
>>   	struct nand_device *nand = mtd_to_nanddev(mtd);
>>   	int ret;
>> @@ -2014,14 +2032,6 @@ static int spinand_init(struct spinand_device *spinand)
>>   	mtd->ecc_step_size = nanddev_get_ecc_conf(nand)->step_size;
>>   	mtd->bitflip_threshold = DIV_ROUND_UP(mtd->ecc_strength * 3, 4);
>>   
>> -	ret = spinand_create_dirmaps(spinand);
>> -	if (ret) {
>> -		dev_err(dev,
>> -			"Failed to create direct mappings for read/write operations (err = %d)\n",
>> -			ret);
>> -		goto err_cleanup_ecc_engine;
>> -	}
>> -
>>   	return 0;
>>   
>>   err_cleanup_ecc_engine:
>> @@ -2050,6 +2060,178 @@ static void spinand_cleanup(struct spinand_device *spinand)
>>   	kfree(spinand->scratchbuf);
>>   }
>>   
>> +/*
>> + * spinand_try_phy_ranked() - Try PHY tuning on variants in performance order.
>> + * @spinand:    SPI NAND device
>> + * @mem:        SPI memory device
>> + * @odtr:       true to iterate ODTR variants, false for SSDR variants
>> + * @tried_mask: bitmask of already-tried variant indices; updated on each try
>> + *
>> + * Iterates the full read variant list in descending performance order,
>> + * skipping variants in @tried_mask, and calls execute_tuning on each until one
>> + * succeeds. The fastest PHY-tunable variant is chosen regardless of which was
>> + * pre-selected at init; ranked iteration finds the best available variant
>> + * without re-trying already-attempted ones.
>> + *
>> + * On success, sets spinand->max_read_op and updates the matching
>> + * odtr_op_templates.read_cache or ssdr_op_templates.read_cache.
>> + */
>> +static bool spinand_try_phy_ranked(struct spinand_device *spinand,
>> +				   struct spi_mem *mem, bool odtr,
>> +				   u32 *tried_mask)
>> +{
>> +	const struct spinand_op_variants *variants = spinand->phy_read_variants;
>> +	const struct spi_mem_op *best;
>> +	int ret;
>> +
>> +	if (!variants)
>> +		return false;
>> +
>> +	while ((best = spinand_op_find_best(spinand, variants, odtr,
>> +					    *tried_mask))) {
> 
> Ok, now I get the mask.
> 
> Why do you care about odtr flag at all? We should just take the fastest
> modes in descending order, I don't really understand why mentioning odtr
> is needed? Maybe your goal is to make a faster loop, taking the fastest
> mode in each case and compare? This should work in most cases, given the
> fact that we tend to order the variants by speed, but they are not
> always in perfect order. I already faced cases where faster modes
> appeared to be down the list because of the game of dummy cycles. But
> that is probably not impacting PHY mode since we are using the fastest
> speed (with only the mode with the maximum number of dummy cycles)
> usable.
> 
> So in the end, if you sort ODTR vs. SSDR for optimization reasons, ok.
> 
>> +		*tried_mask |= BIT(best - variants->ops);
>> +		spinand->max_read_op = *best;
>> +		spinand->max_read_op.max_freq = 0;
>> +		ret = spi_mem_execute_tuning(mem, &spinand->max_read_op,
>> +					     &spinand->max_write_op);
>> +		if (ret && ret != -EOPNOTSUPP)
>> +			dev_warn(&mem->spi->dev, "%s PHY tuning failed: %d\n",
>> +				 odtr ? "ODTR" : "SSDR", ret);
>> +		if (!ret && spinand->max_read_op.max_freq) {
>> +			if (odtr)
>> +				spinand->odtr_op_templates.read_cache = best;
>> +			else
>> +				spinand->ssdr_op_templates.read_cache = best;
>> +			return true;
>> +		}
>> +	}
>> +	return false;
>> +}
>> +
>> +/*
>> + * spinand_reset_max_ops() - Copy op templates and zero max_freq on both.
>> + * @spinand:    SPI NAND device
>> + * @templates:  op template set to copy from
>> + *
>> + * Called before execute_tuning so max_freq starts at zero; execute_tuning sets
>> + * it to the validated clock rate only on success. A non-zero max_freq means
>> + * PHY-validated; zero means the base rate applies.
>> + */
>> +static void spinand_reset_max_ops(struct spinand_device *spinand,
>> +				  struct spinand_mem_ops *templates)
>> +{
>> +	spinand->max_read_op = *templates->read_cache;
>> +	spinand->max_read_op.max_freq = 0;
>> +	spinand->max_write_op = *templates->write_cache;
>> +	spinand->max_write_op.max_freq = 0;
>> +}
>> +
>> +/*
>> + * spinand_configure_phy() - Negotiate the optimal PHY operating point.
>> + * @spinand:    SPI NAND device
>> + * @mem:        SPI memory device
>> + *
>> + * Tries the pre-selected variant first.  If the controller signals that
>> + * PHY tuning is not applicable for that specific op, iterates all remaining
>> + * variants in performance order.  For devices that support both DTR and SDR
>> + * interfaces, DTR variants are tried first; if all fail the device is
>> + * switched to SDR mode and SDR variants are tried.  On full failure the
>> + * device falls back to the best available non-PHY mode.  Devices that
>> + * support only SDR skip the DTR ranked pass entirely.
>> + *
>> + * PHY failure is never fatal.
>> + *
>> + * Note: tried_mask is u32, supporting up to 32 variants total across both
>> + * ODTR and SSDR. Flash devices with more than 32 read variants are not
>> + * supported.
>> + */
>> +static void spinand_configure_phy(struct spinand_device *spinand,
>> +				  struct spi_mem *mem)
>> +{
>> +	u32 tried_mask;
>> +	int ret;
>> +
>> +	spinand_reset_max_ops(spinand, spinand->op_templates);
>> +
>> +	ret = spi_mem_execute_tuning(mem, &spinand->max_read_op,
>> +				     &spinand->max_write_op);
>> +	if (ret && ret != -EOPNOTSUPP)
>> +		dev_warn(&mem->spi->dev, "Failed to execute PHY tuning: %d\n",
>> +			 ret);
> 
> I guess we don't want to warn in this case. You can keep it as a dbg log
> if you want.

...

> 
>> +	/*
>> +	 * Any non-zero return or a set max_freq means we are done (error,
>> +	 * unsupported, or success). Fallback only for the op-specific "skip"
>> +	 * signal: ret == 0 with max_freq still 0.
>> +	 */
>> +	if (ret || spinand->max_read_op.max_freq)
>> +		return;
>> +
>> +	if (!mem->spi->post_config_max_speed_hz || spinand->bus_iface == SSDR ||
>> +	    !spinand->phy_read_variants)
>> +		return;
> 
> Why not moving this earlier? (except the SSDR check) this way we return
> even earlier if the tuning cannot happen.
> 
> I am not sure I understand your use of phy_read_variants here. Can it
> really be NULL?

I had a look at it, you're right - it can't be. Removing the check in v5.

> 
>> +	if (WARN_ON(spinand->phy_read_variants->nops > 32))
>> +		return;
>> +
>> +	/* Mark the pre-selected ODTR variant as already tried */
>> +	tried_mask = BIT(spinand->odtr_op_templates.read_cache -
>> +			 spinand->phy_read_variants->ops);
>> +
>> +	dev_dbg(&mem->spi->dev,
>> +		"PHY tuning skipped for current op; searching for best PHY variant\n");
>> +
>> +	/* Pass 1: try all remaining ODTR variants in performance order */
>> +	if (spinand_try_phy_ranked(spinand, mem, true, &tried_mask))
>> +		return;
>> +
>> +	/*
>> +	 * Pass 2: switch to SSDR and try all SSDR variants in performance
>> +	 * order.
>> +	 *
>> +	 * Only enter if we actually have SSDR support and a reconfigure
>> +	 * callback.
> 
> It is not possible to enable ODTR without the configure callback.
> 
>> The hardware is still in ODTR mode here so no
>> +	 * configure_chip call is needed to undo; just set up the ODTR non-PHY
>> +	 * fallback and return.
>> +	 */
>> +	if (!spinand->ssdr_op_templates.read_cache ||
>> +	    !spinand->ssdr_op_templates.write_cache ||
> 
> I don't think this is ever possible, is it?
> 
>> +	    !spinand->configure_chip)
>> +		goto use_odtr_non_phy;
> 
> I don't think we will ever use this path.
> 
>> +
>> +	if (spinand->configure_chip(spinand, SSDR))
>> +		goto use_odtr_non_phy;
> 
> We should handle chips with no ODTR support. I'm not sure the logic is
> robust enough for them. Have you tried by commenting out the ODTR
> variants of your chip?

Didn't try this, but yeah current code may not be robust for non-ODTR
flashes. I'll look into this and make changes accordingly in v5.

> 
>> +
>> +	spinand->op_templates = &spinand->ssdr_op_templates;
>> +	spinand->bus_iface = SSDR;
>> +	spinand->max_write_op = *spinand->ssdr_op_templates.write_cache;
>> +	spinand->max_write_op.max_freq = 0;
>> +
>> +	/*
>> +	 * Only ODTR variants were candidates in Pass 1; SSDR bit positions
>> +	 * are clear
>> +	 */
>> +	if (spinand_try_phy_ranked(spinand, mem, false, &tried_mask))
>> +		return;
>> +
>> +	/*
>> +	 * All PHY attempts exhausted.  Revert to ODTR for non-PHY DTR
>> +	 * operation.  If revert fails, stay in SSDR — a mode mismatch
>> +	 * (ODTR op templates on SSDR-mode device) would corrupt data.
>> +	 */
>> +	if (spinand->configure_chip(spinand, ODTR)) {
>> +		dev_warn(&mem->spi->dev,
>> +			 "Failed to revert to ODTR, staying in SSDR non-PHY\n");
>> +		spinand_reset_max_ops(spinand, &spinand->ssdr_op_templates);
>> +		return;
>> +	}
>> +
>> +use_odtr_non_phy:
>> +	spinand->op_templates = &spinand->odtr_op_templates;
>> +	spinand->bus_iface = ODTR;
>> +	spinand_reset_max_ops(spinand, &spinand->odtr_op_templates);
>> +}
> 
>>   static int spinand_probe(struct spi_mem *mem)
>>   {
>>   	struct spinand_device *spinand;
>> @@ -2072,6 +2254,20 @@ static int spinand_probe(struct spi_mem *mem)
>>   	if (ret)
>>   		return ret;
>>   
>> +	/*
>> +	 * Negotiate the best PHY operating point before creating dirmaps so
>> +	 * the validated frequency is available at dirmap construction time.
>> +	 */
>> +	spinand_configure_phy(spinand, mem);
>> +
>> +	ret = spinand_create_dirmaps(spinand);
>> +	if (ret) {
>> +		dev_err(&mem->spi->dev,
>> +			"Failed to create direct mappings for read/write operations (err = %d)\n",
>> +			ret);
>> +		goto err_spinand_cleanup;
>> +	}
>> +
>>   	ret = mtd_device_register(mtd, NULL, 0);
>>   	if (ret)
>>   		goto err_spinand_cleanup;
>> diff --git a/include/linux/mtd/spinand.h b/include/linux/mtd/spinand.h
>> index ec6efcfeef83..50a8319cf11e 100644
>> --- a/include/linux/mtd/spinand.h
>> +++ b/include/linux/mtd/spinand.h
>> @@ -791,8 +791,19 @@ struct spinand_device {
>>   	struct spinand_mem_ops *op_templates;
>>   	enum spinand_bus_interface bus_iface;
>>   
>> +	/*
>> +	 * Full read variant list (ODTR and SSDR ops together), saved when ODTR
>> +	 * templates are valid. Used by spinand_configure_phy() to iterate all
>> +	 * candidates when the pre-selected variant cannot be PHY-tuned.
>> +	 */
>> +	const struct spinand_op_variants *phy_read_variants;
> 
> Why do you prefix this pointer name with "phy"? The variants pointed are
> basically all read variants, no more no less. Even though you want to
> use them for PHY tuning purposes, I don't think it is wise to name the
> member here.

Agreed, will rename it in v5. Also, will keep other suggestions in mind
before preparing v5.

Thanks,
Santhosh.

> 
>> +
>>   	struct spinand_dirmap *dirmaps;
>>   
>> +	/* Persistent op templates updated by execute_tuning with validated speed. */
>> +	struct spi_mem_op max_read_op;
>> +	struct spi_mem_op max_write_op;
>> +
>>   	int (*select_target)(struct spinand_device *spinand,
>>   			     unsigned int target);
>>   	unsigned int cur_target;
> 
> Thanks,
> Miquèl


  reply	other threads:[~2026-07-16  6:03 UTC|newest]

Thread overview: 50+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-18  7:37 [PATCH v4 00/16] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 01/16] spi: dt-bindings: add spi-max-post-config-frequency property Santhosh Kumar K
2026-06-18 16:36   ` Conor Dooley
2026-06-22  9:14   ` Krzysztof Kozlowski
2026-06-22 19:46     ` Conor Dooley
2026-06-29 15:41     ` Miquel Raynal
2026-07-16  5:52       ` Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 02/16] spi: dt-bindings: add spi-phy-pattern-partition property Santhosh Kumar K
2026-06-18  7:50   ` sashiko-bot
2026-06-22  9:17   ` Krzysztof Kozlowski
2026-06-22 17:11     ` Miquel Raynal
2026-07-16  5:54       ` Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 03/16] spi: parse spi-max-post-config-frequency into post_config_max_speed_hz Santhosh Kumar K
2026-06-18  7:54   ` sashiko-bot
2026-06-29 15:43   ` Miquel Raynal
2026-06-18  7:37 ` [PATCH v4 04/16] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
2026-06-18  8:02   ` sashiko-bot
2026-07-02 13:32   ` Miquel Raynal
2026-06-18  7:37 ` [PATCH v4 05/16] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning() Santhosh Kumar K
2026-06-18  7:57   ` sashiko-bot
2026-06-18  7:37 ` [PATCH v4 06/16] spi: cadence-quadspi: move cqspi_readdata_capture earlier Santhosh Kumar K
2026-06-18  7:48   ` sashiko-bot
2026-06-18  7:37 ` [PATCH v4 07/16] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 08/16] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-06-18  7:59   ` sashiko-bot
2026-06-19 17:33   ` Mark Brown
2026-07-16  5:56     ` Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 09/16] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383) Santhosh Kumar K
2026-06-18  8:04   ` sashiko-bot
2026-07-02 13:35   ` Miquel Raynal
2026-07-16  5:58     ` Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 10/16] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
2026-06-18  7:57   ` sashiko-bot
2026-06-18  7:37 ` [PATCH v4 11/16] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
2026-06-18  7:53   ` sashiko-bot
2026-06-18  7:37 ` [PATCH v4 12/16] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
2026-06-18  7:53   ` sashiko-bot
2026-06-18  7:37 ` [PATCH v4 13/16] mtd: spinand: extract variant ranking logic into spinand_op_find_best() Santhosh Kumar K
2026-07-02 13:41   ` Miquel Raynal
2026-07-16  6:00     ` Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 14/16] mtd: spinand: negotiate optimal PHY operating point before dirmap creation Santhosh Kumar K
2026-06-18  8:02   ` sashiko-bot
2026-07-02 15:08   ` Miquel Raynal
2026-07-16  6:02     ` Santhosh Kumar K [this message]
2026-06-18  7:37 ` [PATCH v4 15/16] mtd: spi-nor: extract read op template construction into helper Santhosh Kumar K
2026-06-18  7:37 ` [PATCH v4 16/16] mtd: spi-nor: run PHY tuning after init and update dirmap frequency Santhosh Kumar K
2026-06-18  8:01   ` sashiko-bot
2026-06-22  4:30 ` [PATCH v4 00/16] spi: cadence-quadspi: add PHY tuning support Mahapatra, Amit Kumar
2026-07-16  6:04   ` Santhosh Kumar K
2026-07-16 13:57     ` Michal Simek

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=1ab838c8-ffd8-488f-b22b-6548723829ef@ti.com \
    --to=s-k6@ti.com \
    --cc=a-dutta@ti.com \
    --cc=broonie@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mtd@lists.infradead.org \
    --cc=linux-spi@vger.kernel.org \
    --cc=miquel.raynal@bootlin.com \
    --cc=mwalle@kernel.org \
    --cc=praneeth@ti.com \
    --cc=pratyush@kernel.org \
    --cc=richard@nod.at \
    --cc=robh@kernel.org \
    --cc=takahiro.kuwano@infineon.com \
    --cc=u-kumar1@ti.com \
    --cc=vigneshr@ti.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