mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/
synced 2025-04-19 20:58:31 +09:00

For i2s and DMIC copiers constraint stream capabilities based on available NHLT configuration. This allows topology to provide generic configuration that handles more hardware, while filtering unavailable ones at runtime. Signed-off-by: Amadeusz Sławiński <amadeuszx.slawinski@linux.intel.com> Link: https://patch.msgid.link/20250407130851.1726800-1-amadeuszx.slawinski@linux.intel.com Reviewed-by: Cezary Rojewski <cezary.rojewski@intel.com> Signed-off-by: Mark Brown <broonie@kernel.org>
1700 lines
46 KiB
C
1700 lines
46 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
//
|
|
// Copyright(c) 2021-2022 Intel Corporation
|
|
//
|
|
// Authors: Cezary Rojewski <cezary.rojewski@intel.com>
|
|
// Amadeusz Slawinski <amadeuszx.slawinski@linux.intel.com>
|
|
//
|
|
|
|
#include <linux/debugfs.h>
|
|
#include <linux/device.h>
|
|
#include <sound/hda_register.h>
|
|
#include <sound/hdaudio_ext.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <sound/soc-acpi.h>
|
|
#include <sound/soc-acpi-intel-match.h>
|
|
#include <sound/soc-component.h>
|
|
#include "avs.h"
|
|
#include "path.h"
|
|
#include "pcm.h"
|
|
#include "topology.h"
|
|
#include "../../codecs/hda.h"
|
|
|
|
struct avs_dma_data {
|
|
struct avs_tplg_path_template *template;
|
|
struct avs_path *path;
|
|
struct avs_dev *adev;
|
|
|
|
/* LINK-stream utilized in BE operations while HOST in FE ones. */
|
|
union {
|
|
struct hdac_ext_stream *link_stream;
|
|
struct hdac_ext_stream *host_stream;
|
|
};
|
|
|
|
struct snd_pcm_hw_constraint_list rate_list;
|
|
struct snd_pcm_hw_constraint_list channels_list;
|
|
struct snd_pcm_hw_constraint_list sample_bits_list;
|
|
|
|
struct work_struct period_elapsed_work;
|
|
struct snd_pcm_substream *substream;
|
|
};
|
|
|
|
static struct avs_tplg_path_template *
|
|
avs_dai_find_path_template(struct snd_soc_dai *dai, bool is_fe, int direction)
|
|
{
|
|
struct snd_soc_dapm_widget *dw = snd_soc_dai_get_widget(dai, direction);
|
|
struct snd_soc_dapm_path *dp;
|
|
enum snd_soc_dapm_direction dir;
|
|
|
|
if (direction == SNDRV_PCM_STREAM_CAPTURE) {
|
|
dir = is_fe ? SND_SOC_DAPM_DIR_OUT : SND_SOC_DAPM_DIR_IN;
|
|
} else {
|
|
dir = is_fe ? SND_SOC_DAPM_DIR_IN : SND_SOC_DAPM_DIR_OUT;
|
|
}
|
|
|
|
dp = list_first_entry_or_null(&dw->edges[dir], typeof(*dp), list_node[dir]);
|
|
if (!dp)
|
|
return NULL;
|
|
|
|
/* Get the other widget, with actual path template data */
|
|
dw = (dp->source == dw) ? dp->sink : dp->source;
|
|
|
|
return dw->priv;
|
|
}
|
|
|
|
static void avs_period_elapsed_work(struct work_struct *work)
|
|
{
|
|
struct avs_dma_data *data = container_of(work, struct avs_dma_data, period_elapsed_work);
|
|
|
|
snd_pcm_period_elapsed(data->substream);
|
|
}
|
|
|
|
void avs_period_elapsed(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct snd_soc_dai *dai = snd_soc_rtd_to_cpu(rtd, 0);
|
|
struct avs_dma_data *data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
schedule_work(&data->period_elapsed_work);
|
|
}
|
|
|
|
static int hw_rule_param_size(struct snd_pcm_hw_params *params, struct snd_pcm_hw_rule *rule);
|
|
static int avs_hw_constraints_init(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct snd_pcm_hw_constraint_list *r, *c, *s;
|
|
struct avs_tplg_path_template *template;
|
|
struct avs_dma_data *data;
|
|
int ret;
|
|
|
|
ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
r = &(data->rate_list);
|
|
c = &(data->channels_list);
|
|
s = &(data->sample_bits_list);
|
|
|
|
template = avs_dai_find_path_template(dai, !rtd->dai_link->no_pcm, substream->stream);
|
|
ret = avs_path_set_constraint(data->adev, template, r, c, s);
|
|
if (ret <= 0)
|
|
return ret;
|
|
|
|
ret = snd_pcm_hw_constraint_list(runtime, 0, SNDRV_PCM_HW_PARAM_RATE, r);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = snd_pcm_hw_constraint_list(runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS, c);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = snd_pcm_hw_constraint_list(runtime, 0, SNDRV_PCM_HW_PARAM_SAMPLE_BITS, s);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dev *adev = to_avs_dev(dai->component->dev);
|
|
struct avs_tplg_path_template *template;
|
|
struct avs_dma_data *data;
|
|
|
|
template = avs_dai_find_path_template(dai, !rtd->dai_link->no_pcm, substream->stream);
|
|
if (!template) {
|
|
dev_err(dai->dev, "no %s path for dai %s, invalid tplg?\n",
|
|
snd_pcm_stream_str(substream), dai->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
data = kzalloc(sizeof(*data), GFP_KERNEL);
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
data->substream = substream;
|
|
data->template = template;
|
|
data->adev = adev;
|
|
INIT_WORK(&data->period_elapsed_work, avs_period_elapsed_work);
|
|
snd_soc_dai_set_dma_data(dai, substream, data);
|
|
|
|
if (rtd->dai_link->ignore_suspend)
|
|
adev->num_lp_paths++;
|
|
|
|
return avs_hw_constraints_init(substream, dai);
|
|
}
|
|
|
|
static void avs_dai_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dma_data *data;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
if (rtd->dai_link->ignore_suspend)
|
|
data->adev->num_lp_paths--;
|
|
|
|
kfree(data->rate_list.list);
|
|
kfree(data->channels_list.list);
|
|
kfree(data->sample_bits_list.list);
|
|
|
|
snd_soc_dai_set_dma_data(dai, substream, NULL);
|
|
kfree(data);
|
|
}
|
|
|
|
static int avs_dai_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *fe_hw_params,
|
|
struct snd_pcm_hw_params *be_hw_params, struct snd_soc_dai *dai,
|
|
int dma_id)
|
|
{
|
|
struct avs_dma_data *data;
|
|
struct avs_path *path;
|
|
int ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
dev_dbg(dai->dev, "%s FE hw_params str %p rtd %p",
|
|
__func__, substream, substream->runtime);
|
|
dev_dbg(dai->dev, "rate %d chn %d vbd %d bd %d\n",
|
|
params_rate(fe_hw_params), params_channels(fe_hw_params),
|
|
params_width(fe_hw_params), params_physical_width(fe_hw_params));
|
|
|
|
dev_dbg(dai->dev, "%s BE hw_params str %p rtd %p",
|
|
__func__, substream, substream->runtime);
|
|
dev_dbg(dai->dev, "rate %d chn %d vbd %d bd %d\n",
|
|
params_rate(be_hw_params), params_channels(be_hw_params),
|
|
params_width(be_hw_params), params_physical_width(be_hw_params));
|
|
|
|
path = avs_path_create(data->adev, dma_id, data->template, fe_hw_params, be_hw_params);
|
|
if (IS_ERR(path)) {
|
|
ret = PTR_ERR(path);
|
|
dev_err(dai->dev, "create path failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
data->path = path;
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_be_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *be_hw_params, struct snd_soc_dai *dai,
|
|
int dma_id)
|
|
{
|
|
struct snd_pcm_hw_params *fe_hw_params = NULL;
|
|
struct snd_soc_pcm_runtime *fe, *be;
|
|
struct snd_soc_dpcm *dpcm;
|
|
|
|
be = snd_soc_substream_to_rtd(substream);
|
|
/* dpcm_fe_dai_open() guarantees the list is not empty at this point. */
|
|
for_each_dpcm_fe(be, substream->stream, dpcm) {
|
|
fe = dpcm->fe;
|
|
fe_hw_params = &fe->dpcm[substream->stream].hw_params;
|
|
}
|
|
|
|
return avs_dai_hw_params(substream, fe_hw_params, be_hw_params, dai, dma_id);
|
|
}
|
|
|
|
static int avs_dai_prepare(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
int ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (!data->path)
|
|
return 0;
|
|
|
|
ret = avs_path_reset(data->path);
|
|
if (ret < 0) {
|
|
dev_err(dai->dev, "reset path failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "pause path failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_nonhda_be_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *hw_params, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (data->path)
|
|
return 0;
|
|
|
|
/* Actual port-id comes from topology. */
|
|
return avs_dai_be_hw_params(substream, hw_params, dai, 0);
|
|
}
|
|
|
|
static int avs_dai_nonhda_be_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
|
|
dev_dbg(dai->dev, "%s: %s\n", __func__, dai->name);
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (data->path) {
|
|
avs_path_free(data->path);
|
|
data->path = NULL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_nonhda_be_trigger(struct snd_pcm_substream *substream, int cmd,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dma_data *data;
|
|
int ret = 0;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0) {
|
|
dev_err(dai->dev, "pause BE path failed: %d\n", ret);
|
|
break;
|
|
}
|
|
|
|
ret = avs_path_run(data->path, AVS_TPLG_TRIGGER_AUTO);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "run BE path failed: %d\n", ret);
|
|
break;
|
|
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "pause BE path failed: %d\n", ret);
|
|
|
|
ret = avs_path_reset(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "reset BE path failed: %d\n", ret);
|
|
break;
|
|
|
|
default:
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct snd_soc_dai_ops avs_dai_nonhda_be_ops = {
|
|
.startup = avs_dai_startup,
|
|
.shutdown = avs_dai_shutdown,
|
|
.hw_params = avs_dai_nonhda_be_hw_params,
|
|
.hw_free = avs_dai_nonhda_be_hw_free,
|
|
.prepare = avs_dai_prepare,
|
|
.trigger = avs_dai_nonhda_be_trigger,
|
|
};
|
|
|
|
static int avs_dai_hda_be_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct hdac_ext_stream *link_stream;
|
|
struct avs_dma_data *data;
|
|
struct hda_codec *codec;
|
|
int ret;
|
|
|
|
ret = avs_dai_startup(substream, dai);
|
|
if (ret)
|
|
return ret;
|
|
|
|
codec = dev_to_hda_codec(snd_soc_rtd_to_codec(rtd, 0)->dev);
|
|
link_stream = snd_hdac_ext_stream_assign(&codec->bus->core, substream,
|
|
HDAC_EXT_STREAM_TYPE_LINK);
|
|
if (!link_stream) {
|
|
avs_dai_shutdown(substream, dai);
|
|
return -EBUSY;
|
|
}
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
data->link_stream = link_stream;
|
|
substream->runtime->private_data = link_stream;
|
|
return 0;
|
|
}
|
|
|
|
static void avs_dai_hda_be_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
snd_hdac_ext_stream_release(data->link_stream, HDAC_EXT_STREAM_TYPE_LINK);
|
|
substream->runtime->private_data = NULL;
|
|
avs_dai_shutdown(substream, dai);
|
|
}
|
|
|
|
static int avs_dai_hda_be_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *hw_params, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (data->path)
|
|
return 0;
|
|
|
|
return avs_dai_be_hw_params(substream, hw_params, dai,
|
|
hdac_stream(data->link_stream)->stream_tag - 1);
|
|
}
|
|
|
|
static int avs_dai_hda_be_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct hdac_ext_stream *link_stream;
|
|
struct hdac_ext_link *link;
|
|
struct hda_codec *codec;
|
|
|
|
dev_dbg(dai->dev, "%s: %s\n", __func__, dai->name);
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (!data->path)
|
|
return 0;
|
|
|
|
link_stream = data->link_stream;
|
|
link_stream->link_prepared = false;
|
|
avs_path_free(data->path);
|
|
data->path = NULL;
|
|
|
|
/* clear link <-> stream mapping */
|
|
codec = dev_to_hda_codec(snd_soc_rtd_to_codec(rtd, 0)->dev);
|
|
link = snd_hdac_ext_bus_get_hlink_by_addr(&codec->bus->core, codec->core.addr);
|
|
if (!link)
|
|
return -EINVAL;
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
snd_hdac_ext_bus_link_clear_stream_id(link, hdac_stream(link_stream)->stream_tag);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_hda_be_prepare(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
const struct snd_soc_pcm_stream *stream_info;
|
|
struct hdac_ext_stream *link_stream;
|
|
struct hdac_ext_link *link;
|
|
struct avs_dma_data *data;
|
|
struct hda_codec *codec;
|
|
struct hdac_bus *bus;
|
|
unsigned int format_val;
|
|
unsigned int bits;
|
|
int ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
link_stream = data->link_stream;
|
|
|
|
if (link_stream->link_prepared)
|
|
return 0;
|
|
|
|
codec = dev_to_hda_codec(snd_soc_rtd_to_codec(rtd, 0)->dev);
|
|
bus = &codec->bus->core;
|
|
stream_info = snd_soc_dai_get_pcm_stream(dai, substream->stream);
|
|
bits = snd_hdac_stream_format_bits(runtime->format, runtime->subformat,
|
|
stream_info->sig_bits);
|
|
format_val = snd_hdac_stream_format(runtime->channels, bits, runtime->rate);
|
|
|
|
snd_hdac_ext_stream_decouple(bus, link_stream, true);
|
|
snd_hdac_ext_stream_reset(link_stream);
|
|
snd_hdac_ext_stream_setup(link_stream, format_val);
|
|
|
|
link = snd_hdac_ext_bus_get_hlink_by_addr(bus, codec->core.addr);
|
|
if (!link)
|
|
return -EINVAL;
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
snd_hdac_ext_bus_link_set_stream_id(link, hdac_stream(link_stream)->stream_tag);
|
|
|
|
ret = avs_dai_prepare(substream, dai);
|
|
if (ret)
|
|
return ret;
|
|
|
|
link_stream->link_prepared = true;
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_hda_be_trigger(struct snd_pcm_substream *substream, int cmd,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dma_data *data;
|
|
int ret = 0;
|
|
|
|
dev_dbg(dai->dev, "entry %s cmd=%d\n", __func__, cmd);
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
snd_hdac_ext_stream_start(data->link_stream);
|
|
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0) {
|
|
dev_err(dai->dev, "pause BE path failed: %d\n", ret);
|
|
break;
|
|
}
|
|
|
|
ret = avs_path_run(data->path, AVS_TPLG_TRIGGER_AUTO);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "run BE path failed: %d\n", ret);
|
|
break;
|
|
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "pause BE path failed: %d\n", ret);
|
|
|
|
snd_hdac_ext_stream_clear(data->link_stream);
|
|
|
|
ret = avs_path_reset(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "reset BE path failed: %d\n", ret);
|
|
break;
|
|
|
|
default:
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct snd_soc_dai_ops avs_dai_hda_be_ops = {
|
|
.startup = avs_dai_hda_be_startup,
|
|
.shutdown = avs_dai_hda_be_shutdown,
|
|
.hw_params = avs_dai_hda_be_hw_params,
|
|
.hw_free = avs_dai_hda_be_hw_free,
|
|
.prepare = avs_dai_hda_be_prepare,
|
|
.trigger = avs_dai_hda_be_trigger,
|
|
};
|
|
|
|
static int hw_rule_param_size(struct snd_pcm_hw_params *params, struct snd_pcm_hw_rule *rule)
|
|
{
|
|
struct snd_interval *interval = hw_param_interval(params, rule->var);
|
|
struct snd_interval to;
|
|
|
|
snd_interval_any(&to);
|
|
to.integer = interval->integer;
|
|
to.max = interval->max;
|
|
/*
|
|
* Commonly 2ms buffer size is used in HDA scenarios whereas 4ms is used
|
|
* when streaming through GPDMA. Align to the latter to account for both.
|
|
*/
|
|
to.min = params_rate(params) / 1000 * 4;
|
|
|
|
if (rule->var == SNDRV_PCM_HW_PARAM_PERIOD_SIZE)
|
|
to.min /= params_periods(params);
|
|
|
|
return snd_interval_refine(interval, &to);
|
|
}
|
|
|
|
static int avs_pcm_hw_constraints_init(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
int ret;
|
|
|
|
ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Avoid wrap-around with wall-clock. */
|
|
ret = snd_pcm_hw_constraint_minmax(runtime, SNDRV_PCM_HW_PARAM_BUFFER_TIME, 20, 178000000);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Adjust buffer and period size based on the audio format. */
|
|
snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_BUFFER_SIZE, hw_rule_param_size, NULL,
|
|
SNDRV_PCM_HW_PARAM_FORMAT, SNDRV_PCM_HW_PARAM_CHANNELS,
|
|
SNDRV_PCM_HW_PARAM_RATE, -1);
|
|
snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, hw_rule_param_size, NULL,
|
|
SNDRV_PCM_HW_PARAM_FORMAT, SNDRV_PCM_HW_PARAM_CHANNELS,
|
|
SNDRV_PCM_HW_PARAM_RATE, -1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_fe_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct hdac_ext_stream *host_stream;
|
|
struct avs_dma_data *data;
|
|
struct hdac_bus *bus;
|
|
int ret;
|
|
|
|
ret = avs_pcm_hw_constraints_init(substream);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = avs_dai_startup(substream, dai);
|
|
if (ret)
|
|
return ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
bus = &data->adev->base.core;
|
|
|
|
host_stream = snd_hdac_ext_stream_assign(bus, substream, HDAC_EXT_STREAM_TYPE_HOST);
|
|
if (!host_stream) {
|
|
avs_dai_shutdown(substream, dai);
|
|
return -EBUSY;
|
|
}
|
|
|
|
data->host_stream = host_stream;
|
|
snd_pcm_set_sync(substream);
|
|
|
|
dev_dbg(dai->dev, "%s fe STARTUP tag %d str %p",
|
|
__func__, hdac_stream(host_stream)->stream_tag, substream);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void avs_dai_fe_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
snd_hdac_ext_stream_release(data->host_stream, HDAC_EXT_STREAM_TYPE_HOST);
|
|
avs_dai_shutdown(substream, dai);
|
|
}
|
|
|
|
static int avs_dai_fe_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *hw_params, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_pcm_hw_params *be_hw_params = NULL;
|
|
struct snd_soc_pcm_runtime *fe, *be;
|
|
struct snd_soc_dpcm *dpcm;
|
|
struct avs_dma_data *data;
|
|
struct hdac_ext_stream *host_stream;
|
|
int ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (data->path)
|
|
return 0;
|
|
|
|
host_stream = data->host_stream;
|
|
|
|
hdac_stream(host_stream)->bufsize = 0;
|
|
hdac_stream(host_stream)->period_bytes = 0;
|
|
hdac_stream(host_stream)->format_val = 0;
|
|
|
|
fe = snd_soc_substream_to_rtd(substream);
|
|
/* dpcm_fe_dai_open() guarantees the list is not empty at this point. */
|
|
for_each_dpcm_be(fe, substream->stream, dpcm) {
|
|
be = dpcm->be;
|
|
be_hw_params = &be->dpcm[substream->stream].hw_params;
|
|
}
|
|
|
|
ret = avs_dai_hw_params(substream, hw_params, be_hw_params, dai,
|
|
hdac_stream(host_stream)->stream_tag - 1);
|
|
if (ret)
|
|
goto create_err;
|
|
|
|
ret = avs_path_bind(data->path);
|
|
if (ret < 0) {
|
|
dev_err(dai->dev, "bind FE <-> BE failed: %d\n", ret);
|
|
goto bind_err;
|
|
}
|
|
|
|
return 0;
|
|
|
|
bind_err:
|
|
avs_path_free(data->path);
|
|
data->path = NULL;
|
|
create_err:
|
|
snd_pcm_lib_free_pages(substream);
|
|
return ret;
|
|
}
|
|
|
|
static int __avs_dai_fe_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct avs_dma_data *data;
|
|
struct hdac_ext_stream *host_stream;
|
|
int ret;
|
|
|
|
dev_dbg(dai->dev, "%s fe HW_FREE str %p rtd %p",
|
|
__func__, substream, substream->runtime);
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
if (!data->path)
|
|
return 0;
|
|
|
|
host_stream = data->host_stream;
|
|
|
|
ret = avs_path_unbind(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "unbind FE <-> BE failed: %d\n", ret);
|
|
|
|
avs_path_free(data->path);
|
|
data->path = NULL;
|
|
snd_hdac_stream_cleanup(hdac_stream(host_stream));
|
|
hdac_stream(host_stream)->prepared = false;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_fe_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
int ret;
|
|
|
|
ret = __avs_dai_fe_hw_free(substream, dai);
|
|
snd_pcm_lib_free_pages(substream);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_fe_prepare(struct snd_pcm_substream *substream, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
const struct snd_soc_pcm_stream *stream_info;
|
|
struct avs_dma_data *data;
|
|
struct hdac_ext_stream *host_stream;
|
|
unsigned int format_val;
|
|
struct hdac_bus *bus;
|
|
unsigned int bits;
|
|
int ret;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
host_stream = data->host_stream;
|
|
|
|
if (hdac_stream(host_stream)->prepared)
|
|
return 0;
|
|
|
|
bus = hdac_stream(host_stream)->bus;
|
|
snd_hdac_ext_stream_decouple(bus, data->host_stream, true);
|
|
snd_hdac_stream_reset(hdac_stream(host_stream));
|
|
|
|
stream_info = snd_soc_dai_get_pcm_stream(dai, substream->stream);
|
|
bits = snd_hdac_stream_format_bits(runtime->format, runtime->subformat,
|
|
stream_info->sig_bits);
|
|
format_val = snd_hdac_stream_format(runtime->channels, bits, runtime->rate);
|
|
|
|
ret = snd_hdac_stream_set_params(hdac_stream(host_stream), format_val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = snd_hdac_ext_host_stream_setup(host_stream, false);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = avs_dai_prepare(substream, dai);
|
|
if (ret)
|
|
return ret;
|
|
|
|
hdac_stream(host_stream)->prepared = true;
|
|
return 0;
|
|
}
|
|
|
|
static void avs_hda_stream_start(struct hdac_bus *bus, struct hdac_ext_stream *host_stream)
|
|
{
|
|
struct hdac_stream *first_running = NULL;
|
|
struct hdac_stream *pos;
|
|
struct avs_dev *adev = hdac_to_avs(bus);
|
|
|
|
list_for_each_entry(pos, &bus->stream_list, list) {
|
|
if (pos->running) {
|
|
if (first_running)
|
|
break; /* more than one running */
|
|
first_running = pos;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If host_stream is a CAPTURE stream and will be the only one running,
|
|
* disable L1SEN to avoid sound clipping.
|
|
*/
|
|
if (!first_running) {
|
|
if (hdac_stream(host_stream)->direction == SNDRV_PCM_STREAM_CAPTURE)
|
|
avs_hda_l1sen_enable(adev, false);
|
|
snd_hdac_stream_start(hdac_stream(host_stream));
|
|
return;
|
|
}
|
|
|
|
snd_hdac_stream_start(hdac_stream(host_stream));
|
|
/*
|
|
* If host_stream is the first stream to break the rule above,
|
|
* re-enable L1SEN.
|
|
*/
|
|
if (list_entry_is_head(pos, &bus->stream_list, list) &&
|
|
first_running->direction == SNDRV_PCM_STREAM_CAPTURE)
|
|
avs_hda_l1sen_enable(adev, true);
|
|
}
|
|
|
|
static void avs_hda_stream_stop(struct hdac_bus *bus, struct hdac_ext_stream *host_stream)
|
|
{
|
|
struct hdac_stream *first_running = NULL;
|
|
struct hdac_stream *pos;
|
|
struct avs_dev *adev = hdac_to_avs(bus);
|
|
|
|
list_for_each_entry(pos, &bus->stream_list, list) {
|
|
if (pos == hdac_stream(host_stream))
|
|
continue; /* ignore stream that is about to be stopped */
|
|
if (pos->running) {
|
|
if (first_running)
|
|
break; /* more than one running */
|
|
first_running = pos;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If host_stream is a CAPTURE stream and is the only one running,
|
|
* re-enable L1SEN.
|
|
*/
|
|
if (!first_running) {
|
|
snd_hdac_stream_stop(hdac_stream(host_stream));
|
|
if (hdac_stream(host_stream)->direction == SNDRV_PCM_STREAM_CAPTURE)
|
|
avs_hda_l1sen_enable(adev, true);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If by stopping host_stream there is only a single, CAPTURE stream running
|
|
* left, disable L1SEN to avoid sound clipping.
|
|
*/
|
|
if (list_entry_is_head(pos, &bus->stream_list, list) &&
|
|
first_running->direction == SNDRV_PCM_STREAM_CAPTURE)
|
|
avs_hda_l1sen_enable(adev, false);
|
|
|
|
snd_hdac_stream_stop(hdac_stream(host_stream));
|
|
}
|
|
|
|
static int avs_dai_fe_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dma_data *data;
|
|
struct hdac_ext_stream *host_stream;
|
|
struct hdac_bus *bus;
|
|
unsigned long flags;
|
|
int ret = 0;
|
|
|
|
data = snd_soc_dai_get_dma_data(dai, substream);
|
|
host_stream = data->host_stream;
|
|
bus = hdac_stream(host_stream)->bus;
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
spin_lock_irqsave(&bus->reg_lock, flags);
|
|
avs_hda_stream_start(bus, host_stream);
|
|
spin_unlock_irqrestore(&bus->reg_lock, flags);
|
|
|
|
/* Timeout on DRSM poll shall not stop the resume so ignore the result. */
|
|
if (cmd == SNDRV_PCM_TRIGGER_RESUME)
|
|
snd_hdac_stream_wait_drsm(hdac_stream(host_stream));
|
|
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0) {
|
|
dev_err(dai->dev, "pause FE path failed: %d\n", ret);
|
|
break;
|
|
}
|
|
|
|
ret = avs_path_run(data->path, AVS_TPLG_TRIGGER_AUTO);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "run FE path failed: %d\n", ret);
|
|
|
|
break;
|
|
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
if (rtd->dai_link->ignore_suspend)
|
|
break;
|
|
fallthrough;
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
ret = avs_path_pause(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "pause FE path failed: %d\n", ret);
|
|
|
|
spin_lock_irqsave(&bus->reg_lock, flags);
|
|
avs_hda_stream_stop(bus, host_stream);
|
|
spin_unlock_irqrestore(&bus->reg_lock, flags);
|
|
|
|
ret = avs_path_reset(data->path);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "reset FE path failed: %d\n", ret);
|
|
break;
|
|
|
|
default:
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
const struct snd_soc_dai_ops avs_dai_fe_ops = {
|
|
.startup = avs_dai_fe_startup,
|
|
.shutdown = avs_dai_fe_shutdown,
|
|
.hw_params = avs_dai_fe_hw_params,
|
|
.hw_free = avs_dai_fe_hw_free,
|
|
.prepare = avs_dai_fe_prepare,
|
|
.trigger = avs_dai_fe_trigger,
|
|
};
|
|
|
|
static ssize_t topology_name_read(struct file *file, char __user *user_buf, size_t count,
|
|
loff_t *ppos)
|
|
{
|
|
struct snd_soc_component *component = file->private_data;
|
|
struct snd_soc_card *card = component->card;
|
|
struct snd_soc_acpi_mach *mach = dev_get_platdata(card->dev);
|
|
char buf[64];
|
|
size_t len;
|
|
|
|
len = scnprintf(buf, sizeof(buf), "%s/%s\n", component->driver->topology_name_prefix,
|
|
mach->tplg_filename);
|
|
|
|
return simple_read_from_buffer(user_buf, count, ppos, buf, len);
|
|
}
|
|
|
|
static const struct file_operations topology_name_fops = {
|
|
.open = simple_open,
|
|
.read = topology_name_read,
|
|
.llseek = default_llseek,
|
|
};
|
|
|
|
static int avs_component_load_libraries(struct avs_soc_component *acomp)
|
|
{
|
|
struct avs_tplg *tplg = acomp->tplg;
|
|
struct avs_dev *adev = to_avs_dev(acomp->base.dev);
|
|
int ret;
|
|
|
|
if (!tplg->num_libs)
|
|
return 0;
|
|
|
|
/* Parent device may be asleep and library loading involves IPCs. */
|
|
ret = pm_runtime_resume_and_get(adev->dev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
avs_hda_power_gating_enable(adev, false);
|
|
avs_hda_clock_gating_enable(adev, false);
|
|
avs_hda_l1sen_enable(adev, false);
|
|
|
|
ret = avs_dsp_load_libraries(adev, tplg->libs, tplg->num_libs);
|
|
|
|
avs_hda_l1sen_enable(adev, true);
|
|
avs_hda_clock_gating_enable(adev, true);
|
|
avs_hda_power_gating_enable(adev, true);
|
|
|
|
if (!ret)
|
|
ret = avs_module_info_init(adev, false);
|
|
|
|
pm_runtime_mark_last_busy(adev->dev);
|
|
pm_runtime_put_autosuspend(adev->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_component_probe(struct snd_soc_component *component)
|
|
{
|
|
struct snd_soc_card *card = component->card;
|
|
struct snd_soc_acpi_mach *mach;
|
|
struct avs_soc_component *acomp;
|
|
struct avs_dev *adev;
|
|
char *filename;
|
|
int ret;
|
|
|
|
dev_dbg(card->dev, "probing %s card %s\n", component->name, card->name);
|
|
mach = dev_get_platdata(card->dev);
|
|
acomp = to_avs_soc_component(component);
|
|
adev = to_avs_dev(component->dev);
|
|
|
|
acomp->tplg = avs_tplg_new(component);
|
|
if (!acomp->tplg)
|
|
return -ENOMEM;
|
|
|
|
if (!mach->tplg_filename)
|
|
goto finalize;
|
|
|
|
/* Load specified topology and create debugfs for it. */
|
|
filename = kasprintf(GFP_KERNEL, "%s/%s", component->driver->topology_name_prefix,
|
|
mach->tplg_filename);
|
|
if (!filename)
|
|
return -ENOMEM;
|
|
|
|
ret = avs_load_topology(component, filename);
|
|
kfree(filename);
|
|
if (ret == -ENOENT && !strncmp(mach->tplg_filename, "hda-", 4)) {
|
|
unsigned int vendor_id;
|
|
|
|
if (sscanf(mach->tplg_filename, "hda-%08x-tplg.bin", &vendor_id) != 1)
|
|
return ret;
|
|
|
|
if (((vendor_id >> 16) & 0xFFFF) == 0x8086)
|
|
mach->tplg_filename = devm_kasprintf(adev->dev, GFP_KERNEL,
|
|
"hda-8086-generic-tplg.bin");
|
|
else
|
|
mach->tplg_filename = devm_kasprintf(adev->dev, GFP_KERNEL,
|
|
"hda-generic-tplg.bin");
|
|
if (!mach->tplg_filename)
|
|
return -ENOMEM;
|
|
filename = kasprintf(GFP_KERNEL, "%s/%s", component->driver->topology_name_prefix,
|
|
mach->tplg_filename);
|
|
if (!filename)
|
|
return -ENOMEM;
|
|
|
|
dev_info(card->dev, "trying to load fallback topology %s\n", mach->tplg_filename);
|
|
ret = avs_load_topology(component, filename);
|
|
kfree(filename);
|
|
}
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = avs_component_load_libraries(acomp);
|
|
if (ret < 0) {
|
|
dev_err(card->dev, "libraries loading failed: %d\n", ret);
|
|
goto err_load_libs;
|
|
}
|
|
|
|
finalize:
|
|
debugfs_create_file("topology_name", 0444, component->debugfs_root, component,
|
|
&topology_name_fops);
|
|
|
|
mutex_lock(&adev->comp_list_mutex);
|
|
list_add_tail(&acomp->node, &adev->comp_list);
|
|
mutex_unlock(&adev->comp_list_mutex);
|
|
|
|
return 0;
|
|
|
|
err_load_libs:
|
|
avs_remove_topology(component);
|
|
return ret;
|
|
}
|
|
|
|
static void avs_component_remove(struct snd_soc_component *component)
|
|
{
|
|
struct avs_soc_component *acomp = to_avs_soc_component(component);
|
|
struct snd_soc_acpi_mach *mach;
|
|
struct avs_dev *adev = to_avs_dev(component->dev);
|
|
int ret;
|
|
|
|
mach = dev_get_platdata(component->card->dev);
|
|
|
|
mutex_lock(&adev->comp_list_mutex);
|
|
list_del(&acomp->node);
|
|
mutex_unlock(&adev->comp_list_mutex);
|
|
|
|
if (mach->tplg_filename) {
|
|
ret = avs_remove_topology(component);
|
|
if (ret < 0)
|
|
dev_err(component->dev, "unload topology failed: %d\n", ret);
|
|
}
|
|
}
|
|
|
|
static int avs_dai_resume_hw_params(struct snd_soc_dai *dai, struct avs_dma_data *data)
|
|
{
|
|
struct snd_pcm_substream *substream;
|
|
struct snd_soc_pcm_runtime *rtd;
|
|
int ret;
|
|
|
|
substream = data->substream;
|
|
rtd = snd_soc_substream_to_rtd(substream);
|
|
|
|
ret = dai->driver->ops->hw_params(substream, &rtd->dpcm[substream->stream].hw_params, dai);
|
|
if (ret)
|
|
dev_err(dai->dev, "hw_params on resume failed: %d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_resume_fe_prepare(struct snd_soc_dai *dai, struct avs_dma_data *data)
|
|
{
|
|
struct hdac_ext_stream *host_stream;
|
|
struct hdac_stream *hstream;
|
|
struct hdac_bus *bus;
|
|
int ret;
|
|
|
|
host_stream = data->host_stream;
|
|
hstream = hdac_stream(host_stream);
|
|
bus = hdac_stream(host_stream)->bus;
|
|
|
|
/* Set DRSM before programming stream and position registers. */
|
|
snd_hdac_stream_drsm_enable(bus, true, hstream->index);
|
|
|
|
ret = dai->driver->ops->prepare(data->substream, dai);
|
|
if (ret) {
|
|
dev_err(dai->dev, "prepare FE on resume failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
writel(host_stream->pphcllpl, host_stream->pphc_addr + AZX_REG_PPHCLLPL);
|
|
writel(host_stream->pphcllpu, host_stream->pphc_addr + AZX_REG_PPHCLLPU);
|
|
writel(host_stream->pphcldpl, host_stream->pphc_addr + AZX_REG_PPHCLDPL);
|
|
writel(host_stream->pphcldpu, host_stream->pphc_addr + AZX_REG_PPHCLDPU);
|
|
|
|
/* As per HW spec recommendation, program LPIB and DPIB to the same value. */
|
|
snd_hdac_stream_set_lpib(hstream, hstream->lpib);
|
|
snd_hdac_stream_set_dpibr(bus, hstream, hstream->lpib);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_dai_resume_be_prepare(struct snd_soc_dai *dai, struct avs_dma_data *data)
|
|
{
|
|
int ret;
|
|
|
|
ret = dai->driver->ops->prepare(data->substream, dai);
|
|
if (ret)
|
|
dev_err(dai->dev, "prepare BE on resume failed: %d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_suspend_fe_hw_free(struct snd_soc_dai *dai, struct avs_dma_data *data)
|
|
{
|
|
struct hdac_ext_stream *host_stream;
|
|
int ret;
|
|
|
|
host_stream = data->host_stream;
|
|
|
|
/* Store position addresses so we can resume from them later on. */
|
|
hdac_stream(host_stream)->lpib = snd_hdac_stream_get_pos_lpib(hdac_stream(host_stream));
|
|
host_stream->pphcllpl = readl(host_stream->pphc_addr + AZX_REG_PPHCLLPL);
|
|
host_stream->pphcllpu = readl(host_stream->pphc_addr + AZX_REG_PPHCLLPU);
|
|
host_stream->pphcldpl = readl(host_stream->pphc_addr + AZX_REG_PPHCLDPL);
|
|
host_stream->pphcldpu = readl(host_stream->pphc_addr + AZX_REG_PPHCLDPU);
|
|
|
|
ret = __avs_dai_fe_hw_free(data->substream, dai);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "hw_free FE on suspend failed: %d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_dai_suspend_be_hw_free(struct snd_soc_dai *dai, struct avs_dma_data *data)
|
|
{
|
|
int ret;
|
|
|
|
ret = dai->driver->ops->hw_free(data->substream, dai);
|
|
if (ret < 0)
|
|
dev_err(dai->dev, "hw_free BE on suspend failed: %d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int avs_component_pm_op(struct snd_soc_component *component, bool be,
|
|
int (*op)(struct snd_soc_dai *, struct avs_dma_data *))
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd;
|
|
struct avs_dma_data *data;
|
|
struct snd_soc_dai *dai;
|
|
int ret;
|
|
|
|
for_each_component_dais(component, dai) {
|
|
data = snd_soc_dai_dma_data_get_playback(dai);
|
|
if (data) {
|
|
rtd = snd_soc_substream_to_rtd(data->substream);
|
|
if (rtd->dai_link->no_pcm == be && !rtd->dai_link->ignore_suspend) {
|
|
ret = op(dai, data);
|
|
if (ret < 0) {
|
|
__snd_pcm_set_state(data->substream->runtime,
|
|
SNDRV_PCM_STATE_DISCONNECTED);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
data = snd_soc_dai_dma_data_get_capture(dai);
|
|
if (data) {
|
|
rtd = snd_soc_substream_to_rtd(data->substream);
|
|
if (rtd->dai_link->no_pcm == be && !rtd->dai_link->ignore_suspend) {
|
|
ret = op(dai, data);
|
|
if (ret < 0) {
|
|
__snd_pcm_set_state(data->substream->runtime,
|
|
SNDRV_PCM_STATE_DISCONNECTED);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avs_component_resume_hw_params(struct snd_soc_component *component, bool be)
|
|
{
|
|
return avs_component_pm_op(component, be, &avs_dai_resume_hw_params);
|
|
}
|
|
|
|
static int avs_component_resume_prepare(struct snd_soc_component *component, bool be)
|
|
{
|
|
int (*prepare_cb)(struct snd_soc_dai *dai, struct avs_dma_data *data);
|
|
|
|
if (be)
|
|
prepare_cb = &avs_dai_resume_be_prepare;
|
|
else
|
|
prepare_cb = &avs_dai_resume_fe_prepare;
|
|
|
|
return avs_component_pm_op(component, be, prepare_cb);
|
|
}
|
|
|
|
static int avs_component_suspend_hw_free(struct snd_soc_component *component, bool be)
|
|
{
|
|
int (*hw_free_cb)(struct snd_soc_dai *dai, struct avs_dma_data *data);
|
|
|
|
if (be)
|
|
hw_free_cb = &avs_dai_suspend_be_hw_free;
|
|
else
|
|
hw_free_cb = &avs_dai_suspend_fe_hw_free;
|
|
|
|
return avs_component_pm_op(component, be, hw_free_cb);
|
|
}
|
|
|
|
static int avs_component_suspend(struct snd_soc_component *component)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* When freeing paths, FEs need to be first as they perform
|
|
* path unbinding.
|
|
*/
|
|
ret = avs_component_suspend_hw_free(component, false);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return avs_component_suspend_hw_free(component, true);
|
|
}
|
|
|
|
static int avs_component_resume(struct snd_soc_component *component)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* When creating paths, FEs need to be last as they perform
|
|
* path binding.
|
|
*/
|
|
ret = avs_component_resume_hw_params(component, true);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = avs_component_resume_hw_params(component, false);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* It is expected that the LINK stream is prepared first. */
|
|
ret = avs_component_resume_prepare(component, true);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return avs_component_resume_prepare(component, false);
|
|
}
|
|
|
|
static const struct snd_pcm_hardware avs_pcm_hardware = {
|
|
.info = SNDRV_PCM_INFO_MMAP |
|
|
SNDRV_PCM_INFO_MMAP_VALID |
|
|
SNDRV_PCM_INFO_INTERLEAVED |
|
|
SNDRV_PCM_INFO_PAUSE |
|
|
SNDRV_PCM_INFO_RESUME |
|
|
SNDRV_PCM_INFO_NO_PERIOD_WAKEUP,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE |
|
|
SNDRV_PCM_FMTBIT_S32_LE,
|
|
.subformats = SNDRV_PCM_SUBFMTBIT_MSBITS_20 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_24 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_MAX,
|
|
.buffer_bytes_max = AZX_MAX_BUF_SIZE,
|
|
.period_bytes_min = 128,
|
|
.period_bytes_max = AZX_MAX_BUF_SIZE / 2,
|
|
.periods_min = 2,
|
|
.periods_max = AZX_MAX_FRAG,
|
|
.fifo_size = 0,
|
|
};
|
|
|
|
static int avs_component_open(struct snd_soc_component *component,
|
|
struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
|
|
/* only FE DAI links are handled here */
|
|
if (rtd->dai_link->no_pcm)
|
|
return 0;
|
|
|
|
return snd_soc_set_runtime_hwparams(substream, &avs_pcm_hardware);
|
|
}
|
|
|
|
static unsigned int avs_hda_stream_dpib_read(struct hdac_ext_stream *stream)
|
|
{
|
|
return readl(hdac_stream(stream)->bus->remap_addr + AZX_REG_VS_SDXDPIB_XBASE +
|
|
(AZX_REG_VS_SDXDPIB_XINTERVAL * hdac_stream(stream)->index));
|
|
}
|
|
|
|
static snd_pcm_uframes_t
|
|
avs_component_pointer(struct snd_soc_component *component, struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
struct avs_dma_data *data;
|
|
struct hdac_ext_stream *host_stream;
|
|
unsigned int pos;
|
|
|
|
data = snd_soc_dai_get_dma_data(snd_soc_rtd_to_cpu(rtd, 0), substream);
|
|
if (!data->host_stream)
|
|
return 0;
|
|
|
|
host_stream = data->host_stream;
|
|
pos = avs_hda_stream_dpib_read(host_stream);
|
|
|
|
if (pos >= hdac_stream(host_stream)->bufsize)
|
|
pos = 0;
|
|
|
|
return bytes_to_frames(substream->runtime, pos);
|
|
}
|
|
|
|
static int avs_component_mmap(struct snd_soc_component *component,
|
|
struct snd_pcm_substream *substream,
|
|
struct vm_area_struct *vma)
|
|
{
|
|
return snd_pcm_lib_default_mmap(substream, vma);
|
|
}
|
|
|
|
#define MAX_PREALLOC_SIZE (32 * 1024 * 1024)
|
|
|
|
static int avs_component_construct(struct snd_soc_component *component,
|
|
struct snd_soc_pcm_runtime *rtd)
|
|
{
|
|
struct snd_soc_dai *dai = snd_soc_rtd_to_cpu(rtd, 0);
|
|
struct snd_pcm *pcm = rtd->pcm;
|
|
|
|
if (dai->driver->playback.channels_min)
|
|
snd_pcm_set_managed_buffer(pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream,
|
|
SNDRV_DMA_TYPE_DEV_SG, component->dev, 0,
|
|
MAX_PREALLOC_SIZE);
|
|
|
|
if (dai->driver->capture.channels_min)
|
|
snd_pcm_set_managed_buffer(pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream,
|
|
SNDRV_DMA_TYPE_DEV_SG, component->dev, 0,
|
|
MAX_PREALLOC_SIZE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct snd_soc_component_driver avs_component_driver = {
|
|
.name = "avs-pcm",
|
|
.probe = avs_component_probe,
|
|
.remove = avs_component_remove,
|
|
.suspend = avs_component_suspend,
|
|
.resume = avs_component_resume,
|
|
.open = avs_component_open,
|
|
.pointer = avs_component_pointer,
|
|
.mmap = avs_component_mmap,
|
|
.pcm_construct = avs_component_construct,
|
|
.module_get_upon_open = 1, /* increment refcount when a pcm is opened */
|
|
.topology_name_prefix = "intel/avs",
|
|
};
|
|
|
|
int avs_soc_component_register(struct device *dev, const char *name,
|
|
const struct snd_soc_component_driver *drv,
|
|
struct snd_soc_dai_driver *cpu_dais, int num_cpu_dais)
|
|
{
|
|
struct avs_soc_component *acomp;
|
|
int ret;
|
|
|
|
acomp = devm_kzalloc(dev, sizeof(*acomp), GFP_KERNEL);
|
|
if (!acomp)
|
|
return -ENOMEM;
|
|
|
|
ret = snd_soc_component_initialize(&acomp->base, drv, dev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* force name change after ASoC is done with its init */
|
|
acomp->base.name = name;
|
|
INIT_LIST_HEAD(&acomp->node);
|
|
|
|
return snd_soc_add_component(&acomp->base, cpu_dais, num_cpu_dais);
|
|
}
|
|
|
|
static struct snd_soc_dai_driver dmic_cpu_dais[] = {
|
|
{
|
|
.name = "DMIC Pin",
|
|
.ops = &avs_dai_nonhda_be_ops,
|
|
.capture = {
|
|
.stream_name = "DMIC Rx",
|
|
.channels_min = 1,
|
|
.channels_max = 4,
|
|
.rates = SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE,
|
|
},
|
|
},
|
|
{
|
|
.name = "DMIC WoV Pin",
|
|
.ops = &avs_dai_nonhda_be_ops,
|
|
.capture = {
|
|
.stream_name = "DMIC WoV Rx",
|
|
.channels_min = 1,
|
|
.channels_max = 4,
|
|
.rates = SNDRV_PCM_RATE_16000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE,
|
|
},
|
|
},
|
|
};
|
|
|
|
int avs_dmic_platform_register(struct avs_dev *adev, const char *name)
|
|
{
|
|
return avs_soc_component_register(adev->dev, name, &avs_component_driver, dmic_cpu_dais,
|
|
ARRAY_SIZE(dmic_cpu_dais));
|
|
}
|
|
|
|
static const struct snd_soc_dai_driver i2s_dai_template = {
|
|
.ops = &avs_dai_nonhda_be_ops,
|
|
.playback = {
|
|
.channels_min = 1,
|
|
.channels_max = 8,
|
|
.rates = SNDRV_PCM_RATE_8000_192000 |
|
|
SNDRV_PCM_RATE_12000 |
|
|
SNDRV_PCM_RATE_24000 |
|
|
SNDRV_PCM_RATE_128000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE |
|
|
SNDRV_PCM_FMTBIT_S32_LE,
|
|
.subformats = SNDRV_PCM_SUBFMTBIT_MSBITS_20 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_24 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_MAX,
|
|
},
|
|
.capture = {
|
|
.channels_min = 1,
|
|
.channels_max = 8,
|
|
.rates = SNDRV_PCM_RATE_8000_192000 |
|
|
SNDRV_PCM_RATE_12000 |
|
|
SNDRV_PCM_RATE_24000 |
|
|
SNDRV_PCM_RATE_128000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE |
|
|
SNDRV_PCM_FMTBIT_S32_LE,
|
|
.subformats = SNDRV_PCM_SUBFMTBIT_MSBITS_20 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_24 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_MAX,
|
|
},
|
|
};
|
|
|
|
int avs_i2s_platform_register(struct avs_dev *adev, const char *name, unsigned long port_mask,
|
|
unsigned long *tdms)
|
|
{
|
|
struct snd_soc_dai_driver *cpus, *dai;
|
|
size_t ssp_count, cpu_count;
|
|
int i, j;
|
|
|
|
ssp_count = adev->hw_cfg.i2s_caps.ctrl_count;
|
|
|
|
cpu_count = 0;
|
|
for_each_set_bit(i, &port_mask, ssp_count)
|
|
if (!tdms || test_bit(0, &tdms[i]))
|
|
cpu_count++;
|
|
if (tdms)
|
|
for_each_set_bit(i, &port_mask, ssp_count)
|
|
cpu_count += hweight_long(tdms[i]);
|
|
|
|
cpus = devm_kcalloc(adev->dev, cpu_count, sizeof(*cpus), GFP_KERNEL);
|
|
if (!cpus)
|
|
return -ENOMEM;
|
|
|
|
dai = cpus;
|
|
for_each_set_bit(i, &port_mask, ssp_count) {
|
|
if (!tdms || test_bit(0, &tdms[i])) {
|
|
memcpy(dai, &i2s_dai_template, sizeof(*dai));
|
|
|
|
dai->name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "SSP%d Pin", i);
|
|
dai->playback.stream_name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "ssp%d Tx", i);
|
|
dai->capture.stream_name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "ssp%d Rx", i);
|
|
|
|
if (!dai->name || !dai->playback.stream_name || !dai->capture.stream_name)
|
|
return -ENOMEM;
|
|
dai++;
|
|
}
|
|
}
|
|
|
|
if (!tdms)
|
|
goto plat_register;
|
|
|
|
for_each_set_bit(i, &port_mask, ssp_count) {
|
|
for_each_set_bit(j, &tdms[i], ssp_count) {
|
|
memcpy(dai, &i2s_dai_template, sizeof(*dai));
|
|
|
|
dai->name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "SSP%d:%d Pin", i, j);
|
|
dai->playback.stream_name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "ssp%d:%d Tx", i, j);
|
|
dai->capture.stream_name =
|
|
devm_kasprintf(adev->dev, GFP_KERNEL, "ssp%d:%d Rx", i, j);
|
|
|
|
if (!dai->name || !dai->playback.stream_name || !dai->capture.stream_name)
|
|
return -ENOMEM;
|
|
dai++;
|
|
}
|
|
}
|
|
|
|
plat_register:
|
|
return avs_soc_component_register(adev->dev, name, &avs_component_driver, cpus, cpu_count);
|
|
}
|
|
|
|
/* HD-Audio CPU DAI template */
|
|
static const struct snd_soc_dai_driver hda_cpu_dai = {
|
|
.ops = &avs_dai_hda_be_ops,
|
|
.playback = {
|
|
.channels_min = 1,
|
|
.channels_max = 8,
|
|
.rates = SNDRV_PCM_RATE_8000_192000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE |
|
|
SNDRV_PCM_FMTBIT_S32_LE,
|
|
.subformats = SNDRV_PCM_SUBFMTBIT_MSBITS_20 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_24 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_MAX,
|
|
},
|
|
.capture = {
|
|
.channels_min = 1,
|
|
.channels_max = 8,
|
|
.rates = SNDRV_PCM_RATE_8000_192000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE |
|
|
SNDRV_PCM_FMTBIT_S32_LE,
|
|
.subformats = SNDRV_PCM_SUBFMTBIT_MSBITS_20 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_24 |
|
|
SNDRV_PCM_SUBFMTBIT_MSBITS_MAX,
|
|
},
|
|
};
|
|
|
|
static void avs_component_hda_unregister_dais(struct snd_soc_component *component)
|
|
{
|
|
struct snd_soc_acpi_mach *mach;
|
|
struct snd_soc_dai *dai, *save;
|
|
struct hda_codec *codec;
|
|
char name[32];
|
|
|
|
mach = dev_get_platdata(component->card->dev);
|
|
codec = mach->pdata;
|
|
snprintf(name, sizeof(name), "%s-cpu", dev_name(&codec->core.dev));
|
|
|
|
for_each_component_dais_safe(component, dai, save) {
|
|
int stream;
|
|
|
|
if (!strstr(dai->driver->name, name))
|
|
continue;
|
|
|
|
for_each_pcm_streams(stream)
|
|
snd_soc_dapm_free_widget(snd_soc_dai_get_widget(dai, stream));
|
|
|
|
snd_soc_unregister_dai(dai);
|
|
}
|
|
}
|
|
|
|
static int avs_component_hda_probe(struct snd_soc_component *component)
|
|
{
|
|
struct snd_soc_dapm_context *dapm;
|
|
struct snd_soc_dai_driver *dais;
|
|
struct snd_soc_acpi_mach *mach;
|
|
struct hda_codec *codec;
|
|
struct hda_pcm *pcm;
|
|
const char *cname;
|
|
int pcm_count = 0, ret, i;
|
|
|
|
mach = dev_get_platdata(component->card->dev);
|
|
if (!mach)
|
|
return -EINVAL;
|
|
|
|
codec = mach->pdata;
|
|
if (list_empty(&codec->pcm_list_head))
|
|
return -EINVAL;
|
|
list_for_each_entry(pcm, &codec->pcm_list_head, list)
|
|
pcm_count++;
|
|
|
|
dais = devm_kcalloc(component->dev, pcm_count, sizeof(*dais),
|
|
GFP_KERNEL);
|
|
if (!dais)
|
|
return -ENOMEM;
|
|
|
|
cname = dev_name(&codec->core.dev);
|
|
dapm = snd_soc_component_get_dapm(component);
|
|
pcm = list_first_entry(&codec->pcm_list_head, struct hda_pcm, list);
|
|
|
|
for (i = 0; i < pcm_count; i++, pcm = list_next_entry(pcm, list)) {
|
|
struct snd_soc_dai *dai;
|
|
|
|
memcpy(&dais[i], &hda_cpu_dai, sizeof(*dais));
|
|
dais[i].id = i;
|
|
dais[i].name = devm_kasprintf(component->dev, GFP_KERNEL,
|
|
"%s-cpu%d", cname, i);
|
|
if (!dais[i].name) {
|
|
ret = -ENOMEM;
|
|
goto exit;
|
|
}
|
|
|
|
if (pcm->stream[0].substreams) {
|
|
dais[i].playback.stream_name =
|
|
devm_kasprintf(component->dev, GFP_KERNEL,
|
|
"%s-cpu%d Tx", cname, i);
|
|
if (!dais[i].playback.stream_name) {
|
|
ret = -ENOMEM;
|
|
goto exit;
|
|
}
|
|
|
|
if (!hda_codec_is_display(codec)) {
|
|
dais[i].playback.formats = pcm->stream[0].formats;
|
|
dais[i].playback.subformats = pcm->stream[0].subformats;
|
|
dais[i].playback.rates = pcm->stream[0].rates;
|
|
dais[i].playback.channels_min = pcm->stream[0].channels_min;
|
|
dais[i].playback.channels_max = pcm->stream[0].channels_max;
|
|
dais[i].playback.sig_bits = pcm->stream[0].maxbps;
|
|
}
|
|
}
|
|
|
|
if (pcm->stream[1].substreams) {
|
|
dais[i].capture.stream_name =
|
|
devm_kasprintf(component->dev, GFP_KERNEL,
|
|
"%s-cpu%d Rx", cname, i);
|
|
if (!dais[i].capture.stream_name) {
|
|
ret = -ENOMEM;
|
|
goto exit;
|
|
}
|
|
|
|
if (!hda_codec_is_display(codec)) {
|
|
dais[i].capture.formats = pcm->stream[1].formats;
|
|
dais[i].capture.subformats = pcm->stream[1].subformats;
|
|
dais[i].capture.rates = pcm->stream[1].rates;
|
|
dais[i].capture.channels_min = pcm->stream[1].channels_min;
|
|
dais[i].capture.channels_max = pcm->stream[1].channels_max;
|
|
dais[i].capture.sig_bits = pcm->stream[1].maxbps;
|
|
}
|
|
}
|
|
|
|
dai = snd_soc_register_dai(component, &dais[i], false);
|
|
if (!dai) {
|
|
dev_err(component->dev, "register dai for %s failed\n",
|
|
pcm->name);
|
|
ret = -EINVAL;
|
|
goto exit;
|
|
}
|
|
|
|
ret = snd_soc_dapm_new_dai_widgets(dapm, dai);
|
|
if (ret < 0) {
|
|
dev_err(component->dev, "create widgets failed: %d\n",
|
|
ret);
|
|
snd_soc_unregister_dai(dai);
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
ret = avs_component_probe(component);
|
|
exit:
|
|
if (ret)
|
|
avs_component_hda_unregister_dais(component);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void avs_component_hda_remove(struct snd_soc_component *component)
|
|
{
|
|
avs_component_remove(component);
|
|
avs_component_hda_unregister_dais(component);
|
|
}
|
|
|
|
static int avs_component_hda_open(struct snd_soc_component *component,
|
|
struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream);
|
|
|
|
if (!rtd->dai_link->no_pcm) {
|
|
struct snd_pcm_hardware hwparams = avs_pcm_hardware;
|
|
struct snd_soc_pcm_runtime *be;
|
|
struct snd_soc_dpcm *dpcm;
|
|
int dir = substream->stream;
|
|
|
|
/*
|
|
* Support the DPCM reparenting while still fulfilling expectations of HDAudio
|
|
* common code - a valid stream pointer at substream->runtime->private_data -
|
|
* by having all FEs point to the same private data.
|
|
*/
|
|
for_each_dpcm_be(rtd, dir, dpcm) {
|
|
struct snd_pcm_substream *be_substream;
|
|
|
|
be = dpcm->be;
|
|
if (be->dpcm[dir].users == 1)
|
|
break;
|
|
|
|
be_substream = snd_soc_dpcm_get_substream(be, dir);
|
|
substream->runtime->private_data = be_substream->runtime->private_data;
|
|
break;
|
|
}
|
|
|
|
/* RESUME unsupported for de-coupled HD-Audio capture. */
|
|
if (dir == SNDRV_PCM_STREAM_CAPTURE)
|
|
hwparams.info &= ~SNDRV_PCM_INFO_RESUME;
|
|
|
|
return snd_soc_set_runtime_hwparams(substream, &hwparams);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct snd_soc_component_driver avs_hda_component_driver = {
|
|
.name = "avs-hda-pcm",
|
|
.probe = avs_component_hda_probe,
|
|
.remove = avs_component_hda_remove,
|
|
.suspend = avs_component_suspend,
|
|
.resume = avs_component_resume,
|
|
.open = avs_component_hda_open,
|
|
.pointer = avs_component_pointer,
|
|
.mmap = avs_component_mmap,
|
|
.pcm_construct = avs_component_construct,
|
|
/*
|
|
* hda platform component's probe() is dependent on
|
|
* codec->pcm_list_head, it needs to be initialized after codec
|
|
* component. remove_order is here for completeness sake
|
|
*/
|
|
.probe_order = SND_SOC_COMP_ORDER_LATE,
|
|
.remove_order = SND_SOC_COMP_ORDER_EARLY,
|
|
.module_get_upon_open = 1,
|
|
.topology_name_prefix = "intel/avs",
|
|
};
|
|
|
|
int avs_hda_platform_register(struct avs_dev *adev, const char *name)
|
|
{
|
|
return avs_soc_component_register(adev->dev, name,
|
|
&avs_hda_component_driver, NULL, 0);
|
|
}
|