设计dapm的主要目的之一,就是希望声卡上的各种部件的电源按需分配,需要的就上电,不需要的就下电,使得整个音频系统总是处于最小的耗电状态,最主要的就是,这一切对用户空间的应用程序是透明的,也就是说,用户空间的应用程序无需关心那个部件何时需要电源,它只要按需要设定好音频路径,播放音频数据,暂停或停止,dapm框架会根据音频路径,完美地对各种部件的电源进行控制,而且精确地按某种顺序进行,防止上下电过程中产生不必要的pop-pop声。这就是本章我们需要讨论的内容。
/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/
ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route这篇文章中的最后一节,我们曾经提出了端点widget这一概念,端点widget位于音频路径的起始端或者末端,所以通常它们就是指codec的输入输出引脚所对应的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 |
下面我贴出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函数的原理差不多,有兴趣的苏浙可以自己查看内核的原码。
在代表声卡的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); } }
在文章 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); } }处于激活状态时,只判断是否有连接到有效的输出路径即可,没有激活时,则需要同时判断是否有连接到输入路径和输出路径。
在扫描dapm_dirty链表时,dapm使用两个链表来分别保存需要上电和需要下电的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的状态改变后,该widget会被加入dapm_dirty链表,然后通过dapm_power_widgets函数来改变整个音频路径上的电源状态,下图展现了这个函数的调用过程:
图1 widget的上电过程
上面我们已经讨论了如何判断一个widget是否需要上电,以及widget的上电过程,一个widget的状态改变如何传递到整个音频路径上的所有widget。这些过程总是需要一个起始点:是谁触动了dapm,使得它需要执行上述的扫描和上电过程?事实上,以下几种情况可以触发dapm发起一次扫描操作:
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依然认为它们是连接在一起的。
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的原本设计目标。