ALSA聲卡驅動中的DAPM詳解之六:精髓所在,牽一髮而動全身

設計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函數和必要的註釋:

[html] view plain copy
  1. static int is_connected_output_ep(struct snd_soc_dapm_widget *widget,  
  2.         struct snd_soc_dapm_widget_list **list)  
  3. {  
  4.         struct snd_soc_dapm_path *path;  
  5.         int con = 0;  
  6.         /*  多個路徑可能使用了同一個widget,如果在遍歷另一個路徑時,*/  
  7.         /*  已經統計過該widget,直接返回output字段即可。            */  
  8.         if (widget->outputs >= 0)  
  9.                 return widget->outputs;  
  10.   
  11.         /*  以下這幾種widget是端點widget,但不是輸出,所以直接返回0,結束該路徑的掃描  */  
  12.         switch (widget->id) {  
  13.         case snd_soc_dapm_supply:  
  14.         case snd_soc_dapm_regulator_supply:  
  15.         case snd_soc_dapm_clock_supply:  
  16.         case snd_soc_dapm_kcontrol:  
  17.                 return 0;  
  18.         default:  
  19.                 break;  
  20.         }  
  21.         /*  對於音頻流widget,如果處於激活狀態,如果沒有休眠,返回1,否則,返回0  */  
  22.         /*  而且對於激活的音頻流widget是端點widget,所以也會結束該路徑的掃描  */  
  23.         /*  如果沒有處於激活狀態,按普通的widget繼續往下執行  */  
  24.         switch (widget->id) {  
  25.         case snd_soc_dapm_adc:  
  26.         case snd_soc_dapm_aif_out:  
  27.         case snd_soc_dapm_dai_out:  
  28.                 if (widget->active) {  
  29.                         widget->outputs = snd_soc_dapm_suspend_check(widget);  
  30.                         return widget->outputs;  
  31.                 }  
  32.         default:  
  33.                 break;  
  34.         }  
  35.   
  36.         if (widget->connected) {  
  37.                 /* 處於連接狀態的輸出引腳,也根據休眠狀態返回1或0 */  
  38.                 if (widget->id == snd_soc_dapm_output && !widget->ext) {  
  39.                         widget->outputs = snd_soc_dapm_suspend_check(widget);  
  40.                         return widget->outputs;  
  41.                 }  
  42.   
  43.                 /* 處於連接狀態的輸出設備,也根據休眠狀態返回1或0 */  
  44.                 if (widget->id == snd_soc_dapm_hp ||  
  45.                     widget->id == snd_soc_dapm_spk ||  
  46.                     (widget->id == snd_soc_dapm_line &&  
  47.                      !list_empty(&widget->sources))) {  
  48.                         widget->outputs = snd_soc_dapm_suspend_check(widget);  
  49.                         return widget->outputs;  
  50.                 }  
  51.         }  
  52.         /*  不是端點widget,循環查詢它的輸出端  */  
  53.         list_for_each_entry(path, &widget->sinks, list_source) {  
  54.                 DAPM_UPDATE_STAT(widget, neighbour_checks);  
  55.   
  56.                 if (path->weak)  
  57.                         continue;  
  58.   
  59.                 if (path->walking)   /* 比較奇怪,防止無限循環的路徑? */  
  60.                         return 1;  
  61.   
  62.                 if (path->walked)  
  63.                         continue;  
  64.   
  65.                 if (path->sink && path->connect) {  
  66.                         path->walked = 1;  
  67.                         path->walking = 1;  
  68.                         ......  
  69.                         /*  遞歸調用,統計每一個輸出端  */  
  70.                         con += is_connected_output_ep(path->sink, list);  
  71.   
  72.                         path->walking = 0;  
  73.                 }  
  74.         }  
  75.   
  76.         widget->outputs = con;  
  77.   
  78.         return con;  
  79. }  
該函數使用了遞歸算法,直到遇到端點widget爲止才停止掃描,把統計到的輸出路徑個數保存在output字段中並返回。is_connected_intput_ep函數的原理差不多,有興趣的蘇浙可以自己查看內核的原碼。

dapm_dirty鏈表

在代表聲卡的snd_soc_card結構中,有一個鏈表字段:dapm_dirty,所有狀態發生了改變的widget,dapm不會立刻處理它的電源狀態,而是需要先掛在該鏈表下面,等待後續的進一步處理:或者是上電,或者是下電。dapm爲我們提供了一個api函數來完成這個動作:

[html] view plain copy
  1. void dapm_mark_dirty(struct snd_soc_dapm_widget *w, const char *reason)  
  2. {  
  3.         if (!dapm_dirty_widget(w)) {  
  4.                 dev_vdbg(w->dapm->dev, "Marking %s dirty due to %s\n",  
  5.                          w->name, reason);  
  6.                 list_add_tail(&w->dirty, &w->dapm->card->dapm_dirty);  
  7.         }  
  8. }  

power_check回調函數

在文章 ALSA聲卡驅動中的DAPM詳解之五:建立widget之間的連接關係中,我們知道,在創建widget的時候,widget的power_check回調函數會根據widget的類型,設置不同的回調函數。當widget的狀態改變後,dapm會遍歷dapm_dirty鏈表,並通過power_check回調函數,決定該widget是否需要上電。大多數的widget的power_check回調被設置爲:dapm_generic_check_power:

[html] view plain copy
  1. static int dapm_generic_check_power(struct snd_soc_dapm_widget *w)  
  2. {  
  3.         int in, out;  
  4.   
  5.         DAPM_UPDATE_STAT(w, power_checks);  
  6.   
  7.         in = is_connected_input_ep(w, NULL);  
  8.         dapm_clear_walk_input(w->dapm, &w->sources);  
  9.         out = is_connected_output_ep(w, NULL);  
  10.         dapm_clear_walk_output(w->dapm, &w->sinks);  
  11.         return out != 0 && in != 0;  
  12. }  
很簡單,分別用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爲例:

[html] view plain copy
  1. static int dapm_dac_check_power(struct snd_soc_dapm_widget *w)  
  2. {  
  3.         int out;  
  4.   
  5.         DAPM_UPDATE_STAT(w, power_checks);  
  6.   
  7.         if (w->active) {  
  8.                 out = is_connected_output_ep(w, NULL);  
  9.                 dapm_clear_walk_output(w->dapm, &w->sinks);  
  10.                 return out != 0;  
  11.         } else {  
  12.                 return dapm_generic_check_power(w);  
  13.         }  
  14. }  
處於激活狀態時,只判斷是否有連接到有效的輸出路徑即可,沒有激活時,則需要同時判斷是否有連接到輸入路徑和輸出路徑。

widget的上電和下電順序

在掃描dapm_dirty鏈表時,dapm使用兩個鏈表來分別保存需要上電和需要下電的widget:

  • up_list           保存需要上電的widget
  • down_list     保存需要下電的widget

dapm內部使用dapm_seq_insert函數把一個widget加入到上述兩個鏈表中的其中一個:

[cpp] view plain copy
  1. static void dapm_seq_insert(struct snd_soc_dapm_widget *new_widget,  
  2.                             struct list_head *list,  
  3.                             bool power_up)  
  4. {  
  5.         struct snd_soc_dapm_widget *w;  
  6.   
  7.         list_for_each_entry(w, list, power_list)  
  8.                 if (dapm_seq_compare(new_widget, w, power_up) < 0) {  
  9.                         list_add_tail(&new_widget->power_list, &w->power_list);  
  10.                         return;  
  11.                 }  
  12.   
  13.         list_add_tail(&new_widget->power_list, list);  
  14. }  
上述函數會按照一定的順序把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回調

[cpp] view plain copy
  1. int snd_soc_dapm_put_volsw(struct snd_kcontrol *kcontrol,  
  2.         struct snd_ctl_elem_value *ucontrol)  
  3. {  
  4.         struct snd_soc_codec *codec = snd_soc_dapm_kcontrol_codec(kcontrol);  
  5.         struct snd_soc_card *card = codec->card;  
  6.         struct soc_mixer_control *mc =  
  7.                 (struct soc_mixer_control *)kcontrol->private_value;  
  8.         unsigned int reg = mc->reg;  
  9.         unsigned int shift = mc->shift;  
  10.         int max = mc->max;  
  11.         unsigned int mask = (1 << fls(max)) - 1;  
  12.         unsigned int invert = mc->invert;  
  13.         unsigned int val;  
  14.         int connect, change;  
  15.         struct snd_soc_dapm_update update;  
  16.         ......  
  17.         /* 從參數中取出要設置的新的設置值 */  
  18.         val = (ucontrol->value.integer.value[0] & mask);  
  19.         connect = !!val;  
  20.   
  21.         if (invert)  
  22.                 val = max - val;  
  23.   
  24.         /* 把新的設置值緩存到kcontrol的影子widget中 */  
  25.         dapm_kcontrol_set_value(kcontrol, val);  
  26.   
  27.         mask = mask << shift;  
  28.         val = val << shift;  
  29.         /* 和實際寄存器中的值進行對比,不一樣時纔會觸發寄存器的寫入 */  
  30.         /* 寄存器通常都會通過regmap機制進行緩存,所以這個測試不會發生實際的寄存器讀取操作 */  
  31.         /* 這裏只是觸發,真正的寄存器寫入操作要在掃描完dapm_dirty鏈表後的執行 */  
  32.         change = snd_soc_test_bits(codec, reg, mask, val);  
  33.         if (change) {  
  34.                 update.kcontrol = kcontrol;  
  35.                 update.reg = reg;  
  36.                 update.mask = mask;  
  37.                 update.val = val;  
  38.   
  39.                 card->update = &update;  
  40.                 /* 觸發dapm的上下電掃描過程 */  
  41.                 soc_dapm_mixer_update_power(card, kcontrol, connect);  
  42.   
  43.                 card->update = NULL;  
  44.         }  
  45.         ......  
  46.         return change;  
  47. }  
其中的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:
[cpp] view plain copy
  1. static int soc_dapm_mixer_update_power(struct snd_soc_card *card,  
  2.                                    struct snd_kcontrol *kcontrol, int connect)  
  3. {  
  4.         struct snd_soc_dapm_path *path;  
  5.         int found = 0;  
  6.   
  7.         /* 更新所有和該kcontrol對應輸入端相連的path的connect字段 */  
  8.         dapm_kcontrol_for_each_path(path, kcontrol) {  
  9.                 found = 1;  
  10.                 path->connect = connect;  
  11.                 /*把自己和相連的source widget加入到dirty鏈表中*/  
  12.                 dapm_mark_dirty(path->source, "mixer connection");  
  13.                 dapm_mark_dirty(path->sink, "mixer update");  
  14.         }  
  15.         /* 發起dapm_dirty鏈表掃描和上下電過程 */  
  16.         if (found)  
  17.                 dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);  
  18.   
  19.         return found;  
  20. }  
最終,還是通過dapm_power_widgets函數,觸發整個音頻路徑的掃描過程,這個函數執行後,因爲kcontrol的狀態改變,被斷開連接的音頻路徑上的所有widget被按順序下電,而重新連上的音頻路徑上的所有widget被順序地上電,所以,儘管我們只改變了mixer kcontrol中的一個輸入端的連接狀態,所有相關的widget的電源狀態都會被重新設定,這一切,都是自動完成的,對用戶空間的應用程序完全透明,實現了dapm的原本設計目標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章