// SPDX-License-Identifier: GPL-2.0+
/*
 * spi-synquacer.c - Socionext Synquacer SPI driver
 * Copyright 2021 Linaro Ltd.
 * Copyright 2021 Socionext, Inc.
 */

#include <clk.h>
#include <dm.h>
#include <log.h>
#include <time.h>
#include <dm/device_compat.h>
#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <spi.h>
#include <wait_bit.h>

#define MCTRL	0x0
#define MEN	0
#define CSEN	1
#define IPCLK	3
#define MES	4
#define SYNCON	5

#define PCC0		0x4
#define PCC(n)		(PCC0 + (n) * 4)
#define RTM		3
#define ACES		2
#define SAFESYNC	16
#define CPHA		0
#define CPOL		1
#define SSPOL		4
#define SDIR		7
#define SS2CD		5
#define SENDIAN		8
#define CDRS_SHIFT	9
#define CDRS_MASK	0x7f

#define TXF		0x14
#define TXE		0x18
#define TXC		0x1c
#define RXF		0x20
#define RXE		0x24
#define RXC		0x28
#define TFES		1
#define TFLETE		4
#define TSSRS		6
#define RFMTE		5
#define RSSRS		6

#define FAULTF		0x2c
#define FAULTC		0x30

#define DMCFG		0x34
#define SSDC		1
#define MSTARTEN	2

#define DMSTART		0x38
#define TRIGGER		0
#define DMSTOP		8
#define CS_MASK		3
#define CS_SHIFT	16
#define DATA_TXRX	0
#define DATA_RX		1
#define DATA_TX		2
#define DATA_MASK	3
#define DATA_SHIFT	26
#define BUS_WIDTH	24

#define DMBCC		0x3c
#define DMSTATUS	0x40
#define RX_DATA_MASK	0x1f
#define RX_DATA_SHIFT	8
#define TX_DATA_MASK	0x1f
#define TX_DATA_SHIFT	16

#define TXBITCNT	0x44

#define FIFOCFG		0x4c
#define BPW_MASK	0x3
#define BPW_SHIFT	8
#define RX_FLUSH	11
#define TX_FLUSH	12
#define RX_TRSHLD_MASK		0xf
#define RX_TRSHLD_SHIFT		0
#define TX_TRSHLD_MASK		0xf
#define TX_TRSHLD_SHIFT		4

#define TXFIFO		0x50
#define RXFIFO		0x90
#define MID		0xfc

#define FIFO_DEPTH	16
#define TX_TRSHLD	4
#define RX_TRSHLD	(FIFO_DEPTH - TX_TRSHLD)

#define TXBIT	1
#define RXBIT	2

DECLARE_GLOBAL_DATA_PTR;

struct synquacer_spi_plat {
	void __iomem *base;
	bool aces, rtm;
};

struct synquacer_spi_priv {
	void __iomem *base;
	bool aces, rtm;
	int speed, cs, mode, rwflag;
	void *rx_buf;
	const void *tx_buf;
	unsigned int tx_words, rx_words;
};

static void read_fifo(struct synquacer_spi_priv *priv)
{
	u32 len = readl(priv->base + DMSTATUS);
	u8 *buf = priv->rx_buf;
	int i;

	len = (len >> RX_DATA_SHIFT) & RX_DATA_MASK;
	len = min_t(unsigned int, len, priv->rx_words);

	for (i = 0; i < len; i++)
		*buf++ = readb(priv->base + RXFIFO);

	priv->rx_buf = buf;
	priv->rx_words -= len;
}

static void write_fifo(struct synquacer_spi_priv *priv)
{
	u32 len = readl(priv->base + DMSTATUS);
	const u8 *buf = priv->tx_buf;
	int i;

	len = (len >> TX_DATA_SHIFT) & TX_DATA_MASK;
	len = min_t(unsigned int, FIFO_DEPTH - len, priv->tx_words);

	for (i = 0; i < len; i++)
		writeb(*buf++, priv->base + TXFIFO);

	priv->tx_buf = buf;
	priv->tx_words -= len;
}

static void synquacer_cs_set(struct synquacer_spi_priv *priv, bool active)
{
	u32 val;

	val = readl(priv->base + DMSTART);
	val &= ~(CS_MASK << CS_SHIFT);
	val |= priv->cs << CS_SHIFT;

	if (active) {
		writel(val, priv->base + DMSTART);

		val = readl(priv->base + DMSTART);
		val &= ~BIT(DMSTOP);
		writel(val, priv->base + DMSTART);
	} else {
		val |= BIT(DMSTOP);
		writel(val, priv->base + DMSTART);

		if (priv->rx_buf) {
			u32 buf[16];

			priv->rx_buf = buf;
			priv->rx_words = 16;
			read_fifo(priv);
		}

		/* wait until slave is deselected */
		while (!(readl(priv->base + TXF) & BIT(TSSRS)) ||
		       !(readl(priv->base + RXF) & BIT(RSSRS)))
			;
	}
}

