1. 程式人生 > >ALSA音效卡驅動中的DAPM詳解之七:dapm事件機制(dapm event)

ALSA音效卡驅動中的DAPM詳解之七:dapm事件機制(dapm event)

前面的六篇文章,我們已經討論了dapm關於動態電源管理的有關知識,包括widget的建立和初始化,widget之間的連線以及widget的上下電順序等等。本章我們準備討論dapm框架中的另一個機制:事件機制。通過dapm事件機制,widget可以對它所關心的dapm事件做出反應,這種機制對於擴充widget的能力非常有用,例如,對於那些位於codec之外的widget,好像喇叭功放、外部的前置放大器等等,由於不是使用codec內部的暫存器進行電源控制,我們就必須利用dapm的事件機制,獲得相應的上下電事件,從而可以定製widget自身的電源控制功能。

/*****************************************************************************************************/ 宣告:本博內容均由

http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝! /*****************************************************************************************************/

dapm event的種類

dapm目前為我們定義了9種dapm event,他們分別是:

事件型別

說明

SND_SOC_DAPM_PRE_PMU

widget要上電前發出的事件

SND_SOC_DAPM_POST_PMU

widget要上電後發出的事件

SND_SOC_DAPM_PRE_PMD

widget要下電前發出的事件

SND_SOC_DAPM_POST_PMD

widget要下電後發出的事件

SND_SOC_DAPM_PRE_REG

音訊路徑設定之前發出的事件

SND_SOC_DAPM_POST_REG

音訊路徑設定之後發出的事件

SND_SOC_DAPM_WILL_PMU

在處理up_list連結串列之前發出的事件

SND_SOC_DAPM_WILL_PMD

在處理down_list連結串列之前發出的事件

SND_SOC_DAPM_PRE_POST_PMD

SND_SOC_DAPM_PRE_PMD和 SND_SOC_DAPM_POST_PMD的合併

前8種每種佔據一個位,所以,我們可以在一個整數中表達多個我們需要關心的dapm事件,只要把它們按位或進行合併即可。

widget的event回撥函式

ALSA音效卡驅動中的DAPM詳解之二:widget-具備路徑和電源管理資訊的kcontrol中,我們已經介紹過代表widget的snd_soc_widget結構,在這個結構體中,有一個event欄位用於儲存該widget的事件回撥函式,同時,event_flags欄位用於儲存該widget需要關心的dapm事件種類,只有event_flags欄位中相應的事件位被設定了的事件才會發到event回撥函式中進行處理。

我們知道,dapm為我們提供了常用widget的定義輔助巨集,使用以下這幾種輔助巨集定義widget時,預設需要我們提供dapm event回撥函式

  • SND_SOC_DAPM_MIC

  • SND_SOC_DAPM_HP

  • SND_SOC_DAPM_SPK

  • SND_SOC_DAPM_LINE

這些widget都是位於codec外部的器件,它們無法使用通用的暫存器操作來控制widget的電源狀態,所以需要我們提供event回撥函式。以下的例子來自dapm的核心文件,外部的喇叭功放通過CORGI_GPIO_APM_ON這個gpio來控制它的電源狀態:

  1. /* turn speaker amplifier on/off depending on use */

  2. static int corgi_amp_event(struct snd_soc_dapm_widget *w, int event)

  3. {

  4. gpio_set_value(CORGI_GPIO_APM_ON, SND_SOC_DAPM_EVENT_ON(event));

  5. return 0;

  6. }

  7. /* corgi machine dapm widgets */

  8. static const struct snd_soc_dapm_widget wm8731_dapm_widgets =

  9. SND_SOC_DAPM_SPK("Ext Spk", corgi_amp_event);

另外,我們也可以通過以下這些帶"_E"字尾的輔助巨集版本來定義需要dapm事件的widget:

  • 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

觸發dapm event

