ASoC框架是一种linux系统声卡驱动框架,是针对嵌入式设备在linux alsa声卡驱动框架基础上进行了一层封装,意在将声卡和cpu两部分的控制代码分离开来;
如上图是音频系统的硬件示意图,声卡通过I2S接口与cpu进行音频数据传输,通过I2C接口与cpu进行控制通讯(数据接口和控制接口也可以是其他);
录音数据通路:麦克风---->声卡–(I2S)->DMA---->内存;
播放数据通路:内存---->DMA–(I2S)->声卡---->扬声器;
如图所示,cpu部分的代码包括DMA控制器部分的代码和I2S控制器接口部分的代码;声卡部分的代码包括声卡寄存器控制部分(I2C接口进行控制)和I2S接口数据格式控制部分;
在ASoC驱动框架中cpu部分称作platform,声卡部分被称作codec,两者通过machine进行匹配连接;machine可以理解为对开发板的抽象,开发板可能包括多个声卡,对应machine部分包含多个link;platform、codec和machine三者的关系在下面段落中会说明;
如上图:
codec: codec是声卡部分的控制代码,会抽象出两个结构体分别为snd_soc_dai_driver
和snd_soc_codec_driver
;
snd_soc_dai_driver
为声卡接口部分的控制代码,如设置该声卡支持播放和录音参数,如采样率(如SNDRV_PCM_RATE_8000_48000
),数据格式(如SNDRV_PCM_FMTBIT_S16_LE
)、通道数等,以及提供声卡相关的操作函数,如snd_soc_dai_ops->set_fmt
设置数据格式、snd_soc_dai_ops->set_sysclk
系统时钟设置等等;
snd_soc_codec_driver
为声卡注册相关控件(controls)、部件(widgets)以及routes snd_soc_dapm_route
;
controls 为控件抽象的结构体为snd_kcontrol_new
,可以为单独的一个控件,控制声卡某一功能,如声卡的播放声音,也可以用于连接两个部件(widget)形成一条通路(route),controls的注册函数snd_soc_add_codec_controls
;
widgets 为部件,抽象的结构体为snd_soc_dapm_widget
,个人感觉部件就是声卡内部的节点的抽象,也是对某些controls做了一层封装,如录音输入引脚、输出引脚以及中间控制多路声音混合的混音器,widgets注册函数snd_soc_dapm_new_controls
;
route 表示的是两个节点之间的通路,抽象的结构体为snd_soc_dapm_route
,用于连接两个widget,两个widget中间通过controls连接形成一条route(貌似controls可以为空),routes注册函数snd_soc_dapm_add_routes
;
snd_soc_dai_driver
和snd_soc_codec_driver
是同时注册的,注册函数为snd_soc_register_platform(struct device *dev, const struct snd_soc_platform_driver *platform_drv)
;
platform: platform为cpu部分的控制代码,抽象出两个结构体snd_soc_dai_driver
和snd_soc_platform_driver
;
snd_soc_dai_driver
和codec的的dai是同类型结构体,功能一样,只是在platform中的dai针对的对象是cpu部分的I2S接口;
snd_soc_dai_driver
的注册函数为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)
;参数cmpnt_drv
需要自己定义(暂时不知道作用), dai_drv
可以包含多个snd_soc_dai_driver
即可以为结构体指针,num_dai
表示结构体数组成员格式;
snd_soc_platform_driver
是控制cpu部分音频数据传输的,一般音频数据在I2S接口与内存之间传输会使用DMA,所以snd_soc_platform_driver
一般为cpu DMA的控制代码;结构体主要成员:
name
:platform driver的名称,
probe
:probe函数,platform和codec匹配成功后调用;
remove
:remove函数,与probe函数做相反的操作;
pcm_new
:DMA内存申请等操作,可使用dma_alloc_writecombine
申请DMA内存;
pcm_free
:DMA内存释放等操作,可使用dma_free_writecombine
释放DMA内存;
ops
:pcm数据流操作函数,即操作DMA来进行数据搬运;
ops.open
:open函数
a 使用snd_soc_set_runtime_hwparams(struct snd_pcm_substream *substream,const struct snd_pcm_hardware *hw)
函数设置snd_pcm_runtime
硬件属性,其中snd_pcm_hardware
需要用户自己定义,规定一些buffer大小、最大最小周期等等,貌似这些参数用户空间也需要访问,然后根据区间设定合适的period、period_size等等再进行数据传输;
b 使用snd_pcm_hw_constraint_integer(struct snd_pcm_runtime *runtime, snd_pcm_hw_param_t var)
确保snd_pcm_runtime
确保缓冲区(buffer)大小是周期(period)大小的倍数;
c 根据当前snd_pcm_runtime->pstr->stream
是SNDRV_PCM_STREAM_PLAYBACK
还是SNDRV_PCM_STREAM_CAPTURE
申请对应的DMA中断等相关操作;
ops.close
:close函数,和open操作相反;
ops.ioctl
:这个可以不用自己写,使用内核的snd_pcm_lib_ioctl
函数;
ops.prepare
:启动播放或录音开始前调用,初始化相关状态参数,比如播放或录音的offset
ops.trigger
:音频传输触发函数,根据传入的命令进行开始\停止\暂停等等操作(如SNDRV_PCM_TRIGGER_START
要设置DMA源、目的地址、传输长度并开始DMA传输);音频的DMA传输是一段一段传输的(DMA每次触发传输的大小为snd_pcm_substream->snd_pcm_runtime->buffer_size
),应用程序每次发起SNDRV_PCM_TRIGGER_START
传输的数据为snd_pcm_substream->snd_pcm_runtime->dma_bytes
大小(即申请的DMA buffer大小,在设定snd_pcm_hardware
的时候就被指定),在DMA中断处理函数中需要做的事情:
a 更新dma buffer指针offset += substream->runtime->buffer_size,判断如果已经读到了DMA buffer尾部()则offset=0(注意:offset变量需要驱动自己定义维护,在ops.pointer
中被返回应用层);
b 调用snd_pcm_period_elapsed
更新pcm状态为下一个周期(period),该函数会调用ops.pointer
函数,在ops.pointer
函数中需要利用dma buffer当前偏移量offset调用bytes_to_frames(struct snd_pcm_runtime *runtime, ssize_t size)
返回frames;
c 驱动还需要自己维护一个变量streaming保存当前的状态,如果streaming=1那么再次设置DMA源、目的地址、传输长度并开始DMA传输(注意streaming状态根据用户传进来的命令在ops.trigger
中被改变,比如用户调用SNDRV_PCM_TRIGGER_STOP
那streaming应该被设为0);
d return IRQ_HANDLED;
ops.pointer
:要利用dma buffer当前偏移量offset调用bytes_to_frames(struct snd_pcm_runtime *runtime, ssize_t size)
返回frames;
后面的成员可以不实现:
ops.get_time_info
:
ops.fill_silence
:
ops.copy_user
:
ops.copy_kernel
:
ops.fill_silence
:
ops.page
:
ops.mmap
:
ops.ack
:
snd_soc_platform_driver
的注册函数为snd_soc_register_platform(struct device *dev,const struct snd_soc_platform_driver *platform_drv)
;
machine: machine部分控制管理platform和codec之间的连接匹配,管理控件(controls)、部件(widgets)以及routes,其抽象的结构体为snd_soc_card
,结构体成员简介:
name
、long_name
、driver_name
、dmi_longname
:snd_soc_card名字相关
snd_card
:alsa声卡层抽象的结构体;
probe
:probe函数
remove
:remove函数
dai_link
:预定义描述platform和codec连接的结构体指针,实现两者绑定配对,匹配方式可以是名字或者设备树节点,可以预定义多个;
num_links
:预定义连接结构体的个数;
dai_link_list
:所有的link最终会添加到这里,内核自动操作;
num_dai_links
:添加进dai_link_list
的link的计数器;
rtd_list
:创建的snd_soc_pcm_runtime
结构体会挂载这个链表,该结构体在snd_soc_register_card()
->snd_soc_instantiate_card()
->soc_bind_dai_link
->soc_new_pcm_runtime()
中被创建,每个link都会创建一个;
num_rtd
:链表内snd_soc_pcm_runtime
个数计数器
controls
:控件;
num_controls
:控件的个数;
dapm_widgets
:widgets组件;
num_dapm_widgets
:widgets组件个数;
dapm_routes
:routes路线;
num_dapm_routes
:数目
of_dapm_widgets
:设备树中定义的widgets
num_of_dapm_widgets
:数目;
of_dapm_routes
:设备树中定义的routes;
num_of_dapm_routes
:数目
component_dev_list
:
widgets
:
paths
:
dapm_list
:
dapm_dirty
:
dapm
:
dapm_stats
:
update
:
simple-card: simple-card框架主要用于设置machine部分参数,需要在设备树中添加相应的sound {
compatible = “simple-audio-card”;…}设备节点;
head_list: 这里的head_list是指在sound/soc/soc-core.c源文件中的platform_list
、codec_list
、和component_list
结构体,在machine部分的snd_soc_card
通过snd_soc_register_card()
注册时会到这三个链表中找相应的设备;
如图所示:
component_list
:该链表挂着snd_soc_component
结构体,在platform_driver、platform_dai、codec_driver和codec_dai(其中codec 的dai 和 driver通过同一个函数一起注册的)注册的时候都会直接或间接的分配一个snd_soc_component
结构体成员(其中platform_driver注册时分配了snd_soc_platform
结构体里面包含snd_soc_component
成员,codec部分注册时分配了snd_soc_codec
结构体里面也包含snd_soc_component
成员,platform_dai注册时时直接申请的snd_soc_component
),然后将关键结构体(platform_driver、platform_dai、codec_driver和codec_daiplatform_dai、codec_driver和codec_dai)直接添加或以某种关系关联到snd_soc_component
中,并把snd_soc_component
注册到component_list
中;
platform_list
:链表上挂结构体是在snd_soc_register_platform()
中创建的snd_soc_platform
,通过该结构体可以找到platform_driver部分;
codec_list
:链表挂的结构体是在snd_soc_register_codec
创建的snd_soc_codec
;
注:
a 由于水平有限,文章中可能有部分每完善的,后续水平够了再作说明;
b 在machine部分的snd_soc_card
注册时(调用snd_soc_register_card()
),会根据名字或设备树节点到component_list
匹配相应的组件,通过匹配的组件再去找platform_dai、codec_dai、codec_driver和platform_driver;platform_driver的匹配过程中先到component_list
找到相应的组件,找到了再到platform_list
中去取到platform_driver;