[PATCH v2 16/19] dmaengine: dw-edma: Add engine recovery infrastructure

From: Koichiro Den

Date: Thu Jul 23 2026 - 04:52:56 EST


Resetting one direction affects every channel in it. Add a worker to
coordinate that recovery.

Gate publication and doorbells, then 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, reset the engine, synchronize hard and deferred
IRQ handling, rebuild configured LL contexts while the engine is disabled,
and then enable it. Honor EDMA_REQ_STOP and EDMA_REQ_PAUSE during the
rebuild. After five reset failures, leave the channels gated.

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

Signed-off-by: Koichiro Den <den@xxxxxxxxxxxxx>
---
Changes in v2:
- New patch.

drivers/dma/dw-edma/dw-edma-core.c | 291 ++++++++++++++++++++++++++++-
drivers/dma/dw-edma/dw-edma-core.h | 30 ++-
2 files changed, 310 insertions(+), 11 deletions(-)

diff --git a/drivers/dma/dw-edma/dw-edma-core.c b/drivers/dma/dw-edma/dw-edma-core.c
index 9305137f85f6..14cd0038dbd2 100644
--- a/drivers/dma/dw-edma/dw-edma-core.c
+++ b/drivers/dma/dw-edma/dw-edma-core.c
@@ -24,6 +24,10 @@
#include "../dmaengine.h"
#include "../virt-dma.h"

+#define DW_EDMA_ENGINE_QUIESCE_TIMEOUT_MS 250
+#define DW_EDMA_ENGINE_RESET_MAX_FAILS 5
+#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)
{
@@ -125,6 +129,7 @@ static void dw_edma_core_reset_ll(struct dw_edma_chan *chan)
chan->ll_head = 0;
chan->ll_done = 0;
dw_edma_ll_irq_idx_discard(chan);
+ 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);
@@ -178,6 +183,7 @@ static bool dw_edma_ll_advance(struct dw_edma_chan *chan, int idx, u32 *old_done

*old_done = chan->ll_done;
chan->ll_done = idx;
+ chan->ll_recovery_pending = false;

return true;
}
@@ -275,6 +281,10 @@ static int dw_edma_start_transfer(struct dw_edma_chan *chan)
struct dw_edma_desc *desc;
struct virt_dma_desc *vd;

+ /* Stop publishing until recovery can rebuild and republish the ring. */
+ if (!chan->non_ll && chan->ll_recovering)
+ return 0;
+
vd = vchan_next_desc(&chan->vc);
if (!vd)
return 0;
@@ -468,8 +478,11 @@ static bool dw_edma_ll_reconcile_and_refill(struct dw_edma_chan *chan)
/* Must be called with vc.lock held. */
static void 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))
+ if (chan->non_ll ||
+ chan->ll_recovering ||
+ chan->request != EDMA_REQ_NONE ||
+ chan->status != EDMA_ST_BUSY ||
+ !dw_edma_ll_pending(chan))
return;

