Linux ALSA声卡驱动之八:ASoC架构中的Platform

1. platform驱动在asoc中的作用
前面几章内容已经说过,asoc被分为machine,platform和codec三大部件,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进行交互。
/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/
2. snd_soc_platform_driver的注册
通常,asoc把snd_soc_platform_driver注册为一个系统的platform_driver,不要被这两个相像的术语所迷惑,前者只是针对asoc子系统的,后者是来自linux的设备驱动模型。我们要做的就是:
定义一个snd_soc_platform_driver结构的实例;
在platform_driver的probe回调中利用asoc的api:snd_soc_register_platform()注册上面定义的实例;
实现snd_soc_platform_driver中的各个回调函数;
以kernel3.3中的/sound/soc/samsung/dma.c为例:
[cpp]view plaincopy
staticstructsnd_soc_platform_driversamsung_asoc_platform={
.ops=&dma_ops,
.pcm_new=dma_new,
.pcm_free=dma_free_dma_buffers,
};
staticint__devinitsamsung_asoc_platform_probe(structplatform_device*pdev)
{
returnsnd_soc_register_platform(&pdev->dev,&samsung_asoc_platform);
}
staticint__devexitsamsung_asoc_platform_remove(structplatform_device*pdev)
{
snd_soc_unregister_platform(&pdev->dev);
return0;
}
staticstructplatform_driverasoc_dma_driver={
.driver={
.name=samsung-audio,
.owner=this_module,
},
.probe=samsung_asoc_platform_probe,
.remove=__devexit_p(samsung_asoc_platform_remove),
};
module_platform_driver(asoc_dma_driver);
snd_soc_register_platform()该函数用于注册一个snd_soc_platform,只有注册以后,它才可以被machine驱动使用。它的代码已经清晰地表达了它的实现过程:
为snd_soc_platform实例申请内存;
从platform_device中获得它的名字,用于machine驱动的匹配工作;
初始化snd_soc_platform的字段;
把snd_soc_platform实例连接到全局链表platform_list中;
调用snd_soc_instantiate_cards,触发声卡的machine、platform、codec、dai等的匹配工作;
3. 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字段中的回调函数;
snd_soc_register_dai 这个函数在上一篇介绍codec驱动的博文中已有介绍,请参考:linux alsa声卡驱动之七:asoc架构中的codec。
snd_soc_dai 该结构在snd_soc_register_dai函数中通过动态内存申请获得, 简要介绍一下几个重要字段:
driver 指向关联的snd_soc_dai_driver结构,由注册时通过参数传入;
playback_dma_data 用于保存该dai播放stream的dma信息,例如dma的目标地址,dma传送单元大小和通道号等;
capture_dma_data 同上,用于录音stream;
platform 指向关联的snd_soc_platform结构;
snd_soc_dai_driver 该结构需要自己根据不同的soc芯片进行定义,关键字段介绍如下:
probe、remove 回调函数,分别在声卡加载和卸载时被调用;
suspend、resume 电源管理回调函数;
ops 指向snd_soc_dai_ops结构,用于配置和控制该dai;
playback snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码率,数据格式等能力;
capture snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码率,数据格式等能力;
4. snd_soc_dai_driver中的ops字段
ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集合,dai的配置和控制几乎都是通过这些回调函数来实现的,这些回调函数基本可以分为3大类,驱动程序可以根据实际情况实现其中的一部分:
工作时钟配置函数 通常由machine驱动调用:
set_sysclk 设置dai的主时钟;
set_pll 设置pll参数;
set_clkdiv 设置分频系数;
dai的格式配置函数 通常由machine驱动调用:
set_fmt 设置dai的格式;
set_tdm_slot 如果dai支持时分复用,用于设置时分复用的slot;
set_channel_map 声道的时分复用映射设置;
set_tristate 设置dai引脚的状态,当与其他dai并联使用同一引脚时需要使用该回调;
标准的snd_soc_ops回调 通常由soc-core在进行pcm操作时调用:
startup
shutdown
hw_params
hw_free
prepare
trigger
抗pop,pop声 由soc-core调用:
digital_mute
以下这些api通常被machine驱动使用,machine驱动在他的snd_pcm_ops字段中的hw_params回调中使用这些api:
snd_soc_dai_set_fmt() 实际上会调用snd_soc_dai_ops或者codec driver中的set_fmt回调;
snd_soc_dai_set_pll() 实际上会调用snd_soc_dai_ops或者codec driver中的set_pll回调;
snd_soc_dai_set_sysclk() 实际上会调用snd_soc_dai_ops或者codec driver中的set_sysclk回调;
snd_soc_dai_set_clkdiv() 实际上会调用snd_soc_dai_ops或者codec driver中的set_clkdiv回调;
snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)的第二个参数fmt在这里特别说一下,asoc目前只是用了它的低16位,并且为它专门定义了一些宏来方便我们使用:
bit 0-3 用于设置接口的格式:
[cpp]view plaincopy
#definesnd_soc_daifmt_i2s1/*i2smode*/
#definesnd_soc_daifmt_right_j2/*rightjustifiedmode*/
#definesnd_soc_daifmt_left_j3/*leftjustifiedmode*/
#definesnd_soc_daifmt_dsp_a4/*ldatamsbafterfrmlrc*/
#definesnd_soc_daifmt_dsp_b5/*ldatamsbduringfrmlrc*/
#definesnd_soc_daifmt_ac976/*ac97*/
#definesnd_soc_daifmt_pdm7/*pulsedensitymodulation*/
bit 4-7 用于设置接口时钟的开关特性:
[cpp]view plaincopy
#definesnd_soc_daifmt_cont(1<<4)/*continuousclock*/
#definesnd_soc_daifmt_gated(2<<4)/*clockisgated*/
bit 8-11 用于设置接口时钟的相位:
[cpp]view plaincopy
#definesnd_soc_daifmt_nb_nf(1<<8)/*normalbitclock+frame*/
#definesnd_soc_daifmt_nb_if(2<<8)/*normalbclk+invfrm*/
#definesnd_soc_daifmt_ib_nf(3<<8)/*invertbclk+norfrm*/
#definesnd_soc_daifmt_ib_if(4<<8)/*invertbclk+frm*/
bit 12-15 用于设置接口主从格式:
[cpp]view plaincopy
#definesnd_soc_daifmt_cbm_cfm(1<<12)/*codecclk&frmmaster*/
#definesnd_soc_daifmt_cbs_cfm(2<<12)/*codecclkslave&frmmaster*/
#definesnd_soc_daifmt_cbm_cfs(3<<12)/*codecclkmaster&frameslave*/
#definesnd_soc_daifmt_cbs_cfs(4 }
pcm_new字段指向了dma_new函数,dma_new函数进一步为playback和capture分别调用preallocate_dma_buffer函数,我们看看preallocate_dma_buffer函数的实现:
[cpp]view plaincopy
staticintpreallocate_dma_buffer(structsnd_pcm*pcm,intstream)
{
structsnd_pcm_substream*substream=pcm->streams[stream].substream;
structsnd_dma_buffer*buf=&substream->dma_buffer;
size_tsize=dma_hardware.buffer_bytes_max;
pr_debug(entered%s\n,__func__);
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);
if(!buf->area)
return-enomem;
buf->bytes=size;
return0;
}
该函数先是获得事先定义好的buffer大小,然后通过dma_alloc_weitecombine函数分配dma内存,然后完成substream->dma_buffer的初始化赋值工作。上述的pcm_new回调会在声卡的建立阶段被调用,调用的详细的过程请参考linux alsas声卡驱动之六:asoc架构中的machine中的图3.1。
在声卡的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的配置和操作。
6.2 dma buffer管理
播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform的dma操作则不停地从该buffer中取出数据,经dai送往codec中。录音时则正好相反,codec源源不断地把a/d转换好的音频数据经过dai送入dma buffer中,而应用程序则不断地从该buffer中读走音频数据。
图6.2.1 环形缓冲区
环形缓冲区正好适合用于这种情景的buffer管理,理想情况下,大小为count的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他回到起始位置。在实际应用中,我们通常都会把这个大小为count的缓冲区虚拟成一个大小为n*count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver也使用了该方法对dma buffer进行管理:
图6.2.2 alsa driver缓冲区管理
snd_pcm_runtime结构中,使用了四个相关的字段来完成这个逻辑缓冲区的管理:
snd_pcm_runtime.hw_ptr_base 环形缓冲区每一圈的基地址,当读写指针越过一圈后,它按buffer size进行移动;
snd_pcm_runtime.status->hw_ptr 硬件逻辑位置,播放时相当于读指针,录音时相当于写指针;
snd_pcm_runtime.control->appl_ptr 应用逻辑位置,播放时相当于写指针,录音时相当于读指针;
snd_pcm_runtime.boundary 扩展后的逻辑缓冲区大小,通常是(2^n)*size;
通过这几个字段,我们可以很容易地获得缓冲区的有效数据,剩余空间等信息,也可以很容易地把当前逻辑位置映射回真实的dma buffer中。例如,获得播放缓冲区的空闲空间:
[csharp]view plaincopy
staticinlinesnd_pcm_uframes_tsnd_pcm_playback_avail(structsnd_pcm_runtime*runtime)
{
snd_pcm_sframes_tavail=runtime->status->hw_ptr+runtime->buffer_size-runtime->control->appl_ptr;
if(availboundary;
elseif((snd_pcm_uframes_t)avail>=runtime->boundary)
avail-=runtime->boundary;
returnavail;
}
要想映射到真正的缓冲区位置,只要减去runtime->hw_ptr_base即可。下面的api用于更新这几个指针的当前位置:
[cpp]view plaincopy
intsnd_pcm_update_hw_ptr(structsnd_pcm_substream*substream)
所以要想通过snd_pcm_playback_avail等函数获得正确的信息前,应该先要调用这个api更新指针位置。
以播放(playback)为例,我现在知道至少有3个途径可以完成对dma buffer的写入:
应用程序调用alsa-lib的snd_pcm_writei、snd_pcm_writen函数;
应用程序使用ioctl:sndrv_pcm_ioctl_writei_frames或sndrv_pcm_ioctl_writen_frames;
应用程序使用alsa-lib的snd_pcm_mmap_begin/snd_pcm_mmap_commit;
以上几种方式最终把数据写入dma buffer中,然后修改runtime->control->appl_ptr的值。
播放过程中,通常会配置成每一个period size生成一个dma中断,中断处理函数最重要的任务就是:
更新dma的硬件的当前位置,该数值通常保存在runtime->private_data中;
调用snd_pcm_period_elapsed函数,该函数会进一步调用snd_pcm_update_hw_ptr0函数更新上述所说的4个缓冲区管理字段,然后唤醒相应的等待进程;
[cpp]view plaincopy
font-family:arial,verdana,sans-serif;>white-space:normal;>
codeclass=cpp>voidsnd_pcm_period_elapsed(structsnd_pcm_substream*substream)
{
structsnd_pcm_runtime*runtime;
unsignedlongflags;
if(pcm_runtime_check(substream))
return;
runtime=substream->runtime;
if(runtime->transfer_ack_begin)
runtime->transfer_ack_begin(substream);
snd_pcm_stream_lock_irqsave(substream,flags);
if(!snd_pcm_running(substream)||
snd_pcm_update_hw_ptr0(substream,1)timer_running)
snd_timer_interrupt(substream->timer,1);
_end:
snd_pcm_stream_unlock_irqrestore(substream,flags);
if(runtime->transfer_ack_end)
runtime->transfer_ack_end(substream);
kill_fasync(&runtime->fasync,sigio,poll_in);
}
如果设置了transfer_ack_begin和transfer_ack_end回调,snd_pcm_period_elapsed还会调用这两个回调函数。
7. 图说代码
最后,反正图也画了,好与不好都传上来供参考一下,以下这张图表达了 asoc中platform驱动的几个重要数据结构之间的关系:
图7.1 asoc platform驱动
一堆的private_data,很重要但也很容易搞混,下面的图不知对大家有没有帮助:
图7.2 private_data

备受关注的“南京集成电路大学”究竟是什么来头?
多源感知安防建设方案应用分析
如果相同价格,华为4G手机和iQOO Pro 5G性能旗舰机,你会选谁?
视频接收机的高质量安全解决方案
2018年中国X86服务器市场历史性高增长
Linux ALSA声卡驱动之八:ASoC架构中的Platform
复杂电路简化的基本原则和经典例题
低压断路器由什么组成
鸿海首次公布9名经营委员会成员,未来将走向群体决策共治时代
高通推出下一代 GPU 架构与ISP,带来极致图形及移动拍摄体验
TD-SCDMA入选信息产业部"十一五"
国家政策带来的福音,新能源汽车发展再提速
中兴2015年前冲刺全球通讯设备前三
量子技术的优势
基于热敏电阻的温度检测系统—第2部分:系统优化与评估
高通为什么积极游说美国政府松绑芯片禁令?
电感线圈
腾讯大动作!收购黑鲨手机,曲线抢下元宇宙入口!
爬虫技术为什么变成了害虫?爬虫技术到底犯了什么错?
华为SVC解决方案有哪些优势?