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

#include <boot/coreboot_tables.h>
#include <console/console.h>
#include <delay.h>
#include <device/device.h>
#include <device/mmio.h>
#include <device/pci.h>
#include <device/pci_ids.h>
#include <gpio.h>
#include <soc/clock.h>
#include <soc/gpio.h>
#include <soc/qcom_qmp_phy.h>
#include <soc/pcie.h>
#include <timer.h>

#define ROOT_PORT_BDF		0x0
#define ATU_CTRL2		PCIE_ATU_UNR_REGION_CTRL2

static struct qcom_pcie_cntlr_t qcom_pcie_cfg;

static inline void dw_pcie_dbi_rd_wr(bool enable)
{
	uint32_t val;
	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;

	val = read32(pcierc->dbi_base + PCIE_DBI_MISC_CONTROL_1_OFF);

	if (enable)
		val |= PCIE_DBI_RO_WR_EN;
	else
		val &= ~PCIE_DBI_RO_WR_EN;

	write32(pcierc->dbi_base + PCIE_DBI_MISC_CONTROL_1_OFF, val);
}

static void dw_pcie_writel_iatu(unsigned int index, uint32_t reg, uint32_t val)
{
	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;
	unsigned int offset = PCIE_GET_ATU_OUTB_UNR_REG_OFFSET(index);

	write32(pcierc->atu_base + offset + reg, val);
}

static uint32_t dw_pcie_readl_iatu(unsigned int index, uint32_t reg)
{
	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;
	unsigned int offset = PCIE_GET_ATU_OUTB_UNR_REG_OFFSET(index);

	return read32(pcierc->atu_base + offset + reg);
}

static void qcom_dw_pcie_prog_outbound_atu(unsigned int index,
					   unsigned int type, uint64_t cpu_addr,
					   uint64_t pcie_addr, uint32_t size)
{
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_LOWER_BASE, lower_32_bits(cpu_addr));
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_UPPER_BASE, upper_32_bits(cpu_addr));
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_LIMIT, lower_32_bits(cpu_addr + size - 1));
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_LOWER_TARGET, lower_32_bits(pcie_addr));
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_UPPER_TARGET, upper_32_bits(pcie_addr));
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_REGION_CTRL1, type);
	dw_pcie_writel_iatu(index, PCIE_ATU_UNR_REGION_CTRL2, PCIE_ATU_ENABLE);

	/* Ensure ATU enable takes effect before any subsequent config access */
	if (retry(LINK_WAIT_MAX_IATU_RETRIES,
		  (dw_pcie_readl_iatu(index, ATU_CTRL2) & PCIE_ATU_ENABLE),
		  udelay(LINK_WAIT_IATU)))
		return;

	printk(BIOS_ERR, "PCIe o/b iATU couldn't be enabled after 5ms\n");
}

static void qcom_pcie_configure_gpios(pcie_cntlr_cfg_t *cfg)
{
	gpio_configure(cfg->perst, 0, GPIO_NO_PULL, GPIO_16MA, GPIO_OUTPUT);
}

/**
 * qcom_dw_pcie_configure() - Configure link capabilities and speed
 *
 * Configure the link capabilities and speed in the PCIe root complex.
 */
static void qcom_dw_pcie_configure(uint32_t cap_speed)
{
	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;

	dw_pcie_dbi_rd_wr(true);

	clrsetbits32(pcierc->dbi_base + PCIE_LINK_CAPABILITY,
		     TARGET_LINK_SPEED_MASK, cap_speed);

	clrsetbits32(pcierc->dbi_base + PCIE_LINK_CTL_2,
		     TARGET_LINK_SPEED_MASK, cap_speed);

	dw_pcie_dbi_rd_wr(false);
	printk(BIOS_INFO, "PCIe Link speed configured in Gen %d\n", cap_speed);
}

/**
 * is_pcie_link_up() - Return the link state
 *
 * Return: true for active link and false for no link
 */
