当前位置: 首页 > news >正文

Linux:ASoC 声卡驱动框架简介

文章目录

  • 1. 前言
  • 2. ASoC 声卡驱动框架
    • 2.1 框架概览
    • 2.2 声卡的建立
      • 2.2.1 注册声卡组件
        • 2.2.1.1 注册 CPU 侧 DAI
        • 2.2.1.2 注册 CODEC
      • 2.2.2 组装 ASoC 声卡
    • 2.3 声卡的内存管理
      • 2.3.1 【DMA 内存管理组件】的注册和初始化
        • 2.3.1.1 `公版`的`【DMA 内存管理组件】`的注册和初始化
        • 2.3.1.2 `自定义`的`【DMA 内存管理组件】`的注册和初始化
      • 2.3.2 【DMA 内存管理组件】的绑定
      • 2.3.3 通过【DMA 内存管理组件】分配 DMA 内存
        • 2.3.3.1 注册声卡时预分配 DMA 内存
        • 2.3.3.2 ioctl(SNDRV_PCM_IOCTL_HW_PARAMS) 触发 DMA 内存分配
      • 2.3.4 设定 DMA 传输源、目的地址
      • 2.3.5 DMA 数据传输
        • 2.3.5.1 DMA 传输准备工作
        • 2.3.5.2 启动 DMA 传输
        • 2.3.5.3 循环 DMA 传输
  • 3. 其它
    • 3.1 声卡其它组成部分
    • 3.2 声卡用户空间接口
    • 3.2 ASoC 声卡框架目录组织

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. ASoC 声卡驱动框架

2.1 框架概览

ASoC(ALSA System-on-Chip) 声卡驱动框架,是对 ALSA 声卡驱动框架的解耦,常见于嵌入式设备,其包含部分:

1. CPU DAI 驱动
   也称为 Platform 驱动,驱动一般由 CPU 芯片厂家提供。

2. CODEC 驱动
   一般是外接的 CODEC,驱动一般由 CODEC 厂家提供。

3. Machine 驱动
   粘合层,负责将 CPU DAI 驱动 和 CODEC 驱动 组合到一起形成声卡驱动。

ASoC 声卡驱动框架如下图:

在这里插入图片描述

从前面可以看到,CPU DAI 驱动CODEC 驱动是彼此独立的,它们通过 Machine 驱动组合起来。在物理连接上,CPU 一侧的 DAICODEC 一侧的 DAI 相连。当然,一个声卡可以包含多个 CPU DAI + CODEC 的组合,本文不讨论这种场景。

2.2 声卡的建立

ASoC 声卡的建立,包含两大步:

1. 注册声卡组件
   包括注册 CPU DAI 和 CODEC 。

2. 组装声卡
   将 CPU DAI 和 相连的 CODEC 组装成声卡。

2.2.1 注册声卡组件

ASoC 声卡组件用结构体 struct snd_soc_component 描述:

/* include/sound/soc.h */

struct snd_soc_component {
	const char *name;
	...
	struct snd_soc_card *card; /* 关联的声卡对象 */
	...
	const struct snd_soc_component_driver *driver; /* ASoC 声卡组件驱动 */
	...
	struct snd_soc_codec *codec;

	/* 接口 */
	int (*probe)(struct snd_soc_component *);
	...
};

注册 ASoC 声卡组件,包括 CPU DAICODEC 的注册。ASoC 框架提供 API 接口 devm_snd_soc_register_component() / snd_soc_register_component() 注册声卡组件。

2.2.1.1 注册 CPU 侧 DAI

以全志的 I2S 接口为例,说明 CPU 侧 DAI 的注册过程:

/* sound/soc/sunxi/sun4i-i2s.c */

static struct snd_soc_dai_driver sun4i_i2s_dai = {
	.probe = sun4i_i2s_dai_probe,
	.capture = {
		.stream_name = "Capture",
		.channels_min = 2,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = SNDRV_PCM_FMTBIT_S16_LE,
	},
	.playback = {
		.stream_name = "Playback",
		.channels_min = 2,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = SNDRV_PCM_FMTBIT_S16_LE,
	},
	.ops = &sun4i_i2s_dai_ops,
	.symmetric_rates = 1,
};

static const struct snd_soc_component_driver sun4i_i2s_component = {
	.name	= "sun4i-dai",
};

...

static int sun4i_i2s_probe(struct platform_device *pdev)
{
	...
	/* 注册 CPU DAI */
	ret = devm_snd_soc_register_component(&pdev->dev,
					      &sun4i_i2s_component,
					      &sun4i_i2s_dai, 1);
	...
	/* 声卡 CPU DAI 的 DMA 初始化,这个后面 2.3 小节分析 */
	ret = snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
	...
}
/* sound/soc/soc-devres.c */

int devm_snd_soc_register_component(struct device *dev,
			 const struct snd_soc_component_driver *cmpnt_drv,
			 struct snd_soc_dai_driver *dai_drv, int num_dai)
{
	...
	ret = snd_soc_register_component(dev, cmpnt_drv, dai_drv, num_dai);
	...
}
/* sound/soc/soc-core.c */

int snd_soc_register_component(struct device *dev,
			       const struct snd_soc_component_driver *component_driver,
			       struct snd_soc_dai_driver *dai_drv,
			       int num_dai)
{
	struct snd_soc_component *component;
	int ret;

	/* 新建 ASoC 声卡组件对象 */
	component = kzalloc(sizeof(*component), GFP_KERNEL);
	...

	/* 初始化 ASoC 声卡组件对象 */
	ret = snd_soc_component_initialize(component, component_driver, dev);
	...

	/* 添加 CPU/CODEC DAI 到 ASoC 声卡组件对象 */
	ret = snd_soc_register_dais(component, dai_drv, num_dai, true);
	...

	/* 添加 ASoC 声卡组件对象 到 全局组件对象列表 @component_list */
	snd_soc_component_add(component);

	return 0;

	...
}

这样,一个 CPU DAI 组件就注册到系统,具体是添加到了 ASoC 声卡组件全局列表 component_list 中。

2.2.1.2 注册 CODEC

ASoC 用结构体 struct snd_soc_codec 描述 CODEC,该结构体包含一个 struct snd_soc_component 成员,所以也是一个 ASoC 组件对象,这有点类似于 C++ 的子类继承:

/* SoC Audio Codec device */
struct snd_soc_codec {
	...
	const struct snd_soc_codec_driver *driver;
	...
	/* component */
	struct snd_soc_component component;
};

ASoC 提供 API 接口 snd_soc_register_codec() 注册 COCEC,这和注册 CPU DAI 使用的 API 接口不一样,但 snd_soc_register_codec()devm_snd_soc_register_component() / snd_soc_register_component() 一样,最终也会添加一个描述 CODECstruct snd_soc_component 对象到 ASoC 声卡组件全局列表 component_list 中。来看一个例子:

/* sound/soc/codecs/pcm5102a.c */

static struct snd_soc_dai_driver pcm5102a_dai = {
	.name = "pcm5102a-hifi",
	.playback = {
		.channels_min = 2,
		.channels_max = 2,
		.rates = SNDRV_PCM_RATE_8000_192000,
		.formats = SNDRV_PCM_FMTBIT_S16_LE |
			   SNDRV_PCM_FMTBIT_S24_LE |
			   SNDRV_PCM_FMTBIT_S32_LE
	},
};

