Sa8155.c Machine 声卡注册流程。

声卡注册流程接口:

static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
	struct snd_soc_pcm_runtime *rtd;
	struct snd_soc_dai_link *dai_link;
	int ret, i;

	mutex_lock(&client_mutex);
	for_each_card_prelinks(card, i, dai_link) {
		ret = soc_init_dai_link(card, dai_link);
		if (ret) {
			dev_err(card->dev, "ASoC: failed to init link %s: %d\n",
				dai_link->name, ret);
			mutex_unlock(&client_mutex);
			return ret;
		}
	}
	mutex_lock_nested(&card->mutex, SND_SOC_CARD_CLASS_INIT);

	snd_soc_dapm_init(&card->dapm, card, NULL);

	/* check whether any platform is ignore machine FE and using topology */
	soc_check_tplg_fes(card);

	/* bind DAIs */
	for_each_card_prelinks(card, i, dai_link) {
		ret = soc_bind_dai_link(card, dai_link);  //为每个dai Link 创建snd_soc_pcm_runtime 并加入list 里面,这里包括Fe dai / be dai这些都是在sa8155.c里面定义,就是machine.if (ret != 0)
			goto probe_end;
	}

	/* bind aux_devs too */
	ret = soc_bind_aux_dev(card);
	if (ret < 0)
		goto probe_end;

	/* add predefined DAI links to the list */
	for_each_card_prelinks(card, i, dai_link) {
		ret = snd_soc_add_dai_link(card, dai_link); //将dai link 添加到 list去。
		if (ret < 0)
			goto probe_end;
	}

	/* card bind complete so register a sound card */
	ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,  //注册声卡,创建了用于用户层调用的ctl 设备节点,用于控制声卡,然后就是注册了声卡信息。
			card->owner, 0, &card->snd_card);
	if (ret < 0) {
		dev_err(card->dev,
			"ASoC: can't create sound card for card %s: %d\n",
			card->name, ret);
		goto probe_end;
	}

	soc_init_card_debugfs(card);  //debug 相关

	soc_resume_init(card);

	ret = snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,  //这里其实就是去创建dapm 的widget控件 kctl,但是在sa8155 平台上,很多都是在snd_soc_component_driver的probe 的接口里面注册的,如msm-pcm-routing-auto.c 中。
					card->num_dapm_widgets);
	if (ret < 0)
		goto probe_end;

	ret = snd_soc_dapm_new_controls(&card->dapm, card->of_dapm_widgets,
					card->num_of_dapm_widgets);
	if (ret < 0)
		goto probe_end;

	/* initialise the sound card only once */
	if (card->probe) {
		ret = card->probe(card); // 在sa8155 平台应该是没有执行到的,在machine 里面创建snd_soc_card 结构体时,没有创建该probe 的回调函数。
		if (ret < 0)
			goto probe_end;
	}

	/* probe all components used by DAI links on this card */
	ret = soc_probe_link_components(card); //其实就是看什么模块去注册了snd_soc_component ,然后就执行注册过的snd_soc_component_driver 里面的probe 的回调函数,这块主要看平台怎么实现,在sa8155 上目前看只有在msm-pcm-routing-auto.c 里面去创建了 component_driver .probe 的回调函数,当然里面还有一些其它回调函数,主要看平台怎么实现。
	if (ret < 0) {
		dev_err(card->dev,
			"ASoC: failed to instantiate card %d\n", ret);
		goto probe_end;
	}

	/* probe auxiliary components */
	ret = soc_probe_aux_devices(card);
	if (ret < 0)
		goto probe_end;

	/*
	 * Find new DAI links added during probing components and bind them.
	 * Components with topology may bring new DAIs and DAI links.
	 */
	for_each_card_links(card, dai_link) {
		if (soc_is_dai_link_bound(card, dai_link))
			continue;

		ret = soc_init_dai_link(card, dai_link);
		if (ret)
			goto probe_end;
		ret = soc_bind_dai_link(card, dai_link);  //不是很清楚这里为啥还要在遍历一遍,这里跟上面调用 soc_bind_dai_link 一样。
		if (ret)
			goto probe_end;
	}

	/* probe all DAI links on this card */
	ret = soc_probe_link_dais(card); //执行的是定义的snd_soc_dai  里面的snd_soc_dai_driver,里面的 .probe 回调函数,这一版都是在定义 fe dai driver 和 BE dai driver 的时候会定义该回调函数,在高通sa8155平台的fe dai driver 的实现中, 是将driver 定义的  .stream_name .aif_name  链接起来为一条dapm route 通路,stream_name <----->air_name    be dai driver 里面也有实现这个Probe 回调函数,作用与 fe dai driver 的一致。if (ret < 0) {
		dev_err(card->dev,
			"ASoC: failed to instantiate card %d\n", ret);
		goto probe_end;
	}

	for_each_card_rtds(card, rtd)
		soc_link_init(card, rtd);  //这个接口就比较重要了,里面会调用 soc_new_pcm 去创建pcm 节点,为每个pcm  创建playback stream/capture stream  (snd_pcm_substream),  绑定 ASoC PCM operations pcm 操作函数(snd_pcm_ops),大体就是这些比较重要。

	snd_soc_dapm_link_dai_widgets(card);
	snd_soc_dapm_connect_dai_link_widgets(card);

	ret = snd_soc_add_card_controls(card, card->controls,
					card->num_controls);
	if (ret < 0)
		goto probe_end;

	ret = snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,
				      card->num_dapm_routes);
	if (ret < 0) {
		if (card->disable_route_checks) {
			dev_info(card->dev,
				 "%s: disable_route_checks set, ignoring errors on add_routes\n",
				 __func__);
		} else {
			dev_err(card->dev,
				 "%s: snd_soc_dapm_add_routes failed: %d\n",
				 __func__, ret);
			goto probe_end;
		}
	}

	ret = snd_soc_dapm_add_routes(&card->dapm, card->of_dapm_routes,
				      card->num_of_dapm_routes);
	if (ret < 0)
		goto probe_end;

	/* try to set some sane longname if DMI is available */
	snd_soc_set_dmi_name(card, NULL);

	snprintf(card->snd_card->shortname, sizeof(card->snd_card->shortname),
		 "%s", card->name);
	snprintf(card->snd_card->longname, sizeof(card->snd_card->longname),
		 "%s", card->long_name ? card->long_name : card->name);
	snprintf(card->snd_card->driver, sizeof(card->snd_card->driver),
		 "%s", card->driver_name ? card->driver_name : card->name);
	for (i = 0; i < ARRAY_SIZE(card->snd_card->driver); i++) {
		switch (card->snd_card->driver[i]) {
		case '_':
		case '-':
		case '\0':
			break;
		default:
			if (!isalnum(card->snd_card->driver[i]))
				card->snd_card->driver[i] = '_';
			break;
		}
	}

	if (card->late_probe) {
		ret = card->late_probe(card);
		if (ret < 0) {
			dev_err(card->dev, "ASoC: %s late_probe() failed: %d\n",
				card->name, ret);
			goto probe_end;
		}
	}

	snd_soc_dapm_new_widgets(card);  // 在sa8155 平台上,widgets 的创建是在msm-pcm-rounting-auto.c 里面定义创建,在msm_routing_probe 里面去创建kctl 控件,以及widgets.

	ret = snd_card_register(card->snd_card); //这里才回调在soc_new_pcm()->snd_pcm_new 里面注册snd_device_ops ops。即dev->ops->dev_register。最终去调用的是snd_pcm_dev_register() 内部调用snd_register_devic()->device_add() 为声卡创建ALSA 设备节点,这个接口里面也会调用snd_register_device(devtype, pcm->card, pcm->device,&snd_pcm_f_ops[cidx], pcm, &pcm->streams[cidx].dev);  注意这个snd_pcm_f_ops[] 这个是pcm_native.c 注册的file_operations ops 接口,供应用层调用使用。if (ret < 0) {
		dev_err(card->dev, "ASoC: failed to register soundcard %d\n",
				ret);
		goto probe_end;
	}

	card->instantiated = 1;
	dapm_mark_endpoints_dirty(card);
	snd_soc_dapm_sync(&card->dapm);

probe_end:
	if (ret < 0)
		soc_cleanup_card_resources(card);

	mutex_unlock(&card->mutex);
	mutex_unlock(&client_mutex);

	return ret;
}

目前手上的车机是高通sa8155 平台,目前看上图的整体的组件架构,高通与我们所熟悉的TinyAlsa 的架构其实是有所不同的。

下面是对应的文件对应的组件的位置。

这个图就展示QC sa8155平台的整体的audio kernel 的框架。
宏观上看的话数据流就是由经 FE-DAL driver  -> Platform driver(q6asm.c) -> Routing driver(q6adm.c) -> BE-DAI(q6afe.c)
理一下对应的组件用的哪些文件。

FE-DAI driver : msm-dai-fe.c
Platform Drivers : msm-pcm-q6-v2.c、msm-compress-q6-v2.c、msm-pcm-loopback-v2.c 、msm-pcm-voip.c、msm-pcm-afe-v2.c(snd_soc_dai_link 配置时,会根据对应的配置来使用不同的驱动,使用哪个驱动会根据配置来进行加载,如loopback.)

Routing Driver : msm-pcm-routint-v2.c
BE-DAL driver : msm-dai-q6-v2.c
Machine driver: sa8155.c  、msm_dailink.h 上面提到的platform 使用哪个驱动,会在里面进行配置。最终会加载到dia link中去 (声卡注册是在该函数的Probe 函数中加载开始,这里面也会去注册对应的dai link 其中包括FE dai 以及 BE dai )

在注册 snd_soc_dai_link 的时候,FE DAI 会去注册PCM 节点,而BE DAI 不会去创能pcm 节点,且在注册pcm 节点的时候会为每个dai link 注册一个 snd_soc_pcm_runtime 是ASOC 灵魂,这也是所谓的DPCM,里面还包含了一个结构体,这个结构体就是 snd_soc_dpcm_runtime dpcm[2]他用于 FE dai 与 BE dai 之间的动态链接。


还有一个在 snd_soc_dai_link 注册的时候,有一个非常重要的宏定义,这个非常让人忽略,分解一下它有助于我们理解dai link 的注册,我分解了一下(下面用到的宏定义是在sound/soc.h 里面):


-------------------begin--------------------------------
这个是在msm_dailink.h 中的宏定义,这个头文件是在sa8155.c 中被引用了的。
逐步分解:
SND_SOC_DAILINK_DEFS(multimedia1,
	DAILINK_COMP_ARRAY(COMP_CPU("MultiMedia1")),
	DAILINK_COMP_ARRAY(COMP_CODEC("snd-soc-dummy", "snd-soc-dummy-dai")),
	DAILINK_COMP_ARRAY(COMP_PLATFORM("msm-pcm-dsp.0")));

SND_SOC_DAILINK_DEFS(multimedia1,
	DAILINK_COMP_ARRAY(.dai_name = "MultiMedia1"),
	DAILINK_COMP_ARRAY(.name = "snd-soc-dummy", .dai_name ="snd-soc-dummy-dai"),
	DAILINK_COMP_ARRAY(.name = "msm-pcm-dsp.0"));

SND_SOC_DAILINK_DEFS(multimedia1, 
    .dai_name = "MultiMedia1",
    .name = "snd-soc-dummy", .dai_name = "snd-soc-dummy-dai",
    .name = "msm-pcm-dsp.0");


SND_SOC_DAILINK_DEF(multimedia1_cpus, .dai_name = "MultiMedia1");
SND_SOC_DAILINK_DEF(multimedia1_codecs, .name = "snd-soc-dummy", .dai_name = "snd-soc-dummy-dai");
SND_SOC_DAILINK_DEF(multimedia1_platforms, .name = "msm-pcm-dsp.0");


static struct snd_soc_dai_link_component multimedia1_cpus[] = {.dai_name = "MultiMedia1"};
static struct snd_soc_dai_link_component multimedia1_codecs[] = {.name = "snd-soc-dummy", .dai_name = "snd-soc-dummy-dai"};
static struct snd_soc_dai_link_component multimedia1_platforms[] = {.name = "msm-pcm-dsp.0"};
分解下来就变成了三个定义的数组,注意这个数组就会在sa8155.c 里面的snd_soc_dai_link 去添加 DAI 的时候会用到。
下面这个是sa8155.c 里面的FE dai 的定义,这个dai 会生成pcm 节点,注意里面的这个SND_SOC_DAILINK_REG 宏定义。
static struct snd_soc_dai_link msm_common_dai_links[] = {
	/* FrontEnd DAI Links */
	{
		.name = MSM_DAILINK_NAME(Media1),
		.stream_name = "MultiMedia1",
		.dynamic = 1,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
		.async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
		.dpcm_playback = 1,
		.dpcm_capture = 1,
		.trigger = {SND_SOC_DPCM_TRIGGER_POST,
			SND_SOC_DPCM_TRIGGER_POST},
		.ignore_suspend = 1,
		/* this dainlink has playback support */
		.ignore_pmdown_time = 1,
		.id = MSM_FRONTEND_DAI_MULTIMEDIA1,
		SND_SOC_DAILINK_REG(multimedia1),
	},
     ........
};

单独拿出来:
SND_SOC_DAILINK_REG(multimedia1);
->
SND_SOC_DAILINK_REGx(multimedia1, SND_SOC_DAILINK_REG3, SND_SOC_DAILINK_REG2, SND_SOC_DAILINK_REG1); = SND_SOC_DAILINK_REG1;
->
SND_SOC_DAILINK_REG1(multimedia1)         SND_SOC_DAILINK_REG3(name##_cpus, name##_codecs, name##_platforms);
->
SND_SOC_DAILINK_REG3(multimedia1_cpus, multimedia1_codecs, multimedia1_platforms);
->

#define SND_SOC_DAILINK_REG3(multimedia1_cpus, multimedia1_codecs, multimedia1_platforms)
	.cpus		= multimedia1_cpus,
	.num_cpus	= ARRAY_SIZE(multimedia1_cpus),
	.codecs		= multimedia1_codecs,
	.num_codecs	= ARRAY_SIZE(multimedia1_codecs),
	.platforms	= multimedia1_platforms,
	.num_platforms	= ARRAY_SIZE(multimedia1_platforms)

所以上面的snd_soc_dai_link 的数组定义就变成了如下的,这些dai 会在声卡注册前会去遍历,并放在对应的list 里面存起来。
static struct snd_soc_dai_link msm_common_dai_links[] = {
	/* FrontEnd DAI Links */
	{
        .name = MSM_DAILINK_NAME(Media1),
        .stream_name = "MultiMedia1",
        .dynamic = 1,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
        .async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
        .dpcm_playback = 1,
        .dpcm_capture = 1,
        .trigger = {SND_SOC_DPCM_TRIGGER_POST,
        	SND_SOC_DPCM_TRIGGER_POST},
        .ignore_suspend = 1,
        /* this dainlink has playback support */
        .ignore_pmdown_time = 1,
        .id = MSM_FRONTEND_DAI_MULTIMEDIA1,
        .cpus		= multimedia1_cpus,   //注意这里的cpus 其实是一个指针,想当于我们把上面的数组的头赋值给了它,下面的也是一样。
        .num_cpus	= ARRAY_SIZE(multimedia1_cpus),
        .codecs		= multimedia1_codecs,
        .num_codecs	= ARRAY_SIZE(multimedia1_codecs),
        .platforms	= multimedia1_platforms,
        .num_platforms	= ARRAY_SIZE(multimedia1_platforms)
	},
     ........
};
---------------------------------------------------end---------------------------------------------

在说一下这个BE dai 注册的时候,有一个 .be_hw_params_fixup 函数指针。

static struct snd_soc_dai_link msm_common_be_dai_links[] = {
	/* Backend AFE DAI Links */
	{
		.name = LPASS_BE_AFE_PCM_RX,
		.stream_name = "AFE Playback",
		.no_pcm = 1,
		.dpcm_playback = 1,
		.id = MSM_BACKEND_DAI_AFE_PCM_RX,
		.be_hw_params_fixup = msm_be_hw_params_fixup,
		/* this dainlink has playback support */
		.ignore_pmdown_time = 1,
		.ignore_suspend = 1,
		SND_SOC_DAILINK_REG(afe_pcm_rx),
	},
};

看文档它是用来修改BE dai 的下行的硬件参数的。

目前看我的 sa8155 平台上也是该设置见如下:


static int msm_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd,
				struct snd_pcm_hw_params *params)
{
	struct snd_soc_dai_link *dai_link = rtd->dai_link;
	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);
	int rc = 0;
	int idx;

	pr_debug("%s: format = %d, rate = %d\n",
		  __func__, params_format(params), params_rate(params));

	switch (dai_link->id) {
	………………
		case MSM_BACKEND_DAI_QUAT_TDM_RX_0:
			channels->min = channels->max =
					tdm_rx_cfg[TDM_QUAT][TDM_0].channels;
			param_set_mask(params, SNDRV_PCM_HW_PARAM_FORMAT,
				       tdm_rx_cfg[TDM_QUAT][TDM_0].bit_format);
			rate->min = rate->max = tdm_rx_cfg[TDM_QUAT][TDM_0].sample_rate;
			break;
	…………….
	default:
		rate->min = rate->max = SAMPLING_RATE_48KHZ;
		break;
	}

