高通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.2 Start pcm open hw/sw parameter prepare trigger write End

上面这张图先放在这里,是一个整体概念,后面再来一点一点的看。

先丢几个问题在这里,但这几个问题暂时不影响整个音频链路的分析。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\n", __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\n",
               __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\n",
               __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封装了一层,这里为什么借用了control,这里应该是control逻辑设备的原因。在声卡创建的时候会创建一个control逻辑设备,跟pcm设备一样,上层应用可以对其进行读写,但是对于control设备来说只能操作control控件,所以,为了达到能让上层应用动态配置音频路径的目的,就必须把mixer、mux、switch这三类路径相关的widgets暴露给上层,使其能够配置,而widgets又不是control逻辑设备的操作对象,所以这里必须有mixer、mux及switch对应的control控件才能完成这项任务,于是就把创建control控件的事情放到了snd_soc_dapm_new_widgets函数中来进行,而该函数是在初始化声卡时会自动调用的。

最后再来记录一点:
声卡初始化时snd_soc_instantiate_card中调用snd_soc_dapm_new_widgets(card)函数到底是为什么。因为在之前记录过,其实在调用这个函数之前该创建的widgets、path都已经创建了,这里为什么还要调用snd_soc_dapm_new_widgets(card)函数。
snd_soc_dapm_new_widgets(card)函数的作用是为mixer,mux和switch类型的widgets创建对应的control,至于为什么要创建,上面已经分析了。那么control被修改后是怎么对widgets造成影响的,这里其实是在control的put和get两个操作里面,详见:soc_dapm_mixer_update_powersoc_dapm_mux_update_power两个函数,这两个函数都是最终在control的put和get里面被调用的。上面两个函数会修改widgets的链接关系,并且调用dapm_power_widgets来刷新widgets的链接状态,实现链路的切换。
在创建mixer的control时还会为每一个control创建一个widgets,这个widgets的作用到底是什么,由于手头的开发板没有使用这个东西,所以具体作用还不清楚,但从代码层面来看感觉是在当control没有真实的reg时会把参数存到这个widgets中?这里后面有机会再来研究一下。

在创建path时,会要求mixer的path name必须为kcontrol的name,见:dapm_connect_mixer函数的path->name = dest->kcontrol_news[i].name;这句话。其原因是在为mixer类型的widgets创建control时会调用dapm_new_mixer函数,里面会根据path name和control name进行匹配。

dapm_new_muxdapm_new_mixer时会调用dapm_kcontrol_add_path函数,里面会list_add_tail(&path->list_kcontrol, &data->paths);有这个操作,这是因为在control控件的put被调用后会调用soc_dapm_mux_update_powersoc_dapm_mixer_update_power来对widgets的链接关系进行更新,在这两个函数中都是通过遍历kcontrol的path来找到对应的path的:dapm_kcontrol_for_each_path(path, kcontrol) {}。这里kconrol的path就是在dapm_new_muxdapm_new_mixer里那个list_add_tail添加的。

4 关于Dynamic PCM

关于DCPM(Dynamic PCM)这里就记录几个问题:DPCM是干什么的,为什么需要DPCM?DPCM的机制是怎样的?

4.1 DPCM是干什么的,为什么需要DPCM

首先盗两个图,都是来自linux官方文档:

 ---------          ---------
|         |  dai   |         |
    CPU    ------->    codec
|         |        |         |
 ---------          ---------
 | Front End PCMs    |  SoC DSP  | Back End DAIs | Audio devices |

                    *************
PCM0 <------------> *           * <----DAI0-----> Codec Headset
                    *           *
PCM1 <------------> *           * <----DAI1-----> Codec Speakers
                    *   DSP     *
PCM2 <------------> *           * <----DAI2-----> MODEM
                    *           *
PCM3 <------------> *           * <----DAI3-----> BT
                    *           *
                    *           * <----DAI4-----> DMIC
                    *           *
                    *           * <----DAI5-----> FM
                    *************

