On Mon, May 04, 2026 at 07:36:03PM -0700, [email protected] wrote: >From: Tze Yee Ng <[email protected]> > >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 <[email protected]> >--- > 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 > >

