static int cnl_ipc_send_msg(struct snd_sof_dev *sdev,
                            struct snd_sof_ipc_msg *msg)
 {
+       struct sof_intel_hda_dev *hdev = sdev->pdata->hw_pdata;
+       struct sof_ipc_cmd_hdr *hdr;
        u32 dr = 0;
        u32 dd = 0;
 
+       /*
+        * Currently the only compact IPC supported is the PM_GATE
+        * IPC which is used for transitioning the DSP between the
+        * D0I0 and D0I3 states. And these are sent only during the
+        * set_power_state() op. Therefore, there will never be a case
+        * that a compact IPC results in the DSP exiting D0I3 without
+        * the host and FW being in sync.
+        */
        if (cnl_compact_ipc_compress(msg, &dr, &dd)) {
                /* send the message via IPC registers */
                snd_sof_dsp_write(sdev, HDA_DSP_BAR, CNL_DSP_REG_HIPCIDD,
                                  dd);
                snd_sof_dsp_write(sdev, HDA_DSP_BAR, CNL_DSP_REG_HIPCIDR,
                                  CNL_DSP_REG_HIPCIDR_BUSY | dr);
-       } else {
-               /* send the message via mailbox */
-               sof_mailbox_write(sdev, sdev->host_box.offset, msg->msg_data,
-                                 msg->msg_size);
-               snd_sof_dsp_write(sdev, HDA_DSP_BAR, CNL_DSP_REG_HIPCIDR,
-                                 CNL_DSP_REG_HIPCIDR_BUSY);
+               return 0;
        }
 
+       /* send the message via mailbox */
+       sof_mailbox_write(sdev, sdev->host_box.offset, msg->msg_data,
+                         msg->msg_size);
+       snd_sof_dsp_write(sdev, HDA_DSP_BAR, CNL_DSP_REG_HIPCIDR,
+                         CNL_DSP_REG_HIPCIDR_BUSY);
+
+       hdr = msg->msg_data;
+
+       /*
+        * Use mod_delayed_work() to schedule the delayed work
+        * to avoid scheduling multiple workqueue items when
+        * IPCs are sent at a high-rate. mod_delayed_work()
+        * modifies the timer if the work is pending.
+        * Also, a new delayed work should not be queued after the
+        * the CTX_SAVE IPC, which is sent before the DSP enters D3.
+        */
+       if (hdr->cmd != (SOF_IPC_GLB_PM_MSG | SOF_IPC_PM_CTX_SAVE))
+               mod_delayed_work(system_wq, &hdev->d0i3_work,
+                                msecs_to_jiffies(SOF_HDA_D0I3_WORK_DELAY_MS));
+
        return 0;
 }
 
 
 
 #include <sound/hdaudio_ext.h>
 #include <sound/hda_register.h>
+#include "../sof-audio.h"
 #include "../ops.h"
 #include "hda.h"
 #include "hda-ipc.h"
        pm_gate.flags = flags;
 
        /* send pm_gate ipc to dsp */
-       return sof_ipc_tx_message(sdev->ipc, pm_gate.hdr.cmd, &pm_gate,
-                                 sizeof(pm_gate), &reply, sizeof(reply));
+       return sof_ipc_tx_message_no_pm(sdev->ipc, pm_gate.hdr.cmd,
+                                       &pm_gate, sizeof(pm_gate), &reply,
+                                       sizeof(reply));
 }
 
 static int hda_dsp_update_d0i3c_register(struct snd_sof_dev *sdev, u8 value)
        };
        int ret;
 
+       /* cancel any attempt for DSP D0I3 */
+       cancel_delayed_work_sync(&hda->d0i3_work);
+
        if (target_state == SOF_DSP_PM_D0) {
                /* Set DSP power state */
                ret = hda_dsp_set_power_state(sdev, &target_dsp_state);
 #endif
        return 0;
 }
+
+void hda_dsp_d0i3_work(struct work_struct *work)
+{
+       struct sof_intel_hda_dev *hdev = container_of(work,
+                                                     struct sof_intel_hda_dev,
+                                                     d0i3_work.work);
+       struct hdac_bus *bus = &hdev->hbus.core;
+       struct snd_sof_dev *sdev = dev_get_drvdata(bus->dev);
+       struct sof_dsp_power_state target_state;
+       int ret;
+
+       target_state.state = SOF_DSP_PM_D0;
+
+       /* DSP can enter D0I3 iff only D0I3-compatible streams are active */
+       if (snd_sof_dsp_only_d0i3_compatible_stream_active(sdev))
+               target_state.substate = SOF_HDA_DSP_PM_D0I3;
+       else
+               target_state.substate = SOF_HDA_DSP_PM_D0I0;
+
+       /* remain in D0I0 */
+       if (target_state.substate == SOF_HDA_DSP_PM_D0I0)
+               return;
+
+       /* This can fail but error cannot be propagated */
+       ret = hda_dsp_set_power_state(sdev, &target_state);
+       if (ret < 0)
+               dev_err_ratelimited(sdev->dev,
+                                   "error: failed to set DSP state %d substate %d\n",
+                                   target_state.state, target_state.substate);
+}
 
        /* set default mailbox offset for FW ready message */
        sdev->dsp_box.offset = HDA_DSP_MBOX_UPLINK_OFFSET;
 
