DSP

高通msm8996平台的ASOC音频路径分析(基于androidN及linux3.1x)

2019-07-13 19:14发布

class="markdown_views prism-atom-one-light">

高通msm8996平台的ASOC音频路径分析(基于androidN及linux3.1x)

tags : msm8996 sound linux android

前言

关于为什么要记录音频链路
音频链路的链接是个什么东西,关于这一点,是一个从开始接触android音频的第一天就困扰的问题,几乎我所有对于音频框架的研究都是针对这个问题在进行,不知不觉中似乎把整个android、linux以及高通adsp的音频框架看了个遍,感觉整个音频框架其实也是围绕着这一点来展开的,所以基本上只有把音频框架的每一个细节都看清楚了才能真正理解音频链路是个什么东西,开始以为只用看一部分,但实际上几乎所有音频代码都跟音频链路息息相关,所以想弄清楚这一块的东西没有捷径可走,只能阅读文档、代码然后在真实环境下做实验。至于怎么算真正弄清音频框架了呢?我觉得标准其实也很简单,能够把音频框架中所有重要结构体的成员的意义以及来龙去脉将清楚就够了,但是这一点远比知道widgets怎么用,、codec driver怎么写或者dai link是什么这些东西要难得多的多。在学习音频框架的过程中经历了多次的困惑,又经历了多次的自以为懂了的过程,直到现在还是有些细节没有彻底的分析过,而每一次的起伏都是一次蜕变,正是在起起伏伏中才慢慢接近事实本质。本文就从音频链路切入记录一些学习过程中网上找不到相关文章(也许有但被埋没掉了)但觉得重要的东西。 这里记录一下asoc中音频路径相关内容,关于更基础的asoc内容在下面几篇文章中已经讲的很清楚了:
ALSA SoC Layer
Linux ALSA 音频系统:物理链路篇
Linux ALSA 音频系统:逻辑设备篇
droidphone的博客
关于dynamic PCM(DPCM)官方说明
上面几个链接里面基本上已经把asoc给说清楚了,但是针对的平台应该都不是高通平台,在高通平台上跟以上链接里所说的内容在部分地方上是有差异的,尤其是fe、be这些概念(这些概念不是高通平台特有的,是asoc中的)的引入导致很多地方跟网上绝大多数文章中所叙述的完全对不上,所以这里主要记录一下针对高通msm8996平台的asoc,其实目前高通其他平台也基本上都是这样。 本文所有分析全部基于msm8996平台+androidN实际执行过程 !!下面所有的内容都是默认对上述几篇文章已经了解了,所以这些文章中讲过的东西这里就不浪费时间记录了!!

0 ASOC音频子系统模型

这里借用linux自说明文档中的描述来说明,文档来自:documention/sound/alsa/soc/dpcm.txt
asoc音频子系统的模型如下图所示:
audio subsystem model.jpg-92.6kB 首先,这里有几个概念,fornt end、back end、platform、mechine、dai、pcm、audio device,分别是指的什么,刚接触音频的时候,被这几个概念搅了好久……这里暂时不对每个概念进行解释,它们的解释到处都可以找到,只是在刚开始接触的时候看了解释也不知道他在说什么……所以就干脆不解释了,后面用着用着有感觉了自然就明白每个概念的意义了。 对于msm8996平台,或者说目前所有的手机平台来说其硬件模型基本上都是这样的:
audio subsystem model s_h.jpg-180.4kB
这个图主要是描述asoc中音频链路中各个环节如何链接、以及每个环节操作的对应硬件。(所有软件均运行在cpu上,只是每个部分控制的硬件不同)
这个图主要是为了描述链接,所以这里面其实是淡化了两个概念:数据流的链接与电源管理(dapm)的链接,关于这两个后面会分开详细记录。当数据在cpu上时,是由cpu直接进行数据搬移,当数据到了adsp及codec后,数据是由adsp及codec进行搬移,但是cpu又得控制adsp、codec对数据搬移的行为,所以采用了dapm(widgets)的方式来对数据路径进行控制,但不亲自参与搬移,故在上图中只有msm-pcm-dsp是来真正做数据搬移的,是真实的数据链路链接,其余所有widgets相关的链路链接都是控制上的逻辑链接,至于底下的数据到底是不是真的按照dapm给定的方式在搬移,这个dapm不进行保证(但实际上必须要求真实数据流就是按照dapm给定的路径在流动,不然就说明程序出现了错误……)。

1 关于高通平台

高通msm8996平台是提供open dsp的,也就是第三方可以通过高通提供的sdk开发dsp上的程序。这里以高通820处理器(msm8996)为例,820处理器采用的是高通hexagon 6x系列的,ap与dsp之间采用share memory进行数据交换,ap将音频数据发送给dsp,dsp中按照配置(acdb中的配置)对音频数据进处理,并将最终处理后的音频数据送入codec或者其他audio设备,比如Bluetooth、modem等。dsp中音频数据处理链路是允许第三方增加新module的(音频数据处理单元),增加的新module可以通过.so的形式添加到dsp中也可以.lib的形式直接编译进dsp的kernel中(需要有高通dsp kernel编译环境)。无论哪种形式都需要配置正确的acdb文件,acdb中指定了该lib的device、topology、module、parameter以及sequence等。如果是.lib形式还需在编译的时候配置对应的.config文件,在该文件中添加相应的module信息,让dsp kernel可以识别该module,在dsp起来之后通过acdb找到该module并根据acdb的配置正确加载。关于dsp这块详细记录内容很多,留到单独的地方进行记录。 这里在简单提一下关于ap与dsp通信,这里是高通平台asoc跟网上其他人所写的asoc出入很大的一个地方。
其他地方的文章中paltform的作用是控制数据在处理器内部(无论是cpu还是dsp)搬移,即:把数据流从接收端口的缓冲区中搬移到发送端口的缓冲区中,并控制dai进行数据传输。按照系统框架的设计,在cpu里面,platform的体现就是当用户打开一个pcm设备并向pcm设备写入音频数据后,platform负责把数据从pcm设备驱动的接收缓冲区中搬移到cpu dai的发送缓冲区中,并通过cpu dai进行数据传输。
但是!!!高通平台好像并不是这样,事实上cpu dai(fe),除了维护了asoc框架的一致性以及充当了dapm上面的一个端点之外并没有其他实质性的作用。作为dai(数字音频接口)最核心的功能应该是提供数据传输接口的dirver,也就是dai driver的,但是这里他并没有提供数据传输能力,真实的数据传输能力(即cpu与dsp通信的能力)是通过smem、smd以及smmu相关driver共同完成的,由platform直接调用。
这样做的原因可能是因为cpu与dsp之间的通信不仅仅涉及到audio还涉及到很多地方,他们是一个公用的driver,不像i2s,一旦给audio用了他就一定是audio独占的接口,但cpu与dsp之间传输的远不止audio的内容,所以这里的dai就不再提供dai driver了,而是让platform自己去调用该平台下的cpu与dsp之间的通信接口。关于cpu与dsp之间的通信比较复杂,这里就不展开了,详细记录在另外的几篇笔记里。

2 音频数据流视角的音频链路

2.1 音频数据流工作过程

先贴一张图出来,这样更直观的可以看出来音频数据流的工作过程:
Screenshot from 2018-06-27 11-49-41.png-315.1kB
这张图是一个音频playback的trace。在androidN下,用网易云音乐播放一首歌,在linux侧,与音频数据流相关(与widgets相关的后面记录)的全部主要行为都记录在这张图中了。
总的来说,一个音频链路的启动分为5个部分: Created with Raphaël 2.1.2Startpcm openhw/sw parameterpreparetriggerwriteEnd 上面这张图先放在这里,是一个整体概念,后面再来一点一点的看。
先丢几个问题在这里,但这几个问题暂时不影响整个音频链路的分析。1、这里播放一首歌,他打开了两个音频链路,一个是pcm0一个是pcm15,pcm0是一个deepbuff,pcm15是一个low latency,为什么暂时不知道。2、ioctrl中0x1,0x3,0x23这几条cmd暂时不分析,跟音频链路关系不大。

2.1.1 pcm open

