Linux ALSA 之八:ALSA ASOC Platform Driver

ALSA ASOC Platform Driver

  • 一、Platform 驱动作用
  • 二、ASOC Platform Driver 代码分析
    • 2.1 Linux Platform Driver & Platform Device 驱动模型
    • 2.2 在 Probe 函数中注册 ASOC Platform Driver(PCM DMA) & DAI Driver(CPU DAI)
    • 2.3 ASoc Platform Driver(PCM DMA) & DAI Driver(CPU DAI) 的 ops 字段
  • 三、音频数据的 DMA 操作


一、Platform 驱动作用

在前面Linux ALSA 之六:ALSA ASoc 架构已经介绍了什么是 ALSA ASoc,以及从 Hw & Sw 来看 ASoc Driver 都分为 Platform & Machine & Codec Driver,在本节中将介绍 Platform Driver。

如前面所述,Platform 指某款 Soc 平台的音频模块,如 Mediatek、Samsung 等。Platform 部件驱动的主要作用是完成音频数据的管理,最终通过 CPU 的数据音频接口(DAI)把音频数据传送给 Codec 进行处理,最终由 Codec 输出驱动耳机或是喇叭的音频信号。

在具体实现上,ASoC 有把 Platform 驱动分为两个部分:

  • PCM DMA
    即对应 linux-3.0 snd_soc_patform_driver,其主要负责管理音频数据,把音频数据通过 dma 或其他操作传送到 cpu dai 中,即负责控制 dma buffer & 将 dma buffer 中的音频数据搬运到后端的 FIFO,如 DSP PCM FIFO、I2S tx FIFO 等。音频 pcm dma 驱动需要在 platform 部件驱动中通过 snd_soc_register_component() 来注册。(即将 pcm dma 注册为一个只有 component_driver 的 component(没有 dai),在 machine 驱动中会将其绑定到 runtime 中,以便可以访问操作)
  • CPU DAI
    主要完成 cpu 一侧的 dai 的参数配置,同时也会通过一定的途径把必要的 dma 等参数与 PCM DMA 进行交互。在嵌入式系统里面通常指 SoC 的 I2S、PCM 总控制器,负责把音频数据从 I2S tx FIFO 搬运到 I2S rx FIFO(这是回放的情形,录制方向相反)。注意,DAI 是 Digital Audio Interface 的简称,分为 cpu dai 和 codec dai,这两者通过 I2S/PCM 总线连接;AIF 是 Audio Interface 的简称,嵌入式系统中一般是 I2S 和 PCM 接口。

【Note】后面描述用 Pcm Dma Driver 代表 Soc Platform Driver;用 Linux Platform Driver 指的是 linux 驱动模型 platform driver(不要被这两个相像的术语所迷惑,前者只是针对 ASoC 子系统的,后者是来自 Linux 的设备驱动模型)

【Note】在上述中会针对 PCM DMA 单独调用 snd_soc_register_component() 注册一个 component(而非使用 dai 对应的 component),猜测是仿照以前需要专门注册 dma platform driver 的 linux kernel 版本的方式,而目前在新的 linux kernel 版本中 PCM DMA & CPU DAI 可以注册在同一 Component,故将 dma component driver 中需要的操作直接在 dai 对应的 component driver 中。

二、ASOC Platform Driver 代码分析

(下面均以 mt2701-afe-pcm.c 为例进行讲解)

2.1 Linux Platform Driver & Platform Device 驱动模型

该部分其实就是 /sound/soc/medaitek/mt2701/mt2701-afe-pcm.c 中的 platform driver 与 /arch/arm/boot/dts/mt2701.dtsi 中的 platform device 进行匹配,匹配成功后调用 mt2701_afe_pcm_dev_probe() 函数。

1)mt2701-afe-pcm.c

static const struct of_device_id mt2701_afe_pcm_dt_match[] = {
	{ .compatible = "mediatek,mt2701-audio", .data = &mt2701_soc_v1 },
	{ .compatible = "mediatek,mt7622-audio", .data = &mt2701_soc_v2 },
	{},
};
MODULE_DEVICE_TABLE(of, mt2701_afe_pcm_dt_match);

