Add an SDHCI driver for the DesignWare Mobile Storage Host Controller (DWC MSHC) integrated in the Metanoia MT2824 SoC. It supports eMMC up to HS400/HS400ES and SD cards, and implements the MMC driver model.
The controller is a DWC MSHC instance, but its PHY programming, voltage/speed-mode handling and tuning model are silicon-specific and differ from the existing DWC MSHC drivers. The PHY pad drive strength, receiver select and weak-pull settings depend on the card type (eMMC vs SD) and are re-applied on every ios, whereas snps_sdhci.c uses fixed pad values. High-speed modes force 1.8V signalling and HS400 uses an explicit PHY DLL lock sequence (set_control_reg()/set_ios_post()), which snps_sdhci.c does not implement. Tuning relies on the generic SDHCI core tuner, whereas snps_sdhci.c uses the hardware auto-tuning block. Keeping a dedicated driver avoids per-variant branches in the shared DWC MSHC drivers, matching how snps_sdhci.c, adi_sdhci.c and cv1800b_sdhci.c are already split in U-Boot. Signed-off-by: Benoît Monin <[email protected]> --- drivers/mmc/Kconfig | 11 + drivers/mmc/Makefile | 1 + drivers/mmc/metanoia_sdhci.c | 494 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 506 insertions(+) diff --git a/drivers/mmc/Kconfig b/drivers/mmc/Kconfig index f9f7aa5cf97..b4ac68c04c3 100644 --- a/drivers/mmc/Kconfig +++ b/drivers/mmc/Kconfig @@ -654,6 +654,17 @@ config MMC_SDHCI_F_SDH30 If you have a controller with this interface, say Y here. If unsure, say N. +config MMC_SDHCI_METANOIA + bool "SDHCI support for Metanoia MT2824 SoC" + depends on MMC_SDHCI + depends on OF_CONTROL + help + Support for SDHCI host controller on Metanoia MT2824 SoC platform. + + If you have a controller with this interface, say Y here. + + If unsure, say N. + config MMC_SDHCI_MSM bool "Qualcomm SDHCI controller" depends on MMC_SDHCI && ARCH_SNAPDRAGON diff --git a/drivers/mmc/Makefile b/drivers/mmc/Makefile index a23336d7d8d..f9e60370dd2 100644 --- a/drivers/mmc/Makefile +++ b/drivers/mmc/Makefile @@ -63,6 +63,7 @@ obj-$(CONFIG_MMC_SDHCI_CADENCE) += sdhci-cadence6.o obj-$(CONFIG_MMC_SDHCI_CV1800B) += cv1800b_sdhci.o obj-$(CONFIG_MMC_SDHCI_AM654) += am654_sdhci.o obj-$(CONFIG_MMC_SDHCI_IPROC) += iproc_sdhci.o +obj-$(CONFIG_MMC_SDHCI_METANOIA) += metanoia_sdhci.o obj-$(CONFIG_MMC_SDHCI_MSM) += msm_sdhci.o obj-$(CONFIG_MMC_SDHCI_MV) += mv_sdhci.o obj-$(CONFIG_MMC_SDHCI_NPCM) += npcm_sdhci.o diff --git a/drivers/mmc/metanoia_sdhci.c b/drivers/mmc/metanoia_sdhci.c new file mode 100644 index 00000000000..e109eebd0db --- /dev/null +++ b/drivers/mmc/metanoia_sdhci.c @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2026 Metanoia Communications Inc., + * Jun Chang, Metanoia Communications Inc. <[email protected]> + * Wilson Kang, Metanoia Communications Inc. <[email protected]> + */ + +#include <config.h> +#include <clk.h> +#include <dm.h> +#include <linux/delay.h> +#include <linux/bitfield.h> +#include <linux/iopoll.h> +#include <linux/sizes.h> +#include <sdhci.h> + +#define DWCMSHC_CTRL_HS400 0x7 +#define EMMC_MIN_FREQ 400000 + +/* PHY register area pointer */ +#define DWC_MSHC_PHY_REGS 0x300 + +#define DWC_MSHC_PHY_CNFG_REG (DWC_MSHC_PHY_REGS + 0x00) +#define PHY_CNFG_RSTN_DEASSERT 0x1 /* Deassert PHY reset */ +#define PHY_PAD_SN_MASK GENMASK(23, 20) +#define PHY_PAD_SN_SD 0x8 +#define PHY_PAD_SN_EMMC 0x8 +#define PHY_PAD_SP_MASK GENMASK(19, 16) +#define PHY_PAD_SP_SD 0x9 +#define PHY_PAD_SP_EMMC 0x8 +#define PHY_PWRGOOD BIT(1) +#define PHY_RSTN BIT(0) + +#define PHY_CMDPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x04) +#define PHY_DATPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x06) +#define PHY_CLKPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x08) +#define PHY_STBPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x0a) +#define PHY_RSTNPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x0c) +#define PHY_SMPLDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x20) +#define PHY_ATDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x21) +#define PHY_DLL_CTRL_REG (DWC_MSHC_PHY_REGS + 0x24) +#define PHY_DLL_CNFG1_REG (DWC_MSHC_PHY_REGS + 0x25) +#define PHY_DLLDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x28) +#define PHY_DLL_STATUS_REG (DWC_MSHC_PHY_REGS + 0x2e) + +#define PHY_RXSEL_MASK GENMASK(2, 0) +#define PHY_RXSEL_180V 0x01 +#define PHY_RXSEL_330V 0x02 + +#define PHY_TXSLEW_CTRL_N_MASK GENMASK(12, 9) +#define PHY_TXSLEW_CTRL_N_EMMC 0x2 +#define PHY_TXSLEW_CTRL_N_SD 0x1 +#define PHY_TXSLEW_CTRL_P_MASK GENMASK(8, 5) +#define PHY_TXSLEW_CTRL_P_EMMC 0x2 +#define PHY_TXSLEW_CTRL_P_SD 0x1 +#define PHY_TXSLEW_CTRL_P_DS 0x3 + +#define PHY_WEAKPULL_EN_PULLUP 0x08 +#define PHY_WEAKPULL_EN_PULLDOWN 0x10 + +/* Vendor specific Registers */ +#define VENDOR_SPECIFIC_AREA_BASE 0x500 + +#define DWCMSHC_HOST_CTRL3_REG (VENDOR_SPECIFIC_AREA_BASE + 0x08) +#define DWCMSHC_CMD_CONFLICT_CHECK BIT(0) +#define DWCMSHC_NEGEDGE_DATAOUT_EN BIT(1) +#define NEDGE_SMPL_EN BIT(7) + +#define DWCMSHC_EMMC_CTRL_REG (VENDOR_SPECIFIC_AREA_BASE + 0x2c) +#define ENH_STROBE_ENABLE BIT(8) +#define CARD_IS_EMMC BIT(0) + +#define DWCMSHC_AT_CTRL_REG (VENDOR_SPECIFIC_AREA_BASE + 0x40) +#define TUNE_CLK_STOP_EN BIT(16) + +static inline int metanoia_boundary_ok(dma_addr_t addr, int len) +{ + return ((addr | (SZ_128M - 1)) == ((addr + len - 1) | (SZ_128M - 1))); +} + +struct metanoia_sdhc_plat { + struct mmc_config cfg; + struct mmc mmc; +}; + +struct metanoia_sdhc { + struct sdhci_host host; +}; + +static void metanoia_sdhci_sw_reset(struct sdhci_host *host, u8 mask) +{ + u8 val; + int retries = 100; + + sdhci_writeb(host, mask, SDHCI_SOFTWARE_RESET); + do { + udelay(1000); + val = sdhci_readb(host, SDHCI_SOFTWARE_RESET); + if (!(val & mask)) + break; + } while (--retries > 0); + + if (retries <= 0) + pr_warn("%s: Reset 0x%x never completed.\n", __func__, + (int)mask); +} + +static bool metanoia_sdhci_is_emmc(struct mmc *mmc) +{ + u32 bus_width; + + if (IS_MMC(mmc)) + return true; + if (IS_SD(mmc)) + return false; + + /* + * Card type may be unknown early (before op-cond completes). Fall back + * to board description (e.g. non-removable in DT -> eMMC). + */ + if (mmc->dev) { + bus_width = dev_read_u32_default(mmc->dev, "bus-width", 0); + if (bus_width == 8) + return true; + if (dev_read_bool(mmc->dev, "non-removable")) + return true; + } + + return false; +} + +void metanoia_sdhci_adma_write_desc(struct sdhci_host *host, void **desc, + dma_addr_t addr, int len, bool end) +{ + int tmplen, offset; + + if (likely(!len || metanoia_boundary_ok(addr, len))) { + sdhci_adma_write_desc(host, desc, addr, len, end); + return; + } + + offset = addr & (SZ_128M - 1); + tmplen = SZ_128M - offset; + sdhci_adma_write_desc(host, desc, addr, tmplen, false); + + addr += tmplen; + len -= tmplen; + sdhci_adma_write_desc(host, desc, addr, len, end); +} + +static int metanoia_sdhci_of_to_plat(struct udevice *dev) +{ + struct metanoia_sdhc_plat *plat = dev_get_plat(dev); + struct sdhci_host *host = dev_get_priv(dev); + struct mmc_config *cfg = &plat->cfg; + int ret; + + host->name = dev->name; + host->ioaddr = dev_read_addr_ptr(dev); + ret = mmc_of_parse(dev, cfg); + if (ret) + return ret; + + return 0; +} + +static int metanoia_sdhci_bind(struct udevice *dev) +{ + struct metanoia_sdhc_plat *plat = dev_get_plat(dev); + + return sdhci_bind(dev, &plat->mmc, &plat->cfg); +} + +static void metanoia_emmc_phy_pad_cfg(struct sdhci_host *host, bool is_emmc) +{ + u16 val; + u16 txslew_n = 0; + u16 txslew_p = 0; + u16 rx_sel = PHY_RXSEL_330V; + + if (host->mmc->signal_voltage == MMC_SIGNAL_VOLTAGE_180) + rx_sel = PHY_RXSEL_180V; + + if (is_emmc) { + txslew_n = PHY_TXSLEW_CTRL_N_EMMC; + txslew_p = PHY_TXSLEW_CTRL_P_EMMC; + } else { + txslew_n = PHY_TXSLEW_CTRL_N_SD; + txslew_p = PHY_TXSLEW_CTRL_P_SD; + } + + val = PHY_WEAKPULL_EN_PULLUP; + val |= FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, txslew_n); + val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, txslew_p); + val |= FIELD_PREP(PHY_RXSEL_MASK, rx_sel); + + sdhci_writew(host, val, PHY_CMDPAD_CNFG_REG); + sdhci_writew(host, val, PHY_DATPAD_CNFG_REG); + sdhci_writew(host, val, PHY_RSTNPAD_CNFG_REG); + + val = FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, txslew_n); + val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, txslew_p); + sdhci_writew(host, val, PHY_CLKPAD_CNFG_REG); + + if (CONFIG_IS_ENABLED(MMC_HS400_SUPPORT)) { + val = PHY_WEAKPULL_EN_PULLDOWN; + val |= FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, PHY_TXSLEW_CTRL_N_SD); + val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, PHY_TXSLEW_CTRL_P_DS); + val |= FIELD_PREP(PHY_RXSEL_MASK, PHY_RXSEL_180V); + sdhci_writew(host, val, PHY_STBPAD_CNFG_REG); + } +} + +static void metanoia_emmc_phy_dl_cfg(struct sdhci_host *host) +{ + sdhci_writeb(host, 0x0C, PHY_SMPLDL_CNFG_REG); + sdhci_writeb(host, 0x0C, PHY_ATDL_CNFG_REG); +} + +static void metanoia_sdhci_apply_phy_type(struct sdhci_host *host, bool is_emmc) +{ + u32 val; + + u32 phy_pad_sn = is_emmc ? PHY_PAD_SN_EMMC : PHY_PAD_SN_SD; + u32 phy_pad_sp = is_emmc ? PHY_PAD_SP_EMMC : PHY_PAD_SP_SD; + + val = sdhci_readl(host, DWC_MSHC_PHY_CNFG_REG); + val &= ~(PHY_PAD_SN_MASK | PHY_PAD_SP_MASK); + val |= FIELD_PREP(PHY_PAD_SN_MASK, phy_pad_sn); + val |= FIELD_PREP(PHY_PAD_SP_MASK, phy_pad_sp); + sdhci_writel(host, val, DWC_MSHC_PHY_CNFG_REG); + + metanoia_emmc_phy_pad_cfg(host, is_emmc); +} + +static void metanoia_sdhci_apply_hs400_phy(struct sdhci_host *host) +{ + u16 clk; + u8 val; + int retries = 10; + + /* configure DLL as user guide suggest */ + clk = sdhci_readw(host, SDHCI_CLOCK_CONTROL); + clk &= ~SDHCI_CLOCK_CARD_EN; + sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL); + + sdhci_writeb(host, 0x20, PHY_DLL_CNFG1_REG); + sdhci_writeb(host, 0x60, PHY_DLLDL_CNFG_REG); + + clk = sdhci_readw(host, SDHCI_CLOCK_CONTROL); + clk |= SDHCI_CLOCK_CARD_EN; + sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL); + udelay(10); + + sdhci_writeb(host, 0x1, PHY_DLL_CTRL_REG); + + do { + udelay(1); + val = sdhci_readb(host, PHY_DLL_STATUS_REG); + if ((val & 0x1) == 0x1) + break; + } while (--retries); + + if (!retries) + pr_err("eMMC dll has not locked\n"); + + val = sdhci_readb(host, PHY_DLL_STATUS_REG); + if (val & 0x2) + pr_err("eMMC dll has locked and has errors\n"); + + metanoia_sdhci_sw_reset(host, 0x3 << 1); +} + +static int metanoia_sdhci_phy_init(struct sdhci_host *host) +{ + u32 val; + int ret; + + /* Wait powergood before deassert phy reset */ + ret = readl_poll_timeout(host->ioaddr + DWC_MSHC_PHY_CNFG_REG, val, + val & PHY_PWRGOOD, 20000); + if (ret) { + pr_warn("%s: Internal clock never stabilised.\n", __func__); + return ret; + } + + val = sdhci_readl(host, DWC_MSHC_PHY_CNFG_REG); + if (!(val & PHY_RSTN)) { + val |= PHY_RSTN; + sdhci_writel(host, val, DWC_MSHC_PHY_CNFG_REG); + } + + /* Set TUNE_CLK_STOP_EN to prevent glitching */ + val = sdhci_readl(host, DWCMSHC_AT_CTRL_REG); + val |= TUNE_CLK_STOP_EN; + sdhci_writel(host, val, DWCMSHC_AT_CTRL_REG); + + return 0; +} + +static int metanoia_sdhci_apply_phy(struct sdhci_host *host) +{ + struct mmc *mmc = host->mmc; + bool is_emmc = metanoia_sdhci_is_emmc(mmc); + bool is_hs400 = is_emmc && (mmc->selected_mode == MMC_HS_400 || + mmc->selected_mode == MMC_HS_400_ES); + u32 emmc_ctrl; + int ret; + + metanoia_emmc_phy_dl_cfg(host); + metanoia_sdhci_apply_phy_type(host, is_emmc); + + emmc_ctrl = sdhci_readl(host, DWCMSHC_EMMC_CTRL_REG); + if (is_emmc) + emmc_ctrl |= CARD_IS_EMMC; + else + emmc_ctrl &= ~CARD_IS_EMMC; + sdhci_writel(host, emmc_ctrl, DWCMSHC_EMMC_CTRL_REG); + + /* + * PHY pad/voltage settings are programmed above based on the current + * ios. Ensure the PHY is out of reset afterwards; otherwise voltage + * switching (e.g. SD 3.3V -> 1.8V) can fail. + */ + ret = metanoia_sdhci_phy_init(host); + if (ret) + return ret; + + if (is_hs400) + metanoia_sdhci_apply_hs400_phy(host); + + return 0; +} + +static int metanoia_sdhci_set_ios_post(struct sdhci_host *host) +{ + struct mmc *mmc = host->mmc; + u32 reg; + int ret; + + /* Disable cmd conflict check, especially for clk 200MHz */ + reg = sdhci_readb(host, DWCMSHC_HOST_CTRL3_REG); + reg &= ~DWCMSHC_CMD_CONFLICT_CHECK; + if (host->mmc->ddr_mode && (mmc->selected_mode == MMC_DDR_52 || + mmc->selected_mode == UHS_DDR50)) { + /* + * Databook claims this bit only for HS400, however, it does + * not work on real silicon. + */ + reg |= DWCMSHC_NEGEDGE_DATAOUT_EN; + } else { + reg &= ~DWCMSHC_NEGEDGE_DATAOUT_EN; + } + + if (mmc->selected_mode == MMC_HS_400 || + mmc->selected_mode == MMC_HS_400_ES) { + /* Samples CMD/DATA with respect to negative edge of cclk_rx */ + reg |= NEDGE_SMPL_EN; + } else { + reg &= ~NEDGE_SMPL_EN; + } + + sdhci_writeb(host, reg, DWCMSHC_HOST_CTRL3_REG); + + ret = metanoia_sdhci_apply_phy(host); + if (ret) + return ret; + + return 0; +} + +static int metanoia_sdhci_set_enhanced_strobe(struct sdhci_host *host) +{ + struct mmc *mmc = host->mmc; + u32 val; + + val = sdhci_readl(host, DWCMSHC_EMMC_CTRL_REG); + if (mmc->selected_mode == MMC_HS_400_ES) + val |= ENH_STROBE_ENABLE; + else + val &= ~ENH_STROBE_ENABLE; + sdhci_writel(host, val, DWCMSHC_EMMC_CTRL_REG); + + return 0; +} + +static void metanoia_sdhci_set_control_reg(struct sdhci_host *host) +{ + struct mmc *mmc = host->mmc; + u32 val; + + if (mmc->selected_mode == MMC_HS || mmc->selected_mode == MMC_HS_52) { + pr_debug("force 1.8v for MMC\n"); + mmc->signal_voltage = MMC_SIGNAL_VOLTAGE_180; + } + + sdhci_set_control_reg(host); + + if (mmc->selected_mode == MMC_HS_400 || + mmc->selected_mode == MMC_HS_400_ES) { + val = sdhci_readw(host, SDHCI_HOST_CONTROL2); + val &= ~SDHCI_CTRL_UHS_MASK; + val |= DWCMSHC_CTRL_HS400; + sdhci_writew(host, val, SDHCI_HOST_CONTROL2); + } + + /* + * Reinitializing the device tries to set it to lower-speed modes + * first, which fails if the Enhanced Strobe bit is set, making + * the device impossible to use. Set the correct value here to + * let reinitialization attempts succeed. + */ + if (CONFIG_IS_ENABLED(MMC_HS400_ES_SUPPORT)) + metanoia_sdhci_set_enhanced_strobe(host); +} + +static const struct sdhci_ops metanoia_sdhci_ops = { + .set_ios_post = metanoia_sdhci_set_ios_post, + .set_control_reg = metanoia_sdhci_set_control_reg, + .set_enhanced_strobe = metanoia_sdhci_set_enhanced_strobe, +#if CONFIG_IS_ENABLED(MMC_SDHCI_ADMA_HELPERS) + .adma_write_desc = metanoia_sdhci_adma_write_desc, +#endif +}; + +static int metanoia_sdhci_probe(struct udevice *dev) +{ + struct mmc_uclass_priv *upriv = dev_get_uclass_priv(dev); + struct metanoia_sdhc_plat *plat = dev_get_plat(dev); + struct metanoia_sdhc *priv = dev_get_priv(dev); + struct mmc_config *cfg = &plat->cfg; + struct sdhci_host *host = &priv->host; + struct clk clk = {}; + ulong clk_rate = 0; + int ret; + + ret = clk_get_by_index(dev, 0, &clk); + if (!ret) { + clk_rate = clk_get_rate(&clk); + if (IS_ERR_VALUE(clk_rate)) + clk_rate = 0; + } + + if (!clk_rate) { + u32 max_freq; + + dev_read_u32(dev, "max-frequency", &max_freq); + clk_rate = max_freq; + } + + if (clk_rate) { + host->max_clk = clk_rate; + if (!cfg->f_max || cfg->f_max > clk_rate) + cfg->f_max = clk_rate; + } else { + host->max_clk = cfg->f_max; + } + + host->ops = &metanoia_sdhci_ops; + host->quirks = SDHCI_QUIRK_WAIT_SEND_CMD | SDHCI_QUIRK_USE_WIDE8; + host->mmc = &plat->mmc; + host->mmc->priv = &priv->host; + host->mmc->dev = dev; + upriv->mmc = host->mmc; + + ret = metanoia_sdhci_apply_phy(host); + if (ret) + return ret; + + ret = sdhci_setup_cfg(cfg, host, cfg->f_max, EMMC_MIN_FREQ); + if (ret) + return ret; + + cfg->host_caps |= MMC_MODE_8BIT | MMC_CAP(MMC_DDR_52); + + return sdhci_probe(dev); +} + +static const struct udevice_id metanoia_sdhci_ids[] = { + { .compatible = "metanoia,mt2824-dwcmshc" }, + { /* sentinel */ } +}; + +U_BOOT_DRIVER(metanoia_sdhci_drv) = { + .name = "metanoia_sdhci", + .id = UCLASS_MMC, + .of_match = metanoia_sdhci_ids, + .of_to_plat = metanoia_sdhci_of_to_plat, + .ops = &sdhci_ops, + .bind = metanoia_sdhci_bind, + .probe = metanoia_sdhci_probe, + .priv_auto = sizeof(struct metanoia_sdhc), + .plat_auto = sizeof(struct metanoia_sdhc_plat), +}; -- 2.55.0
