DSP

qcom 音频相关的dsp driver笔记(基于msm8996平台)

2019-07-13 19:38发布

原址
0 前言
1 关于acdb
1.1 从audio_calibration.c说起
1.2 关于acdb配置的注册
1.3 关于acdb配置过程
2 关于dsp driver
3 关于asm
4 关于adm
5 关于afe
6 关于apr消息
7 关于channel map
附录 关于afe port id 关于音频框架的大致架构,高通文档原话:  这里其实就说明了,qcom的音频框架底层驱动主要有asoc driver(在这一篇笔记里面记录的)、slimbus、acdb和adsp组成,这篇笔记就主要记录一下acdb、adsp相关的,并且记录一下跟slimbus相关的channel map的内容。 1 关于acdb
目前来说,对于acdb文件,我的理解是一个dsp的参数配置文件。 
在audio这一块,dsp里面最主要分为了三个部分,asm、adm和afe。另外的lsm是干什么用的,目前暂时还没用上……也没看到手上的开发板有哪里用到了……所以暂时不管。 那么asm、adm及afe的管理工作或者说他们的driver都是在ap(cpu)上运行的,也就是对应的q6asm.c、q6adm.c、q6afe.c,这些driver负责提供对这些module的控制接口,以及配置接口,当有音频流需要播放时,platform driver或者dai driver里面调用对应的接口打开、配置正确的asm、adm及afe。 除此之外,android在打开一个linux音频逻辑设备进行播放时,还会把asm、adm及afe需要的相应配置从acdb文件中读出来,通过audio calibration这个逻辑设备配置给q6asm.c、q6adm.c、q6afe.c。这些module会将配置保存,并且在open相应module时使用这些配置。 详细过程在各个module里面记录。 启动音频时,acdb相关的过程如下图所示: 
audio_calibration是一个misc类型的逻辑设备,android侧就是通过该逻辑设备与linux侧进行acdb数据交互的。 libacdbloader.so是qcom的一个闭源库,里面主要是提供了对acdb文件的操作,以及与audio_calibration设备的交互,acdb文件配置的最终操作都是调用该库里面的方法完成。 关于acdb来看下高通的原话吧: 
…… 上面的就不翻译了…… 这里记录一下acdb文件里面记录的东西是怎么配置给dsp的,下面以asm为例来进行记录,所有使用到acdb配置的模块工作过程都一模一样。 1.1 从audio_calibration.c说起
audio_calibration是”msm_audio_cal”逻辑设备的driver,该设备其实就是一个接口类型的逻辑设备,提供一个kernel与userspace交互的通道,userspace通过”msm_audio_cal”设备来进行audio calibration。 该driver非常简单就不详细记录了,其中的一个关键函数是audio_cal_register(),该函数的作用简单来说就是让其他的模块给audio_calibration提供一组回调接口,比如set、get等,同时指明该回调接口处理的数据类型,所有能识别的数据类型qcom已经定义好了,第三方如果要增加新类型必须得修改qcom的audio_calibration.c代码……没必要…… 1.2 关于acdb配置的注册
以asm为例,在module init(q6asm_init_cal_data()函数)的时候就进行注册,注册调用的函数为: int cal_utils_create_cal_types(int num_cal_types,
            struct cal_type_data **cal_type,
            struct cal_type_info *info)
{
    ……
    for (i = 0; i < num_cal_types; i++) {
        ……
        cal_type[i] = create_cal_type_data(&info[i]);
        ……
        ret = audio_cal_register(1, &info[i].reg);
        ……
    }
done:
    return ret;
}

其中,info的内容是在q6asm_init_cal_data()函数中写死了的,这个是根据各模块自己的需求来决定。 
这里主要就是做了两件事,1、创建一个cal_type_data,创建该结构所需要的参数由info提供;2、向audio_calibration注册一个数据侦听,也就是当audio_calibration收到该模块想要的数据类型后调用一下info[i].reg中提供的回调函数。 回过头来,cal_type_data这个东西对于asm来说是个啥…… 
无论是asm、adm还是afe,都定义了一个这个:static struct cal_type_data *cal_data[ASM_MAX_CAL_TYPES];,这个东西其实就是保存acdb或者其他调音参数的数据结构。 struct audio_cal_callbacks {
    int (*alloc) (int32_t cal_type, size_t data_size, void *data);
    int (*dealloc) (int32_t cal_type, size_t data_size, void *data);
    int (*pre_cal) (int32_t cal_type, size_t data_size, void *data);
    int (*set_cal) (int32_t cal_type, size_t data_size, void *data);
    int (*get_cal) (int32_t cal_type, size_t data_size, void *data);
    int (*post_cal) (int32_t cal_type, size_t data_size, void *data);
}; struct audio_cal_reg {
    int32_t             cal_type;
    struct audio_cal_callbacks  callbacks;
}; struct cal_util_callbacks {
    int (*map_cal)
        (int32_t cal_type, struct cal_block_data *cal_block);
    int (*unmap_cal)
        (int32_t cal_type, struct cal_block_data *cal_block);
    bool (*match_block)
        (struct cal_block_data *cal_block, void *user_data);
}; struct cal_type_info {
    struct audio_cal_reg        reg;
    struct cal_util_callbacks   cal_util_callbacks;
}; struct cal_type_data {
    struct cal_type_info        info;
    struct mutex            lock;
    struct list_head        cal_blocks;
}; struct cal_block_data {
    size_t          client_info_size;
    void            *client_info;
    void            *cal_info;
    struct list_head    list;
    struct cal_data     cal_data;
    struct mem_map_data map_data;
    int32_t         buffer_number;
};