pcm open起源于逻辑pcm设备的open,也就是在linux userspace里面去fopen一个pcm设备,根据linux设备驱动框架,这里会调用注册设备时提供的open接口。这个接口在pcm_native.c文件中。 const struct file_operations snd_pcm_f_ops[2] = { { .owner = THIS_MODULE, .write = snd_pcm_write, .aio_write = snd_pcm_aio_write, .open = snd_pcm_playback_open, .release = snd_pcm_release, .llseek = no_llseek, .poll = snd_pcm_playback_poll, .unlocked_ioctl = snd_pcm_playback_ioctl, .compat_ioctl = snd_pcm_ioctl_compat, .mmap = snd_pcm_mmap, .fasync = snd_pcm_fasync, .get_unmapped_area = snd_pcm_get_unmapped_area, }, { .owner = THIS_MODULE, .read = snd_pcm_read, .aio_read = snd_pcm_aio_read, .open = snd_pcm_capture_open, .release = snd_pcm_release, .llseek = no_llseek, .poll = snd_pcm_capture_poll, .unlocked_ioctl = snd_pcm_capture_ioctl, .compat_ioctl = snd_pcm_ioctl_compat, .mmap = snd_pcm_mmap, .fasync = snd_pcm_fasync, .get_unmapped_area = snd_pcm_get_unmapped_area, } }; 至于为什么是这个ops,后面在创建音频数据流时说明,先默认就是这个。
数组长度为2,这里容易理解,0为playback,1为capture。这里以playback为例来说明。 static int snd_pcm_playback_open(struct inode *inode, struct file *file) { struct snd_pcm *pcm; int err = nonseekable_open(inode, file); if (err < 0) return err; pcm = snd_lookup_minor_data(iminor(inode), SNDRV_DEVICE_TYPE_PCM_PLAYBACK); err = snd_pcm_open(file, pcm, SNDRV_PCM_STREAM_PLAYBACK); if (pcm) snd_card_unref(pcm->card); return err; } 这里容易理解,先通过inode找到打开文件对应的pcm数据实体,这里怎么找到的,最开始的链接文章里面有写,就不复述了。
接着,根据图中打印信息可以看到open的函数调用关系:
snd_pcm_playback_open->snd_pcm_open->snd_pcm_open_file->snd_pcm_open_substream->(substream->ops->open())
所以,实质上是调用创建substream时注册的open函数,这里在溯源回去,这个函数在soc-pcm.c中: int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num) { …… /* ASoC PCM operations */ if (rtd->dai_link->dynamic) { rtd->ops.open = dpcm_fe_dai_open; rtd->ops.hw_params = dpcm_fe_dai_hw_params; rtd->ops.prepare = dpcm_fe_dai_prepare; rtd->ops.trigger = dpcm_fe_dai_trigger; rtd->ops.hw_free = dpcm_fe_dai_hw_free; rtd->ops.close = dpcm_fe_dai_close; rtd->ops.pointer = soc_pcm_pointer; rtd->ops.delay_blk = soc_pcm_delay_blk; rtd->ops.ioctl = soc_pcm_ioctl; rtd->ops.compat_ioctl = soc_pcm_compat_ioctl; } else { rtd->ops.open = soc_pcm_open; rtd->ops.hw_params = soc_pcm_hw_params; rtd->ops.prepare = soc_pcm_prepare; rtd->ops.trigger = soc_pcm_trigger; rtd->ops.hw_free = soc_pcm_hw_free; rtd->ops.close = soc_pcm_close; rtd->ops.pointer = soc_pcm_pointer; rtd->ops.delay_blk = soc_pcm_delay_blk; rtd->ops.ioctl = soc_pcm_ioctl; rtd->ops.compat_ioctl = soc_pcm_compat_ioctl; } if (platform->driver->ops) { rtd->ops.ack = platform->driver->ops->ack; rtd->ops.copy = platform->driver->ops->copy; rtd->ops.silence = platform->driver->ops->silence; rtd->ops.page = platform->driver->ops->page; rtd->ops.mmap = platform->driver->ops->mmap; rtd->ops.restart = platform->driver->ops->restart; } if (playback) snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops); if (capture) snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops); …… } 其中snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);这一句话就是把rtd的ops赋值给了substream的ops,就是前面调用的substream->ops->open()。至于为什么是这里,同样,开头链接的几篇文章中已经说的很清楚了。 这里出现了一个新的问题,那就是open函数到底是调用的dpcm_fe_dai_open还是soc_pcm_open?这个地方需要重点记录一下,因为到目前为止没有看到网上有关于这块的任何记录,几乎都是默认这里调用的是soc_pcm_open但事实是在存在dsp的平台上都不可能调用soc_pcm_open而必须调用dpcm_fe_dai_open,那么这个就涉及到dynamic pcm了,dynamic pcm和pcm在形式上可以说即是同一个东西但又是完全不同的东西,这里专门抽一个章节出来记录这一块,传送门–>关于Dynamic PCM的记录。总之,说直接一点,我觉得dpcm和dapm这两个东西没搞清楚,asoc都算还没入门,这两个东西是alsa soc相对于alsa最大区别。 那么根据最开始图中的打印,发现在open过程中,最终还是为了来打开跟这个stream所相关的所有dai、platform、dai link: static int soc_pcm_open(struct snd_pcm_substream *substream) { …… /* startup the audio subsystem */ if (cpu_dai->driver->ops && cpu_dai->driver->ops->startup) { ret = cpu_dai->driver->ops->startup(substream, cpu_dai); …… } if (platform->driver->ops && platform->driver->ops->open) { ret = platform->driver->ops->open(substream); …… } for (i = 0; i < rtd->num_codecs; i++) { codec_dai = rtd->codec_dais[i]; if (codec_dai->driver->ops && codec_dai->driver->ops->startup) { ret = codec_dai->driver->ops->startup(substream, codec_dai); …… } …… } if (rtd->dai_link->ops && rtd->dai_link->ops->startup) { ret = rtd->dai_link->ops->startup(substream); } …… } 这个函数其实很长,不仅仅是这些调用,其中还有GPIO管脚的配置、runtime hw parameter的配置等等,这里为了体现主体,所以简化了。
那么,记录到这里,其实主要原因就是想了解一下为什么要在open的时候分别调用这些接口。
  • cpu dai:根据msm8996平台来看,其fe的cpu dai driver:msm-dai-fe.c里面仅仅就是添加了一条SNDRV_PCM_HW_PARAM_RATErule,关于hw param及rule相关点击–>hw param及rule。be的cpu dai drivver里面的startup函数直接没实现。
  • codec dai:startup函数没实现。
  • dai link:startup函数没实现。
  • platform:简单来说就是设置了hw parameter并且添加了几条rule,同时调用q6asm_audio_client_alloc函数创建了一个audio_client。重点来了audio_client是个什么鬼……这还真是高通平台专享……这个东西要讲清楚极其复杂,所以在另外的blog中进行专门记录,这里只简单提一下,这个东西就是cpu与dsp通信的接口,包括了smem、smd、smmu、apr等等。
