[PATCH 08/17] dmaengine: dw-edma: Make DMA link list work as a circular buffer
From: Koichiro Den
Date: Mon Jun 15 2026 - 11:50:34 EST
From: Frank Li <Frank.Li@xxxxxxx>
The existing code rebuilds the entire link list from the beginning and
resets the DMA link header for each transfer, which is unnecessary.
The DMA link list can be treated as a circular buffer, where new DMA
requests are appended at ll_head with the appropriate CB flags and ring
the doorbell, without rebuilding the whole list.
Switch to this circular-buffer model to prepare for dynamically adding
new requests while the DMA engine is running.
Signed-off-by: Frank Li <Frank.Li@xxxxxxx>
[den: fixed partial-append start_burst accounting; refreshed the fixed
link element before each new lap; dropped the unused first argument;
fixed checkpatch.pl issues; polished doorbell wording]
Signed-off-by: Koichiro Den <den@xxxxxxxxxxxxx>
---
Based on the original submission from Frank:
https://lore.kernel.org/dmaengine/20260109-edma_dymatic-v1-3-9a98c9c98536@xxxxxxx/
drivers/dma/dw-edma/dw-edma-core.c | 71 +++++++++++++++++++++++-------
drivers/dma/dw-edma/dw-edma-core.h | 25 ++++++++++-
2 files changed, 79 insertions(+), 17 deletions(-)
diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index 5e41b1aab450..cac03c59bfe4 100644
--- a/drivers/dma/dw-edma/dw-edma-core.c
+++ b/drivers/dma/dw-edma/dw-edma-core.c
@@ -52,7 +52,6 @@ dw_edma_alloc_desc(struct dw_edma_chan *chan, u32 nburst)
desc->chan = chan;
desc->nburst = nburst;
- desc->cb = true;
return desc;
}
@@ -62,27 +61,64 @@ static void vchan_free_desc(struct virt_dma_desc *vdesc)
kfree(vd2dw_edma_desc(vdesc));
}
-static void dw_edma_core_start(struct dw_edma_desc *desc, bool first)
+static void dw_edma_core_reset_ll(struct dw_edma_chan *chan)
+{
+ chan->ll_head = 0;
+ chan->ll_end = 0;
+ chan->cb = true;
+
+ dw_edma_core_ll_link(chan, chan->ll_max - 1, chan->cb,
+ chan->ll_region.paddr);
+
+ dw_edma_core_ch_enable(chan);
+}
+
+static u32 dw_edma_core_get_free_num(struct dw_edma_chan *chan)
+{
+ /*
+ * Max entries is ll_max - 1 because last one used for link back to
+ * start of ll_region.
+ */
+ return (chan->ll_end + chan->ll_max - 2 - chan->ll_head) %
+ (chan->ll_max - 1);
+}
+
+static void dw_edma_core_start(struct dw_edma_desc *desc)
{
struct dw_edma_chan *chan = desc->chan;
u32 i = 0;
+ u32 free;
+
+ for (i = desc->start_burst; i < desc->nburst; i++) {
+ free = dw_edma_core_get_free_num(chan);
- for (i = 0; i < desc->nburst; i++) {
- if (i == chan->ll_max - 1)
+ if (!free)
break;
- dw_edma_core_ll_data(chan, &desc->burst[i + desc->start_burst],
- i, desc->cb,
- i == desc->nburst - 1 || i == chan->ll_max - 2);
- }
+ /*
+ * Refresh the link element before filling the last data slot so
+ * the next lap has the updated CB value.
+ */
+ if (chan->ll_head == chan->ll_max - 2)
+ dw_edma_core_ll_link(chan, chan->ll_max - 1, chan->cb,
+ chan->ll_region.paddr);
- desc->done_burst = desc->start_burst;
- desc->start_burst += i;
+ /* Enable irq for last free entry or last burst */
+ dw_edma_core_ll_data(chan, &desc->burst[i],
+ chan->ll_head, chan->cb,
+ i == desc->nburst - 1 || free == 1);
+
+ chan->ll_head++;
- dw_edma_core_ll_link(chan, i, desc->cb, chan->ll_region.paddr);
+ if (chan->ll_head == chan->ll_max - 1) {
+ chan->cb = !chan->cb;
+ chan->ll_head = 0;
+ }
+ }
- if (first)
- dw_edma_core_ch_enable(chan);
+ desc->done_burst = desc->start_burst;
+ desc->start_burst = i;
+ desc->ll_end = chan->ll_head;
dw_edma_core_ch_doorbell(chan);
}
@@ -91,6 +127,10 @@ static int dw_edma_start_transfer(struct dw_edma_chan *chan)
{
struct dw_edma_desc *desc;
struct virt_dma_desc *vd;
+ int index = dw_edma_core_ll_cur_idx(chan);
+
+ if (index < 0)
+ dw_edma_core_reset_ll(chan);
vd = vchan_next_desc(&chan->vc);
if (!vd)
@@ -100,9 +140,7 @@ static int dw_edma_start_transfer(struct dw_edma_chan *chan)
if (!desc)
return 0;
- dw_edma_core_start(desc, !desc->start_burst);
-
- desc->cb = !desc->cb;
+ dw_edma_core_start(desc);
return 1;
}
@@ -569,6 +607,7 @@ static void dw_edma_done_interrupt(struct dw_edma_chan *chan)
DMA_TRANS_NOERROR);
list_del(&vd->node);
vchan_cookie_complete(vd);
+ chan->ll_end = desc->ll_end;
}
/* Continue transferring if there are remaining chunks or issued requests.
diff --git a/drivers/dma/dw-edma/dw-edma-core.h b/drivers/dma/dw-edma/dw-edma-core.h
index d68c4592c617..46af4ea3ae5f 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -60,9 +60,10 @@ struct dw_edma_desc {
u32 alloc_sz;
u32 xfer_sz;
+ u32 ll_end;
+
u32 done_burst;
u32 start_burst;
- u8 cb;
u32 nburst;
struct dw_edma_burst burst[] __counted_by(nburst);
};
@@ -73,9 +74,31 @@ struct dw_edma_chan {
int id;
enum dw_edma_dir dir;
+ /*
+ * New LL entries are appended at ll_head. Entries between ll_end
+ * and ll_head, modulo the LL ring, are owned by DMA; the rest are
+ * owned by software.
+ *
+ * software-owned DMA-owned software-owned
+ * +---------------+-------------------+---------------+
+ * ^ ^ ^
+ * 0 ll_end ll_head
+ *
+ * The link entry points back to the region start. ll_head == ll_end
+ * means all entries are software-owned and previous DMA work is
+ * done.
+ *
+ * Software always keeps at least one free entry, so the ring is
+ * never completely DMA-owned.
+ */
+ u32 ll_head;
+ u32 ll_end;
+
u32 ll_max;
struct dw_edma_region ll_region; /* Linked list */
+ bool cb;
+
struct msi_msg msi;
enum dw_edma_request request;
--
2.51.0