[PATCH v5 15/24] dmaengine: dw-edma: Recheck stopped LL channels before restart
From: Koichiro Den
Date: Wed Aug 12 2026 - 12:13:35 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.
For eDMA, permit the initial kick after an LL reset, then only one restart
after each software-confirmed STOP boundary. Consume the permission with
the doorbell under the event lock. This prevents a newly asserted STOP from
being cleared before the hard IRQ records it.
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 v5:
- Read jiffies once for both the recheck-deadline comparison and the
remaining-delay calculation. (Sashiko)
- Gate eDMA kicks with a one-shot credit armed by LL reset or a
software-confirmed STOP, preventing a later kick from clearing an
unrecorded STOP status. (Sashiko)
drivers/dma/dw-edma/dw-edma-core.c | 225 +++++++++++++++++++++++++++--
drivers/dma/dw-edma/dw-edma-core.h | 7 +
2 files changed, 220 insertions(+), 12 deletions(-)
diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index 13c45549e08b..8325e296cb2a 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)
@@ -112,6 +113,12 @@ static bool dw_edma_abort_is_pending(struct dw_edma_chan *chan)
return chan->abort_pending;
}
+static bool dw_edma_ll_uses_restart_credit(struct dw_edma_chan *chan)
+{
+ return chan->dw->chip->mf == EDMA_MF_EDMA_LEGACY ||
+ chan->dw->chip->mf == EDMA_MF_EDMA_UNROLL;
+}
+
/* Must be called with vc.lock held. */
static bool
dw_edma_ll_snapshot_take(struct dw_edma_chan *chan,
@@ -148,10 +155,33 @@ 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. */
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;
}
@@ -184,6 +214,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. */
@@ -195,6 +227,8 @@ static void dw_edma_core_reset_ll(struct dw_edma_chan *chan)
chan->ll_region.paddr);
scoped_guard(spinlock_irqsave, dw_edma_event_lock(chan)) {
+ if (dw_edma_ll_uses_restart_credit(chan))
+ chan->ll_restart_armed = true;
dw_edma_ll_event_discard_locked(chan);
}
dw_edma_core_ch_enable(chan);
@@ -425,7 +459,7 @@ static bool dw_edma_ll_consume_progress(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
@@ -444,7 +478,7 @@ static bool dw_edma_ll_consume_progress(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;
@@ -475,6 +509,10 @@ static bool dw_edma_ll_consume_progress(struct dw_edma_chan *chan, int idx)
WARN_ON_ONCE(done);
+out:
+ if (advanced)
+ dw_edma_ll_recheck_cancel(chan);
+
return advanced;
}
@@ -518,6 +556,61 @@ dw_edma_ll_recycle_idx(struct dw_edma_chan *chan, int idx,
return idx == chan->ll_max ? chan->ll_max - 1 : idx - 1;
}
+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);
+ if (dw_edma_ll_uses_restart_credit(chan))
+ chan->ll_restart_armed = true;
+ }
+
+ return dw_edma_ll_consume_progress(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)
{
/*
@@ -531,6 +624,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));
@@ -545,18 +647,85 @@ static void dw_edma_core_ch_doorbell(struct dw_edma_chan *chan)
if (chan->ll_irq.event != DW_EDMA_LL_EVENT_NONE)
return;
+ /* One confirmed stop permits one eDMA restart. */
+ if (dw_edma_ll_uses_restart_credit(chan)) {
+ if (!chan->ll_restart_armed)
+ return;
+ chan->ll_restart_armed = false;
+ }
+
dw_edma_ll_event_discard_locked(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, now;
+
+ 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;
+
+ now = jiffies;
+ if (time_before(now, chan->ll_recheck_at)) {
+ delay = chan->ll_recheck_at - now;
+ 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,
@@ -684,7 +853,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;
@@ -734,7 +903,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);
}
@@ -1084,7 +1253,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)
@@ -1123,6 +1292,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);
@@ -1140,6 +1314,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);
}
@@ -1189,6 +1366,9 @@ static void dw_edma_record_irq(struct dw_edma_chan *chan, unsigned int events)
if (chan->ll_irq.event != DW_EDMA_LL_EVENT_STOP ||
snapshot.event == DW_EDMA_LL_EVENT_STOP)
chan->ll_irq = snapshot;
+ if (dw_edma_ll_uses_restart_credit(chan))
+ chan->ll_restart_armed =
+ chan->ll_irq.event == DW_EDMA_LL_EVENT_STOP;
pending |= DW_EDMA_DEFERRED_LL;
}
@@ -1378,6 +1558,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);
@@ -1431,6 +1614,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;
@@ -1786,23 +1971,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 27cab6ca5e67..aaaee312e973 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -120,10 +120,15 @@ struct dw_edma_chan {
struct dw_edma_ll_snapshot ll_irq;
/* ABORT is terminal and remains pending across LL state changes. */
bool abort_pending;
+ /* One-shot eDMA restart credit, protected by event_lock. */
+ bool ll_restart_armed;
/* Native HDMA lock storage. */
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 +177,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