mirror of
https://source.denx.de/u-boot/u-boot.git
synced 2026-06-02 09:46:37 +03:00
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;
};
Signed-off-by: Tze Yee Ng <tze.yee.ng@altera.com>
422 lines
11 KiB
C
422 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Copyright (C) 2016 Socionext Inc.
|
|
* Author: Masahiro Yamada <yamada.masahiro@socionext.com>
|
|
* Copyright (C) 2025 Altera Corporation <www.altera.com>
|
|
*/
|
|
|
|
#include <dm.h>
|
|
#include <asm/global_data.h>
|
|
#include <dm/device_compat.h>
|
|
#include <linux/bitfield.h>
|
|
#include <linux/bitops.h>
|
|
#include <linux/bug.h>
|
|
#include <linux/io.h>
|
|
#include <linux/iopoll.h>
|
|
#include <linux/sizes.h>
|
|
#include <linux/libfdt.h>
|
|
#include <mmc.h>
|
|
#include <reset.h>
|
|
#include <sdhci.h>
|
|
#include "sdhci-cadence.h"
|
|
|
|
struct sdhci_cdns_phy_cfg {
|
|
const char *property;
|
|
u8 addr;
|
|
};
|
|
|
|
static const struct sdhci_cdns_phy_cfg sdhci_cdns_phy_cfgs[] = {
|
|
{ "cdns,phy-input-delay-sd-highspeed", SDHCI_CDNS_PHY_DLY_SD_HS, },
|
|
{ "cdns,phy-input-delay-legacy", SDHCI_CDNS_PHY_DLY_SD_DEFAULT, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr12", SDHCI_CDNS_PHY_DLY_UHS_SDR12, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr25", SDHCI_CDNS_PHY_DLY_UHS_SDR25, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr50", SDHCI_CDNS_PHY_DLY_UHS_SDR50, },
|
|
{ "cdns,phy-input-delay-sd-uhs-ddr50", SDHCI_CDNS_PHY_DLY_UHS_DDR50, },
|
|
{ "cdns,phy-input-delay-mmc-highspeed", SDHCI_CDNS_PHY_DLY_EMMC_SDR, },
|
|
{ "cdns,phy-input-delay-mmc-ddr", SDHCI_CDNS_PHY_DLY_EMMC_DDR, },
|
|
{ "cdns,phy-dll-delay-sdclk", SDHCI_CDNS_PHY_DLY_SDCLK, },
|
|
{ "cdns,phy-dll-delay-sdclk-hsmmc", SDHCI_CDNS_PHY_DLY_HSMMC, },
|
|
{ "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)
|
|
{
|
|
void __iomem *reg = plat->hrs_addr + SDHCI_CDNS_HRS04;
|
|
u32 tmp;
|
|
int ret;
|
|
|
|
tmp = FIELD_PREP(SDHCI_CDNS_HRS04_WDATA, data) |
|
|
FIELD_PREP(SDHCI_CDNS_HRS04_ADDR, addr);
|
|
writel(tmp, reg);
|
|
|
|
tmp |= SDHCI_CDNS_HRS04_WR;
|
|
writel(tmp, reg);
|
|
|
|
ret = readl_poll_timeout(reg, tmp, tmp & SDHCI_CDNS_HRS04_ACK, 10);
|
|
if (ret)
|
|
return ret;
|
|
|
|
tmp &= ~SDHCI_CDNS_HRS04_WR;
|
|
writel(tmp, reg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sdhci_cdns_phy_init(struct sdhci_cdns_plat *plat,
|
|
const void *fdt, int nodeoffset)
|
|
{
|
|
const fdt32_t *prop;
|
|
int ret, i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(sdhci_cdns_phy_cfgs); i++) {
|
|
prop = fdt_getprop(fdt, nodeoffset,
|
|
sdhci_cdns_phy_cfgs[i].property, NULL);
|
|
if (!prop)
|
|
continue;
|
|
|
|
ret = sdhci_cdns_write_phy_reg(plat,
|
|
sdhci_cdns_phy_cfgs[i].addr,
|
|
fdt32_to_cpu(*prop));
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int sdhci_cdns_get_hrs06_mode(struct mmc *mmc)
|
|
{
|
|
unsigned int mode;
|
|
|
|
if (IS_SD(mmc)) {
|
|
mode = SDHCI_CDNS_HRS06_MODE_SD;
|
|
} else {
|
|
switch (mmc->selected_mode) {
|
|
case MMC_LEGACY:
|
|
mode = SDHCI_CDNS_HRS06_MODE_SD; /* use this for Legacy */
|
|
break;
|
|
|
|
case MMC_HS:
|
|
case MMC_HS_52:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_SDR;
|
|
break;
|
|
|
|
case UHS_DDR50:
|
|
case MMC_DDR_52:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_DDR;
|
|
break;
|
|
|
|
case UHS_SDR104:
|
|
case MMC_HS_200:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_HS200;
|
|
break;
|
|
|
|
case MMC_HS_400:
|
|
case MMC_HS_400_ES:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_HS400;
|
|
break;
|
|
|
|
default:
|
|
mode = SDHCI_CDNS_HRS06_MODE_SD;
|
|
break;
|
|
}
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
static void sdhci_cdns_set_control_reg(struct sdhci_host *host)
|
|
{
|
|
struct mmc *mmc = host->mmc;
|
|
struct sdhci_cdns_plat *plat = dev_get_plat(mmc->dev);
|
|
u32 mode, tmp;
|
|
|
|
/*
|
|
* Select HRS06 mode based on card type and selected timing mode.
|
|
* For SD cards, always use SD mode (000b) as per Cadence user guide,
|
|
* section 12.7 (HRS06), Part Number: IP6061.
|
|
* For eMMC, use selected_mode to pick the appropriate mode.
|
|
*/
|
|
mode = sdhci_cdns_get_hrs06_mode(mmc);
|
|
|
|
tmp = readl(plat->hrs_addr + SDHCI_CDNS_HRS06);
|
|
tmp &= ~SDHCI_CDNS_HRS06_MODE;
|
|
tmp |= FIELD_PREP(SDHCI_CDNS_HRS06_MODE, mode);
|
|
writel(tmp, plat->hrs_addr + SDHCI_CDNS_HRS06);
|
|
|
|
/*
|
|
* For SD cards, program standard SDHCI Host Control2 UHS/voltage
|
|
* registers for UHS-I support.
|
|
*/
|
|
if (IS_SD(mmc))
|
|
sdhci_set_control_reg(host);
|
|
|
|
if (SDHCI_GET_VERSION(host) >= SDHCI_SPEC_420)
|
|
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,
|
|
unsigned int val)
|
|
{
|
|
struct mmc *mmc = &plat->mmc;
|
|
struct sdhci_host *host = dev_get_priv(mmc->dev);
|
|
void __iomem *reg = plat->hrs_addr + SDHCI_CDNS_HRS06;
|
|
u32 tmp;
|
|
int i, ret;
|
|
|
|
if (SDHCI_GET_VERSION(host) >= SDHCI_SPEC_420)
|
|
return sdhci_cdns6_set_tune_val(plat, val);
|
|
|
|
if (WARN_ON(!FIELD_FIT(SDHCI_CDNS_HRS06_TUNE, val)))
|
|
return -EINVAL;
|
|
|
|
tmp = readl(reg);
|
|
tmp &= ~SDHCI_CDNS_HRS06_TUNE;
|
|
tmp |= FIELD_PREP(SDHCI_CDNS_HRS06_TUNE, val);
|
|
|
|
/*
|
|
* Workaround for IP errata:
|
|
* The IP6116 SD/eMMC PHY design has a timing issue on receive data
|
|
* path. Send tune request twice.
|
|
*/
|
|
for (i = 0; i < 2; i++) {
|
|
tmp |= SDHCI_CDNS_HRS06_TUNE_UP;
|
|
writel(tmp, reg);
|
|
|
|
ret = readl_poll_timeout(reg, tmp,
|
|
!(tmp & SDHCI_CDNS_HRS06_TUNE_UP), 1);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __maybe_unused sdhci_cdns_execute_tuning(struct udevice *dev,
|
|
unsigned int opcode)
|
|
{
|
|
struct sdhci_cdns_plat *plat = dev_get_plat(dev);
|
|
struct mmc *mmc = &plat->mmc;
|
|
int cur_streak = 0;
|
|
int max_streak = 0;
|
|
int end_of_streak = 0;
|
|
int ret;
|
|
int i;
|
|
|
|
/*
|
|
* This function performs the tuning process for both SD and eMMC
|
|
* interfaces. It sweeps through all available tuning points,
|
|
* sending tuning commands at each step.
|
|
*/
|
|
for (i = 0; i < SDHCI_CDNS_MAX_TUNING_LOOP; i++) {
|
|
if (sdhci_cdns_set_tune_val(plat, i) ||
|
|
mmc_send_tuning(mmc, opcode)) { /* bad */
|
|
cur_streak = 0;
|
|
} else { /* good */
|
|
cur_streak++;
|
|
if (cur_streak > max_streak) {
|
|
max_streak = cur_streak;
|
|
end_of_streak = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!max_streak) {
|
|
dev_err(dev, "no tuning point found\n");
|
|
return -EIO;
|
|
}
|
|
|
|
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;
|
|
|
|
static int sdhci_cdns_bind(struct udevice *dev)
|
|
{
|
|
struct sdhci_cdns_plat *plat = dev_get_plat(dev);
|
|
|
|
return sdhci_bind(dev, &plat->mmc, &plat->cfg);
|
|
}
|
|
|
|
static int sdhci_cdns_probe(struct udevice *dev)
|
|
{
|
|
DECLARE_GLOBAL_DATA_PTR;
|
|
struct mmc_uclass_priv *upriv = dev_get_uclass_priv(dev);
|
|
struct sdhci_cdns_plat *plat = dev_get_plat(dev);
|
|
struct sdhci_host *host = dev_get_priv(dev);
|
|
struct reset_ctl_bulk reset_bulk;
|
|
fdt_addr_t base;
|
|
int ret;
|
|
|
|
base = dev_read_addr(dev);
|
|
if (base == FDT_ADDR_T_NONE)
|
|
return -EINVAL;
|
|
|
|
plat->hrs_addr = devm_ioremap(dev, base, SZ_1K);
|
|
if (!plat->hrs_addr)
|
|
return -ENOMEM;
|
|
|
|
ret = reset_get_bulk(dev, &reset_bulk);
|
|
if (!ret)
|
|
reset_deassert_bulk(&reset_bulk);
|
|
|
|
host->name = dev->name;
|
|
host->ioaddr = plat->hrs_addr + SDHCI_CDNS_SRS_BASE;
|
|
host->ops = &sdhci_cdns_ops;
|
|
host->quirks |= SDHCI_QUIRK_WAIT_SEND_CMD;
|
|
sdhci_cdns_mmc_ops = sdhci_ops;
|
|
#if CONFIG_IS_ENABLED(MMC_SUPPORTS_TUNING)
|
|
sdhci_cdns_mmc_ops.execute_tuning = sdhci_cdns_execute_tuning;
|
|
#endif
|
|
|
|
ret = mmc_of_parse(dev, &plat->cfg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (device_is_compatible(dev, "cdns,sd6hc"))
|
|
ret = sdhci_cdns6_phy_init(dev, plat);
|
|
else
|
|
ret = sdhci_cdns_phy_init(plat, gd->fdt_blob, dev_of_offset(dev));
|
|
if (ret)
|
|
return ret;
|
|
|
|
host->mmc = &plat->mmc;
|
|
host->mmc->dev = dev;
|
|
ret = sdhci_setup_cfg(&plat->cfg, host, plat->cfg.f_max, 0);
|
|
if (ret)
|
|
return ret;
|
|
|
|
upriv->mmc = &plat->mmc;
|
|
host->mmc->priv = host;
|
|
|
|
return sdhci_probe(dev);
|
|
}
|
|
|
|
static const struct udevice_id sdhci_cdns_match[] = {
|
|
{ .compatible = "socionext,uniphier-sd4hc" },
|
|
{ .compatible = "cdns,sd4hc" },
|
|
{ .compatible = "altr,agilex5-sd6hc" },
|
|
{ .compatible = "cdns,sd6hc" },
|
|
{ /* sentinel */ }
|
|
};
|
|
|
|
U_BOOT_DRIVER(sdhci_cdns) = {
|
|
.name = "sdhci-cdns",
|
|
.id = UCLASS_MMC,
|
|
.of_match = sdhci_cdns_match,
|
|
.bind = sdhci_cdns_bind,
|
|
.probe = sdhci_cdns_probe,
|
|
.priv_auto = sizeof(struct sdhci_host),
|
|
.plat_auto = sizeof(struct sdhci_cdns_plat),
|
|
.ops = &sdhci_cdns_mmc_ops,
|
|
};
|