设计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;/* 把新的设置值缓存到kcontrol的影子widget中 */dapm_kcontrol_set_value(kcontrol, val);mask = mask << shift;val = val << shift;/* 和实际寄存器中的值进行对比,不一样时才会触发寄存器的写入 *//* 寄存器通常都会通过regmap机制进行缓存,所以这个测试不会发生实际的寄存器读取操作 *//* 这里只是触发,真正的寄存器写入操作要在扫描完dapm_dirty链表后的执行 */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;/* 触发dapm的上下电扫描过程 */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;/* 更新所有和该kcontrol对应输入端相连的path的connect字段 */dapm_kcontrol_for_each_path(path, kcontrol) {found = 1;path->connect = connect;/*把自己和相连的source widget加入到dirty链表中*/dapm_mark_dirty(path->source, "mixer connection");dapm_mark_dirty(path->sink, "mixer update");}/* 发起dapm_dirty链表扫描和上下电过程 */if (found)dapm_power_widgets(card, SND_SOC_DAPM_STREAM_NOP);return found;
}

最终,还是通过dapm_power_widgets函数,触发整个音频路径的扫描过程,这个函数执行后,因为kcontrol的状态改变,被断开连接的音频路径上的所有widget被按顺序下电,而重新连上的音频路径上的所有widget被顺序地上电,所以,尽管我们只改变了mixer kcontrol中的一个输入端的连接状态,所有相关的widget的电源状态都会被重新设定,这一切,都是自动完成的,对用户空间的应用程序完全透明,实现了dapm的原本设计目标。

ALSA声卡驱动中的DAPM详解之六:精髓所在,牵一发而动全身相关推荐

  1. ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

    前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path.之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开 ...

  2. ALSA声卡驱动中的DAPM详解之一:kcontrol

    DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态 ...

  3. linux usb驱动中的urb详解

    linux 内核中的 USB 代码和所有的 USB 设备通讯使用称为 urb 的东西( USB request block). 这个请求块用 struct urb 结构描述并且可在 include/l ...

  4. Linux ALSA声卡驱动之八:ASoC架构中的Platform

    1.  Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件,Platform驱动的主要作用是完成音频数据的管理,最终通过C ...

  5. Linux ALSA声卡驱动之四:Codec 以及Codec_dai

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  6. Linux ALSA声卡驱动之二:Platform

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  7. Linux ALSA声卡驱动之五:Machine 以及ALSA声卡的注册

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  8. Linux ALSA声卡驱动之三:Platform之Cpu_dai

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  9. Java中JDBC连接数据库详解

    今天动力节点java学院小编分享的是JDBC连接数据库的相关知识,希望通过看过此文,各位小伙伴对DBC连接数据库有所了解,下面就跟随小编一起来看看JDBC连接数据库的知识吧. 一.JDBC连接数据库概 ...

最新文章

  1. 360safe:腾讯QQ也见不得人[图]
  2. 关于软件工程课程的期望
  3. Git 只拉取部分文件
  4. python中文编码-彻底弄懂python编码
  5. 标准输入流和输出流分别是啥,高效字符流的方法
  6. lastindexof方法_Java Vector lastIndexOf()方法与示例
  7. oracle提交数据按键,Oracle PLSQL - 仅提交数据库链接(Oracle PLSQL - Commit only database link)...
  8. 基于MySQL和JavaFX的学生管理系统
  9. Maven——安装(二)
  10. 【mysql快速入门】牛客网:查询所有列查询多列查询结果去重查询结构返回限制行数将查询后的列重新命名
  11. AlphaGo Zero算法简介
  12. Arduino 系列传感器应用
  13. LINUX彻底清除历史记录命令
  14. 小米2022校招前端实习一面总结
  15. Vue3使用Swiper
  16. WHQL认证的必要性
  17. php实现url伪静态化
  18. discuz 3.1修改浏览器顶部标题 - Powered by Discuz!
  19. 巴黎时装周儿童单元深圳站代言人伊朵,精彩演绎儿童时装
  20. 房贷利率下调 现在是买房的时机吗?

热门文章

  1. 原创:进化论带来对人类的思考
  2. iText in Action 2nd3.1节(Introducing the concept of direct content)读书笔记
  3. CK-FA012-2M分体式高频读写头|读头在刀具管理的应用与性能说明
  4. 行业看点丨 华人科学家找到“天使粒子” 量子计算或成现实
  5. 2023零基础入门网络安全,看这一篇就够了
  6. 【AXIS2 调用WebService报错】The given SCOPAction ..... does not math an operation
  7. 上海大学计算机学院同等学力申硕,上海大学同等学力申硕开设专业
  8. Android基础控件(一)
  9. 话筒好坏测试软件,测试话筒好坏怎么测试?
  10. signature=05cceb4078926b40e205074b176e5db4,BOOKS