[PATCH v4 15/24] dmaengine: dw-edma: Recheck stopped LL channels before restart
From: Koichiro Den
Date: Wed Jul 29 2026 - 10:44:13 EST
A DMA_LLP sample taken for a DONE interrupt through the eDMA-compatible
interface can race with the final DMA_LLP update and leave the recorded
consumer boundary one entry short. If the channel stops there, no later
interrupt reports the missing progress.
Before restarting a stopped LL channel, sample DMA_LLP again and reclaim
any newly visible progress. If the channel still reports running, do not
ring another doorbell. It may stop at a CB mismatch just after the
status read, so schedule one delayed recheck. Also recheck after actually
kicking a stopped channel, in case no event reports the result of the kick.
Native HDMA reports STOP separately and does not need this recheck.
Serialize the stopped-state check, LLP sample and interrupt-status clear
against IRQ capture. Do not reconcile a stopped boundary while ABORT is
pending. Keep stopped-boundary reconciliation separate from refilling so
STOP and PAUSE drain paths can consume it without publishing more entries.
Signed-off-by: Koichiro Den <den@xxxxxxxxxxxxx>
---
Changes in v4:
- Arm a recheck after kicking a stopped channel, not only after observing
it running. (Sashiko)
- Keep zero reserved as the cancelled recheck sentinel across jiffies
wrap. (Sashiko)
- Rephrase misleading "legacy eDMA" wording; it refers to the
eDMA-compatible interface, not a specific map format.
drivers/dma/dw-edma/dw-edma-core.c | 204 +++++++++++++++++++++++++++--
drivers/dma/dw-edma/dw-edma-core.h | 5 +
2 files changed, 197 insertions(+), 12 deletions(-)
diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index 16d3d2a8d1c2..0d1e6f48cac1 100644
--- a/drivers/dma/dw-edma/dw-edma-core.c
+++ b/drivers/dma/dw-edma/dw-edma-core.c
@@ -27,6 +27,7 @@
/* Empirically chosen progress interval. */
#define DW_EDMA_LL_PROGRESS_INTERVAL 4
+#define DW_EDMA_LL_RECHECK_DELAY_MS 30
static inline
struct dw_edma_desc *vd2dw_edma_desc(struct virt_dma_desc *vd)
@@ -148,6 +149,27 @@ static void dw_edma_ll_event_discard_locked(struct dw_edma_chan *chan)
dw_edma_ll_snapshot_discard_locked(chan);
}
+/* Must be called with vc.lock held. */
+static void dw_edma_ll_recheck_cancel(struct dw_edma_chan *chan)
+{
+ chan->ll_recheck_at = 0;
+ cancel_delayed_work(&chan->ll_recheck_work);
+}
+
+/* Must be called with vc.lock held. */
+static void dw_edma_ll_recheck_schedule(struct dw_edma_chan *chan)
+{
+ unsigned long delay =
+ msecs_to_jiffies(DW_EDMA_LL_RECHECK_DELAY_MS);
+
+ if (unlikely(READ_ONCE(chan->dw->teardown)))
+ return;
+
+ /* Keep zero reserved for a cancelled recheck across jiffies wrap. */
+ chan->ll_recheck_at = jiffies + delay ?: 1;
+ mod_delayed_work(chan->dw->wq, &chan->ll_recheck_work, delay);
+}
+
/* Must be called with vc.lock held for an LL channel. */
static void dw_edma_ll_event_discard(struct dw_edma_chan *chan)
{
@@ -160,6 +182,8 @@ static void dw_edma_ll_event_discard(struct dw_edma_chan *chan)
static void
dw_edma_set_request(struct dw_edma_chan *chan, enum dw_edma_request request)
{
+ if (!chan->non_ll && chan->request != request)
+ dw_edma_ll_recheck_cancel(chan);
chan->request = request;
}
@@ -192,6 +216,8 @@ static void dw_edma_core_reset_ll(struct dw_edma_chan *chan)
{
u32 i;
+ dw_edma_ll_recheck_cancel(chan);
+
chan->ll_head = 0;
chan->ll_done = 0;
/* Drop stale CB bits before reusing the circular LL ring. */
@@ -426,7 +452,7 @@ static bool dw_edma_ll_clean_pending(struct dw_edma_chan *chan, int idx)
if (WARN_ON_ONCE(desc->done_burst > desc->start_burst ||
desc->start_burst > desc->nburst))
- return advanced;
+ goto out;
/*
* start_burst is the next burst to append. done_burst counts
@@ -445,7 +471,7 @@ static bool dw_edma_ll_clean_pending(struct dw_edma_chan *chan, int idx)
gap = dw_edma_core_get_ll_dist(chan, chan->ll_done,
desc->ll_start);
if (gap > done)
- return advanced;
+ goto out;
chan->ll_done = desc->ll_start;
done -= gap;
@@ -476,6 +502,10 @@ static bool dw_edma_ll_clean_pending(struct dw_edma_chan *chan, int idx)
WARN_ON_ONCE(done);
+out:
+ if (advanced)
+ dw_edma_ll_recheck_cancel(chan);
+
return advanced;
}
@@ -531,6 +561,59 @@ dw_edma_ll_consume_progress(struct dw_edma_chan *chan, int idx)
return dw_edma_ll_clean_pending(chan, idx);
}
+static bool dw_edma_ll_has_hdma_stop_event(struct dw_edma_chan *chan)
+{
+ return chan->dw->chip->mf == EDMA_MF_HDMA_NATIVE;
+}
+
+/*
+ * Must be called with vc.lock held. A DONE-time DMA_LLP sample may miss
+ * the final burst element. For the eDMA-compatible interrupt interface,
+ * accept a fresh LLP sample only when status is STOPPED and transfer size
+ * is zero. Native HDMA reports STOP directly.
+ */
+static bool dw_edma_ll_reconcile_stopped(struct dw_edma_chan *chan)
+{
+ int idx;
+
+ scoped_guard(spinlock_irqsave, dw_edma_event_lock(chan)) {
+ if (dw_edma_abort_latch_locked(chan))
+ return false;
+
+ /*
+ * Leave an IRQ-captured event to its worker. Otherwise pair the
+ * stopped boundary with the status clear before another kick.
+ */
+ if (chan->ll_irq.event != DW_EDMA_LL_EVENT_NONE ||
+ dw_edma_core_ch_status(chan) != DMA_COMPLETE)
+ return false;
+
+ /* Native HDMA reports STOP without a transfer-size check. */
+ if (!dw_edma_ll_has_hdma_stop_event(chan) &&
+ dw_edma_core_ch_transfer_size(chan) != 0)
+ return false;
+
+ idx = dw_edma_ll_recycle_idx(chan,
+ dw_edma_core_ll_cur_idx(chan),
+ DW_EDMA_LL_EVENT_STOP);
+ dw_edma_core_ll_irq_clear(chan);
+ }
+
+ return dw_edma_ll_clean_pending(chan, idx);
+}
+
+static bool dw_edma_ll_reconcile_and_refill(struct dw_edma_chan *chan)
+{
+ if (!dw_edma_ll_reconcile_stopped(chan))
+ return false;
+
+ dw_edma_start_transfer(chan);
+ chan->status = dw_edma_ll_pending(chan) ?
+ EDMA_ST_BUSY : EDMA_ST_IDLE;
+
+ return true;
+}
+
static void dw_edma_core_ll_sync(struct dw_edma_chan *chan)
{
/*
@@ -544,6 +627,15 @@ static void dw_edma_core_ll_sync(struct dw_edma_chan *chan)
/* Must be called with vc.lock held for an LL channel. */
static void dw_edma_core_ch_doorbell(struct dw_edma_chan *chan)
{
+ if (unlikely(READ_ONCE(chan->dw->teardown)))
+ return;
+
+ dw_edma_ll_recheck_cancel(chan);
+
+ /*
+ * Complete the remote LL publication before serializing the new
+ * hardware run with IRQ capture.
+ */
dw_edma_core_ll_sync(chan);
guard(spinlock_irqsave)(dw_edma_event_lock(chan));
@@ -562,14 +654,73 @@ static void dw_edma_core_ch_doorbell(struct dw_edma_chan *chan)
dw_edma_core_do_ch_doorbell(chan);
}
-/* Must be called with vc.lock held. */
-static void dw_edma_core_ch_maybe_doorbell(struct dw_edma_chan *chan)
+/*
+ * Must be called with vc.lock held. Return true when published work is still
+ * running and may need one later stop recheck.
+ */
+static bool dw_edma_core_ch_maybe_doorbell(struct dw_edma_chan *chan)
{
if (chan->non_ll || chan->request != EDMA_REQ_NONE ||
- chan->status != EDMA_ST_BUSY || !dw_edma_ll_pending(chan))
- return;
+ chan->status != EDMA_ST_BUSY || !dw_edma_ll_pending(chan) ||
+ dw_edma_abort_is_pending(chan))
+ return false;
+
+ /*
+ * While running, both eDMA and HDMA consume newly published
+ * elements without another doorbell.
+ */
+ if (dw_edma_core_ch_status(chan) == DMA_IN_PROGRESS)
+ return true;
+
+ dw_edma_ll_reconcile_and_refill(chan);
+ if (!dw_edma_ll_pending(chan))
+ return false;
dw_edma_core_ch_doorbell(chan);
+ if (!dw_edma_ll_has_hdma_stop_event(chan))
+ dw_edma_ll_recheck_schedule(chan);
+
+ return false;
+}
+
+/*
+ * eDMA may stop at a CB mismatch just after reporting RUNNING.
+ * Recheck once so the stopped tail is not left pending.
+ */
+static void
+dw_edma_core_ch_maybe_doorbell_or_recheck(struct dw_edma_chan *chan)
+{
+ if (dw_edma_core_ch_maybe_doorbell(chan) &&
+ !dw_edma_ll_has_hdma_stop_event(chan))
+ dw_edma_ll_recheck_schedule(chan);
+}
+
+static void dw_edma_ll_recheck_work(struct work_struct *work)
+{
+ struct dw_edma_chan *chan =
+ container_of(to_delayed_work(work), struct dw_edma_chan,
+ ll_recheck_work);
+ unsigned long delay;
+
+ guard(spinlock_irqsave)(&chan->vc.lock);
+
+ if (unlikely(READ_ONCE(chan->dw->teardown))) {
+ chan->ll_recheck_at = 0;
+ return;
+ }
+
+ if (!chan->ll_recheck_at)
+ return;
+
+ if (time_before(jiffies, chan->ll_recheck_at)) {
+ delay = chan->ll_recheck_at - jiffies;
+ mod_delayed_work(chan->dw->wq, &chan->ll_recheck_work, delay);
+ return;
+ }
+
+ chan->ll_recheck_at = 0;
+ if (chan->request == EDMA_REQ_NONE)
+ dw_edma_core_ch_maybe_doorbell(chan);
}
static void dw_edma_device_caps(struct dma_chan *dchan,
@@ -697,7 +848,7 @@ static int dw_edma_device_resume(struct dma_chan *dchan)
chan->status = EDMA_ST_BUSY;
if (!dw_edma_start_transfer(chan))
chan->status = EDMA_ST_IDLE;
- dw_edma_core_ch_maybe_doorbell(chan);
+ dw_edma_core_ch_maybe_doorbell_or_recheck(chan);
}
return err;
@@ -747,7 +898,7 @@ static void dw_edma_device_issue_pending(struct dma_chan *dchan)
dw_edma_ll_snapshot_discard(chan);
chan->status = EDMA_ST_BUSY;
dw_edma_start_transfer(chan);
- dw_edma_core_ch_maybe_doorbell(chan);
+ dw_edma_core_ch_maybe_doorbell_or_recheck(chan);
}
spin_unlock_irqrestore(&chan->vc.lock, flags);
}
@@ -1097,7 +1248,7 @@ static void dw_edma_ll_interrupt(struct dw_edma_chan *chan)
}
out:
- dw_edma_core_ch_maybe_doorbell(chan);
+ dw_edma_core_ch_maybe_doorbell_or_recheck(chan);
}
static bool dw_edma_abort_interrupt(struct dw_edma_chan *chan)
@@ -1136,6 +1287,11 @@ static void dw_edma_irq_work(struct work_struct *work)
irq_work);
unsigned int events;
+ if (unlikely(READ_ONCE(chan->dw->teardown))) {
+ atomic_set(&chan->irq_pending, 0);
+ return;
+ }
+
do {
events = atomic_xchg(&chan->irq_pending, 0);
@@ -1153,6 +1309,9 @@ static void dw_edma_irq_work(struct work_struct *work)
static void dw_edma_queue_irq_work(struct dw_edma_chan *chan,
unsigned int events)
{
+ if (unlikely(READ_ONCE(chan->dw->teardown)))
+ return;
+
atomic_or(events, &chan->irq_pending);
queue_work(chan->dw->wq, &chan->irq_work);
}
@@ -1390,6 +1549,9 @@ static void dw_edma_device_synchronize(struct dma_chan *dchan)
struct dw_edma_chan *chan = dchan2dw_edma_chan(dchan);
dw_edma_wait_termination(dchan);
+ scoped_guard(spinlock_irqsave, &chan->vc.lock)
+ dw_edma_ll_recheck_cancel(chan);
+ cancel_delayed_work_sync(&chan->ll_recheck_work);
cancel_work_sync(&chan->irq_work);
atomic_set(&chan->irq_pending, 0);
dw_edma_irq_events_discard(chan);
@@ -1443,6 +1605,8 @@ static int dw_edma_channel_setup(struct dw_edma *dw, u32 wr_alloc, u32 rd_alloc)
chan->status = EDMA_ST_IDLE;
chan->irq_mode = dw_edma_get_default_irq_mode(chan);
INIT_WORK(&chan->irq_work, dw_edma_irq_work);
+ INIT_DELAYED_WORK(&chan->ll_recheck_work,
+ dw_edma_ll_recheck_work);
atomic_set(&chan->irq_pending, 0);
chan->ll_irq.idx = -1;
chan->ll_irq.event = DW_EDMA_LL_EVENT_NONE;
@@ -1798,23 +1962,39 @@ int dw_edma_remove(struct dw_edma_chip *chip)
if (!dw)
return -ENODEV;
+ /*
+ * Stop new clients and asynchronous hardware access before dismantling
+ * their execution context.
+ */
+ WRITE_ONCE(dw->teardown, true);
+ dma_async_device_unregister(&dw->dma);
+
+ /*
+ * Drain channel work that may have passed the teardown gate before
+ * stopping the hardware. IRQ handlers remain installed while it is
+ * active.
+ */
+ for (i = 0; i < dw->wr_ch_cnt + dw->rd_ch_cnt; i++) {
+ disable_delayed_work_sync(&dw->chan[i].ll_recheck_work);
+ cancel_work_sync(&dw->chan[i].irq_work);
+ }
+
if (chip->flags & DW_EDMA_CHIP_PARTIAL)
err = dw_edma_core_quiesce(dw);
else
dw_edma_core_off(dw);
- /* Free irqs */
+ /* No new hardware event can be raised after the quiesce. */
for (i = (dw->nr_irqs - 1); i >= 0; i--)
free_irq(chip->ops->irq_vector(dev, i), &dw->irq[i]);
dw_edma_emul_irq_free(dw);
+ /* Drain IRQ work queued by a handler that raced with the gate. */
for (i = 0; i < dw->wr_ch_cnt + dw->rd_ch_cnt; i++)
cancel_work_sync(&dw->chan[i].irq_work);
destroy_workqueue(dw->wq);
- /* Deregister eDMA device */
- dma_async_device_unregister(&dw->dma);
list_for_each_entry_safe(chan, _chan, &dw->dma.channels,
vc.chan.device_node) {
tasklet_kill(&chan->vc.task);
diff --git a/drivers/dma/dw-edma/dw-edma-core.h b/drivers/dma/dw-edma/dw-edma-core.h
index 36fe8c69769d..b9e928b8d8fb 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -124,6 +124,9 @@ struct dw_edma_chan {
spinlock_t event_lock_per_chan;
spinlock_t *event_lock; /* Selected event lock */
+ struct delayed_work ll_recheck_work;
+ unsigned long ll_recheck_at;
+
u32 ll_max; /* Data entries */
struct dw_edma_region ll_region; /* Linked list */
bool ll_valid; /* LL context programmed */
@@ -172,6 +175,8 @@ struct dw_edma {
*/
struct workqueue_struct *wq;
+ bool teardown; /* Gate asynchronous hardware access */
+
raw_spinlock_t lock; /* Protect v0 shared registers */
/* Per-direction lock storage for the eDMA interrupt registers. */
spinlock_t event_lock_per_dir[2];
--
2.51.0