done:
	return rc;
}

/* TDM default config */
static struct dev_config tdm_rx_cfg[TDM_INTERFACE_MAX][TDM_PORT_MAX] = {
……….
{ /* QUAT TDM */
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 20}, /* RX_0 */ /*Media, Tuner, Dolby ch 0-19*/  //这里的20 值的是通道数据,我目前这个项目就是20 ch 用于media 
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 8}, /* RX_1 */ /*Navi,TTS,KTV,Chime_TTS*/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1}, /* RX_2 */ /*Phone*/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1}, /* RX_3 */ /*Ringtone*/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_4 */ /**/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2}, /* RX_5 */ /*Notify, Boot_Music*/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_6 */ /**/
	{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_7 */ /**/
},
……..
};

struct dev_config {
	u32 sample_rate;
	u32 bit_format;
	u32 channels;
};

目前看 这个就是在设置 tinymix 的时候设置audio router 的值的时候会触发调用到该接口上来进行设置(这个有待考证,可能是初始化的时候调用,因为下面这里的配置会在通路打开的时候会再去设置一次的,所以这里有怀疑,但是可以肯定是输出的20ch),后续再考证。

   <path name="media-playback">
        <ctl name="QUAT_TDM_RX_0 Channels" value="Twenty" />
        <ctl name="QUAT_TDM_RX_0 Audio Mixer MultiMedia1" value="1" />
    </path>
这里是在mixer_paths_adp.xml 中进行定义的。

//pcm_native.c 里面注册的 pcm 的操作函数,即file_operations 结构体,而该结构体的注册是在pcm.c 里面的snd_pcm_dev_register() 里面调用snd_register_device(devtype, pcm->card, pcm->device, &snd_pcm_f_ops[cidx], pcm,&pcm->streams[cidx].dev); 注册的。

const struct file_operations snd_pcm_f_ops[2] = {
	{
		.owner =		THIS_MODULE,
		.write =		snd_pcm_write,
		.write_iter =		snd_pcm_writev,
		.open =			snd_pcm_playback_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_poll,
		.unlocked_ioctl =	snd_pcm_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	},
	{
		.owner =		THIS_MODULE,
		.read =			snd_pcm_read,
		.read_iter =		snd_pcm_readv,
		.open =			snd_pcm_capture_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_poll,
		.unlocked_ioctl =	snd_pcm_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	}
};

snd_pcm_dev_register() 是在声卡创建的流程中的 _snd_pcm_new() 调用处理流程中,注册的 .dev_register = snd_pcm_dev_register;

static struct snd_device_ops ops = {
		.dev_free = snd_pcm_dev_free,
		.dev_register = snd_pcm_dev_register,
		.dev_disconnect = snd_pcm_dev_disconnect,
};

而 .dev_register 会在哪里调用执行的呢,是在声卡注册流程中的最后一个步骤中,调用执行,即snd_soc_instantiate_card(card); 接口中,调用

snd_card_register(card->snd_card); 进行调用执行。

int snd_card_register(struct snd_card *card)
{
	int err;

	if (snd_BUG_ON(!card))
		return -EINVAL;

	if (!card->registered) {
		err = device_add(&card->card_dev);
		if (err < 0)
			return err;
		card->registered = true;
	}

	if ((err = snd_device_register_all(card)) < 0)  //该接口调用进去执行。
		return err;
	mutex_lock(&snd_card_mutex);
	if (snd_cards[card->number]) {
		/* already registered */
		mutex_unlock(&snd_card_mutex);
		return snd_info_card_register(card); /* register pending info */
	}
	if (*card->id) {
		/* make a unique id name from the given string */
		char tmpid[sizeof(card->id)];
		memcpy(tmpid, card->id, sizeof(card->id));
		snd_card_set_id_no_lock(card, tmpid, tmpid);
	} else {
		/* create an id from either shortname or longname */
		const char *src;
		src = *card->shortname ? card->shortname : card->longname;
		snd_card_set_id_no_lock(card, src,
					retrieve_id_from_card_name(src));
	}
	snd_cards[card->number] = card;
	mutex_unlock(&snd_card_mutex);
	err = snd_info_card_register(card);
	if (err < 0)
		return err;
	…………
	return 0;
}

最终在 core/device.c 里面的接口(snd_device_register) -> __snd_device_register(struct snd_device *dev)调用。
static int __snd_device_register(struct snd_device *dev)
{
	if (dev->state == SNDRV_DEV_BUILD) {
		if (dev->ops->dev_register) {
			int err = dev->ops->dev_register(dev);  //就是这里进行调用。
			if (err < 0)
				return err;
		}
		dev->state = SNDRV_DEV_REGISTERED;
	}
	return 0;
}

OK 现在我们来最追一下pcm_open 的调用流程,其它的也差不多就是这个流程。

pcm open起源于逻辑pcm设备的open,也就是在linux userspace里面去fopen一个pcm设备,根据linux设备驱动框架,这里会调用注册设备时提供的open接口。这个接口在pcm_native.c文件中,即上面的 .open = snd_pcm_playback_open,所以它会去调用snd_pcm_playback_open(),该接口的调用流程:snd_pcm_playback_open() -> snd_pcm_open() -> snd_pcm_open_file -> snd_pcm_open_substream() -> substream->ops->open(substream);那么这里的open()调用的是哪里?
其实这里就是声卡注册的时候调用的 soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num);  内部注册的 Asoc PCM operations

/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
     ..............
	/* ASoC PCM operations */
	if (rtd->dai_link->dynamic) { // 这里是dynamic 配置是在我们配置dai link(snd_soc_dai_link) 节点的时候进行配置的。
		rtd->ops.open		= dpcm_fe_dai_open;
		rtd->ops.hw_params	= dpcm_fe_dai_hw_params;
		rtd->ops.prepare	= dpcm_fe_dai_prepare;
		rtd->ops.trigger	= dpcm_fe_dai_trigger;
		rtd->ops.hw_free	= dpcm_fe_dai_hw_free;
		rtd->ops.close		= dpcm_fe_dai_close;
		rtd->ops.pointer	= soc_pcm_pointer;
		rtd->ops.ioctl		= snd_soc_pcm_component_ioctl;
#ifdef CONFIG_AUDIO_QGKI
		rtd->ops.compat_ioctl   = soc_pcm_compat_ioctl;
		rtd->ops.delay_blk	= soc_pcm_delay_blk;
#endif
	} else {
		rtd->ops.open		= soc_pcm_open;
		rtd->ops.hw_params	= soc_pcm_hw_params;
		rtd->ops.prepare	= soc_pcm_prepare;
		rtd->ops.trigger	= soc_pcm_trigger;
		rtd->ops.hw_free	= soc_pcm_hw_free;
		rtd->ops.close		= soc_pcm_close;
		rtd->ops.pointer	= soc_pcm_pointer;
		rtd->ops.ioctl		= snd_soc_pcm_component_ioctl;
#ifdef CONFIG_AUDIO_QGKI
		rtd->ops.compat_ioctl   = soc_pcm_compat_ioctl;
		rtd->ops.delay_blk	= soc_pcm_delay_blk;
#endif
	}

	for_each_rtdcom(rtd, rtdcom) {
		const struct snd_pcm_ops *ops = rtdcom->component->driver->ops;

		if (!ops)
			continue;

		if (ops->copy_user)
			rtd->ops.copy_user	= snd_soc_pcm_component_copy_user;
		if (ops->page)
			rtd->ops.page		= snd_soc_pcm_component_page;
		if (ops->mmap)
			rtd->ops.mmap		= snd_soc_pcm_component_mmap;
	}

	if (playback)
		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);

	if (capture)
		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops);
	ret = snd_soc_pcm_component_new(pcm);
        ............
}
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops); 就是将&rtd->ops 赋值给了 substream->ops;

void snd_pcm_set_ops(struct snd_pcm *pcm, int direction,
		     const struct snd_pcm_ops *ops)
{
	struct snd_pcm_str *stream = &pcm->streams[direction];
	struct snd_pcm_substream *substream;

	for (substream = stream->substream; substream != NULL; substream = substream->next)
		substream->ops = ops; // ->>>>
}

所以上面 的open 就是调用的是:dpcm_fe_dai_open 接口。看下该接口的重要调用流程:dpcm_fe_dai_open() - > dpcm_fe_dai_startup() -> soc_pcm_open() (/* start the DAI frontend */)   其实最终还是会调用 soc_pcm_open() 该接口来打开 fe dai。


static int soc_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct snd_soc_component *component;
	struct snd_soc_rtdcom_list *rtdcom;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	struct snd_soc_dai *codec_dai;
    .........
	/* startup the audio subsystem */
	ret = snd_soc_dai_startup(cpu_dai, substream);//->其实执行的是 cpu_dai->driver->ops->startup(substream, cpu_dai);
    .........

	ret = soc_pcm_components_open(substream, &component);  // - > 其实执行的是 component->driver->ops->open(substream);
	if (ret < 0)
		goto component_err;

	for_each_rtd_codec_dai(rtd, i, codec_dai) {
		ret = snd_soc_dai_startup(codec_dai, substream);//->其实执行的是 codec_dai->driver->ops->startup(substream, codec_dai); 
    .........

	if (rtd->dai_link->ops->startup) {
		ret = rtd->dai_link->ops->startup(substream);
		if (ret < 0) {
			pr_err("ASoC: %s startup failed: %d\n",
			       rtd->dai_link->name, ret);
			goto machine_err;
		}
	}

	/* Dynamic PCM DAI links compat checks use dynamic capabilities */
	if (rtd->dai_link->dynamic || rtd->dai_link->no_pcm)
		goto dynamic;

	/* Check that the codec and cpu DAIs are compatible */
	soc_pcm_init_runtime_hw(substream);

      ..........
	return ret;
}

目前看 是调用了.startup 而目前看我的环境中,只有在sa8155.c 里面注册 snd_soc_dai_link  BE dai 的时候,添加了这个ops 的注册。

 
{
		.name = LPASS_BE_PRI_TDM_RX_0,
		.stream_name = "Primary TDM0 Playback",
		.no_pcm = 1,
		.dpcm_playback = 1,
		.id = MSM_BACKEND_DAI_PRI_TDM_RX_0,
		.be_hw_params_fixup = msm_tdm_be_hw_params_fixup,
		.ops = &sa8155_tdm_be_ops,
		.ignore_suspend = 1,
		.ignore_pmdown_time = 1,
		SND_SOC_DAILINK_REG(pri_tdm_rx_0),
},
static struct snd_soc_ops sa8155_tdm_be_ops = {
	.hw_params = sa8155_tdm_snd_hw_params,
	.startup = sa8155_tdm_snd_startup,
	.shutdown = sa8155_tdm_snd_shutdown
};

这里的sa8155_tdm_snd_startup 是设置TDM 的pin 控制引脚的参数,我这的板子是使用的是TDM 这里肯定是用了的,但是目前还不确定是否在这里进行调用的! 看log  会在这里调用。

sa8155_tdm_snd_startup: substream = subdevice #0, stream = 0, dai name = QUAT_TDM_RX_0, dai id = 36912
(我有一个理解的猜想,但是目前这个猜想没有人给我准确的答案,其实这个Tinyalsa 的架构,这三个 platform 、code、Machine driver ),其实都有一套自己可实现的结构体,而平台方,如高通他只是按照这个Tinyalsa 的架构来实现自己的代码,其它有些结构体的接口有些实现了,有些没有实现,且并不是按照这个标准的alsa 的架构进行代码结构的设计,而这个接口在  kernel/msm-5.4/sound/soc/soc-pcm.c 这个是alsa 的标准接口,不受平台的约束定制,所以在代码里面就可以理解了在有些ops 上的调用接口之前,会先判断该ops 接口函数是否是NULL 的,然后才会去调用执行,如果为空,那么就证明平台(如高通),在其对应的 cpu_dai / code_dai 其实就没有实现该接口,因为它用不着,所以就没有实现,所以各有不同。)

再说一下这个接口: snd_soc_pcm_component_new(pcm); 它是在 soc_new_pcm 中调用来看一下它做了啥


int snd_soc_pcm_component_new(struct snd_pcm *pcm)
{
	struct snd_soc_pcm_runtime *rtd = pcm->private_data;
	struct snd_soc_rtdcom_list *rtdcom;
	struct snd_soc_component *component;
	int ret;

	for_each_rtdcom(rtd, rtdcom) {
		component = rtdcom->component;

		if (component->driver->pcm_new) {
			ret = component->driver->pcm_new(rtd);
			if (ret < 0)
				return ret;
		}
	}

	return 0;
}

这里去遍历了rtd->component_list  去获取了对应component ,然后去判断是否有去注册.pcm_new ,如果有组件去注册了这个回调函数,然后就去执行了这个回调函数,这个回调函数是在 高通sa8155 平台的 platform(msm-pcm-q6-v2.c) 里面有注册,是通过snd_soc_component_driver 注册的。

static struct snd_soc_component_driver msm_soc_component = {
	.name		= DRV_NAME,
	.ops		= &msm_pcm_ops,
	.pcm_new	= msm_asoc_pcm_new,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
	.delay_blk      = msm_pcm_delay_blk,
#endif /* CONFIG_AUDIO_QGKI */
};
然后在msm_pcm_probe()  里面通过 snd_soc_register_component(&pdev->dev,&msm_soc_component,NULL, 0);  接口注册。

tinymix "Audio Stream 29 App Type Cfg" 69936 61 48000 103 1024   //先记录一下这条命令,后面是有用的。

接着我们来分析 msm_asoc_pcm_new() 这个接口。

static int msm_asoc_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
	struct snd_card *card = rtd->card->snd_card;
	int ret = 0;

	if (!card->dev->coherent_dma_mask)
		card->dev->coherent_dma_mask = DMA_BIT_MASK(32);

	ret = msm_pcm_add_controls(rtd); //这里很重要,至少在我当前的这个项目中
	if (ret) {
		pr_err("%s, kctl add failed:%d\n", __func__, ret);
		return ret;
	}

	ret = msm_pcm_add_volume_control(rtd, SNDRV_PCM_STREAM_PLAYBACK);
	if (ret)
		pr_err("%s: Could not add pcm Volume Control %d\n",
			__func__, ret);
	ret = msm_pcm_add_volume_control(rtd, SNDRV_PCM_STREAM_CAPTURE);
	if (ret)
		pr_err("%s: Could not add pcm Volume Control %d\n",
			__func__, ret);
	ret = msm_pcm_add_compress_control(rtd);
	if (ret)
		pr_err("%s: Could not add pcm Compress Control %d\n",
			__func__, ret);

	ret = msm_pcm_add_audio_adsp_stream_cmd_control(rtd);
	if (ret)
		pr_err("%s: Could not add pcm ADSP Stream Cmd Control\n",
			__func__);

	ret = msm_pcm_add_audio_adsp_stream_callback_control(rtd);
	if (ret)
		pr_err("%s: Could not add pcm ADSP Stream Callback Control\n",
			__func__);

	return ret;
}

