// SPDX-License-Identifier:    GPL-2.0
/*
 * Copyright (C) 2018 Marvell International Ltd.
 */

#include <dm.h>
#include <malloc.h>
#include <miiphy.h>
#include <misc.h>
#include <pci.h>
#include <pci_ids.h>
#include <phy.h>
#include <asm/global_data.h>
#include <asm/io.h>
#include <linux/ctype.h>
#include <linux/delay.h>

#define PCI_DEVICE_ID_OCTEONTX_SMI 0xA02B

DECLARE_GLOBAL_DATA_PTR;

enum octeontx_smi_mode {
	CLAUSE22 = 0,
	CLAUSE45 = 1,
};

enum {
	SMI_OP_C22_WRITE = 0,
	SMI_OP_C22_READ = 1,

	SMI_OP_C45_ADDR = 0,
	SMI_OP_C45_WRITE = 1,
	SMI_OP_C45_PRIA = 2,
	SMI_OP_C45_READ = 3,
};

union smi_x_clk {
	u64 u;
	struct smi_x_clk_s {
		int phase:8;
		int sample:4;
		int preamble:1;
		int clk_idle:1;
		int reserved_14_14:1;
		int sample_mode:1;
		int sample_hi:5;
		int reserved_21_23:3;
		int mode:1;
	} s;
};

union smi_x_cmd {
	u64 u;
	struct smi_x_cmd_s {
		int reg_adr:5;
		int reserved_5_7:3;
		int phy_adr:5;
		int reserved_13_15:3;
		int phy_op:2;
	} s;
};

union smi_x_wr_dat {
	u64 u;
	struct smi_x_wr_dat_s {
		unsigned int dat:16;
		int val:1;
		int pending:1;
	} s;
};

union smi_x_rd_dat {
	u64 u;
	struct smi_x_rd_dat_s {
		unsigned int dat:16;
		int val:1;
		int pending:1;
	} s;
};

union smi_x_en {
	u64 u;
	struct smi_x_en_s {
		int en:1;
	} s;
};

#define SMI_X_RD_DAT	0x10ull
#define SMI_X_WR_DAT	0x08ull
#define SMI_X_CMD	0x00ull
#define SMI_X_CLK	0x18ull
#define SMI_X_EN	0x20ull

struct octeontx_smi_priv {
	void __iomem *baseaddr;
	enum octeontx_smi_mode mode;
};

#define MDIO_TIMEOUT 10000

void octeontx_smi_setmode(struct mii_dev *bus, enum octeontx_smi_mode mode)
{
	struct octeontx_smi_priv *priv = bus->priv;
	union smi_x_clk smix_clk;

	smix_clk.u = readq(priv->baseaddr + SMI_X_CLK);
	smix_clk.s.mode = mode;
	smix_clk.s.preamble = mode == CLAUSE45;
	writeq(smix_clk.u, priv->baseaddr + SMI_X_CLK);

	priv->mode = mode;
}

int octeontx_c45_addr(struct mii_dev *bus, int addr, int devad, int regnum)
{
	struct octeontx_smi_priv *priv = bus->priv;

	union smi_x_cmd smix_cmd;
	union smi_x_wr_dat smix_wr_dat;
	unsigned long timeout = MDIO_TIMEOUT;

	smix_wr_dat.u = 0;
	smix_wr_dat.s.dat = regnum;

	writeq(smix_wr_dat.u, priv->baseaddr + SMI_X_WR_DAT);

	smix_cmd.u = 0;
	smix_cmd.s.phy_op = SMI_OP_C45_ADDR;
	smix_cmd.s.phy_adr = addr;
	smix_cmd.s.reg_adr = devad;

	writeq(smix_cmd.u, priv->baseaddr + SMI_X_CMD);

	do {
		smix_wr_dat.u = readq(priv->baseaddr + SMI_X_WR_DAT);
		udelay(100);
		timeout--;
	} while (smix_wr_dat.s.pending && timeout);

	return timeout == 0;
}

