前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path。之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开始,我们要从代码入手,分析dapm的详细工作原理:
在讨论widget的注册之前,我们先了解另一个概念:dapm context,直译过来的意思是dapm上下文,这个好像不好理解,其实我们可以这么理解:dapm把整个音频系统,按照功能和偏置电压级别,划分为若干个电源域,每个域包含各自的widget,每个域中的所有widget通常都处于同一个偏置电压级别上,而一个电源域就是一个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; /* Use BIAS_OFF instead of STANDBY */ struct snd_soc_dapm_update *update; void (*seq_notifier)(struct snd_soc_dapm_context *, enum snd_soc_dapm_type, int); struct device *dev; /* from parent - for debug */ struct snd_soc_codec *codec; /* parent codec */ struct snd_soc_platform *platform; /* parent platform */ struct snd_soc_card *card; /* parent card */ /* used during DAPM updates */ 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的取值范围是以下几种:
struct snd_soc_codec { ...... /* dapm */ struct snd_soc_dapm_context dapm; ...... }; struct snd_soc_platform { ...... /* dapm */ struct snd_soc_dapm_context dapm; ...... }; struct snd_soc_card { ...... /* dapm */ struct snd_soc_dapm_context dapm; ...... }; : struct snd_soc_dai { ...... /* dapm */ 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用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 { ...... /* Default control and setup, added after probe() is run */ 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 { ...... /* Default control and setup, added after probe() is run */ 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 { ...... /* * Card-specific routes and widgets. */ 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的创建工作。
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; /* All TWL4030 output pins are floating */ snd_soc_dapm_nc_pin(dapm, "EARPIECE"); ...... //注册kcontrol控件 ret = snd_soc_dapm_new_controls(dapm, omap3pandora_out_dapm_widgets, ARRAY_SIZE(omap3pandora_out_dapm_widgets)); if (ret < 0) return ret; //注册machine的音频路径 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; /* Not comnnected */ snd_soc_dapm_nc_pin(dapm, "HSMIC"); ...... //注册kcontrol控件 ret = snd_soc_dapm_new_controls(dapm, omap3pandora_in_dapm_widgets, ARRAY_SIZE(omap3pandora_in_dapm_widgets)); if (ret < 0) return ret; //注册machine音频路径 return snd_soc_dapm_add_routes(dapm, omap3pandora_in_map, ARRAY_SIZE(omap3pandora_in_map)); } /* Digital audio interface glue - connects codec <--> CPU */ 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, } };
上面几节的内容介绍了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) { ...... /* Create DAPM widgets for each DAI stream */ 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; // 创建播放 dai widget 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; } // 创建录音 dai widget 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。
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); /* Create DAPM widgets for each DAI stream */ 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的代码里没有贴出来,有兴趣的读者自己查看一下它的代码即可。
一条完整的dapm音频路径,必然有起点和终点,我们把位于这些起点和终点的widget称之为端点widget。以下这些类型的widget可以成为端点widget:
codec的输入输出引脚: