ALSA声卡驱动详解

更新时间:2024-01-18 03:53:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

1.ALSA声卡驱动中的DAPM详解之一:kcontrol

DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态下。DAPM对用户空间的应用程序来说是透明的,所有与电源相关的开关都在ASoc core中完成。用户空间的应用程序无需对代码做出修改,也无需重新编译,DAPM根据当前激活的音频流(playback/capture)和声卡中的mixer等的配置来决定那些音频控件的电源开关被打开或关闭。

/*****************************************************************************************************/

声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!

/*****************************************************************************************************/

DAPM控件是由普通的soc音频控件演变而来的,所以本章的内容我们先从普通的soc音频控件开始。 snd_kcontrol_new结构

在正式讨论DAPM之前,我们需要先搞清楚ASoc中的一个重要的概念:

kcontrol,不熟悉的读者需要浏览一下我之前的文章:Linux ALSA声卡驱动之四:Control设备的创建。通常,一个kcontrol代表着一个mixer(混音器),或者是一个mux(多路开关),又或者是一个音量控制器等等。 从上述文章中我们知道,定义一个kcontrol主要就是定义一个snd_kcontrol_new结构,为了方便讨论,这里再次给出它的定义: [cpp] view plaincopy

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 {

1

snd_kcontrol_tlv_rw_t *c; const unsigned int *p; } tlv;

unsigned long private_value; };

回到Linux ALSA声卡驱动之四:Control设备的创建中,我们知道,对于每个控件,我们需要定义一个和他对应的snd_kcontrol_new结构,这些snd_kcontrol_new结构会在声卡的初始化阶段,通过

snd_soc_add_codec_controls函数注册到系统中,用户空间就可以通过amixer或alsamixer等工具查看和设定这些控件的状态。

snd_kcontrol_new结构中,几个主要的字段是get,put,private_value,get回调函数用于获取该控件当前的状态值,而put回调函数则用于设置控件的状态值,而private_value字段则根据不同的控件类型有不同的意义,比如对于普通的控件,private_value字段可以用来定义该控件所对应的寄存器的地址以及对应的控制位在寄存器中的位置信息。值得庆幸的是,ASoc系统已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于include/sound/soc.h中。下面我们分别讨论一下如何用这些预设的宏定义来定义一些常用的控件。 简单型的控件

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

#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) }

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

#define SOC_DOUBLE_VALUE(xreg, shift_left, shift_right, xmax, xinvert) \\

((unsigned long)&(struct soc_mixer_control) \\

{.reg = xreg, .rreg = xreg, .shift = shift_left, \\

.rshift = shift_right, .max = xmax, .platform_max = xmax, \\

.invert = xinvert})

#define SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert) \\

2

SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert) 这里实际上是定义了一个soc_mixer_control结构,然后把该结构的地址赋值给了private_value字段,soc_mixer_control结构是这样的: [cpp] view plaincopy /* mixer control */

struct soc_mixer_control {

int min, max, platform_max;

unsigned int reg, rreg, shift, rshift, invert; };

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

int snd_soc_get_volsw(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) {

struct soc_mixer_control *mc =

(struct soc_mixer_control *)kcontrol->private_value;

struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);

unsigned int reg = mc->reg; unsigned int reg2 = mc->rreg; unsigned int shift = mc->shift; unsigned int rshift = mc->rshift; int max = mc->max;

unsigned int mask = (1 << fls(max)) - 1; unsigned int invert = mc->invert;

ucontrol->value.integer.value[0] =

(snd_soc_read(codec, reg) >> shift) & mask; if (invert)

ucontrol->value.integer.value[0] =

max - ucontrol->value.integer.value[0];

if (snd_soc_volsw_is_stereo(mc)) { if (reg == reg2)

ucontrol->value.integer.value[1] =

(snd_soc_read(codec, reg) >> rshift) & mask;

else

ucontrol->value.integer.value[1] =

(snd_soc_read(codec, reg2) >> shift) & mask;

3

if (invert)

ucontrol->value.integer.value[1] = max - ucontrol->value.integer.value[1]; }

return 0; }

上述代码一目了然,从private_value字段取出soc_mixer_control结构,利用该结构的信息,访问对应的寄存器,返回相应的值。

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

#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) }

从他的定义可以看出,用于设定寄存器信息的private_value字段的定义和

SOC_SINGLE是一样的,甚至put、get回调函数也是使用同一套,唯一不同的是增加了一个tlv_array参数,并把它赋值给了tlv.p字段。关于tlv,已经在Linux ALSA声卡驱动之四:Control设备的创建中进行了阐述。用户空间可以通过对声卡的control设备发起以下两种ioctl来访问tlv字段所指向的数组: SNDRV_CTL_IOCTL_TLV_READ SNDRV_CTL_IOCTL_TLV_WRITE

SNDRV_CTL_IOCTL_TLV_COMMAND

通常,tlv_array用来描述寄存器的设定值与它所代表的实际意义之间的映射关系,最常用的就是用于音量控件时,设定值与对应的dB值之间的映射关系,请看以下例子:

[cpp] view plaincopy

static const DECLARE_TLV_DB_SCALE(mixin_boost_tlv, 0, 900, 0);

static const struct snd_kcontrol_new wm1811_snd_controls[] = { SOC_SINGLE_TLV(\, WM8994_INPUT_MIXER_1, 7, 1, 0,

mixin_boost_tlv),

4

SOC_SINGLE_TLV(\, WM8994_INPUT_MIXER_1, 8, 1, 0,

mixin_boost_tlv), };

DECLARE_TLV_DB_SCALE用于定义一个dB值映射的tlv_array,上述的例子表明,该tlv的类型是SNDRV_CTL_TLVT_DB_SCALE,寄存器的最小值对应是0dB,寄存器每增加一个单位值,对应的dB数增加是9dB(0.01dB*900),而由接下来的两组SOC_SINGLE_TLV定义可以看出,我们定义了两个boost控件,寄存器的地址都是WM8994_INPUT_MIXER_1,控制位分别是第7bit和第8bit,最大值是1,所以,该控件只能设定两个数值0和1,对应的dB值就是0dB和9dB。

SOC_DOUBLE 与SOC_SINGLE相对应,区别是SOC_SINGLE只控制一个变量,而SOC_DOUBLE则可以同时在一个寄存器中控制两个相似的变量,最常用的就是用于一些立体声的控件,我们需要同时对左右声道进行控制,因为多了一个声道,参数也就相应地多了一个shift位移值, [cpp] view plaincopy

#define SOC_DOUBLE(xname, reg, shift_left, shift_right, 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_DOUBLE_VALUE(reg, shift_left, shift_right, \\

max, invert) }

SOC_DOUBLE_R 与SOC_DOUBLE类似,对于左右声道的控制寄存器不一样的情况,使用SOC_DOUBLE_R来定义,参数中需要指定两个寄存器地址。 SOC_DOUBLE_TLV 与SOC_SINGLE_TLV对应的立体声版本,通常用于立体声音量控件的定义。

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

Mixer控件

Mixer控件用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由地混合在一起,形成混合后的输出:

5

图1 Mixer混音器

对于Mixer控件,我们可以认为是多个简单控件的组合,通常,我们会为mixer的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反应在代码上,就是定义一个soc_kcontrol_new数组: [cpp] view plaincopy

static const struct snd_kcontrol_new left_speaker_mixer[] = { SOC_SINGLE(\, WM8993_SPEAKER_MIXER, 7, 1, 0), SOC_SINGLE(\, WM8993_SPEAKER_MIXER, 5, 1, 0), SOC_SINGLE(\, WM8993_SPEAKER_MIXER, 3, 1, 0), SOC_SINGLE(\, WM8993_SPEAKER_MIXER, 6, 1, 0), };

以上这个mixer使用寄存器WM8993_SPEAKER_MIXER的第3,5,6,7位来分别控制4个输入端的开启和关闭。 Mux控件

mux控件与mixer控件类似,也是多个输入端和一个输出端的组合控件,与mixer控件不同的是,mux控件的多个输入端同时只能有一个被选中。因此,mux控件所对应的寄存器,通常可以设定一段连续的数值,每个不同的数值对应不同的输入端被打开,与上述的mixer控件不同,ASoc用soc_enum结构来描述mux控件的寄存器信息:

[cpp] view plaincopy

/* enumerated kcontrol */ struct soc_enum {

unsigned short reg; unsigned short reg2; unsigned char shift_l; unsigned char shift_r; unsigned int max; unsigned int mask;

const char * const *texts; const unsigned int *values; };

两个寄存器地址和位移字段:reg,reg2,shift_l,shift_r,用于描述左右声道的控制寄存器信息。字符串数组指针用于描述每个输入端对应的名字,value字段则指

6

向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果value是一组连续的值,通常我们可以忽略values参数。下面我们先看看如何定义一个mux控件:

第一步,定义字符串和values数组,以下的例子因为values是连续的,所以不用定义:

[cpp] view plaincopy

static const char *drc_path_text[] = { \, \ };

第二步,利用ASoc提供的辅助宏定义soc_enum结构,用于描述寄存器: [cpp] view plaincopy

static const struct soc_enum drc_path =

SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);

第三步,利痛ASoc提供的辅助宏,定义soc_kcontrol_new结构,该结构最后用于注册该mux控件: [cpp] view plaincopy

static const struct snd_kcontrol_new wm8993_snd_controls[] = { SOC_DOUBLE_TLV(......), ......

SOC_ENUM(\, drc_path), ...... }

以上几步定义了一个叫DRC PATH的mux控件,该控件具有两个输入选择,分别是来自ADC和DAC,用寄存器WM8993_DRC_CONTROL_1控制。其中,soc_enum结构使用了辅助宏SOC_ENUM_SINGLE来定义,该宏的声明如下: [cpp] view plaincopy

#define SOC_ENUM_DOUBLE(xreg, xshift_l, xshift_r, xmax, xtexts) \\

{ .reg = xreg, .shift_l = xshift_l, .shift_r = xshift_r, \\

.max = xmax, .texts = xtexts, \\

.mask = xmax ? roundup_pow_of_two(xmax) - 1 : 0} #define SOC_ENUM_SINGLE(xreg, xshift, xmax, xtexts) \\

SOC_ENUM_DOUBLE(xreg, xshift, xshift, xmax, xtexts) 定义soc_kcontrol_new结构时使用了SOC_ENUM,列出它的定义如下: [cpp] view plaincopy

#define SOC_ENUM(xname, xenum) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,\\ .info = snd_soc_info_enum_double, \\

.get = snd_soc_get_enum_double, .put = snd_soc_put_enum_double, \\

7

.private_value = (unsigned long)&xenum }

思想如此统一,依然是使用private_value字段记录soc_enum结构,不过几个回调函数变了,我们看看get回调对应的snd_soc_get_enum_double函数: [cpp] view plaincopy

int snd_soc_get_enum_double(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) {

struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);

struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;

unsigned int val;

val = snd_soc_read(codec, e->reg); ucontrol->value.enumerated.item[0]

= (val >> e->shift_l) & e->mask; if (e->shift_l != e->shift_r)

ucontrol->value.enumerated.item[1] = (val >> e->shift_r) & e->mask;

return 0; }

通过info回调函数则可以获取某个输入端所对应的名字,其实就是从soc_enum结构的texts数组中获得: [cpp] view plaincopy

int snd_soc_info_enum_double(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo) {

struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;

uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; uinfo->count = e->shift_l == e->shift_r ? 1 : 2; uinfo->value.enumerated.items = e->max;

if (uinfo->value.enumerated.item > e->max - 1)

uinfo->value.enumerated.item = e->max - 1; strcpy(uinfo->value.enumerated.name,

e->texts[uinfo->value.enumerated.item]); return 0; }

以下是另外几个常用于定义mux控件的宏:

SOC_VALUE_ENUM_SINGLE 用于定义带values字段的soc_enum结构。

8

SOC_VALUE_ENUM_DOUBLE SOC_VALUE_ENUM_SINGLE的立体声版本。

SOC_VALUE_ENUM 用于定义带values字段snd_kcontrol_new结构,这个有点特别,我们还是看看它的定义: [cpp] view plaincopy

#define SOC_VALUE_ENUM(xname, xenum) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname,\\ .info = snd_soc_info_enum_double, \\

.get = snd_soc_get_value_enum_double, \\ .put = snd_soc_put_value_enum_double, \\ .private_value = (unsigned long)&xenum } 从定义可以看出来,回调函数被换掉了,我们看看他的get回调: [cpp] view plaincopy

int snd_soc_get_value_enum_double(struct snd_kcontrol *kcontrol,

struct snd_ctl_elem_value *ucontrol) {

struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);

struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;

unsigned int reg_val, val, mux;

reg_val = snd_soc_read(codec, e->reg); val = (reg_val >> e->shift_l) & e->mask; for (mux = 0; mux < e->max; mux++) { if (val == e->values[mux]) break; }

ucontrol->value.enumerated.item[0] = mux; if (e->shift_l != e->shift_r) {

val = (reg_val >> e->shift_r) & e->mask; for (mux = 0; mux < e->max; mux++) { if (val == e->values[mux]) break; }

ucontrol->value.enumerated.item[1] = mux; }

return 0; }

与SOC_ENUM定义的mux不同,它没有直接返回寄存器的设定值,而是通过soc_enum结构中的values字段做了一次转换,与values数组中查找和寄存器相

9

等的值,然后返回他在values数组中的索引值,所以,尽管寄存器的值可能是不连续的,但返回的值是连续的。

通常,我们还可以用以下几个辅助宏定义soc_enum结构,其实和上面所说的没什么区别,只是可以偷一下懒,省掉struct soc_enum xxxx=几个单词而已: SOC_ENUM_SINGLE_DECL SOC_ENUM_DOUBLE_DECL

SOC_VALUE_ENUM_SINGLE_DECL SOC_VALUE_ENUM_DOUBLE_DECL 其它控件

其实,除了以上介绍的几种常用的控件,ASoc还为我们提供了另外一些控件定义辅助宏,详细的请读者参考include/sound/soc.h。这里列举几个: 需要自己定义get和put回调时,可以使用以下这些带EXT的版本: SOC_SINGLE_EXT SOC_DOUBLE_EXT SOC_SINGLE_EXT_TLV SOC_DOUBLE_EXT_TLV SOC_DOUBLE_R_EXT_TLV SOC_ENUM_EXT 1.ALSA声卡驱动中的DAPM详解之二:widget-具备路径和电源管理信息的kcontrol 上一篇文章中,我们介绍了音频驱动中对基本控制单元的封装:kcontrol。利用kcontrol,我们可以完成对音频系统中的mixer,mux,音量控制,音效控制,以及各种开关量的控制,通过对各种kcontrol的控制,使得音频硬件能够按照我们预想的结果进行工作。同时我们可以看到,kcontrol还是有以下几点不足: 只能描述自身,无法描述各个kcontrol之间的连接关系; 没有相应的电源管理机制;

没有相应的时间处理机制来响应播放、停止、上电、下电等音频事件;

为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序; 当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol;

/*****************************************************************************************************/

声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!

/*****************************************************************************************************/

10

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

DAPM的基本单元:widget

文章的开头,我们说明了一下目前kcontrol的一些不足,而DAPM框架为了解决这些问题,引入了widget这一概念,所谓widget,其实可以理解为是kcontrol的进一步升级和封装,她同样是指音频系统中的某个部件,比如mixer,mux,输入输出引脚,电源供应器等等,甚至,我们可以定义虚拟的widget,例如playback stream widget。widget把kcontrol和动态电源管理进行了有机的结合,同时还具备音频路径的连结功能,一个widget可以与它相邻的widget有某种动态的连结关系。在DAPM框架中,widget用结构体snd_soc_dapm_widget来描述: [cpp] view plaincopy

struct snd_soc_dapm_widget {

enum snd_soc_dapm_type id;

const char *name; /* widget name */

......

/* dapm control */

int reg; /* negative reg = no direct dapm */

unsigned char shift; /* bits to shift */

unsigned int value; /* widget current value */

unsigned int mask; /* non-shifted mask */ ......

int (*power_check)(struct snd_soc_dapm_widget *w);

int (*event)(struct snd_soc_dapm_widget*, struct snd_kcontrol *, int);

/* kcontrols that relate to this widget */ int num_kcontrols;

const struct snd_kcontrol_new *kcontrol_news; struct snd_kcontrol **kcontrols;

/* widget input and outputs */ struct list_head sources; struct list_head sinks; ......

11

};

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

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

*name 该widget的名字

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

*codec *platform 指向该widget所属的codec和platform。

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

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

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

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

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

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

value on_val off_val 电源状态的当前只,开启时和关闭时所对应的值。 power invert 用于指示该widget当前是否处于上电状态,invert则用于表明power字段是否需要逻辑反转。

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

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

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

force 该位被设置后,将会不管widget当前的状态,强制更新至新的电源状态。 ignore_suspend new_power power_checked 这些电源管理相关的字段。 subseq 该widget目前在上电或下电队列中的排序编号,为了防止在上下电的过程中出现pop-pop声,DAPM会给每个widget分配合理的上下电顺序。 *power_check 用于检查该widget是否应该上电或下电的回调函数指针。 event_flags 该字段是一个位或字段,每个位代表该widget会关注某个DAPM事件通知。只有被关注的通知事件会被发送到widget的事件处理回调函数中。 *event DAPM事件处理回调函数指针。

12

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

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

power_list 每次更新整个dapm的电源状态时,会根据一定的算法扫描所有的widget,然后把需要变更电源状态的widget利用该字段链接到一个上电或下电的链表中,扫描完毕后,dapm系统会遍历这两个链表执行相应的上电或下电操作。 dirty 链表字段,widget的状态变更后,dapm系统会利用该字段,把该widget加入到一个dirty链表中,稍后会对dirty链表进行扫描,以执行整个路径的更新。 inputs 该widget的所有有效路径中,连接到输入端的路径数量。 outputs 该widget的所有有效路径中,连接到输出端的路径数量。

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

以上我们对snd_soc_dapm_widget结构的各个字段所代表的意义一一做出了说明,这里只是让大家现有个概念,至于每个字段的详细作用,我们会在以后相关的章节中提及。 widget的种类

在DAPM框架中,把各种不同的widget划分为不同的种类,

snd_soc_dapm_widget结构中的id字段用来表示该widget的种类,可选的种类都定义在一个枚举中: [cpp] view plaincopy /* dapm widget types */

enum snd_soc_dapm_type {......} 下面我们逐个解释一下这些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对应一个耳机。

13

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结构。 widget之间的连接器:path

之前已经提到,一个widget是有输入和输出的,而且widget之间是可以动态地进行连接的,那它们是用什么来连接两个widget的呢?DAPM为我们提出了path这一概念,path相当于电路中的一根跳线,它把一个widget的输出端和另一个widget的输入端连接在一起,path用snd_soc_dapm_path结构来描述: [cpp] view plaincopy

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; struct snd_kcontrol *kcontrol;

/* 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 */

14

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; };

当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输入。

图1 widget通过path进行连接

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

connect,walked,walking,weak是几个辅助字段,用于帮助所有path的遍历。 widget的连接关系:route

通过上一节的内容,我们知道,一个路径的连接至少包含以下几个元素:起始端widget,跳线path,到达端widget,在DAPM中,用snd_soc_dapm_route结构来描述这样一个连接关系: [cpp] view plaincopy

15

struct snd_soc_dapm_route { const char *sink; const char *control; const char *source;

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之间的连接,具体的连接代码分析,我们留到以后的章节中讨论。

1.ALSA声卡驱动中的DAPM详解之三:如何定义各种widget 上一节中,介绍了DAPM框架中几个重要的数据结构:snd_soc_dapm_widget,snd_soc_dapm_path,snd_soc_dapm_route。其中snd_soc_dapm_path无需我们自己定义,它会在注册snd_soc_dapm_route时动态地生成,但是对于系统中的widget和route,我们是需要自己进行定义的,另外,widget所包含的kcontrol与普通的kcontrol有所不同,它们的定义方法与标准的kcontrol也有所不同。本节的内容我将会介绍如何使用DAPM系统提供的一些辅助宏定义来定义各种类型的widget和它所用到的kcontrol。

/*****************************************************************************************************/

声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!

/*****************************************************************************************************/ 定义widget

和普通的kcontrol一样,DAPM框架为我们提供了大量的辅助宏用来定义各种各样的widget控件,这些宏定义根据widget的类型,按照它们的电源所在的域,被分为了几个域,他们分别是:

codec域 比如VREF和VMID等提供参考电压的widget,这些widget通常在codec的probe/remove回调中进行控制,当然,在工作中如果没有音频流时,也可以适当地进行控制它们的开启与关闭。

16

platform域 位于该域上的widget通常是针对平台或板子的一些需要物理连接的输入/输出接口,例如耳机、扬声器、麦克风,因为这些接口在每块板子上都可能不一样,所以通常它们是在machine驱动中进行定义和控制,并且也可以由用户空间的应用程序通过某种方式来控制它们的打开和关闭。

音频路径域 一般是指codec内部的mixer、mux等控制音频路径的widget,这些widget可以根据用户空间的设定连接关系,自动设定他们的电源状态。

音频数据流域 是指那些需要处理音频数据流的widget,例如ADC、DAC等等。 codec域widget的定义

目前,DAPM框架只提供了定义一个codec域widget的辅助宏: [cpp] view plaincopy

#define SND_SOC_DAPM_VMID(wname) \\

{ .id = snd_soc_dapm_vmid, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0}

platform域widget的定义

DAPM框架为我们提供了多种platform域widget的辅助定义宏: [cpp] view plaincopy

#define SND_SOC_DAPM_SIGGEN(wname) \\

{ .id = snd_soc_dapm_siggen, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM } #define SND_SOC_DAPM_INPUT(wname) \\

{ .id = snd_soc_dapm_input, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM } #define SND_SOC_DAPM_OUTPUT(wname) \\

{ .id = snd_soc_dapm_output, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM } #define SND_SOC_DAPM_MIC(wname, wevent) \\

{ .id = snd_soc_dapm_mic, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \\

.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD}

#define SND_SOC_DAPM_HP(wname, wevent) \\

{ .id = snd_soc_dapm_hp, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \\

17

.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}

#define SND_SOC_DAPM_SPK(wname, wevent) \\

{ .id = snd_soc_dapm_spk, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \\

.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}

#define SND_SOC_DAPM_LINE(wname, wevent) \\

{ .id = snd_soc_dapm_line, .name = wname, .kcontrol_news = NULL, \\

.num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \\

.event_flags = SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD}

以上这些widget分别对应信号发生器,输入引脚,输出引脚,麦克风,耳机,扬声器,线路输入接口。其中的reg字段被设置为SND_SOC_NOPM(-1),表明这些widget是没有寄存器控制位来控制widget的电源状态的。麦克风,耳机,扬声器,线路输入接口这几种widget,还可以定义一个dapm事件回调函数wevent,从event_flags字段的设置可以看出,他们只会响应

SND_SOC_DAPM_POST_PMU(上电后)和SND_SOC_DAPM_PMD(下电前)事件,这几个widget通常会在machine驱动中定义,而

SND_SOC_DAPM_INPUT和SND_SOC_DAPM_OUTPUT则用来定义codec芯片的输出输入脚,通常在codec驱动中定义,最后,在machine驱动中增加相应的route,把麦克风和耳机等widget与相应的codec输入输出引脚的widget连接起来。

音频路径(path)域widget的定义

这种widget通常是对普通kcontrols控件的再封装,增加音频路径和电源管理功能,所以这种widget会包含一个或多个kcontrol,普通kcontrol的定义方法我们在ALSA声卡驱动中的DAPM详解之一:kcontrol中已经介绍过,不过这些被包含的kcontrol不能使用这种方法定义,它们需要使用dapm框架提供的定义宏来定义,详细的讨论我们后面有介绍。这里先列出这些widget的定义宏: [cpp] view plaincopy

#define SND_SOC_DAPM_PGA(wname, wreg, wshift, winvert,\\ wcontrols, wncontrols) \\

{ .id = snd_soc_dapm_pga, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = wncontrols}

#define SND_SOC_DAPM_OUT_DRV(wname, wreg, wshift, winvert,\\ wcontrols, wncontrols) \\

18

{ .id = snd_soc_dapm_out_drv, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = wncontrols}

#define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert, \\ wcontrols, wncontrols)\\

{ .id = snd_soc_dapm_mixer, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = wncontrols}

#define SND_SOC_DAPM_MIXER_NAMED_CTL(wname, wreg, wshift, winvert, \\

wcontrols, wncontrols)\\

{ .id = snd_soc_dapm_mixer_named_ctl, .name = wname, .reg = wreg, \\

.shift = wshift, .invert = winvert, .kcontrol_news = wcontrols, \\

.num_kcontrols = wncontrols}

#define SND_SOC_DAPM_MICBIAS(wname, wreg, wshift, winvert) \\ { .id = snd_soc_dapm_micbias, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = NULL, .num_kcontrols = 0}

#define SND_SOC_DAPM_SWITCH(wname, wreg, wshift, winvert, wcontrols) \\

{ .id = snd_soc_dapm_switch, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = 1}

#define SND_SOC_DAPM_MUX(wname, wreg, wshift, winvert, wcontrols) \\

{ .id = snd_soc_dapm_mux, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = 1}

#define SND_SOC_DAPM_VIRT_MUX(wname, wreg, wshift, winvert, wcontrols) \\

{ .id = snd_soc_dapm_virt_mux, .name = wname, .reg = wreg, .shift = wshift, \\

.invert = winvert, .kcontrol_news = wcontrols, .num_kcontrols = 1}

#define SND_SOC_DAPM_VALUE_MUX(wname, wreg, wshift, winvert, wcontrols) \\

{ .id = snd_soc_dapm_value_mux, .name = wname, .reg = wreg, \\

19

.shift = wshift, .invert = winvert, .kcontrol_news = wcontrols, \\

.num_kcontrols = 1}

可以看出,这些widget的reg和shift字段是需要赋值的,说明这些widget是有相应的电源控制寄存器的,DAPM框架在扫描和更新音频路径时,会利用这些寄存器来控制widget的电源状态,使得它们的供电状态是按需分配的,需要的时候(在有效的音频路径上)上电,不需要的时候(不再有效的音频路径上)下电。这些widget需要完成和之前介绍的mixer、mux等控件同样的功能,实际上,这是通过它们包含的kcontrol控件来完成的,这些kcontrol我们需要在定义widget前先定义好,然后通过wcontrols和num_kcontrols参数传递给这些辅助定义宏。 如果需要自定义这些widget的dapm事件处理回调函数,也可以使用下面这些带“_E”后缀的版本:

SND_SOC_DAPM_PGA_E

SND_SOC_DAPM_OUT_DRV_E SND_SOC_DAPM_MIXER_E

SND_SOC_DAPM_MIXER_NAMED_CTL_E SND_SOC_DAPM_SWITCH_E SND_SOC_DAPM_MUX_E

SND_SOC_DAPM_VIRT_MUX_E

音频数据流(stream)域widget的定义

这些widget主要包含音频输入/输出接口,ADC/DAC等等: [cpp] view plaincopy

#define SND_SOC_DAPM_AIF_IN(wname, stname, wslot, wreg, wshift, winvert) \\

{ .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname, \\

.reg = wreg, .shift = wshift, .invert = winvert }

#define SND_SOC_DAPM_AIF_IN_E(wname, stname, wslot, wreg, wshift, winvert, \\

wevent, wflags) \\

{ .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname, \\

.reg = wreg, .shift = wshift, .invert = winvert, \\ .event = wevent, .event_flags = wflags }

#define SND_SOC_DAPM_AIF_OUT(wname, stname, wslot, wreg, wshift, winvert) \\

{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \\

.reg = wreg, .shift = wshift, .invert = winvert }

#define SND_SOC_DAPM_AIF_OUT_E(wname, stname, wslot, wreg, wshift, winvert, \\

wevent, wflags) \\

20

{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \\

.reg = wreg, .shift = wshift, .invert = winvert, \\ .event = wevent, .event_flags = wflags }

#define SND_SOC_DAPM_DAC(wname, stname, wreg, wshift, winvert) \\

{ .id = snd_soc_dapm_dac, .name = wname, .sname = stname, .reg = wreg, \\

.shift = wshift, .invert = winvert}

#define SND_SOC_DAPM_DAC_E(wname, stname, wreg, wshift, winvert, \\

wevent, wflags) \\

{ .id = snd_soc_dapm_dac, .name = wname, .sname = stname, .reg = wreg, \\

.shift = wshift, .invert = winvert, \\ .event = wevent, .event_flags = wflags}

#define SND_SOC_DAPM_ADC(wname, stname, wreg, wshift, winvert) \\

{ .id = snd_soc_dapm_adc, .name = wname, .sname = stname, .reg = wreg, \\

.shift = wshift, .invert = winvert}

#define SND_SOC_DAPM_ADC_E(wname, stname, wreg, wshift, winvert, \\

wevent, wflags) \\

{ .id = snd_soc_dapm_adc, .name = wname, .sname = stname, .reg = wreg, \\

.shift = wshift, .invert = winvert, \\ .event = wevent, .event_flags = wflags} #define SND_SOC_DAPM_CLOCK_SUPPLY(wname) \\

{ .id = snd_soc_dapm_clock_supply, .name = wname, \\ .reg = SND_SOC_NOPM, .event = dapm_clock_event, \\

.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD }

除了上面这些widget,还有另外三种widget没有提供显示的定义方法,它们的种类id分别是:

snd_soc_dapm_dai_in

snd_soc_dapm_dai_out snd_soc_dapm_dai_link

还记得我们在Linux ALSA声卡驱动之七:ASoC架构中的Codec中的

snd_soc_dai结构吗?每个codec有多个dai,而cpu(通常就是指某个soc cpu芯片)也会有多个dai,dai注册时,dapm系统会为每个dai创建一个

21

snd_soc_dapm_dai_in或snd_soc_dapm_dai_out类型的widget,通常,这两种widget会和codec中具有相同的stream name的widget进行连接。另外一种情况,当系统中具有多个音频处理器(比如多个codec)时,他们之间可能会通过某两个dai进行连接,当machine驱动确认有这种配置时(通过判断dai_links结构中的param字段),会为他们建立一个dai link把他们绑定在一起,因为有连接关系,两个音频处理器之间的widget的电源状态就可以互相传递。 除了还有几个通用的widget,他们的定义方法如下: [cpp] view plaincopy

#define SND_SOC_DAPM_REG(wid, wname, wreg, wshift, wmask, won_val, woff_val) \\

{ .id = wid, .name = wname, .kcontrol_news = NULL, .num_kcontrols = 0, \\

.reg = -((wreg) + 1), .shift = wshift, .mask = wmask, \\ .on_val = won_val, .off_val = woff_val, .event = dapm_reg_event, \\

.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD}

#define SND_SOC_DAPM_SUPPLY(wname, wreg, wshift, winvert, wevent, wflags) \\

{ .id = snd_soc_dapm_supply, .name = wname, .reg = wreg, \\

.shift = wshift, .invert = winvert, .event = wevent, \\ .event_flags = wflags}

#define SND_SOC_DAPM_REGULATOR_SUPPLY(wname, wdelay, wflags) \\

{ .id = snd_soc_dapm_regulator_supply, .name = wname, \\ .reg = SND_SOC_NOPM, .shift = wdelay, .event = dapm_regulator_event, \\

.event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD, \\

.invert = wflags} 定义dapm kcontrol

上一节提到,对于音频路径上的mixer或mux类型的widget,它们包含了若干个kcontrol,这些被包含的kcontrol实际上就是我们之前讨论的mixer和mux等,dapm利用这些kcontrol完成音频路径的控制。不过,对于widget来说,它的任务还不止这些,dapm还要动态地管理这些音频路径的连结关系,以便可以根据这些连接关系来控制这些widget的电源状态,如果按照普通的方法定义这些

kcontrol,是无法达到这个目的的,因此,dapm为我们提供了另外一套定义宏,由它们完成这些被widget包含的kcontrol的定义。 [cpp] view plaincopy

#define SOC_DAPM_SINGLE(xname, reg, shift, max, invert) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\

22

.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) }

#define SOC_DAPM_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\ .info = snd_soc_info_volsw, \\

.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | SNDRV_CTL_ELEM_ACCESS_READWRITE,\\

.tlv.p = (tlv_array), \\

.get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw, \\

.private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) }

#define SOC_DAPM_ENUM(xname, xenum) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\ .info = snd_soc_info_enum_double, \\ .get = snd_soc_dapm_get_enum_double, \\ .put = snd_soc_dapm_put_enum_double, \\ .private_value = (unsigned long)&xenum }

#define SOC_DAPM_ENUM_VIRT(xname, xenum) \\ { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\ .info = snd_soc_info_enum_double, \\ .get = snd_soc_dapm_get_enum_virt, \\ .put = snd_soc_dapm_put_enum_virt, \\

.private_value = (unsigned long)&xenum }

#define SOC_DAPM_ENUM_EXT(xname, xenum, xget, xput) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\ .info = snd_soc_info_enum_double, \\ .get = xget, \\ .put = xput, \\

.private_value = (unsigned long)&xenum } #define SOC_DAPM_VALUE_ENUM(xname, xenum) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \\ .info = snd_soc_info_enum_double, \\

.get = snd_soc_dapm_get_value_enum_double, \\ .put = snd_soc_dapm_put_value_enum_double, \\ .private_value = (unsigned long)&xenum } #define SOC_DAPM_PIN_SWITCH(xname) \\

{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname \ch\, \\

.info = snd_soc_dapm_info_pin_switch, \\ .get = snd_soc_dapm_get_pin_switch, \\

23

.put = snd_soc_dapm_put_pin_switch, \\ .private_value = (unsigned long)xname }

可以看出,SOC_DAPM_SINGLE对应与普通控件的SOC_SINGLE,

SOC_DAPM_SINGLE_TLV对应SOC_SINGLE_TLV......,相比普通的kcontrol控件,dapm的kcontrol控件只是把info,get,put回调函数换掉了。dapm

kcontrol的put回调函数不仅仅会更新控件本身的状态,他还会把这种变化传递到相邻的dapm kcontrol,相邻的dapm kcontrol又会传递这个变化到他自己相邻的dapm kcontrol,知道音频路径的末端,通过这种机制,只要改变其中一个widget的连接状态,与之相关的所有widget都会被扫描并测试一下自身是否还在有效的音频路径中,从而可以动态地改变自身的电源状态,这就是dapm的精髓所在。这里我只提一下这种概念,后续的章节会有较为详细的代码分析过程。

建立widget和route

上面介绍了一大堆的辅助宏,那么,对于一个实际的系统,我们怎么定义我们需要的widget?怎样定义widget的连接关系?下面我们还是以Wolfson公司的codec芯片WM8993为例子来了解这个过程。

第一步,利用辅助宏定义widget所需要的dapm kcontrol: [cpp] view plaincopy

static const struct snd_kcontrol_new left_speaker_mixer[] = { SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 7, 1, 0), SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 5, 1, 0), SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 3, 1, 0),

SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 6, 1, 0), };

static const struct snd_kcontrol_new right_speaker_mixer[] = { SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 6, 1, 0), SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 4, 1, 0), SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 2, 1, 0),

SOC_DAPM_SINGLE(\, WM8993_SPEAKER_MIXER, 0, 1, 0), };

static const char *aif_text[] = { \, \ };

static const struct soc_enum aifinl_enum =

SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 15, 2, aif_text);

24

static const struct snd_kcontrol_new aifinl_mux = SOC_DAPM_ENUM(\, aifinl_enum);

static const struct soc_enum aifinr_enum =

SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 14, 2, aif_text);

static const struct snd_kcontrol_new aifinr_mux = SOC_DAPM_ENUM(\, aifinr_enum); 以上,我们定义了wm8993中左右声道的speaker mixer控件:

left_speaker_mixer和right_speaker_mixer,同时还为左右声道各定义了一个叫做AIFINL Mux和AIFINR Mux的输入选择mux控件。

第二步,定义真正的widget,包含第一步定义好的dapm控件: [cpp] view plaincopy

static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {

......

SND_SOC_DAPM_AIF_IN(\, \, 0, SND_SOC_NOPM, 0, 0),

SND_SOC_DAPM_AIF_IN(\, \, 1, SND_SOC_NOPM, 0, 0),

......

SND_SOC_DAPM_MUX(\, SND_SOC_NOPM, 0, 0, &aifinl_mux),

SND_SOC_DAPM_MUX(\, SND_SOC_NOPM, 0, 0, &aifinr_mux),

SND_SOC_DAPM_MIXER(\, WM8993_POWER_MANAGEMENT_3, 8, 0, left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer)),

SND_SOC_DAPM_MIXER(\, WM8993_POWER_MANAGEMENT_3, 9, 0, right_speaker_mixer, ARRAY_SIZE(right_speaker_mixer)), ...... };

这一步,为左右声道各自定义了一个mux widget:DACL Mux和DACR Mux,实际的多路选择由dapm kcontrol:aifinl_mux和aifinr_mux,来完成,因为传入了SND_SOC_NOPM参数,这两个widget不具备电源属性,但是mux的切换会影响到与之相连的其它具备电源属性的电源状态。我们还为左右声道的扬声器各自定义了一个mixer widget:SPKL和SPKR,具体的mixer控制由上一步定义的

25

left_speaker_mixer和right_speaker_mixer来完成,两个widget具备电源属性,所以,当这两个widget在一条有效的音频路径上时,dapm框架可以通过寄存器WM8993_POWER_MANAGEMENT_3的第8位和第9位控制它的电源状态。

第三步,定义这些widget的连接路径: [cpp] view plaincopy

static const struct snd_soc_dapm_route routes[] = { ......

{ \, \, \ }, { \, \, \ }, { \, \, \ }, { \, \, \ },

......

{ \, \, \ }, { \, NULL, \ },

{ \, \, \ }, { \, NULL, \ }, };

通过第一步的定义,我们知道DACL Mux和DACR Mux有两个输入引脚,分别是 Left Right

而SPKL和SPKR有四个输入选择引脚,分别是: Input Switch

IN1LP Switch/IN1RP Switch Output Switch DAC Switch

所以,很显然,上面的路径定义的意思就是: AIFINL连接到DACL Mux的Left输入脚 AIFINR连接到DACL Mux的Right输入脚 AIFINL连接到DACR Mux的Left输入脚 AIFINR连接到DACR Mux的Right输入脚 DACL连接到SPKL的DAC Switch输入脚 DACR连接到SPKR的DAC Switch输入脚

第四步,在codec驱动的probe回调中注册这些widget和路径: [cpp] view plaincopy

static int wm8993_probe(struct snd_soc_codec *codec) {

......

snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets,

26

ARRAY_SIZE(wm8993_dapm_widgets));

......

snd_soc_dapm_add_routes(dapm, routes, ARRAY_SIZE(routes));

...... }

在machine驱动中,我们可以用同样的方式定义和注册板子特有的widget和路径信息。

1.ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path。之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开始,我们要从代码入手,分析dapm的详细工作原理: 如何注册widget

如何连接两个widget

一个widget的状态裱画如何传递到整个音频路径中

/*****************************************************************************************************/

声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!

/*****************************************************************************************************/

dapm context

在讨论widget的注册之前,我们先了解另一个概念:dapm context,直译过来的意思是dapm上下文,这个好像不好理解,其实我们可以这么理解:dapm把整个音频系统,按照功能和偏置电压级别,划分为若干个电源域,每个域包含各自的widget,每个域中的所有widget通常都处于同一个偏置电压级别上,而一个电源域就是一个dapm context,通常会有以下几种dapm context: 属于codec中的widget位于一个dapm context中 属于platform的widget位于一个dapm context中 属于整个声卡的widget位于一个dapm context中

对于音频系统的硬件来说,通常要提供合适的偏置电压才能正常地工作,有了dapm context这种组织方式,我们可以方便地对同一组widget进行统一的偏置电压管理,ASoc用snd_soc_dapm_context结构来表示一个dapm context: [cpp] view plaincopy

27

struct snd_soc_dapm_context {

enum snd_soc_bias_level bias_level;

enum snd_soc_bias_level suspend_bias_level; struct delayed_work delayed_work;

unsigned int idle_bias_off:1; /* Use BIAS_OFF instead of STANDBY */

struct snd_soc_dapm_update *update;

void (*seq_notifier)(struct snd_soc_dapm_context *, enum snd_soc_dapm_type, int);

struct device *dev; /* from parent - for debug */ struct snd_soc_codec *codec; /* parent codec */

struct snd_soc_platform *platform; /* parent platform */

struct snd_soc_card *card; /* parent card */

/* used during DAPM updates */

enum snd_soc_bias_level target_bias_level; struct list_head list;

int (*stream_event)(struct snd_soc_dapm_context *dapm, int event);

#ifdef CONFIG_DEBUG_FS

struct dentry *debugfs_dapm; #endif };

snd_soc_bias_level的取值范围是以下几种: SND_SOC_BIAS_OFF

SND_SOC_BIAS_STANDBY SND_SOC_BIAS_PREPARE SND_SOC_BIAS_ON

snd_soc_dapm_context被内嵌到代表codec、platform、card、dai的结构体中: [cpp] view plaincopy struct snd_soc_codec { ......

/* dapm */

struct snd_soc_dapm_context dapm; ...... };

struct snd_soc_platform {

28

......

/* dapm */

struct snd_soc_dapm_context dapm; ...... };

struct snd_soc_card { ......

/* dapm */

struct snd_soc_dapm_context dapm; ...... }; :

struct snd_soc_dai { ......

/* dapm */

struct snd_soc_dapm_widget *playback_widget; struct snd_soc_dapm_widget *capture_widget; struct snd_soc_dapm_context dapm; ...... };

代表widget结构snd_soc_dapm_widget中,有一个snd_soc_dapm_context结构指针,指向所属的codec、platform、card、或dai的dapm结构。同时,所有的dapm结构,通过它的list字段,链接到代表声卡的snd_soc_card结构的dapm_list链表头字段。 创建和注册widget

我们已经知道,一个widget用snd_soc_dapm_widget结构体来描述,通常,我们会根据音频硬件的组成,分别在声卡的codec驱动、platform驱动和machine驱动中定义一组widget,这些widget用数组进行组织,我们一般会使用dapm框架提供的大量的辅助宏来定义这些widget数组,辅助宏的说明请参考前一偏文章:ALSA声卡驱动中的DAPM详解之三:如何定义各种widget。 codec驱动中注册 我们知道,我们会通过ASoc提供的api函数

snd_soc_register_codec来注册一个codec驱动,该函数的第二个参数是一个snd_soc_codec_driver结构指针,这个snd_soc_codec_driver结构需要我们在codec驱动中显式地进行定义,其中有几个与dapm框架有关的字段: [cpp] view plaincopy

struct snd_soc_codec_driver { ......

/* Default control and setup, added after probe() is run */

const struct snd_kcontrol_new *controls; int num_controls;

29

const struct snd_soc_dapm_widget *dapm_widgets; int num_dapm_widgets;

const struct snd_soc_dapm_route *dapm_routes; int num_dapm_routes; ...... }

我们只要把我们定义好的snd_soc_dapm_widget结构数组的地址和widget的数量赋值到dapm_widgets和num_dapm_widgets字段即可,这样,经过

snd_soc_register_codec注册codec后,在machine驱动匹配上该codec时,系统会判断这两个字段是否被赋值,如果有,它会调佣dapm框架提供的api来创建和注册widget,注意这里我说还要创建这个词,你可能比较奇怪,既然代表

widget的snd_soc_dapm_widget结构数组已经在codec驱动中定义好了,为什么还要在创建?事实上,我们在codec驱动中定义的widget数组只是作为一个模板,dapm框架会根据该模板重新申请内存并初始化各个widget。我们看看实际的例子可能是这样的: [cpp] view plaincopy

static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {

......

SND_SOC_DAPM_SUPPLY(\, SND_SOC_NOPM, 0, 0, NULL, 0),

SND_SOC_DAPM_AIF_IN(\, \, 0, SND_SOC_NOPM, 0, 0),

SND_SOC_DAPM_AIF_IN(\, \, 1, SND_SOC_NOPM, 0, 0),

...... };

static struct snd_soc_codec_driver soc_codec_dev_wm8993 = { .probe = codec_xxx_probe, ......

.dapm_widgets = &wm8993_dapm_widgets[0],

.num_dapm_widgets = ARRAY_SIZE(wm8993_dapm_widgets),

...... };

static int codec_wm8993_i2c_probe(struct i2c_client *i2c, const struct i2c_device_id *id) {

......

ret = snd_soc_register_codec(&i2c->dev,

&soc_codec_dev_wm8993, &wm8993_dai, 1); ......

30

}

上面这种注册方法有个缺点,有时候我们为了代码的清晰,可能会根据功能把不同的widget定义成多个数组,但是snd_soc_codec_driver中只有一个

dapm_widgets字段,无法设定多个widget数组,这时候,我们需要主动在codec的probe回调中调用dapm框架提供的api来创建这些widget: [cpp] view plaincopy

static int wm8993_probe(struct snd_soc_codec *codec) {

......

snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets, ARRAY_SIZE(wm8993_dapm_widgets));

...... }

实际上,对于第一种方法,snd_soc_register_codec内部其实也是调用

snd_soc_dapm_new_controls来完成的。后面会有关于这个函数的详细分析。

platform驱动中注册 和codec驱动一样,我们会通过ASoc提供的api函数snd_soc_register_platform来注册一个platform驱动,该函数的第二个参数是一个snd_soc_platform_driver结构指针,snd_soc_platform_driver结构中同样也包含了与dapm相关的字段: [cpp] view plaincopy

struct snd_soc_platform_driver { ......

/* Default control and setup, added after probe() is run */

const struct snd_kcontrol_new *controls; int num_controls;

const struct snd_soc_dapm_widget *dapm_widgets; int num_dapm_widgets;

const struct snd_soc_dapm_route *dapm_routes; int num_dapm_routes; ...... }

要注册platform级别的widget,和codec驱动一样,只要把定义好的widget数组赋值给dapm_widgets和num_dapm_widgets字段即可,

snd_soc_register_platform函数注册paltform后,当machine驱动匹配上该

platform时,系统会自动完成创建和注册的工作。同理,我们也可以在platform驱动的probe回调函数中主动使用snd_soc_dapm_new_controls来完成widget的创建工作。具体的代码和codec驱动是类似的,这里就不贴了。

machine驱动中注册 有些widget可能不是位于codec中,例如一个独立的耳机放大器,或者是喇叭功放等,这种widget通常需要在machine驱动中注册,通常

31

他们的dapm context也从属于声卡(snd_soc_card)域。做法依然和codec驱动类似,通过代表声卡的snd_soc_card结构中的几个dapm字段完成: [cpp] view plaincopy struct snd_soc_card { ...... /*

* Card-specific routes and widgets. */

const struct snd_soc_dapm_widget *dapm_widgets; int num_dapm_widgets;

const struct snd_soc_dapm_route *dapm_routes; int num_dapm_routes; bool fully_routed; ...... }

只要把定义好的widget数组和数量赋值给dapm_widgets指针和

num_dapm_widgets即可,注册声卡使用的api:snd_soc_register_card(),也会通过snd_soc_dapm_new_controls来完成widget的创建工作。

注册音频路径

系统中注册的各种widget需要互相连接在一起才能协调工作,连接关系通过

snd_soc_dapm_route结构来定义,关于如何用snd_soc_dapm_route结构来定义路径信息,请参考:ALSA声卡驱动中的DAPM详解之三:如何定义各种widget中的\建立widget和route\一节的内容。通常,所有的路径信息会用一个

snd_soc_dapm_route结构数组来定义。和widget一样,路径信息也分别存在与codec驱动,machine驱动和platform驱动中,我们一样有两种方式来注册音频路径信息:

通过snd_soc_codec_driver/snd_soc_platform_driver/snd_soc_card结构中的dapm_routes和num_dapm_routes字段;

在codec、platform的的probe回调中主动注册音频路径,machine驱动中则通过snd_soc_dai_link结构的init回调函数来注册音频路径;

两种方法最终都是通过调用snd_soc_dapm_add_routes函数来完成音频路径的注册工作的。以下的代码片段是omap的pandora板子的machine驱动,使用第二种方法注册路径信息: [cpp] view plaincopy

static const struct snd_soc_dapm_widget omap3pandora_in_dapm_widgets[] = {

SND_SOC_DAPM_MIC(\, NULL), SND_SOC_DAPM_MIC(\, NULL), SND_SOC_DAPM_LINE(\, NULL),

32

};

static const struct snd_soc_dapm_route omap3pandora_out_map[] = {

{\, NULL, \},

{\, NULL, \}, {\, NULL, \},

{\, NULL, \}, };

static const struct snd_soc_dapm_route omap3pandora_in_map[] = {

{\, NULL, \}, {\, NULL, \},

{\, NULL, \}, {\, NULL, \},

{\, NULL, \},

{\, NULL, \}, };