我們已經定義好了帶有event回撥的widget,那麼,在那裡觸發這些dapm event?答案是:在dapm_power_widgets函式的處理過程中,dapm_power_widgets函式我們已經在ALSA音效卡驅動中的DAPM詳解之六:精髓所在,牽一髮而動全身中做了詳細的分析,其中,在所有需要處理電源變化的widget被分別放入up_list和down_list連結串列後,會相應地發出各種dapm事件:

  1. static int dapm_power_widgets(struct snd_soc_card *card, int event)

  2. {

  3. ......

  4. list_for_each_entry(w, &down_list, power_list) {

  5. dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMD);

  6. }

  7. list_for_each_entry(w, &up_list, power_list) {

  8. dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMU);

  9. }

  10. /* Power down widgets first; try to avoid amplifying pops. */

  11. dapm_seq_run(card, &down_list, event, false);

  12. dapm_widget_update(card);

  13. /* Now power up. */

  14. dapm_seq_run(card, &up_list, event, true);

  15. ......

  16. }

可見,在真正地進行上電和下電之前,dapm向down_list連結串列中的每個widget發出SND_SOC_DAPM_WILL_PMD事件,而向up_list連結串列中的每個widget發出SND_SOC_DAPM_WILL_PMU事件。在處理上下電的函式dapm_seq_run中,會呼叫dapm_seq_run_coalesced函式執行真正的暫存器操作,進行widget的電源控制,dapm_seq_run_coalesced也會發出另外幾種dapm事件:

  1. static void dapm_seq_run_coalesced(struct snd_soc_card *card,

  2. struct list_head *pending)

  3. {

  4. ......

  5. list_for_each_entry(w, pending, power_list) {

  6. ......

  7. /* Check for events */

  8. dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMU);

  9. dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMD);

  10. }

  11. if (reg >= 0) {

  12. ......

  13. pop_wait(card->pop_time);

  14. soc_widget_update_bits_locked(w, reg, mask, value);

  15. }

  16. list_for_each_entry(w, pending, power_list) {

  17. dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMU);

  18. dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMD);

  19. }

  20. }

另外,負責更新音訊路徑的dapm_widget_update函式中也會發出dapm事件:

  1. static void dapm_widget_update(struct snd_soc_card *card)

  2. {

  3. struct snd_soc_dapm_update *update = card->update;

  4. struct snd_soc_dapm_widget_list *wlist;

  5. struct snd_soc_dapm_widget *w = NULL;

  6. unsigned int wi;

  7. int ret;

  8. if (!update || !dapm_kcontrol_is_powered(update->kcontrol))

  9. return;

  10. wlist = dapm_kcontrol_get_wlist(update->kcontrol);

  11. for (wi = 0; wi < wlist->num_widgets; wi++) {

  12. w = wlist->widgets[wi];

  13. if (w->event && (w->event_flags & SND_SOC_DAPM_PRE_REG)) {

  14. ret = w->event(w, update->kcontrol, SND_SOC_DAPM_PRE_REG);

  15. ......

  16. }

  17. }

  18. ......

  19. /* 更新kcontrol的值,改變音訊路徑 */

  20. ret = soc_widget_update_bits_locked(w, update->reg, update->mask,

  21. update->val);

  22. ......

  23. for (wi = 0; wi < wlist->num_widgets; wi++) {

  24. w = wlist->widgets[wi];

  25. if (w->event && (w->event_flags & SND_SOC_DAPM_POST_REG)) {

  26. ret = w->event(w, update->kcontrol, SND_SOC_DAPM_POST_REG);

  27. ......

  28. }

  29. }

  30. }

可見,改變路徑的前後,分別發出了SND_SOC_DAPM_PRE_REG事件和SND_SOC_DAPM_POST_REG事件。

dai widget與stream widget

dai widget    在ALSA音效卡驅動中的DAPM詳解之四:在驅動程式中初始化並註冊widget和route一文中,我們已經討論過dai widget,dai widget又分為cpu dai widget和codec dai widget,它們在machine驅動分別匹配上相應的codec和platform後,由soc_probe_platform和soc_probe_codec這兩個函式通過呼叫dapm的api函式:

  • snd_soc_dapm_new_dai_widgets