从以上逻辑来看,这个msm_asoc_pcm_new() 接口里面创建了很多控件,用来设置一些kernel 所需要的一下参数等,也可以理解成为一个widgiet,但是它不是用于数据流,而是用于控制流,每个接口接口里面有定义struct snd_kcontrol *kctl; 然后注册回调函数 kctl->put、kctl->get,用于上层控制流操作使用,如上层会调用: mixer_ctl_set_array() 接口进行控制设置,一般会先通过  mixer_get_ctl_by_name() 获取控制命令的ctl 句柄,然后再封装命令参数,调用mixer_ctl_set_array() 进行设置。

接着我们再继续分析msm_pcm_add_controls()

static int msm_pcm_add_controls(struct snd_soc_pcm_runtime *rtd)
{
	int ret = 0;

	pr_debug("%s\n", __func__);
	ret = msm_pcm_add_chmap_controls(rtd);
	if (ret)
		pr_err("%s: pcm add controls failed:%d\n", __func__, ret);
	ret = msm_pcm_add_app_type_controls(rtd); //我仅关注这里,其它两个也是添加kctl 控件,逻辑也是差不多的,我们目前就分析这个
	if (ret)
		pr_err("%s: pcm add app type controls failed:%d\n",
			__func__, ret);
	ret = msm_pcm_add_channel_mixer_controls(rtd);  //这个里面创建的widgit kctl 控件也用到了。
	if (ret)
		pr_err("%s: pcm add channel mixer controls failed:%d\n",
			__func__, ret);
	return ret;
}

通过这些创建的kctl 的widgit 控件,会在音频流被打开前会被设置,设置后会在kernel 中保存,一般是在高通的msm-pcm-routing-auto.c 中先把设置的参数保存起来,然后在流打开的时候再获取这些设置保存的参数,再通过apr 设置给ADSP。

static int msm_pcm_add_app_type_controls(struct snd_soc_pcm_runtime *rtd)
{
	struct snd_pcm *pcm = rtd->pcm;
	struct snd_pcm_usr *app_type_info;
	struct snd_kcontrol *kctl;
	const char *playback_mixer_ctl_name	= "Audio Stream";
	const char *capture_mixer_ctl_name	= "Audio Stream Capture";
	const char *deviceNo		= "NN";
	const char *suffix		= "App Type Cfg";
	int ctl_len, ret = 0;

	if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
		ctl_len = strlen(playback_mixer_ctl_name) + 1 +
				strlen(deviceNo) + 1 + strlen(suffix) + 1;
		pr_debug("%s: Playback app type cntrl add\n", __func__);
		ret = snd_pcm_add_usr_ctls(pcm, SNDRV_PCM_STREAM_PLAYBACK, // snd_pcm_add_usr_ctls 这个接口就是在创建这个控件,然后通过app_type_info 返回控件结构信息。
					NULL, 1, ctl_len, rtd->dai_link->id,
					&app_type_info);
		if (ret < 0) {
			pr_err("%s: playback app type cntrl add failed: %d\n",
				__func__, ret);
			return ret;
		}
		kctl = app_type_info->kctl;  //或者上面接口函数返回的snd_kcontrol 的 实际的操作指针
		snprintf(kctl->id.name, ctl_len, "%s %d %s",
			playback_mixer_ctl_name, rtd->pcm->device, suffix);  // 这里就将我们的控件名变为:“Audio Stream 0 App Type Cfg”
		kctl->put = msm_pcm_playback_app_type_cfg_ctl_put;  // 赋值回调的操作函数。
		kctl->get = msm_pcm_playback_app_type_cfg_ctl_get;
	}

	if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) { // 这里就是“Audio Stream Capture”创建的流程。
		ctl_len = strlen(capture_mixer_ctl_name) + 1 +
				strlen(deviceNo) + 1 + strlen(suffix) + 1;
		pr_debug("%s: Capture app type cntrl add\n", __func__);
		ret = snd_pcm_add_usr_ctls(pcm, SNDRV_PCM_STREAM_CAPTURE,
					NULL, 1, ctl_len, rtd->dai_link->id,
					&app_type_info);
		if (ret < 0) {
			pr_err("%s: capture app type cntrl add failed: %d\n",
				__func__, ret);
			return ret;
		}
		kctl = app_type_info->kctl;
		snprintf(kctl->id.name, ctl_len, "%s %d %s",
			capture_mixer_ctl_name, rtd->pcm->device, suffix);
		kctl->put = msm_pcm_capture_app_type_cfg_ctl_put;
		kctl->get = msm_pcm_capture_app_type_cfg_ctl_get;
	}

	return 0;
}

然后我们在看一下 kctl->put = msm_pcm_playback_app_type_cfg_ctl_put; 在使用的时候干了啥。

static int msm_pcm_playback_app_type_cfg_ctl_put(struct snd_kcontrol *kcontrol,
					struct snd_ctl_elem_value *ucontrol)
{
	u64 fe_id = kcontrol->private_value;
	int session_type = SESSION_TYPE_RX;
	int be_id = ucontrol->value.integer.value[3];
	struct msm_pcm_stream_app_type_cfg cfg_data = {0, 0, 48000, 0, 0};
	int ret = 0;

	cfg_data.app_type = ucontrol->value.integer.value[0];
	cfg_data.acdb_dev_id = ucontrol->value.integer.value[1];
	if (ucontrol->value.integer.value[2] != 0)
		cfg_data.sample_rate = ucontrol->value.integer.value[2];
	if (ucontrol->value.integer.value[4] != 0)
		cfg_data.copp_token = ucontrol->value.integer.value[4];
	if (ucontrol->value.integer.value[5] != 0)
		cfg_data.bit_width = ucontrol->value.integer.value[5];
	pr_debug("%s: fe_id- %llu session_type- %d be_id- %d app_type- %d acdb_dev_id- %d"
		"sample_rate- %d copp_token- %d bit_width- %d\n",
		__func__, fe_id, session_type, be_id, cfg_data.app_type, cfg_data.acdb_dev_id,
		cfg_data.sample_rate, cfg_data.copp_token, cfg_data.bit_width);
	//将我们通过控件设置下来的值全部存到了cfg_data  中,这是一个stream app type config 配置结构体。
	ret = msm_pcm_routing_reg_stream_app_type_cfg(fe_id, session_type,
						      be_id, &cfg_data);
	if (ret < 0)
		pr_err("%s: msm_pcm_routing_reg_stream_app_type_cfg failed returned %d\n",
			__func__, ret);

	return ret;
}

这里是在msm-pcm-routing-v2.h 里面定义,对应下面的msm_pcm_routing_reg_stream_app_type_cfg 接口在msm-pcm-routing-v2.c 里面。

struct msm_pcm_stream_app_type_cfg {
	int app_type;
	int acdb_dev_id;
	int sample_rate;
	uint32_t copp_token;
	int bit_width;
};

int msm_pcm_routing_reg_stream_app_type_cfg(
	int fedai_id, int session_type, int be_id,
	struct msm_pcm_stream_app_type_cfg *cfg_data)
{
	………….
	fe_dai_app_type_cfg[fedai_id][session_type][be_id] = *cfg_data;
	//这里就将刚才我们设置配置参数,赋值给了这个三维数组。它是一个全局变量,它会在流打开的时候会来读取这个数组里面的值,然后一起设置给ADSP。
	/*
	 * Store the BE ID of the configuration information set as the latest so
	 * the get mixer control knows what to return.
	 */
	last_be_id_configured[fedai_id][session_type] = be_id;

done:
	return ret;
}

然后咱们再说说在哪里使用的;

在audio_hw.c 里面的enable_audio_router() 接口里面调用,这个接口是干嘛的就不多说了,看下调用流程:

enable_audio_route() --->(utils.c)audio_extn_utils_send_app_type_cfg() --->send_app_type_cfg_for_device() --->  send_app_type_cfg_for_device()


static int send_app_type_cfg_for_device(struct audio_device *adev,
                                        struct audio_usecase *usecase,
                                        int split_snd_device)
{
    char mixer_ctl_name[MAX_LENGTH_MIXER_CONTROL_IN_INT];
    size_t app_type_cfg[MAX_LENGTH_MIXER_CONTROL_IN_INT] = {0};
    int len = 0, rc;
    struct mixer_ctl *ctl;
    int pcm_device_id = 0, acdb_dev_id, app_type;
    int snd_device = split_snd_device, snd_device_be_idx = -1;
    int32_t sample_rate = DEFAULT_OUTPUT_SAMPLING_RATE;
    struct streams_io_cfg *s_info = NULL;
    struct listnode *node = NULL;
    int bd_app_type = 0;
    ...............

    if (usecase->type == PCM_PLAYBACK || usecase->type == TRANSCODE_LOOPBACK_RX) {
        pcm_device_id = platform_get_pcm_device_id(usecase->id, PCM_PLAYBACK);
        snprintf(mixer_ctl_name, sizeof(mixer_ctl_name),
            "Audio Stream %d App Type Cfg", pcm_device_id);
    } .............

    ctl = mixer_get_ctl_by_name(adev->mixer, mixer_ctl_name);
    ...............
    if (voice_is_in_call_rec_stream(usecase->stream.in) && usecase->type == PCM_CAPTURE) {
        snd_device_t voice_device = voice_get_incall_rec_snd_device(usecase->in_snd_device);
        acdb_dev_id = platform_get_snd_device_acdb_id(voice_device);
        ALOGV("acdb id for voice call use case %d", acdb_dev_id);
    } else {
        acdb_dev_id = platform_get_snd_device_acdb_id(snd_device);
    }
    if (acdb_dev_id <= 0) {
        ALOGE("%s: Couldn't get the acdb dev id", __func__);
        rc = -EINVAL;
        goto exit_send_app_type_cfg;
    }

    ..............
    if ((usecase->type == PCM_PLAYBACK) && (usecase->stream.out != NULL)) {
        /* Interactive streams are supported with only direct app type id.
         * Get Direct profile app type and use it for interactive streams
         */
        list_for_each(node, &adev->streams_output_cfg_list) {
            s_info = node_to_item(node, struct streams_io_cfg, list);
            if (s_info->flags.out_flags == (audio_output_flags_t)(AUDIO_OUTPUT_FLAG_BD |
                                            AUDIO_OUTPUT_FLAG_DIRECT_PCM |
                                            AUDIO_OUTPUT_FLAG_DIRECT))
                bd_app_type = s_info->app_type_cfg.app_type;
        }
        if (usecase->stream.out->flags == (audio_output_flags_t)AUDIO_OUTPUT_FLAG_INTERACTIVE)
            app_type = bd_app_type;
        else
            app_type = usecase->stream.out->app_type_cfg.app_type;
        app_type_cfg[len++] = app_type;
        app_type_cfg[len++] = acdb_dev_id;
        app_type_cfg[len++] = sample_rate;

        if (snd_device_be_idx > 0)
            app_type_cfg[len++] = snd_device_be_idx;

        //add copp_token
        ALOGD("%s: usecase->id= %d, snd_device_be_idx= %d.", __func__, usecase->id, snd_device_be_idx);
        if ((usecase->id == USECASE_AUDIO_PLAYBACK_MEDIA) || (usecase->id == USECASE_AUDIO_PLAYBACK_NAV_GUIDANCE) ||
        (usecase->id == USECASE_AUDIO_PLAYBACK_VR_TTS) || (usecase->id == USECASE_AUDIO_PLAYBACK_TTS_CAR) ||
        (usecase->id == USECASE_AUDIO_PLAYBACK_CHIME_TTS))
        {
            app_type_cfg[len++] = copp_token;
            ALOGD("%s: usecase->id= %d, snd_device_be_idx= %d, copp_token=1024.", __func__, usecase->id, snd_device_be_idx);
        }

         ALOGI("%s PLAYBACK app_type %d, acdb_dev_id %d, sample_rate %d, snd_device_be_idx %d",
             __func__, app_type, acdb_dev_id, sample_rate, snd_device_be_idx);

    } ..........

    if(ctl)
        mixer_ctl_set_array(ctl, app_type_cfg, len);  //这里就是调用设置了,最终就会调用到msm_pcm_routing_reg_stream_app_type_cfg() 在kernel 侧,msm-pcm-routing-auto.c
    .................
}

-------------------------------------------概念--------------------------------------------

ADM (Audio Device Manager): ADSP 上的一个服务,负责管理音频设备的连接、数据流和处理。它接收来自 ASM (Audio Stream Manager) 的数据,并将其路由到正确的 AFE (Audio Front End) 端口,反之亦然。

COPP (Command and Output Post Processor): ADM 内部的一个逻辑单元,代表一个特定的音频处理路径实例。每个 COPP 绑定到一个 AFE 端口,并配置了特定的参数(如采样率、通道数)和处理拓扑(如 EQ、音量控制、MCH Mixer 等)。

Topology: 定义了 COPP 内部的处理模块及其连接方式。例如,一个简单的播放拓扑可能只包含音量控制;一个复杂的语音通话拓扑可能包含 EC (Echo Cancellation)、NS (Noise Suppression) 等模块。

EC Ref (Echo Cancellation Reference): 在录音场景下,为了消除扬声器播放的声音被麦克风再次收录而产生的回声,需要提供一个来自播放路径的参考信号给 ADM/ASM 进行回声消除处理。endpoint_id_2 就是用来指定这个参考信号来源的 AFE 端口。

---------------------------------------------------------------------------------------------

Pcm_prepare 的调用 最终按照上面的代码流程追,在我sa8155 的平台上,是调用到了 platform driver 里面的msm-pcm-q6-v2.c 里面的msm_pcm_prepare() 接口函数。

static int msm_pcm_prepare(struct snd_pcm_substream *substream)
{
	int ret = 0;

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		ret = msm_pcm_playback_prepare(substream);
	else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
		ret = msm_pcm_capture_prepare(substream);
	return ret;
}

(简单来说就是当数据准备好之后,根据实际数据来设置一些音频流的参数,同样,在prepare被调用后函数内会分别调用所有的dai、platform、dai link自己的prepare函数,如果需要处理prepare的话各自就实现该函数。

不过在高通平台上似乎没有那么简单,首先,fe的platform在prepare的时候会操作msm_audio,向dsp设置相关的操作,be的platform在prepare的时候要打开一个adm,并对dsp内部音频链路上的处理过程进行配置,这块跟dsp结合很紧,里面东西也很多,下次在单独的文章中分析。fe的cpu dai以及codec dai都没有实现,be的cpu dai在prepare时会启动dsp上对应的afe port(其实就是dsp音频链路最后的处理单元,这个处理单元后面就是音频输出的物理接口了),be的codec dai设置了一下silm接口的宽度。

所以,可以认为prepare也是设置参数,只是跟hw params不同,这里的参数基本上都是围绕音频数据流来进行的,而hw params时的参数更多的是关心硬件特性。)

该接口会根据stream 的类型来选择执行 msm_pcm_playback_prepare(substream); / msm_pcm_capture_prepare(substream); 在msm_pcm_playback_prepare 接口里面有一个比较重要的函数,

int msm_pcm_routing_reg_phy_stream(int fedai_id, int perf_mode,int dspst_id, int stream_type)

后面我们再来慢慢的走读一下,目前看该接口里面的代码看不太懂。

Trigger 调用

Pcm_start 的调用流程中会显示的去调用Trigger 的流程,也可以不需要显示的调用,也会自动启动,但是这个动作对于操作启动PCM的数据流是必要的。

