// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2023 Starfive.
 *   Author: Kuan Lim Lee <kuanlim.lee@starfivetech.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 <sdhci.h>
#include "sdhci-cadence.h"

/* IO Delay Information */
#define SDHCI_CDNS_HRS07		0x1C
#define   SDHCI_CDNS_HRS07_RW_COMPENSATE	GENMASK(20, 16)
#define   SDHCI_CDNS_HRS07_IDELAY_VAL		GENMASK(4, 0)

/* PHY Control and Status */
#define SDHCI_CDNS_HRS09		0x24
#define   SDHCI_CDNS_HRS09_RDDATA_EN		BIT(16)
#define   SDHCI_CDNS_HRS09_RDCMD_EN		BIT(15)
#define   SDHCI_CDNS_HRS09_EXTENDED_WR_MODE	BIT(3)
#define   SDHCI_CDNS_HRS09_EXTENDED_RD_MODE	BIT(2)
#define   SDHCI_CDNS_HRS09_PHY_INIT_COMPLETE	BIT(1)
#define   SDHCI_CDNS_HRS09_PHY_SW_RESET		BIT(0)

/* SDCLK adjustment */
#define SDHCI_CDNS_HRS10		0x28
#define   SDHCI_CDNS_HRS10_HCSDCLKADJ		GENMASK(19, 16)

/* CMD/DAT output delay */
#define SDHCI_CDNS_HRS16		0x40

/* PHY Special Function Registers */
/* register to control the DQ related timing */
#define PHY_DQ_TIMING_REG_ADDR		0x2000

/* register to control the DQS related timing */
#define PHY_DQS_TIMING_REG_ADDR		0x2004

/* register to control the gate and loopback control related timing */
#define PHY_GATE_LPBK_CTRL_REG_ADDR	0x2008

/* register to control the Master DLL logic */
#define PHY_DLL_MASTER_CTRL_REG_ADDR	0x200C

/* register to control the Slave DLL logic */
#define PHY_DLL_SLAVE_CTRL_REG_ADDR	0x2010
#define PHY_DLL_SLAVE_CTRL_REG_READ_DQS_CMD_DELAY	GENMASK(31, 24)
#define PHY_DLL_SLAVE_CTRL_REG_READ_DQS_DELAY		GENMASK(7, 0)

#define SDHCI_CDNS6_PHY_CFG_NUM		5
#define SDHCI_CDNS6_CTRL_CFG_NUM	4

struct sdhci_cdns6_phy_cfg {
	const char *property;
	u32 val;
};

struct sdhci_cdns6_ctrl_cfg {
	const char *property;
	u32 val;
};

static struct sdhci_cdns6_phy_cfg sd_ds_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-sd-ds", 0x00380004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-sd-ds", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-sd-ds", 0x00000000, },
	{ "cdns,phy-dq-timing-delay-sd-ds", 0x00000001, },
	{ "cdns,phy-dll-master-ctrl-sd-ds", 0x00800004, },
};

static struct sdhci_cdns6_phy_cfg sd_hs_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-sd-hs", 0x00380004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-sd-hs", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-sd-hs", 0x00000000, },
	{ "cdns,phy-dq-timing-delay-sd-hs", 0x00000001, },
	{ "cdns,phy-dll-master-ctrl-sd-hs", 0x00800004, },
};

static struct sdhci_cdns6_phy_cfg emmc_sdr_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-emmc-sdr", 0x00380004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-emmc-sdr", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-emmc-sdr", 0x00000000, },
	{ "cdns,phy-dq-timing-delay-emmc-sdr", 0x00000001, },
	{ "cdns,phy-dll-master-ctrl-emmc-sdr", 0x00800004, },
};

static struct sdhci_cdns6_phy_cfg emmc_ddr_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-emmc-ddr", 0x00380004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-emmc-ddr", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-emmc-ddr", 0x00000000, },
	{ "cdns,phy-dq-timing-delay-emmc-ddr", 0x10000001, },
	{ "cdns,phy-dll-master-ctrl-emmc-ddr", 0x00800004, },
};

static struct sdhci_cdns6_phy_cfg emmc_hs200_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-emmc-hs200", 0x00380004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-emmc-hs200", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-emmc-hs200", 0x00DADA00, },
	{ "cdns,phy-dq-timing-delay-emmc-hs200", 0x00000001, },
	{ "cdns,phy-dll-master-ctrl-emmc-hs200", 0x00000004, },
};

