| // 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. All rights reserved. |
| // |
| |
| #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" |
| |
| 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; |
| |
| /* trigger a single pipeline */ |
| if (trigger_list->count == 1) |
| return sof_ipc4_set_pipeline_state(sdev, trigger_list->pipeline_ids[0], 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 id, u32 state) |
| { |
| struct sof_ipc4_msg msg = {{ 0 }}; |
| u32 primary; |
| |
| dev_dbg(sdev->dev, "ipc4 set pipeline %d state %d", id, state); |
| |
| primary = state; |
| primary |= SOF_IPC4_GLB_PIPE_STATE_ID(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_to_trigger_list(struct snd_sof_dev *sdev, int state, |
| 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; |
| |
| if (pipeline->skip_during_fe_trigger) |
| 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) |
| trigger_list->pipeline_ids[trigger_list->count++] = |
| pipe_widget->instance_id; |
| break; |
| case SOF_IPC4_PIPE_RESET: |
| /* RESET if the pipeline is neither running nor paused */ |
| if (!spipe->started_count && !spipe->paused_count) |
| trigger_list->pipeline_ids[trigger_list->count++] = |
| pipe_widget->instance_id; |
| 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)) |
| trigger_list->pipeline_ids[trigger_list->count++] = |
| pipe_widget->instance_id; |
| 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) |
| return; |
| |
| /* set state for pipeline if it was just triggered */ |
| for (i = 0; i < trigger_list->count; i++) { |
| if (trigger_list->pipeline_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_stream_pipeline_list *pipeline_list, |
| int state, int cmd) |
| { |
| bool allocate, enable, set_fifo_size; |
| struct sof_ipc4_msg msg = {{ 0 }}; |
| int i; |
| |
| 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. */ |
| allocate = false; |
| enable = false; |
| set_fifo_size = false; |
| break; |
| default: |
| dev_err(sdev->dev, "Unexpected state %d", 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) { |
| dev_err(sdev->dev, |
| "All pipelines in chained DMA stream 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 (allocate) |
| msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ALLOCATE_MASK; |
| |
| if (enable) |
| msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ENABLE_MASK; |
| |
| return sof_ipc_tx_message_no_reply(sdev->ipc, &msg, 0); |
| } |
| |
| 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 = asoc_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; |
| int ret; |
| int i; |
| |
| dev_dbg(sdev->dev, "trigger cmd: %d state: %d\n", cmd, state); |
| |
| spcm = snd_sof_find_spcm_dai(component, rtd); |
| if (!spcm) |
| return -EINVAL; |
| |
| 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) |
| return sof_ipc4_chain_dma_trigger(sdev, pipeline_list, state, cmd); |
| |
| /* allocate memory for the pipeline data */ |
| trigger_list = kzalloc(struct_size(trigger_list, pipeline_ids, pipeline_list->count), |
| GFP_KERNEL); |
| if (!trigger_list) |
| return -ENOMEM; |
| |
| mutex_lock(&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); |
| } |
| 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); |
| } |
| |
| /* 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) { |
| dev_err(sdev->dev, "failed to pause all pipelines\n"); |
| goto free; |
| } |
| |
| /* 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) |
| 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) { |
| dev_err(sdev->dev, "failed to set final state %d for all pipelines\n", state); |
| goto free; |
| } |
| |
| /* 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: |
| mutex_unlock(&ipc4_data->pipeline_state_mutex); |
| kfree(trigger_list); |
| 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_PUSH: |
| state = SOF_IPC4_PIPE_PAUSED; |
| break; |
| 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_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 void 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 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; |
| |
| 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)) { |
| /* set current config for all DAI's with matching name */ |
| list_for_each_entry(dai, &sdev->dai_list, list) |
| if (!strcmp(slink->link->name, dai->name)) |
| dai->current_config = le32_to_cpu(hw_config->id); |
| break; |
| } |
| } |
| } |
| |
| /* |
| * 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; |
| |
| /* |
| * Copier does not change sampling rate, so we |
| * need to only consider the input pin information. |
| */ |
| for (i = 0; i < num_input_formats; i++) { |
| unsigned int val = pin_fmts[i].audio_fmt.sampling_frequency; |
| |
| if (i == 0) |
| be_rate = val; |
| else 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(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_sof_dev *sdev = snd_soc_component_get_drvdata(component); |
| struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, 0); |
| struct sof_ipc4_copier *ipc4_copier; |
| bool use_chain_dma = false; |
| int dir; |
| |
| 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 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; |
| |
| if (pipeline->use_chain_dma) |
| use_chain_dma = true; |
| } |
| } |
| |
| /* Chain DMA does not use copiers, so no fixup needed */ |
| if (!use_chain_dma) { |
| int ret = sof_ipc4_pcm_dai_link_fixup_rate(sdev, params, ipc4_copier); |
| |
| if (ret) |
| return ret; |
| } |
| |
| switch (ipc4_copier->dai_type) { |
| case SOF_DAI_INTEL_SSP: |
| ipc4_ssp_dai_config_pcm_params_match(sdev, (char *)rtd->dai_link->name, params); |
| break; |
| default: |
| break; |
| } |
| |
| 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; |
| int stream; |
| |
| for_each_pcm_streams(stream) { |
| pipeline_list = &spcm->stream[stream].pipeline_list; |
| kfree(pipeline_list->pipelines); |
| pipeline_list->pipelines = NULL; |
| 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_timestamp_info *stream_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_each_pcm_streams(stream) { |
| pipeline_list = &spcm->stream[stream].pipeline_list; |
| |
| /* allocate memory for max number of pipeline IDs */ |
| pipeline_list->pipelines = kcalloc(ipc4_data->max_num_pipelines, |
| sizeof(struct snd_sof_widget *), GFP_KERNEL); |
| if (!pipeline_list->pipelines) { |
| sof_ipc4_pcm_free(sdev, spcm); |
| return -ENOMEM; |
| } |
| |
| if (!support_info) |
| continue; |
| |
| stream_info = kzalloc(sizeof(*stream_info), GFP_KERNEL); |
| if (!stream_info) { |
| sof_ipc4_pcm_free(sdev, spcm); |
| return -ENOMEM; |
| } |
| |
| spcm->stream[stream].private = stream_info; |
| } |
| |
| return 0; |
| } |
| |
| static void sof_ipc4_build_time_info(struct snd_sof_dev *sdev, struct snd_sof_pcm_stream *spcm) |
| { |
| 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 *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(spcm->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; |
| } |
| |
| info = spcm->private; |
| info->host_copier = host_copier; |
| info->dai_copier = dai_copier; |
| 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, info->llp_offset, &llp_slot, sizeof(llp_slot)); |
| if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) |
| break; |
| |
| 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 */ |
| 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, info->llp_offset, &llp_slot, sizeof(llp_slot)); |
| if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) |
| break; |
| |
| info->llp_offset += sizeof(llp_slot); |
| } |
| |
| if (i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS) |
| return; |
| |
| /* check EVAD slot */ |
| info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_evad_reading_slot) + |
| sdev->fw_info_box.offset; |
| sof_mailbox_read(sdev, info->llp_offset, &llp_slot, sizeof(llp_slot)); |
| if (llp_slot.node_id != dai_copier->data.gtw_cfg.node_id) { |
| dev_info(sdev->dev, "no llp found, fall back to default HDA path"); |
| 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 = asoc_substream_to_rtd(substream); |
| struct sof_ipc4_timestamp_info *time_info; |
| struct snd_sof_pcm *spcm; |
| |
| spcm = snd_sof_find_spcm_dai(component, rtd); |
| time_info = spcm->stream[substream->stream].private; |
| /* 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 int sof_ipc4_get_stream_start_offset(struct snd_sof_dev *sdev, |
| struct snd_pcm_substream *substream, |
| struct snd_sof_pcm_stream *stream, |
| 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; |
| u64 stream_start_position; |
| 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; |
| |
| 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; |
| |
| stream_start_position = ppl_reg.stream_start_offset; |
| 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 offset to sample count */ |
| do_div(stream_start_position, dai_sample_size); |
| time_info->stream_start_offset = stream_start_position; |
| |
| return 0; |
| } |
| |
| static snd_pcm_sframes_t sof_ipc4_pcm_delay(struct snd_soc_component *component, |
| struct snd_pcm_substream *substream) |
| { |
| struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); |
| struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); |
| struct sof_ipc4_timestamp_info *time_info; |
| struct sof_ipc4_llp_reading_slot llp; |
| snd_pcm_uframes_t head_ptr, tail_ptr; |
| struct snd_sof_pcm_stream *stream; |
| struct snd_sof_pcm *spcm; |
| u64 tmp_ptr; |
| int ret; |
| |
| spcm = snd_sof_find_spcm_dai(component, rtd); |
| if (!spcm) |
| return 0; |
| |
| stream = &spcm->stream[substream->stream]; |
| time_info = stream->private; |
| if (!time_info) |
| return 0; |
| |
| /* |
| * 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, stream, time_info); |
| if (ret < 0) |
| return 0; |
| } |
| |
| /* |
| * HDaudio links don't support the LLP counter reported by firmware |
| * the link position is read directly from hardware registers. |
| */ |
| if (!time_info->llp_offset) { |
| tmp_ptr = snd_sof_pcm_get_stream_position(sdev, component, substream); |
| if (!tmp_ptr) |
| return 0; |
| } else { |
| sof_mailbox_read(sdev, time_info->llp_offset, &llp, sizeof(llp)); |
| tmp_ptr = ((u64)llp.reading.llp_u << 32) | llp.reading.llp_l; |
| } |
| |
| /* In two cases dai dma position is not accurate |
| * (1) dai pipeline is started before host pipeline |
| * (2) multiple streams mixed into one. Each stream has the same dai dma position |
| * |
| * Firmware calculates correct stream_start_offset for all cases including above two. |
| * Driver subtracts stream_start_offset from dai dma position to get accurate one |
| */ |
| tmp_ptr -= time_info->stream_start_offset; |
| |
| /* Calculate the delay taking into account that both pointer can wrap */ |
| div64_u64_rem(tmp_ptr, substream->runtime->boundary, &tmp_ptr); |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { |
| head_ptr = substream->runtime->status->hw_ptr; |
| tail_ptr = tmp_ptr; |
| } else { |
| head_ptr = tmp_ptr; |
| tail_ptr = substream->runtime->status->hw_ptr; |
| } |
| |
| if (head_ptr < tail_ptr) |
| return substream->runtime->boundary - tail_ptr + head_ptr; |
| |
| return head_ptr - tail_ptr; |
| } |
| |
| 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, |
| .delay = sof_ipc4_pcm_delay, |
| .ipc_first_on_start = true, |
| .platform_stop_during_hw_free = true, |
| }; |