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結構,爲了方便討論,這裏再次給出它的定義:
- 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;
- };
回到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大小等等)。我們看看這個宏是如何定義的:
- #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,我們看看它的定義:
- #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) \
- SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert)
這裏實際上是定義了一個soc_mixer_control結構,然後把該結構的地址賦值給了private_value字段,soc_mixer_control結構是這樣的:
- /* mixer control */
- struct soc_mixer_control {
- int min, max, platform_max;
- unsigned int reg, rreg, shift, rshift, invert;
- };
看來soc_mixer_control是控件特徵的真正描述者,它確定了該控件對應寄存器的地址,位移值,最大值和是否邏輯取反等特性,控件的put回調函數和get回調函數需要藉助該結構來訪問實際的寄存器。我們看看這get回調函數的定義:
- 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;
- 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均衡器等等。
- #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值之間的映射關係,請看以下例子:
- static const DECLARE_TLV_DB_SCALE(mixin_boost_tlv, 0, 900, 0);
-
- static const struct snd_kcontrol_new wm1811_snd_controls[] = {
- SOC_SINGLE_TLV("MIXINL IN1LP Boost Volume", WM8994_INPUT_MIXER_1, 7, 1, 0,
- mixin_boost_tlv),
- SOC_SINGLE_TLV("MIXINL IN1RP Boost Volume", 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位移值,
- #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控件用於音頻通道的路由控制,由多個輸入和一個輸出組成,多個輸入可以自由地混合在一起,形成混合後的輸出:

圖1 Mixer混音器
對於Mixer控件,我們可以認爲是多個簡單控件的組合,通常,我們會爲mixer的每個輸入端都單獨定義一個簡單控件來控制該路輸入的開啓和關閉,反應在代碼上,就是定義一個soc_kcontrol_new數組:
- static const struct snd_kcontrol_new left_speaker_mixer[] = {
- SOC_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
- SOC_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
- SOC_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
- SOC_SINGLE("DAC Switch", 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控件的寄存器信息:
- /* 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字段則指向一個數組,該數組定義了寄存器可以選擇的值,每個值對應一個輸入端,如果value是一組連續的值,通常我們可以忽略values參數。下面我們先看看如何定義一個mux控件:
第一步,定義字符串和values數組,以下的例子因爲values是連續的,所以不用定義:
- static const char *drc_path_text[] = {
- "ADC",
- "DAC"
- };
第二步,利用ASoc提供的輔助宏定義soc_enum結構,用於描述寄存器:
- static const struct soc_enum drc_path =
- SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);
第三步,利痛ASoc提供的輔助宏,定義soc_kcontrol_new結構,該結構最後用於註冊該mux控件:
- static const struct snd_kcontrol_new wm8993_snd_controls[] = {
- SOC_DOUBLE_TLV(......),
- ......
- SOC_ENUM("DRC Path", drc_path),
- ......
- }
以上幾步定義了一個叫DRC PATH的mux控件,該控件具有兩個輸入選擇,分別是來自ADC和DAC,用寄存器WM8993_DRC_CONTROL_1控制。其中,soc_enum結構使用了輔助宏SOC_ENUM_SINGLE來定義,該宏的聲明如下:
- #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,列出它的定義如下:
- #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, \
- .private_value = (unsigned long)&xenum }
思想如此統一,依然是使用private_value字段記錄soc_enum結構,不過幾個回調函數變了,我們看看get回調對應的snd_soc_get_enum_double函數:
- 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數組中獲得:
- 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結構。
SOC_VALUE_ENUM_DOUBLE SOC_VALUE_ENUM_SINGLE的立體聲版本。
SOC_VALUE_ENUM 用於定義帶values字段snd_kcontrol_new結構,這個有點特別,我們還是看看它的定義:
- #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回調:
- 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數組中查找和寄存器相等的值,然後返回他在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
ALSA聲卡驅動中的DAPM詳解之二:widget-具備路徑和電源管理信息的kcontrol
上一篇文章中,我們介紹了音頻驅動中對基本控制單元的封裝:kcontrol。利用kcontrol,我們可以完成對音頻系統中的mixer,mux,音量控制,音效控制,以及各種開關量的控制,通過對各種kcontrol的控制,使得音頻硬件能夠按照我們預想的結果進行工作。同時我們可以看到,kcontrol還是有以下幾點不足:
- 只能描述自身,無法描述各個kcontrol之間的連接關係;
- 沒有相應的電源管理機制;
- 沒有相應的時間處理機制來響應播放、停止、上電、下電等音頻事件;
- 爲了防止pop-pop聲,需要用戶程序關注各個kcontrol上電和下電的順序;
- 當一個音頻路徑不再有效時,不能自動關閉該路徑上的所有的kcontrol;
/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
爲此,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來描述:
- 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;
- ......
- };
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事件處理回調函數指針。
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的種類,可選的種類都定義在一個枚舉中:
- /* 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對應一個耳機。
- 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結構來描述:
- 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 */
-
- 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結構來描述這樣一個連接關係:
- 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之間的連接,具體的連接代碼分析,我們留到以後的章節中討論。
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回調中進行控制,當然,在工作中如果沒有音頻流時,也可以適當地進行控制它們的開啓與關閉。
- platform域 位於該域上的widget通常是針對平臺或板子的一些需要物理連接的輸入/輸出接口,例如耳機、揚聲器、麥克風,因爲這些接口在每塊板子上都可能不一樣,所以通常它們是在machine驅動中進行定義和控制,並且也可以由用戶空間的應用程序通過某種方式來控制它們的打開和關閉。
- 音頻路徑域 一般是指codec內部的mixer、mux等控制音頻路徑的widget,這些widget可以根據用戶空間的設定連接關係,自動設定他們的電源狀態。
- 音頻數據流域 是指那些需要處理音頻數據流的widget,例如ADC、DAC等等。
codec域widget的定義
目前,DAPM框架只提供了定義一個codec域widget的輔助宏:
- #define SND_SOC_DAPM_VMID(wname) \
- { .id = snd_soc_dapm_vmid, .name = wname, .kcontrol_news = NULL, \
- .num_kcontrols = 0}
platform域widget的定義
DAPM框架爲我們提供了多種platform域widget的輔助定義宏:
- #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, \
- .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的定義宏:
- #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) \
- { .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, \
- .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等等:
- #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) \
- { .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創建一個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,他們的定義方法如下:
- #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的定義。
- #define SOC_DAPM_SINGLE(xname, reg, shift, max, invert) \
- { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \
- .info = snd_soc_info_volsw, \
- .get = snd_soc_dapm_get_volsw, .put = snd_soc_dapm_put_volsw, \
- .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) }
- #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 " Switch", \
- .info = snd_soc_dapm_info_pin_switch, \
- .get = snd_soc_dapm_get_pin_switch, \
- .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:
- static const struct snd_kcontrol_new left_speaker_mixer[] = {
- SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
- SOC_DAPM_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
- SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
- SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
- };
-
- static const struct snd_kcontrol_new right_speaker_mixer[] = {
- SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
- SOC_DAPM_SINGLE("IN1RP Switch", WM8993_SPEAKER_MIXER, 4, 1, 0),
- SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 2, 1, 0),
- SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 0, 1, 0),
- };
-
- static const char *aif_text[] = {
- "Left", "Right"
- };
-
- static const struct soc_enum aifinl_enum =
- SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 15, 2, aif_text);
-
-
- static const struct snd_kcontrol_new aifinl_mux =
- SOC_DAPM_ENUM("AIFINL Mux", 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 Mux", aifinr_enum);
以上,我們定義了wm8993中左右聲道的speaker mixer控件:left_speaker_mixer和right_speaker_mixer,同時還爲左右聲道各定義了一個叫做AIFINL Mux和AIFINR Mux的輸入選擇mux控件。
第二步,定義真正的widget,包含第一步定義好的dapm控件:
- static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {
- ......
- SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
- SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),
- ......
- SND_SOC_DAPM_MUX("DACL Mux", SND_SOC_NOPM, 0, 0, &aifinl_mux),
- SND_SOC_DAPM_MUX("DACR Mux", SND_SOC_NOPM, 0, 0, &aifinr_mux),
-
- SND_SOC_DAPM_MIXER("SPKL", WM8993_POWER_MANAGEMENT_3, 8, 0,
- left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer)),
- SND_SOC_DAPM_MIXER("SPKR", 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控制由上一步定義的left_speaker_mixer和right_speaker_mixer來完成,兩個widget具備電源屬性,所以,當這兩個widget在一條有效的音頻路徑上時,dapm框架可以通過寄存器WM8993_POWER_MANAGEMENT_3的第8位和第9位控制它的電源狀態。
第三步,定義這些widget的連接路徑:
- static const struct snd_soc_dapm_route routes[] = {
- ......
-
- { "DACL Mux", "Left", "AIFINL" },
- { "DACL Mux", "Right", "AIFINR" },
- { "DACR Mux", "Left", "AIFINL" },
- { "DACR Mux", "Right", "AIFINR" },
-
- ......
-
- { "SPKL", "DAC Switch", "DACL" },
- { "SPKL", NULL, "CLK_SYS" },
-
- { "SPKR", "DAC Switch", "DACR" },
- { "SPKR", NULL, "CLK_SYS" },
- };
通過第一步的定義,我們知道DACL Mux和DACR Mux有兩個輸入引腳,分別是
而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和路徑:
- 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_dapm_add_routes(dapm, routes, ARRAY_SIZE(routes));
- ......
- }
在machine驅動中,我們可以用同樣的方式定義和註冊板子特有的widget和路徑信息。
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:
- 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的結構體中:
- struct snd_soc_codec {
- ......
- /* dapm */
- struct snd_soc_dapm_context dapm;
- ......
- };
-
- struct snd_soc_platform {
- ......
- /* 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框架有關的字段:
- struct snd_soc_codec_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;
- ......
- }
我們只要把我們定義好的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。我們看看實際的例子可能是這樣的:
- static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {
- ......
- SND_SOC_DAPM_SUPPLY("VMID", SND_SOC_NOPM, 0, 0, NULL, 0),
- SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
- SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 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);
- ......
- }
上面這種註冊方法有個缺點,有時候我們爲了代碼的清晰,可能會根據功能把不同的widget定義成多個數組,但是snd_soc_codec_driver中只有一個dapm_widgets字段,無法設定多個widget數組,這時候,我們需要主動在codec的probe回調中調用dapm框架提供的api來創建這些widget:
- 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相關的字段:
- 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驅動中註冊,通常他們的dapm context也從屬於聲卡(snd_soc_card)域。做法依然和codec驅動類似,通過代表聲卡的snd_soc_card結構中的幾個dapm字段完成:
- 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驅動,使用第二種方法註冊路徑信息:
- static const struct snd_soc_dapm_widget omap3pandora_in_dapm_widgets[] = {
- SND_SOC_DAPM_MIC("Mic (internal)", NULL),
- SND_SOC_DAPM_MIC("Mic (external)", NULL),
- SND_SOC_DAPM_LINE("Line In", NULL),
- };
-
- static const struct snd_soc_dapm_route omap3pandora_out_map[] = {
- {"PCM DAC", NULL, "APLL Enable"},
- {"Headphone Amplifier", NULL, "PCM DAC"},
- {"Line Out", NULL, "PCM DAC"},
- {"Headphone Jack", NULL, "Headphone Amplifier"},
- };
-
- static const struct snd_soc_dapm_route omap3pandora_in_map[] = {
- {"AUXL", NULL, "Line In"},
- {"AUXR", NULL, "Line In"},
-
- {"MAINMIC", NULL, "Mic (internal)"},
- {"Mic (internal)", NULL, "Mic Bias 1"},
-
- {"SUBMIC", NULL, "Mic (external)"},
- {"Mic (external)", NULL, "Mic Bias 2"},
- };
- 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, "EARPIECE");
- ......
- //註冊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));
- }
-
- 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, "HSMIC");
- ......
- //註冊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 = "PCM1773",
- ......
- .init = omap3pandora_out_init,
- }, {
- .name = "TWL4030",
- .stream_name = "Line/Mic In",
- ......
- .init = omap3pandora_in_init,
- }
- };
dai widget
上面幾節的內容介紹了codec、platform以及machine級別的widget和route的註冊方法,在dapm框架中,還有另外一種widget,它代表了一個dai(數字音頻接口),關於dai的描述,請參考:Linux ALSA聲卡驅動之七:ASoC架構中的Codec。dai按所在的位置,又分爲cpu dai和codec dai,在硬件上,通常一個cpu dai會連接一個codec dai,而在machine驅動中,我們要在snd_soc_card結構中指定一個叫做snd_soc_dai_link的結構,該結構定義了聲卡使用哪一個cpu dai和codec dai進行連接。在Asoc中,一個dai用snd_soc_dai結構來表述,其中有幾個字段和dapm框架有關:
- 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結構指針:
- static struct snd_soc_dai_driver wm8993_dai = {
- .name = "wm8993-hifi",
- .playback = {
- .stream_name = "Playback",
- .channels_min = 1,
- .channels_max = 2,
- .rates = WM8993_RATES,
- .formats = WM8993_FORMATS,
- .sig_bits = 24,
- },
- .capture = {
- .stream_name = "Capture",
- .channels_min = 1,
- .channels_max = 2,
- .rates = WM8993_RATES,
- .formats = WM8993_FORMATS,
- .sig_bits = 24,
- },
- .ops = &wm8993_ops,
- .symmetric_rates = 1,
- };
-
- 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:
- 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的代碼:
- 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;
- 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函數中,通過比較dai->dev和platform->dev,挑選出屬於該platform的dai,然後通過snd_soc_dapm_new_dai_widgets爲cpu dai創建相應的widget:
- 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;
- }
從上面的代碼我們也可以看出,在上面的」創建和註冊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就會認爲它是一條完整音頻路徑的開始和結束,從而結束一次掃描動作。至於代碼的分析,先讓我歇一會......,我會在後面的文章中討論。
ALSA聲卡驅動中的DAPM詳解之五:建立widget之間的連接關係
前面我們主要着重於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之間具備連接能力,我們還需要第二個函數:
這個函數會根據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鏈表中,我們看看他的定義:
- 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,
- "ASoC: Failed to create DAPM control %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重新分配內存,並把模板的內容拷貝過來:
- 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的類型做不同的處理:
- 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);
- ......
- }
- 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的名稱前加入必要的前綴:
- if (dapm->codec && dapm->codec->name_prefix)
- w->name = kasprintf(GFP_KERNEL, "%s %s",
- dapm->codec->name_prefix, widget->name);
- else
- w->name = kasprintf(GFP_KERNEL, "%s", widget->name);
然後,爲不同類型的widget設置合適的power_check電源狀態回調函數,widget類型和對應的power_check回調函數設置如下表所示:
widget的power_check回調函數
widget類型 |
power_check回調函數 |
mixer類: snd_soc_dapm_switch snd_soc_dapm_mixer snd_soc_dapm_mixer_named_ctl |
dapm_generic_check_power |
mux類: snd_soc_dapm_mux snd_soc_dapm_mux snd_soc_dapm_mux |
dapm_generic_check_power |
snd_soc_dapm_dai_out |
dapm_adc_check_power |
snd_soc_dapm_dai_in |
dapm_dac_check_power |
端點類: 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 snd_soc_dapm_spk snd_soc_dapm_hp snd_soc_dapm_mic snd_soc_dapm_line snd_soc_dapm_dai_link |
dapm_generic_check_power |
電源/時鐘/影子widget: snd_soc_dapm_supply snd_soc_dapm_regulator_supply snd_soc_dapm_clock_supply snd_soc_dapm_kcontrol |
dapm_supply_check_power |
其它類型 |
dapm_always_on_check_power |
當音頻路徑發生變化時,power_check回調會被調用,用於檢查該widget的電源狀態是否需要更新。power_check設置完成後,需要設置widget所屬的codec、platform和dapm context,幾個用於音頻路徑的鏈表也需要初始化,然後,把該widget加入到聲卡的widgets鏈表中:
- 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狀態:
- /* 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鏈表中
爲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有關:
- 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);
-
- #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;
- 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現在還沒有被創建:
- 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:
- 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字段中:
- /* 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可能使用同一個寄存器):
- w->new = 1;
-
- dapm_mark_dirty(w, "new widget");
- dapm_debugfs_add_widget(w);
最後,通過dapm_power_widgets函數,統一處理所有位於dapm_dirty鏈表上的widget的狀態改變:
- dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);
- ......
- return 0;
dapm mixer kcontrol
上一節中,我們提到,對於mixer類型的dapm kcontrol,我們會使用dapm_new_mixer來完成具體的創建工作,先看代碼後分析:
- static int dapm_new_mixer(struct snd_soc_dapm_widget *w)
- {
- int i, ret;
- struct snd_soc_dapm_path *path;
-
- /* add kcontrol */
- <span style="font-family:Arial,Helvetica,sans-serif">(1)</span> 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。
(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來實現這一點:
- 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:
- 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;
- int ret;
-
- (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);
- 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的創建函數是個空函數,所以我們不用太關注它:
- 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;
- }
dapm_create_or_share_mixmux_kcontrol函數
上面所說的mixer類型和mux類型的widget,在創建他們所包含的dapm kcontrol時,最後其實都是使用了dapm_create_or_share_mixmux_kcontrol函數來完成創建工作的,所以在這裏我們有必要分析一下這個函數的工作原理。這個函數中有很大一部分代碼實在處理kcontrol的名字是否要加入codec的前綴,我們會忽略這部分的代碼,感興趣的讀者可以自己查看內核的代碼,路徑在: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創建函數,請參看:
Linux ALSA聲卡驅動之四:Control設備的創建中的「創建control「一節的內容。
(4) 如果widget支持autodisable特性,創建與該kcontrol所對應的影子widget,該影子widget的類型是:snd_soc_dapm_kcontrol。
(5) 標準的kcontrol創建函數,請參看:
Linux ALSA聲卡驅動之四:Control設備的創建中的「創建control「一節的內容。
(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數組中,供路徑的控制和匹配之用。
爲widget建立連接關係
如果widget之間沒有連接關係,dapm就無法實現動態的電源管理工作,正是widget之間有了連結關係,這些連接關係形成了一條所謂的完成的音頻路徑,dapm可以順着這條路徑,統一控制路徑上所有widget的電源狀態,前面我們已經知道,widget之間是使用snd_soc_path結構進行連接的,驅動要做的是定義一個snd_soc_route結構數組,該數組的每個條目描述了目的widget的和源widget的名稱,以及控制這個連接的kcontrol的名稱,最終,驅動程序使用api函數snd_soc_dapm_add_routes來註冊這些連接信息,接下來我們就是要分析該函數的具體實現方式:
- int snd_soc_dapm_add_routes(struct snd_soc_dapm_context *dapm,
- const struct snd_soc_dapm_route *route, int num)
- {
- int i, r, ret = 0;
-
- 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函數看看:
- 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:
- if (!wsink)
- wsink = wtsink;
- if (!wsource)
- wsource = wtsource;
最後,使用來增加一條連接信息:
- ret = snd_soc_dapm_add_path(dapm, wsource, wsink, route->control,
- route->connected);
- ......
-
- return 0;
- }
snd_soc_dapm_add_path函數是整個調用鏈條中的關鍵,我們來分析一下:
- static int snd_soc_dapm_add_path(struct snd_soc_dapm_context *dapm,
- struct snd_soc_dapm_widget *wsource, struct snd_soc_dapm_widget *wsink,
- const char *control,
- int (*connected)(struct snd_soc_dapm_widget *source,
- struct snd_soc_dapm_widget *sink))
- {
- struct snd_soc_dapm_path *path;
- int ret;
-
- path = kzalloc(sizeof(struct snd_soc_dapm_path), GFP_KERNEL);
- if (!path)
- return -ENOMEM;
-
- 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結構中的幾個鏈表。
- /* 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具有外部連接關係。
- 源widget是一個輸出腳,如果目的widget是spk、hp、line或input,則認爲源widget具有外部連接關係。
- 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和目的widget加入到dapm_dirty鏈表中。如果沒有kcontrol來控制該連接關係,則這是一個靜態連接,直接用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的作者們想着以後可能會有所擴展吧。
- 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]);
- if (ret != 0)
- goto err;
- break;
- case snd_soc_dapm_switch:
- case snd_soc_dapm_mixer:
- case snd_soc_dapm_mixer_named_ctl:
- ret = dapm_connect_mixer(dapm, wsource, wsink, path, control);
- if (ret != 0)
- goto err;
- break;
目的widget如果是mixer和mux類型,分別用dapm_connect_mixer和dapm_connect_mux函數完成連接工作,這兩個函數我們後面再講。
- 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;
- err:
- kfree(path);
- return ret;
- }
hp、mic、line和spk這幾種widget屬於外部器件,也只是簡單地連接在一起,不過connect字段默認爲是未連接狀態。
現在,我們回過頭來看看目的widget是mixer和mux這兩種類型時的連接方式:
dapm_connect_mixer 用該函數連接一個目的widget爲mixer類型的所有輸入端:
- 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)
- {
- int i;
-
- /* 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類型的所有輸入端:
- 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即可。
設計dapm的主要目的之一,就是希望聲卡上的各種部件的電源按需分配,需要的就上電,不需要的就下電,使得整個音頻系統總是處於最小的耗電狀態,最主要的就是,這一切對用戶空間的應用程序是透明的,也就是說,用戶空間的應用程序無需關心那個部件何時需要電源,它只要按需要設定好音頻路徑,播放音頻數據,暫停或停止,dapm框架會根據音頻路徑,完美地對各種部件的電源進行控制,而且精確地按某種順序進行,防止上下電過程中產生不必要的pop-pop聲。這就是本章我們需要討論的內容。
/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
統計widget連接至端點widget的路徑個數
ALSA聲卡驅動中的DAPM詳解之四:在驅動程序中初始化並註冊widget和route這篇文章中的最後一節,我們曾經提出了端點widget這一概念,端點widget位於音頻路徑的起始端或者末端,所以通常它們就是指codec的輸入輸出引腳所對應的widget,或者是外部器件對應的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 snd_soc_dapm_mic |
音頻流(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 |
影子widget |
snd_soc_dapm_kcontrol |
dapm要給一個widget上電的其中一個前提條件是:這個widget位於一條完整的音頻路徑上,而一條完整的音頻路徑的兩頭,必須是輸入/輸出引腳,或者是一個外部音頻設備,又或者是一個處於激活狀態的音頻流widget,也就是上表中的前三項,上表中的後兩項,它們可以位於路徑的末端,但不是構成完成音頻路徑的必要條件,我們只用它來判斷掃描一條路徑的結束條件。dapm提供了兩個內部函數,用來統計一個widget連接到輸出引腳、輸入引腳、激活的音頻流widget的有效路徑個數:
- is_connected_output_ep 返回連接至輸出引腳或激活狀態的輸出音頻流的路徑數量
- is_connected_input_ep 返回連接至輸入引腳或激活狀態的輸入音頻流的路徑數量
下面我貼出is_connected_output_ep函數和必要的註釋:
- static int is_connected_output_ep(struct snd_soc_dapm_widget *widget,
- struct snd_soc_dapm_widget_list **list)
- {
- struct snd_soc_dapm_path *path;
- int con = 0;
- /* 多個路徑可能使用了同一個widget,如果在遍歷另一個路徑時,*/
- /* 已經統計過該widget,直接返回output字段即可。 */
- if (widget->outputs >= 0)
- return widget->outputs;
-
- /* 以下這幾種widget是端點widget,但不是輸出,所以直接返回0,結束該路徑的掃描 */
- switch (widget->id) {
- case snd_soc_dapm_supply:
- case snd_soc_dapm_regulator_supply:
- case snd_soc_dapm_clock_supply:
- case snd_soc_dapm_kcontrol:
- return 0;
- default:
- break;
- }
- /* 對於音頻流widget,如果處於激活狀態,如果沒有休眠,返回1,否則,返回0 */
- /* 而且對於激活的音頻流widget是端點widget,所以也會結束該路徑的掃描 */
- /* 如果沒有處於激活狀態,按普通的widget繼續往下執行 */
- switch (widget->id) {
- case snd_soc_dapm_adc:
- case snd_soc_dapm_aif_out:
- case snd_soc_dapm_dai_out:
- if (widget->active) {
- widget->outputs = snd_soc_dapm_suspend_check(widget);
- return widget->outputs;
- }
- default:
- break;
- }
-
- if (widget->connected) {
- /* 處於連接狀態的輸出引腳,也根據休眠狀態返回1或0 */
- if (widget->id == snd_soc_dapm_output && !widget->ext) {
- widget->outputs = snd_soc_dapm_suspend_check(widget);
- return widget->outputs;
- }
-
- /* 處於連接狀態的輸出設備,也根據休眠狀態返回1或0 */
- if (widget->id == snd_soc_dapm_hp ||
- widget->id == snd_soc_dapm_spk ||
- (widget->id == snd_soc_dapm_line &&
- !list_empty(&widget->sources))) {
- widget->outputs = snd_soc_dapm_suspend_check(widget);
- return widget->outputs;
- }
- }
- /* 不是端點widget,循環查詢它的輸出端 */
- list_for_each_entry(path, &widget->sinks, list_source) {
- DAPM_UPDATE_STAT(widget, neighbour_checks);
-
- if (path->weak)
- continue;
-
- if (path->walking) /* 比較奇怪,防止無限循環的路徑? */
- return 1;
-
- if (path->walked)
- continue;
-
- if (path->sink && path->connect) {
- path->walked = 1;
- path->walking = 1;