0%

Linux驱动之ALSA(四)Platform驱动

平台驱动程序可以注册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设备驱动开发-约翰-马德奥》