1. 程式人生 > >qcom 音訊相關的dsp driver筆記(基於msm8996平臺)

qcom 音訊相關的dsp driver筆記(基於msm8996平臺)

原址 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\n", 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\n"); } 其實如果不算通訊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 \r\n", 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\n", __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]\n",                    __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到底是以什麼原則來定義的……