本文是对ALSA ASoC架构的总结。
1. ASoC简介 ASoC的出现是为了解决以下问题:
ASoC的代码:kernel/sound/soc。它不能单独存在,它是建立在标准ALSA驱动之上的,必须和标准的ALSA驱动模型相结合才能工作。
1.1 硬件架构 嵌入式系统中的音频分为三个部分组成:
Machine 主板中和音频有关的外围电路
SoC 嵌入式系统中的platform
platform 指的是嵌入式系统所使用的平台,比如高通平台、Intel平台等。与音频相关的通常包括该SoC中的时钟、DMA、I2S、PCM、Slimbus等。
codec 具有音频数字接口DAI,主要用于音频路径选择、增益控制以及DA/AD转换等等。
1.2 软件架构 ASoC软件分为:Machine驱动、Platform驱动、codec驱动、CPU DAI驱动、codec DAI驱动。
所有SoC和codec所需要的不是自身特性的、可以有多种实现形式的功能都应该放在machine驱动中完成。比如SoC需要的时钟、codec的GPIO等。
它包含了SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,Slimbus等)。不能包含任何与板子与机器相关的代码。
如果platform中包含有ADSP,那么这一部分所需要实现的功能是将音频送到ADSP为止。因此platform驱动是实现ALSA的核心,其一般需要是实现 snd_pcm_ops。
要求codec驱动是平台无关的。它包含该codec所拥有的音频接口的配置和控制。
CPU DAI驱动主要负责 platform 端音频接口的搭建。
codec DAI驱动主要负责codec端音频接口的搭建。
2. ASoC数据结构
分析之前流程先看一下ASoC的核心数据结构:
数据结构一: struct snd_soc_card
这个数据结构是ASoC声卡的抽象,封装了snd_card声卡。其一般定义在machine驱动中。
数据结构二:struct snd_soc_platform
这个数据结构用于抽象 platform。
name 用于唯一标识platform
driver 是platform的驱动
数据结构三:struct snd_soc_platform_driver
struct snd_pcm_ops *ops 平台的pcm操作函数
数据结构四:struct snd_soc_codec
这个数据结构用于抽象 codec
name 用于唯一标识codec
driver 是该codec的驱动
数据结构五:struct snd_soc_codec_driver
该数据结构用于时候codec的驱动。
新版本中用 platform和codec都用 struct snd_soc_component代替。
数据结构六:struct snd_soc_dai
该数据结构是ASoC中所有dai的抽象。包含了一个DAI的运行信息。
数据结构七:struct snd_soc_dai_driver
该数据结构用于描述和DAI的驱动,实现DAI的能力
数据结构八:struct snd_soc_dai_link
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 struct snd_soc_dai_link { const char *name; const char *stream_name; struct snd_soc_dai_link_component *cpus; unsigned int num_cpus; struct snd_soc_dai_link_component *codecs; unsigned int num_codecs; struct snd_soc_dai_link_component *platforms; unsigned int num_platforms; int id; const struct snd_soc_pcm_stream *params; unsigned int num_params; unsigned int dai_fmt; enum snd_soc_dpcm_trigger trigger[2 ]; int (*init)(struct snd_soc_pcm_runtime *rtd); int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd, struct snd_pcm_hw_params *params); const struct snd_soc_ops *ops; const struct snd_soc_compr_ops *compr_ops; bool nonatomic; unsigned int playback_only:1 ; unsigned int capture_only:1 ; unsigned int ignore_suspend:1 ; unsigned int symmetric_rates:1 ; unsigned int symmetric_channels:1 ; unsigned int symmetric_samplebits:1 ; unsigned int no_pcm:1 ; unsigned int dynamic:1 ; #ifdef CONFIG_AUDIO_QGKI unsigned int dynamic_be:1 ; #endif unsigned int no_host_mode:2 ; unsigned int dpcm_capture:1 ; unsigned int dpcm_playback:1 ; unsigned int dpcm_merged_format:1 ; unsigned int dpcm_merged_chan:1 ; unsigned int dpcm_merged_rate:1 ; unsigned int ignore_pmdown_time:1 ; unsigned int ignore:1 ; struct list_head list; struct snd_soc_dobj dobj; #ifdef CONFIG_AUDIO_QGKI enum snd_soc_async_ops async_ops; #endif };
这个数据结构非常重要,一般在machine驱动中定义。表示DAI所连接的内容,也就是描述了所采用的音频硬件系统中的某一种音频硬件连接形式的软件表示。
codec_name 用于表示系统中所采用的codec;
platform_name 表示系统中所采用的platform;
cpu_dai_name 表示系统中所采用的 CPU DAI;
codec_dai_name 表示系统各种所采用的 codec DAI。
数据结构九:struct snd_soc_pcm_runtime
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct snd_soc_pcm_runtime { struct device *dev; struct snd_soc_card *card; struct snd_soc_dai_link *dai_link; struct snd_pcm_ops ops; unsigned int params_select; struct snd_soc_dpcm_runtime dpcm[2 ]; long pmdown_time; #ifdef CONFIG_AUDIO_QGKI int err_ops; #endif struct snd_pcm *pcm; struct snd_compr *compr; struct snd_soc_dai *codec_dai; struct snd_soc_dai *cpu_dai; struct snd_soc_dai **codec_dais; unsigned int num_codecs; struct delayed_work delayed_work; #ifdef CONFIG_DEBUG_FS struct dentry *debugfs_dpcm_root; #endif unsigned int num; struct list_head list; struct list_head component_list; unsigned int dev_registered:1 ; unsigned int pop_wait:1 ; unsigned int fe_compr:1 ; };
这个也是一个非常重要的数据结构。它表示和SoC machine DAI的配置,把一个codec和cpu DAI粘连到一起。它记录了由snd_soc_dai_link 指定的某一种 音频系统硬件连接形式的信息。
3. ASoC的软件架构 3.1 component的注册
根据分析这个函数主要完成了如下工作:
实例化一个component
对component进行初始化:初始化它的链表、格式化名字、参数赋值等
注册component的dais到系统中(稍后分析)
添加component到全局的component_list中
尝试重新bind之前unbind的card
3.2 dai的注册 ASoC的dai并不区分cpu dai还是 codec dai,主要是根据dai_link中的cpu_dai_name和codec_dai_name来确定一个DAI是cpu_dai还是codec_dai。
小结:
component和dai的注册都会实例化一个相应的实体
name标识它们的唯一实体
component和dai都会指定相应的driver,这个driver是相应驱动的实现
component和dai的注册都会挂载到相应的全局数据链表上
component和dai的注册都会调用 snd_soc_instantiate_cards重新构建ASoC声卡
cpu dai和codec dai注册用的是相同的接口,根据dai的name,结合dai_link中给定的 cpu_dai_name和codec_dai_name来确定该dai的类型。
3.3 ASoC声卡的创建 一般在machin驱动中定义snd_soc_card并指定音频系统中所有可能的dai_link。
高通的dai link是在machine driver的probe中创建card,然后把dai link赋给card的。
我们一般在代码的dai link中看不到dai name的定义,因为这被放到设备树中定义了,通过解析设备树来实现audio routing。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static struct snd_soc_dai_link msm_mi2s_be_dai_links[] = { { .name = LPASS_BE_SEC_MI2S_RX, .stream_name = LPASS_BE_SEC_MI2S_RX, .playback_only = 1 , .trigger = {SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST}, .ops = &msm_common_be_ops, .ignore_suspend = 1 , .ignore_pmdown_time = 1 , SND_SOC_DAILINK_REG (sec_mi2s_rx), }, { .name = LPASS_BE_SEC_MI2S_TX, .stream_name = LPASS_BE_SEC_MI2S_TX, .capture_only = 1 , .trigger = {SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST}, .ops = &msm_common_be_ops, .ignore_suspend = 1 , SND_SOC_DAILINK_REG (sec_mi2s_tx), }, { .name = LPASS_BE_TERT_MI2S_RX, .stream_name = LPASS_BE_TERT_MI2S_RX, .playback_only = 1 , .trigger = {SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST}, .ops = &msm_common_be_ops, .ignore_suspend = 1 , .ignore_pmdown_time = 1 , SND_SOC_DAILINK_REG (tert_mi2s_rx), }, { .name = LPASS_BE_TERT_MI2S_TX, .stream_name = LPASS_BE_TERT_MI2S_TX, .capture_only = 1 , .trigger = {SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST}, .ops = &msm_common_be_ops, .ignore_suspend = 1 , SND_SOC_DAILINK_REG (tert_mi2s_tx), }, };
比如上面是我们定义的be dai link,我们也看不到codec dai的name。原因是在extend_codec_i2s_be_dailinks() 中从设备树中解析获取的codec dai的名字。这实现了解耦。
3.3.1 card和dai link的准备 首先,在machine driver monaco.c 中,注册了一个platform驱动。
在它的probe函数 msm_asoc_machine_probe() 中完成声卡的初始化。
populate_snd_card_dailinks() 函数主要功能:
snd_soc_of_parse_audio_routing() 函数主要功能:
解析设备树中的”qcom,audio-routing”的strings,组成routings,保存到card中:
1 2 3 card->num_of_dapm_routes = num_routes; card->of_dapm_routes = routes;
msm_populate_dai_link_component_of_node() :
从设备树中获取dai_link codec的name和of_node
这里我们还会添加自己的dai_link,并通过解析设备树获取codec name。
3.3.2 ASoC声卡的注册 其中最核心的是 snd_soc_instantiate_card() 函数。这是对声卡进行实例化,并注册到系统中。下面详细分析这个函数。
soc_init_dai_link()
1 2 3 4 5 6 7 8 9 10 11 mutex_lock (&client_mutex);for_each_card_prelinks(card, i, dai_link) { ret = soc_init_dai_link (card, dai_link); if (ret) { dev_err (card->dev, "ASoC: failed to init link %s: %d\n" , dai_link->name, ret); mutex_unlock (&client_mutex); return ret; } }
soc_init_dai_link() 的主要工作是:
card->dai_link[] 中保存了之前传入的link信息,现在需要对其做初始化
遍历dai_link上的codec,做合法性判断:
codec->name和codec->of_node只能设置其中一个;
codec->dai_name 必须被指定
如果这时候codec还没有被注册,则延后card注册
遍历dai_link上的platform,做合法性判断:
platform->name 和 platform->of_node 只能设置一个
如果这时候platform还没有被注册,则延后card注。
对link的cpus参数做合法性判断:
cpus->name和cpus->of_node只能设置一个
如果cpus还没有注册,则延后card注册
至少cpus->dai_name或者cpus->name/of_node中的其中一个需要被设置
soc_bind_dai_link()
/* bind DAIs */ for_each_card_prelinks(card, i, dai_link) { ret = soc_bind_dai_link(card, dai_link); if (ret != 0) goto probe_end; }
soc_bind_dai_link() 的主要工作如下:
如果这个link已经bind过,则返回
创建一个新的rtd,跟card绑定
1 rtd = soc_new_pcm_runtime (card, dai_link);
1 2 3 rtd->cpu_dai = snd_soc_find_dai (dai_link->cpus); snd_soc_rtdcom_add (rtd, rtd->cpu_dai->component);
1 2 3 rtd->codec_dais[i] = snd_soc_find_dai (codec); snd_soc_rtdcom_add (rtd, rtd->codec_dais[i]->component);
这样一来,每个link就对应一个rtd信息,保存了dai_link中指定的codec、platform、cpu_dai以及codec_dai信息。
snd_soc_add_dai_link()
1 2 3 4 5 for_each_card_prelinks(card, i, dai_link) { ret = snd_soc_add_dai_link (card, dai_link); if (ret < 0 ) goto probe_end; }
把定义好的dai_link添加到card->dai_link_list中
snd_card_new()
dai_link bind完成了,现在开始注册声卡(alsa core的)。
1 2 3 4 5 6 7 8 9 10 11 12 ret = snd_card_new (card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, card->owner, 0 , &card->snd_card); ... soc_init_card_debugfs (card);soc_resume_init (card);ret = snd_soc_dapm_new_controls (&card->dapm, card->dapm_widgets, card->num_dapm_widgets); ... ret = snd_soc_dapm_new_controls (&card->dapm, card->of_dapm_widgets, card->num_of_dapm_widgets);
并调用一次声卡的probe:
1 2 3 4 5 if (card->probe) { ret = card->probe (card); if (ret < 0 ) goto probe_end; }
soc_probe_link_components(card)
主要通过 soc_probe_component() 完成对 component 的probe动作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ret = snd_soc_component_module_get_when_probe (component); component->card = card; soc_set_name_prefix (card, component);soc_init_component_debugfs (component);snd_soc_dapm_init (dapm, card, component);ret = snd_soc_dapm_new_controls (dapm, component->driver->dapm_widgets, component->driver->num_dapm_widgets); for_each_component_dais(component, dai) { ret = snd_soc_dapm_new_dai_widgets (dapm, dai); ... } ret = snd_soc_component_probe (component); if (component->init) { ret = component->init (component); ... } list_add (&component->card_list, &card->component_dev_list);ret = snd_soc_add_component_controls (component, component->driver->controls, component->driver->num_controls); ret = snd_soc_dapm_add_routes (dapm, component->driver->dapm_routes, component->driver->num_dapm_routes);
component的probe函数中,最主要是调用snd_soc_component_probe() .
1 2 3 4 5 6 int snd_soc_component_probe (struct snd_soc_component *component) { if (component->driver->probe) return component->driver->probe (component); return 0 ; }
另外还对component的dapm进行了初始化,并向系统各种添加了component的controls和routes。
soc_probe_link_dais(card)
card的rtd中保存了cpu_dai和codec_dai信息,先probe cpu_dai,再probe codec_dai。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static int soc_probe_link_dais (struct snd_soc_card *card) { struct snd_soc_dai *codec_dai; struct snd_soc_pcm_runtime *rtd; int i, order, ret; for_each_comp_order(order) { for_each_card_rtds(card, rtd) { ret = soc_probe_dai (rtd->cpu_dai, order); if (ret) return ret; for_each_rtd_codec_dai(rtd, i, codec_dai) { ret = soc_probe_dai (codec_dai, order); if (ret) return ret; } } } return 0 ; } int snd_soc_dai_probe (struct snd_soc_dai *dai) { if (dai->driver->probe) return dai->driver->probe (dai); return 0 ; }
就是调用dai->driver的probe函数。
soc_link_init(card, rtd)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static int soc_link_init (struct snd_soc_card *card, struct snd_soc_pcm_runtime *rtd) { if (dai_link->init) { ret = dai_link->init (rtd); } ... if (dai_link->dai_fmt) { ret = snd_soc_runtime_set_dai_fmt (rtd, dai_link->dai_fmt); } ... soc_rtd_init (rtd, dai_link->name); .... snd_soc_dai_compress_new (cpu_dai, rtd, num); ... soc_new_pcm (rtd, num); ret = soc_link_dai_pcm_new (&cpu_dai, 1 , rtd); if (ret < 0 ) return ret; ret = soc_link_dai_pcm_new (rtd->codec_dais, rtd->num_codecs, rtd); }
对于每一个dai_link,都会调用 snd_pcm_new 创建一个 ALSA PCM设备组建,PCM设备号为num,num为dai_link中的顺序号。
除此之外,定义了一系列的ops接口函数,用于rtd的ops,并通过snd_pcm_set_ops接口将其设置为PCM设备组件的操作函数集。
snd_card_register()
调用alsa core的接口注册声卡。
回顾一下主要的内容:
ASoC声卡一般在machine驱动中定义,在定义时一般初始化dai_link,dai_link指定了ASoC系统所有可能的音频连接形式。 dai_link的dai通常会在设备树中指定。
初始化ASoC声卡时,调用ALSA声卡创建接口创建了一个ALSA声卡。
在每一个dai_link中指定了codec_name、platform_name、cpu_dai_name以及codec_dai_name,根据它们找到注册到ASoC系统中对应的codec、platform、cpu_dai以及codec_dai,并保存到rtd[num]中。一个dai_link对应一个rtd。
初始化时调用driver->probe() 对ASoC的每一个组件进行初始化。
每一个num都会创建一个PCM设备节点。ASoC还为每一个PCM设备关联了一个snd_pcm_ops,这个ops时ASoC各个组件要重要实现的内容。
最后,对于每一个num,rtd[num]中保存了上面提到的所有ASoC信息,这样一来,每个PCM设备文件都对应一个rtd[num],进而唯一对应ASoC组件信息。
3.4 DAPM DAPM(Dynamic Audio Power Management),是为了使基于Linux的移动设备子音频系统,在任何状态下都工作在最小功耗下引入的。
一个kcontrol代表着一个mixer,或者muxer,又或是一个音量控制器。
kcontrol 和widget的区分:
kcontrol 是起到实实在在的控制作用,比如它会通过控制寄存器来实现对电源的控制或者路径的选择(但是也有不控制寄存器的)
widget 是硬件部件,比如mixer, muxer,pga gain等等
mix:一个输出可以对应有个输入
mux: 一个输出有多个输入来源,但是只能选择其中一个
一个定义widget和route的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static const struct snd_kcontrol_new wm8900_loutmix_controls[] = { SOC_DAPM_SINGLE ("LINPUT3 Bypass Switch" , WM8900_REG_LOUTMIXCTL1, 7 , 1 , 0 ), SOC_DAPM_SINGLE ("AUX Bypass Switch" , WM8900_REG_AUXOUT_CTL, 7 , 1 , 0 ), SOC_DAPM_SINGLE ("Left Input Mixer Switch" , WM8900_REG_BYPASS1, 7 , 1 , 0 ), SOC_DAPM_SINGLE ("Right Input Mixer Switch" , WM8900_REG_BYPASS2, 3 , 1 , 0 ), SOC_DAPM_SINGLE ("DACL Switch" , WM8900_REG_LOUTMIXCTL1, 8 , 1 , 0 ), }; static const struct snd_soc_dapm_widget wm8900_dapm_widgets[] = { ... SND_SOC_DAPM_MIXER ("Left Output Mixer" , WM8900_REG_POWER3, 3 , 0 , wm8900_loutmix_controls, ARRAY_SIZE (wm8900_loutmix_controls)), ... }; static const struct snd_soc_dapm_route audio_map[] = { ... {"Left Output Mixer" , "Left Input Mixer Switch" , "Left Input Mixer" }, ... };
上面的route的意思是:”Left Input Mixer”是source widget,”Left Output Mixer”是sink widget,source要连接到sink,需要经过执行 “Left Input Mixer Switch” 这个control。control直接操作寄存器,实现了对mix的选择控制。
widget和path的示意图:
widget是带有路径和电源管理信息的kcontrol,它是dapm的基本单元。
widget和widget之间通过 route 来连接,底层是snd_soc_dapm_path。
按照widget所在的电源域,分为几类:
codec 域
platform 域
音频路径域
音频数据流域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 snd_soc_dapm_input 该widget对应一个输入引脚。 snd_soc_dapm_output 该widget对应一个输出引脚。 snd_soc_dapm_mux 该widget对应一个mux控件。 snd_soc_dapm_virt_mux 该widget对应一个虚拟的mux控件。 snd_soc_dapm_value_mux 该widget对应一个value类型的mux控件。 snd_soc_dapm_mixer 该widget对应一个mixer控件。 snd_soc_dapm_mixer_named_ctl 该widget对应一个mixer控件,但是对应的kcontrol的名字不会加入widget的名字作为前缀。 snd_soc_dapm_pga 该widget对应一个pga控件(可编程增益控件)。 snd_soc_dapm_out_drv 该widget对应一个输出驱动控件 snd_soc_dapm_adc 该widget对应一个ADC snd_soc_dapm_dac 该widget对应一个DAC snd_soc_dapm_micbias 该widget对应一个麦克风偏置电压控件 snd_soc_dapm_mic 该widget对应一个麦克风。 snd_soc_dapm_hp 该widget对应一个耳机。 snd_soc_dapm_spk 该widget对应一个扬声器。 snd_soc_dapm_line 该widget对应一个线路输入。 snd_soc_dapm_switch 该widget对应一个模拟开关。 snd_soc_dapm_vmid 该widget对应一个codec的vmid偏置电压。 snd_soc_dapm_pre machine级别的专用widget,会先于其它widget执行检查操作。 snd_soc_dapm_post machine级别的专用widget,会后于其它widget执行检查操作。 snd_soc_dapm_supply 对应一个电源或是时钟源。 snd_soc_dapm_regulator_supply 对应一个外部regulator稳压器。 snd_soc_dapm_clock_supply 对应一个外部时钟源。 snd_soc_dapm_aif_in 对应一个数字音频输入接口,比如I2S接口的输入端。 snd_soc_dapm_aif_out 对应一个数字音频输出接口,比如I2S接口的输出端。 snd_soc_dapm_siggen 对应一个信号发生器。 snd_soc_dapm_dai_in 对应一个platform或codec域的输入DAI结构。 snd_soc_dapm_dai_out 对应一个platform或codec域的输出DAI结构。 snd_soc_dapm_dai_link 用于链接一对输入/输出DAI结构。
3.4.3 dapm context 按照功能和偏置电压级别,划分为多个电源域,每个域包含各自的widget,每个域中所有的widget处于同一个偏置电压级别上,而一个电源域就是一个dapm coontext。
通常有几个dapm context:
codec 的 dapm context
platform 的 dapm context
声卡的dapm context
1、在codec中注册widget
可以在注册codec驱动时,在 snd_soc_codec_driver中静态指定 snd_soc_dapm_widget结构数组;
1 2 3 4 5 6 7 8 9 10 11 12 struct snd_soc_codec_driver { ...... 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; ...... }
也可以动态注册:
1 2 snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets, ARRAY_SIZE(wm8993_dapm_widgets));
2、在platform中注册widget
与codec类似,静态和动态注册两种方法
3、machine中注册widget
定义声卡中静态注册。
3.4.5 注册音频路径 widget 是一个个独立的部件,只有通过route,才能让widget连起来,构成整个音频路径。
1、静态注册:
通过snd_soc_codec_driver/snd_soc_platform_driver/snd_soc_card结构中的dapm_routes和num_dapm_routes字段;
2、动态注册:
codec、platform驱动的probe中,或者machine中通过在snd_soc_dai_linnk结构的init回调函数中通过 snd_soc_dapm_add_routes() 手动注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; ... ret = snd_soc_dapm_new_controls (dapm, omap3pandora_in_dapm_widgets, ARRAY_SIZE (omap3pandora_in_dapm_widgets)); return snd_soc_dapm_add_routes (dapm, omap3pandora_in_map, ARRAY_SIZE (omap3pandora_in_map)); }
dai widget 是一种特殊的widget,它是通过 dailink 被系统定义和添加进来的。dai 分为cpu dai和codec dai,所以dai widget 又分为 cpu dai widget 和 codec dai widget。
不管是cpu dai还是 codec dai,通常会同时传输播放和录音的能力,所以可以看到 snd_soc_dai中有两个widget指针:
struct snd_soc_dai {
......
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
struct snd_soc_dapm_context dapm;
......
}
codec dai widget
在添加codec的时候,我们一般会指定dai driver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 , };
在probe的时候会被系统创建和添加dai widget,soc_probe_codec() 调用snd_soc_dapm_new_dai_widgets()。
snd_soc_dapm_new_dai_widgets() 中会调用snd_soc_dapm_new_control()创建playback和capture的widget。
cpu dai widget
soc_probe_platform()中也会通过 snd_soc_dapm_new_controls() 创建widget,snd_soc_dapm_new_dai_widgets()创建dai widgets,snd_soc_dapm_add_routes()从创建path。
一条完整的dapm路径必然有始有终,只有特定的widget才能作为起点和终点,它们叫做端点widget。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 codec的输入输出引脚: snd_soc_dapm_output snd_soc_dapm_input 外接的音频设备: snd_soc_dapm_hp snd_soc_dapm_spk snd_soc_dapm_line 音频流(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 snd_soc_dapm_kcontrol
3.5 DPCM 3.5.1 DPCM的引入 一般的音频链接方式是:
--------- ---------
| | dai | |
CPU -------> codec
| | | |
--------- ---------
CPU的codec之间的连接是通过machine sriver中的dai link设置的,在做硬件的时候就以及确定了。一个dai link 就对应一个逻辑设备。
但是现在音频系统很多是带dsp的,因此变成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | Front End PCMs | SoC DSP | Back End DAIs | Audio devices | ************* PCM0 <------------> * * <----DAI0-----> Codec Headset * * PCM1 <------------> * * <----DAI1-----> Codec Speakers * DSP * PCM2 <------------> * * <----DAI2-----> MODEM * * PCM3 <------------> * * <----DAI3-----> BT * * * * <----DAI4-----> DMIC * * * * <----DAI5-----> FM *************
PCM设备没有直接接到外设上,而是接在DSP上。DSP内部的数据是通过软件的routing来控制的,FE的PCM0要接到哪里,从哪个BE输出,是动态决定的,于是引入了DPCM。
3.5.2 no_pcm和dynamic
原先 cpu->codec,一条dai就对应一个pcm设备。
现在引入dsp之后,变成了fe dai和be dai两条dai,实际的音频流没有变化,所以按照原来方式创建两个pcm设备就不合理了,因此fe dai继续创建pcm设备,be dai就通过设置 .no_pcm = 1 来告诉系统不需要创建pcm设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static struct snd_soc_dai_link machine_dais[] = { ..... { .name = "Codec Headset" , .cpu_dai_name = "ssp-dai.0" , .platform_name = "snd-soc-dummy" , .no_pcm = 1 , .codec_name = "rt5640.0-001c" , .codec_dai_name = "rt5640-aif1" , .ignore_suspend = 1 , .ignore_pmdown_time = 1 , .be_hw_params_fixup = hswult_ssp0_fixup, .ops = &haswell_ops, }, ..... < other BE DAI links here > };
FE DAI的定义时指定 .dynamic = 1,告诉系统这个dai在通过 soc_new_pcm()创建pcm设备的时候,绑定dynamic的ops。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static struct snd_soc_dai_link machine_dais[] = {{ .name = "PCM0 System" , .stream_name = "System Playback" , .cpu_dai_name = "System Pin" , .platform_name = "dsp-audio" , .codec_name = "snd-soc-dummy" , .codec_dai_name = "snd-soc-dummy-dai" , .dynamic = 1 , .trigger = {SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_POST}, }, ..... < other FE and BE DAI links here > };
绑定ops:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if (rtd->dai_link->dynamic) { rtd->ops.open = dpcm_fe_dai_open; rtd->ops.hw_params = dpcm_fe_dai_hw_params; rtd->ops.prepare = dpcm_fe_dai_prepare; rtd->ops.trigger = dpcm_fe_dai_trigger; rtd->ops.hw_free = dpcm_fe_dai_hw_free; rtd->ops.close = dpcm_fe_dai_close; rtd->ops.pointer = soc_pcm_pointer; rtd->ops.ioctl = snd_soc_pcm_component_ioctl; #ifdef CONFIG_AUDIO_QGKI rtd->ops.compat_ioctl = soc_pcm_compat_ioctl; rtd->ops.delay_blk = soc_pcm_delay_blk; #endif } else { rtd->ops.open = soc_pcm_open; rtd->ops.hw_params = soc_pcm_hw_params; rtd->ops.prepare = soc_pcm_prepare; rtd->ops.trigger = soc_pcm_trigger; rtd->ops.hw_free = soc_pcm_hw_free; rtd->ops.close = soc_pcm_close; rtd->ops.pointer = soc_pcm_pointer; rtd->ops.ioctl = snd_soc_pcm_component_ioctl; #ifdef CONFIG_AUDIO_QGKI rtd->ops.compat_ioctl = soc_pcm_compat_ioctl; rtd->ops.delay_blk = soc_pcm_delay_blk; #endif }
3.5.3 hostless Hostless PCM streams
这种不需要经过CPU的stream,叫做hostless PCM stream。比如手机中的modem通话:
1 2 3 4 5 6 7 8 9 10 11 12 13 ************* PCM0 <------------> * * <----DAI0-----> Codec Headset * * PCM1 <------------> * * <====DAI1=====> Codec Speakers/Mic * DSP * PCM2 <------------> * * <====DAI2=====> MODEM * * PCM3 <------------> * * <----DAI3-----> BT * * * * <----DAI4-----> DMIC * * * * <----DAI5-----> FM *************
这种情况下,PCM数据是通过DSP来route的。
Host可以通过两种方式控制 hostles link:
CODEC<–>CODEC link
Creating codec to codec dai link for ALSA dapm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 static const struct snd_soc_pcm_stream dsp_codec_params = { .formats = SNDRV_PCM_FMTBIT_S24_LE, .rate_min = 48000 , .rate_max = 48000 , .channels_min = 2 , .channels_max = 2 , }; { .name = "CPU-DSP" , .stream_name = "CPU-DSP" , .cpu_dai_name = "samsung-i2s.0" , .codec_name = "codec-2, .codec_dai_name = " codec-2 -dai_name", .platform_name = " samsung-i2s.0 ", .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM, .ignore_suspend = 1, .c2c_params = &dsp_codec_params, .num_c2c_params = 1, }, { .name = " DSP-CODEC", .stream_name = " DSP-CODEC", .cpu_dai_name = " wm0010-sdi2", .codec_name = " codec-3 , .codec_dai_name = "codec-3-dai_name" , .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM, .ignore_suspend = 1 , .c2c_params = &dsp_codec_params, .num_c2c_params = 1 , },
注意:上面的 c2c_params 就是用来告诉dapm,这个dai_link是一个codec to codec 的链接。
在dapm core中,会创建一个route,链接cpu_dai的playback widget和codec_dai widget,用于playback。对capture则相反。
为了trigger这个route,DAPM还需要会找到一个有效的端点,它可以是上面playback和capture路径上有效的source/sink端点。比如”AIF Playback”
4. 高通asoc音频通路 4.1 非audioreach架构 高通平台非audioreach架构时,asoc的dai分为FE dai和BE dai,FE dai负责连接AP->ADSP,BE dai则负责连接ADSP->Codec,ADSP里面数据的route则通过msm-pcm-routing构建的widget’s path来完成。
4.2 audioreach架构 在audioreach上,这些东西都简化了,TinyALSA插件通过agm->gsl, 再到kernel的Audio-Pkt和GPR,把数据和命令发送到ADSP,这里不再使用ASoC,所以高通的platform driver都是一些stub和dummy的实现。
问题汇总
1、 dapm是如何解决上下电杂音问题的?
上下电顺序:通过定义各个widget的subseq,控制上下电的顺序,来防止出现pop音。
静音控制:电源切换时,先静音音频路径。
2、 control设备和pcm设备是何时创建的?
control设备是创建声卡时创建的,一个声卡一般只有一个control设备;
pcm设备是创建dai link的时候创建的,playback和capture一条substream对应一个pcm设备。
参考链接 https://blog.csdn.net/whshiyun/article/details/80889838
https://blog.csdn.net/droidphone/category_1118446.html
https://docs.kernel.org/sound/soc/dpcm.html