/*
 * Copyright (C) 2016 Cavium, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 */

#include <linux/acpi.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/pci.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/phy.h>
#include <linux/of.h>
#include <linux/of_mdio.h>
#include <linux/of_net.h>

#include "nic.h"
#include "thunder_bgx.h"

#define DRV_NAME	"thunder-xcv"
#define DRV_VERSION	"1.0"

/* Register offsets */
#define XCV_RESET		0x00
#define   PORT_EN		BIT_ULL(63)
#define   CLK_RESET		BIT_ULL(15)
#define   DLL_RESET		BIT_ULL(11)
#define   COMP_EN		BIT_ULL(7)
#define   TX_PKT_RESET		BIT_ULL(3)
#define   TX_DATA_RESET		BIT_ULL(2)
#define   RX_PKT_RESET		BIT_ULL(1)
#define   RX_DATA_RESET		BIT_ULL(0)
#define XCV_DLL_CTL		0x10
#define   CLKRX_BYP		BIT_ULL(23)
#define   CLKTX_BYP		BIT_ULL(15)
#define XCV_COMP_CTL		0x20
#define   DRV_BYP		BIT_ULL(63)
#define XCV_CTL			0x30
#define XCV_INT			0x40
#define XCV_INT_W1S		0x48
#define XCV_INT_ENA_W1C		0x50
#define XCV_INT_ENA_W1S		0x58
#define XCV_INBND_STATUS	0x80
#define XCV_BATCH_CRD_RET	0x100

struct xcv {
	void __iomem		*reg_base;
	struct pci_dev		*pdev;
};

static struct xcv *xcv;

/* Supported devices */
static const struct pci_device_id xcv_id_table[] = {
	{ PCI_DEVICE(PCI_VENDOR_ID_CAVIUM, 0xA056) },
	{ 0, }  /* end of table */
};

MODULE_AUTHOR("Cavium Inc");
MODULE_DESCRIPTION("Cavium Thunder RGX/XCV Driver");
MODULE_LICENSE("GPL v2");
MODULE_VERSION(DRV_VERSION);
MODULE_DEVICE_TABLE(pci, xcv_id_table);

void xcv_init_hw(void)
{
	u64  cfg;

	/* Take DLL out of reset */
	cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
	cfg &= ~DLL_RESET;
	writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);

	/* Take clock tree out of reset */
	cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
	cfg &= ~CLK_RESET;
	writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);
	/* Wait for DLL to lock */
	msleep(1);

	/* Configure DLL - enable or bypass
	 * TX no bypass, RX bypass
	 */
	cfg = readq_relaxed(xcv->reg_base + XCV_DLL_CTL);
	cfg &= ~0xFF03;
	cfg |= CLKRX_BYP;
	writeq_relaxed(cfg, xcv->reg_base + XCV_DLL_CTL);

	/* Enable compensation controller and force the
	 * write to be visible to HW by readig back.
	 */
	cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
	cfg |= COMP_EN;
	writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);
	readq_relaxed(xcv->reg_base + XCV_RESET);
	/* Wait for compensation state machine to lock */
	msleep(10);

	/* enable the XCV block */
	cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
	cfg |= PORT_EN;
	writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);

	cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
	cfg |= CLK_RESET;
	writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);
}
EXPORT_SYMBOL(xcv_init_hw);

void xcv_setup_link(bool link_up, int link_speed)
{
	u64  cfg;
	int speed = 2;

	if (!xcv) {
		pr_err("XCV init not done, probe may have failed\n");
		return;
	}

	if (link_speed == 100)
		speed = 1;
	else if (link_speed == 10)
		speed = 0;

	if (link_up) {
		/* set operating speed */
		cfg = readq_relaxed(xcv->reg_base + XCV_CTL);
		cfg &= ~0x03;
		cfg |= speed;
		writeq_relaxed(cfg, xcv->reg_base + XCV_CTL);

		/* Reset datapaths */
		cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
		cfg |= TX_DATA_RESET | RX_DATA_RESET;
		writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);

		/* Enable the packet flow */
		cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
		cfg |= TX_PKT_RESET | RX_PKT_RESET;
		writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);

		/* Return credits to RGX */
		writeq_relaxed(0x01, xcv->reg_base + XCV_BATCH_CRD_RET);
	} else {
		/* Disable packet flow */
		cfg = readq_relaxed(xcv->reg_base + XCV_RESET);
		cfg &= ~(TX_PKT_RESET | RX_PKT_RESET);
		writeq_relaxed(cfg, xcv->reg_base + XCV_RESET);
		readq_relaxed(xcv->reg_base + XCV_RESET);
	}
}
EXPORT_SYMBOL(xcv_setup_link);

static int xcv_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	int err;
	struct device *dev = &pdev->dev;

	xcv = devm_kzalloc(dev, sizeof(struct xcv), GFP_KERNEL);
	if (!xcv)
		return -ENOMEM;
	xcv->pdev = pdev;

	pci_set_drvdata(pdev, xcv);

	err = pci_enable_device(pdev);
	if (err) {
		dev_err(dev, "Failed to enable PCI device\n");
		goto err_kfree;
	}

	err = pci_request_regions(pdev, DRV_NAME);
	if (err) {
		dev_err(dev, "PCI request regions failed 0x%x\n", err);
		goto err_disable_device;
	}

	/* MAP configuration registers */
	xcv->reg_base = pcim_iomap(pdev, PCI_CFG_REG_BAR_NUM, 0);
	if (!xcv->reg_base) {
		dev_err(dev, "XCV: Cannot map CSR memory space, aborting\n");
		err = -ENOMEM;
		goto err_release_regions;
	}

	return 0;

err_release_regions:
	pci_release_regions(pdev);
err_disable_device:
	pci_disable_device(pdev);
err_kfree:
	devm_kfree(dev, xcv);
	xcv = NULL;
	return err;
}

static void xcv_remove(struct pci_dev *pdev)
{
	struct device *dev = &pdev->dev;

	if (xcv) {
		devm_kfree(dev, xcv);
		xcv = NULL;
	}

	pci_release_regions(pdev);
	pci_disable_device(pdev);
}

static struct pci_driver xcv_driver = {
	.name = DRV_NAME,
	.id_table = xcv_id_table,
	.probe = xcv_probe,
	.remove = xcv_remove,
};

static int __init xcv_init_module(void)
{
	pr_info("%s, ver %s\n", DRV_NAME, DRV_VERSION);

	return pci_register_driver(&xcv_driver);
}

static void __exit xcv_cleanup_module(void)
{
	pci_unregister_driver(&xcv_driver);
}

module_init(xcv_init_module);
module_exit(xcv_cleanup_module);