static void synquacer_spi_config(struct udevice *dev, void *rx, const void *tx)
{
	struct udevice *bus = dev->parent;
	struct synquacer_spi_priv *priv = dev_get_priv(bus);
	struct dm_spi_slave_plat *slave_plat = dev_get_parent_plat(dev);
	u32 val, div, bus_width;
	int rwflag;

	rwflag = (rx ? 1 : 0) | (tx ? 2 : 0);

	/* if nothing to do */
	if (slave_plat->mode == priv->mode &&
	    rwflag == priv->rwflag &&
	    slave_plat->cs[0] == priv->cs &&
	    slave_plat->max_hz == priv->speed)
		return;

	priv->rwflag = rwflag;
	priv->cs = slave_plat->cs[0];
	priv->mode = slave_plat->mode;
	priv->speed = slave_plat->max_hz;

	if (priv->mode & SPI_TX_DUAL)
		bus_width = 2;
	else if (priv->mode & SPI_TX_QUAD)
		bus_width = 4;
	else if (priv->mode & SPI_TX_OCTAL)
		bus_width = 8;
	else
		bus_width = 1; /* default is single bit mode */

	div = DIV_ROUND_UP(125000000, priv->speed);

	val = readl(priv->base + PCC(priv->cs));
	val &= ~BIT(RTM);
	val &= ~BIT(ACES);
	val &= ~BIT(SAFESYNC);
	if ((priv->mode & (SPI_TX_DUAL | SPI_RX_DUAL)) && div < 3)
		val |= BIT(SAFESYNC);
	if ((priv->mode & (SPI_TX_QUAD | SPI_RX_QUAD)) && div < 6)
		val |= BIT(SAFESYNC);

	if (priv->mode & SPI_CPHA)
		val |= BIT(CPHA);
	else
		val &= ~BIT(CPHA);

	if (priv->mode & SPI_CPOL)
		val |= BIT(CPOL);
	else
		val &= ~BIT(CPOL);

	if (priv->mode & SPI_CS_HIGH)
		val |= BIT(SSPOL);
	else
		val &= ~BIT(SSPOL);

	if (priv->mode & SPI_LSB_FIRST)
		val |= BIT(SDIR);
	else
		val &= ~BIT(SDIR);

	if (priv->aces)
		val |= BIT(ACES);

	if (priv->rtm)
		val |= BIT(RTM);

	val |= (3 << SS2CD);
	val |= BIT(SENDIAN);

	val &= ~(CDRS_MASK << CDRS_SHIFT);
	val |= ((div >> 1) << CDRS_SHIFT);

	writel(val, priv->base + PCC(priv->cs));

	val = readl(priv->base + FIFOCFG);
	val &= ~(BPW_MASK << BPW_SHIFT);
	val |= (0 << BPW_SHIFT);
	writel(val, priv->base + FIFOCFG);

	val = readl(priv->base + DMSTART);
	val &= ~(DATA_MASK << DATA_SHIFT);

	if (tx && rx)
		val |= (DATA_TXRX << DATA_SHIFT);
	else if (rx)
		val |= (DATA_RX << DATA_SHIFT);
	else
		val |= (DATA_TX << DATA_SHIFT);

	val &= ~(3 << BUS_WIDTH);
	val |= ((bus_width >> 1) << BUS_WIDTH);
	writel(val, priv->base + DMSTART);
}

static int synquacer_spi_xfer(struct udevice *dev, unsigned int bitlen,
			      const void *tx_buf, void *rx_buf,
			      unsigned long flags)
{
	struct udevice *bus = dev->parent;
	struct synquacer_spi_priv *priv = dev_get_priv(bus);
	u32 val, words, busy = 0;

	val = readl(priv->base + FIFOCFG);
	val |= (1 << RX_FLUSH);
	val |= (1 << TX_FLUSH);
	writel(val, priv->base + FIFOCFG);

	synquacer_spi_config(dev, rx_buf, tx_buf);

	priv->tx_buf = tx_buf;
	priv->rx_buf = rx_buf;

	words = bitlen / 8;

	if (tx_buf) {
		busy |= BIT(TXBIT);
		priv->tx_words = words;
	} else {
		busy &= ~BIT(TXBIT);
		priv->tx_words = 0;
	}

	if (rx_buf) {
		busy |= BIT(RXBIT);
		priv->rx_words = words;
	} else {
		busy &= ~BIT(RXBIT);
		priv->rx_words = 0;
	}

	if (flags & SPI_XFER_BEGIN)
		synquacer_cs_set(priv, true);

	if (tx_buf)
		write_fifo(priv);

	if (rx_buf) {
		val = readl(priv->base + FIFOCFG);
		val &= ~(RX_TRSHLD_MASK << RX_TRSHLD_SHIFT);
		val |= ((priv->rx_words > FIFO_DEPTH ?
			RX_TRSHLD : priv->rx_words) << RX_TRSHLD_SHIFT);
		writel(val, priv->base + FIFOCFG);
	}

	writel(~0, priv->base + TXC);
	writel(~0, priv->base + RXC);

	/* Trigger */
	if (flags & SPI_XFER_BEGIN) {
		val = readl(priv->base + DMSTART);
		val |= BIT(TRIGGER);
		writel(val, priv->base + DMSTART);
	}

	while (busy & (BIT(RXBIT) | BIT(TXBIT))) {
		if (priv->rx_words)
			read_fifo(priv);
		else
			busy &= ~BIT(RXBIT);

		if (priv->tx_words) {
			write_fifo(priv);
		} else {
			/* wait for shifter to empty out */
			while (!(readl(priv->base + TXF) & BIT(TFES)))
				cpu_relax();

			busy &= ~BIT(TXBIT);
		}
	}

	if (flags & SPI_XFER_END)
		synquacer_cs_set(priv, false);

	return 0;
}