1、显式启动 (Explicit Start):

场景: 应用程序希望完全控制何时开始播放/录音。例如,需要同步多个 PCM 流,或者在特定时刻精确启动。

配置: 将 start_threshold 设置为一个大于可用缓冲区大小(avail min)的值,或者设置为一个特殊值(如 buffer_size + 1)。这样即使缓冲区中有数据,也不会自动启动。

操作: 在这种模式下,应用程序必须在准备好初始数据后,显式地调用 snd_pcm_start() 来启动数据传输。如果在 start_threshold 条件满足前尝试读写数据而没有显式启动,可能会导致 EPIPE 错误或等待。

2、自动启动 (Auto-start / Implicit Start):

场景: 应用程序将数据写入/读取缓冲区后,希望 PCM 流能自动开始运行。

配置: 将 start_threshold 设置为一个小于或等于可用缓冲区大小(avail min)的值(通常设置为小于等于 period_size)。最常见的配置是将其设置为 period_size 或更小。

操作: 在这种模式下,应用程序不需要调用 snd_pcm_start()。当应用程序第一次调用 snd_pcm_writei() (播放) 或 snd_pcm_readi() (录音),并且写入/可读取的数据量达到了 start_threshold 设定的值时,ALSA 库或内核会自动触发 pcm_start 的流程,从而调用驱动的 .trigger 回调函数启动硬件。

对于上面的自动启动的模式,追一下代码:

其实是在pcm_write() 接口里面进行调用的,pcm_write() -> ioctl(pcm-<fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x) ->

snd_pcm_xferi_frames_ioctl()  ->  snd_pcm_lib_write() -> __snd_pcm_lib_xfer() 就是在这个接口里面会去添加判断。

/* the common loop for read/write data */
snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
				     void *data, bool interleaved,
				     snd_pcm_uframes_t size, bool in_kernel)
{
	..............
	while (size > 0) {
		...........
		if (is_playback &&
		    runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
		    snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
			err = snd_pcm_start(substream);
			if (err < 0)
				goto _end_unlock;
		}
	}
    ........
}

is_playback: 确保这是播放(Playback)方向的操作。

runtime->status->state == SNDRV_PCM_STATE_PREPARED: PCM 流必须仍处于 PREPARED 状态(尚未启动)。

snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold:

snd_pcm_playback_hw_avail(runtime): 计算缓冲区中已经写入但尚未被硬件读取播放的数据量(即 "可用" 给硬件播放的数据量)。

这个条件检查累积的待播放数据量是否达到了 start_threshold,调用 snd_pcm_start(substream) 来启动播放。(在写数据的时候会先去判断runtime的状态,如果runtime还是prepared状态,且已经收到的数据超过了启动发送门限值了,则会调用trigger,来完成实际的数据搬移。所以上层应用往往会省略start操作,在prepare之后便直接开始写数据了。 )

对于上面的trigger 调用,注册的dai_link 是dynamic 的就是 rtd->ops.trigger = dpcm_fe_dai_trigger; 而非dynamic 的是 rtd->ops.trigger = soc_pcm_trigger;但是追踪代码,最后都是调用的是soc_pcm_trigger 该接口。


static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_component *component;
	struct snd_soc_rtdcom_list *rtdcom;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	struct snd_soc_dai *codec_dai;
	int i, ret;

	for_each_rtd_codec_dai(rtd, i, codec_dai) {
		ret = snd_soc_dai_trigger(codec_dai, substream, cmd);
		if (ret < 0)
			return ret;
	}

	for_each_rtdcom(rtd, rtdcom) {
		component = rtdcom->component;

		ret = snd_soc_component_trigger(component, substream, cmd); //其实操作的是 component->driver->ops->trigger(substream, cmd);
		//这里就直接调用到了高通的msm-pcm-q6-v2.c 里面的 .trigger        = msm_pcm_trigger,  看msm_pcm_trigger(substream,cmd) 的入参也只有两个。
		if (ret < 0)
			return ret;
	}

	ret = snd_soc_dai_trigger(cpu_dai, substream, cmd);
	if (ret < 0)
		return ret;

	if (rtd->dai_link->ops->trigger) {
		ret = rtd->dai_link->ops->trigger(substream, cmd);
		if (ret < 0)
			return ret;
	}

	return 0;

这里的msm_pcm_ops 是在 platform 中进行注册的,高通的文件是在msm-pcm-q6-v2.c 里面

static const struct snd_pcm_ops msm_pcm_ops = {
	.open           = msm_pcm_open,
	.copy_user	= msm_pcm_copy,
	.hw_params	= msm_pcm_hw_params,
	.close          = msm_pcm_close,
	.ioctl          = msm_pcm_ioctl,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
	.compat_ioctl   = msm_pcm_compat_ioctl,
#endif /* CONFIG_AUDIO_QGKI */
	.prepare        = msm_pcm_prepare,
	.trigger        = msm_pcm_trigger,
	.pointer        = msm_pcm_pointer,
	.mmap		= msm_pcm_mmap,
};

static struct snd_soc_component_driver msm_soc_component = { 
	.name		= DRV_NAME,
	.ops		= &msm_pcm_ops,
	.pcm_new	= msm_asoc_pcm_new,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
	.delay_blk      = msm_pcm_delay_blk,
#endif /* CONFIG_AUDIO_QGKI */
};

static int msm_pcm_probe(struct platform_device *pdev)
{
	……………. //这里使用的是snd_soc_register_component 进行注册的,所以注册的就是一个component 。
	return snd_soc_register_component(&pdev->dev,
					&msm_soc_component,
					NULL, 0);
}
//所以说高通的pcm 的相关操作只在  platform driver 中实现了。

static int msm_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	int ret = 0;
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct msm_audio *prtd = runtime->private_data;
#ifdef CONFIG_SND_SOC_MSM_QDSP6V2_VM
        static int first_time = 0;
#else
        static int first_time = 1;
#endif

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
		if (first_time) {
#ifdef CONFIG_MSM_BOOT_STATS
			place_marker("K - Early chime");
#endif
			first_time = 0;
		}
		pr_debug("%s: Trigger start\n", __func__);
		ret = q6asm_run_nowait(prtd->audio_client, 0, 0, 0); //播放的时候是调用这里
		break;
	case SNDRV_PCM_TRIGGER_STOP:
		pr_debug("SNDRV_PCM_TRIGGER_STOP\n");
		if (substream->stream != SNDRV_PCM_STREAM_PLAYBACK) {
			atomic_set(&prtd->start, 0);
			prtd->enabled = STOPPED;
			ret = q6asm_cmd_nowait(prtd->audio_client, CMD_PAUSE);
			break;
		}
		if (!atomic_read(&prtd->start)) {
			pr_info("%s: not running\n", __func__);
			break;
		}
		atomic_set(&prtd->start, 0);
		/* pending CMD_EOS isn't expected */
		WARN_ON_ONCE(test_bit(CMD_EOS, &prtd->cmd_pending));
		set_bit(CMD_EOS, &prtd->cmd_pending);
		ret = q6asm_cmd_nowait(prtd->audio_client, CMD_EOS);
		if (ret)
			clear_bit(CMD_EOS, &prtd->cmd_pending);
		break;
	case SNDRV_PCM_TRIGGER_SUSPEND:
	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
		pr_debug("SNDRV_PCM_TRIGGER_PAUSE\n");
		ret = q6asm_cmd_nowait(prtd->audio_client, CMD_PAUSE);
		atomic_set(&prtd->start, 0);
		break;
	default:
		ret = -EINVAL;
		break;
	}

	return ret;
}

Write 调用

这里肯定就是指的咱们的pcm_write 调用,接着我们上面的pcm_write 的接口调用的流程来继续分析,最终还是调用到了__snd_pcm_lib_xfer() 接口内部去处理。

pcm_write() 接口里面进行调用的,pcm_write() -> ioctl(pcm-<fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x) ->

snd_pcm_xferi_frames_ioctl()  ->  snd_pcm_lib_write() -> __snd_pcm_lib_xfer()


/* the common loop for read/write data */
snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
				     void *data, bool interleaved,
				     snd_pcm_uframes_t size, bool in_kernel)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	snd_pcm_uframes_t xfer = 0;
	snd_pcm_uframes_t offset = 0;
	snd_pcm_uframes_t avail;
	pcm_copy_f writer;
	pcm_transfer_f transfer;
	bool nonblock;
	bool is_playback;
	int err;

	err = pcm_sanity_check(substream);
	if (err < 0)
		return err;

	is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
	if (interleaved) {
		if (runtime->access != SNDRV_PCM_ACCESS_RW_INTERLEAVED &&
		    runtime->channels > 1)
			return -EINVAL;
		writer = interleaved_copy;
	} else {
		if (runtime->access != SNDRV_PCM_ACCESS_RW_NONINTERLEAVED)
			return -EINVAL;
		writer = noninterleaved_copy;
	}

	if (!data) {
		if (is_playback)
			transfer = fill_silence;
		else
			return -EINVAL;
	} else if (in_kernel) {
		if (substream->ops->copy_kernel)
			transfer = substream->ops->copy_kernel;
		else
			transfer = is_playback ?
				default_write_copy_kernel : default_read_copy_kernel;
	} else {
		if (substream->ops->copy_user)
			transfer = (pcm_transfer_f)substream->ops->copy_user;  //下面的writer() 接口调用的是interleaved_copy/noninterleaved_copy
            //而接口interleaved_copy 里面调用的是transfer 这里我们的数据是来自用户空间,所以说transfer = (pcm_transfer_f)substream->ops->copy_user; 复制的是runtime 里面的ops->copy_user;
		else
			transfer = is_playback ?
				default_write_copy : default_read_copy;
	}

	..............
	if (!is_playback &&
	    runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
	    size >= runtime->start_threshold) {  //这里是captrue 录音 
		err = snd_pcm_start(substream);
		if (err < 0)
			goto _end_unlock;
	}

	avail = snd_pcm_avail(substream);

	while (size > 0) {
        ............
		frames = size > avail ? avail : size;
		appl_ptr = READ_ONCE(runtime->control->appl_ptr);
		appl_ofs = appl_ptr % runtime->buffer_size;
		cont = runtime->buffer_size - appl_ofs;
		if (frames > cont)
			frames = cont;
		if (snd_BUG_ON(!frames)) {
			err = -EINVAL;
			goto _end_unlock;
		}
		snd_pcm_stream_unlock_irq(substream);
		err = writer(substream, appl_ofs, data, offset, frames,
			     transfer);
		.........

		offset += frames;
		size -= frames;
		xfer += frames;
		avail -= frames;
		if (is_playback &&
		    runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
		    snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
			err = snd_pcm_start(substream);
			if (err < 0)
				goto _end_unlock;
		}
	}
    ...........
}

而transfer = (pcm_transfer_f)substream->ops->copy_user;  里面的copy_user 是调用的哪里,就是上面注册的snd_pcm_ops(msm-pcm-q6-v2.c 当然使用哪个驱动文件里面的内容,需要看你自己dai link的配置,sa8155 平台是在 msm_dailink.h 里面进行配置,上面我们应该提到过)。 .copy_user        = msm_pcm_copy;那么我们看这两是在哪里进行赋值链接上的。

就在soc_new_pcm()内部;


/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
    ............
	for_each_rtdcom(rtd, rtdcom) {
		const struct snd_pcm_ops *ops = rtdcom->component->driver->ops;

		if (!ops)
			continue;

		if (ops->copy_user)
			rtd->ops.copy_user	= snd_soc_pcm_component_copy_user;// 就是在这里进行链接赋值的
		if (ops->page)
			rtd->ops.page		= snd_soc_pcm_component_page;
		if (ops->mmap)
			rtd->ops.mmap		= snd_soc_pcm_component_mmap;
	}

	if (playback)
		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops); //substream->ops = ops; 在这个接口里面将ops 赋值给音频流substream->ops 的

	if (capture)
		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops);
    .....
	return ret;
}


#define for_each_rtdcom(rtd, rtdcom) \  //遍历通过snd_soc_register_component(&pdev->dev,&msm_soc_component,NULL, 0); 注册的component_list。可以在跟踪一下snd_soc_register_component() 这个是在platform 里面的probe里面调用。
list_for_each_entry(rtdcom, &(rtd)->component_list, list)


int snd_soc_pcm_component_copy_user(struct snd_pcm_substream *substream,
				    int channel, unsigned long pos,
				    void __user *buf, unsigned long bytes)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_rtdcom_list *rtdcom;
	struct snd_soc_component *component;

	for_each_rtdcom(rtd, rtdcom) { //遍历component_list 找到对应的component->driver。
		component = rtdcom->component;

		/* FIXME. it returns 1st copy now */
		if (component->driver->ops &&
		    component->driver->ops->copy_user)
			return component->driver->ops->copy_user( //返回上面提到注册的ops (snd_pcm_ops) 	.copy_user	= msm_pcm_copy,
				substream, channel, pos, buf, bytes);
	}

	return -EINVAL;
}

static int msm_pcm_playback_copy(struct snd_pcm_substream *substream, int a,
	unsigned long hwoff, void __user *buf, unsigned long fbytes)
{
	int ret = 0;
	int xfer = 0;
	char *bufptr = NULL;
	void *data = NULL;
	uint32_t idx = 0;
	uint32_t size = 0;
	uint32_t retries = 0;

	struct snd_pcm_runtime *runtime = substream->runtime;
	struct msm_audio *prtd = runtime->private_data;

	pr_debug("%s: prtd->out_count = %d\n",
				__func__, atomic_read(&prtd->out_count));

	while ((fbytes > 0) && (retries < MAX_PB_COPY_RETRIES)) {
		if (prtd->reset_event) {
			pr_err("%s: In SSR return ENETRESET before wait\n",
				__func__);
			return -ENETRESET;
		}

		ret = wait_event_timeout(the_locks.write_wait,
				(atomic_read(&prtd->out_count)),
				msecs_to_jiffies(TIMEOUT_MS));
		if (!ret) {
			pr_err("%s: wait_event_timeout failed\n", __func__);
			ret = -ETIMEDOUT;
			goto fail;
		}
		ret = 0;

		if (prtd->reset_event) {
			pr_err("%s: In SSR return ENETRESET after wait\n",
				__func__);
			return -ENETRESET;
		}

		if (!atomic_read(&prtd->out_count)) {
			pr_err("%s: pcm stopped out_count 0\n", __func__);
			return 0;
		}

		data = q6asm_is_cpu_buf_avail(IN, prtd->audio_client, &size,
			&idx);
		if (data == NULL) {
			retries++;
			continue;
		} else {
			retries = 0;
		}

		if (fbytes > size)
			xfer = size;
		else
			xfer = fbytes;

		bufptr = data;
		if (bufptr) {
			pr_debug("%s:fbytes =%lu: xfer=%d size=%d\n",
				 __func__, fbytes, xfer, size);
			if (copy_from_user(bufptr, buf, xfer)) {
				ret = -EFAULT;
				pr_err("%s: copy_from_user failed\n",
					__func__);
				q6asm_cpu_buf_release(IN, prtd->audio_client);
				goto fail;
			}
			buf += xfer;
			fbytes -= xfer;
			pr_debug("%s:fbytes = %lu: xfer=%d\n", __func__,
				 fbytes, xfer);
			if (atomic_read(&prtd->start)) {
				pr_debug("%s:writing %d bytes of buffer to dsp\n",
						__func__, xfer);
				ret = q6asm_write(prtd->audio_client, xfer,  //最终是调用这个接口向ADSP 进行写数据。
							0, 0, NO_TIMESTAMP);
				if (ret < 0) {
					ret = -EFAULT;
					q6asm_cpu_buf_release(IN,
						prtd->audio_client);
					goto fail;
				}
			} else
				atomic_inc(&prtd->out_needed);
			atomic_dec(&prtd->out_count);
		}
	}
fail:
	if (retries >= MAX_PB_COPY_RETRIES)
		ret = -ENOMEM;

	return  ret;
}

