1.嵌入式音频系统硬件连接
下图所示的嵌入式设备使用IIS将音频数据发送给编解码器。对编解码器的I/O寄存器的编程通过IIC总线进行。
2.音频体系结构-ALSA
ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主流音频体系结构
在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬件的控制。
3.ALSA设备文件
ALSA驱动核心会创建和管理一些设备节点,比如:
/dev/snd/controlC0: 一个控制结点,(应用程序用它来控制声卡,例如通道选择,音量的控制等)
/dev/snd/pcmC0D0p:用于播放的pcm设备
/dev/snd/pcmC0D0c:用于录音的pcm设备
C0D0代表的是声卡0中的设备0,最后一个c代表capture,最后一个p代表playback。
4.声卡的建立流程
第一步,创建snd_card的一个实例
snd_card可以说是整个ALSA音频驱动最顶层的一个结构,整个声卡的软件逻辑结构开始于该结构,几乎所有与声音相关的逻辑设备都是在snd_card的管理之下.
第二步,创建声卡的功能部件(逻辑设备),例如PCM, Mixer等,并将逻辑设备与步骤一创建的声卡绑定
通常, alsa-driver的已经提供了一些常用的部件的创建函数,PCM ---- snd_pcm_new()、CONTROL -- snd_ctl_create()
第三步,将声卡注册进ALSA框架
经过以上的创建步骤之后,声卡的逻辑结构如下图所示:
5.PCM设备的创建
对于一个pcm设备,可以生成两个设备文件,一个用于playback,一个用于capture,代码中也确定了他们的命名规则:
- playback -- pcmCxDxp,通常系统中只有一个声卡和一个pcm,它就是pcmC0D0p
- capture -- pcmCxDxc,通常系统中只有一个声卡和一个pcm,它就是pcmC0D0c
新建一个pcm设备的过程:
- snd_card_create ,pcm是声卡下的一个设备(部件),所以第一步是要创建一个声卡
- snd_pcm_new, 调用该api创建一个pcm,在该api中会做以下事情:
建立playback stream,相应的substream也同时建立
建立capture stream,相应的substream也同时建立
调用snd_device_new()把该pcm挂到声卡中,参数ops中的dev_register字段指向了函数
snd_pcm_dev_register,这个回调函数会在声卡的注册阶段被调用
- snd_pcm_set_ops, 设置操作该pcm的控制/操作接口函数,参数中的snd_pcm_ops结构中的函数通常就是我们驱动要实现的函数
- snd_card_register 注册声卡,在这个阶段会遍历声卡下的所有逻辑设备,并且调用各设备的注册回调函数,对于pcm,就是第二步提到的snd_pcm_dev_register函数,该回调函数建立了和用户空间应用程序( alsa-lib)通信所用的设备文件节点:/dev/snd/pcmCxxDxxp和/dev/snd/pcmCxxDxxc
6.Control设备的创建
Control设备和PCM设备一样,都属于声卡下的逻辑设备。用户空间的应用程序通过alsa-lib访问该Control设备,读取或设置control的状态,从而达到控制音频Codec进行各种Mixer等控制操作。
要自定义一个Control,我们首先要定义3各回调函数:info,get和put。然后,定义一个snd_kcontrol_new结构:
static struct snd_kcontrol_new my_control __devinitdata = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "PCM Playback Switch",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
.private_value = 0xffff,
.info = my_control_info,
.get = my_control_get,
.put = my_control_put
};
iface字段指出了control的类型
name字段是该control的名字
index字段用于保存该control的在该卡中的编号。
access字段包含了该control的访问类型。
private_value字段包含了一个任意的长整数类型值。
info回调函数用于获取control的详细信息
get回调函数用于读取control的当前值,并返回给用户空间的应用程序。
put回调函数用于把应用程序的控制值设置到control中。
我们需要在我们的驱动程序初始化时主动调用snd_pcm_new()函数创建pcm设备,而control设备则在snd_card_create()内被创建,snd_card_create()通过调用snd_ctl_create()函数创建control设备节点。所以我们无需显式地创建control设备,只要建立声卡,control设备被自动地创建。
7.移动设备中的ALSA(ASoC)
ASoC--ALSA System on Chip ,是为了更好地支持嵌入式处理器和移动设备中的音频Codec的一套软件体系。ASoC不能单独存在,它建立在标准ALSA驱动之上,必须和标准的ALSA驱动框架相结合才能工作。
在软件层面, ASoC也把嵌入式设备的音频系统同样分为3大部分, Machine, Platform和Codec
Machine驱动:跟单板相关,绑定Platform和Codec驱动,即表明使用的是哪个Platform,哪个CPU DAI、DMA、Codec和Codec DAI。
Platform驱动 :它包含了该SoC平台的音频DMA和音频接口DAI的配置和控制( I2S, PCM等等)
Codec驱动 :它包含了一些音频的控件( Controls),音频接口, DAMP(动态音频电源管理)的定义和某些Codec IO功能。所有的Codec驱动都要提供以下特性:
- Codec DAI 和 PCM的配置信息;
- Codec的IO控制方式( I2C, SPI等);
- Mixer和其他的音频控件;
- Codec的ALSA音频操作接口;
8.ASoC架构中的Machine
ASoC把声卡注册为Platform Device。
Machine驱动在一个重要的数据结构snd_soc_dai_link中,指定了Platform、 Codec、 codec_dai、 cpu_dai的名字,稍后Machine驱动将会利用这些名字去匹配已经在系统中注册的platform, codec, dai,这些注册的部件都是在另外相应的Platform驱动和Codec驱动的代码文件中定义的,这样看来, Machine驱动的设备初始化代码无非就是选择合适Platform和Codec以及dai,用他们填充以上几个数据结构,然后注册Platform设备即可。
platform总线会匹配这两个名字相同的device和driver,同时会触发soc_probe的调用,它正是整个ASoC驱动初始化的入口。
在soc_probe函数中会完成以下任务:
- 调用标准的alsa函数创建声卡实例
- 挨个调用了codec, dai和platform驱动的probe函数
- 调用了soc_new_pcm()函数用于创建标准alsa驱动的pcm逻辑设备
- 最后则是调用标准alsa驱动的声卡注册函数对声卡进行注册
9.ASoC架构中的Codec
在移动设备中, Codec的作用可以归结为4种,分别是:
- 对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号
- 对Mic、 Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音信号转变CPU能够处理的数字信号
- 对音频通路进行控制,比如播放音乐,收听调频收音机,又或者接听电话时,音频信号在codec内的流通路线是不一样的
- 对音频信号做出相应的处理,例如音量控制,功率放大, EQ控制等等
描述Codec的最主要的几个数据结构分别是:
snd_soc_codec, snd_soc_codec_driver, snd_soc_dai, snd_soc_dai_driver,其中的snd_soc_dai和snd_soc_dai_driver在ASoC的Platform驱动中也会使用到, Platform和Codec的DAI通过snd_soc_dai_link结构,在Machine驱动中进行绑定连接。
Codec驱动的步骤:
- 定义snd_soc_codec_driver和snd_soc_dai_driver的实例,然后调用snd_soc_register_codec函数对Codec进行注册。
在snd_soc_register_codec函数中,它申请了一个snd_soc_codec结构的实例:确定codec的名字,这个名字很重要, Machine驱动定义的snd_soc_dai_link中会指定每个link的codec和dai的名字,进行匹配绑定时就是通过和这里的名字比较,从而找到该Codec的!然后初始化snd_soc_codec结构的各个字段,多数字段的值来自上面定义的snd_soc_codec_driver的实例。
- 通过snd_soc_register_dais函数对本Codec的dai进行注册
在snd_soc_register_dais函数中为每个snd_soc_dai实例分配内存,确定dai的名字,用snd_soc_dai_driver实例的字段对它进行必要初始化
- 最后,它把codec实例链接到全局链表codec_list中,把dai链接到全局链表dai_list中,并且调snd_soc_instantiate_cards函数触发Machine驱动进行一次匹配绑定操作
10.ASoC架构中的Platform
Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接口( DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或者是喇叭的音信信号。在具体实现上, ASoC有把Platform驱动分为两个部分: snd_soc_platform_driver和snd_soc_dai_driver。其中, platform_driver负责管理音频数据,把音频数据通过dma或其他操作传送至cpu dai中, dai_driver则主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma等参数与snd_soc_platform_driver进行交互。
snd_soc_platform_driver的注册
实现该驱动大致可以分为以下几个步骤:
- 定义一个snd_soc_platform_driver结构的实例;
- 在platform_driver的probe回调中利用ASoC的API: snd_soc_register_platform()注册上面定义的实例;
- 实现snd_soc_platform_driver中的各个回调函数
snd_soc_platform_driver中的ops字段是一个snd_pcm_ops结构,实现该结构中的各个回调函数是soc platform驱动的主要工作,他们基本都涉及dma操作以及dma buffer的管理等工作。下面介绍几个重要的回调函数:
- ops.open:当应用程序打开一个pcm设备时,该函数会被调用
- ops.hw_params:驱动的hw_params阶段,该函数会被调用,该函数会通过snd_soc_dai_get_dma_data函数获得对应的dai的dma参数
- ops.prepare:正式开始数据传送之前会调用该函数,该函数通常会完成dma操作的必要准备工作。
- ops.trigger:数据传送的开始,暂停,恢复和停止时,该函数会被调用。
- ops.pointer:该函数返回传送数据的当前位置
cpu的snd_soc_dai driver驱动的注册
dai驱动通常对应cpu的一个或几个I2S/PCM接口,与snd_soc_platform一样, dai驱动也是实现为一个platform driver,实现一个dai驱动大致可以分为以下几个步骤:
- 定义一个snd_soc_dai_driver结构的实例;
- 在对应的platform_driver中的probe回调中通过API: snd_soc_register_dai或者snd_soc_register_dais,注
- 册snd_soc_dai实例;
- 实现snd_soc_dai_driver结构中的probe、 suspend等回调;
- 实现snd_soc_dai_driver结构中的snd_soc_dai_ops字段中的回调函数
它的ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集合, dai的配置和控制几乎都是通过这些回调函数来实现的,这些回调函数基本可以分为3大类,驱动程序可以根据实际情况实现其中的一部分:
- 工作时钟配置函数 通常由machine驱动调用
- 标准的snd_soc_ops回调 通常由soc-core在进行PCM操作时调用
- 抗pop, pop声 由soc-core调用
11.音频数据的dma操作
soc-platform驱动的最主要功能就是要完成音频数据的传送,大多数情况下,音频数据都是通过dma来完成的
申请dma buffer
在声卡的建立阶段,pcm_new回调函数会被调用,函数进一步为playback和capture分别调用preallocate_dma_buffer函数分配dma内存,然后完成substream->dma_buffer的初始化赋值工作
在声卡的hw_params阶段, snd_soc_platform_driver结构的ops->hw_params会被调用,在该回调用,通常会使用api: snd_pcm_set_runtime_buffer()把substream->dma_buffer的数值拷贝到substream->runtime的相关字段中( .dma_area, .dma_addr, .dma_bytes),这样以后就可以通过substream->runtime获得这些地址和大小信息了。
dma buffer获得后,即是获得了dma操作的源地址,那么目的地址在哪里?其实目的地址当然是在dai中,也就是前面介绍的snd_soc_dai结构的playback_dma_data和capture_dma_data字段中,而这两个字段的值也是在hw_params阶段,由snd_soc_dai_driver结构的ops->hw_params回调,利用api: snd_soc_dai_set_dma_data进行设置的。紧随其后, snd_soc_platform_driver结构的ops->hw_params回调利用api: snd_soc_dai_get_dma_data获得这些dai的dma信息,其中就包括了dma的目的地址信息。这些dma信息通常还会被保存在substream->runtime->private_data中,以便在substream的整个生命周期中可以随时获得这些信息,从而完成对dma的配置和操作
dma buffer管理
播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform的dma操作则不停地从该buffer中取
出数据,经dai送往codec中。录音时则正好相反, codec源源不断地把A/D转换好的音频数据经过dai送入dma
buffer中,而应用程序则不断地从该buffer中读走音频数据。
以上只是参考别人博客,概括性的总结了一下Linux音频子系统,如果想深入了解,可以查看博客:
http://blog.csdn.net/droidphone/article/details/6289712