Linux 驱动之 ALSA(五)Machine 驱动

ASoC 架构的设计方式是平台和编解码器类驱动程序必须绑定在一起才能构建音频设备。这种绑定可以在所谓的机器驱动程序或设备树中完成,每一个机器驱动程序和设备树都是与特定机器相关的。也就是说,机器驱动程序针对特定系统,并且不同的板卡需要不同的机器驱动程序。

Machine 驱动程序开发流程

一般来说,机器驱动程序的职责包括以下内容

  • 使用适当的 CPU 和编解码器 DAI 填充 struct snd_soc_dai_link 结构体
  • 物理编解码器时钟设置(如果有的话)和编解码器初始化主/从配置(如果有的话)
  • 定义 DAPM widget 以路由物理编解码器内部并根据需要完成 DAPM 路径
  • 根据需要将运行时采样频率传播到各个编解码器驱动程序

鉴于此,机器类驱动程序的开发可执行以下流程。

  • 编解码器驱动程序注册组件驱动程序、DAI 驱动程序以及它们的操作函数
  • 平台驱动程序注册组件驱动程序、PCM 驱动程序、CPU DAI 驱动程序和它们的操作函数,并根据需要设置回放和采集操作
  • 机器层在编解码器和 CPU 之间创建 DAI 链接并注册声卡和 PCM 设备

现在我们已经了解了机器类驱动程序的开发流程,接下来从步骤 (1) 开始展开叙述,这包括填充 DAI 链接。

DAI 链接

DAI 链接是 CPU 和编解码器 DAI 之间链接的逻辑表示。它在 Kemmel 中使用 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
96
97
struct snd_soc_dai_link {
/* config - must be set by machine driver */
const char *name; /* Codec name */
const char *stream_name; /* Stream name */

/*
* You MAY specify the link's CPU-side device, either by device name,
* or by DT/OF node, but not both. If this information is omitted,
* the CPU-side DAI is matched using .cpu_dai_name only, which hence
* must be globally unique. These fields are currently typically used
* only for codec to codec links, or systems using device tree.
*/
/*
* You MAY specify the DAI name of the CPU DAI. If this information is
* omitted, the CPU-side DAI is matched using .cpu_name/.cpu_of_node
* only, which only works well when that device exposes a single DAI.
*/
struct snd_soc_dai_link_component *cpus;
unsigned int num_cpus;

/*
* You MUST specify the link's codec, either by device name, or by
* DT/OF node, but not both.
*/
/* You MUST specify the DAI name within the codec */
struct snd_soc_dai_link_component *codecs;
unsigned int num_codecs;

/*
* You MAY specify the link's platform/PCM/DMA driver, either by
* device name, or by DT/OF node, but not both. Some forms of link
* do not need a platform. In such case, platforms are not mandatory.
*/
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;

/* 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 */
};

此链接是在机器驱动程序中设置的。它应该指定 cpu dai、codec dai 和使用的平台。在设置完成之后,DAI 链接将被输送到表示声卡的 struct snd_soc_card。struct snd_soc_dai_link 结构体中的元素解释如下:

  • name: 这是任意选择的。它可以是任何内
  • codec_dai_name: 这必须与编解码器芯片驱动程序中的 snd_soc_dai_driver.name 字段相匹配。编解码器可能有一个或多个 DAI。开发人员可参考编解码器驱动程序来识别 DAI 名称
  • cpu_dai_name: 该名称必须与 CPU DAI 驱动程序中的 snd_soc_dai_driver.name 字段相匹配
  • stream_name: 这是该链接的流名称
  • init: 这是 DAI 链接初始化回调函数。它通常用于添加与 DAI 链接相关的 widget 或其他类型的一次性设置
  • dai_fmt: 这应该使用支持的格式和时钟配置进行设置,同时对于 CPU 和 CODEC DAI 驱动程序来说应该是一致的。稍后将介绍该字段的可能位标志
  • ops: 该字段属于 struct snd_soc_ops 类型。它应该与 DAI 链接的机器级 PCM 操作一起设置,包括 startup、hw_params、prepare、trigger、hw_free、shutdown 等。稍后将详细介绍该字段
  • codec_name: 如果已经设置,则这应该是编解码器驱动程序的名称,如 platform_driver.driver.name 或 i2c_driver.driver.name
  • codec_of_node: 这是与编解码器关联的设备树节点
  • cpu_name: 如果已经设置,则这应该是 CPU DAI 驱动程序 CPU 的名称
  • cpu_of_node: 这是与 CPU DAI 关联的设备树节
  • platform_namme 或 platform_of_node: 这是对提供 DMA 功能的平台节点的名称或 DT 节点引用
  • playback_only 和 capture_only: 仅在单向链接的情况下使用,如 SPDIF。如果这是一个仅输出链接(仅回放),则 playback_only 和 capture_only 必须分别设置为 true 和 false。而对于仅输入链接,则应使用相反的值。