int octeontx_phy_read(struct mii_dev *bus, int addr, int devad, int regnum)
{
	struct octeontx_smi_priv *priv = bus->priv;
	union smi_x_cmd smix_cmd;
	union smi_x_rd_dat smix_rd_dat;
	unsigned long timeout = MDIO_TIMEOUT;
	int ret;

	enum octeontx_smi_mode mode = (devad < 0) ? CLAUSE22 : CLAUSE45;

	debug("RD: Mode: %u, baseaddr: %p, addr: %d, devad: %d, reg: %d\n",
	      mode, priv->baseaddr, addr, devad, regnum);

	octeontx_smi_setmode(bus, mode);

	if (mode == CLAUSE45) {
		ret = octeontx_c45_addr(bus, addr, devad, regnum);

		debug("RD: ret: %u\n", ret);

		if (ret)
			return 0;
	}

	smix_cmd.u = 0;
	smix_cmd.s.phy_adr = addr;

	if (mode == CLAUSE45) {
		smix_cmd.s.reg_adr = devad;
		smix_cmd.s.phy_op = SMI_OP_C45_READ;
	} else {
		smix_cmd.s.reg_adr = regnum;
		smix_cmd.s.phy_op = SMI_OP_C22_READ;
	}

	writeq(smix_cmd.u, priv->baseaddr + SMI_X_CMD);

	do {
		smix_rd_dat.u = readq(priv->baseaddr + SMI_X_RD_DAT);
		udelay(10);
		timeout--;
	} while (smix_rd_dat.s.pending && timeout);

	debug("SMIX_RD_DAT: %lx\n", (unsigned long)smix_rd_dat.u);

	return smix_rd_dat.s.dat;
}

int octeontx_phy_write(struct mii_dev *bus, int addr, int devad, int regnum,
		       u16 value)
{
	struct octeontx_smi_priv *priv = bus->priv;
	union smi_x_cmd smix_cmd;
	union smi_x_wr_dat smix_wr_dat;
	unsigned long timeout = MDIO_TIMEOUT;
	int ret;

	enum octeontx_smi_mode mode = (devad < 0) ? CLAUSE22 : CLAUSE45;

	debug("WR: Mode: %u, baseaddr: %p, addr: %d, devad: %d, reg: %d\n",
	      mode, priv->baseaddr, addr, devad, regnum);

	if (mode == CLAUSE45) {
		ret = octeontx_c45_addr(bus, addr, devad, regnum);

		debug("WR: ret: %u\n", ret);

		if (ret)
			return ret;
	}

	smix_wr_dat.u = 0;
	smix_wr_dat.s.dat = value;

	writeq(smix_wr_dat.u, priv->baseaddr + SMI_X_WR_DAT);

	smix_cmd.u = 0;
	smix_cmd.s.phy_adr = addr;

	if (mode == CLAUSE45) {
		smix_cmd.s.reg_adr = devad;
		smix_cmd.s.phy_op = SMI_OP_C45_WRITE;
	} else {
		smix_cmd.s.reg_adr = regnum;
		smix_cmd.s.phy_op = SMI_OP_C22_WRITE;
	}

	writeq(smix_cmd.u, priv->baseaddr + SMI_X_CMD);

	do {
		smix_wr_dat.u = readq(priv->baseaddr + SMI_X_WR_DAT);
		udelay(10);
		timeout--;
	} while (smix_wr_dat.s.pending && timeout);

	debug("SMIX_WR_DAT: %lx\n", (unsigned long)smix_wr_dat.u);

	return timeout == 0;
}

int octeontx_smi_reset(struct mii_dev *bus)
{
	struct octeontx_smi_priv *priv = bus->priv;

	union smi_x_en smi_en;

	smi_en.s.en = 0;
	writeq(smi_en.u, priv->baseaddr + SMI_X_EN);

	smi_en.s.en = 1;
	writeq(smi_en.u, priv->baseaddr + SMI_X_EN);

	octeontx_smi_setmode(bus, CLAUSE22);

	return 0;
}

/* PHY XS initialization, primarily for RXAUI
 *
 */
