Audio System 五 之 DAPM分析


十、DAPM分析

10.1 DAPM简介

DAPM是 Dynamic Audio Power Management 的缩写,直译过来就是动态音频电源管理的意思,
DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态下。

DAPM对用户空间的应用程序来说是透明的,所有与电源相关的开关都在ASoc core中完成。
用户空间的应用程序无需对代码做出修改,也无需重新编译,DAPM根据当前激活的音频流(playback/capture)和声卡中的mixer等的配置来决定那些音频控件的电源开关被打开或关闭。

DAPM是基于kcontrol改进过后的相应框架,增加了相应的电源管理机制,其电源管理机制其实就是按照相应的音频路径,完美的对各种部件的电源进行控制,而且按照某种顺序进行。


10.2 kcontrol

通常,一个kcontrol代表着一个mixer(混音器),或者是一个mux(多路开关),又或者是一个音量控制器等等。
从上述文章中我们知道,定义一个kcontrol主要就是定义一个snd_kcontrol_new 结构,

[->/include/sound/control.h]
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;
};

对于每个控件,我们需要定义一个和他对应的snd_kcontrol_new结构,
这些snd_kcontrol_new结构会在声卡的初始化阶段,
通过snd_soc_dapm_new_controls()函数注册到系统中,
用户空间就可以通过tinymix查看和设定这些控件的状态。

编译 /external/tinyalsa/ 得到 tinymix, tinyplay, tinycap,Push 到手机执行 tinymix 可得到如下类似信息。

......
990		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia10    Off
991		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia11    Off
992		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia12    Off
993		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia13    Off
994		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia14    Off
995		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia15    Off
996		BOOL	1	QUAT_MI2S_RX Audio Mixer MultiMedia16    Off
997		BOOL	1	MI2S_RX Audio Mixer MultiMedia1          Off
998		BOOL	1	MI2S_RX Audio Mixer MultiMedia2          Off
999		BOOL	1	MI2S_RX Audio Mixer MultiMedia3          Off
1000	BOOL	1	MI2S_RX Audio Mixer MultiMedia4          Off
1001	BOOL	1	MI2S_RX Audio Mixer MultiMedia5          Off
1002	BOOL	1	MI2S_RX Audio Mixer MultiMedia6          Off
......

snd_kcontrol_new 结构中,几个主要的字段是 get,put,private_value,

  • get
    get 回调函数用于获取该控件当前的状态值,

  • put
    而 put 回调函数则用于设置控件的状态值,

  • private_value
    而private_value字段则根据不同的控件类型有不同的意义,
    比如对于普通的控件,private_value字段可以用来定义该控件所对应的寄存器的地址以及对应的控制位在寄存器中的位置信息。

值得庆幸的是,ASoc系统已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于include/sound/soc.h中。
下面我们分别讨论一下如何用这些预设的宏定义来定义一些常用的控件。


10.2.1 简单型的控件


10.2.1.1 SOC_SINGLE

SOC_SINGLE应该算是最简单的控件了,这种控件只有一个控制量,
比如一个开关,或者是一个数值变量(比如Codec中某个频率,FIFO大小等等)。
我们看看这个宏是如何定义的:

[->/include/sound/soc.h]
#define SOC_SINGLE(xname, reg, shift, max, invert) \
{	.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
	.info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
	.put = snd_soc_put_volsw, \
	.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }

宏定义的参数分别是:
xname(该控件的名字),
reg(该控件对应的寄存器的地址),
shift(控制位在寄存器中的位移),
max(控件可设置的最大值),
invert(设定值是否逻辑取反)。
这里又使用了一个宏来定义private_value字段:SOC_SINGLE_VALUE,我们看看它的定义:

[->/include/sound/soc.h]
#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_SINGLE_VALUE(xreg, xshift, xmax, xinvert, xautodisable) \
	SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert, xautodisable)

这里实际上是定义了一个soc_mixer_control结构,然后把该结构的地址赋值给了private_value字段,soc_mixer_control结构是这样的:

[->/include/sound/soc.h]
/* mixer control */
struct soc_mixer_control {
	int min, max, platform_max;
	int reg, rreg;
	unsigned int shift, rshift;
	unsigned int sign_bit;
	unsigned int invert:1;
	unsigned int autodisable:1;
};

看来soc_mixer_control是控件特征的真正描述者,它确定了该控件对应寄存器的地址,位移值,最大值和是否逻辑取反等特性,控件的put回调函数和get回调函数需要借助该结构来访问实际的寄存器。

10.2.1.2 SOC_SINGLE_TLV SOC_SINGLE_TLV

SOC_SINGLE_TLV SOC_SINGLE_TLV 是 SOC_SINGLE 的一种扩展,
主要用于定义那些有增益控制的控件,例如音量控制器,EQ均衡器等等。

[->/include/sound/soc.h]
#define SOC_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array) \
{	.iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
	.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\
		 SNDRV_CTL_ELEM_ACCESS_READWRITE,\
	.tlv.p = (tlv_array), \
	.info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\
	.put = snd_soc_put_volsw, \
	.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }

从他的定义可以看出,用于设定寄存器信息的private_value字段的定义和SOC_SINGLE是一样的,
甚至put、get回调函数也是使用同一套,
唯一不同的是增加了一个tlv_array参数,并把它赋值给了tlv.p字段。

用户空间可以通过对声卡的control设备发起以下两种ioctl来访问tlv字段所指向的数组:
• SNDRV_CTL_IOCTL_TLV_READ
• SNDRV_CTL_IOCTL_TLV_WRITE
• SNDRV_CTL_IOCTL_TLV_COMMAND


10.2.1.2 SOC_DOUBLE

SOC_DOUBLE 与SOC_SINGLE相对应,
区别是SOC_SINGLE只控制一个变量,
而SOC_DOUBLE则可以同时在一个寄存器中控制两个相似的变量,

最常用的就是用于一些立体声的控件,我们需要同时对左右声道进行控制,因为多了一个声道,参数也就相应地多了一个shift位移值.


10.2.1.3 SOC_DOUBLE_R

SOC_DOUBLE_R 与 SOC_DOUBLE 类似,对于左右声道的控制寄存器不一样的情况,
使用 SOC_DOUBLE_R 来定义,参数中需要指定两个寄存器地址。


10.2.1.4 SOC_DOUBLE_TLV

SOC_DOUBLE_TLV 与 SOC_SINGLE_TLV对应的立体声版本,通常用于立体声音量控件的定义。


10.2.1.5 SOC_DOUBLE_R_TLV

SOC_DOUBLE_R_TLV 左右声道有独立寄存器控制的SOC_DOUBLE_TLV版本



10.2.2 Mixer控件

Mixer控件用于音频通道的路由控制,由多个输入和一个输出组成,
多个输入可以自由地混合在一起,形成混合后的输出:
在这里插入图片描述
对于Mixer控件,我们可以认为是多个简单控件的组合,
通常,我们会为mixer的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反应在代码上,就是定义一个soc_kcontrol_new数组:

[->/sound/soc/codecs/wcd9335.c]
static const struct snd_kcontrol_new aif4_vi_mixer[] = {
	SOC_SINGLE_EXT("SPKR_VI_1", SND_SOC_NOPM, TASHA_TX14, 1, 0,
			tasha_vi_feed_mixer_get, tasha_vi_feed_mixer_put),
	SOC_SINGLE_EXT("SPKR_VI_2", SND_SOC_NOPM, TASHA_TX15, 1, 0,
			tasha_vi_feed_mixer_get, tasha_vi_feed_mixer_put),
};

10.2.3 Mux控件

mux控件与mixer控件类似,也是多个输入端和一个输出端的组合控件.
与mixer控件不同的是,mux控件的多个输入端同时只能有一个被选中。

因此,mux控件所对应的寄存器,通常可以设定一段连续的数值,每个不同的数值对应不同的输入端被打开,
与上述的mixer控件不同,ASoc用soc_enum结构来描述mux控件的寄存器信息:

[->/include/sound/soc.h]
/* enumerated kcontrol */
struct soc_enum {
	int reg;
	unsigned char shift_l;
	unsigned char shift_r;
	unsigned int items;
	unsigned int mask;
	const char * const *texts;
	const unsigned int *values;
};

两个寄存器地址和位移字段:reg,reg2,shift_l,shift_r,用于描述左右声道的控制寄存器信息。

字符串数组指针用于描述每个输入端对应的名字,value字段则指向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果value是一组连续的值,通常我们可以忽略values参数。


10.3 widget、path、route

前面一节中,我们介绍了音频驱动中对基本控制单元的封装:kcontrol。

利用kcontrol,我们可以完成对音频系统中的mixer,mux,音量控制,音效控制,以及各种开关量的控制,
通过对各种kcontrol的控制,使得音频硬件能够按照我们预想的结果进行工作。


同时我们可以看到,kcontrol还是有以下几点不足:

  • 只能描述自身,无法描述各个kcontrol之间的连接关系;
  • 没有相应的电源管理机制;
  • 没有相应的时间处理机制来响应播放、停止、上电、下电等音频事件;
  • 为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序;
  • 当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol;

为此,DAPM框架正是为了要解决以上这些问题而诞生的,DAPM目前已经是ASoc中的重要组成部分,
让我们先从DAPM的数据结构开始,了解它的设计思想和工作原理。


10.3.1 DAPM的基本单元:widget

文章的开头,我们说明了一下目前kcontrol的一些不足,
而DAPM框架为了解决这些问题,引入了widget这一概念。

所谓widget,其实可以理解为是kcontrol的进一步升级和封装,它同样是指音频系统中的某个部件,
比如mixer,mux,输入输出引脚,电源供应器等等,甚至,我们可以定义虚拟的widget,例如playback stream widget。

widget 把 kcontrol 和动态电源管理进行了有机的结合,同时还具备音频路径的连结功能,一个widget可以与它相邻的widget有某种动态的连结关系。

在DAPM框架中,widget用结构体snd_soc_dapm_widget来描述:

[->/include/sound/soc-dapm.h]
/* dapm widget */
struct snd_soc_dapm_widget {
	enum snd_soc_dapm_type id;
	const char *name;		/* widget name */
	const char *sname;	/* stream name */
	struct snd_soc_codec *codec;
	struct list_head list;
	struct snd_soc_dapm_context *dapm;

	void *priv;				/* widget specific data */
	struct regulator *regulator;		/* attached regulator */
	const struct snd_soc_pcm_stream *params; /* params for dai links */

	/* dapm control */
	int reg;				/* negative reg = no direct dapm */
	unsigned char shift;			/* bits to shift */
	unsigned int mask;			/* non-shifted mask */
	unsigned int on_val;			/* on state value */
	unsigned int off_val;			/* off state value */
	unsigned char power:1;			/* block power status */
	unsigned char active:1;			/* active stream on DAC, ADC's */
	unsigned char connected:1;		/* connected codec pin */
	unsigned char new:1;			/* cnew complete */
	unsigned char ext:1;			/* has external widgets */
	unsigned char force:1;			/* force state */
	unsigned char ignore_suspend:1;         /* kept enabled over suspend */
	unsigned char new_power:1;		/* power from this run */
	unsigned char power_checked:1;		/* power checked this run */
	int subseq;				/* sort within widget type */
	......
	/* widget input and outputs */
	struct list_head sources;
	struct list_head sinks;
	......
};

snd_soc_dapm_widget结构比较大,为了简洁一些,这里我没有列出该结构体的完整字段,不过不用担心,下面我会说明每个字段的意义:

  1. id
    该widget的类型值,比如snd_soc_dapm_output,snd_soc_dapm_mixer等等。

  2. *name 该widget的名字

  3. *sname
    代表该widget所在stream的名字,比如对于snd_soc_dapm_dai_in类型的widget,会使用该字段。

  4. codec platform
    指向该widget所属的codec和platform。

  5. list
    所有注册到系统中的widget都会通过该list,链接到代表声卡的snd_soc_card结构的widgets链表头字段中。

  6. *dapm snd_soc_dapm_context
    结构指针,ASoc把系统划分为多个dapm域,每个widget属于某个dapm域,同一个域代表着同样的偏置电压供电策略,
    比如,同一个codec中的widget通常位于同一个dapm域,而平台上的widget可能又会位于另外一个platform域中。

  7. *priv
    有些widget可能需要一些专有的数据,可以使用该字段来保存,像snd_soc_dapm_dai_in类型的widget,会使用该字段来记住与之相关联的snd_soc_dai结构指针。

  8. *regulator
    对于snd_soc_dapm_regulator_supply类型的widget,该字段指向与之相关的regulator结构指针。

  9. *params
    目前对于snd_soc_dapm_dai_link类型的widget,指向该dai的配置信息的snd_soc_pcm_stream结构。

  10. reg shift mask
    这3个字段用来控制该widget的电源状态,分别对应控制信息所在的寄存器地址,位移值和屏蔽值。

  11. value on_val off_val
    电源状态的当前只,开启时和关闭时所对应的值。

  12. power invert
    用于指示该widget当前是否处于上电状态,invert则用于表明power字段是否需要逻辑反转。

  13. active connected
    分别表示该widget是否处于激活状态和连接状态,
    当和相邻的widget有连接关系时,connected位会被置1,否则置0。

new 我们定义好的widget(snd_soc_dapm_widget结构),在注册到声卡中时需要进行实例化,该字段用来表示该widget是否已经被实例化。

  1. ext
    表示该widget当前是否有外部连接,比如连接mic,耳机,喇叭等等。

  2. force
    该位被设置后,将会不管widget当前的状态,强制更新至新的电源状态。

  3. ignore_suspend new_power power_checked
    这些电源管理相关的字段。

  4. subseq
    该widget目前在上电或下电队列中的排序编号,
    为了防止在上下电的过程中出现pop-pop声,DAPM会给每个widget分配合理的上下电顺序。

  5. *power_check
    用于检查该widget是否应该上电或下电的回调函数指针。
    event_flags 该字段是一个位或字段,每个位代表该widget会关注某个DAPM事件通知。
    只有被关注的通知事件会被发送到widget的事件处理回调函数中。

  6. *event
    DAPM事件处理回调函数指针。

  7. num_kcontrols / kcontrol_news / *kcontrols
    这3个字段用来描述与该widget所包含的kcontrol控件,例如一个mixer控件或者是一个mux控件。

  8. sources sinks
    两个链表字段,两个widget如果有连接关系,会通过一个snd_soc_dapm_path结构进行连接,
    sources链表用于链接所有的输入path,sinks链表用于链接所有的输出path。

  9. power_list
    每次更新整个dapm的电源状态时,会根据一定的算法扫描所有的widget,
    然后把需要变更电源状态的widget利用该字段链接到一个上电或下电的链表中,
    扫描完毕后,dapm系统会遍历这两个链表执行相应的上电或下电操作。

  10. dirty
    链表字段,widget的状态变更后,dapm系统会利用该字段,
    把该widget加入到一个dirty链表中,稍后会对dirty链表进行扫描,以执行整个路径的更新。

  11. inputs
    该widget的所有有效路径中,连接到输入端的路径数量。

  12. outputs
    该widget的所有有效路径中,连接到输出端的路径数量。

  13. *clk
    对于snd_soc_dapm_clock_supply类型的widget,指向相关联的clk结构指针。

以上我们对snd_soc_dapm_widget结构的各个字段所代表的意义一一做出了说明,这里只是让大家现有个概念


10.3.2 widget的种类

在DAPM框架中,把各种不同的widget划分为不同的种类,
snd_soc_dapm_widget结构中的id字段用来表示该widget的种类,可选的种类都定义在一个枚举中:

[->/include/sound/soc-dapm.h]
/* dapm widget types */
enum snd_soc_dapm_type {
	snd_soc_dapm_input = 0,		/* input pin */
	snd_soc_dapm_output,		/* output pin */
	......

下面我们逐个解释一下这些widget的种类:

snd_soc_dapm_input 				该widget对应一个输入引脚。
snd_soc_dapm_output 			该widget对应一个输出引脚。
snd_soc_dapm_mux 				该widget对应一个mux控件。
snd_soc_dapm_virt_mux 			该widget对应一个虚拟的mux控件。
snd_soc_dapm_value_mux 			该widget对应一个value类型的mux控件。
snd_soc_dapm_mixer 				该widget对应一个mixer控件。
snd_soc_dapm_mixer_named_ctl 	该widget对应一个mixer控件,\
									但是对应的kcontrol的名字不会加入widget的名字作为前缀。
snd_soc_dapm_pga 				该widget对应一个pga控件(可编程增益控件)。
snd_soc_dapm_out_drv 			该widget对应一个输出驱动控件
snd_soc_dapm_adc 				该widget对应一个ADC
snd_soc_dapm_dac 				该widget对应一个DAC
snd_soc_dapm_micbias 			该widget对应一个麦克风偏置电压控件
snd_soc_dapm_mic 				该widget对应一个麦克风。
snd_soc_dapm_hp 				该widget对应一个耳机。
snd_soc_dapm_spk 				该widget对应一个扬声器。
snd_soc_dapm_line 				该widget对应一个线路输入。
snd_soc_dapm_switch 			该widget对应一个模拟开关。
snd_soc_dapm_vmid 				该widget对应一个codec的vmid偏置电压。
snd_soc_dapm_pre 				machine级别的专用widget,会先于其它widget执行检查操作。
snd_soc_dapm_post 				machine级别的专用widget,会后于其它widget执行检查操作。
snd_soc_dapm_supply 			对应一个电源或是时钟源。
snd_soc_dapm_regulator_supply 	对应一个外部regulator稳压器。
snd_soc_dapm_clock_supply 		对应一个外部时钟源。
snd_soc_dapm_aif_in 			对应一个数字音频输入接口,比如I2S接口的输入端。
snd_soc_dapm_aif_out 			对应一个数字音频输出接口,比如I2S接口的输出端。
snd_soc_dapm_siggen 			对应一个信号发生器。
snd_soc_dapm_dai_in 			对应一个platform或codec域的输入DAI结构。
snd_soc_dapm_dai_out 			对应一个platform或codec域的输出DAI结构。
snd_soc_dapm_dai_link 			用于链接一对输入/输出DAI结构。

10.3.3 widget之间的连接器:path

之前已经提到,一个widget是有输入和输出的,而且widget之间是可以动态地进行连接的,那它们是用什么来连接两个widget的呢?

DAPM为我们提出了path这一概念,path相当于电路中的一根跳线,它把一个widget的输出端和另一个widget的输入端连接在一起,

path用snd_soc_dapm_path结构来描述:

[->/include/sound/soc-dapm.h]
/* dapm audio path between two widgets */
struct snd_soc_dapm_path {
	const char *name;

	/* source (input) and sink (output) widgets */
	struct snd_soc_dapm_widget *source;
	struct snd_soc_dapm_widget *sink;

	/* status */
	u32 connect:1;	/* source and sink widgets are connected */
	u32 walked:1;	/* path has been walked */
	u32 walking:1;  /* path is in the process of being walked */
	u32 weak:1;	/* path ignored for power management */

	int (*connected)(struct snd_soc_dapm_widget *source, struct snd_soc_dapm_widget *sink);

	struct list_head list_source;
	struct list_head list_sink;
	struct list_head list_kcontrol;
	struct list_head list;
};

当widget之间发生连接关系时,snd_soc_dapm_path 作为连接者,
它的source字段会指向该连接的起始端 widget,而它的sink字段会指向该连接的到达端 widget,

还记得前面 snd_soc_dapm_widget 结构中的两个链表头字段:sources 和 sinks 么?

widget 的输入端和输出端可能连接着多个 path,所有输入端的 snd_soc_dapm_path 结构通过 list_sink 字段挂在widget 的 souces 链表中,同样,所有输出端的snd_soc_dapm_path结构通过list_source字段挂在widget的sinks链表中。


这里可能大家会被搞得晕呼呼的,一会source,一会sink,不要紧,只要记住,
连接的路径是这样的: 起始端widget的输出–>path的输入–>path的输出–>到达端widget输入。

在这里插入图片描述

另外,snd_soc_dapm_path结构的list字段用于把所有的path注册到声卡中,其实就是挂在snd_soc_card结构的paths链表头字段中。
如果你要自己定义方法来检查path的当前连接状态,你可以提供自己的connected回调函数指针。

connect,walked,walking,weak是几个辅助字段,用于帮助所有path的遍历。


10.3.4 widget的连接关系:route

通过上一节的内容,我们知道,一个路径的连接至少包含以下几个元素:
起始端widget,跳线path,到达端widget。

在DAPM中,用snd_soc_dapm_route结构来描述这样一个连接关系:

[->/include/sound/soc-dapm.h]
struct snd_soc_dapm_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);
};
  • sink 指向到达端widget的名字字符串,
  • source 指向起始端widget的名字字符串,
  • control 指向负责控制该连接所对应的kcontrol名字字符串,
  • connected 回调则定义了上一节所提到的自定义连接检查回调函数。

该结构的意义很明显就是:
source通过一个kcontrol,和sink连接在一起,现在是否处于连接状态,请调用connected回调函数检查。

这里直接使用名字字符串来描述连接关系,所有定义好的route,最后都要注册到dapm系统中,
dapm会根据这些名字找出相应的widget,并动态地生成所需要的snd_soc_dapm_path结构,
正确地处理各个链表和指针的关系,实现两个widget之间的连接


10.4 建立widget之间的连接关系

前面我们主要着重于codec、platform、machine驱动程序中如何使用和建立dapm所需要的widget,route,这些是音频驱动开发人员必须要了解的内容,
经过前几章的介绍,我们应该知道如何在alsa音频驱动的3大部分(codec、platform、machine)中,按照所使用的音频硬件结构,定义出相应的widget,kcontrol,以及必要的音频路径,
而在本节中,我们将会深入dapm的核心部分,看看各个widget之间是如何建立连接关系,形成一条完整的音频路径。

前面我们已经简单地介绍过,驱动程序需要使用以下api函数创建widget:

  • snd_soc_dapm_new_controls()
    实际上,这个函数只是创建widget的第一步,它为每个widget分配内存,初始化必要的字段,然后把这些widget挂在代表声卡的snd_soc_card的widgets链表字段中。

要使widget之间具备连接能力,我们还需要第二个函数:

  • snd_soc_dapm_new_widgets()
    这个函数会根据widget的信息,创建widget所需要的dapm kcontrol,这些dapm kcontol的状态变化,代表着音频路径的变化,从而影响着各个widget的电源状态。

看到函数的名称可能会迷惑一下,实际上,snd_soc_dapm_new_controls的作用更多地是创建widget,而snd_soc_dapm_new_widget的作用则更多地是创建widget所包含的kcontrol,所以在我看来,这两个函数名称应该换过来叫更好!下面我们分别介绍一下这两个函数是如何工作的。

10.4.1 创建widget

snd_soc_dapm_new_controls()函数完成widget的创建工作,
并把这些创建好的widget注册在声卡的widgets链表中,

我们看看他的定义:

[->/sound/soc/soc-dapm.c]
int snd_soc_dapm_new_controls(struct snd_soc_dapm_context *dapm,
	const struct snd_soc_dapm_widget *widget,
	int num)
{
	struct snd_soc_dapm_widget *w;

	mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);
	for (i = 0; i < num; i++) {
		w = snd_soc_dapm_new_control(dapm, widget);
		if (!w) {
			dev_err(dapm->dev, "ASoC: Failed to create DAPM control %s\n", widget->name);
			ret = -ENOMEM;
			break;
		}
		widget++;
	}
	mutex_unlock(&dapm->card->dapm_mutex);
	return ret;
}

该函数只是简单的一个循环,为传入的widget模板数组依次调用snd_soc_dapm_new_control函数,
实际的工作由snd_soc_dapm_new_control完成,继续进入该函数,看看它做了那些工作。

我们之前已经说过,驱动中定义的snd_soc_dapm_widget数组,只是作为一个模板,所以,snd_soc_dapm_new_control所做的第一件事,就是为该widget重新分配内存,并把模板的内容拷贝过来:

[->/sound/soc/soc-dapm.c]
static struct snd_soc_dapm_widget *
snd_soc_dapm_new_control(struct snd_soc_dapm_context *dapm,
			 const struct snd_soc_dapm_widget *widget)
{
	struct snd_soc_dapm_widget *w;
	const char *prefix;
	int ret;

	if ((w = dapm_cnew_widget(widget)) == NULL)
		return NULL;
    //由dapm_cnew_widget完成内存申请和拷贝模板的动作。接下来,根据widget的类型做不同的处理:
	switch (w->id) {
	case snd_soc_dapm_regulator_supply:
	......
    }
	prefix = soc_dapm_prefix(dapm);
	//对于snd_soc_dapm_regulator_supply类型的widget,根据widget的名称获取对应的regulator结构,对于snd_soc_dapm_clock_supply类型的widget,根据widget的名称,获取对应的clock结构。接下来,根据需要,在widget的名称前加入必要的前缀:
	if (prefix) {
		w->name = kasprintf(GFP_KERNEL, "%s %s", prefix, widget->name);
		if (widget->sname)
			w->sname = kasprintf(GFP_KERNEL, "%s %s", prefix,
					     widget->sname);
	} else {
		w->name = kasprintf(GFP_KERNEL, "%s", widget->name);
		if (widget->sname)
			w->sname = kasprintf(GFP_KERNEL, "%s", widget->sname);
	}
	......

在这里插入图片描述

当音频路径发生变化时,power_check回调会被调用,用于检查该widget的电源状态是否需要更新。
power_check设置完成后,需要设置widget所属的codec、platform和dapm context,
几个用于音频路径的链表也需要初始化,然后,把该widget加入到声卡的widgets链表中:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_control()
w->dapm = dapm;  
w->codec = dapm->codec;  
w->platform = dapm->platform;  
INIT_LIST_HEAD(&w->sources);  
INIT_LIST_HEAD(&w->sinks);  
INIT_LIST_HEAD(&w->list);  
INIT_LIST_HEAD(&w->dirty);  
list_add(&w->list, &dapm->card->widgets);

几个链表的作用如下:
sources 用于链接所有连接到该widget输入端的snd_soc_path结构
sinks 用于链接所有连接到该widget输出端的snd_soc_path结构
list 用于链接到声卡的widgets链表
dirty 用于链接到声卡的dapm_dirty链表
最后,把widget设置为connect状态:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_control()
/* machine layer set ups unconnected pins and insertions */  
w->connected = 1;  
return w;

connected字段代表着引脚的连接状态,目前,只有以下这些widget使用connected字段:
snd_soc_dapm_output
snd_soc_dapm_input
snd_soc_dapm_hp
snd_soc_dapm_spk
snd_soc_dapm_line
snd_soc_dapm_vmid
snd_soc_dapm_mic
snd_soc_dapm_siggen

驱动程序可以使用以下这些api来设置引脚的连接状态:
snd_soc_dapm_enable_pin
snd_soc_dapm_force_enable_pin
snd_soc_dapm_disable_pin
snd_soc_dapm_nc_pin

到此,widget已经被正确地创建并初始化,而且被挂在声卡的widgets链表中,
以后我们就可以通过声卡的widgets链表来遍历所有的widget,
再次强调一下snd_soc_dapm_new_controls函数所完成的主要功能:

  • 为widget分配内存,并拷贝参数中传入的在驱动中定义好的模板
  • 设置power_check回调函数
  • 把widget挂在声卡的widgets链表中

10.4.2 为widget建立dapm kcontrol

定义一个widget,我们需要指定两个很重要的内容:
一个是用于控制widget的电源状态的reg/shift等寄存器信息,
另一个是用于控制音频路径切换的dapm kcontrol信息,
这些dapm kcontrol有它们自己的reg/shift寄存器信息用于切换widget的路径连接方式。

前一节的内容中,我们只是创建了widget的实例,并把它们注册到声卡的widgts链表中,
但是到目前为止,包含在widget中的dapm kcontrol并没有建立起来,
dapm框架在声卡的初始化阶段,等所有的widget(包括machine、platform、codec)都创建好之后,
通过snd_soc_dapm_new_widgets函数,创建widget内包含的dapm kcontrol,
并初始化widget的初始电源状态和音频路径的初始连接状态。

我们看看声卡的初始化函数,都有那些初始化与dapm有关:

[->/sound/soc/soc-dapm.c]
static int snd_soc_instantiate_card(struct snd_soc_card *card)  
{  
        ......  
        /* card bind complete so register a sound card */  
        ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, card->owner, 0, &card->snd_card);  
        ......  
   
        card->dapm.bias_level = SND_SOC_BIAS_OFF;  
        card->dapm.dev = card->dev;  
        card->dapm.card = card;  
        list_add(&card->dapm.list, &card->dapm_list);  
        ......  
        if (card->dapm_widgets)    /* 创建machine级别的widget  */  
                snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,  
                                          card->num_dapm_widgets);  
        ......  
        snd_soc_dapm_link_dai_widgets(card);  /*  连接dai widget  */  
  
        if (card->controls)    /*  建立machine级别的普通kcontrol控件  */  
                snd_soc_add_card_controls(card, card->controls, card->num_controls);  
  
        if (card->dapm_routes)    /*  注册machine级别的路径连接信息  */  
                snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,  
                                        card->num_dapm_routes);  
        ......  
  
        if (card->fully_routed)    /*  如果该标志被置位,自动把codec中没有路径连接信息的引脚设置为无用widget  */  
                list_for_each_entry(codec, &card->codec_dev_list, card_list)  
                        snd_soc_dapm_auto_nc_codec_pins(codec);  
  
        snd_soc_dapm_new_widgets(card);    /*初始化widget包含的dapm kcontrol、电源状态和连接状态*/  
  
        ret = snd_card_register(card->snd_card);  
        ......  
        card->instantiated = 1;  
        snd_soc_dapm_sync(&card->dapm);  
        ......  
        return 0;  
}

正如我添加的注释中所示,在完成machine级别的widget和route处理之后,
调用的snd_soc_dapm_new_widgets函数,来为所有已经注册的widget初始化他们所包含的dapm kcontrol,
并初始化widget的电源状态和路径连接状态。

下面我们看看snd_soc_dapm_new_widgets函数的工作过程。


10.4.2.1 snd_soc_dapm_new_widgets()函数

该函数通过声卡的widgets链表,遍历所有已经注册了的widget,其中的new字段用于判断该widget是否已经执行过。

snd_soc_dapm_new_widgets函数,如果num_kcontrols字段有数值,表明该widget包含有若干个dapm kcontrol,那么就需要为这些kcontrol分配一个指针数组,并把数组的首地址赋值给widget的kcontrols字段,该数组存放着指向这些kcontrol的指针,当然现在这些都是空指针,因为实际的kcontrol现在还没有被创建:

[->/sound/soc/soc-dapm.c]
int snd_soc_dapm_new_widgets(struct snd_soc_card *card)  
{  
	......  
	list_for_each_entry(w, &card->widgets, list)  
	{                 
		if (w->new) continue;                  
		if (w->num_kcontrols) {  
			w->kcontrols = kzalloc(w->num_kcontrols * sizeof(struct snd_kcontrol *),  GFP_KERNEL);  
			......  
		}

接着,对几种能影响音频路径的widget,创建并初始化它们所包含的dapm kcontrol:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
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_virt_mux:  
case snd_soc_dapm_value_mux:  
        dapm_new_mux(w);  
        break;  
case snd_soc_dapm_pga:  
case snd_soc_dapm_out_drv:  
        dapm_new_pga(w);  
        break;  
default:  
        break;  
}

需要用到的创建函数分别是:
dapm_new_mixer() 对于mixer类型,用该函数创建dapm kcontrol;
dapm_new_mux() 对于mux类型,用该函数创建dapm kcontrol;
dapm_new_pga() 对于pga类型,用该函数创建dapm kcontrol;

然后,根据widget寄存器的当前值,初始化widget的电源状态,并设置到power字段中:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
/* Read the initial power state from the device */  
if (w->reg >= 0) {  
        val = soc_widget_read(w, w->reg) >> w->shift;  
        val &= w->mask;  
        if (val == w->on_val)  
                w->power = 1;  
}

接着,设置 new 字段,表明该 widget 已经初始化完成,
我们还要把该 widget 加入到声卡的 dapm_dirty 链表中,表明该widget的状态发生了变化,稍后在合适的时刻,dapm框架会扫描dapm_dirty链表,统一处理所有已经变化的widget。为什么要统一处理?因为dapm要控制各种widget的上下电顺序,同时也是为了减少寄存器的读写次数(多个widget可能使用同一个寄存器):

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
w->new = 1;  
  
dapm_mark_dirty(w, "new widget");  
dapm_debugfs_add_widget(w);

最后,通过dapm_power_widgets函数,统一处理所有位于dapm_dirty链表上的widget的状态改变:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);  
......  
return 0;

最后,通过dapm_power_widgets函数,统一处理所有位于dapm_dirty链表上的widget的状态改变:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);  
......  
return 0;

最后通过dapm_power_widgets函数,统一处理所有位于dapm_dirty链表上的widget的状态改变:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_new_widgets()]
dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);  
......  
return 0;

10.4.2.2 dapm mixer kcontrol

上一节中,我们提到,对于mixer类型的dapm kcontrol,
我们会使用dapm_new_mixer来完成具体的创建工作,先看代码后分析:

[->/sound/soc/soc-dapm.c]
static int dapm_new_mixer(struct snd_soc_dapm_widget *w)  
{  
        int i, ret;  
        struct snd_soc_dapm_path *path;  
  
        /* add kcontrol */1for (i = 0; i < w->num_kcontrols; i++) {                                  
                /* match name */2list_for_each_entry(path, &w->sources, list_sink) {               
                        /* mixer/mux paths name must match control name */3if (path->name != (char *)w->kcontrol_news[i].name)       
                                continue;4if (w->kcontrols[i]) {                                   
                                dapm_kcontrol_add_path(w->kcontrols[i], path);  
                                continue;  
                        }5)                        ret = dapm_create_or_share_mixmux_kcontrol(w, i);        
                        if (ret < 0)  
                                return ret;6dapm_kcontrol_add_path(w->kcontrols[i], path);           
                }  
        }  
  
        return 0;  
}

(1) 因为一个mixer是由多个kcontrol组成的,每个kcontrol控制着mixer的一个输入端的开启和关闭,所以,该函数会根据kcontrol的数量做循环,逐个建立对应的kcontrol。

(2)(3)之前多次提到,widget之间使用snd_soc_path进行连接,widget的sources链表保存着所有和输入端连接的snd_soc_path结构,所以我们可以用kcontrol模板中指定的名字来匹配对应的snd_soc_path结构。

(4) 因为一个输入脚可能会连接多个输入源,所以可能在上一个输入源的path关联时已经创建了这个kcontrol,
所以这里判断kcontrols指针数组中对应索引中的指针值,如果已经赋值,说明kcontrol已经在之前创建好了,
所以我们只要简单地把连接该输入端的path加入到kcontrol的path_list链表中,
并且增加一个虚拟的影子widget,该影子widget连接和输入端对应的源widget,
因为使用了kcontrol本身的reg/shift等寄存器信息,
所以实际上控制的是该kcontrol的开和关,
这个影子widget只有在kcontrol的autodisable字段被设置的情况下才会被创建,
该特性使得source的关闭时,与之连接的mixer的输入端也可以自动关闭,
这个特性通过dapm_kcontrol_add_path来实现这一点:

[->/sound/soc/soc-dapm.c]
static void dapm_kcontrol_add_path(const struct snd_kcontrol *kcontrol,  
        struct snd_soc_dapm_path *path)  
{  
        struct dapm_kcontrol_data *data = snd_kcontrol_chip(kcontrol);  
        /*  把kcontrol连接的path加入到paths链表中  */  
        /*  paths链表所在的dapm_kcontrol_data结构会保存在kcontrol的private_data字段中  */  
        list_add_tail(&path->list_kcontrol, &data->paths);  
  
        if (data->widget) {  
                snd_soc_dapm_add_path(data->widget->dapm, data->widget,  
                    path->source, NULL, NULL);  
        }  
}

(5) 如果kcontrol之前没有被创建,则通过dapm_create_or_share_mixmux_kcontrol创建这个输入端的kcontrol,同理,kcontrol对应的影子widget也会通过dapm_kcontrol_add_path判断是否需要创建。


10.4.2.3 dapm mux kcontrol

因为一个widget最多只会包含一个mux类型的damp kcontrol,
所以他的创建方法稍有不同,dapm框架使用dapm_new_mux函数来创建mux类型的dapm kcontrol:

[->/sound/soc/soc-dapm.c]
static int dapm_new_mux(struct snd_soc_dapm_widget *w)  
{         
        struct snd_soc_dapm_context *dapm = w->dapm;  
        struct snd_soc_dapm_path *path;  
          
(1)     if (w->num_kcontrols != 1) {  
                dev_err(dapm->dev, "ASoC: mux %s has incorrect number of controls\n",  w->name);  
                return -EINVAL;  
        }  
  
        if (list_empty(&w->sources)) {  
                dev_err(dapm->dev, "ASoC: mux %s has no paths\n", w->name);  
                return -EINVAL;  
        }  
  
(2)     ret = dapm_create_or_share_mixmux_kcontrol(w, 0);  

(3)       list_for_each_entry(path, &w->sources, list_sink)  
                dapm_kcontrol_add_path(w->kcontrols[0], path);  
        return 0;  
}

(1) 对于mux类型的widget,因为只会有一个kcontrol,所以在这里做一下判断。
(2) 同样地,和mixer类型一样,也使用dapm_create_or_share_mixmux_kcontrol来创建这个kcontrol。
(3) 对每个输入端所连接的path都加入dapm_kcontrol_data结构的paths链表中,并且创建一个影子widget,用于支持autodisable特性。


10.4.2.4 dapm pga kcontrol

目前对于pga类型的widget,kcontrol的创建函数是个空函数,所以我们不用太关注它:

[->/sound/soc/soc-dapm.c]
static int dapm_new_pga(struct snd_soc_dapm_widget *w)
{
	if (w->num_kcontrols)
		dev_err(w->dapm->dev,
			"ASoC: PGA controls not supported: '%s'\n", w->name);
	return 0;
}

10.4.2.5 dapm_create_or_share_mixmux_kcontrol函数

上面所说的mixer类型和mux类型的widget,在创建他们所包含的dapm kcontrol时,
最后其实都是使用了dapm_create_or_share_mixmux_kcontrol函数来完成创建工作的,
所以在这里我们有必要分析一下这个函数的工作原理。

这个函数中有很大一部分代码实在处理kcontrol的名字是否要加入codec的前缀,
我们会忽略这部分的代码,感兴趣的读者可以自己查看内核的代码,

路径在:sound/soc/soc-dapm.c中,简化后的代码如下:

[->/sound/soc/soc-dapm.c]
static int dapm_create_or_share_mixmux_kcontrol(struct snd_soc_dapm_widget *w, int kci)  
{  
          ......  
(1)       shared = dapm_is_shared_kcontrol(dapm, w, &w->kcontrol_news[kci],  &kcontrol);  
     
(2)       if (!kcontrol) {  
(3)            kcontrol = snd_soc_cnew(&w->kcontrol_news[kci], NULL, name,prefix);  
               ......  
               kcontrol->private_free = dapm_kcontrol_free;  
(4)            ret = dapm_kcontrol_data_alloc(w, kcontrol);  
                ......  
(5)            ret = snd_ctl_add(card, kcontrol);  
                ......  
        }  
(6)     ret = dapm_kcontrol_add_widget(kcontrol, w);  
        ......  
(7)     w->kcontrols[kci] = kcontrol;  
        return 0;  
}

(1) 为了节省内存,通过kcontrol名字的匹配查找,如果这个kcontrol已经在其他widget中已经创建好了,那我们不再创建,dapm_is_shared_kcontrol的参数kcontrol会返回已经创建好的kcontrol的指针。
(2) 如果kcontrol指针被赋值,说明在(1)中查找到了其他widget中同名的kcontrol,我们不用再次创建,只要共享该kcontrol即可。
(3) 标准的kcontrol创建函数,
(4) 如果widget支持autodisable特性,创建与该kcontrol所对应的影子widget,该影子widget的类型是:snd_soc_dapm_kcontrol。
(5) 标准的kcontrol创建函数,
(6) 把所有共享该kcontrol的影子widget(snd_soc_dapm_kcontrol),加入到kcontrol的private_data字段所指向的dapm_kcontrol_data结构中。
(7) 把创建好的kcontrol指针赋值到widget的kcontrols数组中。