static bool is_pcie_link_up(struct qcom_pcie_cntlr_t *pci)
{
	/* Read link status register */
	return !!(read32(pci->cntlr_cfg->elbi + PCIE3X2_ELBI_SYS_STTS) & XMLH_LINK_UP);
}

/**
 * wait_link_up() - Wait for the link to come up
 *
 * Return: true for active line and false for no link (timeout)
 */
static bool wait_link_up(struct qcom_pcie_cntlr_t *pci)
{
	/* Check if the link is up or not */
	if (retry(LINK_WAIT_MAX_RETRIES, is_pcie_link_up(pci), mdelay(PCIE_LINK_UP_MS)))
		return true;

	printk(BIOS_ERR, "PCIe link is not up even after 1sec\n");
	return false;
}

static enum cb_err qcom_pcie_dw_link_up(struct qcom_pcie_cntlr_t *pcie)
{
	if (is_pcie_link_up(pcie)) {
		printk(BIOS_INFO, "PCIe Link is already up\n");
		return CB_SUCCESS;
	}

	/* DW pre link configurations */
	qcom_dw_pcie_configure(LINK_SPEED_GEN_2);

	/* enable link training */
	setbits32(pcie->cntlr_cfg->parf + PCIE_PARF_LTSSM, LTSSM_EN);

	/* Check that link was established */
	if (wait_link_up(pcie)) {
		printk(BIOS_INFO, "PCIe link is up\n");
		return CB_SUCCESS;
	}

	/*
	 * Link can be established in Gen 1 as it failed to establish in Gen2.
	 * So allow some time to do it.
	 */
	udelay(100);

	return CB_ERR;
}

/**
 * Returns root port config space address or endpoint config space address.
 * For endpoint config address, mapping would be done with ATU.
 */
static void *qcom_dw_pcie_get_config_addr(struct qcom_pcie_cntlr_t *pcierc,
					  pci_devfn_t dev)
{
	unsigned int atu_type, cfg_size;
	void *cfg_address;
	uint32_t bus, busdev, devfn;
	pcie_cntlr_cfg_t *cntlr = pcierc->cntlr_cfg;
	bus = PCI_DEV2SEGBUS(dev);
	devfn = PCI_DEV2DEVFN(dev);

	busdev = PCIE_ATU_BUS(bus) |
		 PCIE_ATU_DEV(PCI_SLOT(devfn)) |
		 PCIE_ATU_FUNC(PCI_FUNC(devfn));

	/* Accessing root port configuration space */
	if (!bus && !devfn) {
		cfg_address = pcierc->cntlr_cfg->dbi_base;
		return cfg_address;
	}

	/* For local bus use CFG0 type */
	atu_type = (bus == 1) ? PCIE_ATU_TYPE_CFG0 : PCIE_ATU_TYPE_CFG1;

	cfg_address = cntlr->cfg_base + (cntlr->cfg_size * devfn);
	cfg_size = cntlr->cfg_size;

	qcom_dw_pcie_prog_outbound_atu(PCIE_ATU_REGION_INDEX1, atu_type,
				       (uint64_t)cfg_address, busdev, cfg_size);
	return cfg_address;
}