static struct platform_driver mt2701_afe_pcm_driver = {
	.driver = {
		   .name = "mt2701-audio",
		   .of_match_table = mt2701_afe_pcm_dt_match,
#ifdef CONFIG_PM
		   .pm = &mt2701_afe_pm_ops,
#endif
	},
	.probe = mt2701_afe_pcm_dev_probe,
	.remove = mt2701_afe_pcm_dev_remove,
};

module_platform_driver(mt2701_afe_pcm_driver);

platform driver 中有注册名为 "mediatek,mt2701-audio"
2)mt2701.dtsi

afe: audio-controller {
			compatible = "mediatek,mt2701-audio";
			interrupts =  <GIC_SPI 104 IRQ_TYPE_LEVEL_LOW>,
				      <GIC_SPI 132 IRQ_TYPE_LEVEL_LOW>;
			interrupt-names	= "afe", "asys";
};

在设备树文件中有注册名为 "mediatek,mt2701-audio"platform device,当 platform driver & platform device 匹配之后则会调用 platform_driver 下的 probe() 函数。

2.2 在 Probe 函数中注册 ASOC Platform Driver(PCM DMA) & DAI Driver(CPU DAI)

1)Platform Driver (PCM DMA)

devm_snd_soc_register_component(&pdev->dev, &mtk_afe_pcm_platform, NULL, 0); //添加一个 dai_drv 为 NULL 的 component_drv => 控制 DMA

在 probe() 函数中会先调用上述函数将 platform driver(struct snd_soc_component_driver mtk_afe_pcm_platform) 注册为一个 component,只有注册后才能被 Machine 驱动使用。它的实现过程:为 snd_soc_component 实例分配内存,初始化相应将 snd_soc_component 实例添加到全局链表 conponent_list 中。


2)DAI Driver (CPU DAI)

//创建 pcm_dais component
devm_snd_soc_register_component(&pdev->dev, &mt2701_afe_pcm_dai_component, mt2701_afe_pcm_dais, ARRAY_SIZE(mt2701_afe_pcm_dais));

在 probe() 函数中后面也会调用上述函数将 dai driver(struct snd_soc_dai_driver mt2701_afe_pcm_dais[]) 注册为一个 component,并且通过 snd_soc_register_dais() 循环将所有的 snd_soc_dai_driver register 为 snd_soc_dai,添加到 component->dai_list 中,最后将该 component 添加到全局链表 component_list 以便后面能被 Machine 驱动使用,代码时序框图如下:
Linux ALSA 之八:ALSA ASOC Platform Driver_第1张图片

2.3 ASoc Platform Driver(PCM DMA) & DAI Driver(CPU DAI) 的 ops 字段

# Note:对于 platform driver ops 字段为 snd_pcm_ops 结构,DAI Driver ops 字段为 snd_soc_dai_ops 结构,对于两者的 ops 字段都会在打开 pcm substream 并进行操作时被调用,在 Machine 驱动中调用 soc_new_pcm() 时会对 substream->ops 进行赋值,该 ops 被调用时最终都会 call platform driver ops & dai driver ops 相应的回调函数,详细见后面的 Machine 小节。

1)Platform Driver (PCM DM) ops 字段
该 ops 字段是 snd_pcm_ops 结构,实现该结构中的各个回调函数是 soc platform 驱动的主要工作,它们基本都涉及 dma 操作以及 dma buffer 的管理等工作,如 mtk_afe_pcm_patform 对应的定义如下:

const struct snd_pcm_ops mtk_afe_pcm_ops = {
	.ioctl = snd_pcm_lib_ioctl,
	//该回调函数返回传送数据的当前位置(pcm 中间层通过调用这个函数来获取 dma 缓冲区的读指针)
	//一般情况下,在中断函数中调用 snd_pcm_period_elapsed() 或在 pcm 中间层更新 Buffer 时会调用它,然后 pcm 中间层会更新指针位置和计算缓冲区可用空间,唤醒那些在等待的线程
	.pointer = mtk_afe_pcm_pointer,
};
EXPORT_SYMBOL_GPL(mtk_afe_pcm_ops);

const struct snd_soc_component_driver mtk_afe_pcm_platform = {
	.name = AFE_PCM_NAME,
	.ops = &mtk_afe_pcm_ops,
	.pcm_new = mtk_afe_pcm_new,
	.pcm_free = mtk_afe_pcm_free,
};
EXPORT_SYMBOL_GPL(mtk_afe_pcm_platform);

