/* SPDX-License-Identifier: GPL-2.0-only */

// This file is used for setting up clocks and get devices out of reset
// For more Information see FU740-C000 Manual Chapter 7 Clocking and Reset

#include <delay.h>
#include <device/mmio.h>
#include <soc/addressmap.h>
#include <soc/clock.h>
#include <gpio.h>
#include <stdint.h>

// Clock frequencies for the cores, ddr and the peripherals are all derived from the hfclk (high frequency clock) and it is always 26 MHz
#define FU740_HFCLK_FREQ (26 * MHz)

struct prci_ctlr {
	u32 hfxosccfg;              // offset 0x00
	u32 core_pllcfg;            // offset 0x04
	u32 core_plloutdiv;         // offset 0x08
	u32 ddr_pllcfg;             // offset 0x0c
	u32 ddr_plloutdiv;          // offset 0x10
	u32 pcieaux_plloutdiv;      // offset 0x14 (undocumented)
	u32 reserved18;             // offset 0x18
	u32 gemgxl_pllcfg;          // offset 0x1c
	u32 gemgxl_plloutdiv;       // offset 0x20
	u32 core_clk_sel_reg;       // offset 0x24
	u32 devices_reset_n;        // offset 0x28
	u32 clk_mux_status;         // offset 0x2C
	u32 cltx_pllcfg;            // offset 0x30 chiplink (undocumented)
	u32 cltx_plloutdiv;         // offset 0x34 chiplink (undocumented)
	u32 dvfs_core_pllcfg;       // offset 0x38
	u32 dvfs_core_plloutdiv;    // offset 0x3C
	u32 corepllsel;             // offset 0x40 (undocumented, but probably same as last gen)
	u8 reserved44[12];          // offset 0x44
	u32 hfpclk_pllcfg;          // offset 0x50
	u32 hfpclk_plloutdiv;       // offset 0x54
	u32 hfpclkpllsel;           // offset 0x58 (undocumented, but probably same as last gen)
	u32 hfpclk_div_reg;         // offset 0x5C
	u8 reserved60[128];         // offset 0x60
	u32 prci_plls;              // offset 0xE0
	u8 reservedE4[12];          // offset 0xE4
	u32 procmoncfg_core_clock;  // offset 0xF0 (undocumented)
} __packed;

static struct prci_ctlr *prci = (void *)FU740_PRCI;

// =================================
// clock selections
// =================================

#define PRCI_COREPLLSEL_MASK 1
#define PRCI_COREPLLSEL_COREPLL 0
#define PRCI_COREPLLSEL_DVFSCOREPLL 1

#define PRCI_CORECLKSEL_MASK 1
#define PRCI_CORECLKSEL_CORECLKPLL 0
#define PRCI_CORECLKSEL_HFCLK 1

#define PRCI_HFPCLKSEL_MASK 1
#define PRCI_HFPCLKSEL_PLL 0
#define PRCI_HFPCLKSEL_HFCLK 1

// ===================================
// pllcfg register format is used by all PLLs
// ===================================

#define PRCI_PLLCFG_DIVR_SHIFT       0
#define PRCI_PLLCFG_DIVF_SHIFT       6
#define PRCI_PLLCFG_DIVQ_SHIFT      15
#define PRCI_PLLCFG_RANGE_SHIFT     18
#define PRCI_PLLCFG_BYPASS_SHIFT    24
#define PRCI_PLLCFG_FSEBYPASS_SHIFT 25
#define PRCI_PLLCFG_LOCK_SHIFT      31

#define PRCI_PLLCFG_DIVR_MASK      (0x03f << PRCI_PLLCFG_DIVR_SHIFT)
#define PRCI_PLLCFG_DIVF_MASK      (0x1ff << PRCI_PLLCFG_DIVF_SHIFT)
#define PRCI_PLLCFG_DIVQ_MASK      (0x007 << PRCI_PLLCFG_DIVQ_SHIFT)
#define PRCI_PLLCFG_RANGE_MASK     (0x007 << PRCI_PLLCFG_RANGE_SHIFT)
#define PRCI_PLLCFG_BYPASS_MASK    (0x001 << PRCI_PLLCFG_BYPASS_SHIFT)
#define PRCI_PLLCFG_FSEBYPASS_MASK (0x001 << PRCI_PLLCFG_FSEBYPASS_SHIFT)
#define PRCI_PLLCFG_LOCK_MASK      (0x001 << PRCI_PLLCFG_LOCK_SHIFT)