static struct snd_soc_codec_driver soc_codec_dev_pcm5102a;

static int pcm5102a_probe(struct platform_device *pdev)
{
	/* 注册 CODEC 组件 */
	return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_pcm5102a,
			&pcm5102a_dai, 1);
}
/* sound/soc/soc-core.c */

int snd_soc_register_codec(struct device *dev,
			   const struct snd_soc_codec_driver *codec_drv,
			   struct snd_soc_dai_driver *dai_drv,
			   int num_dai)
{
	...
	struct snd_soc_codec *codec;
	...

	/* 新建 CODEC 对象 */
	codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);
	...

	codec->component.codec = codec; /* 绑定 snd_soc_component 和 snd_soc_codec */

	/* 初始化 CODEC 对象 */
	ret = snd_soc_component_initialize(&codec->component,
			&codec_drv->component_driver, dev);
	
	...

	/* 添加到 CODEC 全局组件对象列表 @component_list 中 */
	mutex_lock(&client_mutex);
	snd_soc_component_add_unlocked(&codec->component);
	list_add(&codec->list, &codec_list);
	mutex_unlock(&client_mutex);

	...
	return 0;
	...
}

2.2.2 组装 ASoC 声卡

现在已经注册了 CPU DAI 组件CODEC 组件,我们就可以建立一个 Machine 驱动,作为一个粘合层,将 CPU DAI 组件CODEC 组件拼接到一起,组成一个声卡。

ASoC 声卡用结构体 struct snd_soc_card 来描述,结构体包含一个 ALSA 声卡对象结构体 struct snd_card 引用指针,这也类似 C++ 的子类继承:

/* SoC card */
struct snd_soc_card {
	const char *name; /* SoC 声卡名称 */
	...
	struct snd_card *snd_card;
	...
	int (*probe)(struct snd_soc_card *card);
	...
	struct snd_soc_dai_link *dai_link;  /* predefined links only */
	int num_links;  /* predefined links only */
	/* SoC 声卡所有的 DAI 连接列表, 包括预定义的 @dai_link  */
	struct list_head dai_link_list; /* all links */
	int num_dai_links; /* @dai_link_list 列表的长度 */
	...
	/* lists of probed devices belonging to this card */
	/* SoC 声卡使用的 组件对象(snd_soc_component: McASP,I2S, Codec) 列表 */
	struct list_head component_dev_list;
	...
	void *drvdata; /* 特定声卡私有数据 */
};

Machine 驱动 在选择拼接的 CPU DAICODEC 时,既可以通过组件的名称,也可以通过组件的 DTS 节点 phandle,来指定要引用的组件,这里以组件的 DTS 节点 phandle 选择组件的方式来举例说明。

Machine 驱动有时候要我们自己编写,但也可以使用目录 sound/soc/genericASoC 框架提供的公版 Machine 驱动。本文以该公版 Machine 驱动为例进行说明。

先看一下 ASoC 声卡的 DTS 配置(以公版 Machine 驱动为例):

sound_i2s {
	compatible = "simple-audio-card"; /* 使用 公版 Machine 驱动 */
	simple-audio-card,name = "I2S-master";
	simple-audio-card,mclk-fs = <256>;
	simple-audio-card,format = "i2s";
	status = "okay";

	simple-audio-card,cpu {
		sound-dai = <&i2s0>; /* 声卡使用的 CPU DAI: i2s0 */
	};

	simple-audio-card,codec {
		sound-dai = <&pcm5102a>; /* 声卡使用的 CODEC: pcm5102a */
	};
};

通过上面的 DTS 配置,将 i2s0 (CPU DAI)pcm5102a (CODEC) 组装为一个声卡,且使用公版 Machine 驱动

/* sound/soc/generic/simple-card.c */

struct simple_card_data {
	struct snd_soc_card snd_card;
	...
};

static int asoc_simple_card_probe(struct platform_device *pdev)
{
	struct simple_card_data *priv;
	...
	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node; /* 声卡 DTS 节点: sound_i2s {...} */
	struct snd_soc_card *card;
	...

	/* Allocate the private data and the DAI link array */
	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); /* 新建 ASoC 声卡对象 */
	...

	/* Init snd_soc_card */
	card = simple_priv_to_card(priv);
	...

	if (np && of_device_is_available(np)) {
		/* 从 DTS 配置提取 CPU DAI 和 CODEC */
		ret = asoc_simple_card_parse_of(priv);
		...
	} else {
		...
	}

	/* 注册初始化 ASoC 声卡对象 */
	ret = devm_snd_soc_register_card(dev, card);
	...

	return 0;
	...
}
/* sound/soc/soc-devres.c */

int devm_snd_soc_register_card(struct device *dev, struct snd_soc_card *card)
{
	struct snd_soc_card **ptr;
	...

	ret = snd_soc_register_card(card);
	...
}
/* sound/soc/soc-core.c */

int snd_soc_register_card(struct snd_soc_card *card)
{
	...
	ret = snd_soc_instantiate_card(card);
	...
}

static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
	...

	/* bind DAIs */
	for (i = 0; i < card->num_links; i++) {
		ret = soc_bind_dai_link(card, &card->dai_link[i]);
		...
	}

	...

	/* card bind complete so register a sound card */
	/*
	 * ASoC 声卡 snd_soc_card 可理解成是对 snd_card 的继承.
	 * 这里新建并初始化一个声卡对象 (snd_card):
	 * . 设置声卡名, ID
	 * . 新建并初始一个控制类声卡设备(controlC%i), 然后添加到声卡的设备列表
	 * . 建立 /proc/asound/card%i 目录
	 * ...
	 */
	ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
			card->owner, 0, &card->snd_card);
	...

	/* initialise the sound card only once */
	if (card->probe) {
		ret = card->probe(card); /* ASoC 声卡 probe */
		...
	}

	/* probe all components used by DAI links on this card */
	/* ASoC 声卡包含 DAI,CODEC 组件 probe */
	for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
			order++) {
		list_for_each_entry(rtd, &card->rtd_list, list) {
			ret = soc_probe_link_components(card, rtd, order);
			...
		}
	}
	
	...

	/* 
	 * 注册 ASoC 声卡到系统: 
	 * . 注册 声卡类 字符设备 节点
	 *   /dev/snd/pcmCxDxp, /dev/snd/pcmCxDxc
	 *   /dev/snd/controlC0
	 * ...
	 */
	ret = snd_card_register(card->snd_card);
	...

	card->instantiated = 1; /* 标记声卡已经初始化 */
	...

	return 0;

	...
}

注册 ASoC 声卡有很多的细节,如路由、组件对象的建立等等,限于篇幅,就不一一细表。这里只重点说一下 soc_bind_dai_link(),该函数会查找注册 ASoC 声卡指定组件(CPU DAI + CODEC)绑定到声卡:

/* sound/soc/soc-core.c */

static int soc_bind_dai_link(struct snd_soc_card *card,
	struct snd_soc_dai_link *dai_link)
{
	struct snd_soc_pcm_runtime *rtd;
	struct snd_soc_dai_link_component *codecs = dai_link->codecs; // CODEC
	struct snd_soc_dai_link_component cpu_dai_component; // CPU DAI
	struct snd_soc_component *component;
	struct snd_soc_dai **codec_dais;

