* [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support
@ 2026-08-25 17:17 Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 01/18] spi: dt-bindings: add spi-max-post-config-frequency-hz property Santhosh Kumar K
` (17 more replies)
0 siblings, 18 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
This series implements PHY tuning support for the Cadence QSPI controller
to enable reliable high-speed operations. Without PHY tuning, controllers
use conservative timing that limits performance. PHY tuning calibrates
RX/TX delay lines to find optimal data capture timing windows, enabling
operation up to the controller's maximum frequency.
Background:
High-speed SPI memory controllers require precise timing calibration for
reliable operation. At higher frequencies, board-to-board variations make
fixed timing parameters inadequate. The Cadence QSPI controller includes
a PHY interface with programmable delay lines (0-127 taps) for RX and TX
paths, but these require runtime calibration to find the valid timing
window.
Approach:
Add SDR/DDR PHY tuning algorithms for the Cadence controller:
SDR Mode Tuning (1D search):
- Searches for two consecutive valid RX delay windows
- Selects the larger window and uses its midpoint for maximum margin
- TX delay fixed at maximum (127) as it's less critical in SDR
DDR Mode Tuning (2D search):
- Finds RX boundaries (rxlow/rxhigh) using TX window sweeps
- Finds TX boundaries (txlow/txhigh) at fixed RX positions
- Defines valid region corners and detects gaps via binary search
- Applies temperature compensation for optimal point selection
- Handles single or dual passing regions with different strategies
Patch description:
Infrastructure (1-5):
- Patch 1: Add spi-max-post-config-frequency-hz to describe maximum
frequency achievable post controller configuration
- Patch 2: Add spi-phy-pattern-partition phandle for
NOR flash PHY tuning pattern location
- Patch 3: Parse spi-max-post-config-frequency-hz in spi.c; adds
spi_device.post_config_max_speed_hz (0 when not set
keeping all existing DT fully compatible)
- Patch 4: Extend spi_mem_adjust_op_freq() with a bypass: if
op->max_freq equals post_config_max_speed_hz, return
immediately leaving op->max_freq unchanged. All other
ops are capped to max_speed_hz. Also require per_op_freq
controller capability for post-config ops in
spi_mem_default_supports_op()
- Patch 5: Add execute_tuning callback to spi_controller_mem_ops and
spi_mem_execute_tuning() wrapper in SPI-MEM core;
serialises access via spi_mem_access_start/end and
rejects GPIO chip selects
Cadence QSPI Implementation (6-14):
- Patch 6: Move cqspi_readdata_capture() earlier (preparatory)
- Patch 7: Add DQS bit to cqspi_readdata_capture() (preparatory)
- Patch 8: Add complete PHY tuning support: DLL management, pattern
verification (NOR via spi-phy-pattern-partition phandle,
NAND via write-to-cache), SDR 1D and DDR 2D search
algorithms with temperature compensation, AM654-specific
execute_tuning entry point
- Patch 9: Reject 2-byte-address DDR operations via a new
CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR quirk flag to work
around AM654 OSPI erratum i2383
- Patch 10: Refactor direct read path for PHY support (preparatory)
- Patch 11: Enable PHY for direct reads at the calibrated frequency
when both transfer boundaries are 16-byte aligned,
falling back to non-PHY DMA otherwise; also reprograms
DLL on chip-select switch when two devices have
different calibrated settings
- Patch 12: Enable PHY for indirect writes of at least
CQSPI_PHY_MIN_INDIRECT_WRITE_LEN bytes
- Patch 13: Reprogram per-device CS timing on any chip-select switch
- Patch 14: Reprogram the PHY DLL (master config and RX/TX taps) on
runtime resume, since the DLL loses its lock when the
OSPI controller is power-gated
MTD core (15-18):
- Patch 15: Extract spinand_select_op_variant() into a shared helper
spinand_op_find_best_variant() with a skip_mask
- Patch 16: Negotiate optimal controller operating point before dirmap
creation; iterate all read variants in performance order
across ODTR and SSDR interfaces; re-pair cont_read_cache after
PHY tuning via protocol-shape matching
- Patch 17: Extract spi_nor_spimem_get_read_op() helper (preparatory)
- Patch 18: Execute controller optimization in spi_nor_probe() before
creating dirmaps
Testing:
This series was tested on TI's
AM62Ax SK with OSPI NAND flash and
AM62Px SK with OSPI NOR flash:
Read throughput:
|----------------------------------------|
| | non-PHY | PHY |
|----------------------------------------|
| OSPI NOR (8D) | 37.5 MB/s | 218 MB/s |
|----------------------------------------|
| OSPI NAND (8S) | 9.2 MB/s | 35.6 MB/s |
|----------------------------------------|
Write throughput:
|----------------------------------------|
| | non-PHY | PHY |
|----------------------------------------|
| OSPI NAND (8S) | 6 MB/s | 9.2 MB/s |
|----------------------------------------|
Test log: https://gist.github.com/santhosh21/fb481d09486d31134a1601e8bd2a5540
Repo: https://github.com/santhosh21/linux/commits/phy_tuning_v9/
Changes in v9:
- Fix off-by-one in DDR PHY TX search loops
- Fix cqspi_tune_phy() to unconditionally execute the disable path
and restore CQSPI_REG_READCAPTURE on enable error rollback
- Fix other, smaller correctness issues raised by sashiko-bot
- Link to v8: https://lore.kernel.org/linux-spi/20260821114822.4040338-1-s-k6@ti.com/
Changes in v8:
- Re-pair cont_read_cache with the selected read_cache variant via
protocol-shape matching after PHY tuning instead of unconditionally
diabling continuous read
- Rebase on v7.2
- Collect tag from Miquel
- Link to v7: https://lore.kernel.org/linux-spi/20260811183313.1550425-1-s-k6@ti.com/
Changes in v7:
- Serialise spi_mem_execute_tuning() with spi_mem_access_start/end and
reject GPIO chip-select configurations
- Fix cqspi_phy_apply_setting() to also write the calibrated
read_delay to CQSPI_REG_READCAPTURE, previously software-only
- Use local op copies for pattern write/verify instead of mutating
the caller's op templates in place
- Fix the READCAPTURE bypass bit in cqspi_tune_phy() to match the
value validated during calibration instead of being hardcoded
- Enable PHY before starting the indirect-write state machine instead
of after; the old order was a guaranteed deadlock
- Add a new patch to reprogram the PHY DLL on runtime resume
- Fix continuous-read and ODTR/SSDR template handling on ranked
fallback in spinand_try_ranked_variant(), preventing data
corruption after a fallback
- Fix other, smaller correctness issues raised by sashiko-bot
- Rebase on v7.2-rc7
- Link to v6: https://lore.kernel.org/linux-spi/20260723110219.3180747-1-s-k6@ti.com/
Changes in v6:
- Add maxItems to spi-max-post-config-frequency-hz property
- Rebase on v7.2-rc4
- Collect tag from Krzysztof
- Link to v5: https://lore.kernel.org/linux-spi/20260717185116.2065505-1-s-k6@ti.com/
Changes in v5:
- Rename spi-max-post-config-frequency to spi-max-post-config-frequency-hz
per DT schema unit suffix convention
- Rename CQSPI_NO_2BYTE_ADDR_PHY_DDR quirk to
CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR for clarity
- Rename SPI NAND identifiers to remove PHY-specific terminology:
spinand_configure_phy() -> spinand_optimize_controller()
spinand_try_phy_ranked() -> spinand_try_ranked_variant()
spinand_reset_max_ops() -> spinand_reset_max_freq_ops()
spinand_op_find_best() -> spinand_op_find_best_variant()
phy_read_variants (field) -> all_read_variants
- Replace bool odtr parameter with enum spinand_bus_interface iface in
spinand_op_find_best_variant() to preserve the existing enumeration
- Replace cqspi_get_phy_pattern_offset() with per-flash probe-time
phandle lookup in cqspi_of_get_flash_pdata(); fixes incorrect pattern
offset selection when multiple flash devices share a controller
- In cqspi_tune_phy(), reprogramme and resync the DLL when the active
device's calibrated settings differ from hardware
- Edit cqspi_phy_apply_setting() to update phy_setting.rx and
phy_setting.tx alongside phy_setting.read_delay
- Reprogram per-device CS timing (CQSPI_REG_DELAY and read-capture register)
on any chip-select switch, not only on clock changes
- Move post_config_max_speed_hz early-return to the top of
spinand_optimize_controller(), before any state is modified
- Remove dead NULL guards on SSDR op templates in Pass 2
- Downgrade tuning-failure log messages from dev_warn to dev_dbg
- Rebase on v7.2-rc3
- Collect tags
- Link to v4: https://lore.kernel.org/linux-spi/20260618073725.84733-1-s-k6@ti.com/
Changes in v4:
- Add spi-max-post-config-frequency instead of extending spi-max-frequency
to accept an optional second value
- Replace spi_mem_apply_base_freq_cap() with spi_mem_adjust_op_freq() extension
- For SPI NOR/NAND, execute PHY tuning before the dirmap creation
- For SPI NAND, execute PHY tuning across all operation variants available,
perform duration comparison, and select the best resulting variant
by taking controller-specific restrictions into account
- Move i2383 check from cqspi_supports_mem_op() to cqspi_am654_ospi_execute_tuning()
- Rename cdns,phy-pattern-partition to spi-phy-pattern-partition,
cqspi_phy_enable to cqspi_tune_phy and f_pdata->use_phy to use_tuned_phy
- Remove redundant spi-max-frequency parsing in driver cqspi_of_get_flash_pdata()
- Extract DMA refactoring into a preparatory patch
- Rebase on v7.1
- Collect tags from Miquel
- Link to v3: https://lore.kernel.org/linux-spi/20260527175527.2247679-1-s-k6@ti.com/
Changes in v3:
- Drop spi-has-dqs DT property; DQS is now enabled automatically when
the selected read operation uses DDR signalling (dtr flags in the op)
- Extend spi-max-frequency to accept an optional second value forming a
[base-freq, max-freq] pair; the presence of two values signals PHY
tuning intent and encodes both the conservative base speed and the
calibration target in one property
- Add base_speed_hz to struct spi_device (spi.c/spi.h) and parse the
two-element array there; single-value DT is fully backward-compatible
- Move frequency enforcement from the cadence driver to core: new
spi_mem_apply_base_freq_cap() called from spi_mem_exec_op() replaces
the per-driver cqspi_op_matches_tuned() and non_phy_clk_rate field
- Propagate the tuned max_freq to dirmap op templates after
execute_tuning() succeeds; store persistent op templates in
spi_nor.max_read_op and spinand.{max_read,max_write}_op so the
frequency writeback survives across the probe call
- Replace NOR pattern partition lookup by name with a
cdns,phy-pattern-partition DT phandle pointing directly to the
partition node
- Add CQSPI_NO_2BYTE_ADDR_PHY_DDR quirk and reject 2-byte-address DDR
ops in cqspi_supports_mem_op() to work around AM654 erratum i2383
- Remove RFC tag
- Rebase on v7.1-rc5
- Collect tags from Miquel
- Link to v2: https://lore.kernel.org/linux-spi/20260113141617.1905039-1-s-k6@ti.com/
Changes in v2:
- Restructure the .execute_tuning() call from spi-mem clients instead
of mtdcore with best read_op and write_op (optional) passed
- Add compatible-specific .execute_tuning() call which can be called by
spi_mem_execute_tuning() if exists
- Handle tuning requirement check by controller instead of spi-mem
clients
- Add support to write the phy_pattern to cache if relevant write_op
is passed or get the partition offset which contains the phy_pattern
- Add tuning algorithm for DDR mode
- Add support for DQS
- Restrict PHY frequency to tuned operations
- Link to v1: https://lore.kernel.org/linux-spi/20250811193219.731851-1-s-k6@ti.com/
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
Pratyush Yadav (1):
mtd: spi-nor: extract read op template construction into helper
Santhosh Kumar K (17):
spi: dt-bindings: add spi-max-post-config-frequency-hz property
spi: dt-bindings: add spi-phy-pattern-partition property
spi: parse spi-max-post-config-frequency-hz into
post_config_max_speed_hz
spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops
spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning()
spi: cadence-quadspi: move cqspi_readdata_capture earlier
spi: cadence-quadspi: add DQS support to read data capture
spi: cadence-quadspi: add PHY tuning support
spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops
(i2383)
spi: cadence-quadspi: refactor direct read path for PHY support
spi: cadence-quadspi: enable PHY for direct reads
spi: cadence-quadspi: enable PHY for indirect writes
spi: cadence-quadspi: reprogram CS timing on every chip-select switch
spi: cadence-quadspi: reprogram PHY DLL on runtime resume
mtd: spinand: extract variant ranking logic into
spinand_op_find_best_variant()
mtd: spinand: negotiate optimal controller operating point before
dirmap creation
mtd: spi-nor: run controller optimization before dirmap creation
.../bindings/spi/cdns,qspi-nor.yaml | 17 +
.../bindings/spi/spi-peripheral-props.yaml | 13 +
drivers/mtd/nand/spi/core.c | 327 ++-
drivers/mtd/spi-nor/core.c | 80 +-
drivers/spi/spi-cadence-quadspi.c | 2295 +++++++++++++++--
drivers/spi/spi-mem.c | 55 +-
drivers/spi/spi.c | 2 +
include/linux/mtd/spi-nor.h | 3 +
include/linux/mtd/spinand.h | 14 +
include/linux/spi/spi-mem.h | 14 +
include/linux/spi/spi.h | 3 +
11 files changed, 2634 insertions(+), 189 deletions(-)
--
2.34.1
^ permalink raw reply [flat|nested] 30+ messages in thread
* [PATCH v9 01/18] spi: dt-bindings: add spi-max-post-config-frequency-hz property
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 02/18] spi: dt-bindings: add spi-phy-pattern-partition property Santhosh Kumar K
` (16 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6, Conor Dooley
Add spi-max-post-config-frequency-hz, a generic property for SPI
peripherals that operate at two distinct clock rates: a conservative
rate always reachable without controller configuration, and a higher
rate achievable only after controller-side configuration.
When both properties are present, spi-max-frequency gives the
conservative pre-configuration rate and spi-max-post-config-frequency-hz
gives the higher post-configuration target.
Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
.../devicetree/bindings/spi/spi-peripheral-props.yaml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml b/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
index 880a9f624566..84976fd3ee1b 100644
--- a/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
+++ b/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
@@ -45,6 +45,12 @@ properties:
description:
Maximum SPI clocking speed of the device in Hz.
+ spi-max-post-config-frequency-hz:
+ maxItems: 1
+ description:
+ Maximum SPI clock frequency in Hz achievable after controller-side
+ configuration.
+
spi-cs-setup-delay-ns:
description:
Delay in nanoseconds to be introduced by the controller after CS is
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 02/18] spi: dt-bindings: add spi-phy-pattern-partition property
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 01/18] spi: dt-bindings: add spi-max-post-config-frequency-hz property Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz Santhosh Kumar K
` (15 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6, Krzysztof Kozlowski
Add spi-phy-pattern-partition, a per-device phandle property on the
flash sub-node that allows the DT author to directly reference the
partition holding the SPI PHY tuning pattern. Used to locate the
pattern data during PHY tuning when the device cannot load the
pattern dynamically.
PHY tuning works by reading a known data pattern from the device
repeatedly while sweeping controller delay parameters until the
capture window is stable. For SPI NAND, the driver loads the pattern
into the page cache once using write-to-cache opcodes, then reads it
during the sweep. SPI NOR devices have no equivalent opcode, so the
pattern must be pre-programmed in a dedicated flash partition. One
partition per device is required to keep the procedure unambiguous
when multiple devices share a bus.
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
.../devicetree/bindings/spi/cdns,qspi-nor.yaml | 17 +++++++++++++++++
.../bindings/spi/spi-peripheral-props.yaml | 7 +++++++
2 files changed, 24 insertions(+)
diff --git a/Documentation/devicetree/bindings/spi/cdns,qspi-nor.yaml b/Documentation/devicetree/bindings/spi/cdns,qspi-nor.yaml
index 891f578b5ac4..277dedcd4246 100644
--- a/Documentation/devicetree/bindings/spi/cdns,qspi-nor.yaml
+++ b/Documentation/devicetree/bindings/spi/cdns,qspi-nor.yaml
@@ -209,5 +209,22 @@ examples:
cdns,tsd2d-ns = <60>;
cdns,tchsh-ns = <60>;
cdns,tslch-ns = <60>;
+ spi-phy-pattern-partition = <&phy_pattern>;
+
+ partitions {
+ compatible = "fixed-partitions";
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ partition@0 {
+ label = "data";
+ reg = <0x0 0x3fc0000>;
+ };
+
+ phy_pattern: partition@3fc0000 {
+ label = "phy-pattern";
+ reg = <0x3fc0000 0x40000>;
+ };
+ };
};
};
diff --git a/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml b/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
index 84976fd3ee1b..cc01b643fe9b 100644
--- a/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
+++ b/Documentation/devicetree/bindings/spi/spi-peripheral-props.yaml
@@ -123,6 +123,13 @@ properties:
description:
Delay, in microseconds, after a write transfer.
+ spi-phy-pattern-partition:
+ $ref: /schemas/types.yaml#/definitions/phandle
+ description:
+ Phandle to the flash partition holding the pre-programmed SPI PHY tuning
+ pattern. Used when the device cannot load the pattern dynamically during
+ PHY tuning.
+
stacked-memories:
description: Several SPI memories can be wired in stacked mode.
This basically means that either a device features several chip
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 01/18] spi: dt-bindings: add spi-max-post-config-frequency-hz property Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 02/18] spi: dt-bindings: add spi-phy-pattern-partition property Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:30 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
` (14 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Add post_config_max_speed_hz to struct spi_device and parse it from
the spi-max-post-config-frequency-hz DT property in of_spi_parse_dt().
This supports SPI devices that operate at two distinct clock rates: a
conservative rate always reachable without controller configuration,
and a higher rate achievable only after controller-side configuration
such as PHY tuning. With both properties set, spi-max-frequency gives
the conservative pre-configuration rate and post_config_max_speed_hz
carries the post-configuration target for the SPI-MEM layer.
Zero when not set, preserving existing behaviour.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi.c | 2 ++
include/linux/spi/spi.h | 3 +++
2 files changed, 5 insertions(+)
diff --git a/drivers/spi/spi.c b/drivers/spi/spi.c
index 5b5b3bc5f0d8..6b01ddb7cc32 100644
--- a/drivers/spi/spi.c
+++ b/drivers/spi/spi.c
@@ -2800,6 +2800,8 @@ static int of_spi_parse_dt(struct spi_controller *ctlr, struct spi_device *spi,
/* Device speed */
if (!of_property_read_u32(nc, "spi-max-frequency", &value))
spi->max_speed_hz = value;
+ if (!of_property_read_u32(nc, "spi-max-post-config-frequency-hz", &value))
+ spi->post_config_max_speed_hz = value;
/* Device CS delays */
of_spi_parse_dt_cs_delay(nc, &spi->cs_setup, "spi-cs-setup-delay-ns");
diff --git a/include/linux/spi/spi.h b/include/linux/spi/spi.h
index 88d17fce02dc..02fd602f29e8 100644
--- a/include/linux/spi/spi.h
+++ b/include/linux/spi/spi.h
@@ -141,6 +141,8 @@ extern void spi_transfer_cs_change_delay_exec(struct spi_message *msg,
* @max_speed_hz: Maximum clock rate to be used with this chip
* (on this board); may be changed by the device's driver.
* The spi_transfer.speed_hz can override this for each transfer.
+ * @post_config_max_speed_hz: Maximum clock rate achievable after controller
+ * configuration (e.g. PHY tuning); zero when not assigned.
* @bits_per_word: Data transfers involve one or more words; word sizes
* like eight or 12 bits are common. In-memory wordsizes are
* powers of two bytes (e.g. 20 bit samples use 32 bits).
@@ -195,6 +197,7 @@ struct spi_device {
struct device dev;
struct spi_controller *controller;
u32 max_speed_hz;
+ u32 post_config_max_speed_hz;
u8 bits_per_word;
bool rt;
#define SPI_NO_TX BIT(31) /* No transmit wire */
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (2 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:30 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 05/18] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning() Santhosh Kumar K
` (13 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Extend spi_mem_adjust_op_freq() with a bypass for post-config ops:
when op->max_freq equals post_config_max_speed_hz (the value written by
execute_tuning on success), return immediately leaving op->max_freq
unchanged. All other ops are capped to max_speed_hz, the
always-reachable base rate. This places the policy at the single
existing frequency-adjustment point so exec_op(), supports_op(), and
calc_op_duration() all see consistent frequencies.
Also extend spi_mem_default_supports_op() to require per_op_freq
controller capability for post-config ops. A post-config op runs at a
different clock rate than the pre-config base; a controller that cannot
switch frequencies per-op cannot execute it correctly.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-mem.c | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/drivers/spi/spi-mem.c b/drivers/spi/spi-mem.c
index 5f973ebfb8b6..cd4bc4c914e7 100644
--- a/drivers/spi/spi-mem.c
+++ b/drivers/spi/spi-mem.c
@@ -206,7 +206,8 @@ bool spi_mem_default_supports_op(struct spi_mem *mem,
return false;
if (op->max_freq &&
- op->max_freq < mem->spi->max_speed_hz) {
+ (op->max_freq < mem->spi->max_speed_hz ||
+ op->max_freq == mem->spi->post_config_max_speed_hz)) {
if (!spi_mem_controller_is_capable(ctlr, per_op_freq))
return false;
}
@@ -623,9 +624,18 @@ EXPORT_SYMBOL_GPL(spi_mem_adjust_op_size);
* Some chips have per-op frequency limitations and must adapt the maximum
* speed. This function allows SPI mem drivers to set @op->max_freq to the
* maximum supported value.
+ *
+ * When @mem->spi->post_config_max_speed_hz is set, ops with @op->max_freq
+ * equal to that value are treated as post-configuration ops (e.g. PHY-tuned)
+ * and are allowed to run at the full post-config rate. All other ops are
+ * capped to @mem->spi->max_speed_hz, the always-reachable base rate.
*/
void spi_mem_adjust_op_freq(struct spi_mem *mem, struct spi_mem_op *op)
{
+ if (mem->spi->post_config_max_speed_hz &&
+ op->max_freq == mem->spi->post_config_max_speed_hz)
+ return;
+
if (!op->max_freq || op->max_freq > mem->spi->max_speed_hz)
op->max_freq = mem->spi->max_speed_hz;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 05/18] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning()
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (3 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 06/18] spi: cadence-quadspi: move cqspi_readdata_capture earlier Santhosh Kumar K
` (12 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Add an execute_tuning callback to struct spi_controller_mem_ops. The
callback receives a mandatory read op template and an optional write op
template. On success the controller sets op->max_freq in each provided
template to the validated clock rate. A callback return of 0 with
max_freq left at zero signals that this specific op variant cannot be
PHY-tuned (e.g. a hardware erratum blocks it), allowing the caller to
try a different variant without treating it as a hard error.
Add the corresponding spi_mem_execute_tuning() wrapper. Like other
mem_ops paths it serialises access with spi_mem_access_start/end for
serialisation and runtime PM, and rejects GPIO CS configurations which
cannot support hardware-level tuning.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-mem.c | 43 +++++++++++++++++++++++++++++++++++++
include/linux/spi/spi-mem.h | 14 ++++++++++++
2 files changed, 57 insertions(+)
diff --git a/drivers/spi/spi-mem.c b/drivers/spi/spi-mem.c
index cd4bc4c914e7..79e89de1ce8d 100644
--- a/drivers/spi/spi-mem.c
+++ b/drivers/spi/spi-mem.c
@@ -693,6 +693,49 @@ u64 spi_mem_calc_op_duration(struct spi_mem *mem, struct spi_mem_op *op)
}
EXPORT_SYMBOL_GPL(spi_mem_calc_op_duration);
+/**
+ * spi_mem_execute_tuning() - Execute controller tuning procedure
+ * @mem: the SPI memory device
+ * @read_op: read operation template (mandatory)
+ * @write_op: write operation template (optional, may be NULL)
+ *
+ * Requests the controller to perform tuning for high-speed operation
+ * using the provided op templates. On success the controller callback
+ * sets @read_op->max_freq (and @write_op->max_freq when non-NULL) to
+ * the validated clock rate.
+ *
+ * Return: 0 on success, -EINVAL if @mem or @read_op is NULL,
+ * -EOPNOTSUPP if the controller doesn't support tuning,
+ * or a negative error code on failure.
+ */
+int spi_mem_execute_tuning(struct spi_mem *mem, struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op)
+{
+ struct spi_controller *ctlr;
+ int ret;
+
+ if (!mem || !read_op)
+ return -EINVAL;
+
+ ctlr = mem->spi->controller;
+ if (!ctlr->mem_ops || !ctlr->mem_ops->execute_tuning)
+ return -EOPNOTSUPP;
+
+ if (spi_get_csgpiod(mem->spi, 0))
+ return -EOPNOTSUPP;
+
+ ret = spi_mem_access_start(mem);
+ if (ret)
+ return ret;
+
+ ret = ctlr->mem_ops->execute_tuning(mem, read_op, write_op);
+
+ spi_mem_access_end(mem);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(spi_mem_execute_tuning);
+
static ssize_t spi_mem_no_dirmap_read(struct spi_mem_dirmap_desc *desc,
u64 offs, size_t len, void *buf)
{
diff --git a/include/linux/spi/spi-mem.h b/include/linux/spi/spi-mem.h
index 0cce6b57242a..b69f671c40e7 100644
--- a/include/linux/spi/spi-mem.h
+++ b/include/linux/spi/spi-mem.h
@@ -348,6 +348,15 @@ static inline void *spi_mem_get_drvdata(struct spi_mem *mem)
* @poll_status: poll memory device status until (status & mask) == match or
* when the timeout has expired. It fills the data buffer with
* the last status value.
+ * @execute_tuning: run the controller tuning procedure using the provided
+ * read and optional write op templates. On success, set
+ * @read_op->max_freq (and @write_op->max_freq when non-NULL)
+ * to the validated clock rate. Return a negative errno on
+ * error. Return -EOPNOTSUPP if the controller has no tuning
+ * capability at all. Return 0 with @read_op->max_freq left at
+ * zero to signal that this specific op cannot be PHY-tuned
+ * (e.g. a hardware erratum blocks it) but another variant may
+ * succeed; the caller will iterate remaining op variants.
*
* This interface should be implemented by SPI controllers providing an
* high-level interface to execute SPI memory operation, which is usually the
@@ -378,6 +387,8 @@ struct spi_controller_mem_ops {
unsigned long initial_delay_us,
unsigned long polling_rate_us,
unsigned long timeout_ms);
+ int (*execute_tuning)(struct spi_mem *mem, struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op);
};
/**
@@ -469,6 +480,9 @@ int spi_mem_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op);
void spi_mem_adjust_op_freq(struct spi_mem *mem, struct spi_mem_op *op);
u64 spi_mem_calc_op_duration(struct spi_mem *mem, struct spi_mem_op *op);
+int spi_mem_execute_tuning(struct spi_mem *mem, struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op);
+
bool spi_mem_supports_op(struct spi_mem *mem,
const struct spi_mem_op *op);
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 06/18] spi: cadence-quadspi: move cqspi_readdata_capture earlier
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (4 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 05/18] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning() Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
` (11 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Move cqspi_readdata_capture() function earlier in the file. This is
preparatory refactoring for upcoming PHY tuning support that needs to
call it without a forward declaration.
No functional changes.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 45 +++++++++++++++----------------
1 file changed, 22 insertions(+), 23 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index ecb0be394696..f8e266372367 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -457,6 +457,28 @@ static int cqspi_wait_idle(struct cqspi_st *cqspi)
}
}
+static void cqspi_readdata_capture(struct cqspi_st *cqspi, const bool bypass,
+ const unsigned int delay)
+{
+ void __iomem *reg_base = cqspi->iobase;
+ unsigned int reg;
+
+ reg = readl(reg_base + CQSPI_REG_READCAPTURE);
+
+ if (bypass)
+ reg |= BIT(CQSPI_REG_READCAPTURE_BYPASS_LSB);
+ else
+ reg &= ~BIT(CQSPI_REG_READCAPTURE_BYPASS_LSB);
+
+ reg &= ~(CQSPI_REG_READCAPTURE_DELAY_MASK
+ << CQSPI_REG_READCAPTURE_DELAY_LSB);
+
+ reg |= (delay & CQSPI_REG_READCAPTURE_DELAY_MASK)
+ << CQSPI_REG_READCAPTURE_DELAY_LSB;
+
+ writel(reg, reg_base + CQSPI_REG_READCAPTURE);
+}
+
static int cqspi_exec_flash_cmd(struct cqspi_st *cqspi, unsigned int reg)
{
void __iomem *reg_base = cqspi->iobase;
@@ -1274,29 +1296,6 @@ static void cqspi_config_baudrate_div(struct cqspi_st *cqspi)
writel(reg, reg_base + CQSPI_REG_CONFIG);
}
-static void cqspi_readdata_capture(struct cqspi_st *cqspi,
- const bool bypass,
- const unsigned int delay)
-{
- void __iomem *reg_base = cqspi->iobase;
- unsigned int reg;
-
- reg = readl(reg_base + CQSPI_REG_READCAPTURE);
-
- if (bypass)
- reg |= BIT(CQSPI_REG_READCAPTURE_BYPASS_LSB);
- else
- reg &= ~BIT(CQSPI_REG_READCAPTURE_BYPASS_LSB);
-
- reg &= ~(CQSPI_REG_READCAPTURE_DELAY_MASK
- << CQSPI_REG_READCAPTURE_DELAY_LSB);
-
- reg |= (delay & CQSPI_REG_READCAPTURE_DELAY_MASK)
- << CQSPI_REG_READCAPTURE_DELAY_LSB;
-
- writel(reg, reg_base + CQSPI_REG_READCAPTURE);
-}
-
static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
unsigned long sclk)
{
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (5 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 06/18] spi: cadence-quadspi: move cqspi_readdata_capture earlier Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:29 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (10 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Add DQS (Data Strobe) parameter to cqspi_readdata_capture() to control
data capture timing. DQS mode uses a dedicated strobe signal for
improved timing margins in high-speed SPI modes.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index f8e266372367..046fa5d69b0f 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -192,6 +192,7 @@ struct cqspi_driver_platdata {
#define CQSPI_REG_READCAPTURE_BYPASS_LSB 0
#define CQSPI_REG_READCAPTURE_DELAY_LSB 1
#define CQSPI_REG_READCAPTURE_DELAY_MASK 0xF
+#define CQSPI_REG_READCAPTURE_DQS_LSB 8
#define CQSPI_REG_SIZE 0x14
#define CQSPI_REG_SIZE_ADDRESS_LSB 0
@@ -458,7 +459,7 @@ static int cqspi_wait_idle(struct cqspi_st *cqspi)
}
static void cqspi_readdata_capture(struct cqspi_st *cqspi, const bool bypass,
- const unsigned int delay)
+ const bool dqs, const unsigned int delay)
{
void __iomem *reg_base = cqspi->iobase;
unsigned int reg;
@@ -476,6 +477,11 @@ static void cqspi_readdata_capture(struct cqspi_st *cqspi, const bool bypass,
reg |= (delay & CQSPI_REG_READCAPTURE_DELAY_MASK)
<< CQSPI_REG_READCAPTURE_DELAY_LSB;
+ if (dqs)
+ reg |= BIT(CQSPI_REG_READCAPTURE_DQS_LSB);
+ else
+ reg &= ~BIT(CQSPI_REG_READCAPTURE_DQS_LSB);
+
writel(reg, reg_base + CQSPI_REG_READCAPTURE);
}
@@ -1317,7 +1323,7 @@ static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
cqspi->sclk = sclk;
cqspi_config_baudrate_div(cqspi);
cqspi_delay(f_pdata);
- cqspi_readdata_capture(cqspi, !cqspi->rclk_en,
+ cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
f_pdata->read_delay);
}
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (6 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:40 ` sashiko-bot
2026-08-26 21:43 ` Mark Brown
2026-08-25 17:17 ` [PATCH v9 09/18] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383) Santhosh Kumar K
` (9 subsequent siblings)
17 siblings, 2 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
The Cadence QSPI controller supports a delay-line PHY for high-speed
operation. Without calibration the PHY is unused and read capture relies
on a fixed delay, limiting throughput at frequencies above the base
operating speed.
Add an execute_tuning callback that performs delay-line calibration using
a known data pattern written to a dedicated flash region. The pattern is
either read from a NOR partition identified by the DT property
spi-phy-pattern-partition, or written to the NAND page cache before
each calibration read.
For DDR protocols (8D-8D-8D) a 2D sweep of (rx_delay, tx_delay) pairs
is performed to find the widest passing region in the combined RX/TX
space. Binary search locates the gap boundary between passing regions
when two separate windows exist; the final operating point is placed at
the centre of the larger region with a small temperature-dependent
offset.
For SDR protocols a 1D sweep of the RX delay is sufficient. Two windows
at adjacent read_delay values are measured; the wider one's midpoint is
selected.
The tuning infrastructure is platform-specific: only am654-based OSPI
controllers populate the execute_tuning hook. All other platform data
entries return -EOPNOTSUPP and are unaffected.
The calibration target is sourced from spi->post_config_max_speed_hz,
populated by the SPI core from the spi-max-post-config-frequency-hz DT
property. Tuning is skipped when post_config_max_speed_hz is zero.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 1774 ++++++++++++++++++++++++++++-
1 file changed, 1761 insertions(+), 13 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index 046fa5d69b0f..f7cda1bc7139 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -6,6 +6,7 @@
// Copyright Intel Corporation (C) 2019-2020. All rights reserved.
// Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com
+#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/completion.h>
#include <linux/delay.h>
@@ -65,15 +66,28 @@ enum {
struct cqspi_st;
+struct phy_setting {
+ u8 rx;
+ u8 tx;
+ u8 read_delay;
+};
+
struct cqspi_flash_pdata {
- struct cqspi_st *cqspi;
- u32 clk_rate;
- u32 read_delay;
- u32 tshsl_ns;
- u32 tsd2d_ns;
- u32 tchsh_ns;
- u32 tslch_ns;
- u8 cs;
+ struct cqspi_st *cqspi;
+ u32 read_delay;
+ u32 tshsl_ns;
+ u32 tsd2d_ns;
+ u32 tchsh_ns;
+ u32 tslch_ns;
+ bool use_dqs;
+ bool use_tuned_phy;
+ bool dll_locked;
+ u8 cs;
+ struct phy_setting phy_setting;
+ struct spi_mem_op phy_read_op;
+ struct spi_mem_op phy_write_op;
+ u32 phy_pattern_partition_offset;
+ bool phy_pattern_partition_valid;
};
static const struct clk_bulk_data cqspi_clks[CLK_QSPI_NUM] = {
@@ -117,6 +131,8 @@ struct cqspi_st {
bool is_jh7110; /* Flag for StarFive JH7110 SoC */
bool is_rzn1; /* Flag for Renesas RZ/N1 SoC */
bool disable_stig_mode;
+ bool phy_tuning_active;
+ u8 phy_saved_dummy;
refcount_t refcount;
refcount_t inflight_ops;
@@ -129,12 +145,15 @@ struct cqspi_driver_platdata {
int (*indirect_read_dma)(struct cqspi_flash_pdata *f_pdata,
u_char *rxbuf, loff_t from_addr, size_t n_rx);
u32 (*get_dma_status)(struct cqspi_st *cqspi);
+ int (*execute_tuning)(struct spi_mem *mem, struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op);
};
/* Operation timeout value */
#define CQSPI_TIMEOUT_MS 500
#define CQSPI_READ_TIMEOUT_MS 10
#define CQSPI_BUSYWAIT_TIMEOUT_US 500
+#define CQSPI_DLL_TIMEOUT_US 300
/* Runtime_pm autosuspend delay */
#define CQSPI_AUTOSUSPEND_TIMEOUT 2000
@@ -148,12 +167,14 @@ struct cqspi_driver_platdata {
/* Register map */
#define CQSPI_REG_CONFIG 0x00
#define CQSPI_REG_CONFIG_ENABLE_MASK BIT(0)
+#define CQSPI_REG_CONFIG_PHY_EN BIT(3)
#define CQSPI_REG_CONFIG_ENB_DIR_ACC_CTRL BIT(7)
#define CQSPI_REG_CONFIG_DECODE_MASK BIT(9)
#define CQSPI_REG_CONFIG_CHIPSELECT_LSB 10
#define CQSPI_REG_CONFIG_DMA_MASK BIT(15)
#define CQSPI_REG_CONFIG_BAUD_LSB 19
#define CQSPI_REG_CONFIG_DTR_PROTO BIT(24)
+#define CQSPI_REG_CONFIG_PHY_PIPELINE BIT(25)
#define CQSPI_REG_CONFIG_DUAL_OPCODE BIT(30)
#define CQSPI_REG_CONFIG_IDLE_LSB 31
#define CQSPI_REG_CONFIG_CHIPSELECT_MASK 0xF
@@ -192,6 +213,7 @@ struct cqspi_driver_platdata {
#define CQSPI_REG_READCAPTURE_BYPASS_LSB 0
#define CQSPI_REG_READCAPTURE_DELAY_LSB 1
#define CQSPI_REG_READCAPTURE_DELAY_MASK 0xF
+#define CQSPI_REG_READCAPTURE_EDGE_LSB 5
#define CQSPI_REG_READCAPTURE_DQS_LSB 8
#define CQSPI_REG_SIZE 0x14
@@ -273,6 +295,28 @@ struct cqspi_driver_platdata {
#define CQSPI_REG_POLLING_STATUS 0xB0
#define CQSPI_REG_POLLING_STATUS_DUMMY_LSB 16
+#define CQSPI_REG_PHY_CONFIG 0xB4
+#define CQSPI_REG_PHY_CONFIG_RX_DEL_LSB 0
+#define CQSPI_REG_PHY_CONFIG_RX_DEL_MASK 0x7F
+#define CQSPI_REG_PHY_CONFIG_TX_DEL_LSB 16
+#define CQSPI_REG_PHY_CONFIG_TX_DEL_MASK 0x7F
+#define CQSPI_REG_PHY_CONFIG_DLL_RESET BIT(30)
+#define CQSPI_REG_PHY_CONFIG_RESYNC BIT(31)
+
+#define CQSPI_REG_PHY_DLL_MASTER 0xB8
+#define CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_LSB 0
+#define CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_MASK 0x7f
+#define CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_VAL 16
+#define CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_LEN 0x7
+#define CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_LSB 20
+#define CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_3 0x2
+#define CQSPI_REG_PHY_DLL_MASTER_BYPASS BIT(23)
+#define CQSPI_REG_PHY_DLL_MASTER_CYCLE BIT(24)
+
+#define CQSPI_REG_DLL_OBS_LOW 0xBC
+#define CQSPI_REG_DLL_OBS_LOW_DLL_LOCK BIT(0)
+#define CQSPI_REG_DLL_OBS_LOW_LOOPBACK_LOCK BIT(15)
+
#define CQSPI_REG_OP_EXT_LOWER 0xE0
#define CQSPI_REG_OP_EXT_READ_LSB 24
#define CQSPI_REG_OP_EXT_WRITE_LSB 16
@@ -321,6 +365,50 @@ struct cqspi_driver_platdata {
#define CQSPI_REG_VERSAL_DMA_VAL 0x602
+#define CQSPI_PHY_INIT_RD 1
+#define CQSPI_PHY_MAX_RD 4
+#define CQSPI_PHY_MAX_DELAY 127
+#define CQSPI_PHY_DDR_SEARCH_STEP 4
+#define CQSPI_PHY_TX_LOOKUP_LOW_START 28
+#define CQSPI_PHY_TX_LOOKUP_LOW_END 48
+#define CQSPI_PHY_TX_LOOKUP_HIGH_START 60
+#define CQSPI_PHY_TX_LOOKUP_HIGH_END 96
+#define CQSPI_PHY_RX_LOW_SEARCH_START 0
+#define CQSPI_PHY_RX_LOW_SEARCH_END 40
+#define CQSPI_PHY_RX_HIGH_SEARCH_START 24
+#define CQSPI_PHY_RX_HIGH_SEARCH_END 127
+#define CQSPI_PHY_TX_LOW_SEARCH_START 0
+#define CQSPI_PHY_TX_LOW_SEARCH_END 64
+#define CQSPI_PHY_TX_HIGH_SEARCH_START 78
+#define CQSPI_PHY_TX_HIGH_SEARCH_END 127
+#define CQSPI_PHY_SEARCH_OFFSET 8
+
+#define CQSPI_PHY_DEFAULT_TEMP 45
+#define CQSPI_PHY_MIN_TEMP -45
+#define CQSPI_PHY_MAX_TEMP 130
+#define CQSPI_PHY_MID_TEMP (CQSPI_PHY_MIN_TEMP + \
+ ((CQSPI_PHY_MAX_TEMP - \
+ CQSPI_PHY_MIN_TEMP) / 2))
+
+/*
+ * PHY tuning pattern for calibrating read data capture delay. This 128-byte
+ * pattern provides sufficient bit transitions across all byte lanes to
+ * reliably detect timing windows at high frequencies.
+ */
+static const u8 phy_tuning_pattern[] __aligned(64) = {
+ 0xFE, 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xFE, 0xFE, 0x01,
+ 0x01, 0x01, 0x01, 0x00, 0x00, 0xFE, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0x00, 0x00, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFE,
+ 0xFE, 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFE, 0x00, 0xFE, 0xFE, 0x01,
+ 0x01, 0x01, 0x01, 0xFE, 0x00, 0xFE, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFE, 0x00, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0xFE,
+ 0xFE, 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0xFE, 0xFE, 0xFE, 0x01,
+ 0x01, 0x01, 0x01, 0x00, 0xFE, 0xFE, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFE, 0xFE,
+ 0xFE, 0xFF, 0x01, 0x01, 0x01, 0x01, 0x01, 0xFE, 0xFE, 0xFE, 0xFE, 0x01,
+ 0x01, 0x01, 0x01, 0xFE, 0xFE, 0xFE, 0xFE, 0x01,
+};
+
static int cqspi_wait_for_bit(const struct cqspi_driver_platdata *ddata,
void __iomem *reg, const u32 mask, bool clr,
bool busywait)
@@ -913,6 +1001,76 @@ static void cqspi_controller_enable(struct cqspi_st *cqspi, bool enable)
writel(reg, reg_base + CQSPI_REG_CONFIG);
}
+static void cqspi_set_dll(void __iomem *reg_base, u8 rx_dll, u8 tx_dll)
+{
+ unsigned int reg;
+
+ reg = readl(reg_base + CQSPI_REG_PHY_CONFIG);
+ reg &= ~((CQSPI_REG_PHY_CONFIG_RX_DEL_MASK
+ << CQSPI_REG_PHY_CONFIG_RX_DEL_LSB) |
+ (CQSPI_REG_PHY_CONFIG_TX_DEL_MASK
+ << CQSPI_REG_PHY_CONFIG_TX_DEL_LSB));
+ reg |= ((rx_dll & CQSPI_REG_PHY_CONFIG_RX_DEL_MASK)
+ << CQSPI_REG_PHY_CONFIG_RX_DEL_LSB) |
+ ((tx_dll & CQSPI_REG_PHY_CONFIG_TX_DEL_MASK)
+ << CQSPI_REG_PHY_CONFIG_TX_DEL_LSB) |
+ CQSPI_REG_PHY_CONFIG_RESYNC;
+ writel(reg, reg_base + CQSPI_REG_PHY_CONFIG);
+}
+
+static int cqspi_resync_dll(struct cqspi_st *cqspi)
+{
+ void __iomem *reg_base = cqspi->iobase;
+ unsigned int reg;
+ int ret;
+
+ ret = cqspi_wait_idle(cqspi);
+ if (ret)
+ return ret;
+
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg &= ~CQSPI_REG_CONFIG_ENABLE_MASK;
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ reg = readl(reg_base + CQSPI_REG_PHY_CONFIG);
+ reg &= ~(CQSPI_REG_PHY_CONFIG_DLL_RESET | CQSPI_REG_PHY_CONFIG_RESYNC);
+ writel(reg, reg_base + CQSPI_REG_PHY_CONFIG);
+
+ reg = readl(reg_base + CQSPI_REG_PHY_DLL_MASTER);
+ reg &= ~(CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_MASK
+ << CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_LSB);
+ reg |= (CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_VAL
+ << CQSPI_REG_PHY_DLL_MASTER_INIT_DELAY_LSB);
+ writel(reg, reg_base + CQSPI_REG_PHY_DLL_MASTER);
+
+ reg = readl(reg_base + CQSPI_REG_PHY_CONFIG);
+ reg |= CQSPI_REG_PHY_CONFIG_DLL_RESET;
+ writel(reg, reg_base + CQSPI_REG_PHY_CONFIG);
+
+ ret = readl_poll_timeout(reg_base + CQSPI_REG_DLL_OBS_LOW, reg,
+ (reg & CQSPI_REG_DLL_OBS_LOW_DLL_LOCK), 0,
+ CQSPI_DLL_TIMEOUT_US);
+ if (ret)
+ goto re_enable;
+
+ ret = readl_poll_timeout(reg_base + CQSPI_REG_DLL_OBS_LOW, reg,
+ (reg & CQSPI_REG_DLL_OBS_LOW_LOOPBACK_LOCK), 0,
+ CQSPI_DLL_TIMEOUT_US);
+ if (ret)
+ goto re_enable;
+
+ reg = readl(reg_base + CQSPI_REG_PHY_CONFIG);
+ reg |= CQSPI_REG_PHY_CONFIG_RESYNC;
+ writel(reg, reg_base + CQSPI_REG_PHY_CONFIG);
+
+re_enable:
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg |= CQSPI_REG_CONFIG_ENABLE_MASK;
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ return ret;
+}
+
static int cqspi_versal_indirect_read_dma(struct cqspi_flash_pdata *f_pdata,
u_char *rxbuf, loff_t from_addr,
size_t n_rx)
@@ -1323,8 +1481,9 @@ static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
cqspi->sclk = sclk;
cqspi_config_baudrate_div(cqspi);
cqspi_delay(f_pdata);
- cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
- f_pdata->read_delay);
+ if (!cqspi->phy_tuning_active)
+ cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
+ f_pdata->read_delay);
}
if (switch_cs || switch_ck)
@@ -1559,10 +1718,1594 @@ static bool cqspi_supports_mem_op(struct spi_mem *mem,
return spi_mem_default_supports_op(mem, op);
}
+static int cqspi_write_pattern_to_cache(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem,
+ const struct spi_mem_op *write_op)
+{
+ struct spi_controller *ctlr = mem->spi->controller;
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ struct spi_mem_op op = *write_op;
+ int ret;
+
+ op.max_freq = mem->spi->max_speed_hz;
+ op.data.nbytes = sizeof(phy_tuning_pattern);
+ op.data.buf.out = phy_tuning_pattern;
+
+ ret = ctlr->mem_ops->exec_op(mem, &op);
+ if (ret) {
+ dev_err(dev, "Failed to write PHY pattern to cache: %d\n", ret);
+ return ret;
+ }
+ dev_dbg(dev, "PHY pattern (%zu bytes) written to cache\n",
+ sizeof(phy_tuning_pattern));
+
+ return 0;
+}
+
+static int cqspi_phy_check_pattern(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem)
+{
+ struct spi_controller *ctlr = mem->spi->controller;
+ struct spi_mem_op op;
+ u8 *read_data;
+ int ret;
+
+ read_data = kmalloc_array(ARRAY_SIZE(phy_tuning_pattern),
+ sizeof(phy_tuning_pattern[0]), GFP_KERNEL);
+ if (!read_data)
+ return -ENOMEM;
+
+ op = f_pdata->phy_read_op;
+ op.data.buf.in = read_data;
+ op.data.nbytes = sizeof(phy_tuning_pattern);
+
+ ret = ctlr->mem_ops->exec_op(mem, &op);
+ if (ret)
+ goto out;
+
+ if (memcmp(read_data, phy_tuning_pattern, sizeof(phy_tuning_pattern)))
+ ret = -EAGAIN;
+
+out:
+ kfree(read_data);
+ return ret;
+}
+
+static void cqspi_phy_set_dll_master(struct cqspi_st *cqspi)
+{
+ void __iomem *reg_base = cqspi->iobase;
+ unsigned int reg;
+
+ reg = readl(reg_base + CQSPI_REG_PHY_DLL_MASTER);
+ reg &= ~((CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_LEN
+ << CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_LSB) |
+ CQSPI_REG_PHY_DLL_MASTER_BYPASS |
+ CQSPI_REG_PHY_DLL_MASTER_CYCLE);
+ reg |= ((CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_3
+ << CQSPI_REG_PHY_DLL_MASTER_DLY_ELMTS_LSB) |
+ CQSPI_REG_PHY_DLL_MASTER_CYCLE);
+
+ writel(reg, reg_base + CQSPI_REG_PHY_DLL_MASTER);
+}
+
+static void cqspi_phy_pre_config(struct cqspi_st *cqspi,
+ struct cqspi_flash_pdata *f_pdata,
+ const bool bypass)
+{
+ void __iomem *reg_base = cqspi->iobase;
+ unsigned int reg;
+ u8 dummy;
+
+ cqspi->phy_tuning_active = true;
+
+ cqspi_readdata_capture(cqspi, bypass, f_pdata->use_dqs,
+ f_pdata->phy_setting.read_delay);
+
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg &= ~(CQSPI_REG_CONFIG_PHY_EN | CQSPI_REG_CONFIG_PHY_PIPELINE);
+ reg |= CQSPI_REG_CONFIG_PHY_EN;
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ reg = readl(reg_base + CQSPI_REG_RD_INSTR);
+ dummy = FIELD_GET(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ reg);
+ cqspi->phy_saved_dummy = dummy;
+ if (dummy)
+ dummy--;
+ reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK << CQSPI_REG_RD_INSTR_DUMMY_LSB);
+ reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ dummy);
+ writel(reg, reg_base + CQSPI_REG_RD_INSTR);
+
+ cqspi_phy_set_dll_master(cqspi);
+}
+
+static void cqspi_phy_post_config(struct cqspi_st *cqspi,
+ const unsigned int delay)
+{
+ void __iomem *reg_base = cqspi->iobase;
+ unsigned int reg;
+
+ cqspi->phy_tuning_active = false;
+
+ /*
+ * Clear the EDGE bit set by cqspi_phy_apply_setting, then restore the
+ * READCAPTURE register to its non-PHY state (bypass, DQS, and delay).
+ */
+ reg = readl(reg_base + CQSPI_REG_READCAPTURE);
+ reg &= ~BIT(CQSPI_REG_READCAPTURE_EDGE_LSB);
+ writel(reg, reg_base + CQSPI_REG_READCAPTURE);
+
+ cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false, delay);
+
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg &= ~(CQSPI_REG_CONFIG_PHY_EN | CQSPI_REG_CONFIG_PHY_PIPELINE);
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ /* Restore the original dummy cycle count saved by cqspi_phy_pre_config. */
+ reg = readl(reg_base + CQSPI_REG_RD_INSTR);
+ reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK << CQSPI_REG_RD_INSTR_DUMMY_LSB);
+ reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ cqspi->phy_saved_dummy);
+ writel(reg, reg_base + CQSPI_REG_RD_INSTR);
+}
+
+static int cqspi_phy_apply_setting(struct cqspi_flash_pdata *f_pdata,
+ struct phy_setting *phy)
+{
+ struct cqspi_st *cqspi = f_pdata->cqspi;
+ unsigned int reg;
+ int ret;
+
+ reg = readl(cqspi->iobase + CQSPI_REG_READCAPTURE);
+ reg |= BIT(CQSPI_REG_READCAPTURE_EDGE_LSB);
+ writel(reg, cqspi->iobase + CQSPI_REG_READCAPTURE);
+
+ cqspi_set_dll(cqspi->iobase, phy->rx, phy->tx);
+
+ ret = cqspi_resync_dll(cqspi);
+ if (ret)
+ return ret;
+
+ f_pdata->dll_locked = true;
+ f_pdata->phy_setting.rx = phy->rx;
+ f_pdata->phy_setting.tx = phy->tx;
+ f_pdata->phy_setting.read_delay = phy->read_delay;
+
+ cqspi_readdata_capture(cqspi, !f_pdata->use_dqs, f_pdata->use_dqs,
+ phy->read_delay);
+
+ return 0;
+}
+
+static int cqspi_find_rx_low_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ do {
+ phy->rx = CQSPI_PHY_RX_LOW_SEARCH_START;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+
+ phy->rx += CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (phy->rx <= CQSPI_PHY_RX_LOW_SEARCH_END);
+
+ phy->read_delay++;
+ } while (phy->read_delay <= CQSPI_PHY_MAX_RD);
+
+ dev_dbg(dev, "Unable to find RX low\n");
+ return -ENOENT;
+}
+
+static int cqspi_find_rx_low_sdr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ phy->rx = 0;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+ phy->rx++;
+ } while (phy->rx <= CQSPI_PHY_MAX_DELAY);
+
+ dev_dbg(dev, "Unable to find RX low\n");
+ return -ENOENT;
+}
+
+static int cqspi_find_rx_high_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ do {
+ phy->rx = CQSPI_PHY_RX_HIGH_SEARCH_END;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+
+ phy->rx -= CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (phy->rx >= CQSPI_PHY_RX_HIGH_SEARCH_START);
+
+ phy->read_delay--;
+ } while (phy->read_delay >= CQSPI_PHY_INIT_RD);
+
+ dev_dbg(dev, "Unable to find RX high\n");
+ return -ENOENT;
+}
+
+static int cqspi_find_rx_high_sdr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy,
+ u8 lowerbound)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ phy->rx = CQSPI_PHY_MAX_DELAY;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+ phy->rx--;
+ } while (phy->rx > lowerbound);
+
+ dev_dbg(dev, "Unable to find RX high\n");
+ return -ENOENT;
+}
+
+static int cqspi_find_tx_low_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ do {
+ phy->tx = CQSPI_PHY_TX_LOW_SEARCH_START;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+
+ phy->tx += CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (phy->tx <= CQSPI_PHY_TX_LOW_SEARCH_END);
+
+ phy->read_delay++;
+ } while (phy->read_delay <= CQSPI_PHY_MAX_RD);
+
+ dev_dbg(dev, "Unable to find TX low\n");
+ return -ENOENT;
+}
+
+static int cqspi_find_tx_high_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem, struct phy_setting *phy)
+{
+ struct device *dev = &f_pdata->cqspi->pdev->dev;
+ int ret;
+
+ do {
+ phy->tx = CQSPI_PHY_TX_HIGH_SEARCH_END;
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, phy);
+ if (!ret) {
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (!ret)
+ return 0;
+ }
+
+ phy->tx -= CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (phy->tx >= CQSPI_PHY_TX_HIGH_SEARCH_START);
+
+ phy->read_delay--;
+ } while (phy->read_delay >= CQSPI_PHY_INIT_RD);
+
+ dev_dbg(dev, "Unable to find TX high\n");
+ return -ENOENT;
+}
+
+static void cqspi_phy_find_gaplow_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem,
+ struct phy_setting *bottomleft,
+ struct phy_setting *topright,
+ struct phy_setting *gaplow)
+{
+ struct phy_setting left, right, mid;
+ int ret;
+
+ left = *bottomleft;
+ right = *topright;
+
+ mid.tx = left.tx + ((right.tx - left.tx) / 2);
+ mid.rx = left.rx + ((right.rx - left.rx) / 2);
+ mid.read_delay = left.read_delay;
+
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, &mid);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ /* The pattern was not found. Go to the lower half. */
+ right.tx = mid.tx;
+ right.rx = mid.rx;
+
+ mid.tx = left.tx + ((mid.tx - left.tx) / 2);
+ mid.rx = left.rx + ((mid.rx - left.rx) / 2);
+ } else {
+ /* The pattern was found. Go to the upper half. */
+ left.tx = mid.tx;
+ left.rx = mid.rx;
+
+ mid.tx = mid.tx + ((right.tx - mid.tx) / 2);
+ mid.rx = mid.rx + ((right.rx - mid.rx) / 2);
+ }
+
+ /* Break the loop if the window has closed. */
+ } while ((right.tx - left.tx >= 2) && (right.rx - left.rx >= 2));
+
+ *gaplow = mid;
+}
+
+static void cqspi_phy_find_gaphigh_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem,
+ struct phy_setting *bottomleft,
+ struct phy_setting *topright,
+ struct phy_setting *gaphigh)
+{
+ struct phy_setting left, right, mid;
+ int ret;
+
+ left = *bottomleft;
+ right = *topright;
+
+ mid.tx = left.tx + ((right.tx - left.tx) / 2);
+ mid.rx = left.rx + ((right.rx - left.rx) / 2);
+ mid.read_delay = right.read_delay;
+
+ do {
+ ret = cqspi_phy_apply_setting(f_pdata, &mid);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ /* The pattern was not found. Go to the upper half. */
+ left.tx = mid.tx;
+ left.rx = mid.rx;
+
+ mid.tx = mid.tx + ((right.tx - mid.tx) / 2);
+ mid.rx = mid.rx + ((right.rx - mid.rx) / 2);
+ } else {
+ /* The pattern was found. Go to the lower half. */
+ right.tx = mid.tx;
+ right.rx = mid.rx;
+
+ mid.tx = left.tx + ((mid.tx - left.tx) / 2);
+ mid.rx = left.rx + ((mid.rx - left.rx) / 2);
+ }
+
+ /* Break the loop if the window has closed. */
+ } while ((right.tx - left.tx >= 2) && (right.rx - left.rx >= 2));
+
+ *gaphigh = mid;
+}
+
+static int cqspi_get_temp(int *temp)
+{
+ /* TODO: read SoC thermal sensor; caller falls back to room temperature */
+ return -EOPNOTSUPP;
+}
+
+static inline void cqspi_phy_reset_setting(struct phy_setting *phy)
+{
+ *phy = (struct phy_setting){ .rx = 0, .tx = 127, .read_delay = 0 };
+}
+
+static int cqspi_phy_tuning_ddr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem)
+{
+ struct cqspi_st *cqspi = f_pdata->cqspi;
+ struct device *dev = &cqspi->pdev->dev;
+ struct phy_setting rxlow, rxhigh, txlow, txhigh;
+ struct phy_setting srxlow, srxhigh;
+ struct phy_setting bottomleft, topright, searchpoint;
+ struct phy_setting gaplow, gaphigh;
+ struct phy_setting backuppoint, backupcornerpoint;
+ int ret, rx_window, temp;
+ bool primary = true, secondary = true;
+
+ /*
+ * DDR tuning: 2D search across RX and TX delays for optimal timing.
+ *
+ * Algorithm: Find RX boundaries (rxlow/rxhigh) using TX window search,
+ * find TX boundaries (txlow/txhigh) at fixed RX, define valid region,
+ * locate gaps via binary search, select final point with temperature
+ * compensation.
+ *
+ * rx
+ * 127 ^
+ * | topright
+ * | *
+ * | xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * | xxxxxxxxxxxxx ++++++++++++
+ * | xxxxxxxxxxxxxx +++++++++++
+ * | xxxxxxxxxxxxxxx ++++++++++
+ * | xxxxxxxxxxxxxxxx +++++++++
+ * | xxxxxxxxxxxxxxxxx ++++++++
+ * | xxxxxxxxxxxxxxxxxx +++++++
+ * | *
+ * | bottomleft
+ * -----------------------------------------> tx
+ * 0 127
+ */
+
+ f_pdata->use_tuned_phy = true;
+
+ /* Golden rxlow search: Find lower RX boundary using TX window sweep */
+
+ /*
+ * rx
+ * 127 ^
+ * | xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | |xxxxx|xxxxx +++++++++++++
+ * | |xxxxx|xxxxxx ++++++++++++
+ * search | |xxxxx|xxxxxxx +++++++++++
+ * rxlow --------->|xxxxx|xxxxxxxx ++++++++++
+ * | |xxxxx|xxxxxxxxx +++++++++
+ * | |xxxxx|xxxxxxxxxx ++++++++
+ * | |xxxxx|xxxxxxxxxxx +++++++
+ * | | |
+ * --------|-----|----------------------------> tx
+ * 0 | | 127
+ * txlow txlow
+ * start end
+ *
+ * |----------------------------------------------------------|
+ * | Primary | Secondary | Final |
+ * | Search | Search | Point |
+ * |---------|-----------|------------------------------------|
+ * | Fail | Fail | Return Fail |
+ * |---------|-----------|------------------------------------|
+ * | Fail | Pass | Return Fail |
+ * |---------|-----------|------------------------------------|
+ * | Pass | Fail | Return Fail |
+ * |---------|-----------|------------------------------------|
+ * | Pass | Pass | rx = min(primary.rx, secondary.rx) |
+ * | | | tx = primary.tx |
+ * | | | read_delay = |
+ * | | | min(primary.read_delay, |
+ * | | | secondary.read_delay) |
+ * |----------------------------------------------------------|
+ */
+
+ /* Primary rxlow: Sweep TX window to find valid RX lower bound */
+
+ rxlow.tx = CQSPI_PHY_TX_LOOKUP_LOW_START;
+ do {
+ dev_dbg(dev, "Searching for Golden Primary rxlow on TX = %d\n",
+ rxlow.tx);
+ rxlow.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_rx_low_ddr(f_pdata, mem, &rxlow);
+ if (!ret)
+ break;
+ rxlow.tx += CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (rxlow.tx <= CQSPI_PHY_TX_LOOKUP_LOW_END);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Golden Primary rxlow: RX: %d TX: %d RD: %d\n", rxlow.rx,
+ rxlow.tx, rxlow.read_delay);
+
+ /* Secondary rxlow: Verify at offset TX for robustness */
+
+ if (rxlow.tx <= (CQSPI_PHY_TX_LOOKUP_LOW_END - CQSPI_PHY_SEARCH_OFFSET))
+ srxlow.tx = rxlow.tx + CQSPI_PHY_SEARCH_OFFSET;
+ else
+ srxlow.tx = CQSPI_PHY_TX_LOOKUP_LOW_END;
+ dev_dbg(dev, "Searching for Golden Secondary rxlow on TX = %d\n",
+ srxlow.tx);
+ srxlow.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_rx_low_ddr(f_pdata, mem, &srxlow);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Golden Secondary rxlow: RX: %d TX: %d RD: %d\n",
+ srxlow.rx, srxlow.tx, srxlow.read_delay);
+
+ rxlow.rx = min(rxlow.rx, srxlow.rx);
+ rxlow.read_delay = min(rxlow.read_delay, srxlow.read_delay);
+ dev_dbg(dev, "Golden Final rxlow: RX: %d TX: %d RD: %d\n", rxlow.rx,
+ rxlow.tx, rxlow.read_delay);
+
+ /* Golden rxhigh search: Find upper RX boundary at fixed TX */
+
+ /*
+ * rx
+ * 127 ^
+ * | |xxxx ++++++++++++++++++++
+ * | |xxxxx +++++++++++++++++++
+ * search | |xxxxxx ++++++++++++++++++
+ * rxhigh --------->|xxxxxxx +++++++++++++++++
+ * on fixed | |xxxxxxxx ++++++++++++++++
+ * tx | |xxxxxxxxx +++++++++++++++
+ * | |xxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * | xxxxxxxxxxxxx ++++++++++++
+ * | xxxxxxxxxxxxxx +++++++++++
+ * | xxxxxxxxxxxxxxx ++++++++++
+ * | xxxxxxxxxxxxxxxx +++++++++
+ * | xxxxxxxxxxxxxxxxx ++++++++
+ * | xxxxxxxxxxxxxxxxxx +++++++
+ * |
+ * -------------------------------------------> tx
+ * 0 127
+ *
+ * |----------------------------------------------------------|
+ * | Primary | Secondary | Final |
+ * | Search | Search | Point |
+ * |---------|-----------|------------------------------------|
+ * | Fail | Fail | Return Fail |
+ * |---------|-----------|------------------------------------|
+ * | Fail | Pass | Choose Secondary |
+ * |---------|-----------|------------------------------------|
+ * | Pass | Fail | Choose Primary |
+ * |---------|-----------|------------------------------------|
+ * | Pass | Pass | if (secondary.rx > primary.rx) |
+ * | | | Choose Secondary |
+ * | | | else |
+ * | | | Choose Primary |
+ * |----------------------------------------------------------|
+ */
+
+ /* Primary rxhigh: Search at rxlow's TX, decrement from max read_delay */
+
+ rxhigh.tx = rxlow.tx;
+ dev_dbg(dev, "Searching for Golden Primary rxhigh on TX = %d\n",
+ rxhigh.tx);
+ rxhigh.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_rx_high_ddr(f_pdata, mem, &rxhigh);
+ if (ret)
+ primary = false;
+ dev_dbg(dev, "Golden Primary rxhigh: RX: %d TX: %d RD: %d\n", rxhigh.rx,
+ rxhigh.tx, rxhigh.read_delay);
+
+ /* Secondary rxhigh: Verify at offset TX */
+
+ if (rxhigh.tx <=
+ (CQSPI_PHY_TX_LOOKUP_LOW_END - CQSPI_PHY_SEARCH_OFFSET))
+ srxhigh.tx = rxhigh.tx + CQSPI_PHY_SEARCH_OFFSET;
+ else
+ srxhigh.tx = CQSPI_PHY_TX_LOOKUP_LOW_END;
+ dev_dbg(dev, "Searching for Golden Secondary rxhigh on TX = %d\n",
+ srxhigh.tx);
+ srxhigh.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_rx_high_ddr(f_pdata, mem, &srxhigh);
+ if (ret)
+ secondary = false;
+ dev_dbg(dev, "Golden Secondary rxhigh: RX: %d TX: %d RD: %d\n",
+ srxhigh.rx, srxhigh.tx, srxhigh.read_delay);
+
+ if (!primary && !secondary)
+ goto out;
+ else if (!primary)
+ rxhigh = srxhigh;
+ else if (secondary && srxhigh.rx > rxhigh.rx)
+ rxhigh = srxhigh;
+ dev_dbg(dev, "Golden Final rxhigh: RX: %d TX: %d RD: %d\n", rxhigh.rx,
+ rxhigh.tx, rxhigh.read_delay);
+
+ primary = true;
+ secondary = true;
+
+ /* If rxlow/rxhigh at same read_delay, search backup at upper TX range */
+
+ if (rxlow.read_delay == rxhigh.read_delay) {
+ dev_dbg(dev, "rxlow and rxhigh at the same read delay.\n");
+
+ /* Backup rxlow: Search at high TX window */
+
+ /*
+ * rx
+ * 127 ^
+ * | xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++|++++|
+ * | xxxxxxxxxxxxx ++++++|++++|
+ * search | xxxxxxxxxxxxxx +++++|++++|
+ * rxlow --------------------------------->|++++|
+ * | xxxxxxxxxxxxxxxx +++|++++|
+ * | xxxxxxxxxxxxxxxxx ++|++++|
+ * | xxxxxxxxxxxxxxxxxx +|++++|
+ * | | |
+ * --------------------------------|----|-----> tx
+ * 0 | | 127
+ * txhigh txhigh
+ * start end
+ *
+ * |-----------------------------------------------------|
+ * | Primary | Secondary | Final |
+ * | Search | Search | Point |
+ * |---------|-----------|-------------------------------|
+ * | Fail | Fail | Return Fail |
+ * |---------|-----------|-------------------------------|
+ * | Fail | Pass | Return Fail |
+ * |---------|-----------|-------------------------------|
+ * | Pass | Fail | Return Fail |
+ * |---------|-----------|-------------------------------|
+ * | Pass | Pass | rx = |
+ * | | | min(primary.rx, secondary.rx)|
+ * | | | tx = primary.tx |
+ * | | | read_delay = |
+ * | | | min(primary.read_delay, |
+ * | | | secondary.read_delay) |
+ * |-----------------------------------------------------|
+ */
+
+ /* Primary backup: Decrement TX from high window end */
+
+ backuppoint.tx = CQSPI_PHY_TX_LOOKUP_HIGH_END;
+ do {
+ dev_dbg(dev,
+ "Searching for Backup Primary rxlow on TX = %d\n",
+ backuppoint.tx);
+ backuppoint.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_rx_low_ddr(f_pdata, mem, &backuppoint);
+ if (!ret)
+ break;
+ backuppoint.tx -= CQSPI_PHY_DDR_SEARCH_STEP;
+ } while (backuppoint.tx >= CQSPI_PHY_TX_LOOKUP_HIGH_START);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Backup Primary rxlow: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ /* Secondary backup: Verify at offset TX */
+
+ if (backuppoint.tx >=
+ (CQSPI_PHY_TX_LOOKUP_HIGH_START + CQSPI_PHY_SEARCH_OFFSET))
+ srxlow.tx = backuppoint.tx - CQSPI_PHY_SEARCH_OFFSET;
+ else
+ srxlow.tx = CQSPI_PHY_TX_LOOKUP_HIGH_START;
+ dev_dbg(dev,
+ "Searching for Backup Secondary rxlow on TX = %d\n",
+ srxlow.tx);
+ srxlow.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_rx_low_ddr(f_pdata, mem, &srxlow);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Backup Secondary rxlow: RX: %d TX: %d RD: %d\n",
+ srxlow.rx, srxlow.tx, srxlow.read_delay);
+
+ backuppoint.rx = min(backuppoint.rx, srxlow.rx);
+ backuppoint.read_delay =
+ min(backuppoint.read_delay, srxlow.read_delay);
+ dev_dbg(dev, "Backup Final rxlow: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ if (backuppoint.rx < rxlow.rx) {
+ rxlow = backuppoint;
+ dev_dbg(dev, "Updating rxlow to the one at TX = %d\n",
+ backuppoint.tx);
+ }
+ dev_dbg(dev, "Final rxlow: RX: %d TX: %d RD: %d\n", rxlow.rx,
+ rxlow.tx, rxlow.read_delay);
+
+ /* Backup rxhigh: Search at fixed backup TX */
+
+ /*
+ * rx
+ * 127 ^
+ * | xxxxx +++++++++++++++++++|
+ * | xxxxxx ++++++++++++++++++|
+ * search | xxxxxxx +++++++++++++++++|
+ * rxhigh -------------------------------------->|
+ * on fixed | xxxxxxxxx +++++++++++++++|
+ * tx | xxxxxxxxxx ++++++++++++++|
+ * | xxxxxxxxxxx +++++++++++++|
+ * | xxxxxxxxxxxx +++++++++++++
+ * | xxxxxxxxxxxxx ++++++++++++
+ * | xxxxxxxxxxxxxx +++++++++++
+ * | xxxxxxxxxxxxxxx ++++++++++
+ * | xxxxxxxxxxxxxxxx +++++++++
+ * | xxxxxxxxxxxxxxxxx ++++++++
+ * | xxxxxxxxxxxxxxxxxx +++++++
+ * |
+ * -------------------------------------------> tx
+ * 0 127
+ *
+ * |-----------------------------------------------------|
+ * | Primary | Secondary | Final |
+ * | Search | Search | Point |
+ * |---------|-----------|-------------------------------|
+ * | Fail | Fail | Return Fail |
+ * |---------|-----------|-------------------------------|
+ * | Fail | Pass | Choose Secondary |
+ * |---------|-----------|-------------------------------|
+ * | Pass | Fail | Choose Primary |
+ * |---------|-----------|-------------------------------|
+ * | Pass | Pass | if (secondary.rx > primary.rx)|
+ * | | | Choose Secondary |
+ * | | | else |
+ * | | | Choose Primary |
+ * |-----------------------------------------------------|
+ */
+
+ /* Primary backup rxhigh: Use backup TX, decrement from max read_delay */
+
+ dev_dbg(dev, "Searching for Backup Primary rxhigh on TX = %d\n",
+ backuppoint.tx);
+ backuppoint.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_rx_high_ddr(f_pdata, mem, &backuppoint);
+ if (ret)
+ primary = false;
+ dev_dbg(dev, "Backup Primary rxhigh: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ /* Secondary backup rxhigh: Verify at offset TX */
+
+ if (backuppoint.tx >=
+ (CQSPI_PHY_TX_LOOKUP_HIGH_START + CQSPI_PHY_SEARCH_OFFSET))
+ srxhigh.tx = backuppoint.tx - CQSPI_PHY_SEARCH_OFFSET;
+ else
+ srxhigh.tx = CQSPI_PHY_TX_LOOKUP_HIGH_START;
+ dev_dbg(dev,
+ "Searching for Backup Secondary rxhigh on TX = %d\n",
+ srxhigh.tx);
+ srxhigh.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_rx_high_ddr(f_pdata, mem, &srxhigh);
+ if (ret)
+ secondary = false;
+ dev_dbg(dev, "Backup Secondary rxhigh: RX: %d TX: %d RD: %d\n",
+ srxhigh.rx, srxhigh.tx, srxhigh.read_delay);
+
+ if (!primary && !secondary)
+ goto out;
+ else if (!primary)
+ backuppoint = srxhigh;
+ else if (secondary && srxhigh.rx > backuppoint.rx)
+ backuppoint = srxhigh;
+ dev_dbg(dev, "Backup Final rxhigh: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ if (backuppoint.rx > rxhigh.rx) {
+ rxhigh = backuppoint;
+ dev_dbg(dev, "Updating rxhigh to the one at TX = %d\n",
+ backuppoint.tx);
+ }
+ dev_dbg(dev, "Final rxhigh: RX: %d TX: %d RD: %d\n", rxhigh.rx,
+ rxhigh.tx, rxhigh.read_delay);
+ }
+
+ /* Golden txlow: Fix RX at 1/4 of RX window, search TX lower bound */
+
+ /*
+ * rx
+ * 127 ^
+ * |
+ * rxhigh --------->xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * fix rx | xxxxxxxxxxxxx ++++++++++++
+ * 1/4 b/w ---------><------->xxxxx +++++++++++
+ * rxlow and | xxxx|xxxxxxxxxx ++++++++++
+ * rxhigh | xxxx|xxxxxxxxxxx +++++++++
+ * | xxxx|xxxxxxxxxxxx ++++++++
+ * rxlow --------->xxxx|xxxxxxxxxxxxx +++++++
+ * | |
+ * ------------|------------------------------> tx
+ * 0 | 127
+ * search
+ * txlow
+ */
+
+ rx_window = rxhigh.rx - rxlow.rx;
+ txlow.rx = rxlow.rx + (rx_window / 4);
+ dev_dbg(dev, "Searching for Golden txlow on RX = %d\n", txlow.rx);
+ txlow.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_tx_low_ddr(f_pdata, mem, &txlow);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Golden txlow: RX: %d TX: %d RD: %d\n", txlow.rx, txlow.tx,
+ txlow.read_delay);
+
+ /* Golden txhigh: Same RX as txlow, decrement from max read_delay */
+
+ /*
+ * rx
+ * 127 ^
+ * |
+ * rxhigh --------->xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * fix rx | xxxxxxxxxxxxx ++++++++++++
+ * 1/4 b/w --------------------------------><----->
+ * rxlow and | xxxxxxxxxxxxxxx ++++++|+++
+ * rxhigh | xxxxxxxxxxxxxxxx +++++|+++
+ * | xxxxxxxxxxxxxxxxx ++++|+++
+ * rxlow --------->xxxxxxxxxxxxxxxxxx +++|+++
+ * | |
+ * ----------------------------------|--------> tx
+ * 0 | 127
+ * search
+ * txhigh
+ */
+
+ txhigh.rx = txlow.rx;
+ dev_dbg(dev, "Searching for Golden txhigh on RX = %d\n", txhigh.rx);
+ txhigh.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_tx_high_ddr(f_pdata, mem, &txhigh);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Golden txhigh: RX: %d TX: %d RD: %d\n", txhigh.rx,
+ txhigh.tx, txhigh.read_delay);
+
+ /* If txlow/txhigh at same read_delay, search backup at 3/4 RX window */
+
+ if (txlow.read_delay == txhigh.read_delay) {
+ /* Backup txlow: Fix RX at 3/4 of RX window */
+
+ /*
+ * rx
+ * 127 ^
+ * |
+ * rxhigh --------->xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * fix rx | xxxxxxx ++++++++++++++++++
+ * 3/4 b/w ---------><----->x +++++++++++++++++
+ * rxlow and | xxxx|xxxx ++++++++++++++++
+ * rxhigh | xxxx|xxxxx +++++++++++++++
+ * | xxxx|xxxxxx ++++++++++++++
+ * | xxxx|xxxxxxx +++++++++++++
+ * | xxxx|xxxxxxxx ++++++++++++
+ * | xxxx|xxxxxxxxx +++++++++++
+ * | xxxx|xxxxxxxxxx ++++++++++
+ * | xxxx|xxxxxxxxxxx +++++++++
+ * | xxxx|xxxxxxxxxxxx ++++++++
+ * rxlow --------->xxxx|xxxxxxxxxxxxx +++++++
+ * | |
+ * ------------|------------------------------> tx
+ * 0 | 127
+ * search
+ * txlow
+ */
+
+ dev_dbg(dev, "txlow and txhigh at the same read delay.\n");
+ backuppoint.rx = rxlow.rx + ((rx_window * 3) / 4);
+ dev_dbg(dev, "Searching for Backup txlow on RX = %d\n",
+ backuppoint.rx);
+ backuppoint.read_delay = CQSPI_PHY_INIT_RD;
+ ret = cqspi_find_tx_low_ddr(f_pdata, mem, &backuppoint);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Backup txlow: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ if (backuppoint.tx < txlow.tx) {
+ txlow = backuppoint;
+ dev_dbg(dev, "Updating txlow with the one at RX = %d\n",
+ backuppoint.rx);
+ }
+ dev_dbg(dev, "Final txlow: RX: %d TX: %d RD: %d\n", txlow.rx,
+ txlow.tx, txlow.read_delay);
+
+ /* Backup txhigh: Same RX as backup txlow, decrement from max */
+
+ /*
+ * rx
+ * 127 ^
+ * |
+ * rxhigh --------->xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * fix rx | xxxxxxx ++++++++++++++++++
+ * 3/4 b/w ------------------------------><------->
+ * rxlow and | xxxxxxxxx +++++++++++|++++
+ * rxhigh | xxxxxxxxxx ++++++++++|++++
+ * | xxxxxxxxxxx +++++++++|++++
+ * | xxxxxxxxxxxx ++++++++|++++
+ * | xxxxxxxxxxxxx +++++++|++++
+ * | xxxxxxxxxxxxxx ++++++|++++
+ * | xxxxxxxxxxxxxxx +++++|++++
+ * | xxxxxxxxxxxxxxxx ++++|++++
+ * | xxxxxxxxxxxxxxxxx +++|++++
+ * rxlow --------->xxxxxxxxxxxxxxxxxx ++|++++
+ * | |
+ * ---------------------------------|---------> tx
+ * 0 | 127
+ * search
+ * txhigh
+ */
+
+ dev_dbg(dev, "Searching for Backup txhigh on RX = %d\n",
+ backuppoint.rx);
+ backuppoint.read_delay = CQSPI_PHY_MAX_RD;
+ ret = cqspi_find_tx_high_ddr(f_pdata, mem, &backuppoint);
+ if (ret)
+ goto out;
+ dev_dbg(dev, "Backup txhigh: RX: %d TX: %d RD: %d\n",
+ backuppoint.rx, backuppoint.tx, backuppoint.read_delay);
+
+ if (backuppoint.tx > txhigh.tx) {
+ txhigh = backuppoint;
+ dev_dbg(dev,
+ "Updating txhigh with the one at RX = %d\n",
+ backuppoint.rx);
+ }
+ dev_dbg(dev, "Final txhigh: RX: %d TX: %d RD: %d\n", txhigh.rx,
+ txhigh.tx, txhigh.read_delay);
+ }
+
+ /* Corner points: Define and verify bottomleft and topright boundaries */
+
+ /*
+ * rx
+ * 127 ^
+ * | topright
+ * | *
+ * rxhigh -----------xxxxx ++++++++++++++++++++
+ * | xxxxxx +++++++++++++++++++
+ * | xxxxxxx ++++++++++++++++++
+ * | xxxxxxxx +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * | xxxxxxxxxxxxx ++++++++++++
+ * | xxxxxxxxxxxxxx +++++++++++
+ * | xxxxxxxxxxxxxxx ++++++++++
+ * | xxxxxxxxxxxxxxxx +++++++++
+ * | xxxxxxxxxxxxxxxxx ++++++++
+ * rxlow -----------xxxxxxxxxxxxxxxxxx +++++++
+ * | * |
+ * | bottom|left |
+ * --------|----------------------------|---> tx
+ * 0 | | 127
+ * | |
+ * txlow txhigh
+ *
+ * Verification: Test point 4 taps inside each corner, adjust
+ * read_delay ±1 if needed to ensure valid corners for gap search.
+ */
+
+ bottomleft.tx = txlow.tx;
+ bottomleft.rx = rxlow.rx;
+ if (txlow.read_delay <= rxlow.read_delay)
+ bottomleft.read_delay = txlow.read_delay;
+ else
+ bottomleft.read_delay = rxlow.read_delay;
+
+ /* Verify bottomleft: Test 4 taps inside, adjust read_delay if needed */
+ backupcornerpoint = bottomleft;
+ backupcornerpoint.tx += 4;
+ backupcornerpoint.rx += 4;
+ ret = cqspi_phy_apply_setting(f_pdata, &backupcornerpoint);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ backupcornerpoint.read_delay--;
+ ret = cqspi_phy_apply_setting(f_pdata, &backupcornerpoint);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ }
+
+ if (ret)
+ goto out;
+
+ bottomleft.read_delay = backupcornerpoint.read_delay;
+
+ topright.tx = txhigh.tx;
+ topright.rx = rxhigh.rx;
+ if (txhigh.read_delay >= rxhigh.read_delay)
+ topright.read_delay = txhigh.read_delay;
+ else
+ topright.read_delay = rxhigh.read_delay;
+
+ /* Verify topright: Test 4 taps inside, adjust read_delay if needed */
+ backupcornerpoint = topright;
+ backupcornerpoint.tx -= 4;
+ backupcornerpoint.rx -= 4;
+ ret = cqspi_phy_apply_setting(f_pdata, &backupcornerpoint);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ backupcornerpoint.read_delay++;
+ ret = cqspi_phy_apply_setting(f_pdata, &backupcornerpoint);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ }
+
+ if (ret)
+ goto out;
+
+ topright.read_delay = backupcornerpoint.read_delay;
+
+ dev_dbg(dev, "topright: RX: %d TX: %d RD: %d\n", topright.rx,
+ topright.tx, topright.read_delay);
+ dev_dbg(dev, "bottomleft: RX: %d TX: %d RD: %d\n", bottomleft.rx,
+ bottomleft.tx, bottomleft.read_delay);
+ cqspi_phy_find_gaplow_ddr(f_pdata, mem, &bottomleft, &topright, &gaplow);
+ dev_dbg(dev, "gaplow: RX: %d TX: %d RD: %d\n", gaplow.rx, gaplow.tx,
+ gaplow.read_delay);
+
+ /* Final point selection: Handle single vs dual passing regions */
+
+ if (bottomleft.read_delay == topright.read_delay) {
+ /*
+ * Single region: Use midpoint with temperature compensation.
+ * Gaplow approximates upper boundary of valid region.
+ *
+ * rx
+ * 127 ^
+ * | gaplow (approx. topright)
+ * | |
+ * rxhigh -----------xxxxxxx| failing
+ * | xxxxxxx| region
+ * | xxxxxxx| <--------------->
+ * | xxxxxxx| +++++++++++++++++
+ * | xxxxxxxxx ++++++++++++++++
+ * | xxxxxxxxxx +++++++++++++++
+ * | xxxxxxxxxxx ++++++++++++++
+ * | xxxxxxxxxxxx +++++++++++++
+ * | xxxxxxxxxxxxx ++++++++++++
+ * | xxxxxxxxxxxxxx +++++++++++
+ * | xxxxxxxxxxxxxxx ++++++++++
+ * | xxxxxxxxxxxxxxxx +++++++++
+ * | xxxxxxxxxxxxxxxxx ++++++++
+ * rxlow -----------xxxxxxxxxxxxxxxxxx +++++++
+ * | * |
+ * | bottom|left |
+ * --------|----------------------------|---> tx
+ * 0 | | 127
+ * | |
+ * txlow txhigh
+ * (same read_delay)
+ *
+ * Temperature compensation: Valid region shifts with temp.
+ * Offset = region_size / (330 / (temp - 42°C))
+ * Factor 330 is empirically determined for this hardware.
+ */
+
+ dev_dbg(dev,
+ "bottomleft and topright at the same read delay.\n");
+
+ topright = gaplow;
+ searchpoint.read_delay = bottomleft.read_delay;
+ searchpoint.tx =
+ bottomleft.tx + ((topright.tx - bottomleft.tx) / 2);
+ searchpoint.rx =
+ bottomleft.rx + ((topright.rx - bottomleft.rx) / 2);
+
+ ret = cqspi_get_temp(&temp);
+ if (ret) {
+ /* Assume room temperature if sensor unavailable */
+ dev_dbg(dev,
+ "Unable to get temperature. Assuming room temperature\n");
+ temp = CQSPI_PHY_DEFAULT_TEMP;
+ }
+
+ if (temp < CQSPI_PHY_MIN_TEMP || temp > CQSPI_PHY_MAX_TEMP) {
+ dev_err(dev,
+ "Temperature outside operating range: %dC\n",
+ temp);
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (temp == CQSPI_PHY_MID_TEMP)
+ temp++; /* Avoid divide-by-zero */
+ dev_dbg(dev, "Temperature: %dC\n", temp);
+
+ /*
+ * Apply temperature offset: positive at high temp, negative at low.
+ * Compute the divisor once and apply to both TX and RX. Use int
+ * arithmetic throughout to avoid u8 wrapping on negative offsets.
+ */
+ temp = 330 / (temp - CQSPI_PHY_MID_TEMP);
+ searchpoint.tx = clamp((int)searchpoint.tx +
+ (topright.tx - bottomleft.tx) / temp,
+ 0, CQSPI_PHY_MAX_DELAY);
+ searchpoint.rx = clamp((int)searchpoint.rx +
+ (topright.rx - bottomleft.rx) / temp,
+ 0, CQSPI_PHY_MAX_DELAY);
+ } else {
+ /*
+ * Dual regions: Gap separates two valid regions, choose larger.
+ *
+ * rx
+ * 127 ^
+ * | topright
+ * | *
+ * rxhigh -----------xxxxx +++++++++++++++++++|
+ * | xxxxxx <region 2> ++++++++|
+ * | xxxxxxx +++++++++++++++++|
+ * | xxxxxxxx ++++++++++++++++|
+ * | xxxxxxxxx +++++++++++++++|
+ * | xxxxxxxxxx ++++++++++++++|
+ * | failing |
+ * | region |
+ * | xxxxxxxxxxxxx +++++++++++|
+ * | xxxxxxxxxxxxxx ++++++++++|
+ * | xxxxxxxxxxxxxxx +++++++++|
+ * | xxxxxxxxxxxxxxxx ++++++++|
+ * | xxxxxxxxx <region 1> +++++++|
+ * rxlow -----------xxxxxxxxxxxxxxxxxx ++++++|
+ * | * |
+ * | bottom|left |
+ * --------|----------------------------|---> tx
+ * 0 | | 127
+ * | |
+ * txlow txhigh
+ *
+ * Strategy: Compare Manhattan distances from gap boundaries to
+ * corners. Choose corner furthest from gap (larger region).
+ * Apply 16-tap margin inward, scale RX proportionally.
+ */
+
+ cqspi_phy_find_gaphigh_ddr(f_pdata, mem, &bottomleft,
+ &topright, &gaphigh);
+ dev_dbg(dev, "gaphigh: RX: %d TX: %d RD: %d\n", gaphigh.rx,
+ gaphigh.tx, gaphigh.read_delay);
+
+ if (topright.tx == bottomleft.tx) {
+ dev_err(dev, "zero TX span in dual-region: cannot compute search point\n");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* Compare Manhattan distances: choose corner furthest from gap */
+ if ((abs(gaplow.tx - bottomleft.tx) +
+ abs(gaplow.rx - bottomleft.rx)) <
+ (abs(gaphigh.tx - topright.tx) +
+ abs(gaphigh.rx - topright.rx))) {
+ /* Topright further: Use Region 2, 16 taps inward */
+ searchpoint = topright;
+ searchpoint.tx = clamp((int)topright.tx - 16,
+ 0, CQSPI_PHY_MAX_DELAY);
+ searchpoint.rx = clamp((int)topright.rx -
+ (16 * (topright.rx - bottomleft.rx)) /
+ (topright.tx - bottomleft.tx),
+ 0, CQSPI_PHY_MAX_DELAY);
+ } else {
+ /* Bottomleft further: Use Region 1, 16 taps inward */
+ searchpoint = bottomleft;
+ searchpoint.tx = clamp((int)bottomleft.tx + 16,
+ 0, CQSPI_PHY_MAX_DELAY);
+ searchpoint.rx = clamp((int)bottomleft.rx +
+ (16 * (topright.rx - bottomleft.rx)) /
+ (topright.tx - bottomleft.tx),
+ 0, CQSPI_PHY_MAX_DELAY);
+ }
+ }
+
+ /* Apply and verify final tuning point */
+ dev_dbg(dev, "Final tuning point: RX: %d TX: %d RD: %d\n",
+ searchpoint.rx, searchpoint.tx, searchpoint.read_delay);
+ ret = cqspi_phy_apply_setting(f_pdata, &searchpoint);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ dev_err(dev,
+ "Failed to find pattern at final calibration point\n");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ f_pdata->phy_setting.read_delay = searchpoint.read_delay;
+ f_pdata->phy_setting.rx = searchpoint.rx;
+ f_pdata->phy_setting.tx = searchpoint.tx;
+out:
+ if (ret)
+ f_pdata->use_tuned_phy = false;
+
+ return ret;
+}
+
+static int cqspi_phy_tuning_sdr(struct cqspi_flash_pdata *f_pdata,
+ struct spi_mem *mem)
+{
+ struct cqspi_st *cqspi = f_pdata->cqspi;
+ struct device *dev = &cqspi->pdev->dev;
+ struct phy_setting rxlow, rxhigh, first, second, final;
+ u8 window1 = 0;
+ u8 window2 = 0;
+ int ret;
+
+ /*
+ * SDR tuning: 1D search for optimal RX delay (TX less critical).
+ * Find two consecutive windows, choose larger, use midpoint.
+ *
+ * rx
+ * 127 ^
+ * | |-----window at----------|
+ * | |-----read_delay = n+1---|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | rxlow(n+1) midpoint rxhigh(n+1)
+ * |
+ * | |---window at--------|
+ * | |---read_delay = n---|
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * | rxlow(n) midpoint rxhigh(n)
+ * |
+ * -----------------------------------------> tx
+ * 0 127
+ * read_delay=n read_delay=n+1
+ */
+
+ f_pdata->use_tuned_phy = true;
+ cqspi_phy_reset_setting(&rxlow);
+ cqspi_phy_reset_setting(&rxhigh);
+ cqspi_phy_reset_setting(&first);
+
+ /* First window: Find rxlow by incrementing read_delay from 0 */
+
+ /*
+ * rx
+ * 127 ^
+ * | |xxxxxxxxxxxxxxxxxxxx|
+ * search | |xxxxxxxxxxxxxxxxxxxx|
+ * rxlow | |xxxxxxxxxxxxxxxxxxxx|
+ * increasing | |xxxxxxxxxxxxxxxxxxxx|
+ * --------->|xxxxxxxxxxxxxxxxxxxx|
+ * read_delay | |xxxxxxxxxxxxxxxxxxx|
+ * until found | |xxxxxxxxxxxxxxxxxxx|
+ * | rxlow
+ * -----------------------------------------> tx
+ * 0 tx fixed at 127
+ */
+
+ do {
+ ret = cqspi_find_rx_low_sdr(f_pdata, mem, &rxlow);
+
+ if (ret)
+ rxlow.read_delay++;
+ } while (ret && rxlow.read_delay <= CQSPI_PHY_MAX_RD);
+
+ if (ret)
+ goto out;
+
+ /* Find rxhigh: Decrement from RX=127 at same read_delay */
+
+ /*
+ * rx
+ * 127 ^ search rxhigh
+ * | (decrement from
+ * | 127 until found)
+ * | |
+ * | |
+ * | v
+ * | |------------------------|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | rxlow rxhigh
+ * -----------------------------------------> tx
+ * 0 tx fixed at 127
+ */
+
+ rxhigh.read_delay = rxlow.read_delay;
+ ret = cqspi_find_rx_high_sdr(f_pdata, mem, &rxhigh, rxlow.rx);
+ if (ret)
+ goto out;
+
+ /* Calculate first window midpoint for max margin */
+
+ /*
+ * rx
+ * 127 ^
+ * | |--------window1---------|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxx * xxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | rxlow ^ rxhigh
+ * ----------------------|------------------> tx
+ * 0 | tx fixed at 127
+ * window1/2
+ */
+
+ first.read_delay = rxlow.read_delay;
+ window1 = rxhigh.rx - rxlow.rx;
+ first.rx = rxlow.rx + (window1 / 2);
+
+ dev_dbg(dev, "First tuning point: RX: %d TX: %d RD: %d\n", first.rx,
+ first.tx, first.read_delay);
+ ret = cqspi_phy_apply_setting(f_pdata, &first);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret || first.read_delay > CQSPI_PHY_MAX_RD)
+ goto out;
+
+ /* Second window: Search at read_delay+1, may differ in size */
+
+ /*
+ * rx
+ * 127 ^
+ * | |-------|
+ * | |xxxxxxx|
+ * | |xxxxxxx|
+ * | |xxxxxxx|
+ * | |xxxxxxx|
+ * | |xxxxxxx|
+ * | rxlow rxhigh
+ * -----------------------------------------> tx
+ * 0
+ * read_delay = n (smaller window)
+ *
+ * rx
+ * 127 ^
+ * | |-----------------|
+ * | |xxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxx|
+ * | rxlow rxhigh
+ * -----------------------------------------> tx
+ * 0
+ * read_delay = n+1 (larger window - better)
+ */
+
+ cqspi_phy_reset_setting(&rxlow);
+ cqspi_phy_reset_setting(&rxhigh);
+ cqspi_phy_reset_setting(&second);
+
+ rxlow.read_delay = first.read_delay + 1;
+ if (rxlow.read_delay > CQSPI_PHY_MAX_RD)
+ goto compare;
+
+ ret = cqspi_find_rx_low_sdr(f_pdata, mem, &rxlow);
+ if (ret)
+ goto compare;
+
+ rxhigh.read_delay = rxlow.read_delay;
+ ret = cqspi_find_rx_high_sdr(f_pdata, mem, &rxhigh, rxlow.rx);
+ if (ret)
+ goto compare;
+
+ /* Calculate second window midpoint */
+
+ /*
+ * rx
+ * 127 ^
+ * | |--------window2---------|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxx * xxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | |xxxxxxxxxxxxxxxxxxxxxxxx|
+ * | rxlow ^ rxhigh
+ * ----------------------|------------------> tx
+ * 0 | tx fixed at 127
+ * window2/2
+ * read_delay = n+1
+ */
+
+ window2 = rxhigh.rx - rxlow.rx;
+ second.rx = rxlow.rx + (window2 / 2);
+ second.read_delay = rxlow.read_delay;
+
+ dev_dbg(dev, "Second tuning point: RX: %d TX: %d RD: %d\n", second.rx,
+ second.tx, second.read_delay);
+ ret = cqspi_phy_apply_setting(f_pdata, &second);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret || second.read_delay > CQSPI_PHY_MAX_RD)
+ window2 = 0;
+
+ /* Window comparison: Choose larger window for better margin */
+
+compare:
+ cqspi_phy_reset_setting(&final);
+ if (window2 > window1) {
+ final.rx = second.rx;
+ final.read_delay = second.read_delay;
+ } else {
+ final.rx = first.rx;
+ final.read_delay = first.read_delay;
+ }
+
+ /* Apply and verify final tuning point */
+
+ dev_dbg(dev, "Final tuning point: RX: %d TX: %d RD: %d\n", final.rx,
+ final.tx, final.read_delay);
+ ret = cqspi_phy_apply_setting(f_pdata, &final);
+ if (!ret)
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+
+ if (ret) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ f_pdata->phy_setting.read_delay = final.read_delay;
+ f_pdata->phy_setting.rx = final.rx;
+ f_pdata->phy_setting.tx = final.tx;
+
+out:
+ if (ret)
+ f_pdata->use_tuned_phy = false;
+
+ return ret;
+}
+
+static int cqspi_am654_ospi_execute_tuning(struct spi_mem *mem,
+ struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op)
+{
+ struct cqspi_st *cqspi =
+ spi_controller_get_devdata(mem->spi->controller);
+ struct cqspi_flash_pdata *f_pdata;
+ struct device *dev = &cqspi->pdev->dev;
+ int ret = 0;
+
+ f_pdata = &cqspi->f_pdata[spi_get_chipselect(mem->spi, 0)];
+
+ /*
+ * spi-max-post-config-frequency-hz must be present for PHY tuning.
+ * If absent, post_config_max_speed_hz is zero and there is no
+ * calibration target, so skip tuning gracefully.
+ */
+ if (!mem->spi->post_config_max_speed_hz) {
+ dev_dbg(dev,
+ "No post-config frequency configured, skipping tuning\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (write_op) {
+ /*
+ * For NAND: write the calibration pattern to the page cache.
+ * The write op runs at the conservative base speed (spi->max_speed_hz)
+ * so the write itself is reliable before PHY calibration.
+ */
+ ret = cqspi_write_pattern_to_cache(f_pdata, mem, write_op);
+ if (ret) {
+ dev_warn(dev,
+ "failed to write pattern to cache: %d, skipping tuning\n",
+ ret);
+ goto out;
+ }
+
+ f_pdata->phy_write_op = *write_op;
+ } else {
+ if (!f_pdata->phy_pattern_partition_valid) {
+ dev_warn(dev, "pattern partition not found, skipping tuning\n");
+ return -EOPNOTSUPP;
+ }
+ }
+
+ /*
+ * Build the read op template for pattern verification. For NOR, point
+ * at the pre-programmed pattern partition rather than the caller's
+ * address without altering the caller's template.
+ */
+ f_pdata->phy_read_op = *read_op;
+ if (!write_op)
+ f_pdata->phy_read_op.addr.val =
+ f_pdata->phy_pattern_partition_offset;
+
+ f_pdata->phy_read_op.max_freq = mem->spi->max_speed_hz;
+
+ ret = cqspi_phy_check_pattern(f_pdata, mem);
+ if (ret) {
+ dev_err(dev, "pattern not found: %d, skipping tuning\n", ret);
+ goto out;
+ }
+
+ f_pdata->phy_read_op.max_freq = mem->spi->post_config_max_speed_hz;
+
+ if (read_op->cmd.dtr || read_op->addr.dtr || read_op->dummy.dtr ||
+ read_op->data.dtr) {
+ f_pdata->use_dqs = true;
+ cqspi_phy_pre_config(cqspi, f_pdata, false);
+ ret = cqspi_phy_tuning_ddr(f_pdata, mem);
+ } else {
+ f_pdata->use_dqs = false;
+ cqspi_phy_pre_config(cqspi, f_pdata, true);
+ ret = cqspi_phy_tuning_sdr(f_pdata, mem);
+ }
+
+ if (ret)
+ dev_warn(dev, "tuning failed: %d\n", ret);
+
+ cqspi_phy_post_config(cqspi, f_pdata->read_delay);
+
+out:
+ if (!ret) {
+ read_op->max_freq = mem->spi->post_config_max_speed_hz;
+ if (write_op) {
+ write_op->max_freq = mem->spi->post_config_max_speed_hz;
+ f_pdata->phy_write_op.max_freq =
+ mem->spi->post_config_max_speed_hz;
+ }
+ }
+
+ return ret;
+}
+
+static int cqspi_mem_op_execute_tuning(struct spi_mem *mem,
+ struct spi_mem_op *read_op,
+ struct spi_mem_op *write_op)
+{
+ struct cqspi_st *cqspi =
+ spi_controller_get_devdata(mem->spi->controller);
+
+ if (!cqspi->ddata || !cqspi->ddata->execute_tuning)
+ return -EOPNOTSUPP;
+
+ return cqspi->ddata->execute_tuning(mem, read_op, write_op);
+}
+
static int cqspi_of_get_flash_pdata(struct platform_device *pdev,
struct cqspi_flash_pdata *f_pdata,
struct device_node *np)
{
+ struct device_node *part_np;
+
if (of_property_read_u32(np, "cdns,read-delay", &f_pdata->read_delay)) {
dev_err(&pdev->dev, "couldn't determine read-delay\n");
return -ENXIO;
@@ -1588,9 +3331,12 @@ static int cqspi_of_get_flash_pdata(struct platform_device *pdev,
return -ENXIO;
}
- if (of_property_read_u32(np, "spi-max-frequency", &f_pdata->clk_rate)) {
- dev_err(&pdev->dev, "couldn't determine spi-max-frequency\n");
- return -ENXIO;
+ part_np = of_parse_phandle(np, "spi-phy-pattern-partition", 0);
+ if (part_np) {
+ f_pdata->phy_pattern_partition_valid =
+ !of_property_read_u32_index(part_np, "reg", 0,
+ &f_pdata->phy_pattern_partition_offset);
+ of_node_put(part_np);
}
return 0;
@@ -1740,6 +3486,7 @@ static const struct spi_controller_mem_ops cqspi_mem_ops = {
.exec_op = cqspi_exec_mem_op,
.get_name = cqspi_get_name,
.supports_op = cqspi_supports_mem_op,
+ .execute_tuning = cqspi_mem_op_execute_tuning,
};
static const struct spi_controller_mem_caps cqspi_mem_caps = {
@@ -2108,6 +3855,7 @@ static const struct cqspi_driver_platdata k2g_qspi = {
static const struct cqspi_driver_platdata am654_ospi = {
.hwcaps_mask = CQSPI_SUPPORTS_OCTAL | CQSPI_SUPPORTS_QUAD,
.quirks = CQSPI_NEEDS_WR_DELAY,
+ .execute_tuning = cqspi_am654_ospi_execute_tuning,
};
static const struct cqspi_driver_platdata intel_lgm_qspi = {
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 09/18] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383)
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (7 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
` (8 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Erratum i2383 on AM654 locks the address phase in PHY DDR mode when a
2-byte column address is used. DDR PHY tuning must not be attempted for
such operations; non-PHY DDR usage is unaffected. [0]
Add CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR quirk and check it in
cqspi_am654_ospi_execute_tuning(). When the erratum applies, return 0
with read_op->max_freq cleared — the op-skip signal that tells the
caller to try another op variant.
[0] https://www.ti.com/lit/er/sprz544c/sprz544c.pdf
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 43 +++++++++++++++++++++----------
1 file changed, 29 insertions(+), 14 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index f7cda1bc7139..fc33bbf56dd6 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -37,19 +37,20 @@
static_assert(CQSPI_MAX_CHIPSELECT <= SPI_DEVICE_CS_CNT_MAX);
/* Quirks */
-#define CQSPI_NEEDS_WR_DELAY BIT(0)
-#define CQSPI_DISABLE_DAC_MODE BIT(1)
-#define CQSPI_SUPPORT_EXTERNAL_DMA BIT(2)
-#define CQSPI_NO_SUPPORT_WR_COMPLETION BIT(3)
-#define CQSPI_SLOW_SRAM BIT(4)
-#define CQSPI_NEEDS_APB_AHB_HAZARD_WAR BIT(5)
-#define CQSPI_RD_NO_IRQ BIT(6)
-#define CQSPI_DMA_SET_MASK BIT(7)
-#define CQSPI_SUPPORT_DEVICE_RESET BIT(8)
-#define CQSPI_DISABLE_STIG_MODE BIT(9)
-#define CQSPI_DISABLE_RUNTIME_PM BIT(10)
-#define CQSPI_NO_INDIRECT_MODE BIT(11)
-#define CQSPI_HAS_WR_PROTECT BIT(12)
+#define CQSPI_NEEDS_WR_DELAY BIT(0)
+#define CQSPI_DISABLE_DAC_MODE BIT(1)
+#define CQSPI_SUPPORT_EXTERNAL_DMA BIT(2)
+#define CQSPI_NO_SUPPORT_WR_COMPLETION BIT(3)
+#define CQSPI_SLOW_SRAM BIT(4)
+#define CQSPI_NEEDS_APB_AHB_HAZARD_WAR BIT(5)
+#define CQSPI_RD_NO_IRQ BIT(6)
+#define CQSPI_DMA_SET_MASK BIT(7)
+#define CQSPI_SUPPORT_DEVICE_RESET BIT(8)
+#define CQSPI_DISABLE_STIG_MODE BIT(9)
+#define CQSPI_DISABLE_RUNTIME_PM BIT(10)
+#define CQSPI_NO_INDIRECT_MODE BIT(11)
+#define CQSPI_HAS_WR_PROTECT BIT(12)
+#define CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR BIT(13)
/* Capabilities */
#define CQSPI_SUPPORTS_OCTAL BIT(0)
@@ -3216,6 +3217,20 @@ static int cqspi_am654_ospi_execute_tuning(struct spi_mem *mem,
return -EOPNOTSUPP;
}
+ /*
+ * Erratum i2383: in PHY DDR mode, a 2-byte column address locks up
+ * the address phase. Skip DDR PHY tuning for such operations.
+ */
+ if ((cqspi->ddata->quirks & CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR) &&
+ read_op->addr.nbytes == 2 &&
+ (read_op->cmd.dtr || read_op->addr.dtr || read_op->dummy.dtr ||
+ read_op->data.dtr)) {
+ dev_dbg(dev,
+ "i2383: skipping DDR PHY tuning (2-byte address)\n");
+ read_op->max_freq = 0;
+ return 0;
+ }
+
if (write_op) {
/*
* For NAND: write the calibration pattern to the page cache.
@@ -3854,7 +3869,7 @@ static const struct cqspi_driver_platdata k2g_qspi = {
static const struct cqspi_driver_platdata am654_ospi = {
.hwcaps_mask = CQSPI_SUPPORTS_OCTAL | CQSPI_SUPPORTS_QUAD,
- .quirks = CQSPI_NEEDS_WR_DELAY,
+ .quirks = CQSPI_NEEDS_WR_DELAY | CQSPI_NO_PHY_TUNING_WITH_ODDR_2B_ADDR,
.execute_tuning = cqspi_am654_ospi_execute_tuning,
};
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (8 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 09/18] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383) Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:31 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
` (7 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Extract the DMA transfer code from cqspi_direct_read_execute() into a
new cqspi_direct_read_dma() helper. Add cqspi_memcpy_fromio() to handle
non-DMA transfers, using 2-byte-aligned I/O reads for 8D-8D-8D mode to
satisfy the minimum 16-bit AHB transaction width required by the Cadence
OSPI controller in that mode.
Change cqspi_direct_read_execute() to take the full spi_mem_op and
post_config_max_speed_hz instead of separate buf/from/len parameters,
matching the interface needed by the PHY-aware version in the following
patch. Thread post_config_max_speed_hz from cqspi_mem_process() through
cqspi_read().
Transfers shorter than CQSPI_PHY_MIN_DIRECT_READ_LEN bytes always use
the memcpy path; longer transfers use DMA when available.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 75 ++++++++++++++++++++++++++-----
1 file changed, 65 insertions(+), 10 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index fc33bbf56dd6..58bca15dbd6e 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -30,6 +30,7 @@
#include <linux/spi/spi.h>
#include <linux/spi/spi-mem.h>
#include <linux/timer.h>
+#include <linux/unaligned.h>
#define CQSPI_NAME "cadence-qspi"
#define CQSPI_MAX_CHIPSELECT 4
@@ -155,6 +156,8 @@ struct cqspi_driver_platdata {
#define CQSPI_READ_TIMEOUT_MS 10
#define CQSPI_BUSYWAIT_TIMEOUT_US 500
#define CQSPI_DLL_TIMEOUT_US 300
+/* Minimum transfer length to use DMA for direct reads */
+#define CQSPI_PHY_MIN_DIRECT_READ_LEN 17
/* Runtime_pm autosuspend delay */
#define CQSPI_AUTOSUSPEND_TIMEOUT 2000
@@ -1529,8 +1532,8 @@ static void cqspi_rx_dma_callback(void *param)
complete(&cqspi->rx_dma_complete);
}
-static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
- u_char *buf, loff_t from, size_t len)
+static int cqspi_direct_read_dma(struct cqspi_flash_pdata *f_pdata, u_char *buf,
+ loff_t from, size_t len)
{
struct cqspi_st *cqspi = f_pdata->cqspi;
struct device *dev = &cqspi->pdev->dev;
@@ -1542,11 +1545,6 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
dma_addr_t dma_dst;
struct device *ddev;
- if (!cqspi->rx_chan || !virt_addr_valid(buf)) {
- memcpy_fromio(buf, cqspi->ahb_base + from, len);
- return 0;
- }
-
ddev = cqspi->rx_chan->device->dev;
dma_dst = dma_map_single(ddev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(ddev, dma_dst)) {
@@ -1588,8 +1586,63 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
return ret;
}
+static void cqspi_memcpy_fromio(const struct spi_mem_op *op, void *to,
+ const void __iomem *from, size_t count)
+{
+ if (op->data.buswidth == 8 && op->data.dtr) {
+ unsigned long from_addr = (unsigned long)from;
+
+ /* Handle unaligned start with 2-byte read */
+ if (count && !IS_ALIGNED(from_addr, 4)) {
+ put_unaligned_le16(__raw_readw(from), to);
+ from += 2;
+ to += 2;
+ count -= 2;
+ }
+
+ /*
+ * Use an explicit 32-bit read loop for the bulk; this
+ * guarantees the minimum 16-bit AHB transaction width required
+ * in 8D-8D-8D mode on all architectures.
+ */
+ while (count >= 4) {
+ put_unaligned_le32(__raw_readl(from), to);
+ from += 4;
+ to += 4;
+ count -= 4;
+ }
+
+ /* Handle remaining 2 bytes */
+ if (count)
+ put_unaligned_le16(__raw_readw(from), to);
+
+ return;
+ }
+
+ memcpy_fromio(to, from, count);
+}
+
+static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
+ const struct spi_mem_op *op,
+ u32 post_config_max_speed_hz)
+{
+ struct cqspi_st *cqspi = f_pdata->cqspi;
+ loff_t from = op->addr.val;
+ size_t len = op->data.nbytes;
+ u_char *buf = op->data.buf.in;
+
+ if (!cqspi->rx_chan || !virt_addr_valid(buf) ||
+ len < CQSPI_PHY_MIN_DIRECT_READ_LEN) {
+ cqspi_memcpy_fromio(op, buf, cqspi->ahb_base + from, len);
+ return 0;
+ }
+
+ return cqspi_direct_read_dma(f_pdata, buf, from, len);
+}
+
static ssize_t cqspi_read(struct cqspi_flash_pdata *f_pdata,
- const struct spi_mem_op *op)
+ const struct spi_mem_op *op,
+ u32 post_config_max_speed_hz)
{
struct cqspi_st *cqspi = f_pdata->cqspi;
const struct cqspi_driver_platdata *ddata = cqspi->ddata;
@@ -1605,7 +1658,8 @@ static ssize_t cqspi_read(struct cqspi_flash_pdata *f_pdata,
if ((cqspi->use_direct_mode && ((from + len) <= cqspi->ahb_size)) ||
(cqspi->ddata && cqspi->ddata->quirks & CQSPI_NO_INDIRECT_MODE))
- return cqspi_direct_read_execute(f_pdata, buf, from, len);
+ return cqspi_direct_read_execute(f_pdata, op,
+ post_config_max_speed_hz);
if (cqspi->use_dma_read && ddata && ddata->indirect_read_dma &&
virt_addr_valid(buf) && ((dma_align & CQSPI_DMA_UNALIGN) == 0))
@@ -1633,7 +1687,8 @@ static int cqspi_mem_process(struct spi_mem *mem, const struct spi_mem_op *op)
!cqspi->disable_stig_mode))
return cqspi_command_read(f_pdata, op);
- return cqspi_read(f_pdata, op);
+ return cqspi_read(f_pdata, op,
+ mem->spi->post_config_max_speed_hz);
}
if (!op->addr.nbytes || !op->data.buf.out)
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (9 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:32 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
` (6 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Add cqspi_tune_phy() to toggle PHY mode around a DMA transfer. On
enable: reprogramme the DLL from the per-CS calibration result when
the hardware values differ (handles alternating PHY reads across two
chip selects with different calibration), configure read-capture for
the tuned delay, assert PHY_EN and PHY_PIPELINE in CONFIG, and
decrement the dummy cycle count by one since the PHY data-capture
pipeline absorbs that latency. The original count is saved in
cqspi->phy_saved_dummy so the disable path restores it exactly,
handling the case where the original count was zero. On disable,
all settings are reversed using the saved count.
Add cqspi_use_tuned_phy() predicate: PHY is activated only when
calibration completed successfully and the op frequency equals the
post-config rate.
For direct reads, PHY is used only when both the start address and
end address are 16-byte aligned. Unaligned transfers fall back to
non-PHY DMA.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 126 +++++++++++++++++++++++++++++-
1 file changed, 125 insertions(+), 1 deletion(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index 58bca15dbd6e..d38a0253e4bd 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -1075,6 +1075,109 @@ static int cqspi_resync_dll(struct cqspi_st *cqspi)
return ret;
}
+static int cqspi_tune_phy(struct cqspi_flash_pdata *f_pdata, bool enable)
+{
+ struct cqspi_st *cqspi = f_pdata->cqspi;
+ void __iomem *reg_base = cqspi->iobase;
+ u32 reg;
+ u8 dummy;
+ int ret;
+
+ if (enable) {
+ ret = cqspi_wait_idle(cqspi);
+ if (ret)
+ return ret;
+
+ /*
+ * CQSPI_REG_PHY_CONFIG holds the RX/TX delay line tap counts
+ * and is a single global register with no per-CS equivalent.
+ * Reprogramme and resync the DLL when the active device's
+ * calibrated values differ from what is currently in hardware,
+ * which happens when alternating PHY reads between two chip
+ * selects with different calibration results.
+ */
+ reg = readl(reg_base + CQSPI_REG_PHY_CONFIG);
+ if (!f_pdata->dll_locked ||
+ ((reg >> CQSPI_REG_PHY_CONFIG_RX_DEL_LSB) &
+ CQSPI_REG_PHY_CONFIG_RX_DEL_MASK) != f_pdata->phy_setting.rx ||
+ ((reg >> CQSPI_REG_PHY_CONFIG_TX_DEL_LSB) &
+ CQSPI_REG_PHY_CONFIG_TX_DEL_MASK) != f_pdata->phy_setting.tx) {
+ cqspi_set_dll(reg_base, f_pdata->phy_setting.rx,
+ f_pdata->phy_setting.tx);
+ ret = cqspi_resync_dll(cqspi);
+ if (ret) {
+ f_pdata->dll_locked = false;
+ return ret;
+ }
+ f_pdata->dll_locked = true;
+ }
+
+ cqspi_readdata_capture(cqspi, !f_pdata->use_dqs, f_pdata->use_dqs,
+ f_pdata->phy_setting.read_delay);
+
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg |= CQSPI_REG_CONFIG_PHY_EN | CQSPI_REG_CONFIG_PHY_PIPELINE;
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ /*
+ * The PHY data-capture pipeline absorbs one dummy cycle's
+ * worth of latency; reduce the count to avoid over-compensation.
+ * Save the original count so the disable path can restore it
+ * exactly, regardless of whether the original was zero.
+ */
+ reg = readl(reg_base + CQSPI_REG_RD_INSTR);
+ dummy = FIELD_GET(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ reg);
+ cqspi->phy_saved_dummy = dummy;
+ if (dummy)
+ dummy--;
+ reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB);
+ reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ dummy);
+ writel(reg, reg_base + CQSPI_REG_RD_INSTR);
+
+ ret = cqspi_wait_idle(cqspi);
+ if (ret) {
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg &= ~(CQSPI_REG_CONFIG_PHY_EN |
+ CQSPI_REG_CONFIG_PHY_PIPELINE);
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+ cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
+ f_pdata->read_delay);
+ reg = readl(reg_base + CQSPI_REG_RD_INSTR);
+ reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB);
+ reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ cqspi->phy_saved_dummy);
+ writel(reg, reg_base + CQSPI_REG_RD_INSTR);
+ }
+ return ret;
+ }
+
+ /* Disable path: execute unconditionally regardless of idle state. */
+ cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
+ f_pdata->read_delay);
+
+ reg = readl(reg_base + CQSPI_REG_CONFIG);
+ reg &= ~(CQSPI_REG_CONFIG_PHY_EN |
+ CQSPI_REG_CONFIG_PHY_PIPELINE);
+ writel(reg, reg_base + CQSPI_REG_CONFIG);
+
+ reg = readl(reg_base + CQSPI_REG_RD_INSTR);
+ reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB);
+ reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
+ << CQSPI_REG_RD_INSTR_DUMMY_LSB,
+ cqspi->phy_saved_dummy);
+ writel(reg, reg_base + CQSPI_REG_RD_INSTR);
+
+ return cqspi_wait_idle(cqspi);
+}
+
static int cqspi_versal_indirect_read_dma(struct cqspi_flash_pdata *f_pdata,
u_char *rxbuf, loff_t from_addr,
size_t n_rx)
@@ -1525,6 +1628,14 @@ static ssize_t cqspi_write(struct cqspi_flash_pdata *f_pdata,
return cqspi_indirect_write_execute(f_pdata, to, buf, len);
}
+static bool cqspi_use_tuned_phy(struct cqspi_flash_pdata *f_pdata,
+ const struct spi_mem_op *op,
+ u32 post_config_max_speed_hz)
+{
+ return f_pdata->use_tuned_phy &&
+ op->max_freq == post_config_max_speed_hz;
+}
+
static void cqspi_rx_dma_callback(void *param)
{
struct cqspi_st *cqspi = param;
@@ -1630,6 +1741,7 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
loff_t from = op->addr.val;
size_t len = op->data.nbytes;
u_char *buf = op->data.buf.in;
+ int ret;
if (!cqspi->rx_chan || !virt_addr_valid(buf) ||
len < CQSPI_PHY_MIN_DIRECT_READ_LEN) {
@@ -1637,7 +1749,19 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
return 0;
}
- return cqspi_direct_read_dma(f_pdata, buf, from, len);
+ if (!cqspi_use_tuned_phy(f_pdata, op, post_config_max_speed_hz))
+ return cqspi_direct_read_dma(f_pdata, buf, from, len);
+
+ if (!IS_ALIGNED(from, 16) || !IS_ALIGNED(from + len, 16))
+ return cqspi_direct_read_dma(f_pdata, buf, from, len);
+
+ ret = cqspi_tune_phy(f_pdata, true);
+ if (ret)
+ return ret;
+ ret = cqspi_direct_read_dma(f_pdata, buf, from, len);
+ ret = cqspi_tune_phy(f_pdata, false) ?: ret;
+
+ return ret;
}
static ssize_t cqspi_read(struct cqspi_flash_pdata *f_pdata,
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (10 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:32 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 13/18] spi: cadence-quadspi: reprogram CS timing on every chip-select switch Santhosh Kumar K
` (5 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Enable PHY for indirect writes of at least CQSPI_PHY_MIN_INDIRECT_WRITE_LEN
bytes. PHY is activated only when tuning completed successfully and the
write op was calibrated at the post-config frequency (phy_write_op.max_freq
matches), which restricts PHY writes to NAND devices where a write op
template was provided to execute_tuning. NOR flash writes are unaffected.
The indirect write is cancelled before disabling PHY in the error path
so that cqspi_wait_idle() inside cqspi_tune_phy() does not time out
while the controller is still waiting for AHB data.
Thread post_config_max_speed_hz from cqspi_mem_process() through
cqspi_write() into cqspi_indirect_write_execute() for the frequency check.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 43 ++++++++++++++++++++++++++-----
1 file changed, 36 insertions(+), 7 deletions(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index d38a0253e4bd..20ece785c97a 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -158,6 +158,8 @@ struct cqspi_driver_platdata {
#define CQSPI_DLL_TIMEOUT_US 300
/* Minimum transfer length to use DMA for direct reads */
#define CQSPI_PHY_MIN_DIRECT_READ_LEN 17
+/* Minimum indirect write length to amortize PHY enable/disable overhead */
+#define CQSPI_PHY_MIN_INDIRECT_WRITE_LEN SZ_1K
/* Runtime_pm autosuspend delay */
#define CQSPI_AUTOSUSPEND_TIMEOUT 2000
@@ -1370,18 +1372,32 @@ static int cqspi_write_setup(struct cqspi_flash_pdata *f_pdata,
static int cqspi_indirect_write_execute(struct cqspi_flash_pdata *f_pdata,
loff_t to_addr, const u8 *txbuf,
- const size_t n_tx)
+ const size_t n_tx,
+ u32 post_config_max_speed_hz)
{
struct cqspi_st *cqspi = f_pdata->cqspi;
struct device *dev = &cqspi->pdev->dev;
void __iomem *reg_base = cqspi->iobase;
unsigned int remaining = n_tx;
unsigned int write_bytes;
+ bool use_tuned_phy_write;
int ret;
if (!refcount_read(&cqspi->refcount))
return -ENODEV;
+ /* Use PHY only for large writes at the calibrated rate */
+ use_tuned_phy_write = n_tx >= CQSPI_PHY_MIN_INDIRECT_WRITE_LEN &&
+ f_pdata->use_tuned_phy &&
+ f_pdata->phy_write_op.max_freq ==
+ post_config_max_speed_hz;
+
+ if (use_tuned_phy_write) {
+ ret = cqspi_tune_phy(f_pdata, true);
+ if (ret)
+ return ret;
+ }
+
writel(to_addr, reg_base + CQSPI_REG_INDIRECTWRSTARTADDR);
writel(remaining, reg_base + CQSPI_REG_INDIRECTWRBYTES);
@@ -1458,17 +1474,28 @@ static int cqspi_indirect_write_execute(struct cqspi_flash_pdata *f_pdata,
/* Clear indirect completion status */
writel(CQSPI_REG_INDIRECTWR_DONE_MASK, reg_base + CQSPI_REG_INDIRECTWR);
- cqspi_wait_idle(cqspi);
+ ret = cqspi_wait_idle(cqspi);
- return 0;
+ if (use_tuned_phy_write)
+ ret = cqspi_tune_phy(f_pdata, false) ?: ret;
+
+ return ret;
failwr:
/* Disable interrupt. */
writel(0, reg_base + CQSPI_REG_IRQMASK);
- /* Cancel the indirect write */
+ /*
+ * Cancel the indirect write before disabling PHY so that
+ * cqspi_wait_idle() inside cqspi_tune_phy() does not time out
+ * while the controller is still waiting for AHB data.
+ */
writel(CQSPI_REG_INDIRECTWR_CANCEL_MASK,
reg_base + CQSPI_REG_INDIRECTWR);
+
+ if (use_tuned_phy_write)
+ cqspi_tune_phy(f_pdata, false);
+
return ret;
}
@@ -1598,7 +1625,8 @@ static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
}
static ssize_t cqspi_write(struct cqspi_flash_pdata *f_pdata,
- const struct spi_mem_op *op)
+ const struct spi_mem_op *op,
+ u32 post_config_max_speed_hz)
{
struct cqspi_st *cqspi = f_pdata->cqspi;
loff_t to = op->addr.val;
@@ -1625,7 +1653,8 @@ static ssize_t cqspi_write(struct cqspi_flash_pdata *f_pdata,
return cqspi_wait_idle(cqspi);
}
- return cqspi_indirect_write_execute(f_pdata, to, buf, len);
+ return cqspi_indirect_write_execute(f_pdata, to, buf, len,
+ post_config_max_speed_hz);
}
static bool cqspi_use_tuned_phy(struct cqspi_flash_pdata *f_pdata,
@@ -1818,7 +1847,7 @@ static int cqspi_mem_process(struct spi_mem *mem, const struct spi_mem_op *op)
if (!op->addr.nbytes || !op->data.buf.out)
return cqspi_command_write(f_pdata, op);
- return cqspi_write(f_pdata, op);
+ return cqspi_write(f_pdata, op, mem->spi->post_config_max_speed_hz);
}
static int cqspi_exec_mem_op(struct spi_mem *mem, const struct spi_mem_op *op)
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 13/18] spi: cadence-quadspi: reprogram CS timing on every chip-select switch
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (11 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume Santhosh Kumar K
` (4 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
cqspi_configure() only reprogrammed per-device CS timing (CQSPI_REG_DELAY)
and the non-PHY read-capture register when the clock frequency changed, not
when the chip-select changed. In a multi-device setup where both devices
operate at the same frequency, a CS switch leaves the previous device's
timing parameters in hardware.
Split the update condition: baud rate divisor is updated on clock change
only (it is frequency-derived), but the per-device delay and read-capture
registers are now updated on any CS or clock switch.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index 20ece785c97a..530335f002fb 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -1610,10 +1610,19 @@ static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
cqspi_chipselect(f_pdata);
}
- /* Setup baudrate divisor and delays */
+ /* Update baudrate only on clock change. */
if (switch_ck) {
cqspi->sclk = sclk;
cqspi_config_baudrate_div(cqspi);
+ }
+
+ /*
+ * Reprogram per-device CS timing and non-PHY read capture on any
+ * chip-select or clock switch. Without this, two devices at the same
+ * frequency would each see the other's timing parameters after a CS
+ * switch, since the lazy clock-only check would never trigger.
+ */
+ if (switch_cs || switch_ck) {
cqspi_delay(f_pdata);
if (!cqspi->phy_tuning_active)
cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (12 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 13/18] spi: cadence-quadspi: reprogram CS timing on every chip-select switch Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:36 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 15/18] mtd: spinand: extract variant ranking logic into spinand_op_find_best_variant() Santhosh Kumar K
` (3 subsequent siblings)
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
The PHY DLL loses its programmed delay values and lock when the OSPI
controller is power-gated during runtime suspend. On resume,
cqspi_controller_init() reinitialises the controller but does not
touch the PHY DLL registers, leaving the DLL in an unlocked state.
Any subsequent PHY read or write would then fail at the cqspi_resync_dll()
call inside cqspi_tune_phy().
After the controller re-initialisation, iterate over all chip selects
and reprogram the DLL with each CS's stored calibrated RX/TX tap counts,
then resync to relock it. One invocation per calibrated CS allows each
calibration to be independently validated after resume. DLL resync
failure is non-fatal: a warning is emitted and resume continues, so the
system degrades to non-PHY speed rather than blocking the resume path.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/spi/spi-cadence-quadspi.c | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
index 530335f002fb..94d67e8bcfbc 100644
--- a/drivers/spi/spi-cadence-quadspi.c
+++ b/drivers/spi/spi-cadence-quadspi.c
@@ -4029,7 +4029,7 @@ static int cqspi_runtime_suspend(struct device *dev)
static int cqspi_runtime_resume(struct device *dev)
{
struct cqspi_st *cqspi = dev_get_drvdata(dev);
- int ret;
+ int cs, ret;
ret = clk_bulk_prepare_enable(CLK_QSPI_NUM, cqspi->clks);
if (ret)
@@ -4042,6 +4042,28 @@ static int cqspi_runtime_resume(struct device *dev)
cqspi->current_cs = -1;
cqspi->sclk = 0;
+
+ for (cs = 0; cs < CQSPI_MAX_CHIPSELECT; cs++) {
+ struct cqspi_flash_pdata *f_pdata = &cqspi->f_pdata[cs];
+
+ if (!f_pdata->use_tuned_phy)
+ continue;
+
+ cqspi_phy_set_dll_master(cqspi);
+
+ cqspi_set_dll(cqspi->iobase, f_pdata->phy_setting.rx,
+ f_pdata->phy_setting.tx);
+ ret = cqspi_resync_dll(cqspi);
+ if (ret) {
+ dev_warn(dev, "cs%d: PHY DLL resync failed on resume: %d, disabling PHY\n",
+ cs, ret);
+ f_pdata->dll_locked = false;
+ f_pdata->use_tuned_phy = false;
+ } else {
+ f_pdata->dll_locked = true;
+ }
+ }
+
return 0;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 15/18] mtd: spinand: extract variant ranking logic into spinand_op_find_best_variant()
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (13 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation Santhosh Kumar K
` (2 subsequent siblings)
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Refactor spinand_select_op_variant() to extract the inner loop into a
shared helper spinand_op_find_best_variant(). The helper takes a
skip_mask bitmask of already-tried variant indices, enabling callers to
iterate variants in ranked performance order while skipping those
already attempted. spinand_select_op_variant() becomes a one-liner
wrapper. No functional change.
Reviewed-by: Miquel Raynal <miquel.raynal@bootlin.com>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/mtd/nand/spi/core.c | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 8bf9301f25e7..7c3341f1fca0 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -1542,9 +1542,22 @@ static int spinand_init_odtr_instruction_set(struct spinand_device *spinand)
return 0;
}
+/*
+ * spinand_op_find_best_variant() - Find the fastest eligible op variant.
+ * @spinand: SPI NAND device
+ * @variants: full variant list to search
+ * @iface: bus interface to consider (ODTR or SSDR)
+ * @skip_mask: bitmask of variant indices to skip (already tried)
+ *
+ * Iterates @variants, evaluates transfer duration for each eligible op, and
+ * returns a pointer to the fastest one not in @skip_mask. Returns NULL when
+ * no eligible variant remains. Used by both variant selection at init time
+ * (skip_mask == 0) and ranked PHY tuning iteration.
+ */
static const struct spi_mem_op *
-spinand_select_op_variant(struct spinand_device *spinand, enum spinand_bus_interface iface,
- const struct spinand_op_variants *variants)
+spinand_op_find_best_variant(struct spinand_device *spinand,
+ const struct spinand_op_variants *variants,
+ enum spinand_bus_interface iface, u32 skip_mask)
{
struct nand_device *nand = spinand_to_nand(spinand);
const struct spi_mem_op *best_variant = NULL;
@@ -1552,15 +1565,16 @@ spinand_select_op_variant(struct spinand_device *spinand, enum spinand_bus_inter
unsigned int i;
for (i = 0; i < variants->nops; i++) {
- struct spi_mem_op op = variants->ops[i];
+ struct spi_mem_op op;
u64 op_duration_ns = 0;
unsigned int nbytes;
int ret;
- if ((iface == SSDR && spinand_op_is_odtr(&op)) ||
- (iface == ODTR && !spinand_op_is_odtr(&op)))
+ if ((skip_mask & BIT(i)) ||
+ spinand_op_is_odtr(&variants->ops[i]) != (iface == ODTR))
continue;
+ op = variants->ops[i];
nbytes = nanddev_per_page_oobsize(nand) +
nanddev_page_size(nand);
@@ -1589,6 +1603,14 @@ spinand_select_op_variant(struct spinand_device *spinand, enum spinand_bus_inter
return best_variant;
}
+static const struct spi_mem_op *
+spinand_select_op_variant(struct spinand_device *spinand,
+ enum spinand_bus_interface iface,
+ const struct spinand_op_variants *variants)
+{
+ return spinand_op_find_best_variant(spinand, variants, iface, 0);
+}
+
/**
* spinand_match_and_init() - Try to find a match between a device ID and an
* entry in a spinand_info table
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (14 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 15/18] mtd: spinand: extract variant ranking logic into spinand_op_find_best_variant() Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:43 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 17/18] mtd: spi-nor: extract read op template construction into helper Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation Santhosh Kumar K
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
PHY calibration must complete before dirmap creation so the validated
max_freq is embedded into the dirmap op templates from the start; a
dirmap built at the base rate cannot be updated later.
Add spinand_optimize_controller() which calls spi_mem_execute_tuning()
on the pre-selected op variant. If the controller returns a per-op skip
signal (ret == 0, max_freq still 0), spinand_try_ranked_variant()
iterates all remaining variants in descending performance order, trying
ODTR first and falling back to SSDR if all ODTR attempts fail.
Optimisation failure is never fatal.
When SSDR is permanently selected, clear odtr_op_templates to prevent
spinand_configure_chip() on resume from re-entering ODTR mode with
dirmaps built for SSDR, which would corrupt data.
For devices with a distinct cont_read op table,
spinand_try_ranked_variant() re-pairs cont_read_cache with the newly
selected read_cache variant rather than disabling continuous reads
outright. Re-pairing uses protocol-shape matching (cmd/addr buswidth,
addr.nbytes, DTR-ness); addr.nbytes is included because different
address byte counts represent different controller configurations
that cannot share calibrated settings. Mode-toggle devices are
unaffected.
Move spinand_create_dirmaps() from spinand_init() to spinand_probe() so
spinand_optimize_controller() can run first.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/mtd/nand/spi/core.c | 311 ++++++++++++++++++++++++++++++++++--
include/linux/mtd/spinand.h | 14 ++
2 files changed, 308 insertions(+), 17 deletions(-)
diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 7c3341f1fca0..6e58e0893736 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))
@@ -1542,22 +1545,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;
+}
+
/*
* spinand_op_find_best_variant() - Find the fastest eligible op variant.
- * @spinand: SPI NAND device
- * @variants: full variant list to search
- * @iface: bus interface to consider (ODTR or SSDR)
- * @skip_mask: bitmask of variant indices to skip (already tried)
+ * @spinand: SPI NAND device
+ * @variants: full variant list to search
+ * @iface: bus interface to consider (ODTR or SSDR)
+ * @skip_mask: bitmask of variant indices to skip (already tried)
+ * @match_shape: if non-NULL, only consider variants whose protocol shape
+ * (see spinand_op_shape_matches()) matches this op
*
* Iterates @variants, evaluates transfer duration for each eligible op, and
* returns a pointer to the fastest one not in @skip_mask. Returns NULL when
- * no eligible variant remains. Used by both variant selection at init time
- * (skip_mask == 0) and ranked PHY tuning iteration.
+ * no eligible variant remains. Used by variant selection at init time
+ * (skip_mask == 0, match_shape == NULL), ranked PHY tuning iteration
+ * (match_shape == NULL), and continuous-read counterpart lookup
+ * (match_shape == the read_cache op just selected).
*/
static const struct spi_mem_op *
spinand_op_find_best_variant(struct spinand_device *spinand,
const struct spinand_op_variants *variants,
- enum spinand_bus_interface iface, u32 skip_mask)
+ enum spinand_bus_interface iface, u32 skip_mask,
+ const struct spi_mem_op *match_shape)
{
struct nand_device *nand = spinand_to_nand(spinand);
const struct spi_mem_op *best_variant = NULL;
@@ -1574,6 +1606,10 @@ spinand_op_find_best_variant(struct spinand_device *spinand,
spinand_op_is_odtr(&variants->ops[i]) != (iface == ODTR))
continue;
+ if (match_shape &&
+ !spinand_op_shape_matches(&variants->ops[i], match_shape))
+ continue;
+
op = variants->ops[i];
nbytes = nanddev_per_page_oobsize(nand) +
nanddev_page_size(nand);
@@ -1608,7 +1644,37 @@ spinand_select_op_variant(struct spinand_device *spinand,
enum spinand_bus_interface iface,
const struct spinand_op_variants *variants)
{
- return spinand_op_find_best_variant(spinand, variants, iface, 0);
+ return spinand_op_find_best_variant(spinand, variants, iface, 0, NULL);
+}
+
+/*
+ * spinand_find_cont_read_variant() - Find the continuous-read counterpart of
+ * a chosen read_cache variant.
+ * @spinand: SPI NAND device
+ * @read_op: the read_cache op that was just selected
+ * @iface: bus interface @read_op belongs to (ODTR or SSDR)
+ *
+ * Devices that implement continuous read via a distinct op template (rather
+ * than a mode toggle around the existing read_cache op) need that template
+ * re-paired whenever a different read_cache variant is selected, since the
+ * two are not guaranteed to sit at the same index in their respective
+ * variant tables (some devices omit continuous-read counterparts for
+ * specific dummy-cycle sub-variants, shifting every later index out of
+ * alignment). Matching by protocol shape instead of index is unaffected by
+ * such gaps. Returns NULL if this device has no continuous-read variant
+ * table, or no matching variant is eligible on this controller.
+ */
+static const struct spi_mem_op *
+spinand_find_cont_read_variant(struct spinand_device *spinand,
+ const struct spi_mem_op *read_op,
+ enum spinand_bus_interface iface)
+{
+ if (!spinand->all_cont_read_variants)
+ return NULL;
+
+ return spinand_op_find_best_variant(spinand,
+ spinand->all_cont_read_variants,
+ iface, 0, read_op);
}
/**
@@ -1745,6 +1811,20 @@ 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;
+ spinand->all_cont_read_variants =
+ info->op_variants.cont_read_cache;
+ }
+
return 0;
}
@@ -1923,7 +2003,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 +2094,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 +2122,198 @@ 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. For devices
+ * with a distinct cont_read_cache variant table (non-NULL
+ * all_cont_read_variants), re-pairs cont_read_cache via protocol-shape
+ * matching and updates cont_read_possible. Mode-toggle devices
+ * (all_cont_read_variants == NULL) are unaffected.
+ */
+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);
+ spinand->cont_read_possible =
+ !!templates->cont_read_cache;
+ }
+ 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 +2336,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..31e3a0dfe8a4 100644
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -792,8 +792,22 @@ 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;
+
+ /* Continuous-read counterpart of all_read_variants, if any. */
+ const struct spinand_op_variants *all_cont_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
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 17/18] mtd: spi-nor: extract read op template construction into helper
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (15 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation Santhosh Kumar K
17 siblings, 0 replies; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
From: Pratyush Yadav <pratyush@kernel.org>
Extract spi_nor_spimem_get_read_op() from the duplicated op-template
construction in spi_nor_spimem_read_data() and
spi_nor_create_read_dirmap().
Using data.nbytes = 2 as a non-zero placeholder lets
spi_nor_spimem_setup_op() configure the data buswidth without a separate
override; callers replace data.nbytes with the actual transfer length
before use. No functional change.
Signed-off-by: Pratyush Yadav <pratyush@kernel.org>
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/mtd/spi-nor/core.c | 66 +++++++++++++++++++++-----------------
1 file changed, 36 insertions(+), 30 deletions(-)
diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c
index ccf4396cdcd0..b683c077a233 100644
--- a/drivers/mtd/spi-nor/core.c
+++ b/drivers/mtd/spi-nor/core.c
@@ -188,6 +188,37 @@ static int spi_nor_controller_ops_erase(struct spi_nor *nor, loff_t offs)
return nor->controller_ops->erase(nor, offs);
}
+/**
+ * spi_nor_spimem_get_read_op() - build a configured read op template
+ * @nor: the spi-nor device
+ *
+ * Returns a spi_mem_op with the command, address format, dummy cycles,
+ * and data buswidth configured for @nor. For direct reads, the caller
+ * must fill in addr.val, data.nbytes, and data.buf.in before use.
+ */
+static struct spi_mem_op spi_nor_spimem_get_read_op(struct spi_nor *nor)
+{
+ /*
+ * data.nbytes must be non-zero so spi_nor_spimem_setup_op()
+ * configures the data buswidth; callers replace it with the
+ * actual transfer length.
+ */
+ struct spi_mem_op op =
+ SPI_MEM_OP(SPI_MEM_OP_CMD(nor->read_opcode, 0),
+ SPI_MEM_OP_ADDR(nor->addr_nbytes, 0, 0),
+ SPI_MEM_OP_DUMMY(nor->read_dummy, 0),
+ SPI_MEM_OP_DATA_IN(2, NULL, 0));
+
+ spi_nor_spimem_setup_op(nor, &op, nor->read_proto);
+
+ /* convert the dummy cycles to the number of bytes */
+ op.dummy.nbytes = (nor->read_dummy * op.dummy.buswidth) / 8;
+ if (spi_nor_protocol_is_dtr(nor->read_proto))
+ op.dummy.nbytes *= 2;
+
+ return op;
+}
+
/**
* spi_nor_spimem_read_data() - read data from flash's memory region via
* spi-mem
@@ -201,21 +232,14 @@ static int spi_nor_controller_ops_erase(struct spi_nor *nor, loff_t offs)
static ssize_t spi_nor_spimem_read_data(struct spi_nor *nor, loff_t from,
size_t len, u8 *buf)
{
- struct spi_mem_op op =
- SPI_MEM_OP(SPI_MEM_OP_CMD(nor->read_opcode, 0),
- SPI_MEM_OP_ADDR(nor->addr_nbytes, from, 0),
- SPI_MEM_OP_DUMMY(nor->read_dummy, 0),
- SPI_MEM_OP_DATA_IN(len, buf, 0));
+ struct spi_mem_op op = spi_nor_spimem_get_read_op(nor);
bool usebouncebuf;
ssize_t nbytes;
int error;
- spi_nor_spimem_setup_op(nor, &op, nor->read_proto);
-
- /* convert the dummy cycles to the number of bytes */
- op.dummy.nbytes = (nor->read_dummy * op.dummy.buswidth) / 8;
- if (spi_nor_protocol_is_dtr(nor->read_proto))
- op.dummy.nbytes *= 2;
+ op.addr.val = from;
+ op.data.nbytes = len;
+ op.data.buf.in = buf;
usebouncebuf = spi_nor_spimem_bounce(nor, &op);
@@ -3712,28 +3736,10 @@ static int spi_nor_create_read_dirmap(struct spi_nor *nor)
{
struct spi_mem_dirmap_info info = {
.op_tmpl = &info.primary_op_tmpl,
- .primary_op_tmpl = SPI_MEM_OP(SPI_MEM_OP_CMD(nor->read_opcode, 0),
- SPI_MEM_OP_ADDR(nor->addr_nbytes, 0, 0),
- SPI_MEM_OP_DUMMY(nor->read_dummy, 0),
- SPI_MEM_OP_DATA_IN(0, NULL, 0)),
+ .primary_op_tmpl = spi_nor_spimem_get_read_op(nor),
.offset = 0,
.length = nor->params->size,
};
- struct spi_mem_op *op = info.op_tmpl;
-
- spi_nor_spimem_setup_op(nor, op, nor->read_proto);
-
- /* convert the dummy cycles to the number of bytes */
- op->dummy.nbytes = (nor->read_dummy * op->dummy.buswidth) / 8;
- if (spi_nor_protocol_is_dtr(nor->read_proto))
- op->dummy.nbytes *= 2;
-
- /*
- * Since spi_nor_spimem_setup_op() only sets buswidth when the number
- * of data bytes is non-zero, the data buswidth won't be set here. So,
- * do it explicitly.
- */
- op->data.buswidth = spi_nor_get_protocol_data_nbits(nor->read_proto);
nor->dirmap.rdesc = devm_spi_mem_dirmap_create(nor->dev, nor->spimem,
&info);
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
` (16 preceding siblings ...)
2026-08-25 17:17 ` [PATCH v9 17/18] mtd: spi-nor: extract read op template construction into helper Santhosh Kumar K
@ 2026-08-25 17:17 ` Santhosh Kumar K
2026-08-25 17:45 ` sashiko-bot
17 siblings, 1 reply; 30+ messages in thread
From: Santhosh Kumar K @ 2026-08-25 17:17 UTC (permalink / raw)
To: broonie, robh, krzk+dt, conor+dt, miquel.raynal, richard,
vigneshr, pratyush, mwalle, takahiro.kuwano
Cc: linux-spi, devicetree, linux-kernel, linux-mtd, praneeth,
sebin.francis, u-kumar1, a-dutta, s-k6
Run controller optimization in spi_nor_probe() before creating dirmaps
so the validated frequency is available at dirmap construction time.
Store the configured read op template in nor->max_read_op and pass it
to spi_mem_execute_tuning(). On success the controller sets
max_read_op.max_freq to the calibrated rate.
spi_nor_spimem_get_read_op() propagates nor->max_read_op.max_freq into
every op it returns, so the validated frequency flows automatically into
the dirmap template and into regular read ops.
Optimization failure is non-fatal; the device operates at the
conservative base rate.
Signed-off-by: Santhosh Kumar K <s-k6@ti.com>
---
drivers/mtd/spi-nor/core.c | 14 ++++++++++++++
include/linux/mtd/spi-nor.h | 3 +++
2 files changed, 17 insertions(+)
diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c
index b683c077a233..2efcdc7f637f 100644
--- a/drivers/mtd/spi-nor/core.c
+++ b/drivers/mtd/spi-nor/core.c
@@ -216,6 +216,9 @@ static struct spi_mem_op spi_nor_spimem_get_read_op(struct spi_nor *nor)
if (spi_nor_protocol_is_dtr(nor->read_proto))
op.dummy.nbytes *= 2;
+ /* Propagate the validated frequency; zero before tuning. */
+ op.max_freq = nor->max_read_op.max_freq;
+
return op;
}
@@ -3843,6 +3846,17 @@ static int spi_nor_probe(struct spi_mem *spimem)
return -ENOMEM;
}
+ /*
+ * Populate the persistent template and run PHY tuning before dirmap
+ * creation so the validated frequency feeds into the dirmap op.
+ * Tuning failure is non-fatal; the device operates at base speed.
+ */
+ nor->max_read_op = spi_nor_spimem_get_read_op(nor);
+
+ ret = spi_mem_execute_tuning(spimem, &nor->max_read_op, NULL);
+ if (ret && ret != -EOPNOTSUPP)
+ dev_dbg(dev, "Controller optimization failed: %d\n", ret);
+
ret = spi_nor_create_read_dirmap(nor);
if (ret)
return ret;
diff --git a/include/linux/mtd/spi-nor.h b/include/linux/mtd/spi-nor.h
index 4b92494827b1..ab498a50f15f 100644
--- a/include/linux/mtd/spi-nor.h
+++ b/include/linux/mtd/spi-nor.h
@@ -422,6 +422,9 @@ struct spi_nor {
struct spi_mem_dirmap_desc *wdesc;
} dirmap;
+ /* Persistent op template updated by execute_tuning with validated speed. */
+ struct spi_mem_op max_read_op;
+
void *priv;
};
--
2.34.1
^ permalink raw reply related [flat|nested] 30+ messages in thread
* Re: [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture
2026-08-25 17:17 ` [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
@ 2026-08-25 17:29 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:29 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: conor+dt, Vignesh Raghavendra, robh, Pratyush Yadav,
Michael Walle, Miquel Raynal, devicetree, Takahiro Kuwano,
Richard Weinberger
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The patch introduces a `dqs` parameter to `cqspi_readdata_capture()` but hardcodes it to `false` in `cqspi_configure()`, unconditionally clearing the hardware DQS bit and breaking platforms that rely on the bootloader to enable it.
--
commit e47fa544a8e52f8469fd8ae3d4eeedbed9dc8a9e
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: add DQS support to read data capture
This commit adds a DQS (Data Strobe) parameter to cqspi_readdata_capture()
to control data capture timing. DQS mode uses a dedicated strobe signal for
improved timing margins in high-speed SPI modes.
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index f8e2663723677..046fa5d69b0f7 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
[ ... ]
> @@ -476,6 +477,11 @@ static void cqspi_readdata_capture(struct cqspi_st *cqspi, const bool bypass,
> reg |= (delay & CQSPI_REG_READCAPTURE_DELAY_MASK)
> << CQSPI_REG_READCAPTURE_DELAY_LSB;
>
> + if (dqs)
> + reg |= BIT(CQSPI_REG_READCAPTURE_DQS_LSB);
> + else
> + reg &= ~BIT(CQSPI_REG_READCAPTURE_DQS_LSB);
> +
[Severity: High]
Does explicitly clearing the DQS bit here destroy hardware state established
by the bootloader?
Previously, this function used a read-modify-write operation that preserved
the existing DQS bit. Platforms that don't utilize the new kernel PHY tuning
infrastructure, but rely on the bootloader to enable DQS, might now
experience read failures and data corruption in high-speed SPI modes when
this state is lost.
> writel(reg, reg_base + CQSPI_REG_READCAPTURE);
> }
>
> @@ -1317,7 +1323,7 @@ static void cqspi_configure(struct cqspi_flash_pdata *f_pdata,
> cqspi->sclk = sclk;
> cqspi_config_baudrate_div(cqspi);
> cqspi_delay(f_pdata);
> - cqspi_readdata_capture(cqspi, !cqspi->rclk_en,
> + cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
> f_pdata->read_delay);
> }
[Severity: High]
Does hardcoding false here unconditionally disable DQS on every configuration
cycle?
Because cqspi_configure() is executed unconditionally on any chip-select or
clock frequency switch, this passes false to cqspi_readdata_capture(). This
will unconditionally clear the hardware DQS bit across all Cadence QSPI
controllers, leading to regressions for boards that require it to remain
enabled.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=7
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz
2026-08-25 17:17 ` [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz Santhosh Kumar K
@ 2026-08-25 17:30 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:30 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Miquel Raynal, Pratyush Yadav, robh, Michael Walle,
Takahiro Kuwano, conor+dt, Richard Weinberger, devicetree,
Vignesh Raghavendra
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The new `post_config_max_speed_hz` property is not clamped to the SPI controller's maximum supported speed during device setup.
- [Medium] `post_config_max_speed_hz` is not copied to ancillary or multiplexed SPI devices, breaking high-speed operations for multi-die packages and proxy devices.
--
commit 5705672f7b6c77c877910c5996844e5029e278fd
Author: Santhosh Kumar K <s-k6@ti.com>
spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz
This commit adds a post_config_max_speed_hz field to struct spi_device to
support SPI devices operating at distinct pre- and post-configuration clock
rates. The new property is parsed from the spi-max-post-config-frequency-hz
Device Tree property.
> diff --git a/include/linux/spi/spi.h b/include/linux/spi/spi.h
> index 88d17fce02dcb..02fd602f29e8b 100644
> --- a/include/linux/spi/spi.h
> +++ b/include/linux/spi/spi.h
[ ... ]
> @@ -195,6 +197,7 @@ struct spi_device {
> struct device dev;
> struct spi_controller *controller;
> u32 max_speed_hz;
> + u32 post_config_max_speed_hz;
[Severity: Medium]
Does the new post_config_max_speed_hz need to be clamped to the controller's
maximum supported speed during device setup?
Looking at drivers/spi/spi.c:__spi_setup(), the existing max_speed_hz is
strictly validated against the hardware capabilities:
if (spi->controller->max_speed_hz &&
(!spi->max_speed_hz ||
spi->max_speed_hz > spi->controller->max_speed_hz))
spi->max_speed_hz = spi->controller->max_speed_hz;
Without similar clamping for post_config_max_speed_hz, a misconfigured
Device Tree could inject an arbitrarily large frequency, causing the
underlying controller to configure hardware for an unsupported rate.
[Severity: Medium]
Should post_config_max_speed_hz also be cloned for ancillary or multiplexed
SPI devices to ensure multi-die packages can use post-config speeds?
Looking at drivers/spi/spi.c:spi_new_ancillary_device(), only the primary
max_speed_hz is copied to secondary logical devices:
/* Take over SPI mode/speed from SPI main device */
ancillary->max_speed_hz = spi->max_speed_hz;
ancillary->mode = spi->mode;
A similar omission exists in drivers/spi/spi-mux.c:spi_mux_select():
/* copy the child device's settings except for the cs */
priv->spi->max_speed_hz = spi->max_speed_hz;
priv->spi->mode = spi->mode;
Without cloning this new property, these proxy devices will have a zero
post-config frequency, forcing secondary dies to operate at the slower
pre-configuration rate.
> u8 bits_per_word;
> bool rt;
> #define SPI_NO_TX BIT(31) /* No transmit wire */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=3
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops
2026-08-25 17:17 ` [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
@ 2026-08-25 17:30 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:30 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: robh, Richard Weinberger, Takahiro Kuwano, conor+dt,
Michael Walle, Miquel Raynal, Pratyush Yadav, Vignesh Raghavendra,
devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Standard SPI memory operations are erroneously rejected on controllers lacking the `per_op_freq` capability if the device tree sets `post_config_max_speed_hz` equal to `max_speed_hz`.
- [Medium] `post_config_max_speed_hz` is not clamped to the controller's maximum speed limit, leading to incorrect theoretical operation duration estimates.
--
commit 77b46afe94c20b6f95b9ec99806440ce9c5f6df0
Author: Santhosh Kumar K <s-k6@ti.com>
spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops
This commit extends spi_mem_adjust_op_freq() with a bypass for post-config
ops and extends spi_mem_default_supports_op() to require the per_op_freq
controller capability for these operations.
> diff --git a/drivers/spi/spi-mem.c b/drivers/spi/spi-mem.c
> index 5f973ebfb8b6e..cd4bc4c914e7f 100644
> --- a/drivers/spi/spi-mem.c
> +++ b/drivers/spi/spi-mem.c
> @@ -206,7 +206,8 @@ bool spi_mem_default_supports_op(struct spi_mem *mem,
> return false;
>
> if (op->max_freq &&
> - op->max_freq < mem->spi->max_speed_hz) {
> + (op->max_freq < mem->spi->max_speed_hz ||
> + op->max_freq == mem->spi->post_config_max_speed_hz)) {
[Severity: High]
Will this erroneously reject standard SPI memory operations on controllers
lacking the per_op_freq capability if the device tree sets
post_config_max_speed_hz equal to max_speed_hz?
If the device tree specifies a spi-max-post-config-frequency-hz identical to
spi-max-frequency, standard generic operations initially have op->max_freq = 0,
which spi_mem_adjust_op_freq() clamps to max_speed_hz.
Subsequently, spi_mem_default_supports_op() checks if
op->max_freq == mem->spi->post_config_max_speed_hz to identify
post-configuration operations. When the max limits are equal, this condition
triggers for all standard operations, strictly demanding the per_op_freq
capability. Controllers without this capability will reject the standard
operations.
Does this overloaded use of the max_freq integer act as an operation type flag
that leads to this conflict?
> if (!spi_mem_controller_is_capable(ctlr, per_op_freq))
> return false;
> }
> @@ -623,9 +624,18 @@ EXPORT_SYMBOL_GPL(spi_mem_adjust_op_size);
> * Some chips have per-op frequency limitations and must adapt the maximum
> * speed. This function allows SPI mem drivers to set @op->max_freq to the
> * maximum supported value.
> + *
> + * When @mem->spi->post_config_max_speed_hz is set, ops with @op->max_freq
> + * equal to that value are treated as post-configuration ops (e.g. PHY-tuned)
> + * and are allowed to run at the full post-config rate. All other ops are
> + * capped to @mem->spi->max_speed_hz, the always-reachable base rate.
> */
> void spi_mem_adjust_op_freq(struct spi_mem *mem, struct spi_mem_op *op)
> {
> + if (mem->spi->post_config_max_speed_hz &&
> + op->max_freq == mem->spi->post_config_max_speed_hz)
> + return;
> +
[Severity: Medium]
Will this early return fail to clamp post_config_max_speed_hz to the
controller's maximum speed limit?
If the device tree specifies a spi-max-post-config-frequency-hz that exceeds
the SPI controller's absolute max_speed_hz, the post_config_max_speed_hz value
is left unbounded.
Because spi_mem_calc_op_duration() calculates time based on this unbounded
value, it could lead to incorrect theoretical operation duration estimates and
prefer operations that appear fast theoretically but are physically clamped to
a slower speed later.
> if (!op->max_freq || op->max_freq > mem->spi->max_speed_hz)
> op->max_freq = mem->spi->max_speed_hz;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=4
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support
2026-08-25 17:17 ` [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
@ 2026-08-25 17:31 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:31 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: robh, Pratyush Yadav, Richard Weinberger, devicetree,
Takahiro Kuwano, Miquel Raynal, Vignesh Raghavendra,
Michael Walle, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] The custom memory-mapped I/O read loop in cqspi_memcpy_fromio() byteswaps and corrupts data on Big Endian architectures.
- [High] Unaligned I/O memory accesses in cqspi_memcpy_fromio() cause hardware exceptions (Alignment Faults) on architectures like ARM/ARM64.
Pre-existing issues:
- [High] Mixed I/O accessors in cqspi_indirect_write_execute() corrupt partial FIFO writes on big-endian architectures.
--
commit ece753244f464ecb928c830b6abeb4d3b99224f1
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: refactor direct read path for PHY support
Extract the DMA transfer code from cqspi_direct_read_execute() into a
new cqspi_direct_read_dma() helper. Add cqspi_memcpy_fromio() to handle
non-DMA transfers, using 2-byte-aligned I/O reads for 8D-8D-8D mode.
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index fc33bbf56dd61..58bca15dbd6ec 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
[ ... ]
> @@ -1588,8 +1586,63 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
> return ret;
> }
>
> +static void cqspi_memcpy_fromio(const struct spi_mem_op *op, void *to,
> + const void __iomem *from, size_t count)
> +{
> + if (op->data.buswidth == 8 && op->data.dtr) {
> + unsigned long from_addr = (unsigned long)from;
> +
> + /* Handle unaligned start with 2-byte read */
> + if (count && !IS_ALIGNED(from_addr, 4)) {
> + put_unaligned_le16(__raw_readw(from), to);
[Severity: High]
Can these MMIO reads cause alignment faults on architectures like ARM/ARM64?
If a user or filesystem requests a SPI read at an unaligned odd byte offset,
the start address from_addr could be odd. If it is odd, !IS_ALIGNED() is true,
and the code executes __raw_readw(from) from an odd device memory address.
Unaligned accesses to device I/O memory are forbidden on some architectures
and can trigger a data abort panic.
> + from += 2;
> + to += 2;
> + count -= 2;
> + }
> +
> + /*
> + * Use an explicit 32-bit read loop for the bulk; this
> + * guarantees the minimum 16-bit AHB transaction width required
> + * in 8D-8D-8D mode on all architectures.
> + */
> + while (count >= 4) {
> + put_unaligned_le32(__raw_readl(from), to);
[Severity: High]
Does combining native-endian __raw_readl() with little-endian
put_unaligned_le32() cause unwanted byte-swapping of stream data on
big-endian platforms?
Because __raw_readl() natively loads a 32-bit word from the bus, and
put_unaligned_le32() forces it into little-endian byte order in memory, the
byte sequence within each chunk gets reversed on big-endian architectures.
This reverses the exact byte stream from device memory, breaking the semantics
guaranteed by the standard memcpy_fromio() fallback.
Additionally, if the initial offset was odd, adding 2 in the block above means
from remains odd. Does executing __raw_readl(from) here also trigger an
unaligned device memory access fault during the 32-bit read?
[ ... ]
[Severity: High]
This isn't a bug introduced by this patch, but while reviewing the I/O
accessors, I noticed a pre-existing issue in cqspi_indirect_write_execute():
drivers/spi/spi-cadence-quadspi.c:cqspi_indirect_write_execute() {
...
if (write_words) {
iowrite32_rep(cqspi->ahb_base, txbuf, write_words);
txbuf += (write_words * 4);
}
if (mod_bytes) {
unsigned int temp = 0xFFFFFFFF;
memcpy(&temp, txbuf, mod_bytes);
iowrite32(temp, cqspi->ahb_base);
txbuf += mod_bytes;
}
...
}
Does using iowrite32() for the remainder bytes corrupt partial FIFO writes
on big-endian systems?
The bulk data loop uses iowrite32_rep(), which is a stream accessor that
preserves byte semantics. However, the remainder writes use iowrite32(),
which byte-swaps on big-endian architectures. This mixes stream and register
accessors for the same FIFO base, which can corrupt the final 1-3 bytes
of an indirect write transfer.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=10
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads
2026-08-25 17:17 ` [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
@ 2026-08-25 17:32 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:32 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Richard Weinberger, Pratyush Yadav, Miquel Raynal, Michael Walle,
Takahiro Kuwano, devicetree, Vignesh Raghavendra, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Bypassing PHY for unaligned or short direct reads at high frequency causes silent data corruption.
- [High] Modifying QSPI configuration registers before the controller is idle causes hardware race conditions.
--
commit 478fb069d36d7f959d00975130c0e1c3988aa09c
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: enable PHY for direct reads
This commit adds support for toggling PHY mode around DMA transfers in the
Cadence QSPI driver. It introduces cqspi_tune_phy() to manage the PHY DLL
settings and configuration, and falls back to non-PHY reads for unaligned or
small transfers.
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index 58bca15dbd6ec..d38a0253e4bda 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
> @@ -1075,6 +1075,109 @@ static int cqspi_tune_phy(struct cqspi_flash_pdata *f_pdata, bool enable)
[ ... ]
> + /* Disable path: execute unconditionally regardless of idle state. */
> + cqspi_readdata_capture(cqspi, !cqspi->rclk_en, false,
> + f_pdata->read_delay);
> +
> + reg = readl(reg_base + CQSPI_REG_CONFIG);
> + reg &= ~(CQSPI_REG_CONFIG_PHY_EN |
> + CQSPI_REG_CONFIG_PHY_PIPELINE);
> + writel(reg, reg_base + CQSPI_REG_CONFIG);
[Severity: High]
Does modifying active configuration registers before waiting for the controller
to become idle cause a hardware race condition?
When a successful direct read completes its DMA transfer, cqspi_tune_phy() is
called to disable the PHY. Because the QSPI prefetches data in direct mode,
the SPI bus may still be actively receiving data when the DMA finishes,
potentially violating IP requirements if the controller is not idle.
[ ... ]
> @@ -1630,6 +1741,7 @@ static int cqspi_direct_read_execute(struct cqspi_flash_pdata *f_pdata,
> loff_t from = op->addr.val;
> size_t len = op->data.nbytes;
> u_char *buf = op->data.buf.in;
> + int ret;
>
> if (!cqspi->rx_chan || !virt_addr_valid(buf) ||
> len < CQSPI_PHY_MIN_DIRECT_READ_LEN) {
> cqspi_memcpy_fromio(op, buf, cqspi->ahb_base + from, len);
> return 0;
> }
[Severity: High]
Can bypassing the PHY for short direct reads at high frequencies cause silent
data corruption?
If a subsystem issues a short read that falls back to cqspi_memcpy_fromio(),
the read will execute at the high post_config_max_speed_hz (e.g., 200MHz DTR)
but without DQS or PHY DLL timing, capturing garbage data.
> - return cqspi_direct_read_dma(f_pdata, buf, from, len);
> +
> + if (!cqspi_use_tuned_phy(f_pdata, op, post_config_max_speed_hz))
> + return cqspi_direct_read_dma(f_pdata, buf, from, len);
> +
> + if (!IS_ALIGNED(from, 16) || !IS_ALIGNED(from + len, 16))
> + return cqspi_direct_read_dma(f_pdata, buf, from, len);
[Severity: High]
Does this unaligned fallback also risk silent data corruption for the same
reason?
Similar to short reads, falling back to cqspi_direct_read_dma() without the
PHY enabled means the controller executes the read at high speed without the
tuned PHY data-capture timing.
> + ret = cqspi_tune_phy(f_pdata, true);
> + if (ret)
> + return ret;
> + ret = cqspi_direct_read_dma(f_pdata, buf, from, len);
> + ret = cqspi_tune_phy(f_pdata, false) ?: ret;
The race condition mentioned earlier triggers here. The DMA transfer completes
and immediately calls cqspi_tune_phy(..., false) without waiting for idle.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=11
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes
2026-08-25 17:17 ` [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
@ 2026-08-25 17:32 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:32 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Miquel Raynal, Michael Walle, Vignesh Raghavendra,
Takahiro Kuwano, robh, conor+dt, Richard Weinberger,
Pratyush Yadav, devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing verification of the current operation frequency (`op->max_freq`) against the calibrated PHY frequency, potentially causing the PHY to be incorrectly enabled for uncalibrated writes.
--
commit f08af848763cec6062f2de1a309900c45e4f895a
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: enable PHY for indirect writes
This commit enables PHY for indirect writes of at least 1KB, restricting
PHY writes to NAND devices where a write op template was provided. It also
cancels the indirect write before disabling PHY in the error path to avoid
timeouts while the controller is still waiting for AHB data.
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index d38a0253e4bda..20ece785c97a4 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
[ ... ]
> @@ -1370,18 +1372,32 @@ static int cqspi_write_setup(struct cqspi_flash_pdata *f_pdata,
>
> static int cqspi_indirect_write_execute(struct cqspi_flash_pdata *f_pdata,
> loff_t to_addr, const u8 *txbuf,
> - const size_t n_tx)
> + const size_t n_tx,
> + u32 post_config_max_speed_hz)
> {
> struct cqspi_st *cqspi = f_pdata->cqspi;
> struct device *dev = &cqspi->pdev->dev;
> void __iomem *reg_base = cqspi->iobase;
> unsigned int remaining = n_tx;
> unsigned int write_bytes;
> + bool use_tuned_phy_write;
> int ret;
>
> if (!refcount_read(&cqspi->refcount))
> return -ENODEV;
>
> + /* Use PHY only for large writes at the calibrated rate */
> + use_tuned_phy_write = n_tx >= CQSPI_PHY_MIN_INDIRECT_WRITE_LEN &&
> + f_pdata->use_tuned_phy &&
> + f_pdata->phy_write_op.max_freq ==
> + post_config_max_speed_hz;
[Severity: High]
Does this logic correctly handle operations where the command-specific speed
limit (op->max_freq) is lower than post_config_max_speed_hz?
If a large indirect write is executed with an op->max_freq less than
post_config_max_speed_hz, the hardware clock will be lowered to op->max_freq.
However, this condition only checks the controller's post-configuration
maximum speed against the calibrated PHY frequency, completely omitting a
check against the actual frequency of the current SPI operation.
Could this lead to the PHY being incorrectly enabled using high-speed DLL
tuning values while operating at a lower clock speed? Applying DLL delay
values calibrated for a high clock speed to a lower clock speed could cause
the PHY data transmission eye to shift, potentially leading to incorrect
sampling and silent data corruption on flash writes.
The read path explicitly validates op->max_freq via cqspi_use_tuned_phy(),
should the write path perform a similar validation before enabling the PHY?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=12
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume
2026-08-25 17:17 ` [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume Santhosh Kumar K
@ 2026-08-25 17:36 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:36 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Pratyush Yadav, robh, Miquel Raynal, Vignesh Raghavendra,
Michael Walle, conor+dt, Takahiro Kuwano, Richard Weinberger,
devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Disabling 'use_tuned_phy' on DLL resync failure bypasses the PHY DLL while continuing to run at high speed, causing silent data corruption.
- [Medium] Iteratively reprogramming the global PHY DLL for all chip selects on runtime resume causes redundant delays and overwrites state.
--
commit 9b2d8bb539c61392a12afe8d25c966ba88c70398
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: reprogram PHY DLL on runtime resume
The PHY DLL loses its programmed delay values and lock when the OSPI
controller is power-gated during runtime suspend. On resume,
cqspi_controller_init() reinitialises the controller but does not
touch the PHY DLL registers, leaving the DLL in an unlocked state.
Any subsequent PHY read or write would then fail at the cqspi_resync_dll()
call inside cqspi_tune_phy().
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index 530335f002fb4..94d67e8bcfbc4 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
[ ... ]
> @@ -4042,6 +4042,28 @@ static int cqspi_runtime_resume(struct device *dev)
>
> cqspi->current_cs = -1;
> cqspi->sclk = 0;
> +
> + for (cs = 0; cs < CQSPI_MAX_CHIPSELECT; cs++) {
> + struct cqspi_flash_pdata *f_pdata = &cqspi->f_pdata[cs];
> +
> + if (!f_pdata->use_tuned_phy)
> + continue;
> +
> + cqspi_phy_set_dll_master(cqspi);
> +
> + cqspi_set_dll(cqspi->iobase, f_pdata->phy_setting.rx,
> + f_pdata->phy_setting.tx);
> + ret = cqspi_resync_dll(cqspi);
[Severity: Medium]
Will this iteratively reprogram the global PHY DLL for all chip selects on
runtime resume, causing redundant delays and overwriting state?
The PHY config register (CQSPI_REG_PHY_CONFIG) is global, yet the resume loop
iterates over all chip selects, writes each CS's RX/TX taps to the global
register, and triggers a full DLL resync each time. Because only the final
CS's taps remain active in hardware, the driver's cqspi_tune_phy() function
will redundantly re-sync the DLL again on the next transfer to any other chip
select, completely negating the purpose of pre-locking them during resume.
Furthermore, master DLL lock is independent of the slave RX/TX taps.
> + if (ret) {
> + dev_warn(dev, "cs%d: PHY DLL resync failed on resume: %d, disabling PHY\n",
> + cs, ret);
> + f_pdata->dll_locked = false;
> + f_pdata->use_tuned_phy = false;
[Severity: Critical]
Does disabling 'use_tuned_phy' here on DLL resync failure bypass the PHY DLL
while continuing to run at high speed, causing silent data corruption?
If the PHY DLL resync fails during resume, setting 'use_tuned_phy = false'
causes subsequent flash operations to bypass the PHY data capture logic.
However, the SPI clock frequency (op->max_freq) is not downgraded and
continues running at the high PHY-tuned speed (e.g., 150-200MHz). The
controller's non-PHY fallback cannot reliably sample data at these high
frequencies, resulting in garbage data being read from or written to the
flash.
> + } else {
> + f_pdata->dll_locked = true;
> + }
> + }
> +
> return 0;
> }
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=14
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support
2026-08-25 17:17 ` [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
@ 2026-08-25 17:40 ` sashiko-bot
2026-08-26 21:43 ` Mark Brown
1 sibling, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:40 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: devicetree, Miquel Raynal, Pratyush Yadav, Michael Walle,
Takahiro Kuwano, conor+dt, robh, Richard Weinberger,
Vignesh Raghavendra
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Manual PHY dummy cycle adjustments are overwritten and clobbered by subsequent read setups.
- [Medium] SDR PHY tuning fails for valid 1-tap wide windows due to an off-by-one error in the loop condition.
- [Medium] Incorrect parsing of 64-bit addresses for 'spi-phy-pattern-partition'.
--
commit 9701c78fc6d77976ffa5b3298e4d3809197ebe5d
Author: Santhosh Kumar K <s-k6@ti.com>
spi: cadence-quadspi: add PHY tuning support
This commit adds an execute_tuning callback for the Cadence QSPI controller
to perform delay-line calibration. It introduces 2D sweeps for DDR protocols
and 1D sweeps for SDR protocols by reading a known data pattern or writing
to the page cache before calibration.
> diff --git a/drivers/spi/spi-cadence-quadspi.c b/drivers/spi/spi-cadence-quadspi.c
> index 046fa5d69b0f7..f7cda1bc71393 100644
> --- a/drivers/spi/spi-cadence-quadspi.c
> +++ b/drivers/spi/spi-cadence-quadspi.c
[ ... ]
> @@ -1559,10 +1718,1594 @@ static bool cqspi_supports_mem_op(struct spi_mem *mem,
> +static void cqspi_phy_pre_config(struct cqspi_st *cqspi,
> + struct cqspi_flash_pdata *f_pdata,
> + const bool bypass)
> +{
[ ... ]
> + reg = readl(reg_base + CQSPI_REG_RD_INSTR);
> + dummy = FIELD_GET(CQSPI_REG_RD_INSTR_DUMMY_MASK
> + << CQSPI_REG_RD_INSTR_DUMMY_LSB,
> + reg);
> + cqspi->phy_saved_dummy = dummy;
> + if (dummy)
> + dummy--;
> + reg &= ~(CQSPI_REG_RD_INSTR_DUMMY_MASK << CQSPI_REG_RD_INSTR_DUMMY_LSB);
> + reg |= FIELD_PREP(CQSPI_REG_RD_INSTR_DUMMY_MASK
> + << CQSPI_REG_RD_INSTR_DUMMY_LSB,
> + dummy);
> + writel(reg, reg_base + CQSPI_REG_RD_INSTR);
[Severity: High]
Does this modification to CQSPI_REG_RD_INSTR get clobbered during tuning?
During the tuning process, cqspi_phy_check_pattern() executes a read via the
SPI core. This eventually calls cqspi_read_setup(), which recalculates the
dummy cycles from the spi_mem_op and unconditionally overwrites
CQSPI_REG_RD_INSTR:
cqspi_read_setup() {
...
dummy_clk = cqspi_calc_dummy(op);
if (dummy_clk)
reg |= (dummy_clk & CQSPI_REG_RD_INSTR_DUMMY_MASK)
<< CQSPI_REG_RD_INSTR_DUMMY_LSB;
writel(reg, reg_base + CQSPI_REG_RD_INSTR);
...
}
Will this overwrite destroy the PHY pipeline adjustment made here before the
tuning read even executes, causing the tuning reads to fail?
> +
> + cqspi_phy_set_dll_master(cqspi);
> +}
[ ... ]
> +static int cqspi_find_rx_high_sdr(struct cqspi_flash_pdata *f_pdata,
> + struct spi_mem *mem, struct phy_setting *phy,
> + u8 lowerbound)
> +{
> + struct device *dev = &f_pdata->cqspi->pdev->dev;
> + int ret;
> +
> + phy->rx = CQSPI_PHY_MAX_DELAY;
> + do {
> + ret = cqspi_phy_apply_setting(f_pdata, phy);
> + if (!ret) {
> + ret = cqspi_phy_check_pattern(f_pdata, mem);
> + if (!ret)
> + return 0;
> + }
> + phy->rx--;
> + } while (phy->rx > lowerbound);
[Severity: Medium]
Does this loop condition prematurely fail for 1-tap wide windows?
If the valid SDR window is exactly 1-tap wide, the upper boundary will be
equal to lowerbound. Because the loop condition is strictly greater-than
(phy->rx > lowerbound), it will exit immediately when reaching lowerbound
without returning success for that tap. Should this be >= lowerbound so
narrow windows are properly detected?
> +
> + dev_dbg(dev, "Unable to find RX high\n");
> + return -ENOENT;
> +}
[ ... ]
> @@ -1588,9 +3331,12 @@ static int cqspi_of_get_flash_pdata(struct platform_device *pdev,
> return -ENXIO;
> }
>
> - if (of_property_read_u32(np, "spi-max-frequency", &f_pdata->clk_rate)) {
> - dev_err(&pdev->dev, "couldn't determine spi-max-frequency\n");
> - return -ENXIO;
> + part_np = of_parse_phandle(np, "spi-phy-pattern-partition", 0);
> + if (part_np) {
> + f_pdata->phy_pattern_partition_valid =
> + !of_property_read_u32_index(part_np, "reg", 0,
> + &f_pdata->phy_pattern_partition_offset);
[Severity: Medium]
Will this truncate 64-bit partition offsets?
If the SPI device specifies #address-cells = <2>, the reg property contains
a 64-bit address. Reading it with of_property_read_u32_index() fetches only
the upper 32 bits (which is 0 for offsets under 4GB). This could lead to the
pattern being read from the wrong flash offset instead of the dedicated
partition.
> + of_node_put(part_np);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=8
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation
2026-08-25 17:17 ` [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation Santhosh Kumar K
@ 2026-08-25 17:43 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:43 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Richard Weinberger, Vignesh Raghavendra, robh, conor+dt,
Michael Walle, Takahiro Kuwano, Miquel Raynal, Pratyush Yadav,
devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Unconditional overwrite of `spinand->cont_read_possible` bypasses system capability checks, risking unsupported continuous reads.
--
commit 6afbb44eacdd7b8d06d61615ac789bcc3b8a5186
Author: Santhosh Kumar K <s-k6@ti.com>
mtd: spinand: negotiate optimal controller operating point before dirmap creation
This commit introduces a mechanism to calibrate the PHY before creating the
dirmap so the validated max frequency is embedded into the dirmap operations.
It adds a ranked fallback iteration to find optimal variants if the
pre-selected one fails, updating op templates and continuous read capabilities
accordingly.
> diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
> index f839438a44ff2..cd353eb8b5374 100644
> --- a/drivers/mtd/nand/spi/core.c
> +++ b/drivers/mtd/nand/spi/core.c
[ ... ]
> @@ -2050,6 +2121,198 @@ static void spinand_cleanup(struct spinand_device *spinand)
[ ... ]
> +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);
> + spinand->cont_read_possible =
> + !!templates->cont_read_cache;
[Severity: High]
Does this code unconditionally overwrite the system capability flag for
continuous reads?
Earlier in the initialization sequence, spinand_cont_read_init() may have
cleared spinand->cont_read_possible due to system-level constraints, such as
pipelined ECC requirements or lack of controller support. By setting it here
based solely on the existence of an op template, it appears we might re-enable
continuous reads on hardware that cannot actually support it.
Could this lead to I/O timeouts or potential data corruption when the subsystem
attempts unsupported continuous read operations?
> + }
> + return true;
> + }
> + }
> + return false;
> +}
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=16
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation
2026-08-25 17:17 ` [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation Santhosh Kumar K
@ 2026-08-25 17:45 ` sashiko-bot
0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-25 17:45 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: Miquel Raynal, Vignesh Raghavendra, devicetree, Michael Walle,
Richard Weinberger, conor+dt, Pratyush Yadav, Takahiro Kuwano,
robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The validated tuning frequency (`nor->max_read_op.max_freq`) is indiscriminately applied to all read operations constructed via `spi_nor_spimem_get_read_op()`. This incorrectly forces low-speed alternative read commands (such as OTP reads) to execute at the high-speed PHY-calibrated frequency.
--
commit 6decf5586ab8fae3319b12bc93ad82202b53b090
Author: Santhosh Kumar K <s-k6@ti.com>
mtd: spi-nor: run controller optimization before dirmap creation
This commit runs controller optimization before creating dirmaps so the
validated frequency is available at dirmap construction time. It stores the
configured read op template and propagates the calibrated rate into regular
read ops. Optimization failure is non-fatal.
> diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c
> index b683c077a233d..2efcdc7f637fb 100644
> --- a/drivers/mtd/spi-nor/core.c
> +++ b/drivers/mtd/spi-nor/core.c
> @@ -216,6 +216,9 @@ static struct spi_mem_op spi_nor_spimem_get_read_op(struct spi_nor *nor)
> if (spi_nor_protocol_is_dtr(nor->read_proto))
> op.dummy.nbytes *= 2;
>
> + /* Propagate the validated frequency; zero before tuning. */
> + op.max_freq = nor->max_read_op.max_freq;
> +
[Severity: High]
Does applying the tuned frequency to all read operations cause issues for
lower-speed commands?
When reading the OTP area via the MTD API, spi_nor_otp_read_secr() in
drivers/mtd/spi-nor/otp.c temporarily changes the opcode and sets the
protocol to 1-1-1, then calls spi_nor_read_data():
drivers/mtd/spi-nor/otp.c:spi_nor_otp_read_secr() {
...
nor->read_opcode = SPINOR_OP_RSECR;
nor->read_proto = SNOR_PROTO_1_1_1;
ret = spi_nor_read_data(nor, addr, len, buf);
...
}
Because spi_nor_spimem_get_read_op() unconditionally sets op.max_freq to the
high-speed PHY-calibrated frequency, will this force the 1-1-1 OTP read to
execute at an inappropriately high clock speed and potentially fail or
return corrupted data?
> return op;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260825171737.776052-1-s-k6@ti.com?part=18
^ permalink raw reply [flat|nested] 30+ messages in thread
* Re: [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support
2026-08-25 17:17 ` [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-08-25 17:40 ` sashiko-bot
@ 2026-08-26 21:43 ` Mark Brown
1 sibling, 0 replies; 30+ messages in thread
From: Mark Brown @ 2026-08-26 21:43 UTC (permalink / raw)
To: Santhosh Kumar K
Cc: robh, krzk+dt, conor+dt, miquel.raynal, richard, vigneshr,
pratyush, mwalle, takahiro.kuwano, linux-spi, devicetree,
linux-kernel, linux-mtd, praneeth, sebin.francis, u-kumar1,
a-dutta
[-- Attachment #1: Type: text/plain, Size: 1870 bytes --]
On Tue, Aug 25, 2026 at 10:47:27PM +0530, Santhosh Kumar K wrote:
> The Cadence QSPI controller supports a delay-line PHY for high-speed
> operation. Without calibration the PHY is unused and read capture relies
> on a fixed delay, limiting throughput at frequencies above the base
> operating speed.
> Add an execute_tuning callback that performs delay-line calibration using
> a known data pattern written to a dedicated flash region. The pattern is
> either read from a NOR partition identified by the DT property
> spi-phy-pattern-partition, or written to the NAND page cache before
> each calibration read.
> struct cqspi_flash_pdata {
> + bool use_dqs;
> + bool use_tuned_phy;
The AM65x has a fun erratum i2189 which mentions that it requires
disabling DQS for writes:
https://www.ti.com.cn/lit/er/sprz452i/sprz452i.pdf
which suggests we might need separate controls for read and write
operation.
> +static int cqspi_write_pattern_to_cache(struct cqspi_flash_pdata *f_pdata,
> + struct spi_mem *mem,
> + const struct spi_mem_op *write_op)
> +{
> + struct spi_controller *ctlr = mem->spi->controller;
> + struct device *dev = &f_pdata->cqspi->pdev->dev;
> + struct spi_mem_op op = *write_op;
> + int ret;
> +
> + op.max_freq = mem->spi->max_speed_hz;
> + op.data.nbytes = sizeof(phy_tuning_pattern);
> + op.data.buf.out = phy_tuning_pattern;
> +
> + ret = ctlr->mem_ops->exec_op(mem, &op);
> + if (ret) {
> + dev_err(dev, "Failed to write PHY pattern to cache: %d\n", ret);
> + return ret;
> + }
> + dev_dbg(dev, "PHY pattern (%zu bytes) written to cache\n",
> + sizeof(phy_tuning_pattern));
> +
> + return 0;
> +}
spinand_write_page() has a multi-operation sequence, I've not checked if
any fancy flashes with DQS support actually need that. _read_page()
looks more straightforward.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 30+ messages in thread
end of thread, other threads:[~2026-08-26 21:43 UTC | newest]
Thread overview: 30+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-25 17:17 [PATCH v9 00/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 01/18] spi: dt-bindings: add spi-max-post-config-frequency-hz property Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 02/18] spi: dt-bindings: add spi-phy-pattern-partition property Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 03/18] spi: parse spi-max-post-config-frequency-hz into post_config_max_speed_hz Santhosh Kumar K
2026-08-25 17:30 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 04/18] spi: spi-mem: teach spi_mem_adjust_op_freq() about post-config ops Santhosh Kumar K
2026-08-25 17:30 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 05/18] spi: spi-mem: add execute_tuning callback and spi_mem_execute_tuning() Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 06/18] spi: cadence-quadspi: move cqspi_readdata_capture earlier Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 07/18] spi: cadence-quadspi: add DQS support to read data capture Santhosh Kumar K
2026-08-25 17:29 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 08/18] spi: cadence-quadspi: add PHY tuning support Santhosh Kumar K
2026-08-25 17:40 ` sashiko-bot
2026-08-26 21:43 ` Mark Brown
2026-08-25 17:17 ` [PATCH v9 09/18] spi: cadence-quadspi: skip DDR PHY tuning for 2-byte-address ops (i2383) Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 10/18] spi: cadence-quadspi: refactor direct read path for PHY support Santhosh Kumar K
2026-08-25 17:31 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 11/18] spi: cadence-quadspi: enable PHY for direct reads Santhosh Kumar K
2026-08-25 17:32 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 12/18] spi: cadence-quadspi: enable PHY for indirect writes Santhosh Kumar K
2026-08-25 17:32 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 13/18] spi: cadence-quadspi: reprogram CS timing on every chip-select switch Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 14/18] spi: cadence-quadspi: reprogram PHY DLL on runtime resume Santhosh Kumar K
2026-08-25 17:36 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 15/18] mtd: spinand: extract variant ranking logic into spinand_op_find_best_variant() Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 16/18] mtd: spinand: negotiate optimal controller operating point before dirmap creation Santhosh Kumar K
2026-08-25 17:43 ` sashiko-bot
2026-08-25 17:17 ` [PATCH v9 17/18] mtd: spi-nor: extract read op template construction into helper Santhosh Kumar K
2026-08-25 17:17 ` [PATCH v9 18/18] mtd: spi-nor: run controller optimization before dirmap creation Santhosh Kumar K
2026-08-25 17:45 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox