diff mbox series

[v5,2/2] dmaengine: Loongson1: Add Loongson1 dmaengine driver

Message ID 20230928121953.524608-3-keguang.zhang@gmail.com (mailing list archive)
State Changes Requested
Headers show
Series Add Loongson1 dmaengine driver | expand

Commit Message

Keguang Zhang Sept. 28, 2023, 12:19 p.m. UTC
This patch adds DMA Engine driver for Loongson1 SoCs.

Signed-off-by: Keguang Zhang <keguang.zhang@gmail.com>
---
V4 -> V5:
   Add DT support
   Use DT data instead of platform data
   Use chan_id of struct dma_chan instead of own id
   Use of_dma_xlate_by_chan_id() instead of ls1x_dma_filter()
   Update the author information to my official name
V3 -> V4:
   Use dma_slave_map to find the proper channel.
   Explicitly call devm_request_irq() and tasklet_kill().
   Fix namespace issue.
   Some minor fixes and cleanups.
V2 -> V3:
   Rename ls1x_dma_filter_fn to ls1x_dma_filter.
V1 -> V2:
   Change the config from 'DMA_LOONGSON1' to 'LOONGSON1_DMA',
   and rearrange it in alphabetical order in Kconfig and Makefile.
   Fix comment style.

 drivers/dma/Kconfig         |   9 +
 drivers/dma/Makefile        |   1 +
 drivers/dma/loongson1-dma.c | 492 ++++++++++++++++++++++++++++++++++++
 3 files changed, 502 insertions(+)
 create mode 100644 drivers/dma/loongson1-dma.c

Comments

Eric Schwarz Sept. 28, 2023, 12:46 p.m. UTC | #1
Hi,

just a quick catch down below for now.

