| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- // SPDX-License-Identifier: GPL-2.0-or-later
- /*
- * Driver for Loongson-1 APB DMA Controller
- *
- * Copyright (C) 2015-2024 Keguang Zhang <keguang.zhang@gmail.com>
- */
- #include <linux/dmapool.h>
- #include <linux/dma-mapping.h>
- #include <linux/init.h>
- #include <linux/interrupt.h>
- #include <linux/iopoll.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 Control Register */
- #define LS1X_DMA_CTRL 0x0
- /* DMA Control Register Bits */
- #define LS1X_DMA_STOP BIT(4)
- #define LS1X_DMA_START BIT(3)
- #define LS1X_DMA_ASK_VALID BIT(2)
- /* DMA Next Field Bits */
- #define LS1X_DMA_NEXT_VALID BIT(0)
- /* DMA Command Field Bits */
- #define LS1X_DMA_RAM2DEV BIT(12)
- #define LS1X_DMA_INT BIT(1)
- #define LS1X_DMA_INT_MASK BIT(0)
- #define LS1X_DMA_LLI_ALIGNMENT 64
- #define LS1X_DMA_LLI_ADDR_MASK GENMASK(31, __ffs(LS1X_DMA_LLI_ALIGNMENT))
- #define LS1X_DMA_MAX_CHANNELS 3
- enum ls1x_dmadesc_offsets {
- LS1X_DMADESC_NEXT = 0,
- LS1X_DMADESC_SADDR,
- LS1X_DMADESC_DADDR,
- LS1X_DMADESC_LENGTH,
- LS1X_DMADESC_STRIDE,
- LS1X_DMADESC_CYCLES,
- LS1X_DMADESC_CMD,
- LS1X_DMADESC_SIZE
- };
- struct ls1x_dma_lli {
- unsigned int hw[LS1X_DMADESC_SIZE];
- dma_addr_t phys;
- struct list_head node;
- } __aligned(LS1X_DMA_LLI_ALIGNMENT);
- struct ls1x_dma_desc {
- struct virt_dma_desc vd;
- struct list_head lli_list;
- };
- struct ls1x_dma_chan {
- struct virt_dma_chan vc;
- struct dma_pool *lli_pool;
- phys_addr_t src_addr;
- phys_addr_t dst_addr;
- enum dma_slave_buswidth src_addr_width;
- enum dma_slave_buswidth dst_addr_width;
- unsigned int bus_width;
- void __iomem *reg_base;
- int irq;
- bool is_cyclic;
- struct ls1x_dma_lli *curr_lli;
- };
- struct ls1x_dma {
- struct dma_device ddev;
- unsigned int nr_chans;
- struct ls1x_dma_chan chan[];
- };
- static irqreturn_t ls1x_dma_irq_handler(int irq, void *data);
- #define to_ls1x_dma_chan(dchan) \
- container_of(dchan, struct ls1x_dma_chan, vc.chan)
- #define to_ls1x_dma_desc(d) \
- container_of(d, struct ls1x_dma_desc, vd)
- static inline struct device *chan2dev(struct dma_chan *chan)
- {
- return &chan->dev->device;
- }
- static inline int ls1x_dma_query(struct ls1x_dma_chan *chan,
- dma_addr_t *lli_phys)
- {
- struct dma_chan *dchan = &chan->vc.chan;
- int val, ret;
- val = *lli_phys & LS1X_DMA_LLI_ADDR_MASK;
- val |= LS1X_DMA_ASK_VALID;
- val |= dchan->chan_id;
- writel(val, chan->reg_base + LS1X_DMA_CTRL);
- ret = readl_poll_timeout_atomic(chan->reg_base + LS1X_DMA_CTRL, val,
- !(val & LS1X_DMA_ASK_VALID), 0, 3000);
- if (ret)
- dev_err(chan2dev(dchan), "failed to query DMA\n");
- return ret;
- }
- static inline int ls1x_dma_start(struct ls1x_dma_chan *chan,
- dma_addr_t *lli_phys)
- {
- struct dma_chan *dchan = &chan->vc.chan;
- struct device *dev = chan2dev(dchan);
- int val, ret;
- val = *lli_phys & LS1X_DMA_LLI_ADDR_MASK;
- val |= LS1X_DMA_START;
- val |= dchan->chan_id;
- writel(val, chan->reg_base + LS1X_DMA_CTRL);
- ret = readl_poll_timeout(chan->reg_base + LS1X_DMA_CTRL, val,
- !(val & LS1X_DMA_START), 0, 1000);
- if (!ret)
- dev_dbg(dev, "start DMA with lli_phys=%pad\n", lli_phys);
- else
- dev_err(dev, "failed to start DMA\n");
- return ret;
- }
- static inline void ls1x_dma_stop(struct ls1x_dma_chan *chan)
- {
- int val = readl(chan->reg_base + LS1X_DMA_CTRL);
- writel(val | LS1X_DMA_STOP, chan->reg_base + LS1X_DMA_CTRL);
- }
- static void ls1x_dma_free_chan_resources(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- struct device *dev = chan2dev(dchan);
- dma_free_coherent(dev, sizeof(struct ls1x_dma_lli),
- chan->curr_lli, chan->curr_lli->phys);
- dma_pool_destroy(chan->lli_pool);
- chan->lli_pool = NULL;
- devm_free_irq(dev, chan->irq, chan);
- vchan_free_chan_resources(&chan->vc);
- }
- static int ls1x_dma_alloc_chan_resources(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- struct device *dev = chan2dev(dchan);
- dma_addr_t phys;
- int ret;
- ret = devm_request_irq(dev, chan->irq, ls1x_dma_irq_handler,
- IRQF_SHARED, dma_chan_name(dchan), chan);
- if (ret) {
- dev_err(dev, "failed to request IRQ %d\n", chan->irq);
- return ret;
- }
- chan->lli_pool = dma_pool_create(dma_chan_name(dchan), dev,
- sizeof(struct ls1x_dma_lli),
- __alignof__(struct ls1x_dma_lli), 0);
- if (!chan->lli_pool)
- return -ENOMEM;
- /* allocate memory for querying the current lli */
- dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
- chan->curr_lli = dma_alloc_coherent(dev, sizeof(struct ls1x_dma_lli),
- &phys, GFP_KERNEL);
- if (!chan->curr_lli) {
- dma_pool_destroy(chan->lli_pool);
- return -ENOMEM;
- }
- chan->curr_lli->phys = phys;
- return 0;
- }
- static void ls1x_dma_free_desc(struct virt_dma_desc *vd)
- {
- struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vd);
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(vd->tx.chan);
- struct ls1x_dma_lli *lli, *_lli;
- list_for_each_entry_safe(lli, _lli, &desc->lli_list, node) {
- list_del(&lli->node);
- dma_pool_free(chan->lli_pool, lli, lli->phys);
- }
- kfree(desc);
- }
- static struct ls1x_dma_desc *ls1x_dma_alloc_desc(void)
- {
- struct ls1x_dma_desc *desc;
- desc = kzalloc(sizeof(*desc), GFP_NOWAIT);
- if (!desc)
- return NULL;
- INIT_LIST_HEAD(&desc->lli_list);
- return desc;
- }
- static int ls1x_dma_prep_lli(struct dma_chan *dchan, struct ls1x_dma_desc *desc,
- struct scatterlist *sgl, unsigned int sg_len,
- enum dma_transfer_direction dir, bool is_cyclic)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- struct ls1x_dma_lli *lli, *prev = NULL, *first = NULL;
- struct device *dev = chan2dev(dchan);
- struct list_head *pos = NULL;
- struct scatterlist *sg;
- unsigned int dev_addr, cmd, i;
- switch (dir) {
- case DMA_MEM_TO_DEV:
- dev_addr = chan->dst_addr;
- chan->bus_width = chan->dst_addr_width;
- cmd = LS1X_DMA_RAM2DEV | LS1X_DMA_INT;
- break;
- case DMA_DEV_TO_MEM:
- dev_addr = chan->src_addr;
- chan->bus_width = chan->src_addr_width;
- cmd = LS1X_DMA_INT;
- break;
- default:
- dev_err(dev, "unsupported DMA direction: %s\n",
- dmaengine_get_direction_text(dir));
- return -EINVAL;
- }
- 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);
- dma_addr_t phys;
- if (!is_dma_copy_aligned(dchan->device, buf_addr, 0, buf_len)) {
- dev_err(dev, "buffer is not aligned\n");
- return -EINVAL;
- }
- /* allocate HW descriptors */
- lli = dma_pool_zalloc(chan->lli_pool, GFP_NOWAIT, &phys);
- if (!lli) {
- dev_err(dev, "failed to alloc lli %u\n", i);
- return -ENOMEM;
- }
- /* setup HW descriptors */
- lli->phys = phys;
- lli->hw[LS1X_DMADESC_SADDR] = buf_addr;
- lli->hw[LS1X_DMADESC_DADDR] = dev_addr;
- lli->hw[LS1X_DMADESC_LENGTH] = buf_len / chan->bus_width;
- lli->hw[LS1X_DMADESC_STRIDE] = 0;
- lli->hw[LS1X_DMADESC_CYCLES] = 1;
- lli->hw[LS1X_DMADESC_CMD] = cmd;
- if (prev)
- prev->hw[LS1X_DMADESC_NEXT] =
- lli->phys | LS1X_DMA_NEXT_VALID;
- prev = lli;
- if (!first)
- first = lli;
- list_add_tail(&lli->node, &desc->lli_list);
- }
- if (is_cyclic) {
- lli->hw[LS1X_DMADESC_NEXT] = first->phys | LS1X_DMA_NEXT_VALID;
- chan->is_cyclic = is_cyclic;
- }
- list_for_each(pos, &desc->lli_list) {
- lli = list_entry(pos, struct ls1x_dma_lli, node);
- print_hex_dump_debug("LLI: ", DUMP_PREFIX_OFFSET, 16, 4,
- lli, sizeof(*lli), false);
- }
- return 0;
- }
- 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 dir,
- unsigned long flags, void *context)
- {
- struct ls1x_dma_desc *desc;
- dev_dbg(chan2dev(dchan), "sg_len=%u flags=0x%lx dir=%s\n",
- sg_len, flags, dmaengine_get_direction_text(dir));
- desc = ls1x_dma_alloc_desc();
- if (!desc)
- return NULL;
- if (ls1x_dma_prep_lli(dchan, desc, sgl, sg_len, dir, false)) {
- ls1x_dma_free_desc(&desc->vd);
- return NULL;
- }
- return vchan_tx_prep(to_virt_chan(dchan), &desc->vd, flags);
- }
- static struct dma_async_tx_descriptor *
- ls1x_dma_prep_dma_cyclic(struct dma_chan *dchan, dma_addr_t buf_addr,
- size_t buf_len, size_t period_len,
- enum dma_transfer_direction dir, unsigned long flags)
- {
- struct ls1x_dma_desc *desc;
- struct scatterlist *sgl;
- unsigned int sg_len;
- unsigned int i;
- int ret;
- dev_dbg(chan2dev(dchan),
- "buf_len=%zu period_len=%zu flags=0x%lx dir=%s\n",
- buf_len, period_len, flags, dmaengine_get_direction_text(dir));
- desc = ls1x_dma_alloc_desc();
- if (!desc)
- return NULL;
- /* allocate the scatterlist */
- sg_len = buf_len / period_len;
- sgl = kmalloc_array(sg_len, sizeof(*sgl), GFP_NOWAIT);
- if (!sgl)
- return NULL;
- sg_init_table(sgl, sg_len);
- for (i = 0; i < sg_len; ++i) {
- sg_set_page(&sgl[i], pfn_to_page(PFN_DOWN(buf_addr)),
- period_len, offset_in_page(buf_addr));
- sg_dma_address(&sgl[i]) = buf_addr;
- sg_dma_len(&sgl[i]) = period_len;
- buf_addr += period_len;
- }
- ret = ls1x_dma_prep_lli(dchan, desc, sgl, sg_len, dir, true);
- kfree(sgl);
- if (ret) {
- ls1x_dma_free_desc(&desc->vd);
- return NULL;
- }
- return vchan_tx_prep(to_virt_chan(dchan), &desc->vd, flags);
- }
- 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->src_addr = config->src_addr;
- chan->src_addr_width = config->src_addr_width;
- chan->dst_addr = config->dst_addr;
- chan->dst_addr_width = config->dst_addr_width;
- return 0;
- }
- static int ls1x_dma_pause(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- int ret;
- guard(spinlock_irqsave)(&chan->vc.lock);
- /* save the current lli */
- ret = ls1x_dma_query(chan, &chan->curr_lli->phys);
- if (!ret)
- ls1x_dma_stop(chan);
- return ret;
- }
- static int ls1x_dma_resume(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- guard(spinlock_irqsave)(&chan->vc.lock);
- return ls1x_dma_start(chan, &chan->curr_lli->phys);
- }
- static int ls1x_dma_terminate_all(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- struct virt_dma_desc *vd;
- LIST_HEAD(head);
- ls1x_dma_stop(chan);
- scoped_guard(spinlock_irqsave, &chan->vc.lock) {
- vd = vchan_next_desc(&chan->vc);
- if (vd)
- vchan_terminate_vdesc(vd);
- vchan_get_all_descriptors(&chan->vc, &head);
- }
- vchan_dma_desc_free_list(&chan->vc, &head);
- return 0;
- }
- static void ls1x_dma_synchronize(struct dma_chan *dchan)
- {
- vchan_synchronize(to_virt_chan(dchan));
- }
- static enum dma_status ls1x_dma_tx_status(struct dma_chan *dchan,
- dma_cookie_t cookie,
- struct dma_tx_state *state)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- struct virt_dma_desc *vd;
- enum dma_status status;
- size_t bytes = 0;
- status = dma_cookie_status(dchan, cookie, state);
- if (status == DMA_COMPLETE)
- return status;
- scoped_guard(spinlock_irqsave, &chan->vc.lock) {
- vd = vchan_find_desc(&chan->vc, cookie);
- if (vd) {
- struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vd);
- struct ls1x_dma_lli *lli;
- dma_addr_t next_phys;
- /* get the current lli */
- if (ls1x_dma_query(chan, &chan->curr_lli->phys))
- return status;
- /* locate the current lli */
- next_phys = chan->curr_lli->hw[LS1X_DMADESC_NEXT];
- list_for_each_entry(lli, &desc->lli_list, node)
- if (lli->hw[LS1X_DMADESC_NEXT] == next_phys)
- break;
- dev_dbg(chan2dev(dchan), "current lli_phys=%pad",
- &lli->phys);
- /* count the residues */
- list_for_each_entry_from(lli, &desc->lli_list, node)
- bytes += lli->hw[LS1X_DMADESC_LENGTH] *
- chan->bus_width;
- }
- }
- dma_set_residue(state, bytes);
- return status;
- }
- static void ls1x_dma_issue_pending(struct dma_chan *dchan)
- {
- struct ls1x_dma_chan *chan = to_ls1x_dma_chan(dchan);
- guard(spinlock_irqsave)(&chan->vc.lock);
- if (vchan_issue_pending(&chan->vc)) {
- struct virt_dma_desc *vd = vchan_next_desc(&chan->vc);
- if (vd) {
- struct ls1x_dma_desc *desc = to_ls1x_dma_desc(vd);
- struct ls1x_dma_lli *lli;
- lli = list_first_entry(&desc->lli_list,
- struct ls1x_dma_lli, node);
- ls1x_dma_start(chan, &lli->phys);
- }
- }
- }
- static irqreturn_t ls1x_dma_irq_handler(int irq, void *data)
- {
- struct ls1x_dma_chan *chan = data;
- struct dma_chan *dchan = &chan->vc.chan;
- struct device *dev = chan2dev(dchan);
- struct virt_dma_desc *vd;
- scoped_guard(spinlock, &chan->vc.lock) {
- vd = vchan_next_desc(&chan->vc);
- if (!vd) {
- dev_warn(dev,
- "IRQ %d with no active desc on channel %d\n",
- irq, dchan->chan_id);
- return IRQ_NONE;
- }
- if (chan->is_cyclic) {
- vchan_cyclic_callback(vd);
- } else {
- list_del(&vd->node);
- vchan_cookie_complete(vd);
- }
- }
- dev_dbg(dev, "DMA IRQ %d on channel %d\n", irq, dchan->chan_id);
- return IRQ_HANDLED;
- }
- static int ls1x_dma_chan_probe(struct platform_device *pdev,
- struct ls1x_dma *dma)
- {
- void __iomem *reg_base;
- int id;
- reg_base = devm_platform_ioremap_resource(pdev, 0);
- if (IS_ERR(reg_base))
- return PTR_ERR(reg_base);
- for (id = 0; id < dma->nr_chans; id++) {
- struct ls1x_dma_chan *chan = &dma->chan[id];
- char pdev_irqname[16];
- snprintf(pdev_irqname, sizeof(pdev_irqname), "ch%d", id);
- chan->irq = platform_get_irq_byname(pdev, pdev_irqname);
- if (chan->irq < 0)
- return dev_err_probe(&pdev->dev, chan->irq,
- "failed to get IRQ for ch%d\n",
- id);
- chan->reg_base = reg_base;
- chan->vc.desc_free = ls1x_dma_free_desc;
- vchan_init(&chan->vc, &dma->ddev);
- }
- return 0;
- }
- static void ls1x_dma_chan_remove(struct ls1x_dma *dma)
- {
- int id;
- for (id = 0; id < dma->nr_chans; id++) {
- struct ls1x_dma_chan *chan = &dma->chan[id];
- if (chan->vc.chan.device == &dma->ddev) {
- list_del(&chan->vc.chan.device_node);
- tasklet_kill(&chan->vc.task);
- }
- }
- }
- static int ls1x_dma_probe(struct platform_device *pdev)
- {
- struct device *dev = &pdev->dev;
- struct dma_device *ddev;
- struct ls1x_dma *dma;
- int ret;
- ret = platform_irq_count(pdev);
- if (ret <= 0 || ret > LS1X_DMA_MAX_CHANNELS)
- return dev_err_probe(dev, -EINVAL,
- "Invalid number of IRQ channels: %d\n",
- ret);
- dma = devm_kzalloc(dev, struct_size(dma, chan, ret), GFP_KERNEL);
- if (!dma)
- return -ENOMEM;
- dma->nr_chans = ret;
- /* initialize DMA device */
- ddev = &dma->ddev;
- ddev->dev = dev;
- ddev->copy_align = DMAENGINE_ALIGN_4_BYTES;
- ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
- BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
- BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
- ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
- BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
- 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_prep_dma_cyclic = ls1x_dma_prep_dma_cyclic;
- ddev->device_config = ls1x_dma_slave_config;
- ddev->device_pause = ls1x_dma_pause;
- ddev->device_resume = ls1x_dma_resume;
- ddev->device_terminate_all = ls1x_dma_terminate_all;
- ddev->device_synchronize = ls1x_dma_synchronize;
- ddev->device_tx_status = ls1x_dma_tx_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 */
- ret = ls1x_dma_chan_probe(pdev, dma);
- if (ret)
- goto err;
- ret = dmaenginem_async_device_register(ddev);
- if (ret) {
- dev_err(dev, "failed to register DMA device\n");
- goto err;
- }
- 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\n");
- goto err;
- }
- platform_set_drvdata(pdev, dma);
- dev_info(dev, "Loongson1 DMA driver registered\n");
- return 0;
- err:
- ls1x_dma_chan_remove(dma);
- return ret;
- }
- static void ls1x_dma_remove(struct platform_device *pdev)
- {
- struct ls1x_dma *dma = platform_get_drvdata(pdev);
- of_dma_controller_free(pdev->dev.of_node);
- ls1x_dma_chan_remove(dma);
- }
- static const struct of_device_id ls1x_dma_match[] = {
- { .compatible = "loongson,ls1b-apbdma" },
- { /* 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 = KBUILD_MODNAME,
- .of_match_table = ls1x_dma_match,
- },
- };
- module_platform_driver(ls1x_dma_driver);
- MODULE_AUTHOR("Keguang Zhang <keguang.zhang@gmail.com>");
- MODULE_DESCRIPTION("Loongson-1 APB DMA Controller driver");
- MODULE_LICENSE("GPL");
|