第一个图是一般的音频链接方式,第二个图是带有dsp的音频链接方式。

所谓DPCM就是动态的PCM,既然是动态那它就要动,这里就又有两个问题:1、为什么要动,不动行不行;2、怎么动?
这一小结分析第一个问题,第二个问题下一小节分析。

首先要明确的就是这里的pcm是指的啥,这里的pcm我觉得就是指的pcm逻辑设备,也就是一个供上层去open的pcm设备。对于没有dsp的系统来说,不存在什么fe和be这种概念,就像第一个图一样,一个dai link就对应一个逻辑设备(不一定全是pcm设备,也可能是其他类型设备),因为dai link是链接cpu dai和codec dai或者其他设备dai的东西,而且这个关系是在硬件制作时已经做死了的,同时每个链接也是绝对有物理意义的(不会出现dummy之类的事),所以在mechine driver中把dai link写死就好了,每次open pcm设备时打开的就是相应的dai link。

然而出现了dsp之后,这一切都发生了变化……可以看到第二幅图,所有的pcm设备都链接到dsp上了,所有的音频外设也都链到dsp上了,所以一个cpu的dai究竟是链接到了哪个codec dai或者其他设备的dai这件事现在就说不准了,因为所有的物理线路全部都接到了dsp上,而dsp内部如何去路由这些数据全是软件说了算,所以这时mechine driver就傻了,说好的由我来描述板卡硬件信息的,结果描述不清楚了,因为硬件工程师也说不清楚cpu出来的数据被丢到哪个音频外设上去了……然而软件工程师也说不清楚这件事,因为软件工程师不知道最终的使用者他想怎么用,最终用户是想把pcm0的数据输出到speaker呢还是输出到headset这件事只有最终用户知道(这里的最终用户不是指的抱着手机刷网页的用户,而是指的手机生产商,比如某米,比如蓝绿两厂这些用户)。那么这个时候软件工程师就想了个办法,把dsp对音频数据的路由做成可配置的,你想把pcm0接到哪里就接到哪里,改改配置文件就行了,于是就有了DPCM……

所以dpcm是干什么?就是为了在数据输入流与数据输出流中间增加一个switch,允许开发者更自由的去控制音频链路,而不是像以前那样,电路板做好了音频链路也就唯一确定了,如果想改一下链路得重新画板子……当然这也是因为出现了dsp之后为适配dsp而做的必要的修改。

在使用了DPCM后音频链路可以支持hostless模式了,这个也是现代手机中必备的,除非哪个厂家跟用户的电池过不去才会放弃这个功能……关于hostless在后面会稍微记录一下。

4.2 DPCM的工作机制

说DPCM的话,就先从dai link说起,dai link中有两个元素dynamicno_pcm

  • 先说no_pcm
    这个成员好理解,就是说这个dai link不创建对应的pcm逻辑设备。为什么有这个需求?我觉得这就跟DPCM的机制有关,因为在DPCM中原来的音频输入流和输出流中间增加了一个switch,所以就被切断了,那么就把本来应该是CPU <----> CODEC这样的链接变成了CPU <----> SWITCH <----> CODEC这样的链接,但是又为了维护框架的一致性,所以每个链接中的<---->还是采取dai link的模式进行链接,这样原本的1个dai link就变成了2个dai link,一个dai link叫做fe dai link,一个dai link叫做be dai link。但是这里会有一个问题,本来一个dai link对应的就是一个音频逻辑设备,CPU <----> CODEC这样一个链接会自动创建出一个pcm逻辑设备,而现在变成了2个dai link,但是实际的音频流并没有发生变化,那如果还按照之前的方式则将创建出2个pcm设备,这就乱了……我的音频流到底该用哪个pcm设备输入呢?所以为了避免这样的现象,几乎所有的be dai link中的no_pcm全部都是1,也就是be不创建逻辑设备。其实我觉得这也是为什么区分be与fe的原因了,fe其实就是音频输入,所以fe都会创建对应的逻辑设备。这里提前提一句,关于为什么会有dpcm_fe_dai_open()soc_pcm_open()的选择,其实跟be设备没有pcm逻辑设备是有直接原因的。

  • 在来说dynamic
    dynamic成员对整个音频框架在逻辑上真正起作用的地方其实只有两个(还有几个地方不太影响整体逻辑):一个是soc-dapm.c中的snd_soc_dapm_connect_dai_link_widgets函数,另一个是soc-pcm.c中的soc_new_pcm函数。
    第一个地方的作用是区别dynamic dai link和非dynamic dai link在创建声卡时dai widgets的链接行为,对于非dynamic dai link在创建声卡时就把dai widgets链接好,而dynamic dai link是在该dai link对应的pcm逻辑设备被打开的时候才会去进行dai widgets的链接。其实这里就是dynamic和非dynamic最核心的区别的体现。
    第二个地方是对整个dynamic和非dynamic最核心的区别的实现。其实soc_new_pcm函数最关键的就是绑定rtd->ops了,如果是dynamic的话就绑定dynamic的ops,如果是非dynamic的,则绑定普通的ops。

