[PATCH v3 15/24] dmaengine: dw-edma: Recheck stopped LL channels before restart

From: Koichiro Den

Date: Mon Jul 27 2026 - 13:22:45 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. 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 v3:
- Rework and rename v2's "Reconcile lost completions from a stopped
LLP re-sample"; retain the stopped LLP re-sample and add one delayed
recheck for interfaces without a dedicated STOP event. (Sashiko)
- Serialize the stopped-state check, LLP sample and interrupt-status
clear against IRQ capture, block reconciliation while ABORT is
pending, and separate stopped reconciliation from refill. (Sashiko)
- Route normal kick paths through the helper that arms the delayed
recheck after observing a running channel. (Sashiko)
- Gate asynchronous work, unregister the DMA device before quiesce,
and quiesce the hardware before freeing IRQs. (Sashiko)

drivers/dma/dw-edma/dw-edma-core.c | 201 +++++++++++++++++++++++++++--
drivers/dma/dw-edma/dw-edma-core.h | 5 +
2 files changed, 194 insertions(+), 12 deletions(-)

diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index d28e3633a866..dbccd11aa92f 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,26 @@ 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;
+
+ chan->ll_recheck_at = jiffies + delay;
+ 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 +181,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 +215,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. */
@@ -430,7 +455,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
@@ -449,7 +474,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;
@@ -480,6 +505,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;
}

@@ -535,6 +564,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 legacy 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(raw_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)
{
/*
@@ -548,6 +630,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_kick(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(raw_spinlock_irqsave)(dw_edma_event_lock(chan));
@@ -566,14 +657,71 @@ static void dw_edma_core_ch_kick(struct dw_edma_chan *chan)
dw_edma_core_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_kick(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,
@@ -701,7 +849,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;
@@ -751,7 +899,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);
}
@@ -1101,7 +1249,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)
@@ -1140,6 +1288,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);

@@ -1157,6 +1310,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);
}
@@ -1394,6 +1550,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);
@@ -1447,6 +1606,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;
@@ -1789,23 +1950,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 3d4844b5c6e3..f56885747e57 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -127,6 +127,9 @@ struct dw_edma_chan {
bool abort_pending;
raw_spinlock_t 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 */
@@ -175,6 +178,8 @@ struct dw_edma {
*/
struct workqueue_struct *wq;

+ bool teardown; /* Gate asynchronous hardware access */
+
raw_spinlock_t lock; /* Protect v0 shared registers */
raw_spinlock_t event_lock[2]; /* Per-direction event scopes */

--
2.51.0