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
next prev parent 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