// SPDX-License-Identifier: GPL-2.0-only /* * tegra_asoc_machine.c - Tegra DAI links parser * * Copyright (c) 2014-2023 NVIDIA CORPORATION. All rights reserved. * */ #include #include #include #include #include #include #include "tegra_asoc_machine.h" #define PREFIX "nvidia-audio-card," #define CELL "#sound-dai-cells" #define DAI "sound-dai" /* DT also uses similar values to specify link type */ enum dai_link_type { PCM_LINK, COMPR_LINK, C2C_LINK, }; struct snd_soc_pcm_stream link_params = { .formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE, .rate_min = 8000, .rate_max = 192000, .channels_min = 1, .channels_max = 16, }; /* find if DAI link or its cpu/codec DAI nodes are disabled */ static bool of_dai_link_is_available(struct device_node *link_node) { struct device_node *child, *dai_node; if (!of_device_is_available(link_node)) return false; for_each_child_of_node(link_node, child) { /* check for "cpu" and "codec" nodes only */ if (of_node_cmp(child->name, "cpu") && of_node_cmp(child->name, "codec")) continue; /* * Skip a codec subnode if DAI property is missing. For a * link with multiple codecs, at least one codec needs to * have DAI property (which is ensured while counting the * number of links that DT exposes). Other codec subnodes * can be empty and populated in override file. */ if (!of_property_read_bool(child, DAI) && !of_node_cmp(child->name, "codec")) continue; dai_node = of_parse_phandle(child, DAI, 0); if (!dai_node) { of_node_put(child); return false; } if (!of_device_is_available(dai_node)) { of_node_put(dai_node); of_node_put(child); return false; } of_node_put(dai_node); } return true; } /* find number of child nodes with given name and containing DAI property */ static int of_get_child_count_with_name(struct device_node *node, const char *name) { struct device_node *child; int num = 0; for_each_child_of_node(node, child) if (!of_node_cmp(child->name, name) && of_property_read_bool(child, DAI)) num++; return num; } static int get_num_dai_links(struct platform_device *pdev, unsigned int *num_links) { struct device_node *top = pdev->dev.of_node; struct device_node *link_node; unsigned int link_count = 0; link_node = of_get_child_by_name(top, PREFIX "dai-link"); if (!link_node) { dev_err(&pdev->dev, "no dai links found\n"); return -ENOENT; } do { if (!of_dai_link_is_available(link_node)) { link_node = of_get_next_child(top, link_node); continue; } link_count++; link_node = of_get_next_child(top, link_node); } while (link_node); *num_links = link_count; return 0; } static int allocate_link_dais(struct platform_device *pdev, struct snd_soc_dai_link *dai_links) { struct device_node *top = pdev->dev.of_node; struct device_node *link_node; unsigned int link_count = 0, num_codecs; link_node = of_get_child_by_name(top, PREFIX "dai-link"); if (!link_node) { dev_err(&pdev->dev, "no dai links found\n"); return -ENOENT; } do { if (!of_dai_link_is_available(link_node)) { link_node = of_get_next_child(top, link_node); continue; } dai_links[link_count].cpus = devm_kzalloc(&pdev->dev, sizeof(*dai_links[link_count].cpus), GFP_KERNEL); if (!dai_links[link_count].cpus) return -ENOMEM; num_codecs = of_get_child_count_with_name(link_node, "codec"); if (!num_codecs) { of_node_put(link_node); dev_err(&pdev->dev, "no codec subnode or sound-dai property\n"); return -EINVAL; } dai_links[link_count].codecs = devm_kzalloc(&pdev->dev, sizeof(*dai_links[link_count].codecs) * num_codecs, GFP_KERNEL); if (!dai_links[link_count].codecs) return -ENOMEM; dai_links[link_count].platforms = devm_kzalloc(&pdev->dev, sizeof(*dai_links[link_count].platforms), GFP_KERNEL); if (!dai_links[link_count].platforms) return -ENOMEM; dai_links[link_count].num_cpus = 1; dai_links[link_count].num_codecs = num_codecs; dai_links[link_count].num_platforms = 1; link_count++; link_node = of_get_next_child(top, link_node); } while (link_node); return 0; } static int get_num_codec_confs(struct platform_device *pdev, int *num_confs) { struct device_node *top = pdev->dev.of_node; struct device_node *link_node, *codec; unsigned int conf_count = 0, num_codecs; link_node = of_get_child_by_name(top, PREFIX "dai-link"); if (!link_node) { dev_err(&pdev->dev, "no dai links found\n"); return -EINVAL; } do { if (!of_dai_link_is_available(link_node)) { link_node = of_get_next_child(top, link_node); continue; } num_codecs = of_get_child_count_with_name(link_node, "codec"); if (!num_codecs) { of_node_put(link_node); dev_err(&pdev->dev, "missing codec subnode\n"); return -EINVAL; } for_each_child_of_node(link_node, codec) { if (of_node_cmp(codec->name, "codec")) continue; if (of_property_read_bool(codec, "prefix")) conf_count++; } link_node = of_get_next_child(top, link_node); } while (link_node); *num_confs = conf_count; return 0; } static void parse_mclk_fs(struct snd_soc_card *card) { struct tegra_machine *machine = snd_soc_card_get_drvdata(card); struct platform_device *pdev = to_platform_device(card->dev); if (of_property_read_u32(pdev->dev.of_node, PREFIX "mclk-fs", &machine->audio_clock.mclk_fs)) dev_dbg(&pdev->dev, "'%smclk-fs' property is missing\n", PREFIX); } static int parse_dai(struct device_node *node, struct snd_soc_dai_link_component *dlc) { struct of_phandle_args args; int ret; ret = of_parse_phandle_with_args(node, DAI, CELL, 0, &args); if (ret) return ret; #if defined(NV_SND_SOC_OF_GET_DAI_NAME_HAS_INDEX_ARG) ret = snd_soc_of_get_dai_name(node, &dlc->dai_name, 0); #else ret = snd_soc_of_get_dai_name(node, &dlc->dai_name); #endif if (ret < 0) return ret; dlc->of_node = args.np; return 0; } static int parse_dt_codec_confs(struct snd_soc_card *card) { struct platform_device *pdev = to_platform_device(card->dev); struct tegra_machine *machine = snd_soc_card_get_drvdata(card); struct device_node *top = pdev->dev.of_node; struct device_node *link_node; struct snd_soc_codec_conf *codec_confs; unsigned int num_confs, i = 0; int err; err = get_num_codec_confs(pdev, &machine->asoc->num_confs); if (err < 0) return err; num_confs = machine->asoc->num_confs; if (!num_confs) return 0; machine->asoc->codec_confs = devm_kcalloc(&pdev->dev, num_confs, sizeof(*codec_confs), GFP_KERNEL); if (!machine->asoc->codec_confs) return -ENOMEM; codec_confs = machine->asoc->codec_confs; link_node = of_get_child_by_name(top, PREFIX "dai-link"); if (!link_node) { dev_err(&pdev->dev, "DAI links not found in DT\n"); return -ENOENT; } do { struct of_phandle_args args; struct device_node *codec; if (!of_dai_link_is_available(link_node)) { link_node = of_get_next_child(top, link_node); continue; } for_each_child_of_node(link_node, codec) { if (of_node_cmp(codec->name, "codec")) continue; if (!of_property_read_bool(codec, "prefix")) continue; err = of_parse_phandle_with_args(codec, DAI, CELL, 0, &args); if (err < 0) { of_node_put(codec); of_node_put(link_node); return err; } codec_confs[i].dlc.of_node = args.np; codec_confs[i].dlc.name = NULL; of_property_read_string(codec, "prefix", &codec_confs[i].name_prefix); i++; } link_node = of_get_next_child(top, link_node); } while (link_node); card->num_configs = num_confs; card->codec_conf = codec_confs; return 0; } static int parse_dai_link_params(struct platform_device *pdev, struct device_node *link_node, struct snd_soc_dai_link *dai_link) { struct snd_soc_pcm_stream *params; char *str; params = devm_kzalloc(&pdev->dev, sizeof(*params), GFP_KERNEL); if (!params) return -ENOMEM; /* Copy default settings */ memcpy(params, &link_params, sizeof(*params)); if (!of_property_read_u32(link_node, "srate", ¶ms->rate_min)) { if (params->rate_min < link_params.rate_min || params->rate_min > link_params.rate_max) { dev_err(&pdev->dev, "Unsupported rate %d for DAI link (%pOF)\n", params->rate_min, link_node); return -EOPNOTSUPP; } params->rate_max = params->rate_min; } if (!of_property_read_u32(link_node, "num-channel", ¶ms->channels_min)) { if (params->channels_min < link_params.channels_min || params->channels_min > link_params.channels_max) { dev_err(&pdev->dev, "Unsupported channel %d for DAI link (%pOF)\n", params->channels_min, link_node); return -EOPNOTSUPP; } params->channels_max = params->channels_min; } if (!of_property_read_string(link_node, "bit-format", (const char **)&str)) { if (!strcmp("s8", str)) { params->formats = SNDRV_PCM_FMTBIT_S8; } else if (!strcmp("s16_le", str)) { params->formats = SNDRV_PCM_FMTBIT_S16_LE; } else if (!strcmp("s24_le", str)) { params->formats = SNDRV_PCM_FMTBIT_S24_LE; } else if (!strcmp("s32_le", str)) { params->formats = SNDRV_PCM_FMTBIT_S32_LE; } else { dev_err(&pdev->dev, "Unsupported format %s for DAI link (%pOF)\n", str, link_node); return -EOPNOTSUPP; } if (!(params->formats & link_params.formats)) { dev_err(&pdev->dev, "Unsupported format %s for DAI link (%pOF)\n", str, link_node); return -EOPNOTSUPP; } } #if defined(NV_SND_SOC_DAI_LINK_STRUCT_HAS_C2C_PARAMS_ARG) /* Linux v6.4 */ dai_link->c2c_params = params; #else dai_link->params = params; #endif return 0; } static int parse_dt_dai_links(struct snd_soc_card *card, struct snd_soc_ops *pcm_ops, struct snd_soc_compr_ops *compr_ops) { struct platform_device *pdev = to_platform_device(card->dev); struct tegra_machine *machine = snd_soc_card_get_drvdata(card); struct device_node *top = pdev->dev.of_node; struct device_node *link_node; struct snd_soc_dai_link *dai_links; unsigned int num_links, link_count = 0; int ret; ret = get_num_dai_links(pdev, &machine->asoc->num_links); if (ret < 0) return ret; num_links = machine->asoc->num_links; if (!num_links) return -EINVAL; dai_links = devm_kcalloc(&pdev->dev, num_links, sizeof(*dai_links), GFP_KERNEL); if (!dai_links) return -ENOMEM; ret = allocate_link_dais(pdev, dai_links); if (ret < 0) return ret; machine->asoc->dai_links = dai_links; link_node = of_get_child_by_name(top, PREFIX "dai-link"); if (!link_node) { dev_err(&pdev->dev, "DAI links not found in DT\n"); return -ENOENT; } do { struct device_node *codec = NULL, *cpu = NULL; struct snd_soc_dai_link *dai_link; int link_type = 0, codec_count = 0; char *link_addr; if (!of_dai_link_is_available(link_node)) { link_node = of_get_next_child(top, link_node); continue; } dev_dbg(&pdev->dev, "parsing (%pOF)\n", link_node); dai_link = &dai_links[link_count]; cpu = of_get_child_by_name(link_node, "cpu"); if (!cpu) { dev_err(&pdev->dev, "cpu subnode is missing"); ret = -ENOENT; goto cleanup; } /* parse CPU DAI */ ret = parse_dai(cpu, dai_link->cpus); if (ret < 0) goto cleanup; for_each_child_of_node(link_node, codec) { /* loop over codecs only */ if (of_node_cmp(codec->name, "codec")) continue; if (!of_property_read_bool(codec, DAI)) { dev_dbg(&pdev->dev, "sound-dai prop missing for (%pOF)\n", codec); codec_count++; continue; } /* parse CODEC DAI */ ret = parse_dai(codec, &dai_link->codecs[codec_count]); if (ret < 0) goto cleanup; codec_count++; } /* set DAI link name */ if (of_property_read_string(link_node, "link-name", &dai_link->name)) { ret = asoc_simple_set_dailink_name( &pdev->dev, dai_link, "%s-%d", "tegra-dlink", link_count); if (ret < 0) goto cleanup; } asoc_simple_parse_daifmt(&pdev->dev, link_node, codec, NULL, &dai_link->dai_fmt); asoc_simple_canonicalize_platform(dai_link->platforms, dai_link->cpus); of_property_read_u32(link_node, "link-type", &link_type); switch (link_type) { case PCM_LINK: dai_link->ops = pcm_ops; break; case COMPR_LINK: dai_link->compr_ops = compr_ops; break; case C2C_LINK: /* Parse DT provided link params */ ret = parse_dai_link_params(pdev, link_node, dai_link); if (ret < 0) goto cleanup; break; default: dev_err(&pdev->dev, "DAI link type invalid\n"); ret = -EINVAL; goto cleanup; } /* * Assign DAI link ID based on DT link address. * This is done to use consistent PCM/Compress device * IDs irrespective of parsing order of DT DAI links. */ link_addr = strrchr(link_node->full_name, '@'); if (!link_addr) { dev_err(&pdev->dev, "Invalid link node (%pOF)\n", link_node); ret = -EINVAL; goto cleanup; } ret = kstrtos32(++link_addr, 10, &dai_link->id); if (ret < 0) { dev_err(&pdev->dev, "Failed to get link node (%pOF) ID\n", link_node); goto cleanup; } link_count++; cleanup: of_node_put(cpu); if (ret < 0) { of_node_put(codec); of_node_put(link_node); return ret; } link_node = of_get_next_child(top, link_node); } while (link_node); card->num_links = num_links; card->dai_link = dai_links; return 0; } int parse_card_info(struct snd_soc_card *card, struct snd_soc_ops *pcm_ops, struct snd_soc_compr_ops *compr_ops) { struct tegra_machine *machine = snd_soc_card_get_drvdata(card); struct device_node *node = card->dev->of_node; int ret; ret = asoc_simple_parse_card_name(card, PREFIX); if (ret < 0) return ret; /* parse machine DAPM widgets */ if (of_property_read_bool(node, PREFIX "widgets")) { ret = snd_soc_of_parse_audio_simple_widgets(card, PREFIX "widgets"); if (ret < 0) return ret; } /*str * Below property of routing map is required only when there * are DAPM input/output widgets available for external codec, * which require them to be connected to machine source/sink * DAPM widgets. */ if (of_property_read_bool(node, PREFIX "routing")) { ret = snd_soc_of_parse_audio_routing(card, PREFIX "routing"); if (ret < 0) return ret; } parse_mclk_fs(card); if (of_property_read_bool(node, "fixed-pll")) { machine->audio_clock.fixed_pll = true; dev_info(card->dev, "PLL configuration is fixed from DT\n"); } ret = parse_dt_dai_links(card, pcm_ops, compr_ops); if (ret < 0) return ret; ret = parse_dt_codec_confs(card); if (ret < 0) return ret; return 0; } EXPORT_SYMBOL_GPL(parse_card_info); struct tegra_machine_control_data { struct snd_soc_pcm_runtime *rtd; unsigned int frame_mode; unsigned int master_mode; }; static int tegra_machine_codec_set_dai_fmt(struct snd_soc_pcm_runtime *rtd, unsigned int frame_mode, unsigned int master_mode) { unsigned int fmt = rtd->dai_link->dai_fmt; if (frame_mode) { fmt &= ~SND_SOC_DAIFMT_FORMAT_MASK; fmt |= frame_mode; } if (master_mode) { fmt &= ~SND_SOC_DAIFMT_MASTER_MASK; master_mode <<= ffs(SND_SOC_DAIFMT_MASTER_MASK) - 1; if (master_mode == SND_SOC_DAIFMT_CBM_CFM) fmt |= SND_SOC_DAIFMT_CBM_CFM; else fmt |= SND_SOC_DAIFMT_CBS_CFS; } return snd_soc_runtime_set_dai_fmt(rtd, fmt); } /* * The order of the below must not be changed as this * aligns with the SND_SOC_DAIFMT_XXX definitions in * include/sound/soc-dai.h. */ static const char * const tegra_machine_frame_mode_text[] = { "None", "i2s", "right-j", "left-j", "dsp-a", "dsp-b", }; static int tegra_machine_codec_get_frame_mode(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct tegra_machine_control_data *data = kcontrol->private_data; ucontrol->value.integer.value[0] = data->frame_mode; return 0; } static int tegra_machine_codec_put_frame_mode(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct tegra_machine_control_data *data = kcontrol->private_data; int err; err = tegra_machine_codec_set_dai_fmt(data->rtd, ucontrol->value.integer.value[0], data->master_mode); if (err) return err; data->frame_mode = ucontrol->value.integer.value[0]; return 0; } static const char * const tegra_machine_master_mode_text[] = { "None", "cbm-cfm", "cbs-cfs", }; static int tegra_machine_codec_get_master_mode(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct tegra_machine_control_data *data = kcontrol->private_data; ucontrol->value.integer.value[0] = data->master_mode; return 0; } static int tegra_machine_codec_put_master_mode(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct tegra_machine_control_data *data = kcontrol->private_data; int err; err = tegra_machine_codec_set_dai_fmt(data->rtd, data->frame_mode, ucontrol->value.integer.value[0]); if (err) return err; data->master_mode = ucontrol->value.integer.value[0]; return 0; } static const struct soc_enum tegra_machine_codec_frame_mode = SOC_ENUM_SINGLE_EXT(ARRAY_SIZE(tegra_machine_frame_mode_text), tegra_machine_frame_mode_text); static const struct soc_enum tegra_machine_codec_master_mode = SOC_ENUM_SINGLE_EXT(ARRAY_SIZE(tegra_machine_master_mode_text), tegra_machine_master_mode_text); static int tegra_machine_add_ctl(struct snd_soc_card *card, struct snd_kcontrol_new *knew, void *private_data, const unsigned char *name) { struct snd_kcontrol *kctl; int ret; kctl = snd_ctl_new1(knew, private_data); if (!kctl) return -ENOMEM; ret = snd_ctl_add(card->snd_card, kctl); if (ret < 0) return ret; return 0; } static int tegra_machine_add_frame_mode_ctl(struct snd_soc_card *card, struct snd_soc_pcm_runtime *rtd, const unsigned char *name, struct tegra_machine_control_data *data) { struct snd_kcontrol_new knew = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = name, .info = snd_soc_info_enum_double, .index = 0, .get = tegra_machine_codec_get_frame_mode, .put = tegra_machine_codec_put_frame_mode, .private_value = (unsigned long)&tegra_machine_codec_frame_mode, }; return tegra_machine_add_ctl(card, &knew, data, name); } static int tegra_machine_add_master_mode_ctl(struct snd_soc_card *card, struct snd_soc_pcm_runtime *rtd, const unsigned char *name, struct tegra_machine_control_data *data) { struct snd_kcontrol_new knew = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = name, .info = snd_soc_info_enum_double, .index = 0, .get = tegra_machine_codec_get_master_mode, .put = tegra_machine_codec_put_master_mode, .private_value = (unsigned long)&tegra_machine_codec_master_mode, }; return tegra_machine_add_ctl(card, &knew, data, name); } int tegra_machine_add_i2s_codec_controls(struct snd_soc_card *card) { struct tegra_machine_control_data *data; struct snd_soc_pcm_runtime *rtd; struct device_node *np; char name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN]; unsigned int id; int ret; list_for_each_entry(rtd, &card->rtd_list, list) { np = rtd->dai_link->cpus->of_node; if (!np) continue; data = devm_kzalloc(card->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->rtd = rtd; data->frame_mode = 0; data->master_mode = 0; if (of_property_read_u32(np, "nvidia,ahub-i2s-id", &id) < 0) continue; snprintf(name, sizeof(name), "I2S%d codec frame mode", id+1); ret = tegra_machine_add_frame_mode_ctl(card, rtd, name, data); if (ret) dev_warn(card->dev, "Failed to add control: %s!\n", name); snprintf(name, sizeof(name), "I2S%d codec master mode", id+1); ret = tegra_machine_add_master_mode_ctl(card, rtd, name, data); if (ret) { dev_warn(card->dev, "Failed to add control: %s!\n", name); return ret; } } return 0; } EXPORT_SYMBOL_GPL(tegra_machine_add_i2s_codec_controls); /* * The order of the following definitions should align with * the 'snd_jack_types' enum as defined in include/sound/jack.h. */ static const char * const tegra_machine_jack_state_text[] = { "None", "HP", "MIC", "HS", }; static const struct soc_enum tegra_machine_jack_state = SOC_ENUM_SINGLE_EXT(ARRAY_SIZE(tegra_machine_jack_state_text), tegra_machine_jack_state_text); static int tegra_machine_codec_get_jack_state(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct snd_soc_jack *jack = kcontrol->private_data; ucontrol->value.integer.value[0] = jack->status; return 0; } static int tegra_machine_codec_put_jack_state(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct snd_soc_jack *jack = kcontrol->private_data; snd_soc_jack_report(jack, ucontrol->value.integer.value[0], SND_JACK_HEADSET); return 0; } int tegra_machine_add_codec_jack_control(struct snd_soc_card *card, struct snd_soc_pcm_runtime *rtd, struct snd_soc_jack *jack) { char name[SNDRV_CTL_ELEM_ID_NAME_MAXLEN]; struct snd_kcontrol_new knew = { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = name, .info = snd_soc_info_enum_double, .index = 0, .get = tegra_machine_codec_get_jack_state, .put = tegra_machine_codec_put_jack_state, .private_value = (unsigned long)&tegra_machine_jack_state, }; if (rtd->dais[rtd->dai_link->num_cpus]->component->name_prefix) snprintf(name, sizeof(name), "%s Jack-state", rtd->dais[rtd->dai_link->num_cpus]->component->name_prefix); else snprintf(name, sizeof(name), "Jack-state"); return tegra_machine_add_ctl(card, &knew, jack, name); } EXPORT_SYMBOL_GPL(tegra_machine_add_codec_jack_control); void release_asoc_phandles(struct tegra_machine *machine) { unsigned int i; if (machine->asoc->dai_links) { for (i = 0; i < machine->asoc->num_links; i++) { of_node_put(machine->asoc->dai_links[i].cpus->of_node); of_node_put(machine->asoc->dai_links[i].codecs->of_node); } } if (machine->asoc->codec_confs) { for (i = 0; i < machine->asoc->num_confs; i++) of_node_put(machine->asoc->codec_confs[i].dlc.of_node); } } EXPORT_SYMBOL_GPL(release_asoc_phandles); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Tegra ASoC Machine Utility Code"); MODULE_LICENSE("GPL v2");