本文是对ALSA ASoC架构的总结。
1. ASoC简介
ASoC的出现是为了解决以下问题:
Codec与SoC CPU的底层耦合过于紧密
音频事件没有标准的方法通知用户
当进行播放和录音时,驱动会让整个Codec上电,造成电量损耗
ASoC的代码:kernel/sound/soc
。它不能单独存在,它是建立在标准ALSA驱动之上的,必须和标准的ALSA驱动模型相结合才能工作。
1.1 硬件架构
嵌入式系统中的音频分为三个部分组成:
Machine 主板中和音频有关的外围电路
SoC 嵌入式系统中的platform
platform 指的是嵌入式系统所使用的平台,比如高通平台、Intel平台等。与音频相关的通常包括该SoC中的时钟、DMA、I2S、PCM、Slimbus等。
- Codec
codec 具有音频数字接口DAI,主要用于音频路径选择、增益控制以及DA/AD转换等等。
1.2 软件架构
ASoC软件分为:Machine驱动、Platform驱动、codec驱动、CPU DAI驱动、codec DAI驱动。
- Machine驱动
所有SoC和codec所需要的不是自身特性的、可以有多种实现形式的功能都应该放在machine驱动中完成。比如SoC需要的时钟、codec的GPIO等。
- platform驱动
它包含了SoC平台的音频DMA和音频接口的配置和控制(I2S,PCM,Slimbus等)。不能包含任何与板子与机器相关的代码。
如果platform中包含有ADSP,那么这一部分所需要实现的功能是将音频送到ADSP为止。因此platform驱动是实现ALSA的核心,其一般需要是实现 snd_pcm_ops。
- codec驱动
要求codec驱动是平台无关的。它包含该codec所拥有的音频接口的配置和控制。
- CPU DAI驱动
CPU DAI驱动主要负责 platform 端音频接口的搭建。
- codec DAI驱动
codec DAI驱动主要负责codec端音频接口的搭建。
2. ASoC数据结构