static int synquacer_spi_set_speed(struct udevice *bus, uint speed)
{
	return 0;
}

static int synquacer_spi_set_mode(struct udevice *bus, uint mode)
{
	return 0;
}

static int synquacer_spi_claim_bus(struct udevice *dev)
{
	return 0;
}

static int synquacer_spi_release_bus(struct udevice *dev)
{
	return 0;
}

static void synquacer_spi_disable_module(struct synquacer_spi_priv *priv)
{
	writel(0, priv->base + MCTRL);
	while (readl(priv->base + MCTRL) & BIT(MES))
		cpu_relax();
}

static void synquacer_spi_init(struct synquacer_spi_priv *priv)
{
	u32 val;

	synquacer_spi_disable_module(priv);

	writel(0, priv->base + TXE);
	writel(0, priv->base + RXE);
	val = readl(priv->base + TXF);
	writel(val, priv->base + TXC);
	val = readl(priv->base + RXF);
	writel(val, priv->base + RXC);
	val = readl(priv->base + FAULTF);
	writel(val, priv->base + FAULTC);

	val = readl(priv->base + DMCFG);
	val &= ~BIT(SSDC);
	val &= ~BIT(MSTARTEN);
	writel(val, priv->base + DMCFG);

	/* Enable module with direct mode */
	val = readl(priv->base + MCTRL);
	val &= ~BIT(IPCLK);
	val &= ~BIT(CSEN);
	val |= BIT(MEN);
	val |= BIT(SYNCON);
	writel(val, priv->base + MCTRL);
}

static void synquacer_spi_exit(struct synquacer_spi_priv *priv)
{
	u32 val;

	synquacer_spi_disable_module(priv);

	/* Enable module with command sequence mode */
	val = readl(priv->base + MCTRL);
	val &= ~BIT(IPCLK);
	val |= BIT(CSEN);
	val |= BIT(MEN);
	val |= BIT(SYNCON);
	writel(val, priv->base + MCTRL);

	while (!(readl(priv->base + MCTRL) & BIT(MES)))
		cpu_relax();
}

static int synquacer_spi_probe(struct udevice *bus)
{
	struct synquacer_spi_plat *plat = dev_get_plat(bus);
	struct synquacer_spi_priv *priv = dev_get_priv(bus);

	priv->base = plat->base;
	priv->aces = plat->aces;
	priv->rtm = plat->rtm;

	synquacer_spi_init(priv);
	return 0;
}

static int synquacer_spi_remove(struct udevice *bus)
{
	struct synquacer_spi_priv *priv = dev_get_priv(bus);

	synquacer_spi_exit(priv);
	return 0;
}

static int synquacer_spi_of_to_plat(struct udevice *bus)
{
	struct synquacer_spi_plat *plat = dev_get_plat(bus);
	struct clk clk;

	plat->base = dev_read_addr_ptr(bus);

	plat->aces = dev_read_bool(bus, "socionext,set-aces");
	plat->rtm = dev_read_bool(bus, "socionext,use-rtm");

	clk_get_by_name(bus, "iHCLK", &clk);
	clk_enable(&clk);

	return 0;
}

static const struct dm_spi_ops synquacer_spi_ops = {
	.claim_bus	= synquacer_spi_claim_bus,
	.release_bus	= synquacer_spi_release_bus,
	.xfer		= synquacer_spi_xfer,
	.set_speed	= synquacer_spi_set_speed,
	.set_mode	= synquacer_spi_set_mode,
};

static const struct udevice_id synquacer_spi_ids[] = {
	{ .compatible = "socionext,synquacer-spi" },
	{ /* Sentinel */ }
};

U_BOOT_DRIVER(synquacer_spi) = {
	.name		= "synquacer_spi",
	.id		= UCLASS_SPI,
	.of_match	= synquacer_spi_ids,
	.ops		= &synquacer_spi_ops,
	.of_to_plat	= synquacer_spi_of_to_plat,
	.plat_auto	= sizeof(struct synquacer_spi_plat),
	.priv_auto	= sizeof(struct synquacer_spi_priv),
	.probe		= synquacer_spi_probe,
	.flags		= DM_FLAG_OS_PREPARE,
	.remove		= synquacer_spi_remove,
};
