From c20cc217203e693c73e27fa9245ae88d1b6ff3a2 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 21:56:12 +0800 Subject: [PATCH 1/9] soundwire: stream: add support for ignoring system suspend Some streams, such as those used for wake-on-voice, must remain active during system suspend. Add an ignore_suspend flag to sdw_stream_runtime and export sdw_bus_ignore_suspend() so bus drivers can check whether any stream on the bus requires it to remain active. Signed-off-by: Bard Liao --- drivers/soundwire/stream.c | 19 +++++++++++++++++++ include/linux/soundwire/sdw.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/drivers/soundwire/stream.c b/drivers/soundwire/stream.c index c158bf91e020f9..76e5c0369ca04f 100644 --- a/drivers/soundwire/stream.c +++ b/drivers/soundwire/stream.c @@ -1688,6 +1688,25 @@ int sdw_enable_stream(struct sdw_stream_runtime *stream) } EXPORT_SYMBOL(sdw_enable_stream); +bool sdw_bus_ignore_suspend(struct sdw_bus *bus) +{ + struct sdw_master_runtime *m_rt; + bool ret = false; + + mutex_lock(&bus->bus_lock); + list_for_each_entry(m_rt, &bus->m_rt_list, bus_node) { + if (m_rt->stream->state == SDW_STREAM_ENABLED && + m_rt->stream->ignore_suspend) { + ret = true; + break; + } + } + mutex_unlock(&bus->bus_lock); + + return ret; +} +EXPORT_SYMBOL(sdw_bus_ignore_suspend); + static int _sdw_disable_stream(struct sdw_stream_runtime *stream) { struct sdw_master_runtime *m_rt; diff --git a/include/linux/soundwire/sdw.h b/include/linux/soundwire/sdw.h index 326db122bbe6ea..ac9755d11daa2d 100644 --- a/include/linux/soundwire/sdw.h +++ b/include/linux/soundwire/sdw.h @@ -978,6 +978,7 @@ struct sdw_stream_params { * @master_list: List of Master runtime(s) in this stream. * master_list can contain only one m_rt per Master instance * for a stream + * @ignore_suspend: stream keeps running during system suspend */ struct sdw_stream_runtime { const char *name; @@ -986,6 +987,7 @@ struct sdw_stream_runtime { enum sdw_stream_type type; int m_rt_count; struct list_head master_list; + bool ignore_suspend; }; /** @@ -1086,6 +1088,7 @@ int sdw_stream_remove_master(struct sdw_bus *bus, int sdw_startup_stream(void *sdw_substream); int sdw_prepare_stream(struct sdw_stream_runtime *stream); int sdw_enable_stream(struct sdw_stream_runtime *stream); +bool sdw_bus_ignore_suspend(struct sdw_bus *bus); int sdw_disable_stream(struct sdw_stream_runtime *stream); int sdw_deprepare_stream(struct sdw_stream_runtime *stream); void sdw_shutdown_stream(void *sdw_substream); From 334de3904a1cf70cf83234f82987862fc8008ff8 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 22:06:50 +0800 Subject: [PATCH 2/9] soundwire: Intel: preserve buses for ignore-suspend streams Honor the SoundWire bus ignore-suspend state in the Intel drivers. Preserve the master across system suspend when a stream requires the bus to remain active. Signed-off-by: Bard Liao --- drivers/soundwire/intel.c | 3 +++ drivers/soundwire/intel_ace2x.c | 3 +++ drivers/soundwire/intel_auxdevice.c | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/drivers/soundwire/intel.c b/drivers/soundwire/intel.c index af65214836b4d0..2064e64c2c5455 100644 --- a/drivers/soundwire/intel.c +++ b/drivers/soundwire/intel.c @@ -953,6 +953,9 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + if (sdw_bus_ignore_suspend(&cdns->bus)) + continue; + dai_runtime = cdns->dai_runtime_array[dai->id]; if (dai_runtime) diff --git a/drivers/soundwire/intel_ace2x.c b/drivers/soundwire/intel_ace2x.c index 642b33ab552606..717531d737f586 100644 --- a/drivers/soundwire/intel_ace2x.c +++ b/drivers/soundwire/intel_ace2x.c @@ -952,6 +952,9 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + if (sdw_bus_ignore_suspend(&cdns->bus)) + continue; + dai_runtime = cdns->dai_runtime_array[dai->id]; if (dai_runtime) diff --git a/drivers/soundwire/intel_auxdevice.c b/drivers/soundwire/intel_auxdevice.c index 5983fbc5725a26..112e6800ba0ec0 100644 --- a/drivers/soundwire/intel_auxdevice.c +++ b/drivers/soundwire/intel_auxdevice.c @@ -650,6 +650,9 @@ static int __maybe_unused intel_suspend(struct device *dev) return 0; } + if (sdw_bus_ignore_suspend(bus)) + return 0; + /* Prevent runtime PM from racing with the code below. */ pm_runtime_disable(dev); @@ -737,6 +740,12 @@ static int __maybe_unused intel_resume(struct device *dev) return 0; } + if (sdw_bus_ignore_suspend(bus)) { + dev_dbg(dev, "SoundWire master %d remained active during system suspend\n", + bus->link_id); + return 0; + } + ret = sdw_intel_link_power_up(sdw); if (ret) { dev_err(dev, "%s failed: %d\n", __func__, ret); From 985b8a43d2ed01892bbe44cdc02817fcc70441f1 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 21:49:39 +0800 Subject: [PATCH 3/9] ASoC: soc_sdw_utils: preserve streams that ignore suspend Do not disable a SoundWire stream on suspend when it is marked to remain active. If the stream is already enabled on resume, no further action is needed. Signed-off-by: Bard Liao --- sound/soc/sdw_utils/soc_sdw_utils.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sound/soc/sdw_utils/soc_sdw_utils.c b/sound/soc/sdw_utils/soc_sdw_utils.c index 7a19de75f32b53..2072e3e215708c 100644 --- a/sound/soc/sdw_utils/soc_sdw_utils.c +++ b/sound/soc/sdw_utils/soc_sdw_utils.c @@ -1902,6 +1902,10 @@ int asoc_sdw_trigger(struct snd_pcm_substream *substream, int cmd) switch (cmd) { case SNDRV_PCM_TRIGGER_RESUME: + if (sdw_stream->state == SDW_STREAM_ENABLED) { + ret = 0; + break; + } /* * The peripherals lose their port configuration when the * controller is power-gated during system suspend, and an @@ -1923,10 +1927,12 @@ int asoc_sdw_trigger(struct snd_pcm_substream *substream, int cmd) break; case SNDRV_PCM_TRIGGER_PAUSE_PUSH: - case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_STOP: ret = sdw_disable_stream(sdw_stream); break; + case SNDRV_PCM_TRIGGER_SUSPEND: + ret = sdw_stream->ignore_suspend ? 0 : sdw_disable_stream(sdw_stream); + break; default: ret = -EINVAL; break; From 7128caf1d29a8fc4a4b0b88654a16d54e22a4fa7 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 27 Oct 2026 22:59:08 +0800 Subject: [PATCH 4/9] ASoC: dapm: Export snd_soc_dapm_mark_endpoints_dirty The follow up commit will use snd_soc_dapm_mark_endpoints_dirty() to invalidate endpoint paths after changing widget ignore_suspend flags. Signed-off-by: Bard Liao --- sound/soc/soc-dapm.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/soc-dapm.c b/sound/soc/soc-dapm.c index 74be5e20949337..436643567f6704 100644 --- a/sound/soc/soc-dapm.c +++ b/sound/soc/soc-dapm.c @@ -355,6 +355,7 @@ void snd_soc_dapm_mark_endpoints_dirty(struct snd_soc_card *card) snd_soc_dapm_mutex_unlock(card); } +EXPORT_SYMBOL_GPL(snd_soc_dapm_mark_endpoints_dirty); /* create a new dapm widget */ static inline struct snd_soc_dapm_widget *dapm_cnew_widget( From 4e95cff35b10c667cfd8f52d75714b8be97ba6f4 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Wed, 21 Oct 2026 13:54:50 +0800 Subject: [PATCH 5/9] ASoC: SOF: manage WoV DAPM retention across suspend Keep connected DAPM endpoint widgets active while SOF retains a wake-on-voice PCM across suspend. Register the card's endpoint widgets for the PCM direction when opening a PCM, along with the DAI widgets of that direction since DAPM only marks them as endpoints once their streams start. Track an ignore_suspend reference count for each registered widget. Determine the connected endpoints of the PCM direction when retaining a stream at SUSPEND, rather than caching them during prepare. Record the references acquired by each stream so shared widgets remain marked until their final WoV user releases them, even if routes change. Restore the original ignore_suspend value on the final release and refresh DAPM endpoint path caches after changing ignore_suspend. Treat widget retention as best-effort: log failures but still mark the stream suspend_ignored so FE/BE suspend bookkeeping and the retained stream resume path remain consistent. A DROP on a suspended PCM does not issue STOP, so suspend_ignored can remain set if userspace prepares the PCM instead of resuming it. Clear suspend_ignored and release DAPM retention from hw_free and close, which may also run without START or RESUME. Clear them in prepare as a final fallback when a suspended PCM is prepared directly while suspend_ignored is still set. Signed-off-by: Bard Liao Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sound/soc/sof/core.c | 1 + sound/soc/sof/pcm.c | 266 +++++++++++++++++++++++++++++++++++++- sound/soc/sof/sof-audio.h | 2 + sound/soc/sof/sof-priv.h | 10 ++ sound/soc/sof/topology.c | 1 + 5 files changed, 276 insertions(+), 4 deletions(-) diff --git a/sound/soc/sof/core.c b/sound/soc/sof/core.c index 1f38fe03e49395..37fca4c37d59bc 100644 --- a/sound/soc/sof/core.c +++ b/sound/soc/sof/core.c @@ -690,6 +690,7 @@ int snd_sof_device_probe(struct device *dev, struct snd_sof_pdata *plat_data) INIT_LIST_HEAD(&sdev->dai_list); INIT_LIST_HEAD(&sdev->dai_link_list); INIT_LIST_HEAD(&sdev->route_list); + INIT_LIST_HEAD(&sdev->ignore_suspend_widget_list); INIT_LIST_HEAD(&sdev->ipc_client_list); INIT_LIST_HEAD(&sdev->ipc_rx_handler_list); INIT_LIST_HEAD(&sdev->fw_state_handler_list); diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 0161c0ae8bbdb4..08901397539ea9 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -66,6 +66,224 @@ void snd_sof_pcm_period_elapsed(struct snd_pcm_substream *substream) } EXPORT_SYMBOL(snd_sof_pcm_period_elapsed); +static void sof_pcm_free_ignore_suspend_widgets(struct list_head *widgets) +{ + struct snd_sof_ignore_suspend_widget *isw, *tmp; + + list_for_each_entry_safe(isw, tmp, widgets, list) { + list_del(&isw->list); + kfree(isw); + } +} + +static int sof_pcm_add_ignore_suspend_widget(struct list_head *widgets, + struct snd_soc_dapm_widget *dapm_widget) +{ + struct snd_sof_ignore_suspend_widget *isw; + + list_for_each_entry(isw, widgets, list) { + if (isw->widget == dapm_widget) + return 0; + } + + isw = kzalloc_obj(*isw); + if (!isw) + return -ENOMEM; + + isw->widget = dapm_widget; + list_add_tail(&isw->list, widgets); + + return 0; +} + +static struct snd_sof_ignore_suspend_widget * +sof_pcm_find_ignore_suspend_widget(struct snd_sof_dev *sdev, struct snd_soc_dapm_widget *widget) +{ + struct snd_sof_ignore_suspend_widget *isw; + + list_for_each_entry(isw, &sdev->ignore_suspend_widget_list, list) { + if (isw->widget == widget) + return isw; + } + + return NULL; +} + +static unsigned int sof_pcm_dir_to_ep(int dir) +{ + return SND_SOC_DAPM_DIR_TO_EP(dir == SNDRV_PCM_STREAM_PLAYBACK ? + SND_SOC_DAPM_DIR_OUT : SND_SOC_DAPM_DIR_IN); +} + +static int sof_pcm_init_ignore_suspend_widgets(struct snd_sof_dev *sdev, + struct snd_soc_card *card, int dir) +{ + enum snd_soc_dapm_type dai_id = dir == SNDRV_PCM_STREAM_PLAYBACK ? + snd_soc_dapm_dai_in : snd_soc_dapm_dai_out; + unsigned int ep = sof_pcm_dir_to_ep(dir); + struct snd_soc_dapm_widget *widget; + LIST_HEAD(widgets); + int ret = 0; + + snd_soc_dapm_mutex_lock(card); + for_each_card_widgets(card, widget) { + /* DAI widgets become endpoints when their streams start. */ + if (!(widget->is_ep & ep) && widget->id != dai_id) + continue; + + if (sof_pcm_find_ignore_suspend_widget(sdev, widget)) + continue; + + ret = sof_pcm_add_ignore_suspend_widget(&widgets, widget); + if (ret) + goto unlock; + } + list_splice_tail_init(&widgets, &sdev->ignore_suspend_widget_list); +unlock: + snd_soc_dapm_mutex_unlock(card); + sof_pcm_free_ignore_suspend_widgets(&widgets); + + return ret; +} + +static int sof_pcm_collect_ignore_suspend_widgets(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *rtd, + struct snd_sof_pcm *spcm, int dir, + struct list_head *widgets) +{ + enum snd_soc_dapm_type dai_id = dir == SNDRV_PCM_STREAM_PLAYBACK ? + snd_soc_dapm_dai_in : snd_soc_dapm_dai_out; + unsigned int ep = sof_pcm_dir_to_ep(dir); + struct snd_soc_dai *dai; + int j; + + for_each_rtd_cpu_dais(rtd, j, dai) { + struct snd_soc_dapm_widget_list *connected_widgets; + struct snd_soc_dapm_widget *widget; + int connected, i, ret; + + connected = snd_soc_dapm_dai_get_connected_widgets(dai, dir, + &connected_widgets, + NULL); + if (connected < 0) { + dev_dbg(sdev->dev, + "failed to enumerate D0i3-compatible PCM %s widgets: %d\n", + spcm->pcm.caps[dir].name, connected); + return connected; + } + + widget = snd_soc_dai_get_widget(dai, dir); + if (widget && widget->is_ep) { + ret = sof_pcm_add_ignore_suspend_widget(widgets, widget); + if (ret) { + snd_soc_dapm_dai_free_widgets(&connected_widgets); + return ret; + } + } + + for_each_dapm_widgets(connected_widgets, i, widget) { + if (!(widget->is_ep & ep) && widget->id != dai_id) + continue; + + ret = sof_pcm_add_ignore_suspend_widget(widgets, widget); + if (ret) { + snd_soc_dapm_dai_free_widgets(&connected_widgets); + return ret; + } + } + + snd_soc_dapm_dai_free_widgets(&connected_widgets); + } + + return 0; +} + +static void sof_pcm_put_widget_ignore_suspend(struct snd_sof_ignore_suspend_widget *isw) +{ + if (WARN_ON_ONCE(!isw->refcount)) + return; + + if (--isw->refcount) + return; + + isw->widget->ignore_suspend = isw->initial_ignore_suspend; +} + +static int sof_pcm_set_widget_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *rtd, + struct snd_sof_pcm *spcm, int dir, + bool ignore_suspend) +{ + struct list_head *retained_widgets = &spcm->stream[dir].ignore_suspend_widgets; + struct snd_sof_ignore_suspend_widget *isw; + LIST_HEAD(widgets); + int ret = 0; + + if (!ignore_suspend && list_empty(retained_widgets)) + return 0; + + if (ignore_suspend) { + ret = sof_pcm_collect_ignore_suspend_widgets(sdev, rtd, spcm, dir, &widgets); + if (ret) + goto out; + } + + snd_soc_dapm_mutex_lock(rtd->card); + list_for_each_entry(isw, &widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (WARN_ON_ONCE(!owned)) { + ret = -EINVAL; + goto unlock; + } + if (owned->refcount == UINT_MAX) { + ret = -EOVERFLOW; + goto unlock; + } + } + + list_for_each_entry(isw, &widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (!owned->refcount) + owned->initial_ignore_suspend = owned->widget->ignore_suspend; + owned->refcount++; + owned->widget->ignore_suspend = 1; + dev_dbg(sdev->dev, "WoV PCM %s ignore suspend for widget %s\n", + spcm->pcm.caps[dir].name, isw->widget->name); + } + if (ignore_suspend) { + list_splice_tail_init(&widgets, retained_widgets); + } else { + list_for_each_entry(isw, retained_widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (WARN_ON_ONCE(!owned)) + continue; + + sof_pcm_put_widget_ignore_suspend(owned); + } + } +unlock: + snd_soc_dapm_mutex_unlock(rtd->card); + if (ret) + goto out; + + if (!ignore_suspend) + sof_pcm_free_ignore_suspend_widgets(retained_widgets); + + snd_soc_dapm_mark_endpoints_dirty(rtd->card); +out: + sof_pcm_free_ignore_suspend_widgets(&widgets); + if (ret) + spcm_err(spcm, dir, "failed to retain WoV widgets: %d\n", ret); + + return ret; +} + int sof_pcm_setup_connected_widgets(struct snd_sof_dev *sdev, struct snd_soc_pcm_runtime *rtd, struct snd_sof_pcm *spcm, struct snd_pcm_hw_params *params, @@ -303,6 +521,10 @@ static int sof_pcm_hw_free(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: hw_free\n"); + if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, substream->stream, false); + spcm->stream[substream->stream].suspend_ignored = false; + } ret = sof_pcm_stream_free(sdev, substream, spcm, substream->stream, true); /* unprepare and free the list of DAPM widgets */ @@ -336,6 +558,16 @@ static int sof_pcm_prepare(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: prepare\n"); + /* + * A DROP on a suspended PCM does not issue STOP. Clear the frontend's + * retained state before a later prepare/start sequence can consume it. + * The backend will check for another retained WoV FE before skipping START. + */ + if (spcm->stream[dir].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + spcm->stream[dir].suspend_ignored = false; + } + if (spcm->prepared[substream->stream]) { if (!spcm->pending_stop[substream->stream]) return 0; @@ -421,6 +653,8 @@ static int sof_pcm_trigger(struct snd_soc_component *component, * not supported, no need to re-start streams that * remained enabled in D0ix. */ + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); spcm->stream[substream->stream].suspend_ignored = false; return 0; } @@ -430,6 +664,8 @@ static int sof_pcm_trigger(struct snd_soc_component *component, break; case SNDRV_PCM_TRIGGER_RESUME: if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); spcm->stream[substream->stream].suspend_ignored = false; return 0; } @@ -448,6 +684,12 @@ static int sof_pcm_trigger(struct snd_soc_component *component, substream->stream == SNDRV_PCM_STREAM_CAPTURE && spcm->stream[substream->stream].d0i3_compatible && spcm->stream[substream->stream].dsp_max_burst_size_in_ms <= 1) { + if (spcm->stream[substream->stream].suspend_ignored) + return 0; + + /* Widget retention is best-effort; keep FE/BE suspend state balanced. */ + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, true); spcm->stream[substream->stream].suspend_ignored = true; return 0; } @@ -566,6 +808,14 @@ static int sof_pcm_open(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: open\n"); + ret = sof_pcm_init_ignore_suspend_widgets(sdev, rtd->card, + substream->stream); + if (ret) { + spcm_err(spcm, substream->stream, + "failed to initialize WoV endpoint widgets: %d\n", ret); + return ret; + } + caps = &spcm->pcm.caps[substream->stream]; /* set runtime config */ @@ -614,7 +864,7 @@ static int sof_pcm_close(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); struct snd_sof_pcm *spcm; - int err; + int ret; /* nothing to do for BE */ if (rtd->dai_link->no_pcm) @@ -626,10 +876,14 @@ static int sof_pcm_close(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: close\n"); - err = snd_sof_pcm_platform_close(sdev, substream); - if (err < 0) { + if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, substream->stream, false); + spcm->stream[substream->stream].suspend_ignored = false; + } + ret = snd_sof_pcm_platform_close(sdev, substream); + if (ret < 0) { spcm_err(spcm, substream->stream, - "platform pcm close failed %d\n", err); + "platform pcm close failed %d\n", ret); /* * keep going, no point in preventing the close * from happening @@ -808,6 +1062,10 @@ static int sof_pcm_probe(struct snd_soc_component *component) static void sof_pcm_remove(struct snd_soc_component *component) { + struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); + + sof_pcm_free_ignore_suspend_widgets(&sdev->ignore_suspend_widget_list); + /* remove topology */ snd_soc_tplg_component_remove(component); } diff --git a/sound/soc/sof/sof-audio.h b/sound/soc/sof/sof-audio.h index 10b41ab18b7990..c691d322eceee4 100644 --- a/sound/soc/sof/sof-audio.h +++ b/sound/soc/sof/sof-audio.h @@ -346,6 +346,8 @@ struct snd_sof_pcm_stream { * active or not while suspending the stream */ bool suspend_ignored; + /* Endpoint references acquired when retaining this stream across suspend. */ + struct list_head ignore_suspend_widgets; struct snd_sof_pcm_stream_pipeline_list pipeline_list; /* used by IPC implementation and core does not touch it */ diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 83e4d83cc27cbb..4b17b58a2b9b8f 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -407,6 +407,14 @@ struct snd_sof_dfsentry { }; }; +/* DAPM endpoint or DAI widget tracked for WoV ignore_suspend management */ +struct snd_sof_ignore_suspend_widget { + struct snd_soc_dapm_widget *widget; + unsigned int refcount; + bool initial_ignore_suspend; + struct list_head list; +}; + /* Debug mapping for any DSP memory or registers that can used for debug */ struct snd_sof_debugfs_map { const char *name; @@ -653,6 +661,8 @@ struct snd_sof_dev { struct list_head dai_list; struct list_head dai_link_list; struct list_head route_list; + /* All card DAPM endpoints/DAI widgets and their WoV ignore_suspend reference counts */ + struct list_head ignore_suspend_widget_list; struct snd_soc_component *component; u32 enabled_cores_mask; /* keep track of enabled cores */ bool led_present; diff --git a/sound/soc/sof/topology.c b/sound/soc/sof/topology.c index 750916b57def74..f91798b21f586f 100644 --- a/sound/soc/sof/topology.c +++ b/sound/soc/sof/topology.c @@ -1773,6 +1773,7 @@ static int sof_dai_load(struct snd_soc_component *scomp, int index, spcm->scomp = scomp; for_each_pcm_streams(stream) { + INIT_LIST_HEAD(&spcm->stream[stream].ignore_suspend_widgets); spcm->stream[stream].comp_id = COMP_ID_UNASSIGNED; if (pcm->compress) snd_sof_compr_init_elapsed_work(&spcm->stream[stream].period_elapsed_work); From e8c1cf9de14333f048fa710d6586230a6f32c047 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 22:10:20 +0800 Subject: [PATCH 6/9] ASoC: SOF: Intel: keep SoundWire streams active for D0i3-compatible PCMs Keep a SoundWire stream active during system suspend when it is used by a WoV stream. WoV streams can be identified by: They are capture streams and. They have the d0i3_compatible flag set and They don't use Deep Buffer. The decision is made in the BE SUSPEND trigger. The BE DAI trigger runs before the SoundWire link trigger, so the ignore_suspend flag of the SoundWire stream is updated before the link trigger decides whether to disable the stream. When a BE is shared by WoV and regular FEs, the BE only receives SUSPEND once the last FE is suspended, and all connected FEs are checked at that point regardless of their open or suspend order. A WoV PCM retained in a previous S0ix suspend stays in the SUSPENDED state and will not receive another SUSPEND trigger. If the system then enters a deeper suspend state, stop its FE and BE pipelines and release its DAPM retention at PM prepare time. Assisted-by: LLM Signed-off-by: Bard Liao Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sound/soc/sof/intel/hda-dai.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index 7a8875a885ab34..4a6553c7bd82b6 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -308,6 +308,27 @@ static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substrea return false; } +static void hda_dai_set_sdw_ignore_suspend(struct snd_soc_dai *dai, int dir, + bool ignore_suspend) +{ + struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(dai, dir); + struct sdw_stream_runtime *sdw_stream; + struct snd_sof_widget *swidget; + struct snd_sof_dai *sof_dai; + + if (!w || !w->dobj.private) + return; + + swidget = w->dobj.private; + sof_dai = swidget->private; + if (!sof_dai || sof_dai->type != SOF_DAI_INTEL_ALH) + return; + + sdw_stream = snd_soc_dai_get_stream(dai, dir); + if (!IS_ERR_OR_NULL(sdw_stream)) + sdw_stream->ignore_suspend = ignore_suspend; +} + /* * In contrast to IPC3, the dai trigger in IPC4 mixes pipeline state changes * (over IPC channel) and DMA state change (direct host register changes). @@ -691,6 +712,16 @@ EXPORT_SYMBOL_NS(sdw_hda_dai_hw_free, "SND_SOC_SOF_INTEL_HDA_COMMON"); int sdw_hda_dai_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *cpu_dai) { + struct snd_sof_dev *sdev = dai_to_sdev(substream, cpu_dai); + bool retain = false; + + if (cmd == SNDRV_PCM_TRIGGER_SUSPEND) + retain = sdev->system_suspend_target == SOF_SUSPEND_S0IX && + hda_dai_has_wov_fe(sdev, substream); + + if (cmd == SNDRV_PCM_TRIGGER_SUSPEND || cmd == SNDRV_PCM_TRIGGER_STOP) + hda_dai_set_sdw_ignore_suspend(cpu_dai, substream->stream, retain); + return hda_dai_trigger(substream, cmd, cpu_dai); } EXPORT_SYMBOL_NS(sdw_hda_dai_trigger, "SND_SOC_SOF_INTEL_HDA_COMMON"); From a2fd4c34b51165ee037380679ab30d29c2b62198 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Fri, 16 Oct 2026 16:10:41 +0800 Subject: [PATCH 7/9] ASoC: soc_sdw_utils: Handle enabled SoundWire streams Multiple FE streams can share a SoundWire BE, so prepare may arrive after another FE has enabled the shared stream. Skip sdw_prepare_stream() when the stream is already enabled. When hw_free is called for an enabled stream, disable it before depreparing. Propagate disable errors rather than attempting to deprepare an enabled stream. Signed-off-by: Bard Liao --- sound/soc/sdw_utils/soc_sdw_utils.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sound/soc/sdw_utils/soc_sdw_utils.c b/sound/soc/sdw_utils/soc_sdw_utils.c index 2072e3e215708c..556284f48a8913 100644 --- a/sound/soc/sdw_utils/soc_sdw_utils.c +++ b/sound/soc/sdw_utils/soc_sdw_utils.c @@ -1880,6 +1880,9 @@ int asoc_sdw_prepare(struct snd_pcm_substream *substream) return PTR_ERR(sdw_stream); } + if (sdw_stream->state == SDW_STREAM_ENABLED) + return 0; + return sdw_prepare_stream(sdw_stream); } EXPORT_SYMBOL_NS(asoc_sdw_prepare, "SND_SOC_SDW_UTILS"); @@ -2011,6 +2014,16 @@ int asoc_sdw_hw_free(struct snd_pcm_substream *substream) return PTR_ERR(sdw_stream); } + if (sdw_stream->state == SDW_STREAM_ENABLED) { + int ret; + + ret = sdw_disable_stream(sdw_stream); + if (ret) { + dev_err(rtd->dev, "failed to disable SoundWire stream: %d\n", ret); + return ret; + } + } + return sdw_deprepare_stream(sdw_stream); } EXPORT_SYMBOL_NS(asoc_sdw_hw_free, "SND_SOC_SDW_UTILS"); From 4903a424073fb99c59b8af877c7ddf045b3d4e97 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Mon, 19 Oct 2026 22:50:31 +0800 Subject: [PATCH 8/9] ASoC: dpcm: Fix shared BE recovery with mixed START and RESUME Two FEs sharing a BE can recover from system suspend differently: one prepares and starts again, while the other resumes. Their recovery callbacks can run concurrently. BE prepare checks the DPCM state and then calls the prepare callbacks without holding the BE stream lock used by trigger. A concurrent RESUME can therefore change the BE from SUSPEND to START and increment be_start while the other FE is still preparing. When prepare returns, it overwrites START with PREPARE. The subsequent START increments be_start to two but skips the hardware trigger and state update because it is not the first user, leaving the active BE in PREPARE. Both later STOP requests reject PREPARE, so neither decrements be_start nor stops the BE. This was observed with regular and retained capture FEs sharing a SoundWire BE: the firmware pipeline remained running and could not be reset before deletion, and the SoundWire stream remained enabled when teardown attempted to deprepare it. Serialize the state checks, prepare callbacks and PREPARE assignment with BE triggers for non-atomic PCMs, using their sleepable stream mutex. Do not hold an atomic PCM spinlock over prepare callbacks, which may sleep. Also handle the opposite recovery order. If another FE has already prepared or started the BE, RESUME must still account for its FE in be_start. Accept PREPARE and START as well as SUSPEND, and trigger only the first user. Use START when the BE has already been prepared, retaining RESUME for a BE that is still suspended. Roll back the increment if the hardware trigger fails. With POST trigger ordering, the BEs are resumed before the FE. If the FE RESUME trigger then fails, the PCM core does not undo it because the stream has not entered the running state, leaving be_start counting this FE. A later prepare and START of the FE would count it again, so the shared BE could never be stopped. Suspend the BEs again in that case. Failed START and PAUSE_RELEASE triggers are already undone by the PCM core, which issues STOP or PAUSE_PUSH through the regular DPCM path. This keeps the BE state and active-user count consistent so that only the last stopping FE shuts down the shared BE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/soc-pcm.c | 48 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index ef8ccf52798eb6..42b065403c2c83 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -2266,14 +2266,22 @@ int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; break; case SNDRV_PCM_TRIGGER_RESUME: - if ((be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND)) + /* + * Another FE may already have prepared or restarted the BE. + * Count this FE as well, but only trigger the first user. + */ + if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PREPARE && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) goto next; be->dpcm[stream].be_start++; if (be->dpcm[stream].be_start != 1) goto next; - ret = soc_pcm_trigger(be_substream, cmd); + ret = soc_pcm_trigger(be_substream, + be->dpcm[stream].state == SND_SOC_DPCM_STATE_PREPARE ? + SNDRV_PCM_TRIGGER_START : cmd); if (ret) { be->dpcm[stream].be_start--; goto next; @@ -2413,6 +2421,21 @@ static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream, fe->dai_link->name, cmd); ret = soc_pcm_trigger(substream, cmd); + /* + * The PCM core undoes failed START and PAUSE_RELEASE triggers, + * but not RESUME because the stream is not running yet. Drop + * the BE reference taken for this FE so be_start stays balanced. + */ + if (ret < 0 && cmd == SNDRV_PCM_TRIGGER_RESUME) { + int rollback_ret; + + rollback_ret = dpcm_be_dai_trigger(fe, substream->stream, + SNDRV_PCM_TRIGGER_SUSPEND); + if (rollback_ret < 0) + dev_err(fe->dev, + "failed to roll back BE resume after FE trigger failure: %d\n", + rollback_ret); + } } end: return snd_soc_ret(fe->dev, ret, "trigger FE cmd: %d failed\n", cmd); @@ -2502,6 +2525,7 @@ static int dpcm_fe_dai_trigger(struct snd_pcm_substream *substream, int cmd) int dpcm_be_dai_prepare(struct snd_soc_pcm_runtime *fe, int stream) { struct snd_soc_dpcm *dpcm; + unsigned long flags = 0; int ret = 0; for_each_dpcm_be(fe, stream, dpcm) { @@ -2510,27 +2534,39 @@ int dpcm_be_dai_prepare(struct snd_soc_pcm_runtime *fe, int stream) struct snd_pcm_substream *be_substream = snd_soc_dpcm_get_substream(be, stream); + /* + * Non-atomic prepare callbacks may sleep. Serialize them with + * BE triggers using the stream mutex, not an atomic stream lock. + */ + if (be_substream->pcm->nonatomic) + snd_pcm_stream_lock_irqsave_nested(be_substream, flags); + /* is this op for this BE ? */ if (!snd_soc_dpcm_can_be_update(fe, be, stream)) - continue; + goto next; if (!snd_soc_dpcm_can_be_prepared(fe, be, stream)) - continue; + goto next; if ((be->dpcm[stream].state != SND_SOC_DPCM_STATE_HW_PARAMS) && (be->dpcm[stream].state != SND_SOC_DPCM_STATE_STOP) && (be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND) && (be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED)) - continue; + goto next; dev_dbg(be->dev, "ASoC: prepare BE %s\n", be->dai_link->name); ret = __soc_pcm_prepare(be, be_substream); if (ret < 0) - break; + goto next; be->dpcm[stream].state = SND_SOC_DPCM_STATE_PREPARE; +next: + if (be_substream->pcm->nonatomic) + snd_pcm_stream_unlock_irqrestore(be_substream, flags); + if (ret < 0) + break; } /* From 13643264f83d6f480b01d589d4beb488cd14e4e4 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 20 Oct 2026 08:05:19 +0800 Subject: [PATCH 9/9] ASoC: SOF: Intel: Handle retained HDA BE trigger recovery A SoundWire capture BE shared by regular and WoV FEs can remain running across S0ix. Its HDA suspend trigger sets suspend_ignored and skips the pipeline and link DMA teardown. Recovery must consume that flag without starting the retained hardware again. The first BE trigger after system resume is not necessarily RESUME. A regular capture FE can recover through prepare followed by START. Also, when another FE has already prepared the shared BE, DPCM translates the first RESUME into START rather than resuming an unprepared stream. Only handling RESUME in the retained-stream shortcut lets START fall through to the normal HDA trigger callbacks. This unnecessarily pauses and restarts the retained IPC4 pipeline, starts the link DMA again and increments the BE pipeline's started_count even though its existing running reference was preserved during suspend. It also leaves suspend_ignored set after recovery. Consume suspend_ignored on either START or RESUME and return without retriggering the retained hardware. DPCM still accounts for the recovering FE in be_start, while the HDA pipeline keeps its existing running reference until the last BE user stops. Ordinary START triggers are unchanged when suspend_ignored is false. With IPC4 capture links the FE is triggered before the BE. If a regular FE has prepared the shared BE before a retained WoV FE resumes, the WoV FE trigger consumes its suspend_ignored flag before DPCM sends START to the BE, and its DPCM state is only updated after the trigger completes. Treat a WoV FE which is being triggered while its PCM is still SUSPENDED as resuming so the retained BE is not restarted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/sof/intel/hda-dai-ops.c | 4 ++++ sound/soc/sof/intel/hda-dai.c | 40 ++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/sound/soc/sof/intel/hda-dai-ops.c b/sound/soc/sof/intel/hda-dai-ops.c index f0be42048db33c..c002c234c45797 100644 --- a/sound/soc/sof/intel/hda-dai-ops.c +++ b/sound/soc/sof/intel/hda-dai-ops.c @@ -580,6 +580,10 @@ static int hda_ipc3_post_trigger(struct snd_sof_dev *sdev, struct snd_soc_dai *c { struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(cpu_dai, substream->stream); + if (sdev->fw_state != SOF_FW_BOOT_COMPLETE && + (cmd == SNDRV_PCM_TRIGGER_STOP || cmd == SNDRV_PCM_TRIGGER_SUSPEND)) + return 0; + switch (cmd) { case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_STOP: diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index 4a6553c7bd82b6..f9f9d8c6212591 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -282,6 +282,25 @@ static int __maybe_unused hda_dai_hw_params(struct snd_pcm_substream *substream, return hda_dai_hw_params_data(substream, params, dai, &data, flags); } +/* + * A retained WoV FE that is being resumed has already consumed its + * suspend_ignored flag when the FE is triggered before the BE, but its + * DPCM state is not updated until the trigger completes. Its PCM is still + * SUSPENDED during the trigger. + */ +static bool hda_dai_fe_is_resuming(struct snd_soc_pcm_runtime *fe) +{ + struct snd_pcm_substream *fe_substream; + + if (fe->dpcm[SNDRV_PCM_STREAM_CAPTURE].runtime_update != SND_SOC_DPCM_UPDATE_FE) + return false; + + fe_substream = snd_soc_dpcm_get_substream(fe, SNDRV_PCM_STREAM_CAPTURE); + + return fe_substream && fe_substream->runtime && + fe_substream->runtime->state == SNDRV_PCM_STATE_SUSPENDED; +} + static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); @@ -292,16 +311,17 @@ static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substrea return false; /* - * Only retain the BE for a connected WoV capture FE that is running - * or has already ignored its suspend trigger. An idle WoV FE may - * still be connected to the BE. + * Only retain the BE for a connected WoV capture FE that is running, + * has already ignored its suspend trigger or is being resumed. An idle + * WoV FE may still be connected to the BE. */ for_each_dpcm_fe(rtd, SNDRV_PCM_STREAM_CAPTURE, dpcm) { spcm = snd_sof_find_spcm_dai(sdev->component, dpcm->fe); if (spcm && spcm->stream[SNDRV_PCM_STREAM_CAPTURE].d0i3_compatible && spcm->stream[SNDRV_PCM_STREAM_CAPTURE].dsp_max_burst_size_in_ms <= 1 && (dpcm->fe->dpcm[SNDRV_PCM_STREAM_CAPTURE].state == SND_SOC_DPCM_STATE_START || - spcm->stream[SNDRV_PCM_STREAM_CAPTURE].suspend_ignored)) + spcm->stream[SNDRV_PCM_STREAM_CAPTURE].suspend_ignored || + hda_dai_fe_is_resuming(dpcm->fe))) return true; } @@ -340,6 +360,7 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i struct hdac_ext_stream *hext_stream; struct sof_intel_hda_stream *hda_stream; struct snd_sof_dev *sdev; + bool skip_fw_ipc; int ret; if (!ops) { @@ -351,13 +372,20 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i dai->name, substream->stream); sdev = dai_to_sdev(substream, dai); + /* + * Firmware IPC is unavailable after a crash, but STOP/SUSPEND must + * still clean up the host-side DMA resources below. + */ + skip_fw_ipc = sdev->fw_state != SOF_FW_BOOT_COMPLETE && + (cmd == SNDRV_PCM_TRIGGER_STOP || cmd == SNDRV_PCM_TRIGGER_SUSPEND); hext_stream = ops->get_hext_stream(sdev, dai, substream); if (!hext_stream) return -EINVAL; hda_stream = hstream_to_sof_hda_stream(hext_stream); - if (cmd == SNDRV_PCM_TRIGGER_RESUME && hda_stream->suspend_ignored) { + if ((cmd == SNDRV_PCM_TRIGGER_START || cmd == SNDRV_PCM_TRIGGER_RESUME) && + hda_stream->suspend_ignored) { hda_stream->suspend_ignored = false; return 0; } @@ -374,7 +402,7 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i return 0; } - if (ops->pre_trigger) { + if (ops->pre_trigger && !skip_fw_ipc) { ret = ops->pre_trigger(sdev, dai, substream, cmd); if (ret < 0) return ret;