來建立的,通常會為playback和capture各自建立一個dai widget,他們的型別分別是:

  • snd_soc_dapm_dai_in      對應playback dai

  • snd_soc_dapm_dai_out    對應capture dai

另外,dai widget的名字是使用stream name來命名的,他通常來自snd_soc_dai_driver中的stream_name欄位。dai widget的sname欄位也使用同樣的名字。

stream widget    stream widget通常是指那些要處理音訊流資料的widget,它們包含以下這幾種型別:

  • snd_soc_dapm_aif_in                 用SND_SOC_DAPM_AIF_IN輔助巨集定義

  • snd_soc_dapm_aif_out               用SND_SOC_DAPM_AIF_OUT輔助巨集定義

  • snd_soc_dapm_dac                    用SND_SOC_DAPM_AIF_DAC輔助巨集定義

  • snd_soc_dapm_adc                    用SND_SOC_DAPM_AIF_ADC輔助巨集定義

對於這幾種widget,我們除了要指定widget的名字外,還要指定他對應的stream的名字,儲存在widget的sname欄位中。

連線dai widget和stream widget

預設情況下,驅動不會通過snd_soc_route來主動定義dai widget和stream widget之間的連線關係,實際上,他們之間的連線關係是由ASoc負責的,在音效卡的初始化函式中,使用snd_soc_dapm_link_dai_widgets函式來建立他們之間的連線關係:

  1. static int snd_soc_instantiate_card(struct snd_soc_card *card)

  2. {

  3. ......

  4. /* card bind complete so register a sound card */

  5. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,

  6. card->owner, 0, &card->snd_card);

  7. ......

  8. if (card->dapm_widgets)

  9. snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,

  10. card->num_dapm_widgets);

  11. /* 建立dai widget和stream widget之間的連線關係 */

  12. snd_soc_dapm_link_dai_widgets(card);

  13. ......

  14. if (card->controls)

  15. snd_soc_add_card_controls(card, card->controls, card->num_controls);

  16. ......

  17. if (card->dapm_routes)

  18. snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,

  19. card->num_dapm_routes);

  20. ......

  21. if (card->fully_routed)

  22. list_for_each_entry(codec, &card->codec_dev_list, card_list)

  23. snd_soc_dapm_auto_nc_codec_pins(codec);

  24. snd_soc_dapm_new_widgets(card);

  25. ret = snd_card_register(card->snd_card);

  26. ......

  27. return 0;

  28. }

我們再來分析一下snd_soc_dapm_link_dai_widgets函式,看看它是如何連線這兩種widget的,它先是遍歷音效卡中所有的widget,找出型別為snd_soc_dapm_dai_in和snd_soc_dapm_dai_out的widget,通過widget的priv欄位,取出widget對應的snd_soc_dai結構指標:

  1. int snd_soc_dapm_link_dai_widgets(struct snd_soc_card *card)

  2. {

  3. struct snd_soc_dapm_widget *dai_w, *w;

  4. struct snd_soc_dai *dai;

  5. /* For each DAI widget... */

  6. list_for_each_entry(dai_w, &card->widgets, list) {

  7. switch (dai_w->id) {

  8. case snd_soc_dapm_dai_in:

  9. case snd_soc_dapm_dai_out:

  10. break;

  11. default:

  12. continue;

  13. }

  14. dai = dai_w->priv;

接著,再次從頭遍歷音效卡中所有的widget,找出能與dai widget相連線的stream widget,第一個前提條件是這兩個widget必須位於同一個dapm context中:

  1. /* ...find all widgets with the same stream and link them */

  2. list_for_each_entry(w, &card->widgets, list) {

  3. if (w->dapm != dai_w->dapm)

  4. continue;

dai widget不會與dai widget相連,所以跳過它們:

  1. switch (w->id) {

  2. case snd_soc_dapm_dai_in:

  3. case snd_soc_dapm_dai_out:

  4. continue;

  5. default:

  6. break;

  7. }

dai widget的名字沒有出現在要連線的widget的stream name中,跳過這個widget:

  1. if (!w->sname || !strstr(w->sname, dai_w->name))

  2. continue;

如果widget的stream name包含了dai的stream name,則匹配成功,連線這兩個widget:

  1. if (dai->driver->playback.stream_name &&

  2. strstr(w->sname,

  3. dai->driver->playback.stream_name)) {

  4. dev_dbg(dai->dev, "%s -> %s\n",

  5. dai->playback_widget->name, w->name);

  6. snd_soc_dapm_add_path(w->dapm,

  7. dai->playback_widget, w, NULL, NULL);

  8. }

  9. if (dai->driver->capture.stream_name &&

  10. strstr(w->sname,

  11. dai->driver->capture.stream_name)) {

  12. dev_dbg(dai->dev, "%s -> %s\n",

  13. w->name, dai->capture_widget->name);

  14. snd_soc_dapm_add_path(w->dapm, w,

  15. dai->capture_widget, NULL, NULL);

  16. }

  17. }

  18. }

  19. return 0;

  20. }

