平台驱动程序可以注册 PCM 驱动程序、CPU DAI 驱动程序及其操作函数,为 PCM 组件预分配缓冲区,并根据需要设置回放和采集操作。换言之,平台驱动程序包含该平台的音频引擎和音频接口驱动程序(如 I2S、AC97 和 PCM)。

平台驱动程序以构成平台的 SoC 为目标。它涉及平台的 DMA(即音频数据在 SoC 中的每个块之间如何传输)和 CPU DAI(即 CPU 向编解码器发送音频数据的路径或 CPU 从编解码器获得音频数据的路径)。

平台驱动程序有两个重要的数据结构体:structsnd_soc_component_driver 和 structsnd_soc_dai_driver。前者负责 DMA 数据管理,后者负责 DAI 的参数配置。当然,前文在讨论编解码器类驱动程序时已经描述过这两种数据结构体,因此,本节将仅介绍与平台代码相关的附加概念。

CPU DAI 驱动程序

在平台侧,大部分工作都可以由 core 完成,尤其是与 DMA 相关的工作。因此,CPU DAI 驱动程序通常只提供组件驱动程序结构中的接口名称,而让 core 完成其余的工作。以下是 STM SPDIF 驱动程序的示例,它在sound/soc/stm/stm32_spdif.c中实现:

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
static const struct snd_soc_dai_ops stm32_spdifrx_pcm_dai_ops = {
.startup = stm32_spdifrx_startup,
.hw_params = stm32_spdifrx_hw_params,
.trigger = stm32_spdifrx_trigger,
.shutdown = stm32_spdifrx_shutdown,
};
static struct snd_soc_dai_driver stm32_spdifrx_dai[] = {
{
.probe = stm32_spdifrx_dai_probe,
.capture = {
.stream_name = "CPU-Capture",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S32_LE |
SNDRV_PCM_FMTBIT_S16_LE,
},
.ops = &stm32_spdifrx_pcm_dai_ops,
}
};
...
static const struct snd_soc_component_driver stm32_spdifrx_component = {
.name = "stm32-spdifrx",
};
...
static int stm32_spdifrx_probe(struct platform_device *pdev)
{
...
ret = snd_dmaengine_pcm_register(&pdev->dev, pcm_config, 0);
if (ret) {
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "PCM DMA register error %d\n", ret);
return ret;
}
ret = snd_soc_register_component(&pdev->dev,
&stm32_spdifrx_component,
stm32_spdifrx_dai,
ARRAY_SIZE(stm32_spdifrx_dai));

...
}

其中 snd_soc_component_driver 只提供了 name 信息,件驱动程序和 DAI 驱动程序都和往常一样通过 snd_soc_register_component() 注册。 struct snd_soc_dai_driver 必须根据实际的 DAI 属性设置,如果需要,应该设置 dai_ops。当然,该设置的很大一部分是由 snd_dmaengine_pcm_register() 完成的,它将根据提供的设置组件驱动程序的 PCM 操作。

Platform DMA 驱动程序

在声音生态系统中,我们有多种类型的设备:PCM、MIDI、混音器、音序器、计时器等。这里的 PCM 指的是脉冲编码调制(pulse code modulation),即对连续变化的模拟信号进行采样、量化和编码以产生数字信号。但要注意,这里它是指处理基于采样的数字音频的设备,而不是 MIDI 等。PCM 层(ALSA 核心的一部分)负责完成所有数字音频工作,例如,准备板卡以进行采集或回放、启动与设备之间的传输等。简而言之,如果你想回放或采集声音,那么你就需要一个 PCM 设备。

PCM 驱动程序通过覆盖由 struct snd_pcm_ops 结构体公开的函数指针来帮助执行 DMA 操作。它与平台无关,仅与 SOC DMA 引擎上游 API 交互。然后,DMA 引擎与特定于平台的 DMA 驱动程序交互以获得正确的 DMA 设置。struct snd_pcm_ops 是一个包含一组回调函数的结构体,这些回调函数与有关 PCM 接口的不同事件相关。在处理 ASoC(不是纯粹的 ALSA)时,只要你使用通用 PCM DMA 引擎框架,就永远不需要按原样实例化此结构体。核心会为你完成这项工作。

音频 DMA 接口

音频 DMA 驱动程序通过 snd_dmaengine_pcm_register() 注册。此函数可以为设备注册一个 struct snd_dmaengine_pcm_config。下面是它的原型:

1
2
3
4
5
6
7
8
/**
* snd_dmaengine_pcm_register - Register a dmaengine based PCM device
* @dev: The parent device for the PCM device
* @config: Platform specific PCM configuration
* @flags: Platform specific quirks
*/
int snd_dmaengine_pcm_register(struct device *dev,
const struct snd_dmaengine_pcm_config *config, unsigned int flags)
  • dev 是 PCM 设备的父设备,通常是&pdev->dev
  • config 是特定于平台的 PCM 配置,其类型为 struct snd_dmaengine_pcm_config。下文将详细介绍这个结构体
  • flags 表示描述如何处理 DMA 通道的附加标志。大多数情况下,它取值为 0。但是,其可能的值已在 include/sound/dmaengine_pcm.h 中定义并且均以 SND_DMAENGINE_为前缀。经常使用的标志包括以下 3 个

    • SND_DMAENGINE_PCM_FLAG_COMPAT /* 表示将使用自定义回调函数来请求通道 */
    • SND_DMAENGINE_PCM_FLAG_NO_DT /* 要求核心不要尝试通过设备树(DT)请求 DMA 通道 */
    • SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX /* 示 PCM 是半双工(half-duplex)的,DMA 通道在采集和回放之间共享 */

在注册之后,通用 PCM DMA 引擎框架将构建合适的 snd_pcm_ops 并设置组件驱动程序的。ops 字段。

Linux 中经典的 DMA 操作流程如下:

  • dma_request_channel: 用于分配 slave 通道(slave channel)
  • dmaengine_slave_config: 设置与 slave 通道和控制器相关参数
  • dma_prep_xxx: 获取事务的描述符
  • dma_cookie = dmaengine_submit(tx): 提交事务并抓取 DMA cookie
  • dma_async_issue_pending(chan): 开始传输并等待回调函数通知

在 ASOC 中,设备树用于将 DMA 通道映射到 PCM 设备。 snd_dmaengine_pcm_register()可以通过 dmaengine_pcm_request_chan_of()请求 DMA 通道,为了执行上述前 3 个步骤,需要为 PCM DMA 引擎核心提供附加信息,可以通过填充 struct snd_dmaengine_pcm_config 来完成。后两步由 PCM DMA 引擎核心透明处理。

struct snd_dmaengine_pcm_config 结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct snd_dmaengine_pcm_config {
int (*prepare_slave_config)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct dma_slave_config *slave_config);
struct dma_chan *(*compat_request_channel)(
struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_substream *substream);
int (*process)(struct snd_pcm_substream *substream,
int channel, unsigned long hwoff,
void *buf, unsigned long bytes);
dma_filter_fn compat_filter_fn;
struct device *dma_dev;
const char *chan_names[SNDRV_PCM_STREAM_LAST + 1];

const struct snd_pcm_hardware *pcm_hardware;
unsigned int prealloc_buffer_size;
};

该结构体主要处理 DMA 通道管理、缓冲区管理和通道配置,具体参数如下:

  • prepare_slave_config: 用于为 PCM 子流填充 DMA slave_config。它将从 PCM 驱动程序的 hwparams 回调函数中调用。
  • compat_request_channel:用于为不使用设备树的 platform 请求 DMA channel。如果设置了它,则。compat_filter_fn 将被忽略
  • compat_filter_fn:当为不使用设备树的 platform 请求 DMA 通道时,它发挥过滤功能,过滤的参数将是 DAI 的 DMA 数据
  • dma_dev:允许为注册 PCM 驱动程序的设备以外的设备请求 DMA 通道,如果设置了它,则将在此设备而不是 DAI 设备上请求 DMA 通道
  • chan_names:这是请求采集/回放 DMA 通道时使用的名称数组,如果设备有多个通道,则每个通道具有不同的 DMA 通道名称
  • pcm_hardware:描述了 PCM 硬件功能
  • prealloc_buffer_size:表示预分配音频缓冲区的大小

PCM DMA 配置可能不会提供注册 API(可能是 NULL),在这种情况下你应该通过 snd_soc_dai_init_dma_data()提供采集和回放 DAI DMA 通道配置。通过这种方法,其他元素将从系统核心派生。

流程梳理

数据流程框图如下:

可以看到,音频数据从用户复制到 DMA 缓冲区,然后,DMA 事务将数据移动到平台音频 TxFIF, 由于它与编解码器的链接(通过它们各自的 DAI), 这些数据将被发送到编解码器,以通过扬声器回放音频。采集操作流则相反,只是扬声器被麦克风取代。

相关函数流程如下: 原图

参考文献

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