struct cal_type_data嵌套了一大堆,其实归结来说: info:里面存放了该参数的类型信息以及一些操作函数,类型信息就是指的该参数属于什么类型,例如:ASM_TOPOLOGY_CAL_TYPE。操作函数就是该参数的回调操作方法,struct audio_cal_callbacks里面的方法其实都已经注册到了audio calibration里面去了,由audio calibration负责调用,这里其实没什么作用……struct cal_util_callbacks的方法由audio_cal_utils.c里面的函数调用,audio_cal_utils.c里面是直接调用的保存在info里面的方法,其中match_block方法是必须存在的,用于判断同一条参数是否已经保存过了。(audio_cal_utils.c里面属于一个audio calibration框架辅助工具库,把所有calibration中所用到的共性行为抽象出来写成了辅助工具函数,每个具体的calibration模块自己写该模块的特性函数,把特性方法提供给audio_cal_utils.c,这也是一种常用的软件模式)
lock:锁……
cal_blocks:实际定义为:struct cal_block_data,其中void *cal_info;是存放实际参数的地方。int32_t buffer_number;是asm用来判断参数是否重复的标志。(见match函数)
1.3 关于acdb配置过程
配置过程简单来说如下: 1、audio calibration收到ioctrl
2、根据参数类型调用注册到该参数类型下的所有模块的set回调函数
3、各模块回调函数被调用后把参数保存在对应参数类型下的cal_data[type]的cal_blocks->cal_info中。
2 关于dsp driver
在记录asm、adm和afe之前先简单记录一下整个dsp driver的构成: 
3 关于asm
asm是干什么的: 
在cpu中,asm对应的其实就是stream,或者说就是一条音频数据流,asm负责完成把音频数据流写给dsp的任务以及对dsp asm的配置工作。 
在dsp中,asm负责音频的编解码以及部分的音效处理。 asm 怎么工作的: 
这里简单记录一下,在音频流开始播放的时候,会调用open函数: static int msm_pcm_open(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct snd_soc_pcm_runtime *soc_prtd = substream->private_data;
    struct msm_audio *prtd;
    int ret = 0;     prtd = kzalloc(sizeof(struct msm_audio), GFP_KERNEL);
    ……
    prtd->substream = substream;
    prtd->audio_client = q6asm_audio_client_alloc(
                (app_cb)event_handler, prtd);
    ……
    prtd->enabled = IDLE;
    prtd->dsp_cnt = 0;
    prtd->set_channel_map = false;
    prtd->reset_event = false;
    runtime->private_data = prtd;
    ……
}

这里跟asm相关的主要是q6asm_audio_client_alloc()函数,简单来说就是向asm注册了一个client,建立了一个该substream与dsp的asm通信的通道,这个通道由q6asm.c来管理。 struct audio_client *q6asm_audio_client_alloc(app_cb cb, void *priv)
{
    struct audio_client *ac;
    int n;
    int lcnt = 0;
    int rc = 0;     ac = kzalloc(sizeof(struct audio_client), GFP_KERNEL);
    ……
    /* 申请一个stream session,qcom规定同时最多有8个session,
     * 这里qcom就是用了一个全局数组来存当前存在stream,然后如果哪个元素空着
     * 就把哪个元素的数组下标返回回来作为该session的id */
    n = q6asm_session_alloc(ac);
    ……
    ac->session = n;
    ac->cb = cb;
    ac->path_delay = UINT_MAX;
    ac->priv = priv;
    ac->io_mode = SYNC_IO_MODE;
    ac->perf_mode = LEGACY_PCM_MODE;
    ac->fptr_cache_ops = NULL;
    /* DSP expects stream id from 1 */
    ac->stream_id = 1;
    ……
    /* 想apr框架申请一个apr通道 */
    ac->apr = apr_register("ADSP", "ASM",
            (apr_fn)q6asm_callback,
            ((ac->session) << 8 | 0x0001),
            ac);
    ……
    ac->mmap_apr = q6asm_mmap_apr_reg();
    if (ac->mmap_apr == NULL) {
        mutex_unlock(&session_lock);
        goto fail_mmap;
    }
    ……
    rc = send_asm_custom_topology(ac);
    if (rc < 0) {
        mutex_unlock(&session_lock);
        goto fail_mmap;
    }
    ……
}

关于q6asm_mmap_apr_reg();这里单独说一下,其实里面也是注册了一个apr的通道,这里为什么要单独注册一个……我觉得这里主要是为了处理asm模块级的消息,详细的在后面记录,这里简单说一下。 
ac->apr = apr_register()是注册了一个到dsp中asm模块下面的对应stream的通信通道,但是这里还需要一个cpu asm到dsp asm模块的通信通道,用于cpu去控制dsp的asm模块,主要体现在两边的smmu后的共享内存上,也就是mmap,所以这里主要是为了两边协商共享内存的地址用。 这之后,系统会调prepare函数: static int msm_pcm_playback_prepare(struct snd_pcm_substream *substream)
{
    ……
    /* 按照一定的格式打开一个asm,也就是告诉dsp的asm现在的这条流的格式,下面好做准备解析 */
    ret = q6asm_open_write_v3(prtd->audio_client,
                  FORMAT_LINEAR_PCM, bits_per_sample);
    /* 简单来说就是吧acdb配下来的参数发给asm模块 */
    ret = q6asm_send_cal(prtd->audio_client);
    ……
    /* 这个session id前面已经记录过是怎么来的了 */
    prtd->session_id = prtd->audio_client->session;
    /* 这个函数的作用在记录adm时来说 */
    ret = msm_pcm_routing_reg_phy_stream(soc_prtd->dai_link->be_id,
            prtd->audio_client->perf_mode,
            prtd->session_id, substream->stream);
    ……
    /* 配置采样率、声道信息 */
    ret = q6asm_media_format_block_multi_ch_pcm_v3(
                prtd->audio_client, runtime->rate,
                runtime->channels, !prtd->set_channel_map,
                prtd->channel_map, bits_per_sample,
                sample_word_size);
    ……
}