// ===================================
// plloutdiv register formats
// ===================================

// registered are used to enable/disable PLLs
#define PRCI_DVFSCORE_PLLOUTDIV_MASK (1 << 24) // Note: u-boot and fu740 manual differ here ...
#define PRCI_HFPCLK_PLLOUTDIV_MASK   (1 << 31) // Note: according to u-boot it is (1 << 24) but if I use that it gets stuck
#define PRCI_DDR_PLLOUTDIV_MASK      (1 << 31)
#define PRCI_GEMGXL_PLLOUTDIV_MASK   (1 << 31)
#define PRCI_CLTX_PLLOUTDIV_MASK     (1 << 24) // undocumented (chiplink tx)
#define PRCI_PCIEAUX_PLLOUTDIV_MASK  (1 <<  0) // undocumented
#define PRCI_CORE_PLLOUTDIV_MASK     (1 << 31) // undocumented

// ===================================
// devicereset register formats
// ===================================

// used to get devices in or out of reset
#define PRCI_DEVICES_RESET_DDR_CTRL_RST (1 << 0) // DDR Controller
#define PRCI_DEVICES_RESET_DDR_AXI_RST  (1 << 1) // DDR Controller AXI Interface
#define PRCI_DEVICES_RESET_DDR_AHB_RST  (1 << 2) // DDR Controller AHB Interface
#define PRCI_DEVICES_RESET_DDR_PHY_RST  (1 << 3) // DDR PHY
#define PRCI_DEVICES_RESET_PCIEAUX_RST  (1 << 4)
#define PRCI_DEVICES_RESET_GEMGXL_RST   (1 << 5) // Gigabit Ethernet Subsystem
#define PRCI_DEVICES_RESET_CLTX_RST     (1 << 6) // chiplink reset (undocumented)

// ===================================
// prci_plls register format
// ===================================

// used to check if certain PLLs are present in the SOC
#define PRCI_PLLS_CLTXPLL     (1 << 0)
#define PRCI_PLLS_GEMGXLPLL   (1 << 1)
#define PRCI_PLLS_DDRPLL      (1 << 2)
#define PRCI_PLLS_HFPCLKPLL   (1 << 3)
#define PRCI_PLLS_DVFSCOREPLL (1 << 4)
#define PRCI_PLLS_COREPLL     (1 << 5)

// ===================================
// clk_mux_status register format
// ===================================

// read only register which is used to set some clock multiplex settings
// the value of this register depends on the state of pins connected to the FU740 SOC
// on the hifive-unmatched board the state of the pins is set by a hardware switch
#define PRCI_CLK_MUX_STATUS_CORECLKPLLSEL (1 << 0)
        // 0 - HFCLK or CORECLK
        // 1 - only HFCLK
#define PRCI_CLK_MUX_STATUS_TLCLKSEL      (1 << 1)
        // 0 - CORECLK/2
        // 1 - CORECLK
#define PRCI_CLK_MUX_STATUS_RTCXSEL       (1 << 2)
        // 0 - use HFXCLK for RTC
        // 1 - use RTCXALTCLKIN for RTC
#define PRCI_CLK_MUX_STATUS_DDRCTRLCLKSEL (1 << 3)
#define PRCI_CLK_MUX_STATUS_DDRPHYCLKSEL  (1 << 4)
#define PRCI_CLK_MUX_STATUS_RESERVED      (1 << 5)
#define PRCI_CLK_MUX_STATUS_GEMGXLCLKSEL  (1 << 6)
#define PRCI_CLK_MUX_STATUS_MAINMEMCLKSEL (1 << 7)

// ===================================
// hfxosccfg register format
// ===================================

#define PRCI_HFXOSCCFG_HFXOSEN   (1 << 30) // Crystal oscillator enable
        // Note: I guess (it is not documented)
        // 0 - XTAL PADS
        // 1 - OSC PADS