分析之前流程先看一下ASoC的核心数据结构:
数据结构一: struct snd_soc_card
这个数据结构是ASoC声卡的抽象,封装了snd_card声卡。其一般定义在machine驱动中。
snd_card是ALSA声卡的抽象
dai_link用于记录ASoC系统中所有可能的音频连接形式
rtd记录ASoC系统中所有可能的音频连接形式的“运行”信息。
数据结构二: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 | struct snd_soc_dai_link { |
这个数据结构非常重要,一般在machine驱动中定义。表示DAI所连接的内容,也就是描述了所采用的音频硬件系统中的某一种音频硬件连接形式的软件表示。
codec_name 用于表示系统中所采用的codec;
platform_name 表示系统中所采用的platform;
cpu_dai_name 表示系统中所采用的 CPU DAI;
codec_dai_name 表示系统各种所采用的 codec DAI。
数据结构九:struct snd_soc_pcm_runtime
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 | static struct snd_soc_dai_link msm_mi2s_be_dai_links[] = { |
比如上面是我们定义的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() 函数主要功能:
card = snd_soc_card_monaco_msm
依次解析设备树中是否支持CC-VA、Bolero、MI2S、TDM、WCN-BT?如果支持,则把对应的dai link添加进来
card->dai_link = dailink
snd_soc_of_parse_audio_routing() 函数主要功能:
- 解析设备树中的”qcom,audio-routing”的strings,组成routings,保存到card中:
1 | card->num_of_dapm_routes = num_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
11mutex_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); |
- rtd和cpu_dai绑定
1 | rtd->cpu_dai = snd_soc_find_dai(dai_link->cpus); |
- rtd和codec_dais绑定
1 | rtd->codec_dais[i] = snd_soc_find_dai(codec); |
rtd和codec_dais绑定
platform和rtd绑定
soc_add_pcm_runtime(card, rtd)
这样一来,每个link就对应一个rtd信息,保存了dai_link中指定的codec、platform、cpu_dai以及codec_dai信息。
- snd_soc_add_dai_link()
1 | for_each_card_prelinks(card, i, dai_link) { |
把定义好的dai_link添加到card->dai_link_list中
- snd_card_new()
dai_link bind完成了,现在开始注册声卡(alsa core的)。
1 | ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, |
并调用一次声卡的probe:
1 | if (card->probe) { |
- soc_probe_link_components(card)
主要通过 soc_probe_component() 完成对 component 的probe动作:
1 | ret = snd_soc_component_module_get_when_probe(component); |
component的probe函数中,最主要是调用snd_soc_component_probe() .
1 | int snd_soc_component_probe(struct snd_soc_component *component) |
另外还对component的dapm进行了初始化,并向系统各种添加了component的controls和routes。
- soc_probe_link_dais(card)
card的rtd中保存了cpu_dai和codec_dai信息,先probe cpu_dai,再probe codec_dai。
1 | static int soc_probe_link_dais(struct snd_soc_card *card) |
就是调用dai->driver的probe函数。
- soc_link_init(card, rtd)
1 | static int soc_link_init(struct snd_soc_card *card, |
对于每一个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的移动设备子音频系统,在任何状态下都工作在最小功耗下引入的。
3.4.1 kcontrol 和widget
一个kcontrol代表着一个mixer,或者muxer,又或是一个音量控制器。
kcontrol 和widget的区分:
kcontrol 是起到实实在在的控制作用,比如它会通过控制寄存器来实现对电源的控制或者路径的选择(但是也有不控制寄存器的)
widget 是硬件部件,比如mixer, muxer,pga gain等等
mix:一个输出可以对应有个输入
mux: 一个输出有多个输入来源,但是只能选择其中一个
一个定义widget和route的例子:
1 | // 1. 定义kcontrol |
上面的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。
3.4.2 定义widget
按照widget所在的电源域,分为几类:
codec 域
platform 域
音频路径域
音频数据流域
1 | snd_soc_dapm_input 该widget对应一个输入引脚。 |
3.4.3 dapm context
按照功能和偏置电压级别,划分为多个电源域,每个域包含各自的widget,每个域中所有的widget处于同一个偏置电压级别上,而一个电源域就是一个dapm coontext。
通常有几个dapm context:
codec 的 dapm context
platform 的 dapm context
声卡的dapm context
3.4.4 注册和创建widget
1、在codec中注册widget
可以在注册codec驱动时,在 snd_soc_codec_driver中静态指定 snd_soc_dapm_widget结构数组;
1 | struct snd_soc_codec_driver { |
也可以动态注册:
1 | snd_soc_dapm_new_controls(dapm, 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 | // example: |
3.4.5 dai widget
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 | static struct snd_soc_dai_driver wm8993_dai = { |
在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。
3.4.6 端点widget
一条完整的dapm路径必然有始有终,只有特定的widget才能作为起点和终点,它们叫做端点widget。
1 | codec的输入输出引脚: |
3.5 DPCM
3.5.1 DPCM的引入
一般的音频链接方式是:
--------- ---------
| | dai | |
CPU -------> codec
| | | |
--------- ---------
CPU的codec之间的连接是通过machine sriver中的dai link设置的,在做硬件的时候就以及确定了。一个dai link 就对应一个逻辑设备。
但是现在音频系统很多是带dsp的,因此变成了:
1 | | Front End PCMs | SoC DSP | Back End DAIs | Audio devices | |
PCM设备没有直接接到外设上,而是接在DSP上。DSP内部的数据是通过软件的routing来控制的,FE的PCM0要接到哪里,从哪个BE输出,是动态决定的,于是引入了DPCM。
3.5.2 no_pcm和dynamic
- 引入no_pcm标记的原因:
原先 cpu->codec,一条dai就对应一个pcm设备。
现在引入dsp之后,变成了fe dai和be dai两条dai,实际的音频流没有变化,所以按照原来方式创建两个pcm设备就不合理了,因此fe dai继续创建pcm设备,be dai就通过设置 .no_pcm = 1 来告诉系统不需要创建pcm设备。
1 | // 定义BE DAI |
- dynamic
FE DAI的定义时指定 .dynamic = 1,告诉系统这个dai在通过 soc_new_pcm()创建pcm设备的时候,绑定dynamic的ops。
1 | // 定义FE DAIs |
绑定ops:
1 | /* ASoC PCM operations */ |
3.5.3 hostless
Hostless PCM streams
这种不需要经过CPU的stream,叫做hostless PCM stream。比如手机中的modem通话:
1 | ************* |
这种情况下,PCM数据是通过DSP来route的。
Host可以通过两种方式控制 hostles link:
配置成Codec<–>Codec的link。这种情况通过DAPM的enable和disable来控制,通常有一个mixer control用来连接和断开两个DAIs之间的path。
Hostless FE。FE和BE DAI links之间,有一个虚拟的连接。这种方式跟通常的FE操作PCM类似,但是需要更多的用户层的code来控制这个link。推荐用 CODEC <–>CODEC的方式。
CODEC<–>CODEC link
Creating codec to codec dai link for ALSA dapm
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