Signed-off-by: Luis Chamberlain <mcgrof@xxxxxxxxxx>
---
drivers/dma/Kconfig | 11 +
drivers/dma/Makefile | 1 +
drivers/dma/fake-dma.c | 718 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 730 insertions(+)
create mode 100644 drivers/dma/fake-dma.c
diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig
index df2d2dc00a05..716531f2c7e2 100644
--- a/drivers/dma/Kconfig
+++ b/drivers/dma/Kconfig
@@ -140,6 +140,17 @@ config DMA_BCM2835
select DMA_ENGINE
select DMA_VIRTUAL_CHANNELS
+config DMA_FAKE
+ tristate "Fake DMA Engine"
+ select DMA_ENGINE
+ select DMA_VIRTUAL_CHANNELS
+ help
+ This implements a fake DMA engine. Useful for testing the DMA API
+ without any hardware requirements, on any architecture which just
+ supporst the DMA engine. Enable this if you want to easily run custom
+ tests on the DMA API without a real DMA engine or the requirement for
+ things like qemu to virtualize it for you.
+
config DMA_JZ4780
tristate "JZ4780 DMA support"
depends on MIPS || COMPILE_TEST
diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile
index 19ba465011a6..c75e4b7ad9f2 100644
--- a/drivers/dma/Makefile
+++ b/drivers/dma/Makefile
@@ -22,6 +22,7 @@ obj-$(CONFIG_AT_XDMAC) += at_xdmac.o
obj-$(CONFIG_AXI_DMAC) += dma-axi-dmac.o
obj-$(CONFIG_BCM_SBA_RAID) += bcm-sba-raid.o
obj-$(CONFIG_DMA_BCM2835) += bcm2835-dma.o
+obj-$(CONFIG_DMA_FAKE) += fake-dma.o
obj-$(CONFIG_DMA_JZ4780) += dma-jz4780.o
obj-$(CONFIG_DMA_SA11X0) += sa11x0-dma.o
obj-$(CONFIG_DMA_SUN4I) += sun4i-dma.o
diff --git a/drivers/dma/fake-dma.c b/drivers/dma/fake-dma.c
new file mode 100644
index 000000000000..ee1d788a2b83
--- /dev/null
+++ b/drivers/dma/fake-dma.c
@@ -0,0 +1,718 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR copyleft-next-0.3.1
+/*
+ * Fake DMA engine test module. This allows us to test DMA engines
+ * without leveraging virtualization.
+ *
+ * Copyright (C) 2025 Luis Chamberlain <mcgrof@xxxxxxxxxx>
+ *
+ * This driver provides an interface to trigger and test the kernel's
+ * module loader through a series of configurations and a few triggers.
+ * To test this driver use the following script as root:
+ *
+ * tools/testing/selftests/dma/fake.sh --help
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/err.h>
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmaengine.h>
+#include <linux/freezer.h>
+#include <linux/init.h>
+#include <linux/kthread.h>
+#include <linux/sched/task.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/random.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+#include <linux/debugfs.h>
+#include <linux/platform_device.h>
+#include "dmaengine.h"
+
+#define FAKE_MAX_DMA_CHANNELS 20
+
+static unsigned int num_channels = FAKE_MAX_DMA_CHANNELS;
+module_param(num_channels, uint, 0644);
+MODULE_PARM_DESC(num_channels, "Number of channels to support (default: 20)");
+
+struct fake_dma_desc {
+ struct dma_async_tx_descriptor txd;
+ dma_addr_t src;
+ dma_addr_t dst;
+ size_t len;
+ enum dma_transaction_type type;
+ int memset_value;
+ /* For XOR/PQ operations */
+ dma_addr_t *src_list; /* Array of source addresses */
+ unsigned int src_cnt; /* Number of sources */
+ dma_addr_t *dst_list; /* Array of destination addresses (for PQ) */
+ unsigned char *pq_coef; /* P+Q coefficients */
+ struct list_head node;
+};
+
+struct fake_dma_chan {
+ struct dma_chan chan;
+ struct list_head active_list;
+ struct list_head queue;
+ struct work_struct work;
+ spinlock_t lock;
+ bool running;
+};
+
+struct fake_dma_device {
+ struct platform_device *pdev;
+ struct dma_device dma_dev;
+ struct fake_dma_chan *channels;
+};
+
+struct fake_dma_device *single_fake_dma;
+
+static struct platform_driver fake_dma_engine_driver = {
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .owner = THIS_MODULE,
+ },
+};
+
+static int fake_dma_create_platform_device(struct fake_dma_device *fake_dma)
+{
+ fake_dma->pdev = platform_device_register_simple("fake-dma-engine", -1, NULL, 0);
+ if (IS_ERR(fake_dma->pdev))
+ return -ENODEV;
+
+ pr_info("Fake DMA platform device created: %s\n",
+ dev_name(&fake_dma->pdev->dev));
+
+ return 0;
+}
+
+static void fake_dma_destroy_platform_device(struct fake_dma_device *fake_dma)
+{
+ if (!fake_dma->pdev)
+ return;
+
+ pr_info("Destroying fake DMA platform device: %s ...\n",
+ dev_name(&fake_dma->pdev->dev));
+ platform_device_unregister(fake_dma->pdev);
+}
+
+static inline struct fake_dma_chan *to_fake_dma_chan(struct dma_chan *c)
+{
+ return container_of(c, struct fake_dma_chan, chan);
+}
+
+static inline struct fake_dma_desc *to_fake_dma_desc(struct dma_async_tx_descriptor *txd)
+{
+ return container_of(txd, struct fake_dma_desc, txd);
+}
+
+/* Galois Field multiplication for P+Q operations */
+static unsigned char gf_mul(unsigned char a, unsigned char b)
+{
+ unsigned char result = 0;
+ unsigned char high_bit_set;
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ if (b & 1)
+ result ^= a;
+ high_bit_set = a & 0x80;
+ a <<= 1;
+ if (high_bit_set)
+ a ^= 0x1b; /* x^8 + x^4 + x^3 + x + 1 */
+ b >>= 1;
+ }
+
+ return result;
+}
+
+/* Processes pending transfers */
+static void fake_dma_work_func(struct work_struct *work)
+{
+ struct fake_dma_chan *vchan = container_of(work, struct fake_dma_chan, work);
+ struct fake_dma_desc *vdesc;
+ struct dmaengine_desc_callback cb;
+ unsigned long flags;
+
+ spin_lock_irqsave(&vchan->lock, flags);
+
+ if (list_empty(&vchan->queue)) {
+ vchan->running = false;
+ spin_unlock_irqrestore(&vchan->lock, flags);
+ return;
+ }
+
+ vdesc = list_first_entry(&vchan->queue, struct fake_dma_desc, node);
+ list_del(&vdesc->node);
+ list_add_tail(&vdesc->node, &vchan->active_list);
+
+ spin_unlock_irqrestore(&vchan->lock, flags);
+
+ /* Actually perform the DMA transfer for memcpy operations */
+ if (vdesc->len) {
+ void *src_virt, *dst_virt;
+ void *p_virt, *q_virt;
+ unsigned char *p_bytes, *q_bytes;
+ unsigned int i, j;
+ unsigned char *dst_bytes;
+
+ switch (vdesc->type) {
+ case DMA_MEMCPY:
+ /* Convert DMA addresses to virtual addresses and perform the copy */
+ src_virt = phys_to_virt(vdesc->src);
+ dst_virt = phys_to_virt(vdesc->dst);
+
+ memcpy(dst_virt, src_virt, vdesc->len);
+ break;
+ case DMA_MEMSET:
+ dst_virt = phys_to_virt(vdesc->dst);
+ memset(dst_virt, vdesc->memset_value, vdesc->len);
+ break;
+ case DMA_XOR:
+ dst_virt = phys_to_virt(vdesc->dst);
+ dst_bytes = (unsigned char *)dst_virt;
+
+ memset(dst_virt, 0, vdesc->len);
+
+ /* XOR all sources into destination */
+ for (i = 0; i < vdesc->src_cnt; i++) {
+ void *src_virt = phys_to_virt(vdesc->src_list[i]);
+ unsigned char *src_bytes = (unsigned char *)src_virt;
+
+ for (j = 0; j < vdesc->len; j++)
+ dst_bytes[j] ^= src_bytes[j];
+ }
+ break;
+ case DMA_PQ:
+ p_virt = phys_to_virt(vdesc->dst_list[0]);
+ q_virt = phys_to_virt(vdesc->dst_list[1]);
+ p_bytes = (unsigned char *)p_virt;
+ q_bytes = (unsigned char *)q_virt;
+
+ /* Initialize P and Q destinations to zero */
+ memset(p_virt, 0, vdesc->len);
+ memset(q_virt, 0, vdesc->len);
+
+ /* Calculate P (XOR of all sources) and Q (weighted XOR) */
+ for (i = 0; i < vdesc->src_cnt; i++) {
+ void *src_virt = phys_to_virt(vdesc->src_list[i]);
+ unsigned char *src_bytes = (unsigned char *)src_virt;
+ unsigned char coef = vdesc->pq_coef[i];
+
+ for (j = 0; j < vdesc->len; j++) {
+ /* P calculation: simple XOR */
+ p_bytes[j] ^= src_bytes[j];
+
+ /* Q calculation: multiply in GF(2^8) and XOR */
+ q_bytes[j] ^= gf_mul(src_bytes[j], coef);
+ }
+ }
+ break;
+ default:
+ pr_warn("fake-dma: Unknown DMA operation type %d\n", vdesc->type);
+ break;
+ }
+ }
+
+ /* Mark descriptor as complete */
+ dma_cookie_complete(&vdesc->txd);
+
+ /* Call completion callback if set */
+ dmaengine_desc_get_callback(&vdesc->txd, &cb);
+ if (cb.callback)
+ cb.callback(cb.callback_param);
+
+ /* Process next transfer if available */
+ spin_lock_irqsave(&vchan->lock, flags);
+ list_del(&vdesc->node);
+
+ /* Free allocated memory for XOR/PQ operations */
+ if (vdesc->type == DMA_XOR || vdesc->type == DMA_PQ) {
+ kfree(vdesc->src_list);
+ if (vdesc->type == DMA_PQ) {
+ kfree(vdesc->dst_list);
+ kfree(vdesc->pq_coef);
+ }
+ }
+
+ kfree(vdesc);
+
+ if (!list_empty(&vchan->queue)) {
+ spin_unlock_irqrestore(&vchan->lock, flags);
+ schedule_work(&vchan->work);
+ } else {
+ vchan->running = false;
+ spin_unlock_irqrestore(&vchan->lock, flags);
+ }
+}
+
+/* Submit descriptor to the DMA engine */
+static dma_cookie_t fake_dma_tx_submit(struct dma_async_tx_descriptor *txd)
+{
+ struct fake_dma_chan *vchan = to_fake_dma_chan(txd->chan);
+ struct fake_dma_desc *vdesc = to_fake_dma_desc(txd);
+ unsigned long flags;
+ dma_cookie_t cookie;
+
+ spin_lock_irqsave(&vchan->lock, flags);
+
+ cookie = dma_cookie_assign(txd);
+ list_add_tail(&vdesc->node, &vchan->queue);
+
+ /* Schedule processing if not already running */
+ if (!vchan->running) {
+ vchan->running = true;
+ schedule_work(&vchan->work);
+ }
+
+ spin_unlock_irqrestore(&vchan->lock, flags);
+
+ return cookie;
+}
+
+static
+struct dma_async_tx_descriptor *fake_dma_prep_memcpy(struct dma_chan *chan,
+ dma_addr_t dest,
+ dma_addr_t src,
+ size_t len,
+ unsigned long flags)
+{
+ struct fake_dma_chan *vchan = to_fake_dma_chan(chan);
+ struct fake_dma_desc *vdesc;
+
+ vdesc = kzalloc(sizeof(*vdesc), GFP_NOWAIT);
+ if (!vdesc)
+ return NULL;
+
+ if (!vchan)
+ return NULL;
+
+ dma_async_tx_descriptor_init(&vdesc->txd, chan);
+ vdesc->type = DMA_MEMCPY;
+ vdesc->txd.tx_submit = fake_dma_tx_submit;
+ vdesc->txd.flags = flags;
+ vdesc->src = src;
+ vdesc->dst = dest;
+ vdesc->len = len;
+ INIT_LIST_HEAD(&vdesc->node);
+
+ return &vdesc->txd;
+}
+
+static
+struct dma_async_tx_descriptor * fake_dma_prep_memset(struct dma_chan *chan,
+ dma_addr_t dest,
+ int value,
+ size_t len,
+ unsigned long flags)
+{
+ struct fake_dma_desc *vdesc;
+
+ vdesc = kzalloc(sizeof(*vdesc), GFP_NOWAIT);
+ if (!vdesc)
+ return NULL;
+
+ dma_async_tx_descriptor_init(&vdesc->txd, chan);
+ vdesc->type = DMA_MEMSET;
+ vdesc->txd.tx_submit = fake_dma_tx_submit;
+ vdesc->txd.flags = flags;
+ vdesc->dst = dest;
+ vdesc->len = len;
+ vdesc->memset_value = value & 0xFF; /* Ensure it's a single byte */
+
+ INIT_LIST_HEAD(&vdesc->node);
+
+ return &vdesc->txd;
+}
+
+static struct dma_async_tx_descriptor *
+fake_dma_prep_xor(struct dma_chan *chan, dma_addr_t dest, dma_addr_t *src,
+ unsigned int src_cnt, size_t len, unsigned long flags)
+{
+ struct fake_dma_desc *vdesc;
+
+ vdesc = kzalloc(sizeof(*vdesc), GFP_NOWAIT);
+ if (!vdesc)
+ return NULL;
+
+ /* Allocate memory for source list */
+ vdesc->src_list = kmalloc(src_cnt * sizeof(dma_addr_t), GFP_NOWAIT);
+ if (!vdesc->src_list) {
+ kfree(vdesc);
+ return NULL;
+ }
+
+ dma_async_tx_descriptor_init(&vdesc->txd, chan);
+ vdesc->type = DMA_XOR;
+ vdesc->txd.tx_submit = fake_dma_tx_submit;
+ vdesc->txd.flags = flags;
+ vdesc->dst = dest;
+ vdesc->len = len;
+ vdesc->src_cnt = src_cnt;
+
+ memcpy(vdesc->src_list, src, src_cnt * sizeof(dma_addr_t));
+
+ INIT_LIST_HEAD(&vdesc->node);
+
+ return &vdesc->txd;
+}
+
+static struct dma_async_tx_descriptor *
+fake_dma_prep_pq(struct dma_chan *chan, dma_addr_t *dst, dma_addr_t *src,
+ unsigned int src_cnt, const unsigned char *scf, size_t len,
+ unsigned long flags)
+{
+ struct fake_dma_desc *vdesc;
+
+ vdesc = kzalloc(sizeof(*vdesc), GFP_NOWAIT);
+ if (!vdesc)
+ return NULL;
+
+ vdesc->src_list = kmalloc(src_cnt * sizeof(dma_addr_t), GFP_NOWAIT);
+ if (!vdesc->src_list) {
+ kfree(vdesc);
+ return NULL;
+ }
+
+ /* Allocate memory for destination list (P and Q) */
+ vdesc->dst_list = kmalloc(2 * sizeof(dma_addr_t), GFP_NOWAIT);
+ if (!vdesc->dst_list) {
+ kfree(vdesc->src_list);
+ kfree(vdesc);
+ return NULL;
+ }
+
+ /* Allocate memory for coefficients */
+ vdesc->pq_coef = kmalloc(src_cnt * sizeof(unsigned char), GFP_NOWAIT);
+ if (!vdesc->pq_coef) {
+ kfree(vdesc->dst_list);
+ kfree(vdesc->src_list);
+ kfree(vdesc);
+ return NULL;
+ }
+
+ dma_async_tx_descriptor_init(&vdesc->txd, chan);
+ vdesc->type = DMA_PQ;
+ vdesc->txd.tx_submit = fake_dma_tx_submit;
+ vdesc->txd.flags = flags;
+ vdesc->len = len;
+ vdesc->src_cnt = src_cnt;
+
+ /* Copy source addresses */
+ memcpy(vdesc->src_list, src, src_cnt * sizeof(dma_addr_t));
+ /* Copy destination addresses (P and Q) */
+ memcpy(vdesc->dst_list, dst, 2 * sizeof(dma_addr_t));
+ /* Copy coefficients */
+ memcpy(vdesc->pq_coef, scf, src_cnt * sizeof(unsigned char));
+
+ INIT_LIST_HEAD(&vdesc->node);
+
+ return &vdesc->txd;
+}
+
+static void fake_dma_issue_pending(struct dma_chan *chan)
+{
+ struct fake_dma_chan *vchan = to_fake_dma_chan(chan);
+ unsigned long flags;
+
+ spin_lock_irqsave(&vchan->lock, flags);
+
+ /* Start processing if not already running and queue not empty */
+ if (!vchan->running && !list_empty(&vchan->queue)) {
+ vchan->running = true;
+ schedule_work(&vchan->work);
+ }
+
+ spin_unlock_irqrestore(&vchan->lock, flags);
+}
+
+static int fake_dma_alloc_chan_resources(struct dma_chan *chan)
+{
+ struct fake_dma_chan *vchan = to_fake_dma_chan(chan);
+
+ INIT_LIST_HEAD(&vchan->active_list);
+ INIT_LIST_HEAD(&vchan->queue);
+ vchan->running = false;
+
+ return 1; /* Number of descriptors allocated */
+}
+
+static void fake_dma_free_chan_resources(struct dma_chan *chan)
+{
+ struct fake_dma_chan *vchan = to_fake_dma_chan(chan);
+ struct fake_dma_desc *vdesc, *_vdesc;
+ unsigned long flags;
+
+ cancel_work_sync(&vchan->work);
+
+ spin_lock_irqsave(&vchan->lock, flags);
+
+ /* Free all descriptors in queue */
+ list_for_each_entry_safe(vdesc, _vdesc, &vchan->queue, node) {
+ list_del(&vdesc->node);
+
+ /* Free allocated memory for XOR/PQ operations */
+ if (vdesc->type == DMA_XOR || vdesc->type == DMA_PQ) {
+ kfree(vdesc->src_list);
+ if (vdesc->type == DMA_PQ) {
+ kfree(vdesc->dst_list);
+ kfree(vdesc->pq_coef);
+ }
+ }
+ kfree(vdesc);
+ }
+
+ /* Free all descriptors in active list */
+ list_for_each_entry_safe(vdesc, _vdesc, &vchan->active_list, node) {
+ list_del(&vdesc->node);
+ /* Free allocated memory for XOR/PQ operations */
+ if (vdesc->type == DMA_XOR || vdesc->type == DMA_PQ) {
+ kfree(vdesc->src_list);
+ if (vdesc->type == DMA_PQ) {
+ kfree(vdesc->dst_list);
+ kfree(vdesc->pq_coef);
+ }
+ }
+ kfree(vdesc);
+ }
+
+ spin_unlock_irqrestore(&vchan->lock, flags);
+}
+
+static void fake_dma_release(struct dma_device *dma_dev)
+{
+ unsigned int i;
+ struct fake_dma_device *fake_dma =
+ container_of(dma_dev, struct fake_dma_device, dma_dev);
+
+ pr_info("refcount for dma device %s hit 0, quiescing...",
+ dev_name(&fake_dma->pdev->dev));
+
+ for (i = 0; i < num_channels; i++) {
+ struct fake_dma_chan *vchan = &fake_dma->channels[i];
+ cancel_work_sync(&vchan->work);
+ }
+
+ put_device(dma_dev->dev);
+}
+
+static void fake_dma_setup_config(struct fake_dma_device *fake_dma)
+{
+ unsigned int i;
+ struct dma_device *dma = &fake_dma->dma_dev;
+
+ dma->dev = get_device(&fake_dma->pdev->dev);
+
+ /* Set multiple capabilities for dmatest compatibility */
+ dma_cap_set(DMA_MEMCPY, dma->cap_mask);
+ dma_cap_set(DMA_MEMSET, dma->cap_mask);
+ dma_cap_set(DMA_XOR, dma->cap_mask);
+ dma_cap_set(DMA_PQ, dma->cap_mask);
+ dma_cap_set(DMA_PRIVATE, dma->cap_mask);
+
+ dma->device_alloc_chan_resources = fake_dma_alloc_chan_resources;
+ dma->device_free_chan_resources = fake_dma_free_chan_resources;
+ dma->device_prep_dma_memcpy = fake_dma_prep_memcpy;
+ dma->device_prep_dma_memset = fake_dma_prep_memset;
+ dma->device_prep_dma_xor = fake_dma_prep_xor;
+ dma->device_prep_dma_pq = fake_dma_prep_pq;
+ dma->device_issue_pending = fake_dma_issue_pending;
+ dma->device_tx_status = dma_cookie_status;
+ dma->device_release = fake_dma_release;
+
+ dma->copy_align = 4; /* 4-byte alignment for memcpy */
+ dma->fill_align = 4; /* 4-byte alignment for memset */
+ dma->xor_align = 4; /* 4-byte alignment for xor */
+ dma->pq_align = 4; /* 4-byte alignment for pq */
+
+ dma->max_xor = 16; /* Support up to 16 XOR sources */
+ dma->max_pq = 16; /* Support up to 16 P+Q sources */
+
+ dma->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
+ BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
+ BIT(DMA_SLAVE_BUSWIDTH_4_BYTES) |
+ BIT(DMA_SLAVE_BUSWIDTH_8_BYTES);
+ dma->dst_addr_widths = dma->src_addr_widths;
+ dma->directions = BIT(DMA_MEM_TO_MEM);
+ dma->residue_granularity = DMA_RESIDUE_GRANULARITY_DESCRIPTOR;
+
+ INIT_LIST_HEAD(&dma->channels);
+
+ for (i = 0; i < num_channels; i++) {
+ struct fake_dma_chan *vchan = &fake_dma->channels[i];
+
+ vchan->chan.device = dma;
+ dma_cookie_init(&vchan->chan);
+
+ spin_lock_init(&vchan->lock);
+ INIT_LIST_HEAD(&vchan->active_list);
+ INIT_LIST_HEAD(&vchan->queue);
+
+ INIT_WORK(&vchan->work, fake_dma_work_func);
+
+ list_add_tail(&vchan->chan.device_node, &dma->channels);
+ }
+}
+
+static int fake_dma_load(void)
+{
+ unsigned int i;
+ int ret;
+ struct fake_dma_device *fake_dma;
+ struct dma_device *dma;
+
+ if (single_fake_dma) {
+ pr_err("Fake DMA device already loaded, skipping...");
+ return -EALREADY;
+ }
+
+ if (num_channels > FAKE_MAX_DMA_CHANNELS)
+ num_channels = FAKE_MAX_DMA_CHANNELS;
+
+ ret = platform_driver_register(&fake_dma_engine_driver);
+ if (ret)
+ return ret;
+
+ fake_dma = kzalloc(sizeof(*fake_dma), GFP_KERNEL);
+ if (!fake_dma) {
+ ret = -ENOMEM;
+ goto out_unregister_driver;
+ }
+
+ fake_dma->channels = kzalloc(sizeof(struct fake_dma_chan) * num_channels,
+ GFP_KERNEL);
+ if (!fake_dma->channels) {
+ ret = -ENOMEM;
+ goto out_free_dma;
+ }
+
+ ret = fake_dma_create_platform_device(fake_dma);
+ if (ret)
+ goto out_free_chans;
+
+ fake_dma->pdev->dev.driver = &fake_dma_engine_driver.driver;
+ ret = device_bind_driver(&fake_dma->pdev->dev);
+ if (ret)
+ goto out_unregister_device;
+
+ fake_dma_setup_config(fake_dma);
+ dma = &fake_dma->dma_dev;
+
+ /* Register with the DMA Engine */
+ ret = dma_async_device_register(dma);
+ if (ret) {
+ ret = -EINVAL;
+ goto out_release_driver;
+ }
+
+ for (i = 0; i < num_channels; i++) {
+ struct fake_dma_chan *vchan = &fake_dma->channels[i];
+ pr_info("Registered fake DMA channel %d (%s)\n",
+ i, dma_chan_name(&vchan->chan));
+ }
+
+ single_fake_dma = fake_dma;
+
+ pr_info("Fake DMA engine: %s registered with %d channels\n",
+ dev_name(&fake_dma->pdev->dev), num_channels);
+
+ pr_info("Fake DMA device name for dmatest: '%s'\n", dev_name(dma->dev));
+ pr_info("Fake DMA device path: '%s'\n", dev_name(&fake_dma->pdev->dev));
+
+ return 0;
+
+out_release_driver:
+ device_release_driver(&fake_dma->pdev->dev);
+out_unregister_device:
+ fake_dma_destroy_platform_device(fake_dma);
+out_free_chans:
+ kfree(fake_dma->channels);
+out_free_dma:
+ kfree(fake_dma);
+ fake_dma = NULL;
+out_unregister_driver:
+ platform_driver_unregister(&fake_dma_engine_driver);
+ return ret;
+}
+
+static void fake_dma_unload(void)
+{
+ struct fake_dma_device *fake_dma = single_fake_dma;
+
+ if (!fake_dma) {
+ pr_info("No fake DMA engines registered yet.\n");
+ return;
+ }
+
+ pr_info("Fake DMA engine: %s unregistering with %d channels ...\n",
+ dev_name(&fake_dma->pdev->dev), num_channels);
+
+ dma_async_device_unregister(&fake_dma->dma_dev);
+
+ /*
+ * dma_async_device_unregister() will call device_release() only
+ * if a channel ever gets busy, so we need to tidy up ourselves
+ * here in case no channels are ever used.
+ */
+ device_release_driver(&fake_dma->pdev->dev);
+ fake_dma_destroy_platform_device(fake_dma);
+
+ kfree(fake_dma->channels);
+ kfree(fake_dma);
+
+ platform_driver_unregister(&fake_dma_engine_driver);
+ single_fake_dma = NULL;
+}
+
+static ssize_t write_file_load(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ fake_dma_load();
+
+ return count;
+}
+
+static const struct file_operations fops_load = {
+ .write = write_file_load,
+ .open = simple_open,
+ .owner = THIS_MODULE,
+ .llseek = default_llseek,
+};
+
+static ssize_t write_file_unload(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ fake_dma_unload();
+
+ return count;
+}
+
+static const struct file_operations fops_unload = {
+ .write = write_file_unload,
+ .open = simple_open,
+ .owner = THIS_MODULE,
+ .llseek = default_llseek,
+};
+
+static int __init fake_dma_init(void)
+{
+ struct dentry *fake_dir;
+
+ fake_dir = debugfs_create_dir("fake-dma", NULL);
+ debugfs_create_file("load", 0600, fake_dir, NULL, &fops_load);
+ debugfs_create_file("unload", 0600, fake_dir, NULL, &fops_unload);
+
+ return fake_dma_load();
+}
+late_initcall(fake_dma_init);
+
+static void __exit fake_dma_exit(void)
+{
+ fake_dma_unload();
+}
+module_exit(fake_dma_exit);
+
+MODULE_DESCRIPTION("Fake DMA Engine test module");
+MODULE_AUTHOR("Luis Chamberlain");
+MODULE_LICENSE("GPL v2");