	...

	rtd = soc_new_pcm_runtime(card, dai_link);
	
	...

	cpu_dai_component.name = dai_link->cpu_name;
	cpu_dai_component.of_node = dai_link->cpu_of_node;
	cpu_dai_component.dai_name = dai_link->cpu_dai_name;
	/* 从 ASoC 声卡组件全局列表 @component_list 查找指定 CPU DAI 组件 */
	rtd->cpu_dai = snd_soc_find_dai(&cpu_dai_component); 
	...
	/* 添加 CPU DAI 组件 ASoC 声卡 */
	snd_soc_rtdcom_add(rtd, rtd->cpu_dai->component);

	...
	
	/* Find CODEC from registered CODECs */
	codec_dais = rtd->codec_dais;
	for (i = 0; i < rtd->num_codecs; i++) {
		/* 从 ASoC 声卡组件全局列表 @component_list 查找指定 CODEC 组件 */
		codec_dais[i] = snd_soc_find_dai(&codecs[i]);
		...
		/* 添加 CODEC 组件 ASoC 声卡 */
		snd_soc_rtdcom_add(rtd, codec_dais[i]->component);
	}

	/* Single codec links expect codec and codec_dai in runtime data */
	rtd->codec_dai = codec_dais[0];
	rtd->codec = rtd->codec_dai->codec;

	...

	soc_add_pcm_runtime(card, rtd);
	return 0;

	...
}

注册完声卡后,已经可以从系统 /dev/snd 目录下看到声卡对应的设备节点。到此,除了声卡内存管理外,我们对 ASoC 声卡框架的描述基本完成,接下来我们来补充对声卡内存管理的说明。

2.3 声卡的内存管理

声卡驱动使用 DMA 方式进行数据传送。以播放为例,看一下声卡数据流动的逻辑框图:

在这里插入图片描述

上图中,DMA Buffer 是通过声卡的 DMA 内存管理组件从系统主存分配的内存;而 FIFOCPU DAI I2S 自带的一块空间。

播放就是用户空间通过 ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES) 将数据写入 DMA Buffer,然后 DMA 硬件再将数据搬运到 I2S FIFO,再从 I2S FIFO 最终传递给 CODEC 的过程。录音的数据传输方向则刚好和播放相反。播放录音都有各自独立DMA Buffer

从前面的分析了解到,ASoC 声卡的主要组成(只关注核心部分):

ASoC 声卡 = 【CPU DAI 组件】 + 【CODEC 组件】

但实际上,我们还需要一个 DMA 内存管理组件,所以:

ASoC 声卡 = 【CPU DAI 组件】 + 【CODEC 组件】 + 【DMA 内存管理组件】

当然,我们也可以认为【DMA 内存管理组件】【CPU DAI 组件】的一部分,因为正是 CPU DAI 内存(如图中的 I2S FIFO)DMA 内存进行交互的,从前面的播放数据流动图中也可以看到这一点。事实上, 通常【DMA 内存管理组件】【CPU DAI 组件】驱动注册。当然,也可以是由独立的驱动注册【DMA 内存管理组件】,如后面的 fsl-dma.c

本小节重点分析【DMA 内存管理组件】的注册和使用过程。【DMA 内存管理组件】主要涉及以下几个部分:

1. 【DMA 内存管理组件】的注册和初始化
   【DMA 内存管理组件】操作接口和参数的设定,
   CPU DAI `DMA 传输地址(DAI FIFO 地址)初始化``DMA 传输通道申请`2. 【DMA 内存管理组件】的绑定
    将【DMA 内存管理组件】绑定到声卡。

3. 通过【DMA 内存管理组件】分配 DMA 内存
   包括`录音、播放`各自的 DMA 传输的内存空间申请。

4. 设定 DMA 传输源、目的地址
   绑定【DMA 内存管理组件】分配 DMA 内存地址 和 CPU DAI 硬件缓冲地址。

5. 启动【DMA 内存管理组件】传输数据

2.3.1 【DMA 内存管理组件】的注册和初始化

【DMA 内存管理组件】有一个 ASoC 子系统提供的公版,同时,CPU DAI 驱动也可以定义自己的【DMA 内存管理组件】ASoC 声卡【DMA 内存管理组件】驱动接口抽象如下:

/* ASoC 声卡 【DMA 内存管理组件】 驱动 */
struct snd_soc_platform_driver {

	int (*probe)(struct snd_soc_platform *);
	int (*remove)(struct snd_soc_platform *);
	struct snd_soc_component_driver component_driver;

	/* pcm creation and destruction */
	/*
	 * 公版  : dmaengine_pcm_new()
	 * 自定义: fsl_dma_new(), ...
	 */
	int (*pcm_new)(struct snd_soc_pcm_runtime *);
	void (*pcm_free)(struct snd_pcm *);

	/* platform stream pcm ops */
	/*
	 * 公版  : &dmaengine_pcm_ops
	 * 自定义: &fsl_dma_ops, ...
	 */
	const struct snd_pcm_ops *ops;

	/* platform stream compress ops */
	const struct snd_compr_ops *compr_ops;
};
2.3.1.1 公版【DMA 内存管理组件】的注册和初始化

以全志 I2S 接口驱动为例,看公版【DMA 内存管理组件】的注册和初始化的细节,过程包括:

a. CPU DAI DMA 传输通道的申请
b. DMA 传输 FIFO 一端地址设定
c. 【DMA 内存管理组件】接口和参数设定
d. 其它

首先,CPU DAI 可通过 DTS 配置使用的DMA 传输通道

i2s0: i2s@01c22000 {
	compatible = "allwinner,sun8i-h3-i2s";
	...
	dmas = <&dma 3>, <&dma 3>;
	dma-names = "rx", "tx";
	...
};

DMA 传输通道之所以由 CPU DAI 驱动申请,一是因为 DMA 内存是和 CPU DAI FIFO 交互;二是因为硬件设计固定了 CPU DAI 使用的 DMA 传输通道DMA 传输通道也是无法随便更改的。

继续看代码实现的细节:

/* sound/soc/sunxi/sun4i-i2s.c */

struct sun4i_i2s {
	...
	/*
	 * I2S DMA 传输信息:
	 * . FIFO 地址
	 * . DMA 通道名、数据位宽
	 * ...
	 */
	struct snd_dmaengine_dai_dma_data	capture_dma_data; /* 录音 */
	struct snd_dmaengine_dai_dma_data	playback_dma_data; /* 播放 */
	...
};

static int sun4i_i2s_probe(struct platform_device *pdev)
{
	struct sun4i_i2s *i2s;
	...

	i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
	...
	
	/* 播放时: DMA 数据的目的地址为通道的 FIFO,即数据从 DMA Buffer -> I2S TX FIFO */
	i2s->playback_dma_data.addr = res->start +
					i2s->variant->reg_offset_txdata;
	i2s->playback_dma_data.maxburst = 8;

	/* 录音时: DMA 数据的目的地址为通道的 FIFO,即数据从 I2S RX FIFO -> DMA Buffer */
	i2s->capture_dma_data.addr = res->start + SUN4I_I2S_FIFO_RX_REG;
	i2s->capture_dma_data.maxburst = 8;
	...
	
	/* 注册 CPU DAI 组件,前面 2.2.2.1 已经分析过了 */
	ret = devm_snd_soc_register_component(&pdev->dev,
					      &sun4i_i2s_component,
					      &sun4i_i2s_dai, 1);
	...
	
	/* ASoC 声卡 【DMA 内存管理组件】 注册和初始化 */
	ret = snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
	...
}
/* sound/soc/soc-generic-dmaengine-pcm.c */

