关于dapm中的widgets与kcontrol

4 篇文章 10 订阅

关于dapm中的widgets与kcontrol

标签(空格分隔): audio


0 前言

最近widgets相关的碰到了点问题,这里全面的分析一下widgets机制,主要针对一些更新和网上现有资源的补充。

1 widgets的创建

创建widgets的过程一般有如下几步:
1、调用snd_soc_register_codec注册一个codec。这个时候只是向声卡里面注册了一个component,然后把widgets和routes分别赋值给了该component的对应成员。

int snd_soc_register_codec(struct device *dev,
			   const struct snd_soc_codec_driver *codec_drv,
			   struct snd_soc_dai_driver *dai_drv,
			   int num_dai)
{
……
	if (codec_drv->controls) {
		codec->component.controls = codec_drv->controls;
		codec->component.num_controls = codec_drv->num_controls;
	}
	if (codec_drv->dapm_widgets) {
		codec->component.dapm_widgets = codec_drv->dapm_widgets;
		codec->component.num_dapm_widgets = codec_drv->num_dapm_widgets;
	}
	if (codec_drv->dapm_routes) {
		codec->component.dapm_routes = codec_drv->dapm_routes;
		codec->component.num_dapm_routes = codec_drv->num_dapm_routes;
	}

	if (codec_drv->probe)
		codec->component.probe = snd_soc_codec_drv_probe;
	if (codec_drv->remove)
		codec->component.remove = snd_soc_codec_drv_remove;
	if (codec_drv->write)
		codec->component.write = snd_soc_codec_drv_write;
	if (codec_drv->read)
		codec->component.read = snd_soc_codec_drv_read;

……
	/* 把codec的component挂到soc-core中的全局静态“component_list”中,
	 * 声卡初始化的时候会自己去遍历所有的component */
	snd_soc_component_add_unlocked(&codec->component);
……
}

2、调用snd_soc_dapm_new_controls()函数把widgets挂载到card->widgets链表里面
只有把widgets挂载到card->widgets里面,此widgets才真正生效,也就是创建成功,否则后面是找不到此widgets的。这里要提一下的是snd_soc_dapm_new_controls()怎么被调的,从代码上来看很好跟踪,就是声卡在初始化的时候调用soc_probe_component()的时候调用的,上面说到过,snd_soc_register_codec函数的调用会注册一个component进声卡,然后可以想象只要注册进声卡的component就都会调用soc_probe_component()来进行probe。但是这是不对的,并不是注册一个codec,这个codec就一定会被probe,我们还得定义这个codec的链接,没有链接的codec都是不会被probe的。那么什么是codec的链接?其实就是一个dai link或者一个aux,必须有dai link链接了这个codec或者定义了aux链接了这个codec,此codec才能被probe,从而此codec的widgets才能被正确加载,否则都是无效的,具体的可以参看之前的博客关于ASoC中的aux设备及prefix

2 path的创建

2.1 创建一个path

这里的path只谈定义在route中的path,其他的什么dai widgets的path,DPCM之类的都大同小异只是创建时机、创建逻辑有些区别,可以参考高通msm8996平台的ASOC音频路径分析这篇。

path其实跟widgets的创建过程和逻辑是一样的,首先也是在注册codec的时候把codec内部的route都注册到component里面,然后在声卡初始化的时候去probe这些component调用snd_soc_dapm_add_routes来创建这些path,这些path的connect标有些是1有些是0,具体的参见snd_soc_dapm_add_path函数,其中比较特殊一点的是mux和mixer两种控件,其connect是0是1根据初始配置来确定。这块新老版本的linux实现差异还比较大,具体是从哪个版本开始改的暂时懒得去查证了。

老版本:(820平台 linux3.18static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
	struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
	const char *control,
	int (*connected)(struct snd_soc_dapm_widget *source,
			 struct snd_soc_dapm_widget *sink))
{
……
/* 如果没有路径选择的path,那么一直处于联通状态,这里其实也就是mixer、mux和switch三种主要类型 */
	/* connect static paths */
	if (control == NULL) {
		list_add(&path->list, &dapm->card->paths);
		list_add(&path->list_sink, &wsink->sources);
		list_add(&path->list_source, &wsource->sinks);
		path->connect = 1;
		return 0;
	}