Am 28.09.2023 um 14:19 schrieb Keguang Zhang:
> This patch adds DMA Engine driver for Loongson1 SoCs.
> 
> Signed-off-by: Keguang Zhang <keguang.zhang@gmail.com>
> ---
> V4 -> V5:
>     Add DT support
>     Use DT data instead of platform data
>     Use chan_id of struct dma_chan instead of own id
>     Use of_dma_xlate_by_chan_id() instead of ls1x_dma_filter()
>     Update the author information to my official name
> V3 -> V4:
>     Use dma_slave_map to find the proper channel.
>     Explicitly call devm_request_irq() and tasklet_kill().
>     Fix namespace issue.
>     Some minor fixes and cleanups.
> V2 -> V3:
>     Rename ls1x_dma_filter_fn to ls1x_dma_filter.
> V1 -> V2:
>     Change the config from 'DMA_LOONGSON1' to 'LOONGSON1_DMA',
>     and rearrange it in alphabetical order in Kconfig and Makefile.
>     Fix comment style.
> 
>   drivers/dma/Kconfig         |   9 +
>   drivers/dma/Makefile        |   1 +
>   drivers/dma/loongson1-dma.c | 492 ++++++++++++++++++++++++++++++++++++
>   3 files changed, 502 insertions(+)
>   create mode 100644 drivers/dma/loongson1-dma.c
> 
> diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig
> index 4ccae1a3b884..0b0d5c61b4a0 100644
> --- a/drivers/dma/Kconfig
> +++ b/drivers/dma/Kconfig
> @@ -369,6 +369,15 @@ config K3_DMA
>   	  Support the DMA engine for Hisilicon K3 platform
>   	  devices.
>   
> +config LOONGSON1_DMA
> +	tristate "Loongson1 DMA support"
> +	depends on MACH_LOONGSON32
> +	select DMA_ENGINE
> +	select DMA_VIRTUAL_CHANNELS
> +	help
> +	  This selects support for the DMA controller in Loongson1 SoCs,
> +	  which is required by Loongson1 NAND and AC97 support.
> +
>   config LPC18XX_DMAMUX
>   	bool "NXP LPC18xx/43xx DMA MUX for PL080"
>   	depends on ARCH_LPC18XX || COMPILE_TEST
> diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile
> index 83553a97a010..887103db5ee3 100644
> --- a/drivers/dma/Makefile
> +++ b/drivers/dma/Makefile
> @@ -47,6 +47,7 @@ obj-$(CONFIG_INTEL_IDMA64) += idma64.o
>   obj-$(CONFIG_INTEL_IOATDMA) += ioat/
>   obj-y += idxd/
>   obj-$(CONFIG_K3_DMA) += k3dma.o
> +obj-$(CONFIG_LOONGSON1_DMA) += loongson1-dma.o
>   obj-$(CONFIG_LPC18XX_DMAMUX) += lpc18xx-dmamux.o
>   obj-$(CONFIG_MILBEAUT_HDMAC) += milbeaut-hdmac.o
>   obj-$(CONFIG_MILBEAUT_XDMAC) += milbeaut-xdmac.o
> diff --git a/drivers/dma/loongson1-dma.c b/drivers/dma/loongson1-dma.c
> new file mode 100644
> index 000000000000..b589103d5ae0
> --- /dev/null
> +++ b/drivers/dma/loongson1-dma.c
> @@ -0,0 +1,492 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * DMA Driver for Loongson-1 SoC
> + *
> + * Copyright (C) 2015-2023 Keguang Zhang <keguang.zhang@gmail.com>
> + */
> +
> +#include <linux/dmapool.h>
> +#include <linux/init.h>
> +#include <linux/interrupt.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/of_dma.h>
> +#include <linux/platform_device.h>
> +#include <linux/slab.h>
> +
> +#include "dmaengine.h"
> +#include "virt-dma.h"
> +
> +/* Loongson 1 DMA Register Definitions */
> +#define LS1X_DMA_CTRL		0x0
> +
> +/* DMA Control Register Bits */
> +#define LS1X_DMA_STOP		BIT(4)
> +#define LS1X_DMA_START		BIT(3)
> +
> +#define LS1X_DMA_ADDR_MASK	GENMASK(31, 6)
> +
> +/* DMA Command Register Bits */
> +#define LS1X_DMA_RAM2DEV		BIT(12)
> +#define LS1X_DMA_TRANS_OVER		BIT(3)
> +#define LS1X_DMA_SINGLE_TRANS_OVER	BIT(2)
> +#define LS1X_DMA_INT			BIT(1)
> +#define LS1X_DMA_INT_MASK		BIT(0)
> +
> +#define LS1X_DMA_MAX_CHANNELS	3
> +
> +struct ls1x_dma_lli {
> +	u32 next;		/* next descriptor address */
> +	u32 saddr;		/* memory DMA address */
> +	u32 daddr;		/* device DMA address */
> +	u32 length;
> +	u32 stride;
> +	u32 cycles;
> +	u32 cmd;
> +} __aligned(64);
> +
> +struct ls1x_dma_hwdesc {
> +	struct ls1x_dma_lli *lli;
> +	dma_addr_t phys;
> +};
> +
> +struct ls1x_dma_desc {
> +	struct virt_dma_desc vdesc;
> +	struct ls1x_dma_chan *chan;
> +
> +	enum dma_transfer_direction dir;
> +	enum dma_transaction_type type;
> +
> +	unsigned int nr_descs;	/* number of descriptors */
> +	unsigned int nr_done;	/* number of completed descriptors */
> +	struct ls1x_dma_hwdesc hwdesc[];	/* DMA coherent descriptors */
> +};
> +
> +struct ls1x_dma_chan {
> +	struct virt_dma_chan vchan;
> +	struct dma_pool *desc_pool;
> +	struct dma_slave_config cfg;
> +
> +	void __iomem *reg_base;
> +	int irq;
> +
> +	struct ls1x_dma_desc *desc;
> +};
> +
> +struct ls1x_dma {
> +	struct dma_device ddev;
> +	void __iomem *reg_base;
> +
> +	unsigned int nr_chans;
> +	struct ls1x_dma_chan chan[];
> +};
> +
> +#define to_ls1x_dma_chan(dchan)		\
> +	container_of(dchan, struct ls1x_dma_chan, vchan.chan)
> +
> +#define to_ls1x_dma_desc(vdesc)		\
> +	container_of(vdesc, struct ls1x_dma_desc, vdesc)
> +
> +/* macros for registers read/write */
> +#define chan_readl(chan, off)		\
> +	readl((chan)->reg_base + (off))
> +
> +#define chan_writel(chan, off, val)	\
> +	writel((val), (chan)->reg_base + (off))
> +
> +static inline struct device *chan2dev(struct dma_chan *chan)
> +{
> +	return &chan->dev->device;
> +}
> +
> +static void ls1x_dma_free_chan_resources(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	vchan_free_chan_resources(&chan->vchan);
> +	dma_pool_destroy(chan->desc_pool);
> +	chan->desc_pool = NULL;
> +}
> +
> +static int ls1x_dma_alloc_chan_resources(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	chan->desc_pool = dma_pool_create(dma_chan_name(dchan),
> +					  dchan->device->dev,
> +					  sizeof(struct ls1x_dma_lli),
> +					  __alignof__(struct ls1x_dma_lli), 0);
> +	if (!chan->desc_pool)
> +		return -ENOMEM;
> +
> +	return 0;
> +}
> +
> +static void ls1x_dma_free_desc(struct virt_dma_desc *vdesc)
> +{
> +	struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vdesc);
> +
> +	if (desc->nr_descs) {
> +		unsigned int i = desc->nr_descs;
> +		struct ls1x_dma_hwdesc *hwdesc;
> +
> +		do {
> +			hwdesc = &desc->hwdesc[--i];
> +			dma_pool_free(desc->chan->desc_pool, hwdesc->lli,
> +				      hwdesc->phys);
> +		} while (i);
> +	}
> +
> +	kfree(desc);
> +}
> +
> +static struct ls1x_dma_desc *ls1x_dma_alloc_desc(struct ls1x_dma_chan *chan,
> +						 int sg_len)
> +{
> +	struct ls1x_dma_desc *desc;
> +
> +	desc = kzalloc(struct_size(desc, hwdesc, sg_len), GFP_NOWAIT);
> +
> +	return desc;
> +}
> +
> +static struct dma_async_tx_descriptor *
> +ls1x_dma_prep_slave_sg(struct dma_chan *dchan, struct scatterlist *sgl,
> +		       unsigned int sg_len,
> +		       enum dma_transfer_direction direction,
> +		       unsigned long flags, void *context)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +	struct dma_slave_config *cfg = &chan->cfg;
> +	struct ls1x_dma_desc *desc;
> +	struct scatterlist *sg;
> +	unsigned int dev_addr, bus_width, cmd, i;
> +
> +	if (!is_slave_direction(direction)) {
> +		dev_err(chan2dev(dchan), "invalid DMA direction!\n");
> +		return NULL;
> +	}
> +
> +	dev_dbg(chan2dev(dchan), "sg_len=%d, dir=%s, flags=0x%lx\n", sg_len,
> +		direction == DMA_MEM_TO_DEV ? "to device" : "from device",
> +		flags);
> +
> +	switch (direction) {
> +	case DMA_MEM_TO_DEV:
> +		dev_addr = cfg->dst_addr;
> +		bus_width = cfg->dst_addr_width;
> +		cmd = LS1X_DMA_RAM2DEV | LS1X_DMA_INT;
> +		break;
> +	case DMA_DEV_TO_MEM:
> +		dev_addr = cfg->src_addr;
> +		bus_width = cfg->src_addr_width;
> +		cmd = LS1X_DMA_INT;
> +		break;
> +	default:
> +		dev_err(chan2dev(dchan),
> +			"unsupported DMA transfer direction! %d\n", direction);
> +		return NULL;
> +	}
> +
> +	/* allocate DMA descriptor */
> +	desc = ls1x_dma_alloc_desc(chan, sg_len);
> +	if (!desc)
> +		return NULL;
> +
> +	for_each_sg(sgl, sg, sg_len, i) {
> +		dma_addr_t buf_addr = sg_dma_address(sg);
> +		size_t buf_len = sg_dma_len(sg);
> +		struct ls1x_dma_hwdesc *hwdesc = &desc->hwdesc[i];
> +		struct ls1x_dma_lli *lli;
> +
> +		if (!is_dma_copy_aligned(dchan->device, buf_addr, 0, buf_len)) {
> +			dev_err(chan2dev(dchan), "%s: buffer is not aligned!\n",
> +				__func__);
> +			goto err;
> +		}
> +
> +		/* allocate HW DMA descriptors */
> +		lli = dma_pool_alloc(chan->desc_pool, GFP_NOWAIT,
> +				     &hwdesc->phys);
> +		if (!lli) {
> +			dev_err(chan2dev(dchan),
> +				"%s: failed to alloc HW DMA descriptor!\n",
> +				__func__);
> +			goto err;
> +		}
> +		hwdesc->lli = lli;
> +
> +		/* config HW DMA descriptors */
> +		lli->saddr = buf_addr;
> +		lli->daddr = dev_addr;
> +		lli->length = buf_len / bus_width;
> +		lli->stride = 0;
> +		lli->cycles = 1;
> +		lli->cmd = cmd;
> +		lli->next = 0;
> +
> +		if (i)
> +			desc->hwdesc[i - 1].lli->next = hwdesc->phys;
> +
> +		dev_dbg(chan2dev(dchan),
> +			"hwdesc=%px, saddr=%08x, daddr=%08x, length=%u\n",
> +			hwdesc, buf_addr, dev_addr, buf_len);
> +	}
> +
> +	/* config DMA descriptor */
> +	desc->chan = chan;
> +	desc->dir = direction;
> +	desc->type = DMA_SLAVE;
> +	desc->nr_descs = sg_len;
> +	desc->nr_done = 0;
> +
> +	return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);
> +err:
> +	desc->nr_descs = i;
> +	ls1x_dma_free_desc(&desc->vdesc);
> +	return NULL;
> +}
> +
> +static int ls1x_dma_slave_config(struct dma_chan *dchan,
> +				 struct dma_slave_config *config)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	chan->cfg = *config;
> +
> +	return 0;
> +}
> +
> +static int ls1x_dma_terminate_all(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +	unsigned long flags;
> +	LIST_HEAD(head);
> +
> +	spin_lock_irqsave(&chan->vchan.lock, flags);
> +
> +	chan_writel(chan, LS1X_DMA_CTRL,
> +		    chan_readl(chan, LS1X_DMA_CTRL) | LS1X_DMA_STOP);
> +	chan->desc = NULL;
> +	vchan_get_all_descriptors(&chan->vchan, &head);
> +
> +	spin_unlock_irqrestore(&chan->vchan.lock, flags);
> +
> +	vchan_dma_desc_free_list(&chan->vchan, &head);
> +
> +	return 0;
> +}
> +
> +static void ls1x_dma_trigger(struct ls1x_dma_chan *chan)
> +{
> +	struct dma_chan *dchan = &chan->vchan.chan;
> +	struct ls1x_dma_desc *desc;
> +	struct virt_dma_desc *vdesc;
> +	unsigned int val;
> +
> +	vdesc = vchan_next_desc(&chan->vchan);
> +	if (!vdesc) {
> +		chan->desc = NULL;
> +		return;
> +	}
> +	chan->desc = desc = to_ls1x_dma_desc(vdesc);
> +
> +	dev_dbg(chan2dev(dchan), "cookie=%d, %u descs, starting hwdesc=%px\n",
> +		dchan->cookie, desc->nr_descs, &desc->hwdesc[0]);
> +
> +	val = desc->hwdesc[0].phys & LS1X_DMA_ADDR_MASK;
> +	val |= dchan->chan_id;
> +	val |= LS1X_DMA_START;
> +	chan_writel(chan, LS1X_DMA_CTRL, val);
> +}
> +
> +static void ls1x_dma_issue_pending(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&chan->vchan.lock, flags);
> +
> +	if (vchan_issue_pending(&chan->vchan) && !chan->desc)
> +		ls1x_dma_trigger(chan);
> +
> +	spin_unlock_irqrestore(&chan->vchan.lock, flags);
> +}
> +
> +static irqreturn_t ls1x_dma_irq_handler(int irq, void *data)
> +{
> +	struct ls1x_dma_chan *chan = data;
> +	struct dma_chan *dchan = &chan->vchan.chan;
> +
> +	dev_dbg(chan2dev(dchan), "DMA IRQ %d on channel %d\n", irq,
> +		dchan->chan_id);
> +	if (!chan->desc) {
> +		dev_warn(chan2dev(dchan),
> +			 "DMA IRQ with no active descriptor on channel %d\n",
> +			 dchan->chan_id);
> +		return IRQ_NONE;
> +	}
> +
> +	spin_lock(&chan->vchan.lock);
> +
> +	if (chan->desc->type == DMA_CYCLIC) {
> +		vchan_cyclic_callback(&chan->desc->vdesc);
> +	} else {
> +		list_del(&chan->desc->vdesc.node);
> +		vchan_cookie_complete(&chan->desc->vdesc);
> +		chan->desc = NULL;
> +	}
> +
> +	ls1x_dma_trigger(chan);
> +
> +	spin_unlock(&chan->vchan.lock);
> +	return IRQ_HANDLED;
> +}
> +
> +static int ls1x_dma_chan_probe(struct platform_device *pdev,
> +			       struct ls1x_dma *dma, int chan_id)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct ls1x_dma_chan *chan = &dma->chan[chan_id];
> +	char pdev_irqname[4];
> +	char *irqname;
> +	int ret;
> +
> +	sprintf(pdev_irqname, "ch%u", chan_id);
> +	chan->irq = platform_get_irq_byname(pdev, pdev_irqname);
> +	if (chan->irq < 0)
> +		return -ENODEV;
> +
> +	irqname = devm_kasprintf(dev, GFP_KERNEL, "%s:%s",
> +				 dev_name(dev), pdev_irqname);
> +	if (!irqname)
> +		return -ENOMEM;
> +
> +	ret = devm_request_irq(dev, chan->irq, ls1x_dma_irq_handler,
> +			       0, irqname, chan);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "failed to request IRQ %u!\n", chan->irq);
> +
> +	chan->reg_base = dma->reg_base;
> +	chan->vchan.desc_free = ls1x_dma_free_desc;
> +	vchan_init(&chan->vchan, &dma->ddev);
> +	dev_info(dev, "%s (irq %d) initialized\n", pdev_irqname, chan->irq);
> +
> +	return 0;
> +}
> +
> +static void ls1x_dma_chan_remove(struct ls1x_dma *dma, int chan_id)
> +{
> +	struct device *dev = dma->ddev.dev;
> +	struct ls1x_dma_chan *chan = &dma->chan[chan_id];
> +
> +	devm_free_irq(dev, chan->irq, chan);
> +	list_del(&chan->vchan.chan.device_node);
> +	tasklet_kill(&chan->vchan.task);
> +}
> +
> +static int ls1x_dma_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct dma_device *ddev;
> +	struct ls1x_dma *dma;
> +	int nr_chans, ret, i;
> +
> +	nr_chans = platform_irq_count(pdev);
> +	if (nr_chans <= 0)
> +		return nr_chans;
> +	if (nr_chans > LS1X_DMA_MAX_CHANNELS)
> +		return dev_err_probe(dev, -EINVAL,
> +				     "nr_chans=%d exceeds the maximum\n",
> +				     nr_chans);
> +
> +	dma = devm_kzalloc(dev, struct_size(dma, chan, nr_chans), GFP_KERNEL);
> +	if (!dma)
> +		return -ENOMEM;
> +
> +	/* initialize DMA device */
> +	dma->reg_base = devm_platform_ioremap_resource(pdev, 0);
> +	if (IS_ERR(dma->reg_base))
> +		return PTR_ERR(dma->reg_base);
> +
> +	ddev = &dma->ddev;
> +	ddev->dev = dev;
> +	ddev->copy_align = DMAENGINE_ALIGN_16_BYTES;
> +	ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
> +	ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
> +	ddev->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV);
> +	ddev->residue_granularity = DMA_RESIDUE_GRANULARITY_SEGMENT;
> +	ddev->device_alloc_chan_resources = ls1x_dma_alloc_chan_resources;
> +	ddev->device_free_chan_resources = ls1x_dma_free_chan_resources;
> +	ddev->device_prep_slave_sg = ls1x_dma_prep_slave_sg;
> +	ddev->device_config = ls1x_dma_slave_config;
> +	ddev->device_terminate_all = ls1x_dma_terminate_all;
> +	ddev->device_tx_status = dma_cookie_status;
> +	ddev->device_issue_pending = ls1x_dma_issue_pending;
> +
> +	dma_cap_set(DMA_SLAVE, ddev->cap_mask);
> +	INIT_LIST_HEAD(&ddev->channels);
> +
> +	/* initialize DMA channels */
> +	for (i = 0; i < nr_chans; i++) {
> +		ret = ls1x_dma_chan_probe(pdev, dma, i);
> +		if (ret)
> +			return ret;
> +	}
> +	dma->nr_chans = nr_chans;
> +
> +	ret = dmaenginem_async_device_register(ddev);
> +	if (ret) {
> +		dev_err(dev, "failed to register DMA device! %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret =
> +	    of_dma_controller_register(dev->of_node, of_dma_xlate_by_chan_id,
> +				       ddev);
> +	if (ret) {
> +		dev_err(dev, "failed to register DMA controller! %d\n", ret);
> +		return ret;
> +	}
> +
> +	platform_set_drvdata(pdev, dma);
> +	dev_info(dev, "Loongson1 DMA driver registered\n");
> +
> +	return 0;
> +}
> +
> +static int ls1x_dma_remove(struct platform_device *pdev)
> +{
> +	struct ls1x_dma *dma = platform_get_drvdata(pdev);
> +	int i;
> +
> +	of_dma_controller_free(pdev->dev.of_node);
> +
> +	for (i = 0; i < dma->nr_chans; i++)
> +		ls1x_dma_chan_remove(dma, i);
> +
> +	return 0;
> +}

Please check recently submitted patchset from Uwe Kleine-König 
<u.kleine-koenig@pengutronix.de> ("[PATCH 00/59] dma: Convert to 
platform remove callback returning void"). (Almost) all DMA drivers are 
now using void version of *_dma_remove().
The function is then hooked in struct platform_driver w/ .remove_new. 
The patchset was applied today by Vinod.

> +static const struct of_device_id ls1x_dma_match[] = {
> +	{ .compatible = "loongson,ls1b-dma" },
> +	{ .compatible = "loongson,ls1c-dma" },
> +	{ /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(of, ls1x_dma_match);
> +
> +static struct platform_driver ls1x_dma_driver = {
> +	.probe	= ls1x_dma_probe,
> +	.remove	= ls1x_dma_remove,

Use .remove_new here.

> +	.driver	= {
> +		.name	= "ls1x-dma",
> +		.of_match_table = ls1x_dma_match,
> +	},
> +};
> +
> +module_platform_driver(ls1x_dma_driver);
> +
> +MODULE_AUTHOR("Keguang Zhang <keguang.zhang@gmail.com>");
> +MODULE_DESCRIPTION("Loongson-1 DMA driver");
> +MODULE_LICENSE("GPL");

Cheers
Eric
Vinod Koul Oct. 4, 2023, 7:43 a.m. UTC | #2
On 28-09-23, 20:19, Keguang Zhang wrote:
> This patch adds DMA Engine driver for Loongson1 SoCs.
> 
> Signed-off-by: Keguang Zhang <keguang.zhang@gmail.com>
> ---
> V4 -> V5:
>    Add DT support
>    Use DT data instead of platform data
>    Use chan_id of struct dma_chan instead of own id
>    Use of_dma_xlate_by_chan_id() instead of ls1x_dma_filter()
>    Update the author information to my official name
> V3 -> V4:
>    Use dma_slave_map to find the proper channel.
>    Explicitly call devm_request_irq() and tasklet_kill().
>    Fix namespace issue.
>    Some minor fixes and cleanups.
> V2 -> V3:
>    Rename ls1x_dma_filter_fn to ls1x_dma_filter.
> V1 -> V2:
>    Change the config from 'DMA_LOONGSON1' to 'LOONGSON1_DMA',
>    and rearrange it in alphabetical order in Kconfig and Makefile.
>    Fix comment style.
> 
>  drivers/dma/Kconfig         |   9 +
>  drivers/dma/Makefile        |   1 +
>  drivers/dma/loongson1-dma.c | 492 ++++++++++++++++++++++++++++++++++++
>  3 files changed, 502 insertions(+)
>  create mode 100644 drivers/dma/loongson1-dma.c
> 
> diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig
> index 4ccae1a3b884..0b0d5c61b4a0 100644
> --- a/drivers/dma/Kconfig
> +++ b/drivers/dma/Kconfig
> @@ -369,6 +369,15 @@ config K3_DMA
>  	  Support the DMA engine for Hisilicon K3 platform
>  	  devices.
>  
> +config LOONGSON1_DMA
> +	tristate "Loongson1 DMA support"
> +	depends on MACH_LOONGSON32
> +	select DMA_ENGINE
> +	select DMA_VIRTUAL_CHANNELS
> +	help
> +	  This selects support for the DMA controller in Loongson1 SoCs,
> +	  which is required by Loongson1 NAND and AC97 support.
> +
>  config LPC18XX_DMAMUX
>  	bool "NXP LPC18xx/43xx DMA MUX for PL080"
>  	depends on ARCH_LPC18XX || COMPILE_TEST
> diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile
> index 83553a97a010..887103db5ee3 100644
> --- a/drivers/dma/Makefile
> +++ b/drivers/dma/Makefile
> @@ -47,6 +47,7 @@ obj-$(CONFIG_INTEL_IDMA64) += idma64.o
>  obj-$(CONFIG_INTEL_IOATDMA) += ioat/
>  obj-y += idxd/
>  obj-$(CONFIG_K3_DMA) += k3dma.o
> +obj-$(CONFIG_LOONGSON1_DMA) += loongson1-dma.o
>  obj-$(CONFIG_LPC18XX_DMAMUX) += lpc18xx-dmamux.o
>  obj-$(CONFIG_MILBEAUT_HDMAC) += milbeaut-hdmac.o
>  obj-$(CONFIG_MILBEAUT_XDMAC) += milbeaut-xdmac.o
> diff --git a/drivers/dma/loongson1-dma.c b/drivers/dma/loongson1-dma.c
> new file mode 100644
> index 000000000000..b589103d5ae0
> --- /dev/null
> +++ b/drivers/dma/loongson1-dma.c
> @@ -0,0 +1,492 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * DMA Driver for Loongson-1 SoC
> + *
> + * Copyright (C) 2015-2023 Keguang Zhang <keguang.zhang@gmail.com>
> + */
> +
> +#include <linux/dmapool.h>
> +#include <linux/init.h>
> +#include <linux/interrupt.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/of_dma.h>
> +#include <linux/platform_device.h>
> +#include <linux/slab.h>
> +
> +#include "dmaengine.h"
> +#include "virt-dma.h"
> +
> +/* Loongson 1 DMA Register Definitions */
> +#define LS1X_DMA_CTRL		0x0
> +
> +/* DMA Control Register Bits */
> +#define LS1X_DMA_STOP		BIT(4)
> +#define LS1X_DMA_START		BIT(3)
> +
> +#define LS1X_DMA_ADDR_MASK	GENMASK(31, 6)
> +
> +/* DMA Command Register Bits */
> +#define LS1X_DMA_RAM2DEV		BIT(12)
> +#define LS1X_DMA_TRANS_OVER		BIT(3)
> +#define LS1X_DMA_SINGLE_TRANS_OVER	BIT(2)
> +#define LS1X_DMA_INT			BIT(1)
> +#define LS1X_DMA_INT_MASK		BIT(0)
> +
> +#define LS1X_DMA_MAX_CHANNELS	3
> +
> +struct ls1x_dma_lli {
> +	u32 next;		/* next descriptor address */
> +	u32 saddr;		/* memory DMA address */
> +	u32 daddr;		/* device DMA address */
> +	u32 length;
> +	u32 stride;
> +	u32 cycles;
> +	u32 cmd;
> +} __aligned(64);
> +
> +struct ls1x_dma_hwdesc {
> +	struct ls1x_dma_lli *lli;
> +	dma_addr_t phys;
> +};
> +
> +struct ls1x_dma_desc {
> +	struct virt_dma_desc vdesc;
> +	struct ls1x_dma_chan *chan;
> +
> +	enum dma_transfer_direction dir;
> +	enum dma_transaction_type type;
> +
> +	unsigned int nr_descs;	/* number of descriptors */
> +	unsigned int nr_done;	/* number of completed descriptors */
> +	struct ls1x_dma_hwdesc hwdesc[];	/* DMA coherent descriptors */
> +};
> +
> +struct ls1x_dma_chan {
> +	struct virt_dma_chan vchan;
> +	struct dma_pool *desc_pool;
> +	struct dma_slave_config cfg;
> +
> +	void __iomem *reg_base;
> +	int irq;
> +
> +	struct ls1x_dma_desc *desc;
> +};
> +
> +struct ls1x_dma {
> +	struct dma_device ddev;
> +	void __iomem *reg_base;
> +
> +	unsigned int nr_chans;
> +	struct ls1x_dma_chan chan[];
> +};
> +
> +#define to_ls1x_dma_chan(dchan)		\
> +	container_of(dchan, struct ls1x_dma_chan, vchan.chan)
> +
> +#define to_ls1x_dma_desc(vdesc)		\
> +	container_of(vdesc, struct ls1x_dma_desc, vdesc)
> +
> +/* macros for registers read/write */
> +#define chan_readl(chan, off)		\
> +	readl((chan)->reg_base + (off))
> +
> +#define chan_writel(chan, off, val)	\
> +	writel((val), (chan)->reg_base + (off))
> +
> +static inline struct device *chan2dev(struct dma_chan *chan)
> +{
> +	return &chan->dev->device;
> +}
> +
> +static void ls1x_dma_free_chan_resources(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	vchan_free_chan_resources(&chan->vchan);
> +	dma_pool_destroy(chan->desc_pool);
> +	chan->desc_pool = NULL;
> +}
> +
> +static int ls1x_dma_alloc_chan_resources(struct dma_chan *dchan)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	chan->desc_pool = dma_pool_create(dma_chan_name(dchan),
> +					  dchan->device->dev,
> +					  sizeof(struct ls1x_dma_lli),
> +					  __alignof__(struct ls1x_dma_lli), 0);
> +	if (!chan->desc_pool)
> +		return -ENOMEM;
> +
> +	return 0;
> +}
> +
> +static void ls1x_dma_free_desc(struct virt_dma_desc *vdesc)
> +{
> +	struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vdesc);
> +
> +	if (desc->nr_descs) {
> +		unsigned int i = desc->nr_descs;
> +		struct ls1x_dma_hwdesc *hwdesc;
> +
> +		do {
> +			hwdesc = &desc->hwdesc[--i];
> +			dma_pool_free(desc->chan->desc_pool, hwdesc->lli,
> +				      hwdesc->phys);
> +		} while (i);
> +	}
> +
> +	kfree(desc);
> +}
> +
> +static struct ls1x_dma_desc *ls1x_dma_alloc_desc(struct ls1x_dma_chan *chan,
> +						 int sg_len)
> +{
> +	struct ls1x_dma_desc *desc;
> +
> +	desc = kzalloc(struct_size(desc, hwdesc, sg_len), GFP_NOWAIT);

why do you need a helper to do kzalloc?

> +
> +	return desc;
> +}
> +
> +static struct dma_async_tx_descriptor *
> +ls1x_dma_prep_slave_sg(struct dma_chan *dchan, struct scatterlist *sgl,
> +		       unsigned int sg_len,
> +		       enum dma_transfer_direction direction,
> +		       unsigned long flags, void *context)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +	struct dma_slave_config *cfg = &chan->cfg;
> +	struct ls1x_dma_desc *desc;
> +	struct scatterlist *sg;
> +	unsigned int dev_addr, bus_width, cmd, i;
> +
> +	if (!is_slave_direction(direction)) {
> +		dev_err(chan2dev(dchan), "invalid DMA direction!\n");
> +		return NULL;
> +	}
> +
> +	dev_dbg(chan2dev(dchan), "sg_len=%d, dir=%s, flags=0x%lx\n", sg_len,
> +		direction == DMA_MEM_TO_DEV ? "to device" : "from device",
> +		flags);
> +
> +	switch (direction) {
> +	case DMA_MEM_TO_DEV:
> +		dev_addr = cfg->dst_addr;
> +		bus_width = cfg->dst_addr_width;
> +		cmd = LS1X_DMA_RAM2DEV | LS1X_DMA_INT;
> +		break;
> +	case DMA_DEV_TO_MEM:
> +		dev_addr = cfg->src_addr;
> +		bus_width = cfg->src_addr_width;
> +		cmd = LS1X_DMA_INT;
> +		break;
> +	default:
> +		dev_err(chan2dev(dchan),
> +			"unsupported DMA transfer direction! %d\n", direction);
> +		return NULL;
> +	}
> +
> +	/* allocate DMA descriptor */
> +	desc = ls1x_dma_alloc_desc(chan, sg_len);
> +	if (!desc)
> +		return NULL;
> +
> +	for_each_sg(sgl, sg, sg_len, i) {
> +		dma_addr_t buf_addr = sg_dma_address(sg);
> +		size_t buf_len = sg_dma_len(sg);
> +		struct ls1x_dma_hwdesc *hwdesc = &desc->hwdesc[i];
> +		struct ls1x_dma_lli *lli;
> +
> +		if (!is_dma_copy_aligned(dchan->device, buf_addr, 0, buf_len)) {
> +			dev_err(chan2dev(dchan), "%s: buffer is not aligned!\n",
> +				__func__);
> +			goto err;
> +		}
> +
> +		/* allocate HW DMA descriptors */
> +		lli = dma_pool_alloc(chan->desc_pool, GFP_NOWAIT,
> +				     &hwdesc->phys);
> +		if (!lli) {
> +			dev_err(chan2dev(dchan),
> +				"%s: failed to alloc HW DMA descriptor!\n",
> +				__func__);
> +			goto err;
> +		}
> +		hwdesc->lli = lli;
> +
> +		/* config HW DMA descriptors */
> +		lli->saddr = buf_addr;
> +		lli->daddr = dev_addr;
> +		lli->length = buf_len / bus_width;
> +		lli->stride = 0;
> +		lli->cycles = 1;
> +		lli->cmd = cmd;
> +		lli->next = 0;
> +
> +		if (i)
> +			desc->hwdesc[i - 1].lli->next = hwdesc->phys;
> +
> +		dev_dbg(chan2dev(dchan),
> +			"hwdesc=%px, saddr=%08x, daddr=%08x, length=%u\n",
> +			hwdesc, buf_addr, dev_addr, buf_len);
> +	}
> +
> +	/* config DMA descriptor */
> +	desc->chan = chan;
> +	desc->dir = direction;
> +	desc->type = DMA_SLAVE;
> +	desc->nr_descs = sg_len;
> +	desc->nr_done = 0;
> +
> +	return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);
> +err:
> +	desc->nr_descs = i;
> +	ls1x_dma_free_desc(&desc->vdesc);
> +	return NULL;
> +}
> +
> +static int ls1x_dma_slave_config(struct dma_chan *dchan,
> +				 struct dma_slave_config *config)
> +{
> +	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
> +
> +	chan->cfg = *config;

You are using only addr and width, why keep full structure?
diff mbox series

Patch

diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig
index 4ccae1a3b884..0b0d5c61b4a0 100644
--- a/drivers/dma/Kconfig
+++ b/drivers/dma/Kconfig
@@ -369,6 +369,15 @@  config K3_DMA
 	  Support the DMA engine for Hisilicon K3 platform
 	  devices.
 
+config LOONGSON1_DMA
+	tristate "Loongson1 DMA support"
+	depends on MACH_LOONGSON32
+	select DMA_ENGINE
+	select DMA_VIRTUAL_CHANNELS
+	help
+	  This selects support for the DMA controller in Loongson1 SoCs,
+	  which is required by Loongson1 NAND and AC97 support.
+
 config LPC18XX_DMAMUX
 	bool "NXP LPC18xx/43xx DMA MUX for PL080"
 	depends on ARCH_LPC18XX || COMPILE_TEST
diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile
index 83553a97a010..887103db5ee3 100644
--- a/drivers/dma/Makefile
+++ b/drivers/dma/Makefile
@@ -47,6 +47,7 @@  obj-$(CONFIG_INTEL_IDMA64) += idma64.o
 obj-$(CONFIG_INTEL_IOATDMA) += ioat/
 obj-y += idxd/
 obj-$(CONFIG_K3_DMA) += k3dma.o
+obj-$(CONFIG_LOONGSON1_DMA) += loongson1-dma.o
 obj-$(CONFIG_LPC18XX_DMAMUX) += lpc18xx-dmamux.o
 obj-$(CONFIG_MILBEAUT_HDMAC) += milbeaut-hdmac.o
 obj-$(CONFIG_MILBEAUT_XDMAC) += milbeaut-xdmac.o
diff --git a/drivers/dma/loongson1-dma.c b/drivers/dma/loongson1-dma.c
new file mode 100644
index 000000000000..b589103d5ae0
--- /dev/null
+++ b/drivers/dma/loongson1-dma.c
@@ -0,0 +1,492 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * DMA Driver for Loongson-1 SoC
+ *
+ * Copyright (C) 2015-2023 Keguang Zhang <keguang.zhang@gmail.com>
+ */
+
+#include <linux/dmapool.h>
+#include <linux/init.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_dma.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+
+#include "dmaengine.h"
+#include "virt-dma.h"
+
+/* Loongson 1 DMA Register Definitions */
+#define LS1X_DMA_CTRL		0x0
+
+/* DMA Control Register Bits */
+#define LS1X_DMA_STOP		BIT(4)
+#define LS1X_DMA_START		BIT(3)
+
+#define LS1X_DMA_ADDR_MASK	GENMASK(31, 6)
+
+/* DMA Command Register Bits */
+#define LS1X_DMA_RAM2DEV		BIT(12)
+#define LS1X_DMA_TRANS_OVER		BIT(3)
+#define LS1X_DMA_SINGLE_TRANS_OVER	BIT(2)
+#define LS1X_DMA_INT			BIT(1)
+#define LS1X_DMA_INT_MASK		BIT(0)
+
+#define LS1X_DMA_MAX_CHANNELS	3
+
+struct ls1x_dma_lli {
+	u32 next;		/* next descriptor address */
+	u32 saddr;		/* memory DMA address */
+	u32 daddr;		/* device DMA address */
+	u32 length;
+	u32 stride;
+	u32 cycles;
+	u32 cmd;
+} __aligned(64);
+
+struct ls1x_dma_hwdesc {
+	struct ls1x_dma_lli *lli;
+	dma_addr_t phys;
+};
+
+struct ls1x_dma_desc {
+	struct virt_dma_desc vdesc;
+	struct ls1x_dma_chan *chan;
+
+	enum dma_transfer_direction dir;
+	enum dma_transaction_type type;
+
+	unsigned int nr_descs;	/* number of descriptors */
+	unsigned int nr_done;	/* number of completed descriptors */
+	struct ls1x_dma_hwdesc hwdesc[];	/* DMA coherent descriptors */
+};
+
+struct ls1x_dma_chan {
+	struct virt_dma_chan vchan;
+	struct dma_pool *desc_pool;
+	struct dma_slave_config cfg;
+
+	void __iomem *reg_base;
+	int irq;
+
+	struct ls1x_dma_desc *desc;
+};
+
+struct ls1x_dma {
+	struct dma_device ddev;
+	void __iomem *reg_base;
+
+	unsigned int nr_chans;
+	struct ls1x_dma_chan chan[];
+};
+
+#define to_ls1x_dma_chan(dchan)		\
+	container_of(dchan, struct ls1x_dma_chan, vchan.chan)
+
+#define to_ls1x_dma_desc(vdesc)		\
+	container_of(vdesc, struct ls1x_dma_desc, vdesc)
+
+/* macros for registers read/write */
+#define chan_readl(chan, off)		\
+	readl((chan)->reg_base + (off))
+
+#define chan_writel(chan, off, val)	\
+	writel((val), (chan)->reg_base + (off))
+
+static inline struct device *chan2dev(struct dma_chan *chan)
+{
+	return &chan->dev->device;
+}
+
+static void ls1x_dma_free_chan_resources(struct dma_chan *dchan)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+
+	vchan_free_chan_resources(&chan->vchan);
+	dma_pool_destroy(chan->desc_pool);
+	chan->desc_pool = NULL;
+}
+
+static int ls1x_dma_alloc_chan_resources(struct dma_chan *dchan)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+
+	chan->desc_pool = dma_pool_create(dma_chan_name(dchan),
+					  dchan->device->dev,
+					  sizeof(struct ls1x_dma_lli),
+					  __alignof__(struct ls1x_dma_lli), 0);
+	if (!chan->desc_pool)
+		return -ENOMEM;
+
+	return 0;
+}
+
+static void ls1x_dma_free_desc(struct virt_dma_desc *vdesc)
+{
+	struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vdesc);
+
+	if (desc->nr_descs) {
+		unsigned int i = desc->nr_descs;
+		struct ls1x_dma_hwdesc *hwdesc;
+
+		do {
+			hwdesc = &desc->hwdesc[--i];
+			dma_pool_free(desc->chan->desc_pool, hwdesc->lli,
+				      hwdesc->phys);
+		} while (i);
+	}
+
+	kfree(desc);
+}
+
+static struct ls1x_dma_desc *ls1x_dma_alloc_desc(struct ls1x_dma_chan *chan,
+						 int sg_len)
+{
+	struct ls1x_dma_desc *desc;
+
+	desc = kzalloc(struct_size(desc, hwdesc, sg_len), GFP_NOWAIT);
+
+	return desc;
+}
+
+static struct dma_async_tx_descriptor *
+ls1x_dma_prep_slave_sg(struct dma_chan *dchan, struct scatterlist *sgl,
+		       unsigned int sg_len,
+		       enum dma_transfer_direction direction,
+		       unsigned long flags, void *context)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+	struct dma_slave_config *cfg = &chan->cfg;
+	struct ls1x_dma_desc *desc;
+	struct scatterlist *sg;
+	unsigned int dev_addr, bus_width, cmd, i;
+
+	if (!is_slave_direction(direction)) {
+		dev_err(chan2dev(dchan), "invalid DMA direction!\n");
+		return NULL;
+	}
+
+	dev_dbg(chan2dev(dchan), "sg_len=%d, dir=%s, flags=0x%lx\n", sg_len,
+		direction == DMA_MEM_TO_DEV ? "to device" : "from device",
+		flags);
+
+	switch (direction) {
+	case DMA_MEM_TO_DEV:
+		dev_addr = cfg->dst_addr;
+		bus_width = cfg->dst_addr_width;
+		cmd = LS1X_DMA_RAM2DEV | LS1X_DMA_INT;
+		break;
+	case DMA_DEV_TO_MEM:
+		dev_addr = cfg->src_addr;
+		bus_width = cfg->src_addr_width;
+		cmd = LS1X_DMA_INT;
+		break;
+	default:
+		dev_err(chan2dev(dchan),
+			"unsupported DMA transfer direction! %d\n", direction);
+		return NULL;
+	}
+
+	/* allocate DMA descriptor */
+	desc = ls1x_dma_alloc_desc(chan, sg_len);
+	if (!desc)
+		return NULL;
+
+	for_each_sg(sgl, sg, sg_len, i) {
+		dma_addr_t buf_addr = sg_dma_address(sg);
+		size_t buf_len = sg_dma_len(sg);
+		struct ls1x_dma_hwdesc *hwdesc = &desc->hwdesc[i];
+		struct ls1x_dma_lli *lli;
+
+		if (!is_dma_copy_aligned(dchan->device, buf_addr, 0, buf_len)) {
+			dev_err(chan2dev(dchan), "%s: buffer is not aligned!\n",
+				__func__);
+			goto err;
+		}
+
+		/* allocate HW DMA descriptors */
+		lli = dma_pool_alloc(chan->desc_pool, GFP_NOWAIT,
+				     &hwdesc->phys);
+		if (!lli) {
+			dev_err(chan2dev(dchan),
+				"%s: failed to alloc HW DMA descriptor!\n",
+				__func__);
+			goto err;
+		}
+		hwdesc->lli = lli;
+
+		/* config HW DMA descriptors */
+		lli->saddr = buf_addr;
+		lli->daddr = dev_addr;
+		lli->length = buf_len / bus_width;
+		lli->stride = 0;
+		lli->cycles = 1;
+		lli->cmd = cmd;
+		lli->next = 0;
+
+		if (i)
+			desc->hwdesc[i - 1].lli->next = hwdesc->phys;
+
+		dev_dbg(chan2dev(dchan),
+			"hwdesc=%px, saddr=%08x, daddr=%08x, length=%u\n",
+			hwdesc, buf_addr, dev_addr, buf_len);
+	}
+
+	/* config DMA descriptor */
+	desc->chan = chan;
+	desc->dir = direction;
+	desc->type = DMA_SLAVE;
+	desc->nr_descs = sg_len;
+	desc->nr_done = 0;
+
+	return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);
+err:
+	desc->nr_descs = i;
+	ls1x_dma_free_desc(&desc->vdesc);
+	return NULL;
+}
+
+static int ls1x_dma_slave_config(struct dma_chan *dchan,
+				 struct dma_slave_config *config)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+
+	chan->cfg = *config;
+
+	return 0;
+}
+
+static int ls1x_dma_terminate_all(struct dma_chan *dchan)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+	unsigned long flags;
+	LIST_HEAD(head);
+
+	spin_lock_irqsave(&chan->vchan.lock, flags);
+
+	chan_writel(chan, LS1X_DMA_CTRL,
+		    chan_readl(chan, LS1X_DMA_CTRL) | LS1X_DMA_STOP);
+	chan->desc = NULL;
+	vchan_get_all_descriptors(&chan->vchan, &head);
+
+	spin_unlock_irqrestore(&chan->vchan.lock, flags);
+
+	vchan_dma_desc_free_list(&chan->vchan, &head);
+
+	return 0;
+}
+
+static void ls1x_dma_trigger(struct ls1x_dma_chan *chan)
+{
+	struct dma_chan *dchan = &chan->vchan.chan;
+	struct ls1x_dma_desc *desc;
+	struct virt_dma_desc *vdesc;
+	unsigned int val;
+
+	vdesc = vchan_next_desc(&chan->vchan);
+	if (!vdesc) {
+		chan->desc = NULL;
+		return;
+	}
+	chan->desc = desc = to_ls1x_dma_desc(vdesc);
+
+	dev_dbg(chan2dev(dchan), "cookie=%d, %u descs, starting hwdesc=%px\n",
+		dchan->cookie, desc->nr_descs, &desc->hwdesc[0]);
+
+	val = desc->hwdesc[0].phys & LS1X_DMA_ADDR_MASK;
+	val |= dchan->chan_id;
+	val |= LS1X_DMA_START;
+	chan_writel(chan, LS1X_DMA_CTRL, val);
+}
+
+static void ls1x_dma_issue_pending(struct dma_chan *dchan)
+{
+	struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
+	unsigned long flags;
+
+	spin_lock_irqsave(&chan->vchan.lock, flags);
+
+	if (vchan_issue_pending(&chan->vchan) && !chan->desc)
+		ls1x_dma_trigger(chan);
+
+	spin_unlock_irqrestore(&chan->vchan.lock, flags);
+}
+
+static irqreturn_t ls1x_dma_irq_handler(int irq, void *data)
+{
+	struct ls1x_dma_chan *chan = data;
+	struct dma_chan *dchan = &chan->vchan.chan;
+
+	dev_dbg(chan2dev(dchan), "DMA IRQ %d on channel %d\n", irq,
+		dchan->chan_id);
+	if (!chan->desc) {
+		dev_warn(chan2dev(dchan),
+			 "DMA IRQ with no active descriptor on channel %d\n",
+			 dchan->chan_id);
+		return IRQ_NONE;
+	}
+
+	spin_lock(&chan->vchan.lock);
+
+	if (chan->desc->type == DMA_CYCLIC) {
+		vchan_cyclic_callback(&chan->desc->vdesc);
+	} else {
+		list_del(&chan->desc->vdesc.node);
+		vchan_cookie_complete(&chan->desc->vdesc);
+		chan->desc = NULL;
+	}
+
+	ls1x_dma_trigger(chan);
+
+	spin_unlock(&chan->vchan.lock);
+	return IRQ_HANDLED;
+}
+
+static int ls1x_dma_chan_probe(struct platform_device *pdev,
+			       struct ls1x_dma *dma, int chan_id)
+{
+	struct device *dev = &pdev->dev;
+	struct ls1x_dma_chan *chan = &dma->chan[chan_id];
+	char pdev_irqname[4];
+	char *irqname;
+	int ret;
+
+	sprintf(pdev_irqname, "ch%u", chan_id);
+	chan->irq = platform_get_irq_byname(pdev, pdev_irqname);
+	if (chan->irq < 0)
+		return -ENODEV;
+
+	irqname = devm_kasprintf(dev, GFP_KERNEL, "%s:%s",
+				 dev_name(dev), pdev_irqname);
+	if (!irqname)
+		return -ENOMEM;
+
+	ret = devm_request_irq(dev, chan->irq, ls1x_dma_irq_handler,
+			       0, irqname, chan);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to request IRQ %u!\n", chan->irq);
+
+	chan->reg_base = dma->reg_base;
+	chan->vchan.desc_free = ls1x_dma_free_desc;
+	vchan_init(&chan->vchan, &dma->ddev);
+	dev_info(dev, "%s (irq %d) initialized\n", pdev_irqname, chan->irq);
+
+	return 0;
+}
+
+static void ls1x_dma_chan_remove(struct ls1x_dma *dma, int chan_id)
+{
+	struct device *dev = dma->ddev.dev;
+	struct ls1x_dma_chan *chan = &dma->chan[chan_id];
+
+	devm_free_irq(dev, chan->irq, chan);
+	list_del(&chan->vchan.chan.device_node);
+	tasklet_kill(&chan->vchan.task);
+}
+
+static int ls1x_dma_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct dma_device *ddev;
+	struct ls1x_dma *dma;
+	int nr_chans, ret, i;
+
+	nr_chans = platform_irq_count(pdev);
+	if (nr_chans <= 0)
+		return nr_chans;
+	if (nr_chans > LS1X_DMA_MAX_CHANNELS)
+		return dev_err_probe(dev, -EINVAL,
+				     "nr_chans=%d exceeds the maximum\n",
+				     nr_chans);
+
+	dma = devm_kzalloc(dev, struct_size(dma, chan, nr_chans), GFP_KERNEL);
+	if (!dma)
+		return -ENOMEM;
+
+	/* initialize DMA device */
+	dma->reg_base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(dma->reg_base))
+		return PTR_ERR(dma->reg_base);
+
+	ddev = &dma->ddev;
+	ddev->dev = dev;
+	ddev->copy_align = DMAENGINE_ALIGN_16_BYTES;
+	ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
+	ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
+	ddev->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV);
+	ddev->residue_granularity = DMA_RESIDUE_GRANULARITY_SEGMENT;
+	ddev->device_alloc_chan_resources = ls1x_dma_alloc_chan_resources;
+	ddev->device_free_chan_resources = ls1x_dma_free_chan_resources;
+	ddev->device_prep_slave_sg = ls1x_dma_prep_slave_sg;
+	ddev->device_config = ls1x_dma_slave_config;
+	ddev->device_terminate_all = ls1x_dma_terminate_all;
+	ddev->device_tx_status = dma_cookie_status;
+	ddev->device_issue_pending = ls1x_dma_issue_pending;
+
+	dma_cap_set(DMA_SLAVE, ddev->cap_mask);
+	INIT_LIST_HEAD(&ddev->channels);
+
+	/* initialize DMA channels */
+	for (i = 0; i < nr_chans; i++) {
+		ret = ls1x_dma_chan_probe(pdev, dma, i);
+		if (ret)
+			return ret;
+	}
+	dma->nr_chans = nr_chans;
+
+	ret = dmaenginem_async_device_register(ddev);
+	if (ret) {
+		dev_err(dev, "failed to register DMA device! %d\n", ret);
+		return ret;
+	}
+
+	ret =
+	    of_dma_controller_register(dev->of_node, of_dma_xlate_by_chan_id,
+				       ddev);
+	if (ret) {
+		dev_err(dev, "failed to register DMA controller! %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, dma);
+	dev_info(dev, "Loongson1 DMA driver registered\n");
+
+	return 0;
+}
+
+static int ls1x_dma_remove(struct platform_device *pdev)
+{
+	struct ls1x_dma *dma = platform_get_drvdata(pdev);
+	int i;
+
+	of_dma_controller_free(pdev->dev.of_node);
+
+	for (i = 0; i < dma->nr_chans; i++)
+		ls1x_dma_chan_remove(dma, i);
+
+	return 0;
+}
+
+static const struct of_device_id ls1x_dma_match[] = {
+	{ .compatible = "loongson,ls1b-dma" },
+	{ .compatible = "loongson,ls1c-dma" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, ls1x_dma_match);
+
+static struct platform_driver ls1x_dma_driver = {
+	.probe	= ls1x_dma_probe,
+	.remove	= ls1x_dma_remove,
+	.driver	= {
+		.name	= "ls1x-dma",
+		.of_match_table = ls1x_dma_match,
+	},
+};
+
+module_platform_driver(ls1x_dma_driver);
+
+MODULE_AUTHOR("Keguang Zhang <keguang.zhang@gmail.com>");
+MODULE_DESCRIPTION("Loongson-1 DMA driver");
+MODULE_LICENSE("GPL");