diff options
Diffstat (limited to 'sound/soc/generic')
-rw-r--r-- | sound/soc/generic/Kconfig | 39 | ||||
-rw-r--r-- | sound/soc/generic/Makefile | 14 | ||||
-rw-r--r-- | sound/soc/generic/audio-graph-card.c | 656 | ||||
-rw-r--r-- | sound/soc/generic/audio-graph-card2-custom-sample.c | 186 | ||||
-rw-r--r-- | sound/soc/generic/audio-graph-card2-custom-sample.dtsi | 400 | ||||
-rw-r--r-- | sound/soc/generic/audio-graph-card2.c | 1236 | ||||
-rw-r--r-- | sound/soc/generic/simple-card-utils.c | 1138 | ||||
-rw-r--r-- | sound/soc/generic/simple-card.c | 839 | ||||
-rw-r--r-- | sound/soc/generic/test-component.c | 656 |
9 files changed, 5164 insertions, 0 deletions
diff --git a/sound/soc/generic/Kconfig b/sound/soc/generic/Kconfig new file mode 100644 index 0000000000..b6df4e26bc --- /dev/null +++ b/sound/soc/generic/Kconfig @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: GPL-2.0-only +config SND_SIMPLE_CARD_UTILS + tristate + +config SND_SIMPLE_CARD + tristate "ASoC Simple sound card support" + select SND_SIMPLE_CARD_UTILS + help + This option enables generic simple sound card support + It also support DPCM of multi CPU single Codec ststem. + +config SND_AUDIO_GRAPH_CARD + tristate "ASoC Audio Graph sound card support" + depends on OF + select SND_SIMPLE_CARD_UTILS + help + This option enables generic simple sound card support + with OF-graph DT bindings. + It also support DPCM of multi CPU single Codec ststem. + +config SND_AUDIO_GRAPH_CARD2 + tristate "ASoC Audio Graph sound card2 support" + depends on OF + select SND_SIMPLE_CARD_UTILS + help + This option enables generic simple sound card2 support + with OF-graph DT bindings. + +config SND_AUDIO_GRAPH_CARD2_CUSTOM_SAMPLE + tristate "ASoC Audio Graph Card2 base custom sample support" + depends on SND_AUDIO_GRAPH_CARD2 + help + This option enables Audio Graph Card2 base custom sample + +config SND_TEST_COMPONENT + tristate "ASoC Test component sound support" + depends on OF + help + This option enables test component sound driver support. diff --git a/sound/soc/generic/Makefile b/sound/soc/generic/Makefile new file mode 100644 index 0000000000..0848621565 --- /dev/null +++ b/sound/soc/generic/Makefile @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0 +snd-soc-simple-card-utils-objs := simple-card-utils.o +snd-soc-simple-card-objs := simple-card.o +snd-soc-audio-graph-card-objs := audio-graph-card.o +snd-soc-audio-graph-card2-objs := audio-graph-card2.o +snd-soc-audio-graph-card2-custom-sample-objs := audio-graph-card2-custom-sample.o +snd-soc-test-component-objs := test-component.o + +obj-$(CONFIG_SND_SIMPLE_CARD_UTILS) += snd-soc-simple-card-utils.o +obj-$(CONFIG_SND_SIMPLE_CARD) += snd-soc-simple-card.o +obj-$(CONFIG_SND_AUDIO_GRAPH_CARD) += snd-soc-audio-graph-card.o +obj-$(CONFIG_SND_AUDIO_GRAPH_CARD2) += snd-soc-audio-graph-card2.o +obj-$(CONFIG_SND_AUDIO_GRAPH_CARD2_CUSTOM_SAMPLE) += snd-soc-audio-graph-card2-custom-sample.o +obj-$(CONFIG_SND_TEST_COMPONENT) += snd-soc-test-component.o diff --git a/sound/soc/generic/audio-graph-card.c b/sound/soc/generic/audio-graph-card.c new file mode 100644 index 0000000000..844a2ef159 --- /dev/null +++ b/sound/soc/generic/audio-graph-card.c @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ASoC audio graph sound card support +// +// Copyright (C) 2016 Renesas Solutions Corp. +// Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> +// +// based on ${LINUX}/sound/soc/generic/simple-card.c + +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/gpio.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/of_graph.h> +#include <linux/platform_device.h> +#include <linux/string.h> +#include <sound/graph_card.h> + +#define DPCM_SELECTABLE 1 + +static int graph_outdrv_event(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_dapm_context *dapm = w->dapm; + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(dapm->card); + + switch (event) { + case SND_SOC_DAPM_POST_PMU: + gpiod_set_value_cansleep(priv->pa_gpio, 1); + break; + case SND_SOC_DAPM_PRE_PMD: + gpiod_set_value_cansleep(priv->pa_gpio, 0); + break; + default: + return -EINVAL; + } + + return 0; +} + +static const struct snd_soc_dapm_widget graph_dapm_widgets[] = { + SND_SOC_DAPM_OUT_DRV_E("Amplifier", SND_SOC_NOPM, + 0, 0, NULL, 0, graph_outdrv_event, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD), +}; + +static const struct snd_soc_ops graph_ops = { + .startup = asoc_simple_startup, + .shutdown = asoc_simple_shutdown, + .hw_params = asoc_simple_hw_params, +}; + +static bool soc_component_is_pcm(struct snd_soc_dai_link_component *dlc) +{ + struct snd_soc_dai *dai = snd_soc_find_dai_with_mutex(dlc); + + if (dai && (dai->component->driver->pcm_construct || + (dai->driver->ops && dai->driver->ops->pcm_new))) + return true; + + return false; +} + +static void graph_parse_convert(struct device *dev, + struct device_node *ep, + struct asoc_simple_data *adata) +{ + struct device_node *top = dev->of_node; + struct device_node *port = of_get_parent(ep); + struct device_node *ports = of_get_parent(port); + struct device_node *node = of_graph_get_port_parent(ep); + + asoc_simple_parse_convert(top, NULL, adata); + if (of_node_name_eq(ports, "ports")) + asoc_simple_parse_convert(ports, NULL, adata); + asoc_simple_parse_convert(port, NULL, adata); + asoc_simple_parse_convert(ep, NULL, adata); + + of_node_put(port); + of_node_put(ports); + of_node_put(node); +} + +static void graph_parse_mclk_fs(struct device_node *top, + struct device_node *ep, + struct simple_dai_props *props) +{ + struct device_node *port = of_get_parent(ep); + struct device_node *ports = of_get_parent(port); + + of_property_read_u32(top, "mclk-fs", &props->mclk_fs); + if (of_node_name_eq(ports, "ports")) + of_property_read_u32(ports, "mclk-fs", &props->mclk_fs); + of_property_read_u32(port, "mclk-fs", &props->mclk_fs); + of_property_read_u32(ep, "mclk-fs", &props->mclk_fs); + + of_node_put(port); + of_node_put(ports); +} + +static int graph_parse_node(struct asoc_simple_priv *priv, + struct device_node *ep, + struct link_info *li, + int *cpu) +{ + struct device *dev = simple_priv_to_dev(priv); + struct device_node *top = dev->of_node; + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + struct snd_soc_dai_link_component *dlc; + struct asoc_simple_dai *dai; + int ret; + + if (cpu) { + dlc = asoc_link_to_cpu(dai_link, 0); + dai = simple_props_to_dai_cpu(dai_props, 0); + } else { + dlc = asoc_link_to_codec(dai_link, 0); + dai = simple_props_to_dai_codec(dai_props, 0); + } + + graph_parse_mclk_fs(top, ep, dai_props); + + ret = asoc_graph_parse_dai(dev, ep, dlc, cpu); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_tdm(ep, dai); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_clk(dev, ep, dai, dlc); + if (ret < 0) + return ret; + + return 0; +} + +static int graph_link_init(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li, + char *name) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + int ret; + + ret = asoc_simple_parse_daifmt(dev, cpu_ep, codec_ep, + NULL, &dai_link->dai_fmt); + if (ret < 0) + return ret; + + dai_link->init = asoc_simple_dai_init; + dai_link->ops = &graph_ops; + if (priv->ops) + dai_link->ops = priv->ops; + + return asoc_simple_set_dailink_name(dev, dai_link, name); +} + +static int graph_dai_link_of_dpcm(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + struct device_node *top = dev->of_node; + struct device_node *ep = li->cpu ? cpu_ep : codec_ep; + char dai_name[64]; + int ret; + + dev_dbg(dev, "link_of DPCM (%pOF)\n", ep); + + if (li->cpu) { + struct snd_soc_card *card = simple_priv_to_card(priv); + struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); + struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); + int is_single_links = 0; + + /* Codec is dummy */ + + /* FE settings */ + dai_link->dynamic = 1; + dai_link->dpcm_merged_format = 1; + + ret = graph_parse_node(priv, cpu_ep, li, &is_single_links); + if (ret) + return ret; + + snprintf(dai_name, sizeof(dai_name), + "fe.%pOFP.%s", cpus->of_node, cpus->dai_name); + /* + * In BE<->BE connections it is not required to create + * PCM devices at CPU end of the dai link and thus 'no_pcm' + * flag needs to be set. It is useful when there are many + * BE components and some of these have to be connected to + * form a valid audio path. + * + * For example: FE <-> BE1 <-> BE2 <-> ... <-> BEn where + * there are 'n' BE components in the path. + */ + if (card->component_chaining && !soc_component_is_pcm(cpus)) { + dai_link->no_pcm = 1; + dai_link->be_hw_params_fixup = asoc_simple_be_hw_params_fixup; + } + + asoc_simple_canonicalize_cpu(cpus, is_single_links); + asoc_simple_canonicalize_platform(platforms, cpus); + } else { + struct snd_soc_codec_conf *cconf = simple_props_to_codec_conf(dai_props, 0); + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); + struct device_node *port; + struct device_node *ports; + + /* CPU is dummy */ + + /* BE settings */ + dai_link->no_pcm = 1; + dai_link->be_hw_params_fixup = asoc_simple_be_hw_params_fixup; + + ret = graph_parse_node(priv, codec_ep, li, NULL); + if (ret < 0) + return ret; + + snprintf(dai_name, sizeof(dai_name), + "be.%pOFP.%s", codecs->of_node, codecs->dai_name); + + /* check "prefix" from top node */ + port = of_get_parent(ep); + ports = of_get_parent(port); + snd_soc_of_parse_node_prefix(top, cconf, codecs->of_node, + "prefix"); + if (of_node_name_eq(ports, "ports")) + snd_soc_of_parse_node_prefix(ports, cconf, codecs->of_node, "prefix"); + snd_soc_of_parse_node_prefix(port, cconf, codecs->of_node, + "prefix"); + + of_node_put(ports); + of_node_put(port); + } + + graph_parse_convert(dev, ep, &dai_props->adata); + + snd_soc_dai_link_set_capabilities(dai_link); + + ret = graph_link_init(priv, cpu_ep, codec_ep, li, dai_name); + + li->link++; + + return ret; +} + +static int graph_dai_link_of(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); + struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); + char dai_name[64]; + int ret, is_single_links = 0; + + dev_dbg(dev, "link_of (%pOF)\n", cpu_ep); + + ret = graph_parse_node(priv, cpu_ep, li, &is_single_links); + if (ret < 0) + return ret; + + ret = graph_parse_node(priv, codec_ep, li, NULL); + if (ret < 0) + return ret; + + snprintf(dai_name, sizeof(dai_name), + "%s-%s", cpus->dai_name, codecs->dai_name); + + asoc_simple_canonicalize_cpu(cpus, is_single_links); + asoc_simple_canonicalize_platform(platforms, cpus); + + ret = graph_link_init(priv, cpu_ep, codec_ep, li, dai_name); + if (ret < 0) + return ret; + + li->link++; + + return 0; +} + +static inline bool parse_as_dpcm_link(struct asoc_simple_priv *priv, + struct device_node *codec_port, + struct asoc_simple_data *adata) +{ + if (priv->force_dpcm) + return true; + + if (!priv->dpcm_selectable) + return false; + + /* + * It is DPCM + * if Codec port has many endpoints, + * or has convert-xxx property + */ + if ((of_get_child_count(codec_port) > 1) || + asoc_simple_is_convert_required(adata)) + return true; + + return false; +} + +static int __graph_for_each_link(struct asoc_simple_priv *priv, + struct link_info *li, + int (*func_noml)(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li), + int (*func_dpcm)(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li)) +{ + struct of_phandle_iterator it; + struct device *dev = simple_priv_to_dev(priv); + struct device_node *node = dev->of_node; + struct device_node *cpu_port; + struct device_node *cpu_ep; + struct device_node *codec_ep; + struct device_node *codec_port; + struct device_node *codec_port_old = NULL; + struct asoc_simple_data adata; + int rc, ret = 0; + + /* loop for all listed CPU port */ + of_for_each_phandle(&it, rc, node, "dais", NULL, 0) { + cpu_port = it.node; + cpu_ep = NULL; + + /* loop for all CPU endpoint */ + while (1) { + cpu_ep = of_get_next_child(cpu_port, cpu_ep); + if (!cpu_ep) + break; + + /* get codec */ + codec_ep = of_graph_get_remote_endpoint(cpu_ep); + codec_port = of_get_parent(codec_ep); + + /* get convert-xxx property */ + memset(&adata, 0, sizeof(adata)); + graph_parse_convert(dev, codec_ep, &adata); + graph_parse_convert(dev, cpu_ep, &adata); + + /* check if link requires DPCM parsing */ + if (parse_as_dpcm_link(priv, codec_port, &adata)) { + /* + * Codec endpoint can be NULL for pluggable audio HW. + * Platform DT can populate the Codec endpoint depending on the + * plugged HW. + */ + /* Do it all CPU endpoint, and 1st Codec endpoint */ + if (li->cpu || + ((codec_port_old != codec_port) && codec_ep)) + ret = func_dpcm(priv, cpu_ep, codec_ep, li); + /* else normal sound */ + } else { + if (li->cpu) + ret = func_noml(priv, cpu_ep, codec_ep, li); + } + + of_node_put(codec_ep); + of_node_put(codec_port); + + if (ret < 0) { + of_node_put(cpu_ep); + return ret; + } + + codec_port_old = codec_port; + } + } + + return 0; +} + +static int graph_for_each_link(struct asoc_simple_priv *priv, + struct link_info *li, + int (*func_noml)(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li), + int (*func_dpcm)(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li)) +{ + int ret; + /* + * Detect all CPU first, and Detect all Codec 2nd. + * + * In Normal sound case, all DAIs are detected + * as "CPU-Codec". + * + * In DPCM sound case, + * all CPUs are detected as "CPU-dummy", and + * all Codecs are detected as "dummy-Codec". + * To avoid random sub-device numbering, + * detect "dummy-Codec" in last; + */ + for (li->cpu = 1; li->cpu >= 0; li->cpu--) { + ret = __graph_for_each_link(priv, li, func_noml, func_dpcm); + if (ret < 0) + break; + } + + return ret; +} + +static int graph_count_noml(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + + if (li->link >= SNDRV_MAX_LINKS) { + dev_err(dev, "too many links\n"); + return -EINVAL; + } + + /* + * DON'T REMOVE platforms + * see + * simple-card.c :: simple_count_noml() + */ + li->num[li->link].cpus = 1; + li->num[li->link].platforms = 1; + + li->num[li->link].codecs = 1; + + li->link += 1; /* 1xCPU-Codec */ + + dev_dbg(dev, "Count As Normal\n"); + + return 0; +} + +static int graph_count_dpcm(struct asoc_simple_priv *priv, + struct device_node *cpu_ep, + struct device_node *codec_ep, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + + if (li->link >= SNDRV_MAX_LINKS) { + dev_err(dev, "too many links\n"); + return -EINVAL; + } + + if (li->cpu) { + /* + * DON'T REMOVE platforms + * see + * simple-card.c :: simple_count_noml() + */ + li->num[li->link].cpus = 1; + li->num[li->link].platforms = 1; + + li->link++; /* 1xCPU-dummy */ + } else { + li->num[li->link].codecs = 1; + + li->link++; /* 1xdummy-Codec */ + } + + dev_dbg(dev, "Count As DPCM\n"); + + return 0; +} + +static int graph_get_dais_count(struct asoc_simple_priv *priv, + struct link_info *li) +{ + /* + * link_num : number of links. + * CPU-Codec / CPU-dummy / dummy-Codec + * dais_num : number of DAIs + * ccnf_num : number of codec_conf + * same number for "dummy-Codec" + * + * ex1) + * CPU0 --- Codec0 link : 5 + * CPU1 --- Codec1 dais : 7 + * CPU2 -/ ccnf : 1 + * CPU3 --- Codec2 + * + * => 5 links = 2xCPU-Codec + 2xCPU-dummy + 1xdummy-Codec + * => 7 DAIs = 4xCPU + 3xCodec + * => 1 ccnf = 1xdummy-Codec + * + * ex2) + * CPU0 --- Codec0 link : 5 + * CPU1 --- Codec1 dais : 6 + * CPU2 -/ ccnf : 1 + * CPU3 -/ + * + * => 5 links = 1xCPU-Codec + 3xCPU-dummy + 1xdummy-Codec + * => 6 DAIs = 4xCPU + 2xCodec + * => 1 ccnf = 1xdummy-Codec + * + * ex3) + * CPU0 --- Codec0 link : 6 + * CPU1 -/ dais : 6 + * CPU2 --- Codec1 ccnf : 2 + * CPU3 -/ + * + * => 6 links = 0xCPU-Codec + 4xCPU-dummy + 2xdummy-Codec + * => 6 DAIs = 4xCPU + 2xCodec + * => 2 ccnf = 2xdummy-Codec + * + * ex4) + * CPU0 --- Codec0 (convert-rate) link : 3 + * CPU1 --- Codec1 dais : 4 + * ccnf : 1 + * + * => 3 links = 1xCPU-Codec + 1xCPU-dummy + 1xdummy-Codec + * => 4 DAIs = 2xCPU + 2xCodec + * => 1 ccnf = 1xdummy-Codec + */ + return graph_for_each_link(priv, li, + graph_count_noml, + graph_count_dpcm); +} + +int audio_graph_parse_of(struct asoc_simple_priv *priv, struct device *dev) +{ + struct snd_soc_card *card = simple_priv_to_card(priv); + struct link_info *li; + int ret; + + li = devm_kzalloc(dev, sizeof(*li), GFP_KERNEL); + if (!li) + return -ENOMEM; + + card->owner = THIS_MODULE; + card->dev = dev; + + ret = graph_get_dais_count(priv, li); + if (ret < 0) + return ret; + + if (!li->link) + return -EINVAL; + + ret = asoc_simple_init_priv(priv, li); + if (ret < 0) + return ret; + + priv->pa_gpio = devm_gpiod_get_optional(dev, "pa", GPIOD_OUT_LOW); + if (IS_ERR(priv->pa_gpio)) { + ret = PTR_ERR(priv->pa_gpio); + dev_err(dev, "failed to get amplifier gpio: %d\n", ret); + return ret; + } + + ret = asoc_simple_parse_widgets(card, NULL); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_routing(card, NULL); + if (ret < 0) + return ret; + + memset(li, 0, sizeof(*li)); + ret = graph_for_each_link(priv, li, + graph_dai_link_of, + graph_dai_link_of_dpcm); + if (ret < 0) + goto err; + + ret = asoc_simple_parse_card_name(card, NULL); + if (ret < 0) + goto err; + + snd_soc_card_set_drvdata(card, priv); + + asoc_simple_debug_info(priv); + + ret = devm_snd_soc_register_card(dev, card); + if (ret < 0) + goto err; + + devm_kfree(dev, li); + return 0; + +err: + asoc_simple_clean_reference(card); + + return dev_err_probe(dev, ret, "parse error\n"); +} +EXPORT_SYMBOL_GPL(audio_graph_parse_of); + +static int graph_probe(struct platform_device *pdev) +{ + struct asoc_simple_priv *priv; + struct device *dev = &pdev->dev; + struct snd_soc_card *card; + + /* Allocate the private data and the DAI link array */ + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + card = simple_priv_to_card(priv); + card->dapm_widgets = graph_dapm_widgets; + card->num_dapm_widgets = ARRAY_SIZE(graph_dapm_widgets); + card->probe = asoc_graph_card_probe; + + if (of_device_get_match_data(dev)) + priv->dpcm_selectable = 1; + + return audio_graph_parse_of(priv, dev); +} + +static const struct of_device_id graph_of_match[] = { + { .compatible = "audio-graph-card", }, + { .compatible = "audio-graph-scu-card", + .data = (void *)DPCM_SELECTABLE }, + {}, +}; +MODULE_DEVICE_TABLE(of, graph_of_match); + +static struct platform_driver graph_card = { + .driver = { + .name = "asoc-audio-graph-card", + .pm = &snd_soc_pm_ops, + .of_match_table = graph_of_match, + }, + .probe = graph_probe, + .remove = asoc_simple_remove, +}; +module_platform_driver(graph_card); + +MODULE_ALIAS("platform:asoc-audio-graph-card"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ASoC Audio Graph Sound Card"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); diff --git a/sound/soc/generic/audio-graph-card2-custom-sample.c b/sound/soc/generic/audio-graph-card2-custom-sample.c new file mode 100644 index 0000000000..a3142be932 --- /dev/null +++ b/sound/soc/generic/audio-graph-card2-custom-sample.c @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// audio-graph-card2-custom-sample.c +// +// Copyright (C) 2020 Renesas Electronics Corp. +// Copyright (C) 2020 Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> +// +#include <linux/module.h> +#include <linux/of_gpio.h> +#include <linux/platform_device.h> +#include <sound/graph_card.h> + +/* + * Custom driver can have own priv + * which includes asoc_simple_priv. + */ +struct custom_priv { + struct asoc_simple_priv simple_priv; + + /* custom driver's own params */ + int custom_params; +}; + +/* You can get custom_priv from simple_priv */ +#define simple_to_custom(simple) container_of((simple), struct custom_priv, simple_priv) + +static int custom_card_probe(struct snd_soc_card *card) +{ + struct asoc_simple_priv *simple_priv = snd_soc_card_get_drvdata(card); + struct custom_priv *custom_priv = simple_to_custom(simple_priv); + struct device *dev = simple_priv_to_dev(simple_priv); + + dev_info(dev, "custom probe\n"); + + custom_priv->custom_params = 1; + + /* you can use generic probe function */ + return asoc_graph_card_probe(card); +} + +static int custom_hook_pre(struct asoc_simple_priv *priv) +{ + struct device *dev = simple_priv_to_dev(priv); + + /* You can custom before parsing */ + dev_info(dev, "hook : %s\n", __func__); + + return 0; +} + +static int custom_hook_post(struct asoc_simple_priv *priv) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_card *card; + + /* You can custom after parsing */ + dev_info(dev, "hook : %s\n", __func__); + + /* overwrite .probe sample */ + card = simple_priv_to_card(priv); + card->probe = custom_card_probe; + + return 0; +} + +static int custom_normal(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + + /* + * You can custom Normal parsing + * before/affter audio_graph2_link_normal() + */ + dev_info(dev, "hook : %s\n", __func__); + + return audio_graph2_link_normal(priv, lnk, li); +} + +static int custom_dpcm(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + + /* + * You can custom DPCM parsing + * before/affter audio_graph2_link_dpcm() + */ + dev_info(dev, "hook : %s\n", __func__); + + return audio_graph2_link_dpcm(priv, lnk, li); +} + +static int custom_c2c(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + + /* + * You can custom Codec2Codec parsing + * before/affter audio_graph2_link_c2c() + */ + dev_info(dev, "hook : %s\n", __func__); + + return audio_graph2_link_c2c(priv, lnk, li); +} + +/* + * audio-graph-card2 has many hooks for your customizing. + */ +static struct graph2_custom_hooks custom_hooks = { + .hook_pre = custom_hook_pre, + .hook_post = custom_hook_post, + .custom_normal = custom_normal, + .custom_dpcm = custom_dpcm, + .custom_c2c = custom_c2c, +}; + +static int custom_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct device *dev = simple_priv_to_dev(priv); + + dev_info(dev, "custom startup\n"); + + return asoc_simple_startup(substream); +} + +/* You can use custom ops */ +static const struct snd_soc_ops custom_ops = { + .startup = custom_startup, + .shutdown = asoc_simple_shutdown, + .hw_params = asoc_simple_hw_params, +}; + +static int custom_probe(struct platform_device *pdev) +{ + struct custom_priv *custom_priv; + struct asoc_simple_priv *simple_priv; + struct device *dev = &pdev->dev; + int ret; + + custom_priv = devm_kzalloc(dev, sizeof(*custom_priv), GFP_KERNEL); + if (!custom_priv) + return -ENOMEM; + + simple_priv = &custom_priv->simple_priv; + simple_priv->ops = &custom_ops; /* customize dai_link ops */ + + /* "audio-graph-card2-custom-sample" is too long */ + simple_priv->snd_card.name = "card2-custom"; + + /* use audio-graph-card2 parsing with own custom hooks */ + ret = audio_graph2_parse_of(simple_priv, dev, &custom_hooks); + if (ret < 0) + return ret; + + /* customize more if needed */ + + return 0; +} + +static const struct of_device_id custom_of_match[] = { + { .compatible = "audio-graph-card2-custom-sample", }, + {}, +}; +MODULE_DEVICE_TABLE(of, custom_of_match); + +static struct platform_driver custom_card = { + .driver = { + .name = "audio-graph-card2-custom-sample", + .of_match_table = custom_of_match, + }, + .probe = custom_probe, + .remove = asoc_simple_remove, +}; +module_platform_driver(custom_card); + +MODULE_ALIAS("platform:asoc-audio-graph-card2-custom-sample"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ASoC Audio Graph Card2 Custom Sample"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); diff --git a/sound/soc/generic/audio-graph-card2-custom-sample.dtsi b/sound/soc/generic/audio-graph-card2-custom-sample.dtsi new file mode 100644 index 0000000000..8acaa2ddb3 --- /dev/null +++ b/sound/soc/generic/audio-graph-card2-custom-sample.dtsi @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * audio-graph-card2-custom-sample.dtsi + * + * Copyright (C) 2020 Renesas Electronics Corp. + * Copyright (C) 2020 Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> + * + * This sample indicates how to use audio-graph-card2 and its + * custom driver. "audio-graph-card2-custom-sample" is the custome driver + * which is using audio-graph-card2. + * + * You can easily use this sample by adding below line on your DT file, + * and add new CONFIG to your .config. + * + * #include "../../../../../sound/soc/generic/audio-graph-card2-custom-sample.dtsi" + * + * CONFIG_SND_AUDIO_GRAPH_CARD2 + * CONFIG_SND_AUDIO_GRAPH_CARD2_CUSTOM_SAMPLE + * CONFIG_SND_TEST_COMPONENT + * + * + * You can indicate more detail each device behavior as debug if you modify + * "compatible" on each test-component. see below + * + * test_cpu { + * - compatible = "test-cpu"; + * + compatible = "test-cpu-verbose"; + * ... + * }; + * + * test_codec { + * - compatible = "test-codec"; + * + compatible = "test-codec-verbose"; + * ... + * }; + * + * + * Below sample doesn't use "format" property, + * because test-component driver (test-cpu/test-codec) is supporting + * snd_soc_dai_ops :: .auto_selectable_formats. + * see + * snd_soc_runtime_get_dai_fmt() + * linux/sound/soc/generic/test-component.c :: test_dai_formats + */ +/ { + /* + * @ : used at links + * + * [Normal] + * cpu0 <-@-----------------> codec0 + * + * [Semi-Multi] + * + * CPU:Codec = 1:N + * + * +-+ + * cpu7 <-@------->| |-> codec12 + * | |-> codec13 + * +-+ + * + * [Multi-CPU/Codec] + * +-+ +-+ + * cpu1 <--| |<-@--------->| |-> codec1 + * cpu2 <--| | | |-> codec2 + * +-+ +-+ + * + * [DPCM] + * + * CPU3/CPU4 are converting rate to 44100 + * + * FE BE + * **** + * cpu3 <-@--* *--@-> codec3 + * cpu4 <-@--* * (44.1kHz) + * **** + * + * [DPCM-Multi] + * + * --NOTE-- + * Multi-FE is not supported by ASoC. + * + * FE BE + * **** +-+ + * cpu5 <-@--* *--@-> | | -> codec4 + * cpu6 <-@--* * | | -> codec5 + * **** +-+ + * + * [Codec2Codec] + * +-@-> codec6 + * | + * +---> codec7 + * + * [Codec2Codec-Multi] + * + * --NOTE-- + * Multi connect N:M is not supported by ASoC. + * + * +-+ + * +-@->| |-> codec8 + * | | |-> codec9 + * | +-+ + * | +-+ + * +--->| |-> codec10 + * | |-> codec11 + * +-+ + */ + audio-graph-card2-custom-sample { + /* + * You can use audio-graph-card2 directly by using + * + * compatible = "audio-graph-card2"; + */ + compatible = "audio-graph-card2-custom-sample"; + + /* for [DPCM] */ + /* BE FE */ + routing = "TC DAI3 Playback", "DAI3 Playback", + "TC DAI3 Playback", "DAI4 Playback", + "DAI3 Capture", "TC DAI3 Capture", + "DAI4 Capture", "TC DAI3 Capture", + /* for [DPCM-Multi] */ + /* BE FE */ + "TC DAI4 Playback", "DAI5 Playback", + "TC DAI5 Playback", "DAI5 Playback", + "TC DAI4 Playback", "DAI6 Playback", + "TC DAI5 Playback", "DAI6 Playback", + "DAI5 Capture", "TC DAI4 Capture", + "DAI5 Capture", "TC DAI5 Capture", + "DAI6 Capture", "TC DAI4 Capture", + "DAI6 Capture", "TC DAI5 Capture", + /* for [Codec2Codec] */ + "TC OUT", "TC DAI7 Playback", + "TC DAI6 Capture", "TC IN", + /* for [Codec2Codec-Multi] */ + "TC OUT", "TC DAI10 Playback", + "TC DAI8 Capture", "TC IN", + "TC OUT", "TC DAI11 Playback", + "TC DAI9 Capture", "TC IN"; + + links = < + /* + * [Normal]: cpu side only + * cpu0/codec0 + */ + &cpu0 + + /* [Semi-Multi] */ + &sm0 + + /* + * [Multi-CPU/Codec]: cpu side only + * cpu1/cpu2/codec1/codec2 + */ + &mcpu0 + + /* + * [DPCM]: both FE / BE + * cpu3/cpu4/codec3 + */ + &fe00 &fe01 &be0 + + /* + * [DPCM-Multi]: both FE / BE + * cpu5/cpu6/codec4/codec5 + */ + &fe10 &fe11 &be1 + + /* + * [Codec2Codec]: cpu side only + * codec6/codec7 + */ + &c2c + + /* + * [Codec2Codec-Multi]: cpu side only + * codec8/codec9/codec10/codec11 + */ + &c2c_m + >; + + multi { + #address-cells = <1>; + #size-cells = <0>; + + ports@0 { + reg = <0>; + #address-cells = <1>; + #size-cells = <0>; + /* [Multi-CPU] */ + mcpu0: port@0 { reg = <0>; mcpu0_ep: endpoint { remote-endpoint = <&mcodec0_ep>; }; }; + port@1 { reg = <1>; mcpu1_ep: endpoint { remote-endpoint = <&cpu1_ep>; }; }; + port@2 { reg = <2>; mcpu2_ep: endpoint { remote-endpoint = <&cpu2_ep>; }; }; + }; + + /* [Multi-Codec] */ + ports@1 { + reg = <1>; + #address-cells = <1>; + #size-cells = <0>; + port@0 { reg = <0>; mcodec0_ep: endpoint { remote-endpoint = <&mcpu0_ep>; }; }; + port@1 { reg = <1>; mcodec1_ep: endpoint { remote-endpoint = <&codec1_ep>; }; }; + port@2 { reg = <2>; mcodec2_ep: endpoint { remote-endpoint = <&codec2_ep>; }; }; + }; + + /* [DPCM-Multi]::BE */ + ports@2 { + reg = <2>; + #address-cells = <1>; + #size-cells = <0>; + port@0 { reg = <0>; mbe_ep: endpoint { remote-endpoint = <&be10_ep>; }; }; + port@1 { reg = <1>; mbe1_ep: endpoint { remote-endpoint = <&codec4_ep>; }; }; + port@2 { reg = <2>; mbe2_ep: endpoint { remote-endpoint = <&codec5_ep>; }; }; + }; + + /* [Codec2Codec-Multi]::CPU */ + ports@3 { + reg = <3>; + #address-cells = <1>; + #size-cells = <0>; + port@0 { reg = <0>; mc2c0_ep: endpoint { remote-endpoint = <&c2cmf_ep>; }; }; + port@1 { reg = <1>; mc2c00_ep: endpoint { remote-endpoint = <&codec8_ep>; }; }; + port@2 { reg = <2>; mc2c01_ep: endpoint { remote-endpoint = <&codec9_ep>; }; }; + }; + + /* [Codec2Codec-Multi]::Codec */ + ports@4 { + reg = <4>; + #address-cells = <1>; + #size-cells = <0>; + port@0 { reg = <0>; mc2c1_ep: endpoint { remote-endpoint = <&c2cmb_ep>; }; }; + port@1 { reg = <1>; mc2c10_ep: endpoint { remote-endpoint = <&codec10_ep>; }; }; + port@2 { reg = <2>; mc2c11_ep: endpoint { remote-endpoint = <&codec11_ep>; }; }; + }; + + /* [Semi-Multi] */ + ports@5 { + reg = <5>; + #address-cells = <1>; + #size-cells = <0>; + port@0 { reg = <0>; smcodec0_ep: endpoint { remote-endpoint = <&cpu7_ep>; }; }; + port@1 { reg = <1>; smcodec1_ep: endpoint { remote-endpoint = <&codec12_ep>; }; }; + port@2 { reg = <2>; smcodec2_ep: endpoint { remote-endpoint = <&codec13_ep>; }; }; + }; + }; + + dpcm { + #address-cells = <1>; + #size-cells = <0>; + + ports@0 { + reg = <0>; + + #address-cells = <1>; + #size-cells = <0>; + /* [DPCM]::FE */ + fe00: port@0 { reg = <0>; fe00_ep: endpoint { remote-endpoint = <&cpu3_ep>; }; }; + fe01: port@1 { reg = <1>; fe01_ep: endpoint { remote-endpoint = <&cpu4_ep>; }; }; + + /* [DPCM-Multi]::FE */ + fe10: port@2 { reg = <2>; fe10_ep: endpoint { remote-endpoint = <&cpu5_ep>; }; }; + fe11: port@3 { reg = <3>; fe11_ep: endpoint { remote-endpoint = <&cpu6_ep>; }; }; + }; + + ports@1 { + reg = <1>; + + #address-cells = <1>; + #size-cells = <0>; + /* [DPCM]::BE */ + be0: port@0 { reg = <0>; be00_ep: endpoint { remote-endpoint = <&codec3_ep>; }; }; + + /* [DPCM-Multi]::BE */ + be1: port@1 { reg = <1>; be10_ep: endpoint { remote-endpoint = <&mbe_ep>; }; }; + }; + }; + + codec2codec { + #address-cells = <1>; + #size-cells = <0>; + /* [Codec2Codec] */ + ports@0 { + reg = <0>; + + #address-cells = <1>; + #size-cells = <0>; + + /* use default settings */ + c2c: port@0 { reg = <0>; c2cf_ep: endpoint { remote-endpoint = <&codec6_ep>; }; }; + port@1 { reg = <1>; c2cb_ep: endpoint { remote-endpoint = <&codec7_ep>; }; }; + }; + + /* [Codec2Codec-Multi] */ + ports@1 { + reg = <1>; + + #address-cells = <1>; + #size-cells = <0>; + + /* use original settings */ + rate = <48000>; + c2c_m: port@0 { reg = <0>; c2cmf_ep: endpoint { remote-endpoint = <&mc2c0_ep>; }; }; + port@1 { reg = <1>; c2cmb_ep: endpoint { remote-endpoint = <&mc2c1_ep>; }; }; + }; + }; + }; + + test_cpu { + /* + * update compatible to indicate more detail behaviour + * if you want. see test-compatible for more detail. + * + * ex) + * - compatible = "test-cpu"; + * + compatible = "test-cpu-verbose"; + */ + compatible = "test-cpu"; + ports { + #address-cells = <1>; + #size-cells = <0>; + + bitclock-master; + frame-master; + /* [Normal] */ + cpu0: port@0 { reg = <0>; cpu0_ep: endpoint { remote-endpoint = <&codec0_ep>; }; }; + + /* [Multi-CPU] */ + port@1 { reg = <1>; cpu1_ep: endpoint { remote-endpoint = <&mcpu1_ep>; }; }; + port@2 { reg = <2>; cpu2_ep: endpoint { remote-endpoint = <&mcpu2_ep>; }; }; + + /* [DPCM]::FE */ + port@3 { reg = <3>; cpu3_ep: endpoint { remote-endpoint = <&fe00_ep>; }; }; + port@4 { reg = <4>; cpu4_ep: endpoint { remote-endpoint = <&fe01_ep>; }; }; + + /* [DPCM-Multi]::FE */ + port@5 { reg = <5>; cpu5_ep: endpoint { remote-endpoint = <&fe10_ep>; }; }; + port@6 { reg = <6>; cpu6_ep: endpoint { remote-endpoint = <&fe11_ep>; }; }; + + /* [Semi-Multi] */ + sm0: port@7 { reg = <7>; cpu7_ep: endpoint { remote-endpoint = <&smcodec0_ep>; }; }; + }; + }; + + test_codec { + /* + * update compatible to indicate more detail behaviour + * if you want. see test-compatible for more detail. + * + * ex) + * - compatible = "test-codec"; + * + compatible = "test-codec-verbose"; + */ + compatible = "test-codec"; + ports { + #address-cells = <1>; + #size-cells = <0>; + + /* + * prefix can be added to *component*, + * see audio-graph-card2::routing + */ + prefix = "TC"; + + /* [Normal] */ + port@0 { reg = <0>; codec0_ep: endpoint { remote-endpoint = <&cpu0_ep>; }; }; + + /* [Multi-Codec] */ + port@1 { reg = <1>; codec1_ep: endpoint { remote-endpoint = <&mcodec1_ep>; }; }; + port@2 { reg = <2>; codec2_ep: endpoint { remote-endpoint = <&mcodec2_ep>; }; }; + + /* [DPCM]::BE */ + port@3 { + convert-rate = <44100>; + reg = <3>; codec3_ep: endpoint { remote-endpoint = <&be00_ep>; }; + }; + + /* [DPCM-Multi]::BE */ + port@4 { reg = <4>; codec4_ep: endpoint { remote-endpoint = <&mbe1_ep>; }; }; + port@5 { reg = <5>; codec5_ep: endpoint { remote-endpoint = <&mbe2_ep>; }; }; + + /* [Codec2Codec] */ + port@6 { bitclock-master; + frame-master; + reg = <6>; codec6_ep: endpoint { remote-endpoint = <&c2cf_ep>; }; }; + port@7 { reg = <7>; codec7_ep: endpoint { remote-endpoint = <&c2cb_ep>; }; }; + + /* [Codec2Codec-Multi] */ + port@8 { bitclock-master; + frame-master; + reg = <8>; codec8_ep: endpoint { remote-endpoint = <&mc2c00_ep>; }; }; + port@9 { reg = <9>; codec9_ep: endpoint { remote-endpoint = <&mc2c01_ep>; }; }; + port@a { reg = <10>; codec10_ep: endpoint { remote-endpoint = <&mc2c10_ep>; }; }; + port@b { reg = <11>; codec11_ep: endpoint { remote-endpoint = <&mc2c11_ep>; }; }; + + /* [Semi-Multi] */ + port@c { reg = <12>; codec12_ep: endpoint { remote-endpoint = <&smcodec1_ep>; }; }; + port@d { reg = <13>; codec13_ep: endpoint { remote-endpoint = <&smcodec2_ep>; }; }; + + }; + }; +}; diff --git a/sound/soc/generic/audio-graph-card2.c b/sound/soc/generic/audio-graph-card2.c new file mode 100644 index 0000000000..b1c675c6b6 --- /dev/null +++ b/sound/soc/generic/audio-graph-card2.c @@ -0,0 +1,1236 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ASoC Audio Graph Card2 support +// +// Copyright (C) 2020 Renesas Electronics Corp. +// Copyright (C) 2020 Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> +// +// based on ${LINUX}/sound/soc/generic/audio-graph-card.c +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/gpio.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/of_gpio.h> +#include <linux/of_graph.h> +#include <linux/platform_device.h> +#include <linux/string.h> +#include <sound/graph_card.h> + +/************************************ + daifmt + ************************************ + ports { + format = "left_j"; + port@0 { + bitclock-master; + sample0: endpoint@0 { + frame-master; + }; + sample1: endpoint@1 { + format = "i2s"; + }; + }; + ... + }; + + You can set daifmt at ports/port/endpoint. + It uses *latest* format, and *share* master settings. + In above case, + sample0: left_j, bitclock-master, frame-master + sample1: i2s, bitclock-master + + If there was no settings, *Codec* will be + bitclock/frame provider as default. + see + graph_parse_daifmt(). + + "format" property is no longer needed on DT if both CPU/Codec drivers are + supporting snd_soc_dai_ops :: .auto_selectable_formats. + see + snd_soc_runtime_get_dai_fmt() + + sample driver + linux/sound/soc/sh/rcar/core.c + linux/sound/soc/codecs/ak4613.c + linux/sound/soc/codecs/pcm3168a.c + linux/sound/soc/soc-utils.c + linux/sound/soc/generic/test-component.c + + ************************************ + Normal Audio-Graph + ************************************ + + CPU <---> Codec + + sound { + compatible = "audio-graph-card2"; + links = <&cpu>; + }; + + CPU { + cpu: port { + bitclock-master; + frame-master; + cpu_ep: endpoint { remote-endpoint = <&codec_ep>; }; }; + }; + + Codec { + port { codec_ep: endpoint { remote-endpoint = <&cpu_ep>; }; }; + }; + + ************************************ + Multi-CPU/Codec + ************************************ + +It has connection part (= X) and list part (= y). +links indicates connection part of CPU side (= A). + + +-+ (A) +-+ + CPU1 --(y) | | <-(X)--(X)-> | | (y)-- Codec1 + CPU2 --(y) | | | | (y)-- Codec2 + +-+ +-+ + + sound { + compatible = "audio-graph-card2"; + +(A) links = <&mcpu>; + + multi { + ports@0 { +(X) (A) mcpu: port@0 { mcpu0_ep: endpoint { remote-endpoint = <&mcodec0_ep>; }; }; +(y) port@1 { mcpu1_ep: endpoint { remote-endpoint = <&cpu1_ep>; }; }; +(y) port@2 { mcpu2_ep: endpoint { remote-endpoint = <&cpu2_ep>; }; }; + }; + ports@1 { +(X) port@0 { mcodec0_ep: endpoint { remote-endpoint = <&mcpu0_ep>; }; }; +(y) port@1 { mcodec1_ep: endpoint { remote-endpoint = <&codec1_ep>; }; }; +(y) port@2 { mcodec2_ep: endpoint { remote-endpoint = <&codec2_ep>; }; }; + }; + }; + }; + + CPU { + ports { + bitclock-master; + frame-master; + port@0 { cpu1_ep: endpoint { remote-endpoint = <&mcpu1_ep>; }; }; + port@1 { cpu2_ep: endpoint { remote-endpoint = <&mcpu2_ep>; }; }; + }; + }; + + Codec { + ports { + port@0 { codec1_ep: endpoint { remote-endpoint = <&mcodec1_ep>; }; }; + port@1 { codec2_ep: endpoint { remote-endpoint = <&mcodec2_ep>; }; }; + }; + }; + + ************************************ + DPCM + ************************************ + + DSP + ************ + PCM0 <--> * fe0 be0 * <--> DAI0: Codec Headset + PCM1 <--> * fe1 be1 * <--> DAI1: Codec Speakers + PCM2 <--> * fe2 be2 * <--> DAI2: MODEM + PCM3 <--> * fe3 be3 * <--> DAI3: BT + * be4 * <--> DAI4: DMIC + * be5 * <--> DAI5: FM + ************ + + sound { + compatible = "audio-graph-card2"; + + // indicate routing + routing = "xxx Playback", "xxx Playback", + "xxx Playback", "xxx Playback", + "xxx Playback", "xxx Playback"; + + // indicate all Front-End, Back-End + links = <&fe0, &fe1, ..., + &be0, &be1, ...>; + + dpcm { + // Front-End + ports@0 { + fe0: port@0 { fe0_ep: endpoint { remote-endpoint = <&pcm0_ep>; }; }; + fe1: port@1 { fe1_ep: endpoint { remote-endpoint = <&pcm1_ep>; }; }; + ... + }; + // Back-End + ports@1 { + be0: port@0 { be0_ep: endpoint { remote-endpoint = <&dai0_ep>; }; }; + be1: port@1 { be1_ep: endpoint { remote-endpoint = <&dai1_ep>; }; }; + ... + }; + }; + }; + + CPU { + ports { + bitclock-master; + frame-master; + port@0 { pcm0_ep: endpoint { remote-endpoint = <&fe0_ep>; }; }; + port@1 { pcm1_ep: endpoint { remote-endpoint = <&fe1_ep>; }; }; + ... + }; + }; + + Codec { + ports { + port@0 { dai0_ep: endpoint { remote-endpoint = <&be0_ep>; }; }; + port@1 { dai1_ep: endpoint { remote-endpoint = <&be1_ep>; }; }; + ... + }; + }; + + ************************************ + Codec to Codec + ************************************ + + +--+ + | |<-- Codec0 <- IN + | |--> Codec1 -> OUT + +--+ + + sound { + compatible = "audio-graph-card2"; + + routing = "OUT" ,"DAI1 Playback", + "DAI0 Capture", "IN"; + + links = <&c2c>; + + codec2codec { + ports { + rate = <48000>; + c2c: port@0 { c2cf_ep: endpoint { remote-endpoint = <&codec0_ep>; }; }; + port@1 { c2cb_ep: endpoint { remote-endpoint = <&codec1_ep>; }; }; + }; + }; + + Codec { + ports { + port@0 { + bitclock-master; + frame-master; + codec0_ep: endpoint { remote-endpoint = <&c2cf_ep>; }; }; + port@1 { codec1_ep: endpoint { remote-endpoint = <&c2cb_ep>; }; }; + }; + }; + +*/ + +enum graph_type { + GRAPH_NORMAL, + GRAPH_DPCM, + GRAPH_C2C, + + GRAPH_MULTI, /* don't use ! Use this only in __graph_get_type() */ +}; + +#define GRAPH_NODENAME_MULTI "multi" +#define GRAPH_NODENAME_DPCM "dpcm" +#define GRAPH_NODENAME_C2C "codec2codec" + +#define port_to_endpoint(port) of_get_child_by_name(port, "endpoint") + +static enum graph_type __graph_get_type(struct device_node *lnk) +{ + struct device_node *np, *parent_np; + enum graph_type ret; + + /* + * target { + * ports { + * => lnk: port@0 { ... }; + * port@1 { ... }; + * }; + * }; + */ + np = of_get_parent(lnk); + if (of_node_name_eq(np, "ports")) { + parent_np = of_get_parent(np); + of_node_put(np); + np = parent_np; + } + + if (of_node_name_eq(np, GRAPH_NODENAME_MULTI)) { + ret = GRAPH_MULTI; + goto out_put; + } + + if (of_node_name_eq(np, GRAPH_NODENAME_DPCM)) { + ret = GRAPH_DPCM; + goto out_put; + } + + if (of_node_name_eq(np, GRAPH_NODENAME_C2C)) { + ret = GRAPH_C2C; + goto out_put; + } + + ret = GRAPH_NORMAL; + +out_put: + of_node_put(np); + return ret; + +} + +static enum graph_type graph_get_type(struct asoc_simple_priv *priv, + struct device_node *lnk) +{ + enum graph_type type = __graph_get_type(lnk); + + /* GRAPH_MULTI here means GRAPH_NORMAL */ + if (type == GRAPH_MULTI) + type = GRAPH_NORMAL; + +#ifdef DEBUG + { + struct device *dev = simple_priv_to_dev(priv); + const char *str = "Normal"; + + switch (type) { + case GRAPH_DPCM: + if (asoc_graph_is_ports0(lnk)) + str = "DPCM Front-End"; + else + str = "DPCM Back-End"; + break; + case GRAPH_C2C: + str = "Codec2Codec"; + break; + default: + break; + } + + dev_dbg(dev, "%pOF (%s)", lnk, str); + } +#endif + return type; +} + +static int graph_lnk_is_multi(struct device_node *lnk) +{ + return __graph_get_type(lnk) == GRAPH_MULTI; +} + +static struct device_node *graph_get_next_multi_ep(struct device_node **port) +{ + struct device_node *ports = of_get_parent(*port); + struct device_node *ep = NULL; + struct device_node *rep = NULL; + + /* + * multi { + * ports { + * => lnk: port@0 { ... }; + * port@1 { ep { ... = rep0 } }; + * port@2 { ep { ... = rep1 } }; + * ... + * }; + * }; + * + * xxx { + * port@0 { rep0 }; + * port@1 { rep1 }; + * }; + */ + do { + *port = of_get_next_child(ports, *port); + if (!*port) + break; + } while (!of_node_name_eq(*port, "port")); + + if (*port) { + ep = port_to_endpoint(*port); + rep = of_graph_get_remote_endpoint(ep); + } + + of_node_put(ep); + of_node_put(ports); + + return rep; +} + +static const struct snd_soc_ops graph_ops = { + .startup = asoc_simple_startup, + .shutdown = asoc_simple_shutdown, + .hw_params = asoc_simple_hw_params, +}; + +static void graph_parse_convert(struct device_node *ep, + struct simple_dai_props *props) +{ + struct device_node *port = of_get_parent(ep); + struct device_node *ports = of_get_parent(port); + struct asoc_simple_data *adata = &props->adata; + + if (of_node_name_eq(ports, "ports")) + asoc_simple_parse_convert(ports, NULL, adata); + asoc_simple_parse_convert(port, NULL, adata); + asoc_simple_parse_convert(ep, NULL, adata); + + of_node_put(port); + of_node_put(ports); +} + +static void graph_parse_mclk_fs(struct device_node *ep, + struct simple_dai_props *props) +{ + struct device_node *port = of_get_parent(ep); + struct device_node *ports = of_get_parent(port); + + if (of_node_name_eq(ports, "ports")) + of_property_read_u32(ports, "mclk-fs", &props->mclk_fs); + of_property_read_u32(port, "mclk-fs", &props->mclk_fs); + of_property_read_u32(ep, "mclk-fs", &props->mclk_fs); + + of_node_put(port); + of_node_put(ports); +} + +static int __graph_parse_node(struct asoc_simple_priv *priv, + enum graph_type gtype, + struct device_node *ep, + struct link_info *li, + int is_cpu, int idx) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + struct snd_soc_dai_link_component *dlc; + struct asoc_simple_dai *dai; + int ret, is_single_links = 0; + + if (is_cpu) { + dlc = asoc_link_to_cpu(dai_link, idx); + dai = simple_props_to_dai_cpu(dai_props, idx); + } else { + dlc = asoc_link_to_codec(dai_link, idx); + dai = simple_props_to_dai_codec(dai_props, idx); + } + + graph_parse_mclk_fs(ep, dai_props); + + ret = asoc_graph_parse_dai(dev, ep, dlc, &is_single_links); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_tdm(ep, dai); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_tdm_width_map(dev, ep, dai); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_clk(dev, ep, dai, dlc); + if (ret < 0) + return ret; + + /* + * set DAI Name + */ + if (!dai_link->name) { + struct snd_soc_dai_link_component *cpus = dlc; + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, idx); + char *cpu_multi = ""; + char *codec_multi = ""; + + if (dai_link->num_cpus > 1) + cpu_multi = "_multi"; + if (dai_link->num_codecs > 1) + codec_multi = "_multi"; + + switch (gtype) { + case GRAPH_NORMAL: + /* run is_cpu only. see audio_graph2_link_normal() */ + if (is_cpu) + asoc_simple_set_dailink_name(dev, dai_link, "%s%s-%s%s", + cpus->dai_name, cpu_multi, + codecs->dai_name, codec_multi); + break; + case GRAPH_DPCM: + if (is_cpu) + asoc_simple_set_dailink_name(dev, dai_link, "fe.%pOFP.%s%s", + cpus->of_node, cpus->dai_name, cpu_multi); + else + asoc_simple_set_dailink_name(dev, dai_link, "be.%pOFP.%s%s", + codecs->of_node, codecs->dai_name, codec_multi); + break; + case GRAPH_C2C: + /* run is_cpu only. see audio_graph2_link_c2c() */ + if (is_cpu) + asoc_simple_set_dailink_name(dev, dai_link, "c2c.%s%s-%s%s", + cpus->dai_name, cpu_multi, + codecs->dai_name, codec_multi); + break; + default: + break; + } + } + + /* + * Check "prefix" from top node + * if DPCM-BE case + */ + if (!is_cpu && gtype == GRAPH_DPCM) { + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, idx); + struct snd_soc_codec_conf *cconf = simple_props_to_codec_conf(dai_props, idx); + struct device_node *rport = of_get_parent(ep); + struct device_node *rports = of_get_parent(rport); + + if (of_node_name_eq(rports, "ports")) + snd_soc_of_parse_node_prefix(rports, cconf, codecs->of_node, "prefix"); + snd_soc_of_parse_node_prefix(rport, cconf, codecs->of_node, "prefix"); + + of_node_put(rport); + of_node_put(rports); + } + + if (is_cpu) { + struct snd_soc_dai_link_component *cpus = dlc; + struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, idx); + + asoc_simple_canonicalize_cpu(cpus, is_single_links); + asoc_simple_canonicalize_platform(platforms, cpus); + } + + return 0; +} + +static int graph_parse_node(struct asoc_simple_priv *priv, + enum graph_type gtype, + struct device_node *port, + struct link_info *li, int is_cpu) +{ + struct device_node *ep; + int ret = 0; + + if (graph_lnk_is_multi(port)) { + int idx; + + of_node_get(port); + + for (idx = 0;; idx++) { + ep = graph_get_next_multi_ep(&port); + if (!ep) + break; + + ret = __graph_parse_node(priv, gtype, ep, + li, is_cpu, idx); + of_node_put(ep); + if (ret < 0) + break; + } + } else { + /* Single CPU / Codec */ + ep = port_to_endpoint(port); + ret = __graph_parse_node(priv, gtype, ep, li, is_cpu, 0); + of_node_put(ep); + } + + return ret; +} + +static void graph_parse_daifmt(struct device_node *node, + unsigned int *daifmt, unsigned int *bit_frame) +{ + unsigned int fmt; + + /* + * see also above "daifmt" explanation + * and samples. + */ + + /* + * ports { + * (A) + * port { + * (B) + * endpoint { + * (C) + * }; + * }; + * }; + * }; + */ + + /* + * clock_provider: + * + * It can be judged it is provider + * if (A) or (B) or (C) has bitclock-master / frame-master flag. + * + * use "or" + */ + *bit_frame |= snd_soc_daifmt_parse_clock_provider_as_bitmap(node, NULL); + +#define update_daifmt(name) \ + if (!(*daifmt & SND_SOC_DAIFMT_##name##_MASK) && \ + (fmt & SND_SOC_DAIFMT_##name##_MASK)) \ + *daifmt |= fmt & SND_SOC_DAIFMT_##name##_MASK + + /* + * format + * + * This function is called by (C) -> (B) -> (A) order. + * Set if applicable part was not yet set. + */ + fmt = snd_soc_daifmt_parse_format(node, NULL); + update_daifmt(FORMAT); + update_daifmt(CLOCK); + update_daifmt(INV); +} + +static void graph_link_init(struct asoc_simple_priv *priv, + struct device_node *port, + struct link_info *li, + int is_cpu_node) +{ + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct device_node *ep; + struct device_node *ports; + unsigned int daifmt = 0, daiclk = 0; + unsigned int bit_frame = 0; + + if (graph_lnk_is_multi(port)) { + of_node_get(port); + ep = graph_get_next_multi_ep(&port); + port = of_get_parent(ep); + } else { + ep = port_to_endpoint(port); + } + + ports = of_get_parent(port); + + /* + * ports { + * (A) + * port { + * (B) + * endpoint { + * (C) + * }; + * }; + * }; + * }; + */ + graph_parse_daifmt(ep, &daifmt, &bit_frame); /* (C) */ + graph_parse_daifmt(port, &daifmt, &bit_frame); /* (B) */ + if (of_node_name_eq(ports, "ports")) + graph_parse_daifmt(ports, &daifmt, &bit_frame); /* (A) */ + + /* + * convert bit_frame + * We need to flip clock_provider if it was CPU node, + * because it is Codec base. + */ + daiclk = snd_soc_daifmt_clock_provider_from_bitmap(bit_frame); + if (is_cpu_node) + daiclk = snd_soc_daifmt_clock_provider_flipped(daiclk); + + dai_link->dai_fmt = daifmt | daiclk; + dai_link->init = asoc_simple_dai_init; + dai_link->ops = &graph_ops; + if (priv->ops) + dai_link->ops = priv->ops; +} + +int audio_graph2_link_normal(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device_node *cpu_port = lnk; + struct device_node *cpu_ep = port_to_endpoint(cpu_port); + struct device_node *codec_port = of_graph_get_remote_port(cpu_ep); + int ret; + + /* + * call Codec first. + * see + * __graph_parse_node() :: DAI Naming + */ + ret = graph_parse_node(priv, GRAPH_NORMAL, codec_port, li, 0); + if (ret < 0) + goto err; + + /* + * call CPU, and set DAI Name + */ + ret = graph_parse_node(priv, GRAPH_NORMAL, cpu_port, li, 1); + if (ret < 0) + goto err; + + graph_link_init(priv, cpu_port, li, 1); +err: + of_node_put(codec_port); + of_node_put(cpu_ep); + + return ret; +} +EXPORT_SYMBOL_GPL(audio_graph2_link_normal); + +int audio_graph2_link_dpcm(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device_node *ep = port_to_endpoint(lnk); + struct device_node *rep = of_graph_get_remote_endpoint(ep); + struct device_node *rport = of_graph_get_remote_port(ep); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + int is_cpu = asoc_graph_is_ports0(lnk); + int ret; + + if (is_cpu) { + /* + * dpcm { + * // Front-End + * ports@0 { + * => lnk: port@0 { ep: { ... = rep }; }; + * ... + * }; + * // Back-End + * ports@0 { + * ... + * }; + * }; + * + * CPU { + * rports: ports { + * rport: port@0 { rep: { ... = ep } }; + * } + * } + */ + /* + * setup CPU here, Codec is already set as dummy. + * see + * asoc_simple_init_priv() + */ + dai_link->dynamic = 1; + dai_link->dpcm_merged_format = 1; + + ret = graph_parse_node(priv, GRAPH_DPCM, rport, li, 1); + if (ret) + goto err; + } else { + /* + * dpcm { + * // Front-End + * ports@0 { + * ... + * }; + * // Back-End + * ports@0 { + * => lnk: port@0 { ep: { ... = rep; }; }; + * ... + * }; + * }; + * + * Codec { + * rports: ports { + * rport: port@0 { rep: { ... = ep; }; }; + * } + * } + */ + /* + * setup Codec here, CPU is already set as dummy. + * see + * asoc_simple_init_priv() + */ + + /* BE settings */ + dai_link->no_pcm = 1; + dai_link->be_hw_params_fixup = asoc_simple_be_hw_params_fixup; + + ret = graph_parse_node(priv, GRAPH_DPCM, rport, li, 0); + if (ret < 0) + goto err; + } + + graph_parse_convert(ep, dai_props); /* at node of <dpcm> */ + graph_parse_convert(rep, dai_props); /* at node of <CPU/Codec> */ + + snd_soc_dai_link_set_capabilities(dai_link); + + graph_link_init(priv, rport, li, is_cpu); +err: + of_node_put(ep); + of_node_put(rep); + of_node_put(rport); + + return ret; +} +EXPORT_SYMBOL_GPL(audio_graph2_link_dpcm); + +int audio_graph2_link_c2c(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct device_node *port0, *port1, *ports; + struct device_node *codec0_port, *codec1_port; + struct device_node *ep0, *ep1; + u32 val = 0; + int ret = -EINVAL; + + /* + * codec2codec { + * ports { + * rate = <48000>; + * => lnk: port@0 { c2c0_ep: { ... = codec0_ep; }; }; + * port@1 { c2c1_ep: { ... = codec1_ep; }; }; + * }; + * }; + * + * Codec { + * ports { + * port@0 { codec0_ep: ... }; }; + * port@1 { codec1_ep: ... }; }; + * }; + * }; + */ + of_node_get(lnk); + port0 = lnk; + ports = of_get_parent(port0); + port1 = of_get_next_child(ports, lnk); + + /* + * Card2 can use original Codec2Codec settings if DT has. + * It will use default settings if no settings on DT. + * see + * asoc_simple_init_for_codec2codec() + * + * Add more settings here if needed + */ + of_property_read_u32(ports, "rate", &val); + if (val) { + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_pcm_stream *c2c_conf; + + c2c_conf = devm_kzalloc(dev, sizeof(*c2c_conf), GFP_KERNEL); + if (!c2c_conf) + goto err1; + + c2c_conf->formats = SNDRV_PCM_FMTBIT_S32_LE; /* update ME */ + c2c_conf->rates = SNDRV_PCM_RATE_8000_384000; + c2c_conf->rate_min = + c2c_conf->rate_max = val; + c2c_conf->channels_min = + c2c_conf->channels_max = 2; /* update ME */ + + dai_link->c2c_params = c2c_conf; + dai_link->num_c2c_params = 1; + } + + ep0 = port_to_endpoint(port0); + ep1 = port_to_endpoint(port1); + + codec0_port = of_graph_get_remote_port(ep0); + codec1_port = of_graph_get_remote_port(ep1); + + /* + * call Codec first. + * see + * __graph_parse_node() :: DAI Naming + */ + ret = graph_parse_node(priv, GRAPH_C2C, codec1_port, li, 0); + if (ret < 0) + goto err2; + + /* + * call CPU, and set DAI Name + */ + ret = graph_parse_node(priv, GRAPH_C2C, codec0_port, li, 1); + if (ret < 0) + goto err2; + + graph_link_init(priv, codec0_port, li, 1); +err2: + of_node_put(ep0); + of_node_put(ep1); + of_node_put(codec0_port); + of_node_put(codec1_port); +err1: + of_node_put(ports); + of_node_put(port0); + of_node_put(port1); + + return ret; +} +EXPORT_SYMBOL_GPL(audio_graph2_link_c2c); + +static int graph_link(struct asoc_simple_priv *priv, + struct graph2_custom_hooks *hooks, + enum graph_type gtype, + struct device_node *lnk, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + GRAPH2_CUSTOM func = NULL; + int ret = -EINVAL; + + switch (gtype) { + case GRAPH_NORMAL: + if (hooks && hooks->custom_normal) + func = hooks->custom_normal; + else + func = audio_graph2_link_normal; + break; + case GRAPH_DPCM: + if (hooks && hooks->custom_dpcm) + func = hooks->custom_dpcm; + else + func = audio_graph2_link_dpcm; + break; + case GRAPH_C2C: + if (hooks && hooks->custom_c2c) + func = hooks->custom_c2c; + else + func = audio_graph2_link_c2c; + break; + default: + break; + } + + if (!func) { + dev_err(dev, "non supported gtype (%d)\n", gtype); + goto err; + } + + ret = func(priv, lnk, li); + if (ret < 0) + goto err; + + li->link++; +err: + return ret; +} + +static int graph_counter(struct device_node *lnk) +{ + /* + * Multi CPU / Codec + * + * multi { + * ports { + * => lnk: port@0 { ... }; + * port@1 { ... }; + * port@2 { ... }; + * ... + * }; + * }; + * + * ignore first lnk part + */ + if (graph_lnk_is_multi(lnk)) + return of_graph_get_endpoint_count(of_get_parent(lnk)) - 1; + /* + * Single CPU / Codec + */ + else + return 1; +} + +static int graph_count_normal(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device_node *cpu_port = lnk; + struct device_node *cpu_ep = port_to_endpoint(cpu_port); + struct device_node *codec_port = of_graph_get_remote_port(cpu_ep); + + /* + * CPU { + * => lnk: port { endpoint { .. }; }; + * }; + */ + /* + * DON'T REMOVE platforms + * see + * simple-card.c :: simple_count_noml() + */ + li->num[li->link].cpus = + li->num[li->link].platforms = graph_counter(cpu_port); + + li->num[li->link].codecs = graph_counter(codec_port); + + of_node_put(cpu_ep); + of_node_put(codec_port); + + return 0; +} + +static int graph_count_dpcm(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device_node *ep = port_to_endpoint(lnk); + struct device_node *rport = of_graph_get_remote_port(ep); + + /* + * dpcm { + * // Front-End + * ports@0 { + * => lnk: port@0 { endpoint { ... }; }; + * ... + * }; + * // Back-End + * ports@1 { + * => lnk: port@0 { endpoint { ... }; }; + * ... + * }; + * }; + */ + + if (asoc_graph_is_ports0(lnk)) { + /* + * DON'T REMOVE platforms + * see + * simple-card.c :: simple_count_noml() + */ + li->num[li->link].cpus = graph_counter(rport); /* FE */ + li->num[li->link].platforms = graph_counter(rport); + } else { + li->num[li->link].codecs = graph_counter(rport); /* BE */ + } + + of_node_put(ep); + of_node_put(rport); + + return 0; +} + +static int graph_count_c2c(struct asoc_simple_priv *priv, + struct device_node *lnk, + struct link_info *li) +{ + struct device_node *ports = of_get_parent(lnk); + struct device_node *port0 = lnk; + struct device_node *port1 = of_get_next_child(ports, lnk); + struct device_node *ep0 = port_to_endpoint(port0); + struct device_node *ep1 = port_to_endpoint(port1); + struct device_node *codec0 = of_graph_get_remote_port(ep0); + struct device_node *codec1 = of_graph_get_remote_port(ep1); + + of_node_get(lnk); + + /* + * codec2codec { + * ports { + * => lnk: port@0 { endpoint { ... }; }; + * port@1 { endpoint { ... }; }; + * }; + * }; + */ + /* + * DON'T REMOVE platforms + * see + * simple-card.c :: simple_count_noml() + */ + li->num[li->link].cpus = + li->num[li->link].platforms = graph_counter(codec0); + + li->num[li->link].codecs = graph_counter(codec1); + + of_node_put(ports); + of_node_put(port1); + of_node_put(ep0); + of_node_put(ep1); + of_node_put(codec0); + of_node_put(codec1); + + return 0; +} + +static int graph_count(struct asoc_simple_priv *priv, + struct graph2_custom_hooks *hooks, + enum graph_type gtype, + struct device_node *lnk, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + GRAPH2_CUSTOM func = NULL; + int ret = -EINVAL; + + if (li->link >= SNDRV_MAX_LINKS) { + dev_err(dev, "too many links\n"); + return ret; + } + + switch (gtype) { + case GRAPH_NORMAL: + func = graph_count_normal; + break; + case GRAPH_DPCM: + func = graph_count_dpcm; + break; + case GRAPH_C2C: + func = graph_count_c2c; + break; + default: + break; + } + + if (!func) { + dev_err(dev, "non supported gtype (%d)\n", gtype); + goto err; + } + + ret = func(priv, lnk, li); + if (ret < 0) + goto err; + + li->link++; +err: + return ret; +} + +static int graph_for_each_link(struct asoc_simple_priv *priv, + struct graph2_custom_hooks *hooks, + struct link_info *li, + int (*func)(struct asoc_simple_priv *priv, + struct graph2_custom_hooks *hooks, + enum graph_type gtype, + struct device_node *lnk, + struct link_info *li)) +{ + struct of_phandle_iterator it; + struct device *dev = simple_priv_to_dev(priv); + struct device_node *node = dev->of_node; + struct device_node *lnk; + enum graph_type gtype; + int rc, ret; + + /* loop for all listed CPU port */ + of_for_each_phandle(&it, rc, node, "links", NULL, 0) { + lnk = it.node; + + gtype = graph_get_type(priv, lnk); + + ret = func(priv, hooks, gtype, lnk, li); + if (ret < 0) + return ret; + } + + return 0; +} + +int audio_graph2_parse_of(struct asoc_simple_priv *priv, struct device *dev, + struct graph2_custom_hooks *hooks) +{ + struct snd_soc_card *card = simple_priv_to_card(priv); + struct link_info *li; + int ret; + + li = devm_kzalloc(dev, sizeof(*li), GFP_KERNEL); + if (!li) + return -ENOMEM; + + card->probe = asoc_graph_card_probe; + card->owner = THIS_MODULE; + card->dev = dev; + + if ((hooks) && (hooks)->hook_pre) { + ret = (hooks)->hook_pre(priv); + if (ret < 0) + goto err; + } + + ret = graph_for_each_link(priv, hooks, li, graph_count); + if (!li->link) + ret = -EINVAL; + if (ret < 0) + goto err; + + ret = asoc_simple_init_priv(priv, li); + if (ret < 0) + goto err; + + priv->pa_gpio = devm_gpiod_get_optional(dev, "pa", GPIOD_OUT_LOW); + if (IS_ERR(priv->pa_gpio)) { + ret = PTR_ERR(priv->pa_gpio); + dev_err(dev, "failed to get amplifier gpio: %d\n", ret); + goto err; + } + + ret = asoc_simple_parse_widgets(card, NULL); + if (ret < 0) + goto err; + + ret = asoc_simple_parse_routing(card, NULL); + if (ret < 0) + goto err; + + memset(li, 0, sizeof(*li)); + ret = graph_for_each_link(priv, hooks, li, graph_link); + if (ret < 0) + goto err; + + ret = asoc_simple_parse_card_name(card, NULL); + if (ret < 0) + goto err; + + snd_soc_card_set_drvdata(card, priv); + + if ((hooks) && (hooks)->hook_post) { + ret = (hooks)->hook_post(priv); + if (ret < 0) + goto err; + } + + asoc_simple_debug_info(priv); + + ret = devm_snd_soc_register_card(dev, card); +err: + devm_kfree(dev, li); + + if (ret < 0) + dev_err_probe(dev, ret, "parse error\n"); + + return ret; +} +EXPORT_SYMBOL_GPL(audio_graph2_parse_of); + +static int graph_probe(struct platform_device *pdev) +{ + struct asoc_simple_priv *priv; + struct device *dev = &pdev->dev; + + /* Allocate the private data and the DAI link array */ + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + return audio_graph2_parse_of(priv, dev, NULL); +} + +static const struct of_device_id graph_of_match[] = { + { .compatible = "audio-graph-card2", }, + {}, +}; +MODULE_DEVICE_TABLE(of, graph_of_match); + +static struct platform_driver graph_card = { + .driver = { + .name = "asoc-audio-graph-card2", + .pm = &snd_soc_pm_ops, + .of_match_table = graph_of_match, + }, + .probe = graph_probe, + .remove = asoc_simple_remove, +}; +module_platform_driver(graph_card); + +MODULE_ALIAS("platform:asoc-audio-graph-card2"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ASoC Audio Graph Card2"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); diff --git a/sound/soc/generic/simple-card-utils.c b/sound/soc/generic/simple-card-utils.c new file mode 100644 index 0000000000..2588ec735d --- /dev/null +++ b/sound/soc/generic/simple-card-utils.c @@ -0,0 +1,1138 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// simple-card-utils.c +// +// Copyright (c) 2016 Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> + +#include <linux/clk.h> +#include <linux/gpio.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_graph.h> +#include <sound/jack.h> +#include <sound/pcm_params.h> +#include <sound/simple_card_utils.h> + +static void asoc_simple_fixup_sample_fmt(struct asoc_simple_data *data, + struct snd_pcm_hw_params *params) +{ + int i; + struct snd_mask *mask = hw_param_mask(params, + SNDRV_PCM_HW_PARAM_FORMAT); + struct { + char *fmt; + u32 val; + } of_sample_fmt_table[] = { + { "s8", SNDRV_PCM_FORMAT_S8}, + { "s16_le", SNDRV_PCM_FORMAT_S16_LE}, + { "s24_le", SNDRV_PCM_FORMAT_S24_LE}, + { "s24_3le", SNDRV_PCM_FORMAT_S24_3LE}, + { "s32_le", SNDRV_PCM_FORMAT_S32_LE}, + }; + + for (i = 0; i < ARRAY_SIZE(of_sample_fmt_table); i++) { + if (!strcmp(data->convert_sample_format, + of_sample_fmt_table[i].fmt)) { + snd_mask_none(mask); + snd_mask_set(mask, of_sample_fmt_table[i].val); + break; + } + } +} + +void asoc_simple_parse_convert(struct device_node *np, + char *prefix, + struct asoc_simple_data *data) +{ + char prop[128]; + + if (!prefix) + prefix = ""; + + /* sampling rate convert */ + snprintf(prop, sizeof(prop), "%s%s", prefix, "convert-rate"); + of_property_read_u32(np, prop, &data->convert_rate); + + /* channels transfer */ + snprintf(prop, sizeof(prop), "%s%s", prefix, "convert-channels"); + of_property_read_u32(np, prop, &data->convert_channels); + + /* convert sample format */ + snprintf(prop, sizeof(prop), "%s%s", prefix, "convert-sample-format"); + of_property_read_string(np, prop, &data->convert_sample_format); +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_convert); + +/** + * asoc_simple_is_convert_required() - Query if HW param conversion was requested + * @data: Link data. + * + * Returns true if any HW param conversion was requested for this DAI link with + * any "convert-xxx" properties. + */ +bool asoc_simple_is_convert_required(const struct asoc_simple_data *data) +{ + return data->convert_rate || + data->convert_channels || + data->convert_sample_format; +} +EXPORT_SYMBOL_GPL(asoc_simple_is_convert_required); + +int asoc_simple_parse_daifmt(struct device *dev, + struct device_node *node, + struct device_node *codec, + char *prefix, + unsigned int *retfmt) +{ + struct device_node *bitclkmaster = NULL; + struct device_node *framemaster = NULL; + unsigned int daifmt; + + daifmt = snd_soc_daifmt_parse_format(node, prefix); + + snd_soc_daifmt_parse_clock_provider_as_phandle(node, prefix, &bitclkmaster, &framemaster); + if (!bitclkmaster && !framemaster) { + /* + * No dai-link level and master setting was not found from + * sound node level, revert back to legacy DT parsing and + * take the settings from codec node. + */ + dev_dbg(dev, "Revert to legacy daifmt parsing\n"); + + daifmt |= snd_soc_daifmt_parse_clock_provider_as_flag(codec, NULL); + } else { + daifmt |= snd_soc_daifmt_clock_provider_from_bitmap( + ((codec == bitclkmaster) << 4) | (codec == framemaster)); + } + + of_node_put(bitclkmaster); + of_node_put(framemaster); + + *retfmt = daifmt; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_daifmt); + +int asoc_simple_parse_tdm_width_map(struct device *dev, struct device_node *np, + struct asoc_simple_dai *dai) +{ + u32 *array_values, *p; + int n, i, ret; + + if (!of_property_read_bool(np, "dai-tdm-slot-width-map")) + return 0; + + n = of_property_count_elems_of_size(np, "dai-tdm-slot-width-map", sizeof(u32)); + if (n % 3) { + dev_err(dev, "Invalid number of cells for dai-tdm-slot-width-map\n"); + return -EINVAL; + } + + dai->tdm_width_map = devm_kcalloc(dev, n, sizeof(*dai->tdm_width_map), GFP_KERNEL); + if (!dai->tdm_width_map) + return -ENOMEM; + + array_values = kcalloc(n, sizeof(*array_values), GFP_KERNEL); + if (!array_values) + return -ENOMEM; + + ret = of_property_read_u32_array(np, "dai-tdm-slot-width-map", array_values, n); + if (ret < 0) { + dev_err(dev, "Could not read dai-tdm-slot-width-map: %d\n", ret); + goto out; + } + + p = array_values; + for (i = 0; i < n / 3; ++i) { + dai->tdm_width_map[i].sample_bits = *p++; + dai->tdm_width_map[i].slot_width = *p++; + dai->tdm_width_map[i].slot_count = *p++; + } + + dai->n_tdm_widths = i; + ret = 0; +out: + kfree(array_values); + + return ret; +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_tdm_width_map); + +int asoc_simple_set_dailink_name(struct device *dev, + struct snd_soc_dai_link *dai_link, + const char *fmt, ...) +{ + va_list ap; + char *name = NULL; + int ret = -ENOMEM; + + va_start(ap, fmt); + name = devm_kvasprintf(dev, GFP_KERNEL, fmt, ap); + va_end(ap); + + if (name) { + ret = 0; + + dai_link->name = name; + dai_link->stream_name = name; + } + + return ret; +} +EXPORT_SYMBOL_GPL(asoc_simple_set_dailink_name); + +int asoc_simple_parse_card_name(struct snd_soc_card *card, + char *prefix) +{ + int ret; + + if (!prefix) + prefix = ""; + + /* Parse the card name from DT */ + ret = snd_soc_of_parse_card_name(card, "label"); + if (ret < 0 || !card->name) { + char prop[128]; + + snprintf(prop, sizeof(prop), "%sname", prefix); + ret = snd_soc_of_parse_card_name(card, prop); + if (ret < 0) + return ret; + } + + if (!card->name && card->dai_link) + card->name = card->dai_link->name; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_card_name); + +static int asoc_simple_clk_enable(struct asoc_simple_dai *dai) +{ + if (dai) + return clk_prepare_enable(dai->clk); + + return 0; +} + +static void asoc_simple_clk_disable(struct asoc_simple_dai *dai) +{ + if (dai) + clk_disable_unprepare(dai->clk); +} + +int asoc_simple_parse_clk(struct device *dev, + struct device_node *node, + struct asoc_simple_dai *simple_dai, + struct snd_soc_dai_link_component *dlc) +{ + struct clk *clk; + u32 val; + + /* + * Parse dai->sysclk come from "clocks = <&xxx>" + * (if system has common clock) + * or "system-clock-frequency = <xxx>" + * or device's module clock. + */ + clk = devm_get_clk_from_child(dev, node, NULL); + simple_dai->clk_fixed = of_property_read_bool( + node, "system-clock-fixed"); + if (!IS_ERR(clk)) { + simple_dai->sysclk = clk_get_rate(clk); + + simple_dai->clk = clk; + } else if (!of_property_read_u32(node, "system-clock-frequency", &val)) { + simple_dai->sysclk = val; + simple_dai->clk_fixed = true; + } else { + clk = devm_get_clk_from_child(dev, dlc->of_node, NULL); + if (!IS_ERR(clk)) + simple_dai->sysclk = clk_get_rate(clk); + } + + if (of_property_read_bool(node, "system-clock-direction-out")) + simple_dai->clk_direction = SND_SOC_CLOCK_OUT; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_clk); + +static int asoc_simple_check_fixed_sysclk(struct device *dev, + struct asoc_simple_dai *dai, + unsigned int *fixed_sysclk) +{ + if (dai->clk_fixed) { + if (*fixed_sysclk && *fixed_sysclk != dai->sysclk) { + dev_err(dev, "inconsistent fixed sysclk rates (%u vs %u)\n", + *fixed_sysclk, dai->sysclk); + return -EINVAL; + } + *fixed_sysclk = dai->sysclk; + } + + return 0; +} + +int asoc_simple_startup(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct simple_dai_props *props = simple_priv_to_props(priv, rtd->num); + struct asoc_simple_dai *dai; + unsigned int fixed_sysclk = 0; + int i1, i2, i; + int ret; + + for_each_prop_dai_cpu(props, i1, dai) { + ret = asoc_simple_clk_enable(dai); + if (ret) + goto cpu_err; + ret = asoc_simple_check_fixed_sysclk(rtd->dev, dai, &fixed_sysclk); + if (ret) + goto cpu_err; + } + + for_each_prop_dai_codec(props, i2, dai) { + ret = asoc_simple_clk_enable(dai); + if (ret) + goto codec_err; + ret = asoc_simple_check_fixed_sysclk(rtd->dev, dai, &fixed_sysclk); + if (ret) + goto codec_err; + } + + if (fixed_sysclk && props->mclk_fs) { + unsigned int fixed_rate = fixed_sysclk / props->mclk_fs; + + if (fixed_sysclk % props->mclk_fs) { + dev_err(rtd->dev, "fixed sysclk %u not divisible by mclk_fs %u\n", + fixed_sysclk, props->mclk_fs); + ret = -EINVAL; + goto codec_err; + } + ret = snd_pcm_hw_constraint_minmax(substream->runtime, SNDRV_PCM_HW_PARAM_RATE, + fixed_rate, fixed_rate); + if (ret < 0) + goto codec_err; + } + + return 0; + +codec_err: + for_each_prop_dai_codec(props, i, dai) { + if (i >= i2) + break; + asoc_simple_clk_disable(dai); + } +cpu_err: + for_each_prop_dai_cpu(props, i, dai) { + if (i >= i1) + break; + asoc_simple_clk_disable(dai); + } + return ret; +} +EXPORT_SYMBOL_GPL(asoc_simple_startup); + +void asoc_simple_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct simple_dai_props *props = simple_priv_to_props(priv, rtd->num); + struct asoc_simple_dai *dai; + int i; + + for_each_prop_dai_cpu(props, i, dai) { + struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(rtd, i); + + if (props->mclk_fs && !dai->clk_fixed && !snd_soc_dai_active(cpu_dai)) + snd_soc_dai_set_sysclk(cpu_dai, + 0, 0, SND_SOC_CLOCK_OUT); + + asoc_simple_clk_disable(dai); + } + for_each_prop_dai_codec(props, i, dai) { + struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(rtd, i); + + if (props->mclk_fs && !dai->clk_fixed && !snd_soc_dai_active(codec_dai)) + snd_soc_dai_set_sysclk(codec_dai, + 0, 0, SND_SOC_CLOCK_IN); + + asoc_simple_clk_disable(dai); + } +} +EXPORT_SYMBOL_GPL(asoc_simple_shutdown); + +static int asoc_simple_set_clk_rate(struct device *dev, + struct asoc_simple_dai *simple_dai, + unsigned long rate) +{ + if (!simple_dai) + return 0; + + if (simple_dai->clk_fixed && rate != simple_dai->sysclk) { + dev_err(dev, "dai %s invalid clock rate %lu\n", simple_dai->name, rate); + return -EINVAL; + } + + if (!simple_dai->clk) + return 0; + + if (clk_get_rate(simple_dai->clk) == rate) + return 0; + + return clk_set_rate(simple_dai->clk, rate); +} + +static int asoc_simple_set_tdm(struct snd_soc_dai *dai, + struct asoc_simple_dai *simple_dai, + struct snd_pcm_hw_params *params) +{ + int sample_bits = params_width(params); + int slot_width, slot_count; + int i, ret; + + if (!simple_dai || !simple_dai->tdm_width_map) + return 0; + + slot_width = simple_dai->slot_width; + slot_count = simple_dai->slots; + + if (slot_width == 0) + slot_width = sample_bits; + + for (i = 0; i < simple_dai->n_tdm_widths; ++i) { + if (simple_dai->tdm_width_map[i].sample_bits == sample_bits) { + slot_width = simple_dai->tdm_width_map[i].slot_width; + slot_count = simple_dai->tdm_width_map[i].slot_count; + break; + } + } + + ret = snd_soc_dai_set_tdm_slot(dai, + simple_dai->tx_slot_mask, + simple_dai->rx_slot_mask, + slot_count, + slot_width); + if (ret && ret != -ENOTSUPP) { + dev_err(dai->dev, "simple-card: set_tdm_slot error: %d\n", ret); + return ret; + } + + return 0; +} + +int asoc_simple_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct asoc_simple_dai *pdai; + struct snd_soc_dai *sdai; + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct simple_dai_props *props = simple_priv_to_props(priv, rtd->num); + unsigned int mclk, mclk_fs = 0; + int i, ret; + + if (props->mclk_fs) + mclk_fs = props->mclk_fs; + + if (mclk_fs) { + struct snd_soc_component *component; + mclk = params_rate(params) * mclk_fs; + + for_each_prop_dai_codec(props, i, pdai) { + ret = asoc_simple_set_clk_rate(rtd->dev, pdai, mclk); + if (ret < 0) + return ret; + } + + for_each_prop_dai_cpu(props, i, pdai) { + ret = asoc_simple_set_clk_rate(rtd->dev, pdai, mclk); + if (ret < 0) + return ret; + } + + /* Ensure sysclk is set on all components in case any + * (such as platform components) are missed by calls to + * snd_soc_dai_set_sysclk. + */ + for_each_rtd_components(rtd, i, component) { + ret = snd_soc_component_set_sysclk(component, 0, 0, + mclk, SND_SOC_CLOCK_IN); + if (ret && ret != -ENOTSUPP) + return ret; + } + + for_each_rtd_codec_dais(rtd, i, sdai) { + ret = snd_soc_dai_set_sysclk(sdai, 0, mclk, SND_SOC_CLOCK_IN); + if (ret && ret != -ENOTSUPP) + return ret; + } + + for_each_rtd_cpu_dais(rtd, i, sdai) { + ret = snd_soc_dai_set_sysclk(sdai, 0, mclk, SND_SOC_CLOCK_OUT); + if (ret && ret != -ENOTSUPP) + return ret; + } + } + + for_each_prop_dai_codec(props, i, pdai) { + sdai = asoc_rtd_to_codec(rtd, i); + ret = asoc_simple_set_tdm(sdai, pdai, params); + if (ret < 0) + return ret; + } + + for_each_prop_dai_cpu(props, i, pdai) { + sdai = asoc_rtd_to_cpu(rtd, i); + ret = asoc_simple_set_tdm(sdai, pdai, params); + if (ret < 0) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_hw_params); + +int asoc_simple_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd, + struct snd_pcm_hw_params *params) +{ + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, rtd->num); + struct asoc_simple_data *data = &dai_props->adata; + struct snd_interval *rate = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE); + struct snd_interval *channels = hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS); + + if (data->convert_rate) + rate->min = + rate->max = data->convert_rate; + + if (data->convert_channels) + channels->min = + channels->max = data->convert_channels; + + if (data->convert_sample_format) + asoc_simple_fixup_sample_fmt(data, params); + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_be_hw_params_fixup); + +static int asoc_simple_init_dai(struct snd_soc_dai *dai, + struct asoc_simple_dai *simple_dai) +{ + int ret; + + if (!simple_dai) + return 0; + + if (simple_dai->sysclk) { + ret = snd_soc_dai_set_sysclk(dai, 0, simple_dai->sysclk, + simple_dai->clk_direction); + if (ret && ret != -ENOTSUPP) { + dev_err(dai->dev, "simple-card: set_sysclk error\n"); + return ret; + } + } + + if (simple_dai->slots) { + ret = snd_soc_dai_set_tdm_slot(dai, + simple_dai->tx_slot_mask, + simple_dai->rx_slot_mask, + simple_dai->slots, + simple_dai->slot_width); + if (ret && ret != -ENOTSUPP) { + dev_err(dai->dev, "simple-card: set_tdm_slot error\n"); + return ret; + } + } + + return 0; +} + +static inline int asoc_simple_component_is_codec(struct snd_soc_component *component) +{ + return component->driver->endianness; +} + +static int asoc_simple_init_for_codec2codec(struct snd_soc_pcm_runtime *rtd, + struct simple_dai_props *dai_props) +{ + struct snd_soc_dai_link *dai_link = rtd->dai_link; + struct snd_soc_component *component; + struct snd_soc_pcm_stream *c2c_params; + struct snd_pcm_hardware hw; + int i, ret, stream; + + /* Do nothing if it already has Codec2Codec settings */ + if (dai_link->c2c_params) + return 0; + + /* Do nothing if it was DPCM :: BE */ + if (dai_link->no_pcm) + return 0; + + /* Only Codecs */ + for_each_rtd_components(rtd, i, component) { + if (!asoc_simple_component_is_codec(component)) + return 0; + } + + /* Assumes the capabilities are the same for all supported streams */ + for_each_pcm_streams(stream) { + ret = snd_soc_runtime_calc_hw(rtd, &hw, stream); + if (ret == 0) + break; + } + + if (ret < 0) { + dev_err(rtd->dev, "simple-card: no valid dai_link params\n"); + return ret; + } + + c2c_params = devm_kzalloc(rtd->dev, sizeof(*c2c_params), GFP_KERNEL); + if (!c2c_params) + return -ENOMEM; + + c2c_params->formats = hw.formats; + c2c_params->rates = hw.rates; + c2c_params->rate_min = hw.rate_min; + c2c_params->rate_max = hw.rate_max; + c2c_params->channels_min = hw.channels_min; + c2c_params->channels_max = hw.channels_max; + + dai_link->c2c_params = c2c_params; + dai_link->num_c2c_params = 1; + + return 0; +} + +int asoc_simple_dai_init(struct snd_soc_pcm_runtime *rtd) +{ + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(rtd->card); + struct simple_dai_props *props = simple_priv_to_props(priv, rtd->num); + struct asoc_simple_dai *dai; + int i, ret; + + for_each_prop_dai_codec(props, i, dai) { + ret = asoc_simple_init_dai(asoc_rtd_to_codec(rtd, i), dai); + if (ret < 0) + return ret; + } + for_each_prop_dai_cpu(props, i, dai) { + ret = asoc_simple_init_dai(asoc_rtd_to_cpu(rtd, i), dai); + if (ret < 0) + return ret; + } + + ret = asoc_simple_init_for_codec2codec(rtd, props); + if (ret < 0) + return ret; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_dai_init); + +void asoc_simple_canonicalize_platform(struct snd_soc_dai_link_component *platforms, + struct snd_soc_dai_link_component *cpus) +{ + /* + * Assumes Platform == CPU + * + * Some CPU might be using soc-generic-dmaengine-pcm. This means CPU and Platform + * are different Component, but are sharing same component->dev. + * + * Let's assume Platform is same as CPU if it doesn't identify Platform on DT. + * see + * simple-card.c :: simple_count_noml() + */ + if (!platforms->of_node) + snd_soc_dlc_use_cpu_as_platform(platforms, cpus); +} +EXPORT_SYMBOL_GPL(asoc_simple_canonicalize_platform); + +void asoc_simple_canonicalize_cpu(struct snd_soc_dai_link_component *cpus, + int is_single_links) +{ + /* + * In soc_bind_dai_link() will check cpu name after + * of_node matching if dai_link has cpu_dai_name. + * but, it will never match if name was created by + * fmt_single_name() remove cpu_dai_name if cpu_args + * was 0. See: + * fmt_single_name() + * fmt_multiple_name() + */ + if (is_single_links) + cpus->dai_name = NULL; +} +EXPORT_SYMBOL_GPL(asoc_simple_canonicalize_cpu); + +void asoc_simple_clean_reference(struct snd_soc_card *card) +{ + struct snd_soc_dai_link *dai_link; + struct snd_soc_dai_link_component *cpu; + struct snd_soc_dai_link_component *codec; + int i, j; + + for_each_card_prelinks(card, i, dai_link) { + for_each_link_cpus(dai_link, j, cpu) + of_node_put(cpu->of_node); + for_each_link_codecs(dai_link, j, codec) + of_node_put(codec->of_node); + } +} +EXPORT_SYMBOL_GPL(asoc_simple_clean_reference); + +int asoc_simple_parse_routing(struct snd_soc_card *card, + char *prefix) +{ + struct device_node *node = card->dev->of_node; + char prop[128]; + + if (!prefix) + prefix = ""; + + snprintf(prop, sizeof(prop), "%s%s", prefix, "routing"); + + if (!of_property_read_bool(node, prop)) + return 0; + + return snd_soc_of_parse_audio_routing(card, prop); +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_routing); + +int asoc_simple_parse_widgets(struct snd_soc_card *card, + char *prefix) +{ + struct device_node *node = card->dev->of_node; + char prop[128]; + + if (!prefix) + prefix = ""; + + snprintf(prop, sizeof(prop), "%s%s", prefix, "widgets"); + + if (of_property_read_bool(node, prop)) + return snd_soc_of_parse_audio_simple_widgets(card, prop); + + /* no widgets is not error */ + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_widgets); + +int asoc_simple_parse_pin_switches(struct snd_soc_card *card, + char *prefix) +{ + char prop[128]; + + if (!prefix) + prefix = ""; + + snprintf(prop, sizeof(prop), "%s%s", prefix, "pin-switches"); + + return snd_soc_of_parse_pin_switches(card, prop); +} +EXPORT_SYMBOL_GPL(asoc_simple_parse_pin_switches); + +int asoc_simple_init_jack(struct snd_soc_card *card, + struct asoc_simple_jack *sjack, + int is_hp, char *prefix, + char *pin) +{ + struct device *dev = card->dev; + struct gpio_desc *desc; + char prop[128]; + char *pin_name; + char *gpio_name; + int mask; + int error; + + if (!prefix) + prefix = ""; + + sjack->gpio.gpio = -ENOENT; + + if (is_hp) { + snprintf(prop, sizeof(prop), "%shp-det", prefix); + pin_name = pin ? pin : "Headphones"; + gpio_name = "Headphone detection"; + mask = SND_JACK_HEADPHONE; + } else { + snprintf(prop, sizeof(prop), "%smic-det", prefix); + pin_name = pin ? pin : "Mic Jack"; + gpio_name = "Mic detection"; + mask = SND_JACK_MICROPHONE; + } + + desc = gpiod_get_optional(dev, prop, GPIOD_IN); + error = PTR_ERR_OR_ZERO(desc); + if (error) + return error; + + if (desc) { + error = gpiod_set_consumer_name(desc, gpio_name); + if (error) + return error; + + sjack->pin.pin = pin_name; + sjack->pin.mask = mask; + + sjack->gpio.name = gpio_name; + sjack->gpio.report = mask; + sjack->gpio.desc = desc; + sjack->gpio.debounce_time = 150; + + snd_soc_card_jack_new_pins(card, pin_name, mask, &sjack->jack, + &sjack->pin, 1); + + snd_soc_jack_add_gpios(&sjack->jack, 1, &sjack->gpio); + } + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_init_jack); + +int asoc_simple_init_aux_jacks(struct asoc_simple_priv *priv, char *prefix) +{ + struct snd_soc_card *card = simple_priv_to_card(priv); + struct snd_soc_component *component; + int found_jack_index = 0; + int type = 0; + int num = 0; + int ret; + + if (priv->aux_jacks) + return 0; + + for_each_card_auxs(card, component) { + type = snd_soc_component_get_jack_type(component); + if (type > 0) + num++; + } + if (num < 1) + return 0; + + priv->aux_jacks = devm_kcalloc(card->dev, num, + sizeof(struct snd_soc_jack), GFP_KERNEL); + if (!priv->aux_jacks) + return -ENOMEM; + + for_each_card_auxs(card, component) { + char id[128]; + struct snd_soc_jack *jack; + + if (found_jack_index >= num) + break; + + type = snd_soc_component_get_jack_type(component); + if (type <= 0) + continue; + + /* create jack */ + jack = &(priv->aux_jacks[found_jack_index++]); + snprintf(id, sizeof(id), "%s-jack", component->name); + ret = snd_soc_card_jack_new(card, id, type, jack); + if (ret) + continue; + + (void)snd_soc_component_set_jack(component, jack, NULL); + } + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_init_aux_jacks); + +int asoc_simple_init_priv(struct asoc_simple_priv *priv, + struct link_info *li) +{ + struct snd_soc_card *card = simple_priv_to_card(priv); + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link; + struct simple_dai_props *dai_props; + struct asoc_simple_dai *dais; + struct snd_soc_dai_link_component *dlcs; + struct snd_soc_codec_conf *cconf = NULL; + int i, dai_num = 0, dlc_num = 0, cnf_num = 0; + + dai_props = devm_kcalloc(dev, li->link, sizeof(*dai_props), GFP_KERNEL); + dai_link = devm_kcalloc(dev, li->link, sizeof(*dai_link), GFP_KERNEL); + if (!dai_props || !dai_link) + return -ENOMEM; + + /* + * dais (= CPU+Codec) + * dlcs (= CPU+Codec+Platform) + */ + for (i = 0; i < li->link; i++) { + int cc = li->num[i].cpus + li->num[i].codecs; + + dai_num += cc; + dlc_num += cc + li->num[i].platforms; + + if (!li->num[i].cpus) + cnf_num += li->num[i].codecs; + } + + dais = devm_kcalloc(dev, dai_num, sizeof(*dais), GFP_KERNEL); + dlcs = devm_kcalloc(dev, dlc_num, sizeof(*dlcs), GFP_KERNEL); + if (!dais || !dlcs) + return -ENOMEM; + + if (cnf_num) { + cconf = devm_kcalloc(dev, cnf_num, sizeof(*cconf), GFP_KERNEL); + if (!cconf) + return -ENOMEM; + } + + dev_dbg(dev, "link %d, dais %d, ccnf %d\n", + li->link, dai_num, cnf_num); + + priv->dai_props = dai_props; + priv->dai_link = dai_link; + priv->dais = dais; + priv->dlcs = dlcs; + priv->codec_conf = cconf; + + card->dai_link = priv->dai_link; + card->num_links = li->link; + card->codec_conf = cconf; + card->num_configs = cnf_num; + + for (i = 0; i < li->link; i++) { + if (li->num[i].cpus) { + /* Normal CPU */ + dai_link[i].cpus = dlcs; + dai_props[i].num.cpus = + dai_link[i].num_cpus = li->num[i].cpus; + dai_props[i].cpu_dai = dais; + + dlcs += li->num[i].cpus; + dais += li->num[i].cpus; + } else { + /* DPCM Be's CPU = dummy */ + dai_link[i].cpus = &asoc_dummy_dlc; + dai_props[i].num.cpus = + dai_link[i].num_cpus = 1; + } + + if (li->num[i].codecs) { + /* Normal Codec */ + dai_link[i].codecs = dlcs; + dai_props[i].num.codecs = + dai_link[i].num_codecs = li->num[i].codecs; + dai_props[i].codec_dai = dais; + + dlcs += li->num[i].codecs; + dais += li->num[i].codecs; + + if (!li->num[i].cpus) { + /* DPCM Be's Codec */ + dai_props[i].codec_conf = cconf; + cconf += li->num[i].codecs; + } + } else { + /* DPCM Fe's Codec = dummy */ + dai_link[i].codecs = &asoc_dummy_dlc; + dai_props[i].num.codecs = + dai_link[i].num_codecs = 1; + } + + if (li->num[i].platforms) { + /* Have Platform */ + dai_link[i].platforms = dlcs; + dai_props[i].num.platforms = + dai_link[i].num_platforms = li->num[i].platforms; + + dlcs += li->num[i].platforms; + } else { + /* Doesn't have Platform */ + dai_link[i].platforms = NULL; + dai_props[i].num.platforms = + dai_link[i].num_platforms = 0; + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_init_priv); + +int asoc_simple_remove(struct platform_device *pdev) +{ + struct snd_soc_card *card = platform_get_drvdata(pdev); + + asoc_simple_clean_reference(card); + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_simple_remove); + +int asoc_graph_card_probe(struct snd_soc_card *card) +{ + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(card); + int ret; + + ret = asoc_simple_init_hp(card, &priv->hp_jack, NULL); + if (ret < 0) + return ret; + + ret = asoc_simple_init_mic(card, &priv->mic_jack, NULL); + if (ret < 0) + return ret; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_graph_card_probe); + +int asoc_graph_is_ports0(struct device_node *np) +{ + struct device_node *port, *ports, *ports0, *top; + int ret; + + /* np is "endpoint" or "port" */ + if (of_node_name_eq(np, "endpoint")) { + port = of_get_parent(np); + } else { + port = np; + of_node_get(port); + } + + ports = of_get_parent(port); + top = of_get_parent(ports); + ports0 = of_get_child_by_name(top, "ports"); + + ret = ports0 == ports; + + of_node_put(port); + of_node_put(ports); + of_node_put(ports0); + of_node_put(top); + + return ret; +} +EXPORT_SYMBOL_GPL(asoc_graph_is_ports0); + +static int graph_get_dai_id(struct device_node *ep) +{ + struct device_node *node; + struct device_node *endpoint; + struct of_endpoint info; + int i, id; + int ret; + + /* use driver specified DAI ID if exist */ + ret = snd_soc_get_dai_id(ep); + if (ret != -ENOTSUPP) + return ret; + + /* use endpoint/port reg if exist */ + ret = of_graph_parse_endpoint(ep, &info); + if (ret == 0) { + /* + * Because it will count port/endpoint if it doesn't have "reg". + * But, we can't judge whether it has "no reg", or "reg = <0>" + * only of_graph_parse_endpoint(). + * We need to check "reg" property + */ + if (of_property_present(ep, "reg")) + return info.id; + + node = of_get_parent(ep); + ret = of_property_present(node, "reg"); + of_node_put(node); + if (ret) + return info.port; + } + node = of_graph_get_port_parent(ep); + + /* + * Non HDMI sound case, counting port/endpoint on its DT + * is enough. Let's count it. + */ + i = 0; + id = -1; + for_each_endpoint_of_node(node, endpoint) { + if (endpoint == ep) + id = i; + i++; + } + + of_node_put(node); + + if (id < 0) + return -ENODEV; + + return id; +} + +int asoc_graph_parse_dai(struct device *dev, struct device_node *ep, + struct snd_soc_dai_link_component *dlc, int *is_single_link) +{ + struct device_node *node; + struct of_phandle_args args = {}; + struct snd_soc_dai *dai; + int ret; + + if (!ep) + return 0; + + node = of_graph_get_port_parent(ep); + + /* + * Try to find from DAI node + */ + args.np = ep; + dai = snd_soc_get_dai_via_args(&args); + if (dai) { + dlc->dai_name = snd_soc_dai_name_get(dai); + dlc->dai_args = snd_soc_copy_dai_args(dev, &args); + if (!dlc->dai_args) + return -ENOMEM; + + goto parse_dai_end; + } + + /* Get dai->name */ + args.np = node; + args.args[0] = graph_get_dai_id(ep); + args.args_count = (of_graph_get_endpoint_count(node) > 1); + + /* + * FIXME + * + * Here, dlc->dai_name is pointer to CPU/Codec DAI name. + * If user unbinded CPU or Codec driver, but not for Sound Card, + * dlc->dai_name is keeping unbinded CPU or Codec + * driver's pointer. + * + * If user re-bind CPU or Codec driver again, ALSA SoC will try + * to rebind Card via snd_soc_try_rebind_card(), but because of + * above reason, it might can't bind Sound Card. + * Because Sound Card is pointing to released dai_name pointer. + * + * To avoid this rebind Card issue, + * 1) It needs to alloc memory to keep dai_name eventhough + * CPU or Codec driver was unbinded, or + * 2) user need to rebind Sound Card everytime + * if he unbinded CPU or Codec. + */ + ret = snd_soc_get_dlc(&args, dlc); + if (ret < 0) { + of_node_put(node); + return ret; + } + +parse_dai_end: + if (is_single_link) + *is_single_link = of_graph_get_endpoint_count(node) == 1; + + return 0; +} +EXPORT_SYMBOL_GPL(asoc_graph_parse_dai); + +/* Module information */ +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); +MODULE_DESCRIPTION("ALSA SoC Simple Card Utils"); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/generic/simple-card.c b/sound/soc/generic/simple-card.c new file mode 100644 index 0000000000..274417e39e --- /dev/null +++ b/sound/soc/generic/simple-card.c @@ -0,0 +1,839 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// ASoC simple sound card support +// +// Copyright (C) 2012 Renesas Solutions Corp. +// Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> + +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/string.h> +#include <sound/simple_card.h> +#include <sound/soc-dai.h> +#include <sound/soc.h> + +#define DPCM_SELECTABLE 1 + +#define DAI "sound-dai" +#define CELL "#sound-dai-cells" +#define PREFIX "simple-audio-card," + +static const struct snd_soc_ops simple_ops = { + .startup = asoc_simple_startup, + .shutdown = asoc_simple_shutdown, + .hw_params = asoc_simple_hw_params, +}; + +static int asoc_simple_parse_platform(struct device_node *node, + struct snd_soc_dai_link_component *dlc) +{ + struct of_phandle_args args; + int ret; + + if (!node) + return 0; + + /* + * Get node via "sound-dai = <&phandle port>" + * it will be used as xxx_of_node on soc_bind_dai_link() + */ + ret = of_parse_phandle_with_args(node, DAI, CELL, 0, &args); + if (ret) + return ret; + + /* dai_name is not required and may not exist for plat component */ + + dlc->of_node = args.np; + + return 0; +} + +static int asoc_simple_parse_dai(struct device *dev, + struct device_node *node, + struct snd_soc_dai_link_component *dlc, + int *is_single_link) +{ + struct of_phandle_args args; + struct snd_soc_dai *dai; + int ret; + + if (!node) + return 0; + + /* + * Get node via "sound-dai = <&phandle port>" + * it will be used as xxx_of_node on soc_bind_dai_link() + */ + ret = of_parse_phandle_with_args(node, DAI, CELL, 0, &args); + if (ret) + return ret; + + /* + * Try to find from DAI args + */ + dai = snd_soc_get_dai_via_args(&args); + if (dai) { + dlc->dai_name = snd_soc_dai_name_get(dai); + dlc->dai_args = snd_soc_copy_dai_args(dev, &args); + if (!dlc->dai_args) + return -ENOMEM; + + goto parse_dai_end; + } + + /* + * FIXME + * + * Here, dlc->dai_name is pointer to CPU/Codec DAI name. + * If user unbinded CPU or Codec driver, but not for Sound Card, + * dlc->dai_name is keeping unbinded CPU or Codec + * driver's pointer. + * + * If user re-bind CPU or Codec driver again, ALSA SoC will try + * to rebind Card via snd_soc_try_rebind_card(), but because of + * above reason, it might can't bind Sound Card. + * Because Sound Card is pointing to released dai_name pointer. + * + * To avoid this rebind Card issue, + * 1) It needs to alloc memory to keep dai_name eventhough + * CPU or Codec driver was unbinded, or + * 2) user need to rebind Sound Card everytime + * if he unbinded CPU or Codec. + */ + ret = snd_soc_get_dlc(&args, dlc); + if (ret < 0) + return ret; + +parse_dai_end: + if (is_single_link) + *is_single_link = !args.args_count; + + return 0; +} + +static void simple_parse_convert(struct device *dev, + struct device_node *np, + struct asoc_simple_data *adata) +{ + struct device_node *top = dev->of_node; + struct device_node *node = of_get_parent(np); + + asoc_simple_parse_convert(top, PREFIX, adata); + asoc_simple_parse_convert(node, PREFIX, adata); + asoc_simple_parse_convert(node, NULL, adata); + asoc_simple_parse_convert(np, NULL, adata); + + of_node_put(node); +} + +static void simple_parse_mclk_fs(struct device_node *top, + struct device_node *np, + struct simple_dai_props *props, + char *prefix) +{ + struct device_node *node = of_get_parent(np); + char prop[128]; + + snprintf(prop, sizeof(prop), "%smclk-fs", PREFIX); + of_property_read_u32(top, prop, &props->mclk_fs); + + snprintf(prop, sizeof(prop), "%smclk-fs", prefix); + of_property_read_u32(node, prop, &props->mclk_fs); + of_property_read_u32(np, prop, &props->mclk_fs); + + of_node_put(node); +} + +static int simple_parse_node(struct asoc_simple_priv *priv, + struct device_node *np, + struct link_info *li, + char *prefix, + int *cpu) +{ + struct device *dev = simple_priv_to_dev(priv); + struct device_node *top = dev->of_node; + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + struct snd_soc_dai_link_component *dlc; + struct asoc_simple_dai *dai; + int ret; + + if (cpu) { + dlc = asoc_link_to_cpu(dai_link, 0); + dai = simple_props_to_dai_cpu(dai_props, 0); + } else { + dlc = asoc_link_to_codec(dai_link, 0); + dai = simple_props_to_dai_codec(dai_props, 0); + } + + simple_parse_mclk_fs(top, np, dai_props, prefix); + + ret = asoc_simple_parse_dai(dev, np, dlc, cpu); + if (ret) + return ret; + + ret = asoc_simple_parse_clk(dev, np, dai, dlc); + if (ret) + return ret; + + ret = asoc_simple_parse_tdm(np, dai); + if (ret) + return ret; + + return 0; +} + +static int simple_link_init(struct asoc_simple_priv *priv, + struct device_node *node, + struct device_node *codec, + struct link_info *li, + char *prefix, char *name) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + int ret; + + ret = asoc_simple_parse_daifmt(dev, node, codec, + prefix, &dai_link->dai_fmt); + if (ret < 0) + return 0; + + dai_link->init = asoc_simple_dai_init; + dai_link->ops = &simple_ops; + + return asoc_simple_set_dailink_name(dev, dai_link, name); +} + +static int simple_dai_link_of_dpcm(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, + bool is_top) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); + struct device_node *top = dev->of_node; + struct device_node *node = of_get_parent(np); + char *prefix = ""; + char dai_name[64]; + int ret; + + dev_dbg(dev, "link_of DPCM (%pOF)\n", np); + + /* For single DAI link & old style of DT node */ + if (is_top) + prefix = PREFIX; + + if (li->cpu) { + struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); + struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); + int is_single_links = 0; + + /* Codec is dummy */ + + /* FE settings */ + dai_link->dynamic = 1; + dai_link->dpcm_merged_format = 1; + + ret = simple_parse_node(priv, np, li, prefix, &is_single_links); + if (ret < 0) + goto out_put_node; + + snprintf(dai_name, sizeof(dai_name), "fe.%s", cpus->dai_name); + + asoc_simple_canonicalize_cpu(cpus, is_single_links); + asoc_simple_canonicalize_platform(platforms, cpus); + } else { + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); + struct snd_soc_codec_conf *cconf; + + /* CPU is dummy */ + + /* BE settings */ + dai_link->no_pcm = 1; + dai_link->be_hw_params_fixup = asoc_simple_be_hw_params_fixup; + + cconf = simple_props_to_codec_conf(dai_props, 0); + + ret = simple_parse_node(priv, np, li, prefix, NULL); + if (ret < 0) + goto out_put_node; + + snprintf(dai_name, sizeof(dai_name), "be.%s", codecs->dai_name); + + /* check "prefix" from top node */ + snd_soc_of_parse_node_prefix(top, cconf, codecs->of_node, + PREFIX "prefix"); + snd_soc_of_parse_node_prefix(node, cconf, codecs->of_node, + "prefix"); + snd_soc_of_parse_node_prefix(np, cconf, codecs->of_node, + "prefix"); + } + + simple_parse_convert(dev, np, &dai_props->adata); + + snd_soc_dai_link_set_capabilities(dai_link); + + ret = simple_link_init(priv, node, codec, li, prefix, dai_name); + +out_put_node: + li->link++; + + of_node_put(node); + return ret; +} + +static int simple_dai_link_of(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, + bool is_top) +{ + struct device *dev = simple_priv_to_dev(priv); + struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); + struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); + struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); + struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); + struct device_node *cpu = NULL; + struct device_node *node = NULL; + struct device_node *plat = NULL; + char dai_name[64]; + char prop[128]; + char *prefix = ""; + int ret, single_cpu = 0; + + cpu = np; + node = of_get_parent(np); + + dev_dbg(dev, "link_of (%pOF)\n", node); + + /* For single DAI link & old style of DT node */ + if (is_top) + prefix = PREFIX; + + snprintf(prop, sizeof(prop), "%splat", prefix); + plat = of_get_child_by_name(node, prop); + + ret = simple_parse_node(priv, cpu, li, prefix, &single_cpu); + if (ret < 0) + goto dai_link_of_err; + + ret = simple_parse_node(priv, codec, li, prefix, NULL); + if (ret < 0) + goto dai_link_of_err; + + ret = asoc_simple_parse_platform(plat, platforms); + if (ret < 0) + goto dai_link_of_err; + + snprintf(dai_name, sizeof(dai_name), + "%s-%s", cpus->dai_name, codecs->dai_name); + + asoc_simple_canonicalize_cpu(cpus, single_cpu); + asoc_simple_canonicalize_platform(platforms, cpus); + + ret = simple_link_init(priv, node, codec, li, prefix, dai_name); + +dai_link_of_err: + of_node_put(plat); + of_node_put(node); + + li->link++; + + return ret; +} + +static int __simple_for_each_link(struct asoc_simple_priv *priv, + struct link_info *li, + int (*func_noml)(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top), + int (*func_dpcm)(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top)) +{ + struct device *dev = simple_priv_to_dev(priv); + struct device_node *top = dev->of_node; + struct device_node *node; + struct device_node *add_devs; + uintptr_t dpcm_selectable = (uintptr_t)of_device_get_match_data(dev); + bool is_top = 0; + int ret = 0; + + /* Check if it has dai-link */ + node = of_get_child_by_name(top, PREFIX "dai-link"); + if (!node) { + node = of_node_get(top); + is_top = 1; + } + + add_devs = of_get_child_by_name(top, PREFIX "additional-devs"); + + /* loop for all dai-link */ + do { + struct asoc_simple_data adata; + struct device_node *codec; + struct device_node *plat; + struct device_node *np; + int num = of_get_child_count(node); + + /* Skip additional-devs node */ + if (node == add_devs) { + node = of_get_next_child(top, node); + continue; + } + + /* get codec */ + codec = of_get_child_by_name(node, is_top ? + PREFIX "codec" : "codec"); + if (!codec) { + ret = -ENODEV; + goto error; + } + /* get platform */ + plat = of_get_child_by_name(node, is_top ? + PREFIX "plat" : "plat"); + + /* get convert-xxx property */ + memset(&adata, 0, sizeof(adata)); + for_each_child_of_node(node, np) { + if (np == add_devs) + continue; + simple_parse_convert(dev, np, &adata); + } + + /* loop for all CPU/Codec node */ + for_each_child_of_node(node, np) { + if (plat == np || add_devs == np) + continue; + /* + * It is DPCM + * if it has many CPUs, + * or has convert-xxx property + */ + if (dpcm_selectable && + (num > 2 || asoc_simple_is_convert_required(&adata))) { + /* + * np + * |1(CPU)|0(Codec) li->cpu + * CPU |Pass |return + * Codec |return|Pass + */ + if (li->cpu != (np == codec)) + ret = func_dpcm(priv, np, codec, li, is_top); + /* else normal sound */ + } else { + /* + * np + * |1(CPU)|0(Codec) li->cpu + * CPU |Pass |return + * Codec |return|return + */ + if (li->cpu && (np != codec)) + ret = func_noml(priv, np, codec, li, is_top); + } + + if (ret < 0) { + of_node_put(codec); + of_node_put(plat); + of_node_put(np); + goto error; + } + } + + of_node_put(codec); + of_node_put(plat); + node = of_get_next_child(top, node); + } while (!is_top && node); + + error: + of_node_put(add_devs); + of_node_put(node); + return ret; +} + +static int simple_for_each_link(struct asoc_simple_priv *priv, + struct link_info *li, + int (*func_noml)(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top), + int (*func_dpcm)(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top)) +{ + int ret; + /* + * Detect all CPU first, and Detect all Codec 2nd. + * + * In Normal sound case, all DAIs are detected + * as "CPU-Codec". + * + * In DPCM sound case, + * all CPUs are detected as "CPU-dummy", and + * all Codecs are detected as "dummy-Codec". + * To avoid random sub-device numbering, + * detect "dummy-Codec" in last; + */ + for (li->cpu = 1; li->cpu >= 0; li->cpu--) { + ret = __simple_for_each_link(priv, li, func_noml, func_dpcm); + if (ret < 0) + break; + } + + return ret; +} + +static void simple_depopulate_aux(void *data) +{ + struct asoc_simple_priv *priv = data; + + of_platform_depopulate(simple_priv_to_dev(priv)); +} + +static int simple_populate_aux(struct asoc_simple_priv *priv) +{ + struct device *dev = simple_priv_to_dev(priv); + struct device_node *node; + int ret; + + node = of_get_child_by_name(dev->of_node, PREFIX "additional-devs"); + if (!node) + return 0; + + ret = of_platform_populate(node, NULL, NULL, dev); + of_node_put(node); + if (ret) + return ret; + + return devm_add_action_or_reset(dev, simple_depopulate_aux, priv); +} + +static int simple_parse_of(struct asoc_simple_priv *priv, struct link_info *li) +{ + struct snd_soc_card *card = simple_priv_to_card(priv); + int ret; + + ret = asoc_simple_parse_widgets(card, PREFIX); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_routing(card, PREFIX); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_pin_switches(card, PREFIX); + if (ret < 0) + return ret; + + /* Single/Muti DAI link(s) & New style of DT node */ + memset(li, 0, sizeof(*li)); + ret = simple_for_each_link(priv, li, + simple_dai_link_of, + simple_dai_link_of_dpcm); + if (ret < 0) + return ret; + + ret = asoc_simple_parse_card_name(card, PREFIX); + if (ret < 0) + return ret; + + ret = simple_populate_aux(priv); + if (ret < 0) + return ret; + + ret = snd_soc_of_parse_aux_devs(card, PREFIX "aux-devs"); + + return ret; +} + +static int simple_count_noml(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top) +{ + if (li->link >= SNDRV_MAX_LINKS) { + struct device *dev = simple_priv_to_dev(priv); + + dev_err(dev, "too many links\n"); + return -EINVAL; + } + + /* + * DON'T REMOVE platforms + * + * Some CPU might be using soc-generic-dmaengine-pcm. This means CPU and Platform + * are different Component, but are sharing same component->dev. + * Simple Card had been supported it without special Platform selection. + * We need platforms here. + * + * In case of no Platform, it will be Platform == CPU, but Platform will be + * ignored by snd_soc_rtd_add_component(). + * + * see + * simple-card-utils.c :: asoc_simple_canonicalize_platform() + */ + li->num[li->link].cpus = 1; + li->num[li->link].platforms = 1; + + li->num[li->link].codecs = 1; + + li->link += 1; + + return 0; +} + +static int simple_count_dpcm(struct asoc_simple_priv *priv, + struct device_node *np, + struct device_node *codec, + struct link_info *li, bool is_top) +{ + if (li->link >= SNDRV_MAX_LINKS) { + struct device *dev = simple_priv_to_dev(priv); + + dev_err(dev, "too many links\n"); + return -EINVAL; + } + + if (li->cpu) { + /* + * DON'T REMOVE platforms + * see + * simple_count_noml() + */ + li->num[li->link].cpus = 1; + li->num[li->link].platforms = 1; + + li->link++; /* CPU-dummy */ + } else { + li->num[li->link].codecs = 1; + + li->link++; /* dummy-Codec */ + } + + return 0; +} + +static int simple_get_dais_count(struct asoc_simple_priv *priv, + struct link_info *li) +{ + struct device *dev = simple_priv_to_dev(priv); + struct device_node *top = dev->of_node; + + /* + * link_num : number of links. + * CPU-Codec / CPU-dummy / dummy-Codec + * dais_num : number of DAIs + * ccnf_num : number of codec_conf + * same number for "dummy-Codec" + * + * ex1) + * CPU0 --- Codec0 link : 5 + * CPU1 --- Codec1 dais : 7 + * CPU2 -/ ccnf : 1 + * CPU3 --- Codec2 + * + * => 5 links = 2xCPU-Codec + 2xCPU-dummy + 1xdummy-Codec + * => 7 DAIs = 4xCPU + 3xCodec + * => 1 ccnf = 1xdummy-Codec + * + * ex2) + * CPU0 --- Codec0 link : 5 + * CPU1 --- Codec1 dais : 6 + * CPU2 -/ ccnf : 1 + * CPU3 -/ + * + * => 5 links = 1xCPU-Codec + 3xCPU-dummy + 1xdummy-Codec + * => 6 DAIs = 4xCPU + 2xCodec + * => 1 ccnf = 1xdummy-Codec + * + * ex3) + * CPU0 --- Codec0 link : 6 + * CPU1 -/ dais : 6 + * CPU2 --- Codec1 ccnf : 2 + * CPU3 -/ + * + * => 6 links = 0xCPU-Codec + 4xCPU-dummy + 2xdummy-Codec + * => 6 DAIs = 4xCPU + 2xCodec + * => 2 ccnf = 2xdummy-Codec + * + * ex4) + * CPU0 --- Codec0 (convert-rate) link : 3 + * CPU1 --- Codec1 dais : 4 + * ccnf : 1 + * + * => 3 links = 1xCPU-Codec + 1xCPU-dummy + 1xdummy-Codec + * => 4 DAIs = 2xCPU + 2xCodec + * => 1 ccnf = 1xdummy-Codec + */ + if (!top) { + li->num[0].cpus = 1; + li->num[0].codecs = 1; + li->num[0].platforms = 1; + + li->link = 1; + return 0; + } + + return simple_for_each_link(priv, li, + simple_count_noml, + simple_count_dpcm); +} + +static int simple_soc_probe(struct snd_soc_card *card) +{ + struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(card); + int ret; + + ret = asoc_simple_init_hp(card, &priv->hp_jack, PREFIX); + if (ret < 0) + return ret; + + ret = asoc_simple_init_mic(card, &priv->mic_jack, PREFIX); + if (ret < 0) + return ret; + + ret = asoc_simple_init_aux_jacks(priv, PREFIX); + if (ret < 0) + return ret; + + return 0; +} + +static int asoc_simple_probe(struct platform_device *pdev) +{ + struct asoc_simple_priv *priv; + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + struct snd_soc_card *card; + struct link_info *li; + int ret; + + /* Allocate the private data and the DAI link array */ + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + card = simple_priv_to_card(priv); + card->owner = THIS_MODULE; + card->dev = dev; + card->probe = simple_soc_probe; + card->driver_name = "simple-card"; + + li = devm_kzalloc(dev, sizeof(*li), GFP_KERNEL); + if (!li) + return -ENOMEM; + + ret = simple_get_dais_count(priv, li); + if (ret < 0) + return ret; + + if (!li->link) + return -EINVAL; + + ret = asoc_simple_init_priv(priv, li); + if (ret < 0) + return ret; + + if (np && of_device_is_available(np)) { + + ret = simple_parse_of(priv, li); + if (ret < 0) { + dev_err_probe(dev, ret, "parse error\n"); + goto err; + } + + } else { + struct asoc_simple_card_info *cinfo; + struct snd_soc_dai_link_component *cpus; + struct snd_soc_dai_link_component *codecs; + struct snd_soc_dai_link_component *platform; + struct snd_soc_dai_link *dai_link = priv->dai_link; + struct simple_dai_props *dai_props = priv->dai_props; + + ret = -EINVAL; + + cinfo = dev->platform_data; + if (!cinfo) { + dev_err(dev, "no info for asoc-simple-card\n"); + goto err; + } + + if (!cinfo->name || + !cinfo->codec_dai.name || + !cinfo->codec || + !cinfo->platform || + !cinfo->cpu_dai.name) { + dev_err(dev, "insufficient asoc_simple_card_info settings\n"); + goto err; + } + + cpus = dai_link->cpus; + cpus->dai_name = cinfo->cpu_dai.name; + + codecs = dai_link->codecs; + codecs->name = cinfo->codec; + codecs->dai_name = cinfo->codec_dai.name; + + platform = dai_link->platforms; + platform->name = cinfo->platform; + + card->name = (cinfo->card) ? cinfo->card : cinfo->name; + dai_link->name = cinfo->name; + dai_link->stream_name = cinfo->name; + dai_link->dai_fmt = cinfo->daifmt; + dai_link->init = asoc_simple_dai_init; + memcpy(dai_props->cpu_dai, &cinfo->cpu_dai, + sizeof(*dai_props->cpu_dai)); + memcpy(dai_props->codec_dai, &cinfo->codec_dai, + sizeof(*dai_props->codec_dai)); + } + + snd_soc_card_set_drvdata(card, priv); + + asoc_simple_debug_info(priv); + + ret = devm_snd_soc_register_card(dev, card); + if (ret < 0) + goto err; + + devm_kfree(dev, li); + return 0; +err: + asoc_simple_clean_reference(card); + + return ret; +} + +static const struct of_device_id simple_of_match[] = { + { .compatible = "simple-audio-card", }, + { .compatible = "simple-scu-audio-card", + .data = (void *)DPCM_SELECTABLE }, + {}, +}; +MODULE_DEVICE_TABLE(of, simple_of_match); + +static struct platform_driver asoc_simple_card = { + .driver = { + .name = "asoc-simple-card", + .pm = &snd_soc_pm_ops, + .of_match_table = simple_of_match, + }, + .probe = asoc_simple_probe, + .remove = asoc_simple_remove, +}; + +module_platform_driver(asoc_simple_card); + +MODULE_ALIAS("platform:asoc-simple-card"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ASoC Simple Sound Card"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); diff --git a/sound/soc/generic/test-component.c b/sound/soc/generic/test-component.c new file mode 100644 index 0000000000..e10e5bf284 --- /dev/null +++ b/sound/soc/generic/test-component.c @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// test-component.c -- Test Audio Component driver +// +// Copyright (C) 2020 Renesas Electronics Corporation +// Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> + +#include <linux/slab.h> +#include <linux/of_device.h> +#include <linux/of_graph.h> +#include <linux/module.h> +#include <linux/workqueue.h> +#include <sound/pcm.h> +#include <sound/soc.h> + +#define TEST_NAME_LEN 32 +struct test_dai_name { + char name[TEST_NAME_LEN]; + char name_playback[TEST_NAME_LEN]; + char name_capture[TEST_NAME_LEN]; +}; + +struct test_priv { + struct device *dev; + struct snd_pcm_substream *substream; + struct delayed_work dwork; + struct snd_soc_component_driver *component_driver; + struct snd_soc_dai_driver *dai_driver; + struct test_dai_name *name; +}; + +struct test_adata { + u32 is_cpu:1; + u32 cmp_v:1; + u32 dai_v:1; +}; + +#define mile_stone(d) dev_info((d)->dev, "%s() : %s", __func__, (d)->driver->name) +#define mile_stone_x(dev) dev_info(dev, "%s()", __func__) + +static int test_dai_set_sysclk(struct snd_soc_dai *dai, + int clk_id, unsigned int freq, int dir) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_set_pll(struct snd_soc_dai *dai, int pll_id, int source, + unsigned int freq_in, unsigned int freq_out) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_set_clkdiv(struct snd_soc_dai *dai, int div_id, int div) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + unsigned int format = fmt & SND_SOC_DAIFMT_FORMAT_MASK; + unsigned int clock = fmt & SND_SOC_DAIFMT_CLOCK_MASK; + unsigned int inv = fmt & SND_SOC_DAIFMT_INV_MASK; + unsigned int master = fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK; + char *str; + + dev_info(dai->dev, "name : %s", dai->name); + + str = "unknown"; + switch (format) { + case SND_SOC_DAIFMT_I2S: + str = "i2s"; + break; + case SND_SOC_DAIFMT_RIGHT_J: + str = "right_j"; + break; + case SND_SOC_DAIFMT_LEFT_J: + str = "left_j"; + break; + case SND_SOC_DAIFMT_DSP_A: + str = "dsp_a"; + break; + case SND_SOC_DAIFMT_DSP_B: + str = "dsp_b"; + break; + case SND_SOC_DAIFMT_AC97: + str = "ac97"; + break; + case SND_SOC_DAIFMT_PDM: + str = "pdm"; + break; + } + dev_info(dai->dev, "format : %s", str); + + if (clock == SND_SOC_DAIFMT_CONT) + str = "continuous"; + else + str = "gated"; + dev_info(dai->dev, "clock : %s", str); + + str = "unknown"; + switch (master) { + case SND_SOC_DAIFMT_BP_FP: + str = "clk provider, frame provider"; + break; + case SND_SOC_DAIFMT_BC_FP: + str = "clk consumer, frame provider"; + break; + case SND_SOC_DAIFMT_BP_FC: + str = "clk provider, frame consumer"; + break; + case SND_SOC_DAIFMT_BC_FC: + str = "clk consumer, frame consumer"; + break; + } + dev_info(dai->dev, "clock : codec is %s", str); + + str = "unknown"; + switch (inv) { + case SND_SOC_DAIFMT_NB_NF: + str = "normal bit, normal frame"; + break; + case SND_SOC_DAIFMT_NB_IF: + str = "normal bit, invert frame"; + break; + case SND_SOC_DAIFMT_IB_NF: + str = "invert bit, normal frame"; + break; + case SND_SOC_DAIFMT_IB_IF: + str = "invert bit, invert frame"; + break; + } + dev_info(dai->dev, "signal : %s", str); + + return 0; +} + +static int test_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) +{ + mile_stone(dai); + + return 0; +} + +static void test_dai_shutdown(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) +{ + mile_stone(dai); +} + +static int test_dai_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, struct snd_soc_dai *dai) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai) +{ + mile_stone(dai); + + return 0; +} + +static int test_dai_bespoke_trigger(struct snd_pcm_substream *substream, + int cmd, struct snd_soc_dai *dai) +{ + mile_stone(dai); + + return 0; +} + +static u64 test_dai_formats = + /* + * Select below from Sound Card, not auto + * SND_SOC_POSSIBLE_DAIFMT_BP_FP + * SND_SOC_POSSIBLE_DAIFMT_BC_FP + * SND_SOC_POSSIBLE_DAIFMT_BP_FC + * SND_SOC_POSSIBLE_DAIFMT_BC_FC + */ + SND_SOC_POSSIBLE_DAIFMT_I2S | + SND_SOC_POSSIBLE_DAIFMT_RIGHT_J | + SND_SOC_POSSIBLE_DAIFMT_LEFT_J | + SND_SOC_POSSIBLE_DAIFMT_DSP_A | + SND_SOC_POSSIBLE_DAIFMT_DSP_B | + SND_SOC_POSSIBLE_DAIFMT_AC97 | + SND_SOC_POSSIBLE_DAIFMT_PDM | + SND_SOC_POSSIBLE_DAIFMT_NB_NF | + SND_SOC_POSSIBLE_DAIFMT_NB_IF | + SND_SOC_POSSIBLE_DAIFMT_IB_NF | + SND_SOC_POSSIBLE_DAIFMT_IB_IF; + +static const struct snd_soc_dai_ops test_ops = { + .set_fmt = test_dai_set_fmt, + .startup = test_dai_startup, + .shutdown = test_dai_shutdown, + .auto_selectable_formats = &test_dai_formats, + .num_auto_selectable_formats = 1, +}; + +static const struct snd_soc_dai_ops test_verbose_ops = { + .set_sysclk = test_dai_set_sysclk, + .set_pll = test_dai_set_pll, + .set_clkdiv = test_dai_set_clkdiv, + .set_fmt = test_dai_set_fmt, + .mute_stream = test_dai_mute_stream, + .startup = test_dai_startup, + .shutdown = test_dai_shutdown, + .hw_params = test_dai_hw_params, + .hw_free = test_dai_hw_free, + .trigger = test_dai_trigger, + .bespoke_trigger = test_dai_bespoke_trigger, + .auto_selectable_formats = &test_dai_formats, + .num_auto_selectable_formats = 1, +}; + +#define STUB_RATES SNDRV_PCM_RATE_8000_384000 +#define STUB_FORMATS (SNDRV_PCM_FMTBIT_S8 | \ + SNDRV_PCM_FMTBIT_U8 | \ + SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_U16_LE | \ + SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S24_3LE | \ + SNDRV_PCM_FMTBIT_U24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE | \ + SNDRV_PCM_FMTBIT_U32_LE) + +static int test_component_probe(struct snd_soc_component *component) +{ + mile_stone(component); + + return 0; +} + +static void test_component_remove(struct snd_soc_component *component) +{ + mile_stone(component); +} + +static int test_component_suspend(struct snd_soc_component *component) +{ + mile_stone(component); + + return 0; +} + +static int test_component_resume(struct snd_soc_component *component) +{ + mile_stone(component); + + return 0; +} + +#define PREALLOC_BUFFER (32 * 1024) +static int test_component_pcm_construct(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + mile_stone(component); + + snd_pcm_set_managed_buffer_all( + rtd->pcm, + SNDRV_DMA_TYPE_DEV, + rtd->card->snd_card->dev, + PREALLOC_BUFFER, PREALLOC_BUFFER); + + return 0; +} + +static void test_component_pcm_destruct(struct snd_soc_component *component, + struct snd_pcm *pcm) +{ + mile_stone(component); +} + +static int test_component_set_sysclk(struct snd_soc_component *component, + int clk_id, int source, unsigned int freq, int dir) +{ + mile_stone(component); + + return 0; +} + +static int test_component_set_pll(struct snd_soc_component *component, int pll_id, + int source, unsigned int freq_in, unsigned int freq_out) +{ + mile_stone(component); + + return 0; +} + +static int test_component_set_jack(struct snd_soc_component *component, + struct snd_soc_jack *jack, void *data) +{ + mile_stone(component); + + return 0; +} + +static void test_component_seq_notifier(struct snd_soc_component *component, + enum snd_soc_dapm_type type, int subseq) +{ + mile_stone(component); +} + +static int test_component_stream_event(struct snd_soc_component *component, int event) +{ + mile_stone(component); + + return 0; +} + +static int test_component_set_bias_level(struct snd_soc_component *component, + enum snd_soc_bias_level level) +{ + mile_stone(component); + + return 0; +} + +static const struct snd_pcm_hardware test_component_hardware = { + /* Random values to keep userspace happy when checking constraints */ + .info = SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID, + .buffer_bytes_max = 32 * 1024, + .period_bytes_min = 32, + .period_bytes_max = 8192, + .periods_min = 1, + .periods_max = 128, + .fifo_size = 256, +}; + +static int test_component_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + + mile_stone(component); + + /* BE's dont need dummy params */ + if (!rtd->dai_link->no_pcm) + snd_soc_set_runtime_hwparams(substream, &test_component_hardware); + + return 0; +} + +static int test_component_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + mile_stone(component); + + return 0; +} + +static int test_component_ioctl(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned int cmd, void *arg) +{ + mile_stone(component); + + return 0; +} + +static int test_component_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + mile_stone(component); + + return 0; +} + +static int test_component_hw_free(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + mile_stone(component); + + return 0; +} + +static int test_component_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + mile_stone(component); + + return 0; +} + +static void test_component_timer_stop(struct test_priv *priv) +{ + cancel_delayed_work(&priv->dwork); +} + +static void test_component_timer_start(struct test_priv *priv) +{ + schedule_delayed_work(&priv->dwork, msecs_to_jiffies(10)); +} + +static void test_component_dwork(struct work_struct *work) +{ + struct test_priv *priv = container_of(work, struct test_priv, dwork.work); + + if (priv->substream) + snd_pcm_period_elapsed(priv->substream); + + test_component_timer_start(priv); +} + +static int test_component_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + struct test_priv *priv = dev_get_drvdata(component->dev); + + mile_stone(component); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + test_component_timer_start(priv); + priv->substream = substream; /* set substream later */ + break; + case SNDRV_PCM_TRIGGER_STOP: + priv->substream = NULL; + test_component_timer_stop(priv); + } + + return 0; +} + +static int test_component_sync_stop(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + mile_stone(component); + + return 0; +} + +static snd_pcm_uframes_t test_component_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + static int pointer; + + if (!runtime) + return 0; + + pointer += 10; + if (pointer > PREALLOC_BUFFER) + pointer = 0; + + /* mile_stone(component); */ + + return bytes_to_frames(runtime, pointer); +} + +static int test_component_get_time_info(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct timespec64 *system_ts, + struct timespec64 *audio_ts, + struct snd_pcm_audio_tstamp_config *audio_tstamp_config, + struct snd_pcm_audio_tstamp_report *audio_tstamp_report) +{ + mile_stone(component); + + return 0; +} + +static int test_component_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd, + struct snd_pcm_hw_params *params) +{ + mile_stone_x(rtd->dev); + + return 0; +} + +/* CPU */ +static const struct test_adata test_cpu = { .is_cpu = 1, .cmp_v = 0, .dai_v = 0, }; +static const struct test_adata test_cpu_vv = { .is_cpu = 1, .cmp_v = 1, .dai_v = 1, }; +static const struct test_adata test_cpu_nv = { .is_cpu = 1, .cmp_v = 0, .dai_v = 1, }; +static const struct test_adata test_cpu_vn = { .is_cpu = 1, .cmp_v = 1, .dai_v = 0, }; +/* Codec */ +static const struct test_adata test_codec = { .is_cpu = 0, .cmp_v = 0, .dai_v = 0, }; +static const struct test_adata test_codec_vv = { .is_cpu = 0, .cmp_v = 1, .dai_v = 1, }; +static const struct test_adata test_codec_nv = { .is_cpu = 0, .cmp_v = 0, .dai_v = 1, }; +static const struct test_adata test_codec_vn = { .is_cpu = 0, .cmp_v = 1, .dai_v = 0, }; + +static const struct of_device_id test_of_match[] = { + { .compatible = "test-cpu", .data = (void *)&test_cpu, }, + { .compatible = "test-cpu-verbose", .data = (void *)&test_cpu_vv, }, + { .compatible = "test-cpu-verbose-dai", .data = (void *)&test_cpu_nv, }, + { .compatible = "test-cpu-verbose-component", .data = (void *)&test_cpu_vn, }, + { .compatible = "test-codec", .data = (void *)&test_codec, }, + { .compatible = "test-codec-verbose", .data = (void *)&test_codec_vv, }, + { .compatible = "test-codec-verbose-dai", .data = (void *)&test_codec_nv, }, + { .compatible = "test-codec-verbose-component", .data = (void *)&test_codec_vn, }, + {}, +}; +MODULE_DEVICE_TABLE(of, test_of_match); + +static const struct snd_soc_dapm_widget widgets[] = { + /* + * FIXME + * + * Just IN/OUT is OK for now, + * but need to be updated ? + */ + SND_SOC_DAPM_INPUT("IN"), + SND_SOC_DAPM_OUTPUT("OUT"), +}; + +static int test_driver_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *node = dev->of_node; + struct device_node *ep; + const struct test_adata *adata = of_device_get_match_data(&pdev->dev); + struct snd_soc_component_driver *cdriv; + struct snd_soc_dai_driver *ddriv; + struct test_dai_name *dname; + struct test_priv *priv; + int num, ret, i; + + num = of_graph_get_endpoint_count(node); + if (!num) { + dev_err(dev, "no port exits\n"); + return -EINVAL; + } + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + cdriv = devm_kzalloc(dev, sizeof(*cdriv), GFP_KERNEL); + ddriv = devm_kzalloc(dev, sizeof(*ddriv) * num, GFP_KERNEL); + dname = devm_kzalloc(dev, sizeof(*dname) * num, GFP_KERNEL); + if (!priv || !cdriv || !ddriv || !dname || !adata) + return -EINVAL; + + priv->dev = dev; + priv->component_driver = cdriv; + priv->dai_driver = ddriv; + priv->name = dname; + + INIT_DELAYED_WORK(&priv->dwork, test_component_dwork); + dev_set_drvdata(dev, priv); + + if (adata->is_cpu) { + cdriv->name = "test_cpu"; + cdriv->pcm_construct = test_component_pcm_construct; + cdriv->pointer = test_component_pointer; + cdriv->trigger = test_component_trigger; + cdriv->legacy_dai_naming = 1; + } else { + cdriv->name = "test_codec"; + cdriv->idle_bias_on = 1; + cdriv->endianness = 1; + } + + cdriv->open = test_component_open; + cdriv->dapm_widgets = widgets; + cdriv->num_dapm_widgets = ARRAY_SIZE(widgets); + + if (adata->cmp_v) { + cdriv->probe = test_component_probe; + cdriv->remove = test_component_remove; + cdriv->suspend = test_component_suspend; + cdriv->resume = test_component_resume; + cdriv->set_sysclk = test_component_set_sysclk; + cdriv->set_pll = test_component_set_pll; + cdriv->set_jack = test_component_set_jack; + cdriv->seq_notifier = test_component_seq_notifier; + cdriv->stream_event = test_component_stream_event; + cdriv->set_bias_level = test_component_set_bias_level; + cdriv->close = test_component_close; + cdriv->ioctl = test_component_ioctl; + cdriv->hw_params = test_component_hw_params; + cdriv->hw_free = test_component_hw_free; + cdriv->prepare = test_component_prepare; + cdriv->sync_stop = test_component_sync_stop; + cdriv->get_time_info = test_component_get_time_info; + cdriv->be_hw_params_fixup = test_component_be_hw_params_fixup; + + if (adata->is_cpu) + cdriv->pcm_destruct = test_component_pcm_destruct; + } + + i = 0; + for_each_endpoint_of_node(node, ep) { + snprintf(dname[i].name, TEST_NAME_LEN, "%s.%d", node->name, i); + ddriv[i].name = dname[i].name; + + snprintf(dname[i].name_playback, TEST_NAME_LEN, "DAI%d Playback", i); + ddriv[i].playback.stream_name = dname[i].name_playback; + ddriv[i].playback.channels_min = 1; + ddriv[i].playback.channels_max = 384; + ddriv[i].playback.rates = STUB_RATES; + ddriv[i].playback.formats = STUB_FORMATS; + + snprintf(dname[i].name_capture, TEST_NAME_LEN, "DAI%d Capture", i); + ddriv[i].capture.stream_name = dname[i].name_capture; + ddriv[i].capture.channels_min = 1; + ddriv[i].capture.channels_max = 384; + ddriv[i].capture.rates = STUB_RATES; + ddriv[i].capture.formats = STUB_FORMATS; + + if (adata->dai_v) + ddriv[i].ops = &test_verbose_ops; + else + ddriv[i].ops = &test_ops; + + i++; + } + + ret = devm_snd_soc_register_component(dev, cdriv, ddriv, num); + if (ret < 0) + return ret; + + mile_stone_x(dev); + + return 0; +} + +static void test_driver_remove(struct platform_device *pdev) +{ + mile_stone_x(&pdev->dev); +} + +static struct platform_driver test_driver = { + .driver = { + .name = "test-component", + .of_match_table = test_of_match, + }, + .probe = test_driver_probe, + .remove_new = test_driver_remove, +}; +module_platform_driver(test_driver); + +MODULE_ALIAS("platform:asoc-test-component"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); +MODULE_DESCRIPTION("ASoC Test Component"); +MODULE_LICENSE("GPL v2"); |