	/* connect dynamic paths */
	switch (wsink->id) {
	case snd_soc_dapm_adc:
	case snd_soc_dapm_dac:
	case snd_soc_dapm_pga:
	case snd_soc_dapm_out_drv:
	case snd_soc_dapm_input:
	case snd_soc_dapm_output:
	case snd_soc_dapm_siggen:
	case snd_soc_dapm_micbias:
	case snd_soc_dapm_vmid:
	case snd_soc_dapm_pre:
	case snd_soc_dapm_post:
	case snd_soc_dapm_supply:
	case snd_soc_dapm_regulator_supply:
	case snd_soc_dapm_clock_supply:
	case snd_soc_dapm_aif_in:
	case snd_soc_dapm_aif_out:
	case snd_soc_dapm_dai_in:
	case snd_soc_dapm_dai_out:
	case snd_soc_dapm_dai_link:
	case snd_soc_dapm_kcontrol:
	/* 其实一般没有进这个case,因为这里的这些widgets本来就不需要control */
		list_add(&path->list, &dapm->card->paths);
		list_add(&path->list_sink, &wsink->sources);
		list_add(&path->list_source, &wsource->sinks);
		path->connect = 1;
		return 0;
	case snd_soc_dapm_mux:
	/* 如果是mux,则去读当前mux的状态,看是选择在哪一个通路上,把选择的通路connect置1,其余都置0 */
		ret = dapm_connect_mux(dapm, wsource, wsink, path, control,
			&wsink->kcontrol_news[0]);
		if (ret != 0)
			goto err;
		break;
	case snd_soc_dapm_switch:
	case snd_soc_dapm_mixer:
	case snd_soc_dapm_mixer_named_ctl:
	/* 如果是mixer或者switch,则去读当前widgets的状态,看是选择在哪一个通路上,
	 * 把使能的通路connect置1,没使能的置0 */
		ret = dapm_connect_mixer(dapm, wsource, wsink, path, control);
		if (ret != 0)
			goto err;
		break;
	case snd_soc_dapm_hp:
	case snd_soc_dapm_mic:
	case snd_soc_dapm_line:
	case snd_soc_dapm_spk:
	/* 这些widgets属于外部设备,默认都是置为断开,这里在哪去connect的还没找到…… */
		list_add(&path->list, &dapm->card->paths);
		list_add(&path->list_sink, &wsink->sources);
		list_add(&path->list_source, &wsource->sinks);
		path->connect = 0;
		return 0;
	}
……
}
新版本:(845平台linux 4.19static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
	struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
	const char *control,
	int (*connected)(struct snd_soc_dapm_widget *source,
			 struct snd_soc_dapm_widget *sink))
{
……
/* 没control的全是connect,可切换的根据实际路径配置进行动态connect */
	/* connect static paths */
	if (control == NULL) {
		path->connect = 1;
	} else {
		switch (wsource->id) {
		case snd_soc_dapm_demux:
			ret = dapm_connect_mux(dapm, path, control, wsource);
			if (ret)
				goto err;
			break;
		default:
			break;
		}

		switch (wsink->id) {
		case snd_soc_dapm_mux:
			ret = dapm_connect_mux(dapm, path, control, wsink);
			if (ret != 0)
				goto err;
			break;
		case snd_soc_dapm_switch:
		case snd_soc_dapm_mixer:
		case snd_soc_dapm_mixer_named_ctl:
			ret = dapm_connect_mixer(dapm, path, control);
			if (ret != 0)
				goto err;
			break;
		default:
			break;
		}
	}
……
}

这里也要记录一点,不是所有的route都是在各个codec里面定义的,高通平台可以把machine driver级别的route定义到dts里面,这样的设计更符合machine driver对于设备的描述级别。

2.2 关于widgets和path中的connect及end point

在widgets的创建中有这样一句话:

snd_soc_dapm_new_control_unlocked(struct snd_soc_dapm_context *dapm,
			 const struct snd_soc_dapm_widget *widget)
{
……
	/* machine layer sets up unconnected pins and insertions */
	w->connected = 1;
……
}

只要创建成功了一个widgets,就把这个widgets的connected设置为1。然后在add path中还有这样的定义:

static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
	struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
	const char *control,
	int (*connected)(struct snd_soc_dapm_widget *source,
			 struct snd_soc_dapm_widget *sink))
{
	/* connect static paths */
	if (control == NULL) {
		path->connect = 1;
	} else {
		switch (wsource->id) {
		case snd_soc_dapm_demux:
			ret = dapm_connect_mux(dapm, path, control, wsource);
……
		default:
			break;
		}

		switch (wsink->id) {
		case snd_soc_dapm_mux:
			ret = dapm_connect_mux(dapm, path, control, wsink);
……
			break;
		case snd_soc_dapm_switch:
		case snd_soc_dapm_mixer:
		case snd_soc_dapm_mixer_named_ctl:
			ret = dapm_connect_mixer(dapm, path, control);
……
			break;
		default:
			break;
		}
	}
……
}

其中dapm_connect_xxx函数都是设置path->connect的,这里的connect和widgets里面的connected对于刚开始来说比较容易弄晕,所以简单记录一下。

  • 首先widgets里面的connected和path的connect都是用来控制power up和power down的,目的相同。
  • 其次,widgets里面的connected标识功能性的选择,启动某一个功能时需要使能某些widgets,关闭某个功能时需要关闭某些widgets,使拥有该功能的path无法启动,这是站在功能的角度去控制path的上电和下电。
  • path的connect一般来说是用来动态控制上下电用的,绝大多数的path connect始终为1,只有mixer、mux、demux和switch这些可供外部path_mixer.xml文件配置的widgets所链接的path才会出现为0的情况。只有在需要打开某些path的时候才会从mixer.xml中去控制对应的control去把path的connect置1,从而完成整条path上电的动作,这是站在链路的角度去控制path的上电和下电。

上面这些比较抽象,下面具体一点。
widgets里面的connected是用set pin来对其进行控制的,就是字面意思。一般在设计driver的时候不会针对某一个项目去设计widgets,而是针对一款芯片去设计widgets,那么driver里面就会把这款芯片的所有功能全部封装进widgets中,但是在实际项目中芯片的所有功能并不是全用,所以就需要connected的控制了,而这种disconnect跟path的disconnect意义不同,这个是站在功能角度来使能或者禁止某一条path,而不是站在path的打开关闭的角度。

与其相对应的,path中的connect,就是站在音频链路打开关闭的角度来控制path,当外部通过xml去控制一条音频链路打开,实际上就是改变了path中的connect,从而触发链路的上下电操作。

对应的代码:

static __always_inline int is_connected_ep(struct snd_soc_dapm_widget *widget,
	struct list_head *list, enum snd_soc_dapm_direction dir,
	int (*fn)(struct snd_soc_dapm_widget *, struct list_head *,
		  bool (*custom_stop_condition)(struct snd_soc_dapm_widget *,
						enum snd_soc_dapm_direction)),
	bool (*custom_stop_condition)(struct snd_soc_dapm_widget *,
				      enum snd_soc_dapm_direction))
{
    /* 如果一个end point的connected为真则检测这个end point的上电状态 */
	if ((widget->is_ep & SND_SOC_DAPM_DIR_TO_EP(dir)) && widget->connected) {
		widget->endpoints[dir] = snd_soc_dapm_suspend_check(widget);
		return widget->endpoints[dir];
	}
    /* 否则如果一个end point的connected为假,则这里的for each将遍历不出任何widgets(end point的要求)
     * 从而实现返回0,示意链路未连接 */
     
