Linux-mtd Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Santhosh Kumar K" <s-k6@ti.com>,
	broonie@kernel.org, robh@kernel.org, krzk+dt@kernel.org,
	conor+dt@kernel.org, miquel.raynal@bootlin.com, richard@nod.at,
	vigneshr@ti.com, pratyush@kernel.org, mwalle@kernel.org,
	takahiro.kuwano@infineon.com
Cc: devicetree@vger.kernel.org, linux-mtd@lists.infradead.org,
	praneeth@ti.com, sebin.francis@ti.com, u-kumar1@ti.com,
	srikanth.boyapally@amd.com, m-shah@ti.com
Subject: Re: [PATCH v11 16/19] mtd: spinand: negotiate optimal controller operating point before dirmap creation
Date: Thu, 05 Nov 2026 11:31:16 +0000	[thread overview]
Message-ID: <sashiko-outbox-198953@kernel.org> (raw)
In-Reply-To: <20261105105832.2134313-17-s-k6@ti.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] mtd: spinand: spinand_create_dirmap() overwrites per-op max_freq limits
- [High] mtd: spinand: missing configure_chip() call in spinand_try_ranked_variant()
- [High] mtd: spinand: spinand_op_find_best_variant() ranks and overclocks low-freq ops
- [Medium] mtd: spinand: broken cont_read_cache matching in spinand_op_shape_matches()
- [Medium] mtd: spinand: missing cont_read_cache shape check on initial tuning success
--

commit 8733b92e8d67a304d901c8ece339d2fe4c54f7ad
Author: Santhosh Kumar K <s-k6@ti.com>

mtd: spinand: negotiate optimal controller operating point before dirmap creation

This commit introduces spinand_optimize_controller() to perform PHY
calibration and ranked variant fallback before creating direct mappings in
spinand_probe(). It also re-pairs continuous-read templates by protocol shape
when falling back to another read variant and embeds the calibrated max_freq
into the dirmap operation templates.

> diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
> index 9dc43c8c4162d..645528b6c2d77 100644
> --- a/drivers/mtd/nand/spi/core.c
> +++ b/drivers/mtd/nand/spi/core.c
> @@ -1300,6 +1300,7 @@ static int spinand_create_dirmap(struct spinand_device *spinand,
>  	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))
> @@ -1310,9 +1311,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;
>  	}

[Severity: High]
Does unconditionally overwriting info.primary_op_tmpl.max_freq and
info.secondary_op_tmpl.max_freq in spinand_create_dirmap() discard the
per-variant datasheet frequency limits defined by flash drivers?

When mem->spi->post_config_max_speed_hz is 0, or when
spi_mem_execute_tuning() is unsupported (-EOPNOTSUPP) or fails in
spinand_optimize_controller(), spinand->max_read_op.max_freq and
spinand->max_write_op.max_freq remain 0.

In spinand_create_dirmap(), info.primary_op_tmpl and info.secondary_op_tmpl
are first initialized from *spinand->op_templates->update_cache,
*spinand->op_templates->read_cache, and
*spinand->op_templates->cont_read_cache, which carry per-variant frequency
caps in drivers such as drivers/mtd/nand/spi/winbond.c (e.g., 54 MHz,
80 MHz, 86 MHz, 104 MHz, 105 MHz, 120 MHz, 124 MHz, 133 MHz, and 162 MHz).

Overwriting max_freq with 0 causes spi_mem_adjust_op_freq() during
dirmap creation:

drivers/spi/spi-mem.c:spi_mem_adjust_op_freq() {
    ...
	if (!op->max_freq || op->max_freq > mem->spi->max_speed_hz)
		op->max_freq = mem->spi->max_speed_hz;
    ...
}

to raise the operation frequency all the way up to mem->spi->max_speed_hz
(for instance, clocking an 80 MHz 1S-4D-4D or 120 MHz 8D-8D-8D op at
166 MHz). Even when tuning succeeds, stamping spinand->max_read_op.max_freq
onto the dirmap templates ignores any lower per-variant max_freq ceiling on
read_cache or cont_read_cache.

