[PATCH v4 22/24] dmaengine: dw-edma: Add engine recovery infrastructure

From: Koichiro Den

Date: Wed Jul 29 2026 - 10:44:37 EST


For eDMA, resetting one direction affects every channel in it. Add a
worker to coordinate that recovery.

Gate publication and doorbells, and prevent channel setup from
re-enabling the direction during recovery. Wait for active LL channels
to stop. If a transfer is still active, compare two transfer-size
samples. Retry when it is moving; treat an unchanged value as frozen.

For a frozen direction, drain IRQ handling so pending ABORT cleanup can
finish. If recovery is still needed, reset the direction, rebuild every
exposed LL context while it remains disabled, publish remote LL writes,
and enable it again. Honor EDMA_REQ_STOP and EDMA_REQ_PAUSE during the
rebuild; keep channels gated and retry if reset keeps failing.

Providers opt in with engine_reset(), engine_enable(), and
ch_transfer_size(). No caller requests recovery until the next patch.

Signed-off-by: Koichiro Den <den@xxxxxxxxxxxxx>
---
Changes in v4:
- Use cancelable delayed work to keep retrying failed engine resets instead
of abandoning recovery after five attempts. (Sashiko)

drivers/dma/dw-edma/dw-edma-core.c | 316 ++++++++++++++++++++++++++++-
drivers/dma/dw-edma/dw-edma-core.h | 22 +-
2 files changed, 333 insertions(+), 5 deletions(-)

diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index cbe60bcd4c9a..616b0658b1ef 100644
--- a/drivers/dma/dw-edma/dw-edma-core.c
+++ b/drivers/dma/dw-edma/dw-edma-core.c
@@ -28,6 +28,10 @@
/* Empirically chosen progress interval. */
#define DW_EDMA_LL_PROGRESS_INTERVAL 4
#define DW_EDMA_LL_RECHECK_DELAY_MS 30
+#define DW_EDMA_ENGINE_QUIESCE_TIMEOUT_MS 250
+#define DW_EDMA_ENGINE_RESET_FAST_ATTEMPTS 5
+#define DW_EDMA_ENGINE_RESET_RETRY_DELAY_MS 5000
+#define DW_EDMA_MAX_DIR_CH MAX(HDMA_MAX_WR_CH, HDMA_MAX_RD_CH)

static inline
struct dw_edma_desc *vd2dw_edma_desc(struct virt_dma_desc *vd)
@@ -187,6 +191,22 @@ dw_edma_set_request(struct dw_edma_chan *chan, enum dw_edma_request request)
chan->request = request;
}

+static void
+dw_edma_engine_recovery_schedule(struct dw_edma_engine_recovery *rec,
+ unsigned long delay)
+{
+ if (unlikely(READ_ONCE(rec->dw->teardown)))
+ return;
+
+ queue_delayed_work(rec->dw->wq, &rec->work, delay);
+}
+
+static void
+dw_edma_engine_recovery_queue(struct dw_edma_engine_recovery *rec)
+{
+ dw_edma_engine_recovery_schedule(rec, 0);
+}
+
static void dw_hdma_set_callback_result(struct virt_dma_desc *vd,
enum dmaengine_tx_result result)
{
@@ -239,6 +259,7 @@ static void dw_edma_core_reset_ll(struct dw_edma_chan *chan)

chan->ll_head = 0;
chan->ll_done = 0;
+ chan->ll_recovery_pending = false;
/* Drop stale CB bits before reusing the circular LL ring. */
for (i = 0; i < chan->ll_max; i++)
dw_edma_core_ll_clear(chan, i);
@@ -399,7 +420,7 @@ static int dw_edma_start_transfer(struct dw_edma_chan *chan)
return 1;
}

- if (dw_edma_abort_is_pending(chan))
+ if (chan->ll_recovering || dw_edma_abort_is_pending(chan))
return 0;