	snd_soc_dapm_widget_for_each_path(widget, rdir, path) {
		DAPM_UPDATE_STAT(widget, neighbour_checks);

		if (path->weak || path->is_supply)
			continue;

		if (path->walking)
			return 1;

		trace_snd_soc_dapm_path(widget, dir, path);

        /* 如果非ep节点,则根据path来判断链路是否链接 */
		if (path->connect) {
			path->walking = 1;
			con += fn(path->node[dir], list, custom_stop_condition);
			path->walking = 0;
		}
	}

	widget->endpoints[dir] = con;

	return con;
}

上面就是path connect和widgets connected的作用机制,但是这里面又出现了一个新的概念:end point
这个东西是2014年10月份alsa的更新加入的新特性,当时叫做:is_sinkis_source后来在2015年8月的更新里面改成了现在看到的is_ep这里具体的区别就不在专门描述了,可以去ALSA的wiki上面看。

关于这里的描述借用官方的说法是:

DAPM widgets can be classified into four categories:

  • supply: Supply widgets do not affect the power state of their
    non-supply widget neighbors and unlike other widgets a
    supply widget is not powered up when it is on an active
    path, but when at least on of its neighbors is powered up.
  • source: A source is a widget that receives data from outside the
    DAPM graph or generates data. This can for example be a
    microphone, the playback DMA or a signal generator. A source
    widget will be considered powered up if there is an active
    path to a sink widget.
  • sink: A sink is a widget that transmits data to somewhere outside
    of the DAPM graph. This can e.g. be a speaker or the capture
    DMA. A sink widget will be considered powered up if there is
    an active path from a source widget.
  • normal: Normal widgets are widgets not covered by the categories
    above. A normal widget will be considered powered up if it
    is on an active path between a source widget and a sink
    widget.

具体到哪些widgets对应哪个分类,这里根据代码里面的逻辑整理了一下,属于endpoint的有如下几个:

snd_soc_dapm_mic
snd_soc_dapm_input
snd_soc_dapm_spk
snd_soc_dapm_hp
snd_soc_dapm_output
snd_soc_dapm_vmid
snd_soc_dapm_siggen
snd_soc_dapm_sink
snd_soc_dapm_line

snd_soc_dapm_dai_in
snd_soc_dapm_dai_out

大多数都是在snd_soc_dapm_new_control_unlocked的时候进行静态赋值的,但是snd_soc_dapm_dai_insnd_soc_dapm_dai_out是在soc_dapm_dai_stream_event里面指定的,而且是动态的:

static void soc_dapm_dai_stream_event(struct snd_soc_dai *dai, int stream,
	int event)
{
	struct snd_soc_dapm_widget *w;
	unsigned int ep;

	if (stream == SNDRV_PCM_STREAM_PLAYBACK)
		w = dai->playback_widget;
	else
		w = dai->capture_widget;

	if (w) {
		dapm_mark_dirty(w, "stream event");

		if (w->id == snd_soc_dapm_dai_in) {
			ep = SND_SOC_DAPM_EP_SOURCE;
			dapm_widget_invalidate_input_paths(w);
		} else {
			ep = SND_SOC_DAPM_EP_SINK;
			dapm_widget_invalidate_output_paths(w);
		}

		switch (event) {
		case SND_SOC_DAPM_STREAM_START:
			w->active = 1;
			w->is_ep = ep;
			break;
		case SND_SOC_DAPM_STREAM_STOP:
			w->active = 0;
			w->is_ep = 0;
			break;
		case SND_SOC_DAPM_STREAM_SUSPEND:
		case SND_SOC_DAPM_STREAM_RESUME:
		case SND_SOC_DAPM_STREAM_PAUSE_PUSH:
		case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:
			break;
		}
	}
}

还有snd_soc_dapm_linesnd_soc_dapm_inputsnd_soc_dapm_output是在dapm_update_widget_flags里面重复刷新的,每当path发生增加和删除操作时需要重新刷新一遍。

关于这点,官方给出的解释如下:

The way the number of input and output paths for a widget is calculated
depends on its category. There are a bunch of factors which decide which
category a widget is. Currently there is no formal classification of these
categories and we calculate the category of the widget based on these
factors whenever we want to know it. This is at least once for every widget
during each power update sequence. The factors which determine the category
of the widgets are mostly static though and if at all change rather seldom.
This patch introduces three new per widget flags, one for each of non-normal
widgets categories. Instead of re-computing the category each time we want
to know them the flags will be checked. For the majority of widgets the
category is solely determined by the widget id, which means it never changes
and only has to be set once when the widget is created. The only widgets
with dynamic categories are:

snd_soc_dapm_dai_out: Is considered a sink iff the capture stream is
	active, otherwise normal.
snd_soc_dapm_dai_in: Is considered a source iff the playback stream
	is active, otherwise normal.
snd_soc_dapm_input: Is considered a sink iff it has no outgoing
	paths, otherwise normal.
snd_soc_dapm_output: Is considered a source iff it has no incoming
	paths, otherwise normal.
snd_soc_dapm_line: Is considered a sink iff it has no outgoing paths
	and is considered a source iff it has no incoming paths,
	otherwise normal.

For snd_soc_dapm_dai_out/snd_soc_dapm_dai_in widgets the category will be
updated when a stream is started or stopped. For the other dynamic widgets
the category will be updated when a path connecting to it is added or
removed.

snd_soc_dapm_linesnd_soc_dapm_inputsnd_soc_dapm_output重复刷新比较好理解,因为每次增加或者删除path后有可能是在output之后又增加了一个ep,这样原来的output就不再是ep,所以需要刷新。

但是snd_soc_dapm_dai_insnd_soc_dapm_dai_out的动态刷新这里目前没有看到明确的说明,猜测是想通过stream来控制path的一个手段。设想这样一个场景,如果snd_soc_dapm_dai_in始终为ep,那么如果这时并没有播放音乐,但是path是链接的,同时供电也是存在的,那么整个音频通路就在没有播放音乐的时候正常工作了,这时不愿意见到的现象,而如果snd_soc_dapm_dai_in的ep状态随stream的状态变化,则没有stream的时候snd_soc_dapm_dai_in为非ep,这时即使path是链接的,供电也是存在的,但是由于snd_soc_dapm_dai_in为非ep,这样在检查path的connect状态时会认为此path链接断开,从而让整个音频链路下电,达到我们期望的目的。

3 widgets中的kcontrol的创建

不是所有的widgets都会创建kcontrol,在老版本中只有:
snd_soc_dapm_switch
snd_soc_dapm_mixer
snd_soc_dapm_mixer_named_ctl
snd_soc_dapm_mux
会真正创建kcontrol,其余的widgets都没有创建kcontrol,即使snd_soc_dapm_pga也会调用dapm_new_pga()函数,但是该函数是空的。

在新版本中除了上述的widgets外,snd_soc_dapm_pgasnd_soc_dapm_out_drvsnd_soc_dapm_dai_link都会创建kcontrol了,但是前提是这些widgets里面配置了kcontrol。

3.1 创建kcontrol

整个过程的关键就是snd_soc_dapm_new_widgets函数:

int snd_soc_dapm_new_widgets(struct snd_soc_card *card)
{
……
	list_for_each_entry(w, &card->widgets, list)
	{
……
		if (w->num_kcontrols) {
			w->kcontrols = kzalloc(w->num_kcontrols *
						sizeof(struct snd_kcontrol *),
						GFP_KERNEL);
……
		switch(w->id) {
		case snd_soc_dapm_switch:
		case snd_soc_dapm_mixer:
		case snd_soc_dapm_mixer_named_ctl:
			dapm_new_mixer(w);
			break;
		case snd_soc_dapm_mux:
		case snd_soc_dapm_demux:
			dapm_new_mux(w);
			break;
		case snd_soc_dapm_pga:
		case snd_soc_dapm_out_drv:
			dapm_new_pga(w);
			break;
		case snd_soc_dapm_dai_link:
			dapm_new_dai_link(w);
			break;
		default:
			break;
		}
……
}

这个函数倒是没什么好记录的,就是根据类型调用对应类型的new,这里以mixer类型为例记录一下,因为mixer类型是这里面最复杂的。

static int dapm_new_mixer(struct snd_soc_dapm_widget *w)
{
……
	/* add kcontrol */
	for (i = 0; i < w->num_kcontrols; i++) {
		/* match name */
		snd_soc_dapm_widget_for_each_source_path(w, path) {
			/* mixer/mux paths name must match control name */
			if (path->name != (char *)w->kcontrol_news[i].name)
				continue;

			if (!w->kcontrols[i]) {
			    /* 创建该widget下的kcontrol */
				ret = dapm_create_or_share_kcontrol(w, i);
				if (ret < 0)
					return ret;
			}
            /* 把链接到该kcontrol的path记录到此kcontrol中 */
			dapm_kcontrol_add_path(w->kcontrols[i], path);

			data = snd_kcontrol_chip(w->kcontrols[i]);
			if (data->widget)
			/* 如果该kcontrol启动的自动关闭功能,则将由于该功能而创建的widgets链接到该kcontrol的源上 */
				snd_soc_dapm_add_path(data->widget->dapm,
						      data->widget,
						      path->source,
						      NULL, NULL);
		}
	}

	return 0;
}

上面这个函数看上去很简单,单纯只是谈怎么创建control,怎么创建widgets其实就像网上很多博客里面写的一样,过程非常简单,但是如果把每个细节问个为什么其实还是要分析一下的,这里先抛出几个问题:

  • path->name和w->kcontrol_news[i].name为什么会一样?
  • data是干什么的,data是哪里来的?
  • data->paths的作用是什么?
  • if (data->widget)为什么这里会为空?
  • snd_soc_dapm_add_path这里的path会对音频链路产生什么影响?

这几个问题没弄明白,kcontrol的创建其实属于完全没搞懂,当然不仅仅是这几个问题,后面的函数分析中还会继续抛出问题。

第一个问题好说,path name是在dapm_connect_mixer()中赋值的,当path的sink name和control name相同时,就用control 那么给这条path命名,所以control name可以和path name匹配上。

第二个问题,data是干什么的,这个问题就复杂一点了,要继续分析dapm_create_or_share_kcontrol函数。

static int dapm_create_or_share_kcontrol(struct snd_soc_dapm_widget *w,
	int kci)
{
……
        /* 利用struct snd_kcontrol_new创建struct snd_kcontrol
         * 这里要注意一下,snd_kcontrol_new一般是在定义的时候静态写死的kcontrol的参数,
         * snd_kcontrol则是真正运行时的一个可以工作的kcontrol */
		kcontrol = snd_soc_cnew(&w->kcontrol_news[kci], NULL, name,
					prefix);
……
        /* 创建struct dapm_kcontrol_data *data */
		ret = dapm_kcontrol_data_alloc(w, kcontrol, name);
……
        /* 把创建的kcontrol添加到声卡中,否则此kcontrol仅仅存在,但是没有加入到运行的kcontrol中 */
		ret = snd_ctl_add(card, kcontrol);
……
    /* 把w记录到kcontrol中 */
	ret = dapm_kcontrol_add_widget(kcontrol, w);
	if (ret == 0)
	    /* 把kcontrol记录到w中 */
		w->kcontrols[kci] = kcontrol;
……
}

这里继续看dapm_kcontrol_data_alloc函数:

static int dapm_kcontrol_data_alloc(struct snd_soc_dapm_widget *widget,
	struct snd_kcontrol *kcontrol, const char *ctrl_name)
{
……
	data = kzalloc(sizeof(*data), GFP_KERNEL);
……
	switch (widget->id) {
	case snd_soc_dapm_switch:
	case snd_soc_dapm_mixer:
	case snd_soc_dapm_mixer_named_ctl:
	    /* 这里的private value就是在snd_ctl_new1()函数中赋值的snd_kcontrol_new中静态定义的
	     * 其过程:SOC_SINGLE_EXT->SOC_SINGLE_VALUE->SOC_DOUBLE_VALUE->struct soc_mixer_control */
		mc = (struct soc_mixer_control *)kcontrol->private_value;
        /* 从而这里的autodisable就是SOC_SINGLE_VALUE中的xautodisable参数,遗憾的是在
         * SOC_SINGLE_EXT中,该值写死为0,也就是始终进不到这里面来,但是可以自己写一个
         * SOC_SINGLE_EXT_XXX或者用alsa提供的SOC_DAPM_SINGLE_AUTODISABLE来做实验 */
		if (mc->autodisable) {
			struct snd_soc_dapm_widget template;
……
            /* 这里就是给temp赋值,把该kcontrol的操作克隆下来 */
			template.reg = mc->reg;
			template.mask = (1 << fls(mc->max)) - 1;
			template.shift = mc->shift;
			if (mc->invert)
				template.off_val = mc->max;
			else
				template.off_val = 0;
			template.on_val = template.off_val;
			/* 这里注意一下id用的是snd_soc_dapm_kcontrol */
			template.id = snd_soc_dapm_kcontrol;
			template.name = name;

			data->value = template.on_val;

            /* 用克隆下来的kcontrol再创建一个widgets,也就是功能上和这个kcontrol一毛一样的
             * 的widgets,这个widgets会在dapm_seq_run_coalesced()的时候来执行配置操作,跟
             * 控制kcontrol实现的作用一样,简单来说也就是通过kcontrol创建了一个该kcontrol的
             * 自动控制,但是这个自动控制中on和off的值都是off,这个原因及用法后面再分析 */
			data->widget =
				snd_soc_dapm_new_control_unlocked(widget->dapm,
				&template);
			kfree(name);
……
		}
		break;
……
	}

	kcontrol->private_data = data;
……
}

该函数创建了kcontrol的data,同时给data->widget赋了值,这个data的定义如下:

struct dapm_kcontrol_data {
	unsigned int value;
	struct snd_soc_dapm_widget *widget;
	struct list_head paths;
	struct snd_soc_dapm_widget_list *wlist;
};

其他的字段每个的意义、作用以及赋值的位置究竟在哪,这个问题还没有解释清楚,所以要进一步弄清楚这个data就需要继续分析其他字段。
回到dapm_create_or_share_kcontrol()函数中,有一句话:dapm_kcontrol_add_widget(kcontrol, w);这里就是给wlist进行了赋值,将包含这个control的widget赋给了wlist
接着,再来看看paths,其赋值在前面提到的dapm_new_mixer()函数中通过调用dapm_kcontrol_add_path()函数中完成的。
最后value字段,其操作在dapm_kcontrol_set_value函数中,这里具体内容结合后面autodisable的使用一起分析。
这样就可以知道真个data的意义与作用了:

  • value:在autodisable功能中记录当前的控件状态,具体后面分析;
  • widgets:如果使能了autodisable功能则创建,否则不创建,具体行为后面分析;
  • paths:记录所有链接到该kcontrol上的path信息,用来power update时遍历(触发)所有与之相连的widgets时使用,例如:soc_dapm_mixer_update_power函数;
  • wlist:记录该kcontrol属于哪个widget,简单说就是一个mixer类型的widgets会创建n个kcontrol,这每个kcontrol的wlist都存着这个mixer的入口。不过这里会存在多个widget share一个kcontrol的情况,所以这里的wlisk是一个数组,但是往往都只取0号元素使用;

结合上面的分析,第三和第四个问题也就分析清楚了,第五个问题后面autodisable中再分析。

3.2 autodisable的过程

创建kcontrol的过程中会涉及到一个autodisable的东西,这个东西到底是干什么的,怎么工作的,这个问题在这里专门分析一下。

首先autodisable的作用,官方的解释如下:

Some devices have the problem that if a internal audio signal source is disabled
the output of the source becomes undefined or goes to a undesired state (E.g.
DAC output goes to ground instead of VMID). In this case it is necessary, in
order to avoid unwanted clicks and pops, to disable any mixer input the signal
feeds into or to active a mute control along the path to the output. Often it is
still desirable to expose the same mixer input control to userspace, so cerain
paths can sill be disabled manually. This means we can not use conventional DAPM
to manage the mixer input control. This patch implements a method for letting
DAPM overwrite the state of a userspace visible control. I.e. DAPM will disable
the control if the path on which the control sits becomes inactive. Userspace
will then only see a cached copy of the controls state. Once DAPM powers the
path up again it will sync the userspace setting with the hardware and give
control back to userspace.

To implement this a new widget type is introduced. One widget of this type will
be created for each DAPM kcontrol which has the auto-disable feature enabled.
For each path that is controlled by the kcontrol the widget will be connected to
the source of that path. The new widget type behaves like a supply widget,
which means it will power up if one of its sinks are powered up and will only
power down if all of its sinks are powered down. In order to only have the mixer
input enabled when the source signal is valid the new widget type will be
disabled before all other widget types and only be enabled after all other
widget types.

E.g. consider the following simplified example. A DAC is connected to a mixer
and the mixer has a control to enable or disable the signal from the DAC.

                    +-------+
 +-----+            |       |
 | DAC |-----[Ctrl]-| Mixer |
 +-----+       :    |       |
 |             :    +-------+
 |             :
 +-------------+
 | Ctrl widget |
 +-------------+

If the control has the auto-disable feature enabled we’ll create a widget for
the control. This widget is connected to the DAC as it is the source for the
mixer input. If the DAC powers up the control widget powers up and if the DAC
powers down the control widget is powered down. As long as the control widget
is powered down the hardware input control is kept disabled and if it is enabled
userspace can freely change the control’s state.

简单来说就是有些widgets里面有control,这些control是受外面控制的,上电的时候可以控制这些control,但是下电的时候如果外面控的时机不合适或者没控,这个时候可能产生杂音,于是就想办法让widget自动控control。

从对widgets的认识来说,无论什么控制都需要创建一个widgets,那么一个kcontrol并没有一个属于该kcontrol的widgets,wlist里面的widgets是包含该kcontrol的widgets而不是控制该kcontrol的widgets,这里光看文字确实比较绕……至于其他博客里面说的影子widgets感觉就更难理解了。关于这个widgets其他的博客里面仅仅点到位置,这里稍微分析一下其具体行为更方便理解。

首先是创建,这个上面已经说过了,当autodisable为1时,创建kcontrol时会去创建一个widgets,并将这个widgets保存在data->widget里面。

然后再调用add path,添加一个path,将该widgets与该kcontrol的输入端相连:

static int dapm_new_mixer(struct snd_soc_dapm_widget *w)
{
……
	/* add kcontrol */
	for (i = 0; i < w->num_kcontrols; i++) {
			if (data->widget)
			/* 如果该kcontrol启动的自动关闭功能,则将由于该功能而创建的widgets链接到该kcontrol的源上 */
				snd_soc_dapm_add_path(data->widget->dapm,
						      data->widget,
						      path->source,
						      NULL, NULL);
		}
	}

	return 0;
}

其实就是官方这张图中所表述的:

                    +-------+
 +-----+            |       |
 | DAC |-----[Ctrl]-| Mixer |
 +-----+       :    |       |
 |             :    +-------+
 |             :
 +-------------+
 | Ctrl widget |
 +-------------+

ctrl widget就是我们为ctrl创建的widget,在最开始我们认为该ctrl是关闭状态,所以widget中的on_valoff_val都是off的值:

static int dapm_kcontrol_data_alloc(struct snd_soc_dapm_widget *widget,
	struct snd_kcontrol *kcontrol, const char *ctrl_name)
{
……
	data = kzalloc(sizeof(*data), GFP_KERNEL);
……
	switch (widget->id) {
	case snd_soc_dapm_switch:
	case snd_soc_dapm_mixer:
	case snd_soc_dapm_mixer_named_ctl:
……
		if (mc->autodisable) {
			struct snd_soc_dapm_widget template;
……
            /* 这里就是给temp赋值,把该kcontrol的操作克隆下来 */
			template.reg = mc->reg;
			template.mask = (1 << fls(mc->max)) - 1;
			template.shift = mc->shift;
			if (mc->invert)
				template.off_val = mc->max;
			else
				template.off_val = 0;
			/* on的值 */
			template.on_val = template.off_val;
			/* 这里注意一下id用的是snd_soc_dapm_kcontrol */
			template.id = snd_soc_dapm_kcontrol;
			template.name = name;
            /* 当前widget的状态 */
			data->value = template.on_val;
……
		}
		break;
……
}

对于一个widget,他控制上下电的逻辑在:

static void dapm_seq_run_coalesced(struct snd_soc_card *card,
				   struct list_head *pending)
{
……
	list_for_each_entry(w, pending, power_list) {
……
        /* 根据当前上电状态判断是给寄存器写on的值还是off的值 */
		if (w->power)
			value |= w->on_val << w->shift;
		else
			value |= w->off_val << w->shift;
……
		/* 调用上下电前响应事件回调函数 */
		dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMU);
		dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMD);
	}

	if (reg >= 0) {
……
        /* 给寄存器写状态值 */
		soc_dapm_update_bits(dapm, reg, mask, value);
	}

	list_for_each_entry(w, pending, power_list) {
	    /* 调用上下电后响应事件回调函数 */
		dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMU);
		dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMD);
	}
}