int snd_dmaengine_pcm_register(struct device *dev,
	const struct snd_dmaengine_pcm_config *config, unsigned int flags)
{
	struct dmaengine_pcm *pcm;
	int ret;

	/* 新建 PCM DMA 引擎对象(dmaengine_pcm) */
	pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
	...

	/* 申请 DMA 通道: Playback + Capture */
	ret = dmaengine_pcm_request_chan_of(pcm, dev, config);
	...

	/* ASoC 声卡 【DMA 内存管理组件】 注册和初始化 */
	ret = snd_soc_add_platform(dev, &pcm->platform,
		&dmaengine_pcm_platform);
	...

	return 0;
	...
}
2.3.1.2 自定义【DMA 内存管理组件】的注册和初始化

自定义【DMA 内存管理组件】的注册和初始化类似,只是定义了不同的组件接口。挑一个 Freescale 例子来看下:

/* sound/soc/fsl/fsl-dma.c */

struct dma_object {
	struct snd_soc_platform_driver dai; /* ASoC 声卡 【DMA 内存管理组件】 驱动 */
	dma_addr_t ssi_stx_phys; /* 播放 DMA 硬件缓冲物理地址 */
	dma_addr_t ssi_srx_phys; /* 录音 DMA 硬件缓冲物理地址 */
	...
};

static int fsl_soc_dma_probe(struct platform_device *pdev)
{
	struct dma_object *dma;
	...

	dma = kzalloc(sizeof(*dma), GFP_KERNEL);
	...

	/* ASoC 声卡 【DMA 内存管理组件】 驱动接口设定 */
	dma->dai.ops = &fsl_dma_ops;
	dma->dai.pcm_new = fsl_dma_new;
	dma->dai.pcm_free = fsl_dma_free_dma_buffers;

	/* Store the SSI-specific information that we need */
	/* 录音、播放 DMA 硬件缓冲地址设定 */
	dma->ssi_stx_phys = res.start + CCSR_SSI_STX0;
	dma->ssi_srx_phys = res.start + CCSR_SSI_SRX0;

	iprop = of_get_property(ssi_np, "fsl,fifo-depth", NULL);
	if (iprop)
		dma->ssi_fifo_depth = be32_to_cpup(iprop);
	else
		/* Older 8610 DTs didn't have the fifo-depth property */
		dma->ssi_fifo_depth = 8;

	...

	/* ASoC 声卡 【DMA 内存管理组件】 注册和初始化 */
	ret = snd_soc_register_platform(&pdev->dev, &dma->dai);
	...
}
/* sound/soc/soc-core.c */

int snd_soc_register_platform(struct device *dev,
		const struct snd_soc_platform_driver *platform_drv)
{
	struct snd_soc_platform *platform;
	...

	platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL);
	...

	ret = snd_soc_add_platform(dev, platform, platform_drv);
	...
}

int snd_soc_add_platform(struct device *dev, struct snd_soc_platform *platform,
		const struct snd_soc_platform_driver *platform_drv)
{
	...
	ret = snd_soc_component_initialize(&platform->component,
			&platform_drv->component_driver, dev);
	...

	platform->dev = dev;
	platform->driver = platform_drv;

	...

	mutex_lock(&client_mutex);
	snd_soc_component_add_unlocked(&platform->component);
	list_add(&platform->list, &platform_list);
	mutex_unlock(&client_mutex);

	...

	return 0;
}

到此,【DMA 内存管理组件】的注册和初始化已经完成,我们看到,通过 API 接口 snd_dmaengine_pcm_register()snd_soc_register_platform(),将【DMA 内存管理组件】注册到系统,也即添加到全局列表 platform_list

虽然【DMA 内存管理组件】已经注册到了系统,但此时它还是一个独立的个体,还不属于任何声卡设备,在能使用它之前,先要将它绑定到声卡。接下来,就看怎样将【DMA 内存管理组件】绑定到声卡。

2.3.2 【DMA 内存管理组件】的绑定

【公版 Machine 驱动 + 公版【DMA 内存管理组件】】的组合为例,来说明 ASoC 声卡【DMA 内存管理组件】绑定到声卡的过程。先看下使用公版 Machine 驱动的声卡的 DTS 配置(同前面 2.2.2,但这里只截取和此处相关部分):

sound_i2s {
	compatible = "simple-audio-card"; /* 使用 公版 Machine 驱动 */
	...

	simple-audio-card,cpu {
		sound-dai = <&i2s0>; /* 声卡使用的 CPU DAI: i2s0 */
	};

	...
};
static int asoc_simple_card_probe(struct platform_device *pdev)
{
	struct simple_card_data *priv;
	struct snd_soc_dai_link *dai_link;
	...

	/* Get the number of DAI links */
	if (np && of_get_child_by_name(np, PREFIX "dai-link"))
		num = of_get_child_count(np);
	else
		num = 1; // num == 1

	/* Allocate the private data and the DAI link array */
	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
	...

	...
	/* DAI 连接对象:连接 CPU DAI 和 CODEC */
	dai_link  = devm_kzalloc(dev, sizeof(*dai_link)  * num, GFP_KERNEL);
	...

	priv->dai_link			= dai_link;
	
	/* Init snd_soc_card */
	card = simple_priv_to_card(priv);
	...
	card->dai_link		= priv->dai_link; /* 设定声卡的 DAI 连接对象 */
	card->num_links		= num; /* 设定声卡的 DAI 连接个数: 这里是 1 个 */

	if (np && of_device_is_available(np)) {
		ret = asoc_simple_card_parse_of(priv); /* (1) 解析声卡 DTS 配置(包括 DAI 配置) */
		...
	} else {
		...
	}

	...

	/* (2) 注册初始化声卡 */
	ret = devm_snd_soc_register_card(dev, card);
	...
}

在上面代码 (1) 解析声卡 DTS 配置(包括 DAI 配置)处,将 DAI 连接对象(struct snd_soc_dai_link) 的 cpu_of_nodeplatform_of_node 都指向了 i2s0,这决定了在声卡注册时,绑定的 CPU DAI i2s0 驱动注册的【DMA 内存管理组件】,后面会看到这个细节。

先看将 DAI 连接对象(struct snd_soc_dai_link) 的 cpu_of_nodeplatform_of_node 都设定为 i2s0 的代码细节:

static int asoc_simple_card_parse_of(struct simple_card_data *priv)
{
	...
	/* Single/Muti DAI link(s) & New style of DT node */
	if (dai_link) {
		...
	} else {
		ret = asoc_simple_card_dai_link_of(node, priv, 0, true);
		...
	}
	...
}

