ALSA音效卡驅動中的DAPM詳解之七:dapm事件機制(dapm event)
前面的六篇文章,我們已經討論了dapm關於動態電源管理的有關知識,包括widget的建立和初始化,widget之間的連線以及widget的上下電順序等等。本章我們準備討論dapm框架中的另一個機制:事件機制。通過dapm事件機制,widget可以對它所關心的dapm事件做出反應,這種機制對於擴充widget的能力非常有用,例如,對於那些位於codec之外的widget,好像喇叭功放、外部的前置放大器等等,由於不是使用codec內部的暫存器進行電源控制,我們就必須利用dapm的事件機制,獲得相應的上下電事件,從而可以定製widget自身的電源控制功能。
/*****************************************************************************************************/
宣告:本博內容均由
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來控制它的電源狀態:
-
/* turn speaker amplifier on/off depending on use */
-
static int corgi_amp_event(struct snd_soc_dapm_widget *w, int event)
-
{
-
gpio_set_value(CORGI_GPIO_APM_ON, SND_SOC_DAPM_EVENT_ON(event));
-
return 0;
-
}
-
/* corgi machine dapm widgets */
-
static const struct snd_soc_dapm_widget wm8731_dapm_widgets =
-
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事件:
-
static int dapm_power_widgets(struct snd_soc_card *card, int event)
-
{
-
......
-
list_for_each_entry(w, &down_list, power_list) {
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMD);
-
}
-
list_for_each_entry(w, &up_list, power_list) {
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_WILL_PMU);
-
}
-
/* Power down widgets first; try to avoid amplifying pops. */
-
dapm_seq_run(card, &down_list, event, false);
-
dapm_widget_update(card);
-
/* Now power up. */
-
dapm_seq_run(card, &up_list, event, true);
-
......
-
}
可見,在真正地進行上電和下電之前,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事件:
-
static void dapm_seq_run_coalesced(struct snd_soc_card *card,
-
struct list_head *pending)
-
{
-
......
-
list_for_each_entry(w, pending, power_list) {
-
......
-
/* Check for events */
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMU);
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_PRE_PMD);
-
}
-
if (reg >= 0) {
-
......
-
pop_wait(card->pop_time);
-
soc_widget_update_bits_locked(w, reg, mask, value);
-
}
-
list_for_each_entry(w, pending, power_list) {
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMU);
-
dapm_seq_check_event(card, w, SND_SOC_DAPM_POST_PMD);
-
}
-
}
另外,負責更新音訊路徑的dapm_widget_update函式中也會發出dapm事件:
-
static void dapm_widget_update(struct snd_soc_card *card)
-
{
-
struct snd_soc_dapm_update *update = card->update;
-
struct snd_soc_dapm_widget_list *wlist;
-
struct snd_soc_dapm_widget *w = NULL;
-
unsigned int wi;
-
int ret;
-
if (!update || !dapm_kcontrol_is_powered(update->kcontrol))
-
return;
-
wlist = dapm_kcontrol_get_wlist(update->kcontrol);
-
for (wi = 0; wi < wlist->num_widgets; wi++) {
-
w = wlist->widgets[wi];
-
if (w->event && (w->event_flags & SND_SOC_DAPM_PRE_REG)) {
-
ret = w->event(w, update->kcontrol, SND_SOC_DAPM_PRE_REG);
-
......
-
}
-
}
-
......
-
/* 更新kcontrol的值,改變音訊路徑 */
-
ret = soc_widget_update_bits_locked(w, update->reg, update->mask,
-
update->val);
-
......
-
for (wi = 0; wi < wlist->num_widgets; wi++) {
-
w = wlist->widgets[wi];
-
if (w->event && (w->event_flags & SND_SOC_DAPM_POST_REG)) {
-
ret = w->event(w, update->kcontrol, SND_SOC_DAPM_POST_REG);
-
......
-
}
-
}
-
}
可見,改變路徑的前後,分別發出了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函式來建立他們之間的連線關係:
-
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);
-
......
-
if (card->dapm_widgets)
-
snd_soc_dapm_new_controls(&card->dapm, card->dapm_widgets,
-
card->num_dapm_widgets);
-
/* 建立dai widget和stream widget之間的連線關係 */
-
snd_soc_dapm_link_dai_widgets(card);
-
......
-
if (card->controls)
-
snd_soc_add_card_controls(card, card->controls, card->num_controls);
-
......
-
if (card->dapm_routes)
-
snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes,
-
card->num_dapm_routes);
-
......
-
if (card->fully_routed)
-
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);
-
ret = snd_card_register(card->snd_card);
-
......
-
return 0;
-
}
我們再來分析一下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結構指標:
-
int snd_soc_dapm_link_dai_widgets(struct snd_soc_card *card)
-
{
-
struct snd_soc_dapm_widget *dai_w, *w;
-
struct snd_soc_dai *dai;
-
/* For each DAI widget... */
-
list_for_each_entry(dai_w, &card->widgets, list) {
-
switch (dai_w->id) {
-
case snd_soc_dapm_dai_in:
-
case snd_soc_dapm_dai_out:
-
break;
-
default:
-
continue;
-
}
-
dai = dai_w->priv;
接著,再次從頭遍歷音效卡中所有的widget,找出能與dai widget相連線的stream widget,第一個前提條件是這兩個widget必須位於同一個dapm context中:
-
/* ...find all widgets with the same stream and link them */
-
list_for_each_entry(w, &card->widgets, list) {
-
if (w->dapm != dai_w->dapm)
-
continue;
dai widget不會與dai widget相連,所以跳過它們:
-
switch (w->id) {
-
case snd_soc_dapm_dai_in:
-
case snd_soc_dapm_dai_out:
-
continue;
-
default:
-
break;
-
}
dai widget的名字沒有出現在要連線的widget的stream name中,跳過這個widget:
-
if (!w->sname || !strstr(w->sname, dai_w->name))
-
continue;
如果widget的stream name包含了dai的stream name,則匹配成功,連線這兩個widget:
-
if (dai->driver->playback.stream_name &&
-
strstr(w->sname,
-
dai->driver->playback.stream_name)) {
-
dev_dbg(dai->dev, "%s -> %s\n",
-
dai->playback_widget->name, w->name);
-
snd_soc_dapm_add_path(w->dapm,
-
dai->playback_widget, w, NULL, NULL);
-
}
-
if (dai->driver->capture.stream_name &&
-
strstr(w->sname,
-
dai->driver->capture.stream_name)) {
-
dev_dbg(dai->dev, "%s -> %s\n",
-
w->name, dai->capture_widget->name);
-
snd_soc_dapm_add_path(w->dapm, w,
-
dai->capture_widget, NULL, NULL);
-
}
-
}
-
}
-
return 0;
-
}
由此可見,dai widget和stream widget是通過stream name進行匹配的,所以,我們在定義codec的stream widget時,它們的stream name必須要包含dai的stream name,這樣才能讓ASoc自動把這兩種widget連線在一起,只有把它們連線在一起,ASoc中的播放、錄音和停止等事件,才能通過dai widget傳遞到codec中,使得codec中的widget能根據目前的播放狀態,動態地開啟或關閉音訊路徑上所有widget的電源。我們看看wm8993中的例子:
-
SND_SOC_DAPM_AIF_OUT("AIFOUTL", "Capture", 0, SND_SOC_NOPM, 0, 0),
-
SND_SOC_DAPM_AIF_OUT("AIFOUTR", "Capture", 1, SND_SOC_NOPM, 0, 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),
分別定義了左右聲道兩個stream name為Capture和Playback的stream widget。對應的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,
-
};
可見,它們的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,造成音訊路徑的混亂:
-
SND_SOC_DAPM_ADC("ADCL", NULL, WM8993_POWER_MANAGEMENT_2, 1, 0),
-
SND_SOC_DAPM_ADC("ADCR", NULL, WM8993_POWER_MANAGEMENT_2, 0, 0),
-
SND_SOC_DAPM_DAC("DACL", NULL, WM8993_POWER_MANAGEMENT_3, 1, 0),
-
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:
-
/* dapm stream operations */
-
#define SND_SOC_DAPM_STREAM_NOP 0x0
-
#define SND_SOC_DAPM_STREAM_START 0x1
-
#define SND_SOC_DAPM_STREAM_STOP 0x2
-
#define SND_SOC_DAPM_STREAM_SUSPEND 0x4
-
#define SND_SOC_DAPM_STREAM_RESUME 0x8
-
#define SND_SOC_DAPM_STREAM_PAUSE_PUSH 0x10
-
#define SND_SOC_DAPM_STREAM_PAUSE_RELEASE 0x20
比如,在soc_pcm_prepare函式中,會發出SND_SOC_DAPM_STREAM_START事件:
-
snd_soc_dapm_stream_event(rtd, substream->stream,
-
SND_SOC_DAPM_STREAM_START);
而在soc_pcm_close函式中,會發出SND_SOC_DAPM_STREAM_STOP事件:
-
snd_soc_dapm_stream_event(rtd,
-
SNDRV_PCM_STREAM_PLAYBACK,
-
SND_SOC_DAPM_STREAM_STOP);
snd_soc_dapm_stream_event函式最終會使用soc_dapm_stream_event函式來完成具體的工作:
-
static void soc_dapm_stream_event(struct snd_soc_pcm_runtime *rtd, int stream,
-
int event)
-
{
-
struct snd_soc_dapm_widget *w_cpu, *w_codec;
-
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
-
struct snd_soc_dai *codec_dai = rtd->codec_dai;
-
if (stream == SNDRV_PCM_STREAM_PLAYBACK) {
-
w_cpu = cpu_dai->playback_widget;
-
w_codec = codec_dai->playback_widget;
-
} else {
-
w_cpu = cpu_dai->capture_widget;
-
w_codec = codec_dai->capture_widget;
-
}
該函式首先從snd_soc_pcm_runtime結構中取出cpu dai widget和codec dai widget,接下來:
-
if (w_cpu) {
-
dapm_mark_dirty(w_cpu, "stream event");
-
switch (event) {
-
case SND_SOC_DAPM_STREAM_START:
-
w_cpu->active = 1;
-
break;
-
case SND_SOC_DAPM_STREAM_STOP:
-
w_cpu->active = 0;
-
break;
-
case SND_SOC_DAPM_STREAM_SUSPEND:
-
case SND_SOC_DAPM_STREAM_RESUME:
-
case SND_SOC_DAPM_STREAM_PAUSE_PUSH:
-
case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:
-
break;
-
}
-
}
把cpu dai widget加入到dapm_dirty連結串列中,根據stream event的型別,把cpu dai widget設定為啟用狀態或非啟用狀態,接下來,對codec dai widget做出同樣的處理:
-
if (w_codec) {
-
dapm_mark_dirty(w_codec, "stream event");
-
switch (event) {
-
case SND_SOC_DAPM_STREAM_START:
-
w_codec->active = 1;
-
break;
-
case SND_SOC_DAPM_STREAM_STOP:
-
w_codec->active = 0;
-
break;
-
case SND_SOC_DAPM_STREAM_SUSPEND:
-
case SND_SOC_DAPM_STREAM_RESUME:
-
case SND_SOC_DAPM_STREAM_PAUSE_PUSH:
-
case SND_SOC_DAPM_STREAM_PAUSE_RELEASE:
-
break;
-
}
-
}
最後,它呼叫了我們熟悉的dapm_power_widgets函式:
-
dapm_power_widgets(rtd->card, event);
-
}
因為dai widget和codec上的stream widget是相連的,所以,dai widget的啟用狀態改變,會沿著音訊路徑傳遞到路徑上的所有widget,等dapm_power_widgets返回後,如果發出的是SND_SOC_DAPM_STREAM_START事件,路徑上的所有widget會處於上電狀態,保證音訊資料流的順利播放,如果發出的是SND_SOC_DAPM_STREAM_STOP事件,路徑上的所有widget會處於下電狀態,保證最小的功耗水平。