Android Audio-标准ASoc架构

本文是对ALSA ASoC架构的总结。

1. ASoC简介

ASoC的出现是为了解决以下问题:

  • Codec与SoC CPU的底层耦合过于紧密

  • 音频事件没有标准的方法通知用户

  • 当进行播放和录音时,驱动会让整个Codec上电,造成电量损耗

ASoC的代码:kernel/sound/soc。它不能单独存在,它是建立在标准ALSA驱动之上的,必须和标准的ALSA驱动模型相结合才能工作。

1.1 硬件架构

嵌入式系统中的音频分为三个部分组成:

platform-codec

  • 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-data-struct

分析之前流程先看一下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
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 {
/* config - must be set by machine driver */
const char *name; /* Codec name */

const char *stream_name; /* 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; /* optional ID for machine driver link identification */
const struct snd_soc_pcm_stream *params;

unsigned int num_params;
unsigned int dai_fmt; /* format to set on init */

enum snd_soc_dpcm_trigger trigger[2]; /* trigger type for DPCM */
/* codec/machine specific init - e.g. add machine controls */

int (*init)(struct snd_soc_pcm_runtime *rtd);
/* optional hw_params re-writing for BE and FE sync */

int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);

/* machine stream operations */
const struct snd_soc_ops *ops;

const struct snd_soc_compr_ops *compr_ops;

/* Mark this pcm with non atomic ops */
bool nonatomic;

/* For unidirectional dai links */
unsigned int playback_only:1;
unsigned int capture_only:1;

/* Keep DAI active over suspend */
unsigned int ignore_suspend:1;

/* Symmetry requirements */
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;

/* Do not create a PCM for this DAI link (Backend link) */
unsigned int no_pcm:1;

/* This DAI link can route to other DAI links at runtime (Frontend)*/
unsigned int dynamic:1;

#ifdef CONFIG_AUDIO_QGKI
/* This DAI link can be reconfigured at runtime (Backend) */
unsigned int dynamic_be:1;
#endif

/*
* This DAI can support no host IO (no pcm data is
* copied to from host)
*/
unsigned int no_host_mode:2;
/* DPCM capture and Playback support */
unsigned int dpcm_capture:1;

unsigned int dpcm_playback:1;

/* DPCM used FE & BE merged format */
unsigned int dpcm_merged_format:1;

/* DPCM used FE & BE merged channel */
unsigned int dpcm_merged_chan:1;

/* DPCM used FE & BE merged rate */
unsigned int dpcm_merged_rate:1;

/* pmdown_time is ignored at stop */
unsigned int ignore_pmdown_time:1;

/* Do not create a PCM for this DAI link (Backend link) */
unsigned int ignore:1;


struct list_head list; /* DAI link list of the soc card */
struct snd_soc_dobj dobj; /* For topology */
#ifdef CONFIG_AUDIO_QGKI
/* this value determines what all ops can be started asynchronously */
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; /* currently selected param for dai link */

/* Dynamic PCM BE runtime data */
struct snd_soc_dpcm_runtime dpcm[2];

long pmdown_time;
#ifdef CONFIG_AUDIO_QGKI
/* err in case of ops failed */
int err_ops;
#endif

/* runtime devices */
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; /* 0-based and monotonic increasing */
struct list_head list; /* rtd list of the soc card */
struct list_head component_list; /* list of connected components */

/* bit field */
unsigned int dev_registered:1;
unsigned int pop_wait:1;
unsigned int fe_compr:1; /* for Dynamic PCM */
};

这个也是一个非常重要的数据结构。它表示和SoC machine DAI的配置,把一个codec和cpu DAI粘连到一起。它记录了由snd_soc_dai_link指定的某一种音频系统硬件连接形式的信息。

3. ASoC的软件架构

3.1 component的注册

register-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。

register-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[] = {
// bt
{
.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),
},
// smartPA
{
.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的名字。这实现了解耦。

machine-probe

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
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() 函数。这是对声卡进行实例化,并注册到系统中。下面详细分析这个函数。

  1. 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中的其中一个需要被设置
  1. 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
2
3
rtd->cpu_dai = snd_soc_find_dai(dai_link->cpus);

snd_soc_rtdcom_add(rtd, rtd->cpu_dai->component);
  • rtd和codec_dais绑定
1
2
3
rtd->codec_dais[i] = snd_soc_find_dai(codec);

snd_soc_rtdcom_add(rtd, rtd->codec_dais[i]->component);
  • rtd和codec_dais绑定

  • platform和rtd绑定

  • soc_add_pcm_runtime(card, rtd)

这样一来,每个link就对应一个rtd信息,保存了dai_link中指定的codec、platform、cpu_dai以及codec_dai信息。

  1. 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中

  1. 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;
}
  1. 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);
/* machine specific init */
if (component->init) {
ret = component->init(component);
...
}

/* see for_each_card_components */
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。

  1. 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;

/* probe the CODEC DAI */
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函数。

  1. 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)
{
// 执行machine相关的初始化
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); // 调用 device_register 注册rtd device
....
snd_soc_dai_compress_new(cpu_dai, rtd, num);
...
// 创建pcm设备组件,PCM号为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设备组件的操作函数集。

  1. snd_card_register()

调用alsa core的接口注册声卡。

回顾一下主要的内容:

  1. ASoC声卡一般在machine驱动中定义,在定义时一般初始化dai_link,dai_link指定了ASoC系统所有可能的音频连接形式。 dai_link的dai通常会在设备树中指定。

  2. 初始化ASoC声卡时,调用ALSA声卡创建接口创建了一个ALSA声卡。

  3. 在每一个dai_link中指定了codec_name、platform_name、cpu_dai_name以及codec_dai_name,根据它们找到注册到ASoC系统中对应的codec、platform、cpu_dai以及codec_dai,并保存到rtd[num]中。一个dai_link对应一个rtd。

  4. 初始化时调用driver->probe() 对ASoC的每一个组件进行初始化。

  5. 每一个num都会创建一个PCM设备节点。ASoC还为每一个PCM设备关联了一个snd_pcm_ops,这个ops时ASoC各个组件要重要实现的内容。

  6. 最后,对于每一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 定义kcontrol
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),
};

// 2. 定义widget,这里是mix类型的widget
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)),
...
};

// 3. 定义route搭建路径
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-path

widget是带有路径和电源管理信息的kcontrol,它是dapm的基本单元。

widget和widget之间通过 route 来连接,底层是snd_soc_dapm_path。

3.4.2 定义widget

按照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

3.4.4 注册和创建widget

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 {
......

/* Default control and setup, added after probe() is run */
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
// example:
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;
...
//注册kcontrol控件
ret = snd_soc_dapm_new_controls(dapm, omap3pandora_in_dapm_widgets,
ARRAY_SIZE(omap3pandora_in_dapm_widgets));
//注册machine音频路径
return snd_soc_dapm_add_routes(dapm, omap3pandora_in_map,
ARRAY_SIZE(omap3pandora_in_map));
}

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
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。

3.4.6 端点widget

一条完整的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

  • 引入no_pcm标记的原因:

原先 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
// 定义BE DAI
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, // 标识be dai
.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 >
};
  • dynamic

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
// 定义FE DAIs

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, // FE DAI
.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
/* ASoC PCM operations */
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。这种情况通过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
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
/*
* this pcm stream only supports 24 bit, 2 channel and
* 48k sampling rate.
*/
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来完成。

qcom-dai

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