#define PRCI_HFXOSCCFG_HFXOSCRDY (1 << 31) // Crystal oscillator ready

struct pll_settings {
	unsigned int divr:6; // divider before PLL loop (reference), equal to divr + 1
	unsigned int divf:9; // VCO feedback divider value, equal to 2 * (divf + 1)
	unsigned int divq:3; // divider after PLL loop, equal to 2^divq
	// PLL filter range (TODO documentation is not really clear on how to set it)
	unsigned int range:3;
	unsigned int bypass:1; // probably used to bypass the PLL
	// internal or external input path (internal = 1, external = 0)
	//WARN this is only a guess since it is undocumented
	unsigned int fsebypass:1;
};

static void configure_pll(u32 *reg, const struct pll_settings *s)
{
	// Write the settings to the register
	u32 c = read32(reg);
	clrsetbits32(&c, PRCI_PLLCFG_DIVR_MASK
		       | PRCI_PLLCFG_DIVF_MASK
		       | PRCI_PLLCFG_DIVQ_MASK
		       | PRCI_PLLCFG_RANGE_MASK
		       | PRCI_PLLCFG_BYPASS_MASK
		       | PRCI_PLLCFG_FSEBYPASS_MASK,
		         (s->divr << PRCI_PLLCFG_DIVR_SHIFT)
		       | (s->divf << PRCI_PLLCFG_DIVF_SHIFT)
		       | (s->divq << PRCI_PLLCFG_DIVQ_SHIFT)
		       | (s->range << PRCI_PLLCFG_RANGE_SHIFT)
		       | (s->bypass << PRCI_PLLCFG_BYPASS_SHIFT)
		       | (s->fsebypass << PRCI_PLLCFG_FSEBYPASS_SHIFT));
	write32(reg, c);

	// Wait for PLL lock
	while (!(read32(reg) & PRCI_PLLCFG_LOCK_MASK))
		;
}

/*
 * Section 7.1 recommends a frequency of 1.0 GHz (up to 1.5 GHz is possible)
 * Section 7.4.2 provides the necessary values
 *
 * COREPLL is set up for ~1 GHz output frequency.
 * divr = 0 (x1), divf = 76 (x154) => (4004 MHz VCO), divq = 2 (/4 Output divider)
 */
static const struct pll_settings corepll_settings = {
	.divr = 0,
	.divf = 76,
	.divq = 2,
	.range = 4,
	.bypass = 0,
	.fsebypass = 1, // external feedback mode is not supported
};

/*
 * Section 7.4.3: DDR and Ethernet Subsystem Clocking and Reset
 *
 * DDRPLL is set up for 933 MHz output frequency.
 * divr = 0 (x1), divf = 71 (x144) => (3744 MHz VCO), divq = 2 (/4 output divider)
 */
static const struct pll_settings ddrpll_settings = {
	.divr = 0,
	.divf = 71,
	.divq = 2,
	.range = 4,
	.bypass = 0,
	.fsebypass = 1, // external feedback mode is not supported
};

/*
 * GEMGXLPLL is set up for 125 MHz output frequency.
 * divr = 0 (x1), divf = 76 (x154) => (4004 MHz VCO), divq = 5 (/32 output divider)
 */
static const struct pll_settings gemgxlpll_settings = {
	.divr = 0,
	.divf = 76,
	.divq = 5,
	.range = 4,
	.bypass = 0,
	.fsebypass = 1, // external feedback mode is not supported
};

/*
 * HFPCLKPLL is set up for 520 MHz output frequency.
 * TODO a lower value should also suffice as well as safe some power
 * divr = 1 (/2), divf = 39 (x80) => (2080 MHz VCO), divq = 2 (/4 output divider)
 */
static const struct pll_settings hfpclkpll_settings = {
	.divr = 1,
	//.divf = 122,
	.divf = 39,
	.divq = 2,
	.range = 4,
	.bypass = 0,
	.fsebypass = 1, // external feedback mode is not supported
};

/*
 * CLTXCLKPLL is set up for 520 MHz output frequency.
 * divr = 1 (/2), divf = 122 (x154) => (4004 MHz VCO), divq = 2 (/4 output divider)
 */
