/*
 * Copyright (C) 2015 Purna Chandra Mandal <purna.mandal@microchip.com>
 *
 * SPDX-License-Identifier:	GPL-2.0+
 *
 */

#include <common.h>
#include <clk.h>
#include <dm.h>
#include <div64.h>
#include <wait_bit.h>
#include <dm/lists.h>
#include <asm/io.h>
#include <mach/pic32.h>
#include <dt-bindings/clock/microchip,clock.h>

DECLARE_GLOBAL_DATA_PTR;

/* Primary oscillator */
#define SYS_POSC_CLK_HZ	24000000

/* FRC clk rate */
#define SYS_FRC_CLK_HZ	8000000

/* Clock Registers */
#define OSCCON		0x0000
#define OSCTUNE		0x0010
#define SPLLCON		0x0020
#define REFO1CON	0x0080
#define REFO1TRIM	0x0090
#define PB1DIV		0x0140

/* SPLL */
#define ICLK_MASK	0x00000080
#define PLLIDIV_MASK	0x00000007
#define PLLODIV_MASK	0x00000007
#define CUROSC_MASK	0x00000007
#define PLLMUL_MASK	0x0000007F
#define FRCDIV_MASK	0x00000007

/* PBCLK */
#define PBDIV_MASK	0x00000007

/* SYSCLK MUX */
#define SCLK_SRC_FRC1	0
#define SCLK_SRC_SPLL	1
#define SCLK_SRC_POSC	2
#define SCLK_SRC_FRC2	7

/* Reference Oscillator Control Reg fields */
#define REFO_SEL_MASK	0x0f
#define REFO_SEL_SHIFT	0
#define REFO_ACTIVE	BIT(8)
#define REFO_DIVSW_EN	BIT(9)
#define REFO_OE		BIT(12)
#define REFO_ON		BIT(15)
#define REFO_DIV_SHIFT	16
#define REFO_DIV_MASK	0x7fff

/* Reference Oscillator Trim Register Fields */
#define REFO_TRIM_REG	0x10
#define REFO_TRIM_MASK	0x1ff
#define REFO_TRIM_SHIFT	23
#define REFO_TRIM_MAX	511

#define ROCLK_SRC_SCLK		0x0
#define ROCLK_SRC_SPLL		0x7
#define ROCLK_SRC_ROCLKI	0x8

/* Memory PLL */
#define MPLL_IDIV		0x3f
#define MPLL_MULT		0xff
#define MPLL_ODIV1		0x7
#define MPLL_ODIV2		0x7
#define MPLL_VREG_RDY		BIT(23)
#define MPLL_RDY		BIT(31)
#define MPLL_IDIV_SHIFT		0
#define MPLL_MULT_SHIFT		8
#define MPLL_ODIV1_SHIFT	24
#define MPLL_ODIV2_SHIFT	27
#define MPLL_IDIV_INIT		0x03
#define MPLL_MULT_INIT		0x32
#define MPLL_ODIV1_INIT		0x02
#define MPLL_ODIV2_INIT		0x01

struct pic32_clk_priv {
	void __iomem *iobase;
	void __iomem *syscfg_base;
};

static ulong pic32_get_pll_rate(struct pic32_clk_priv *priv)
{
	u32 iclk, idiv, odiv, mult;
	ulong plliclk, v;

	v = readl(priv->iobase + SPLLCON);
	iclk = (v & ICLK_MASK);
	idiv = ((v >> 8) & PLLIDIV_MASK) + 1;
	odiv = ((v >> 24) & PLLODIV_MASK);
	mult = ((v >> 16) & PLLMUL_MASK) + 1;

	plliclk = iclk ? SYS_FRC_CLK_HZ : SYS_POSC_CLK_HZ;

	if (odiv < 2)
		odiv = 2;
	else if (odiv < 5)
		odiv = (1 << odiv);
	else
		odiv = 32;

	return ((plliclk / idiv) * mult) / odiv;
}

static ulong pic32_get_sysclk(struct pic32_clk_priv *priv)
{
	ulong v;
	ulong hz;
	ulong div, frcdiv;
	ulong curr_osc;

	/* get clk source */
	v = readl(priv->iobase + OSCCON);
	curr_osc = (v >> 12) & CUROSC_MASK;
	switch (curr_osc) {
	case SCLK_SRC_FRC1:
	case SCLK_SRC_FRC2:
		frcdiv = ((v >> 24) & FRCDIV_MASK);
		div = ((1 << frcdiv) + 1) + (128 * (frcdiv == 7));
		hz = SYS_FRC_CLK_HZ / div;
		break;

	case SCLK_SRC_SPLL:
		hz = pic32_get_pll_rate(priv);
		break;

	case SCLK_SRC_POSC:
		hz = SYS_POSC_CLK_HZ;
		break;

	default:
		hz = 0;
		printf("clk: unknown sclk_src.\n");
		break;
	}

	return hz;
}

