Implement software tuning algorithm to enable UHS-I SDR modes for SD
card operation and HS200 mode for eMMC. This adds both TX and RX delay
line tuning based on the SpacemiT K1 controller capabilities.
Algorithm features:
- Add tuning register definitions (RX_CFG, DLINE_CTRL, DLINE_CFG)
- Conditional tuning: only for high-speed modes (≥100MHz)
- TX tuning: configure transmit delay line with optimal values
(dline_reg=0, delaycode=127) to ensure optimal signal output timing
- RX tuning: single-pass window detection algorithm testing full
delay range (0-255) to find optimal receive timing window
- Retry mechanism: multiple fallback delays within optimal window
for improved reliability
Tested-by: Anand Moon <linux.amoon@gmail.com>
Signed-off-by: Iker Pedrosa <ikerpedrosam@gmail.com>
---
drivers/mmc/host/sdhci-of-k1.c | 172 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 172 insertions(+)
diff --git a/drivers/mmc/host/sdhci-of-k1.c b/drivers/mmc/host/sdhci-of-k1.c
index 01afdadcf70796704b272ee5a31543afd5e01188..83d7f9fad5c1fba2f07624ee657cd979a1c7e25d 100644
--- a/drivers/mmc/host/sdhci-of-k1.c
+++ b/drivers/mmc/host/sdhci-of-k1.c
@@ -69,6 +69,28 @@
#define SDHC_PHY_DRIVE_SEL GENMASK(2, 0)
#define SDHC_RX_BIAS_CTRL BIT(5)
+#define SPACEMIT_SDHC_RX_CFG_REG 0x118
+#define SDHC_RX_SDCLK_SEL0_MASK GENMASK(1, 0)
+#define SDHC_RX_SDCLK_SEL1_MASK GENMASK(3, 2)
+#define SDHC_RX_SDCLK_SEL1 FIELD_PREP(SDHC_RX_SDCLK_SEL1_MASK, 1)
+
+#define SPACEMIT_SDHC_DLINE_CTRL_REG 0x130
+#define SDHC_DLINE_PU BIT(0)
+#define SDHC_RX_DLINE_CODE_MASK GENMASK(23, 16)
+#define SDHC_TX_DLINE_CODE_MASK GENMASK(31, 24)
+
+#define SPACEMIT_SDHC_DLINE_CFG_REG 0x134
+#define SDHC_RX_DLINE_REG_MASK GENMASK(7, 0)
+#define SDHC_RX_DLINE_GAIN BIT(8)
+#define SDHC_TX_DLINE_REG_MASK GENMASK(23, 16)
+
+#define SPACEMIT_RX_DLINE_REG 9
+#define SPACEMIT_RX_TUNE_DELAY_MIN 0x0
+#define SPACEMIT_RX_TUNE_DELAY_MAX 0xFF
+
+#define SPACEMIT_TX_TUNING_DLINE_REG 0x00
+#define SPACEMIT_TX_TUNING_DELAYCODE 127
+
struct spacemit_sdhci_host {
struct clk *clk_core;
struct clk *clk_io;
@@ -96,6 +118,50 @@ static inline void spacemit_sdhci_clrsetbits(struct sdhci_host *host, u32 clr, u
sdhci_writel(host, val, reg);
}
+static void spacemit_sdhci_set_rx_delay(struct sdhci_host *host, u8 delay)
+{
+ spacemit_sdhci_clrsetbits(host, SDHC_RX_DLINE_CODE_MASK,
+ FIELD_PREP(SDHC_RX_DLINE_CODE_MASK, delay),
+ SPACEMIT_SDHC_DLINE_CTRL_REG);
+}
+
+static void spacemit_sdhci_set_tx_delay(struct sdhci_host *host, u8 delay)
+{
+ spacemit_sdhci_clrsetbits(host, SDHC_TX_DLINE_CODE_MASK,
+ FIELD_PREP(SDHC_TX_DLINE_CODE_MASK, delay),
+ SPACEMIT_SDHC_DLINE_CTRL_REG);
+}
+
+static void spacemit_sdhci_set_tx_dline_reg(struct sdhci_host *host, u8 dline_reg)
+{
+ spacemit_sdhci_clrsetbits(host, SDHC_TX_DLINE_REG_MASK,
+ FIELD_PREP(SDHC_TX_DLINE_REG_MASK, dline_reg),
+ SPACEMIT_SDHC_DLINE_CFG_REG);
+}
+
+static void spacemit_sdhci_tx_tuning_prepare(struct sdhci_host *host)
+{
+ spacemit_sdhci_setbits(host, SDHC_TX_MUX_SEL, SPACEMIT_SDHC_TX_CFG_REG);
+ spacemit_sdhci_setbits(host, SDHC_DLINE_PU, SPACEMIT_SDHC_DLINE_CTRL_REG);
+ udelay(5);
+}
+
+static void spacemit_sdhci_prepare_tuning(struct sdhci_host *host)
+{
+ spacemit_sdhci_clrsetbits(host, SDHC_RX_DLINE_REG_MASK,
+ FIELD_PREP(SDHC_RX_DLINE_REG_MASK, SPACEMIT_RX_DLINE_REG),
+ SPACEMIT_SDHC_DLINE_CFG_REG);
+
+ spacemit_sdhci_setbits(host, SDHC_DLINE_PU, SPACEMIT_SDHC_DLINE_CTRL_REG);
+ udelay(5);
+
+ spacemit_sdhci_clrsetbits(host, SDHC_RX_SDCLK_SEL1_MASK, SDHC_RX_SDCLK_SEL1,
+ SPACEMIT_SDHC_RX_CFG_REG);
+
+ if (host->mmc->ios.timing == MMC_TIMING_MMC_HS200)
+ spacemit_sdhci_setbits(host, SDHC_HS200_USE_RFIFO, SPACEMIT_SDHC_PHY_FUNC_REG);
+}
+
static void spacemit_sdhci_reset(struct sdhci_host *host, u8 mask)
{
sdhci_reset(host, mask);
@@ -191,6 +257,111 @@ static unsigned int spacemit_sdhci_clk_get_max_clock(struct sdhci_host *host)
return clk_get_rate(pltfm_host->clk);
}
+static int spacemit_sdhci_execute_tuning(struct sdhci_host *host, u32 opcode)
+{
+ int current_len = 0, current_start = 0;
+ int max_pass_len = 0, max_pass_start = 0;
+ struct mmc_host *mmc = host->mmc;
+ struct mmc_ios ios = mmc->ios;
+ u8 final_delay;
+ int ret = 0;
+ int i;
+
+ /*
+ * Tuning is required for SDR50/SDR104, HS200/HS400 cards and
+ * if clock frequency is greater than 100MHz in these modes.
+ */
+ if (host->clock < 100 * 1000 * 1000 ||
+ !(ios.timing == MMC_TIMING_MMC_HS200 ||
+ ios.timing == MMC_TIMING_UHS_SDR50 ||
+ ios.timing == MMC_TIMING_UHS_SDR104))
+ return 0;
+
+ if (mmc->caps2 & MMC_CAP2_NO_MMC) {
+ spacemit_sdhci_set_tx_dline_reg(host, SPACEMIT_TX_TUNING_DLINE_REG);
+ spacemit_sdhci_set_tx_delay(host, SPACEMIT_TX_TUNING_DELAYCODE);
+ spacemit_sdhci_tx_tuning_prepare(host);
+
+ dev_dbg(mmc_dev(host->mmc), "TX tuning: dline_reg=%d, delaycode=%d\n",
+ SPACEMIT_TX_TUNING_DLINE_REG, SPACEMIT_TX_TUNING_DELAYCODE);
+ }
+
+ spacemit_sdhci_prepare_tuning(host);
+
+ for (i = SPACEMIT_RX_TUNE_DELAY_MIN; i <= SPACEMIT_RX_TUNE_DELAY_MAX; i++) {
+ spacemit_sdhci_set_rx_delay(host, i);
+ ret = mmc_send_tuning(host->mmc, opcode, NULL);
+
+ dev_dbg(mmc_dev(host->mmc), "RX delay %d: %s\n",
+ i, ret == 0 ? "pass" : "fail");
+
+ if (ret == 0) {
+ /* Test passed - extend current window */
+ if (current_len == 0)
+ current_start = i;
+ current_len++;
+ } else {
+ /* Test failed - check if current window is best so far */
+ if (current_len > max_pass_len) {
+ max_pass_len = current_len;
+ max_pass_start = current_start;
+ }
+ current_len = 0;
+ }
+ }
+
+ if (current_len > max_pass_len) {
+ max_pass_len = current_len;
+ max_pass_start = current_start;
+ }
+
+ if (max_pass_len < 3) {
+ dev_err(mmc_dev(host->mmc), "Tuning failed: no stable window found\n");
+ return -EIO;
+ }
+
+ final_delay = max_pass_start + max_pass_len / 2;
+ spacemit_sdhci_set_rx_delay(host, final_delay);
+ ret = mmc_send_tuning(host->mmc, opcode, NULL);
+ if (ret) {
+ u8 retry_delays[] = {
+ max_pass_start + max_pass_len / 4,
+ max_pass_start + (3 * max_pass_len) / 4,
+ max_pass_start,
+ max_pass_start + max_pass_len - 1
+ };
+ int retry_count = ARRAY_SIZE(retry_delays);
+
+ dev_warn(mmc_dev(mmc), "Primary delay %d failed, trying alternatives\n",
+ final_delay);
+
+ for (i = 0; i < retry_count; i++) {
+ if (retry_delays[i] >= SPACEMIT_RX_TUNE_DELAY_MIN &&
+ retry_delays[i] <= SPACEMIT_RX_TUNE_DELAY_MAX) {
+ spacemit_sdhci_set_rx_delay(host, retry_delays[i]);
+ ret = mmc_send_tuning(host->mmc, opcode, NULL);
+ if (!ret) {
+ final_delay = retry_delays[i];
+ dev_info(mmc_dev(mmc), "Retry successful with delay %d\n",
+ final_delay);
+ break;
+ }
+ }
+ }
+
+ if (ret) {
+ dev_err(mmc_dev(mmc), "All retry attempts failed\n");
+ return -EIO;
+ }
+ }
+
+ dev_dbg(mmc_dev(host->mmc),
+ "Tuning successful: window %d-%d, using delay %d\n",
+ max_pass_start, max_pass_start + max_pass_len - 1, final_delay);
+
+ return 0;
+}
+
static int spacemit_sdhci_pre_select_hs400(struct mmc_host *mmc)
{
struct sdhci_host *host = mmc_priv(mmc);
@@ -314,6 +485,7 @@ static const struct sdhci_ops spacemit_sdhci_ops = {
.set_clock = spacemit_sdhci_set_clock,
.set_uhs_signaling = spacemit_sdhci_set_uhs_signaling,
.voltage_switch = spacemit_sdhci_voltage_switch,
+ .platform_execute_tuning = spacemit_sdhci_execute_tuning,
};
static const struct sdhci_pltfm_data spacemit_sdhci_k1_pdata = {
--
2.53.0
On 16/03/2026 16:03, Iker Pedrosa wrote:
> Implement software tuning algorithm to enable UHS-I SDR modes for SD
> card operation and HS200 mode for eMMC. This adds both TX and RX delay
> line tuning based on the SpacemiT K1 controller capabilities.
>
> Algorithm features:
> - Add tuning register definitions (RX_CFG, DLINE_CTRL, DLINE_CFG)
> - Conditional tuning: only for high-speed modes (≥100MHz)
> - TX tuning: configure transmit delay line with optimal values
> (dline_reg=0, delaycode=127) to ensure optimal signal output timing
> - RX tuning: single-pass window detection algorithm testing full
> delay range (0-255) to find optimal receive timing window
> - Retry mechanism: multiple fallback delays within optimal window
> for improved reliability
>
> Tested-by: Anand Moon <linux.amoon@gmail.com>
> Signed-off-by: Iker Pedrosa <ikerpedrosam@gmail.com>
Acked-by: Adrian Hunter <adrian.hunter@intel.com>
> ---
> drivers/mmc/host/sdhci-of-k1.c | 172 +++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 172 insertions(+)
>
> diff --git a/drivers/mmc/host/sdhci-of-k1.c b/drivers/mmc/host/sdhci-of-k1.c
> index 01afdadcf70796704b272ee5a31543afd5e01188..83d7f9fad5c1fba2f07624ee657cd979a1c7e25d 100644
> --- a/drivers/mmc/host/sdhci-of-k1.c
> +++ b/drivers/mmc/host/sdhci-of-k1.c
> @@ -69,6 +69,28 @@
> #define SDHC_PHY_DRIVE_SEL GENMASK(2, 0)
> #define SDHC_RX_BIAS_CTRL BIT(5)
>
> +#define SPACEMIT_SDHC_RX_CFG_REG 0x118
> +#define SDHC_RX_SDCLK_SEL0_MASK GENMASK(1, 0)
> +#define SDHC_RX_SDCLK_SEL1_MASK GENMASK(3, 2)
> +#define SDHC_RX_SDCLK_SEL1 FIELD_PREP(SDHC_RX_SDCLK_SEL1_MASK, 1)
> +
> +#define SPACEMIT_SDHC_DLINE_CTRL_REG 0x130
> +#define SDHC_DLINE_PU BIT(0)
> +#define SDHC_RX_DLINE_CODE_MASK GENMASK(23, 16)
> +#define SDHC_TX_DLINE_CODE_MASK GENMASK(31, 24)
> +
> +#define SPACEMIT_SDHC_DLINE_CFG_REG 0x134
> +#define SDHC_RX_DLINE_REG_MASK GENMASK(7, 0)
> +#define SDHC_RX_DLINE_GAIN BIT(8)
> +#define SDHC_TX_DLINE_REG_MASK GENMASK(23, 16)
> +
> +#define SPACEMIT_RX_DLINE_REG 9
> +#define SPACEMIT_RX_TUNE_DELAY_MIN 0x0
> +#define SPACEMIT_RX_TUNE_DELAY_MAX 0xFF
> +
> +#define SPACEMIT_TX_TUNING_DLINE_REG 0x00
> +#define SPACEMIT_TX_TUNING_DELAYCODE 127
> +
> struct spacemit_sdhci_host {
> struct clk *clk_core;
> struct clk *clk_io;
> @@ -96,6 +118,50 @@ static inline void spacemit_sdhci_clrsetbits(struct sdhci_host *host, u32 clr, u
> sdhci_writel(host, val, reg);
> }
>
> +static void spacemit_sdhci_set_rx_delay(struct sdhci_host *host, u8 delay)
> +{
> + spacemit_sdhci_clrsetbits(host, SDHC_RX_DLINE_CODE_MASK,
> + FIELD_PREP(SDHC_RX_DLINE_CODE_MASK, delay),
> + SPACEMIT_SDHC_DLINE_CTRL_REG);
> +}
> +
> +static void spacemit_sdhci_set_tx_delay(struct sdhci_host *host, u8 delay)
> +{
> + spacemit_sdhci_clrsetbits(host, SDHC_TX_DLINE_CODE_MASK,
> + FIELD_PREP(SDHC_TX_DLINE_CODE_MASK, delay),
> + SPACEMIT_SDHC_DLINE_CTRL_REG);
> +}
> +
> +static void spacemit_sdhci_set_tx_dline_reg(struct sdhci_host *host, u8 dline_reg)
> +{
> + spacemit_sdhci_clrsetbits(host, SDHC_TX_DLINE_REG_MASK,
> + FIELD_PREP(SDHC_TX_DLINE_REG_MASK, dline_reg),
> + SPACEMIT_SDHC_DLINE_CFG_REG);
> +}
> +
> +static void spacemit_sdhci_tx_tuning_prepare(struct sdhci_host *host)
> +{
> + spacemit_sdhci_setbits(host, SDHC_TX_MUX_SEL, SPACEMIT_SDHC_TX_CFG_REG);
> + spacemit_sdhci_setbits(host, SDHC_DLINE_PU, SPACEMIT_SDHC_DLINE_CTRL_REG);
> + udelay(5);
> +}
> +
> +static void spacemit_sdhci_prepare_tuning(struct sdhci_host *host)
> +{
> + spacemit_sdhci_clrsetbits(host, SDHC_RX_DLINE_REG_MASK,
> + FIELD_PREP(SDHC_RX_DLINE_REG_MASK, SPACEMIT_RX_DLINE_REG),
> + SPACEMIT_SDHC_DLINE_CFG_REG);
> +
> + spacemit_sdhci_setbits(host, SDHC_DLINE_PU, SPACEMIT_SDHC_DLINE_CTRL_REG);
> + udelay(5);
> +
> + spacemit_sdhci_clrsetbits(host, SDHC_RX_SDCLK_SEL1_MASK, SDHC_RX_SDCLK_SEL1,
> + SPACEMIT_SDHC_RX_CFG_REG);
> +
> + if (host->mmc->ios.timing == MMC_TIMING_MMC_HS200)
> + spacemit_sdhci_setbits(host, SDHC_HS200_USE_RFIFO, SPACEMIT_SDHC_PHY_FUNC_REG);
> +}
> +
> static void spacemit_sdhci_reset(struct sdhci_host *host, u8 mask)
> {
> sdhci_reset(host, mask);
> @@ -191,6 +257,111 @@ static unsigned int spacemit_sdhci_clk_get_max_clock(struct sdhci_host *host)
> return clk_get_rate(pltfm_host->clk);
> }
>
> +static int spacemit_sdhci_execute_tuning(struct sdhci_host *host, u32 opcode)
> +{
> + int current_len = 0, current_start = 0;
> + int max_pass_len = 0, max_pass_start = 0;
> + struct mmc_host *mmc = host->mmc;
> + struct mmc_ios ios = mmc->ios;
> + u8 final_delay;
> + int ret = 0;
> + int i;
> +
> + /*
> + * Tuning is required for SDR50/SDR104, HS200/HS400 cards and
> + * if clock frequency is greater than 100MHz in these modes.
> + */
> + if (host->clock < 100 * 1000 * 1000 ||
> + !(ios.timing == MMC_TIMING_MMC_HS200 ||
> + ios.timing == MMC_TIMING_UHS_SDR50 ||
> + ios.timing == MMC_TIMING_UHS_SDR104))
> + return 0;
> +
> + if (mmc->caps2 & MMC_CAP2_NO_MMC) {
> + spacemit_sdhci_set_tx_dline_reg(host, SPACEMIT_TX_TUNING_DLINE_REG);
> + spacemit_sdhci_set_tx_delay(host, SPACEMIT_TX_TUNING_DELAYCODE);
> + spacemit_sdhci_tx_tuning_prepare(host);
> +
> + dev_dbg(mmc_dev(host->mmc), "TX tuning: dline_reg=%d, delaycode=%d\n",
> + SPACEMIT_TX_TUNING_DLINE_REG, SPACEMIT_TX_TUNING_DELAYCODE);
> + }
> +
> + spacemit_sdhci_prepare_tuning(host);
> +
> + for (i = SPACEMIT_RX_TUNE_DELAY_MIN; i <= SPACEMIT_RX_TUNE_DELAY_MAX; i++) {
> + spacemit_sdhci_set_rx_delay(host, i);
> + ret = mmc_send_tuning(host->mmc, opcode, NULL);
> +
> + dev_dbg(mmc_dev(host->mmc), "RX delay %d: %s\n",
> + i, ret == 0 ? "pass" : "fail");
> +
> + if (ret == 0) {
> + /* Test passed - extend current window */
> + if (current_len == 0)
> + current_start = i;
> + current_len++;
> + } else {
> + /* Test failed - check if current window is best so far */
> + if (current_len > max_pass_len) {
> + max_pass_len = current_len;
> + max_pass_start = current_start;
> + }
> + current_len = 0;
> + }
> + }
> +
> + if (current_len > max_pass_len) {
> + max_pass_len = current_len;
> + max_pass_start = current_start;
> + }
> +
> + if (max_pass_len < 3) {
> + dev_err(mmc_dev(host->mmc), "Tuning failed: no stable window found\n");
> + return -EIO;
> + }
> +
> + final_delay = max_pass_start + max_pass_len / 2;
> + spacemit_sdhci_set_rx_delay(host, final_delay);
> + ret = mmc_send_tuning(host->mmc, opcode, NULL);
> + if (ret) {
> + u8 retry_delays[] = {
> + max_pass_start + max_pass_len / 4,
> + max_pass_start + (3 * max_pass_len) / 4,
> + max_pass_start,
> + max_pass_start + max_pass_len - 1
> + };
> + int retry_count = ARRAY_SIZE(retry_delays);
> +
> + dev_warn(mmc_dev(mmc), "Primary delay %d failed, trying alternatives\n",
> + final_delay);
> +
> + for (i = 0; i < retry_count; i++) {
> + if (retry_delays[i] >= SPACEMIT_RX_TUNE_DELAY_MIN &&
> + retry_delays[i] <= SPACEMIT_RX_TUNE_DELAY_MAX) {
> + spacemit_sdhci_set_rx_delay(host, retry_delays[i]);
> + ret = mmc_send_tuning(host->mmc, opcode, NULL);
> + if (!ret) {
> + final_delay = retry_delays[i];
> + dev_info(mmc_dev(mmc), "Retry successful with delay %d\n",
> + final_delay);
> + break;
> + }
> + }
> + }
> +
> + if (ret) {
> + dev_err(mmc_dev(mmc), "All retry attempts failed\n");
> + return -EIO;
> + }
> + }
> +
> + dev_dbg(mmc_dev(host->mmc),
> + "Tuning successful: window %d-%d, using delay %d\n",
> + max_pass_start, max_pass_start + max_pass_len - 1, final_delay);
> +
> + return 0;
> +}
> +
> static int spacemit_sdhci_pre_select_hs400(struct mmc_host *mmc)
> {
> struct sdhci_host *host = mmc_priv(mmc);
> @@ -314,6 +485,7 @@ static const struct sdhci_ops spacemit_sdhci_ops = {
> .set_clock = spacemit_sdhci_set_clock,
> .set_uhs_signaling = spacemit_sdhci_set_uhs_signaling,
> .voltage_switch = spacemit_sdhci_voltage_switch,
> + .platform_execute_tuning = spacemit_sdhci_execute_tuning,
> };
>
> static const struct sdhci_pltfm_data spacemit_sdhci_k1_pdata = {
>
© 2016 - 2026 Red Hat, Inc.