static void qcom_dw_pcie_setup_rc(struct qcom_pcie_cntlr_t *pcie)
{
	uint32_t val;
	pcie_cntlr_cfg_t *pcierc = pcie->cntlr_cfg;
	/*
	 * Enable DBI read-only registers for writing/updating configuration.
	 * Write permission gets disabled towards the end of this function.
	 */
	dw_pcie_dbi_rd_wr(true);

	val = read32(pcierc->dbi_base + PCIE_PORT_LINK_CONTROL);
	/* Set the number of lanes */
	val &= ~PORT_LINK_MODE_MASK;

	switch (pcierc->lanes) {
	case 1:
		val |= PORT_LINK_MODE_1_LANES;
		break;
	case 2:
		val |= PORT_LINK_MODE_2_LANES;
		break;
	case 4:
		val |= PORT_LINK_MODE_4_LANES;
		break;
	case 8:
		val |= PORT_LINK_MODE_8_LANES;
		break;
	default:
		printk(BIOS_INFO, "PCIe num-lanes %u: invalid value\n",
		       pcierc->lanes);
		return;
	}

	write32(pcierc->dbi_base + PCIE_PORT_LINK_CONTROL, val);

	/* Set link width speed control register */
	val = read32(pcierc->dbi_base + PCIE_LINK_WIDTH_SPEED_CONTROL);
	val &= ~PORT_LOGIC_LINK_WIDTH_MASK;

	switch (pcierc->lanes) {
	case 1:
		val |= PORT_LOGIC_LINK_WIDTH_1_LANES;
		break;
	case 2:
		val |= PORT_LOGIC_LINK_WIDTH_2_LANES;
		break;
	case 4:
		val |= PORT_LOGIC_LINK_WIDTH_4_LANES;
		break;
	case 8:
		val |= PORT_LOGIC_LINK_WIDTH_8_LANES;
		break;
	default:
		printk(BIOS_INFO, "PCIe invalid lanes option\n");
		return;
	}

	write32(pcierc->dbi_base + PCIE_LINK_WIDTH_SPEED_CONTROL, val);

	/* Setup bus numbers */
	clrsetbits32(pcierc->dbi_base + PCI_PRIMARY_BUS, TYPE1_HDR_BUS_NUM_MASK,
		     ROOT_PORT_BUS_NUM);

	/* Disable SMMU */
	write32(pcierc->parf + PCIE_PARF_BDF_TO_SID_CFG, BDF_TO_SID_BYPASS);

	/* Configure ATU for outbound accesses */
	qcom_dw_pcie_prog_outbound_atu(PCIE_ATU_REGION_INDEX0,
				       PCIE_ATU_TYPE_MEM,
				       pcierc->mem.phys_start,
				       ROOT_PORT_BDF, pcierc->mem.size);

	/* Program correct class for RC */
	write16(pcierc->dbi_base + PCI_CLASS_DEVICE, PCI_CLASS_BRIDGE_PCI);

	/* Disable write permission right after the update */
	dw_pcie_dbi_rd_wr(false);
}

static void qcom_qmp_phy_config_lane(void *base, const struct qcom_qmp_phy_init_tbl tbl[],
				     unsigned int num, uint8_t ln_mask)
{
	unsigned int i;
	const struct qcom_qmp_phy_init_tbl *t = tbl;

	for (i = 0; i < num; i++, t++) {
		if (!(t->lane_mask & ln_mask))
			continue;
		write32(base + t->offset, t->val);
	}
}

static void qcom_qmp_phy_configure(void *base, const struct qcom_qmp_phy_init_tbl tbl[],
				   unsigned int num)
{
	qcom_qmp_phy_config_lane(base, tbl, num, 0xff);
}

static enum cb_err qcom_qmp_phy_power_on(pcie_qmp_phy_cfg_t *qphy)
{
	uint64_t lock_us;

	/* Release powerdown mode and allow endpoint refclk drive */
	write32(qphy->pcs + QPHY_PCS_PWR_DWN_CNTRL, SW_PWRDN | REFCLK_DRV_DSBL);

	/* Serdes configuration */
	qcom_qmp_phy_configure(qphy->serdes, qphy->serdes_tbl, qphy->serdes_tbl_num);