static int omap3pandora_out_init(struct snd_soc_pcm_runtime *rtd) {

struct snd_soc_codec *codec = rtd->codec;

struct snd_soc_dapm_context *dapm = &codec->dapm; int ret;

/* All TWL4030 output pins are floating */ snd_soc_dapm_nc_pin(dapm, \); ......

//注册kcontrol控件

ret = snd_soc_dapm_new_controls(dapm, omap3pandora_out_dapm_widgets,

ARRAY_SIZE(omap3pandora_out_dapm_widgets));

if (ret < 0)

return ret;

//注册machine的音频路径

return snd_soc_dapm_add_routes(dapm, omap3pandora_out_map,

ARRAY_SIZE(omap3pandora_out_map)); }

33

static int omap3pandora_in_init(struct snd_soc_pcm_runtime *rtd) {

struct snd_soc_codec *codec = rtd->codec;

struct snd_soc_dapm_context *dapm = &codec->dapm; int ret;

/* Not comnnected */

snd_soc_dapm_nc_pin(dapm, \); ......

//注册kcontrol控件

ret = snd_soc_dapm_new_controls(dapm, omap3pandora_in_dapm_widgets,

ARRAY_SIZE(omap3pandora_in_dapm_widgets));

if (ret < 0)

return ret; //注册machine音频路径

return snd_soc_dapm_add_routes(dapm, omap3pandora_in_map,

ARRAY_SIZE(omap3pandora_in_map)); }

/* Digital audio interface glue - connects codec <--> CPU */ static struct snd_soc_dai_link omap3pandora_dai[] = { {

.name = \, ......

.init = omap3pandora_out_init, }, {

.name = \,

.stream_name = \, ......

.init = omap3pandora_in_init, } };

dai widget

上面几节的内容介绍了codec、platform以及machine级别的widget和route的注册方法,在dapm框架中,还有另外一种widget,它代表了一个dai(数字音频接口),关于dai的描述,请参考:Linux ALSA声卡驱动之七:ASoC架构中的Codec。dai按所在的位置,又分为cpu dai和codec dai,在硬件上,通常一个

34

cpu dai会连接一个codec dai,而在machine驱动中,我们要在snd_soc_card结构中指定一个叫做snd_soc_dai_link的结构,该结构定义了声卡使用哪一个cpu dai和codec dai进行连接。在Asoc中,一个dai用snd_soc_dai结构来表述,其中有几个字段和dapm框架有关: [cpp] view plaincopy struct snd_soc_dai { ......

struct snd_soc_dapm_widget *playback_widget; struct snd_soc_dapm_widget *capture_widget; struct snd_soc_dapm_context dapm; ...... }

dai由codec驱动和平台代码中的iis或pcm接口驱动注册,machine驱动负责找到snd_soc_dai_link中指定的一对cpu/codec dai,并把它们进行绑定。不管是cpu dai还是codec dai,通常会同时传输播放和录音的音频流的能力,所以我们可以看到,snd_soc_dai中有两个widget指针,分别代表播放流和录音流。这两个dai widget是何时创建的呢?下面我们逐一进行分析。 codec dai widget

首先,codec驱动在注册codec时,会传入该codec所支持的dai个数和记录dai信息的snd_soc_dai_driver结构指针: [cpp] view plaincopy

static struct snd_soc_dai_driver wm8993_dai = { .name = \, .playback = {

.stream_name = \, .channels_min = 1, .channels_max = 2,

.rates = WM8993_RATES,

.formats = WM8993_FORMATS, .sig_bits = 24, },

.capture = {

.stream_name = \, .channels_min = 1, .channels_max = 2,

.rates = WM8993_RATES,

.formats = WM8993_FORMATS, .sig_bits = 24, },

.ops = &wm8993_ops, .symmetric_rates = 1, };

35

static int wm8993_i2c_probe(struct i2c_client *i2c,

const struct i2c_device_id *id) {

......

ret = snd_soc_register_codec(&i2c->dev,

&soc_codec_dev_wm8993, &wm8993_dai, 1); ...... }

这回使得ASoc把codec的dai注册到系统中,并把这些dai都挂在全局链表变量dai_list中,然后,在codec被machine驱动匹配后,soc_probe_codec函数会被调用,他会通过全局链表变量dai_list查找所有属于该codec的dai,调用

snd_soc_dapm_new_dai_widgets函数来生成该dai的播放流widget和录音流widget:

[cpp] view plaincopy

static int soc_probe_codec(struct snd_soc_card *card, struct snd_soc_codec *codec) {

......

/* Create DAPM widgets for each DAI stream */ list_for_each_entry(dai, &dai_list, list) { if (dai->dev != codec->dev) continue;

snd_soc_dapm_new_dai_widgets(&codec->dapm, dai); }

...... }

我们看看snd_soc_dapm_new_dai_widgets的代码: [cpp] view plaincopy

int snd_soc_dapm_new_dai_widgets(struct snd_soc_dapm_context *dapm,

struct snd_soc_dai *dai) {

struct snd_soc_dapm_widget template; struct snd_soc_dapm_widget *w;

WARN_ON(dapm->dev != dai->dev);

memset(&template, 0, sizeof(template)); template.reg = SND_SOC_NOPM;

// 创建播放 dai widget if (dai->driver->playback.stream_name) { template.id = snd_soc_dapm_dai_in;

36

template.name = dai->driver->playback.stream_name;

template.sname = dai->driver->playback.stream_name;

w = snd_soc_dapm_new_control(dapm, &template);

w->priv = dai;

dai->playback_widget = w; }

// 创建录音 dai widget

if (dai->driver->capture.stream_name) {

template.id = snd_soc_dapm_dai_out; template.name = dai->driver->capture.stream_name;

template.sname = dai->driver->capture.stream_name;

w = snd_soc_dapm_new_control(dapm, &template);

w->priv = dai;

dai->capture_widget = w; }

return 0; }

分别为Playback和Capture创建了一个widget,widget的priv字段指向了该dai,这样通过widget就可以找到相应的dai,并且widget的名字就是snd_soc_dai_driver结构的stream_name。

cpu dai widget

这里顺便说一个小意外,昨天晚上手贱,执行了一下git pull,版本升级到了3.12 rc7,结果发现ASoc的代码有所变化,于是稍稍纠结了一下,用新的代码继续还是恢复之前的3.10 rc5?经过查看了一些变化后,发现还是新的版本改进得更合理,现在决定,后面的内容都是基于3.12 rc7了。如果大家发现后面贴的代码和之前贴的有差异的地方,自己比较一下这两个版本的代码吧!

回到cpu dai,以前的内核版本由驱动通过snd_soc_register_dais注册,新的版本中,这个函数变为了soc-core的内部函数,驱动改为使用

snd_soc_register_component注册,snd_soc_register_component函数再通过调用snd_soc_register_dai/snd_soc_register_dais来完成实际的注册工作。和codec dai widget一样,cpu dai widget也发生在machine驱动匹配上相应的platform驱动之后,soc_probe_platform会被调用,在soc_probe_platform函数

37

中,通过比较dai->dev和platform->dev,挑选出属于该platform的dai,然后通过snd_soc_dapm_new_dai_widgets为cpu dai创建相应的widget: [cpp] view plaincopy

static int soc_probe_platform(struct snd_soc_card *card,

struct snd_soc_platform *platform) {

int ret = 0;

const struct snd_soc_platform_driver *driver = platform->driver;

struct snd_soc_dai *dai;

......

if (driver->dapm_widgets)

snd_soc_dapm_new_controls(&platform->dapm, driver->dapm_widgets, driver->num_dapm_widgets);

/* Create DAPM widgets for each DAI stream */ list_for_each_entry(dai, &dai_list, list) { if (dai->dev != platform->dev) continue;

snd_soc_dapm_new_dai_widgets(&platform->dapm, dai); }

platform->dapm.idle_bias_off = 1;

......

if (driver->controls)

snd_soc_add_platform_controls(platform, driver->controls,

driver->num_controls); if (driver->dapm_routes)

snd_soc_dapm_add_routes(&platform->dapm, driver->dapm_routes,

driver->num_dapm_routes); ......

return 0; }

38

从上面的代码我们也可以看出,在上面的”创建和注册widget“一节提到的第一种方法,即通过给snd_soc_platform_driver结构的dapm_widgets和

num_dapm_widgets字段赋值,ASoc会自动为我们创建所需的widget,真正执行创建工作就在上面所列的soc_probe_platform函数中完成的,普通的kcontrol和音频路径也是一样的原理。反推回来,codec的widget也是一样的,在