下面就针对dynamic的ops进行一下分析,因为这里就是dynamic dai link的实现机制,这里简单分析一下几个关键函数。

4.2.1 open

dynamic pcm设备的open函数为dpcm_fe_dai_open

static int dpcm_fe_dai_open(struct snd_pcm_substream *fe_substream)
{
    ……
    /* 找到当前所有已经激活的链路(widgets) */
    ret = dpcm_path_get(fe, stream, &list);
    ……
    /* 在所有已经激活的链路中找到所有的be dai,并把be dai与此fe dai链接 */
    /* calculate valid and active FE <-> BE dpcms */
    dpcm_process_paths(fe, stream, &list, 1);
    /* 真正打开fe dai */
    ret = dpcm_fe_dai_startup(fe_substream);
    ……
}

这个函数理解起来比较简单,就是要注意一点,这个其实前面在分析no_pcm的时候已经说了,只有fe dai link才会创建pcm逻辑设备,也只有创建了pcm逻辑设备的dai link(对应到这里应该是stream)才会被调用rtd->ops->open(),所以这里的dpcm_process_paths(fe, stream, &list, 1);这句话必然就是把当前的fe和所有已经激活的be链接,be可能可以同时存在多个。

再来看dpcm_fe_dai_startup函数:

static int dpcm_fe_dai_startup(struct snd_pcm_substream *fe_substream)
{
    ……
    /* fe do trigger,其实没有执行,放在这里具体作用不清楚 */
    dpcm_set_fe_update_state(fe, stream, SND_SOC_DPCM_UPDATE_FE);
    ……
    /* 实际open be,其实里面还是调用的soc_pcm_open() */
    ret = dpcm_be_dai_startup(fe, fe_substream->stream);
    ……
    /* 还是靠soc_pcm_open来完成open操作 */
    /* start the DAI frontend */
    ret = soc_pcm_open(fe_substream);
    ……
    /* 设置状态 */
    fe->dpcm[stream].state = SND_SOC_DPCM_STATE_OPEN;
    /* 设置dia driver中的参数到stream中 */
    dpcm_set_fe_runtime(fe_substream);
    /* 约束一下当前的rates的参数 */
    snd_pcm_limit_hw_rates(runtime);

    /* fe do trigger,其实没有执行,放在这里具体作用不清楚 */
    dpcm_set_fe_update_state(fe, stream, SND_SOC_DPCM_UPDATE_NO);
    ……
}

所以可以看到,dynamic pcm里面其实就是先通过当前widgets的链接关系找到所有已经激活的be dai,然后把这个fe dai和所有激活的be dai进行链接,并且对每个be调用soc_pcm_open函数,最后对fe自己也调用soc_pcm_open函数,完成pcm的实际open操作。关于pcm open相关的东西前面已经分析过了,这里就不在对soc_pcm_open函数进行分析。

这里就又有一个地方可以理解了,从框架上来说,每个pcm设备都需要open之后才能使用,而由于be没有创建pcm逻辑设备,所以be的open操作得由fe来间接执行,以保证框架的一致性。

4.2.2 hw_params

同样,fe调用dpcm_fe_dai_hw_params函数进行hw params的配置,最终分别对fe和be调用soc_pcm_hw_params函数完成实际的hw_params操作。这里需要提一下的就是be的hw_params参数其实是拷贝的fe的,这一点在函数dpcm_be_dai_hw_params中可以看到:

        /* copy params for each dpcm */
        memcpy(&dpcm->hw_params, &fe->dpcm[stream].hw_params,
                sizeof(struct snd_pcm_hw_params));

这也是后面提到的,为什么还需要fixup来进行hw parameter的设置。

4.2.3 prepare

没什么说的,还是分别对fe和be调用的soc_pcm_prepare函数。

4.2.4 trigger

也一样,分别对fe和be调用的soc_pcm_trigger函数。只是这里有一个地方要提一下,因为这里跟dai link中间的一个元素是可以对应上的。
dpcm_fe_dai_trigger中调用了dpcm_fe_dai_do_trigger,该函数最终调用soc_pcm_trigger来完成实际的trigger。但是在dai link中有一个元素:trigger会对这里造成影响,这里不好解释,代码贴出来一看就明白:

static int dpcm_fe_dai_do_trigger(struct snd_pcm_substream *substream, int cmd)
{
    ……
    switch (trigger) {
    case SND_SOC_DPCM_TRIGGER_PRE:
        /* call trigger on the frontend before the backend. */
        ret = soc_pcm_trigger(substream, cmd);
        ……
        ret = dpcm_be_dai_trigger(fe, substream->stream, cmd);
        ……
    case SND_SOC_DPCM_TRIGGER_POST:
        /* call trigger on the frontend after the backend. */
        ret = dpcm_be_dai_trigger(fe, substream->stream, cmd);
        ……
        ret = soc_pcm_trigger(substream, cmd);
        ……
    case SND_SOC_DPCM_TRIGGER_BESPOKE:
        /* bespoke trigger() - handles both FE and BEs */
        ……
        ret = soc_pcm_bespoke_trigger(substream, cmd);
        ……
    default:
        ……
    }
    ……
}

4.3 关于hostless

开始以为是自己的打开方式不对,高通用了什么奇技淫巧来实现打电话的hostless播放,后来用了一上午的时间发现其实是我手上的开发板压根就没有这一块……所以根本就不存在什么打电话这一说,不存在的,打不了,别想了,包括跟通话相关的音频这一块,全没有……所以这一节的记录没办法用实际的运行结果来说明问题,暂时只记录一下对这一块的认识,过几天看看MTK平台的开发板上有没有这块内容,如果有的话抽时间补上。

这一块简单来说就是CODEC <----> CODEC的link过程,那么这个特殊的link怎么实现呢,其实是依赖与dai link中的一个元素:params。简单来说就是一旦这个成员不为NULL,则asoc就认为这一个dai link是一个CODEC <----> CODEC的dai link,那么就特殊对待,如果为NULL,则认为是一个普通的dai link,要创建pcm的,与之相关的代码见:
soc-core.c中的soc_probe_link_dais函数,在创建pcm设备那里有对params的判断,如果params不为NULL,则调用soc_link_dai_widgets函数,这个函数的作用就是把dai link中的cpu dai的play与codec dai的capture通过widgets链接,把cpu dai的capture与codec dai的play通过widgets链接,这样就实现了从CODEC到CODEC的数据流了,关于这一块可以参考官方文档:
Creating codec to codec dai link for ALSA dapm
但是这篇文档里面有个地方好像有点问题,我对照了一下我手上的kernel代码,它里面第一个dai link也有params参数,但是实际kernel里面的第一个dai link里面是没有params参数的,不知道哪里是对的,因为手上也没有对应的平台无法测试,但是感觉kernel代码里面是对的……