	/* Tx, Rx, and PCS configurations */
	qcom_qmp_phy_config_lane(qphy->tx0, qphy->tx_tbl, qphy->tx_tbl_num, 1);
	qcom_qmp_phy_config_lane(qphy->tx0, qphy->tx_tbl_sec, qphy->tx_tbl_num_sec, 1);
	qcom_qmp_phy_config_lane(qphy->tx1, qphy->tx_tbl, qphy->tx_tbl_num, 2);
	qcom_qmp_phy_config_lane(qphy->tx1, qphy->tx_tbl_sec, qphy->tx_tbl_num_sec, 2);
	qcom_qmp_phy_config_lane(qphy->rx0, qphy->rx_tbl, qphy->rx_tbl_num, 1);
	qcom_qmp_phy_config_lane(qphy->rx0, qphy->rx_tbl_sec, qphy->rx_tbl_num_sec, 1);
	qcom_qmp_phy_config_lane(qphy->rx1, qphy->rx_tbl, qphy->rx_tbl_num, 2);
	qcom_qmp_phy_config_lane(qphy->rx1, qphy->rx_tbl_sec, qphy->rx_tbl_num_sec, 2);
	qcom_qmp_phy_configure(qphy->pcs, qphy->pcs_tbl, qphy->pcs_tbl_num);
	qcom_qmp_phy_configure(qphy->pcs, qphy->pcs_tbl_sec, qphy->pcs_tbl_num_sec);
	qcom_qmp_phy_configure(qphy->pcs_misc, qphy->pcs_misc_tbl, qphy->pcs_misc_tbl_num);
	qcom_qmp_phy_configure(qphy->pcs_misc, qphy->pcs_misc_tbl_sec, qphy->pcs_tbl_num_sec);

	/* Release software reset of PCS/Serdes */
	clrbits32(qphy->pcs + QPHY_SW_RESET, SW_RESET);

	/* start PCS/Serdes to operation mode */
	write32(qphy->pcs  + QPHY_START_CTRL, PCS_START | SERDES_START);

	/*
	 * Wait for PHY initialization to be done
	 * PCS_STATUS: wait for 1ms for PHY STATUS bit to be set
	 */
	lock_us = wait_us(1000, !(read32(qphy->qmp_phy_base + QPHY_PCS_STATUS) & PHY_STATUS));
	if (!lock_us) {
		printk(BIOS_ERR, "PCIe QMP PHY PLL failed to lock in 1ms\n");
		return CB_ERR;
	}

	printk(BIOS_DEBUG, "PCIe QPHY Initialized took %lldus\n", lock_us);

	qcom_dw_pcie_enable_pipe_clock();

	return CB_SUCCESS;
}

/*
 * Reset the PCIe PHY core.
 * Allow 100us delay to ensure reset is asserted.
 */
static void qcom_qmp_reset_phy(struct qcom_pcie_cntlr_t *pcie)
{
	clock_reset_bcr(pcie->cntlr_cfg->qmp_phy_bcr, 1);
	udelay(100);
	clock_reset_bcr(pcie->cntlr_cfg->qmp_phy_bcr, 0);
}

/*
 * Reset the PCIe controller core.
 * Allow 100us delay to ensure reset is asserted.
 */
static void qcom_dw_pcie_reset_core(struct qcom_pcie_cntlr_t *pcie)
{
	clock_reset_bcr(pcie->cntlr_cfg->pcie_bcr, 1);
	udelay(100);
	clock_reset_bcr(pcie->cntlr_cfg->pcie_bcr, 0);
}

static enum cb_err qcom_dw_pcie_enable(struct qcom_pcie_cntlr_t *pcie)
{
	int ret;

	pcie_cntlr_cfg_t *pcierc = pcie->cntlr_cfg;
	pcie_qmp_phy_cfg_t *qmp_phy = pcie->qmp_phy_cfg;

	/* assert PCIe reset link to keep EP in reset */
	gpio_set(pcierc->perst, 0);

	/* Enable PCIe controller and SoC specific clocks */
	ret = qcom_dw_pcie_enable_clock();
	if (ret) {
		printk(BIOS_ERR, "Enable PCIe clocks failed\n");
		return ret;
	}

	/* Reset the controller */
	qcom_dw_pcie_reset_core(pcie);