最后trigger和write就不记录了…… 其实asm里面最麻烦的地方就在于共享内存的管理这块,这里要是有机会的话就单独记录一下…… 4 关于adm
adm:audio device management 
adm设备包括路由矩阵和acdb device两个东西。 
acdb device是个什么东西呢,其实这个东西就是QACT里面的Tools->DeviceDesigner里面的一个device…… 
所以,这个adm管理的东西就是QACT里面的这个device,对应到dsp里面,就是链接copp和audio font port的东西,关于这个qcom的音频框架overview文档中有对应的示意图,这里就不贴了。 device : 
把他叫做一个设备也没错,可以理解为这个设备有n个输入(最大8),每个输入都是一个音频流,还有一个输出,这个输出也是一个音频流,每个输入都是一个asm的数据,每个输出都是把数据给afe。这个里面我觉得最主要就是做了两件事,混音和调音。因为输入可能是多个,但输出只有一个,所以这里面必须根据输出的channel数、采样率(可能需要重采样)、位宽进行混音,合并成一个与afe匹配的输出流。再就是调音,或者说是音效(effect),这里各个音效公司可以把自己需要的音效处理进行集成。 routing: 
所谓routing,就是指的路由,因为adm主要两个作用,一个是路由矩阵,一个是音频处理。音频处理这一块主要依赖于acdb文件,因为这一块只能配置dsp中已有的模块和拓扑的行为,所以不做重点记录,主要记录一下关于路由矩阵这块。因为asoc中是采用的dynamic pcm,既然是dynamic pcm,那么fe和be具体的连接工作由dsp完成,所以dsp的driver必须告诉dsp他需要怎么连接,那么这个工作在msm8996平台上就是由be platform driver完成的,也就是msm-pcm-routing-v2.c这个文件,这个文件操作dsp中的adm模块,来实现数据的路由,所以,adm模块才是最终数据路由的执行者。那么既然要进行路由,有几个东西就必须知道:1、dsp接入了那些数据;2、dsp要把这些数据从哪些端口送出去;3、每条数据需要用什么处理算法。其实整个be platform driver就是围绕这三个问题展开的,这也是为什么这个dirver的命名中有routing这个词。 在这里面有几个关键的全局变量:msm_bedais,fe_dai_map,fe_dai_app_type_cfg和cal_data。 
其中cal_data变量在前面已经说了,这里就不详细记录。 
msm_bedais:记录了be dai的相关信息,其中主要包括那些fe的session会送到该be dai的afe port中。 
fe_dai_map:记录了当前所有工作的fe session的信息 
fe_dai_app_type_cfg: 
…… 上面是qcom的文档中写的,其实这个app type就是选择copp的标志,这里结合qact中的device来一起看就比较好理解,不然估计是理解不能…… session_copp_map:至于这个全局变量,不算特别重要就不记录了,简单来说就是保存了每个session要经过的copp,这个东西的作用就是给dsp发参数配置时,会从里面读取一下数据。后面分析代码的时候稍微会提一下这个东西的。 所以说,上面的三个问题分别由上述的三个全局变量解答了,那么再就是这几个变量的值是哪里来的,这里结合打印信息来看: [   85.351447] gift_dsp : kctl name = SLIM RX0 MUX 
[   85.353027] gift_dsp : kctl name = SLIM RX1 MUX 
[   85.355048] gift_dsp : kctl name = SLIM_0_RX Channels 
[   85.355908] gift_dsp : kctl name = RX INT7_1 MIX1 INP0 
[   85.368966] gift_dsp : kctl name = RX INT8_1 MIX1 INP0 
[   85.369722] gift_dsp : kctl name = SpkrLeft COMP Switch 
[   85.369782] gift_dsp : kctl name = SpkrRight COMP Switch 
[   85.369831] gift_dsp : kctl name = SpkrLeft BOOST Switch 
[   85.369881] gift_dsp : kctl name = SpkrRight BOOST Switch 
[   85.369930] gift_dsp : kctl name = SpkrLeft VISENSE Switch 
[   85.369979] gift_dsp : kctl name = SpkrRight VISENSE Switch 
[   85.370079] gift_dsp : kctl name = SpkrLeft SWR DAC_Port Switch 
[   85.370867] gift_dsp : kctl name = SpkrRight SWR DAC_Port Switch 
[   85.372261] gift_dsp : kctl name = Audio Stream 15 App Type Cfg 
[   85.372277] gift_dsp msm_pcm_routing_reg_stream_app_type_cfg: fedai_id 4, session_type 0, app_type 69937, acdb_dev_id 15, sample_rate 48000
[   85.372454] gift_dsp msm_routing_set_cal topology (0x00010314), acdb_id (0x0000000f), path (0), app_type (0x00011131), sample_rate (0), 
[   85.372836] gift_dsp : kctl name = SLIMBUS_0_RX Audio Mixer MultiMedia5 
[   85.372842] gift_dsp : msm_pcm_routing_process_audio: reg 2 val 4 set 1
[   85.374045] gift_dsp msm_pcm_routing_hw_params: BE Sample Rate (48000) format (2) be_id 2, channel (2)
[   85.374994] gift_dsp : kctl name = Playback Channel Map15 
[   85.406330] gift_dsp : !!!!!!!!!!! 
[   85.421707] gift_dsp : adm_open:port 0x4000 path:1 rate:48000 mode:2 perf_mode:1,topo_id 66324
[   85.424604] gift_dsp : msm_pcm_routing_reg_phy_stream: setting idx bit of fe:4, type: 0, be:2
[   85.642493] gift_dsp : kctl name = Audio Stream 0 App Type Cfg 
[   85.642510] gift_dsp msm_pcm_routing_reg_stream_app_type_cfg: fedai_id 0, session_type 0, app_type 69936, acdb_dev_id 15, sample_rate 48000
[   85.642697] gift_dsp msm_routing_set_cal topology (0x00010314), acdb_id (0x0000000f), path (0), app_type (0x00011130), sample_rate (-319468453), 
[   85.643140] gift_dsp : kctl name = SLIMBUS_0_RX Audio Mixer MultiMedia1 
[   85.643146] gift_dsp : msm_pcm_routing_process_audio: reg 2 val 0 set 1
[   85.644683] gift_dsp : kctl name = Playback Channel Map0 
[   85.652305] gift_dsp : adm_open:port 0x4000 path:1 rate:48000 mode:2 perf_mode:0,topo_id 66324
[   85.655030] gift_dsp : msm_pcm_routing_reg_phy_stream: setting idx bit of fe:0, type: 0, be:2

所以: msm_bedais在hw parameters里面赋值
fe_dai_app_type_cfg在Audio Stream %xx App Type Cfg的control里面赋值
cal_data(topology相关)通过acdb loader把acdb里面的内容发下来。在adm init时会向audio calibration注册cal_data,并且提供set函数:msm_routing_set_cal,当acdb set参数时回调该函数,完成对cal_data的写操作
fe_dai_map在msm_pcm_routing_reg_phy_stream函数中赋值,该函数是msm_pcm_prepare时调用的,也就是fe的platform在prepare时调用
adm_open是在msm_pcm_routing_reg_phy_stream函数中创建的,在be platform prepare时由于fe_dai_map里面的stream id还没有获取(msm_pcm_prepare时才能获取),所以往往adm_open并不是在be platform的prepare中创建的,也就是不是在msm_pcm_routing_prepare中创建的
msm_bedais[reg].fe_sessions在msm_pcm_routing_process_audio()里面被设置的,msm_pcm_routing_process_audio()函数是在SLIMBUS_0_RX Audio Mixer MultiMedia5控件被control写后触发调用的。这个控件的定义:
static const struct snd_kcontrol_new slimbus_rx_mixer_controls[] = {
    ……
    SOC_SINGLE_EXT("MultiMedia5", MSM_BACKEND_DAI_SLIMBUS_0_RX,
        MSM_FRONTEND_DAI_MULTIMEDIA5, 1, 0, msm_routing_get_audio_mixer,
        msm_routing_put_audio_mixer),
    ……
} static const struct snd_soc_dapm_widget msm_qdsp6_widgets[] = {
    ……
    SND_SOC_DAPM_MIXER("SLIMBUS_0_RX Audio Mixer", SND_SOC_NOPM, 0, 0,
        slimbus_rx_mixer_controls, ARRAY_SIZE(slimbus_rx_mixer_controls)),
    ……
}

