概述中提到音频Platform驱动主要作用是音频数据的传输,这里又细分为两步:
· 把音频数据从dma buffer通过dma操作搬运到cpu_dai FIFO,这部分驱动用snd_soc_platform_driver描述,后面分析用pcm_dma指代它。
· 把音频数据从cpu_dai FIFO通过数字音频接口(I2S/PCM/AC97)传送到codec_dai,这部分驱动用snd_soc_dai_driver描述,后面分析用cpu_dai指代它。
那么dma buffer中的音频数据从哪而来?保留这个问题,在后面章节pcm native分析。
cpu dai
从上图我们可知,实现一个cpu_dai驱动主要工作有:
· 实现dai操作函数,见snd_soc_dai_ops字段定义,用于配置控制dai,如系统时钟设置set_sysclk()、格式设置set_fmt()、硬件参数设置hw_params()、触发dai启动或停止传输trigger()等。
· 实现dai_drv的probe函数(dai初始化工作)、remove函数(dai卸载工作)、suspend/resume函数(dai电源管理配置)
· 初始化dai_drv实例snd_soc_dai_driver,包括playback和capture的能力描述信息、dai操作函数集指针、probe/remove回调、电源管理相关的suspend/resume回调。
· 通过snd_soc_register_dai()把初始化完成的snd_soc_dai_driver注册到soc-core:首先创建并初始化一个snd_soc_dai实例,然后把该snd_soc_dai实例插入到dai_list链表,Machine驱动初始化时会遍历该链表,以找到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驱动的主体,主要工作是配置好相关寄存器让硬件正常运转,因此这依赖于具体平台的cpu dai specification。snd_soc_dai_ops字段的详细说明见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初始化时,如何设定platform_device的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, // 置该resource标识为MEM资源
},
[1] = {
.start = DMACH_I2S0_TX, // start字段保存的是用于回放的dma通道号
.end = DMACH_I2S0_TX,
.flags = IORESOURCE_DMA, // 置该resource标识为DMA资源
},
[2] = {
.start = DMACH_I2S0_RX, // start字段保存的是用于录制的dma通道号
.end = DMACH_I2S0_RX,
.flags = IORESOURCE_DMA, // 置该resource标识为DMA资源
},
[3] = {
.start = DMACH_I2S0S_TX, // start字段保存的是用于回放的idma通道号
.end = DMACH_I2S0S_TX,
.flags = IORESOURCE_DMA, // 置该resource标识为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资源。
以上内容摘自:http://21cnbao.blog.51cto.com/109393/337609
pcm dma
pcm数据管理可以说是ALSA系统中最核心的部分,这部分的工作有两个:
· copy_from_user把用户态的音频数据拷贝到dma buffer;
· 启动dma把音频数据从dma buffer传送到I2S tx FIFO,这是回放的情形。
当数据送到I2S tx FIFO后,剩下的是启动I2S把数据传送到codec,然后DAC把数字PCM信号转换成模拟信号,送给SPK/HS输出,关于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_dma驱动特有的实现
· 浅紫色:pcm native关键结构
· 浅橙色:snd_soc_platform是pcm_dma注册时所创建的platform实例
我们先看看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通道号、传输单元大小。
pcm operations
pcm_dma操作函数的实现是本模块实现的主体,它用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逻辑设备时,会回调该函数,用于为runtime设定硬件约束,为runtime的private_data申请一个私有结构,保存dma资源如通道号、传输单元、缓冲区信息、IO设备信息等。代码如下:
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__);
// 设置substream运行期信息中的hw字段,可以把dma_hardware看成该platform的硬件约束
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中的private_data字段指向runtime_data
runtime->private_data = prtd;
return 0;
}
· hw_params:设置pcm硬件参数时(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系统将会把dma buffer数据传送到FIFO(回放的情形)。
注意:每次dma传输完成后,都要调用snd_pcm_period_elapsed()告知pcm native一个周期的数据已经传送到FIFO上了,然后再次调用dma_enqueue(),dma传输...如此循环,直到触发trigger()停止dma传输。
· trigger:pcm数据传送开始、停止、暂停、恢复时,会回调该函数启动或停止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;
pr_debug("Entered %s\n", __func__);
res = prtd->dma_pos - prtd->dma_start; // 当前位置减去首地址,其实就是已传输数据的size
pr_debug("Pointer offset: %lu\n", res);
/* 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
}
dma buffer allocation
在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; // dma buffer大小不得超过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, // 为playback stream分配的dma buffer
SNDRV_PCM_STREAM_PLAYBACK);
if (ret)
goto out;
}
if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
ret = preallocate_dma_buffer(pcm, // 为capture stream分配的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。
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注册到asoc-core。
· 创建一个snd_soc_platform实例,包含platform_drv(snd_soc_platform_driver)的相关信息,封装给asoc-core使用。
· 把以上创建并初始化好的platform插入到platform_list链表上,Machine驱动初始化时会遍历该链表,以找到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负责把FIFO中的数据传送到codec。
至于alsa如何把音频数据从userspace拷贝到dma buffer,如何管理dma buffer,如何启动I2S和DMA传输,见后续pcm native分析。