Re: [PATCH v2 2/2] mmc: sdhci-of-dwcmshc: Add HPE GSC eMMC support
From: Shawn Lin
Date: Mon Mar 09 2026 - 21:14:35 EST
在 2026/03/10 星期二 5:13, nick.hawkins@xxxxxxx 写道:
From: Nick Hawkins <nick.hawkins@xxxxxxx>
Add support for the eMMC controller integrated in the HPE GSC (ARM64
Cortex-A53) BMC SoC under the new 'hpe,gsc-dwcmshc' compatible
string.
The HPE GSC eMMC controller is based on the DesignWare Cores MSHC IP
but requires several platform-specific adjustments:
Clock mux (dwcmshc_hpe_set_clock):
The GSC SoC wires SDHCI_CLOCK_CONTROL.freq_sel directly to a clock
mux rather than a divider. Forcing freq_sel = 1 when the requested
clock is 200 MHz (HS200) selects the correct high-speed clock source.
Using the generic sdhci_set_clock() would otherwise leave the mux on
the wrong source after tuning.
Auto-tuning / vendor config (dwcmshc_hpe_vendor_specific):
Disables the command-conflict check (DWCMSHC_HOST_CTRL3 BIT(0)) and
programs ATCTRL = 0x021f0005:
BIT(0) auto-tuning circuit enable
BIT(2) centre-phase auto-tuning
BIT(16) tune-clock-stop enable
BITS[18:17] pre-change delay = 3
BITS[20:19] post-change delay = 3
BIT(25) sample-window threshold enable
This combination is required for reliable HS200 signal integrity on
the GSC PCB trace topology.
Reset (dwcmshc_hpe_reset):
Calls sdhci_reset(), re-applies the vendor config above, and then
sets DWCMSHC_CARD_IS_EMMC unconditionally. The GSC controller
clears this bit on every reset; leaving it clear causes card-detect
mis-identification on an eMMC-only slot.
UHS signaling (dwcmshc_hpe_set_uhs_signaling):
Mirrors upstream dwcmshc_set_uhs_signaling() but always sets
CARD_IS_EMMC regardless of timing mode, for the same reason.
Init (dwcmshc_hpe_gsc_init):
Obtains the SoC register block via the 'hpe,gxp-sysreg' syscon
phandle and sets SCGSyncDis (BIT(18)) in MSHCCS (offset 0x110)
to allow the HS200 RX delay lines to settle while the card clock
is stopped during auto-tuning. Enables SDHCI v4 mode.
Quirks:
SDHCI_QUIRK_CAP_CLOCK_BASE_BROKEN: base clock not advertised in
capabilities; must be obtained from the DTS 'clocks' property.
SDHCI_QUIRK2_PRESET_VALUE_BROKEN: preset-value registers are not
populated in the GSC ROM.
All HPE-specific code is isolated to the new hpe_gsc_init / hpe_ops /
hpe_gsc_pdata symbols. No existing platform (Rockchip, T-Head, sg2042,
etc.) is affected.
Signed-off-by: Nick Hawkins <nick.hawkins@xxxxxxx>
---
drivers/mmc/host/sdhci-of-dwcmshc.c | 173 ++++++++++++++++++++++++++++
1 file changed, 173 insertions(+)
diff --git a/drivers/mmc/host/sdhci-of-dwcmshc.c b/drivers/mmc/host/sdhci-of-dwcmshc.c
index 2b75a36c096b..78f5480f4662 100644
--- a/drivers/mmc/host/sdhci-of-dwcmshc.c
+++ b/drivers/mmc/host/sdhci-of-dwcmshc.c
@@ -1245,6 +1245,156 @@ static int sg2042_init(struct device *dev, struct sdhci_host *host,
ARRAY_SIZE(clk_ids), clk_ids);
}
+/*
+ * HPE GSC-specific vendor configuration: disable command conflict check
+ * and program Auto-Tuning Control register.
+ *
+ * ATCTRL value 0x021f0005 field breakdown:
+ * BIT(0) - Auto-tuning circuit enabled
+ * BIT(2) - Center-phase auto-tuning
+ * BIT(16) - Tune clock stop enable
+ * BITS[18:17] - Pre-change delay = 3
+ * BITS[20:19] - Post-change delay = 3
+ * BIT(25) - Sample window threshold enable
+ */
+static void dwcmshc_hpe_vendor_specific(struct sdhci_host *host)
+{
+ struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
+ struct dwcmshc_priv *dwc_priv = sdhci_pltfm_priv(pltfm_host);
+ u8 extra;
+
+ extra = sdhci_readb(host, dwc_priv->vendor_specific_area1 + DWCMSHC_HOST_CTRL3);
+ extra &= ~BIT(0);
+ sdhci_writeb(host, extra, dwc_priv->vendor_specific_area1 + DWCMSHC_HOST_CTRL3);
+ sdhci_writel(host, 0x021f0005, dwc_priv->vendor_specific_area1 + DWCMSHC_EMMC_ATCTRL);
Although you break it down in the comment, but it's still hard to read
and hard to change in the feature if needed. Would you consider defining
some macros and then OR-ing these macros together here?
+}
+
+static void dwcmshc_hpe_reset(struct sdhci_host *host, u8 mask)
+{
+ struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
+ struct dwcmshc_priv *dwc_priv = sdhci_pltfm_priv(pltfm_host);
+ u16 ctrl;
+
+ dwcmshc_reset(host, mask);
+
+ dwcmshc_hpe_vendor_specific(host);
+
+ /* HPE GSC eMMC always needs CARD_IS_EMMC set after reset */
+ ctrl = sdhci_readw(host, dwc_priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
+ ctrl |= DWCMSHC_CARD_IS_EMMC;
+ sdhci_writew(host, ctrl, dwc_priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
+}
+
+static void dwcmshc_hpe_set_uhs_signaling(struct sdhci_host *host,
+ unsigned int timing)
+{
This entire function is 99% copied from dwcmshc_set_uhs_signaling,
please wrap it like:
static void dwcmshc_hpe_set_uhs_signaling()
{
dwcmshc_set_uhs_signaling();
/* HPE GSC: always set CARD_IS_EMMC for all timing modes */
ctrl = sdhci_readw(host, priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
ctrl |= DWCMSHC_CARD_IS_EMMC;
sdhci_writew(host, ctrl, priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
}
+ struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
+ struct dwcmshc_priv *priv = sdhci_pltfm_priv(pltfm_host);
+ u16 ctrl, ctrl_2;
+
+ ctrl_2 = sdhci_readw(host, SDHCI_HOST_CONTROL2);
+ ctrl_2 &= ~SDHCI_CTRL_UHS_MASK;
+
+ /* HPE GSC: always set CARD_IS_EMMC for all timing modes */
+ ctrl = sdhci_readw(host, priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
+ ctrl |= DWCMSHC_CARD_IS_EMMC;
+ sdhci_writew(host, ctrl, priv->vendor_specific_area1 + DWCMSHC_EMMC_CONTROL);
+
+ if ((timing == MMC_TIMING_MMC_HS200) ||
+ (timing == MMC_TIMING_UHS_SDR104))
+ ctrl_2 |= SDHCI_CTRL_UHS_SDR104;
+ else if (timing == MMC_TIMING_UHS_SDR12)
+ ctrl_2 |= SDHCI_CTRL_UHS_SDR12;
+ else if ((timing == MMC_TIMING_UHS_SDR25) ||
+ (timing == MMC_TIMING_MMC_HS))
+ ctrl_2 |= SDHCI_CTRL_UHS_SDR25;
+ else if (timing == MMC_TIMING_UHS_SDR50)
+ ctrl_2 |= SDHCI_CTRL_UHS_SDR50;
+ else if ((timing == MMC_TIMING_UHS_DDR50) ||
+ (timing == MMC_TIMING_MMC_DDR52))
+ ctrl_2 |= SDHCI_CTRL_UHS_DDR50;
+ else if (timing == MMC_TIMING_MMC_HS400)
+ ctrl_2 |= DWCMSHC_CTRL_HS400;
+
+ if (priv->flags & FLAG_IO_FIXED_1V8)
+ ctrl_2 |= SDHCI_CTRL_VDD_180;
+ sdhci_writew(host, ctrl_2, SDHCI_HOST_CONTROL2);
+}
+
+/*
+ * HPE GSC eMMC controller clock setup.
+ *
+ * The GSC SoC wires the freq_sel field of SDHCI_CLOCK_CONTROL directly to a
+ * clock mux rather than a divider. Force freq_sel = 1 when running at
+ * 200 MHz (HS200) so the mux selects the correct clock source.
+ */
+static void dwcmshc_hpe_set_clock(struct sdhci_host *host, unsigned int clock)
+{
+ u16 clk;
+
+ host->mmc->actual_clock = 0;
+
+ sdhci_writew(host, 0, SDHCI_CLOCK_CONTROL);
+
+ if (clock == 0)
+ return;
+
+ clk = sdhci_calc_clk(host, clock, &host->mmc->actual_clock);
+
+ if (host->mmc->actual_clock == 200000000)
+ clk |= (1 << SDHCI_DIVIDER_SHIFT);
+
+ sdhci_enable_clk(host, clk);
+}
+
+/*
+ * HPE GSC eMMC controller init.
+ *
+ * The GSC SoC requires configuring MSHCCS. Bit 18 (SCGSyncDis) disables clock
+ * synchronisation for phase-select values going to the HS200 RX delay lines,
+ * allowing the card clock to be stopped while the delay selection settles and
+ * the phase shift is applied. This must be used together with the ATCTRL
+ * settings programmed in dwcmshc_hpe_vendor_specific():
+ * AT_CTRL_R.TUNE_CLK_STOP_EN = 0x1
+ * AT_CTRL_R.POST_CHANGE_DLY = 0x3
+ * AT_CTRL_R.PRE_CHANGE_DLY = 0x3
+ *
+ * The DTS node provides a syscon phandle ('hpe,gxp-sysreg') to access
+ * this register at offset 0x110 within the SoC control block.
+ */
+#define HPE_GSC_MSHCCS_OFFSET 0x110
+#define HPE_GSC_MSHCCS_SCGSYNCDIS BIT(18)
+
+static int dwcmshc_hpe_gsc_init(struct device *dev, struct sdhci_host *host,
+ struct dwcmshc_priv *dwc_priv)
+{
+ struct regmap *soc_ctrl;
+ int ret;
+
+ /* Disable cmd conflict check and configure auto-tuning */
+ dwcmshc_hpe_vendor_specific(host);
+
+ /* Look up the GXP sysreg syscon for MSHCCS access */
+ soc_ctrl = syscon_regmap_lookup_by_phandle(dev->of_node, "hpe,gxp-sysreg");
+ if (IS_ERR(soc_ctrl)) {
+ dev_err(dev, "failed to get hpe,gxp-sysreg syscon\n");
+ return PTR_ERR(soc_ctrl);
+ }
+
+ /* Set SCGSyncDis (bit 18) to disable sync on HS200 RX delay lines */
+ ret = regmap_update_bits(soc_ctrl, HPE_GSC_MSHCCS_OFFSET,
+ HPE_GSC_MSHCCS_SCGSYNCDIS,
+ HPE_GSC_MSHCCS_SCGSYNCDIS);
+ if (ret) {
+ dev_err(dev, "failed to set SCGSyncDis in MSHCCS\n");
+ return ret;
+ }
+
+ sdhci_enable_v4_mode(host);
+
+ return 0;
+}
+
static void sdhci_eic7700_set_clock(struct sdhci_host *host, unsigned int clock)
{
struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
@@ -1834,6 +1984,25 @@ static const struct dwcmshc_pltfm_data sdhci_dwcmshc_eic7700_pdata = {
.init = eic7700_init,
};
+static const struct sdhci_ops sdhci_dwcmshc_hpe_ops = {
+ .set_clock = dwcmshc_hpe_set_clock,
+ .set_bus_width = sdhci_set_bus_width,
+ .set_uhs_signaling = dwcmshc_hpe_set_uhs_signaling,
+ .get_max_clock = dwcmshc_get_max_clock,
+ .reset = dwcmshc_hpe_reset,
+ .adma_write_desc = dwcmshc_adma_write_desc,
+ .irq = dwcmshc_cqe_irq_handler,
+};
+
+static const struct dwcmshc_pltfm_data sdhci_dwcmshc_hpe_gsc_pdata = {
+ .pdata = {
+ .ops = &sdhci_dwcmshc_hpe_ops,
+ .quirks = SDHCI_QUIRK_CAP_CLOCK_BASE_BROKEN,
+ .quirks2 = SDHCI_QUIRK2_PRESET_VALUE_BROKEN,
+ },
+ .init = dwcmshc_hpe_gsc_init,
+};
+
static const struct cqhci_host_ops dwcmshc_cqhci_ops = {
.enable = dwcmshc_sdhci_cqe_enable,
.disable = sdhci_cqe_disable,
@@ -1942,6 +2111,10 @@ static const struct of_device_id sdhci_dwcmshc_dt_ids[] = {
.compatible = "eswin,eic7700-dwcmshc",
.data = &sdhci_dwcmshc_eic7700_pdata,
},
+ {
+ .compatible = "hpe,gsc-dwcmshc",
+ .data = &sdhci_dwcmshc_hpe_gsc_pdata,
+ },
{},
};
MODULE_DEVICE_TABLE(of, sdhci_dwcmshc_dt_ids);