static const struct pll_settings cltxpll_settings = {
	.divr = 1,
	.divf = 39,
	.divq = 2,
	.range = 4,
	.bypass = 0,
	.fsebypass = 1, // external feedback mode is not supported
};

static void init_coreclk(void)
{
	// we can't modify the coreclk PLL while we are running on it, so let coreclk devise
	// its clock from hfclk before modifying PLL
	clrsetbits32(&prci->core_clk_sel_reg, PRCI_CORECLKSEL_MASK, PRCI_CORECLKSEL_HFCLK);

	// only configure pll if it is present
	if (!(read32(&prci->prci_plls) & PRCI_PLLS_COREPLL)) {
		return;
	}

	configure_pll(&prci->core_pllcfg, &corepll_settings);

	// switch coreclk multiplexer to use corepll as clock source again
	clrsetbits32(&prci->core_clk_sel_reg, PRCI_CORECLKSEL_MASK, PRCI_CORECLKSEL_CORECLKPLL);
}

static void init_ddrclk(void)
{
	// only configure pll if it is present
	if (!(read32(&prci->prci_plls) & PRCI_PLLS_DDRPLL)) {
		return;
	}

	// disable ddr clock output before reconfiguring the PLL
	u32 cfg1 = read32(&prci->ddr_plloutdiv);
	clrbits32(&cfg1, PRCI_DDR_PLLOUTDIV_MASK);
	write32(&prci->ddr_plloutdiv, cfg1);

	configure_pll(&prci->ddr_pllcfg, &ddrpll_settings);

	// PLL is ready/locked so enable it (its gated)
	setbits32(&cfg1, PRCI_DDR_PLLOUTDIV_MASK);
	write32(&prci->ddr_plloutdiv, cfg1);
}

static void init_gemgxlclk(void)
{
	// only configure pll if it is present
	if (!(read32(&prci->prci_plls) & PRCI_PLLS_GEMGXLPLL)) {
		return;
	}

	// disable gemgxl clock output before reconfiguring the PLL
	u32 cfg1 = read32(&prci->gemgxl_plloutdiv);
	clrbits32(&cfg1, PRCI_GEMGXL_PLLOUTDIV_MASK);
	write32(&prci->gemgxl_plloutdiv, cfg1);

	configure_pll(&prci->gemgxl_pllcfg, &gemgxlpll_settings);

	// PLL is ready/locked so enable it (its gated)
	setbits32(&cfg1, PRCI_GEMGXL_PLLOUTDIV_MASK);
	write32(&prci->gemgxl_plloutdiv, cfg1);
}

/*
 * Configure High Frequency peripheral clock which is used by
 * UART, SPI, GPIO, I2C and PWM subsystem
 */
static void init_hfpclk(void)
{
	// we can't modify the hfpclk PLL while we are running on it, so let pclk devise
	// its clock from hfclk before modifying PLL
	u32 hfpclksel = read32(&prci->hfpclkpllsel);
	hfpclksel |= PRCI_HFPCLKSEL_HFCLK;
	write32(&prci->hfpclkpllsel, hfpclksel);

	configure_pll(&prci->hfpclk_pllcfg, &hfpclkpll_settings);

	// PLL is ready/locked so enable it (its gated)
	u32 hfpclk_plloutdiv = read32(&prci->hfpclk_plloutdiv);
	hfpclk_plloutdiv |=  PRCI_HFPCLK_PLLOUTDIV_MASK;
	write32(&prci->hfpclk_plloutdiv, hfpclk_plloutdiv);

	mdelay(1);

	// switch to using PLL for hfpclk
	clrbits32(&prci->hfpclkpllsel, PRCI_HFPCLKSEL_MASK);

	udelay(70);
}

static void reset_deassert(u8 reset_index)
{
	u32 device_reset = read32(&prci->devices_reset_n);
	device_reset |= reset_index;
	write32(&prci->devices_reset_n, device_reset);
}

