This is a sample implementation of a driver using "avalon-dma" to perform data transfers between target device memory and system memory: +----------+ +----------+ +----------+ | RAM |<-->| Avalon |<---PCIe--->| Host | +----------+ +----------+ +----------+ The target device is expected to use only Avalon-MM DMA Interface for PCIe to initiate DMA transactions - without custom hardware specifics to make such transfers possible. Unlike "dmatest" driver, the contents of DMAed data is not manipulated by "avalon-test" in any way. It is basically pass-through and the the data are fully dependent on the target device implementation. Thus, it is up to the users to analyze received or provide meaningful transmitted data. CC: dmaengine@xxxxxxxxxxxxxxx Reported-by: kbuild test robot <lkp@xxxxxxxxx> Signed-off-by: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> --- drivers/dma/Kconfig | 1 + drivers/dma/Makefile | 1 + drivers/dma/avalon-test/Kconfig | 12 + drivers/dma/avalon-test/Makefile | 14 + drivers/dma/avalon-test/avalon-dev.c | 108 +++++ drivers/dma/avalon-test/avalon-dev.h | 33 ++ drivers/dma/avalon-test/avalon-ioctl.c | 101 +++++ drivers/dma/avalon-test/avalon-ioctl.h | 13 + drivers/dma/avalon-test/avalon-mmap.c | 74 +++ drivers/dma/avalon-test/avalon-mmap.h | 13 + drivers/dma/avalon-test/avalon-sg-buf.c | 132 ++++++ drivers/dma/avalon-test/avalon-sg-buf.h | 27 ++ drivers/dma/avalon-test/avalon-xfer.c | 575 ++++++++++++++++++++++++ drivers/dma/avalon-test/avalon-xfer.h | 29 ++ include/uapi/linux/avalon-ioctl.h | 34 ++ 15 files changed, 1167 insertions(+) create mode 100644 drivers/dma/avalon-test/Kconfig create mode 100644 drivers/dma/avalon-test/Makefile create mode 100644 drivers/dma/avalon-test/avalon-dev.c create mode 100644 drivers/dma/avalon-test/avalon-dev.h create mode 100644 drivers/dma/avalon-test/avalon-ioctl.c create mode 100644 drivers/dma/avalon-test/avalon-ioctl.h create mode 100644 drivers/dma/avalon-test/avalon-mmap.c create mode 100644 drivers/dma/avalon-test/avalon-mmap.h create mode 100644 drivers/dma/avalon-test/avalon-sg-buf.c create mode 100644 drivers/dma/avalon-test/avalon-sg-buf.h create mode 100644 drivers/dma/avalon-test/avalon-xfer.c create mode 100644 drivers/dma/avalon-test/avalon-xfer.h create mode 100644 include/uapi/linux/avalon-ioctl.h diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig index f6f43480a4a4..4b3c6a6baf4c 100644 --- a/drivers/dma/Kconfig +++ b/drivers/dma/Kconfig @@ -670,6 +670,7 @@ source "drivers/dma/sh/Kconfig" source "drivers/dma/ti/Kconfig" source "drivers/dma/avalon/Kconfig" +source "drivers/dma/avalon-test/Kconfig" # clients comment "DMA Clients" diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile index fd7e11417b73..eb3ee7f6cac6 100644 --- a/drivers/dma/Makefile +++ b/drivers/dma/Makefile @@ -76,6 +76,7 @@ obj-$(CONFIG_XGENE_DMA) += xgene-dma.o obj-$(CONFIG_ZX_DMA) += zx_dma.o obj-$(CONFIG_ST_FDMA) += st_fdma.o obj-$(CONFIG_AVALON_DMA) += avalon/ +obj-$(CONFIG_AVALON_TEST) += avalon-test/ obj-y += mediatek/ obj-y += qcom/ diff --git a/drivers/dma/avalon-test/Kconfig b/drivers/dma/avalon-test/Kconfig new file mode 100644 index 000000000000..b4d22720ce23 --- /dev/null +++ b/drivers/dma/avalon-test/Kconfig @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (c) 2019, The Linux Foundation. All rights reserved. +# Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> +# +# Avalon DMA engine +# +config AVALON_TEST + select AVALON_DMA + tristate "Intel Avalon-MM DMA Interface for PCIe test driver" + help + This selects a test driver for Avalon-MM DMA Interface for PCI diff --git a/drivers/dma/avalon-test/Makefile b/drivers/dma/avalon-test/Makefile new file mode 100644 index 000000000000..2387fc41c3ad --- /dev/null +++ b/drivers/dma/avalon-test/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (c) 2019, The Linux Foundation. All rights reserved. +# Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> +# +# Avalon DMA driver +# +obj-$(CONFIG_AVALON_TEST) += avalon-test.o + +avalon-test-objs := avalon-dev.o \ + avalon-ioctl.o \ + avalon-mmap.o \ + avalon-sg-buf.o \ + avalon-xfer.o diff --git a/drivers/dma/avalon-test/avalon-dev.c b/drivers/dma/avalon-test/avalon-dev.c new file mode 100644 index 000000000000..937ac3663efd --- /dev/null +++ b/drivers/dma/avalon-test/avalon-dev.c @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/fs.h> +#include <linux/pci.h> + +#include "avalon-dev.h" +#include "avalon-ioctl.h" +#include "avalon-mmap.h" + +unsigned int mem_base = 0x70000000; +module_param(mem_base, uint, 0644); +MODULE_PARM_DESC(mem_base, "Device memory base (default: 0x70000000)"); + +unsigned int mem_size = 0x10000000; +module_param(mem_size, uint, 0644); +MODULE_PARM_DESC(mem_size, "Device memory size (default: 0x10000000)"); + +unsigned int dma_size = 0x200000; +module_param(dma_size, uint, 0644); +MODULE_PARM_DESC(dma_size, "DMA buffer transfer size (default: 0x200000)"); + +unsigned int dma_size_sg = 0x10000000; +module_param(dma_size_sg, uint, 0644); +MODULE_PARM_DESC(dma_size_sg, + "DMA scatter list transfer size (default: 0x10000000)"); + +unsigned int nr_dma_reps = 4; +module_param(nr_dma_reps, uint, 0644); +MODULE_PARM_DESC(nr_dma_reps, "Device memory size (default: 4)"); + +unsigned int dmas_per_cpu = 8; +module_param(dmas_per_cpu, uint, 0644); +MODULE_PARM_DESC(dmas_per_cpu, "Device memory size (default: 8)"); + +static const struct file_operations avalon_dev_fops = { + .llseek = generic_file_llseek, + .unlocked_ioctl = avalon_dev_ioctl, + .mmap = avalon_dev_mmap, +}; + +static struct avalon_dev avalon_dev; + +static bool filter(struct dma_chan *chan, void *filter_param) +{ + return !strcmp(chan->device->dev->driver->name, "avalon-dma"); +} + +static int __init avalon_drv_init(void) +{ + struct avalon_dev *adev = &avalon_dev; + struct dma_chan *chan; + dma_cap_mask_t mask; + int ret; + + if (!IS_ALIGNED(mem_base, PAGE_SIZE) || + !IS_ALIGNED(mem_size, PAGE_SIZE) || + !IS_ALIGNED(dma_size, sizeof(u32)) || + !IS_ALIGNED(dma_size_sg, sizeof(u32))) + return -EINVAL; + + dma_cap_zero(mask); + dma_cap_set(DMA_SLAVE, mask); + + chan = dma_request_channel(mask, filter, NULL); + if (!chan) + return -ENODEV; + + adev->dma_chan = chan; + + adev->misc_dev.minor = MISC_DYNAMIC_MINOR; + adev->misc_dev.name = DEVICE_NAME; + adev->misc_dev.nodename = DEVICE_NAME; + adev->misc_dev.fops = &avalon_dev_fops; + adev->misc_dev.mode = 0644; + + ret = misc_register(&adev->misc_dev); + if (ret) { + dma_release_channel(chan); + return ret; + } + + dma_size = min(dma_size_sg, mem_size); + dma_size_sg = min(dma_size_sg, mem_size); + + return 0; +} + +static void __exit avalon_drv_exit(void) +{ + struct avalon_dev *adev = &avalon_dev; + + misc_deregister(&adev->misc_dev); + dma_release_channel(adev->dma_chan); +} + +module_init(avalon_drv_init); +module_exit(avalon_drv_exit); + +MODULE_AUTHOR("Alexander Gordeev <a.gordeev.box@xxxxxxxxx>"); +MODULE_DESCRIPTION("Avalon DMA control driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/dma/avalon-test/avalon-dev.h b/drivers/dma/avalon-test/avalon-dev.h new file mode 100644 index 000000000000..ad8f2f5717fa --- /dev/null +++ b/drivers/dma/avalon-test/avalon-dev.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#ifndef __AVALON_DEV_H__ +#define __AVALON_DEV_H__ + +#include <linux/dmaengine.h> +#include <linux/miscdevice.h> + +#define DEVICE_NAME "avalon-dev" + +extern unsigned int mem_base; +extern unsigned int mem_size; +extern unsigned int dma_size; +extern unsigned int dma_size_sg; +extern unsigned int nr_dma_reps; +extern unsigned int dmas_per_cpu; + +struct avalon_dev { + struct dma_chan *dma_chan; + struct miscdevice misc_dev; +}; + +static inline struct device *chan_to_dev(struct dma_chan *chan) +{ + return chan->device->dev; +} + +#endif diff --git a/drivers/dma/avalon-test/avalon-ioctl.c b/drivers/dma/avalon-test/avalon-ioctl.c new file mode 100644 index 000000000000..39a6a7050ee5 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-ioctl.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/fs.h> +#include <linux/uio.h> + +#include <uapi/linux/avalon-ioctl.h> + +#include "avalon-ioctl.h" +#include "avalon-xfer.h" + +long avalon_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct avalon_dev *adev = container_of(file->private_data, + struct avalon_dev, misc_dev); + struct dma_chan *chan = adev->dma_chan; + struct iovec iovec[2]; + void __user *buf = NULL, *buf_rd = NULL, *buf_wr = NULL; + size_t len = 0, len_rd = 0, len_wr = 0; + int ret = -EINVAL; + + switch (cmd) { + case IOCTL_AVALON_DMA_GET_INFO: { + struct avalon_dma_info info = { + .mem_addr = mem_base, + .mem_size = mem_size, + .dma_size = dma_size, + .dma_size_sg = dma_size_sg, + }; + + if (copy_to_user((void __user *)arg, &info, sizeof(info))) + return -EFAULT; + + return 0; + } + case IOCTL_AVALON_DMA_SET_INFO: + return -EINVAL; + case IOCTL_AVALON_DMA_READ: + case IOCTL_AVALON_DMA_WRITE: + case IOCTL_AVALON_DMA_READ_SG: + case IOCTL_AVALON_DMA_WRITE_SG: + case IOCTL_AVALON_DMA_READ_SG_SMP: + case IOCTL_AVALON_DMA_WRITE_SG_SMP: + if (copy_from_user(iovec, (void __user *)arg, sizeof(iovec[0]))) + return -EFAULT; + + buf = iovec[0].iov_base; + len = iovec[0].iov_len; + + break; + case IOCTL_AVALON_DMA_RDWR: + case IOCTL_AVALON_DMA_RDWR_SG: + if (copy_from_user(iovec, (void __user *)arg, sizeof(iovec))) + return -EFAULT; + + buf_rd = iovec[0].iov_base; + len_rd = iovec[0].iov_len; + + buf_wr = iovec[1].iov_base; + len_wr = iovec[1].iov_len; + + break; + default: + return -EINVAL; + }; + + switch (cmd) { + case IOCTL_AVALON_DMA_READ: + ret = xfer_single(chan, DMA_DEV_TO_MEM, buf, len); + break; + case IOCTL_AVALON_DMA_WRITE: + ret = xfer_single(chan, DMA_MEM_TO_DEV, buf, len); + break; + case IOCTL_AVALON_DMA_RDWR: + ret = xfer_rw_single(chan, buf_rd, len_rd, buf_wr, len_wr); + break; + case IOCTL_AVALON_DMA_READ_SG: + ret = xfer_sg(chan, DMA_DEV_TO_MEM, buf, len, false); + break; + case IOCTL_AVALON_DMA_WRITE_SG: + ret = xfer_sg(chan, DMA_MEM_TO_DEV, buf, len, false); + break; + case IOCTL_AVALON_DMA_READ_SG_SMP: + ret = xfer_sg(chan, DMA_DEV_TO_MEM, buf, len, true); + break; + case IOCTL_AVALON_DMA_WRITE_SG_SMP: + ret = xfer_sg(chan, DMA_MEM_TO_DEV, buf, len, true); + break; + case IOCTL_AVALON_DMA_RDWR_SG: + ret = xfer_rw_sg(chan, buf_rd, len_rd, buf_wr, len_wr); + break; + }; + + return ret; +} diff --git a/drivers/dma/avalon-test/avalon-ioctl.h b/drivers/dma/avalon-test/avalon-ioctl.h new file mode 100644 index 000000000000..a10ab0b5d67c --- /dev/null +++ b/drivers/dma/avalon-test/avalon-ioctl.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#ifndef __AVALON_IOCTL_H__ +#define __AVALON_IOCTL_H__ + +long avalon_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg); + +#endif diff --git a/drivers/dma/avalon-test/avalon-mmap.c b/drivers/dma/avalon-test/avalon-mmap.c new file mode 100644 index 000000000000..434a8f8d4720 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-mmap.c @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#include <linux/kernel.h> +#include <linux/fs.h> +#include <linux/dma-direction.h> + +#include "avalon-dev.h" +#include "avalon-mmap.h" +#include "avalon-sg-buf.h" + +static void avalon_drv_vm_close(struct vm_area_struct *vma) +{ + struct dma_sg_buf *sg_buf = vma->vm_private_data; + + dma_sg_buf_free(sg_buf); +} + +static const struct vm_operations_struct avalon_drv_vm_ops = { + .close = avalon_drv_vm_close, +}; + +int avalon_dev_mmap(struct file *file, struct vm_area_struct *vma) +{ + struct avalon_dev *adev = container_of(file->private_data, + struct avalon_dev, misc_dev); + struct device *dev = chan_to_dev(adev->dma_chan); + unsigned long addr = vma->vm_start; + unsigned long size = vma->vm_end - vma->vm_start; + enum dma_data_direction dir; + struct dma_sg_buf *sg_buf; + int ret; + int i; + + if (!IS_ALIGNED(addr, PAGE_SIZE) || !IS_ALIGNED(size, PAGE_SIZE)) + return -EINVAL; + if ((vma->vm_pgoff * PAGE_SIZE + size) > mem_size) + return -EINVAL; + if (!(((vma->vm_flags & (VM_READ | VM_WRITE)) == VM_READ) || + ((vma->vm_flags & (VM_READ | VM_WRITE)) == VM_WRITE))) + return -EINVAL; + if (!(vma->vm_flags & VM_SHARED)) + return -EINVAL; + + vma->vm_ops = &avalon_drv_vm_ops; + + if (vma->vm_flags & VM_WRITE) + dir = DMA_TO_DEVICE; + else + dir = DMA_FROM_DEVICE; + + sg_buf = dma_sg_buf_alloc(dev, size, dir, GFP_KERNEL); + if (IS_ERR(sg_buf)) + return PTR_ERR(sg_buf); + + for (i = 0; size > 0; i++) { + ret = vm_insert_page(vma, addr, sg_buf->pages[i]); + if (ret) { + dma_sg_buf_free(sg_buf); + return ret; + } + + addr += PAGE_SIZE; + size -= PAGE_SIZE; + }; + + vma->vm_private_data = sg_buf; + + return 0; +} diff --git a/drivers/dma/avalon-test/avalon-mmap.h b/drivers/dma/avalon-test/avalon-mmap.h new file mode 100644 index 000000000000..3d3878f236cf --- /dev/null +++ b/drivers/dma/avalon-test/avalon-mmap.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#ifndef __AVALON_MMAP_H__ +#define __AVALON_MMAP_H__ + +int avalon_dev_mmap(struct file *file, struct vm_area_struct *vma); + +#endif diff --git a/drivers/dma/avalon-test/avalon-sg-buf.c b/drivers/dma/avalon-test/avalon-sg-buf.c new file mode 100644 index 000000000000..31a578e20cc1 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-sg-buf.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#include <linux/kernel.h> +#include <linux/dma-mapping.h> +#include <linux/vmalloc.h> +#include <linux/slab.h> + +#include "avalon-sg-buf.h" + +static int dma_sg_alloc_compacted(struct dma_sg_buf *buf, gfp_t gfp_flags) +{ + unsigned int last_page = 0; + int size = buf->size; + + while (size > 0) { + struct page *pages; + int order; + int i; + + order = get_order(size); + if ((PAGE_SIZE << order) > size) + order--; + + pages = NULL; + while (!pages) { + pages = alloc_pages(gfp_flags | __GFP_NOWARN, order); + if (pages) + break; + + if (order == 0) { + while (last_page--) + __free_page(buf->pages[last_page]); + return -ENOMEM; + } + order--; + } + + split_page(pages, order); + for (i = 0; i < (1 << order); i++) + buf->pages[last_page++] = &pages[i]; + + size -= PAGE_SIZE << order; + } + + return 0; +} + +struct dma_sg_buf *dma_sg_buf_alloc(struct device *dev, + unsigned long size, + enum dma_data_direction dma_dir, + gfp_t gfp_flags) +{ + struct dma_sg_buf *buf; + struct sg_table *sgt; + int ret; + int num_pages; + + buf = kzalloc(sizeof(*buf), GFP_KERNEL); + if (!buf) + return ERR_PTR(-ENOMEM); + + buf->dma_dir = dma_dir; + buf->size = size; + buf->num_pages = size >> PAGE_SHIFT; + + buf->pages = kvcalloc(buf->num_pages, sizeof(struct page *), GFP_KERNEL); + if (!buf->pages) + goto free_buf; + + ret = dma_sg_alloc_compacted(buf, gfp_flags); + if (ret) + goto free_arr; + + ret = sg_alloc_table_from_pages(&buf->sgt, buf->pages, + buf->num_pages, 0, size, + GFP_KERNEL); + if (ret) + goto free_pages; + + buf->dev = get_device(dev); + + sgt = &buf->sgt; + + sgt->nents = dma_map_sg_attrs(buf->dev, sgt->sgl, sgt->orig_nents, + buf->dma_dir, DMA_ATTR_SKIP_CPU_SYNC); + if (!sgt->nents) + goto free_sgt; + + buf->vaddr = vm_map_ram(buf->pages, buf->num_pages, -1, PAGE_KERNEL); + if (!buf->vaddr) + goto unmap_sg; + + return buf; + +unmap_sg: + dma_unmap_sg_attrs(buf->dev, sgt->sgl, sgt->orig_nents, + buf->dma_dir, DMA_ATTR_SKIP_CPU_SYNC); +free_sgt: + put_device(buf->dev); + sg_free_table(&buf->sgt); +free_pages: + num_pages = buf->num_pages; + while (num_pages--) + __free_page(buf->pages[num_pages]); +free_arr: + kvfree(buf->pages); +free_buf: + kfree(buf); + + return ERR_PTR(-ENOMEM); +} + +void dma_sg_buf_free(struct dma_sg_buf *buf) +{ + struct sg_table *sgt = &buf->sgt; + int i = buf->num_pages; + + dma_unmap_sg_attrs(buf->dev, sgt->sgl, sgt->orig_nents, + buf->dma_dir, DMA_ATTR_SKIP_CPU_SYNC); + vm_unmap_ram(buf->vaddr, buf->num_pages); + sg_free_table(&buf->sgt); + while (--i >= 0) + __free_page(buf->pages[i]); + kvfree(buf->pages); + put_device(buf->dev); + kfree(buf); +} diff --git a/drivers/dma/avalon-test/avalon-sg-buf.h b/drivers/dma/avalon-test/avalon-sg-buf.h new file mode 100644 index 000000000000..a5cce1c18714 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-sg-buf.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * + * Avalon DMA driver + */ +#ifndef __AVALON_SG_BUF_H__ +#define __AVALON_SG_BUF_H__ + +struct dma_sg_buf { + struct device *dev; + void *vaddr; + struct page **pages; + enum dma_data_direction dma_dir; + struct sg_table sgt; + size_t size; + unsigned int num_pages; +}; + +struct dma_sg_buf *dma_sg_buf_alloc(struct device *dev, + unsigned long size, + enum dma_data_direction dma_dir, + gfp_t gfp_flags); +void dma_sg_buf_free(struct dma_sg_buf *buf); + +#endif diff --git a/drivers/dma/avalon-test/avalon-xfer.c b/drivers/dma/avalon-test/avalon-xfer.c new file mode 100644 index 000000000000..57f3543bc7c3 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-xfer.c @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/dma-mapping.h> +#include <linux/uaccess.h> +#include <linux/kthread.h> +#include <linux/sched/signal.h> +#include <linux/dmaengine.h> + +#include "avalon-xfer.h" +#include "avalon-sg-buf.h" + +struct callback_info { + struct completion completion; + atomic_t counter; +}; + +static void init_callback_info(struct callback_info *info, int value) +{ + init_completion(&info->completion); + + /* + * Pairs with smp_rmb() in xfer_callback() + */ + atomic_set(&info->counter, value); + smp_wmb(); +} + +static void xfer_callback(void *arg) +{ + struct callback_info *info = arg; + + /* + * Pairs with smp_wmb() in init_callback_info() + */ + smp_rmb(); + if (atomic_dec_and_test(&info->counter)) + complete(&info->completion); +} + +static int config_chan(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr) +{ + struct dma_slave_config config = { + .direction = direction, + .src_addr = dev_addr, + .dst_addr = dev_addr, + }; + + return dmaengine_slave_config(chan, &config); +} + +static int submit_tx(struct dma_chan *chan, + struct dma_async_tx_descriptor *tx, + dma_async_tx_callback callback, void *callback_param) +{ + dma_cookie_t cookie; + + tx->callback = callback; + tx->callback_param = callback_param; + + cookie = dmaengine_submit(tx); + if (cookie < 0) { + dmaengine_terminate_sync(chan); + return cookie; + } + + return 0; +} + +static +int submit_xfer_single(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + dma_addr_t host_addr, unsigned int size, + dma_async_tx_callback callback, void *callback_param) +{ + struct dma_async_tx_descriptor *tx; + int ret; + + ret = config_chan(chan, direction, dev_addr); + if (ret) + return ret; + + tx = dmaengine_prep_slave_single(chan, + host_addr, size, direction, 0); + if (!tx) + return -ENOMEM; + + ret = submit_tx(chan, tx, callback, callback_param); + if (ret) + return ret; + + return 0; +} + +static +int submit_xfer_sg(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + struct scatterlist *sg, unsigned int sg_len, + dma_async_tx_callback callback, void *callback_param) +{ + struct dma_async_tx_descriptor *tx; + int ret; + + ret = config_chan(chan, direction, dev_addr); + if (ret) + return ret; + + tx = dmaengine_prep_slave_sg(chan, sg, sg_len, direction, 0); + if (!tx) + return -ENOMEM; + + ret = submit_tx(chan, tx, callback, callback_param); + if (ret) + return ret; + + return 0; +} + +int xfer_single(struct dma_chan *chan, + enum dma_transfer_direction direction, + void __user *user_buf, size_t user_len) +{ + struct device *dev = chan_to_dev(chan); + dma_addr_t dma_addr; + enum dma_data_direction dma_dir; + void *buf; + struct callback_info info; + int ret; + int i; + + if (user_len < dma_size) + return -EINVAL; + if (direction == DMA_MEM_TO_DEV) + dma_dir = DMA_TO_DEVICE; + else if (direction == DMA_DEV_TO_MEM) + dma_dir = DMA_FROM_DEVICE; + else + return -EINVAL; + + user_len = min_t(size_t, user_len, dma_size); + + buf = kzalloc(dma_size, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + if (direction == DMA_MEM_TO_DEV) { + if (copy_from_user(buf, user_buf, user_len)) { + ret = -EFAULT; + goto free_buf; + } + } + + dma_addr = dma_map_single(dev, buf, dma_size, dma_dir); + if (dma_mapping_error(dev, dma_addr)) { + ret = -ENOMEM; + goto free_buf; + } + + init_callback_info(&info, nr_dma_reps); + + for (i = 0; i < nr_dma_reps; i++) { + ret = submit_xfer_single(chan, direction, + mem_base, dma_addr, dma_size, + xfer_callback, &info); + if (ret) + goto unmap_buf; + } + + dma_async_issue_pending(chan); + + ret = wait_for_completion_interruptible(&info.completion); + if (ret) + goto unmap_buf; + + if (direction == DMA_DEV_TO_MEM) { + if (copy_to_user(user_buf, buf, user_len)) + ret = -EFAULT; + } + +unmap_buf: + dma_unmap_single(dev, dma_addr, dma_size, dma_dir); + +free_buf: + kfree(buf); + + return ret; +} + +int xfer_rw_single(struct dma_chan *chan, + void __user *user_buf_rd, size_t user_len_rd, + void __user *user_buf_wr, size_t user_len_wr) +{ + struct device *dev = chan_to_dev(chan); + dma_addr_t target_rd = mem_base; + dma_addr_t target_wr = target_rd + dma_size; + dma_addr_t dma_addr_rd, dma_addr_wr; + void *buf_rd, *buf_wr; + struct callback_info info; + int ret; + int i; + + if ((user_len_rd < dma_size) || (user_len_wr < dma_size)) + return -EINVAL; + + user_len_rd = min_t(size_t, user_len_rd, dma_size); + user_len_wr = min_t(size_t, user_len_wr, dma_size); + + buf_rd = kzalloc(dma_size, GFP_KERNEL); + if (!buf_rd) { + ret = -ENOMEM; + goto alloc_err; + } + + buf_wr = kzalloc(dma_size, GFP_KERNEL); + if (!buf_wr) { + ret = -ENOMEM; + goto free_buf_rd; + } + + if (copy_from_user(buf_wr, user_buf_wr, user_len_wr)) { + ret = -EFAULT; + goto free_buf_wr; + } + + dma_addr_rd = dma_map_single(dev, buf_rd, dma_size, DMA_DEV_TO_MEM); + if (dma_mapping_error(dev, dma_addr_rd)) { + ret = -ENOMEM; + goto free_buf_wr; + } + + dma_addr_wr = dma_map_single(dev, buf_wr, dma_size, DMA_MEM_TO_DEV); + if (dma_mapping_error(dev, dma_addr_rd)) { + ret = -ENOMEM; + goto unmap_buf_rd; + } + + init_callback_info(&info, 2 * nr_dma_reps); + + for (i = 0; i < nr_dma_reps; i++) { + ret = submit_xfer_single(chan, DMA_MEM_TO_DEV, + target_wr, dma_addr_wr, dma_size, + xfer_callback, &info); + if (ret) + goto unmap_buf_wr; + + ret = submit_xfer_single(chan, DMA_DEV_TO_MEM, + target_rd, dma_addr_rd, dma_size, + xfer_callback, &info); + if (ret) + goto unmap_buf_wr; + } + + dma_async_issue_pending(chan); + + ret = wait_for_completion_interruptible(&info.completion); + if (ret) + goto unmap_buf_wr; + + if (copy_to_user(user_buf_rd, buf_rd, user_len_rd)) + ret = -EFAULT; + +unmap_buf_wr: + dma_unmap_single(dev, dma_addr_wr, dma_size, DMA_MEM_TO_DEV); + +unmap_buf_rd: + dma_unmap_single(dev, dma_addr_rd, dma_size, DMA_DEV_TO_MEM); + +free_buf_wr: + kfree(buf_wr); + +free_buf_rd: + kfree(buf_rd); + +alloc_err: + return ret; +} + +static int kthread_xfer_rw_sg(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + struct scatterlist *sg, unsigned int sg_len, + dma_async_tx_callback callback) +{ + struct callback_info info; + int ret; + int i; + + while (!kthread_should_stop()) { + init_callback_info(&info, nr_dma_reps); + + for (i = 0; i < nr_dma_reps; i++) { + ret = submit_xfer_sg(chan, direction, + dev_addr, sg, sg_len, + callback, &info); + if (ret) + return ret; + } + + dma_async_issue_pending(chan); + + ret = wait_for_completion_interruptible(&info.completion); + if (ret) + return ret; + } + + return 0; +} + +struct kthread_xfer_rw_sg_data { + struct dma_chan *chan; + enum dma_transfer_direction direction; + dma_addr_t dev_addr; + struct scatterlist *sg; + unsigned int sg_len; + dma_async_tx_callback callback; +}; + +static int __kthread_xfer_rw_sg(void *__data) +{ + struct kthread_xfer_rw_sg_data *data = __data; + + return kthread_xfer_rw_sg(data->chan, data->direction, + data->dev_addr, data->sg, data->sg_len, + data->callback); +} + +static int __xfer_sg_smp(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + struct scatterlist *sg, unsigned int sg_len, + dma_async_tx_callback callback) +{ + struct kthread_xfer_rw_sg_data data = { + chan, direction, + dev_addr, sg, sg_len, + callback + }; + struct task_struct *task; + struct task_struct **tasks; + int nr_tasks = dmas_per_cpu * num_online_cpus(); + int n, cpu; + int ret = 0; + int i = 0; + + tasks = kmalloc_array(nr_tasks, sizeof(tasks[0]), GFP_KERNEL); + if (!tasks) + return -ENOMEM; + + for (n = 0; n < dmas_per_cpu; n++) { + for_each_online_cpu(cpu) { + if (i >= nr_tasks) { + ret = -ENOMEM; + goto kthread_err; + } + + task = kthread_create(__kthread_xfer_rw_sg, + &data, "avalon-dma-%d-%d", + cpu, n); + if (IS_ERR(task)) { + ret = PTR_ERR(task); + goto kthread_err; + } + + kthread_bind(task, cpu); + + tasks[i] = task; + i++; + } + } + + for (n = 0; n < i; n++) + wake_up_process(tasks[n]); + + /* + * Run child kthreads until user sent a signal (i.e Ctrl+C) + * and clear the signal to avid user program from being killed. + */ + schedule_timeout_interruptible(MAX_SCHEDULE_TIMEOUT); + flush_signals(current); + +kthread_err: + while (--i >= 0) + kthread_stop(tasks[i]); + + kfree(tasks); + + return ret; +} + +static int __xfer_sg(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + struct scatterlist *sg, unsigned int sg_len, + dma_async_tx_callback callback) +{ + struct callback_info info; + int ret; + int i; + + init_callback_info(&info, nr_dma_reps); + + for (i = 0; i < nr_dma_reps; i++) { + ret = submit_xfer_sg(chan, direction, dev_addr, sg, sg_len, + callback, &info); + if (ret) + return ret; + } + + dma_async_issue_pending(chan); + + ret = wait_for_completion_interruptible(&info.completion); + if (ret) + return ret; + + return 0; +} + +static struct vm_area_struct *get_vma(unsigned long addr, + unsigned long size) +{ + struct vm_area_struct *vma; + unsigned long vm_size; + + vma = find_vma(current->mm, addr); + if (!vma || (vma->vm_start != addr)) + return ERR_PTR(-ENXIO); + + vm_size = vma->vm_end - vma->vm_start; + if (size > vm_size) + return ERR_PTR(-EINVAL); + + return vma; +} + +int xfer_sg(struct dma_chan *chan, + enum dma_transfer_direction direction, + void __user *user_buf, size_t user_len, + bool is_smp) +{ + struct device *dev = chan_to_dev(chan); + int (*xfer)(struct dma_chan *chan, + enum dma_transfer_direction direction, + dma_addr_t dev_addr, + struct scatterlist *sg, unsigned int sg_len, + dma_async_tx_callback callback); + struct vm_area_struct *vma; + struct dma_sg_buf *sg_buf; + dma_addr_t dma_addr; + int ret; + + vma = get_vma((unsigned long)user_buf, user_len); + if (IS_ERR(vma)) + return PTR_ERR(vma); + + sg_buf = vma->vm_private_data; + switch (sg_buf->dma_dir) { + case DMA_TO_DEVICE: + if (direction != DMA_MEM_TO_DEV) + return -EINVAL; + break; + case DMA_FROM_DEVICE: + if (direction != DMA_DEV_TO_MEM) + return -EINVAL; + break; + default: + return -EINVAL; + } + + if (is_smp) + xfer = __xfer_sg_smp; + else + xfer = __xfer_sg; + + dma_addr = mem_base + vma->vm_pgoff * PAGE_SIZE; + + dma_sync_sg_for_device(dev, + sg_buf->sgt.sgl, sg_buf->sgt.nents, + sg_buf->dma_dir); + + ret = xfer(chan, direction, + dma_addr, sg_buf->sgt.sgl, sg_buf->sgt.nents, + xfer_callback); + + dma_sync_sg_for_cpu(dev, + sg_buf->sgt.sgl, sg_buf->sgt.nents, + sg_buf->dma_dir); + + return ret; +} + +int xfer_rw_sg(struct dma_chan *chan, + void __user *user_buf_rd, size_t user_len_rd, + void __user *user_buf_wr, size_t user_len_wr) +{ + struct device *dev = chan_to_dev(chan); + dma_addr_t dma_addr_rd, dma_addr_wr; + struct callback_info info; + struct vm_area_struct *vma_rd, *vma_wr; + struct dma_sg_buf *sg_buf_rd, *sg_buf_wr; + int ret; + int i; + + vma_rd = get_vma((unsigned long)user_buf_rd, user_len_rd); + if (IS_ERR(vma_rd)) + return PTR_ERR(vma_rd); + + vma_wr = get_vma((unsigned long)user_buf_wr, user_len_wr); + if (IS_ERR(vma_wr)) + return PTR_ERR(vma_wr); + + sg_buf_rd = vma_rd->vm_private_data; + sg_buf_wr = vma_wr->vm_private_data; + + if ((sg_buf_rd->dma_dir != DMA_TO_DEVICE) || + (sg_buf_wr->dma_dir != DMA_FROM_DEVICE)) + return -EINVAL; + + dma_addr_rd = mem_base + vma_rd->vm_pgoff * PAGE_SIZE; + dma_addr_wr = mem_base + vma_wr->vm_pgoff * PAGE_SIZE; + + init_callback_info(&info, 2 * nr_dma_reps); + + dma_sync_sg_for_device(dev, + sg_buf_rd->sgt.sgl, + sg_buf_rd->sgt.nents, + DMA_FROM_DEVICE); + dma_sync_sg_for_device(dev, + sg_buf_wr->sgt.sgl, + sg_buf_wr->sgt.nents, + DMA_TO_DEVICE); + + for (i = 0; i < nr_dma_reps; i++) { + ret = submit_xfer_sg(chan, DMA_MEM_TO_DEV, + dma_addr_wr, + sg_buf_wr->sgt.sgl, + sg_buf_wr->sgt.nents, + xfer_callback, &info); + if (ret) + goto submit_err; + + ret = submit_xfer_sg(chan, DMA_DEV_TO_MEM, + dma_addr_rd, + sg_buf_rd->sgt.sgl, + sg_buf_rd->sgt.nents, + xfer_callback, &info); + if (ret) + goto submit_err; + } + + dma_async_issue_pending(chan); + + ret = wait_for_completion_interruptible(&info.completion); + +submit_err: + dma_sync_sg_for_cpu(dev, + sg_buf_rd->sgt.sgl, + sg_buf_rd->sgt.nents, + DMA_DEV_TO_MEM); + dma_sync_sg_for_cpu(dev, + sg_buf_wr->sgt.sgl, + sg_buf_wr->sgt.nents, + DMA_MEM_TO_DEV); + + return ret; +} diff --git a/drivers/dma/avalon-test/avalon-xfer.h b/drivers/dma/avalon-test/avalon-xfer.h new file mode 100644 index 000000000000..32bc6c0e7fc9 --- /dev/null +++ b/drivers/dma/avalon-test/avalon-xfer.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2019, The Linux Foundation. All rights reserved. + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + * + * Avalon DMA driver + */ +#ifndef __AVALON_XFER_H__ +#define __AVALON_XFER_H__ + +#include <linux/dmaengine.h> + +#include "avalon-dev.h" + +int xfer_single(struct dma_chan *chan, + enum dma_transfer_direction direction, + void __user *user_buf, size_t user_len); +int xfer_rw_single(struct dma_chan *chan, + void __user *user_buf_rd, size_t user_len_rd, + void __user *user_buf_wr, size_t user_len_wr); +int xfer_sg(struct dma_chan *chan, + enum dma_transfer_direction direction, + void __user *user_buf, size_t user_len, + bool is_smp); +int xfer_rw_sg(struct dma_chan *chan, + void __user *user_buf_rd, size_t user_len_rd, + void __user *user_buf_wr, size_t user_len_wr); + +#endif diff --git a/include/uapi/linux/avalon-ioctl.h b/include/uapi/linux/avalon-ioctl.h new file mode 100644 index 000000000000..a42d7b4bc0a2 --- /dev/null +++ b/include/uapi/linux/avalon-ioctl.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ +/* + * Avalon DMA driver + * + * Author: Alexander Gordeev <a.gordeev.box@xxxxxxxxx> + */ +#ifndef _UAPI_LINUX_AVALON_IOCTL_H__ +#define _UAPI_LINUX_AVALON_IOCTL_H__ + +#include <linux/types.h> + +#define AVALON_DEVICE_NAME "avalon-dev" + +struct avalon_dma_info { + size_t mem_addr; + size_t mem_size; + size_t dma_size; + size_t dma_size_sg; +} __attribute((packed)); + +#define AVALON_SIG 'V' + +#define IOCTL_AVALON_DMA_GET_INFO _IOR(AVALON_SIG, 0, struct avalon_dma_info) +#define IOCTL_AVALON_DMA_SET_INFO _IOW(AVALON_SIG, 1, struct avalon_dma_info) +#define IOCTL_AVALON_DMA_READ _IOR(AVALON_SIG, 2, struct iovec) +#define IOCTL_AVALON_DMA_WRITE _IOW(AVALON_SIG, 3, struct iovec) +#define IOCTL_AVALON_DMA_RDWR _IOWR(AVALON_SIG, 4, struct iovec[2]) +#define IOCTL_AVALON_DMA_READ_SG _IOR(AVALON_SIG, 5, struct iovec) +#define IOCTL_AVALON_DMA_WRITE_SG _IOW(AVALON_SIG, 6, struct iovec) +#define IOCTL_AVALON_DMA_RDWR_SG _IOWR(AVALON_SIG, 7, struct iovec[2]) +#define IOCTL_AVALON_DMA_READ_SG_SMP _IOR(AVALON_SIG, 8, struct iovec) +#define IOCTL_AVALON_DMA_WRITE_SG_SMP _IOW(AVALON_SIG, 9, struct iovec) + +#endif -- 2.24.0