音频数据流的创建

这里主要记录音频数据流为什么会被创建,或者说在什么情况下会创建一个pcm或者compress设备,这些设备创建时的配置在哪,这些配置有什么作用,了解了这些才能真正自己进行添加或者修改音频设备。

这里就通过pcm设备的创建来进行记录,compress设备跟pcm设备类似,control设备(不是control控件,这两个东西一定要区分,control设备实在linux下可以通过声卡访问到的一个linux设备,跟pcm一样;control控件是声卡的一个配置项,就像一个配置参数一样,一般来说是通过对control设备的ioctrl操作进行操作的)是在声卡创建时默认都会创建的。

那么如果想要创建一个pcm设备,该怎么做?那就是定义一个dai link!!!但是并不是所有dai link都会创建pcm设备,关于pcm的创建开头链接的文章中讲的很清楚了,但是在现代移动平台上基本上全是dynamic pcm了。

在上面的流程图中,我画了一个在Machine probe 里面创建PCM 的流程图,里面最开始就创建了control设备。

dapm视角的音频链路

dapm、widgets在开头链接中的文章已经说的很清楚了,下面就记录一下那些文章中没提或者简单说了一下但是又确实实际使用的东西。最后再用手头开发板开发板实际的widgets做一个示例。

我不知道这个东西的学名,但我更愿意叫他软件定义硬件,这是一种“面向对象”的硬件编程思想,对于复杂的硬件编程是极其有效的方式,linux中很多地方都有这种思想的体现,但在dapm中更是用到了极致。

在dapm中,对于框架软件看到的是一个个抽象过的widgets,框架软件可以自由组合、使用这些widgets,就行搭积木一样,每一个widgets都是一块积木,这些积木通过胶水——path去链接,这一切的一切都由软件来控制,框架软件并不用去关心这每个积木怎么工作,只用关心每个积木是什么形状,根据图纸(硬件链接关系)拼出各种各样的图案。对于widgets,它只用关心一个通用模型,关心他要操作的对象,至于什么时候操作,被放置在哪个位置,widgets完全不用去关心。

dai widgets之间如何链接

在分析这个问题前,先简单记录一下另外一个问题,用widgets链接dai干什么?

因为音频链路的通路是通过widgets的路径来描述的,而不同硬件设备间的链接是通过dai link来描述的,那么根据damp的理念,在音频链路上dai link也应该对应一个widgets,该widgets分别链接两边硬件的widgets,以实现垮硬件的widgets通路。

关于dai的链接,两个问题,1、dai widgets怎么创建的,2、dai widgets怎么链接的。

 dai widgets的创建

dai widgets如果想写死,通过静态方式定义出来,我觉得是完全可以的,但是既然已经定义了dai link,那么就可以根据dai link来自动创建dai widgets了。关于dai link的创建在开头链接的文章中已经记录了,这里再提一下,就不详细说明。

dai widgets在声卡创建阶段会去调用soc_probe_link_components函数probe所有注册进来的component(关于component看一下register相关函数和component list就明白了),soc_probe_link_components中会对每个component调用snd_soc_dapm_new_dai_widgets。这里有一点提一下,就是在创建的widget中,其name和sname都是dai driver中的stream name,这是因为后面的链接时会去匹配这个名字,这里留到后面链接那里再记录。

dai widgets间的链接

在创建完dai widgets后会继续调用snd_soc_dapm_connect_dai_link_widgets函数进行dai widgets的链接:

void snd_soc_dapm_connect_dai_link_widgets(struct snd_soc_card *card)
{
	struct snd_soc_pcm_runtime *rtd; msm-dai-fe.c

	/* for each BE DAI link... */
	for_each_card_rtds(card, rtd)  {
		/*
		 * dynamic FE links have no fixed DAI mapping.
		 * CODEC<->CODEC links have no direct connection.
		 */
#ifdef CONFIG_AUDIO_QGKI
		if (rtd->dai_link->dynamic || rtd->dai_link->dynamic_be)
#else
		if (rtd->dai_link->dynamic)
#endif
			continue;

		dapm_connect_dai_link_widgets(card, rtd);
	}
}

这里添加一个重要的理解!!!!!!!!!!!!!!

就是说在高通的平台,这里的msm-dai-fe.c 里面的 snd_soc_dai_driver 结构体定义。

static struct snd_soc_dai_driver msm_fe_dais[] = {
	{
		.playback = {
			.stream_name = "MultiMedia1 Playback",
			.aif_name = "MM_DL1",
			.rates = (SNDRV_PCM_RATE_8000_384000|
					SNDRV_PCM_RATE_KNOT),
			.formats = (SNDRV_PCM_FMTBIT_S16_LE |
						SNDRV_PCM_FMTBIT_S24_LE |
						SNDRV_PCM_FMTBIT_S24_3LE |
						SNDRV_PCM_FMTBIT_S32_LE),
			.channels_min = 1,
			.channels_max = 32,
			.rate_min =     8000,
			.rate_max =	384000,
		},
		.capture = {
			.stream_name = "MultiMedia1 Capture",
			.aif_name = "MM_UL1",
			.rates = (SNDRV_PCM_RATE_8000_384000|
					SNDRV_PCM_RATE_KNOT),
			.formats = (SNDRV_PCM_FMTBIT_S16_LE |
				    SNDRV_PCM_FMTBIT_S24_LE |
				    SNDRV_PCM_FMTBIT_S24_3LE |
				    SNDRV_PCM_FMTBIT_S32_LE),
			.channels_min = 1,
			.channels_max = 32,
			.rate_min =     8000,
			.rate_max =	48000,
		},
		.ops = &msm_fe_Multimedia_dai_ops,
		.name = "MultiMedia1",
		.probe = fe_dai_probe,
	},
};

这里是这里在整个sa8155的平台属于FE dai driver,这里其实是跟pcm 的创建有着很大的关系,其它可以理解它的定义就是限制pcm 节点的硬件参数的,这里的定义更加侧重于硬件底层的DAI接口的静态能力。

而在高通平台的Machine driver 它对应的文件就是sa8155.c 这里是声卡注册的起源,也是开始调用的地方,这里面定义了很多dai link,这里的dai link 我们再来理解一下,snd_soc_dai_link。


/* Digital audio interface glue - connects codec <---> CPU */
static struct snd_soc_dai_link msm_common_dai_links[] = {
	/* FrontEnd DAI Links */
	{
		.name = MSM_DAILINK_NAME(Media1),
		.stream_name = "MultiMedia1",
		.dynamic = 1,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
		.async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
		.dpcm_playback = 1,
		.dpcm_capture = 1,
		.trigger = {SND_SOC_DPCM_TRIGGER_POST,
			SND_SOC_DPCM_TRIGGER_POST},
		.ignore_suspend = 1,
		/* this dainlink has playback support */
		.ignore_pmdown_time = 1,
		.id = MSM_FRONTEND_DAI_MULTIMEDIA1,
		SND_SOC_DAILINK_REG(multimedia1),
	},
};

上面说到会根据这里的dai link 去创建pcm 节点,而这里的“MultiMedia1” 就非常关键,在声卡创建的时候 会去遍历对应的component ,因为它是通过接口snd_soc_register_component  去注册的,然后 snd_soc_register_card() 函数内部根据对应的“MultiMedia1”去配置,然后就去创建对应的pcm 节点,而snd_soc_dai_link 里面的配置更注重于pcm 的特性。

static int snd_soc_instantiate_card(struct snd_soc_card *card) 该接口里面的 soc_probe_link_dais(card); 这个接口内部会把msm-dai-fe.c 里面注册的snd_soc_dai_driver 里面注册的每个dai 都遍历一遍,并执行每个的 .probe = fe_dai_probe, 函数。

snd_soc_instantiate_card() -> soc_bind_dai_link() -> snd_soc_find_dai()

/* bind DAIs */

for_each_card_prelinks(card, i, dai_link) {  // 这里就是在编译所有的dai link,这里的dai link 就是在sa8155.c 里面注册的 dai link,里面包含了fe dai 、be dai,fe dai 会被创建pcm 节点,而be dai 则不会创建pcm 节点。

ret = soc_bind_dai_link(card, dai_link);

if (ret != 0)

goto probe_end;

}


// 这个就是在sa8155.c 里面定义的dai link
static struct snd_soc_dai_link msm_common_be_dai_links[] = {
	/* Backend DAI Links */
	{
		.name = LPASS_BE_PRI_TDM_RX_0,
		.stream_name = "Primary TDM0 Playback",
		.no_pcm = 1,
		.dpcm_playback = 1,
		.id = MSM_BACKEND_DAI_PRI_TDM_RX_0,
		.be_hw_params_fixup = msm_tdm_be_hw_params_fixup,
		.ops = &sa8155_tdm_be_ops,
		.ignore_suspend = 1,
		.ignore_pmdown_time = 1,
		SND_SOC_DAILINK_REG(pri_tdm_rx_0),
	},
}

这个是在msm-dai-q6-v2.c 里面注册的 snd_soc_dai_driver,这两就会去绑定,就是be dai,其实这里的类似跟 FE DAI 的创建是一样的,只不过fe dai 会去创建pcm 节点,跟里面的一个配置有关系。.no_pcm = 1, / .no_pcm = 0, 就是这个区别。

static struct snd_soc_dai_driver msm_dai_q6_tdm_dai[] = {
	{
		.playback = {
			.stream_name = "Primary TDM0 Playback",
			.aif_name = "PRI_TDM_RX_0",
			.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |
				SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_48000 |
				SNDRV_PCM_RATE_176400 | SNDRV_PCM_RATE_352800,
			.formats = SNDRV_PCM_FMTBIT_S16_LE |
				   SNDRV_PCM_FMTBIT_S24_LE |
				   SNDRV_PCM_FMTBIT_S32_LE,
			.channels_min = 1,
			.channels_max = 16,
			.rate_min = 8000,
			.rate_max = 352800,
		},
		.name = "PRI_TDM_RX_0",
		.ops = &msm_dai_q6_tdm_ops,
		.id = AFE_PORT_ID_PRIMARY_TDM_RX,
		.probe = msm_dai_q6_dai_tdm_probe,
		.remove = msm_dai_q6_dai_tdm_remove,
	},

dai widgets如和与其他widgets链接

这一块依赖于两个地方,一个地方是通过stream name去匹配,另一个是靠aif去指定。

在声卡初始化的时候,snd_soc_instantiate_card中调用snd_soc_dapm_link_dai_widgets函数来完成的。snd_soc_dapm_link_dai_widgets函数会去遍历每一个dai widgets,然后遍历所有的非dai widgets,如果非dai widgets的stream name与dai widgets的name相同,则把两个widgets进行链接。这也是为什么创建dai widgets时name一定要是stream name的原因之一了。

aif的绑定则是在dai dirver被probe时完成的,同样在snd_soc_instantiate_card中会调用soc_probe_link_dais函数,该函数会把每个dai driver注册时提供的probe都调用一遍,如果配置了aif的话这里就应该把aif与dai widgets进行链接。这里其实也是依赖于dai widgets的name必须为stream name,由于在probe函数中,因此这个函数属于dai driver,不应该去获取widgets的信息,所以其实这里链接的两个widgets的name一个是aif的name,另一个是stream name,其一般代码如下:

 snd_soc_instantiate_card() -> soc_probe_link_dais()


/* probe all DAI links on this card */   //这里其实就备注得很清楚。
	ret = soc_probe_link_dais(card);
	if (ret < 0) {
		dev_err(card->dev,
			"ASoC: failed to instantiate card %d\n", ret);
		goto probe_end;
	}



static int soc_probe_link_dais(struct snd_soc_card *card)
{
	struct snd_soc_dai *codec_dai;
	struct snd_soc_pcm_runtime *rtd;
	int i, order, ret;

	for_each_comp_order(order) {
		for_each_card_rtds(card, rtd) {

			dev_dbg(card->dev,
				"ASoC: probe %s dai link %d late %d\n",
				card->name, rtd->num, order);

			ret = soc_probe_dai(rtd->cpu_dai, order);   //实际执行的是 dai->driver->probe(dai)  这个就是我们在 注册 snd_soc_dai_driver 赋值的probe 函数。对应上面我们说提到的,dai driver 高通sa8155 平台上的就是在msm-dai-fe.c 里面的接口函数fe_dai_probe(),这个是FE dai  的probe 接口函数,而be dai 对应的接口函数是  msm_dai_q6_dai_tdm_probe() ,这个文件里面注册了很多的snd_soc_register_component()  组件,这里看区别就是BE dai涉及到的使用的协议不一样,所以使用的驱动是不一样的,这里我们这里举例的是使用的TDM,因为我手里的项目是TDM 的。
			if (ret)
				return ret;

			/* probe the CODEC DAI */
			for_each_rtd_codec_dai(rtd, i, codec_dai) {
				ret = soc_probe_dai(codec_dai, order);
				if (ret)
					return ret;
			}
		}
	}

	return 0;
}

// 这个是FE dai 触发的probe 函数
static int fe_dai_probe(struct snd_soc_dai *dai)
{
    // 1. 参数检查
    struct snd_soc_dapm_route intercon; // 声明一个局部变量来存储路由规则
    struct snd_soc_dapm_context *dapm;  // 指向 DAPM 上下文的指针

    if (!dai || !dai->driver) { // 检查传入的 DAI 指针和其驱动是否有效
        pr_err("%s invalid params\n", __func__); // 如果无效,打印错误日志
        return -EINVAL; // 返回错误码
    }

    // 2. 获取 DAPM 上下文
    dapm = snd_soc_component_get_dapm(dai->component); 
    // 获取与这个 DAI 所属的 Component 相关联的 DAPM 上下文。这是为了后面能向这个 Component 的 DAPM 图添加路由。

    // 3. 初始化路由变量
    memset(&intercon, 0, sizeof(intercon)); 
    // 将局部变量 intercon 清零,确保结构体初始状态干净。

    // 4. 处理 Playback (播放) 方向的 DAPM 路由
    if (dai->driver->playback.stream_name && // 检查 DAI 驱动是否定义了 Playback 流名称
        dai->driver->playback.aif_name) {    // 检查 DAI 驱动是否定义了 Playback 接口名称 (AIF Name)
        
        // 调试信息:记录正在为哪个 widget 添加路由
        dev_dbg(dai->dev, "%s add route for widget %s",
               __func__, dai->driver->playback.stream_name);

        // 设置路由规则 - 关键部分
        intercon.source = dai->driver->playback.stream_name; 
        // 路由的起点 (Source) 是 stream_name,通常是 "MultiMedia1 Playback" 这样的名字。
        // 在 DAPM 图中,这通常代表与 PCM 设备关联的虚拟 widget。
        
        intercon.sink = dai->driver->playback.aif_name;      
        // 路由的终点 (Sink) 是 aif_name,例如 "MM_DL1"。
        // 这是在 DAI 驱动中定义的接口名称,代表 DAI 的输出端口。

        // 调试信息:记录源和汇
        dev_dbg(dai->dev, "%s src %s sink %s\n",
               __func__, intercon.source, intercon.sink);

        // 添加路由到 DAPM 图
        snd_soc_dapm_add_routes(dapm, &intercon, 1); 
        // 将刚刚设置好的这条路由规则添加到第 2 步获取的 DAPM 上下文中。
        // 这会在 DAPM 图中创建一条从 "MultiMedia1 Playback" 到 "MM_DL1" 的连接。

        // 设置忽略挂起
        snd_soc_dapm_ignore_suspend(dapm, intercon.source); 
        // 告诉 DAPM 系统,即使系统进入挂起 (suspend) 状态,也不要自动关闭 "MultiMedia1 Playback" 这个 widget。
        // 因为前端 DAI 通常与应用层交互,应用层可能在后台保持连接。
    }

    // 5. 处理 Capture (录音) 方向的 DAPM 路由 - 逻辑与 Playback 类似
    if (dai->driver->capture.stream_name && // 检查 Capture 流名称
       dai->driver->capture.aif_name) {     // 检查 Capture 接口名称 (AIF Name)

        // 调试信息
        dev_dbg(dai->dev, "%s add route for widget %s",
               __func__, dai->driver->capture.stream_name);

        // 设置路由规则 - 注意 Source 和 Sink 是反过来的
        intercon.sink = dai->driver->capture.stream_name;  
        // Capture 方向:数据流向是 DAI -> PCM,所以在 DAPM 中,
        // AIF Name ("MM_UL1") 是 Source,Stream Name ("MultiMedia1 Capture") 是 Sink。
        
        intercon.source = dai->driver->capture.aif_name;   
        // Capture 的 AIF Name 作为 Source

        // 调试信息
        dev_dbg(dai->dev, "%s src %s sink %s\n",
               __func__, intercon.source, intercon.sink);

        // 添加路由
        snd_soc_dapm_add_routes(dapm, &intercon, 1); 

        // 设置忽略挂起 - 这次是 Sink (因为 Capture 流向反了)
        snd_soc_dapm_ignore_suspend(dapm, intercon.sink); 
    }

    // 6. 返回成功
    return 0; 
}

这里相当于就是创建了两条路由通路:

1、source = "MultiMedia1 Playback"sink = "MM_DL1"

2、source = "MM_UL1"sink = "MultiMedia1 Capture"

"MultiMedia1 Playback" -> "MM_DL1":

作用: 这条路由将代表用户空间 PCM 播放流的虚拟 widget ("MultiMedia1 Playback") 与代表 FE DAI 输出接口的虚拟 widget ("MM_DL1") 连接起来。

使用: 当应用程序开始播放时,"MultiMedia1 Playback" 变活跃。DAPM 在路径计算时,会通过这条路由知道信号流需要经过 "MM_DL1"。如果 "MM_DL1" 连接到后续的 "QUAT_TDM_RX_0 Audio Mixer" (根据你之前定义的 intercon 路由),那么 DAPM 就会知道需要打开混音器和相关的 BE DAI ("QUAT_TDM_RX_0")。

"MM_UL1" -> "MultiMedia1 Capture":

作用: 这条路由将代表 FE DAI 输入接口的虚拟 widget ("MM_UL1") 与代表用户空间 PCM 录音流的虚拟 widget ("MultiMedia1 Capture") 连接起来。

使用: 当应用程序开始录音时,"MultiMedia1 Capture" 变活跃。DAPM 在路径计算时,会反向追溯(因为它是 sink),通过这条路由找到 "MM_UL1"。如果之前有路由将硬件输入连接到 "MM_UL1" (例如 "TERT_TDM_TX_0" -> "MM_UL1"),那么 DAPM 就会知道需要打开相关的 BE DAI ("TERT_TDM_TX_0")。

"MM_UL1" 这里的widget 是在 Routing driver 里面被创建的,即msm-pcm-routing-auto.c 里面 被创建(我们这里就只举例了其中的一个)。

static const struct snd_soc_dapm_widget msm_qdsp6_widgets[] = {
	/* Frontend AIF */
	/* Widget name equals to Front-End DAI name<Need confirmation>,
	 * Stream name must contains substring of front-end dai name
	 */
	SND_SOC_DAPM_AIF_IN("MM_DL1", "MultiMedia1 Playback", 0, 0, 0, 0),
}

struct snd_soc_dapm_route {  //注意这是个route 的结构体定义。
	const char *sink;
	const char *control;
	const char *source;