在大多数情况下。cpu_of_node 和。platform_of_node 是相同的,因为 CPU DAI 驱动程序和 DMA PCM 驱动程序是由同一设备实现的。也就是说,你必须通过名称或 of_node 指定链接的编解码器,但不能同时使用两者。你必须对 CPU 和平台执行相同操作。但是,必须至少指定 CPU DAI 名称或 CPU 设备名称/节点之一。

获取 CPU 和编解码器节点

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
sound {
compatible = "fsl,imx6ul-evk-wm8960",
"fsl,imx-audio-wm8960";
model = "wm8960-audio";
cpu-dai = <&sai2>;
audio-codec = <&codec>;
asrc-controller = <&asrc>;
codec-master;
gpr = <&gpr 4 0x100000 0x100000>;
hp-det = <3 0>;
/*hp-det-gpios = <&gpio5 4 0>;
mic-det-gpios = <&gpio5 4 0>;*/
audio-routing =
"Headphone Jack", "HP_L",
"Headphone Jack", "HP_R",
"Ext Spk", "SPK_LP",
"Ext Spk", "SPK_LN",
"Ext Spk", "SPK_RP",
"Ext Spk", "SPK_RN",
"LINPUT2", "Mic Jack",
"LINPUT3", "Mic Jack",
"RINPUT1", "Main MIC",
"RINPUT2", "Main MIC",
"Mic Jack", "MICB",
"Main MIC", "MICB",
"CPU-Playback", "ASRC-Playback",
"Playback", "CPU-Playback",
"ASRC-Capture", "CPU-Capture",
"CPU-Capture", "Capture";
status = "okay";
};

在上述机器节点中可以看到,编解码器和 CPU 分别通过 audio-codec 和 cpu-dai 属性传递,传递的方法是引用它们的 phandle。 只要机器驱动程序是由开发人员自己编写的,那么这些属性名称就不会是标准化的(当然,如果你使用的是 simple-card 机器驱动程序,则又另当别论,因为它需要一些预定义的名称)。在机器驱动程序中,你将看到以下类似内容:

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
static int imx_wm8960_probe(struct platform_device *pdev)
{
struct device_node *cpu_np, *codec_np = NULL;
struct platform_device *cpu_pdev;
struct imx_priv *priv = &card_priv;
struct i2c_client *codec_dev;
struct imx_wm8960_data *data;
struct platform_device *asrc_pdev = NULL;
struct device_node *asrc_np;
u32 width;
int ret;

priv->pdev = pdev;

cpu_np = of_parse_phandle(pdev->dev.of_node, "cpu-dai", 0);
if (!cpu_np) {
dev_err(&pdev->dev, "cpu dai phandle missing or invalid\n");
ret = -EINVAL;
goto fail;
}

codec_np = of_parse_phandle(pdev->dev.of_node, "audio-codec", 0);
if (!codec_np) {
dev_err(&pdev->dev, "phandle missing or invalid\n");
ret = -EINVAL;
goto fail;
}
[...]
}

可以看到,上述代码片段使用了 of_parse_phandle() 来获取节点引用。

Machine 路由

编解码器引脚