以上基本上就把adm这块给解释清楚了,最后再来看一下具体的路由矩阵: /* multiple copp per stream. */
struct route_payload {
    /* copp_idx与port_id其实是一一对应的关系,这里就是定义出了
     * copp与afe port的对应关系 */
    unsigned int copp_idx[MAX_COPPS_PER_PORT];
    unsigned int port_id[MAX_COPPS_PER_PORT];
    int app_type;
    int acdb_dev_id;
    int sample_rate;
    unsigned short num_copps;
    unsigned int session_id;
}; int msm_pcm_routing_reg_phy_stream(int fedai_id, int perf_mode,
                    int dspst_id, int stream_type)
{
    ……
    struct route_payload payload;
    ……
    for (i = 0; i < MSM_BACKEND_DAI_MAX; i++) {
        if (!is_be_dai_extproc(i) &&
               (afe_get_port_type(msm_bedais[i].port_id) == port_type) &&
               (msm_bedais[i].active) &&
               (test_bit(fedai_id, &msm_bedais[i].fe_sessions))) {
            ……
            for (j = 0; j < MAX_COPPS_PER_PORT; j++) {
                unsigned long copp =
                    session_copp_map[fedai_id][session_type][i];
                if (test_bit(j, &copp)) {
                    payload.port_id[num_copps] =
                            msm_bedais[i].port_id;
                    payload.copp_idx[num_copps] = j;
                    num_copps++;
                }
            }
            ……
        }
    }
    ……
    if (num_copps) {
        payload.num_copps = num_copps;
        payload.session_id = fe_dai_map[fedai_id][session_type].strm_id;
        payload.app_type =
            fe_dai_app_type_cfg[fedai_id][session_type].app_type;
        payload.acdb_dev_id =
            fe_dai_app_type_cfg[fedai_id][session_type].acdb_dev_id;
        payload.sample_rate =
            fe_dai_app_type_cfg[fedai_id][session_type].sample_rate;
        /* 把这一条stream与对应adm device的绑定消息发送给dsp */
        adm_matrix_map(path_type, payload, perf_mode);
        msm_pcm_routng_cfg_matrix_map_pp(payload, path_type, perf_mode);
    }
    ……
}

那么acdb device在这里面到底是个什样的存在?这里凭自己的理解稍微画一下,这里qcom好像也没有明确的说,所以暂且这样认为: 
5 关于afe
afe的作用:把adm输出的音频数据发送给codec,同时还可以进行一些音频处理。 本来关于afe port的内容是准备稍微记录一下的,后来越来越发现这里有很大的迷惑性,而这迷惑性完全来自于qcom自身编码的不严谨,所以还是单独抽出来记录一下关于afe port的相关内容。 afe是在be dai prepare的时候被打开的,比如:msm-dai-q6-v2.c里面,调用了函数: int afe_port_start(u16 port_id, union afe_port_config *afe_config,
    u32 rate) /* This function is no blocking */
{
    /* 校验参数有效性 */
    ……
    /* 检查afe的apr是否注册了,如果没注册,则给afe注册一个apr */
    ret = afe_q6_interface_prepare();
    ……
    /* Also send the topology id here: */
    port_index = afe_get_port_index(port_id);
    if (!(this_afe.afe_cal_mode[port_index] == AFE_CAL_MODE_NONE)) {
        /* One time call: only for first time */
        /* 都是在发apr消息,配置afe的参数,有些参数是通过acdb文件拿到的,例如topology的值 */
        afe_send_custom_topology();
        afe_send_port_topology_id(port_id);
        afe_send_cal(port_id);
        afe_send_hw_delay(port_id, rate);
    }     /* Start SW MAD module */
    /* 这里的MAD有什么用暂时不清楚……好像是一种数据类型? */
    mad_type = afe_port_get_mad_type(port_id);
    if (mad_type != MAD_HW_NONE && mad_type != MAD_SW_AUDIO) {
        ……
        ret = afe_turn_onoff_hw_mad(mad_type, true);
        ……
    }     /* aanc 相关 */
    if ((this_afe.aanc_info.aanc_active) &&
        (this_afe.aanc_info.aanc_tx_port == port_id)) {
        ……
        ret = afe_aanc_start(this_afe.aanc_info.aanc_tx_port,
                this_afe.aanc_info.aanc_rx_port);
        ……
    }
    /* 给afe配置参数,包括channel map信息,详见:
     * struct afe_audioif_config_command config 结构 */
    config.hdr.hdr_field = APR_HDR_FIELD(APR_MSG_TYPE_SEQ_CMD,
                APR_HDR_LEN(APR_HDR_SIZE), APR_PKT_VER);
    ……
    config.hdr.opcode = AFE_PORT_CMD_SET_PARAM_V2;
    ……
    config.port = *afe_config;     ret = afe_apr_send_pkt(&config, &this_afe.wait[index]);
    ……     /* 发送AFE_PORT_CMD_DEVICE_START消息 */
    ret = afe_send_cmd_port_start(port_id);
    ……
}

对于afe,这里有一个有趣的地方,整个q6afe向apr只注册了一个src port为0xFFFFFFFF的apr svc,这样一来,所有跟afe模块相关的apr消息其实都是q6afe来统一管理了,这个地方个人觉得qcom没有处理好…… afe有一个很关键的地方,就是channel map,这一块单独在后面记录。 6 关于apr消息
apr是基于smd和smmu映射的,这里的记录不去分析smd和smmu,这两个东西不是几句话能说清楚的……这里只用明白一个概念,smd可以提供若干个通道,让不同设备进行数据交换;smmu可以把外部设备的访问映射为对一段地址的访问,也就是说如果外部设备有一段可以访问的内存,那么可以直接通过smmu把这段内存映射给cpu,cpu可以直接去访问这段内存。 首先说下几个结构…… struct apr_client {
    uint8_t id;/* 非APR_CLIENT_AUDIO即APR_CLIENT_VOICE */
    uint8_t svc_cnt;/* 使用了的svc的个数 */
    uint8_t rvd;
    struct mutex m_lock;
    struct apr_svc_ch_dev *handle;/* 通信操作的实体接口,这里其实就是smd的handle,只是被封装过一次 */
    struct apr_svc svc[APR_SVC_MAX];/* apr server,每个server的基本信息已经写死了,例如“svc_tbl_qdsp6” */
}; #define APR_DEST_MODEM 0
#define APR_DEST_QDSP6 1
#define APR_DEST_MAX   2 #define APR_CLIENT_AUDIO    0x0
#define APR_CLIENT_VOICE    0x1
#define APR_CLIENT_MAX      0x2 /* 全局变量,一个2×2的数组,保存了所有apr通信的参数 */
static struct apr_client client[APR_DEST_MAX][APR_CLIENT_MAX];