所以,当我们给kcontrol创建了一个widgets,并且将这个widgets链接到其源widgets上后,当源widgets下电或者上电时就会调用kcontrol创建的widgets,从而对kcontrol实现自动控制,同时控制的值为on_valoff_val

那么这里就又有一个问题了,on_valoff_val初始时都是off,那么是不是意味着这里永远都是off,这样就会产生错误,当整整power on的时候,由于on val是off的值,所以会把off的值写下去,导致硬件实际是下电的状态。这种现象肯定不会,否则这个机制就错了,那么就又有两个问题:1、on val是何时何地被改写的?2、为什么要改写on val的值,而不是像普通widgets一样,在初始的时候就把on和off的值分别填好?

首先第一个问题,on val是在dapm_kcontrol_set_value函数里面设置的,该函数是在kcontrol的put中调用的,例如snd_soc_dapm_put_volsw函数就是SOC_DAPM_SINGLE_AUTODISABLEput函数实体。如果想自己写一个SOC_DAPM_SINGLE_XXX那么就在自己在put函数中调用dapm_kcontrol_set_value函数或者直接调用snd_soc_dapm_put_volsw函数,否则就会出现上述的那个问题,会发现使能了autodisable的widgets始终无法上电。给一个示例:

static int gift_mixer_put(struct snd_kcontrol *kcontrol,
			     struct snd_ctl_elem_value *ucontrol)
{
    /* 必须调用这个函数或者dapm_kcontrol_set_value函数,这里有点像函数的继承关系一样,
     * 必须继承dapm_kcontrol_set_value或者其派生类 */
	snd_soc_dapm_put_volsw(kcontrol, ucontrol);

