DSP

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

2019-07-13 11:00发布

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

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

tags: linux sound msm8996

0 前言

本文的主要参考文档:
…… 关于音频框架的大致架构,高通文档原话:
…… 这里其实就说明了,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相关的过程如下图所示:
acdb.jpg-134.7kB 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的构成:
dsp driver.jpg-181.7kB

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_bedaisfe_dai_mapfe_dai_app_type_cfgcal_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好像也没有明确的说,所以暂且这样认为:
acdb device.jpg-174.5kB

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; 为cpu侧拿到的内存的物理地址。 u32 mem_map_handle; 为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_lswpayload_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_chswcd9xxx中是顶一个的一个指针,那么到底有多少个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 MUXSLIM 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 MUXSLIM RX1 MUX的配置后,tasha_p->dai[AIF_MIX1_PB].wcd9xxx_ch_list中就应该挂接了2个rx_chs了。把链路物理信息放到了codec dai中只是开始,还