所以,从上面来看,总结一下open到底是想干什么,感觉就是针对该stream的硬件特性以及通信接口进行初始化,所以所有跟硬件有关的初始化都应该放到open中完成。

2.1.2 hw/sw parameter

设置当前stream需要使用的hw/sw 参数,关于hw parameter点击这里—>对就是这里

2.1.3 prepare

prepare是干嘛的,首先引用kernel里面的一段注释: /* * Called by ALSA when the PCM substream is prepared, can set format, sample * rate, etc. This function is non atomic and can be called multiple times, * it can refer to the runtime info. */ 简单来说就是当数据准备好之后,根据实际数据来设置一些音频流的参数,同样,在prepare被调用后函数内会分别调用所有的dai、platform、dai link自己的prepare函数,如果需要处理prepare的话各自就实现该函数。 不过在高通平台上似乎没有那么简单,首先,fe的platform在prepare的时候会操作msm_audio,向dsp设置相关的操作,be的platform在prepare的时候要打开一个adm,并对dsp内部音频链路上的处理过程进行配置,这块跟dsp结合很紧,里面东西也很多,下次在单独的文章中分析。fe的cpu dai以及codec dai都没有实现,be的cpu dai在prepare时会启动dsp上对应的afe port(其实就是dsp音频链路最后的处理单元,这个处理单元后面就是音频输出的物理接口了),be的codec dai设置了一下silm接口的宽度。 所以,可以认为prepare也是设置参数,只是跟hw params不同,这里的参数基本上都是围绕音频数据流来进行的,而hw params时的参数更多的是关心硬件特性。

