設計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;
-
......
-
/* 遞歸調用,統計每一個輸出端 */
-
con += is_connected_output_ep(path->sink, list);
-
-
path->walking = 0;
-
}
-
}
-
-
widget->outputs = con;
-
-
return con;
-
}
該函數使用了遞歸算法,直到遇到端點widget爲止才停止掃描,把統計到的輸出路徑個數保存在output字段中並返回。is_connected_intput_ep函數的原理差不多,有興趣的蘇浙可以自己查看內核的原碼。
dapm_dirty鏈表
在代表聲卡的snd_soc_card結構中,有一個鏈表字段:dapm_dirty,所有狀態發生了改變的widget,dapm不會立刻處理它的電源狀態,而是需要先掛在該鏈表下面,等待後續的進一步處理:或者是上電,或者是下電。dapm爲我們提供了一個api函數來完成這個動作:
-
void dapm_mark_dirty(struct snd_soc_dapm_widget *w, const char *reason)
-
{
-
if (!dapm_dirty_widget(w)) {
-
dev_vdbg(w->dapm->dev, "Marking %s dirty due to %s\n",
-
w->name, reason);
-
list_add_tail(&w->dirty, &w->dapm->card->dapm_dirty);
-
}
-
}
power_check回調函數
在文章 ALSA聲卡驅動中的DAPM詳解之五:建立widget之間的連接關係中,我們知道,在創建widget的時候,widget的power_check回調函數會根據widget的類型,設置不同的回調函數。當widget的狀態改變後,dapm會遍歷dapm_dirty鏈表,並通過power_check回調函數,決定該widget是否需要上電。大多數的widget的power_check回調被設置爲:dapm_generic_check_power:
-
static int dapm_generic_check_power(struct snd_soc_dapm_widget *w)
-
{
-
int in, out;
-
-
DAPM_UPDATE_STAT(w, power_checks);
-
-
in = is_connected_input_ep(w, NULL);
-
dapm_clear_walk_input(w->dapm, &w->sources);
-
out = is_connected_output_ep(w, NULL);
-
dapm_clear_walk_output(w->dapm, &w->sinks);
-
return out != 0 && in != 0;
-
}
很簡單,分別用is_connected_output_ep和is_connected_input_ep得到該widget是否有同時連接到一個輸入端和一個輸出端,如果是,返回1來表示該widget需要上電。
對於snd_soc_dapm_dai_out和snd_soc_dapm_dai_in類型,power_check回調是dapm_adc_check_power和dapm_dac_check_power,這裏以dapm_dac_check_power爲例:
-
static int dapm_dac_check_power(struct snd_soc_dapm_widget *w)
-
{
-
int out;
-
-
DAPM_UPDATE_STAT(w, power_checks);
-
-
if (w->active) {
-
out = is_connected_output_ep(w, NULL);
-
dapm_clear_walk_output(w->dapm, &w->sinks);
-
return out != 0;
-
} else {
-
return dapm_generic_check_power(w);
-
}
-
}
處於激活狀態時,只判斷是否有連接到有效的輸出路徑即可,沒有激活時,則需要同時判斷是否有連接到輸入路徑和輸出路徑。
widget的上電和下電順序
在掃描dapm_dirty鏈表時,dapm使用兩個鏈表來分別保存需要上電和需要下電的widget:
- up_list 保存需要上電的widget
- down_list 保存需要下電的widget
dapm內部使用dapm_seq_insert函數把一個widget加入到上述兩個鏈表中的其中一個:
-
static void dapm_seq_insert(struct snd_soc_dapm_widget *new_widget,
-
struct list_head *list,
-
bool power_up)
-
{
-
struct snd_soc_dapm_widget *w;
-
-
list_for_each_entry(w, list, power_list)
-
if (dapm_seq_compare(new_widget, w, power_up) < 0) {
-
list_add_tail(&new_widget->power_list, &w->power_list);
-
return;
-
}
-
-
list_add_tail(&new_widget->power_list, list);
-
}
上述函數會按照一定的順序把widget加入到鏈表中,從而保證正確的上下電順序:
上電順序 |
下電順序 |
static int dapm_up_seq[] = {
[snd_soc_dapm_pre] = 0,
[snd_soc_dapm_supply] = 1,
[snd_soc_dapm_regulator_supply] = 1,
[snd_soc_dapm_clock_supply] = 1,
[snd_soc_dapm_micbias] = 2,
[snd_soc_dapm_dai_link] = 2,
[snd_soc_dapm_dai_in] = 3,
[snd_soc_dapm_dai_out] = 3,
[snd_soc_dapm_aif_in] = 3,
[snd_soc_dapm_aif_out] = 3,
[snd_soc_dapm_mic] = 4,
[snd_soc_dapm_mux] = 5,
[snd_soc_dapm_virt_mux] = 5,
[snd_soc_dapm_value_mux] = 5,
[snd_soc_dapm_dac] = 6,
[snd_soc_dapm_switch] = 7,
[snd_soc_dapm_mixer] = 7,
[snd_soc_dapm_mixer_named_ctl] = 7,
[snd_soc_dapm_pga] = 8,
[snd_soc_dapm_adc] = 9,
[snd_soc_dapm_out_drv] = 10,
[snd_soc_dapm_hp] = 10,
[snd_soc_dapm_spk] = 10,
[snd_soc_dapm_line] = 10,
[snd_soc_dapm_kcontrol] = 11,
[snd_soc_dapm_post] = 12,
}; |
static int dapm_down_seq[] = {
[snd_soc_dapm_pre] = 0,
[snd_soc_dapm_kcontrol] = 1,
[snd_soc_dapm_adc] = 2,
[snd_soc_dapm_hp] = 3,
[snd_soc_dapm_spk] = 3,
[snd_soc_dapm_line] = 3,
[snd_soc_dapm_out_drv] = 3,
[snd_soc_dapm_pga] = 4,
[snd_soc_dapm_switch] = 5,
[snd_soc_dapm_mixer_named_ctl] = 5,
[snd_soc_dapm_mixer] = 5,
[snd_soc_dapm_dac] = 6,
[snd_soc_dapm_mic] = 7,
[snd_soc_dapm_micbias] = 8,
[snd_soc_dapm_mux] = 9,
[snd_soc_dapm_virt_mux] = 9,
[snd_soc_dapm_value_mux] = 9,
[snd_soc_dapm_aif_in] = 10,
[snd_soc_dapm_aif_out] = 10,
[snd_soc_dapm_dai_in] = 10,
[snd_soc_dapm_dai_out] = 10,
[snd_soc_dapm_dai_link] = 11,
[snd_soc_dapm_clock_supply] = 12,
[snd_soc_dapm_regulator_supply] = 12,
[snd_soc_dapm_supply] = 12,
[snd_soc_dapm_post] = 13,
}; |
widget的上下電過程
dapm_power_widgets
當一個widget的狀態改變後,該widget會被加入dapm_dirty鏈表,然後通過dapm_power_widgets函數來改變整個音頻路徑上的電源狀態,下圖展現了這個函數的調用過程:
圖1 widget的上電過程
- 可見,該函數通過遍歷dapm_dirty鏈表,對每個鏈表中的widget調用dapm_power_one_widget,dapm_power_one_widget函數除了處理自身的狀態改變外,還把自身的變化傳遞到和它相連的鄰居widget中,結果就是,所有需要上電的widget會被放在up_list鏈表中,而所有需要下電的widget會被放在down_list鏈表中,這個函數我們稍後再討論。
- 遍歷down_list鏈表,向其中的widget發出SND_SOC_DAPM_WILL_PMD事件,感興趣該事件的widget的event回調會被調用。
- 遍歷up_list鏈表,向其中的widget發出SND_SOC_DAPM_WILL_PMU事件,感興趣該事件的widget的event回調會被調用。
- 通過dapm_seq_run函數,處理down_list中的widget,使它們按定義好的順序依次下電。
- 通過dapm_widget_update函數,切換觸發該次狀態變化的widget的kcontrol中的寄存器值,對應的結果就是:改變音頻路徑。
- 通過dapm_seq_run函數,處理up_list中的widget,使它們按定義好的順序依次上電。
- 對每個dapm context發出狀態改變回調。
- 適當的延時,防止pop-pop聲。
dapm_power_one_widget
dapm_power_widgets的第一步,就是遍歷dapm_dirty鏈表,對每個鏈表中的widget調用dapm_power_one_widget,把需要上電和需要下電的widget分別加入到up_list和down_list鏈表中,同時,他還會把受到影響的鄰居widget再次加入到dapm_dirty鏈表的末尾,通過這個動作,聲卡中所以受到影響的widget都會被“感染”,依次被加到dapm_dirty鏈表,然後依次被執行dapm_power_one_widget函數。下圖展示了dapm_power_one_widget函數的調用序列:
圖二 dapm_power_one_widget函數調用過程
- 通過dapm_widget_power_check,調用widget的power_check回調函數,獲得該widget新的電源狀態。
- 調用dapm_widget_set_power,“感染”與之相連的鄰居widget。
- 遍歷source widget,通過dapm_widget_set_peer_power函數,把處於連接狀態的source widget加入dapm_dirty鏈表中。
- 遍歷sink widget,通過dapm_widget_set_peer_power函數,把處於連接狀態的sink widget加入dapm_dirty鏈表中。
- 根據第一步得到的新的電源狀態,把widget加入到up_list或down_list鏈表中。
可見,通過該函數,一個widget的狀態改變,鄰居widget會受到“感染”而被加入到dapm_dirty鏈表的末尾,所以掃描到鏈表的末尾時,鄰居widget也會執行同樣的操作,從而“感染”鄰居的鄰居,直到沒有新的widget被加入dapm_dirty鏈表爲止,這時,所有受到影響的widget都被加入到up_list或down_li鏈表中,等待後續的上下電操作。這就是文章的標題所說的那樣:
牽一髮而動全身。
dapm_seq_run
參看圖一的上電過程,當所有需要上電或下電的widget都被加入到dapm_dirty鏈表後,接着會通過dapm_seq_run處理down_list鏈表上的widget,把該鏈表上的widget按順序下電,然後通過dapm_widget_update更新widget中的kcontrol(這個kcontrol通常就是觸發本次狀態改變的觸發源),接着又通過apm_seq_run處理up_list鏈表上的widget,把該鏈表上的widget按順序上電。最終的上電或下電操作需要通過codec的寄存器來實現,因爲定義widget時,如果這是一個帶電源控制的widget,我們必須提供reg/shift等字段的設置值,如果該widget無需寄存器控制電源狀態,則reg字段必須賦值爲:
- SND_SOC_NOPM (該宏定義的實際值是-1)
具體實現上,dapm框架使用了一點技巧:如果位於同一個上下電順序的幾個widget使用了同一個寄存器地址(一個寄存器可能使用不同的位來控制不同的widget的電源狀態),dapm_seq_run通過dapm_seq_run_coalesced函數合併這幾個widget的變更,然後只需要把合併後的值一次寫入寄存器即可。
dapm kcontrol的put回調
上面我們已經討論瞭如何判斷一個widget是否需要上電,以及widget的上電過程,一個widget的狀態改變如何傳遞到整個音頻路徑上的所有widget。這些過程總是需要一個起始點:是誰觸動了dapm,使得它需要執行上述的掃描和上電過程?事實上,以下幾種情況可以觸發dapm發起一次掃描操作:
- 聲卡初始化階段,snd_soc_dapm_new_widgets函數創建widget包含的kcontrol後,會觸發一次掃描操作。
- 用戶空間的應用程序修改了widget中包含的dapm kcontrol的配置值時,會觸發一次掃描操作。
- pcm的打開或關閉,會通過音頻流widget觸發一次掃描操作。
- 驅動程序在改變了某個widget並把它加入到dapm_dirty鏈表後,主動調用snd_soc_dapm_sync函數觸發掃描操作。
這裏我們主要討論一下第二種,用戶空間對kcontrol的修改,最終都會調用到kcontrol的put回調函數。對於常用的dapm kcontrol,系統已經爲我們定義好了它們的put回調函數:
- snd_soc_dapm_put_volsw mixer類型的dapm kcontrol使用的put回調
- snd_soc_dapm_put_enum_double mux類型的dapm kcontrol使用的put回調
- snd_soc_dapm_put_enum_virt 虛擬mux類型的dapm kcontrol使用的put回調
- snd_soc_dapm_put_value_enum_double 控制值不連續的mux類型的dapm kcontrol使用的put回調
- snd_soc_dapm_put_pin_switch 引腳類dapm kcontrol使用的put回調
我們以mixer類型的dapm kcontrol的put回調講解一下觸發的過程:
圖三 mixer dapm kcontrol的put回調
-
int snd_soc_dapm_put_volsw(struct snd_kcontrol *kcontrol,
-
struct snd_ctl_elem_value *ucontrol)
-
{
-
struct snd_soc_codec *codec = snd_soc_dapm_kcontrol_codec(kcontrol);
-
struct snd_soc_card *card = codec->card;
-
struct soc_mixer_control *mc =
-
(struct soc_mixer_control *)kcontrol->private_value;
-
unsigned int reg = mc->reg;
-
unsigned int shift = mc->shift;
-
int max = mc->max;
-
unsigned int mask = (1 << fls(max)) - 1;
-
unsigned int invert = mc->invert;
-
unsigned int val;
-
int connect, change;
-
struct snd_soc_dapm_update update;
-
......
-
-
val = (ucontrol->value.integer.value[0] & mask);
-
connect = !!val;
-
-
if (invert)
-
val = max - val;
-
-
-
dapm_kcontrol_set_value(kcontrol, val);
-
-
mask = mask << shift;
-
val = val << shift;
-
-
-
-
change = snd_soc_test_bits(codec, reg, mask, val);
-
if (change) {
-
update.kcontrol = kcontrol;
-
update.reg = reg;
-
update.mask = mask;
-
update.val = val;
-
-
card->update = &update;
-
-
soc_dapm_mixer_update_power(card, kcontrol, connect);
-
-
card->update = NULL;
-
}
-
......
-
return change;
-
}
其中的dapm_kcontrol_set_value函數用於把設置值緩存到kcontrol對應的影子widget,影子widget是爲了實現autodisable特性而創建的一個虛擬widget,影子widget的輸出連接到kcontrol的source widget,影子widget的寄存器被設置爲和kcontrol一樣的寄存器地址,這樣當source widget被關閉時,會觸發影子widget被關閉,其作用就是kcontrol也被自動關閉從而在物理上斷開與source widget的連接,但是此時邏輯連接依然有效,dapm依然認爲它們是連接在一起的。
觸發dapm進行電源狀態掃描關鍵的函數是soc_dapm_mixer_update_power:
-
static int soc_dapm_mixer_update_power(struct snd_soc_card *card,
-
struct snd_kcontrol *kcontrol, int connect)
-
{
-
struct snd_soc_dapm_path *path;
-
int found = 0;
-
-
-
dapm_kcontrol_for_each_path(path, kcontrol) {
-
found = 1;
-
path->connect = connect;
-
-
dapm_mark_dirty(path->source, "mixer connection");
-
dapm_mark_dirty(path->sink, "mixer update");
-
}
-
-
if (found)
-
dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);
-
-
return found;
-
}
最終,還是通過dapm_power_widgets函數,觸發整個音頻路徑的掃描過程,這個函數執行後,因爲kcontrol的狀態改變,被斷開連接的音頻路徑上的所有widget被按順序下電,而重新連上的音頻路徑上的所有widget被順序地上電,所以,儘管我們只改變了mixer kcontrol中的一個輸入端的連接狀態,所有相關的widget的電源狀態都會被重新設定,這一切,都是自動完成的,對用戶空間的應用程序完全透明,實現了dapm的原本設計目標。