int rxaui_phy_xs_init(struct mii_dev *bus, int phy_addr)
{
	int reg;
	ulong start_time;
	int phy_id1, phy_id2;
	int oui, model_number;

	phy_id1 = octeontx_phy_read(bus, phy_addr, 1, 0x2);
	phy_id2 = octeontx_phy_read(bus, phy_addr, 1, 0x3);
	model_number = (phy_id2 >> 4) & 0x3F;
	debug("%s model %x\n", __func__, model_number);
	oui = phy_id1;
	oui <<= 6;
	oui |= (phy_id2 >> 10) & 0x3F;
	debug("%s oui %x\n", __func__, oui);
	switch (oui) {
	case 0x5016:
		if (model_number == 9) {
			debug("%s +\n", __func__);
			/* Perform hardware reset in XGXS control */
			reg = octeontx_phy_read(bus, phy_addr, 4, 0x0);
			if ((reg & 0xffff) < 0)
				goto read_error;
			reg |= 0x8000;
			octeontx_phy_write(bus, phy_addr, 4, 0x0, reg);

			start_time = get_timer(0);
			do {
				reg = octeontx_phy_read(bus, phy_addr, 4, 0x0);
				if ((reg & 0xffff) < 0)
					goto read_error;
			} while ((reg & 0x8000) && get_timer(start_time) < 500);
			if (reg & 0x8000) {
				printf("HW reset for M88X3120 PHY failed");
				printf("MII_BMCR: 0x%x\n", reg);
				return -1;
			}
			/* program 4.49155 with 0x5 */
			octeontx_phy_write(bus, phy_addr, 4, 0xc003, 0x5);
		}
		break;
	default:
		break;
	}

	return 0;

read_error:
	debug("M88X3120 PHY config read failed\n");
	return -1;
}

int octeontx_smi_probe(struct udevice *dev)
{
	pci_dev_t bdf = dm_pci_get_bdf(dev);
	struct octeontx_smi_priv *priv;
	struct mii_dev *bus;
	int ret, cnt = 0;
	ofnode subnode;
	u64 baseaddr;

	debug("SMI PCI device: %x\n", bdf);
	if (!dm_pci_map_bar(dev, PCI_BASE_ADDRESS_0, 0, 0, PCI_REGION_TYPE, PCI_REGION_MEM)) {
		printf("Failed to map PCI region for bdf %x\n", bdf);
		return -1;
	}

	dev_for_each_subnode(subnode, dev) {
		if (!ofnode_device_is_compatible(subnode,
						 "cavium,thunder-8890-mdio"))
			continue;
		if (ofnode_read_u64(subnode, "reg", &baseaddr))
			continue;
		bus = mdio_alloc();
		priv = malloc(sizeof(*priv));
		if (!bus || !priv) {
			printf("Failed to allocate OcteonTX MDIO bus # %u\n",
			       dev_seq(dev));
			ret = -ENOMEM;
			goto error_ret;
		}

		bus->read = octeontx_phy_read;
		bus->write = octeontx_phy_write;
		bus->reset = octeontx_smi_reset;
		bus->priv = priv;

		priv->mode = CLAUSE22;
		priv->baseaddr = (void __iomem *)baseaddr;
		debug("mdio base addr %p\n", priv->baseaddr);

		/* use given name or generate its own unique name */
		snprintf(bus->name, MDIO_NAME_LEN, "smi%d", cnt++);

		ret = mdio_register(bus);
		if (ret)
			goto error_ret;
	}
	return 0;

error_ret:
	if (bus)
		free(bus);
	if (priv)
		free(priv);
	return ret;
}

static const struct udevice_id octeontx_smi_ids[] = {
	{ .compatible = "cavium,thunder-8890-mdio-nexus" },
	{}
};

U_BOOT_DRIVER(octeontx_smi) = {
	.name	= "octeontx_smi",
	.id	= UCLASS_MISC,
	.probe	= octeontx_smi_probe,
	.of_match = octeontx_smi_ids,
};

static struct pci_device_id octeontx_smi_supported[] = {
	{ PCI_VDEVICE(CAVIUM, PCI_DEVICE_ID_CAVIUM_SMI) },
	{}
};

U_BOOT_PCI_DEVICE(octeontx_smi, octeontx_smi_supported);