static int asoc_simple_card_dai_link_of(struct device_node *node,
					struct simple_card_data *priv,
					int idx,
					bool is_top_level_node)
{
	...
	
	/*
	 * 解析 cpu DTS 节点, 如 "simple-audio-card,cpu": 
	 * dai_link->cpu_of_node = i2s0
	 */
	ret = asoc_simple_card_parse_cpu(cpu, dai_link,
					 DAI, CELL, &single_cpu);
	...
	/* dai_link->platform_of_node = dai_link->cpu_of_node; */
	ret = asoc_simple_card_canonicalize_dailink(dai_link);
	...
}

最后来看 CPU DAI i2s0 驱动注册的【DMA 内存管理组件】是如何绑定到声卡的:

devm_snd_soc_register_card()
	snd_soc_register_card()
		snd_soc_instantiate_card()
			soc_bind_dai_link()
static int soc_bind_dai_link(struct snd_soc_card *card,
	struct snd_soc_dai_link *dai_link)
{
	...
	/* find one from the set of registered platforms */
	list_for_each_entry(platform, &platform_list, list) {
		platform_of_node = platform->dev->of_node;
		...
		if (dai_link->platform_of_node) { /* 按 DTS 节点匹配 */
			/*
			 * 已知:
			 * @platform_of_node: CPU DAI 接口注册的【DMA 内存管理组件】,绑定到 i2s0 设备(的 DTS 节点)
			 * @dai_link->platform_of_node: 注册声卡前,也设定为 i2s0
			 * 所以这里 platform_of_node == dai_link->platform_of_node,也即 
			 */
			if (platform_of_node != dai_link->platform_of_node)
				continue;
		} else { /* 按名称匹配 */
			if (strcmp(platform->component.name, platform_name)) /* 名称匹配失败 */
				continue;
		}

		rtd->platform = platform; /* i2s0 注册的 【DMA 内存管理组件】 绑定到声卡 */
	}
	...
}

到此,一个 ASoC 声卡终于完整了,它的最后一个部分【DMA 内存管理组件】 也已经添加完成。当然,这里只分析了公版【DMA 内存管理组件】 的绑定过程,对自定义【DMA 内存管理组件】感兴趣的读者,可自行分析。

随着ASoC 声卡【DMA 内存管理组件】 的绑定完成,声卡就具备了内存管理能力。接下来,我们看如何使用ASoC 声卡的【DMA 内存管理组件】 来分配 DMA 内存空间

2.3.3 通过【DMA 内存管理组件】分配 DMA 内存

声卡 DMA 内存空间的申请,可能在不同的时间点:

1. 注册声卡时预分配 DMA 内存
2. ioctl(SNDRV_PCM_IOCTL_HW_PARAMS) 设置硬件参数的分配 DMA 内存

声卡从主存分配的 DMA 内存相关信息用数据结构 struct snd_dma_buffer 管理的:

/* include/sound/memalloc.h */

/*
 * info for buffer allocation
 */
struct snd_dma_buffer {
	struct snd_dma_device dev;	/* device type */
	unsigned char *area;	/* virtual pointer (分配的主存的虚拟地址) */
	dma_addr_t addr;	/* physical address (分配的主存的总线地址) */
	size_t bytes;		/* buffer size in bytes */
	void *private_data;	/* private for allocator; don't touch */
};

它们是基于每 PCM 流(录音、播放)管理的,记录在 struct snd_pcm_substreamdma_buffer 成员中:

/* include/sound/pcm.h */

struct snd_pcm_substream {
	...
	struct snd_dma_buffer dma_buffer;
	...
};

分配声卡 DMA 内存时,会修改对应 PCM 流对象struct snd_dma_buffer 信息,接下来看两种不同场景下分配的细节。

2.3.3.1 注册声卡时预分配 DMA 内存

同样的,我们这里只分析公版【DMA 内存管理组件】DMA 内存预分配过程,它发生在注册声卡期间。注册声卡期间 DMA 内存预分配不是必定发生的,它需满足一定的条件,看代码细节:

/* sound/soc/pcm_memory.c */

static int preallocate_dma = 1; /* 非 0 值表示 PCM 运行时 DMA 数据缓冲 预分配 */
module_param(preallocate_dma, int, 0444);
MODULE_PARM_DESC(preallocate_dma, "Preallocate DMA memory when the PCM devices are initialized.");

static int maximum_substreams = 4;
module_param(maximum_substreams, int, 0444);
MODULE_PARM_DESC(maximum_substreams, "Maximum substreams with preallocated DMA memory.");

static int snd_pcm_lib_preallocate_pages1(struct snd_pcm_substream *substream,
					  size_t size, size_t max)
{

	if (size > 0 && preallocate_dma && substream->number < maximum_substreams) // 需满足条件才发生预分配
		preallocate_pcm_pages(substream, size);
	...
}

我们可以看到,是否发生预分配,和变量 preallocate_dmamaximum_substreams 有关,还和传入的 size 参数有关。来看注册声卡时预分配 DMA 内存的完整流程(此时【DMA 内存管理组件】绑定已经完成):

devm_snd_soc_register_card()
	snd_soc_register_card()
		snd_soc_instantiate_card()
/* sound/soc/soc-core.c */

static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
	...
	/* bind DAIs */
	for (i = 0; i < card->num_links; i++) {
		ret = soc_bind_dai_link(card, &card->dai_link[i]); // 包括 绑定 【DMA 内存管理组件】 到声卡
		...
	}
	...
	/* probe all DAI links on this card */
	for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDER_LAST;
			order++) {
		list_for_each_entry(rtd, &card->rtd_list, list) {
			ret = soc_probe_link_dais(card, rtd, order);
			...
		}
	}
	...
}

static int soc_probe_link_dais(struct snd_soc_card *card,
		struct snd_soc_pcm_runtime *rtd, int order)
{
	...
	if (cpu_dai->driver->compress_new) {
		...
	} else {
		if (!dai_link->params) { /* 如果 DAI 连接 没有设置 PCM 流参数(这是大多数情形) */
			/* create the pcm */
			ret = soc_new_pcm(rtd, rtd->num);
			...
		} else {
			...
		}
	}
	...
}
/* sound/soc/soc-generic-dmaengine-pcm.c */

int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
	...
	if (platform->driver->pcm_new) {
		/* 用【DMA 内存管理组件】分配 DMA 缓冲 */
		ret = platform->driver->pcm_new(rtd); /* dmaengine_pcm_new(), fsl_dma_new(), ... */
		...
	}
	...
}
/* sound/soc/soc-core.c */

static int dmaengine_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
	...
	/* 获取 预分配 DMA 缓冲大小 */
	if (config && config->prealloc_buffer_size) {
		prealloc_buffer_size = config->prealloc_buffer_size;
		max_buffer_size = config->pcm_hardware->buffer_bytes_max;
	} else {
		prealloc_buffer_size = 512 * 1024;
		max_buffer_size = SIZE_MAX;
	}
	...
	/* 为 播放、录音 PCM 流分别分配 DMA 缓冲 */
	for (i = SNDRV_PCM_STREAM_PLAYBACK; i <= SNDRV_PCM_STREAM_CAPTURE; i++) {
		...
		/*
		 * 分配 DMA 缓冲:
		 * 首先尝试 SNDRV_DMA_TYPE_DEV_IRAM 分配, 
		 * 如果失败继续尝试 SNDRV_DMA_TYPE_DEV 分配.
		 */
		ret = snd_pcm_lib_preallocate_pages(substream,
				SNDRV_DMA_TYPE_DEV_IRAM,
				dmaengine_dma_dev(pcm, substream),
				prealloc_buffer_size,
				max_buffer_size);
		...
	}
}
/* sound/soc/pcm_memory.c */

