Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
CL Wang 3001 99.11% 2 66.67%
Felix Gu 27 0.89% 1 33.33%
Total 3028 3


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Driver for Andes ATCSPI200 SPI Controller
 *
 * Copyright (C) 2025 Andes Technology Corporation.
 */

#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/completion.h>
#include <linux/dev_printk.h>
#include <linux/dmaengine.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/jiffies.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi-mem.h>

/* Register definitions  */
#define ATCSPI_TRANS_FMT	0x10	/* SPI transfer format register */
#define ATCSPI_TRANS_CTRL	0x20	/* SPI transfer control register */
#define ATCSPI_CMD		0x24	/* SPI command register */
#define ATCSPI_ADDR		0x28	/* SPI address register */
#define ATCSPI_DATA		0x2C	/* SPI data register */
#define ATCSPI_CTRL		0x30	/* SPI control register */
#define ATCSPI_STATUS		0x34	/* SPI status register */
#define ATCSPI_TIMING		0x40	/* SPI interface timing register */
#define ATCSPI_CONFIG		0x7C	/* SPI configuration register */

/* Transfer format register */
#define TRANS_FMT_CPHA		BIT(0)
#define TRANS_FMT_CPOL		BIT(1)
#define TRANS_FMT_DATA_MERGE_EN	BIT(7)
#define TRANS_FMT_DATA_LEN_MASK	GENMASK(12, 8)
#define TRANS_FMT_ADDR_LEN_MASK	GENMASK(17, 16)
#define TRANS_FMT_DATA_LEN(x)	FIELD_PREP(TRANS_FMT_DATA_LEN_MASK, (x) - 1)
#define TRANS_FMT_ADDR_LEN(x)	FIELD_PREP(TRANS_FMT_ADDR_LEN_MASK, (x) - 1)

/* Transfer control register */
#define TRANS_MODE_MASK		GENMASK(27, 24)
#define TRANS_MODE_W_ONLY	FIELD_PREP(TRANS_MODE_MASK, 1)
#define TRANS_MODE_R_ONLY	FIELD_PREP(TRANS_MODE_MASK, 2)
#define TRANS_MODE_NONE_DATA	FIELD_PREP(TRANS_MODE_MASK, 7)
#define TRANS_MODE_DMY_READ	FIELD_PREP(TRANS_MODE_MASK, 9)
#define TRANS_FIELD_DECNZ(m, x)	((x) ? FIELD_PREP(m, (x) - 1) : 0)
#define TRANS_RD_TRANS_CNT(x)	TRANS_FIELD_DECNZ(GENMASK(8, 0), x)
#define TRANS_DUMMY_CNT(x)	TRANS_FIELD_DECNZ(GENMASK(10, 9), x)
#define TRANS_WR_TRANS_CNT(x)	TRANS_FIELD_DECNZ(GENMASK(20, 12), x)
#define TRANS_DUAL_QUAD(x)	FIELD_PREP(GENMASK(23, 22), (x))
#define TRANS_ADDR_FMT		BIT(28)
#define TRANS_ADDR_EN		BIT(29)
#define TRANS_CMD_EN		BIT(30)

/* Control register */
#define CTRL_SPI_RST		BIT(0)
#define CTRL_RX_FIFO_RST	BIT(1)
#define CTRL_TX_FIFO_RST	BIT(2)
#define CTRL_RX_DMA_EN		BIT(3)
#define CTRL_TX_DMA_EN		BIT(4)

/* Status register */
#define ATCSPI_ACTIVE		BIT(0)
#define ATCSPI_RX_EMPTY		BIT(14)
#define ATCSPI_TX_FULL		BIT(23)

/* Interface timing setting */
#define TIMING_SCLK_DIV_MASK	GENMASK(7, 0)
#define TIMING_SCLK_DIV_MAX	0xFE

/* Configuration register */
#define RXFIFO_SIZE(x)		FIELD_GET(GENMASK(3, 0), (x))
#define TXFIFO_SIZE(x)		FIELD_GET(GENMASK(7, 4), (x))