if (!chan->ll_valid)
@@ -549,8 +570,10 @@ static bool dw_edma_ll_clean_pending(struct dw_edma_chan *chan, int idx)
WARN_ON_ONCE(done);

out:
- if (advanced)
+ if (advanced) {
dw_edma_ll_recheck_cancel(chan);
+ chan->ll_recovery_pending = false;
+ }

return advanced;
}
@@ -707,6 +730,7 @@ static void dw_edma_core_ch_doorbell(struct dw_edma_chan *chan)
static bool dw_edma_core_ch_maybe_doorbell(struct dw_edma_chan *chan)
{
if (chan->non_ll ||
+ chan->ll_recovering ||
chan->status != EDMA_ST_BUSY ||
!dw_edma_ll_pending(chan) ||
dw_edma_abort_is_pending(chan))
@@ -785,6 +809,283 @@ static void dw_edma_ll_recheck_work(struct work_struct *work)
dw_edma_core_ch_maybe_doorbell(chan);
}

+static bool
+dw_edma_engine_recovery_needed(struct dw_edma_engine_recovery *rec)
+{
+ struct dw_edma *dw = rec->dw;
+ u16 off = rec->dir == EDMA_DIR_WRITE ? 0 : dw->wr_ch_cnt;
+ u16 cnt = rec->dir == EDMA_DIR_WRITE ? dw->wr_ch_cnt : dw->rd_ch_cnt;
+ struct dw_edma_chan *chan;
+ bool found = false;
+ bool needed;
+ u16 i;
+
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ scoped_guard(spinlock_irqsave, &chan->vc.lock) {
+ needed = chan->ll_recovery_pending && chan->configured &&
+ !chan->non_ll &&
+ chan->status != EDMA_ST_PAUSE;
+ if (needed && chan->request == EDMA_REQ_NONE &&
+ dw_edma_ll_reconcile_and_refill(chan))
+ dw_edma_core_ch_maybe_doorbell(chan);
+ needed = needed && chan->ll_recovery_pending &&
+ dw_edma_ll_pending(chan);
+ if (!needed)
+ chan->ll_recovery_pending = false;
+ }
+ found |= needed;
+ }
+
+ return found;
+}
+
+static void
+dw_edma_engine_recovery_release(struct dw_edma_engine_recovery *rec)
+{
+ struct dw_edma *dw = rec->dw;
+ u16 off = rec->dir == EDMA_DIR_WRITE ? 0 : dw->wr_ch_cnt;
+ u16 cnt = rec->dir == EDMA_DIR_WRITE ? dw->wr_ch_cnt : dw->rd_ch_cnt;
+ struct dw_edma_chan *chan;
+ u16 i;
+
+ rec->fails = 0;
+
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ guard(spinlock_irqsave)(&chan->vc.lock);
+ chan->ll_recovering = false;
+
+ if (!READ_ONCE(dw->teardown) &&
+ chan->configured && !chan->non_ll &&
+ chan->request == EDMA_REQ_NONE &&
+ chan->status != EDMA_ST_PAUSE) {
+ dw_edma_start_transfer(chan);
+ chan->status = dw_edma_ll_pending(chan) ?
+ EDMA_ST_BUSY : EDMA_ST_IDLE;
+ dw_edma_core_ch_maybe_doorbell(chan);
+ }
+ }
+
+ WRITE_ONCE(rec->active, false);
+}
+
+static void
+dw_edma_engine_recovery_drain_irqs(struct dw_edma_engine_recovery *rec)
+{
+ struct dw_edma *dw = rec->dw;
+ struct device *dev = dw->chip->dev;
+ u16 off = rec->dir == EDMA_DIR_WRITE ? 0 : dw->wr_ch_cnt;
+ u16 cnt = rec->dir == EDMA_DIR_WRITE ? dw->wr_ch_cnt : dw->rd_ch_cnt;
+ struct dw_edma_chan *chan;
+ u16 i;
+
+ /* Drain events observed before the engine context changes. */
+ for (i = 0; i < dw->nr_irqs; i++) {
+ if (bitmap_empty(rec->dir == EDMA_DIR_WRITE ?
+ dw->irq[i].wr_mask : dw->irq[i].rd_mask, cnt))
+ continue;
+
+ synchronize_irq(dw->chip->ops->irq_vector(dev, i));
+ }
+
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ flush_work(&chan->irq_work);
+ }
+}
+
+static void dw_edma_engine_recovery_work(struct work_struct *work)
+{
+ struct dw_edma_engine_recovery *rec =
+ container_of(to_delayed_work(work),
+ struct dw_edma_engine_recovery, work);
+ struct dw_edma *dw = rec->dw;
+ const char *dir_name = str_write_read(rec->dir == EDMA_DIR_WRITE);
+ u16 off = rec->dir == EDMA_DIR_WRITE ? 0 : dw->wr_ch_cnt;
+ u16 cnt = rec->dir == EDMA_DIR_WRITE ? dw->wr_ch_cnt : dw->rd_ch_cnt;
+ unsigned long timeout =
+ jiffies + msecs_to_jiffies(DW_EDMA_ENGINE_QUIESCE_TIMEOUT_MS);
+ struct dw_edma_chan *chan;
+ bool configured_ll;
+ bool busy;
+ u16 i;
+
+ /*
+ * Recovery resets a whole direction. The legacy eDMA register map does
+ * not support partial ownership, while unrolled eDMA partial ownership
+ * and delegation are direction-granular. Native HDMA permits per-channel
+ * ownership, but does not provide direction recovery operations.
+ */
+ if (unlikely(READ_ONCE(dw->teardown)))
+ return;
+
+ if (!dw_edma_engine_recovery_needed(rec)) {
+ if (READ_ONCE(rec->active))
+ dw_edma_engine_recovery_release(rec);
+ return;
+ }
+
+ WRITE_ONCE(rec->active, true);
+
+ /* Gate each channel before inspecting or resetting the direction. */
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ guard(spinlock_irqsave)(&chan->vc.lock);
+ chan->ll_recovering = true;
+ }
+
+ /*
+ * Gated channels stop at their first unpublished element. Wait for that
+ * point before resetting; otherwise a restart could skip the remainder
+ * of an in-flight element.
+ */
+ do {
+ busy = false;
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ scoped_guard(spinlock_irqsave, &chan->vc.lock)
+ configured_ll = chan->configured && !chan->non_ll;
+ if (configured_ll &&
+ dw_edma_core_ch_status(chan) == DMA_IN_PROGRESS)
+ busy = true;
+ }
+ if (!busy)
+ break;
+ fsleep(1000);
+ } while (time_before(jiffies, timeout));
+
+ /* Progress after queueing makes this recovery request obsolete. */
+ if (!dw_edma_engine_recovery_needed(rec)) {
+ dw_edma_engine_recovery_release(rec);
+ return;
+ }
+
+ if (busy) {
+ DECLARE_BITMAP(busy_mask, DW_EDMA_MAX_DIR_CH) = { 0 };
+ u32 tsz[DW_EDMA_MAX_DIR_CH];
+ bool moving = false;
+
+ /*
+ * A transfer_size change or a transition out of DMA_IN_PROGRESS
+ * shows movement after the first sample. Retry while producers
+ * remain gated. If neither occurs, treat the channel as frozen
+ * within one element.
+ */
+ for (i = 0; i < cnt; i++) {
+ chan = &dw->chan[off + i];
+ scoped_guard(spinlock_irqsave, &chan->vc.lock)
+ configured_ll = chan->configured && !chan->non_ll;
+ if (configured_ll &&
+ dw_edma_core_ch_status(chan) == DMA_IN_PROGRESS) {
+ __set_bit(i, busy_mask);
+ tsz[i] = dw_edma_core_ch_transfer_size(chan);
+ }
+ }
+ fsleep(1000);
+ for_each_set_bit(i, busy_mask, cnt) {
+ chan = &dw->chan[off + i];
+ if (dw_edma_core_ch_status(chan) != DMA_IN_PROGRESS ||
+ tsz[i] != dw_edma_core_ch_transfer_size(chan))
+ moving = true;
+ }
+ if (!dw_edma_engine_recovery_needed(rec)) {
+ dw_edma_engine_recovery_release(rec);
+ return;
+ }
+ if (moving) {
+ dev_warn_ratelimited(dw->chip->dev,
+ "%s engine quiesce timed out with transfers still progressing, retrying\n",
+ dir_name);
+ dw_edma_engine_recovery_queue(rec);
+ return;
+ }
+
+ dev_warn(dw->chip->dev,
+ "%s engine reset with a channel frozen mid-element\n",
+ dir_name);
+ }
+
+ if (!dw_edma_engine_recovery_needed(rec)) {
+ dw_edma_engine_recovery_release(rec);
+ return;
+ }
+
+ /*
+ * Let a terminal ABORT finish its normal channel cleanup before a
+ * direction-wide reset. It may also make recovery unnecessary.
+ */
+ dw_edma_engine_recovery_drain_irqs(rec);
+ if (!dw_edma_engine_recovery_needed(rec)) {
+ dw_edma_engine_recovery_release(rec);
+ return;
+ }
+
+ if (!dw->core->engine_reset(dw, rec->dir)) {
+ unsigned long delay = 0;
+
+ /*
+ * Keep channels gated and do not re-enable the engine unless
+ * ENGINE_EN clears.
+ */
+ if (++rec->fails >= DW_EDMA_ENGINE_RESET_FAST_ATTEMPTS)
+ delay = msecs_to_jiffies(DW_EDMA_ENGINE_RESET_RETRY_DELAY_MS);
+ if (rec->fails == DW_EDMA_ENGINE_RESET_FAST_ATTEMPTS)
+ dev_err(dw->chip->dev,
+ "%s engine did not drain after %u attempts; retrying every %u ms\n",
+ dir_name, rec->fails,
+ DW_EDMA_ENGINE_RESET_RETRY_DELAY_MS);
+ dw_edma_engine_recovery_schedule(rec, delay);
+ return;
+ }
+
+ if (rec->fails)
+ dev_warn(dw->chip->dev, "%s engine drained after %u retries\n",
+ dir_name, rec->fails);
+
+ /* Drain an event that raced with the engine reset. */
+ dw_edma_engine_recovery_drain_irqs(rec);
+
+ /*
+ * Keep the engine disabled until every LL channel context is rebuilt.
+ * On the tested integration, re-enabling a preserved context resumed an
+ * old element without a doorbell, causing an IOMMU fault and stalling the
+ * engine again. Reset each ring so it starts from a fresh context, and
+ * complete remote LL publication before re-enabling the direction.
+ */
+ for (i = 0; i < cnt; i++) {
+ bool stop;
+
+ chan = &dw->chan[off + i];
+ guard(spinlock_irqsave)(&chan->vc.lock);
+
+ if (chan->non_ll)
+ continue;
+
+ /* Honor EDMA_REQ_STOP and EDMA_REQ_PAUSE instead of republishing. */
+ stop = chan->request == EDMA_REQ_STOP;
+ if (stop)
+ dw_edma_terminate_all_descs(chan);
+
+ dw_edma_core_reset_ll(chan);
+ dw_edma_core_ll_sync(chan);
+
+ if (stop) {
+ dw_edma_set_request(chan, EDMA_REQ_NONE);
+ chan->status = EDMA_ST_IDLE;
+ } else if (chan->request == EDMA_REQ_PAUSE) {
+ dw_edma_set_request(chan, EDMA_REQ_NONE);
+ chan->status = EDMA_ST_PAUSE;
+ }
+ }
+
+ dw->core->engine_enable(dw, rec->dir);
+ dw_edma_engine_recovery_release(rec);
+
+ dev_warn(dw->chip->dev, "%s engine was reset to recover a stalled channel\n",
+ dir_name);
+}
+
static void dw_edma_device_caps(struct dma_chan *dchan,
struct dma_slave_caps *caps)
{
@@ -1977,6 +2278,13 @@ int dw_edma_probe(struct dw_edma_chip *chip)
for (i = 0; i < ARRAY_SIZE(dw->event_lock_per_dir); i++)
spin_lock_init(&dw->event_lock_per_dir[i]);

+ for (i = 0; i < ARRAY_SIZE(dw->eng_recovery); i++) {
+ dw->eng_recovery[i].dw = dw;
+ dw->eng_recovery[i].dir = i;
+ INIT_DELAYED_WORK(&dw->eng_recovery[i].work,
+ dw_edma_engine_recovery_work);
+ }
+
/*
* chip->ll_*_cnt describes the channels exposed by this instance. Keep
* the usable hardware counts separate for partial ownership checks.
@@ -2095,6 +2403,10 @@ int dw_edma_remove(struct dw_edma_chip *chip)
WRITE_ONCE(dw->teardown, true);
dma_async_device_unregister(&dw->dma);

+ /* A recovery worker can re-enable the engine. */
+ disable_delayed_work_sync(&dw->eng_recovery[0].work);
+ disable_delayed_work_sync(&dw->eng_recovery[1].work);
+
/*
* Drain channel work that may have passed the teardown gate before
* stopping the hardware. IRQ handlers remain installed while it is
diff --git a/drivers/dma/dw-edma/dw-edma-core.h b/drivers/dma/dw-edma/dw-edma-core.h
index 36f3def1a55e..a902a24d20ba 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -126,6 +126,9 @@ struct dw_edma_chan {

struct delayed_work ll_recheck_work;
unsigned long ll_recheck_at;
+ /* Per-channel recovery state. */
+ bool ll_recovery_pending;
+ bool ll_recovering;

u32 ll_max; /* Data entries */
struct dw_edma_region ll_region; /* Linked list */
@@ -156,6 +159,18 @@ struct dw_edma_irq {
DECLARE_BITMAP(rd_mask, HDMA_MAX_RD_CH);
};

+/*
+ * Direction-wide recovery state. active records that recovery has gated the
+ * channels; workqueue state coalesces duplicate requests.
+ */
+struct dw_edma_engine_recovery {
+ bool active;
+ unsigned int fails;
+ struct delayed_work work;
+ struct dw_edma *dw;
+ enum dw_edma_dir dir;
+};
+
struct dw_edma {
char name[32];

@@ -170,12 +185,13 @@ struct dw_edma {
struct dw_edma_chan *chan;

/*
- * WQ_HIGHPRI keeps completion processing responsive under heavy load;
- * WQ_UNBOUND lets different channels run on different CPUs.
+ * WQ_HIGHPRI keeps completion and recovery work responsive under heavy
+ * load; WQ_UNBOUND lets independent work run on different CPUs.
*/
struct workqueue_struct *wq;

bool teardown; /* Gate asynchronous hardware access */
+ struct dw_edma_engine_recovery eng_recovery[2];

raw_spinlock_t lock; /* Protect v0 shared registers */
/* Per-direction lock storage for the eDMA interrupt registers. */
@@ -372,7 +388,7 @@ static inline void dw_edma_core_do_ch_doorbell(struct dw_edma_chan *chan)
static inline void dw_edma_core_ch_enable(struct dw_edma_chan *chan)
{
chan->dw->core->ch_enable(chan);
- if (chan->dw->core->engine_enable)
+ if (!chan->ll_recovering && chan->dw->core->engine_enable)
chan->dw->core->engine_enable(chan->dw, chan->dir);
}

--
2.51.0