/*
 * Altera SPI driver
 *
 * Copyright (C) 2008 Thomas Chou <thomas@wytron.com.tw>
 *
 * Based on spi_s3c24xx.c, which is:
 * Copyright (c) 2006 Ben Dooks
 * Copyright (c) 2006 Simtec Electronics
 *	Ben Dooks <ben@simtec.co.uk>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi_bitbang.h>
#include <linux/io.h>
#include <linux/of.h>

#define DRV_NAME "spi_altera"

#define ALTERA_SPI_RXDATA	0
#define ALTERA_SPI_TXDATA	4
#define ALTERA_SPI_STATUS	8
#define ALTERA_SPI_CONTROL	12
#define ALTERA_SPI_SLAVE_SEL	20

#define ALTERA_SPI_STATUS_ROE_MSK	0x8
#define ALTERA_SPI_STATUS_TOE_MSK	0x10
#define ALTERA_SPI_STATUS_TMT_MSK	0x20
#define ALTERA_SPI_STATUS_TRDY_MSK	0x40
#define ALTERA_SPI_STATUS_RRDY_MSK	0x80
#define ALTERA_SPI_STATUS_E_MSK		0x100

#define ALTERA_SPI_CONTROL_IROE_MSK	0x8
#define ALTERA_SPI_CONTROL_ITOE_MSK	0x10
#define ALTERA_SPI_CONTROL_ITRDY_MSK	0x40
#define ALTERA_SPI_CONTROL_IRRDY_MSK	0x80
#define ALTERA_SPI_CONTROL_IE_MSK	0x100
#define ALTERA_SPI_CONTROL_SSO_MSK	0x400

struct altera_spi {
	/* bitbang has to be first */
	struct spi_bitbang bitbang;
	struct completion done;

	void __iomem *base;
	int irq;
	int len;
	int count;
	int bytes_per_word;
	unsigned long imr;

	/* data buffers */
	const unsigned char *tx;
	unsigned char *rx;
};

static inline struct altera_spi *altera_spi_to_hw(struct spi_device *sdev)
{
	return spi_master_get_devdata(sdev->master);
}

static void altera_spi_chipsel(struct spi_device *spi, int value)
{
	struct altera_spi *hw = altera_spi_to_hw(spi);

	if (spi->mode & SPI_CS_HIGH) {
		switch (value) {
		case BITBANG_CS_INACTIVE:
			writel(1 << spi->chip_select,
			       hw->base + ALTERA_SPI_SLAVE_SEL);
			hw->imr |= ALTERA_SPI_CONTROL_SSO_MSK;
			writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
			break;

		case BITBANG_CS_ACTIVE:
			hw->imr &= ~ALTERA_SPI_CONTROL_SSO_MSK;
			writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
			writel(0, hw->base + ALTERA_SPI_SLAVE_SEL);
			break;
		}
	} else {
		switch (value) {
		case BITBANG_CS_INACTIVE:
			hw->imr &= ~ALTERA_SPI_CONTROL_SSO_MSK;
			writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
			break;

		case BITBANG_CS_ACTIVE:
			writel(1 << spi->chip_select,
			       hw->base + ALTERA_SPI_SLAVE_SEL);
			hw->imr |= ALTERA_SPI_CONTROL_SSO_MSK;
			writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
			break;
		}
	}
}

static int altera_spi_setupxfer(struct spi_device *spi, struct spi_transfer *t)
{
	return 0;
}

static int altera_spi_setup(struct spi_device *spi)
{
	return 0;
}

static inline unsigned int hw_txbyte(struct altera_spi *hw, int count)
{
	if (hw->tx) {
		switch (hw->bytes_per_word) {
		case 1:
			return hw->tx[count];
		case 2:
			return (hw->tx[count * 2]
				| (hw->tx[count * 2 + 1] << 8));
		}
	}
	return 0;
}

static int altera_spi_txrx(struct spi_device *spi, struct spi_transfer *t)
{
	struct altera_spi *hw = altera_spi_to_hw(spi);

	hw->tx = t->tx_buf;
	hw->rx = t->rx_buf;
	hw->count = 0;
	hw->bytes_per_word = t->bits_per_word / 8;
	hw->len = t->len / hw->bytes_per_word;

	if (hw->irq >= 0) {
		/* enable receive interrupt */
		hw->imr |= ALTERA_SPI_CONTROL_IRRDY_MSK;
		writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);

		/* send the first byte */
		writel(hw_txbyte(hw, 0), hw->base + ALTERA_SPI_TXDATA);

		wait_for_completion(&hw->done);
		/* disable receive interrupt */
		hw->imr &= ~ALTERA_SPI_CONTROL_IRRDY_MSK;
		writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
	} else {
		/* send the first byte */
		writel(hw_txbyte(hw, 0), hw->base + ALTERA_SPI_TXDATA);

		while (1) {
			unsigned int rxd;

			while (!(readl(hw->base + ALTERA_SPI_STATUS) &
				 ALTERA_SPI_STATUS_RRDY_MSK))
				cpu_relax();

			rxd = readl(hw->base + ALTERA_SPI_RXDATA);
			if (hw->rx) {
				switch (hw->bytes_per_word) {
				case 1:
					hw->rx[hw->count] = rxd;
					break;
				case 2:
					hw->rx[hw->count * 2] = rxd;
					hw->rx[hw->count * 2 + 1] = rxd >> 8;
					break;
				}
			}

			hw->count++;

			if (hw->count < hw->len)
				writel(hw_txbyte(hw, hw->count),
				       hw->base + ALTERA_SPI_TXDATA);
			else
				break;
		}

	}

	return hw->count * hw->bytes_per_word;
}