	/* Note: currently only supported for links where source is a supply */
	int (*connected)(struct snd_soc_dapm_widget *source,
			 struct snd_soc_dapm_widget *sink);

	struct snd_soc_dobj dobj;
};

static const struct snd_soc_dapm_route intercon_tdm[] = {   // 可以理解为一个静态路由表
	{"QUAT_TDM_RX_0 Audio Mixer", "MultiMedia1", "MM_DL1"},  // 拆分:sink  = QUAT_TDM_RX_0 Audio Mixer(后端 ) control = MultiMedia1  source = MM_DL1(这个就是我们那个创建的widget 控件)
};

/* Not used but frame seems to require it */
static int msm_routing_probe(struct snd_soc_component *component)
{
	snd_soc_dapm_new_controls(&component->dapm, msm_qdsp6_widgets,ARRAY_SIZE(msm_qdsp6_widgets)); //这里是创建“MM_DL1”widget 控件
	snd_soc_dapm_add_routes(&component->dapm, intercon,ARRAY_SIZE(intercon));
	snd_soc_dapm_add_routes_tdm(component);  // 调用: snd_soc_dapm_add_routes(&component->dapm, intercon_tdm, ARRAY_SIZE(intercon_tdm));//这里就把静态路由表 添加到 dapm routers 中
}

这里如果不去创建,那前面就会找不到。对应的widget 控件, "MM_DL1" 可以看成一个交接点。

 

BE dai 触发的.probe 接口函数msm_dai_q6_dai_tdm_probe()  -> msm_dai_q6_dai_add_route()

static struct snd_soc_dai_driver msm_dai_q6_tdm_dai[] = {
{
	.playback = {
		.stream_name = "Quaternary TDM0 Playback",
		.aif_name = "QUAT_TDM_RX_0",
		.rates = SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_8000 |
			SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000 |
			SNDRV_PCM_RATE_176400 | SNDRV_PCM_RATE_352800,
		.formats = SNDRV_PCM_FMTBIT_S16_LE |
			   SNDRV_PCM_FMTBIT_S24_LE |
			   SNDRV_PCM_FMTBIT_S32_LE,
		.channels_min = 1,
		.channels_max = 16,
		.rate_min = 8000,
		.rate_max = 352800,
	},
	.name = "QUAT_TDM_RX_0",
	.ops = &msm_dai_q6_tdm_ops,
	.id = AFE_PORT_ID_QUATERNARY_TDM_RX,
	.probe = msm_dai_q6_dai_tdm_probe,
	.remove = msm_dai_q6_dai_tdm_remove,
	},
};

static int msm_dai_q6_dai_add_route(struct snd_soc_dai *dai)
{
	struct snd_soc_dapm_route intercon;
	struct snd_soc_dapm_context *dapm;

	if (!dai) {
		pr_err("%s: Invalid params dai\n", __func__);
		return -EINVAL;
	}
	if (!dai->driver) {
		pr_err("%s: Invalid params dai driver\n", __func__);
		return -EINVAL;
	}
	dapm = snd_soc_component_get_dapm(dai->component);
	memset(&intercon, 0, sizeof(intercon));
	if (dai->driver->playback.stream_name &&
		dai->driver->playback.aif_name) {
		dev_dbg(dai->dev, "%s: add route for widget %s",
				__func__, dai->driver->playback.stream_name);
		intercon.source = dai->driver->playback.aif_name;
		intercon.sink = dai->driver->playback.stream_name;
		dev_dbg(dai->dev, "%s: src %s sink %s\n",
				__func__, intercon.source, intercon.sink);
		snd_soc_dapm_add_routes(dapm, &intercon, 1);
		snd_soc_dapm_ignore_suspend(dapm, intercon.sink);
	}
	if (dai->driver->capture.stream_name &&
		dai->driver->capture.aif_name) {
		dev_dbg(dai->dev, "%s: add route for widget %s",
				__func__, dai->driver->capture.stream_name);
		intercon.sink = dai->driver->capture.aif_name;
		intercon.source = dai->driver->capture.stream_name;
		dev_dbg(dai->dev, "%s: src %s sink %s\n",
				__func__, intercon.source, intercon.sink);
		snd_soc_dapm_add_routes(dapm, &intercon, 1);
		snd_soc_dapm_ignore_suspend(dapm, intercon.source);
	}
	return 0;
}
这里跟上面BE dai 的probe 接口一下,添加dapm route

播放:"QUAT_TDM_RX_0---> "Quaternary TDM0 Playback

录制:"Quaternary TDM0  Capture---> "QUAT_TDM_RX_0

 

再看在msm-pcm-routing-auto.c 里面定义的静态通路。

static const struct snd_soc_dapm_route intercon_tdm[] = {
	{"QUAT_TDM_RX_0 Audio Mixer", "MultiMedia1", "MM_DL1"},   //从右往左读
	{"QUAT_TDM_RX_0", NULL, "QUAT_TDM_RX_0 Audio Mixer"},
}

static const struct snd_soc_dapm_widget msm_qdsp6_widgets[] = {
	/* Frontend AIF */
	/* Widget name equals to Front-End DAI name<Need confirmation>,
	 * Stream name must contains substring of front-end dai name
	 */
	SND_SOC_DAPM_AIF_IN("MM_DL1", "MultiMedia1 Playback", 0, 0, 0, 0),
	
}
"QUAT_TDM_RX_0 Audio Mixer" 是定义的一个 Mixer 控件,是一个widgits
static const struct snd_soc_dapm_widget msm_qdsp6_widgets_tdm[] = {
	SND_SOC_DAPM_AIF_OUT("QUAT_TDM_RX_0", "Quaternary TDM0 Playback",
				0, 0, 0, 0),
	SND_SOC_DAPM_MIXER("QUAT_TDM_RX_0 Audio Mixer", SND_SOC_NOPM, 0, 0,
				quat_tdm_rx_0_mixer_controls,
				ARRAY_SIZE(quat_tdm_rx_0_mixer_controls)),
}

因为在 {"QUAT_TDM_RX_0 Audio Mixer", "MultiMedia1", "MM_DL1"},  使用了一个 "MultiMedia1" 它是一kcontrol,所以需要注册一个snd_kcontrol_new。
static const struct snd_kcontrol_new quat_tdm_rx_0_mixer_controls[] = {
	SOC_DOUBLE_EXT("MultiMedia1", SND_SOC_NOPM,
	MSM_BACKEND_DAI_QUAT_TDM_RX_0,
	MSM_FRONTEND_DAI_MULTIMEDIA1, 1, 0, msm_routing_get_audio_mixer,
	msm_routing_put_audio_mixer),
};
这些定义的widgets 也会通过上面的msm_routing_probe() 进行注册。

再讲解一下这个DAPM 的widget

struct snd_kcontrol_new {
	snd_ctl_elem_iface_t iface;	/* interface identifier */
	unsigned int device;		/* device/client number */
	unsigned int subdevice;		/* subdevice (substream) number */
	const unsigned char *name;	/* ASCII name of item */
	unsigned int index;		/* index of item */
	unsigned int access;		/* access rights */
	unsigned int count;		/* count of same elements */
	snd_kcontrol_info_t *info;
	snd_kcontrol_get_t *get;
	snd_kcontrol_put_t *put;
	union {
		snd_kcontrol_tlv_rw_t *c;
		const unsigned int *p;
	} tlv;
	unsigned long private_value;
};


static const struct snd_kcontrol_new quat_tdm_rx_0_mixer_controls[] = {
	SOC_DOUBLE_EXT("MultiMedia1", SND_SOC_NOPM,
	MSM_BACKEND_DAI_QUAT_TDM_RX_0,
	MSM_FRONTEND_DAI_MULTIMEDIA1, 1, 0, msm_routing_get_audio_mixer,
	msm_routing_put_audio_mixer),
	……………………….后面还定义了很多个MultiMedia
};

#define SOC_DOUBLE_VALUE(xreg, shift_left, shift_right, xmax, xinvert, xautodisable) \
	((unsigned long)&(struct soc_mixer_control) \
	{.reg = xreg, .rreg = xreg, .shift = shift_left, \
	.rshift = shift_right, .max = xmax, .platform_max = xmax, \
	.invert = xinvert, .autodisable = xautodisable})


#define SOC_DOUBLE_EXT(xname, reg, shift_left, shift_right, max, invert,\
	 xhandler_get, xhandler_put) \
{	.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = (xname),\
	.info = snd_soc_info_volsw, \
	.get = xhandler_get, .put = xhandler_put, \
	.private_value = \
		SOC_DOUBLE_VALUE(reg, shift_left, shift_right, max, invert, 0) }
这两个宏定义就是将SOC_DOUBLE_EXT 宏定义给展开,然后将我们写入的值给赋值到这个snd_kcontrol_new 结构体内部,做好创建kctl 的准备填充。
static const struct snd_soc_dapm_widget msm_qdsp6_widgets_tdm[] = {
	SND_SOC_DAPM_AIF_OUT("QUAT_TDM_RX_0", "Quaternary TDM0 Playback",
				0, 0, 0, 0),
	SND_SOC_DAPM_MIXER("QUAT_TDM_RX_0 Audio Mixer", SND_SOC_NOPM, 0, 0,
				quat_tdm_rx_0_mixer_controls,
				ARRAY_SIZE(quat_tdm_rx_0_mixer_controls)),
}

#define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert, \
	 wcontrols, wncontrols)\
{	.id = snd_soc_dapm_mixer, .name = wname, \
	SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \
	.kcontrol_news = wcontrols, .num_kcontrols = wncontrols
}

这里就是创建一个 DAPM mixer widget 的控件,它可以是一对一,可以是一对多,这里就是一对多的关系。

.kcontrol_news 的类型其实就是 snd_kcontrol_new 结构体。

而后会调用snd_soc_dapm_new_controls 创建  snd_kcontrol  创建一个kctl 控件,用于用户使用,而后里面的,而.put 被赋值成了msm_routing_put_audio_mixer()   .get 被赋值成了msm_routing_get_audio_mixer(),Private Data: 控件的私有数据 (private_value) 被设置为包含 arg1 (MSM_BACKEND_DAI_QUAT_TDM_RX_0) 和 arg2 (MSM_FRONTEND_DAI_MULTIMEDIA1) 的信息。

static void snd_soc_dapm_new_controls_tdm(struct snd_soc_component *component)
{
        snd_soc_dapm_new_controls(&component->dapm,
                                msm_qdsp6_widgets_tdm,
                                ARRAY_SIZE(msm_qdsp6_widgets_tdm));
}

由于在注册SND_SOC_DAPM_MIXER 的时候我们添加的是一个数组即quat_tdm_rx_0_mixer_controls,所以说就会注册size(quat_tdm_rx_0_mixer_controls) kctl,供用户使用。

"MultiMedia1"-------> "QUAT_TDM_RX_0 Audio Mixer"

"MultiMedia2"------->

"MultiMedia3"------->

OK 我们梳理一下这个通路的建立。

在FE DAI 里面的 会触发fe_dai_probe() 接口,会把我们注册的snd_soc_dai_driver里面的  .stream_name = "MultiMedia1 Playback" .aif_name = "MM_DL1",

给链接起来,组成一个路由通路,source 是 "MultiMedia1 Playback"它代表一个pcm 数据流,而"MM_DL1" 是一个 sink,流向是"MultiMedia1 Playback" ---> "MM_DL1" ,而后在BE dai 里面有定义snd_soc_dai_driver 里面的  .stream_name = "Quaternary TDM0 Playback"    .aif_name = "QUAT_TDM_RX_0",  在BE DAI 触发的probe 函数里面 playbacke 流向是 source 是"QUAT_TDM_RX_0" sink 是"Quaternary TDM0 Playback" 。那么到此我们两端的FE DAI、BE DAI 的dapm route 就建立完成了,然后我们在 msm-pcm-routing-auto.c 里面定义的静态通路。

static const struct snd_soc_dapm_route intercon_tdm[] = {
	{"QUAT_TDM_RX_0 Audio Mixer", "MultiMedia1", "MM_DL1"},
	{"QUAT_TDM_RX_0", NULL, "QUAT_TDM_RX_0 Audio Mixer"},
}

下面这三都去创建了widgits ,不然在链接的时候找不到。

