哲学里有三个终极命题:你是谁?从何处来?到何处去?而对于dapm机制,我们可以这样提问:dapm是什么,起到何作用?dapm是如何建立的?dapm又是如何触发的?下面会就这三个问题进行分析。
首先是dapm是什么?这在DAPM之一:概述中提及了,就是音频电源动态管理。相信电源管理大家都不会陌生。dapm设计的目的就是只有需要时才打开必要的部件(widget),不需要时则关闭部件,达到省电的目的,这在便携设备中非常重要。这里“需要”的意思是音频播放录音等操作,“部件”的意思是ADC、DAC、PGA、、MIXER、SPK等等。除此之外,dapm还肩负一个重要角色,就是音频路径,关于这,相信在DAPM之二:audio paths与dapm kcontrol已经描述得足够详细了。因此,这篇仅对电源管理这块做一些分析。
我们先分析dapm模块(源码soc-dapm.c)中的一些重要函数:
更新widget中的reg的值,一般可以认为控制widget的开关。
更新指定寄存器reg、指定位mask的值。
这个函数会在dapm触发时调用,主要是为了减少通电/断电PGA产生的POP音。如下详细分析了这个函数:
/* ramps the volume up or down to minimise pops before or after a * DAPM power event */ static int dapm_set_pga(struct snd_soc_dapm_widget *widget, int power) { //取得该widget的kcontrol,这个kcontrol应该是用于调节PGA的音量的,请留意这点。 //对于PGA widget,一般是单路输入,单路输出,带gain调整。 const struct snd_kcontrol_new *k = widget->kcontrols; //检查该widget是否为muted,从下面看来,当widget power up时,置muted=0,反之置1 if (widget->muted && !power) return 0; if (!widget->muted && power) return 0; if (widget->num_kcontrols && k) { //以下操作取得kcontrol的reg、shift和mask等,用于调整该PGA widget的增益gain。 struct soc_mixer_control *mc = (struct soc_mixer_control *)k->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; if (power) { int i; /* power up has happended, increase volume to last level */ //power up widget,则将保存值恢复到widget。注意:这是一个逐步的过程, //从0至保存值saved_value逐步恢复,我想是这是为了减少POP。 if (invert) { for (i = max; i > widget->saved_value; i--) snd_soc_update_bits(widget->codec, reg, mask, i); } else { for (i = 0; i < widget->saved_value; i++) snd_soc_update_bits(widget->codec, reg, mask, i); } widget->muted = 0; } else { /* power down is about to occur, decrease volume to mute */ //power down widget,保存widget当前的gain值,并逐步减少该widget的音量,直至0。 int val = snd_soc_read(widget->codec, reg); int i = widget->saved_value = (val >> shift) & mask; if (invert) { for (; i < mask; i++) snd_soc_update_bits(widget->codec, reg, mask, i); } else { for (; i > 0; i--) snd_soc_update_bits(widget->codec, reg, mask, i); } widget->muted = 1; } } return 0; }这里有个建议:当遇到PGA部件时,最好按照规范来写这个dapm widget,该widget应包含gain调整的kcontrol,这样系统会减少通电/断电时可能带来的POP音。
小结:dapm触发时会调用dapm_set_pga,控制PGA widgets的音量逐步增大/减少,目的是减少通电/断电时可能带来的POP音。
1、is_connected_output_ep和is_connected_input_ep,及其endpoint
is_connected_output_ep挺有趣的,widget一路往后走,直至达到output endpoint,统计widget到endpoints的路径数目。同理is_connected_input_ep是取得widget连接到input endpoint的路径数目,但走的方向是相反的。目的是检查路径的完整性,保证通道是贯穿input endpoint和output endpoint的。
何为output endpoint?snd_soc_dapm_adc、snd_soc_dapm_aif_out、snd_soc_dapm_output、snd_soc_dapm_hp、snd_soc_dapm_spk这些类型的widgets就是。
何为input endpoint?snd_soc_dapm_dac、snd_soc_dapm_aif_in、snd_soc_dapm_input、snd_soc_dapm_vmid、snd_soc_dapm_mic这些类型的widgets就是。
如果连接到output endpoint的路径数目为0或者连接到input endpoint的路径数目为0,那么说明该widget没有构成任意合法的完整的音频路径,因此也就应该关闭该widget;如果该widget有连接,则需要打开该widget。在dapm_power_widgets里详细分析。【实话说,代码注释是这样,但往下走到dapm_power_widgets太复杂了,我暂时未能完全分析。】
2、path->connect
连接状态是根据path->connect来判断的,改变mixer/mux部件的path->connect有两个地方:
(1)、在dapm kcontrols建立时,会检查mixer/mux的连接状态,具体见dapm_set_path_status:判定这个path对应的source是否被选中了,是则置p->connect = 1,否则置p->connect = 0。这个过程在<DAPM之二:audio paths与dapm kcontrol>有具体的描述。
(2)、上层通过alsa_amixer或其他来操作dapm kcontrols时,会更新widget路径的连接状态path->connect,简略流程如下:
amixer-应用层[alsa_amixer cset name='Left Output Mixer Left Input Mixer Switch' 1] |->snd_ctl_ioctl-系统调用 |->snd_ctl_elem_write_user-内核钩子函数 |->snd_ctl_elem_wirte- |->snd_ctl_find_id-遍历kcontrol链表找到name字段匹配的kctl |->kctl->put()-调用kctl的成员函数put() |->snd_soc_dapm_put_volsw |->dapm_mixer_update_power 更新path->connect
pga、adc、dac、input、output等部件初始化path->connect=1,hp、spk、mic等部件初始化path->connect=0,具体见snd_soc_dapm_add_route函数。
3、path的相关链表
每一个mixer/mux widget都有数个path,path的数目与mixer/mux widget的kcontrols数目是一致的,有N个kcontrols,就可以构成N个path。每个path都会通过list_add(&path->list, &codec->dapm_paths)添加到链表codec->dapm_paths上;每个path,都会通过INIT_LIST_HEAD(&path->list_sink)创建一个path->list_sink节点,然后通过list_add(&path->list_sink, &wsink->sources)将节点添加到链表wsink->sources上;每个path,都会通过INIT_LIST_HEAD(&path->list_source)创建一个path->list_source节点,然后通过list_add(&path->list_source, &wsource->sinks)将节点添加到链表wsource->sink上。每个path都会挂到source widget和sink widget上的链表上,因此可通过widget->sources找到该widget作为sources的所有paths,通过widget->sinks找到该widget作为sinks的所有paths。
path的建立过程分析见DAPM之二:audio paths与dapm kcontrol,这里为了方便理解,画了一个示意图。其实这些应该要放到之前章节的,写得有点乱了,后面会整理一个有条理的文档出来。
4、is_connected_output_ep流程
理解以上三点,应该可以探讨is_connected_output_ep的流程了。is_connected_input_ep类似的。图解一下:
图中浅绿色为input endpoint,浅蓝色为output endpoint。如红色线路对于Input Mixer,它如何找到到达output endpoint的路径呢?
(1)、首先会找到Input Mixer->sinks,发现有两个分别是ADC和Output Mixer;
(2)、检查Input Mixer到ADC这个path是否连接,接着检查到ADC是属于snd_soc_dapm_adc类型的widget,返回1,因为这个widget属于output endpoint类型并且是连接上的;
(3)、检查Input Mixer到Output Mixer这个path是否连接的,如果不是则跳出;如果是则继续往下走,由于这个widget不属于output endpoint,继续找Output Mixer->sinks,有两个分别是HP和SPK;
(4)、再检查到HP是连接到Output Mixer,而且HP也属于output endpoint,因此Input Mixer->Output Mixer->HP这个通道也是贯穿output endpoint的。
以上是is_connected_output_ep的过程,至于is_connected_input_ep,则遍历的方向是相反的(即widget->sources),往前遍历到MIC(input endpoint)。当然已走过的path会置walked标识,不会重复检查。
只有is_connected_output_ep和is_connected_input_ep同时不为0(即往前能到input endpoint,往后能到output endpoint),才认为widget位于一条完整合法的音频通道中。
/* * Recursively check for a completed path to an active or physically connected * output widget. Returns number of complete paths. */ static int is_connected_output_ep(struct snd_soc_dapm_widget *widget) { struct snd_soc_dapm_path *path; int con = 0; if (widget->id == snd_soc_dapm_supply) return 0; //如果widget类型是adc或aif_out,则到达endpoint,置widget->endpoint路径合法。 switch (widget->id) { case snd_soc_dapm_adc: case snd_soc_dapm_aif_out: if (widget->active) return 1; default: break; } //如果widget类型是output、hp、spk等,则到达endpoint,置widget->endpoint路径合法。 if (widget->connected) { /* connected pin ? */ if (widget->id == snd_soc_dapm_output && !widget->ext) return 1; /* connected jack or spk ? */ if (widget->id == snd_soc_dapm_hp || widget->id == snd_soc_dapm_spk || (widget->id == snd_soc_dapm_line && !list_empty(&widget->sources))) return 1; } //这里比较难以理解,dapm的链表很复杂,后面具体提一下,但不保证能表达清楚。 //这里递归遍历widget->sink,直至到达一个endpoint或者遍历完毕,过程中检查path连接状态。 //is_connected_output_ep是将widget作为source往前递归找到output endpoint; //is_connected_input_ep是将widget作为sink往后递归找到input endpoint。 list_for_each_entry(path, &widget->sinks, list_source) { //这条路径已走过,找下一个path if (path->walked) continue; if (path->sink && path->connect) { path->walked = 1; con += is_connected_output_ep(path->sink); } } return con; }首先要理解INIT_LIST_HEAD、list_add和list_for_each_entry等Linux kernel链表操作,这里不多提。对于list_for_each_entry(path, &widget->sinks, list_source),链表头head是widget->sinks,member是list_source,这是结构体struct snd_soc_dapm_path中的一个成员。
注意3中,path建立时链表的操作:list_add(&path->list_source, &wsource->sinks),这里将节点path->list_source添加到链表wsource->sinks中,那么如何理解wsource->sinks呢?我认为是记录连接到wsource上的每一个sink(同理wsink->sources用于记录连接到wsink上的每一个source)。因此,得到一个widget,无论角度往前遍历还是往后遍历,都可找到所有的paths。
小结:这部分写得太多了,因为细节来说的确比较复杂(我改了又改,现在还不能保证细节上是否还有错的地方),要掌握endpoint的概念、path连接状态是如何更新的、什么是完整的音频路径complete paths,难点是path的链表分析。但原理是简单的,看以上的图解就比较清晰了。这两个函数检查一个widget是否同时贯穿input endpoint和output endpoint,是则认为该widget要power up。
/* Generic check to see if a widget should be powered. */ static int dapm_generic_check_power(struct snd_soc_dapm_widget *w) { int in, out; in = is_connected_input_ep(w); dapm_clear_walk(w->codec); out = is_connected_output_ep(w); dapm_clear_walk(w->codec); return out != 0 && in != 0; }检查一个widget是否需要打开,调用is_connected_input_ep检查该widget是否连接到input endpoint,is_connected_output_ep检查该widget是否连接到output endpoint。一个widget只有同时连接到input endpoint和output endpoint,才认为这个widget需要打开。
这其实是一系列的函数,分别是dapm_generic_check_power、dapm_adc_check_power、dapm_dac_check_power、dapm_supply_check_power,视widget的类型使用。如mixer/mux的widget,有w->power_check = dapm_generic_check_power;adc/aif_out的widget,有w->power_check = dapm_adc_check_power。具体见snd_soc_dapm_new_widgets。
核心函数,开关指定的widget,dapm机制绕来绕去最终就是为了控制widget的开和关。但是原理是非常简单的,复杂的是其他部分,如检查widget是否贯穿input/out endpoint、power up/down list顺序等前期处理细节。
/* Standard power change method, used to apply power changes to most * widgets. */ static int dapm_generic_apply_power(struct snd_soc_dapm_widget *w) { int ret; /* call any power change event handlers */ if (w->event) pr_debug("power %s event for %s flags %x\n", w->power ? "on" : "off", w->name, w->event_flags); //在开关widget前,先执行标识为SND_SOC_DAPM_PRE_PMU/SND_SOC_DAPM_PRE_PMD的event //回调函数分支。event flag定义见dapm.h。 /* power up pre event */ if (w->power && w->event && (w->event_flags & SND_SOC_DAPM_PRE_PMU)) { ret = w->event(w, NULL, SND_SOC_DAPM_PRE_PMU); if (ret < 0) return ret; } /* power down pre event */ if (!w->power && w->event && (w->event_flags & SND_SOC_DAPM_PRE_PMD)) { ret = w->event(w, NULL, SND_SOC_DAPM_PRE_PMD); if (ret < 0) return ret; } //关闭widget前,如果该widget属于PGA部件,调用dapm_set_pga控制pga部件音量逐步减少, //减少部件断电时产生的POP音。 /* Lower PGA volume to reduce pops */ if (w->id == snd_soc_dapm_pga && !w->power) dapm_set_pga(w, w->power); //操作widget开启或关闭。 dapm_update_bits(w); //开启widget后,如果该widget属于PGA部件,调用dapm_set_pga控制pga部件音量逐步增大, //减少部件断电时产生的POP音。 /* Raise PGA volume to reduce pops */ if (w->id == snd_soc_dapm_pga && w->power) dapm_set_pga(w, w->power); //在开关widget后,执行标识为SND_SOC_DAPM_POST_PMU/SND_SOC_DAPM_POST_PMD的event //回调函数分支。event flag定义见dapm.h。 /* power up post event */ if (w->power && w->event && (w->event_flags & SND_SOC_DAPM_POST_PMU)) { ret = w->event(w, NULL, SND_SOC_DAPM_POST_PMU); if (ret < 0) return ret; } /* power down post event */ if (!w->power && w->event && (w->event_flags & SND_SOC_DAPM_POST_PMD)) { ret = w->event(w, NULL, SND_SOC_DAPM_POST_PMD); if (ret < 0) return ret; } return 0; }
/* Insert a widget in order into a DAPM power sequence. */ static void dapm_seq_insert(struct snd_soc_dapm_widget *new_widget, struct list_head *list, int sort[]) { struct snd_soc_dapm_widget *w; list_for_each_entry(w, list, power_list) if (dapm_seq_compare(new_widget, w, sort) < 0) { list_add_tail(&new_widget->power_list, &w->power_list); return; } list_add_tail(&new_widget->power_list, list); }
这个函数的作用是:将widget按照一定的顺序插入到链表list中。在power up/down时,为了减少可能由此产生的POP音,必须按照一定的顺序来操作这些widgets,这个顺序就是由dapm_seq_insert来指定的。
power down时,顺序如下:
static int dapm_down_seq[] = { [snd_soc_dapm_pre] = 0, [snd_soc_dapm_adc] = 1, [snd_soc_dapm_hp] = 2, [snd_soc_dapm_spk] = 2, [snd_soc_dapm_pga] = 4, [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_value_mux] = 9, [snd_soc_dapm_aif_in] = 10, [snd_soc_dapm_aif_out] = 10, [snd_soc_dapm_supply] = 11, [snd_soc_dapm_post] = 12, };power up时,顺序如下:
static int dapm_up_seq[] = { [snd_soc_dapm_pre] = 0, [snd_soc_dapm_supply] = 1, [snd_soc_dapm_micbias] = 2, [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_value_mux] = 5, [snd_soc_dapm_dac] = 6, [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_hp] = 10, [snd_soc_dapm_spk] = 10, [snd_soc_dapm_post] = 11, };
可以看出,这个顺序是合理的,符合音频depop常规原理。
总结:dapm机制分析上篇就到这里吧,介绍了一些重要的子函数细节,对于dapm机制提及不多,下篇会涉及到dapm的建立和触发过程。其实就我个人而言,最难理解的就是这些细节了。为了尽量消除POP,dapm_set_pga负责pga部件的depop,dapm_seq_insert负责组织widgets power up/down操作次序;power_check系列函数用于检查指定widget是否需要power up,由此又涉及到endpoint、complete paths等概念。