Linux ALSA 音频系统:物理链路篇02
- Platform
概述中提到音频 Platform 驱动主要用于音频数据传输,这里又细分为两步:
- 启动 dma 设备,把音频数据从 dma buffer 搬运到 cpu_dai FIFO,这部分驱动用 snd_soc_platform_driver 描述,后面分析用 pcm_dma 指代它。
- 启动数字音频接口控制器(I2S/PCM/AC97),把音频数据从 cpu_dai FIFO 传送到 codec_dai,这部分驱动用 snd_soc_dai_driver 描述,后面分析用 cpu_dai 指代它。
那么 dma buffer 中的音频数据从何而来?保留这个问题,在后面章节 pcm native 分析。
我们浏览下 platform_drv 中的几个重要结构体,其中浅蓝色部分是 cpu_dai 相关的,浅绿色部分是 pcm_dma 相关的。snd_soc_dai 是 cpu_dai 注册时所创建的 dai 实例,snd_soc_platform 是 pcm_dma 注册时所创建的 platform 实例,这些实例方便 soc-core 管理。
4.1. cpu dai
一个典型的 I2S 总线控制器框图:
各模块描述如下,摘自 S3C44B0 的数据手册:
Bus interface, register bank, and state machine(BRFC) - Bus interface logic and FIFO access are controlled by the state machine. 3-bit dual prescaler(IPSR) - One prescaler is used as the master clock generator of the IIS bus interface and the other is used as the external CODEC clock generator. 16-byte FIFOs(TXFIFO, RXFIFO) - In transmit data transfer, data are written to TXFIFO, and, in the receive data transfer, data are read from RXFIFO. Master IISCLK generaor(SCLKG) - In master mode, serial bit clock is generated from the master clock. Channel generator and state machine(CHNC) - IISCLK and IISLRCK are generated and controlled by the channel state machine. 16-bit shift register(SFTR) - Parallel data is shifted to serial data output in the transmit mode, and serial data input is shifted to parallel data in the receive mode.
再回顾下 I2S 总线协议,这是音频驱动开发最基本的内容了:
- BCLK:位时钟,对应数字音频的每一位数据;BCLK = 声道数 * 采样频率 * 采样位数;
- LRCLK:帧时钟,构成一个完整的声音单元;双声道的情况下,LRCLK = 0 时表示是左声道的数据,= 1 时表示是右声道的数据;LRCLK = 采样频率;
- DACDAT:下行数据;
- ADCDAT:上行数据;
- 数据的最高位总是出现在 LRCLK 跳变后的第 2 个 BCLK 脉冲处。
对于 cpu_dai 驱动,从上面的类图我们可知,主要工作有:
- 实现 dai 操作函数,见 snd_soc_dai_ops 定义,用于配置和操作音频数字接口控制器,如时钟配置 set_sysclk()、格式配置 set_fmt()、硬件参数配置 hw_params()、启动/停止数据传输 trigger() 等;
- 实现 probe 函数(初始化)、remove 函数(卸载)、suspend/resume 函数(电源管理);
- 初始化 snd_soc_dai_driver 实例,包括回放和录制的能力描述、dai 操作函数集、probe/remove 回调、电源管理相关的 suspend/resume 回调;
- 通过 snd_soc_register_dai() 把初始化完成的 snd_soc_dai_driver 注册到 soc-core:首先创建一个 snd_soc_dai 实例,然后把该 snd_soc_dai 实例插入到 dai_list 链表(声卡注册时会遍历该链表,找到 dai_link 声明的 cpu_dai 并绑定)。
/**
* snd_soc_register_dai - Register a DAI with the ASoC core
*
* @dai: DAI to register
*/
int snd_soc_register_dai(struct device *dev,
struct snd_soc_dai_driver *dai_drv)
{
struct snd_soc_dai *dai;
dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);
if (dai == NULL)
return -ENOMEM;
/* create DAI component name */
dai->name = fmt_single_name(dev, &dai->id);
if (dai->name == NULL) {
kfree(dai);
return -ENOMEM;
}
dai->dev = dev;
dai->driver = dai_drv;
if (!dai->driver->ops)
dai->driver->ops = &null_dai_ops;
mutex_lock(&client_mutex);
list_add(&dai->list, &dai_list);
mutex_unlock(&client_mutex);
return 0;
}
dai 操作函数的实现是 cpu_dai 驱动的主体,需要配置好相关寄存器让 I2S/PCM 总线控制器正常运转,snd_soc_dai_ops 字段的详细说明见 3.4. Codec audio operations 章节。
cpu_dai 驱动应该算是这个系列中最简单的一环,因此不多花费笔墨在这里了。倒是某些平台上,dma 设备信息(总线地址、通道号、传输单元大小)是在这里初始化的,这点要留意,这些 dma 设备信息在 pcm_dma 驱动中用到。以 Exynos 平台为例,代码位置 sound/soc/samsung/i2s.c。
Samsung Exynos 平台的音频 dma 设备信息用 s3c_dma_params 结构体描述:
struct s3c_dma_params {
struct s3c2410_dma_client *client; /* stream identifier */
int channel; /* Channel ID */
dma_addr_t dma_addr;
int dma_size; /* Size of the DMA transfer */
unsigned ch;
struct samsung_dma_ops *ops;
};
- client:流标识符
- channel:通道号
- dma_addr:设备的总线地址,这里通常指向 I2S tx FIFO 或 I2S rx FIFO 首地址
- dma_size:dma 传输单元大小
- ops:平台 dma 操作函数
sound/soc/samsung/i2s.c 中设置 dma 设备信息的相关代码片段:
struct i2s_dai {
// ...
/* Driver for this DAI */
struct snd_soc_dai_driver i2s_dai_drv;
/* DMA parameters */
struct s3c_dma_params dma_playback; // playback dma 描述信息
struct s3c_dma_params dma_capture; // capture dma 描述信息
struct s3c_dma_params idma_playback;// playback idma 描述信息,idma 仅用于回放,用于三星平台的 LPA(低功耗音频)模式
// ...
};
static __devinit int samsung_i2s_probe(struct platform_device *pdev)
{
// ...
// 从 platform_device 中取得 resource,得到 playback dma 通道号
res = platform_get_resource(pdev, IORESOURCE_DMA, 0);
if (!res) {
dev_err(&pdev->dev, "Unable to get I2S-TX dma resource\n");
return -ENXIO;
}
dma_pl_chan = res->start; // dma_pl_chan 中的 pl 是 playback 简写
// 从 platform_device 中取得 resource,得到 capture dma 通道号
res = platform_get_resource(pdev, IORESOURCE_DMA, 1);
if (!res) {
dev_err(&pdev->dev, "Unable to get I2S-RX dma resource\n");
return -ENXIO;
}
dma_cp_chan = res->start; // dma_cp_chan 中的 cp 是 capture 的简写
// 从 platform_device 中取得 resource,得到 playback idma 通道号
res = platform_get_resource(pdev, IORESOURCE_DMA, 2);
if (res)
dma_pl_sec_chan = res->start;
else
dma_pl_sec_chan = 0;
// 从 platform_device 中取得 resource,得到 I2S 的基地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Unable to get I2S SFR address\n");
return -ENXIO;
}
if (!request_mem_region(res->start, resource_size(res),
"samsung-i2s")) {
dev_err(&pdev->dev, "Unable to request SFR region\n");
return -EBUSY;
}
regs_base = res->start;
// ...
pri_dai->dma_playback.dma_addr = regs_base + I2STXD; // 设置 playback dma 设备地址为 I2S tx FIFO 地址
pri_dai->dma_capture.dma_addr = regs_base + I2SRXD; // 设置 capture dma 设备地址为 I2S rx FIFO 地址
pri_dai->dma_playback.client =
(struct s3c2410_dma_client *)&pri_dai->dma_playback;
pri_dai->dma_capture.client =
(struct s3c2410_dma_client *)&pri_dai->dma_capture;
pri_dai->dma_playback.channel = dma_pl_chan; // 设置 playback dma 通道号
pri_dai->dma_capture.channel = dma_cp_chan; // 设置 capture dma 通道号
pri_dai->src_clk = i2s_cfg->src_clk;
pri_dai->dma_playback.dma_size = 4; // 设置 playback dma 传输单元大小为 4 个字节
pri_dai->dma_capture.dma_size = 4; // 设置 capture dma 传输单元大小为 4 个字节
我们再看看 Board 初始化时,如何设定这些 resource,文件
arch/arm/mach-exynos/dev-audio.c:
static struct resource exynos4_i2s0_resource[] = {
[0] = {
.start = EXYNOS4_PA_I2S0, // start 字段保存的是 I2S 基地址
.end = EXYNOS4_PA_I2S0 + 0x100 - 1,
.flags = IORESOURCE_MEM, // 标识为 MEM 资源
},
[1] = {
.start = DMACH_I2S0_TX, // start 字段保存的是用于回放的 dma 通道号
.end = DMACH_I2S0_TX,
.flags = IORESOURCE_DMA, // 标识为 DMA 资源
},
[2] = {
.start = DMACH_I2S0_RX, // start 字段保存的是用于录制的 dma 通道号
.end = DMACH_I2S0_RX,
.flags = IORESOURCE_DMA, // 标识为 DMA 资源
},
[3] = {
.start = DMACH_I2S0S_TX, // start 字段保存的是用于回放的 idma 通道号
.end = DMACH_I2S0S_TX,
.flags = IORESOURCE_DMA, // 标识为 DMA 资源
},
};
struct platform_device exynos4_device_i2s0 = {
.name = "samsung-i2s", // platform_device 名称标识为 "samsung-i2s",与 i2s.c 中的samsung_i2s_driver 匹配
.id = 0,
.num_resources = ARRAY_SIZE(exynos4_i2s0_resource),
.resource = exynos4_i2s0_resource,
.dev = {
.platform_data = &i2sv5_pdata,
},
};
当 samsung_i2s_driver 初始化时,通过 platform_get_resource() 函数来获取 platform_device 声明的 resource。
struct resource 结构中我们通常关心 start、end 和 flags 这 3 个字段,分别标明资源的开始值、结束值和类型。flags 可以为 IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORESOURCE_DMA 等。start、end 的含义会随着 flags 而变更,如当 flags 为 IORESOURCE_MEM 时,start、end 分别表示该 platform_device 占据的内存的开始地址和结束地址;当 flags 为 IORESOURCE_IRQ 时,start、end 分别表示该 platform_device 使用的中断号的开始值和结束值,如果只使用了 1 个中断号,开始和结束值相同。对于同种类型的资源而言,可以有多份,譬如说某设备占据了 2 个内存区域,则可以定义 2 个 IORESOURCE_MEM 资源。
4.2. pcm dma
PCM 数据管理可以说是 ALSA 系统中最核心的部分,这部分的工作有两个(回放情形):
- copy_from_user 把用户态的音频数据拷贝到 dma buffer 中;
- 启动 dma 设备把音频数据从 dma buffer 传送到 I2S tx FIFO。
当数据送到 I2S tx FIFO 后,剩下的是启动 I2S 控制器把数据传送到 Codec,然后 DAC 把音频数字信号转换成模拟信号,再输出到 SPK/HP。关于 I2S (cpu_dai) 和 Codec,在以上两章已经描述清楚了。
为什么要使用 dma 传输?两个原因:首先在数据传输过程中,不需要 cpu 的参与,节省 cpu 的开销;其次传输速度快,提高硬件设备的吞吐量。对于 ARM,它不能直接把数据从 A 地址搬运到 B 地址,只能把数据从 A 地址搬运到一个寄存器,然后再从这个寄存器搬运到 B 地址;而 dma 有突发(Burst)传输能力,这种模式下一次能传输几个甚至十几个字节的数据,尤其适合大数据的高速传输。一个 dma 传输块里面,可以划分为若干个周期,每传输完一个周期产生一个中断。
档的初衷是为了描述清楚 pcm 数据流向,这里先剖析 pcm_dma 驱动,以便后面 pcm native 的分析。以 Exynos 平台为例,代码位置 sound/soc/samsung/dma.c。
- 浅绿色:pcm_dma 驱动共有的结构及接口定义
- 浅灰色:samsung exynos 平台特有的实现
- 浅紫色:pcm native 关键结构
- 浅橙色:snd_soc_platform 是pcm_dma 注册时所创建的 platform 实例
snd_pcm_substream 是 pcm native 关键结构体,上图可以看出这个结构体包含了音频数据传输所需的重要信息:pcm ops 操作函数集和 dma buffer。
我们先看看 dma 设备相关的结构,对于回放来说,dma 设备把内存缓冲区的音频数据传送到 I2S tx FIFO;对于录制来说,dma 设备把 I2S rx FIFO 的音频数据传送到内存缓存区。因此在 dma 设备传输之前,必须确定 data buffer 和 I2S FIFO 的信息。
snd_dma_buffer:数据缓存区,用于保存从用户态拷贝过来的音频数据;包含 dma buffer 的物理首地址,虚拟首地址、大小等信息;其中物理地址用于设定 dma 传输的源地址(回放情形)或目的地址(录制情形),虚拟地址用于与用户态之间的音频数据拷贝。
s3c_dma_params:dma 设备描述,包括设备总线地址(回放情形下为 I2S tx FIFO 首地址,设置为 dma 传输的目的地址)、dma 通道号、dma 传输单元大小,这些信息在 i2s.c 中初始化(具体见上一小节)。
runtime_data:dma 运行期信息
- state:记录 dma 设备状态,启动或停止;
- dma_loaded:dma 装载计数,每当启动一次 dma 传输,该计数加一;每当完成一次 dma 传输,该计数减一;
- dma_period:dma 周期数据大小;
- dma_start:指向 dma buffer 物理首地址;
- dma_pos:记录 dma buffer 当前指针位置,当 dma 每传输一次,都会更新该指针;
- dma_end:dma buffer 结束位置;
- params:dma 设备描述信息,包括设备总线地址、dma 通道号、传输单元大小。
4.2.1. pcm operations
操作函数的实现是本模块的主体,见 snd_pcm_ops 结构体描述:
struct snd_pcm_ops {
int (*open)(struct snd_pcm_substream *substream);
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream * substream,
unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream);
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
int (*copy)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos,
void __user *buf, snd_pcm_uframes_t count);
int (*silence)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
struct page *(*page)(struct snd_pcm_substream *substream,
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
};
下面介绍几个重要的接口:
- open:打开 pcm 逻辑设备时,回调该函数设定 dma 设备的硬件约束;并申请一个私有结构,保存 dma 设备资源如通道号、传输单元、缓冲区信息、IO 信息等,保存在 runtime->private_data。代码如下:
static const struct snd_pcm_hardware dma_hardware = {
.info = SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_PAUSE |
SNDRV_PCM_INFO_RESUME,
.formats = SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_U16_LE |
SNDRV_PCM_FMTBIT_U8 |
SNDRV_PCM_FMTBIT_S8,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 128*1024,
.period_bytes_min = PAGE_SIZE,
.period_bytes_max = PAGE_SIZE*2,
.periods_min = 2,
.periods_max = 128,
.fifo_size = 32,
};
static int dma_open(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct runtime_data *prtd;
pr_debug("Entered %s\n", __func__);
// 设置 dma 设备的硬件约束
snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
snd_soc_set_runtime_hwparams(substream, &dma_hardware);
// 为 runtime_data 分配内存,用于保存 dma 资源,包括缓冲区信息、IO 设备信息、通道号、传输单元大小
prtd = kzalloc(sizeof(struct runtime_data), GFP_KERNEL);
if (prtd == NULL)
return -ENOMEM;
spin_lock_init(&prtd->lock);
// runtime 的私有数据指向 runtime_data
runtime->private_data = prtd;
return 0;
}
- hw_params:设置硬件参数时(cmd=SNDRV_PCM_IOCTL_HW_PARAMS),回调该函数初始化 dma 资源,包括通道号、传输单元、缓冲区信息、IO 设备信息等。代码如下:
static int dma_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct runtime_data *prtd = runtime->private_data;
struct snd_soc_pcm_runtime *rtd = substream->private_data;
unsigned long totbytes = params_buffer_bytes(params);
struct s3c_dma_params *dma =
snd_soc_dai_get_dma_data(rtd->cpu_dai, substream); // 从 cpu_dai 驱动 i2s.c 取得 dma 设备资源
struct samsung_dma_info dma_info;
/* return if this is a bufferless transfer e.g.
* codec <--> BT codec or GSM modem -- lg FIXME */
if (!dma)
return 0;
/* this may get called several times by oss emulation
* with different params -HW */
if (prtd->params == NULL) {
/* prepare DMA */
prtd->params = dma; // 该字段保存的是 dma 设备资源,如 I2S tx FIFO 地址、dma 通道号、dma 传输单元等
prtd->params->ops = samsung_dma_get_ops(); // 平台的 dma 操作函数,这些操作函数实现见:arch/arm/plat-samsung/dma-ops.c
//...
prtd->params->ch = prtd->params->ops->request(
prtd->params->channel, &dma_info);
}
snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 这里把 dma buffer 相关信息赋给 substream runtime,注意 dma_buffer 在创建 pcm 逻辑设备时分配
runtime->dma_bytes = totbytes;
spin_lock_irq(&prtd->lock);
prtd->dma_loaded = 0;
prtd->dma_period = params_period_bytes(params);
prtd->dma_start = runtime->dma_addr; // dma buffer 物理首地址
prtd->dma_pos = prtd->dma_start;
prtd->dma_end = prtd->dma_start + totbytes;
spin_unlock_irq(&prtd->lock);
return 0;
}
- prepare:当数据已准备好(cmd=SNDRV_PCM_IOCTL_PREPARE),回调该函数告知 dma 设备数据已就绪。代码如下:
static int dma_prepare(struct snd_pcm_substream *substream)
{
struct runtime_data *prtd = substream->runtime->private_data;
int ret = 0;
pr_debug("Entered %s\n", __func__);
/* return if this is a bufferless transfer e.g.
* codec <--> BT codec or GSM modem -- lg FIXME */
if (!prtd->params)
return 0;
/* flush the DMA channel */
prtd->params->ops->flush(prtd->params->ch);
prtd->dma_loaded = 0; // 初始化 dma 装载计数
prtd->dma_pos = prtd->dma_start; // 设置 dma buffer 当前指针为 dma buffer 首地址
/* enqueue dma buffers */
dma_enqueue(substream); // 插入到 dma 传输队列中
return ret;
}
dma_enqueue() 函数,把当前 dma buffer 插入到 dma 传输队列中。当触发 trigger() 启动 dma 设备传输后,将会把 dma buffer 数据传送到 FIFO(回放情形)。
注意:每次 dma 传输完一个周期的数据后,都要调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经传送到 FIFO 上了,然后再次调用 dma_enqueue(),dma 传输…如此循环,直到触发 trigger() 停止 dma 传输。
- trigger:数据传送 开始/停止/暂停/恢复 时,回调该函数启动或停止 dma 传输(当上层第一次调用 pcm_write() 时,触发 trigger() 启动 dma 传输;当上层调用 pcm_stop() 或 pcm_drop() 时,触发 trigger() 停止 dma 传输)。trigger() 函数里面的操作必须是原子的,不能调用可能睡眠的操作,并且应尽量简单。代码如下:
static int dma_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct runtime_data *prtd = substream->runtime->private_data;
int ret = 0;
pr_debug("Entered %s\n", __func__);
spin_lock(&prtd->lock);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
prtd->state |= ST_RUNNING;
prtd->params->ops->trigger(prtd->params->ch); // 启动 dma 传输
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
prtd->state &= ~ST_RUNNING;
prtd->params->ops->stop(prtd->params->ch); // 停止 dma 传输
break;
default:
ret = -EINVAL;
break;
}
spin_unlock(&prtd->lock);
return ret;
}
- pointer:dma 每完成一次传输,都会调用该函数获得传输数据的当前位置,这样 pcm native 可计算 dma buffer 指针位置及可用空间。该函数也是原子的。代码如下:
static snd_pcm_uframes_t
dma_pointer(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct runtime_data *prtd = runtime->private_data;
unsigned long res;
res = prtd->dma_pos - prtd->dma_start; // 当前位置减去首地址,其实就是已传输数据的大小
/* we seem to be getting the odd error from the pcm library due
* to out-of-bounds pointers. this is maybe due to the dma engine
* not having loaded the new values for the channel before being
* called... (todo - fix )
*/
if (res >= snd_pcm_lib_buffer_bytes(substream)) {
if (res == snd_pcm_lib_buffer_bytes(substream))
res = 0;
}
return bytes_to_frames(substream->runtime, res); // 单位转化为 frames
}
4.2.2. dma buffer allocation
在 4.2.1. pcm operations 小节,数次提及 dma buffer,即 dma 数据缓冲区。dma buffer 的分配,一般发生在 pcm_dma 驱动初始化阶段 probe() 或 pcm 逻辑设备创建阶段 pcm_new()。代码如下:
static int preallocate_dma_buffer(struct snd_pcm *pcm, int stream)
{
struct snd_pcm_substream *substream = pcm->streams[stream].substream;
struct snd_dma_buffer *buf = &substream->dma_buffer;
size_t size = dma_hardware.buffer_bytes_max; // 缓冲区大小不得超过 hardware 中的buffer_bytes_max
buf->dev.type = SNDRV_DMA_TYPE_DEV;
buf->dev.dev = pcm->card->dev;
buf->private_data = NULL;
buf->area = dma_alloc_writecombine(pcm->card->dev, size,
&buf->addr, GFP_KERNEL); // area 字段是 dma buffer 虚拟首地址,addr 字段是 dma buffer 物理首地址
if (!buf->area)
return -ENOMEM;
buf->bytes = size;
return 0;
}
static int dma_new(struct snd_soc_pcm_runtime *rtd)
{
struct snd_card *card = rtd->card->snd_card;
struct snd_pcm *pcm = rtd->pcm;
int ret = 0;
if (!card->dev->dma_mask)
card->dev->dma_mask = &dma_mask;
if (!card->dev->coherent_dma_mask)
card->dev->coherent_dma_mask = DMA_BIT_MASK(32);
if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
ret = preallocate_dma_buffer(pcm, // 为回放子流分配的 dma buffer
SNDRV_PCM_STREAM_PLAYBACK);
if (ret)
goto out;
}
if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
ret = preallocate_dma_buffer(pcm, // 为录制子流分配的 dma buffer
SNDRV_PCM_STREAM_CAPTURE);
if (ret)
goto out;
}
out:
return ret;
}
static struct snd_soc_platform_driver samsung_asoc_platform = {
.ops = &dma_ops,
.pcm_new = dma_new,
.pcm_free = dma_free_dma_buffers,
};
当 soc-core 调用 soc_new_pcm() 创建 pcm 逻辑设备时,会回调 pcm_new() 完成 dma buffer 内存分配,注意回放子流和录制子流有着各自的 dma buffer。
4.2.3. pcm dma register
上两个小节,我们介绍了 pcm_dma 接口函数的作用及实现和 dma buffer 的分配,本小节分析 pcm_dma 注册过程。
当 platform_driver:
static struct platform_driver asoc_dma_driver = {
.driver = {
.name = "samsung-audio",
.owner = THIS_MODULE,
},
.probe = samsung_asoc_platform_probe,
.remove = __devexit_p(samsung_asoc_platform_remove),
};
与 .name = "samsung-audio" 的 platform_device(该 platform_device 在
arch/arm/plat-samsung/devs.c 中注册)匹配后,系统会回调
samsung_asoc_platform_probe() 注册 platform:
static struct snd_soc_platform_driver samsung_asoc_platform = {
.ops = &dma_ops,
.pcm_new = dma_new,
.pcm_free = dma_free_dma_buffers,
};
static int __devinit samsung_asoc_platform_probe(struct platform_device *pdev)
{
return snd_soc_register_platform(&pdev->dev, &samsung_asoc_platform);
}
snd_soc_register_platform:将 platform_drv 注册到 soc-core。
- 创建一个 snd_soc_platform 实例,包含 platform_drv(snd_soc_platform_driver)的相关信息,封装给 soc-core 使用;
- 把以上创建的 platform 实例插入到 platform_list 链表上(声卡注册时会遍历该链表,找到 dai_link 声明的 platform 并绑定)。
代码实现:
/**
* snd_soc_register_platform - Register a platform with the ASoC core
*
* @platform: platform to register
*/
int snd_soc_register_platform(struct device *dev,
struct snd_soc_platform_driver *platform_drv)
{
struct snd_soc_platform *platform;
platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL);
if (platform == NULL)
return -ENOMEM;
/* create platform component name */
platform->name = fmt_single_name(dev, &platform->id);
if (platform->name == NULL) {
kfree(platform);
return -ENOMEM;
}
platform->dev = dev;
platform->driver = platform_drv;
platform->dapm.dev = dev;
platform->dapm.platform = platform;
platform->dapm.stream_event = platform_drv->stream_event;
mutex_init(&platform->mutex);
mutex_lock(&client_mutex);
list_add(&platform->list, &platform_list);
mutex_unlock(&client_mutex);
return 0;
}
至此,完成了 Platform 驱动的实现。回放情形下,pcm_dma 设备负责把 dma buffer 中的数据搬运到 I2S tx FIFO,I2S 总线控制器负责把 I2S tx FIFO 中的数据传送到 Codec。至于 alsa 如何把音频数据从 userspace 拷贝到 dma buffer,如何管理 dma buffer,如何启动 I2S 和 DMA 传输,这里面一环扣一环,见后续 pcm native 分析。
- Machine
章节 3. Codec 和 4. Platform 介绍了 Codec、Platform 驱动,但仅有 Codec、Platform 驱动是不能工作的,需要一个角色把 codec、codec_dai、cpu_dai、platform 给链结起来才能构成一个完整的音频链路,这个角色就由 machine_drv 承担了。如下是一个典型的智能手机音频框图:
+------------+ +---------------------+ +------------+
| | | | | |
| | + CODEC + | |
| AP +------>AIF1 AIF3+------> PA +->SPK
| | + +-----+ +-----+ + | |
| | | | DSP | | DAC | | | |
+------------+ | +-----+ +-----+ | +------------+
| +-----+ +-----+ |
| | DSP | | DAC | |
| +-----+ +-----+ |
+------------+ | +-----+ +-----+ | +------------+
| | | | DSP | | ADC | | | |
| | + +-----+ +-----+ + | |
| BB +------>AIF2 +-----+ +-----+ AIF4+------> BTSCO |
| | + | DSP | | ADC | + | |
| | | +-----+ +-----+ | | |
+------------+ +----------+----------+ +------------+
| | |
+MIC +HP +EARP
组成了 4 个音频链路(dai_link):
- AP<>AIF1:AP(应用处理器)与 Codec 之间的链路,多媒体声音
- BB<>AIF2:BB(基带处理器)与 Codec 之间的链路,通话语音
- PA<>AIF3:PA(智能功率放大器)与 Codec 之间的链路,外放输出
- BTSCO<>AIF4:BTSCO(蓝牙)与 Codec 之间的链路,蓝牙耳机输出
snd_soc_dai_link 结构体:
struct snd_soc_dai_link {
/* config - must be set by machine driver */
const char *name; /* Codec name */
const char *stream_name; /* Stream name */
const char *codec_name; /* for multi-codec */
const struct device_node *codec_of_node;
const char *platform_name; /* for multi-platform */
const struct device_node *platform_of_node;
const char *cpu_dai_name;
const struct device_node *cpu_dai_of_node;
const char *codec_dai_name;
unsigned int dai_fmt; /* format to set on init */
/* Keep DAI active over suspend */
unsigned int ignore_suspend:1;
/* Symmetry requirements */
unsigned int symmetric_rates:1;
/* pmdown_time is ignored at stop */
unsigned int ignore_pmdown_time:1;
/* codec/machine specific init - e.g. add machine controls */
int (*init)(struct snd_soc_pcm_runtime *rtd);
/* machine stream operations */
struct snd_soc_ops *ops;
};
注释比较详细,重点介绍如下几个字段:
- codec_name:音频链路需要绑定的 codec 名称,声卡注册时会遍历 codec_list,找到同名的 codec 并绑定;
- platform_name:音频链路需要绑定的 platform 名称,声卡注册时会遍历 platform_list,找到同名的 platform 并绑定;
- cpu_dai_name:音频链路需要绑定的 cpu_dai 名称,声卡注册时会遍历 dai_list,找到同名的 dai 并绑定;
- codec_dai_name:音频链路需要绑定的 codec_dai 名称,声卡注册时会遍历 dai_list,找到同名的 dai 并绑定;
- ops:重点留意 hw_params() 回调,一般来说这个回调是要实现的,用于配置 codec、codec_dai、cpu_dai 的数据格式和系统时钟。在 3.4. Codec audio operations 小节中有描述。
goni_wm8994.c 中的 dai_link 定义,两个音频链路分别用于 Media 和 Voice:
static struct snd_soc_dai_link goni_dai[] = {
{
.name = "WM8994",
.stream_name = "WM8994 HiFi",
.cpu_dai_name = "samsung-i2s.0",
.codec_dai_name = "wm8994-aif1",
.platform_name = "samsung-audio",
.codec_name = "wm8994-codec.0-001a",
.init = goni_wm8994_init,
.ops = &goni_hifi_ops,
}, {
.name = "WM8994 Voice",
.stream_name = "Voice",
.cpu_dai_name = "goni-voice-dai",
.codec_dai_name = "wm8994-aif2",
.codec_name = "wm8994-codec.0-001a",
.ops = &goni_voice_ops,
},
};
除了 dai_link,机器中一些特定的音频控件和音频事件也可以在 machine_drv 定义,如耳机插拔检测、外部功放打开关闭等。
我们再分析 machine_drv 初始化过程:
static struct snd_soc_card goni = {
.name = "goni",
.owner = THIS_MODULE,
.dai_link = goni_dai,
.num_links = ARRAY_SIZE(goni_dai),
.dapm_widgets = goni_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(goni_dapm_widgets),
.dapm_routes = goni_dapm_routes,
.num_dapm_routes = ARRAY_SIZE(goni_dapm_routes),
};
static int __init goni_init(void)
{
int ret;
goni_snd_device = platform_device_alloc("soc-audio", -1);
if (!goni_snd_device)
return -ENOMEM;
/* register voice DAI here */
ret = snd_soc_register_dai(&goni_snd_device->dev, &voice_dai);
if (ret) {
platform_device_put(goni_snd_device);
return ret;
}
platform_set_drvdata(goni_snd_device, &goni);
ret = platform_device_add(goni_snd_device);
if (ret) {
snd_soc_unregister_dai(&goni_snd_device->dev);
platform_device_put(goni_snd_device);
}
return ret;
}
- 创建一个 .name="soc-audio" 的 platform_device 实例;
- 设置 platform_device 的私有数据 snd_soc_card;
- 然后注册 platform_device 到系统中;
- 再然后呢?好像没有了…
但是真的没有了吗?别忘了,platform_device 还有个好伙伴 platform_driver 跟它配对。而 .name="soc-audio" 的 platform_driver 定义在 soc-core.c 中:
/* ASoC platform driver */
static struct platform_driver soc_driver = {
.driver = {
.name = "soc-audio",
.owner = THIS_MODULE,
.pm = &snd_soc_pm_ops,
},
.probe = soc_probe,
.remove = soc_remove,
};
static int __init snd_soc_init(void)
{
// ...
snd_soc_util_init();
return platform_driver_register(&soc_driver);
}
module_init(snd_soc_init);
两者匹配后,soc_probe() 会被调用,继而调用 snd_soc_register_card() 注册声卡。由于该过程很冗长,这里不一一贴代码分析了,但整个流程是比较简单的,流程图如下:
- 取出 platform_device 的私有数据,该私有数据就是 snd_soc_card ;
- snd_soc_register_card() 为每个 dai_link 分配一个 snd_soc_pcm_runtime 实例,别忘了之前提过 snd_soc_pcm_runtime 是 ASoC 的桥梁,保存着 codec、codec_dai、cpu_dai、platform 等硬件设备实例。
- 随后的工作都在 snd_soc_instantiate_card() 进行:
- 遍历 dai_list、codec_list、platform_list 链表,为每个音频链路找到对应的 cpu_dai、codec_dai、codec、platform;找到的 cpu_dai、codec_dai、codec、platform 保存到 snd_soc_pcm_runtime ,完成音频链路的设备绑定;
- 调用 snd_card_create() 创建声卡;
- soc_probe_dai_link() 依次回调 cpu_dai、codec、platform、codec_dai 的 probe() 函数,完成各音频设备的初始化,随后调用 soc_new_pcm() 创建 pcm 逻辑设备(因为涉及到本系列的重点内容,后面具体分析这个函数);
- 最后调用 snd_card_register() 注册声卡。
soc_new_pcm 源码分析:
/* create a new pcm */
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
struct snd_soc_codec *codec = rtd->codec;
struct snd_soc_platform *platform = rtd->platform;
struct snd_soc_dai *codec_dai = rtd->codec_dai;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
struct snd_pcm_ops *soc_pcm_ops = &rtd->ops;
struct snd_pcm *pcm;
char new_name[64];
int ret = 0, playback = 0, capture = 0;
// 初始化 snd_soc_pcm_runtime 的 ops 字段,成员函数其实依次调用 machine、codec_dai、cpu_dai、platform 的回调;如 soc_pcm_hw_params:
// |-> rtd->dai_link->ops->hw_params()
// |-> codec_dai->driver->ops->hw_params()
// |-> cpu_dai->driver->ops->hw_params()
// |-> platform->driver->ops->hw_params()
// 在这里把底层硬件的操作接口抽象起来,pcm native 不用知道底层硬件细节
soc_pcm_ops->open = soc_pcm_open;
soc_pcm_ops->close = soc_pcm_close;
soc_pcm_ops->hw_params = soc_pcm_hw_params;
soc_pcm_ops->hw_free = soc_pcm_hw_free;
soc_pcm_ops->prepare = soc_pcm_prepare;
soc_pcm_ops->trigger = soc_pcm_trigger;
soc_pcm_ops->pointer = soc_pcm_pointer;
/* check client and interface hw capabilities */
snprintf(new_name, sizeof(new_name), "%s %s-%d",
rtd->dai_link->stream_name, codec_dai->name, num);
if (codec_dai->driver->playback.channels_min)
playback = 1;
if (codec_dai->driver->capture.channels_min)
capture = 1;
// 创建 pcm 逻辑设备
ret = snd_pcm_new(rtd->card->snd_card, new_name,
num, playback, capture, &pcm);
if (ret < 0) {
printk(KERN_ERR "asoc: can't create pcm for codec %s\n", codec->name);
return ret;
}
/* DAPM dai link stream work */
INIT_DELAYED_WORK(&rtd->delayed_work, close_delayed_work);
rtd->pcm = pcm;
pcm->private_data = rtd; // pcm 的私有数据指向 snd_soc_pcm_runtime
if (platform->driver->ops) {
// 初始化 snd_soc_pcm_runtime 的 ops 字段,这些与 pcm_dma 操作相关,一般我们只用留意 pointer 回调
soc_pcm_ops->mmap = platform->driver->ops->mmap;
soc_pcm_ops->pointer = platform->driver->ops->pointer;
soc_pcm_ops->ioctl = platform->driver->ops->ioctl;
soc_pcm_ops->copy = platform->driver->ops->copy;
soc_pcm_ops->silence = platform->driver->ops->silence;
soc_pcm_ops->ack = platform->driver->ops->ack;
soc_pcm_ops->page = platform->driver->ops->page;
}
if (playback)
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, soc_pcm_ops); // 把 soc_pcm_ops 赋给 playback substream 的 ops 字段
if (capture)
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, soc_pcm_ops); // 把 soc_pcm_ops 赋给 capture substream 的 ops 字段
// 回调 dma 驱动的 pcm_new(),进行 dma buffer 内存分配和 dma 设备初始化
if (platform->driver->pcm_new) {
ret = platform->driver->pcm_new(rtd);
if (ret < 0) {
pr_err("asoc: platform pcm constructor failed\n");
return ret;
}
}
pcm->private_free = platform->driver->pcm_free;
printk(KERN_INFO "asoc: %s <-> %s mapping ok\n", codec_dai->name,
cpu_dai->name);
return ret;
}
可见 soc_new_pcm() 最主要的工作是创建 pcm 逻辑设备,创建回放子流和录制子流实例,并初始化回放子流和录制子流的 pcm 操作函数(数据搬运时,需要调用这些函数来驱动 codec、codec_dai、cpu_dai、dma 设备工作)。