	return 0;
}

#define SOC_SINGLE_EXT_GIFT(xname, xreg, xshift, xmax, xinvert) \
{	.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
	.info = snd_soc_info_volsw, \
	.get = gift_mixer_get, .put = gift_mixer_put, \
	.private_value = SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert, 1) }

所以,on val实在control被上层应用设置时一起被设置的。

第二个问题,为什么要这样做?其实原因是因为kcontrol的外部控制永远是第一优先级,也就是说如果外部kcontrol要你下电,链路无论是上电还是下电,都必须得关;如果是外部说上电,链路才有资格考虑到底开不开,以避免链路下电但是widgets没下电的情况,但是一旦链路上电之后,该widgets还要能自动上电。
因此,这个widgets不能是一个预先把on和off写好的widgets,否则外部的下电控制就不是第一优先级了,可能外部说下电,但是链路上电了,结果widgets就工作了,因为snd_soc_dapm_kcontrol类型的widgets其path的connect始终为1(snd_soc_dapm_add_path中已经分析过)。而如果不把connect设置为1的话那么path的连通性就又依赖于外部的control设置,类似于dapm_connect_mixer中一样,而这样又违背了autodisable的设计初衷,autodisable就是为了解决在没有外部control设置时kcontrol的自动操作问题,所以这里的widgets必须是path的connect为1的类型。

这样我觉得才算是把其他博客中提到的“影子”widgets给说清楚了。

4 关于mixer path文件如何对音频链路产生作用的

mixer path文件其实就是操作control。

在前面提到了dapm_connect_mixer函数,这里的分析主要就围绕connect来分析。
前面已经提过,音频链路的通断是由path中的connect字段决定的,而connect字段在初始化的时候,绝大多数path都是1,也就是链接状态,只有部分widgets的path connect是用函数表示的:

static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
	struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
	const char *control,
	int (*connected)(struct snd_soc_dapm_widget *source,
			 struct snd_soc_dapm_widget *sink))
{
……
	/* connect static paths */
	if (control == NULL) {
		path->connect = 1;
	} else {
		switch (wsource->id) {
		case snd_soc_dapm_demux:
			ret = dapm_connect_mux(dapm, path, control, wsource);
……
		}

		switch (wsink->id) {
		case snd_soc_dapm_mux:
			ret = dapm_connect_mux(dapm, path, control, wsink);
……
		case snd_soc_dapm_switch:
		case snd_soc_dapm_mixer:
		case snd_soc_dapm_mixer_named_ctl:
			ret = dapm_connect_mixer(dapm, path, control);
……
		}
	}
……
}

这几个connect函数其实也很简单,比如说一个mixer,有若干条path分别链接在该mixer下的不同kcontrol上,根据该widgets下的kcontrol的值,使能了的话对应的connect就是1,否则就是0.

其实关键就是connect在什么时候被置1和置0。这里connect的操作是soc_dapm_connect_path函数实现的,当需要的时候就通过该函数改变path的connect属性,这个函数又是在kcontrol被外界设置的时候调用的,比如说一个mixer,上层应通过control的方式设置后,系统会自动调用该control的put方法,一般来说soc_dapm_connect_path就是在put方法中调用的,例如:

SND_SOC_DAPM_MIXER("RX INT1 SPLINE MIX", SND_SOC_NOPM, 0, 0,
		rx_int1_spline_mix_switch,
		ARRAY_SIZE(rx_int1_spline_mix_switch)),
		
static const struct snd_kcontrol_new rx_int1_spline_mix_switch[] = {
	SOC_DAPM_SINGLE("HPHL Switch", SND_SOC_NOPM, 0, 1, 0)
};