2.1.4 trigger

这个没什么好说的,就是启动传输,trigger只在fe platform中实现了,因为在msm8996平台上也只有cpu需要启动对dsp的数据传输,而trigger中也并不是启动的dma传输,是高通自己cpu与dsp的通信接口,所以,其实在整个音频链路中并不是绝对的dma操作,dma控制器也并不是都在cpu上,所以网上其他文章写的也只是一个通用模型,针对不同平台区别其实比较大,比如高通平台,这里的数据搬移跟cpu关系并不到,应该是cpu与dsp通过smmu映射了一段内存,然后通过smd把内存地址通知对方,dsp再通过dma之类的操作去搬移数据(由于高通不开放dsp源码,根据linux和高通相关说明文档,暂时猜测是这样,即便不是这样我觉得也跟这差不远)。 这里有一点需要说明一下,trigger并不是只有在应用层发送SNDRV_PCM_IOCTL_START(0x42)命令后才会执行的,从前面那个启动打印的截图中可以看到0x42这条指令并没有发送,但是trigger依然执行了,其实这里的trigger是在write里面被执行的: static snd_pcm_sframes_t snd_pcm_lib_write1(struct snd_pcm_substream *substream, unsigned long data, snd_pcm_uframes_t size, int nonblock, transfer_f transfer) { …… if (runtime->status->state == SNDRV_PCM_STATE_PREPARED && snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) { err = snd_pcm_start(substream); if (err < 0) goto _end_unlock; } …… } 可以看到,在写数据的时候会先去判断runtime的状态,如果runtime还是prepared状态,且已经收到的数据超过了启动发送门限值了,则会调用trigger,来完成实际的数据搬移。所以上层应用往往会省略start操作,在prepare之后便直接开始写数据了。

2.1.5 write

wirte,顾名思义,就是上层应用把音频数据一点一点的写下来,这个就不做多的解释了,至于每次写的长度什么的,开头链接里面的文章已经说的很详细了。

2.2 音频数据流的创建

这里主要记录音频数据流为什么会被创建,或者说在什么情况下会创建一个pcm或者compress设备,这些设备创建时的配置在哪,这些配置有什么作用,了解了这些才能真正自己进行添加或者修改音频设备。
这里就通过pcm设备的创建来进行记录,compress设备跟pcm设备类似,control设备(不是control控件,这两个东西一定要区分,control设备实在linux下可以通过声卡访问到的一个linux设备,跟pcm一样;control控件是声卡的一个配置项,就像一个配置参数一样,一般来说是通过对control设备的ioctrl操作进行操作的)是在声卡创建时默认都会创建的,别人以及写的很清楚了。 那么如果想要创建一个pcm设备,该怎么做?那就是定义一个dai link!!!但是并不是所有dai link都会创建pcm设备,关于pcm的创建开头链接的文章中讲的很清楚了,但是在现代移动平台上基本上全是dynamic pcm了,关于DPCM的内容在后面有详细的分析,这里暂时跳过。

3 dapm视角的音频链路

dapm、widgets在开头链接中的文章已经说的很清楚了,下面就记录一下那些文章中没提或者简单说了一下但是又确实实际使用的东西。最后再用手头开发板开发板实际的widgets做一个示例。 我不知道这个东西的学名,但我更愿意叫他软件定义硬件,这是一种“面向对象”的硬件编程思想,对于复杂的硬件编程是极其有效的方式,linux中很多地方都有这种思想的体现,但在dapm中更是用到了极致。 在dapm中,对于框架软件看到的是一个个抽象过的widgets,框架软件可以自由组合、使用这些widgets,就行搭积木一样,每一个widgets都是一块积木,这些积木通过胶水——path去链接,这一切的一切都由软件来控制,框架软件并不用去关心这每个积木怎么工作,只用关心每个积木是什么形状,根据图纸(硬件链接关系)拼出各种各样的图案。对于widgets,它只用关心一个通用模型,关心他要操作的对象,至于什么时候操作,被放置在哪个位置,widgets完全不用去关心。

