[PATCH v7 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation

From: Santhosh Kumar K

Date: Tue Aug 11 2026 - 14:41:54 EST


Add spinand_optimize_controller() which calls spi_mem_execute_tuning()
to negotiate the best operating point for the SPI NAND device before
creating dirmaps. The validated max_freq is then embedded into the
dirmap op templates so the controller uses the optimised frequency for
all subsequent page I/O.

The function tries the pre-selected op variant first. If the controller
signals that optimisation is not applicable for that specific op
(ret == 0 with max_freq still zero), spinand_try_ranked_variant()
iterates the remaining variants in descending performance order. For
devices supporting both ODTR and SSDR interfaces, ODTR variants are
tried first; if all ODTR variants fail, the bus is switched to SSDR and
SSDR variants are tried. On full failure the device reverts to ODTR
non-optimised mode.

Add spinand_reset_max_freq_ops() to copy op templates with max_freq
zeroed before each execute_tuning call, enforcing the invariant that a
non-zero max_freq only results from a successful operation.

Optimisation failure is never fatal; the device operates at the
conservative base rate.

When optimisation permanently selects SSDR (either because a ranked SSDR
variant succeeds or because reverting to ODTR after exhausting all
variants fails), clear the odtr_op_templates pointers. This prevents
spinand_configure_chip() on resume from re-entering ODTR mode with
dirmaps that were built for SSDR, which would cause data corruption.
Reusing the existing NULL-check mechanism avoids the need for a separate
flag and is consistent with how spinand_configure_chip() itself handles
ODTR configure_chip() failure.

Move spinand_create_dirmaps() from spinand_init() to spinand_probe() so
the validated frequency is available at dirmap construction time.

Signed-off-by: Santhosh Kumar K <s-k6@xxxxxx>
---
drivers/mtd/nand/spi/core.c | 221 ++++++++++++++++++++++++++++++++++--
include/linux/mtd/spinand.h | 11 ++
2 files changed, 223 insertions(+), 9 deletions(-)

diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 7c3341f1fca0..dfc9d0871e91 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -1284,6 +1284,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))
@@ -1294,9 +1295,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))
@@ -1745,6 +1748,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
+ * ranked controller optimization. Only saved when all ODTR
+ * templates are valid; spinand_optimize_controller() uses this
+ * to fall back to the next-best variant when needed.
+ */
+ if (spinand->odtr_op_templates.read_cache &&
+ spinand->odtr_op_templates.write_cache &&
+ spinand->odtr_op_templates.update_cache)
+ spinand->all_read_variants = info->op_variants.read_cache;
+
return 0;
}

@@ -1923,7 +1937,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;
@@ -2015,14 +2028,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:
@@ -2051,6 +2056,190 @@ static void spinand_cleanup(struct spinand_device *spinand)
kfree(spinand->scratchbuf);
}

+/*
+ * spinand_try_ranked_variant() - Try controller optimization on variants in
+ * performance order.
+ * @spinand: SPI NAND device
+ * @mem: SPI memory device
+ * @iface: bus interface to iterate (ODTR or SSDR)
+ * @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. 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_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;
+ const struct spi_mem_op *best;
+ int ret;
+
+ if (!variants)
+ return false;
+
+ while ((best = spinand_op_find_best_variant(spinand, variants, iface,
+ *tried_mask))) {
+ *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) {
+ if (iface == ODTR)
+ spinand->odtr_op_templates.read_cache = best;
+ else
+ spinand->ssdr_op_templates.read_cache = best;
+ spinand->cont_read_possible = false;
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * 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;
+}
+
+/*
+ * spinand_optimize_controller() - Negotiate the optimal controller operating
+ * point for the SPI NAND device.
+ * @spinand: SPI NAND device
+ * @mem: SPI memory device
+ *
+ * Tries the pre-selected variant first. If the controller signals that
+ * optimization 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-optimized mode. Devices that
+ * support only SDR skip the DTR ranked pass entirely.
+ *
+ * Optimization 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_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;
+
+ /* SSDR-only devices have no ranked ODTR fallback available. */
+ if (spinand->bus_iface == SSDR || !spinand->all_read_variants)
+ return;
+
+ if (WARN_ON(spinand->all_read_variants->nops > 32))
+ return;
+
+ /* Mark the pre-selected ODTR variant as already tried. */
+ tried_mask = BIT(spinand->odtr_op_templates.read_cache -
+ spinand->all_read_variants->ops);
+
+ dev_dbg(&mem->spi->dev,
+ "Optimization skipped for current op; searching for best variant\n");
+
+ /* Pass 1: try all remaining ODTR variants in performance order. */
+ if (spinand_try_ranked_variant(spinand, mem, ODTR, &tried_mask))
+ return;
+
+ /*
+ * Pass 2: switch to SSDR and try all SSDR variants in performance
+ * order. configure_chip is guaranteed non-NULL here: reaching ODTR
+ * mode requires it.
+ */
+ if (WARN_ON(!spinand->configure_chip))
+ goto use_odtr_fallback;
+
+ if (spinand->configure_chip(spinand, SSDR))
+ goto use_odtr_fallback;
+
+ 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 bits are clear.
+ * Clear ODTR templates on success so spinand_configure_chip() on
+ * resume does not re-enter ODTR with mismatched SSDR dirmaps.
+ */
+ if (spinand_try_ranked_variant(spinand, mem, SSDR, &tried_mask)) {
+ spinand->odtr_op_templates.read_cache = NULL;
+ spinand->odtr_op_templates.write_cache = NULL;
+ spinand->odtr_op_templates.update_cache = NULL;
+ return;
+ }
+
+ /*
+ * All attempts exhausted. Revert to ODTR for non-optimized DTR
+ * operation. If revert fails, stay in SSDR — a mode mismatch
+ * (ODTR op templates on SSDR-mode device) would corrupt data.
+ * Clear ODTR templates in either case to prevent resume from
+ * re-entering ODTR with mismatched SSDR dirmaps.
+ */
+ if (spinand->configure_chip(spinand, ODTR)) {
+ dev_warn(&mem->spi->dev,
+ "Failed to revert to ODTR, staying in SSDR\n");
+ spinand->odtr_op_templates.read_cache = NULL;
+ spinand->odtr_op_templates.write_cache = NULL;
+ spinand->odtr_op_templates.update_cache = NULL;
+ spinand_reset_max_freq_ops(spinand, &spinand->ssdr_op_templates);
+ return;
+ }
+
+use_odtr_fallback:
+ spinand->op_templates = &spinand->odtr_op_templates;
+ spinand->bus_iface = ODTR;
+ spinand_reset_max_freq_ops(spinand, &spinand->odtr_op_templates);
+}
+
static int spinand_probe(struct spi_mem *mem)
{
struct spinand_device *spinand;
@@ -2073,6 +2262,20 @@ static int spinand_probe(struct spi_mem *mem)
if (ret)
return ret;

+ /*
+ * Negotiate the best controller operating point before creating dirmaps
+ * so the validated frequency is available at dirmap construction time.
+ */
+ spinand_optimize_controller(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 5f4c00ae72a7..7eacda949719 100644
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -792,8 +792,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_optimize_controller() for ranked
+ * fallback when the pre-selected variant cannot be controller-optimized.
+ */
+ const struct spinand_op_variants *all_read_variants;
+
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;
--
2.34.1