int snd_pcm_lib_preallocate_pages(struct snd_pcm_substream *substream,
				  int type, struct device *data,
				  size_t size, size_t max)
{
	substream->dma_buffer.dev.type = type;
	substream->dma_buffer.dev.dev = data;
	return snd_pcm_lib_preallocate_pages1(substream, size, max);
}

static int preallocate_pcm_pages(struct snd_pcm_substream *substream, size_t size)
{
	struct snd_dma_buffer *dmab = &substream->dma_buffer;
	...

	do {
		if ((err = snd_dma_alloc_pages(dmab->dev.type, dmab->dev.dev,
					       size, dmab)) < 0) {
			if (err != -ENOMEM)
				return err; /* fatal error */
		} else
			return 0;
		size >>= 1;
	} while (size >= snd_minimum_buffer);
	...
}
/* sound/core/memalloc.c */

int snd_dma_alloc_pages(int type, struct device *device, size_t size,
			struct snd_dma_buffer *dmab)
{
	...
	dmab->dev.type = type;
	dmab->dev.dev = device;
	dmab->bytes = 0;
	switch (type) { /* 从指定类型的内存区间分配 */
	...
#ifdef CONFIG_GENERIC_ALLOCATOR
	case SNDRV_DMA_TYPE_DEV_IRAM:
		snd_malloc_dev_iram(dmab, size);
		if (dmab->area) /* 尝试从 IRAM 类型内存池 分配成功 */
			break; /* 结束分配工作 */
		/* Internal memory might have limited size and no enough space,
		 * so if we fail to malloc, try to fetch memory traditionally.
		 */
		/* 从 IRAM 类型内存池 分配失败, 继续尝试 SNDRV_DMA_TYPE_DEV 类型分配 */ 
		dmab->dev.type = SNDRV_DMA_TYPE_DEV;
#endif /* CONFIG_GENERIC_ALLOCATOR */
	...
	}
	...
	dmab->bytes = size;
	return 0;
}

对声卡 DMA 内存预分配的情形,已经分析完成。可以看到,注册声卡时预分配 DMA 内存,是默认的情形。接下来,看通过 ioctl(SNDRV_PCM_IOCTL_HW_PARAMS) 触发 DMA 内存分配的情形。

2.3.3.2 ioctl(SNDRV_PCM_IOCTL_HW_PARAMS) 触发 DMA 内存分配
ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &hw_params)
	...
	snd_pcm_ioctl()
		snd_pcm_common_ioctl()
			snd_pcm_hw_params_user()
				snd_pcm_hw_params()

static int snd_pcm_hw_params(struct snd_pcm_substream *substream,
			     struct snd_pcm_hw_params *params)
{
	...
	if (substream->ops->hw_params != NULL) {
		err = substream->ops->hw_params(substream, params); /* soc_pcm_hw_params() */
		...
	}
	...
}
/* sound/soc/soc-pcm.c */

static int soc_pcm_hw_params(struct snd_pcm_substream *substream,
				struct snd_pcm_hw_params *params)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_platform *platform = rtd->platform; // 指代 i2s0 注册的 【DMA 内存管理组件】
	...

	if (platform->driver->ops && platform->driver->ops->hw_params) {
		ret = platform->driver->ops->hw_params(substream, params); /* dmaengine_pcm_hw_params() */
		...
	}
	
	...
}
/* sound/soc/soc-generic-dmaengine-pcm.c */

static int dmaengine_pcm_hw_params(struct snd_pcm_substream *substream,
	struct snd_pcm_hw_params *params)
{
	...	
	return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
}
/* sound/soc/pcm_memory.c */

int snd_pcm_lib_malloc_pages(struct snd_pcm_substream *substream, size_t size)
{
	...
	
	if (substream->dma_buffer.area != NULL &&
	    substream->dma_buffer.bytes >= size) {
	    /* 已经在声卡注册时预分配了 DMA 内存且空间大小满足一起,不用重新分配,直接使用 */
	    dmab = &substream->dma_buffer; /* use the pre-allocated buffer */
	}  else { /* 如果没有预分配 DMA 缓冲,此时分配 */
		dmab = kzalloc(sizeof(*dmab), GFP_KERNEL);
		...
		dmab->dev = substream->dma_buffer.dev;
		if (snd_dma_alloc_pages(substream->dma_buffer.dev.type,
					substream->dma_buffer.dev.dev,
					size, dmab) < 0) {
			kfree(dmab);
			return -ENOMEM;
		}
	}
	/* 复制分配的 DMA 内存信息到PCM 流运行时管理数据 */
	snd_pcm_set_runtime_buffer(substream, dmab);
	runtime->dma_bytes = size;
	return 1;			/* area was changed */
}

接下来的分配流程进入函数 snd_dma_alloc_pages(),这和前面 2.3.3.1 小节一样,不再赘叙。函数 snd_pcm_set_runtime_buffer() 还得看一下,它将分配的 DMA 内存信息(注册声卡时预分配的,或 当前场景分配的),复制到 PCM 流运行时管理数据中,因为播放、录音时使用的是运行时管理数据。来看细节:

/* include/sound/pcm.h */

static inline void snd_pcm_set_runtime_buffer(struct snd_pcm_substream *substream,
					      struct snd_dma_buffer *bufp)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	if (bufp) {
		runtime->dma_buffer_p = bufp;
		runtime->dma_area = bufp->area;
		runtime->dma_addr = bufp->addr;
		runtime->dma_bytes = bufp->bytes;
	} else {
		runtime->dma_buffer_p = NULL;
		runtime->dma_area = NULL;
		runtime->dma_addr = 0;
		runtime->dma_bytes = 0;
	}
}

2.3.4 设定 DMA 传输源、目的地址

声卡通过【DMA 内存管理组件】分配了 DMA 缓冲,用于和 CPU DAI 的硬件缓冲 FIFO 之间传输数据。如:

播放:
            音频数据
DMA Buffer --------> I2S TX FIFO
             DMA

录音:
            音频数据
I2S TX FIFO --------> DMA Buffer
              DMA

分配的 DMA 缓冲给出了传输一方的地址,要完成传输,还得知道另一方的地址,也即 CPU DAI 的硬件缓冲 FIFO 的地址。从前面的代码分析得知,分配的 DMA 内存地址信息记录在 PCM 流对象 struct snd_pcm_substreamdma_buffer 和 运行时数据 runtime 中,runtime 中是 dma_buffer 数据的副本;而 CPU DAI 硬件缓冲(如 I2S FIFO)的地址信息,则是记录在 CPU DAI 驱动自身管理的数据中,如前面的例子中:

/* sound/soc/sunxi/sun4i-i2s.c */

struct sun4i_i2s {
	...
	/*
	 * I2S DMA 传输信息:
	 * . FIFO 地址
	 * . DMA 通道名、数据位宽
	 * ...
	 */
	struct snd_dmaengine_dai_dma_data	capture_dma_data; /* 录音 */
	struct snd_dmaengine_dai_dma_data	playback_dma_data; /* 播放 */
	...
};