static ulong pic32_get_pbclk(struct pic32_clk_priv *priv, int periph)
{
	void __iomem *reg;
	ulong div, clk_freq;

	WARN_ON((periph < PB1CLK) || (periph > PB7CLK));

	clk_freq = pic32_get_sysclk(priv);

	reg = priv->iobase + PB1DIV + (periph - PB1CLK) * 0x10;
	div = (readl(reg) & PBDIV_MASK) + 1;

	return clk_freq / div;
}

static ulong pic32_get_cpuclk(struct pic32_clk_priv *priv)
{
	return pic32_get_pbclk(priv, PB7CLK);
}

static ulong pic32_set_refclk(struct pic32_clk_priv *priv, int periph,
			      int parent_rate, int rate, int parent_id)
{
	void __iomem *reg;
	u32 div, trim, v;
	u64 frac;

	WARN_ON((periph < REF1CLK) || (periph > REF5CLK));

	/* calculate dividers,
	 *   rate = parent_rate / [2 * (div + (trim / 512))]
	 */
	if (parent_rate <= rate) {
		div = 0;
		trim = 0;
	} else {
		div = parent_rate / (rate << 1);
		frac = parent_rate;
		frac <<= 8;
		do_div(frac, rate);
		frac -= (u64)(div << 9);
		trim = (frac >= REFO_TRIM_MAX) ? REFO_TRIM_MAX : (u32)frac;
	}

	reg = priv->iobase + REFO1CON + (periph - REF1CLK) * 0x20;

	/* disable clk */
	writel(REFO_ON | REFO_OE, reg + _CLR_OFFSET);

	/* wait till previous src change is active */
	wait_for_bit(__func__, reg, REFO_DIVSW_EN | REFO_ACTIVE,
		     false, CONFIG_SYS_HZ, false);

	/* parent_id */
	v = readl(reg);
	v &= ~(REFO_SEL_MASK << REFO_SEL_SHIFT);
	v |= (parent_id << REFO_SEL_SHIFT);

	/* apply rodiv */
	v &= ~(REFO_DIV_MASK << REFO_DIV_SHIFT);
	v |= (div << REFO_DIV_SHIFT);
	writel(v, reg);

	/* apply trim */
	v = readl(reg + REFO_TRIM_REG);
	v &= ~(REFO_TRIM_MASK << REFO_TRIM_SHIFT);
	v |= (trim << REFO_TRIM_SHIFT);
	writel(v, reg + REFO_TRIM_REG);

	/* enable clk */
	writel(REFO_ON | REFO_OE, reg + _SET_OFFSET);

	/* switch divider */
	writel(REFO_DIVSW_EN, reg + _SET_OFFSET);

	/* wait for divider switching to complete */
	return wait_for_bit(__func__, reg, REFO_DIVSW_EN, false,
			    CONFIG_SYS_HZ, false);
}

static ulong pic32_get_refclk(struct pic32_clk_priv *priv, int periph)
{
	u32 rodiv, rotrim, rosel, v, parent_rate;
	void __iomem *reg;
	u64 rate64;

	WARN_ON((periph < REF1CLK) || (periph > REF5CLK));

	reg = priv->iobase + REFO1CON + (periph - REF1CLK) * 0x20;
	v = readl(reg);
	/* get rosel */
	rosel = (v >> REFO_SEL_SHIFT) & REFO_SEL_MASK;
	/* get div */
	rodiv = (v >> REFO_DIV_SHIFT) & REFO_DIV_MASK;

	/* get trim */
	v = readl(reg + REFO_TRIM_REG);
	rotrim = (v >> REFO_TRIM_SHIFT) & REFO_TRIM_MASK;

	if (!rodiv)
		return 0;

	/* get parent rate */
	switch (rosel) {
	case ROCLK_SRC_SCLK:
		parent_rate = pic32_get_cpuclk(priv);
		break;
	case ROCLK_SRC_SPLL:
		parent_rate = pic32_get_pll_rate(priv);
		break;
	default:
		parent_rate = 0;
		break;
	}

	/* Calculation
	 * rate = parent_rate / [2 * (div + (trim / 512))]
	 */
	if (rotrim) {
		rodiv <<= 9;
		rodiv += rotrim;
		rate64 = parent_rate;
		rate64 <<= 8;
		do_div(rate64, rodiv);
		v = (u32)rate64;
	} else {
		v = parent_rate / (rodiv << 1);
	}
	return v;
}