这个地方为什么要叫client,说实话不是特别清楚,这个client的server是谁?也不是特别清楚……但是他的作用倒是很明显,所有需要通过apr通信的模块都会把自己的一些信息保存进这个client中,然后通过handle与dsp交互数据,这些后面具体记录。 这里再稍微记录一下这个结构: typedef int32_t (*apr_fn)(struct apr_client_data *data, void *priv); struct apr_svc {
    uint16_t id;/* server id,例如:APR_SVC_AFE */
    uint16_t dest_id;/* 目的器件的id,例如:APR_DEST_QDSP6 */
    uint16_t client_id;/* server所属的client的id 例如:APR_CLIENT_AUDIO */
    uint16_t dest_domain;/* 域id,跟dest id可以对应,例如:APR_DOMAIN_ADSP */
    uint8_t rvd;
    uint8_t port_cnt;/* 向该server注册的的port数量 */
    uint8_t svc_cnt;
    uint8_t need_reset;
    apr_fn port_fn[APR_MAX_PORTS];/* 每个port的数据接收回调函数 */
    void *port_priv[APR_MAX_PORTS];/* 回调函数的参数 */
    apr_fn fn;/* 整个server接收数据的回调函数,只有在找不到对应port的回调函数时才会被调用 */
    void *priv;/* server回调函数的参数 */
    struct mutex m_lock;
    spinlock_t w_lock;
    uint8_t pkt_owner;
};

关于apr的行为,这里就记录两点:注册和接收回调 struct apr_svc *apr_register(char *dest, char *svc_name, apr_fn svc_fn,
                uint32_t src_port, void *priv)
{
    ……
    dest_id = apr_get_dest_id(dest);     if (dest_id == APR_DEST_QDSP6) {
        /* 检查dsp状态 */
        ……
    } else if (dest_id == APR_DEST_MODEM) {
        /* 检查modem状态 */
        ……
    }
    /* 根据sve name和domain id获取该server对应的client id、service序列号和service id
     * 这些东西都在apr.c里面写死了,见:svc_tbl_qdsp6和svc_tbl_voice两个全局数组 */
    if (apr_get_svc(svc_name, domain_id, &client_id, &svc_idx, &svc_id)) {
        ……
    }     clnt = &client[dest_id][client_id];
    ……
    if (!clnt->handle && can_open_channel) {
        /* 实际打开通信channel,adsp这里就是smd的一个channel,并返回该channel的一个handle */
        clnt->handle = apr_tal_open(client_id, dest_id,
                APR_DL_SMD, apr_cb_func, NULL);
        ……
    }
    ……
    svc = &clnt->svc[svc_idx];
    ……
    svc->id = svc_id;
    svc->dest_id = dest_id;
    svc->client_id = client_id;
    svc->dest_domain = domain_id;
    svc->pkt_owner = APR_PKT_OWNER_DRIVER;     /* 这里如果源端口为0xFFFFFFFF,则表示设置该service的接收回调函数,其余值(若有效)
     * 则为设置对应port的接收回调函数 */
    if (src_port != 0xFFFFFFFF) {
        temp_port = ((src_port >> 8) * 8) + (src_port & 0xFF);
        ……
        if (!svc->port_cnt && !svc->svc_cnt)
            clnt->svc_cnt++;
        svc->port_cnt++;
        svc->port_fn[temp_port] = svc_fn;
        svc->port_priv[temp_port] = priv;
    } else {
        if (!svc->fn) {
            if (!svc->port_cnt && !svc->svc_cnt)
                clnt->svc_cnt++;
            svc->fn = svc_fn;
            if (svc->port_cnt)
                svc->svc_cnt++;
            svc->priv = priv;
        }
    }
    ……
}

注册函数就记录这么多,关于apr_tal_open()背后的操作今后有机会再记录。 接着是接收回调函数,这个接收回调是指的clnt->handle的回调,也就是当smd收到数据后调用apr模块的函数,然后apr再在该回调中去调用service或者port的回调函数,过程如下: void apr_cb_func(void *buf, int len, void *priv)
{
    /* 一直在校验接收参数的有效性 */
    ……
    /* 根据接收的参数找到client */
    apr_client = &client[src][clnt];
    for (i = 0; i < APR_SVC_MAX; i++)
        /* 从client中找到对应的service */
        if (apr_client->svc[i].id == svc) {
            pr_debug("%d ", apr_client->svc[i].id);
            c_svc = &apr_client->svc[i];
            break;
        }
    ……
    temp_port = ((data.dest_port >> 8) * 8) + (data.dest_port & 0xFF);
    ……
    /* 如果有port能处理该数据则让该port的回调函数处理,
     * 否则让service的回调函数处理该数据(src port为0xFFFFFFFF的那一条注册命令) */
    if (c_svc->port_cnt && c_svc->port_fn[temp_port])
        c_svc->port_fn[temp_port](&data,  c_svc->port_priv[temp_port]);
    else if (c_svc->fn)
        c_svc->fn(&data, c_svc->priv);
    else
        pr_err("APR: Rxed a packet for NULL callback ");
}

其实如果不算通信channel相关的内容的话到这里apr的最进本的工作原理就说的差不多了,接下来主要记录一下apr的消息怎么发送。 
首先是apr消息的hdr: struct apr_hdr {
    uint16_t hdr_field;
    uint16_t pkt_size;
    uint8_t src_svc;
    uint8_t src_domain;
    uint16_t src_port;
    uint8_t dest_svc;
    uint8_t dest_domain;
    uint16_t dest_port;
    uint32_t token;
    uint32_t opcode;
};

给一个最简单的示例: static int xxxxxxx_fill_apr_hdr(struct apr_hdr *apr_hdr, uint32_t port,
             uint32_t opcode, uint32_t apr_msg_size)
{
    if (apr_hdr == NULL) {
        pr_err("[  err][%s] %s: invalid APR pointer ", LOG_FLAG, __func__);
        return -EINVAL;
    }     apr_hdr->hdr_field = APR_HDR_FIELD(APR_MSG_TYPE_SEQ_CMD,
        APR_HDR_LEN(APR_HDR_SIZE), APR_PKT_VER);
    apr_hdr->pkt_size = apr_msg_size; /* total len, include the hdr */
    apr_hdr->src_svc = APR_SVC_XXX;
    apr_hdr->src_domain = APR_DOMAIN_APPS;
    apr_hdr->src_port = port;/* apr port id, dsp will use this value as dest_port when response this cmd */
    apr_hdr->dest_svc = APR_SVC_XXX;
    apr_hdr->dest_domain = APR_DOMAIN_ADSP;
    apr_hdr->dest_port = 0;
    apr_hdr->token = port;
    apr_hdr->opcode = opcode;     return 0;
}

如果了解一般通信协议的设计的话,这里就很好理解,无非就是要告诉对方,我是谁,从我的哪个service的哪个port发出去的什么类型的消息,该消息要给谁,给到哪个service的哪个port。 
这里dest port一般填0,为什么还真不太清楚……src port填我们期望接收到dsp回复的port,简单来说就是dsp在应答这条消息时会把接收到的src port作为应答消息的dest port,然后我们在注册apr service的时候填的src port如果与这里应答消息的dest port匹配上了,则该消息将调用注册apr时提供的src port对应的回调函数处理此应答消息。 一般的apr消息格式如下: 
hdr + 对应消息数据0 + ... + 对应消息数据n + 附加内容 
例如: struct adm_cmd_set_pspd_mtmx_strtr_params_v5 {
    struct apr_hdr hdr;
    /* LSW of parameter data payload address.*/
    u32     payload_addr_lsw;
    /* MSW of parameter data payload address.*/
    u32     payload_addr_msw;
    /* Memory map handle returned by ADM_CMD_SHARED_MEM_MAP_REGIONS */
    /* command. If mem_map_handle is zero implies the message is in */
    /* the payload */
    u32     mem_map_handle;
    /* Size in bytes of the variable payload accompanying this */
    /* message or in shared memory. This is used for parsing the */
    /* parameter payload. */
    u32     payload_size;
    u16     direction;
    u16     sessionid;
    u16     deviceid;
    u16     reserved;
} __packed;

struct apr_hdr hdr;就是hdr,其余所有都为“对应消息数据”,这里最麻烦的就是“附加内容”。 
因为smd只能提供一个比较小的内存(好像是因为这个),所以,如果两边需要交互一个比较大的消息内容时往往会重新映射一段内存(利用smmu),然后发送消息的那一方把数据写到映射的内存中,并把内存地址在apr消息中告诉给对端,对端利用收到的apr消息,解析出附加内容的内存地址,然后读出附加内容。在上面代码中:     u32     payload_addr_lsw;
    u32     payload_addr_msw;
1
2
为cpu侧拿到的内存的物理地址。     u32     mem_map_handle;
1
为dsp那边对这段地址的映射句柄,这个值是dsp发送过来的,我估计是dsp侧对于这一段内存的记录信息的一个标志,dsp侧依靠这个值就可以找到cpu侧映射的内存在dsp侧所看到的地址。那么这些内存是怎么来的,这里稍微记录一下: 首先,cpu通过msm_audio_ion_alloc()函数去申请一段内存,这段内存可以是利用smmu映射出来的一段dsp的内存;
接着,cpu向dsp发送AFE_SERVICE_CMD_SHARED_MEM_MAP_REGIONS类型的apr消息,把刚刚得到的那一段内存的物理地址告诉dsp
最后,dsp通过发送AFE_SERVICE_CMDRSP_SHARED_MEM_MAP_REGIONS类型的apr消息,把dsp这边映射这段内存后的一个句柄告诉cpu,cpu把这个句柄保存下来。
当cpu要通过上述内存与dsp交互数据时,cpu首先把数据写入这段内存中,并把cpu侧的内存物理地址填入payload_addr_lsw和payload_addr_msw中,同时,把dsp发送过来的内存映射句柄填入mem_map_handle中,最后发送该apr消息,即可完成数据交互。
7 关于channel map
channel map,跟物理链路控制器相关。怎么理解这个问题呢……首先看下linux官方文档的一段话: PCM is another 4 wire interface, very similar to I2S, which can support a more flexible protocol. It has bit clock (BCLK) and sync (SYNC) lines that are used to synchronise the link whilst the Tx and Rx lines are used to transmit and receive the audio data. Bit clock usually varies depending on sample rate whilst sync runs at the sample rate. PCM also supports Time Division Multiplexing (TDM) in that several devices can use the bus simultaneously (this is sometimes referred to as network mode). 简单来说就是linux音频系统支持TDM特性,也就是pcm一个接口传输n个channel的数据,然后要让codec知道那个slot是传输的哪个channel的数据,channel map最主要的工作就在于此。(从文档描述和代码上来看是这样的)由于手上的硬件是qcom msm8996平台,该平台使用的不是pcm接口也不是i2s接口,所以这里没法进行实际的测试,对于pcm的channel map作用只能推断。 在msm8996平台上,qcom用来他们自己的slimbus的方式来链接dsp和codec(还有一些其他的设备),那么这里的channel map是干了什么,这一节主要记录下与之相关的内容。 slimbus:qcom的一个链接dsp、codec的总线,具体总线的工作时序和协议不清楚,但是从driver里面来看,该总线设计了若干个channel(好像是32个:slim-msm-ngd.c dev->ctrl.nchans = MSM_SLIM_NCHANS;),codec和dsp之间占用了ch_num为:     unsigned int rx_ch[TASHA_RX_MAX] = {144, 145, 146, 147, 148, 149, 150,
                        151, 152, 153, 154, 155, 156};
    unsigned int tx_ch[TASHA_TX_MAX] = {128, 129, 130, 131, 132, 133,
                        134, 135, 136, 137, 138, 139,
                        140, 141, 142, 143};

的channel,这里的ch_num感觉没有实际上的意义,仅仅是该channel的一个名称,可以用来唯一确定这个channel,但是有一点,就是不能跟其他使用slimbus的地方重复,所有qcom把tx的ch定义为128~143,rx的定义为144~156。 上述定义是在声卡初始化dai link被创建时通过dai link的init来实现的,相关代码见:msm8996.c中的msm_audrx_init()函数。该函数中调用了: snd_soc_dai_set_channel_map(codec_dai, ARRAY_SIZE(tx_ch),
                    tx_ch, ARRAY_SIZE(rx_ch), rx_ch);

来实现对codec的channel进行初始化: static int tasha_set_channel_map(struct snd_soc_dai *dai,
                 unsigned int tx_num, unsigned int *tx_slot,
                 unsigned int rx_num, unsigned int *rx_slot)
{
    ……
    if (tasha->intf_type == WCD9XXX_INTERFACE_TYPE_SLIMBUS) {
        wcd9xxx_init_slimslave(core, core->slim->laddr,
                       tx_num, tx_slot, rx_num, rx_slot);
        ……
    }
    return 0;
}

int wcd9xxx_init_slimslave(struct wcd9xxx *wcd9xxx, u8 wcd9xxx_pgd_la,
               unsigned int tx_num, unsigned int *tx_slot,
               unsigned int rx_num, unsigned int *rx_slot)
{
    …… 
    ret = wcd9xxx_configure_ports(wcd9xxx);
    ……
    if (wcd9xxx->rx_chs) {
        wcd9xxx->num_rx_port = rx_num;
        for (i = 0; i < rx_num; i++) {
            wcd9xxx->rx_chs[i].ch_num = rx_slot[i];
            INIT_LIST_HEAD(&wcd9xxx->rx_chs[i].list);
        }
        ret = wcd9xxx_alloc_slim_sh_ch(wcd9xxx, wcd9xxx_pgd_la,
                        wcd9xxx->num_rx_port,
                        wcd9xxx->rx_chs,
                        SLIM_SINK);
        ……
    } 
    ……
}