+       INIT_DELAYED_WORK(&hdev->d0i3_work, hda_dsp_d0i3_work);
+
        return 0;
 
 free_ipc_irq:
        struct pci_dev *pci = to_pci_dev(sdev->dev);
        const struct sof_intel_dsp_desc *chip = hda->desc;
 
+       /* cancel any attempt for DSP D0I3 */
+       cancel_delayed_work_sync(&hda->d0i3_work);
+
 #if IS_ENABLED(CONFIG_SND_SOC_SOF_HDA)
        /* codec removal, invoke bus_device_remove */
        snd_hdac_ext_bus_device_remove(bus);
 
 #define SOF_HDA_PLAYBACK               0
 #define SOF_HDA_CAPTURE                        1
 
+/*
+ * Time in ms for opportunistic D0I3 entry delay.
+ * This has been deliberately chosen to be long to avoid race conditions.
+ * Could be optimized in future.
+ */
+#define SOF_HDA_D0I3_WORK_DELAY_MS     5000
+
 /* HDA DSP D0 substate */
 enum sof_hda_D0_substate {
        SOF_HDA_DSP_PM_D0I0,    /* default D0 substate */
 
        /* DMIC device */
        struct platform_device *dmic_dev;
+
+       /* delayed work to enter D0I3 opportunistically */
+       struct delayed_work d0i3_work;
 };
 
 static inline struct hdac_bus *sof_to_bus(struct snd_sof_dev *s)
 void hda_dsp_dump(struct snd_sof_dev *sdev, u32 flags);
 void hda_ipc_dump(struct snd_sof_dev *sdev);
 void hda_ipc_irq_dump(struct snd_sof_dev *sdev);
+void hda_dsp_d0i3_work(struct work_struct *work);
 
 /*
  * DSP PCM Operations.
 
        spin_unlock_irq(&sdev->ipc_lock);
 
        if (ret < 0) {
-               /* So far IPC TX never fails, consider making the above void */
                dev_err_ratelimited(sdev->dev,
                                    "error: ipc tx failed with error %d\n",
                                    ret);
 int sof_ipc_tx_message(struct snd_sof_ipc *ipc, u32 header,
                       void *msg_data, size_t msg_bytes, void *reply_data,
                       size_t reply_bytes)
+{
+       const struct sof_dsp_power_state target_state = {
+               .state = SOF_DSP_PM_D0,
+       };
+       int ret;
+
+       /* ensure the DSP is in D0 before sending a new IPC */
+       ret = snd_sof_dsp_set_power_state(ipc->sdev, &target_state);
+       if (ret < 0) {
+               dev_err(ipc->sdev->dev, "error: resuming DSP %d\n", ret);
+               return ret;
+       }
+
+       return sof_ipc_tx_message_no_pm(ipc, header, msg_data, msg_bytes,
+                                       reply_data, reply_bytes);
+}
+EXPORT_SYMBOL(sof_ipc_tx_message);
+
+/*
+ * send IPC message from host to DSP without modifying the DSP state.
+ * This will be used for IPC's that can be handled by the DSP
+ * even in a low-power D0 substate.
+ */
+int sof_ipc_tx_message_no_pm(struct snd_sof_ipc *ipc, u32 header,
+                            void *msg_data, size_t msg_bytes,
+                            void *reply_data, size_t reply_bytes)
 {
        int ret;
 
 
        return ret;
 }
-EXPORT_SYMBOL(sof_ipc_tx_message);
+EXPORT_SYMBOL(sof_ipc_tx_message_no_pm);
 
 /* handle reply message from DSP */
 int snd_sof_ipc_reply(struct snd_sof_dev *sdev, u32 msg_id)
 
 int sof_ipc_tx_message(struct snd_sof_ipc *ipc, u32 header,
                       void *msg_data, size_t msg_bytes, void *reply_data,
                       size_t reply_bytes);
+int sof_ipc_tx_message_no_pm(struct snd_sof_ipc *ipc, u32 header,
+                            void *msg_data, size_t msg_bytes,
+                            void *reply_data, size_t reply_bytes);
 
 /*
  * Trace/debug