3.1 dai widgets之间如何链接

在分析这个问题前,先简单记录一下另外一个问题,用widgets链接dai干什么?
因为音频链路的通路是通过widgets的路径来描述的,而不同硬件设备间的链接是通过dai link来描述的,那么根据damp的理念,在音频链路上dai link也应该对应一个widgets,该widgets分别链接两边硬件的widgets,以实现垮硬件的widgets通路。 关于dai的链接,两个问题,1、dai widgets怎么创建的,2、dai widgets怎么链接的。

3.1.1 dai widgets的创建

dai widgets如果想写死,通过静态方式定义出来,我觉得是完全可以的,但是既然已经定义了dai link,那么就可以根据dai link来自动创建dai widgets了。关于dai link的创建在开头链接的文章中已经记录了,这里再提一下,就不详细说明。 dai widgets在声卡创建阶段会去调用soc_probe_link_components函数probe所有注册进来的component(关于component看一下register相关函数和component list就明白了),soc_probe_link_components中会对每个component调用snd_soc_dapm_new_dai_widgets。这里有一点提一下,就是在创建的widget中,其name和sname都是dai driver中的stream name,这是因为后面的链接时会去匹配这个名字,这里留到后面链接那里再记录。

3.1.2 dai widgets间的链接

在创建完dai widgets后会继续调用snd_soc_dapm_connect_dai_link_widgets函数进行dai widgets的链接: void snd_soc_dapm_connect_dai_link_widgets(struct snd_soc_card *card) { struct snd_soc_pcm_runtime *rtd = card->rtd; int i; /* for each BE DAI link... */ for (i = 0; i < card->num_rtd; i++) { rtd = &card->rtd[i]; /* * dynamic FE links have no fixed DAI mapping. * CODEC<->CODEC links have no direct connection. */ if (rtd->dai_link->dynamic || rtd->dai_link->params) continue; dapm_connect_dai_link_widgets(card, rtd); } } 可以看到,只有在静态且NULL != rtd->dai_link->params时(hostless的biao)才会链接,这里的链接是在创建声卡时的链接,也就是说这个时候链接关系建立就再也不会改变了。
那么剩下的,dynmic的链接和hostless的链接什么时候做呢,这块在后面有详细记录,这里简单说一下,dynamic的链接是在一个音频流被打开的时候进行的,可以根据打开的音频流不同建立不同的连接关系,而hostless的链接则是在soc_probe_link_dais函数中创建pcm设备时根据rtd->dai_link->params的值来判断的。

3.2 dai widgets如和与其他widgets链接

这一块依赖于两个地方,一个地方是通过stream name去匹配,另一个是靠aif去指定。
在声卡初始化的时候,snd_soc_instantiate_card中调用snd_soc_dapm_link_dai_widgets函数来完成的。snd_soc_dapm_link_dai_widgets函数会去遍历每一个dai widgets,然后遍历所有的非dai widgets,如果非dai widgets的stream name与dai widgets的name相同,则把两个widgets进行链接。这也是为什么创建dai widgets时name一定要是stream name的原因之一了。
aif的绑定则是在dai dirver被probe时完成的,同样在snd_soc_instantiate_card中会调用soc_probe_link_dais函数,该函数会把每个dai driver注册时提供的probe都调用一遍,如果配置了aif的话这里就应该把aif与dai widgets进行链接。这里其实也是依赖于dai widgets的name必须为stream name,由于在probe函数中,因此这个函数属于dai driver,不应该去获取widgets的信息,所以其实这里链接的两个widgets的name一个是aif的name,另一个是stream name,其一般代码如下: static int fe_dai_probe(struct snd_soc_dai *dai) { struct snd_soc_dapm_route intercon; struct snd_soc_dapm_context *dapm; if (!dai || !dai->driver) { pr_err("%s invalid params ", __func__); return -EINVAL; } dapm = snd_soc_component_get_dapm(dai->component); memset(&intercon, 0 , sizeof(intercon)); if (dai->driver->playback.stream_name && dai->driver->playback.aif_name) { dev_dbg(dai->dev, "%s add route for widget %s", __func__, dai->driver->playback.stream_name); intercon.source = dai->driver->playback.stream_name; intercon.sink = dai->driver->playback.aif_name; dev_dbg(dai->dev, "%s src %s sink %s ", __func__, intercon.source, intercon.sink); snd_soc_dapm_add_routes(dapm, &intercon, 1); } if (dai->driver->capture.stream_name && dai->driver->capture.aif_name) { dev_dbg(dai->dev, "%s add route for widget %s", __func__, dai->driver->capture.stream_name); intercon.sink = dai->driver->capture.stream_name; intercon.source = dai->driver->capture.aif_name; dev_dbg(dai->dev, "%s src %s sink %s ", __func__, intercon.source, intercon.sink); snd_soc_dapm_add_routes(dapm, &intercon, 1); } return 0; }