编解码器引脚 (codec pin) 可以连接到板卡接口 (board connector)。可用的编解码器引脚在编解码器驱动程序中使用 SND_SOC_DAPM_INPUT 和 SND_SOC_DAPM_OUTPUT 宏定义。可以在编解码器驱动程序中使用 grep 命令搜索这些宏,以找到可用的引脚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = {
SND_SOC_DAPM_INPUT("LINPUT1"),
SND_SOC_DAPM_INPUT("RINPUT1"),
SND_SOC_DAPM_INPUT("LINPUT2"),
SND_SOC_DAPM_INPUT("RINPUT2"),
SND_SOC_DAPM_INPUT("LINPUT3"),
SND_SOC_DAPM_INPUT("RINPUT3"),

[...]

SND_SOC_DAPM_OUTPUT("SPK_LP"),
SND_SOC_DAPM_OUTPUT("SPK_LN"),
SND_SOC_DAPM_OUTPUT("HP_L"),
SND_SOC_DAPM_OUTPUT("HP_R"),
SND_SOC_DAPM_OUTPUT("SPK_RP"),
SND_SOC_DAPM_OUTPUT("SPK_RN"),
SND_SOC_DAPM_OUTPUT("OUT3"),
};

接下来,我们来看看这些引脚如何连接到板卡。

板卡接口

板卡接口 (board connector) 在机器驱动程序中定义,其位于 struct snd_soc_card 的 struct snd_soc_dapm_widget 部分中。 大多数时候,这些板卡接口是虚拟的。它们只是与编解码器引脚(这是真实的)连接的逻辑表示。

1
2
3
4
5
6
static const struct snd_soc_dapm_widget imx_wm8960_dapm_widgets[] = {
SND_SOC_DAPM_HP("Headphone Jack", NULL),
SND_SOC_DAPM_SPK("Ext Spk", NULL),
SND_SOC_DAPM_MIC("Mic Jack", NULL),
SND_SOC_DAPM_MIC("Main MIC", NULL),
};

接下来,让我们看看如何将这个接口连接到编解码器引脚。

机器路由

设备树路由

即前面列出的,这里再列以下:

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
sound {
compatible = "fsl,imx6ul-evk-wm8960",
"fsl,imx-audio-wm8960";
model = "wm8960-audio";
cpu-dai = <&sai2>;
audio-codec = <&codec>;
asrc-controller = <&asrc>;
codec-master;
gpr = <&gpr 4 0x100000 0x100000>;
hp-det = <3 0>;
/*hp-det-gpios = <&gpio5 4 0>;
mic-det-gpios = <&gpio5 4 0>;*/
audio-routing =
"Headphone Jack", "HP_L",
"Headphone Jack", "HP_R",
"Ext Spk", "SPK_LP",
"Ext Spk", "SPK_LN",
"Ext Spk", "SPK_RP",
"Ext Spk", "SPK_RN",
"LINPUT2", "Mic Jack",
"LINPUT3", "Mic Jack",
"RINPUT1", "Main MIC",
"RINPUT2", "Main MIC",
"Mic Jack", "MICB",
"Main MIC", "MICB",
"CPU-Playback", "ASRC-Playback",
"Playback", "CPU-Playback",
"ASRC-Capture", "CPU-Capture",
"CPU-Capture", "Capture";
status = "okay";
};

来自设备树的路由期望以某种格式给出音频映射。也就是说,条目被解析为字符串对,第一个是连接的接收方,第二个是连接的源。大多数情况下,这些连接被具体化为编解码器引脚和板卡接口的映射。源和接收方的有效名称取决于硬件绑定,如下所示。

  • 编解码器:使用名称定义引脚
  • 机器:使用名称定义接口或插孔 (jack)

静态路由

静态路由包括从机器驱动程序定义一个 DAPM 路由映射并将其直接分配给声卡,当然,使用这种方法也有一个缺点,那就是如果不重新编译内核,就无法更改路由。这里不再做过多说明了。

时钟与格式注意事项

时钟和格式设置辅助函数

ASoC 核心提供了辅助函数来更改这些配置。具体如下所示:

1
2
3
4
5
6
7
int snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt);
int snd_soc_dai_set_pll(struct snd_soc_dai *dai,
int pll_id, int source, unsigned int freq_in, unsigned int freq_out);
int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id,
unsigned int freq, int dir);
int snd_soc_dai_set_clkdiv(struct snd_soc_dai *dai,
int div_id, int div);