SND_SOC_DAPM_AIF_IN("MM_DL1", "MultiMedia1 Playback", 0, 0, 0, 0),
SND_SOC_DAPM_AIF_OUT("QUAT_TDM_RX_0", "Quaternary TDM0 Playback",0, 0, 0, 0),
SND_SOC_DAPM_MIXER("QUAT_TDM_RX_0 Audio Mixer", SND_SOC_NOPM, 0, 0,quat_tdm_rx_0_mixer_controls,ARRAY_SIZE(quat_tdm_rx_0_mixer_controls))

注意 由于intercon_tdm[]  里面我们注册的静态路由表, {"QUAT_TDM_RX_0 Audio Mixer", "MultiMedia1", "MM_DL1"}, 由于在 这个路由表中,添加了const char *control; 即  "MultiMedia1" ,所以这条route 要工作,需要MultiMedia1 处于active 的状态,所以我们需要将这个“MultiMedia1 ”置为ON ,上面讲过这是一个kctl,所以可通过mix set 进行设置,而后就处理active状态,后由于 {"QUAT_TDM_RX_0", NULL, "QUAT_TDM_RX_0 Audio Mixer"}, 中没有控制widgit,所以处于常开的状态,所以我们在上层只需要把MultiMedia1 mix set 1,后再打开pcm流,然后整体的通路就建立成功了。

所以 这样就链接了一条通路了:

(FE dai)"MultiMedia1 Playback" --->(Routing) "MM_DL1"--->(MultiMedia1  ON/OFF)--->"QUAT_TDM_RX_0 Audio Mixer"--->"QUAT_TDM_RX_0"--->(be dai)"Quaternary TDM0 Playback"

DPCM 工作机制

说DPCM的话,就先从dai link说起,dai link中有两个元素dynamic、no_pcm。

先说no_pcm

这个成员好理解,就是说这个dai link不创建对应的pcm逻辑设备。为什么有这个需求?我觉得这就跟DPCM的机制有关,因为在DPCM中原来的音频输入流和输出流中间增加了一个switch,所以就被切断了,那么就把本来应该是CPU <----> CODEC这样的链接变成了CPU <----> SWITCH <----> CODEC这样的链接,但是又为了维护框架的一致性,所以每个链接中的<---->还是采取dai link的模式进行链接,这样原本的1个dai link就变成了2个dai link,一个dai link叫做fe dai link,一个dai link叫做be dai link。但是这里会有一个问题,本来一个dai link对应的就是一个音频逻辑设备,CPU <----> CODEC这样一个链接会自动创建出一个pcm逻辑设备,而现在变成了2个dai link,但是实际的音频流并没有发生变化,那如果还按照之前的方式则将创建出2个pcm设备,这就乱了……我的音频流到底该用哪个pcm设备输入呢?所以为了避免这样的现象,几乎所有的be dai link中的no_pcm全部都是1,也就是be不创建逻辑设备。其实我觉得这也是为什么区分be与fe的原因了,fe其实就是音频输入,所以fe都会创建对应的逻辑设备。这里提前提一句,关于为什么会有dpcm_fe_dai_open()与soc_pcm_open()的选择,其实跟be设备没有pcm逻辑设备是有直接原因的。

在来说dynamic

dynamic成员对整个音频框架在逻辑上真正起作用的地方其实只有两个(还有几个地方不太影响整体逻辑):一个是soc-dapm.c中的snd_soc_dapm_connect_dai_link_widgets函数(这个函数走读了一下,这个接口就是用于连接CPU be dai 与 CODEC 的BE dai,而在目前我的高通sa8155 平台上面是没有这种连接的,因为音频数据流是在音频数据流在 ADSP 内部的路由和连接是由 ADSP 固件和相关驱动管理的 ),另一个是soc-pcm.c中的soc_new_pcm函数。

第一个地方的作用是区别dynamic dai link和非dynamic dai link在创建声卡时dai widgets的链接行为,对于非dynamic dai link在创建声卡时就把dai widgets链接好,而dynamic dai link是在该dai link对应的pcm逻辑设备被打开的时候才会去进行dai widgets的链接。其实这里就是dynamic和非dynamic最核心的区别的体现。

第二个地方是对整个dynamic和非dynamic最核心的区别的实现。其实soc_new_pcm函数最关键的就是绑定rtd->ops了,如果是dynamic的话就绑定dynamic的ops,如果是非dynamic的,则绑定普通的ops。

下面我们分析一下Dynamic link dai 绑定的  ops ,上面我们也分析过一部分,最终Dynamic link dai 绑定的ops 执行函数最终也会去调用我们普通的ops 的函数接口,就是非 Dynamic 的绑定的 rtd 的函数,其实多余的一部分就是dapm 的一些处理.

OPEN 接口

static int dpcm_fe_dai_open(struct snd_pcm_substream *fe_substream)
{
	…………..
	ret = dpcm_path_get(fe, stream, &list); //基于当前的 DAPM 图状态,找出所有从这个 FE 可能通往 BE 的路径。
	……………
	/* calculate valid and active FE <-> BE dpcms */
	dpcm_process_paths(fe, stream, &list, 1);

	ret = dpcm_fe_dai_startup(fe_substream); //打开fe dai 里面会调用 注册的ops.startup  Wimbledon之前也提到过。
	if (ret < 0) {
		/* clean up all links */
		for_each_dpcm_be(fe, stream, dpcm)
			dpcm->state = SND_SOC_DPCM_LINK_STATE_FREE;

		dpcm_be_disconnect(fe, stream);
		fe->dpcm[stream].runtime = NULL;
	}
 
	dpcm_clear_pending_state(fe, stream); 
	dpcm_path_put(&list);
	mutex_unlock(&fe->card->mutex);
	return ret;
}

这个函数理解起来比较简单,就是要注意一点,这个其实前面在分析no_pcm的时候已经说了,只有fe dai link才会创建pcm逻辑设备,也只有创建了pcm逻辑设备的dai link(对应到这里应该是stream)才会被调用rtd->ops->open(),所以这里的dpcm_process_paths(fe, stream, &list, 1);

*************************************************

struct snd_pcm_substream {
	struct snd_pcm *pcm;
	struct snd_pcm_str *pstr;
	void *private_data;		/* copied from pcm->private_data */
	int number;
	char name[32];			/* substream name */
	int stream;			/* stream (direction) */
	struct pm_qos_request latency_pm_qos_req; /* pm_qos request */
	size_t buffer_bytes_max;	/* limit ring buffer size */
	struct snd_dma_buffer dma_buffer;
	size_t dma_max;
	/* -- hardware operations -- */
	const struct snd_pcm_ops *ops;
	/* -- runtime information -- */
	struct snd_pcm_runtime *runtime;
        /* -- timer section -- */
	struct snd_timer *timer;		/* timer */
	unsigned timer_running: 1;	/* time is running */
	long wait_time;	/* time in ms for R/W to wait for avail */
	/* -- next substream -- */
	struct snd_pcm_substream *next;
	/* -- linked substreams -- */
	struct list_head link_list;	/* linked list member */
	struct snd_pcm_group self_group;	/* fake group for non linked substream (with substream lock inside) */
	struct snd_pcm_group *group;		/* pointer to current group */
	/* -- assigned files -- */
	int ref_count;
	atomic_t mmap_count;
	unsigned int f_flags;
	void (*pcm_release)(struct snd_pcm_substream *);
	struct pid *pid;
    ...............
}

snd_pcm_substream  可以理解为内核中表示一个具体音频数据的抽象,在alsa 中每个dai link 都会被创建一个snd_pcm_substream(这里包括 FE dai  和 BE dai ),其代表一个单向的音频流,在该结构体中,还创建了一个重要的结构体 struct snd_pcm_runtime *runtime; 它表示一个音频流在运行时的数据,它存储了该流的所有的动态的配置信息,如:runtime->hw_params, runtime->sw_params 等。这个结构体会 在 _snd_pcm_new () 内部调用snd_pcm_new_stream被创建,Playback\Capture 都会被创建。

****************************************

static int dpcm_fe_dai_startup(struct snd_pcm_substream *fe_substream)
{
	struct snd_soc_pcm_runtime *fe = fe_substream->private_data;
	struct snd_pcm_runtime *runtime = fe_substream->runtime;
	int stream = fe_substream->stream, ret = 0;

	dpcm_set_fe_update_state(fe, stream, SND_SOC_DPCM_UPDATE_FE);

	ret = dpcm_be_dai_startup(fe, fe_substream->stream); //这个接口里面去获取了 BE dai 的substream,获取之后也通过soc_pcm_open() 去open BE dai 相关的操作,也会去执行.startup 回调,如果实现了对应的ops。
	if (ret < 0) {
		dev_err(fe->dev,"ASoC: failed to start some BEs %d\n", ret);
		goto be_err;
	}

	dev_dbg(fe->dev, "ASoC: open FE %s\n", fe->dai_link->name);

	/* start the DAI frontend */
	ret = soc_pcm_open(fe_substream);  // 这里是直接去操作 FE dai 的Substream,里面也是去执行 Open 需要的相关操作。
	if (ret < 0) {
		dev_err(fe->dev,"ASoC: failed to start FE %d\n", ret);
		goto unwind;
	}

	fe->dpcm[stream].state = SND_SOC_DPCM_STATE_OPEN; //设置当前的状态为 open

	dpcm_set_fe_runtime(fe_substream);  //设置dia driver 中的参数设置到 runtime ,计算整个可能的链路的所能支持的硬件参数
	snd_pcm_limit_hw_rates(runtime);

	ret = dpcm_apply_symmetry(fe_substream, stream);
	if (ret < 0) {
		dev_err(fe->dev, "ASoC: failed to apply dpcm symmetry %d\n",msm
			ret);
		goto unwind;
	}

	dpcm_set_fe_update_state(fe, stream, SND_SOC_DPCM_UPDATE_NO);return 0;

unwind:
	dpcm_be_dai_startup_unwind(fe, fe_substream->stream);
be_err:
	dpcm_set_fe_update_state(fe, stream, SND_SOC_DPCM_UPDATE_NO);
	return ret;
}

这里就又有一个地方可以理解了,从框架上来说,每个pcm设备都需要open之后才能使用,而由于be没有创建pcm逻辑设备,所以be的open操作得由fe来间接执行dpcm_be_dai_startup(),以保证框架的一致性。

Trigger

trigger  这里我们之前也讲过了,这里我们讲一个trigger 触发的时序上的一个设置,即先触发FE dai 时候BE dai,这里可在我们定义be dai/fe dai 的时候可进行配置定义,在sa8155 平台的sa8155.c 里面的snd_soc_dai_link的配置。

static struct snd_soc_dai_link msm_common_dai_links[] = {
	/* FrontEnd DAI Links */
	{
		.name = MSM_DAILINK_NAME(Media1),
		.stream_name = "MultiMedia1",
		.dynamic = 1,
#if IS_ENABLED(CONFIG_AUDIO_QGKI)
		.async_ops = ASYNC_DPCM_SND_SOC_PREPARE,
#endif /* CONFIG_AUDIO_QGKI */
		.dpcm_playback = 1,
		.dpcm_capture = 1,
		.trigger = {SND_SOC_DPCM_TRIGGER_POST,
			SND_SOC_DPCM_TRIGGER_POST},
		.ignore_suspend = 1,
		/* this dainlink has playback support */
		.ignore_pmdown_time = 1,
		.id = MSM_FRONTEND_DAI_MULTIMEDIA1,
		SND_SOC_DAILINK_REG(multimedia1),
	},
};

就是这个.trigger 的配置,它是一个数组分别是 Playback、Capture 的配置,在我目前的板载配置里,都是配置的是{SND_SOC_DPCM_TRIGGER_POST。即:

启动时 (START/RESUME/...): 先触发 FE (MultiMedia1),再触发 BE (连接到它的后端)。

停止时 (STOP/SUSPEND/...): 先触发 BE (后端),再触发 FE (MultiMedia1)。

static int dpcm_fe_dai_do_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_soc_pcm_runtime *fe = substream->private_data;
	int stream = substream->stream;
	int ret = 0;
	enum snd_soc_dpcm_trigger trigger = fe->dai_link->trigger[stream];

	fe->dpcm[stream].runtime_update = SND_SOC_DPCM_UPDATE_FE;

	switch (trigger) {
	case SND_SOC_DPCM_TRIGGER_PRE:
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
		case SNDRV_PCM_TRIGGER_DRAIN:
			ret = dpcm_dai_trigger_fe_be(substream, cmd, true);
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			ret = dpcm_dai_trigger_fe_be(substream, cmd, false);
			break;
		default:
			ret = -EINVAL;
			break;
		}
		break;
	case SND_SOC_DPCM_TRIGGER_POST:
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
		case SNDRV_PCM_TRIGGER_DRAIN:
			ret = dpcm_dai_trigger_fe_be(substream, cmd, false);
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			ret = dpcm_dai_trigger_fe_be(substream, cmd, true);
			break;
		default:
			ret = -EINVAL;
			break;
		}
		break;
……………………………..
};

static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream,
				  int cmd, bool fe_first)
{
	struct snd_soc_pcm_runtime *fe = substream->private_data;
	int ret;

	/* call trigger on the frontend before the backend. */
	if (fe_first) { //根据上面传入的true or  false 进行执行。最终都是调用的soc_pcm_trigger 进行触发设置。
		dev_dbg(fe->dev, "ASoC: pre trigger FE %s cmd %d\n",
			fe->dai_link->name, cmd);

		ret = soc_pcm_trigger(substream, cmd); //先触发FE dai 的trigger 
		if (ret < 0)
			return ret;

		ret = dpcm_be_dai_trigger(fe, substream->stream, cmd); // 再触发BE dai 的trigger 
		return ret;
	}

	/* call trigger on the frontend after the backend. */
	ret = dpcm_be_dai_trigger(fe, substream->stream, cmd);
	if (ret < 0)
		return ret;

	dev_dbg(fe->dev, "ASoC: post trigger FE %s cmd %d\n",
		fe->dai_link->name, cmd);

	ret = soc_pcm_trigger(substream, cmd);

	return ret;
}

PRE 和 POST 触发模式是为了精确控制音频子系统中不同组件的启停时序,从而:
1、防止数据丢失或截断。
2、确保硬件在接收或发送数据前/后处于正确的状态。
3、实现稳定、可靠的音频数据流传输。

PCM hw_params

rtd->dai_link->dynamic 的回调函数是  dapm_fe_dai_hw_params()

dynamic 的回调函数是 soc_pcm_hw_params()

其实最终都是调用的soc_pcm_hw_params()

pcm hw parameter是对音频系统使用的硬件参数的记录,目前看在高通sa8155 平台上,dai link 的ops ,dai driver 的ops 以及component 的ops 里面都有hw_params 的回调接口函数,并真正的执行操作了,dai link 的ops->hw_params = sa8155_tdm_snd_hw_params ; 主要设置TDM 的相关的配置参数,确定并记录当前通路的tdm 配置。 Dai driver 的 ops->hw_params        = msm_dai_q6_tdm_hw_params,  也是TDM slot 的maping 的相关配置记录如:tdm_group、tdm 、slot_mapping 等。其次就是 component driver   ops->hw_params = msm_pcm_routing_hw_params;记录当前 substream 的 rate、channels 、format 参数到msm_bedais[]中。

hw parameter什么时候配置,谁来配置:

这个问题其实前面已经说了,是由需要播放音频流的上层应用程序来配置,配置方式就是通过pcm逻辑设备的ioctrl接口,发送0x10/0x11命令来进行配置。在打开音频流后上层应用通过此接口通知音频driver该使用什么硬件参数。这一点从前面章节图中的打印信息可以看出,android的音频框架在open了pcm设备后立即发送0x1命令查询driver info,然后就发送了0x11命令,来设置hw parameter。各driver的hw_params调用过程:

ioctrl cmd:0x11->snd_pcm_ioctl_hw_params_compat->snd_pcm_hw_params->(substream->ops->hw_params)->dpcm_fe_dai_hw_params->fe/be:soc_pcm_hw_params->各driver的hw_params函数。