如果一直tracewcd9xxx_alloc_slim_sh_ch()函数下去,会发现其实就是再向slimbus配置channel的占用情况。 
这样一来,wcd9xxx->rx_chs这一个成员的内容就全部填好了,这个成员在后面很重要,所以这里特别说一声,该参数的channel相关的内容都是在这里填写的。 
但是,wcd9xxx->rx_chs在wcd9xxx中是顶一个的一个指针,那么到底有多少个rx_chs,每个rx_chs中的其他成员的内容在哪里填的呢?这个问题其实直觉告诉我应该在probe之类的初始化的函数中找答案: static int tasha_codec_probe(struct snd_soc_codec *codec)
{
    struct wcd9xxx *control;
    ……
    ptr = devm_kzalloc(codec->dev, (sizeof(tasha_rx_chs) +
               sizeof(tasha_tx_chs)), GFP_KERNEL);
    ……         
    control->rx_chs = ptr;
    memcpy(control->rx_chs, tasha_rx_chs, sizeof(tasha_rx_chs));
    ……
}

其中tasha_rx_chs是一个全局变量,已经把每个port定义好了,这样依赖rx_chs的port和channel都定义好了。 以上只是把port和channel定义好了,但是实际上在什么时候使用哪个port、哪个channel,这些channel和port又是怎么跟具体的aif关联的,这些才是最关键的。 
当在播放音频之前,上层(android)会根据配置文件(xxx.xml)来配置一些control(通过control逻辑设备)以打开物理音频链路。这一块另外一篇笔记高通msm8996平台的ASOC音频路径分析中已经记录过了,这里就不再记录。下面直接列举control的控制过程: [   42.123093] gift_dsp : kctl name = SLIM RX0 MUX 
[   42.123110] gift_dsp slim_rx_mux_put: wname SLIM RX0 MUX cname SLIM RX0 MUX value 5 shift 0 item 5
[   42.123975] gift_dsp : kctl name = SLIM RX1 MUX 
[   42.123988] gift_dsp slim_rx_mux_put: wname SLIM RX1 MUX cname SLIM RX1 MUX value 5 shift 1 item 5
[   42.125526] gift_dsp : kctl name = SLIM_0_RX Channels 
[   42.125539] gift_dsp msm_slim_0_rx_ch_put: msm_slim_0_rx_ch = 2
[   42.126302] gift_dsp : kctl name = RX INT7_1 MIX1 INP0 
[   42.127509] gift_dsp : kctl name = RX INT8_1 MIX1 INP0 
[   42.128286] gift_dsp : kctl name = SpkrLeft COMP Switch 
[   42.128338] gift_dsp : kctl name = SpkrRight COMP Switch 
[   42.128384] gift_dsp : kctl name = SpkrLeft BOOST Switch 
[   42.128428] gift_dsp : kctl name = SpkrRight BOOST Switch 
[   42.128471] gift_dsp : kctl name = SpkrLeft VISENSE Switch 
[   42.128516] gift_dsp : kctl name = SpkrRight VISENSE Switch 
[   42.128562] gift_dsp : kctl name = SpkrLeft SWR DAC_Port Switch 
[   42.129477] gift_dsp : kctl name = SpkrRight SWR DAC_Port Switch 
[   42.131947] gift_dsp : kctl name = Audio Stream 15 App Type Cfg 
[   42.132909] gift_dsp : kctl name = SLIMBUS_0_RX Audio Mixer MultiMedia5 
[   42.134902] gift_dsp msm_slim_0_rx_be_hw_params_fixup: format = 2, rate = 48000, channels = 2
[   42.134917] gift_dsp tasha_get_channel_map: dai->id 7, rx_num 2
[   42.134922] gift_dsp msm_snd_hw_params: rx_0_ch=2, rx_ch_count=0, rx_ch_cnt=2
[   42.134943] gift_dsp msm_dai_q6_set_channel_map: SLIMBUS_0_RX cnt[2] ch[144 145]
[   42.136845] gift_dsp : kctl name = Playback Channel Map15 
[   42.178973] gift_dsp tasha_codec_enable_slimrx: event called! codec name tasha_codec num_dai 11
[   42.208103] gift_dsp gift_pcm : msm_pcm_prepare : name = pcm15p!!! 
[   42.209709] gift_dsp : adm_open:port 0x4000 path:1 rate:48000 mode:2 perf_mode:1,topo_id 66324

硬件是双声道,所以这里一开始就会去控制SLIM RX0 MUX和SLIM RX1 MUX两个control,在这两个control的put函数: static int slim_rx_mux_put(struct snd_kcontrol *kcontrol,
               struct snd_ctl_elem_value *ucontrol)
{
    ……
    /* value need to match the Virtual port and AIF number */
    switch (rx_port_value) {
    ……
    case 5:
        ……
        list_add_tail(&core->rx_chs[port_id].list,
                  &tasha_p->dai[AIF_MIX1_PB].wcd9xxx_ch_list);
        break;
    ……
    }
rtn:
    mutex_unlock(&codec->mutex);
    snd_soc_dapm_mux_update_power(widget->dapm, kcontrol,
                    rx_port_value, e, update);     return 0;
    ……
}

可以看到这里会把这两个声道(一个声道是一个port)的rx_chs信息加入tasha_p->dai[AIF_MIX1_PB].wcd9xxx_ch_list中,tasha_p->dai[AIF_MIX1_PB]表示当前数据传输的codec侧的dai,至于为什么是AIF_MIX1_PB,这个也是在上面提到的笔记里面有记录。所以经过SLIM RX0 MUX和SLIM RX1 MUX的配置后,tasha_p->dai[AIF_MIX1_PB].wcd9xxx_ch_list中就应该挂接了2个rx_chs了。把链路物理信息放到了codec dai中只是开始,还并没有真正去配置硬件的寄存器,让codec芯片知道究竟该怎么配置dai。 
在上面那个函数中,设置完rx_chs后立马调用了snd_soc_dapm_mux_update_power(),这个函数就是在音频链路中让自己上电,当音频链路全部上电后会触发”AIF MIX1 PB”widget的事件,该widget的回调函数:tasha_codec_enable_slimrx()。这个函数里面看上去应该是真正的配置codec和slimbus的地方,让slimbus的channel和codec的port在物理上对应起来。这里之所以是“应该”,是因为这一块还没完全弄清楚,目前只是从代码上看起来比较像,但是由于没有datasheet和相关文档,所以不知道他底下到底配置的那些是什么……所以只能猜测一下…… 这里基本上把codec这边的channel map说清楚了,还有dsp那边的channel map没说…… 
dsp这里的channel map的操作在msm8996.c里面,也就是machine driver里面: static int msm_snd_hw_params(struct snd_pcm_substream *substream,
                 struct snd_pcm_hw_params *params)
{
    ……
    if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
        ret = snd_soc_dai_get_channel_map(codec_dai,
                    &tx_ch_cnt, tx_ch, &rx_ch_cnt , rx_ch);
        ……
        if (dai_link->be_id == MSM_BACKEND_DAI_SLIMBUS_5_RX) {
            ……
        } else {
            pr_debug("gift_dsp %s: rx_0_ch=%d, rx_ch_count=%d, rx_ch_cnt=%u ", __func__,
                  msm_slim_0_rx_ch, rx_ch_count, rx_ch_cnt);
            rx_ch_count = msm_slim_0_rx_ch;
        }
        ret = snd_soc_dai_set_channel_map(cpu_dai, 0, 0,
                          rx_ch_count, rx_ch);
        ……
        }
    }
    ……
}