由此可見,dai widget和stream widget是通過stream name進行匹配的,所以,我們在定義codec的stream widget時,它們的stream name必須要包含dai的stream name,這樣才能讓ASoc自動把這兩種widget連線在一起,只有把它們連線在一起,ASoc中的播放、錄音和停止等事件,才能通過dai widget傳遞到codec中,使得codec中的widget能根據目前的播放狀態,動態地開啟或關閉音訊路徑上所有widget的電源。我們看看wm8993中的例子:

  1. SND_SOC_DAPM_AIF_OUT("AIFOUTL", "Capture", 0, SND_SOC_NOPM, 0, 0),

  2. SND_SOC_DAPM_AIF_OUT("AIFOUTR", "Capture", 1, SND_SOC_NOPM, 0, 0),

  3. SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),

  4. SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),

分別定義了左右聲道兩個stream name為Capture和Playback的stream widget。對應的dai driver結構定義如下:

  1. static struct snd_soc_dai_driver wm8993_dai = {

  2. .name = "wm8993-hifi",

  3. .playback = {

  4. .stream_name = "Playback",

  5. .channels_min = 1,

  6. .channels_max = 2,

  7. .rates = WM8993_RATES,

  8. .formats = WM8993_FORMATS,

  9. .sig_bits = 24,

  10. },

  11. .capture = {

  12. .stream_name = "Capture",

  13. .channels_min = 1,

  14. .channels_max = 2,

  15. .rates = WM8993_RATES,

  16. .formats = WM8993_FORMATS,

  17. .sig_bits = 24,

  18. },

  19. .ops = &wm8993_ops,

  20. .symmetric_rates = 1,

  21. };

可見,它們的stream name是一樣的,音效卡初始化階段會把它們連線在一起。需要注意的是,如果我們定義了snd_soc_dapm_aif_in和snd_soc_dapm_aif_out型別的stream widget,並指定了他們的stream name,在定義DAC或ADC對應的widget時,它們的stream name最好不要也使用相同的名字,否則,dai widget即會連線上AIF,也會連線上DAC/ADC,造成音訊路徑的混亂:

  1. SND_SOC_DAPM_ADC("ADCL", NULL, WM8993_POWER_MANAGEMENT_2, 1, 0),

  2. SND_SOC_DAPM_ADC("ADCR", NULL, WM8993_POWER_MANAGEMENT_2, 0, 0),

  3. SND_SOC_DAPM_DAC("DACL", NULL, WM8993_POWER_MANAGEMENT_3, 1, 0),

  4. SND_SOC_DAPM_DAC("DACR", NULL, WM8993_POWER_MANAGEMENT_3, 0, 0),

stream event