关于hostless中的fe,这一点其实官方文档里面已经说明了,它只是一个控制管理的通道,我觉得这个fe可能主要就是起到open啊、hw_param啊之类的作用,就像前面所说,上层应用无法直接控制be,必须通过一个fe来实现,所以这里必须有一个fe。至于fe与be的链接,这里其实就是当be被激活后fe在open时会自动全部链接,这一点在前面已经分析过了。

5 关于pcm hw param及pcm hw rule

pcm hw parameter是对音频系统使用的硬件参数的记录。在之前总是看到dai driver或者其他dirver里面有一个hw_params的回调函数,虽然知道它是用来设置硬件参数的,但是hw params的来龙去脉还是不太清楚,这里就专门花点时间分析、记录一下。

  • hw parameter的作用:
    hw parameter表示当前音频流使用的硬件参数,比如当前音频流使用的format,声道数,使用的哪个音频输出端口等等。因为一个codec或者一个dai可以支持多种格式,比如一个dai可以传16bit pcm、24bit pcm,那么现在究竟是使用的哪一种格式呢,这个问题就由上层应用来指定,比如说上层应用需要播放一个16bit的pcm,那么就会打开一个pcm逻辑设备,并通过ioctrl来通知音频驱动当前播放的音频流的参数,音频驱动根据这些配置来进行对应的操作,比如控制i2s接口的配置或者slimbus的通道(msm8996就是控制slimbus的channel)等等。

  • hw parameter什么时候配置,谁来配置:
    这个问题其实前面已经说了,是由需要播放音频流的上层应用程序来配置,配置方式就是通过pcm逻辑设备的ioctrl接口,发送0x10/0x11命令来进行配置。在打开音频流后上层应用通过此接口通知音频driver该使用什么硬件参数。这一点从前面章节图中的打印信息可以看出,android的音频框架在open了pcm设备后立即发送0x1命令查询driver info,然后就发送了0x11命令,来设置hw parameter。各driver的hw_params调用过程:
    ioctrl cmd:0x11->snd_pcm_ioctl_hw_params_compat->snd_pcm_hw_params->
    (substream->ops->hw_params)->dpcm_fe_dai_hw_params->fe/be:soc_pcm_hw_params->各driver的hw_params函数

  • hw param与hw rule之间的关系:
    这里抽象一点说,具体怎么操作的后面详细记录。所谓rule,就是对hw parameter的约束,比如某个dai只支持16bit传输,结果现在上面来了一个stream是24bit的,这个时候上层应用程序通过0x11命令发送hw parameter告诉driver有一个24bit的流要来了,这个时候driver回去check自己能不能处理24bit的stream,如果能,就把自己设置为24bit模式,如果不行则配置不生效。所以,rule的添加都是音频驱动中各个driver在open的时候自己添加到该stream的rules中的,比如dai driver会把自己的rule在open的时候添加到stream中,platform driver也会添加,总之只要需要对stream有约束的driver都可以在open的时候把自己的约束添加给stream的rules中,这样,当上层应用给该stream配置hw parameter时,stream对照着自己的rules一条一条检查合法性就行了。

5.1 hw param相关

相关文件:include\uapi\asound.h
定义:

struct snd_interval {
    unsigned int min, max;
    unsigned int openmin:1,
             openmax:1,
             integer:1,
             empty:1;
};

#define SNDRV_MASK_MAX  256

struct snd_mask {
    __u32 bits[(SNDRV_MASK_MAX+31)/32];
};

struct snd_pcm_hw_params {
    unsigned int flags;
    struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK - 
                   SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
    struct snd_mask mres[5];    /* reserved masks */
    struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
                        SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
    struct snd_interval ires[9];    /* reserved intervals */
    unsigned int rmask;     /* W: requested masks */
    unsigned int cmask;     /* R: changed masks */
    unsigned int info;      /* R: Info flags for returned setup */
    unsigned int msbits;        /* R: used most significant bits */
    unsigned int rate_num;      /* R: rate numerator */
    unsigned int rate_den;      /* R: rate denominator */
    snd_pcm_uframes_t fifo_size;    /* R: chip FIFO size in frames */
    unsigned char reserved[64]; /* reserved for future */
};