static struct sdhci_cdns6_phy_cfg emmc_hs400_phy_cfgs[] = {
	{ "cdns,phy-dqs-timing-delay-emmc-hs400", 0x00280004, },
	{ "cdns,phy-gate-lpbk-ctrl-delay-emmc-hs400", 0x01A00040, },
	{ "cdns,phy-dll-slave-ctrl-emmc-hs400", 0x00DAD800, },
	{ "cdns,phy-dq-timing-delay-emmc-hs400", 0x00000001, },
	{ "cdns,phy-dll-master-ctrl-emmc-hs400", 0x00000004, },
};

static struct sdhci_cdns6_ctrl_cfg sd_ds_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-sd-ds", 0x0001800C, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-sd-ds", 0x00020000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-sd-ds", 0x00000000, },
	{ "cdns,ctrl-hrs07-timing-delay-sd-ds", 0x00080000, },
};

static struct sdhci_cdns6_ctrl_cfg sd_hs_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-sd-hs", 0x0001800C, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-sd-hs", 0x00030000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-sd-hs", 0x00000000, },
	{ "cdns,ctrl-hrs07-timing-delay-sd-hs", 0x00080000, },
};

static struct sdhci_cdns6_ctrl_cfg emmc_sdr_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-emmc-sdr", 0x0001800C, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-emmc-sdr", 0x00030000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-emmc-sdr", 0x00000000, },
	{ "cdns,ctrl-hrs07-timing-delay-emmc-sdr", 0x00080000, },
};

static struct sdhci_cdns6_ctrl_cfg emmc_ddr_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-emmc-ddr", 0x0001800C, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-emmc-ddr", 0x00020000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-emmc-ddr", 0x11000001, },
	{ "cdns,ctrl-hrs07-timing-delay-emmc-ddr", 0x00090001, },
};

static struct sdhci_cdns6_ctrl_cfg emmc_hs200_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-emmc-hs200", 0x00018000, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-emmc-hs200", 0x00080000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-emmc-hs200", 0x00000000, },
	{ "cdns,ctrl-hrs07-timing-delay-emmc-hs200", 0x00090000, },
};

static struct sdhci_cdns6_ctrl_cfg emmc_hs400_ctrl_cfgs[] = {
	{ "cdns,ctrl-hrs09-timing-delay-emmc-hs400", 0x00018000, },
	{ "cdns,ctrl-hrs10-lpbk-ctrl-delay-emmc-hs400", 0x00080000, },
	{ "cdns,ctrl-hrs16-slave-ctrl-emmc-hs400", 0x11000000, },
	{ "cdns,ctrl-hrs07-timing-delay-emmc-hs400", 0x00080000, },
};

static u32 sdhci_cdns6_read_phy_reg(struct sdhci_cdns_plat *plat, u32 addr)
{
	writel(addr, plat->hrs_addr + SDHCI_CDNS_HRS04);
	return readl(plat->hrs_addr + SDHCI_CDNS_HRS05);
}

static void sdhci_cdns6_write_phy_reg(struct sdhci_cdns_plat *plat, u32 addr, u32 val)
{
	writel(addr, plat->hrs_addr + SDHCI_CDNS_HRS04);
	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;
	u32 tmp;
	int ret;

	tmp = readl(reg);
	tmp &= ~SDHCI_CDNS_HRS09_PHY_SW_RESET;

	/* Switch On DLL Reset */
	if (reset)
		tmp |= FIELD_PREP(SDHCI_CDNS_HRS09_PHY_SW_RESET, 0);
	else
		tmp |= FIELD_PREP(SDHCI_CDNS_HRS09_PHY_SW_RESET, 1);

	writel(tmp, reg);

	/* After reset, wait until HRS09.PHY_INIT_COMPLETE is set to 1 within 3000us*/
	if (!reset) {
		ret = readl_poll_timeout(reg, tmp, (tmp & SDHCI_CDNS_HRS09_PHY_INIT_COMPLETE),
					 3000);
	}

	return ret;
}