/*
@@ -486,6 +499,258 @@ static void dw_edma_core_ch_maybe_doorbell(struct dw_edma_chan *chan)
dw_edma_core_ch_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 (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_sync_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;
+
+ /* Wait until no hard IRQ can queue an event from the old contexts. */
+ 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];
+ cancel_work_sync(&chan->irq_work);
+
+ guard(spinlock_irqsave)(&chan->vc.lock);
+ atomic_set(&chan->irq_pending, 0);
+ dw_edma_ll_irq_idx_discard(chan);
+ }
+}
+
+static void dw_edma_engine_recovery_work(struct work_struct *work)
+{
+ struct dw_edma_engine_recovery *rec =
+ container_of(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;
+
+ 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))
+ goto out_release;
+
+ 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))
+ goto out_release;
+ if (moving) {
+ dev_warn_ratelimited(dw->chip->dev,
+ "%s engine quiesce timed out with transfers still progressing, retrying\n",
+ dir_name);
+ queue_work(dw->wq, &rec->work);
+ 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))
+ goto out_release;
+
+ if (!dw->core->engine_reset(dw, rec->dir)) {
+ /*
+ * Keep channels gated and do not re-enable the engine unless
+ * ENGINE_EN clears.
+ */
+ if (++rec->fails >= DW_EDMA_ENGINE_RESET_MAX_FAILS) {
+ dev_err(dw->chip->dev,
+ "%s engine did not drain after %u attempts; leaving channels gated\n",
+ dir_name, rec->fails);
+ return;
+ }
+ queue_work(dw->wq, &rec->work);
+ return;
+ }
+
+ if (rec->fails)
+ dev_warn(dw->chip->dev, "%s engine drained after %u retries\n",
+ dir_name, rec->fails);
+
+ dw_edma_engine_recovery_sync_irqs(rec);
+
+ /*
+ * Keep the engine disabled until every configured 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.
+ */
+ for (i = 0; i < cnt; i++) {
+ bool stop;
+
+ chan = &dw->chan[off + i];
+ guard(spinlock_irqsave)(&chan->vc.lock);
+
+ if (chan->configured && !chan->non_ll) {
+ /* 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);
+
+ if (stop) {
+ chan->request = EDMA_REQ_NONE;
+ chan->status = EDMA_ST_IDLE;
+ } else if (chan->request == EDMA_REQ_PAUSE) {
+ chan->request = 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);
+ return;
+
+out_release:
+ dw_edma_engine_recovery_release(rec);
+}
+
static void dw_edma_device_caps(struct dma_chan *dchan,
struct dma_slave_caps *caps)
{
@@ -1640,6 +1905,13 @@ int dw_edma_probe(struct dw_edma_chip *chip)

raw_spin_lock_init(&dw->lock);

+ for (i = 0; i < ARRAY_SIZE(dw->eng_recovery); i++) {
+ dw->eng_recovery[i].dw = dw;
+ dw->eng_recovery[i].dir = i;
+ INIT_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.
@@ -1738,11 +2010,6 @@ int dw_edma_remove(struct dw_edma_chip *chip)
if (!dw)
return -ENODEV;

- if (chip->flags & DW_EDMA_CHIP_PARTIAL)
- err = dw_edma_core_quiesce(dw);
- else
- dw_edma_core_off(dw);
-
/* Free irqs */
for (i = (dw->nr_irqs - 1); i >= 0; i--)
free_irq(chip->ops->irq_vector(dev, i), &dw->irq[i]);
@@ -1751,6 +2018,16 @@ int dw_edma_remove(struct dw_edma_chip *chip)
for (i = 0; i < dw->wr_ch_cnt + dw->rd_ch_cnt; i++)
cancel_work_sync(&dw->chan[i].irq_work);

+ /* Prevent a running recovery worker from requeueing itself. */
+ disable_work_sync(&dw->eng_recovery[0].work);
+ disable_work_sync(&dw->eng_recovery[1].work);
+
+ /* A recovery worker can re-enable the engine, so stop it last. */
+ if (chip->flags & DW_EDMA_CHIP_PARTIAL)
+ err = dw_edma_core_quiesce(dw);
+ else
+ dw_edma_core_off(dw);
+
destroy_workqueue(dw->wq);

/* Deregister eDMA device */
diff --git a/drivers/dma/dw-edma/dw-edma-core.h b/drivers/dma/dw-edma/dw-edma-core.h
index e3b42ad14f78..5520f49a76f1 100644
--- a/drivers/dma/dw-edma/dw-edma-core.h
+++ b/drivers/dma/dw-edma/dw-edma-core.h
@@ -104,6 +104,10 @@ struct dw_edma_chan {
bool ll_irq_stopped;
bool ll_irq_requested;

+ /* Per-channel recovery state. */
+ bool ll_recovery_pending;
+ bool ll_recovering;
+
u32 ll_max; /* Data entries */
struct dw_edma_region ll_region; /* Linked list */
bool ll_valid; /* LL context programmed */
@@ -133,6 +137,18 @@ struct dw_edma_irq {
DECLARE_BITMAP(rd_mask, HDMA_MAX_RD_CH);
};

+/*
+ * Direction-wide recovery state. active prevents duplicate recovery work;
+ * each channel's ll_recovering flag gates publication and doorbells.
+ */
+struct dw_edma_engine_recovery {
+ bool active;
+ unsigned int fails;
+ struct work_struct work;
+ struct dw_edma *dw;
+ enum dw_edma_dir dir;
+};
+
struct dw_edma {
char name[32];

@@ -147,11 +163,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;

+ struct dw_edma_engine_recovery eng_recovery[2];
+
raw_spinlock_t lock; /* Protect v0 shared registers */

struct dw_edma_chip *chip;
@@ -167,6 +185,10 @@ struct dw_edma_core_ops {
int (*ch_quiesce)(struct dw_edma_chan *chan);
u16 (*ch_count)(struct dw_edma *dw, enum dw_edma_dir dir);
enum dma_status (*ch_status)(struct dw_edma_chan *chan);
+ /*
+ * Recovery is available only when ch_transfer_size, engine_reset, and
+ * engine_enable are all provided.
+ */
u32 (*ch_transfer_size)(struct dw_edma_chan *chan);
irqreturn_t (*handle_int)(struct dw_edma_irq *dw_irq, enum dw_edma_dir dir,
dw_edma_handler_t done,
@@ -180,7 +202,7 @@ struct dw_edma_core_ops {
int (*ll_cur_idx)(struct dw_edma_chan *chan);
/* Called with chan->vc.lock held. */
bool (*ll_irq)(struct dw_edma_desc *desc, u32 i, u32 free);
- /* Reset one direction, discard its IRQ status, and leave it disabled. */
+ /* Reset one direction, clear its IRQ status, and leave it disabled. */
bool (*engine_reset)(struct dw_edma *dw, enum dw_edma_dir dir);
void (*engine_enable)(struct dw_edma *dw, enum dw_edma_dir dir);
void (*ch_doorbell)(struct dw_edma_chan *chan);
@@ -313,7 +335,7 @@ static inline void dw_edma_core_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