前幾篇文章我們從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;
-
-
struct snd_soc_dapm_update *update;
-
-
void (*seq_notifier)(struct snd_soc_dapm_context *,
-
enum snd_soc_dapm_type, int);
-
-
struct device *dev;
-
struct snd_soc_codec *codec;
-
struct snd_soc_platform *platform;
-
struct snd_soc_card *card;
-
-
-
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 {
-
......
-
-
struct snd_soc_dapm_context dapm;
-
......
-
};
-
-
struct snd_soc_platform {
-
......
-
-
struct snd_soc_dapm_context dapm;
-
......
-
};
-
-
struct snd_soc_card {
-
......
-
-
struct snd_soc_dapm_context 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;
-
......
-
};
代表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 {
-
......
-
-
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 {
-
......
-
-
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 {
-
......
-
-
-
-
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;
-
-
-
snd_soc_dapm_nc_pin(dapm, "EARPIECE");
-
......
-
-
ret = snd_soc_dapm_new_controls(dapm, omap3pandora_out_dapm_widgets,
-
ARRAY_SIZE(omap3pandora_out_dapm_widgets));
-
if (ret < 0)
-
return ret;
-
-
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;
-
-
-
snd_soc_dapm_nc_pin(dapm, "HSMIC");
-
......
-
-
ret = snd_soc_dapm_new_controls(dapm, omap3pandora_in_dapm_widgets,
-
ARRAY_SIZE(omap3pandora_in_dapm_widgets));
-
if (ret < 0)
-
return ret;
-
-
return snd_soc_dapm_add_routes(dapm, omap3pandora_in_map,
-
ARRAY_SIZE(omap3pandora_in_map));
-
}
-
-
-
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)
-
{
-
......
-
-
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;
-
-
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;
-
}
-
-
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);
-
-
-
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就會認爲它是一條完整音頻路徑的開始和結束,從而結束一次掃描動作。至於代碼的分析,先讓我歇一會......,我會在後面的文章中討論。