From fcadda288c974add41660f0b63836ec4f318dfdb 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 b4f68356708fe4..5965abde882911 100644 --- a/drivers/soundwire/stream.c +++ b/drivers/soundwire/stream.c @@ -1679,6 +1679,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 1b03bfad2e6bbe..50960ab8baf3b0 100644 --- a/include/linux/soundwire/sdw.h +++ b/include/linux/soundwire/sdw.h @@ -976,6 +976,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; @@ -984,6 +985,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 0f1b4225649fa6a0627d8f4b43b134aeff2593e2 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 901a71262094f9..fd12035c38ee3c 100644 --- a/drivers/soundwire/intel_auxdevice.c +++ b/drivers/soundwire/intel_auxdevice.c @@ -647,6 +647,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); @@ -734,6 +737,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 eef173f8f0998b8c223a86a6aca1a142789571bc 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 14aee10b40ba7c..e3ee8f385952f0 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 9c600f3f15fc661da300e07a41a029034bffb106 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 22:10:20 +0800 Subject: [PATCH 4/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 indetified by: They are capture streams and. They have the d0i3_compatible flag set and They don't use Deep Buffer. Assisted-by: LLM Signed-off-by: Bard Liao Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sound/soc/sof/intel/hda-common-ops.c | 1 + sound/soc/sof/intel/hda-dai.c | 31 +++++++-------- sound/soc/sof/intel/hda.c | 59 ++++++++++++++++++++++++++++ sound/soc/sof/intel/hda.h | 3 ++ sound/soc/sof/ops.h | 9 +++++ sound/soc/sof/pcm.c | 48 ++++++++++++++++++++++ sound/soc/sof/pm.c | 10 ++++- sound/soc/sof/sof-audio.h | 1 + sound/soc/sof/sof-priv.h | 4 ++ 9 files changed, 148 insertions(+), 18 deletions(-) diff --git a/sound/soc/sof/intel/hda-common-ops.c b/sound/soc/sof/intel/hda-common-ops.c index aa91f2b6fd657b..f65b0a34fe3b6d 100644 --- a/sound/soc/sof/intel/hda-common-ops.c +++ b/sound/soc/sof/intel/hda-common-ops.c @@ -54,6 +54,7 @@ const struct snd_sof_dsp_ops sof_hda_common_ops = { .pcm_hw_params = hda_dsp_pcm_hw_params, .pcm_hw_free = hda_dsp_stream_hw_free, .pcm_trigger = hda_dsp_pcm_trigger, + .pcm_set_sdw_ignore_suspend = hda_dsp_set_sdw_ignore_suspend, .pcm_pointer = hda_dsp_pcm_pointer, .pcm_ack = hda_dsp_pcm_ack, diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index 7a8875a885ab34..e1193c9692f547 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -282,26 +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); } -static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream) +/* + * snd_sof_prepare() already selected the WoV PCMs to keep active in S0ix and + * marked them suspend_ignored. Reuse that decision so the DSP and SoundWire + * sides of the BE always agree. + */ +static bool hda_dai_has_retained_wov_fe(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct snd_soc_dpcm *dpcm; struct snd_sof_pcm *spcm; - if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + if (sdev->system_suspend_target != SOF_SUSPEND_S0IX || + substream->stream != SNDRV_PCM_STREAM_CAPTURE) 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. - */ 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)) + if (spcm && spcm->stream[SNDRV_PCM_STREAM_CAPTURE].suspend_ignored) return true; } @@ -342,13 +341,13 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i } /* - * Set the suspend_ignored flag for BE which is used with - * D0I3-compatible streams used for WoV to keep the firmware pipeline - * running. + * Keep the BE firmware pipeline running for WoV PCMs retained by + * snd_sof_prepare(). The SoundWire ignore_suspend flag is managed by + * snd_sof_prepare()/snd_sof_complete() before any SUSPEND trigger. */ if (cmd == SNDRV_PCM_TRIGGER_SUSPEND && - sdev->system_suspend_target == SOF_SUSPEND_S0IX && - hda_dai_has_wov_fe(sdev, substream)) { + snd_soc_substream_to_rtd(substream)->dai_link->no_pcm && + hda_dai_has_retained_wov_fe(sdev, substream)) { hda_stream->suspend_ignored = true; return 0; } diff --git a/sound/soc/sof/intel/hda.c b/sound/soc/sof/intel/hda.c index e1815bfe358b5d..6bbae46800e8b0 100644 --- a/sound/soc/sof/intel/hda.c +++ b/sound/soc/sof/intel/hda.c @@ -58,6 +58,61 @@ static int sdw_clock_stop_quirks = SDW_INTEL_CLK_STOP_BUS_RESET; module_param(sdw_clock_stop_quirks, int, 0444); MODULE_PARM_DESC(sdw_clock_stop_quirks, "SOF SoundWire clock stop quirks"); +static void hda_dsp_set_sdw_be_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *be, + int dir, bool ignore_suspend) +{ + struct snd_soc_dai *dai; + int i; + + for_each_rtd_cpu_dais(be, i, dai) { + struct snd_soc_dapm_widget *w; + struct snd_sof_widget *swidget; + struct snd_sof_dai *sof_dai; + struct sdw_stream_runtime *sdw_stream; + + w = snd_soc_dai_get_widget(dai, dir); + if (!w || !w->dobj.private) + continue; + + swidget = w->dobj.private; + sof_dai = swidget->private; + if (!sof_dai || sof_dai->type != SOF_DAI_INTEL_ALH) + continue; + + sdw_stream = snd_soc_dai_get_stream(dai, dir); + if (!IS_ERR_OR_NULL(sdw_stream)) { + dev_dbg(sdev->dev, + "WoV-state: BE=%s target=%d ignore_suspend=%d->%d sdw_state=%d\n", + be->dai_link->name, sdev->system_suspend_target, + sdw_stream->ignore_suspend, ignore_suspend, sdw_stream->state); + sdw_stream->ignore_suspend = ignore_suspend; + } + } +} + +void hda_dsp_set_sdw_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + bool ignore_suspend) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + struct snd_soc_dpcm *be_link; + int dir = substream->stream; + + if (dir != SNDRV_PCM_STREAM_CAPTURE) + return; + + if (rtd->dai_link->no_pcm) { + hda_dsp_set_sdw_be_ignore_suspend(sdev, rtd, dir, ignore_suspend); + return; + } + + snd_soc_dpcm_mutex_assert_held(rtd); + + for_each_dpcm_be(rtd, dir, be_link) + hda_dsp_set_sdw_be_ignore_suspend(sdev, be_link->be, dir, ignore_suspend); +} + static int sdw_params_stream(struct device *dev, struct sdw_intel_stream_params_data *params_data) { @@ -416,6 +471,10 @@ static inline bool hda_dsp_sdw_check_mic_privacy_irq(struct snd_sof_dev *sdev) static inline void hda_dsp_sdw_process_mic_privacy(struct snd_sof_dev *sdev) { } +void hda_dsp_set_sdw_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + bool ignore_suspend) { } + #endif /* IS_ENABLED(CONFIG_SND_SOC_SOF_INTEL_SOUNDWIRE) */ /* pre fw run operations */ diff --git a/sound/soc/sof/intel/hda.h b/sound/soc/sof/intel/hda.h index bc11519427649f..1baed0b706b7da 100644 --- a/sound/soc/sof/intel/hda.h +++ b/sound/soc/sof/intel/hda.h @@ -680,6 +680,9 @@ int hda_dsp_stream_hw_free(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream); int hda_dsp_pcm_trigger(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream, int cmd); +void hda_dsp_set_sdw_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + bool ignore_suspend); snd_pcm_uframes_t hda_dsp_pcm_pointer(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream); int hda_dsp_pcm_ack(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream); diff --git a/sound/soc/sof/ops.h b/sound/soc/sof/ops.h index 19605de680a319..57014aadc849e9 100644 --- a/sound/soc/sof/ops.h +++ b/sound/soc/sof/ops.h @@ -544,6 +544,15 @@ snd_sof_pcm_platform_trigger(struct snd_sof_dev *sdev, return 0; } +static inline void +snd_sof_pcm_platform_set_sdw_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + bool ignore_suspend) +{ + if (sof_ops(sdev) && sof_ops(sdev)->pcm_set_sdw_ignore_suspend) + sof_ops(sdev)->pcm_set_sdw_ignore_suspend(sdev, substream, ignore_suspend); +} + /* Firmware loading. If the fw_filename argument is NULL, the firmware file set in the * default profile will be loaded. Otherwise, it must contain the full path to the file. */ diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 0161c0ae8bbdb4..110c0a953a3b11 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -259,6 +259,54 @@ static int sof_pcm_stream_free(struct snd_sof_dev *sdev, return err; } +void sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev) +{ + struct snd_soc_card *card; + struct snd_sof_pcm *spcm; + + if (!sdev->component || !sof_ops(sdev)->pcm_set_sdw_ignore_suspend) + return; + + card = sdev->component->card; + if (!card || !snd_soc_card_is_instantiated(card)) + return; + + snd_soc_dpcm_mutex_lock(card); + /* Clear every connected BE before applying the union of active WoV routes. */ + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_pcm_substream *substream = + spcm->stream[SNDRV_PCM_STREAM_CAPTURE].substream; + + if (substream) + snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, false); + } + + if (sdev->system_suspend_target != SOF_SUSPEND_S0IX) + goto out; + + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_sof_pcm_stream *stream = &spcm->stream[SNDRV_PCM_STREAM_CAPTURE]; + struct snd_pcm_substream *substream = stream->substream; + unsigned long flags; + + if (!substream || !stream->d0i3_compatible || + stream->dsp_max_burst_size_in_ms > 1) + continue; + + snd_pcm_stream_lock_irqsave(substream, flags); + if (substream->runtime && (snd_pcm_running(substream) || stream->suspend_ignored)) { + dev_dbg(sdev->dev, + "WoV-state: PM retain PCM=%s target=%d suspended=%d\n", + spcm->pcm.caps[SNDRV_PCM_STREAM_CAPTURE].name, + sdev->system_suspend_target, stream->suspend_ignored); + snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, true); + } + snd_pcm_stream_unlock_irqrestore(substream, flags); + } +out: + snd_soc_dpcm_mutex_unlock(card); +} + int sof_pcm_free_all_streams(struct snd_sof_dev *sdev) { struct snd_pcm_substream *substream; diff --git a/sound/soc/sof/pm.c b/sound/soc/sof/pm.c index b5d239b12e59d2..54ee21c32bd95b 100644 --- a/sound/soc/sof/pm.c +++ b/sound/soc/sof/pm.c @@ -388,10 +388,10 @@ int snd_sof_prepare(struct device *dev) */ if (sdev->fw_state == SOF_FW_CRASHED || sdev->fw_state == SOF_FW_BOOT_FAILED) - return 0; + goto update_sdw; if (!desc->use_acpi_target_states) - return 0; + goto update_sdw; #if defined(CONFIG_ACPI) switch (acpi_target_system_state()) { @@ -414,6 +414,10 @@ int snd_sof_prepare(struct device *dev) } #endif +update_sdw: + /* PRE-ordered capture backends receive SUSPEND before their frontends. */ + sof_pcm_update_sdw_ignore_suspend(sdev); + return 0; } EXPORT_SYMBOL(snd_sof_prepare); @@ -423,5 +427,7 @@ void snd_sof_complete(struct device *dev) struct snd_sof_dev *sdev = dev_get_drvdata(dev); sdev->system_suspend_target = SOF_SUSPEND_NONE; + /* Clear the completed or aborted suspend decision after controller resume. */ + sof_pcm_update_sdw_ignore_suspend(sdev); } EXPORT_SYMBOL(snd_sof_complete); diff --git a/sound/soc/sof/sof-audio.h b/sound/soc/sof/sof-audio.h index 10b41ab18b7990..2d19e119afe377 100644 --- a/sound/soc/sof/sof-audio.h +++ b/sound/soc/sof/sof-audio.h @@ -683,6 +683,7 @@ static inline void snd_sof_compr_init_elapsed_work(struct work_struct *work) { } int sof_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, struct snd_pcm_hw_params *params); /* PM */ +void sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev); bool snd_sof_stream_suspend_ignored(struct snd_sof_dev *sdev); bool snd_sof_dsp_only_d0i3_compatible_stream_active(struct snd_sof_dev *sdev); diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 83e4d83cc27cbb..3815ca1fd0d795 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -252,6 +252,10 @@ struct snd_sof_dsp_ops { int (*pcm_trigger)(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream, int cmd); /* optional */ + /* set connected SoundWire BE retention during system PM prepare/complete */ + void (*pcm_set_sdw_ignore_suspend)(struct snd_sof_dev *sdev, + struct snd_pcm_substream *substream, + bool ignore_suspend); /* optional */ /* host stream pointer */ snd_pcm_uframes_t (*pcm_pointer)(struct snd_sof_dev *sdev, From 711339b1c20cf8e9fb461a8aae404fbd56a7c84d Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Wed, 21 Oct 2026 11:26:14 +0800 Subject: [PATCH 5/9] ASoC: soc-component: export snd_soc_pcm_component_trigger() SOF needs to stop a frontend pipeline that was retained across S0ix when the system later transitions to S3. That frontend is already in the suspended state, so it will not receive a SUSPEND trigger, and SOF must issue the component STOP trigger directly. Export snd_soc_pcm_component_trigger() so it can be used from the SOF module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/soc-component.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/soc-component.c b/sound/soc/soc-component.c index 463ea227cac819..44fd2315b17443 100644 --- a/sound/soc/soc-component.c +++ b/sound/soc/soc-component.c @@ -1224,6 +1224,7 @@ int snd_soc_pcm_component_trigger(struct snd_pcm_substream *substream, return ret; } +EXPORT_SYMBOL_GPL(snd_soc_pcm_component_trigger); int snd_soc_pcm_component_pm_runtime_get(struct snd_soc_pcm_runtime *rtd, void *stream) From 213a9d8bb4e52fa28a32e05e6ac1bdf7c6d3b98d Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Fri, 2 Oct 2026 17:42:38 +0800 Subject: [PATCH 6/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. Track ownership per PCM stream so shared widgets remain marked until their final WoV user releases them. Refresh DAPM endpoint path caches after changing ignore_suspend, and release each stream's ownership on resume, hw_free, or close. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/sof/core.c | 8 + sound/soc/sof/pcm.c | 347 ++++++++++++++++++++++++++++++++++++-- sound/soc/sof/pm.c | 10 +- sound/soc/sof/sof-audio.h | 2 +- sound/soc/sof/sof-priv.h | 9 + 5 files changed, 355 insertions(+), 21 deletions(-) diff --git a/sound/soc/sof/core.c b/sound/soc/sof/core.c index 1f38fe03e49395..49520db269e0ca 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); @@ -751,6 +752,7 @@ int snd_sof_device_remove(struct device *dev) { struct snd_sof_dev *sdev = dev_get_drvdata(dev); struct snd_sof_pdata *pdata = sdev->pdata; + struct snd_sof_ignore_suspend_widget *isw, *tmp; int ret; bool aborted = false; @@ -770,6 +772,12 @@ int snd_sof_device_remove(struct device *dev) */ snd_sof_machine_unregister(sdev, pdata); + /* topology is unloaded by now, free any leftover ignore_suspend widget entries */ + list_for_each_entry_safe(isw, tmp, &sdev->ignore_suspend_widget_list, list) { + list_del(&isw->list); + kfree(isw); + } + /* * Balance the runtime pm usage count in case we are faced with an * exception and we forcably prevented D3 power state to preserve diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 110c0a953a3b11..8c0e049337cfe0 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -66,6 +66,163 @@ void snd_sof_pcm_period_elapsed(struct snd_pcm_substream *substream) } EXPORT_SYMBOL(snd_sof_pcm_period_elapsed); +static void sof_pcm_update_widget_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_sof_pcm_stream *stream, + struct snd_soc_dapm_widget *widget, + int *ret) +{ + struct snd_sof_ignore_suspend_widget *isw; + bool owned = false; + + list_for_each_entry(isw, &sdev->ignore_suspend_widget_list, list) { + if (isw->widget == widget) { + owned = true; + + if (isw->stream == stream) + return; + } + } + + /* Do not take ownership of a widget already held by another subsystem. */ + if (widget->ignore_suspend && !owned) + return; + + isw = kzalloc_obj(*isw); + if (!isw) { + *ret = -ENOMEM; + return; + } + + isw->widget = widget; + isw->stream = stream; + list_add_tail(&isw->list, &sdev->ignore_suspend_widget_list); + widget->ignore_suspend = 1; +} + +static void sof_pcm_release_widget_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_sof_pcm_stream *stream) +{ + struct snd_sof_ignore_suspend_widget *isw, *tmp; + + list_for_each_entry_safe(isw, tmp, &sdev->ignore_suspend_widget_list, list) { + struct snd_soc_dapm_widget *widget = isw->widget; + bool owned = false; + + if (isw->stream != stream) + continue; + + list_del(&isw->list); + kfree(isw); + + list_for_each_entry(isw, &sdev->ignore_suspend_widget_list, list) { + if (isw->widget == widget) { + owned = true; + break; + } + } + + if (!owned) + widget->ignore_suspend = 0; + } +} + +static void sof_pcm_refresh_dapm_paths(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *rtd, + struct snd_sof_pcm *spcm, int dir) +{ + struct snd_soc_dai *dai; + int j; + + for_each_rtd_cpu_dais(rtd, j, dai) { + struct snd_soc_dapm_widget_list *connected_widgets; + int connected; + + connected = snd_soc_dapm_dai_get_connected_widgets(dai, dir, + &connected_widgets, + NULL); + if (connected >= 0) { + snd_soc_dapm_dai_free_widgets(&connected_widgets); + } else { + dev_dbg(sdev->dev, + "failed to refresh D0i3-compatible PCM %s paths: %d\n", + spcm->pcm.caps[dir].name, connected); + } + } +} + +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 snd_sof_pcm_stream *stream = &spcm->stream[dir]; + struct snd_soc_dai *dai; + int j, ret = 0; + + if (!ignore_suspend) { + snd_soc_dapm_mutex_lock(rtd->card); + sof_pcm_release_widget_ignore_suspend(sdev, stream); + snd_soc_dapm_mutex_unlock(rtd->card); + stream->suspend_ignored = false; + sof_pcm_refresh_dapm_paths(sdev, rtd, spcm, dir); + return 0; + } + + 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; + + 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); + ret = connected; + break; + } + + snd_soc_dapm_mutex_lock(rtd->card); + widget = snd_soc_dai_get_widget(dai, dir); + if (widget && widget->is_ep) + sof_pcm_update_widget_ignore_suspend(sdev, stream, widget, &ret); + + for_each_dapm_widgets(connected_widgets, i, widget) { + if (ret) + break; + if (!widget->is_ep) + continue; + + dev_dbg(sdev->dev, + "WoV PCM %s ignore suspend for widget %s\n", + spcm->pcm.caps[dir].name, widget->name); + sof_pcm_update_widget_ignore_suspend(sdev, stream, widget, &ret); + if (ret) + break; + } + snd_soc_dapm_mutex_unlock(rtd->card); + snd_soc_dapm_dai_free_widgets(&connected_widgets); + if (ret) + break; + } + + if (ret) { + snd_soc_dapm_mutex_lock(rtd->card); + sof_pcm_release_widget_ignore_suspend(sdev, stream); + snd_soc_dapm_mutex_unlock(rtd->card); + stream->suspend_ignored = false; + } else { + stream->suspend_ignored = true; + } + + /* Recalculate endpoint caches using the updated ignore_suspend flags. */ + sof_pcm_refresh_dapm_paths(sdev, rtd, spcm, dir); + + 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, @@ -259,52 +416,184 @@ static int sof_pcm_stream_free(struct snd_sof_dev *sdev, return err; } -void sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev) +int sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev) { struct snd_soc_card *card; struct snd_sof_pcm *spcm; + int dir = SNDRV_PCM_STREAM_CAPTURE; + int ret = 0; - if (!sdev->component || !sof_ops(sdev)->pcm_set_sdw_ignore_suspend) - return; + if (!sdev->component) + return 0; card = sdev->component->card; if (!card || !snd_soc_card_is_instantiated(card)) - return; + return 0; snd_soc_dpcm_mutex_lock(card); /* Clear every connected BE before applying the union of active WoV routes. */ list_for_each_entry(spcm, &sdev->pcm_list, list) { struct snd_pcm_substream *substream = - spcm->stream[SNDRV_PCM_STREAM_CAPTURE].substream; + spcm->stream[dir].substream; if (substream) snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, false); } - if (sdev->system_suspend_target != SOF_SUSPEND_S0IX) + if (sdev->system_suspend_target == SOF_SUSPEND_NONE) { + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_sof_pcm_stream *stream = &spcm->stream[dir]; + struct snd_pcm_substream *substream = stream->substream; + struct snd_soc_pcm_runtime *rtd; + unsigned long flags; + bool running; + + if (!substream || !stream->suspend_ignored) + continue; + + /* + * A running PCM did not receive its suspend trigger, so + * discard retention acquired during prepare. Keep it for + * suspended PCMs until their normal resume/prepare path. + */ + snd_pcm_stream_lock_irqsave(substream, flags); + running = substream->runtime && snd_pcm_running(substream); + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (!running) + continue; + + rtd = snd_soc_substream_to_rtd(substream); + ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + if (ret) + break; + } goto out; + } + + if (sdev->system_suspend_target != SOF_SUSPEND_S0IX) { + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_sof_pcm_stream *stream = &spcm->stream[dir]; + struct snd_pcm_substream *substream = stream->substream; + struct snd_soc_pcm_runtime *rtd; + int runtime_update; + + if (!substream || !stream->suspend_ignored) + continue; + + rtd = snd_soc_substream_to_rtd(substream); + + /* + * A PCM already suspended during S0ix won't receive another + * SUSPEND trigger. Stop its FE pipeline and restart/stop its + * BE references so retained hardware is shut down before the + * bus powers off. + */ + runtime_update = rtd->dpcm[dir].runtime_update; + rtd->dpcm[dir].runtime_update = SND_SOC_DPCM_UPDATE_FE; + if (rtd->dai_link->trigger[dir] == SND_SOC_DPCM_TRIGGER_POST) + ret = snd_soc_pcm_component_trigger(substream, + SNDRV_PCM_TRIGGER_STOP, 0); + if (!ret) + ret = dpcm_be_dai_trigger(rtd, dir, SNDRV_PCM_TRIGGER_RESUME); + if (!ret) + ret = dpcm_be_dai_trigger(rtd, dir, SNDRV_PCM_TRIGGER_STOP); + if (!ret && + rtd->dai_link->trigger[dir] == SND_SOC_DPCM_TRIGGER_PRE) + ret = snd_soc_pcm_component_trigger(substream, + SNDRV_PCM_TRIGGER_STOP, 0); + rtd->dpcm[dir].runtime_update = runtime_update; + if (ret) + break; + + ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + if (ret) + break; + } + goto out; + } list_for_each_entry(spcm, &sdev->pcm_list, list) { - struct snd_sof_pcm_stream *stream = &spcm->stream[SNDRV_PCM_STREAM_CAPTURE]; + struct snd_sof_pcm_stream *stream = &spcm->stream[dir]; struct snd_pcm_substream *substream = stream->substream; + struct snd_soc_pcm_runtime *rtd; unsigned long flags; + bool active; + bool was_suspend_ignored; if (!substream || !stream->d0i3_compatible || stream->dsp_max_burst_size_in_ms > 1) continue; snd_pcm_stream_lock_irqsave(substream, flags); - if (substream->runtime && (snd_pcm_running(substream) || stream->suspend_ignored)) { - dev_dbg(sdev->dev, - "WoV-state: PM retain PCM=%s target=%d suspended=%d\n", - spcm->pcm.caps[SNDRV_PCM_STREAM_CAPTURE].name, - sdev->system_suspend_target, stream->suspend_ignored); - snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, true); + was_suspend_ignored = stream->suspend_ignored; + active = substream->runtime && + (snd_pcm_running(substream) || was_suspend_ignored); + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (!active) + continue; + + if (was_suspend_ignored) { + snd_pcm_stream_lock_irqsave(substream, flags); + active = substream->runtime && stream->suspend_ignored; + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (active) + snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, + substream, true); + continue; } + + dev_dbg(sdev->dev, + "WoV-state: PM retain PCM=%s target=%d suspended=%d\n", + spcm->pcm.caps[dir].name, + sdev->system_suspend_target, stream->suspend_ignored); + + rtd = snd_soc_substream_to_rtd(substream); + ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, true); + if (ret) + break; + + snd_pcm_stream_lock_irqsave(substream, flags); + active = substream->runtime && snd_pcm_running(substream); snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (!active) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + continue; + } + + snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, true); + } + + if (ret) { + /* Do not leave a partially retained path if suspend is aborted. */ + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_sof_pcm_stream *stream = &spcm->stream[dir]; + struct snd_pcm_substream *substream = stream->substream; + struct snd_soc_pcm_runtime *rtd; + unsigned long flags; + bool running; + + if (!substream) + continue; + + snd_pcm_stream_lock_irqsave(substream, flags); + running = substream->runtime && snd_pcm_running(substream); + snd_pcm_stream_unlock_irqrestore(substream, flags); + + if (running && stream->suspend_ignored) { + rtd = snd_soc_substream_to_rtd(substream); + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + } + + snd_sof_pcm_platform_set_sdw_ignore_suspend(sdev, substream, false); + } } out: snd_soc_dpcm_mutex_unlock(card); + return ret; } int sof_pcm_free_all_streams(struct snd_sof_dev *sdev) @@ -351,6 +640,8 @@ static int sof_pcm_hw_free(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: hw_free\n"); + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); ret = sof_pcm_stream_free(sdev, substream, spcm, substream->stream, true); /* unprepare and free the list of DAPM widgets */ @@ -384,6 +675,14 @@ 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); + if (spcm->prepared[substream->stream]) { if (!spcm->pending_stop[substream->stream]) return 0; @@ -469,7 +768,8 @@ static int sof_pcm_trigger(struct snd_soc_component *component, * not supported, no need to re-start streams that * remained enabled in D0ix. */ - spcm->stream[substream->stream].suspend_ignored = false; + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); return 0; } @@ -478,11 +778,12 @@ static int sof_pcm_trigger(struct snd_soc_component *component, break; case SNDRV_PCM_TRIGGER_RESUME: if (spcm->stream[substream->stream].suspend_ignored) { - spcm->stream[substream->stream].suspend_ignored = false; + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); return 0; } spcm_err(spcm, substream->stream, "RESUME trigger is not supported\n"); - return -EINVAL; + return -ENOSYS; case SNDRV_PCM_TRIGGER_SUSPEND: /* * Set the suspend_ignored flag for D0I3-compatible streams used @@ -496,7 +797,11 @@ 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) { - spcm->stream[substream->stream].suspend_ignored = true; + /* Set ignore_suspend to the DAPM widgets */ + ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, true); + if (ret < 0) + return ret; return 0; } /* On suspend the DMA must be stopped in DSPless mode */ @@ -505,6 +810,12 @@ static int sof_pcm_trigger(struct snd_soc_component *component, fallthrough; case SNDRV_PCM_TRIGGER_STOP: + if (cmd == SNDRV_PCM_TRIGGER_STOP && + substream->stream == SNDRV_PCM_STREAM_CAPTURE && + spcm->stream[substream->stream].suspend_ignored) + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); + ipc_first = true; if (pcm_ops && pcm_ops->reset_hw_params_during_stop) reset_hw_params = true; @@ -674,6 +985,8 @@ static int sof_pcm_close(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: close\n"); + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, substream->stream, false); + err = snd_sof_pcm_platform_close(sdev, substream); if (err < 0) { spcm_err(spcm, substream->stream, diff --git a/sound/soc/sof/pm.c b/sound/soc/sof/pm.c index 54ee21c32bd95b..75cdf464f330b1 100644 --- a/sound/soc/sof/pm.c +++ b/sound/soc/sof/pm.c @@ -378,6 +378,7 @@ int snd_sof_prepare(struct device *dev) { struct snd_sof_dev *sdev = dev_get_drvdata(dev); const struct sof_dev_desc *desc = sdev->pdata->desc; + int ret; /* will suspend to S3 by default */ sdev->system_suspend_target = SOF_SUSPEND_S3; @@ -416,9 +417,11 @@ int snd_sof_prepare(struct device *dev) update_sdw: /* PRE-ordered capture backends receive SUSPEND before their frontends. */ - sof_pcm_update_sdw_ignore_suspend(sdev); + ret = sof_pcm_update_sdw_ignore_suspend(sdev); + if (ret) + sdev->system_suspend_target = SOF_SUSPEND_NONE; - return 0; + return ret; } EXPORT_SYMBOL(snd_sof_prepare); @@ -428,6 +431,7 @@ void snd_sof_complete(struct device *dev) sdev->system_suspend_target = SOF_SUSPEND_NONE; /* Clear the completed or aborted suspend decision after controller resume. */ - sof_pcm_update_sdw_ignore_suspend(sdev); + if (sof_pcm_update_sdw_ignore_suspend(sdev) < 0) + dev_warn(sdev->dev, "failed to update WoV suspend state on system resume\n"); } EXPORT_SYMBOL(snd_sof_complete); diff --git a/sound/soc/sof/sof-audio.h b/sound/soc/sof/sof-audio.h index 2d19e119afe377..7e84fd74bf093c 100644 --- a/sound/soc/sof/sof-audio.h +++ b/sound/soc/sof/sof-audio.h @@ -683,7 +683,7 @@ static inline void snd_sof_compr_init_elapsed_work(struct work_struct *work) { } int sof_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, struct snd_pcm_hw_params *params); /* PM */ -void sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev); +int sof_pcm_update_sdw_ignore_suspend(struct snd_sof_dev *sdev); bool snd_sof_stream_suspend_ignored(struct snd_sof_dev *sdev); bool snd_sof_dsp_only_d0i3_compatible_stream_active(struct snd_sof_dev *sdev); diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 3815ca1fd0d795..620de872b00bb4 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -411,6 +411,13 @@ struct snd_sof_dfsentry { }; }; +/* DAPM widget whose ignore_suspend flag was set by a WoV PCM */ +struct snd_sof_ignore_suspend_widget { + struct snd_soc_dapm_widget *widget; + struct snd_sof_pcm_stream *stream; + 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; @@ -657,6 +664,8 @@ struct snd_sof_dev { struct list_head dai_list; struct list_head dai_link_list; struct list_head route_list; + /* DAPM widgets marked ignore_suspend by WoV PCMs */ + struct list_head ignore_suspend_widget_list; struct snd_soc_component *component; u32 enabled_cores_mask; /* keep track of enabled cores */ bool led_present; From c2bff20c2ad29ad6a64bdf91fc7714202f60d70f 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: Skip prepare for enabled SoundWire streams Multiple front-end streams may share a SoundWire back-end, so a prepare request can arrive after the shared stream is already enabled. Return success without calling sdw_prepare_stream() again in that case. 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 e3ee8f385952f0..31e649fada688f 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 1badcc9d66a8761db1ba7d7f1d30e7e85e9de1c2 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..629d4b9e324d08 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 223704ab2dae6b99c0d9e38773ebd49e33bbd012 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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/sof/intel/hda-dai.c | 48 +++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index e1193c9692f547..c617969c0d18df 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -282,6 +282,32 @@ static int __maybe_unused hda_dai_hw_params(struct snd_pcm_substream *substream, return hda_dai_hw_params_data(substream, params, dai, &data, flags); } +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); + struct snd_soc_dpcm *dpcm; + struct snd_sof_pcm *spcm; + + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + 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. + */ + 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)) + return true; + } + + return false; +} + /* * snd_sof_prepare() already selected the WoV PCMs to keep active in S0ix and * marked them suspend_ignored. Reuse that decision so the DSP and SoundWire @@ -318,6 +344,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) { @@ -329,6 +356,12 @@ 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) @@ -340,6 +373,12 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i return 0; } + if (cmd == SNDRV_PCM_TRIGGER_START && hda_stream->suspend_ignored) { + hda_stream->suspend_ignored = false; + if (hda_dai_has_wov_fe(sdev, substream)) + return 0; + } + /* * Keep the BE firmware pipeline running for WoV PCMs retained by * snd_sof_prepare(). The SoundWire ignore_suspend flag is managed by @@ -352,7 +391,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; @@ -364,7 +403,12 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i return ret; } - if (ops->post_trigger) { + /* + * IPC4 uses post_trigger to update local pipeline bookkeeping on STOP; + * IPC3's post_trigger sends IPC and must be skipped after a firmware crash. + */ + if (ops->post_trigger && + (!skip_fw_ipc || sdev->pdata->ipc_type == SOF_IPC_TYPE_4)) { ret = ops->post_trigger(sdev, dai, substream, cmd); if (ret < 0) return ret;