diff --git a/drivers/soundwire/cadence_master.h b/drivers/soundwire/cadence_master.h index f4e41a9ab16589..2cba93eea1a79a 100644 --- a/drivers/soundwire/cadence_master.h +++ b/drivers/soundwire/cadence_master.h @@ -89,6 +89,8 @@ struct sdw_cdns_stream_config { * @link_id: Master link id * @suspended: status set when suspended, to be used in .prepare * @paused: status set in .trigger, to be used in suspend + * @retention_revoked: stream retention was revoked in .prepare, to be restored if + * the system suspend is aborted * @direction: stream direction */ struct sdw_cdns_dai_runtime { @@ -100,6 +102,7 @@ struct sdw_cdns_dai_runtime { int link_id; bool suspended; bool paused; + bool retention_revoked; int direction; }; diff --git a/drivers/soundwire/intel.c b/drivers/soundwire/intel.c index af65214836b4d0..a159ca8d869a56 100644 --- a/drivers/soundwire/intel.c +++ b/drivers/soundwire/intel.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -952,11 +953,19 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) for_each_component_dais(component, dai) { struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + struct sdw_stream_runtime *stream; dai_runtime = cdns->dai_runtime_array[dai->id]; + if (!dai_runtime) + continue; - if (dai_runtime) - dai_runtime->suspended = true; + stream = dai_runtime->stream; + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + stream && stream->ignore_suspend && + stream->state == SDW_STREAM_ENABLED) + continue; + + dai_runtime->suspended = true; } return 0; diff --git a/drivers/soundwire/intel_ace2x.c b/drivers/soundwire/intel_ace2x.c index 642b33ab552606..39ffe722812aba 100644 --- a/drivers/soundwire/intel_ace2x.c +++ b/drivers/soundwire/intel_ace2x.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -951,11 +952,19 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) for_each_component_dais(component, dai) { struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + struct sdw_stream_runtime *stream; dai_runtime = cdns->dai_runtime_array[dai->id]; + if (!dai_runtime) + continue; - if (dai_runtime) - dai_runtime->suspended = true; + stream = dai_runtime->stream; + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + stream && stream->ignore_suspend && + stream->state == SDW_STREAM_ENABLED) + continue; + + dai_runtime->suspended = true; } return 0; diff --git a/drivers/soundwire/intel_auxdevice.c b/drivers/soundwire/intel_auxdevice.c index 5983fbc5725a26..ba937e03ca3f78 100644 --- a/drivers/soundwire/intel_auxdevice.c +++ b/drivers/soundwire/intel_auxdevice.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -576,13 +577,40 @@ int intel_resume_child_device(struct device *dev, void *data) return 0; } +/* Drop the revoke record, and re-enable the streams if the suspend was aborted. */ +static void intel_retention_finish(struct device *dev, struct sdw_cdns *cdns, bool restore) +{ + int i, ret; + + for (i = 0; i < cdns->pcm.num_pdi; i++) { + struct sdw_cdns_dai_runtime *dai_runtime = cdns->dai_runtime_array[i]; + struct sdw_stream_runtime *stream; + + if (!dai_runtime || !dai_runtime->retention_revoked) + continue; + + dai_runtime->retention_revoked = false; + stream = dai_runtime->stream; + if (!restore || !stream) + continue; + + ret = sdw_enable_stream(stream); + if (ret) { + dev_err(dev, "failed to re-enable retained stream %s: %d\n", + stream->name, ret); + continue; + } + stream->ignore_suspend = true; + } +} + static int __maybe_unused intel_pm_prepare(struct device *dev) { struct sdw_cdns *cdns = dev_get_drvdata(dev); struct sdw_intel *sdw = cdns_to_intel(cdns); struct sdw_bus *bus = &cdns->bus; u32 clock_stop_quirks; - int ret; + int i, ret; if (bus->prop.hw_disabled || !sdw->startup_done) { dev_dbg(dev, "SoundWire master %d is disabled or not-started, ignoring\n", @@ -633,9 +661,52 @@ static int __maybe_unused intel_pm_prepare(struct device *dev) dev_err(dev, "%s: intel_resume_child_device failed: %d\n", __func__, ret); } + /* + * Revoke retention from an earlier S0ix before any controller or codec + * suspends. An already-suspended PCM will not issue another trigger. + */ + if (pm_suspend_target_state != PM_SUSPEND_TO_IDLE) { + for (i = 0; i < cdns->pcm.num_pdi; i++) { + struct sdw_cdns_dai_runtime *dai_runtime = cdns->dai_runtime_array[i]; + struct sdw_stream_runtime *stream; + + if (!dai_runtime || !dai_runtime->stream) + continue; + + stream = dai_runtime->stream; + if (!stream->ignore_suspend) + continue; + + if (stream->state == SDW_STREAM_ENABLED) { + ret = sdw_disable_stream(stream); + if (ret) { + dev_err(dev, "failed to disable retained stream %s: %d\n", + stream->name, ret); + /* .complete is not called if .prepare fails */ + intel_retention_finish(dev, cdns, true); + return ret; + } + dai_runtime->retention_revoked = true; + } + stream->ignore_suspend = false; + } + } + return 0; } +static void __maybe_unused intel_pm_complete(struct device *dev) +{ + struct sdw_cdns *cdns = dev_get_drvdata(dev); + struct sdw_intel *sdw = cdns_to_intel(cdns); + + if (cdns->bus.prop.hw_disabled || !sdw->startup_done) + return; + + /* intel_suspend() drops the record, so it is still set only if the suspend was aborted */ + intel_retention_finish(dev, cdns, true); +} + static int __maybe_unused intel_suspend(struct device *dev) { struct sdw_cdns *cdns = dev_get_drvdata(dev); @@ -650,6 +721,17 @@ static int __maybe_unused intel_suspend(struct device *dev) return 0; } + /* The suspend goes ahead, the streams are restarted by the PCM RESUME trigger. */ + intel_retention_finish(dev, cdns, false); + + /* + * ignore_suspend is set by the PCM trigger for S0ix and may remain set + * if a later sleep transition does not issue another PCM trigger. + */ + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + sdw_bus_ignore_suspend(bus)) + return 0; + /* Prevent runtime PM from racing with the code below. */ pm_runtime_disable(dev); @@ -737,6 +819,13 @@ static int __maybe_unused intel_resume(struct device *dev) return 0; } + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + 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); @@ -859,6 +948,7 @@ static int __maybe_unused intel_resume_runtime(struct device *dev) static const struct dev_pm_ops intel_pm = { .prepare = intel_pm_prepare, + .complete = intel_pm_complete, SET_SYSTEM_SLEEP_PM_OPS(intel_suspend, intel_resume) SET_RUNTIME_PM_OPS(intel_suspend_runtime, intel_resume_runtime, NULL) }; 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); diff --git a/sound/soc/sdw_utils/soc_sdw_utils.c b/sound/soc/sdw_utils/soc_sdw_utils.c index 7a19de75f32b53..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"); @@ -1902,6 +1905,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 +1930,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; @@ -2005,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"); diff --git a/sound/soc/soc-dapm.c b/sound/soc/soc-dapm.c index 74be5e20949337..c28d9e1f68bf1a 100644 --- a/sound/soc/soc-dapm.c +++ b/sound/soc/soc-dapm.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include "soc-internal.h" @@ -1415,18 +1416,62 @@ static int dapm_new_dai_link(struct snd_soc_dapm_widget *w) * the ALSA card - when we are suspending the ALSA state for the card * is set to D3. */ +static bool dapm_be_has_active_ignore_suspend_fe(struct snd_soc_dapm_widget *widget) +{ + struct snd_soc_pcm_runtime *be; + struct snd_soc_dpcm *dpcm; + struct snd_soc_dai *be_dai, *fe_dai; + struct snd_soc_dapm_widget *fe_widget; + int i, j, stream; + + if (widget->id == snd_soc_dapm_dai_in) + stream = SNDRV_PCM_STREAM_PLAYBACK; + else if (widget->id == snd_soc_dapm_dai_out) + stream = SNDRV_PCM_STREAM_CAPTURE; + else + return false; + + /* DPCM client links are stable while the card is in system suspend. */ + for_each_card_rtds(widget->dapm->card, be) { + if (!be->dai_link->no_pcm) + continue; + + for_each_rtd_dais(be, i, be_dai) { + if (snd_soc_dai_get_widget(be_dai, stream) != widget) + continue; + + for_each_dpcm_fe(be, stream, dpcm) { + if (dpcm->state == SND_SOC_DPCM_LINK_STATE_FREE) + continue; + + for_each_rtd_cpu_dais(dpcm->fe, j, fe_dai) { + fe_widget = snd_soc_dai_get_widget(fe_dai, stream); + if (fe_widget && fe_widget->active && + fe_widget->ignore_suspend) + return true; + } + } + } + } + + return false; +} + static int dapm_suspend_check(struct snd_soc_dapm_widget *widget) { struct device *dev = snd_soc_dapm_to_dev(widget->dapm); int level = snd_power_get_state(widget->dapm->card->snd_card); + bool ignore_suspend; switch (level) { case SNDRV_CTL_POWER_D3hot: case SNDRV_CTL_POWER_D3cold: - if (widget->ignore_suspend) + ignore_suspend = widget->ignore_suspend || + dapm_be_has_active_ignore_suspend_fe(widget); + if (ignore_suspend) dev_dbg(dev, "ASoC: %s ignoring suspend\n", widget->name); - return widget->ignore_suspend; + return ignore_suspend; default: return 1; } diff --git a/sound/soc/sof/intel/hda-common-ops.c b/sound/soc/sof/intel/hda-common-ops.c index aa91f2b6fd657b..b39ed71582cf7f 100644 --- a/sound/soc/sof/intel/hda-common-ops.c +++ b/sound/soc/sof/intel/hda-common-ops.c @@ -103,6 +103,7 @@ const struct snd_sof_dsp_ops sof_hda_common_ops = { .runtime_resume = hda_dsp_runtime_resume, .runtime_idle = hda_dsp_runtime_idle, .set_hw_params_upon_resume = hda_dsp_set_hw_params_upon_resume, + .release_retained_streams = hda_dsp_release_retained_streams, /* ALSA HW info flags */ .hw_info = SNDRV_PCM_INFO_MMAP | 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 7a8875a885ab34..1fc578e683e93f 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -282,32 +282,73 @@ static int __maybe_unused hda_dai_hw_params(struct snd_pcm_substream *substream, return hda_dai_hw_params_data(substream, params, dai, &data, flags); } +/* + * The FE retention flag is set by the FE trigger, which may run after the BE trigger, so + * it is only used for an FE which is already suspended. A WoV FE which is retained is + * either running (its SUSPEND is being handled) or already suspended with + * suspend_ignored set and its PCM still prepared. PREPARED or PAUSED PCMs are also + * marked SUSPENDED without a SUSPEND trigger, so suspend_ignored is required for them. + * Its DPCM state is not updated until the trigger completes. + */ +static bool hda_dai_fe_retains(struct snd_sof_dev *sdev, struct snd_soc_pcm_runtime *fe) +{ + const int dir = SNDRV_PCM_STREAM_CAPTURE; + struct snd_pcm_substream *fe_substream; + struct snd_sof_pcm *spcm; + + spcm = snd_sof_find_spcm_dai(sdev->component, fe); + if (!spcm || !spcm->stream[dir].d0i3_compatible || + spcm->stream[dir].dsp_max_burst_size_in_ms > 1) + return false; + + if (fe->dpcm[dir].state == SND_SOC_DPCM_STATE_START) + return true; + + fe_substream = snd_soc_dpcm_get_substream(fe, dir); + + return spcm->stream[dir].suspend_ignored && spcm->prepared[dir] && + 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); 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. - */ + /* An idle WoV FE may still be connected to the BE, only retain for an active one. */ 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 (hda_dai_fe_retains(sdev, dpcm->fe)) return true; } 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). @@ -319,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) { @@ -330,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; } @@ -353,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; @@ -691,6 +740,17 @@ 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 || + cmd == SNDRV_PCM_TRIGGER_START || cmd == SNDRV_PCM_TRIGGER_RESUME) + 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"); @@ -752,6 +812,60 @@ static int hda_dai_suspend(struct hdac_bus *bus) return 0; } +/* + * A retained WoV BE gets no SUSPEND trigger when its PCM stays suspended into a + * D3 transition, so run the SUSPEND sequence here, before its pipeline is freed. + */ +static int hda_dai_release_retained_streams(struct snd_sof_dev *sdev) +{ + struct hdac_bus *bus = sof_to_bus(sdev); + bool skip_fw_ipc = sdev->fw_state != SOF_FW_BOOT_COMPLETE; + struct hdac_stream *s; + int ret; + + list_for_each_entry(s, &bus->stream_list, list) { + struct hdac_ext_stream *hext_stream = stream_to_hdac_ext_stream(s); + const struct hda_dai_widget_dma_ops *ops; + struct sof_intel_hda_stream *hda_stream; + struct snd_pcm_substream *substream; + struct snd_soc_dai *cpu_dai; + + hda_stream = hstream_to_sof_hda_stream(hext_stream); + substream = hext_stream->link_substream; + if (!hda_stream->suspend_ignored || !substream) + continue; + + cpu_dai = snd_soc_rtd_to_cpu(snd_soc_substream_to_rtd(substream), 0); + ops = hda_dai_get_ops(substream, cpu_dai); + if (!ops) + return -EINVAL; + + if (ops->pre_trigger && !skip_fw_ipc) { + ret = ops->pre_trigger(sdev, cpu_dai, substream, SNDRV_PCM_TRIGGER_SUSPEND); + if (ret < 0) + return ret; + } + + if (ops->trigger) { + ret = ops->trigger(sdev, cpu_dai, substream, SNDRV_PCM_TRIGGER_SUSPEND); + if (ret < 0) + return ret; + } + + if (ops->post_trigger) { + ret = ops->post_trigger(sdev, cpu_dai, substream, SNDRV_PCM_TRIGGER_SUSPEND); + if (ret < 0) + return ret; + } + + ret = hda_link_dma_cleanup(substream, hext_stream, cpu_dai, true); + if (ret < 0) + return ret; + } + + return 0; +} + static void ssp_set_dai_drv_ops(struct snd_sof_dev *sdev, struct snd_sof_dsp_ops *ops) { const struct sof_intel_dsp_desc *chip; @@ -1025,3 +1139,12 @@ int hda_dsp_dais_suspend(struct snd_sof_dev *sdev) return 0; } + +int hda_dsp_release_retained_streams(struct snd_sof_dev *sdev) +{ +#if IS_ENABLED(CONFIG_SND_SOC_SOF_HDA_LINK) + return hda_dai_release_retained_streams(sdev); +#else + return 0; +#endif +} diff --git a/sound/soc/sof/intel/hda-dsp.c b/sound/soc/sof/intel/hda-dsp.c index ec49aeaa9bd7af..43a18ba4f6b785 100644 --- a/sound/soc/sof/intel/hda-dsp.c +++ b/sound/soc/sof/intel/hda-dsp.c @@ -852,7 +852,6 @@ static int hda_suspend(struct snd_sof_dev *sdev, bool runtime_suspend) hda_dsp_ctrl_ppcap_enable(sdev, false); hda_dsp_ctrl_ppcap_int_enable(sdev, false); skip_dsp: - /* disable hda bus irq and streams */ hda_dsp_ctrl_stop_chip(sdev); diff --git a/sound/soc/sof/intel/hda.h b/sound/soc/sof/intel/hda.h index bc11519427649f..b3ef9aa68d2d74 100644 --- a/sound/soc/sof/intel/hda.h +++ b/sound/soc/sof/intel/hda.h @@ -956,6 +956,7 @@ int hda_data_stream_cleanup(struct device *dev, struct snd_dma_buffer *dmab, /* common dai driver */ extern struct snd_soc_dai_driver skl_dai[]; int hda_dsp_dais_suspend(struct snd_sof_dev *sdev); +int hda_dsp_release_retained_streams(struct snd_sof_dev *sdev); /* * Platform Specific HW abstraction Ops. diff --git a/sound/soc/sof/ops.h b/sound/soc/sof/ops.h index 19605de680a319..d3f3c46b14d3ca 100644 --- a/sound/soc/sof/ops.h +++ b/sound/soc/sof/ops.h @@ -276,6 +276,13 @@ static inline int snd_sof_dsp_hw_params_upon_resume(struct snd_sof_dev *sdev) return 0; } +static inline int snd_sof_dsp_release_retained_streams(struct snd_sof_dev *sdev) +{ + if (sof_ops(sdev)->release_retained_streams) + return sof_ops(sdev)->release_retained_streams(sdev); + return 0; +} + static inline int snd_sof_dsp_set_clk(struct snd_sof_dev *sdev, u32 freq) { if (sof_ops(sdev)->set_clk) diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 0161c0ae8bbdb4..1c3ee8511b5e62 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -66,6 +66,37 @@ void snd_sof_pcm_period_elapsed(struct snd_pcm_substream *substream) } EXPORT_SYMBOL(snd_sof_pcm_period_elapsed); +static int sof_pcm_set_dai_ignore_suspend(struct snd_soc_pcm_runtime *rtd, int dir) +{ + struct snd_soc_dai *dai = snd_soc_rtd_to_cpu(rtd, 0); + struct snd_soc_dapm_widget *widget = snd_soc_dai_get_widget(dai, dir); + struct snd_soc_dapm_widget_list *list; + int i, ret; + + if (!widget) + return -ENODEV; + + ret = snd_soc_dapm_dai_get_connected_widgets(dai, dir, &list, NULL); + if (ret < 0) + return ret; + + ret = -ENODEV; + snd_soc_dapm_mutex_lock(rtd->card); + for (i = 0; i < list->num_widgets; i++) { + if (list->widgets[i]->is_ep & SND_SOC_DAPM_EP_SOURCE) { + list->widgets[i]->ignore_suspend = 1; + ret = 0; + } + } + if (!ret) + widget->ignore_suspend = 1; + snd_soc_dapm_mutex_unlock(rtd->card); + + snd_soc_dapm_dai_free_widgets(&list); + + 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, @@ -216,6 +247,19 @@ static int sof_pcm_stream_free(struct snd_sof_dev *sdev, int err = 0; if (spcm->prepared[substream->stream]) { + if (spcm->stream[dir].suspend_ignored && pcm_ops && pcm_ops->trigger) { + ret = pcm_ops->trigger(sdev->component, substream, spcm, + SNDRV_PCM_TRIGGER_STOP, dir); + if (ret < 0) { + spcm_err(spcm, substream->stream, + "retained stream stop failed %d\n", ret); + err = ret; + } + if (!pcm_ops->platform_stop_during_hw_free) + snd_sof_pcm_platform_trigger(sdev, substream, + SNDRV_PCM_TRIGGER_STOP); + } + /* stop DMA first if needed */ if (pcm_ops && pcm_ops->platform_stop_during_hw_free) snd_sof_pcm_platform_trigger(sdev, substream, @@ -236,6 +280,8 @@ static int sof_pcm_stream_free(struct snd_sof_dev *sdev, spcm->pending_stop[substream->stream] = false; } + spcm->stream[dir].suspend_ignored = false; + /* reset the DMA */ ret = snd_sof_pcm_platform_hw_free(sdev, substream); if (ret < 0) { @@ -269,8 +315,7 @@ int sof_pcm_free_all_streams(struct snd_sof_dev *sdev) for_each_pcm_streams(dir) { substream = spcm->stream[dir].substream; - if (!substream || !substream->runtime || - spcm->stream[dir].suspend_ignored) + if (!substream || !substream->runtime) continue; if (spcm->stream[dir].list) { @@ -337,12 +382,12 @@ static int sof_pcm_prepare(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: prepare\n"); if (spcm->prepared[substream->stream]) { + /* a retained stream is still running, nothing to rebuild */ if (!spcm->pending_stop[substream->stream]) return 0; /* - * this case should be reached in case of xruns where we absolutely - * want to free-up and reset all PCM/DMA resources + * Reset PCM/DMA resources after an xrun. */ ret = sof_pcm_stream_free(sdev, substream, spcm, substream->stream, true); if (ret < 0) @@ -448,6 +493,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) { + ret = sof_pcm_set_dai_ignore_suspend(rtd, substream->stream); + if (ret < 0) + spcm_err(spcm, substream->stream, + "failed to retain DAPM widgets for WoV: %d\n", ret); + spcm->stream[substream->stream].suspend_ignored = true; return 0; } @@ -499,6 +549,10 @@ static int sof_pcm_trigger(struct snd_soc_component *component, break; } + if (!ret && (cmd == SNDRV_PCM_TRIGGER_STOP || + cmd == SNDRV_PCM_TRIGGER_PAUSE_PUSH)) + spcm->stream[substream->stream].suspend_ignored = false; + /* free PCM if reset_hw_params is set and the STOP IPC is successful */ if (!ret && reset_hw_params) ret = sof_pcm_stream_free(sdev, substream, spcm, substream->stream, false); @@ -636,6 +690,7 @@ static int sof_pcm_close(struct snd_soc_component *component, */ } + spcm->stream[substream->stream].suspend_ignored = false; spcm->stream[substream->stream].substream = NULL; return 0; diff --git a/sound/soc/sof/pm.c b/sound/soc/sof/pm.c index b5d239b12e59d2..30f817a411f9da 100644 --- a/sound/soc/sof/pm.c +++ b/sound/soc/sof/pm.c @@ -243,6 +243,15 @@ static int sof_suspend(struct device *dev, bool runtime_suspend) if (runtime_suspend && !sof_ops(sdev)->runtime_suspend) return 0; + /* A retained stream must be stopped while its pipelines still exist. */ + if (!runtime_suspend && old_state == SOF_DSP_PM_D0 && target_state != SOF_DSP_PM_D0) { + ret = snd_sof_dsp_release_retained_streams(sdev); + if (ret < 0) { + dev_err(sdev->dev, "error: releasing retained streams %d\n", ret); + return ret; + } + } + /* we need to tear down pipelines only if the DSP hardware is * active, which happens for PCI devices. if the device is * suspended, it is brought back to full power and then diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 83e4d83cc27cbb..1e3d13c469168f 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -319,6 +319,7 @@ struct snd_sof_dsp_ops { int (*runtime_resume)(struct snd_sof_dev *sof_dev); /* optional */ int (*runtime_idle)(struct snd_sof_dev *sof_dev); /* optional */ int (*set_hw_params_upon_resume)(struct snd_sof_dev *sdev); /* optional */ + int (*release_retained_streams)(struct snd_sof_dev *sdev); /* optional */ int (*set_power_state)(struct snd_sof_dev *sdev, const struct sof_dsp_power_state *target_state); /* optional */