上面这段代码,简单来说就做了两件事,从codec dai里面把channel map读出来(其实就是这里:tasha_p->dai[AIF_MIX1_PB].wcd9xxx_ch_list),然后再把这个channel map写给cpu dai。至于里面的:rx_ch_count = msm_slim_0_rx_ch;就是单独配置了channel数,以单独配置的channel数为准,这里其实msm_slim_0_rx_ch和rx_ch_cnt都是2…… 
msm_snd_hw_params()这个函数的调用位置: static struct snd_soc_ops msm8996_be_ops = {
    .hw_params = msm_snd_hw_params,
};

其实就是dai link的opt,所以在hw_params时就会自动调过来。 
这里的cpu dai set channel map函数为: static int msm_dai_q6_set_channel_map(struct snd_soc_dai *dai,
                unsigned int tx_num, unsigned int *tx_slot,
                unsigned int rx_num, unsigned int *rx_slot) {
    ……
    switch (dai->id) {
    case SLIMBUS_0_RX:
    case SLIMBUS_1_RX:
    case SLIMBUS_2_RX:
    case SLIMBUS_3_RX:
    case SLIMBUS_4_RX:
    case SLIMBUS_5_RX:
    case SLIMBUS_6_RX:
        ……
        for (i = 0; i < rx_num; i++) {
            dai_data->port_config.slim_sch.shared_ch_mapping[i] =
                rx_slot[i];
            pr_debug("%s: find number of channels[%d] ch[%d] ",
                   __func__, i, rx_slot[i]);
        }
        dai_data->port_config.slim_sch.num_channels = rx_num;
        ……
        break;
        ……
    }
    ……
}

可以看到,这里其实是把channel信息写入了port_config结构中,这里也仅仅是软件层面上的操作,并没有触发实际的硬件行为,在cpu侧真正触发硬件行为的是在msm_dai_q6_prepare()里面调用afe_port_start()函数打开afe时,配置给dsp的afe模块,然后afe模块再去控制dsp的硬件完成的,关于afe在前面进行了记录。 至此,msm8996平台的channel map这一块基本上可以理解清楚了,唯一的悬念就是slimbus那里,codec到底配置了一些什么,以使得codec的port和slimbus的channel对应上的。这里今后如果能找到相关文档再回来记录…… 附录 关于afe port id
afe里面有个afe port,这个东西在打开afe(afe_port_start())时是dai->id确定的,在打开open adm时也会用到,open adm时的这个port id是prepare时,根据: be_id = rtd->dai_link->be_id;
bedai = &msm_bedais[be_id];
bedai->active = 1;

去激活了dai link里面定义的be id对应的msm_bedais[],然后再在msm_pcm_routing_reg_phy_stream()函数调用时去查询所有active为1的msm_bedais[],然后进行open adm。 
这里的dai link中be dai赋的值要与msm_bedais[]中的顺序完全一致,msm_bedais[]为一个adm中的全局变量。 总觉得这里这样做有点危险,不符合同一参数唯一性……如果不注意可能导致afe里面和adm里面还有dai link里面以及dai里面的定义有出入…… 再来说下afe port的定义: 
apr_audio-v2.h里面定义了所有的afe port,但是有个有趣的地方: /* Slimbus Multi channel port id pool  */
#define SLIMBUS_0_RX        0x4000
#define SLIMBUS_0_TX        0x4001
#define SLIMBUS_1_RX        0x4002
…… /*  Start of the range of port IDs for SLIMbus devices. */
#define AFE_PORT_ID_SLIMBUS_RANGE_START 0x4000 /*  End of the range of port IDs for SLIMbus devices. */
#define AFE_PORT_ID_SLIMBUS_RANGE_END
    (AFE_PORT_ID_SLIMBUS_RANGE_START +
    AFE_PORT_ID_SLIMBUS_RANGE_SIZE-1)
……

这里一样写的是有缺陷的……而且容易给人造成误解,这里的SLIMBUS_0_RX就是AFE_PORT_ID_SLIMBUS_RANGE_START,即使SLIMBUS_0_RX不是起始的slimbus的值,那么这里的AFE_PORT_ID_SLIMBUS_RANGE_START肯定不能用一个立即数,而必须用已经定义好的slimbus的编号的宏。或者AFE_PORT_ID_SLIMBUS_RANGE_START用立即数定义,但是slimbus的定义应该都是基于AFE_PORT_ID_SLIMBUS_RANGE_START的,这里qcom的写法是非常不严谨的编码方式!!! 同样,由于这个地方定义的不严谨,又导致了另外一个地方的定义看上去很不统一: struct msm_pcm_routing_bdai_data {
    u16 port_id; /* AFE port ID */
    u8 active; /* track if this backend is enabled */
    unsigned long fe_sessions; /* Front-end sessions */
    u64 port_sessions; /* track Tx BE ports -> Rx BE
                * number of BE should not exceed
                * the size of this field
                */
    unsigned int  sample_rate;
    unsigned int  channel;
    unsigned int  format;
    u32 compr_passthr_mode;
    char *name;
}; struct msm_pcm_routing_bdai_data msm_bedais[MSM_BACKEND_DAI_MAX] = {
    { PRIMARY_I2S_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_PRI_I2S_RX},
    { PRIMARY_I2S_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_PRI_I2S_TX},
    { SLIMBUS_0_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_SLIMBUS_0_RX},
    { SLIMBUS_0_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_SLIMBUS_0_TX},
    { HDMI_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_HDMI},
    { INT_BT_SCO_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_INT_BT_SCO_RX},
    { INT_BT_SCO_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_INT_BT_SCO_TX},
    { INT_FM_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_INT_FM_RX},
    { INT_FM_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_INT_FM_TX},
    { RT_PROXY_PORT_001_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_AFE_PCM_RX},
    { RT_PROXY_PORT_001_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_AFE_PCM_TX},
    { AFE_PORT_ID_PRIMARY_PCM_RX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_AUXPCM_RX},
    { AFE_PORT_ID_PRIMARY_PCM_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_AUXPCM_TX},
    { VOICE_PLAYBACK_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_VOICE_PLAYBACK_TX},
    { VOICE2_PLAYBACK_TX, 0, 0, 0, 0, 0, 0, 0, LPASS_BE_VOICE2_PLAYBACK_TX},
    ……
};

可以看到,这里面的每行第一个元素应该填写afe port id的,结果这里port id的命名乱七八糟……一会是AFE_PORT_ID_XXX开头,一会是没有前缀的,很容易给人造成困惑,这个afe port到底是以什么原则来定义的……