需要注意的是,如果kcontol支持autodisable特性,一旦kcontrol由于source的关闭而被自动关闭,则用户空间只能操作该kcontrol的cache值,只有该kcontrol再次打开时,该cache值才会被真正地更新到寄存器中。


现在。我们总结一下,创建一个widget所包含的kcontrol所做的工作:

  • 循环每一个输入端,为每个输入端依次执行下面的一系列操作
  • 为每个输入端创建一个kcontrol,能共享的则直接使用创建好的kcontrol
  • kcontrol的private_data字段保存着这些共享widget的信息
  • 如果支持autodisable特性,每个输入端还要额外地创建一个虚拟的snd_soc_dapm_kcontrol类型的影子widget,该影子widget也记录在private_data字段中
  • 创建好的kcontrol会依次存放在widget的kcontrols数组中,供路径的控制和匹配之用。

10.4.2.6 为widget建立连接关系

如果widget之间没有连接关系,dapm就无法实现动态的电源管理工作,
正是widget之间有了连结关系,这些连接关系形成了一条所谓的完成的音频路径,
dapm可以顺着这条路径,统一控制路径上所有widget的电源状态,

前面我们已经知道,widget之间是使用snd_soc_path结构进行连接的,
驱动要做的是定义一个snd_soc_route结构数组,该数组的每个条目描述了目的widget的和源widget的名称,
以及控制这个连接的kcontrol的名称,

最终,驱动程序使用api函数snd_soc_dapm_add_routes来注册这些连接信息,

接下来我们就是要分析该函数的具体实现方式:

[->/sound/soc/soc-dapm.c]
int snd_soc_dapm_add_routes(struct snd_soc_dapm_context *dapm, const struct snd_soc_dapm_route *route, int num)  
{  
        mutex_lock_nested(&dapm->card->dapm_mutex, SND_SOC_DAPM_CLASS_INIT);  
        for (i = 0; i < num; i++) {  
                r = snd_soc_dapm_add_route(dapm, route);  
                ......  
                route++;  
        }  
        mutex_unlock(&dapm->card->dapm_mutex);  
        return ret;  
}

该函数只是一个循环,依次对参数传入的数组调用snd_soc_dapm_add_route,
主要的工作由snd_soc_dapm_add_route完成。我们进入snd_soc_dapm_add_route函数看看:

[->/sound/soc/soc-dapm.c]
static int snd_soc_dapm_add_route(struct snd_soc_dapm_context *dapm,  
                                  const struct snd_soc_dapm_route *route)  
{  
        struct snd_soc_dapm_widget *wsource = NULL, *wsink = NULL, *w;  
        struct snd_soc_dapm_widget *wtsource = NULL, *wtsink = NULL;  
        const char *sink;  
        const char *source;  
        ......  
        list_for_each_entry(w, &dapm->card->widgets, list) {  
                if (!wsink && !(strcmp(w->name, sink))) {  
                        wtsink = w;  
                        if (w->dapm == dapm)  
                                wsink = w;  
                        continue;  
                }  
                if (!wsource && !(strcmp(w->name, source))) {  
                        wtsource = w;  
                        if (w->dapm == dapm)  
                                wsource = w;  
                }  
        }

上面的代码我再次省略了关于名称前缀的处理部分。
我们可以看到,用widget的名字来比较,遍历声卡的widgets链表,找出源widget和目的widget的指针,
这段代码虽然正确,但我总感觉少了一个判断退出循环的条件,
如果链表的开头就找到了两个widget,还是要遍历整个链表才结束循环,好浪费时间。
下面,如果在本dapm context中没有找到,则使用别的dapm context中找到的widget:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_route()]
if (!wsink)  
        wsink = wtsink;  
if (!wsource)  
        wsource = wtsource;

最后,使用来增加一条连接信息:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_route()]
        ret = snd_soc_dapm_add_path(dapm, wsource, wsink, route->control, route->connected);  
        ......  
        return 0;  
}

snd_soc_dapm_add_path函数是整个调用链条中的关键,我们来分析一下:

[->/sound/soc/soc-dapm.c]
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))  
{  
        struct snd_soc_dapm_path *path;  
  
        path = kzalloc(sizeof(struct snd_soc_dapm_path), GFP_KERNEL);  
  
        path->source = wsource;  
        path->sink = wsink;  
        path->connected = connected;  
        INIT_LIST_HEAD(&path->list);  
        INIT_LIST_HEAD(&path->list_kcontrol);  
        INIT_LIST_HEAD(&path->list_source);  
        INIT_LIST_HEAD(&path->list_sink);

函数的一开始,首先为这个连接分配了一个 snd_soc_path 结构,
path 的 source 和 sink 字段分别指向源 widget 和目的 widget,
connected 字段保存 connected 回调函数,
初始化几个 snd_soc_path 结构中的几个链表。

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_path()]
/* check for external widgets */  
        if (wsink->id == snd_soc_dapm_input) {  
                if (wsource->id == snd_soc_dapm_micbias ||  
                        wsource->id == snd_soc_dapm_mic ||  
                        wsource->id == snd_soc_dapm_line ||  
                        wsource->id == snd_soc_dapm_output)  
                        wsink->ext = 1;  
        }  
        if (wsource->id == snd_soc_dapm_output) {  
                if (wsink->id == snd_soc_dapm_spk ||  
                        wsink->id == snd_soc_dapm_hp ||  
                        wsink->id == snd_soc_dapm_line ||  
                        wsink->id == snd_soc_dapm_input)  
                        wsource->ext = 1;  
        }

这段代码用于判断是否有外部连接关系,如果有,置位widget的ext字段。
判断方法从代码中可以方便地看出:
目的 widget 是一个输入脚,如果源 widget 是 mic、line、micbias 或 output,则认为目的 widget 具有外部连接关系。
源 widge t是一个输出脚,如果目的 widget 是 spk、hp、line或input,则认为源widget具有外部连接关系。

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_path()]
dapm_mark_dirty(wsource, "Route added");  
dapm_mark_dirty(wsink, "Route added");  
  
