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
一侧的 DAI
和 CODEC
一侧的 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 DAI
和 CODEC
的注册。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()
一样,最终也会添加一个描述 CODEC
的 struct 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 DAI
和 CODEC
时,既可以通过组件的名称
,也可以通过组件的 DTS 节点 phandle
,来指定要引用的组件,这里以组件的 DTS 节点 phandle
选择组件的方式来举例说明。
Machine 驱动
有时候要我们自己编写,但也可以使用目录 sound/soc/generic
下 ASoC
框架提供的公版 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 内存管理组件
从系统主存分配的内存;而 FIFO
是 CPU 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_node
和 platform_of_node
都指向了 i2s0
,这决定了在声卡注册时,绑定的 CPU DAI
i2s0
驱动注册的【DMA 内存管理组件】
,后面会看到这个细节。
先看将 DAI 连接对象
(struct snd_soc_dai_link
) 的 cpu_of_node
和 platform_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_substream
的 dma_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_dma
和 maximum_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_substream
的 dma_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_dai
的 playback_dma_data
和 capture_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 驱动