通过源码来看,这里应该是把hw parameter分为了两类,一类是离散值类型,类是范围内连续值类型。简单来说就是第一类里面的值必须与rule规定的具体值相等才算有效,第二类只要在rule的范围类就算有效,两种类型的数据分别用masksintervals来表示。
由于intervals比较明了,就不记录,这里主要记录一下masks。
首先每个masksintervals数组长度的定义,其实就是每个该类型的参数占一个数组元素,例如mask类型的parameter总共有3个,那么这里就是masks[3],这块具体可见宏定义。
其次每个mask都是一个snd_mask类型,snd_mask类型其实就是一个数组,数组的长度为8个32bit元素。这里这样定义的目的其实是因为#define SNDRV_MASK_MAX 256这个宏,这个宏的意义是说每个parameter的取值范围为0~255,本来0~255范围的值用一个8bit的数据就可以表示了,但是由于一个hw parameter可能可以同时存在多个值(例如format,一个接口可以支持16bit,24bit,32bit等等),所以要用mask来进行表示,那么0~255取值范围的mask就需要用256bit来表示,所以这里的snd_mask为一个8个元素,每个元素32bit的数组,这样就正好可以表示一个取值范围为0~255的parameter的mask。
所以,在设置parameter的值时操作函数如下:

msm8996.c

static inline struct snd_mask *param_to_mask(struct snd_pcm_hw_params *p,
                         int n)
{
    return &(p->masks[n - SNDRV_PCM_HW_PARAM_FIRST_MASK]);
}

static void param_set_mask(struct snd_pcm_hw_params *p, int n, unsigned bit)
{
    if (bit >= SNDRV_MASK_MAX)
        return;
    if (param_is_mask(n)) {
        struct snd_mask *m = param_to_mask(p, n);
        /* 认为参数的取值范围为0~63,所以只初始化2个元素 */
        m->bits[0] = 0;
        m->bits[1] = 0;
        /* 数组中一个元素记录低5bit,数组总共8个数组元素,用数组下标来记录高3bit */
        m->bits[bit >> 5] |= (1 << (bit & 31));
    }
}

5.2 rule相关

关键文件及函数:

  • pcm_native.c : snd_pcm_hw_refinesnd_pcm_hw_constraints_initsnd_pcm_hw_constraints_complete
  • pcm_lib.c : snd_pcm_hw_rule_add

typedef int (*snd_pcm_hw_rule_func_t)(struct snd_pcm_hw_params *params,
                      struct snd_pcm_hw_rule *rule);

struct snd_pcm_hw_rule {
    unsigned int cond;
    snd_pcm_hw_rule_func_t func;
    int var;
    int deps[4];
    void *private;
};

struct snd_pcm_hw_constraints {
    struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK - 
             SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
    struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
                 SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
    unsigned int rules_num;
    unsigned int rules_all;
    struct snd_pcm_hw_rule *rules;
};

int snd_pcm_hw_rule_add(struct snd_pcm_runtime *runtime, unsigned int cond,
            int var,
            snd_pcm_hw_rule_func_t func, void *private,
            int dep, ...)

rule的作用在前面已经分析了,这里主要分析下rule究竟是如何达到这个目的的。