soc_probe_codec中会做同样的事情,只是我上面贴出来soc_probe_codec的代码里没有贴出来,有兴趣的读者自己查看一下它的代码即可。

花了这么多篇幅来讲解dai widget,好像现在看来它还没有什么用处。嗯,不要着急,实际上dai widget是一条完整dapm音频路径的重要元素,没有她,我们无法完成dapm的动态电源管理工作,因为它是音频流和其他widget的纽带,细节我们要留到下一篇文章中来阐述了。 端点widget

一条完整的dapm音频路径,必然有起点和终点,我们把位于这些起点和终点的widget称之为端点widget。以下这些类型的widget可以成为端点widget: codec的输入输出引脚: snd_soc_dapm_output snd_soc_dapm_input 外接的音频设备: snd_soc_dapm_hp snd_soc_dapm_spk snd_soc_dapm_line

音频流(stream domain): snd_soc_dapm_adc snd_soc_dapm_dac snd_soc_dapm_aif_out snd_soc_dapm_aif_in snd_soc_dapm_dai_out snd_soc_dapm_dai_in 电源、时钟和其它: snd_soc_dapm_supply

snd_soc_dapm_regulator_supply snd_soc_dapm_clock_supply snd_soc_dapm_kcontrol

当声卡上的其中一个widget的状态发生改变时,从这个widget开始,dapm框架会向前和向后遍历路径上的所有widget,判断每个widget的状态是否需要跟着变更,到达这些端点widget就会认为它是一条完整音频路径的开始和结束,从而结束一次扫描动作。至于代码的分析,先让我歇一会......,我会在后面的文章中讨论。

1.ALSA声卡驱动中的DAPM详解之五:建立widget之间的连接关系 39

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

/*****************************************************************************************************/

声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!

/*****************************************************************************************************/

前面我们已经简单地介绍过,驱动程序需要使用以下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,所以在我看来,这两个函数名称应该换过来叫更好!下面我们分别介绍一下这两个函数是如何工作的。

创建widget:snd_soc_dapm_new_controls

snd_soc_dapm_new_controls函数完成widget的创建工作,并把这些创建好的widget注册在声卡的widgets链表中,我们看看他的定义: [cpp] view plaincopy

int snd_soc_dapm_new_controls(struct snd_soc_dapm_context *dapm,

const struct snd_soc_dapm_widget *widget, int num) {

......

for (i = 0; i < num; i++) {

w = snd_soc_dapm_new_control(dapm, widget); if (!w) {

dev_err(dapm->dev,

40

\rol %s\\n\,

widget->name); ret = -ENOMEM; break; } widget++; }

......

return ret; }

该函数只是简单的一个循环,为传入的widget模板数组依次调用

snd_soc_dapm_new_control函数,实际的工作由snd_soc_dapm_new_control完成,继续进入该函数,看看它做了那些工作。

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

[cpp] view plaincopy

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; int ret;

if ((w = dapm_cnew_widget(widget)) == NULL) return NULL;

由dapm_cnew_widget完成内存申请和拷贝模板的动作。接下来,根据widget的类型做不同的处理: [cpp] view plaincopy

switch (w->id) {

case snd_soc_dapm_regulator_supply:

w->regulator = devm_regulator_get(dapm->dev, w->name);

......

if (w->on_val & SND_SOC_DAPM_REGULATOR_BYPASS) {

ret = regulator_allow_bypass(w->regulator, true);

......

41

}

break;

case snd_soc_dapm_clock_supply: #ifdef CONFIG_CLKDEV_LOOKUP

w->clk = devm_clk_get(dapm->dev, w->name); ...... #else

return NULL; #endif

break; default:

break; }

对于snd_soc_dapm_regulator_supply类型的widget,根据widget的名称获取对应的regulator结构,对于snd_soc_dapm_clock_supply类型的widget,根据widget的名称,获取对应的clock结构。接下来,根据需要,在widget的名称前加入必要的前缀:

[cpp] view plaincopy widget的power_check回调函数 widget类型 power_check回调函数 mixer类: snd_soc_dapm_switch dapm_generic_check_power snd_soc_dapm_mixer snd_soc_dapm_mixer_named_ctl mux类: snd_soc_dapm_mux snd_soc_dapm_mux snd_soc_dapm_mux snd_soc_dapm_dai_out snd_soc_dapm_dai_in 端点类: snd_soc_dapm_adc snd_soc_dapm_aif_out snd_soc_dapm_dac snd_soc_dapm_aif_in snd_soc_dapm_pga snd_soc_dapm_out_drv snd_soc_dapm_input snd_soc_dapm_output snd_soc_dapm_micbias dapm_generic_check_power dapm_adc_check_power dapm_dac_check_power dapm_generic_check_power 42

snd_soc_dapm_spk snd_soc_dapm_hp snd_soc_dapm_mic snd_soc_dapm_line snd_soc_dapm_dai_link 电源/时钟/影子widget: snd_soc_dapm_supply snd_soc_dapm_regulator_supply dapm_supply_check_power snd_soc_dapm_clock_supply snd_soc_dapm_kcontrol dapm_always_on_check_power 其它类型 if (dapm->codec && dapm->codec->name_prefix) w->name = kasprintf(GFP_KERNEL, \,

dapm->codec->name_prefix, widget->name); else

w->name = kasprintf(GFP_KERNEL, \, widget->name); 然后,为不同类型的widget设置合适的power_check电源状态回调函数,widget类型和对应的power_check回调函数设置如下表所示:

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

/* machine layer set ups unconnected pins and insertions */ w->connected = 1; return w;

43

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链表中 为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有关: [cpp] view plaincopy

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); ......

44

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);

#ifdef CONFIG_DEBUG_FS

snd_soc_dapm_debugfs_init(&card->dapm, card->debugfs_card_root); #endif

......

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;

45

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函数的工作过程。 snd_soc_dapm_new_widgets函数

该函数通过声卡的widgets链表,遍历所有已经注册了的widget,其中的new字段用于判断该widget是否已经执行过snd_soc_dapm_new_widgets函数,如果num_kcontrols字段有数值,表明该widget包含有若干个dapm kcontrol,那么就需要为这些kcontrol分配一个指针数组,并把数组的首地址赋值给widget的kcontrols字段,该数组存放着指向这些kcontrol的指针,当然现在这些都是空指针,因为实际的kcontrol现在还没有被创建: [cpp] view plaincopy

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:

[cpp] view plaincopy 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:

46

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字段中:

[cpp] view plaincopy

/* 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可能使用同一个寄存器): [cpp] view plaincopy w->new = 1;

dapm_mark_dirty(w, \); dapm_debugfs_add_widget(w);

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

dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP); ...... return 0;

dapm mixer kcontrol

47

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

static int dapm_new_mixer(struct snd_soc_dapm_widget *w) {

int i, ret;

struct snd_soc_dapm_path *path;

/* add kcontrol */

(1) for (i = 0; i < w->num_kcontrols; i++) { /* match name */

(2) list_for_each_entry(path, &w->sources, list_sink) {

/* mixer/mux paths name must match control name */

(3) if (path->name != (char *)w->kcontrol_news[i].name)

continue;

(4) if (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;

(6) dapm_kcontrol_add_path(w->kcontrols[i], path); } }

return 0; }

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

48

(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来实现这一点: [cpp] view plaincopy

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判断是否需要创建。

dapm mux kcontrol

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

[cpp] view plaincopy

static int dapm_new_mux(struct snd_soc_dapm_widget *w) {

49

struct snd_soc_dapm_context *dapm = w->dapm; struct snd_soc_dapm_path *path; int ret;

(1) if (w->num_kcontrols != 1) { dev_err(dapm->dev,

\trols\\n\,

w->name); return -EINVAL; }

if (list_empty(&w->sources)) { dev_err(dapm->dev, \, w->name); return -EINVAL; }

(2) ret = dapm_create_or_share_mixmux_kcontrol(w, 0); if (ret < 0)

return ret;

(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特性。 dapm pga kcontrol

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

[cpp] view plaincopy

static int dapm_new_pga(struct snd_soc_dapm_widget *w) {

if (w->num_kcontrols)

dev_err(w->dapm->dev,

\\, w->name);

return 0;

50

本文来源:https://www.bwwdw.com/article/wjto.html

Top