	/* configure PCIe to RC mode */
	write32(pcierc->parf + PCIE_PARF_DEVICE_TYPE, DEVICE_TYPE_RC);

	/* Power on the PHY */
	clrbits32(pcierc->parf + PCIE_PARF_PHY_CTRL, PHY_PWR_DOWN);

	/* MAC PHY_POWERDOWN MUX DISABLE */
	clrbits32(pcierc->parf + PCIE_PARF_SYS_CTRL, MAC_PHY_PWRDOWN_MUX_EN);

	/* Bypass MHI as its not needed */
	setbits32(pcierc->parf + PCIE_PARF_MHI_CLOCK_RESET_CTRL, MHI_BYPASS);

	qcom_qmp_reset_phy(pcie);

	/* Initialize QMP PHY */
	ret = qcom_qmp_phy_power_on(qmp_phy);
	if (ret) {
		printk(BIOS_ERR, "PCIe PHY init failed\n");
		return ret;
	}

	qcom_dw_pcie_setup_rc(pcie);

	/* de-assert PCIe reset link to bring EP out of reset */
	gpio_set(pcierc->perst, 1);

	/* establisth the link */
	ret = qcom_pcie_dw_link_up(pcie);
	if (ret) {
		printk(BIOS_ERR, "PCIe Link init failed\n");
		return ret;
	}

	return CB_SUCCESS;
}

/**
 * Fill coreboot table with PCIe info.
 * It allows exporting this info to payloads.
 */
enum cb_err fill_lb_pcie(struct lb_pcie *pcie)
{
	if (!mainboard_needs_pcie_init())
		return CB_ERR_NOT_IMPLEMENTED;

	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;
	pcie->ctrl_base = (uintptr_t)pcierc->dbi_base;
	return CB_SUCCESS;
}

/* map_bus function for mapping pcie_s_{read/write}_configXX() functions */
volatile union pci_bank *pci_map_bus(pci_devfn_t dev)
{
	void *config_addr = NULL;

	config_addr = qcom_dw_pcie_get_config_addr(&qcom_pcie_cfg, dev);
	return (void *)config_addr;
}

/* PCI domain ops read_resources callback */
void qcom_pci_domain_read_resources(struct device *dev)
{
	struct resource *res;
	pcie_cntlr_cfg_t *pcierc = qcom_pcie_cfg.cntlr_cfg;

	/* Initialize the system-wide I/O space constraints. */
	res = new_resource(dev, IOINDEX_SUBTRACTIVE(0, 0));
	res->base = pcierc->io.phys_start;
	res->limit = pcierc->io.size - 1;
	res->flags = IORESOURCE_IO | IORESOURCE_SUBTRACTIVE | IORESOURCE_ASSIGNED;

	/* Initialize the system-wide memory resources constraints. */
	res = new_resource(dev, IOINDEX_SUBTRACTIVE(1, 0));
	res->base = pcierc->mem.phys_start;
	res->limit = pcierc->mem.size - 1;
	res->flags = IORESOURCE_MEM | IORESOURCE_SUBTRACTIVE | IORESOURCE_ASSIGNED;
}

/* PCI domain ops enable callback */
void qcom_setup_pcie_host(struct device *dev)
{
	gcom_pcie_get_config(&qcom_pcie_cfg);

	/* Ensure PCIe endpoints are powered-on before initiating PCIe link training */
	gcom_pcie_power_on_ep();

	printk(BIOS_INFO, "Setup PCIe in RC mode\n");

	/* Configure PERST gpio */
	qcom_pcie_configure_gpios(qcom_pcie_cfg.cntlr_cfg);

	if (!qcom_dw_pcie_enable(&qcom_pcie_cfg))
		printk(BIOS_NOTICE, "PCIe enumerated succussfully..\n");
	else
		printk(BIOS_EMERG, "Failed to enable PCIe\n");

}