#define SOC_DAPM_SINGLE(xname, reg, shift, max, invert) \
{	.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
	.info = snd_soc_info_volsw, \
	.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw, \
	.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
	
int snd_soc_dapm_put_volsw(struct snd_kcontrol *kcontrol,
	struct snd_ctl_elem_value *ucontrol)
{
……
		ret = soc_dapm_mixer_update_power(card, kcontrol, connect);
……
}

static int soc_dapm_mixer_update_power(struct snd_soc_card *card,
				   struct snd_kcontrol *kcontrol, int connect)
{
……
	/* find dapm widget path assoc with kcontrol */
	dapm_kcontrol_for_each_path(path, kcontrol) {
		found = 1;
		soc_dapm_connect_path(path, connect, "mixer update");
	}
……
}

static void soc_dapm_connect_path(struct snd_soc_dapm_path *path,
	bool connect, const char *reason)
{
……
	path->connect = connect;
	dapm_mark_dirty(path->source, reason);
	dapm_mark_dirty(path->sink, reason);
	dapm_path_invalidate(path);
}

整个调用顺序就是上面所描述的顺序,首先会创建一个mixer类型的widget,然后根据该widget下包含的control,创建相应的kcontrol,这时上层应用通过对kcontrol的写,调用snd_soc_dapm_put_volsw函数,最终改写了path的connect属性,使音频链路链接关系发生了变化,这时在对音频链路update便可根据connect的状态自动完成上下电控制。

5 关于ignore_suspend

这个其实是个很容易被忽略的东西,总的来说,这个东西的作用就是系统通过pm管理snd card时下不下电的标志。
这里简单分析一下pm对于snd card的作用机制,pm一般是通过suspend或者resume来控制设备上下电的:

void dapm_mark_endpoints_dirty(struct snd_soc_card *card)
{
……
	list_for_each_entry(w, &card->widgets, list) {
		if (w->ignore_suspend)
			continue;
		if (w->is_ep) {
			dapm_mark_dirty(w, "Rechecking endpoints");
			if (w->is_ep & SND_SOC_DAPM_EP_SINK)
				dapm_widget_invalidate_output_paths(w);
			if (w->is_ep & SND_SOC_DAPM_EP_SOURCE)
				dapm_widget_invalidate_input_paths(w);
		}
	}
……
}

int snd_soc_suspend(struct device *dev)
{
    /* we're going to block userspace touching us until resume completes */
	snd_power_change_state(card->snd_card, SNDRV_CTL_POWER_D3hot);
……
	/* Recheck all endpoints too, their state is affected by suspend */
	dapm_mark_endpoints_dirty(card);
	snd_soc_dapm_sync(&card->dapm);
……
}

const struct dev_pm_ops snd_soc_pm_ops = {
	.suspend = snd_soc_suspend,
	.resume = snd_soc_resume,
	.freeze = snd_soc_suspend,
	.thaw = snd_soc_resume,
	.poweroff = snd_soc_poweroff,
	.restore = snd_soc_resume,
};
EXPORT_SYMBOL_GPL(snd_soc_pm_ops);

/* ASoC platform driver */
static struct platform_driver soc_driver = {
	.driver		= {
		.name		= "soc-audio",
		.pm		= &snd_soc_pm_ops,
	},
	.probe		= soc_probe,
	.remove		= soc_remove,
};

那么很明显,对于widgets的suspend作用就在于此,只要设置end point类型widget的ignore_suspend为1,则可以使这个端点之内的所有widget不受suspend的影响,否则当suspend的时候,会把该端点widget加入到dirty list中,再调用check power重新刷新widget链路,同时由于suspend时会先设置声卡的状态为SNDRV_CTL_POWER_D3hot,所以再去检查端点的通断时会返回0

static __always_inline int is_connected_ep(struct snd_soc_dapm_widget *widget,
	struct list_head *list, enum snd_soc_dapm_direction dir,
	int (*fn)(struct snd_soc_dapm_widget *, struct list_head *,
		  bool (*custom_stop_condition)(struct snd_soc_dapm_widget *,
						enum snd_soc_dapm_direction)),
	bool (*custom_stop_condition)(struct snd_soc_dapm_widget *,
				      enum snd_soc_dapm_direction))
{
……
	if ((widget->is_ep & SND_SOC_DAPM_DIR_TO_EP(dir)) && widget->connected) {
		widget->endpoints[dir] = snd_soc_dapm_suspend_check(widget);
		return widget->endpoints[dir];
	}
……
}

static int snd_soc_dapm_suspend_check(struct snd_soc_dapm_widget *widget)
{
	int level = snd_power_get_state(widget->dapm->card->snd_card);

	switch (level) {
	case SNDRV_CTL_POWER_D3hot:
	case SNDRV_CTL_POWER_D3cold:
		if (widget->ignore_suspend)
			dev_dbg(widget->dapm->dev, "ASoC: %s ignoring suspend\n",
				widget->name);
		return widget->ignore_suspend;
	default:
		return 1;
	}
}

从而,设置了ignore_suspend则会导致d->bias_leveld->target_bias_levelSND_SOC_BIAS_ON从而不会codec下电:

int snd_soc_suspend(struct device *dev)
{
……
		/* If there are paths active then the CODEC will be held with
		 * bias _ON and should not be suspended. */
		if (!codec->suspended) {
		    /* 这里为SND_SOC_BIAS_ON */
			switch (snd_soc_dapm_get_bias_level(dapm)) {
			case SND_SOC_BIAS_STANDBY:
				/*
				 * If the CODEC is capable of idle
				 * bias off then being in STANDBY
				 * means it's doing something,
				 * otherwise fall through.
				 */
				if (dapm->idle_bias_off) {
					dev_dbg(codec->dev,
						"ASoC: idle_bias_off CODEC on over suspend\n");
					break;
				}

			case SND_SOC_BIAS_OFF:
				if (codec->driver->suspend)
					codec->driver->suspend(codec);
				codec->suspended = 1;
				if (codec->component.regmap)
					regcache_mark_dirty(codec->component.regmap);
				/* deactivate pins to sleep state */
				pinctrl_pm_select_sleep_state(codec->dev);
				break;
			default:
				dev_dbg(codec->dev,
					"ASoC: CODEC is on over suspend\n");
				break;
			}
		}
……
}

总之简单来说,如果有些功能期望在cpu执行休眠后还要能用的链路一定要设置ignore_suspend标志,否则cpu休眠后其就被下电了。

6 ASoC关于widgets的变更

前面提到的很多内容都可以从alsa官方的更新邮件中找到缘由,这些东西是通过看代码无法获知的,因为很多改动的历史原因都是为了解决当时碰到的具体问题,而这些问题都如实的记录在了这里,这里列举了大部分跟前面分析相关的邮件。

ASoC: Allow DAI links to be kept active over suspend
ASoC: Support leaving paths enabled over system suspend
ASoC: Refactor DAPM suspend handling
ASoC: dapm: Implement mixer input auto-disable
ASoC: dapm: Introduce toplevel widget categories
ASoC: dapm: Mark endpoints instead of IO widgets dirty during
ASoC: dapm: Use more aggressive caching
ASoC: dapm: Add demux support
ASoC: dapm: Add widget path iterators
ASoC: dapm: Consolidate input and output path handling

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值