[PATCH] mmc: sdhci-cadence: trigger tuning for SD HS mode on SD6HC (v6) PHY

tze.yee.ng at altera.com tze.yee.ng at altera.com
Tue May 5 04:36:03 CEST 2026


From: Tze Yee Ng <tze.yee.ng at altera.com>

The Cadence SD6HC (SDHCI spec v4.20+) controller uses a soft PHY whose
DLL delay characteristics vary with PVT (Process, Voltage, Temperature)
and board-level trace routing.

A static delay value programmed via device tree for SD High Speed mode is
insufficient because the optimal sampling point varies per board, SD card,
and operating conditions. Runtime calibration is required.

While the SD Physical Layer Specification does not mandate tuning for
SD HS mode (only for UHS-I SDR50/SDR104), the Cadence SD6HC PHY
requires runtime calibration of its receive data delay line to find a
valid sampling window under constrained clock conditions.

The tuning is triggered from the set_ios_post callback because at that
moment hardware has committed the new bus width, clock frequency, and speed
mode to the controller registers. This ensuring the tuning sequence runs
at the correct SD HS operating conditions.

The tuning is gated by a device tree property "cdns,sd-hs-tuning" so
that only boards requiring runtime calibration opt in. When enabled,
the driver performs a 40-tap DLL sweep using CMD19 to find the largest
consecutive passing window, then programs the midpoint into
PHY_DLL_SLAVE_CTRL_REG.

To enable on a board, add to the MMC node in device tree:

    &mmc {
        cdns,sd-hs-tuning;
    };

Signed-off-by: Tze Yee Ng <tze.yee.ng at altera.com>
---
 drivers/mmc/sdhci-cadence.c  | 108 ++++++++++++++++++++++++++++++++++-
 drivers/mmc/sdhci-cadence.h  |   5 ++
 drivers/mmc/sdhci-cadence6.c |  45 ++++++++++++++-
 3 files changed, 156 insertions(+), 2 deletions(-)

diff --git a/drivers/mmc/sdhci-cadence.c b/drivers/mmc/sdhci-cadence.c
index 5bbc18dfa51..a76f9e8d6bd 100644
--- a/drivers/mmc/sdhci-cadence.c
+++ b/drivers/mmc/sdhci-cadence.c
@@ -39,6 +39,9 @@ static const struct sdhci_cdns_phy_cfg sdhci_cdns_phy_cfgs[] = {
 	{ "cdns,phy-dll-delay-strobe", SDHCI_CDNS_PHY_DLY_STROBE, },
 };
 
+static int __maybe_unused sdhci_cdns_execute_tuning(struct udevice *dev,
+						    unsigned int opcode);
+
 static int sdhci_cdns_write_phy_reg(struct sdhci_cdns_plat *plat,
 				    u8 addr, u8 data)
 {
@@ -155,8 +158,93 @@ static void sdhci_cdns_set_control_reg(struct sdhci_host *host)
 		sdhci_cdns6_phy_adj(mmc->dev, plat, mmc->selected_mode);
 }
 
