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

Peng Fan peng.fan at oss.nxp.com
Wed May 6 04:14:27 CEST 2026


On Mon, May 04, 2026 at 07:36:03PM -0700, tze.yee.ng at altera.com wrote:
>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;

Has this property been accepted by Linux Upstream?

Regards
Peng

>    };
>
>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