> @@ -1559,22 +1562,51 @@ static int spinand_init_odtr_instruction_set(struct spinand_device *spinand)
>  	return 0;
>  }
>  
> +/**
> + * spinand_op_shape_matches() - Check whether two ops share the same protocol
> + *				 shape.
> + * @a: first op
> + * @b: second op
> + *
> + * Compares cmd/addr/data buswidth, addr.nbytes, and DTR-ness, ignoring
> + * dummy cycle count and address value. A continuous-read op and its
> + * corresponding read_cache op are expected to differ only in those ignored
> + * fields: continuous read typically needs fewer dummy cycles, and always
> + * targets an internally tracked address rather than the caller's.
> + */
> +static bool spinand_op_shape_matches(const struct spi_mem_op *a,
> +				     const struct spi_mem_op *b)
> +{
> +	return a->cmd.buswidth == b->cmd.buswidth &&
> +	       a->cmd.dtr == b->cmd.dtr &&
> +	       a->addr.nbytes == b->addr.nbytes &&
> +	       a->addr.buswidth == b->addr.buswidth &&
> +	       a->addr.dtr == b->addr.dtr &&
> +	       a->data.buswidth == b->data.buswidth &&
> +	       a->data.dtr == b->data.dtr;
> +}

[Severity: Medium]
As reported by Sashiko on v4 [1], can spinand_op_shape_matches() ever match
an SSDR continuous-read variant in spinand_find_cont_read_variant(), or pair
the right dummy-cycle sub-variant in ODTR mode?

In drivers/mtd/nand/spi/winbond.c (the only driver defining cont_read_cache
variant tables), all SSDR read_cache variants use a 2-byte address
(addr.nbytes == 2, dummy.nbytes == ndummy), whereas all SSDR continuous-read
variants convert one address byte into dummy cycles:

#define WINBOND_CONT_READ_FROM_CACHE_1S_8S_8S_OP(ndummy, buf, len, freq) \
	SPI_MEM_OP(SPI_MEM_OP_CMD(0xcb, 1),				\
		   SPI_MEM_OP_ADDR(1, 0, 8),				\
		   SPI_MEM_OP_DUMMY(ndummy - 1, 8),			\
		   SPI_MEM_OP_DATA_IN(len, buf, 8),			\
		   SPI_MEM_OP_MAX_FREQ(freq))

Because spinand_op_shape_matches() requires a->addr.nbytes == b->addr.nbytes,
spinand_find_cont_read_variant(spinand, best, SSDR) rejects every SSDR entry
and always returns NULL, which causes spinand_try_ranked_variant() to set
spinand->cont_read_possible = false on every SSDR fallback.

Conversely, for ODTR variants where WINBOND_CONT_READ_FROM_CACHE_8D_8D_8D_OP
also has addr.nbytes == 2, spinand_op_shape_matches() ignores dummy cycle
counts and spinand_find_cont_read_variant() passes skip_mask == 0 to
spinand_op_find_best_variant(). If read_cache falls back to a different
dummy-cycle sub-variant, won't spinand_find_cont_read_variant() still pick
the first cont_read_cache sub-variant with a mismatched dummy cycle count,
even though both ops share a single hardware VCR dummy clock register?

[ ... ]

> @@ -2073,6 +2144,199 @@ static void spinand_cleanup(struct spinand_device *spinand)
>  	kfree(spinand->scratchbuf);
>  }
>  
> +/**
> + * spinand_try_ranked_variant() - Try controller optimization on variants in
> + *				  performance order.
[ ... ]
> +static bool spinand_try_ranked_variant(struct spinand_device *spinand,
> +				       struct spi_mem *mem,
> +				       enum spinand_bus_interface iface,
> +				       u32 *tried_mask)
> +{
> +	const struct spinand_op_variants *variants = spinand->all_read_variants;
> +	struct spinand_mem_ops *templates = iface == ODTR ?
> +		&spinand->odtr_op_templates : &spinand->ssdr_op_templates;
> +	const struct spi_mem_op *best;
> +	int ret;
> +
> +	if (!variants)
> +		return false;
> +
> +	while ((best = spinand_op_find_best_variant(spinand, variants, iface,
> +						    *tried_mask, NULL))) {
> +		*tried_mask |= BIT(best - variants->ops);
> +		spinand->max_read_op = *best;
> +		spinand->max_read_op.max_freq = 0;
> +		spinand->max_write_op.max_freq = 0;
> +		ret = spi_mem_execute_tuning(mem, &spinand->max_read_op,
> +					     &spinand->max_write_op);
> +		if (ret && ret != -EOPNOTSUPP)
> +			dev_dbg(&mem->spi->dev, "%s optimization failed: %d\n",
> +				iface == ODTR ? "ODTR" : "SSDR", ret);
> +		if (!ret && spinand->max_read_op.max_freq) {
> +			templates->read_cache = best;
> +			if (spinand->all_cont_read_variants) {
> +				templates->cont_read_cache =
> +					spinand_find_cont_read_variant(spinand, best, iface);
> +				/* Only clear; never re-enable a denied capability. */
> +				if (!templates->cont_read_cache)
> +					spinand->cont_read_possible = false;
> +			}
> +			return true;
> +		}
> +	}
> +	return false;
> +}