int sdhci_cdns6_phy_adj(struct udevice *dev, struct sdhci_cdns_plat *plat, u32 mode)
{
	struct sdhci_cdns6_phy_cfg *sdhci_cdns6_phy_cfgs;
	struct sdhci_cdns6_ctrl_cfg *sdhci_cdns6_ctrl_cfgs;
	u32 tmp;
	int i, ret;

	switch (mode) {
	case UHS_SDR12:
	case MMC_LEGACY:
		sdhci_cdns6_phy_cfgs = sd_ds_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = sd_ds_ctrl_cfgs;
		break;

	case SD_HS:
	case UHS_SDR25:
	case MMC_HS:
		sdhci_cdns6_phy_cfgs = sd_hs_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = sd_hs_ctrl_cfgs;
		break;

	case UHS_SDR50:
	case MMC_HS_52:
		sdhci_cdns6_phy_cfgs = emmc_sdr_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = emmc_sdr_ctrl_cfgs;
		break;

	case UHS_DDR50:
	case MMC_DDR_52:
		sdhci_cdns6_phy_cfgs = emmc_ddr_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = emmc_ddr_ctrl_cfgs;
		break;

	case UHS_SDR104:
	case MMC_HS_200:
		sdhci_cdns6_phy_cfgs = emmc_hs200_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = emmc_hs200_ctrl_cfgs;
		break;

	case MMC_HS_400:
	case MMC_HS_400_ES:
		sdhci_cdns6_phy_cfgs = emmc_hs400_phy_cfgs;
		sdhci_cdns6_ctrl_cfgs = emmc_hs400_ctrl_cfgs;
		break;
	default:
		return -EINVAL;
	}

	for (i = 0; i < SDHCI_CDNS6_PHY_CFG_NUM; i++)
		dev_read_u32(dev, sdhci_cdns6_phy_cfgs[i].property, &sdhci_cdns6_phy_cfgs[i].val);

	for (i = 0; i < SDHCI_CDNS6_CTRL_CFG_NUM; i++)
		dev_read_u32(dev, sdhci_cdns6_ctrl_cfgs[i].property, &sdhci_cdns6_ctrl_cfgs[i].val);

	/* Switch On the DLL Reset */
	sdhci_cdns6_reset_phy_dll(plat, true);

	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);
	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);
	if (ret) {
		printf("sdhci_cdns6_reset_phy is not completed\n");
		return ret;
	}

	/* Set PHY DQ TIMING control register */
	sdhci_cdns6_write_phy_reg(plat, PHY_DQ_TIMING_REG_ADDR, sdhci_cdns6_phy_cfgs[3].val);

	/* Set HRS09 register */
	tmp = readl(plat->hrs_addr + SDHCI_CDNS_HRS09);
	tmp &= ~(SDHCI_CDNS_HRS09_EXTENDED_WR_MODE |
		 SDHCI_CDNS_HRS09_EXTENDED_RD_MODE |
		 SDHCI_CDNS_HRS09_RDDATA_EN |
		 SDHCI_CDNS_HRS09_RDCMD_EN);
	tmp |= sdhci_cdns6_ctrl_cfgs[0].val;
	writel(tmp, plat->hrs_addr + SDHCI_CDNS_HRS09);

	/* Set HRS10 register */
	tmp = readl(plat->hrs_addr + SDHCI_CDNS_HRS10);
	tmp &= ~SDHCI_CDNS_HRS10_HCSDCLKADJ;
	tmp |= sdhci_cdns6_ctrl_cfgs[1].val;
	writel(tmp, plat->hrs_addr + SDHCI_CDNS_HRS10);

	/* Set HRS16 register */
	writel(sdhci_cdns6_ctrl_cfgs[2].val, plat->hrs_addr + SDHCI_CDNS_HRS16);

	/* Set HRS07 register */
	writel(sdhci_cdns6_ctrl_cfgs[3].val, plat->hrs_addr + SDHCI_CDNS_HRS07);

	return 0;
}

int sdhci_cdns6_phy_init(struct udevice *dev, struct sdhci_cdns_plat *plat)
{
	return sdhci_cdns6_phy_adj(dev, plat, MMC_LEGACY);
}

int sdhci_cdns6_set_tune_val(struct sdhci_cdns_plat *plat, unsigned int val)
{
	u32 tmp, tuneval;
	int ret;

	tuneval = (val * 256) / SDHCI_CDNS_MAX_TUNING_LOOP;

	tmp = sdhci_cdns6_read_phy_reg(plat, PHY_DLL_SLAVE_CTRL_REG_ADDR);
	tmp &= ~(PHY_DLL_SLAVE_CTRL_REG_READ_DQS_CMD_DELAY |
		 PHY_DLL_SLAVE_CTRL_REG_READ_DQS_DELAY);
	tmp |= FIELD_PREP(PHY_DLL_SLAVE_CTRL_REG_READ_DQS_CMD_DELAY, tuneval) |
		FIELD_PREP(PHY_DLL_SLAVE_CTRL_REG_READ_DQS_DELAY, tuneval);

	/* Switch On the DLL Reset */
	sdhci_cdns6_reset_phy_dll(plat, true);

	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) {
		printf("sdhci_cdns6_reset_phy is not completed\n");
		return ret;
	}

	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);
}
