/*
 *  DMA DEV_TO_MEM testing
 */
#include <linux/dma-mapping.h>
#include <linux/dmaengine.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/of.h>

int dma_mode = 0;
module_param_named(mode, dma_mode, uint, 0644);
MODULE_PARM_DESC(mode, "dma mode: 0 - cyclic, 1 - sg");

int bufsz = 2;
module_param_named(buf, bufsz, uint, 0644);
MODULE_PARM_DESC(buf, "tgt buffer size in times of src fifo (default 2)");

#define FIFO_SIZE	4	/* in bytes, must be less than PAGE_SIZE/2 */

struct dmatest_info {
	struct platform_device *pdev;
	struct dma_chan	       *dma;
	dma_cookie_t cookie;
	char* buf;
	dma_addr_t buf_dma;
	int buf_size;
	char *fifo;
	dma_addr_t fifo_dma;
	int fifo_size;
	struct dma_async_tx_descriptor *tx;
	int cnt;
	int tdowned;
};

static void dmatest_rx_callback(void *param)
{
	struct dmatest_info *info = param;

	dev_info(&info->pdev->dev, "in dma callback(%d)\n", info->cnt++);
	print_hex_dump(KERN_INFO, "", DUMP_PREFIX_OFFSET, 16, 1,
			info->buf, 32, true);

	if (info->cnt > 2) {
		dev_info(&info->pdev->dev, "=== term dma\n");
		dmaengine_terminate_async(info->dma);
		info->tdowned = 1;
	} else {
		memset(info->buf, 0, 32);
	}
}

static int config_dma(struct dmatest_info *info)
{
        struct device *dev = &info->pdev->dev;
        struct dma_slave_config conf;
        struct dma_async_tx_descriptor *tx;
        int ret;

        memset(&conf, 0, sizeof(conf));
        conf.direction = DMA_DEV_TO_MEM;
        conf.src_addr_width = 2;
        conf.src_maxburst = info->fifo_size / conf.src_addr_width;
        conf.src_addr = info->fifo_dma;
        ret = dmaengine_slave_config(info->dma, &conf);
        if (ret < 0) {
		dev_err(dev, "dma slave config failed (%d)\n", ret);
                return ret;
	}

	if (dma_mode) {
		tx = dmaengine_prep_slave_single(info->dma, info->buf_dma,
				info->buf_size, DMA_DEV_TO_MEM,
				DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
		dev_info(dev, "slave single dma preparation passed\n");
	} else {
		tx = dmaengine_prep_dma_cyclic(info->dma, info->buf_dma,
				info->buf_size, conf.src_maxburst,
				DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
		dev_info(dev, "cyclic dma preparation passed\n");
	}

        if (!tx) {
                dev_err(dev, "dma preparation failed\n");
                return -ENODEV;
        }

	info->tx = tx;
        tx->callback = dmatest_rx_callback;
        tx->callback_param = info;
        info->cookie = dmaengine_submit(tx);

        return 0;
}

static int __init dev_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct dmatest_info *info;
	dma_cap_mask_t mask;
	int ret;

	dev_info(dev, "dev_probe()");
	info = devm_kzalloc(dev, sizeof(struct dmatest_info), GFP_KERNEL);

	if (!info)
		return -ENOMEM;

	info->pdev = pdev;

	/* Allocate a page. The first half is the buffer. The second half
	 * is the FIFO, but only use the FIFO_SIZE */
	info->buf = dma_alloc_coherent(dev, PAGE_SIZE, &info->buf_dma,
			GFP_KERNEL);
	if (!info->buf) {
		dev_err(dev, "dma buffer allocation failed\n");
		return -ENOMEM;
	}
	info->buf_size = FIFO_SIZE * bufsz;
	info->fifo = info->buf + PAGE_SIZE/2;
	info->fifo_dma = info->buf_dma + PAGE_SIZE/2 + 2;
	info->fifo_size = FIFO_SIZE;

	dma_cap_zero(mask);
	dma_cap_set(DMA_SLAVE, mask);
	dma_cap_set(DMA_CYCLIC, mask);

	info->dma = dma_request_chan(dev, "rx");
	if (IS_ERR(info->dma)) {
		dev_err(dev, "dma channel request failed (%ld)\n",
				PTR_ERR(info->dma));
		return PTR_ERR(info->dma);
	}

	ret = config_dma(info);
	if (ret) {
		dev_err(dev, "config dma failed (%d)\n", ret);
		goto err_dma;
	}

	platform_set_drvdata(pdev, info);

	memcpy(info->fifo, "1234567890abcdef", 16);
	print_hex_dump(KERN_INFO, "", DUMP_PREFIX_OFFSET, 16, 1,
			info->fifo, 32, true);
	print_hex_dump(KERN_INFO, "", DUMP_PREFIX_OFFSET, 16, 1,
			info->buf, 32, true);

	dma_async_issue_pending(info->dma);
	dev_info(dev, "dma pending queue activated\n");
	return 0;

err_dma:
	dma_release_channel(info->dma);
	return ret;
}

static int __exit dev_remove(struct platform_device *pdev)
{
	struct dmatest_info *info = platform_get_drvdata(pdev);

	dev_info(&pdev->dev, "dev_remove()");
	if (info->dma) {
		if (info->tdowned)
			dmaengine_synchronize(info->dma);
		else
			dmaengine_terminate_sync(info->dma);
		dma_release_channel(info->dma);
	}
	if (info->buf)
		dma_free_coherent(&pdev->dev, PAGE_SIZE, info->buf, info->buf_dma);

	return 0;
}

static const struct of_device_id of_ids[] = {
	{ .compatible = "ti,dma_test" },
	{},
};
MODULE_DEVICE_TABLE(of, of_ids);

static struct platform_driver dmatest_driver = {
	.probe = dev_probe,
	.remove = dev_remove,
	.driver = {
		.name = "dmatest",
		.of_match_table = of_ids,
		.owner = THIS_MODULE,
	}
};

module_platform_driver(dmatest_driver);

MODULE_LICENSE("GPL v2");