static ulong pic32_get_mpll_rate(struct pic32_clk_priv *priv)
{
	u32 v, idiv, mul;
	u32 odiv1, odiv2;
	u64 rate;

	v = readl(priv->syscfg_base + CFGMPLL);
	idiv = v & MPLL_IDIV;
	mul = (v >> MPLL_MULT_SHIFT) & MPLL_MULT;
	odiv1 = (v >> MPLL_ODIV1_SHIFT) & MPLL_ODIV1;
	odiv2 = (v >> MPLL_ODIV2_SHIFT) & MPLL_ODIV2;

	rate = (SYS_POSC_CLK_HZ / idiv) * mul;
	do_div(rate, odiv1);
	do_div(rate, odiv2);

	return (ulong)rate;
}

static int pic32_mpll_init(struct pic32_clk_priv *priv)
{
	u32 v, mask;

	/* initialize */
	v = (MPLL_IDIV_INIT << MPLL_IDIV_SHIFT) |
	    (MPLL_MULT_INIT << MPLL_MULT_SHIFT) |
	    (MPLL_ODIV1_INIT << MPLL_ODIV1_SHIFT) |
	    (MPLL_ODIV2_INIT << MPLL_ODIV2_SHIFT);

	writel(v, priv->syscfg_base + CFGMPLL);

	/* Wait for ready */
	mask = MPLL_RDY | MPLL_VREG_RDY;
	return wait_for_bit(__func__, priv->syscfg_base + CFGMPLL, mask,
			    true, get_tbclk(), false);
}

static void pic32_clk_init(struct udevice *dev)
{
	const void *blob = gd->fdt_blob;
	struct pic32_clk_priv *priv;
	ulong rate, pll_hz;
	char propname[50];
	int i;

	priv = dev_get_priv(dev);
	pll_hz = pic32_get_pll_rate(priv);

	/* Initialize REFOs as not initialized and enabled on reset. */
	for (i = REF1CLK; i <= REF5CLK; i++) {
		snprintf(propname, sizeof(propname),
			 "microchip,refo%d-frequency", i - REF1CLK + 1);
		rate = fdtdec_get_int(blob, dev->of_offset, propname, 0);
		if (rate)
			pic32_set_refclk(priv, i, pll_hz, rate, ROCLK_SRC_SPLL);
	}

	/* Memory PLL */
	pic32_mpll_init(priv);
}

static ulong pic32_clk_get_rate(struct udevice *dev)
{
	struct pic32_clk_priv *priv = dev_get_priv(dev);

	return pic32_get_cpuclk(priv);
}

static ulong pic32_get_periph_rate(struct udevice *dev, int periph)
{
	struct pic32_clk_priv *priv = dev_get_priv(dev);
	ulong rate;

	switch (periph) {
	case PB1CLK ... PB7CLK:
		rate = pic32_get_pbclk(priv, periph);
		break;
	case REF1CLK ... REF5CLK:
		rate = pic32_get_refclk(priv, periph);
		break;
	case PLLCLK:
		rate = pic32_get_pll_rate(priv);
		break;
	case MPLL:
		rate = pic32_get_mpll_rate(priv);
		break;
	default:
		rate = 0;
		break;
	}

	return rate;
}

static ulong pic32_set_periph_rate(struct udevice *dev, int periph, ulong rate)
{
	struct pic32_clk_priv *priv = dev_get_priv(dev);
	ulong pll_hz;

	switch (periph) {
	case REF1CLK ... REF5CLK:
		pll_hz = pic32_get_pll_rate(priv);
		pic32_set_refclk(priv, periph, pll_hz, rate, ROCLK_SRC_SPLL);
		break;
	default:
		break;
	}

	return rate;
}

static struct clk_ops pic32_pic32_clk_ops = {
	.get_rate = pic32_clk_get_rate,
	.set_periph_rate = pic32_set_periph_rate,
	.get_periph_rate = pic32_get_periph_rate,
};

static int pic32_clk_probe(struct udevice *dev)
{
	struct pic32_clk_priv *priv = dev_get_priv(dev);
	fdt_addr_t addr;
	fdt_size_t size;

	addr = fdtdec_get_addr_size(gd->fdt_blob, dev->of_offset, "reg", &size);
	if (addr == FDT_ADDR_T_NONE)
		return -EINVAL;

	priv->iobase = ioremap(addr, size);
	if (!priv->iobase)
		return -EINVAL;

	priv->syscfg_base = pic32_get_syscfg_base();

	/* initialize clocks */
	pic32_clk_init(dev);

	return 0;
}

static const struct udevice_id pic32_clk_ids[] = {
	{ .compatible = "microchip,pic32mzda-clk"},
	{}
};

U_BOOT_DRIVER(pic32_clk) = {
	.name		= "pic32_clk",
	.id		= UCLASS_CLK,
	.of_match	= pic32_clk_ids,
	.flags		= DM_FLAG_PRE_RELOC,
	.ops		= &pic32_pic32_clk_ops,
	.probe		= pic32_clk_probe,
	.priv_auto_alloc_size = sizeof(struct pic32_clk_priv),
};
