/*
 * B53 register access through SPI
 *
 * Copyright (C) 2011-2013 Jonas Gorski <jogo@openwrt.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <asm/unaligned.h>

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/platform_data/b53.h>

#include "b53_priv.h"

#define B53_SPI_DATA		0xf0

#define B53_SPI_STATUS		0xfe
#define B53_SPI_CMD_SPIF	BIT(7)
#define B53_SPI_CMD_RACK	BIT(5)

#define B53_SPI_CMD_READ	0x00
#define B53_SPI_CMD_WRITE	0x01
#define B53_SPI_CMD_NORMAL	0x60
#define B53_SPI_CMD_FAST	0x10

#define B53_SPI_PAGE_SELECT	0xff

static inline int b53_spi_read_reg(struct spi_device *spi, u8 reg, u8 *val,
				     unsigned len)
{
	u8 txbuf[2];

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_READ;
	txbuf[1] = reg;

	return spi_write_then_read(spi, txbuf, 2, val, len);
}

static inline int b53_spi_clear_status(struct spi_device *spi)
{
	unsigned int i;
	u8 rxbuf;
	int ret;

	for (i = 0; i < 10; i++) {
		ret = b53_spi_read_reg(spi, B53_SPI_STATUS, &rxbuf, 1);
		if (ret)
			return ret;

		if (!(rxbuf & B53_SPI_CMD_SPIF))
			break;

		mdelay(1);
	}

	if (i == 10)
		return -EIO;

	return 0;
}

static inline int b53_spi_set_page(struct spi_device *spi, u8 page)
{
	u8 txbuf[3];

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = B53_SPI_PAGE_SELECT;
	txbuf[2] = page;

	return spi_write(spi, txbuf, sizeof(txbuf));
}

static inline int b53_prepare_reg_access(struct spi_device *spi, u8 page)
{
	int ret = b53_spi_clear_status(spi);

	if (ret)
		return ret;

	return b53_spi_set_page(spi, page);
}

static int b53_spi_prepare_reg_read(struct spi_device *spi, u8 reg)
{
	u8 rxbuf;
	int retry_count;
	int ret;

	ret = b53_spi_read_reg(spi, reg, &rxbuf, 1);
	if (ret)
		return ret;

	for (retry_count = 0; retry_count < 10; retry_count++) {
		ret = b53_spi_read_reg(spi, B53_SPI_STATUS, &rxbuf, 1);
		if (ret)
			return ret;

		if (rxbuf & B53_SPI_CMD_RACK)
			break;

		mdelay(1);
	}

	if (retry_count == 10)
		return -EIO;

	return 0;
}

static int b53_spi_read(struct b53_device *dev, u8 page, u8 reg, u8 *data,
			unsigned len)
{
	struct spi_device *spi = dev->priv;
	int ret;

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	ret = b53_spi_prepare_reg_read(spi, reg);
	if (ret)
		return ret;

	return b53_spi_read_reg(spi, B53_SPI_DATA, data, len);
}

static int b53_spi_read8(struct b53_device *dev, u8 page, u8 reg, u8 *val)
{
	return b53_spi_read(dev, page, reg, val, 1);
}

static int b53_spi_read16(struct b53_device *dev, u8 page, u8 reg, u16 *val)
{
	int ret = b53_spi_read(dev, page, reg, (u8 *)val, 2);

	if (!ret)
		*val = le16_to_cpu(*val);

	return ret;
}

static int b53_spi_read32(struct b53_device *dev, u8 page, u8 reg, u32 *val)
{
	int ret = b53_spi_read(dev, page, reg, (u8 *)val, 4);

	if (!ret)
		*val = le32_to_cpu(*val);

	return ret;
}

static int b53_spi_read48(struct b53_device *dev, u8 page, u8 reg, u64 *val)
{
	int ret;

	*val = 0;
	ret = b53_spi_read(dev, page, reg, (u8 *)val, 6);
	if (!ret)
		*val = le64_to_cpu(*val);

	return ret;
}

static int b53_spi_read64(struct b53_device *dev, u8 page, u8 reg, u64 *val)
{
	int ret = b53_spi_read(dev, page, reg, (u8 *)val, 8);

	if (!ret)
		*val = le64_to_cpu(*val);

	return ret;
}

static int b53_spi_write8(struct b53_device *dev, u8 page, u8 reg, u8 value)
{
	struct spi_device *spi = dev->priv;
	int ret;
	u8 txbuf[3];

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = reg;
	txbuf[2] = value;

	return spi_write(spi, txbuf, sizeof(txbuf));
}

static int b53_spi_write16(struct b53_device *dev, u8 page, u8 reg, u16 value)
{
	struct spi_device *spi = dev->priv;
	int ret;
	u8 txbuf[4];

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = reg;
	put_unaligned_le16(value, &txbuf[2]);

	return spi_write(spi, txbuf, sizeof(txbuf));
}

static int b53_spi_write32(struct b53_device *dev, u8 page, u8 reg, u32 value)
{
	struct spi_device *spi = dev->priv;
	int ret;
	u8 txbuf[6];

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = reg;
	put_unaligned_le32(value, &txbuf[2]);

	return spi_write(spi, txbuf, sizeof(txbuf));
}

static int b53_spi_write48(struct b53_device *dev, u8 page, u8 reg, u64 value)
{
	struct spi_device *spi = dev->priv;
	int ret;
	u8 txbuf[10];

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = reg;
	put_unaligned_le64(value, &txbuf[2]);

	return spi_write(spi, txbuf, sizeof(txbuf) - 2);
}

static int b53_spi_write64(struct b53_device *dev, u8 page, u8 reg, u64 value)
{
	struct spi_device *spi = dev->priv;
	int ret;
	u8 txbuf[10];

	ret = b53_prepare_reg_access(spi, page);
	if (ret)
		return ret;

	txbuf[0] = B53_SPI_CMD_NORMAL | B53_SPI_CMD_WRITE;
	txbuf[1] = reg;
	put_unaligned_le64(value, &txbuf[2]);

	return spi_write(spi, txbuf, sizeof(txbuf));
}

static struct b53_io_ops b53_spi_ops = {
	.read8 = b53_spi_read8,
	.read16 = b53_spi_read16,
	.read32 = b53_spi_read32,
	.read48 = b53_spi_read48,
	.read64 = b53_spi_read64,
	.write8 = b53_spi_write8,
	.write16 = b53_spi_write16,
	.write32 = b53_spi_write32,
	.write48 = b53_spi_write48,
	.write64 = b53_spi_write64,
};

static int b53_spi_probe(struct spi_device *spi)
{
	struct b53_device *dev;
	int ret;

	dev = b53_switch_alloc(&spi->dev, &b53_spi_ops, spi);
	if (!dev)
		return -ENOMEM;

	if (spi->dev.platform_data)
		dev->pdata = spi->dev.platform_data;

	ret = b53_switch_register(dev);
	if (ret)
		return ret;

	spi_set_drvdata(spi, dev);

	return 0;
}

static int b53_spi_remove(struct spi_device *spi)
{
	struct b53_device *dev = spi_get_drvdata(spi);

	if (dev)
		b53_switch_remove(dev);

	return 0;
}

static const struct of_device_id b53_of_match[] = {
	{ .compatible = "brcm,bcm5325" },
	{ .compatible = "brcm,bcm53115" },
	{ .compatible = "brcm,bcm53125" },
	{ .compatible = "brcm,bcm53128" },
	{ .compatible = "brcm,bcm5365" },
	{ .compatible = "brcm,bcm5395" },
	{ .compatible = "brcm,bcm5397" },
	{ .compatible = "brcm,bcm5398" },
	{ /* sentinel */ },
};

static struct spi_driver b53_spi_driver = {
	.driver = {
		.name	= "b53-switch",
		.bus	= &spi_bus_type,
		.owner	= THIS_MODULE,
		.of_match_table = b53_of_match,
	},
	.probe	= b53_spi_probe,
	.remove	= b53_spi_remove,
};

module_spi_driver(b53_spi_driver);

MODULE_AUTHOR("Jonas Gorski <jogo@openwrt.org>");
MODULE_DESCRIPTION("B53 SPI access driver");
MODULE_LICENSE("Dual BSD/GPL");