把dai widget和stream widget連線在一起,就是為了能把ASoc中的pcm處理部分和dapm進行關聯,pcm的處理過程中,會通過發出stream event來通知dapm系統,重新掃描並調整音訊路徑上各個widget的電源狀態,目前dapm提供了以下幾種stream event:

  1. /* dapm stream operations */

  2. #define SND_SOC_DAPM_STREAM_NOP 0x0

  3. #define SND_SOC_DAPM_STREAM_START 0x1

  4. #define SND_SOC_DAPM_STREAM_STOP 0x2

  5. #define SND_SOC_DAPM_STREAM_SUSPEND 0x4

  6. #define SND_SOC_DAPM_STREAM_RESUME 0x8

  7. #define SND_SOC_DAPM_STREAM_PAUSE_PUSH 0x10

  8. #define SND_SOC_DAPM_STREAM_PAUSE_RELEASE 0x20

比如,在soc_pcm_prepare函式中,會發出SND_SOC_DAPM_STREAM_START事件:

  1. snd_soc_dapm_stream_event(rtd, substream->stream,

  2. SND_SOC_DAPM_STREAM_START);

而在soc_pcm_close函式中,會發出SND_SOC_DAPM_STREAM_STOP事件:

  1. snd_soc_dapm_stream_event(rtd,

  2. SNDRV_PCM_STREAM_PLAYBACK,

  3. SND_SOC_DAPM_STREAM_STOP);

snd_soc_dapm_stream_event函式最終會使用soc_dapm_stream_event函式來完成具體的工作:

  1. static void soc_dapm_stream_event(struct snd_soc_pcm_runtime *rtd, int stream,

  2. int event)

  3. {

  4. struct snd_soc_dapm_widget *w_cpu, *w_codec;

  5. struct snd_soc_dai *cpu_dai = rtd->cpu_dai;

  6. struct snd_soc_dai *codec_dai = rtd->codec_dai;

  7. if (stream == SNDRV_PCM_STREAM_PLAYBACK) {

  8. w_cpu = cpu_dai->playback_widget;

  9. w_codec = codec_dai->playback_widget;

  10. } else {

  11. w_cpu = cpu_dai->capture_widget;

  12. w_codec = codec_dai->capture_widget;

  13. }

該函式首先從snd_soc_pcm_runtime結構中取出cpu dai widget和codec dai widget,接下來:

  1. if (w_cpu) {

  2. dapm_mark_dirty(w_cpu, "stream event");

  3. switch (event) {

  4. case SND_SOC_DAPM_STREAM_START:

  5. w_cpu->active = 1;

  6. break;

  7. case SND_SOC_DAPM_STREAM_STOP:

  8. w_cpu->active = 0;

  9. break;

  10. case SND_SOC_DAPM_STREAM_SUSPEND:

  11. case SND_SOC_DAPM_STREAM_RESUME:

  12. case SND_SOC_DAPM_STREAM_PAUSE_PUSH:

  13. case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:

  14. break;

  15. }

  16. }

把cpu dai widget加入到dapm_dirty連結串列中,根據stream event的型別,把cpu dai widget設定為啟用狀態或非啟用狀態,接下來,對codec dai widget做出同樣的處理:

  1. if (w_codec) {

  2. dapm_mark_dirty(w_codec, "stream event");

  3. switch (event) {

  4. case SND_SOC_DAPM_STREAM_START:

  5. w_codec->active = 1;

  6. break;

  7. case SND_SOC_DAPM_STREAM_STOP:

  8. w_codec->active = 0;

  9. break;

  10. case SND_SOC_DAPM_STREAM_SUSPEND:

  11. case SND_SOC_DAPM_STREAM_RESUME:

  12. case SND_SOC_DAPM_STREAM_PAUSE_PUSH:

  13. case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:

  14. break;

  15. }

  16. }

最後,它呼叫了我們熟悉的dapm_power_widgets函式:

  1. dapm_power_widgets(rtd->card, event);

  2. }

因為dai widget和codec上的stream widget是相連的,所以,dai widget的啟用狀態改變,會沿著音訊路徑傳遞到路徑上的所有widget,等dapm_power_widgets返回後,如果發出的是SND_SOC_DAPM_STREAM_START事件,路徑上的所有widget會處於上電狀態,保證音訊資料流的順利播放,如果發出的是SND_SOC_DAPM_STREAM_STOP事件,路徑上的所有widget會處於下電狀態,保證最小的功耗水平。