[Severity: High]
Doesn't spinand_try_ranked_variant() need to update templates->read_cache and
call spinand->configure_chip(spinand, iface) for each candidate variant
before running spi_mem_execute_tuning() (and restore it if all candidates
fail)?

On ODTR-capable Winbond flashes (W35N01JW, W35N02JW, and W35N04JW),
spinand->configure_chip (w35n0xjw_vcr_cfg() in drivers/mtd/nand/spi/winbond.c)
inspects spinand->odtr_op_templates.read_cache or
spinand->ssdr_op_templates.read_cache to program the flash chip's Volatile
Configuration Registers with that variant's exact dummy clock count
(W35N01JW_VCR_DUMMY_CLOCK_REG) and I/O mode (W35N01JW_VCR_IO_MODE_REG:
OCTAL_DDR, OCTAL_SDR, or SINGLE_SDR):

drivers/mtd/nand/spi/winbond.c:w35n0xjw_vcr_cfg() {
    ...
	switch (iface) {
	case SSDR:
		ref_op = spinand->ssdr_op_templates.read_cache;
		break;
	case ODTR:
		ref_op = spinand->odtr_op_templates.read_cache;
		break;
	default:
		return -EOPNOTSUPP;
	}

	dummy_cycles = ((ref_op->dummy.nbytes * 8) / ref_op->dummy.buswidth) /
		(ref_op->dummy.dtr ? 2 : 1);
    ...
}

In spinand_optimize_controller(), configure_chip() is only called once before
Pass 1 (during spinand_init_flash()) and once before Pass 2 using the initial
read_cache template. When spinand_try_ranked_variant() iterates across
variants in read_cache_octal_variants that have different dummy cycle counts
(e.g., 24 vs 16 dummy bytes in ODTR, or 20, 16, 12, and 8 dummy cycles in
SSDR) or a different VCR I/O mode (1S-8S-8S OCTAL_SDR vs 1S-1S-1S
SINGLE_SDR), the flash hardware remains programmed for the initial template's
dummy cycles and I/O mode both during spi_mem_execute_tuning() and after
templates->read_cache = best is committed.

> +
> +/**
> + * spinand_reset_max_freq_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
> + * controller-optimized; zero means the base rate applies.
> + */
> +static void spinand_reset_max_freq_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;
> +}

[Severity: High]
As reported by Miquel Raynal on v3 [2], won't evaluating variants at the
pre-config base clock in spinand_op_find_best_variant() and then zeroing
max_read_op.max_freq before spi_mem_execute_tuning() select low-dummy-cycle,
low-frequency variants and overclock them at post_config_max_speed_hz?

When spi-max-post-config-frequency-hz is set in DT, mem->spi->max_speed_hz
holds the lower pre-configuration base clock (e.g., 25 MHz) while
mem->spi->post_config_max_speed_hz holds the high-speed target (e.g., 120 MHz
or 166 MHz).

In spinand_op_find_best_variant(), spi_mem_adjust_op_freq() clamps every
candidate in variants->ops[i] (such as Winbond's read_cache_octal_variants:
8D-8D-8D with 24 dummy bytes @ 120 MHz vs 16 dummy bytes @ 86 MHz, or
1S-8S-8S with 20 dummy bytes @ unlimited, 16 @ 162 MHz, 12 @ 124 MHz, and
8 @ 86 MHz) down to the 25 MHz base rate in mem->spi->max_speed_hz.

At the same 25 MHz frequency, same-buswidth ops differ only in dummy cycle
count, so spi_mem_calc_op_duration() ranks the variant with the fewest dummy
cycles and lowest frequency limit (e.g., 86 MHz) as the fastest variant, and
ranks the 20-dummy-cycle 166 MHz variant last.

Then spinand_reset_max_freq_ops() and spinand_try_ranked_variant() zero
spinand->max_read_op.max_freq = 0, discarding the 86 MHz limit and allowing
spi_mem_execute_tuning() to set max_read_op.max_freq to
post_config_max_speed_hz (120 MHz or 166 MHz), violating the flash datasheet's
minimum dummy cycles for that clock rate.