static int soc_pcm_hw_params(struct snd_pcm_substream *substream,
				struct snd_pcm_hw_params *params)
{
    .........
	if (rtd->dai_link->ops->hw_params) {
		ret = rtd->dai_link->ops->hw_params(substream, params);  //执行dai link 的 hw_params 
    ............
		}
	}
    ..................

	ret = snd_soc_dai_hw_params(cpu_dai, substream, params);  // 执行了dai link 的 .be_hw_params_fixup 以及 dai driver  dai->driver->ops->hw_params .
    ...............
	for_each_rtdcom(rtd, rtdcom) {
		component = rtdcom->component;

		ret = snd_soc_component_hw_params(component, substream, params); // 执行 component driver 的ops->hw_params.
        ..........
	}
    ..........................
	return ret;
}

hw param与hw rule之间的关系:

这里抽象一点说,具体怎么操作的后面详细记录。所谓rule,就是对hw parameter的约束,比如某个dai只支持16bit传输,结果现在上面来了一个stream是24bit的,这个时候上层应用程序通过0x11命令发送hw parameter告诉driver有一个24bit的流要来了,这个时候driver回去check自己能不能处理24bit的stream,如果能,就把自己设置为24bit模式,如果不行则配置不生效。所以,rule的添加都是音频驱动中各个driver在open的时候自己添加到该stream的rules中的,比如dai driver会把自己的rule在open的时候添加到stream中,platform driver也会添加,总之只要需要对stream有约束的driver都可以在open的时候把自己的约束添加给stream的rules中,这样,当上层应用给该stream配置hw parameter时,stream对照着自己的rules一条一条检查合法性就行了。

struct snd_interval {
	unsigned int min, max;
	unsigned int openmin:1,
		     openmax:1,
		     integer:1,
		     empty:1;
};

#define SNDRV_MASK_MAX	256

struct snd_mask {
	__u32 bits[(SNDRV_MASK_MAX+31)/32];
};

struct snd_pcm_hw_params {
	unsigned int flags;
	struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK -
			       SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
	struct snd_mask mres[5];	/* reserved masks */
	struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
				        SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
	struct snd_interval ires[9];	/* reserved intervals */
	unsigned int rmask;		/* W: requested masks */
	unsigned int cmask;		/* R: changed masks */
	unsigned int info;		/* R: Info flags for returned setup */
	unsigned int msbits;		/* R: used most significant bits */
	unsigned int rate_num;		/* R: rate numerator */
	unsigned int rate_den;		/* R: rate denominator */
	snd_pcm_uframes_t fifo_size;	/* R: chip FIFO size in frames */
	unsigned char reserved[64];	/* reserved for future */
};

通过源码来看,这里应该是把hw parameter分为了两类,一类是离散值类型,类是范围内连续值类型。简单来说就是第一类里面的值必须与rule规定的具体值相等才算有效,第二类只要在rule的范围类就算有效,两种类型的数据分别用masks和intervals来表示。

由于intervals比较明了,就不记录,这里主要记录一下masks。

首先每个masks和intervals数组长度的定义,其实就是每个该类型的参数占一个数组元素,例如mask类型的parameter总共有3个,那么这里就是masks[3],这块具体可见宏定义。

其次每个mask都是一个snd_mask类型,snd_mask类型其实就是一个数组,数组的长度为8个32bit元素。这里这样定义的目的其实是因为#define SNDRV_MASK_MAX 256这个宏,这个宏的意义是说每个parameter的取值范围为0~255,本来0~255范围的值用一个8bit的数据就可以表示了,但是由于一个hw parameter可能可以同时存在多个值(例如format,一个接口可以支持16bit,24bit,32bit等等),所以要用mask来进行表示,那么0~255取值范围的mask就需要用256bit来表示,所以这里的snd_mask为一个8个元素,每个元素32bit的数组,这样就正好可以表示一个取值范围为0~255的parameter的mask。

所以,在设置parameter的值时操作函数如下:

Sa8155.c
static inline int param_is_mask(int p)
{
	return (p >= SNDRV_PCM_HW_PARAM_FIRST_MASK) &&
			(p <= SNDRV_PCM_HW_PARAM_LAST_MASK);
}

static inline struct snd_mask *param_to_mask(struct snd_pcm_hw_params *p,
					     int n)
{
	return &(p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK]);
}

static void param_set_mask(struct snd_pcm_hw_params *p, int n,
			   unsigned int bit)
{
	if (bit >= SNDRV_MASK_MAX)
		return;
	if (param_is_mask(n)) {
		struct snd_mask *m = param_to_mask(p, n);

		m->bits[0] = 0;
		m->bits[1] = 0;
		m->bits[bit >> 5] |= (1 << (bit & 31));
	}
}

在上面提到在 soc_pcm_hw_params() 接口内部会调用dai_link->be_hw_params_fixup(); be_hw_params_fixup  ->  /msm_tdm_be_hw_params_fixup()/  msm_be_hw_params_fixup()    来设置硬件的BE dai 端口的驱动能力,设置固定的值,通过这种方式,驱动程序精确地向 ALSA 框架(进而向用户空间应用)声明了该后端 DAI 接口的能力,即它只支持一组非常具体的硬件配置。而在高通的sa8155 平台,在sa8155.c 里面都有对应对应的TDM 后端进行 default config 配置,就是每个不同的TDM 配置的硬件的参数配置是根据(snd_soc_pcm_runtime *rtd),   cup_dai 传入的id (cpu_dai->id)来进行配置。

在sa8155 平台上,它有5组 PCM/TDM 的硬件端口,每个端口的驱动能力是不一样的。

struct dev_config {
	u32 sample_rate;
	u32 bit_format;
	u32 channels;
};
/* TDM default config */
static struct dev_config tdm_rx_cfg[TDM_INTERFACE_MAX][TDM_PORT_MAX] = {
	………………..
	{ /* QUAT TDM */
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 20}, /* RX_0 */ /*Media, Tuner, Dolby ch 0-19*/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 8}, /* RX_1 */ /*Navi,TTS,KTV,Chime_TTS*/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1}, /* RX_2 */ /*Phone*/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1}, /* RX_3 */ /*Ringtone*/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_4 */ /**/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2}, /* RX_5 */ /*Notify, Boot_Music*/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_6 */ /**/
		{SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 0}, /* RX_7 */ /**/
	},
	……………………….
	};

再来说一下这个.be_hw_params_fixup 这个是在 dai_link 定义的回调函数,在.hw_params 设置中会回调该函数。说一下为啥要调用它。

在现代移动平台基本上都是有dsp的,就像一开始那个图里面描述的那样,那么在be端,最终的一些输出在设计上可能希望是一个约定好的固定值,比如dsp输出的音频数据流始终是48000hz采样率,那么这个时候就需要在设置完hw params之后强行把be的hw param刷成dsp输出的实际参数,这个时候就用到了fixup。那么这里为什么不在rule里面做,用rule来拦截不满足要求的设置呢?我觉得原因可能在于dsp的输入其实是可以接受非48000hz采样率的数据的,然后dsp内部的程序会把输入的各种各样的采样率统一转换成一个固定采样率作为输出。而用ioctrl进行hw params的设置时其实是对fe进行的设置,be的hw params是从把fe的hw params memcpy过去的,所以如果没有fixup,则be的参数等价于fe的参数,但是如果dsp的输出端如果希望与输入端有不同的参数,则只能通过fixup来实现。

逻辑上,dsp的输出端的hw params是设备制造商自己来约定的,比如设备制造商希望dsp输出到codec的采样率统一为48000hz,所以fixup函数应该由mechine driver来提供,这就是为什么在msm8996平台上be_hw_params_fixup函数都在sa8155.c 文件中了。

补充一句,be_hw_params_fixup是在dai link结构体中,是在mechine driver定义dai link时进行赋值的。

Rule 相关记录
涉及到的文件以及接口函数:
	 pcm_native.c : snd_pcm_hw_refine、snd_pcm_hw_constraints_init、snd_pcm_hw_constraints_complete
 pcm_lib.c : snd_pcm_hw_rule_add

struct snd_pcm_hw_rule;
typedef int (*snd_pcm_hw_rule_func_t)(struct snd_pcm_hw_params *params,
				      struct snd_pcm_hw_rule *rule);

struct snd_pcm_hw_rule {
	unsigned int cond;
	int var;
	int deps[4];

	snd_pcm_hw_rule_func_t func;
	void *private;
};

struct snd_pcm_hw_constraints {
	struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK -
			 SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
	struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
			     SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
	unsigned int rules_num;
	unsigned int rules_all;
	struct snd_pcm_hw_rule *rules;
};



int snd_pcm_hw_rule_add(struct snd_pcm_runtime *runtime, unsigned int cond,
			int var,
			snd_pcm_hw_rule_func_t func, void *private,
			int dep, ...)
{
	struct snd_pcm_hw_constraints *constrs = &runtime->hw_constraints;
	struct snd_pcm_hw_rule *c;
	unsigned int k;
	va_list args;
	va_start(args, dep);
	if (constrs->rules_num >= constrs->rules_all) {
		struct snd_pcm_hw_rule *new;
		unsigned int new_rules = constrs->rules_all + 16;
		new = krealloc(constrs->rules, new_rules * sizeof(*c),
			       GFP_KERNEL);
		if (!new) {
			va_end(args);
			return -ENOMEM;
		}
		constrs->rules = new;
		constrs->rules_all = new_rules;
	}
	c = &constrs->rules[constrs->rules_num];
	c->cond = cond;
	c->func = func;
	c->var = var;
	c->private = private;
	k = 0;
	while (1) {
		if (snd_BUG_ON(k >= ARRAY_SIZE(c->deps))) {
			va_end(args);
			return -EINVAL;
		}
		c->deps[k++] = dep;
		if (dep < 0)
			break;
		dep = va_arg(args, int);
	}
	constrs->rules_num++;
	va_end(args);
	return 0;
}

rule的作用在前面已经分析了,这里主要分析下rule究竟是如何达到这个目的的。

首先,rule的添加

前面已经说过,rule是在pcm逻辑设备open时添加的,而且是由各个部分的dirver自己添加自己的rule,具体添加的接口是snd_pcm_hw_rule_add函数,所有的rule都是通过这个函数最终被添加的。

然而除了各个driver添加自己特性的rule之外,还有一个地方会添加rule,那就是在open pcm的一开始会调用snd_pcm_hw_constraints_init函数,他会针对所有hw param创建一个最基本,或者说最宽松的rule。那么这里就出现了一个问题,同一个hw param可能会被多次创建rule,对于这个东西的处理是在snd_pcm_hw_refine中进行的,这里留到后面来分析。

其次,rule添加到哪里

snd_pcm_hw_rule_add函数中有一个参数:runtime,这个东西是与substream对应的,其实就是substream的运行时状态信息,在runtime中有一个snd_pcm_hw_constraints类型成员,这个成员就是rule的存放点,所有通过snd_pcm_hw_rule_add添加的rule都被存放到snd_pcm_hw_constraints类型成员的rules中。关于snd_pcm_hw_constraints中的masks和intervals,后面再说。

再次,rule add中的其他参数都是干嘛的

runtime:

前面已经说过。

cond:

rule的约束,在上层配下来hw parameter时,会根据rule来检测配置的参数有效性,那么这个cond在这里的作用就是让这一条rule失效,也就是不去检查参数是否满足这条rule。在检查参数时有这样一句话:if (r->cond && !(r->cond & params->flags)) continue;,这里的params就是struct snd_pcm_hw_params,也就是说,如果在添加rule时设置了cond,且上层在配置hw param时设置同样的flags,则可以让该rule失效。那么这个东西究竟在什么情况下使用,关于这一点暂时还没搞清楚,因为还没发现哪里使用了这个功能……

var:

这个var实际上就是参数标识,标识该rule是针对的哪一个参数,例如:SNDRV_PCM_HW_PARAM_FORMAT,参数标识的定义见:asound.h。

func:

我觉得应该叫约束函数,当需要判断某个hw parameter是否满足该rule的要求时,通过调用该函数实现判断,这些函数的实现都在pcm_lib.c中,在add的时候选择一个能够处理rule期望处理的参数类型的func填进去。那么这个函数怎么实现,或者为什么像pcm_lib.c中那样实现,其实全部是跟参数类型相关的,不容参数类型判断有效的方式是不一样的,所以这里可以看到很多不同的约束函数。

private:

在func中判断hw param是否满足rule约束时所需要的数据。private由add rule时指定,private的类型根据func的不同而不同,具体需要什么类型的private要根据当前func中的需求来确定,所以这里需要了解当前选择的func的特性。

dep:

dependence的意思,就是依赖。这个参数也是为func服务的,跟private一样,不同的func所需要的依赖不同,目前一个rule可以支持最多4个依赖。

每个依赖其实都是一个参数,这里举个栗子:SNDRV_PCM_HW_PARAM_FORMAT类型的参数需要依赖SNDRV_PCM_HW_PARAM_SAMPLE_BITS参数。这个比较好理解,如果format是SNDRV_PCM_FORMAT_U16_LE,那么这个stream的采样率的物理位宽就必须是16,如果不是则自相矛盾了。

最后,rule是在哪里起作用的

这个问题就得说一下snd_pcm_hw_refine函数了,这个函数在整个hw param相关的内容中应该是绝对核心的地位,这里就大概分析一下他的执行过程:

int snd_pcm_hw_refine(struct snd_pcm_substream *substream, 
              struct snd_pcm_hw_params *params)
{
    ……
    unsigned int rstamps[constrs->rules_num];
    unsigned int vstamps[SNDRV_PCM_HW_PARAM_LAST_INTERVAL + 1];
    unsigned int stamp = 2;
    ……
    /* 检查params中mask类型的参数是否满足substream->runtime->hw_constraints中mask类型的参数要求
     * substream->runtime->hw_constraints应该是表示此substream允许该参数设置成哪些值,检查params
     * 要设置的值是否在substream->runtime->hw_constraints中,如果不是,则把params中的该值给置0
     * */
    for (k = SNDRV_PCM_HW_PARAM_FIRST_MASK; k <= SNDRV_PCM_HW_PARAM_LAST_MASK; k++) {
        ……
    }

    /* 同上,这是检查intervals的参数 */
    for (k = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; k <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; k++) {
        ……
    }

    ……

    for (k = 0; k < constrs->rules_num; k++)
        rstamps[k] = 0;
    for (k = 0; k <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; k++) 
        vstamps[k] = (params->rmask & (1 << k)) ? 1 : 0;

    /* 遍历rule,检查params是否满足每条rule的约束 */
    do {
        again = 0;
        for (k = 0; k < constrs->rules_num; k++) {
            ……
            /* 调用rule的约束函数,对params施加约束 */
            changed = r->func(params, r);
            ……

            /* 这一段代码就是处理多条rule约束同一参数的地方,大致的执行过程:
             * 当多条rule约束同一参数时,每条rule都会调用约束函数来处理该参数,如果
             * 参数被rule给修改了,则表示该参数被此rule约束了,一旦一个rule修改了
             * 该参数的值,那么所有其他的rule都必须重新对该参数调用一次约束函数,直到
             * 所有约束该参数的rule都不再会修改该参数的值为止
             * rstamps、vstamps、stamp、again这几个变量都是转为此服务的
             * */
            rstamps[k] = stamp;
            if (changed && r->var >= 0) {
                params->cmask |= (1 << r->var);
                vstamps[r->var] = stamp;
                again = 1;
            }
            if (changed < 0)
                return changed;
            stamp++;
        }
    } while (again);

    /* 设置msbits */
    if (!params->msbits) {
        ……
    }

    /* 设置rate */
    if (!params->rate_den) {
        ……
    }

    /* 设置fifo size */
    if (!params->fifo_size) {
            ……
        }
    }
    ……
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