static void init_cltx(void)
{
	// disable hfpclkpll before configuring it
	u32 cfg1 = read32(&prci->cltx_plloutdiv);
	clrbits32(&cfg1, PRCI_CLTX_PLLOUTDIV_MASK);
	write32(&prci->cltx_plloutdiv, cfg1);

	configure_pll(&prci->cltx_pllcfg, &cltxpll_settings);

	// PLL is ready/locked so enable it (its gated)
	setbits32(&cfg1, PRCI_CLTX_PLLOUTDIV_MASK);
	write32(&prci->cltx_plloutdiv, cfg1);

	// get chiplink out of reset
	reset_deassert(PRCI_DEVICES_RESET_CLTX_RST);

	udelay(70);
}

void clock_init(void)
{
	// first configure the coreclk (used by HARTs) to get maximum speed early on
	init_coreclk();

	// put all devices in reset (e.g. DDR, ethernet, pcie) before configuring their clocks
	write32(&prci->devices_reset_n, 0);

	// initialize clock used by DDR subsystem
	init_ddrclk();

	// get DDR controller out of reset
	reset_deassert(PRCI_DEVICES_RESET_DDR_CTRL_RST);

	// wait at least one full DDR controller clock cycle
	asm volatile ("fence");

	// get DDR controller (register interface) out of reset
	// get DDR subsystem PHY out of reset
	reset_deassert(PRCI_DEVICES_RESET_DDR_AXI_RST |
	               PRCI_DEVICES_RESET_DDR_AHB_RST |
	               PRCI_DEVICES_RESET_DDR_PHY_RST);

	// we need to wait 256 full ddrctrl clock cycles until we can interact with the DDR subsystem
	for (int i = 0; i < 256; i++)
		asm volatile ("nop");

	if (read32(&prci->prci_plls) & PRCI_PLLS_HFPCLKPLL) {
		// set hfclk as reference for peripheral clock since we don't have the PLL
		//clrsetbits32(&prci->hfpclkpllsel, PRCI_HFPCLKSEL_MASK, PRCI_HFPCLKSEL_HFCLK);
		init_hfpclk();
	} else if (read32(&prci->prci_plls) & PRCI_PLLS_CLTXPLL) {
		// Note: this path has never been tested since the platforms tested with
		// always have HFPCLKPLL
		init_cltx();
		// get chiplink out of reset
		reset_deassert(PRCI_DEVICES_RESET_CLTX_RST);
	}

	// GEMGXL init VSC8541 PHY reset sequence;
	gpio_set_direction(GEMGXL_RST, GPIO_OUTPUT);
	gpio_set(GEMGXL_RST, 1);

	udelay(1);

	/* Reset PHY again to enter unmanaged mode */
	gpio_set(GEMGXL_RST, 0);
	udelay(1);
	gpio_set(GEMGXL_RST, 1);
	mdelay(15);

	init_gemgxlclk();

	// get ethernet out of reset
	reset_deassert(PRCI_DEVICES_RESET_GEMGXL_RST);
}

// get the peripheral clock frequency used by UART (probably also SPI, GPIO, I2C and PWM)
int clock_get_pclk(void)
{
	u64 pclk = FU740_HFCLK_FREQ;

	// check if hfpclkpll is present and
	// check if hfpclkpll is selected in the multiplexer TODO
	// check if hpfclkpll is enabled
	if ((read32(&prci->prci_plls) & PRCI_PLLS_HFPCLKPLL) &&
	    (read32(&prci->hfpclk_plloutdiv) & PRCI_HFPCLK_PLLOUTDIV_MASK)) {
		int hfpclk_pllcfg = read32(&prci->hfpclk_pllcfg);
		int divr = (hfpclk_pllcfg & PRCI_PLLCFG_DIVR_MASK) >> PRCI_PLLCFG_DIVR_SHIFT;
		int divf = (hfpclk_pllcfg & PRCI_PLLCFG_DIVF_MASK) >> PRCI_PLLCFG_DIVF_SHIFT;
		int divq = (hfpclk_pllcfg & PRCI_PLLCFG_DIVQ_MASK) >> PRCI_PLLCFG_DIVQ_SHIFT;
		pclk /= (divr + 1); // reference divider
		pclk *= (2 * (divf + 1)); // feedback divider
		pclk /= (1 << divq); // output divider
	}

	// divider value before pclk seems to be (hfpclkdiv + 2). Not mentioned in fu740 manual though.
	return pclk / (read32(&prci->hfpclk_div_reg) + 2);
}