/* 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;  
}

因为增加了连结关系,所以把源 widget 和目的 widge t加入到 dapm_dirty 链表中。
如果没有kcontrol来控制该连接关系,则这是一个静态连接,直接用 path 把它们连接在一起。

在接着往下看:

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_path()]
/* 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:  
        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;

按照目的widget来判断,如果属于以上这些类型,直接把它们连接在一起即可,
这段感觉有点多余,因为通常以上这些类型的widget本来也没有kcontrol,
直接用上一段代码就可以了,也许是dapm的作者们想着以后可能会有所扩展吧。

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_path()]
case snd_soc_dapm_mux:  
case snd_soc_dapm_virt_mux:  
case snd_soc_dapm_value_mux:  
        ret = dapm_connect_mux(dapm, wsource, wsink, path, control,  
                &wsink->kcontrol_news[0]);  
        break;  
case snd_soc_dapm_switch:  
case snd_soc_dapm_mixer:  
case snd_soc_dapm_mixer_named_ctl:  
        ret = dapm_connect_mixer(dapm, wsource, wsink, path, control);  
        break;

目的 widget 如果是 mixer 和 mux 类型,分别用 dapm_connect_mixer 和 dapm_connect_mux 函数完成连接工作,这两个函数我们后面再讲。

[->/sound/soc/soc-dapm.c:snd_soc_dapm_add_path()]
        case snd_soc_dapm_hp:  
        case snd_soc_dapm_mic:  
        case snd_soc_dapm_line:  
        case snd_soc_dapm_spk:  
                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;  
        }  
        return 0;  
}

hp、mic、line和spk这几种widget属于外部器件,也只是简单地连接在一起,不过connect字段默认为是未连接状态。

现在,我们回过头来看看目的widget是mixer和mux这两种类型时的连接方式:

  • dapm_connect_mixer 用该函数连接一个目的 widget 为 mixer 类型的所有输入端:
[->/sound/soc/soc-dapm.c]
static int dapm_connect_mixer(struct snd_soc_dapm_context *dapm,  
        struct snd_soc_dapm_widget *src, struct snd_soc_dapm_widget *dest,  
        struct snd_soc_dapm_path *path, const char *control_name)  
{  
        /* search for mixer kcontrol */  
        for (i = 0; i < dest->num_kcontrols; i++) {  
                if (!strcmp(control_name, dest->kcontrol_news[i].name)) {  
                        list_add(&path->list, &dapm->card->paths);  
                        list_add(&path->list_sink, &dest->sources);  
                        list_add(&path->list_source, &src->sinks);  
                        path->name = dest->kcontrol_news[i].name;  
                        dapm_set_path_status(dest, path, i);  
                        return 0;  
                }  
        }  
        return -ENODEV;  
}

用需要用来连接的kcontrol的名字,和目的widget中的kcontrol模板数组中的名字相比较,
找出该 kcontrol 在 widget 中的编号,path 的名字设置为该 kcontrol 的名字,
然后用 dapm_set_path_status函数来初始化该输入端的连接状态。

连接两个widget的链表操作和其他widget是一样的。

  • dapm_connect_mux 用该函数连接一个目的widget是mux类型的所有输入端:
[->/sound/soc/soc-dapm.c]
static int dapm_connect_mux(struct snd_soc_dapm_context *dapm,  
        struct snd_soc_dapm_widget *src, struct snd_soc_dapm_widget *dest,  
        struct snd_soc_dapm_path *path, const char *control_name,  
        const struct snd_kcontrol_new *kcontrol)  
{  
        struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;  
        int i;  
  
        for (i = 0; i < e->max; i++) {  
                if (!(strcmp(control_name, e->texts[i]))) {  
                        list_add(&path->list, &dapm->card->paths);  
                        list_add(&path->list_sink, &dest->sources);  
                        list_add(&path->list_source, &src->sinks);  
                        path->name = (char*)e->texts[i];  
                        dapm_set_path_status(dest, path, 0);  
                        return 0;  
                }  
        }  
  
        return -ENODEV;  
}

和mixer类型一样用名字进行匹配,只不过mux类型的kcontrol只需一个,
所以要通过private_value字段所指向的soc_enum结构找出匹配的输入脚编号,
最后也是通过dapm_set_path_status函数来初始化该输入端的连接状态,
因为只有一个kcontrol,所以第三个参数是0。

连接两个widget的链表操作和其他widget也是一样的。

dapm_set_path_status 该函数根据传入widget中的kcontrol编号,读取实际寄存器的值,
根据寄存器的值来初始化这个path是否处于连接状态,详细的代码这里就不贴了。


当widget之间通过path进行连接之后,他们之间的关系就如下图所示:

在这里插入图片描述
到这里为止,我们为声卡创建并初始化好了所需的widget,各个widget也通过path连接在了一起,
接下来,dapm等待用户的指令,一旦某个dapm kcontrol被用户空间改变,
利用这些连接关系,dapm会重新创建音频路径,脱离音频路径的widget会被下电,加入音频路径的widget会被上电,所有的上下电动作都会自动完成,
用户空间的应用程序无需关注这些变化,它只管按需要改变某个dapm kcontrol即可。




参考资料(特别感谢各位前辈的分析和图示):

Android音频模块启动流程分析

Jhuster的专栏​ Android音频开发

高通audio offload学习 | Thinking

DroidPhone的专栏 - CSDN博客

alsa音频架构1-CSDN博客

alsa音频架构2-ASoc - CSDN博客

alsa音频架构3-Pcm - CSDN博客

alsa音频架构4-声卡控制 - CSDN博客

Linux ALSA 音频系统:逻辑设备篇 - CSDN博客

Linux ALSA 音频系统:物理链路篇 - CSDN博客

专栏:MultiMedia框架总结(基于6.0源码) - CSDN博客

Android 音频系统:从 AudioTrack 到 AudioFlinger - CSDN博客

AZURE - CSDN博客 - ALSA-Android Audio

AZURE - CSDN博客 - ANDROID音频系统

Audio驱动总结–ALSA | Winddoing’s Blog

audio HAL - 牧 天 - 博客园

林学森的Android专栏 - CSDN博客

深入剖析Android音频 - CSDN博客Yangwen123

播放框架 - 标签 - Tocy - 博客园

Android-7.0-Nuplayer概述 - CSDN博客

Android-7.0-Nuplayer-启动流程 - CSDN博客

Android Media Player 框架分析-Nuplayer(1) - CSDN博客

Android Media Player 框架分析-AHandler AMessage ALooper - CSDN博客

Android N Audio播放 start真面目- (六篇) CSDN博客

深入理解Android音视频同步机制(五篇)NuPlayer的avsync逻辑 - CSDN博客

wangyf的专栏 - CSDN博客-MT6737 Android N 平台 Audio系统学习

Android 7.0 Audio: Mediaplayer - CSDN博客

Android 7.0 Audio-相关类浅析- CSDN博客

Android N Audio播放六:如何读取buffer - CSDN博客

Fuchsia OS中的RPC机制-FIDL - CSDN博客

高通Audio中ASOC的codec驱动 - yooooooo - 博客园

发布了329 篇原创文章 · 获赞 66 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Ciellee/article/details/101754071