static irqreturn_t altera_spi_irq(int irq, void *dev)
{
	struct altera_spi *hw = dev;
	unsigned int rxd;

	rxd = readl(hw->base + ALTERA_SPI_RXDATA);
	if (hw->rx) {
		switch (hw->bytes_per_word) {
		case 1:
			hw->rx[hw->count] = rxd;
			break;
		case 2:
			hw->rx[hw->count * 2] = rxd;
			hw->rx[hw->count * 2 + 1] = rxd >> 8;
			break;
		}
	}

	hw->count++;

	if (hw->count < hw->len)
		writel(hw_txbyte(hw, hw->count), hw->base + ALTERA_SPI_TXDATA);
	else
		complete(&hw->done);

	return IRQ_HANDLED;
}

static int altera_spi_probe(struct platform_device *pdev)
{
	struct altera_spi_platform_data *platp = pdev->dev.platform_data;
	struct altera_spi *hw;
	struct spi_master *master;
	struct resource *res;
	int err = -ENODEV;

	master = spi_alloc_master(&pdev->dev, sizeof(struct altera_spi));
	if (!master)
		return err;

	/* setup the master state. */
	master->bus_num = pdev->id;
	master->num_chipselect = 16;
	master->mode_bits = SPI_CS_HIGH;
	master->setup = altera_spi_setup;

	hw = spi_master_get_devdata(master);
	platform_set_drvdata(pdev, hw);

	/* setup the state for the bitbang driver */
	hw->bitbang.master = spi_master_get(master);
	if (!hw->bitbang.master)
		return err;
	hw->bitbang.setup_transfer = altera_spi_setupxfer;
	hw->bitbang.chipselect = altera_spi_chipsel;
	hw->bitbang.txrx_bufs = altera_spi_txrx;

	/* find and map our resources */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		goto exit_busy;
	if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res),
				     pdev->name))
		goto exit_busy;
	hw->base = devm_ioremap_nocache(&pdev->dev, res->start,
					resource_size(res));
	if (!hw->base)
		goto exit_busy;
	/* program defaults into the registers */
	hw->imr = 0;		/* disable spi interrupts */
	writel(hw->imr, hw->base + ALTERA_SPI_CONTROL);
	writel(0, hw->base + ALTERA_SPI_STATUS);	/* clear status reg */
	if (readl(hw->base + ALTERA_SPI_STATUS) & ALTERA_SPI_STATUS_RRDY_MSK)
		readl(hw->base + ALTERA_SPI_RXDATA);	/* flush rxdata */
	/* irq is optional */
	hw->irq = platform_get_irq(pdev, 0);
	if (hw->irq >= 0) {
		init_completion(&hw->done);
		err = devm_request_irq(&pdev->dev, hw->irq, altera_spi_irq, 0,
				       pdev->name, hw);
		if (err)
			goto exit;
	}
	/* find platform data */
	if (!platp)
		hw->bitbang.master->dev.of_node = pdev->dev.of_node;

	/* register our spi controller */
	err = spi_bitbang_start(&hw->bitbang);
	if (err)
		goto exit;
	dev_info(&pdev->dev, "base %p, irq %d\n", hw->base, hw->irq);

	return 0;

exit_busy:
	err = -EBUSY;
exit:
	platform_set_drvdata(pdev, NULL);
	spi_master_put(master);
	return err;
}

static int altera_spi_remove(struct platform_device *dev)
{
	struct altera_spi *hw = platform_get_drvdata(dev);
	struct spi_master *master = hw->bitbang.master;

	spi_bitbang_stop(&hw->bitbang);
	platform_set_drvdata(dev, NULL);
	spi_master_put(master);
	return 0;
}

#ifdef CONFIG_OF
static const struct of_device_id altera_spi_match[] = {
	{ .compatible = "ALTR,spi-1.0", },
	{},
};
MODULE_DEVICE_TABLE(of, altera_spi_match);
#endif /* CONFIG_OF */

static struct platform_driver altera_spi_driver = {
	.probe = altera_spi_probe,
	.remove = altera_spi_remove,
	.driver = {
		.name = DRV_NAME,
		.owner = THIS_MODULE,
		.pm = NULL,
		.of_match_table = of_match_ptr(altera_spi_match),
	},
};
module_platform_driver(altera_spi_driver);

MODULE_DESCRIPTION("Altera SPI driver");
MODULE_AUTHOR("Thomas Chou <thomas@wytron.com.tw>");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:" DRV_NAME);