在上面的辅助函数列表中,各字段解释如下:

  • snd_soc_dai_set_fmt 可为时钟主从关系、音频格式和信号反转等设置 DAI 格式
  • snd_soc_dai_set_pll 可配置时钟 PIL
  • snd_soc_dai_set_sysckk 可配置时钟源
  • snd_soc_dai_set_clkdiv 可配置时钟分频器

这些辅助函数中的每一个都将在底层 DAI 的驱动程序操作中调用适当的回调函数。例如,使用 CPU DAI 调用 snd_soc_dai_set_fmt() 辅助函数时,将会调用此 CPU DAI 的 dai->driver->ops->set_fmt 回调函数。 接下来,我们将分别介绍可以分配给 DAI 或 dai_link.fommat 字段的格式/标志的实际列表。它可以分为格式、时钟源和时钟分频器 3 类。

格式

时钟主从关系

与时钟主从关系相关的标志包括以下 3 部分:

  • SND_SOC_DAIFMT_CBM_CFM:CPU 是位时钟 (bit clock) 和帧同步 (firamesync) 的从属 (slave)。这也意味着编解码器是两者的主控 (master)
  • SND_SOC_DAIFMT_CBS_CFS:CPU 是位时钟和帧同步的主控。这也意味着编解码器是两者的从属
  • SND_SOC_DAIFMT_CBM_CFS:CPU 是位时钟的从属和帧同步的主控。这也意味着编解码器是前者的主控和后者的从属

音频格式

与音频格式相关的标志包括以下 9 部分

  • SND_SOC_DAIFMT_DSP_A: 帧同步为 1 位时钟宽度,1 位延迟
  • SND_SOC_DAIFMT_DSP_B: 帧同步为 1 位时钟宽度,0 位延迟。此格式可用于 TDM 协议
  • SND_SOC_DAIFMT_I2S: 帧同步为 1 个音频字宽,1 位延迟,I2S 模式
  • SND_SOC_DAIEMT_RIGHT_J: 右对齐模式
  • SND_SOC_DAIFMT_LEFT_J: 左对齐模式
  • SND_SOC_DATFMT_DSP_A: 帧同步为 1 位时钟宽度,1 位延迟
  • SND_SOC_DAIFMT_AC97: AC97 模式
  • SND_SOC_DAIFMT_PDM: 脉冲密度调制 (pulse density modulation,PDM)
  • SND_SOC_DAIFMT_DSP_B: 帧同步为 1 位时钟宽度,1 位延迟

信号反转

与信号反转 (signal inversion) 相关的标志包括以下 4 部分:

  • SND_SOC_DAIEMT_NB_NF: 正常位时钟 (normal bit clock),正常帧同步 (nonmal fame sync)。CPU 发送器在位时钟的下降沿移出数据,接收方在上升沿采样数据。CPU 帧同步发生器在帧同步的上升沿启动帧。CPU 侧的 I2S 推荐使用该参数
  • SND_SOC_DAIFMT_NB_FF: 正常位时钟,反转帧同步。CPU 发送器在位时钟的下降沿移出数据,接收方在上升沿采样数据。CPU 帧同步发生器在帧同步的下降沿启动帧
  • SND_SOC_DAIFMT_IB_NF: 反转位时钟,正常帧同步。CPU 发送器在位时钟的上升沿移出数据,接收方在下降沿采样数据。CPU 帧同步发生器在帧同步的上升沿启动帧
  • SND_SOC_DAIEMT_IB_F: 反转位时钟,反转帧同步。CPU 发送器在位时钟的上升沿移出数据,接收方在下降沿采样数据。CPU 帧同步发生器在帧同步的下降沿启动帧。此配置可用于 PCM 模式(如蓝牙或基于调制解调器的音频芯片)

时钟源

时钟源可通过 snd_soc_dai_set_sysclk() 辅助函数配置。以下是让 ALSA 知道使用哪个时钟的方向参数。

  • SND_SOC_CLOCK_IN: 这意味着将内部时钟用于系统时钟
  • SND_SOC_CLOCK_OUT: 这意味着将外部时钟用于系统时钟

时钟分频器

时钟分频器:(clock divider) 可以通过 snd_soc_dai_set_clkdiv() 辅助函数配置

Machine 初始化流程

原图

参考文献

《Linux 设备驱动开发-约翰-马德奥》