/* driver configurations */
#define ATCSPI_MAX_TRANS_LEN	512
#define ATCSPI_MAX_SPEED_HZ	50000000
#define ATCSPI_RDY_TIMEOUT_US	1000000
#define ATCSPI_XFER_TIMEOUT(n)	((n) * 10)
#define ATCSPI_MAX_CS_NUM	1
#define ATCSPI_DMA_THRESHOLD	256
#define ATCSPI_BITS_PER_UINT	8
#define ATCSPI_DATA_MERGE_EN	1
#define ATCSPI_DMA_SUPPORT	1

/**
 * struct atcspi_dev - Andes ATCSPI200 SPI controller private data
 * @host:           Pointer to the SPI controller structure.
 * @mutex_lock:     A mutex to protect concurrent access to the controller.
 * @dma_completion: A completion to signal the end of a DMA transfer.
 * @dev:            Pointer to the device structure.
 * @regmap:         Register map for accessing controller registers.
 * @clk:            Pointer to the controller's functional clock.
 * @dma_addr:       The physical address of the SPI data register for DMA.
 * @clk_rate:       The cached frequency of the functional clock.
 * @sclk_rate:      The target frequency for the SPI clock (SCLK).
 * @txfifo_size:    The size of the transmit FIFO in bytes.
 * @rxfifo_size:    The size of the receive FIFO in bytes.
 * @data_merge:     A flag indicating if the data merge mode is enabled for
 *                  the current transfer.
 * @use_dma:        Enable DMA mode if ATCSPI_DMA_SUPPORT is set and DMA is
 *                  successfully configured.
 */
struct atcspi_dev {
	struct spi_controller	*host;
	struct mutex		mutex_lock;
	struct completion	dma_completion;
	struct device		*dev;
	struct regmap		*regmap;
	struct clk		*clk;
	dma_addr_t		dma_addr;
	unsigned int		clk_rate;
	unsigned int		sclk_rate;
	unsigned int		txfifo_size;
	unsigned int		rxfifo_size;
	bool			data_merge;
	bool			use_dma;
};

static int atcspi_wait_fifo_ready(struct atcspi_dev *spi,
				  enum spi_mem_data_dir dir)
{
	unsigned int val;
	unsigned int mask;
	int ret;

	mask = (dir == SPI_MEM_DATA_OUT) ? ATCSPI_TX_FULL : ATCSPI_RX_EMPTY;
	ret = regmap_read_poll_timeout(spi->regmap,
				       ATCSPI_STATUS,
				       val,
				       !(val & mask),
				       0,
				       ATCSPI_RDY_TIMEOUT_US);
	if (ret)
		dev_info(spi->dev, "Timed out waiting for FIFO ready\n");

	return ret;
}

static int atcspi_xfer_data_poll(struct atcspi_dev *spi,
				 const struct spi_mem_op *op)
{
	void *rx_buf = op->data.buf.in;
	const void *tx_buf = op->data.buf.out;
	unsigned int val;
	int trans_bytes = op->data.nbytes;
	int num_byte;
	int ret = 0;

	num_byte = spi->data_merge ? 4 : 1;
	while (trans_bytes) {
		if (op->data.dir == SPI_MEM_DATA_OUT) {
			ret = atcspi_wait_fifo_ready(spi, SPI_MEM_DATA_OUT);
			if (ret)
				return ret;

			if (spi->data_merge)
				val = *(unsigned int *)tx_buf;
			else
				val = *(unsigned char *)tx_buf;
			regmap_write(spi->regmap, ATCSPI_DATA, val);
			tx_buf = (unsigned char *)tx_buf + num_byte;
		} else {
			ret = atcspi_wait_fifo_ready(spi, SPI_MEM_DATA_IN);
			if (ret)
				return ret;

			regmap_read(spi->regmap, ATCSPI_DATA, &val);
			if (spi->data_merge)
				*(unsigned int *)rx_buf = val;
			else
				*(unsigned char *)rx_buf = (unsigned char)val;
			rx_buf = (unsigned char *)rx_buf + num_byte;
		}
		trans_bytes -= num_byte;
	}

	return ret;
}

static void atcspi_set_trans_ctl(struct atcspi_dev *spi,
				 const struct spi_mem_op *op)
{
	unsigned int tc = 0;

	if (op->cmd.nbytes)
		tc |= TRANS_CMD_EN;
	if (op->addr.nbytes)
		tc |= TRANS_ADDR_EN;
	if (op->addr.buswidth > 1)
		tc |= TRANS_ADDR_FMT;
	if (op->data.nbytes) {
		unsigned int width_code;

		width_code = ffs(op->data.buswidth) - 1;
		if (unlikely(width_code > 3)) {
			WARN_ON_ONCE(1);
			width_code = 0;
		}
		tc |= TRANS_DUAL_QUAD(width_code);

		if (op->data.dir == SPI_MEM_DATA_IN) {
			if (op->dummy.nbytes)
				tc |= TRANS_MODE_DMY_READ |
				      TRANS_DUMMY_CNT(op->dummy.nbytes);
			else
				tc |= TRANS_MODE_R_ONLY;
			tc |= TRANS_RD_TRANS_CNT(op->data.nbytes);
		} else {
			tc |= TRANS_MODE_W_ONLY |
			      TRANS_WR_TRANS_CNT(op->data.nbytes);
		}
	} else {
		tc |= TRANS_MODE_NONE_DATA;
	}
	regmap_write(spi->regmap, ATCSPI_TRANS_CTRL, tc);
}

static void atcspi_set_trans_fmt(struct atcspi_dev *spi,
				 const struct spi_mem_op *op)
{
	unsigned int val;

	regmap_read(spi->regmap, ATCSPI_TRANS_FMT, &val);
	if (op->data.nbytes) {
		if (ATCSPI_DATA_MERGE_EN && ATCSPI_BITS_PER_UINT == 8 &&
		    !(op->data.nbytes % 4)) {
			val |= TRANS_FMT_DATA_MERGE_EN;
			spi->data_merge = true;
		} else {
			val &= ~TRANS_FMT_DATA_MERGE_EN;
			spi->data_merge = false;
		}
	}

	val = (val & ~TRANS_FMT_ADDR_LEN_MASK) |
	      TRANS_FMT_ADDR_LEN(op->addr.nbytes);
	regmap_write(spi->regmap, ATCSPI_TRANS_FMT, val);
}

static void atcspi_prepare_trans(struct atcspi_dev *spi,
				 const struct spi_mem_op *op)
{
	atcspi_set_trans_fmt(spi, op);
	atcspi_set_trans_ctl(spi, op);
	if (op->addr.nbytes)
		regmap_write(spi->regmap, ATCSPI_ADDR, op->addr.val);
	regmap_write(spi->regmap, ATCSPI_CMD, op->cmd.opcode);
}

static int atcspi_adjust_op_size(struct spi_mem *mem, struct spi_mem_op *op)
{
	struct atcspi_dev *spi;

	spi = spi_controller_get_devdata(mem->spi->controller);
	op->data.nbytes = min(op->data.nbytes, ATCSPI_MAX_TRANS_LEN);

	/* DMA needs to be aligned to 4 byte */
	if (spi->use_dma && op->data.nbytes >= ATCSPI_DMA_THRESHOLD)
		op->data.nbytes = ALIGN_DOWN(op->data.nbytes, 4);

	return 0;
}

static int atcspi_dma_config(struct atcspi_dev *spi, bool is_rx)
{
	struct dma_slave_config conf = { 0 };
	struct dma_chan *chan;

	if (is_rx) {
		chan = spi->host->dma_rx;
		conf.direction = DMA_DEV_TO_MEM;
		conf.src_addr = spi->dma_addr;
	} else {
		chan = spi->host->dma_tx;
		conf.direction = DMA_MEM_TO_DEV;
		conf.dst_addr = spi->dma_addr;
	}
	conf.dst_maxburst = spi->rxfifo_size / 2;
	conf.src_maxburst = spi->txfifo_size / 2;

	if (spi->data_merge) {
		conf.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
		conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
	} else {
		conf.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
		conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	}

	return dmaengine_slave_config(chan, &conf);
}

static void atcspi_dma_callback(void *arg)
{
	struct completion *dma_completion = arg;

	complete(dma_completion);
}