3.3 关于mix_path.xml及dts配置

关于xml是如何被解析,如何被加载以及如何被配置的,这些都属于android的内容,在另外的地方进行记录,这里只简单记录一下文件格式并且进行音频流路径分析。
dts文件: sound-9335 { …… qcom,audio-routing = "AIF4 VI", "MCLK", "RX_BIAS", "MCLK", "MADINPUT", "MCLK", "AMIC1", "MIC BIAS3", "MIC BIAS3", "Analog Mic1", "AMIC2", "MIC BIAS2", "MIC BIAS2", "Headset Mic", "AMIC3", "MIC BIAS3", "MIC BIAS3", "ANCRight Headset Mic", "AMIC4", "MIC BIAS3", "MIC BIAS3", "ANCLeft Headset Mic", "AMIC5", "MIC BIAS4", "MIC BIAS4", "Handset Mic", "AMIC6", "MIC BIAS4", "MIC BIAS4", "Analog Mic6", "DMIC0", "MIC BIAS1", "MIC BIAS1", "Digital Mic0", "DMIC1", "MIC BIAS1", "MIC BIAS1", "Digital Mic1", "DMIC2", "MIC BIAS3", "MIC BIAS3", "Digital Mic2", "DMIC3", "MIC BIAS3", "MIC BIAS3", "Digital Mic3", "DMIC4", "MIC BIAS4", "MIC BIAS4", "Digital Mic4", "DMIC5", "MIC BIAS4", "MIC BIAS4", "Digital Mic5", "SpkrLeft IN", "SPK1 OUT", "SpkrRight IN", "SPK2 OUT", "SpkrMono IN", "SpkrLeft SPKR"; …… }; sound-9335 { qcom,model = "msm8996-tasha-cdp-snd-card"; qcom,hdmi-audio-rx; asoc-codec = <&stub_codec>, <&hdmi_audio>; asoc-codec-names = "msm-stub-codec.1", "msm-hdmi-audio-codec-rx"; qcom,us-euro-gpios = <&pm8994_mpps 2 0>; qcom,wsa-max-devs = <3>; qcom,wsa-devs = <&wsa881x_211>, <&wsa881x_212>, <&sia8108_spk>, <&wsa881x_213>, <&wsa881x_214>; qcom,wsa-aux-dev-prefix = "SpkrLeft", "SpkrRight", "SpkrMono", "SpkrLeft", "SpkrRight"; }; sound-9335是分开定义的,所以有多个地方共同定义一个sound-9335设备,这里就是要注意prefix,因为xml以及dts的route里面都会是加了prefix的,而代码里面的widgets是没有加的。再就是这里的SpkrMono是我自己后来添的,就是在SpkrLeft SPKR后面再接了一个Mono,只是为了验证自己的driver能够通过dapm进行上下电的管理。 xml文件: file name : mixer_paths_tasha.xml <ctl name="SLIM RX0 MUX" value="ZERO" /> <path name="deep-buffer-playback"> <ctl name="SLIMBUS_0_RX Audio Mixer MultiMedia1" value="1" /> path> <path name="speaker"> <ctl name="SLIM RX0 MUX" value="AIF_MIX1_PB" /> <ctl name="SLIM RX1 MUX" value="AIF_MIX1_PB" /> <ctl name="SLIM_0_RX Channels" value="Two" /> <ctl name="RX INT7_1 MIX1 INP0" value="RX0" /> <ctl name="RX INT8_1 MIX1 INP0" value="RX1" /> <ctl name="SpkrLeft COMP Switch" value="1" /> <ctl name="SpkrRight COMP Switch" value="1" /> <ctl name="SpkrLeft BOOST Switch" value="1" /> <ctl name="SpkrRight BOOST Switch" value="1" /> <ctl name="SpkrLeft VISENSE Switch" value="1" /> <ctl name="SpkrRight VISENSE Switch" value="1" /> <ctl name="SpkrLeft SWR DAC_Port Switch" value="1" /> <ctl name="SpkrRight SWR DAC_Port Switch" value="1" /> path> 这是一个speaker stereo链路的配置,path就是android播放的时候会去选择的,然后配置下来进行链路激活,这里的ctl与widgets是一一对应的,value就是设置给widgets的参数。在播放的时候”deep-buffer-playback” path是对adsp的配置,”speaker” path是对codec以及pa的配置。 那么在我手上的这个msm8996平台的stereo speaker链路的widgets路径就是: CPU : "MultiMedia1 Playback"--> DSP : "MM_DL1"-->"SLIMBUS_0_RX Audio Mixer("MultiMedia1")"-->"SLIMBUS_0_RX"--> "Slimbus Playback(dai)"--> CODEC : "AIF Mix Playback(dai)"-->"AIF MIX1 PB"-->"SLIM RX0 MUX("AIF_MIX1_PB")"--> "SLIM RX0"-->"RX INT7_1 MIX1 INP0"("RX0")-->"RX INT7_1 MIX1"--> "RX INT7 SPLINE MIX"-->"RX INT7 SEC MIX"-->"RX INT7 MIX2"-->"RX INT7 INTERP"--> "RX INT7 CHAIN"-->"SPK1 OUT"--> PA : "SpkrLeft IN"-->"SpkrLeft SWR DAC_Port Switch"-->"SpkrLeft RDAC"--> "SpkrLeft SPKR PGA"-->"SpkrLeft SPKR" 上面只写了left的,right是一样的,然后pa中的widgets的name是已经加上了prefix的,而pa driver中route定义的是没有加prefix的,这里有坑,需注意。 "MultiMedia1 Playback"-->"MM_DL1"是通过fe的cpu dai driver在probe时创建的一个dai-->aif的链接,为什么?因为fe的cpu dai链接的是一个dummy codec,是需要dynamic绑定的,所以没办法通过dai link进行链接,所以这里只能通过aif的方式,在cpu dai这边指定cpu dai的输出接口,在be的paltfom driver中把此aif作为输入,进行route。 "SLIMBUS_0_RX"-->"Slimbus Playback(dai)"链接则是通过be的cpu dai driver在probe时创建的一个aif-->dai的链接。 "Slimbus Playback(dai)"-->"AIF Mix Playback(dai)"则是通过dai link进行链接的,因为这里的两个dai是静态的link,在创建声卡时就链接好了的。 "SPK1 OUT"-->"SpkrLeft IN"的链接是在dts文件描述声卡时定义的。 其余的链接都是各dirver中的route,加上xml对mix或mux的配置进行的链接。 至此,已经可以通过dts、xml以及driver代码静态分析出在android上层应用打开一个音频流后数据流的路径到底是怎么样的了。

3.4 widgets与control的关系

widgets与control的关系……前面几篇链接里面已经说了不少了,但是我现在突然觉得这两个东西其实没什么关系……硬要说关系的话就是在mixer、switch和mux类型的widgets里面借用了一下control的内容……我觉得强调widgets是基于control的这一点不一定准确
1、这两套东西的目的不同
widgets的目的是管理设备的上电下电,control是对设备的配置,比如滤波器、gain等。
2、可以独立控制硬件的能力
除了mix、mux和switch类型的widgets外,其余的widgets对硬件的控制根本就不依赖于control……这一点从dapm_power_widgets里面就可以看出来,该函数最终调用soc_dapm_update_bits函数进行硬件配置,但是这里的配置跟control无关,而且跟control的put操作是调用的同一个函数,所以说widgets其实根本可以不依赖于control。
3、mix、mux、switch为什么用control
mix、mux类型的widgets其实就是把mix、mux类型的control封装了一层,这