+static __maybe_unused bool sdhci_cdns_sd_needs_tuning(struct mmc *mmc)
+{
+	struct sdhci_cdns_plat *plat = dev_get_plat(mmc->dev);
+
+	if (!IS_SD(mmc))
+		return false;
+
+	if (!dev_read_bool(mmc->dev, "cdns,sd-hs-tuning"))
+		return false;
+
+	/* Already tuned for this mode */
+	if (plat->tuned_mode == mmc->selected_mode)
+		return false;
+
+	switch (mmc->selected_mode) {
+	case SD_HS:
+		return mmc->bus_width == 4;
+	/* Add future modes here, e.g.:
+	 * case UHS_SDR50:
+	 *	return true;
+	 */
+	default:
+		return false;
+	}
+}
+
+static int sdhci_cdns_set_ios_post(struct sdhci_host *host)
+{
+	struct mmc *mmc = host->mmc;
+	struct sdhci_cdns_plat *plat = dev_get_plat(mmc->dev);
+	int ret __maybe_unused;
+	/*
+	 * The SD6HC soft PHY requires runtime DLL delay calibration
+	 * for SD High Speed mode. The default PHY_DLL_SLAVE_CTRL_REG
+	 * values (READ_DQS_CMD_DELAY and READ_DQS_DELAY = 0) do not
+	 * provide sufficient timing margin due to PVT and board trace
+	 * variations.
+	 *
+	 * Tuning is performed once per entry into SD_HS mode
+	 * (tracked by plat->tuned_mode state). The calibrated PHY delay
+	 * values remain valid while the card stays in SD_HS mode, and
+	 * leaving that tuned mode clears the state so re-entering SD_HS
+	 * triggers tuning again.
+	 *
+	 * This must be done in set_ios_post (not set_control_reg)
+	 * because the SDHCI controller must already be operating at
+	 * the target bus width, clock, and speed mode before CMD19
+	 * tuning commands can succeed.
+	 */
+
+	if (IS_ENABLED(CONFIG_MMC_SUPPORTS_TUNING)) {
+		if (SDHCI_GET_VERSION(host) >= SDHCI_SPEC_420 &&
+		    sdhci_cdns_sd_needs_tuning(mmc)) {
+			ret = sdhci_cdns_execute_tuning(mmc->dev,
+							MMC_CMD_SEND_TUNING_BLOCK);
+			if (ret) {
+				dev_err(mmc->dev,
+					"SD_HS tuning failed (ret=%d), using default PHY\n",
+					ret);
+				/* Restore default PHY settings and avoid retrying in this mode */
+				sdhci_cdns6_phy_adj(mmc->dev, plat,
+						    mmc->selected_mode);
+				plat->tuned_mode = mmc->selected_mode;
+				plat->tuned_dll_slave_ctrl = sdhci_cdns6_phy_get_dll_slave(plat);
+				return 0;
+			}
+			/*
+			 * Tuning succeeded. The tuned_mode is already set by
+			 * execute_tuning(), so the tuned value will be preserved
+			 * across subsequent PHY reconfigurations.
+			 */
+			dev_dbg(mmc->dev, "SD_HS tuning successful\n");
+		}
+
+		/* Reset when mode changes away from a tuned mode */
+		if (mmc->selected_mode != plat->tuned_mode) {
+			plat->tuned_mode = MMC_MODES_END;
+			plat->tuned_dll_slave_ctrl = 0;
+		}
+	}
+
+	return 0;
+}
+
 static const struct sdhci_ops sdhci_cdns_ops = {
 	.set_control_reg = sdhci_cdns_set_control_reg,
+	.set_ios_post = sdhci_cdns_set_ios_post,
 };
 
 static int sdhci_cdns_set_tune_val(struct sdhci_cdns_plat *plat,
@@ -204,6 +292,7 @@ static int __maybe_unused sdhci_cdns_execute_tuning(struct udevice *dev,
 	int cur_streak = 0;
 	int max_streak = 0;
 	int end_of_streak = 0;
+	int ret;
 	int i;
 
 	/*
@@ -229,7 +318,24 @@ static int __maybe_unused sdhci_cdns_execute_tuning(struct udevice *dev,
 		return -EIO;
 	}
 
-	return sdhci_cdns_set_tune_val(plat, end_of_streak - max_streak / 2);
+	ret = sdhci_cdns_set_tune_val(plat, end_of_streak - max_streak / 2);
+	if (ret)
+		return ret;
+
+	/*
+	 * Mark this mode as tuned. This is critical for both driver tuning
+	 * (SD_HS via set_ios_post) and framework tuning (UHS_SDR104, MMC_HS_200,
+	 * MMC_HS_400) so that subsequent PHY reconfigurations restore the
+	 * calibrated DLL value instead of overwriting with DT defaults.
+	 *
+	 * For HS400, tuning is performed while the controller is in HS200 mode
+	 * (mmc->selected_mode == MMC_HS_200 and mmc->hs400_tuning == true).
+	 * Record the tuned mode as MMC_HS_400 so the calibrated DLL value is
+	 * preserved across the HS200→HS400 transition.
+	 */
+	plat->tuned_mode = mmc->hs400_tuning ? MMC_HS_400 : mmc->selected_mode;
+
+	return 0;
 }
 
 static struct dm_mmc_ops sdhci_cdns_mmc_ops;
diff --git a/drivers/mmc/sdhci-cadence.h b/drivers/mmc/sdhci-cadence.h
index 7101f00b75b..ea517491860 100644
--- a/drivers/mmc/sdhci-cadence.h
+++ b/drivers/mmc/sdhci-cadence.h
@@ -7,6 +7,8 @@
 #ifndef SDHCI_CADENCE_H_
 #define SDHCI_CADENCE_H_
 
+#include <mmc.h>
+
 /* HRS - Host Register Set (specific to Cadence) */
 /* PHY access port */
 #define SDHCI_CDNS_HRS04		0x10
@@ -60,10 +62,13 @@ struct sdhci_cdns_plat {
 	struct mmc_config cfg;
 	struct mmc mmc;
 	void __iomem *hrs_addr;
+	enum bus_mode tuned_mode;
+	u32 tuned_dll_slave_ctrl;
 };
 
 int sdhci_cdns6_phy_adj(struct udevice *dev, struct sdhci_cdns_plat *plat, u32 mode);
 int sdhci_cdns6_phy_init(struct udevice *dev, struct sdhci_cdns_plat *plat);
 int sdhci_cdns6_set_tune_val(struct sdhci_cdns_plat *plat, unsigned int val);
+u32 sdhci_cdns6_phy_get_dll_slave(struct sdhci_cdns_plat *plat);
 
 #endif
diff --git a/drivers/mmc/sdhci-cadence6.c b/drivers/mmc/sdhci-cadence6.c
index ca1086e2359..c8b42532e17 100644
--- a/drivers/mmc/sdhci-cadence6.c
+++ b/drivers/mmc/sdhci-cadence6.c
@@ -173,6 +173,30 @@ static void sdhci_cdns6_write_phy_reg(struct sdhci_cdns_plat *plat, u32 addr, u3
 	writel(val, plat->hrs_addr + SDHCI_CDNS_HRS05);
 }
 
+static bool sdhci_cdns6_mode_is_tuned(struct sdhci_cdns_plat *plat, u32 mode)
+{
+	/*
+	 * Check if the given mode has a valid tuned DLL value.
+	 * Only modes that support tuning (driver or framework) can have
+	 * valid tuned values. This prevents the initial state (tuned_mode=0)
+	 * from falsely matching MMC_LEGACY.
+	 */
+	if (plat->tuned_mode != mode)
+		return false;
+
+	switch (mode) {
+	case SD_HS:		/* Driver tuning via set_ios_post */
+	case UHS_SDR50:		/* Future driver tuning support */
+	case UHS_SDR104:	/* Framework tuning */
+	case MMC_HS_200:	/* Framework tuning */
+	case MMC_HS_400:	/* Framework tuning */
+	case MMC_HS_400_ES:	/* Framework tuning */
+		return true;
+	default:
+		return false;
+	}
+}
+
 static int sdhci_cdns6_reset_phy_dll(struct sdhci_cdns_plat *plat, bool reset)
 {
 	void __iomem *reg = plat->hrs_addr + SDHCI_CDNS_HRS09;
@@ -259,7 +283,18 @@ int sdhci_cdns6_phy_adj(struct udevice *dev, struct sdhci_cdns_plat *plat, u32 m
 	sdhci_cdns6_write_phy_reg(plat, PHY_DQS_TIMING_REG_ADDR, sdhci_cdns6_phy_cfgs[0].val);
 	sdhci_cdns6_write_phy_reg(plat, PHY_GATE_LPBK_CTRL_REG_ADDR, sdhci_cdns6_phy_cfgs[1].val);
 	sdhci_cdns6_write_phy_reg(plat, PHY_DLL_MASTER_CTRL_REG_ADDR, sdhci_cdns6_phy_cfgs[4].val);
-	sdhci_cdns6_write_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR, sdhci_cdns6_phy_cfgs[2].val);
+	if (sdhci_cdns6_mode_is_tuned(plat, mode)) {
+		/*
+		 * Use previously saved tuned DLL slave control value.
+		 * Note: 0 is a valid tuned value (e.g., optimal tap at position 0),
+		 * so we check both mode match AND that it's a tunable mode.
+		 */
+		sdhci_cdns6_write_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR,
+					  plat->tuned_dll_slave_ctrl);
+	} else {
+		sdhci_cdns6_write_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR,
+					  sdhci_cdns6_phy_cfgs[2].val);
+	}
 
 	/* Switch Off the DLL Reset */
 	ret = sdhci_cdns6_reset_phy_dll(plat, false);
@@ -318,6 +353,9 @@ int sdhci_cdns6_set_tune_val(struct sdhci_cdns_plat *plat, unsigned int val)
 
 	sdhci_cdns6_write_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR, tmp);
 
+	/* Store tuned DLL slave control value which will be reapplied via set_ios(). */
+	plat->tuned_dll_slave_ctrl = tmp;
+
 	/* Switch Off the DLL Reset */
 	ret = sdhci_cdns6_reset_phy_dll(plat, false);
 	if (ret) {
@@ -327,3 +365,8 @@ int sdhci_cdns6_set_tune_val(struct sdhci_cdns_plat *plat, unsigned int val)
 
 	return 0;
 }
+
+u32 sdhci_cdns6_phy_get_dll_slave(struct sdhci_cdns_plat *plat)
+{
+	return sdhci_cdns6_read_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR);
+}
-- 
2.43.7



More information about the U-Boot mailing list