static int atcspi_dma_trans(struct atcspi_dev *spi,
			    const struct spi_mem_op *op)
{
	struct dma_async_tx_descriptor *desc;
	struct dma_chan *dma_ch;
	struct sg_table sgt;
	enum dma_transfer_direction dma_dir;
	dma_cookie_t cookie;
	unsigned int ctrl;
	int timeout;
	int ret;

	regmap_read(spi->regmap, ATCSPI_CTRL, &ctrl);
	ctrl |= CTRL_TX_DMA_EN | CTRL_RX_DMA_EN;
	regmap_write(spi->regmap, ATCSPI_CTRL, ctrl);
	if (op->data.dir == SPI_MEM_DATA_IN) {
		ret = atcspi_dma_config(spi, TRUE);
		dma_dir = DMA_DEV_TO_MEM;
		dma_ch = spi->host->dma_rx;
	} else {
		ret = atcspi_dma_config(spi, FALSE);
		dma_dir = DMA_MEM_TO_DEV;
		dma_ch = spi->host->dma_tx;
	}
	if (ret)
		return ret;

	ret = spi_controller_dma_map_mem_op_data(spi->host, op, &sgt);
	if (ret)
		return ret;

	desc = dmaengine_prep_slave_sg(dma_ch, sgt.sgl, sgt.nents, dma_dir,
				       DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
	if (!desc) {
		ret = -ENOMEM;
		goto exit_unmap;
	}

	reinit_completion(&spi->dma_completion);
	desc->callback = atcspi_dma_callback;
	desc->callback_param = &spi->dma_completion;
	cookie = dmaengine_submit(desc);
	ret = dma_submit_error(cookie);
	if (ret)
		goto exit_unmap;

	dma_async_issue_pending(dma_ch);
	timeout = msecs_to_jiffies(ATCSPI_XFER_TIMEOUT(op->data.nbytes));
	if (!wait_for_completion_timeout(&spi->dma_completion, timeout)) {
		ret = -ETIMEDOUT;
		dmaengine_terminate_all(dma_ch);
	}

exit_unmap:
	spi_controller_dma_unmap_mem_op_data(spi->host, op, &sgt);

	return ret;
}

static int atcspi_exec_mem_op(struct spi_mem *mem, const struct spi_mem_op *op)
{
	struct spi_device *spi_dev = mem->spi;
	struct atcspi_dev *spi;
	unsigned int val;
	int ret;

	spi = spi_controller_get_devdata(spi_dev->controller);
	mutex_lock(&spi->mutex_lock);
	atcspi_prepare_trans(spi, op);
	if (op->data.nbytes) {
		if (spi->use_dma && op->data.nbytes >= ATCSPI_DMA_THRESHOLD)
			ret = atcspi_dma_trans(spi, op);
		else
			ret = atcspi_xfer_data_poll(spi, op);
		if (ret) {
			dev_info(spi->dev, "SPI transmission failed\n");
			goto exec_mem_exit;
		}
	}

	ret = regmap_read_poll_timeout(spi->regmap,
				       ATCSPI_STATUS,
				       val,
				       !(val & ATCSPI_ACTIVE),
				       0,
				       ATCSPI_RDY_TIMEOUT_US);
	if (ret)
		dev_info(spi->dev, "Timed out waiting for ATCSPI_ACTIVE\n");

exec_mem_exit:
	mutex_unlock(&spi->mutex_lock);

	return ret;
}

static const struct spi_controller_mem_ops atcspi_mem_ops = {
	.exec_op = atcspi_exec_mem_op,
	.adjust_op_size = atcspi_adjust_op_size,
};

static int atcspi_setup(struct atcspi_dev *spi)
{
	unsigned int ctrl_val;
	unsigned int val;
	int actual_spi_sclk_f;
	int ret;
	unsigned char div;

	ctrl_val = CTRL_TX_FIFO_RST | CTRL_RX_FIFO_RST | CTRL_SPI_RST;
	regmap_write(spi->regmap, ATCSPI_CTRL, ctrl_val);
	ret = regmap_read_poll_timeout(spi->regmap,
				       ATCSPI_CTRL,
				       val,
				       !(val & ctrl_val),
				       0,
				       ATCSPI_RDY_TIMEOUT_US);
	if (ret)
		return dev_err_probe(spi->dev, ret,
				     "Timed out waiting for ATCSPI_CTRL\n");

	val = TRANS_FMT_DATA_LEN(ATCSPI_BITS_PER_UINT) |
	      TRANS_FMT_CPHA | TRANS_FMT_CPOL;
	regmap_write(spi->regmap, ATCSPI_TRANS_FMT, val);

	regmap_read(spi->regmap, ATCSPI_CONFIG, &val);
	spi->txfifo_size = BIT(TXFIFO_SIZE(val) + 1);
	spi->rxfifo_size = BIT(RXFIFO_SIZE(val) + 1);

	regmap_read(spi->regmap, ATCSPI_TIMING, &val);
	val &= ~TIMING_SCLK_DIV_MASK;

	/*
	 * The SCLK_DIV value 0xFF is special and indicates that the
	 * SCLK rate should be the same as the SPI clock rate.
	 */
	if (spi->sclk_rate >= spi->clk_rate) {
		div = TIMING_SCLK_DIV_MASK;
	} else {
		/*
		 * The divider value is determined as follows:
		 * 1. If the divider can generate the exact target frequency,
		 *    use that setting.
		 * 2. If an exact match is not possible, select the closest
		 *    available setting that is lower than the target frequency.
		 */
		div = (spi->clk_rate + (spi->sclk_rate * 2 - 1)) /
		      (spi->sclk_rate * 2) - 1;

		/* Check if the actual SPI clock is lower than the target */
		actual_spi_sclk_f = spi->clk_rate / ((div + 1) * 2);
		if (actual_spi_sclk_f < spi->sclk_rate)
			dev_info(spi->dev,
				 "Clock adjusted %d to %d due to divider limitation",
				 spi->sclk_rate, actual_spi_sclk_f);

		if (div > TIMING_SCLK_DIV_MAX)
			return dev_err_probe(spi->dev, -EINVAL,
					     "Unsupported SPI clock %d\n",
					     spi->sclk_rate);
	}
	val |= div;
	regmap_write(spi->regmap, ATCSPI_TIMING, val);

	return ret;
}

static int atcspi_init_resources(struct platform_device *pdev,
				 struct atcspi_dev *spi,
				 struct resource **mem_res)
{
	void __iomem *base;
	const struct regmap_config atcspi_regmap_cfg = {
		.name = "atcspi",
		.reg_bits = 32,
		.val_bits = 32,
		.cache_type = REGCACHE_NONE,
		.reg_stride = 4,
		.pad_bits = 0,
		.max_register = ATCSPI_CONFIG
	};

	base = devm_platform_get_and_ioremap_resource(pdev, 0, mem_res);
	if (IS_ERR(base))
		return dev_err_probe(spi->dev, PTR_ERR(base),
				     "Failed to get ioremap resource\n");

	spi->regmap = devm_regmap_init_mmio(spi->dev, base,
					    &atcspi_regmap_cfg);
	if (IS_ERR(spi->regmap))
		return dev_err_probe(spi->dev, PTR_ERR(spi->regmap),
				     "Failed to init regmap\n");

	spi->clk = devm_clk_get(spi->dev, NULL);
	if (IS_ERR(spi->clk))
		return dev_err_probe(spi->dev, PTR_ERR(spi->clk),
				     "Failed to get SPI clock\n");

	spi->sclk_rate = ATCSPI_MAX_SPEED_HZ;
	return 0;
}

static int atcspi_configure_dma(struct atcspi_dev *spi)
{
	spi->host->dma_rx = devm_dma_request_chan(spi->dev, "rx");
	if (IS_ERR(spi->host->dma_rx))
		return PTR_ERR(spi->host->dma_rx);

	spi->host->dma_tx = devm_dma_request_chan(spi->dev, "tx");
	if (IS_ERR(spi->host->dma_tx))
		return PTR_ERR(spi->host->dma_tx);

	init_completion(&spi->dma_completion);

	return 0;
}

static int atcspi_enable_clk(struct atcspi_dev *spi)
{
	int ret;

	ret = clk_prepare_enable(spi->clk);
	if (ret)
		return dev_err_probe(spi->dev, ret,
				     "Failed to enable clock\n");

	spi->clk_rate = clk_get_rate(spi->clk);
	if (!spi->clk_rate)
		return dev_err_probe(spi->dev, -EINVAL,
				     "Failed to get SPI clock rate\n");

	return 0;
}

static void atcspi_init_controller(struct platform_device *pdev,
				   struct atcspi_dev *spi,
				   struct spi_controller *host,
				   struct resource *mem_res)
{
	/* Get the physical address of the data register for DMA transfers. */
	spi->dma_addr = (dma_addr_t)(mem_res->start + ATCSPI_DATA);

	/* Initialize controller properties */
	host->bus_num = pdev->id;
	host->mode_bits = SPI_CPOL | SPI_CPHA | SPI_RX_QUAD | SPI_TX_QUAD;
	host->num_chipselect = ATCSPI_MAX_CS_NUM;
	host->mem_ops = &atcspi_mem_ops;
	host->max_speed_hz = spi->sclk_rate;
}

static int atcspi_probe(struct platform_device *pdev)
{
	struct spi_controller *host;
	struct atcspi_dev *spi;
	struct resource *mem_res;
	int ret;

	host = spi_alloc_host(&pdev->dev, sizeof(*spi));
	if (!host)
		return -ENOMEM;

	spi = spi_controller_get_devdata(host);
	spi->host = host;
	spi->dev = &pdev->dev;
	dev_set_drvdata(&pdev->dev, host);

	ret = atcspi_init_resources(pdev, spi, &mem_res);
	if (ret)
		goto free_controller;

	ret = atcspi_enable_clk(spi);
	if (ret)
		goto free_controller;

	atcspi_init_controller(pdev, spi, host, mem_res);

	ret = atcspi_setup(spi);
	if (ret)
		goto disable_clk;

	ret = devm_spi_register_controller(&pdev->dev, host);
	if (ret) {
		dev_err_probe(spi->dev, ret,
			      "Failed to register SPI controller\n");
		goto disable_clk;
	}

	spi->use_dma = false;
	if (ATCSPI_DMA_SUPPORT) {
		ret = atcspi_configure_dma(spi);
		if (ret)
			dev_info(spi->dev,
				 "Failed to init DMA, fallback to PIO mode\n");
		else
			spi->use_dma = true;
	}
	mutex_init(&spi->mutex_lock);

	return 0;

disable_clk:
	clk_disable_unprepare(spi->clk);

free_controller:
	spi_controller_put(host);
	return ret;
}

static int atcspi_suspend(struct device *dev)
{
	struct spi_controller *host = dev_get_drvdata(dev);
	struct atcspi_dev *spi = spi_controller_get_devdata(host);

	spi_controller_suspend(host);

	clk_disable_unprepare(spi->clk);

	return 0;
}

static int atcspi_resume(struct device *dev)
{
	struct spi_controller *host = dev_get_drvdata(dev);
	struct atcspi_dev *spi = spi_controller_get_devdata(host);
	int ret;

	ret = clk_prepare_enable(spi->clk);
	if (ret)
		return ret;

	ret = atcspi_setup(spi);
	if (ret)
		goto disable_clk;

	ret = spi_controller_resume(host);
	if (ret)
		goto disable_clk;

	return ret;

disable_clk:
	clk_disable_unprepare(spi->clk);

	return ret;
}

static DEFINE_SIMPLE_DEV_PM_OPS(atcspi_pm_ops, atcspi_suspend, atcspi_resume);

static const struct of_device_id atcspi_of_match[] = {
	{ .compatible = "andestech,qilai-spi", },
	{ .compatible = "andestech,ae350-spi", },
	{ /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, atcspi_of_match);

static struct platform_driver atcspi_driver = {
	.probe = atcspi_probe,
	.driver = {
		.name = "atcspi200",
		.owner	= THIS_MODULE,
		.of_match_table = atcspi_of_match,
		.pm = pm_sleep_ptr(&atcspi_pm_ops)
	}
};
module_platform_driver(atcspi_driver);

MODULE_AUTHOR("CL Wang <cl634@andestech.com>");
MODULE_DESCRIPTION("Andes ATCSPI200 SPI controller driver");
MODULE_LICENSE("GPL");