首先,rule的添加
前面已经说过,rule是在pcm逻辑设备open时添加的,而且是由各个部分的dirver自己添加自己的rule,具体添加的接口是snd_pcm_hw_rule_add函数,所有的rule都是通过这个函数最终被添加的。
然而除了各个driver添加自己特性的rule之外,还有一个地方会添加rule,那就是在open pcm的一开始会调用snd_pcm_hw_constraints_init函数,他会针对所有hw param创建一个最基本,或者说最宽松的rule。那么这里就出现了一个问题,同一个hw param可能会被多次创建rule,对于这个东西的处理是在snd_pcm_hw_refine中进行的,这里留到后面来分析。
其次,rule添加到哪里
snd_pcm_hw_rule_add函数中有一个参数:runtime,这个东西是与substream对应的,其实就是substream的运行时状态信息,在runtime中有一个snd_pcm_hw_constraints类型成员,这个成员就是rule的存放点,所有通过snd_pcm_hw_rule_add添加的rule都被存放到snd_pcm_hw_constraints类型成员的rules中。关于snd_pcm_hw_constraints中的masks和intervals,后面再说。
再次,rule add中的其他参数都是干嘛的

  • runtime
    前面已经说过。
  • cond:
    rule的约束,在上层配下来hw parameter时,会根据rule来检测配置的参数有效性,那么这个cond在这里的作用就是让这一条rule失效,也就是不去检查参数是否满足这条rule。在检查参数时有这样一句话:if (r->cond && !(r->cond & params->flags)) continue;,这里的params就是struct snd_pcm_hw_params,也就是说,如果在添加rule时设置了cond,且上层在配置hw param时设置同样的flags,则可以让该rule失效。那么这个东西究竟在什么情况下使用,关于这一点暂时还没搞清楚,因为还没发现哪里使用了这个功能……
  • var:
    这个var实际上就是参数标识,标识该rule是针对的哪一个参数,例如:SNDRV_PCM_HW_PARAM_FORMAT,参数标识的定义见:asound.h
  • func:
    我觉得应该叫约束函数,当需要判断某个hw parameter是否满足该rule的要求时,通过调用该函数实现判断,这些函数的实现都在pcm_lib.c中,在add的时候选择一个能够处理rule期望处理的参数类型的func填进去。那么这个函数怎么实现,或者为什么像pcm_lib.c中那样实现,其实全部是跟参数类型相关的,不容参数类型判断有效的方式是不一样的,所以这里可以看到很多不同的约束函数。
  • private:
    在func中判断hw param是否满足rule约束时所需要的数据。private由add rule时指定,private的类型根据func的不同而不同,具体需要什么类型的private要根据当前func中的需求来确定,所以这里需要了解当前选择的func的特性。
  • dep:
    dependence的意思,就是依赖。这个参数也是为func服务的,跟private一样,不同的func所需要的依赖不同,目前一个rule可以支持最多4个依赖。
    每个依赖其实都是一个参数,这里举个栗子:SNDRV_PCM_HW_PARAM_FORMAT类型的参数需要依赖SNDRV_PCM_HW_PARAM_SAMPLE_BITS参数。这个比较好理解,如果format是SNDRV_PCM_FORMAT_U16_LE,那么这个stream的采样率的物理位宽就必须是16,如果不是则自相矛盾了。

最后,rule是在哪里起作用的
这个问题就得说一下snd_pcm_hw_refine函数了,这个函数在整个hw param相关的内容中应该是绝对核心的地位,这里就大概分析一下他的执行过程:

int snd_pcm_hw_refine(struct snd_pcm_substream *substream, 
              struct snd_pcm_hw_params *params)
{
    ……
    unsigned int rstamps[constrs->rules_num];
    unsigned int vstamps[SNDRV_PCM_HW_PARAM_LAST_INTERVAL + 1];
    unsigned int stamp = 2;
    ……
    /* 检查params中mask类型的参数是否满足substream->runtime->hw_constraints中mask类型的参数要求
     * substream->runtime->hw_constraints应该是表示此substream允许该参数设置成哪些值,检查params
     * 要设置的值是否在substream->runtime->hw_constraints中,如果不是,则把params中的该值给置0
     * */
    for (k = SNDRV_PCM_HW_PARAM_FIRST_MASK; k <= SNDRV_PCM_HW_PARAM_LAST_MASK; k++) {
        ……
    }

    /* 同上,这是检查intervals的参数 */
    for (k = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; k <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; k++) {
        ……
    }

    ……

    for (k = 0; k < constrs->rules_num; k++)
        rstamps[k] = 0;
    for (k = 0; k <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; k++) 
        vstamps[k] = (params->rmask & (1 << k)) ? 1 : 0;