> +
> +/**
> + * spinand_optimize_controller() - Negotiate the optimal controller operating
> + *				   point for the SPI NAND device.
[ ... ]
> +static void spinand_optimize_controller(struct spinand_device *spinand,
> +					struct spi_mem *mem)
> +{
> +	u32 tried_mask;
> +	int ret;
> +
> +	/* Skip entirely when no post-config target is configured. */
> +	if (!mem->spi->post_config_max_speed_hz)
> +		return;
> +
> +	spinand_reset_max_freq_ops(spinand, spinand->op_templates);
> +
> +	ret = spi_mem_execute_tuning(mem, &spinand->max_read_op,
> +				     &spinand->max_write_op);
> +	if (ret && ret != -EOPNOTSUPP)
> +		dev_dbg(&mem->spi->dev, "Controller optimization failed: %d\n",
> +			ret);
> +
> +	/*
> +	 * 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;

[Severity: Medium]
As reported by Sashiko on v3 [3], if the initial spi_mem_execute_tuning()
call succeeds here, doesn't spinand_optimize_controller() return without
verifying that spinand->op_templates->cont_read_cache matches the protocol
shape of spinand->op_templates->read_cache?

In spinand_match_and_init(), cont_read_cache is selected independently from
info->op_variants.cont_read_cache without checking addr.nbytes equality
against read_cache. When the initial tuning succeeds, spinand_create_dirmap()
stamps spinand->max_read_op.max_freq onto info.secondary_op_tmpl even when
cont_read_cache has a different addr.nbytes (e.g., 1 address byte instead of
2 address bytes on Winbond SSDR variants) or a different protocol shape that
was never validated by spinand_op_shape_matches().

[1] https://lore.kernel.org/r/20260618080209.5701E1F000E9@smtp.kernel.org
[2] https://lore.kernel.org/r/87mrxjg9zi.fsf@bootlin.com
[3] https://lore.kernel.org/r/20260527190440.9DB221F000E9@smtp.kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261105105832.2134313-1-s-k6@ti.com?part=16

______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

  reply	other threads:[~2026-11-05 11:31 UTC|newest]

Thread overview: 29+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 10:58 [PATCH v11 00/19] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 01/19] spi: dt-bindings: add spi-max-post-config-frequency-hz property Santhosh Kumar K
2026-11-05 16:41   ` Krzysztof Kozlowski
2026-11-05 16:42     ` Krzysztof Kozlowski
2026-11-05 10:58 ` [PATCH v11 02/19] spi: dt-bindings: add spi-phy-pattern-partition property Santhosh Kumar K
2026-11-05 16:42   ` Krzysztof Kozlowski
2026-11-05 10:58 ` [PATCH v11 03/19] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz Santhosh Kumar K
2026-11-05 11:18   ` sashiko-bot
2026-11-05 10:58 ` [PATCH v11 04/19] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
2026-11-05 11:24   ` sashiko-bot
2026-11-05 10:58 ` [PATCH v11 05/19] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning() Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 06/19] spi: cadence-quadspi: move cqspi_readdata_capture earlier Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 07/19] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 08/19] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-11-05 11:22   ` sashiko-bot
2026-11-05 10:58 ` [PATCH v11 09/19] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383) Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 10/19] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 11/19] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
2026-11-05 11:27   ` sashiko-bot
2026-11-05 10:58 ` [PATCH v11 12/19] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
2026-11-05 11:21   ` sashiko-bot
2026-11-05 10:58 ` [PATCH v11 13/19] spi: cadence-quadspi: reprogram CS timing on every chip-select switch Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 14/19] spi: cadence-quadspi: reprogram PHY DLL on runtime resume Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 15/19] mtd: spinand: extract variant ranking logic into spinand_op_find_best_variant() Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 16/19] mtd: spinand: negotiate optimal controller operating point before dirmap creation Santhosh Kumar K
2026-11-05 11:31   ` sashiko-bot [this message]
2026-11-05 10:58 ` [PATCH v11 17/19] mtd: spi-nor: extract read op template construction into helper Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 18/19] mtd: spi-nor: run controller optimization before dirmap creation Santhosh Kumar K
2026-11-05 10:58 ` [PATCH v11 19/19] mtd: spi-nor: reset max_read_op frequency for security register reads Santhosh Kumar K

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=sashiko-outbox-198953@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=broonie@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-mtd@lists.infradead.org \
    --cc=m-shah@ti.com \
    --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=s-k6@ti.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=sebin.francis@ti.com \
    --cc=srikanth.boyapally@amd.com \
    --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