static int sun4i_i2s_probe(struct platform_device *pdev)
{
	/* 播放时: DMA 数据的目的地址为通道的 FIFO */
	i2s->playback_dma_data.addr = res->start +
					i2s->variant->reg_offset_txdata;
	i2s->playback_dma_data.maxburst = 8;

	/* 录音时: DMA 数据的目的地址为通道的 FIFO */
	i2s->capture_dma_data.addr = res->start + SUN4I_I2S_FIFO_RX_REG;
	i2s->capture_dma_data.maxburst = 8;
}

/* sound/soc/fsl/fsl-dma.c */

struct dma_object {
	...
	dma_addr_t ssi_stx_phys; /* 播放 DMA 硬件缓冲物理地址 */
	dma_addr_t ssi_srx_phys; /* 录音 DMA 硬件缓冲物理地址 */
	...
};

static int fsl_soc_dma_probe(struct platform_device *pdev)
{
	struct dma_object *dma;
	...

	dma = kzalloc(sizeof(*dma), GFP_KERNEL);
	...

	/* Store the SSI-specific information that we need */
	/* 录音、播放 DMA 硬件缓冲地址设定 */
	dma->ssi_stx_phys = res.start + CCSR_SSI_STX0;
	dma->ssi_srx_phys = res.start + CCSR_SSI_SRX0;
	...
}

ASoC 声卡驱动框架下,将这些 CPU DAI 的 FIFO 地址管理在 DAI 对象数据结构 struct snd_soc_daiplayback_dma_datacapture_dma_data 成员中:

/*
 * Digital Audio Interface runtime data.
 *
 * Holds runtime data for a DAI.
 */
struct snd_soc_dai {
	...
	/* DAI DMA data */
	void *playback_dma_data; /* DAI DMA TX FIFO 地址 (播放时数据 DMA 目的地址) */
	void *capture_dma_data; /* DAI DMA RX FIFO 地址 (录音时数据 DMA 源地址) */
	...
};

注册声卡 CPU DAI 驱动 probe 时,拷贝到 ASoC 声卡对象

snd_soc_register_card()
	snd_soc_instantiate_card()
		soc_probe_link_dais()
			soc_probe_dai()
				dai->driver->probe(dai)
					sun4i_i2s_dai_probe()
static int sun4i_i2s_dai_probe(struct snd_soc_dai *dai)
{
	struct sun4i_i2s *i2s = snd_soc_dai_get_drvdata(dai);

	snd_soc_dai_init_dma_data(dai,
				  &i2s->playback_dma_data,
				  &i2s->capture_dma_data);

	...

	return 0;
}
/* include/sound/soc-dai.h */

static inline void snd_soc_dai_init_dma_data(struct snd_soc_dai *dai,
					     void *playback, void *capture)
{
	dai->playback_dma_data = playback;
	dai->capture_dma_data = capture;
}

也就是完成了:

snd_soc_dai::playback_dma_data = sun4i_i2s::playback_dma_data
snd_soc_dai::capture_dma_data = sun4i_i2s::capture_dma_data

到此,声卡 PCM 流数据对象就知晓了 DMA 数据传输双方的地址,于是就可以进行音频数据的 DMA 传输了。接下来就分析频数据的 DMA 传输的启动过程

2.3.5 DMA 数据传输

2.3.5.1 DMA 传输准备工作

ioctl(SNDRV_PCM_IOCTL_HW_PARAMS) 做了 DMA 启动传输前的准备工作

ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &hw_params)
	...
	snd_pcm_ioctl()
		snd_pcm_common_ioctl()
			snd_pcm_hw_params_user()
				snd_pcm_hw_params()
					soc_pcm_hw_params()
						dmaengine_pcm_hw_params()
/* sound/soc/soc-generic-dmaengine-pcm.c */

static int dmaengine_pcm_hw_params(struct snd_pcm_substream *substream,
	struct snd_pcm_hw_params *params)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct dmaengine_pcm *pcm = soc_platform_to_pcm(rtd->platform);
	struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream);
	int (*prepare_slave_config)(struct snd_pcm_substream *substream,
			struct snd_pcm_hw_params *params,
			struct dma_slave_config *slave_config);
	struct dma_slave_config slave_config;
	...

	if (!pcm->config)
		prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config;
	else
		prepare_slave_config = pcm->config->prepare_slave_config;

	if (prepare_slave_config) {
		// DMA 传输配置初始化。
		//
		// 这里假定走 snd_dmaengine_pcm_prepare_slave_config() ,
		// 自定义的接口也会间接调用 snd_dmaengine_pcm_prepare_slave_config() 。
		ret = prepare_slave_config(substream, params, &slave_config);
		...
		// 保存 DMA 传输配置信息 @slave_config
		ret = dmaengine_slave_config(chan, &slave_config);
		...
	}

	//return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
}

int snd_dmaengine_pcm_prepare_slave_config(struct snd_pcm_substream *substream,
	struct snd_pcm_hw_params *params, struct dma_slave_config *slave_config)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_dmaengine_dai_dma_data *dma_data;
	...

	/*
	 * 获取的正是前面举例中 sun4i_i2s_dai_probe() -> snd_soc_dai_init_dma_data() 
	 * 设置的 CPU I2S FIFO 地址信息。
	 */
	dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);

	...

	snd_dmaengine_pcm_set_config_from_dai_data(substream, dma_data,
		slave_config);

	return 0;
}
/* sound/core/pcm_dmaengine.c */

void snd_dmaengine_pcm_set_config_from_dai_data(
	const struct snd_pcm_substream *substream,
	const struct snd_dmaengine_dai_dma_data *dma_data,
	struct dma_slave_config *slave_config)
{
	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { /* DMA 数据流向: DMA_MEM_TO_DEV */
		slave_config->dst_addr = dma_data->addr; /* 设置 DMA 数据目的地址为 I2S FIFO */
		slave_config->dst_maxburst = dma_data->maxburst;
		if (dma_data->flags & SND_DMAENGINE_PCM_DAI_FLAG_PACK)
			slave_config->dst_addr_width =
				DMA_SLAVE_BUSWIDTH_UNDEFINED;
		if (dma_data->addr_width != DMA_SLAVE_BUSWIDTH_UNDEFINED)
			slave_config->dst_addr_width = dma_data->addr_width;
	} else { /* DMA 数据流向: DMA_DEV_TO_MEM */
		slave_config->src_addr = dma_data->addr; /* 设置 DMA 数据源地址为 I2S FIFO */
		slave_config->src_maxburst = dma_data->maxburst;
		if (dma_data->flags & SND_DMAENGINE_PCM_DAI_FLAG_PACK)
			slave_config->src_addr_width =
				DMA_SLAVE_BUSWIDTH_UNDEFINED;
		if (dma_data->addr_width != DMA_SLAVE_BUSWIDTH_UNDEFINED)
			slave_config->src_addr_width = dma_data->addr_width;
	}

	slave_config->slave_id = dma_data->slave_id;
}
2.3.5.2 启动 DMA 传输

可以通过 ioctl(SNDRV_PCM_IOCTL_START) 显式的启动 DMA 传输,或者在 ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES) 隐式触发:

ioctl(SNDRV_PCM_IOCTL_START)
	...
	snd_pcm_start_lock_irq()
		snd_pcm_action_lock_irq(&snd_pcm_action_start, substream, SNDRV_PCM_STATE_RUNNING)
			...
			snd_pcm_do_start()
/* sound/soc/pcm_native.c */

static int snd_pcm_do_start(struct snd_pcm_substream *substream, int state)
{
	if (substream->runtime->trigger_master != substream)
		return 0;
	/* 触发 声卡硬件底层 各组件、DAI 接口的 trigger 回调 */
	return substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_START); /* soc_pcm_trigger() */
}
/* sound/soc/soc-pcm.c */

static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_platform *platform = rtd->platform;
	...

	if (platform->driver->ops && platform->driver->ops->trigger) {
		ret = platform->driver->ops->trigger(substream, cmd); /* snd_dmaengine_pcm_trigger() */
		if (ret < 0)
			return ret;
	}

	...
}
/* sound/core/pcm_dmaengine.c */

int snd_dmaengine_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream);
	struct snd_pcm_runtime *runtime = substream->runtime;
	int ret;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
		ret = dmaengine_pcm_prepare_and_submit(substream); /* 周期性的 DMA */
		if (ret)
			return ret;
		/*
		 * 启动 DMA 传输: 
		 * 播放: DMA Buffer -> I2S TX FIFO
		 * 录音:I2S RX FIFO -> DMA Buffer
		 */
		dma_async_issue_pending(prtd->dma_chan);
		break;
	...
	}

	return 0;
}

static int dmaengine_pcm_prepare_and_submit(struct snd_pcm_substream *substream)
{
	struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream);
	struct dma_chan *chan = prtd->dma_chan;
	struct dma_async_tx_descriptor *desc;
	enum dma_transfer_direction direction;
	unsigned long flags = DMA_CTRL_ACK;

	direction = snd_pcm_substream_to_dma_direction(substream);
	
	...
	
	desc = dmaengine_prep_dma_cyclic(chan,
		substream->runtime->dma_addr,
		snd_pcm_lib_buffer_bytes(substream),
		snd_pcm_lib_period_bytes(substream), direction, flags);

	desc->callback = dmaengine_pcm_dma_complete;
	desc->callback_param = substream;
	prtd->cookie = dmaengine_submit(desc);

	return 0;
}
2.3.5.3 循环 DMA 传输

声卡 DMA 数据传输是一个循环往复的过程,一次传输完成后,会重启下次传输。更多关于 DMA 的细节,可参考博文章节 4.5 DMA 使用范例 。

3. 其它

3.1 声卡其它组成部分

ASoC 声卡还包括 DAPM(Dynamic Audio Power Management),即动态电源管理路由组件(widget) 等其它部分,本文未作涉及,感兴趣的读者可查阅相关资料。

3.2 声卡用户空间接口

在目录 /proc/asound 下,声卡提供一系列用户空间接口,方便查看调试。

# cat /proc/asound/cards
 0 [Loopback       ]: Loopback - Loopback
                      Loopback 1
 1 [SDINOUT        ]: SDINOUT - SDINOUT
                      DEP EVK IMX8 I2S

# cat /proc/asound/pcm
00-00: Loopback PCM : Loopback PCM : playback 8 : capture 8
00-01: Loopback PCM : Loopback PCM : playback 8 : capture 8
01-00: HiFi1 dep-i2s-0 :  : playback 1 : capture 1
01-01: HiFi2 dep-i2s-1 :  : playback 1 : capture 1

# cat /proc/asound/devices 
  0: [ 0]   : control
 16: [ 0- 0]: digital audio playback
 17: [ 0- 1]: digital audio playback
 24: [ 0- 0]: digital audio capture
 25: [ 0- 1]: digital audio capture
 32: [ 1]   : control
 33:        : timer
 48: [ 1- 0]: digital audio playback
 49: [ 1- 1]: digital audio playback
 56: [ 1- 0]: digital audio capture
 57: [ 1- 1]: digital audio captur

# ls -l /proc/asound/card1
total 0
-r--r--r--    1 root     root             0 Jan  1 00:04 id
dr-xr-xr-x    3 root     root             0 Jan  1 00:04 pcm0c
dr-xr-xr-x    3 root     root             0 Jan  1 00:04 pcm0p
dr-xr-xr-x    3 root     root             0 Jan  1 00:04 pcm1c
dr-xr-xr-x    3 root     root             0 Jan  1 00:04 pcm1p

# cat /proc/asound/card1/pcm0c/info 
card: 1
device: 0
subdevice: 0
stream: CAPTURE
id: HiFi1 dep-i2s-0
name: 
subname: subdevice #0
class: 0
subclass: 0
subdevices_count: 1
subdevices_avail: 1

# ls -l /proc/asound/card1/pcm0c/sub0/
total 0
-r--r--r--    1 root     root             0 Jan  1 00:05 hw_params
-r--r--r--    1 root     root             0 Jan  1 00:05 info
-r--r--r--    1 root     root             0 Jan  1 00:05 status
-r--r--r--    1 root     root             0 Jan  1 00:05 sw_params

3.2 ASoC 声卡框架目录组织

include/sound/*: 声卡数据结构定义
sound/core/*.c,*.h: 所有声卡的核心公共代码,ASoC 驱动框架是基于其上的
sound/soc/*.c: ASoC 核心公共代码
sound/soc/generic/*.h,*.c: 公版 Machine 驱动
sound/soc/厂商名/*.c,*.h:各厂商 CPU DAI 驱动,自定义 Machine 驱动 
sound/soc/codec/*.c,*.h: 各种适配到 ASoC 驱动框架的 CODEC 驱动

相关文章:

  • nginx 实战配置
  • Pinia入门
  • 【20250215】二叉树:144.二叉树的前序遍历
  • 电脑桌面便利贴,备忘录软件哪个好?
  • vue-cli-service权限不足(Linux运行vue)
  • CAS单点登录(第7版)25.通知
  • 腾讯大数据基于 StarRocks 的向量检索探索
  • Android ListPreference使用
  • Java八股文详细文档.3(基于黑马、ChatGPT、DeepSeek)
  • 大话风险-风险模型监测三道防线
  • 在 Mac ARM 架构上使用 nvm 安装 Node.js 版本 16.20.2
  • Springboot核心:统一异常处理
  • QEMU 搭建 Ubuntu x86 虚拟机
  • Stable diffusion只换衣服的方法
  • 计算机网络知识速记 :HTTP多个TCP连接的实现方式
  • 在蓝耘平台使用4090显卡跑一下深度学习算法-教学文章
  • ‌OpenAI GPT-4.5技术详解与未来展望
  • kafka动态监听主题
  • Flutter PIP 插件 ---- iOS Video Call
  • w211医疗报销系统的设计与实现
  • 这座古村,藏着多少赣韵风华
  • 西甲上海足球学院揭幕,用“足球方法论”试水中国青训
  • 道指跌逾100点,特斯拉涨近5%
  • 第四轮伊美核谈判将于11日在阿曼举行
  • 工程院院士葛世荣获聘任为江西理工大学校长
  • 虚假认定实质性重组、高估不良债权价值,原中国华融资产重庆分公司被罚180万元