其中 component->driver->pcm_new() 会在 Machine 调用 soc_new_pcm() 创建 pcm device 时会调用(详细见 Machine 小节),主要实现是调用 snd_pcm_lib_preallocate_pages_for_all() 函数对 DMA 进行预分配

对于 ops->pointer() 返回传送数据的当前位置。pcm 中间层通过调用这个函数来获取 dma 缓冲区的读指针。一般情况下,在中断函数中调用 snd_pcm_period_elapsed() 或在 pcm 中间层更新 Buffer 时会调用它,然后 pcm 中间层会更新指针位置和计算缓冲区可用空间,唤醒那些在等待的线程,代码示例实现如下:

static snd_pcm_uframes_t mtk_afe_pcm_pointer
			 (struct snd_pcm_substream *substream)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_component *component = snd_soc_rtdcom_lookup(rtd, AFE_PCM_NAME);
	struct mtk_base_afe *afe = snd_soc_component_get_drvdata(component);
	struct mtk_base_afe_memif *memif = &afe->memif[rtd->cpu_dai->id];
	const struct mtk_base_memif_data *memif_data = memif->data;
	struct regmap *regmap = afe->regmap;
	struct device *dev = afe->dev;
	int reg_ofs_base = memif_data->reg_ofs_base;
	int reg_ofs_cur = memif_data->reg_ofs_cur;
	unsigned int hw_ptr = 0, hw_base = 0;
	int ret, pcm_ptr_bytes;

	//读 reg 获取 current offset
	ret = regmap_read(regmap, reg_ofs_cur, &hw_ptr);
	if (ret || hw_ptr == 0) {
		dev_err(dev, "%s hw_ptr err\n", __func__);
		pcm_ptr_bytes = 0;
		goto POINTER_RETURN_FRAMES;
	}

	//读 reg 获取 base offset
	ret = regmap_read(regmap, reg_ofs_base, &hw_base);
	if (ret || hw_base == 0) {
		dev_err(dev, "%s hw_ptr err\n", __func__);
		pcm_ptr_bytes = 0;
		goto POINTER_RETURN_FRAMES;
	}

	//计算得出在缓冲区中 pcm_ptr
	pcm_ptr_bytes = hw_ptr - hw_base;

POINTER_RETURN_FRAMES:
	//由于 pcm 中间层都是以 frames 为单位
	//For pcm:
	//	1 frame => format2bytes*channels;
	//	1 sample => format2bytes
	return bytes_to_frames(substream->runtime, pcm_ptr_bytes);
}

ops 各个函数均需要取得一个 snd_pcm_runtime 结构体指针,这个指针可以通过 substream->runtime 来获得。snd_pcm_runtime 是运行时的信息,当打开一个 pcm substream 时,pcm 中间层就会为该 pcm substream 分配一个 pcm_runtime,它拥有很多种信息:hw_params、sw_params 配置拷贝,缓冲区指针信息,mmap 记录,自旋锁等。snd_pcm_runtime 对于驱动程序操作集函数都是只读的,仅 pcm 中间层可以改变或更新这些信息。


2)DAI Driver (CPU DAI) ops 字段
snd_soc_dai_driver 结构需要自己根据不同的 soc 芯片进行定义,关键字介绍如下:

成员 作用
probe、remove 回调函数,分别在声卡加载和卸载时被调用
suspend、resume 电源管理回调函数
ops 指向 snd_soc_dai_ops 结构,用于配置和控制该 dai
playback snd_soc_pcm_stream 结构,用于指出该 dai 支持的声道数、采样率、数据格式等
capture snd_soc_pcm_stream 结构,用于指出该 dai 支持的声道数、采样率、数据格式等

mt2701-afe-pcm.c 定义 snd_soc_dai_driver 结构体数据代码示例如下:

static struct snd_soc_dai_driver mt2701_afe_pcm_dais[] = {
	/* FE DAIs: memory intefaces to CPU */
	{
		.name = "PCMO0",
		.id = MT2701_MEMIF_DL1,
		.suspend = mtk_afe_dai_suspend,
		.resume = mtk_afe_dai_resume,
		.playback = {
			.stream_name = "DL1",
			.channels_min = 1,
			.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)
		},
		.ops = &mt2701_single_memif_dai_ops,
	},
	{
		.name = "PCM_multi",
		.id = MT2701_MEMIF_DLM,
		.suspend = mtk_afe_dai_suspend,
		.resume = mtk_afe_dai_resume,
		.playback = {
			.stream_name = "DLM",
			.channels_min = 1,
			.channels_max = 8,
			.rates = SNDRV_PCM_RATE_8000_192000,
			.formats = (SNDRV_PCM_FMTBIT_S16_LE
				| SNDRV_PCM_FMTBIT_S24_LE
				| SNDRV_PCM_FMTBIT_S32_LE)

		},
		.ops = &mt2701_dlm_memif_dai_ops,
	},
	{
		.name = "PCM0",
		.id = MT2701_MEMIF_UL1,
		.suspend = mtk_afe_dai_suspend,
		.resume = mtk_afe_dai_resume,
		.capture = {
			.stream_name = "UL1",
			.channels_min = 1,
			.channels_max = 2,
			.rates = SNDRV_PCM_RATE_8000_48000,
			.formats = (SNDRV_PCM_FMTBIT_S16_LE
				| SNDRV_PCM_FMTBIT_S24_LE
				| SNDRV_PCM_FMTBIT_S32_LE)
		},
		.ops = &mt2701_single_memif_dai_ops,
	},
	{
		.name = "PCM1",
		.id = MT2701_MEMIF_UL2,
		.suspend = mtk_afe_dai_suspend,
		.resume = mtk_afe_dai_resume,
		.capture = {
			.stream_name = "UL2",
			.channels_min = 1,
			.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)

		},
		.ops = &mt2701_single_memif_dai_ops,
	},
	...
}

定义如上,其中 ops 字段指向一个 snd_soc_dai_ops 结构,该结构实际上是一组回调函数集合,dai 的配置和控制几乎都是通过这些回调函数来实现的,其定义代码示例如下:

/* FE DAIs */
static const struct snd_soc_dai_ops mt2701_single_memif_dai_ops = {
	.startup	= mt2701_simple_fe_startup,
	.shutdown	= mtk_afe_fe_shutdown,
	.hw_params	= mt2701_simple_fe_hw_params,
	.hw_free	= mtk_afe_fe_hw_free,
	.prepare	= mtk_afe_fe_prepare,
	.trigger	= mtk_afe_fe_trigger,
};

标准的 snd_soc_ops 回调,通常由 soc-core 在进行 pcm 操作时调用:

  • startup
  • shutdown
  • hw_params
  • hw_free
  • prepare
  • trigger

三、音频数据的 DMA 操作

# Note:关于 DMA 操作一般会涉及源地址 & 目的地址配置,但是对于 MTK 比较特殊,比如 mt2701-afe-pcm.c,FE DAI 的话仅仅设置了源地址,而没有设置目的地址,理解上是由于使用的 DMA Engine 已经绑定好相应的 I2S/PCM 等 IO 口,即目的地址已被固定,故只要设置了源地址 & Enable Dma Engine 后则将源地址 audio data 搬到 I2S/PCM 等 IO 口,最后由 I2S/PCM 等接口传送到 DSP Input Port;BE DAI 的话源地址 & 目的地址均不需要要设置,理解上是 cpu dai <-> 也均已经在硬件上固定。

From Linux Kernel Document 一句话:

DMA IO to/from DSP Buffers(if applicable)

如前面“Linux ALSA 之三:简单的 ALSA Driver 实现”之 3.2 设置 PCM 操作中描述所知,当需要分配 DMA Memory 时会在 hw_params 中调用 snd_pcm_lib_malloc_pages() 函数进行分配 DMA Memory,并且将 DMA Memory 对应的物理地址设置给 DMA Engine,即设置 DMA Engine 源地址。同上,在打开 Pcm Substream 时也会调用 Platform Driver ops->hw_params & Dai Driver ops->hw_params,故在实现时可以在两者之一的 hw_params 操作如上内容。

参考链接:
linux-alsa详解5 ASOC-platform

你可能感兴趣的:(Linux内核设计与实现,ALSA,ALSA,ASOC,音频驱动,linux内核)