| // SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) | |
| // | |
| // This file is provided under a dual BSD/GPLv2 license. When using or | |
| // redistributing this file, you may do so under either license. | |
| // | |
| // Copyright(c) 2022 Intel Corporation | |
| // | |
| #include <sound/pcm_params.h> | |
| #include <sound/sof/ipc4/header.h> | |
| #include "sof-audio.h" | |
| #include "sof-priv.h" | |
| #include "ops.h" | |
| #include "ipc4-priv.h" | |
| #include "ipc4-topology.h" | |
| #include "ipc4-fw-reg.h" | |
| /** | |
| * struct sof_ipc4_timestamp_info - IPC4 timestamp info | |
| * @host_copier: the host copier of the pcm stream | |
| * @dai_copier: the dai copier of the pcm stream | |
| * @stream_start_offset: reported by fw in memory window (converted to | |
| * frames at host_copier sampling rate) | |
| * @stream_end_offset: reported by fw in memory window (converted to | |
| * frames at host_copier sampling rate) | |
| * @llp_offset: llp offset in memory window | |
| * @delay: Calculated and stored in pointer callback. The stored value is | |
| * returned in the delay callback. Expressed in frames at host copier | |
| * sampling rate. | |
| */ | |
| struct sof_ipc4_timestamp_info { | |
| struct sof_ipc4_copier *host_copier; | |
| struct sof_ipc4_copier *dai_copier; | |
| u64 stream_start_offset; | |
| u64 stream_end_offset; | |
| u32 llp_offset; | |
| snd_pcm_sframes_t delay; | |
| }; | |
| /** | |
| * struct sof_ipc4_pcm_stream_priv - IPC4 specific private data | |
| * @time_info: pointer to time info struct if it is supported, otherwise NULL | |
| * @chain_dma_allocated: indicates the ChainDMA allocation state | |
| */ | |
| struct sof_ipc4_pcm_stream_priv { | |
| struct sof_ipc4_timestamp_info *time_info; | |
| bool chain_dma_allocated; | |
| }; | |
| /* | |
| * Modulus to use to compare host and link position counters. The sampling | |
| * rates may be different, so the raw hardware counters will wrap | |
| * around at different times. To calculate differences, use | |
| * DELAY_BOUNDARY as a common modulus. This value must be smaller than | |
| * the wrap-around point of any hardware counter, and larger than any | |
| * valid delay measurement. | |
| */ | |
| #define DELAY_BOUNDARY U32_MAX | |
| #define DELAY_MAX (DELAY_BOUNDARY >> 1) | |
| static inline struct sof_ipc4_timestamp_info * | |
| sof_ipc4_sps_to_time_info(struct snd_sof_pcm_stream *sps) | |
| { | |
| struct sof_ipc4_pcm_stream_priv *stream_priv = sps->private; | |
| return stream_priv->time_info; | |
| } | |
| static | |
| char *sof_ipc4_set_multi_pipeline_state_debug(struct snd_sof_dev *sdev, char *buf, size_t size, | |
| struct ipc4_pipeline_set_state_data *trigger_list) | |
| { | |
| int i, offset = 0; | |
| for (i = 0; i < trigger_list->count; i++) { | |
| offset += snprintf(buf + offset, size - offset, " %d", | |
| trigger_list->pipeline_instance_ids[i]); | |
| if (offset >= size - 1) { | |
| buf[size - 1] = '\0'; | |
| break; | |
| } | |
| } | |
| return buf; | |
| } | |
| static int sof_ipc4_set_multi_pipeline_state(struct snd_sof_dev *sdev, u32 state, | |
| struct ipc4_pipeline_set_state_data *trigger_list) | |
| { | |
| struct sof_ipc4_msg msg = {{ 0 }}; | |
| u32 primary, ipc_size; | |
| char debug_buf[32]; | |
| /* trigger a single pipeline */ | |
| if (trigger_list->count == 1) | |
| return sof_ipc4_set_pipeline_state(sdev, trigger_list->pipeline_instance_ids[0], | |
| state); | |
| dev_dbg(sdev->dev, "Set pipelines %s to state %d%s", | |
| sof_ipc4_set_multi_pipeline_state_debug(sdev, debug_buf, sizeof(debug_buf), | |
| trigger_list), | |
| state, sof_ipc4_pipeline_state_str(state)); | |
| primary = state; | |
| primary |= SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_SET_PIPELINE_STATE); | |
| primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); | |
| primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); | |
| msg.primary = primary; | |
| /* trigger multiple pipelines with a single IPC */ | |
| msg.extension = SOF_IPC4_GLB_PIPE_STATE_EXT_MULTI; | |
| /* ipc_size includes the count and the pipeline IDs for the number of pipelines */ | |
| ipc_size = sizeof(u32) * (trigger_list->count + 1); | |
| msg.data_size = ipc_size; | |
| msg.data_ptr = trigger_list; | |
| return sof_ipc_tx_message_no_reply(sdev->ipc, &msg, ipc_size); | |
| } | |
| int sof_ipc4_set_pipeline_state(struct snd_sof_dev *sdev, u32 instance_id, u32 state) | |
| { | |
| struct sof_ipc4_msg msg = {{ 0 }}; | |
| u32 primary; | |
| dev_dbg(sdev->dev, "Set pipeline %d to state %d%s", instance_id, state, | |
| sof_ipc4_pipeline_state_str(state)); | |
| primary = state; | |
| primary |= SOF_IPC4_GLB_PIPE_STATE_ID(instance_id); | |
| primary |= SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_SET_PIPELINE_STATE); | |
| primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); | |
| primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); | |
| msg.primary = primary; | |
| return sof_ipc_tx_message_no_reply(sdev->ipc, &msg, 0); | |
| } | |
| EXPORT_SYMBOL(sof_ipc4_set_pipeline_state); | |
| static void sof_ipc4_add_pipeline_by_priority(struct ipc4_pipeline_set_state_data *trigger_list, | |
| struct snd_sof_widget *pipe_widget, | |
| s8 *pipe_priority, bool ascend) | |
| { | |
| struct sof_ipc4_pipeline *pipeline = pipe_widget->private; | |
| int i, j; | |
| for (i = 0; i < trigger_list->count; i++) { | |
| /* add pipeline from low priority to high */ | |
| if (ascend && pipeline->priority < pipe_priority[i]) | |
| break; | |
| /* add pipeline from high priority to low */ | |
| else if (!ascend && pipeline->priority > pipe_priority[i]) | |
| break; | |
| } | |
| for (j = trigger_list->count - 1; j >= i; j--) { | |
| trigger_list->pipeline_instance_ids[j + 1] = trigger_list->pipeline_instance_ids[j]; | |
| pipe_priority[j + 1] = pipe_priority[j]; | |
| } | |
| trigger_list->pipeline_instance_ids[i] = pipe_widget->instance_id; | |
| trigger_list->count++; | |
| pipe_priority[i] = pipeline->priority; | |
| } | |
| static void | |
| sof_ipc4_add_pipeline_to_trigger_list(struct snd_sof_dev *sdev, int state, | |
| struct snd_sof_pipeline *spipe, | |
| struct ipc4_pipeline_set_state_data *trigger_list, | |
| s8 *pipe_priority) | |
| { | |
| struct snd_sof_widget *pipe_widget = spipe->pipe_widget; | |
| struct sof_ipc4_pipeline *pipeline = pipe_widget->private; | |
| if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) | |
| return; | |
| switch (state) { | |
| case SOF_IPC4_PIPE_RUNNING: | |
| /* | |
| * Trigger pipeline if all PCMs containing it are paused or if it is RUNNING | |
| * for the first time | |
| */ | |
| if (spipe->started_count == spipe->paused_count) | |
| sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, | |
| false); | |
| break; | |
| case SOF_IPC4_PIPE_RESET: | |
| /* RESET if the pipeline is neither running nor paused */ | |
| if (!spipe->started_count && !spipe->paused_count) | |
| sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, | |
| true); | |
| break; | |
| case SOF_IPC4_PIPE_PAUSED: | |
| /* Pause the pipeline only when its started_count is 1 more than paused_count */ | |
| if (spipe->paused_count == (spipe->started_count - 1)) | |
| sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, | |
| true); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| static void | |
| sof_ipc4_update_pipeline_state(struct snd_sof_dev *sdev, int state, int cmd, | |
| struct snd_sof_pipeline *spipe, | |
| struct ipc4_pipeline_set_state_data *trigger_list) | |
| { | |
| struct snd_sof_widget *pipe_widget = spipe->pipe_widget; | |
| struct sof_ipc4_pipeline *pipeline = pipe_widget->private; | |
| int i; | |
| if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) | |
| return; | |
| /* set state for pipeline if it was just triggered */ | |
| for (i = 0; i < trigger_list->count; i++) { | |
| if (trigger_list->pipeline_instance_ids[i] == pipe_widget->instance_id) { | |
| pipeline->state = state; | |
| break; | |
| } | |
| } | |
| switch (state) { | |
| case SOF_IPC4_PIPE_PAUSED: | |
| switch (cmd) { | |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: | |
| /* | |
| * increment paused_count if the PAUSED is the final state during | |
| * the PAUSE trigger | |
| */ | |
| spipe->paused_count++; | |
| break; | |
| case SNDRV_PCM_TRIGGER_STOP: | |
| case SNDRV_PCM_TRIGGER_SUSPEND: | |
| /* | |
| * decrement started_count if PAUSED is the final state during the | |
| * STOP trigger | |
| */ | |
| spipe->started_count--; | |
| break; | |
| default: | |
| break; | |
| } | |
| break; | |
| case SOF_IPC4_PIPE_RUNNING: | |
| switch (cmd) { | |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: | |
| /* decrement paused_count for RELEASE */ | |
| spipe->paused_count--; | |
| break; | |
| case SNDRV_PCM_TRIGGER_START: | |
| case SNDRV_PCM_TRIGGER_RESUME: | |
| /* increment started_count for START/RESUME */ | |
| spipe->started_count++; | |
| break; | |
| default: | |
| break; | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| /* | |
| * The picture below represents the pipeline state machine wrt PCM actions corresponding to the | |
| * triggers and ioctls | |
| * +---------------+ | |
| * | | | |
| * | INIT | | |
| * | | | |
| * +-------+-------+ | |
| * | | |
| * | | |
| * | START | |
| * | | |
| * | | |
| * +----------------+ +------v-------+ +-------------+ | |
| * | | START | | HW_FREE | | | |
| * | RUNNING <-------------+ PAUSED +--------------> + RESET | | |
| * | | PAUSE | | | | | |
| * +------+---------+ RELEASE +---------+----+ +-------------+ | |
| * | ^ | |
| * | | | |
| * | | | |
| * | | | |
| * | PAUSE | | |
| * +---------------------------------+ | |
| * STOP/SUSPEND | |
| * | |
| * Note that during system suspend, the suspend trigger is followed by a hw_free in | |
| * sof_pcm_trigger(). So, the final state during suspend would be RESET. | |
| * Also, since the SOF driver doesn't support full resume, streams would be restarted with the | |
| * prepare ioctl before the START trigger. | |
| */ | |
| /* | |
| * Chained DMA is a special case where there is no processing on | |
| * DSP. The samples are just moved over by host side DMA to a single | |
| * buffer on DSP and directly from there to link DMA. However, the | |
| * model on SOF driver has two notional pipelines, one at host DAI, | |
| * and another at link DAI. They both shall have the use_chain_dma | |
| * attribute. | |
| */ | |
| static int sof_ipc4_chain_dma_trigger(struct snd_sof_dev *sdev, | |
| struct snd_sof_pcm *spcm, int direction, | |
| struct snd_sof_pcm_stream_pipeline_list *pipeline_list, | |
| int state, int cmd) | |
| { | |
| struct sof_ipc4_fw_data *ipc4_data = sdev->private; | |
| struct sof_ipc4_pcm_stream_priv *stream_priv; | |
| bool allocate, enable, set_fifo_size; | |
| struct sof_ipc4_msg msg = {{ 0 }}; | |
| int ret, i; | |
| stream_priv = spcm->stream[direction].private; | |
| switch (state) { | |
| case SOF_IPC4_PIPE_RUNNING: /* Allocate and start chained dma */ | |
| allocate = true; | |
| enable = true; | |
| /* | |
| * SOF assumes creation of a new stream from the presence of fifo_size | |
| * in the message, so we must leave it out in pause release case. | |
| */ | |
| if (cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) | |
| set_fifo_size = false; | |
| else | |
| set_fifo_size = true; | |
| break; | |
| case SOF_IPC4_PIPE_PAUSED: /* Disable chained DMA. */ | |
| allocate = true; | |
| enable = false; | |
| set_fifo_size = false; | |
| break; | |
| case SOF_IPC4_PIPE_RESET: /* Disable and free chained DMA. */ | |
| /* ChainDMA can only be reset if it has been allocated */ | |
| if (!stream_priv->chain_dma_allocated) | |
| return 0; | |
| allocate = false; | |
| enable = false; | |
| set_fifo_size = false; | |
| break; | |
| default: | |
| spcm_err(spcm, direction, "Unexpected pipeline state %d\n", state); | |
| return -EINVAL; | |
| } | |
| msg.primary = SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_CHAIN_DMA); | |
| msg.primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); | |
| msg.primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); | |
| /* | |
| * To set-up the DMA chain, the host DMA ID and SCS setting | |
| * are retrieved from the host pipeline configuration. Likewise | |
| * the link DMA ID and fifo_size are retrieved from the link | |
| * pipeline configuration. | |
| */ | |
| for (i = 0; i < pipeline_list->count; i++) { | |
| struct snd_sof_pipeline *spipe = pipeline_list->pipelines[i]; | |
| struct snd_sof_widget *pipe_widget = spipe->pipe_widget; | |
| struct sof_ipc4_pipeline *pipeline = pipe_widget->private; | |
| if (!pipeline->use_chain_dma) { | |
| spcm_err(spcm, direction, | |
| "All pipelines in chained DMA path should have use_chain_dma attribute set."); | |
| return -EINVAL; | |
| } | |
| msg.primary |= pipeline->msg.primary; | |
| /* Add fifo_size (actually DMA buffer size) field to the message */ | |
| if (set_fifo_size) | |
| msg.extension |= pipeline->msg.extension; | |
| } | |
| if (direction == SNDRV_PCM_STREAM_CAPTURE) { | |
| /* | |
| * For ChainDMA the DMA ids are unique with the following mapping: | |
| * playback: 0 - (num_playback_streams - 1) | |
| * capture: num_playback_streams - (num_playback_streams + | |
| * num_capture_streams - 1) | |
| * | |
| * Add the num_playback_streams offset to the DMA ids stored in | |
| * msg.primary in case capture | |
| */ | |
| msg.primary += SOF_IPC4_GLB_CHAIN_DMA_HOST_ID(ipc4_data->num_playback_streams); | |
| msg.primary += SOF_IPC4_GLB_CHAIN_DMA_LINK_ID(ipc4_data->num_playback_streams); | |
| } | |
| if (allocate) | |
| msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ALLOCATE_MASK; | |
| if (enable) | |
| msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ENABLE_MASK; | |
| ret = sof_ipc_tx_message_no_reply(sdev->ipc, &msg, 0); | |
| /* Update the ChainDMA allocation state */ | |
| if (!ret) | |
| stream_priv->chain_dma_allocated = allocate; | |
| return ret; | |
| } | |
| static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream, int state, int cmd) | |
| { | |
| struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); | |
| struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); | |
| struct snd_sof_pcm_stream_pipeline_list *pipeline_list; | |
| struct sof_ipc4_fw_data *ipc4_data = sdev->private; | |
| struct ipc4_pipeline_set_state_data *trigger_list; | |
| struct snd_sof_widget *pipe_widget; | |
| struct sof_ipc4_pipeline *pipeline; | |
| struct snd_sof_pipeline *spipe; | |
| struct snd_sof_pcm *spcm; | |
| u8 *pipe_priority; | |
| int ret; | |
| int i; | |
| spcm = snd_sof_find_spcm_dai(component, rtd); | |
| if (!spcm) | |
| return -EINVAL; | |
| spcm_dbg(spcm, substream->stream, "cmd: %d, state: %d\n", cmd, state); | |
| pipeline_list = &spcm->stream[substream->stream].pipeline_list; | |
| /* nothing to trigger if the list is empty */ | |
| if (!pipeline_list->pipelines || !pipeline_list->count) | |
| return 0; | |
| spipe = pipeline_list->pipelines[0]; | |
| pipe_widget = spipe->pipe_widget; | |
| pipeline = pipe_widget->private; | |
| /* | |
| * If use_chain_dma attribute is set we proceed to chained DMA | |
| * trigger function that handles the rest for the substream. | |
| */ | |
| if (pipeline->use_chain_dma) { | |
| struct sof_ipc4_timestamp_info *time_info; | |
| time_info = sof_ipc4_sps_to_time_info(&spcm->stream[substream->stream]); | |
| ret = sof_ipc4_chain_dma_trigger(sdev, spcm, substream->stream, | |
| pipeline_list, state, cmd); | |
| if (ret || !time_info) | |
| return ret; | |
| if (state == SOF_IPC4_PIPE_PAUSED) { | |
| /* | |
| * Record the DAI position for delay reporting | |
| * To handle multiple pause/resume/xrun we need to add | |
| * the positions to simulate how the firmware behaves | |
| */ | |
| u64 pos = snd_sof_pcm_get_dai_frame_counter(sdev, component, | |
| substream); | |
| time_info->stream_end_offset += pos; | |
| } else if (state == SOF_IPC4_PIPE_RESET) { | |
| /* Reset the end offset as the stream is stopped */ | |
| time_info->stream_end_offset = 0; | |
| } | |
| return 0; | |
| } | |
| /* allocate memory for the pipeline data */ | |
| trigger_list = kzalloc_flex(*trigger_list, pipeline_instance_ids, | |
| pipeline_list->count); | |
| if (!trigger_list) | |
| return -ENOMEM; | |
| pipe_priority = kzalloc(pipeline_list->count, GFP_KERNEL); | |
| if (!pipe_priority) { | |
| kfree(trigger_list); | |
| return -ENOMEM; | |
| } | |
| guard(mutex)(&ipc4_data->pipeline_state_mutex); | |
| /* | |
| * IPC4 requires pipelines to be triggered in order starting at the sink and | |
| * walking all the way to the source. So traverse the pipeline_list in the order | |
| * sink->source when starting PCM's and in the reverse order to pause/stop PCM's. | |
| * Skip the pipelines that have their skip_during_fe_trigger flag set. If there is a fork | |
| * in the pipeline, the order of triggering between the left/right paths will be | |
| * indeterministic. But the sink->source trigger order sink->source would still be | |
| * guaranteed for each fork independently. | |
| */ | |
| if (state == SOF_IPC4_PIPE_RUNNING || state == SOF_IPC4_PIPE_RESET) | |
| for (i = pipeline_list->count - 1; i >= 0; i--) { | |
| spipe = pipeline_list->pipelines[i]; | |
| sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list, | |
| pipe_priority); | |
| } | |
| else | |
| for (i = 0; i < pipeline_list->count; i++) { | |
| spipe = pipeline_list->pipelines[i]; | |
| sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list, | |
| pipe_priority); | |
| } | |
| /* return if all pipelines are in the requested state already */ | |
| if (!trigger_list->count) { | |
| ret = 0; | |
| goto free; | |
| } | |
| /* no need to pause before reset or before pause release */ | |
| if (state == SOF_IPC4_PIPE_RESET || cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) | |
| goto skip_pause_transition; | |
| /* | |
| * set paused state for pipelines if the final state is PAUSED or when the pipeline | |
| * is set to RUNNING for the first time after the PCM is started. | |
| */ | |
| ret = sof_ipc4_set_multi_pipeline_state(sdev, SOF_IPC4_PIPE_PAUSED, trigger_list); | |
| if (ret < 0) { | |
| spcm_err(spcm, substream->stream, "failed to pause all pipelines\n"); | |
| /* | |
| * workaround: if the firmware is crashed or the IPC timed out | |
| * while setting the pipeline state we must ignore the error | |
| * code and proceed to set adjust the local pipeline states. | |
| * | |
| * If the firmware is crashed we will not send IPC messages | |
| * and we are going to see errors printed, but the state of the | |
| * widgets will be correct for the next boot. | |
| */ | |
| if (sdev->fw_state != SOF_FW_CRASHED && ret != -ETIMEDOUT) | |
| goto free; | |
| ret = 0; | |
| } | |
| /* update PAUSED state for all pipelines just triggered */ | |
| for (i = 0; i < pipeline_list->count ; i++) { | |
| spipe = pipeline_list->pipelines[i]; | |
| sof_ipc4_update_pipeline_state(sdev, SOF_IPC4_PIPE_PAUSED, cmd, spipe, | |
| trigger_list); | |
| } | |
| /* return if this is the final state */ | |
| if (state == SOF_IPC4_PIPE_PAUSED) { | |
| struct sof_ipc4_timestamp_info *time_info; | |
| /* | |
| * Invalidate the stream_start_offset to make sure that it is | |
| * going to be updated if the stream resumes | |
| */ | |
| time_info = sof_ipc4_sps_to_time_info(&spcm->stream[substream->stream]); | |
| if (time_info) | |
| time_info->stream_start_offset = SOF_IPC4_INVALID_STREAM_POSITION; | |
| goto free; | |
| } | |
| skip_pause_transition: | |
| /* else set the RUNNING/RESET state in the DSP */ | |
| ret = sof_ipc4_set_multi_pipeline_state(sdev, state, trigger_list); | |
| if (ret < 0) { | |
| spcm_err(spcm, substream->stream, | |
| "failed to set final state %d for all pipelines\n", | |
| state); | |
| /* | |
| * workaround: if the firmware is crashed or the IPC timed out | |
| * while setting the pipeline state we must ignore the error | |
| * code and proceed to set adjust the local pipeline states. | |
| * | |
| * If the firmware is crashed we will not send IPC messages | |
| * and we are going to see errors printed, but the state of the | |
| * widgets will be correct for the next boot. | |
| */ | |
| if (sdev->fw_state != SOF_FW_CRASHED && ret != -ETIMEDOUT) | |
| goto free; | |
| ret = 0; | |
| } | |
| /* update RUNNING/RESET state for all pipelines that were just triggered */ | |
| for (i = 0; i < pipeline_list->count; i++) { | |
| spipe = pipeline_list->pipelines[i]; | |
| sof_ipc4_update_pipeline_state(sdev, state, cmd, spipe, trigger_list); | |
| } | |
| free: | |
| kfree(trigger_list); | |
| kfree(pipe_priority); | |
| return ret; | |
| } | |
| static int sof_ipc4_pcm_trigger(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream, int cmd) | |
| { | |
| int state; | |
| /* determine the pipeline state */ | |
| switch (cmd) { | |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: | |
| case SNDRV_PCM_TRIGGER_RESUME: | |
| case SNDRV_PCM_TRIGGER_START: | |
| state = SOF_IPC4_PIPE_RUNNING; | |
| break; | |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: | |
| case SNDRV_PCM_TRIGGER_SUSPEND: | |
| case SNDRV_PCM_TRIGGER_STOP: | |
| state = SOF_IPC4_PIPE_PAUSED; | |
| break; | |
| default: | |
| dev_err(component->dev, "%s: unhandled trigger cmd %d\n", __func__, cmd); | |
| return -EINVAL; | |
| } | |
| /* set the pipeline state */ | |
| return sof_ipc4_trigger_pipelines(component, substream, state, cmd); | |
| } | |
| static int sof_ipc4_pcm_hw_free(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream) | |
| { | |
| /* command is not relevant with RESET, so just pass 0 */ | |
| return sof_ipc4_trigger_pipelines(component, substream, SOF_IPC4_PIPE_RESET, 0); | |
| } | |
| static int ipc4_ssp_dai_config_pcm_params_match(struct snd_sof_dev *sdev, | |
| const char *link_name, | |
| struct snd_pcm_hw_params *params) | |
| { | |
| struct snd_sof_dai_link *slink; | |
| struct snd_sof_dai *dai; | |
| bool dai_link_found = false; | |
| int current_config = -1; | |
| bool partial_match; | |
| int i; | |
| list_for_each_entry(slink, &sdev->dai_link_list, list) { | |
| if (!strcmp(slink->link->name, link_name)) { | |
| dai_link_found = true; | |
| break; | |
| } | |
| } | |
| if (!dai_link_found) | |
| return 0; | |
| /* | |
| * Find the first best matching hardware config: | |
| * rate + format + channels are matching | |
| * rate + channel are matching | |
| * | |
| * The copier cannot do rate and/or channel conversion. | |
| */ | |
| for (i = 0; i < slink->num_hw_configs; i++) { | |
| struct snd_soc_tplg_hw_config *hw_config = &slink->hw_configs[i]; | |
| if (params_rate(params) == le32_to_cpu(hw_config->fsync_rate) && | |
| params_width(params) == le32_to_cpu(hw_config->tdm_slot_width) && | |
| params_channels(params) <= le32_to_cpu(hw_config->tdm_slots)) { | |
| current_config = le32_to_cpu(hw_config->id); | |
| partial_match = false; | |
| /* best match found */ | |
| break; | |
| } else if (current_config < 0 && | |
| params_rate(params) == le32_to_cpu(hw_config->fsync_rate) && | |
| params_channels(params) <= le32_to_cpu(hw_config->tdm_slots)) { | |
| current_config = le32_to_cpu(hw_config->id); | |
| partial_match = true; | |
| /* keep looking for better match */ | |
| } | |
| } | |
| if (current_config < 0) { | |
| dev_err(sdev->dev, | |
| "%s: No suitable hw_config found for %s (num_hw_configs: %d)\n", | |
| __func__, slink->link->name, slink->num_hw_configs); | |
| return -EINVAL; | |
| } | |
| dev_dbg(sdev->dev, | |
| "hw_config for %s: %d (num_hw_configs: %d) with %s match\n", | |
| slink->link->name, current_config, slink->num_hw_configs, | |
| partial_match ? "partial" : "full"); | |
| list_for_each_entry(dai, &sdev->dai_list, list) | |
| if (!strcmp(slink->link->name, dai->name)) | |
| dai->current_config = current_config; | |
| return 0; | |
| } | |
| /* | |
| * Fixup DAI link parameters for sampling rate based on | |
| * DAI copier configuration. | |
| */ | |
| static int sof_ipc4_pcm_dai_link_fixup_rate(struct snd_sof_dev *sdev, | |
| struct snd_pcm_hw_params *params, | |
| struct sof_ipc4_copier *ipc4_copier) | |
| { | |
| struct sof_ipc4_pin_format *pin_fmts = ipc4_copier->available_fmt.input_pin_fmts; | |
| struct snd_interval *rate = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE); | |
| int num_input_formats = ipc4_copier->available_fmt.num_input_formats; | |
| unsigned int fe_rate = params_rate(params); | |
| bool fe_be_rate_match = false; | |
| bool single_be_rate = true; | |
| unsigned int be_rate; | |
| int i; | |
| if (WARN_ON_ONCE(!num_input_formats)) | |
| return -EINVAL; | |
| /* | |
| * Copier does not change sampling rate, so we | |
| * need to only consider the input pin information. | |
| */ | |
| be_rate = pin_fmts[0].audio_fmt.sampling_frequency; | |
| for (i = 0; i < num_input_formats; i++) { | |
| unsigned int val = pin_fmts[i].audio_fmt.sampling_frequency; | |
| if (val != be_rate) | |
| single_be_rate = false; | |
| if (val == fe_rate) { | |
| fe_be_rate_match = true; | |
| break; | |
| } | |
| } | |
| /* | |
| * If rate is different than FE rate, topology must | |
| * contain an SRC. But we do require topology to | |
| * define a single rate in the DAI copier config in | |
| * this case (FE rate may be variable). | |
| */ | |
| if (!fe_be_rate_match) { | |
| if (!single_be_rate) { | |
| dev_err(sdev->dev, "Unable to select sampling rate for DAI link\n"); | |
| return -EINVAL; | |
| } | |
| rate->min = be_rate; | |
| rate->max = rate->min; | |
| } | |
| return 0; | |
| } | |
| static int sof_ipc4_pcm_dai_link_fixup_channels(struct snd_sof_dev *sdev, | |
| struct snd_pcm_hw_params *params, | |
| struct sof_ipc4_copier *ipc4_copier) | |
| { | |
| struct sof_ipc4_pin_format *pin_fmts = ipc4_copier->available_fmt.input_pin_fmts; | |
| struct snd_interval *channels = hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS); | |
| int num_input_formats = ipc4_copier->available_fmt.num_input_formats; | |
| unsigned int fe_channels = params_channels(params); | |
| bool fe_be_match = false; | |
| bool single_be_channels = true; | |
| unsigned int be_channels, val; | |
| int i; | |
| if (WARN_ON_ONCE(!num_input_formats)) | |
| return -EINVAL; | |
| /* | |
| * Copier does not change channels, so we | |
| * need to only consider the input pin information. | |
| */ | |
| be_channels = SOF_IPC4_AUDIO_FORMAT_CFG_CHANNELS_COUNT(pin_fmts[0].audio_fmt.fmt_cfg); | |
| for (i = 0; i < num_input_formats; i++) { | |
| val = SOF_IPC4_AUDIO_FORMAT_CFG_CHANNELS_COUNT(pin_fmts[i].audio_fmt.fmt_cfg); | |
| if (val != be_channels) | |
| single_be_channels = false; | |
| if (val == fe_channels) { | |
| fe_be_match = true; | |
| break; | |
| } | |
| } | |
| /* | |
| * If channels is different than FE channels, topology must contain a | |
| * module which can change the number of channels. But we do require | |
| * topology to define a single channels in the DAI copier config in | |
| * this case (FE channels may be variable). | |
| */ | |
| if (!fe_be_match) { | |
| if (!single_be_channels) { | |
| dev_err(sdev->dev, "Unable to select channels for DAI link\n"); | |
| return -EINVAL; | |
| } | |
| channels->min = be_channels; | |
| channels->max = be_channels; | |
| } | |
| return 0; | |
| } | |
| static int sof_ipc4_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, | |
| struct snd_pcm_hw_params *params) | |
| { | |
| struct snd_soc_component *component = snd_soc_rtdcom_lookup(rtd, SOF_AUDIO_PCM_DRV_NAME); | |
| struct snd_sof_dai *dai = snd_sof_find_dai(component, rtd->dai_link->name); | |
| struct snd_mask *fmt = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT); | |
| struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); | |
| struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); | |
| struct sof_ipc4_audio_format *ipc4_fmt; | |
| struct sof_ipc4_copier *ipc4_copier; | |
| bool single_bitdepth = false; | |
| u32 valid_bits = 0; | |
| int dir, ret; | |
| if (!dai) { | |
| dev_err(component->dev, "%s: No DAI found with name %s\n", __func__, | |
| rtd->dai_link->name); | |
| return -EINVAL; | |
| } | |
| ipc4_copier = dai->private; | |
| if (!ipc4_copier) { | |
| dev_err(component->dev, "%s: No private data found for DAI %s\n", | |
| __func__, rtd->dai_link->name); | |
| return -EINVAL; | |
| } | |
| for_each_pcm_streams(dir) { | |
| struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(cpu_dai, dir); | |
| if (w) { | |
| struct sof_ipc4_available_audio_format *available_fmt = | |
| &ipc4_copier->available_fmt; | |
| struct snd_sof_widget *swidget = w->dobj.private; | |
| struct snd_sof_widget *pipe_widget = swidget->spipe->pipe_widget; | |
| struct sof_ipc4_pipeline *pipeline = pipe_widget->private; | |
| /* Chain DMA does not use copiers, so no fixup needed */ | |
| if (pipeline->use_chain_dma) | |
| return 0; | |
| if (dir == SNDRV_PCM_STREAM_PLAYBACK) { | |
| if (sof_ipc4_copier_is_single_bitdepth(sdev, | |
| available_fmt->output_pin_fmts, | |
| available_fmt->num_output_formats)) { | |
| ipc4_fmt = &available_fmt->output_pin_fmts->audio_fmt; | |
| single_bitdepth = true; | |
| } | |
| } else { | |
| if (sof_ipc4_copier_is_single_bitdepth(sdev, | |
| available_fmt->input_pin_fmts, | |
| available_fmt->num_input_formats)) { | |
| ipc4_fmt = &available_fmt->input_pin_fmts->audio_fmt; | |
| single_bitdepth = true; | |
| } | |
| } | |
| } | |
| } | |
| ret = sof_ipc4_pcm_dai_link_fixup_rate(sdev, params, ipc4_copier); | |
| if (ret) | |
| return ret; | |
| ret = sof_ipc4_pcm_dai_link_fixup_channels(sdev, params, ipc4_copier); | |
| if (ret) | |
| return ret; | |
| if (single_bitdepth) { | |
| snd_mask_none(fmt); | |
| valid_bits = SOF_IPC4_AUDIO_FORMAT_CFG_V_BIT_DEPTH(ipc4_fmt->fmt_cfg); | |
| dev_dbg(component->dev, "Set %s to %d bit format\n", dai->name, valid_bits); | |
| } | |
| /* Set format if it is specified */ | |
| switch (valid_bits) { | |
| case 16: | |
| snd_mask_set_format(fmt, SNDRV_PCM_FORMAT_S16_LE); | |
| break; | |
| case 24: | |
| snd_mask_set_format(fmt, SNDRV_PCM_FORMAT_S24_LE); | |
| break; | |
| case 32: | |
| snd_mask_set_format(fmt, SNDRV_PCM_FORMAT_S32_LE); | |
| break; | |
| default: | |
| break; | |
| } | |
| if (ipc4_copier->dai_type == SOF_DAI_INTEL_SSP) | |
| return ipc4_ssp_dai_config_pcm_params_match(sdev, | |
| (char *)rtd->dai_link->name, | |
| params); | |
| return 0; | |
| } | |
| static void sof_ipc4_pcm_free(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) | |
| { | |
| struct snd_sof_pcm_stream_pipeline_list *pipeline_list; | |
| struct sof_ipc4_pcm_stream_priv *stream_priv; | |
| int stream; | |
| for_each_pcm_streams(stream) { | |
| pipeline_list = &spcm->stream[stream].pipeline_list; | |
| kfree(pipeline_list->pipelines); | |
| pipeline_list->pipelines = NULL; | |
| stream_priv = spcm->stream[stream].private; | |
| kfree(stream_priv->time_info); | |
| kfree(spcm->stream[stream].private); | |
| spcm->stream[stream].private = NULL; | |
| } | |
| } | |
| static int sof_ipc4_pcm_setup(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) | |
| { | |
| struct snd_sof_pcm_stream_pipeline_list *pipeline_list; | |
| struct sof_ipc4_fw_data *ipc4_data = sdev->private; | |
| struct sof_ipc4_pcm_stream_priv *stream_priv; | |
| struct sof_ipc4_timestamp_info *time_info; | |
| bool support_info = true; | |
| u32 abi_version; | |
| u32 abi_offset; | |
| int stream; | |
| abi_offset = offsetof(struct sof_ipc4_fw_registers, abi_ver); | |
| sof_mailbox_read(sdev, sdev->fw_info_box.offset + abi_offset, &abi_version, | |
| sizeof(abi_version)); | |
| if (abi_version < SOF_IPC4_FW_REGS_ABI_VER) | |
| support_info = false; | |
| /* For delay reporting the get_host_byte_counter callback is needed */ | |
| if (!sof_ops(sdev) || !sof_ops(sdev)->get_host_byte_counter) | |
| support_info = false; | |
| for_each_pcm_streams(stream) { | |
| pipeline_list = &spcm->stream[stream].pipeline_list; | |
| /* allocate memory for max number of pipeline IDs */ | |
| pipeline_list->pipelines = kzalloc_objs(*pipeline_list->pipelines, | |
| ipc4_data->max_num_pipelines); | |
| if (!pipeline_list->pipelines) { | |
| sof_ipc4_pcm_free(sdev, spcm); | |
| return -ENOMEM; | |
| } | |
| stream_priv = kzalloc_obj(*stream_priv); | |
| if (!stream_priv) { | |
| sof_ipc4_pcm_free(sdev, spcm); | |
| return -ENOMEM; | |
| } | |
| spcm->stream[stream].private = stream_priv; | |
| /* Delay reporting is only supported on playback */ | |
| if (!support_info || stream == SNDRV_PCM_STREAM_CAPTURE) | |
| continue; | |
| time_info = kzalloc_obj(*time_info); | |
| if (!time_info) { | |
| sof_ipc4_pcm_free(sdev, spcm); | |
| return -ENOMEM; | |
| } | |
| stream_priv->time_info = time_info; | |
| } | |
| return 0; | |
| } | |
| static void sof_ipc4_build_time_info(struct snd_sof_dev *sdev, struct snd_sof_pcm_stream *sps) | |
| { | |
| struct sof_ipc4_copier *host_copier = NULL; | |
| struct sof_ipc4_copier *dai_copier = NULL; | |
| struct sof_ipc4_llp_reading_slot llp_slot; | |
| struct sof_ipc4_timestamp_info *time_info; | |
| struct snd_soc_dapm_widget *widget; | |
| struct snd_sof_dai *dai; | |
| int i; | |
| /* find host & dai to locate info in memory window */ | |
| for_each_dapm_widgets(sps->list, i, widget) { | |
| struct snd_sof_widget *swidget = widget->dobj.private; | |
| if (!swidget) | |
| continue; | |
| if (WIDGET_IS_AIF(swidget->widget->id)) { | |
| host_copier = swidget->private; | |
| } else if (WIDGET_IS_DAI(swidget->widget->id)) { | |
| dai = swidget->private; | |
| dai_copier = dai->private; | |
| } | |
| } | |
| /* both host and dai copier must be valid for time_info */ | |
| if (!host_copier || !dai_copier) { | |
| dev_err(sdev->dev, "host or dai copier are not found\n"); | |
| return; | |
| } | |
| time_info = sof_ipc4_sps_to_time_info(sps); | |
| time_info->host_copier = host_copier; | |
| time_info->dai_copier = dai_copier; | |
| time_info->llp_offset = offsetof(struct sof_ipc4_fw_registers, | |
| llp_gpdma_reading_slots) + sdev->fw_info_box.offset; | |
| /* find llp slot used by current dai */ | |
| for (i = 0; i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS; i++) { | |
| sof_mailbox_read(sdev, time_info->llp_offset, &llp_slot, sizeof(llp_slot)); | |
| if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) | |
| break; | |
| time_info->llp_offset += sizeof(llp_slot); | |
| } | |
| if (i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS) | |
| return; | |
| /* if no llp gpdma slot is used, check aggregated sdw slot */ | |
| time_info->llp_offset = offsetof(struct sof_ipc4_fw_registers, | |
| llp_sndw_reading_slots) + sdev->fw_info_box.offset; | |
| for (i = 0; i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS; i++) { | |
| sof_mailbox_read(sdev, time_info->llp_offset, &llp_slot, sizeof(llp_slot)); | |
| if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) | |
| break; | |
| time_info->llp_offset += sizeof(llp_slot); | |
| } | |
| if (i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS) | |
| return; | |
| /* check EVAD slot */ | |
| time_info->llp_offset = offsetof(struct sof_ipc4_fw_registers, | |
| llp_evad_reading_slot) + sdev->fw_info_box.offset; | |
| sof_mailbox_read(sdev, time_info->llp_offset, &llp_slot, sizeof(llp_slot)); | |
| if (llp_slot.node_id != dai_copier->data.gtw_cfg.node_id) | |
| time_info->llp_offset = 0; | |
| } | |
| static int sof_ipc4_pcm_hw_params(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream, | |
| struct snd_pcm_hw_params *params, | |
| struct snd_sof_platform_stream_params *platform_params) | |
| { | |
| struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); | |
| struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); | |
| struct sof_ipc4_timestamp_info *time_info; | |
| struct snd_sof_pcm *spcm; | |
| spcm = snd_sof_find_spcm_dai(component, rtd); | |
| if (!spcm) | |
| return -EINVAL; | |
| time_info = sof_ipc4_sps_to_time_info(&spcm->stream[substream->stream]); | |
| /* delay calculation is not supported by current fw_reg ABI */ | |
| if (!time_info) | |
| return 0; | |
| time_info->stream_start_offset = SOF_IPC4_INVALID_STREAM_POSITION; | |
| time_info->llp_offset = 0; | |
| sof_ipc4_build_time_info(sdev, &spcm->stream[substream->stream]); | |
| return 0; | |
| } | |
| static u64 sof_ipc4_frames_dai_to_host(struct sof_ipc4_timestamp_info *time_info, u64 value) | |
| { | |
| u64 dai_rate, host_rate; | |
| if (!time_info->dai_copier || !time_info->host_copier) | |
| return value; | |
| /* | |
| * copiers do not change sampling rate, so we can use the | |
| * out_format independently of stream direction | |
| */ | |
| dai_rate = time_info->dai_copier->data.out_format.sampling_frequency; | |
| host_rate = time_info->host_copier->data.out_format.sampling_frequency; | |
| if (!dai_rate || !host_rate || dai_rate == host_rate) | |
| return value; | |
| /* take care not to overflow u64, rates can be up to 768000 */ | |
| if (value > U32_MAX) { | |
| value = div64_u64(value, dai_rate); | |
| value *= host_rate; | |
| } else { | |
| value *= host_rate; | |
| value = div64_u64(value, dai_rate); | |
| } | |
| return value; | |
| } | |
| static int sof_ipc4_get_stream_start_offset(struct snd_sof_dev *sdev, | |
| struct snd_pcm_substream *substream, | |
| struct snd_sof_pcm_stream *sps, | |
| struct sof_ipc4_timestamp_info *time_info) | |
| { | |
| struct sof_ipc4_copier *host_copier = time_info->host_copier; | |
| struct sof_ipc4_copier *dai_copier = time_info->dai_copier; | |
| struct sof_ipc4_pipeline_registers ppl_reg; | |
| u32 dai_sample_size; | |
| u32 ch, node_index; | |
| u32 offset; | |
| if (!host_copier || !dai_copier) | |
| return -EINVAL; | |
| if (host_copier->data.gtw_cfg.node_id == SOF_IPC4_INVALID_NODE_ID) { | |
| return -EINVAL; | |
| } else if (host_copier->data.gtw_cfg.node_id == SOF_IPC4_CHAIN_DMA_NODE_ID) { | |
| /* | |
| * While the firmware does not support time_info reporting for | |
| * streams using ChainDMA, it is granted that ChainDMA can only | |
| * be used on Host+Link pairs where the link position is | |
| * accessible from the host side. | |
| * | |
| * Enable delay calculation in case of ChainDMA via host | |
| * accessible registers. | |
| * | |
| * The ChainDMA prefills the link DMA with a preamble | |
| * of zero samples. Set the stream start offset based | |
| * on size of the preamble (driver provided fifo size | |
| * multiplied by 2.5). We add 1ms of margin as the FW | |
| * will align the buffer size to DMA hardware | |
| * alignment that is not known to host. | |
| */ | |
| int pre_ms = SOF_IPC4_CHAIN_DMA_BUF_SIZE_MS * 5 / 2 + 1; | |
| time_info->stream_start_offset = pre_ms * substream->runtime->rate / MSEC_PER_SEC; | |
| goto out; | |
| } | |
| node_index = SOF_IPC4_NODE_INDEX(host_copier->data.gtw_cfg.node_id); | |
| offset = offsetof(struct sof_ipc4_fw_registers, pipeline_regs) + node_index * sizeof(ppl_reg); | |
| sof_mailbox_read(sdev, sdev->fw_info_box.offset + offset, &ppl_reg, sizeof(ppl_reg)); | |
| if (ppl_reg.stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) | |
| return -EINVAL; | |
| ch = dai_copier->data.out_format.fmt_cfg; | |
| ch = SOF_IPC4_AUDIO_FORMAT_CFG_CHANNELS_COUNT(ch); | |
| dai_sample_size = (dai_copier->data.out_format.bit_depth >> 3) * ch; | |
| /* convert offsets to frame count */ | |
| time_info->stream_start_offset = ppl_reg.stream_start_offset; | |
| do_div(time_info->stream_start_offset, dai_sample_size); | |
| time_info->stream_end_offset = ppl_reg.stream_end_offset; | |
| do_div(time_info->stream_end_offset, dai_sample_size); | |
| /* convert to host frame time */ | |
| time_info->stream_start_offset = | |
| sof_ipc4_frames_dai_to_host(time_info, time_info->stream_start_offset); | |
| time_info->stream_end_offset = | |
| sof_ipc4_frames_dai_to_host(time_info, time_info->stream_end_offset); | |
| out: | |
| /* Initialize the delay value to 0 (no delay) */ | |
| time_info->delay = 0; | |
| return 0; | |
| } | |
| static int sof_ipc4_pcm_pointer(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream, | |
| snd_pcm_uframes_t *pointer) | |
| { | |
| struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); | |
| struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); | |
| struct sof_ipc4_timestamp_info *time_info; | |
| struct sof_ipc4_llp_reading_slot llp; | |
| snd_pcm_uframes_t head_cnt, tail_cnt; | |
| struct snd_sof_pcm_stream *sps; | |
| u64 dai_cnt, host_cnt, host_ptr; | |
| struct snd_sof_pcm *spcm; | |
| int ret; | |
| spcm = snd_sof_find_spcm_dai(component, rtd); | |
| if (!spcm) | |
| return -EOPNOTSUPP; | |
| sps = &spcm->stream[substream->stream]; | |
| time_info = sof_ipc4_sps_to_time_info(sps); | |
| if (!time_info) | |
| return -EOPNOTSUPP; | |
| /* | |
| * stream_start_offset is updated to memory window by FW based on | |
| * pipeline statistics and it may be invalid if host query happens before | |
| * the statistics is complete. And it will not change after the first initiailization. | |
| */ | |
| if (time_info->stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) { | |
| ret = sof_ipc4_get_stream_start_offset(sdev, substream, sps, time_info); | |
| if (ret < 0) | |
| return -EOPNOTSUPP; | |
| } | |
| /* For delay calculation we need the host counter */ | |
| host_cnt = snd_sof_pcm_get_host_byte_counter(sdev, component, substream); | |
| /* Store the original value to host_ptr */ | |
| host_ptr = host_cnt; | |
| /* convert the host_cnt to frames */ | |
| host_cnt = div64_u64(host_cnt, frames_to_bytes(substream->runtime, 1)); | |
| /* | |
| * If the LLP counter is not reported by firmware in the SRAM window | |
| * then read the dai (link) counter via host accessible means if | |
| * available. | |
| */ | |
| if (!time_info->llp_offset) { | |
| dai_cnt = snd_sof_pcm_get_dai_frame_counter(sdev, component, substream); | |
| if (!dai_cnt) | |
| return -EOPNOTSUPP; | |
| } else { | |
| sof_mailbox_read(sdev, time_info->llp_offset, &llp, sizeof(llp)); | |
| dai_cnt = ((u64)llp.reading.llp_u << 32) | llp.reading.llp_l; | |
| } | |
| dai_cnt = sof_ipc4_frames_dai_to_host(time_info, dai_cnt); | |
| dai_cnt += time_info->stream_end_offset; | |
| /* In two cases dai dma counter is not accurate | |
| * (1) dai pipeline is started before host pipeline | |
| * (2) multiple streams mixed into one. Each stream has the same dai dma | |
| * counter | |
| * | |
| * Firmware calculates correct stream_start_offset for all cases | |
| * including above two. | |
| * Driver subtracts stream_start_offset from dai dma counter to get | |
| * accurate one | |
| */ | |
| /* | |
| * On stream start the dai counter might not yet have reached the | |
| * stream_start_offset value which means that no frames have left the | |
| * DSP yet from the audio stream (on playback, capture streams have | |
| * offset of 0 as we start capturing right away). | |
| * In this case we need to adjust the distance between the counters by | |
| * increasing the host counter by (offset - dai_counter). | |
| * Otherwise the dai_counter needs to be adjusted to reflect the number | |
| * of valid frames passed on the DAI side. | |
| * | |
| * The delay is the difference between the counters on the two | |
| * sides of the DSP. | |
| */ | |
| if (dai_cnt < time_info->stream_start_offset) { | |
| host_cnt += time_info->stream_start_offset - dai_cnt; | |
| dai_cnt = 0; | |
| } else { | |
| dai_cnt -= time_info->stream_start_offset; | |
| } | |
| /* Convert to a common base before comparisons */ | |
| dai_cnt &= DELAY_BOUNDARY; | |
| host_cnt &= DELAY_BOUNDARY; | |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { | |
| head_cnt = host_cnt; | |
| tail_cnt = dai_cnt; | |
| } else { | |
| head_cnt = dai_cnt; | |
| tail_cnt = host_cnt; | |
| } | |
| if (unlikely(head_cnt < tail_cnt)) | |
| time_info->delay = DELAY_BOUNDARY - tail_cnt + head_cnt; | |
| else | |
| time_info->delay = head_cnt - tail_cnt; | |
| if (time_info->delay > DELAY_MAX) { | |
| spcm_dbg_ratelimited(spcm, substream->stream, | |
| "inaccurate delay, host %llu dai_cnt %llu", | |
| host_cnt, dai_cnt); | |
| time_info->delay = 0; | |
| } | |
| /* | |
| * Convert the host byte counter to PCM pointer which wraps in buffer | |
| * and it is in frames | |
| */ | |
| div64_u64_rem(host_ptr, snd_pcm_lib_buffer_bytes(substream), &host_ptr); | |
| *pointer = bytes_to_frames(substream->runtime, host_ptr); | |
| return 0; | |
| } | |
| static snd_pcm_sframes_t sof_ipc4_pcm_delay(struct snd_soc_component *component, | |
| struct snd_pcm_substream *substream) | |
| { | |
| struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); | |
| struct sof_ipc4_timestamp_info *time_info; | |
| struct snd_sof_pcm *spcm; | |
| spcm = snd_sof_find_spcm_dai(component, rtd); | |
| if (!spcm) | |
| return 0; | |
| time_info = sof_ipc4_sps_to_time_info(&spcm->stream[substream->stream]); | |
| /* | |
| * Report the stored delay value calculated in the pointer callback. | |
| * In the unlikely event that the calculation was skipped/aborted, the | |
| * default 0 delay returned. | |
| */ | |
| if (time_info) | |
| return time_info->delay; | |
| /* No delay information available, report 0 as delay */ | |
| return 0; | |
| } | |
| const struct sof_ipc_pcm_ops ipc4_pcm_ops = { | |
| .hw_params = sof_ipc4_pcm_hw_params, | |
| .trigger = sof_ipc4_pcm_trigger, | |
| .hw_free = sof_ipc4_pcm_hw_free, | |
| .dai_link_fixup = sof_ipc4_pcm_dai_link_fixup, | |
| .pcm_setup = sof_ipc4_pcm_setup, | |
| .pcm_free = sof_ipc4_pcm_free, | |
| .pointer = sof_ipc4_pcm_pointer, | |
| .delay = sof_ipc4_pcm_delay, | |
| .ipc_first_on_start = true, | |
| .platform_stop_during_hw_free = true, | |
| }; |