    /* 遍历rule,检查params是否满足每条rule的约束 */
    do {
        again = 0;
        for (k = 0; k < constrs->rules_num; k++) {
            ……
            /* 调用rule的约束函数,对params施加约束 */
            changed = r->func(params, r);
            ……

            /* 这一段代码就是处理多条rule约束同一参数的地方,大致的执行过程:
             * 当多条rule约束同一参数时,每条rule都会调用约束函数来处理该参数,如果
             * 参数被rule给修改了,则表示该参数被此rule约束了,一旦一个rule修改了
             * 该参数的值,那么所有其他的rule都必须重新对该参数调用一次约束函数,直到
             * 所有约束该参数的rule都不再会修改该参数的值为止
             * rstamps、vstamps、stamp、again这几个变量都是转为此服务的
             * */
            rstamps[k] = stamp;
            if (changed && r->var >= 0) {
                params->cmask |= (1 << r->var);
                vstamps[r->var] = stamp;
                again = 1;
            }
            if (changed < 0)
                return changed;
            stamp++;
        }
    } while (again);

    /* 设置msbits */
    if (!params->msbits) {
        ……
    }

    /* 设置rate */
    if (!params->rate_den) {
        ……
    }

    /* 设置fifo size */
    if (!params->fifo_size) {
            ……
        }
    }
    ……
}

在上述函数中牵扯出了之前遗留的一个地方:substream->runtime->hw_constraints结构体中的masksintervals两个元素,这两个元素哪里来的,是干什么的,研究这块的时候还纠结了很久,后来发现其实这两个东西确实就是在snd_pcm_hw_constraints_init中赋值的,而且赋的值是any,也就是全体有效后来结合snd_pcm_hw_refine函数,发现这两个东西的作用应该是用来过滤不允许设置的值的,但是在我手上的系统下好像没有不允许设置的值,所以这两个元素就一直是any状态,也就是没有什么实质性的作用。

5.3 关于hw parameter的fixup

关于hw parameter的fixup操作,在linux自说明文档中已经写了,这里引用其中的描述:

The BE above also exports some PCM operations and a “fixup” callback. The fixup
callback is used by the machine driver to (re)configure the DAI based upon the
FE hw params. i.e. the DSP may perform SRC or ASRC from the FE to BE.

e.g. DSP converts all FE hw params to run at fixed rate of 48k, 16bit, stereo for
DAI0. This means all FE hw_params have to be fixed in the machine driver for
DAI0 so that the DAI is running at desired configuration regardless of the FE
configuration.

e文这里就不翻译了,就分析一下这段话的含义。
在现代移动平台基本上都是有dsp的,就像一开始那个图里面描述的那样,那么在be端,最终的一些输出在设计上可能希望是一个约定好的固定值,比如dsp输出的音频数据流始终是48000hz采样率,那么这个时候就需要在设置完hw params之后强行把be的hw param刷成dsp输出的实际参数,这个时候就用到了fixup。那么这里为什么不在rule里面做,用rule来拦截不满足要求的设置呢?我觉得原因可能在于dsp的输入其实是可以接受非48000hz采样率的数据的,然后dsp内部的程序会把输入的各种各样的采样率统一转换成一个固定采样率作为输出。而用ioctrl进行hw params的设置时其实是对fe进行的设置,be的hw params是从把fe的hw params memcpy过去的,所以如果没有fixup,则be的参数等价于fe的参数,但是如果dsp的输出端如果希望与输入端有不同的参数,则只能通过fixup来实现。
逻辑上,dsp的输出端的hw params是设备制造商自己来约定的,比如设备制造商希望dsp输出到codec的采样率统一为48000hz,所以fixup函数应该由mechine driver来提供,这就是为什么在msm8996平台上be_hw_params_fixup函数都在msm8996.c文件中了。
补充一句,be_hw_params_fixup是在dai link结构体中,是在mechine driver定义dai link时进行赋值的。

GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