[linux audio driver] 高通平台MI2S总线调试

硬件平台:高通SM6125

基线代码:LA.UM.8.11.1.c1

git clone https://git.codelinaro.org/clo/la/platform/vendor/opensource/audio-kernel.git

git clone https://git.codelinaro.org/clo/la/kernel/msm-4.14.git

下载完后分别进入对应的目录,执行如下命令切到LA.UM.8.11.1.c1 分支。

git checkout remotes/origin/LA.UM.8.11.1.c1

目录

目录

Overview 

1 IPC处理器间通信

1.1 服务端 AFE Service

1.2 客户端 q6afe.c

1.3 APR 理解

2 MI2S总线

2.1 I2S特性

2.2 Audio设备树文件

2.3 GPIO配置和解析流程

2.3.1 GPIO pinctrl 配置

2.3.2 GPIO pinctrl解析和调用

2.4 MI2S lines配置和解析

2.5 MI2S音频流参数

2.6 MI2S主从模式

2.7 MCLK配置和解析

2.8 波形实测



Overview 

        高通平台提供了丰富的音频I/O接口,包括SLIMbus、MI2S/PCM/TDM和SoundWire等。开发者能够方便地连接和控制音频codec、无线通信模块、蓝牙、FM模块和Display Port等设备。这些接口的多样性和灵活性为开发者提供了更多选择和创造空间,使他们能够灵活应用不同的接口来满足各种音频应用需求。

表1 高通部分SOC音频接口支持情况

SM6125

SDM660/SDM630

SDM845

  • Two SLIMbus interfaces

One dedicatedSLIMbus interface for WCNSS (Bluetooth, FM) and another for the WCD9340/WCD9341/WCD9335/WCD9326 codec

  • Three MI2S (2×data) interfaces
  • One MI2S (4×data) interface
  • SoundWire interface for WCD9370/WCD9375 and the speaker amp
  • Display port interface
  • TDM support
  • 2 SLIMbus interfaces

One dedicatedSLIMbus interface for WCN3990 and other for WCD9335/WCD9340/WCD9341 codec

  • 3 MI2S (2 ×data) interfaces
  • 1 MI2S (4 ×data) interface
  • SoundWire interface
  • TDM not supported
  • SLIMbus(WCD9341)

DedicatedSLIMbusinterface for WCN3990 (A2DP tunnel, SCO/FM Rx data)

Dedicated SPI port for WCDWDSP for fasterIPC / firmware download.

  • 4x MI2S interfaces
  • One speaker I2S interface
  • 3 stereo full-duplexor quad-channel half-duplex MI2S interface (MI2S_1, MI2S_2 & MI2S_3)
  • One to eightchannel I2S interface (MI2S_4)
  • TDM support
  • DP v1.4 support

HDMI Txnot supported

        整个系统而言,ADSP只是一个“小弟”,它配合AP做audio、sensor,charge(部分soc)相关的工作,扮演协处理器的角色;但对于Audio系统而言,ADSP是绝对的“大哥”,负责Audio I/O 驱动、DMA、时钟管理,音频算法处理、音频流路由等,是Audio模块的实际执行者,AP、Modem其它处理器只属于它的客户端。“哪有什么岁月静好,总有人替你负重前行”。AP端的Linux asoc驱动和HAL层代码都必须围绕ADSP的框架进行设计和匹配。

1 IPC处理器间通信

        音频接口控制器的参数配置和管理是在ADSP中实现了,AP侧(Linux Driver)主要负责参数的解析和传递,两者之间通信模型是Client-Server(C/S)结构,使用共享内存的方式作为通信物理链路,APR负责客户端和服务端的注册、路由和消息传递。

1.1 服务端 AFE Service

AFE SVC启动流程:
int main(void)
    +elite_framework_start()
        +CreateEliteStaticService()
            +aud_create_stat_afe_svc()
                +//创建"AfeS" SVC, 进入工作循环函数afe_svc_work_loop
                +qurt_elite_thread_launch(...,"AfeS"...,afe_svc_work_loop,...) 
 ------------------------------
 afe_svc_work_loop讲解:
源码路径:adsp_proc\avs\main\afe\services\static\src\
 static elite_svc_msg_handler_func afe_stat_serv_handler_ptr[] =
{
    afe_svc_custom_msg_handler,        //0 - ELITE_CUSTOM_MSG
    afe_svc_start,                     //1 - ELITE_CMD_START_SERVICE     
    afe_svc_destroy,                   //2 - ELITE_CMD_DESTROY_SERVICE
    elite_svc_unsupported,             //3 - ELITE_CMD_CONNECT
    elite_svc_unsupported,             //4 - ELITE_CMD_DISCONNECT
    elite_svc_unsupported,             //5 - ELITE_CMD_PAUSE
    elite_svc_unsupported,             //6 - ELITE_CMD_RESUME
    elite_svc_unsupported,             //7 - ELITE_CMD_FLUSH
    elite_svc_unsupported,             //8 - ELITE_CMD_SET_PARAM
    elite_svc_unsupported,             //9 - ELITE_CMD_GET_PARAM
    elite_svc_unsupported,             //A - ELITE_DATA_BUFFER
    elite_svc_unsupported,             //B - ELITE_DATA_MEDIA_TYPE
    elite_svc_unsupported,             //C - ELITE_DATA_EOS
    elite_svc_unsupported,             //D - ELITE_DATA_RAW_BUFFER
    elite_svc_unsupported,             //E - ELITE_CMD_STOP_SERVICE
    afe_svc_apr_msg_handler,           //F - ELITE_APR_PACKET
};
 +afe_svc_work_loop (void* pAudStatAfeSvc)    
     +for(;;)
     +{
          ...
          +//睡眠等待消息队列,有消息时被唤醒,并根据消息类型执行不同的处理函数,如上afe_stat_serv_handler_ptr
          +unChannelStatus = qurt_elite_channel_wait(pDevServChannel, unCurrentBitfield) ;
          +result = elite_svc_process_cmd_queue(pAfeSvc, pHDevService, afe_stat_serv_handler_ptr, afe_stat_serv_cmd_table_size);
          ...
      +}    
           
 elite_framework启动完成后,会向afe svc发送ELITE_CMD_START_SERVIC命令,调用afe_svc_start,注册回调函数,从而具备了监听客户端的能力。
 +afe_svc_start()
 +{
    +char aAfeSvcName[] = "qcom.adsp.afe"; //关键
    //Registers a service callback by name with the APR
    //当apr解析到是发给AfeSvc的消息调用afe_svc_apr_call_back_fct,
    //afe_svc_apr_call_back_fct主要工作内容是解析从客户端传来的elite_apr_packet_t,解析、封装成消息,并push到AfeSvc消息队列中,afe_svc_work_loop被唤醒继续执行。
    +elite_apr_if_register_by_name( &(pAfeSvc->apr_handle), &usRetAddr, aAfeSvcName, ulAfeNameSize, &afe_svc_apr_call_back_fct,NULL)                                   
 +}     

1.2 客户端 q6afe.c

vendor\qcom\opensource\audio-kernel\dsp
以设置时钟为例:
+int afe_set_lpass_clk_cfg(int index, struct afe_clk_set *cfg)
    +int afe_q6_interface_prepare(void) // Clients call to register
        +|if (this_afe.apr == NULL) 
            +|this_afe.apr = apr_register("ADSP", "AFE", afe_callback,0xFFFFFFFF, &this_afe); //目的地为adsp afe
    +//配置参数的param_hdr,param_hdr和cfg封装成apr pack包,并发送        
    +q6afe_svc_pack_and_set_param_in_band(index, param_hdr,
(u8 *) cfg)
        +struct param_hdr_v3 param_hdr;
        +param_hdr.module_id = AFE_MODULE_CLOCK_SET;
        +param_hdr.instance_id = INSTANCE_ID_0;
        +param_hdr.param_id = AFE_PARAM_ID_CLOCK_SET;
        +param_hdr.param_size = sizeof(struct afe_clk_set);
        +q6afe_svc_pack_and_set_param_in_band(index, param_hdr,(u8 *) cfg);

1.3 APR 理解

        APR(Aysnc packet router)跨越不同的软件层和子系统,实现了一套统一的API,是AP、ADSP、Modem等处理器间通信基础框架。本文不再讲解APR的实现细节(不影响对其他模块的理解,出问题和客制化需求的可能性较小,省点时间^-^),将它理解为子系统通信的桥梁即可。

说明:

DAI: ASoC Digital Audio Interface (DAI) , ASoC中的概念数字音频接口

BE :Back End , ASoC的概念,音频后端

BE DAIs:Back End DAIs,ASoC的概念 音频后端数字音频接口,如I2S、PCM和TDM等

AFE:Audio Front End,高通ADSP Elite的概念,音频前端,如MI2S、PCM和TDM等

BE和AFE从字面意思看是相反的,但实际上它们指的是同一个概念

高通ADSP最新AudioReach SPF(Signal Processing Framework)框架已经用EP Container替换了AFE的概念,本文讨论的是ADSP Elite框架的音频调试

      本文以高通SM6125平台为例,同时支持5组MI2S信号.

组号

I2S 接口名称

TDM 接口名称

PCM 接口名称

引脚信息

1

Primary MI2S

Primary TDM

Primary PCM

■ pri_mi2s_sclk – GPIO_113

■ pri_mi2s_ws – GPIO_114

■ pri_mi2s_data0 – GPIO_115

■ pri_mi2s_data1 – GPIO_116

2

Secondary MI2S

Secondary TDM

Secondary PCM

■ sec_mi2s_sclk – GPIO_125

■ sec_mi2s_ws – GPIO_126

■ sec_mi2s_data0 – GPIO_127

■ sec_mi2s_data1 – GPIO_128

3

Tertiary MI2S

Tertiary TDM

Tertiary MI2S

■ ter_mi2s_sclk – GPIO_18

■ ter_mi2s_ws – GPIO_19

■ ter_mi2s_data0 – GPIO_20

■ ter_mi2s_data1 – GPIO_21

4

Quaternary MI2S

Quaternary TDM

Quaternary PCM

■ qua_mi2s_clk – GPIO_106

■ qua_mi2s_ws – GPIO_107

■ qua_mi2s_data0 –

GPIO_108

■ qua_mi2s_data1 –

GPIO_104

■ qua_mi2s_data2 – GPIO_110

■ qua_mi2s_data3 – GPIO_111

5

Quinary MI2S

Quinary TDM

Quinary PCM

■ quin_mi2s_clk – GPIO_121

■ quin_mi2s_ws – GPIO_122

■ quin_mi2s_data0 –

GPIO_123

■ quin_mi2s_data1 –

GPIO_124

■ MCLK1 GPIO_119

■ MCLK2 GPIO_118

2 MI2S总线

2.1 I2S特性

在SM6125上,MI2S功能支持以下内容:
■ 具有MI2S硬件接口,同时支持支持TDM和AUXPCM
■ 主从模式配置
■ 数据格式
    □ 16位I2S
    □ 24位左对齐(32位帧中的24位数据左对齐,低位补0)
    □ 32位I2S
■ 采样率
    □ 主模式下的8、16、32、44.1、48、88.2、96、176.4、192和384 kHz
    □ 从模式下的所有标准采样率
■ 最大位时钟频率为24.586 MHz
■ 可配置为Rx/Tx的串行数据线;每个数据线可以传输2个声道的数据(立体声数据)
    □ MI2S_1、MI2S_2、MI2S_3和MI2S_5在半双工模式(Rx或Tx)下最多支持4个声道,或在全双工模式下支持立体声
    □ MI2S_4在半双工模式(Rx或Tx)下支持最多8个声道,或在全双工模式下支持4个声道

2.2 Audio设备树文件

trinket-iot-idp.dts
 python DeviceTreeExplorer.py trinket-iot-idp.dts
`-- trinket-iot-idp.dts
    |-- trinket-iot.dtsi
    |   |-- trinket.dtsi
    |   |   |-- skeleton64.dtsi
...
    |   |   |-- trinket-qupv3.dtsi   
    |   |   |-- trinket-pinctrl.dtsi
      //gpio pinctrl定义
    |   |   |-- trinket-ion.dtsi
 ...

    |   |   |-- trinket-audio.dtsi        //machine driver device tree
    |   |   |   |-- msm-audio-lpass.dtsi  //BE DAIs,PCM等platform device tree
    |   |   |-- trinket-thermal.dtsi
    |   |   |-- pm8008.dtsi
    |-- trinket-iot-idp.dtsi
    |   `-- trinket-idp.dtsi
...

trinket-iot-idp-overlay.dts
python DeviceTreeExplorer.py trinket-iot-idp-overlay.dts
`-- trinket-iot-idp-overlay.dts
    |-- trinket-iot-idp.dtsi
    |   `-- trinket-idp.dtsi
    |       |-- trinket-thermal-overlay.dtsi
    |       |-- trinket-sde-display.dtsi
    |       |   |-- dsi-panel-td4330-truly-singlemipi-fhd-cmd.dtsi
...        

    |       |-- trinket-camera-sensor-idp.dtsi
    |       |-- smb1355.dtsi
    |       |-- qg-batterydata-ascent-3450mah.dtsi
    |       |-- qg-batterydata-mlp356477-2800mah.dtsi
    |-- trinket-audio-overlay.dtsi // overlay,当前基线diable

arch/arm64/boot/dts/qcom/trinket-audio.dtsi

将MI2S,qcom,auxpcm-audio-intf属性设置为1,使msm_mi2s_be_dai_links可以被ASOC加载。

&q6core {
    sm6150_auto_snd{
        compatible = "qcom,sm6150-asoc-snd";
                qcom,model = "trinket-qrd-snd-card";
        qcom,mi2s-audio-intf = <1>;
        qcom,auxpcm-audio-intf = <1>;
        qcom,wcn-btfm = <1>;
                  ....
                  }
  ...
  }
  
static struct snd_soc_card *populate_snd_card_dailinks(struct device *dev)
{
    ...
        if (of_property_read_bool(dev->of_node,
                      "qcom,mi2s-audio-intf")) {
            memcpy(msm_auto_dai_links + total_links,
                   msm_mi2s_be_dai_links,
                   sizeof(msm_mi2s_be_dai_links));
            total_links += ARRAY_SIZE(msm_mi2s_be_dai_links);
        }
  ...      
}

2.3 GPIO配置和解析流程

本章节以Tertiary MI2S 为例讲解MI2S的配置步骤及驱动框架,硬件GPIO信息:

■ ter_mi2s_sclk – GPIO_18

■ ter_mi2s_ws – GPIO_19

■ ter_mi2s_data0 – GPIO_20

■ ter_mi2s_data1 – GPIO_21

当上述GPIO复用为MI2S/TDM/PCM功能时,其GPIO pinctrl function均为"ter_mi2s"。

2.3.1 GPIO pinctrl 配置

arch\arm64\boot\dts\qcom\trinket-pinctrl.dtsi

先检查是否有其他设备占用GPIO18-GPIO21,如有先要移除。默认代码是以ter_tdm作为文件属性,为了更好理解,我们先改为ter_mi2s_tdm_auxpcm。trinket-pinctrl.dtsi增加如下GPIO配置内容:

ter_mi2s_tdm_auxpcm {
    ter_mi2s_tdm_auxpcm_sleep: ter_mi2s_tdm_auxpcm_sleep {
        mux {
            pins = "gpio18", "gpio19";
            function = "gpio";
        };

        config {
            pins = "gpio18", "gpio19";
            drive-strength = <2>;   /* 2 mA */
            bias-pull-down;         /* PULL DOWN */
            input-enable;
        };
    };

    ter_mi2s_tdm_auxpcm_active: ter_mi2s_tdm_auxpcm_active {
        mux {
            pins = "gpio18", "gpio19";
            function = "ter_mi2s";
        };

        config {
            pins = "gpio18", "gpio19";
            drive-strength = <8>;   /* 8 mA */
            bias-disable;           /* NO PULL */
            output-high;
        };
    };
};

ter_mi2s_tdm_auxpcm_din {
    ter_mi2s_tdm_auxpcm_din_sleep: ter_mi2s_tdm_auxpcm_din_sleep {
        mux {
            pins = "gpio20";
            function = "gpio";
        };

        config {
            pins = "gpio20";
            drive-strength = <2>;   /* 2 mA */
            bias-pull-down;         /* PULL DOWN */
            input-enable;
        };
    };

    ter_mi2s_tdm_auxpcm_din_active: ter_mi2s_tdm_auxpcm_din_active {
        mux {
            pins = "gpio20";
            function = "ter_mi2s";
        };

        config {
            pins = "gpio20";
            drive-strength = <8>;   /* 8 mA */
            bias-disable;           /* NO PULL */
        };
    };
};

ter_mi2s_tdm_auxpcm_dout {
    ter_mi2s_tdm_auxpcm_dout_sleep: ter_mi2s_tdm_auxpcm_dout_sleep {
        mux {
            pins = "gpio21";
            function = "gpio";
        };

        config {
            pins = "gpio21";
            drive-strength = <2>;   /* 2 mA */
            bias-pull-down;         /* PULL DOWN */
            input-enable;
        };
    };

    ter_mi2s_tdm_auxpcm_dout_active: ter_mi2s_tdm_auxpcm_dout_active {
        mux {
            pins = "gpio21";
            function = "ter_mi2s";
        };

        config {
            pins = "gpio21";
            drive-strength = <8>;   /* 8 mA */
            bias-disable;           /* NO PULL */
            output-high;
        };
    };
};

        小技巧:Pinctrl中的function字段,可以参考平台的 drivers\pinctrl\qcom\pinctrl-[project].c定义 ,如下需要将GPIO18,GPIO19,GPIO20和GPIO21配置为ter_mi2s功能。

static const struct msm_pingroup trinket_groups[] = {
    ...
    [18] = PINGROUP(18, EAST, WSA_CLK, qup13, ter_mi2s, NA, NA, NA, NA, NA,
NA),
    [19] = PINGROUP(19, EAST, WSA_DATA, qup13, ter_mi2s, NA, NA, NA, NA,NA, NA),
    [20] = PINGROUP(20, EAST, qup13, ter_mi2s, qdss_gpio4, NA, NA, NA, NA,NA, NA),
    [21] = PINGROUP(21, EAST, qup13, ter_mi2s, NA, qdss_gpio5, NA, NA, NA,NA, NA),    
    ...
}

接着将pinctrl属性加入到BE DAI节点,为了配合machine driver的代码放到tdm_tert_rx中

arch/arm64/boot/dts/qcom/msm-audio-lpass.dtsi

tdm_tert_rx: qcom,msm-dai-tdm-tert-rx {
    compatible = "qcom,msm-dai-tdm";
...
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&tert_tdm_active &tert_tdm_din_active
        &tert_tdm_dout_active>;
    pinctrl-1 = <&tert_tdm_sleep &tert_tdm_din_sleep
        &tert_tdm_dout_sleep>;
...
};

修改sm6150_auto_snd声卡节点,文件路径:arch/arm64/boot/dts/qcom/trinket-audio.dtsi

&q6core {
    sm6150_auto_snd{
        compatible = "qcom,sm6150-asoc-snd";
                qcom,model = "trinket-qrd-snd-card";
        qcom,mi2s-audio-intf = <1>;
        qcom,auxpcm-audio-intf = <1>;
        qcom,wcn-btfm = <1>;
        qcom,msm-mi2s-master = <1>, <1>, <1>, <1>, <1>;
        ..
        qcom,tert-tdm-gpios = <&tdm_tert_rx>;
        ...
        }
}        

2.3.2 GPIO pinctrl解析和调用

        高通当前基线代码,对gpio 解析也所有变化,直接在machine driver probe里面获取、保存。

struct msm_asoc_mach_data { //声卡全局数据结构定义
    struct msm_pinctrl_info pinctrl_info[TDM_INTERFACE_MAX]; //解析后的pinctrl 信息
    struct mi2s_conf mi2s_intf_conf[MI2S_MAX];
    struct tdm_conf tdm_intf_conf[TDM_INTERFACE_MAX];
    struct auxpcm_conf auxpcm_intf_conf[AUX_PCM_MAX];
};

static const char *const pin_states[] = {"sleep", "active"};

 //DTS配置时候,声卡中的GPIO节点属性名称需要以下面的对应
static const char *const tdm_gpio_phandle[] = {"qcom,pri-tdm-gpios",
                        "qcom,sec-tdm-gpios",
                        "qcom,tert-tdm-gpios",
                        "qcom,quat-tdm-gpios",
                        "qcom,quin-tdm-gpios"};


static int msm_asoc_machine_probe(struct platform_device *pdev)
{
...
    ret = msm_get_pinctrl(pdev); //解析gpio信息
    if (!ret) {
        pr_debug("%s: pinctrl parsing successful\n", __func__);
    } else {
        dev_dbg(&pdev->dev,
            "%s: pinctrl parsing failed with %d\n",
            __func__, ret);
        ret = 0;
    }
...
}

static int msm_get_pinctrl(struct platform_device *pdev)
{
...
    for (i = TDM_PRI; i < TDM_INTERFACE_MAX; i++) {
        np = of_parse_phandle(pdev->dev.of_node,
                    tdm_gpio_phandle[i], 0);
        pr_err("%s Parse pinctrl info from devicetree \n", tdm_gpio_phandle[i]);
        if (!np) {
            pr_err("%s: device node %s is null\n",
                    __func__, tdm_gpio_phandle[i]);
            continue;
        }

        pdev_np = of_find_device_by_node(np);
        if (!pdev_np) {
            pr_err("%s: platform device not found\n", __func__);
            continue;
        }

        pinctrl_info = &pdata->pinctrl_info[i];
        if (pinctrl_info == NULL) {
            pr_err("%s: pinctrl info is null\n", __func__);
            continue;
        }

        pinctrl = devm_pinctrl_get(&pdev_np->dev);
        if (IS_ERR_OR_NULL(pinctrl)) {
            pr_err("%s: fail to get pinctrl handle\n", __func__);
            goto err;
        }
        pinctrl_info->pinctrl = pinctrl;

        /* get all the states handles from Device Tree */
        pinctrl_info->sleep = pinctrl_lookup_state(pinctrl,
                            "sleep");
        if (IS_ERR(pinctrl_info->sleep)) {
            pr_err("%s: could not get sleep pin state\n", __func__);
            goto err;
        }
        pinctrl_info->active = pinctrl_lookup_state(pinctrl,
                            "default");
        if (IS_ERR(pinctrl_info->active)) {
            pr_err("%s: could not get active pin state\n",
                __func__);
            goto err;
        }

        /* Reset the TLMM pins to a sleep state */
        ret = pinctrl_select_state(pinctrl_info->pinctrl,
                        pinctrl_info->sleep);
        if (ret != 0) {
            pr_err("%s: set pin state to sleep failed with %d\n",
                __func__, ret);
            ret = -EIO;
            goto err;
        }
...
}

        如连接上 MI2S 后端的PCM流打开或者关闭,操作函数startup和shutdown被调用,打开和关闭ter_mi2s功能。

TIPS:这是AP唯一实际设置MI2S 硬件配置的地方,后续的所有操作都只是转发给到ADSP执行。

static int msm_mi2s_snd_startup(struct snd_pcm_substream *substream)
{
    int ret = 0;
..
    if (pinctrl_info->pinctrl) {
        ret_pinctrl = msm_set_pinctrl(pinctrl_info,
                          STATE_ACTIVE);
        if (ret_pinctrl)
            pr_err("%s: MI2S TLMM pinctrl set failed with %d\n",
                __func__, ret_pinctrl);

..
    return ret;
}

static void msm_mi2s_snd_shutdown(struct snd_pcm_substream *substream)
{
....
pinctrl_info = &pdata->pinctrl_info[index];
    if (pinctrl_info->pinctrl) {
        ret_pinctrl = msm_set_pinctrl(pinctrl_info,
                          STATE_SLEEP);
        if (ret_pinctrl)
            pr_err("%s: MI2S TLMM pinctrl set failed with %d\n",
                __func__, ret_pinctrl);
    }
 ...
}

2.4 MI2S lines配置和解析

vendor/qcom/opensource/audio-kernel/asoc/msm-dai-q6-v2.c

static const struct of_device_id msm_dai_q6_mi2s_dev_dt_match[] = {
    { .compatible = "qcom,msm-dai-q6-mi2s", },
    { }
};

MODULE_DEVICE_TABLE(of, msm_dai_q6_mi2s_dev_dt_match);

static struct platform_driver msm_dai_q6_mi2s_driver = {
    .probe  = msm_dai_q6_mi2s_dev_probe,
    .remove  = msm_dai_q6_mi2s_dev_remove,
    .driver = {
        .name = "msm-dai-q6-mi2s",
        .owner = THIS_MODULE,
        .of_match_table = msm_dai_q6_mi2s_dev_dt_match,
    },
};

msm_dai_q6_mi2s_dev_probe
    +struct msm_mi2s_pdata *mi2s_pdata;
    //读取Device ID
    +of_property_read_u32(pdev->dev.of_node, q6_mi2s_dev_id,&mi2s_intf);
    //申请msm_dai_q6_mi2s_dai_data,作为设备platform_data
    +mi2s_pdata = kzalloc(sizeof(struct msm_mi2s_pdata), GFP_KERNEL);
    +of_property_read_u32(pdev->dev.of_node, "qcom,msm-mi2s-rx-lines",&rx_line);
    +of_property_read_u32(pdev->dev.of_node, "qcom,msm-mi2s-tx-lines",&tx_line);
    +mi2s_pdata->rx_sd_lines = rx_line;
    +mi2s_pdata->tx_sd_lines = tx_line;
    +mi2s_pdata->intf_id = mi2s_intf;   
    //申请msm_dai_q6_mi2s_dai_data,作为设备的私有数据
    +dai_data = kzalloc(sizeof(struct msm_dai_q6_mi2s_dai_data),GFP_KERNEL); 
    +dev_set_drvdata(&pdev->dev, dai_data);
    +pdev->dev.platform_data = mi2s_pdata;
    //配置校验,同时根据lines重新设置mi2s dai driver支持的channel数值
    //One data line Support two Channel
    +msm_dai_q6_mi2s_platform_data_validation(pdev,
&msm_dai_q6_mi2s_dai[mi2s_intf]);
    //调用ASOC snd_soc_register_component函数,将Digital Audio Interface Driver注册到Asoc框架中,供ASOC框架使用。
    +snd_soc_register_component(&pdev->dev, &msm_q6_mi2s_dai_component,
&msm_dai_q6_mi2s_dai[mi2s_intf], 1);  

2.5 MI2S音频流参数

        MI2S设备的音频流参数包括采样率、数据格式和通道,vendor/qcom/opensource/audio-kernel/asoc/sm6150.c 在mi2s_rx_cfg和mi2s_tx_cfg 结构中设置了如下默认值。

/* Default configuration of MI2S channels */
static struct dev_config mi2s_rx_cfg[] = {
    [PRIM_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2},
    [SEC_MI2S]  = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2},
    [TERT_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2},
    [QUAT_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2},
    [QUIN_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 2},
};

static struct dev_config mi2s_tx_cfg[] = {
    [PRIM_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1},
    [SEC_MI2S]  = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1},
    [TERT_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1},
    [QUAT_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1},
    [QUIN_MI2S] = {SAMPLING_RATE_48KHZ, SNDRV_PCM_FORMAT_S16_LE, 1},
};

同时定义了一系列kcontrol供用户层读取或者设置。

static const struct snd_kcontrol_new msm_snd_controls[] = {
    ...
SOC_ENUM_EXT("TERT_MI2S_RX Channels", tert_mi2s_rx_chs,
            msm_mi2s_rx_ch_get, msm_mi2s_rx_ch_put),
    SOC_ENUM_EXT("TERT_MI2S_TX Channels", tert_mi2s_tx_chs,
            msm_mi2s_tx_ch_get, msm_mi2s_tx_ch_put),
    ...
SOC_ENUM_EXT("TERT_MI2S_RX Format", mi2s_rx_format,
            msm_mi2s_rx_format_get, msm_mi2s_rx_format_put),
    SOC_ENUM_EXT("TERT_MI2S_TX Format", mi2s_tx_format,
            msm_mi2s_tx_format_get, msm_mi2s_tx_format_put),  
 ...   
   }
   
trinket:/ # tinymix "TERT_MI2S_RX Format"
TERT_MI2S_RX Format: >S16_LE S24_LE S24_3LE S32_LE
trinket:/ # tinymix "TERT_MI2S_RX Format" S32_LE
trinket:/ # tinymix "TERT_MI2S_RX Format"
TERT_MI2S_RX Format: S16_LE S24_LE S24_3LE >S32_LE

2.6 MI2S主从模式

qcom,msm-mi2s-master设置为1表示为主模式,<>排位号对应MI2S设备号

arch/arm64/boot/dts/qcom/trinket-audio.dtsi
sm6150_auto_snd{
    ..
    qcom,msm-mi2s-master = <1>, <1>, <1>, <1>, <1>;
    ..
}

        平台SOC作为主模式时,需要输出BLCK和WS信号用于数据传输和同步。我们知道AP是不直接管理时钟的,它需要将时钟信息传递给ADSP进行设置,双方事先约定数据格式afe_clk_set

//CLK ID 定义
vendor/qcom/opensource/audio-kernel/include/dsp/apr_audio-v2.h

     struct afe_clk_set {

        uint32_t clk_set_minor_version;

        uint32_t clk_id;

        uint32_t clk_freq_in_hz;

        uint16_t clk_attri;

        uint16_t clk_root;
    

        uint32_t enable;
    };


* Clock ID Enumeration Define. */
/* Clock ID for Primary I2S IBIT */
 /*内部时钟 master模式 */
#define Q6AFE_LPASS_CLK_ID_PRI_MI2S_IBIT                          0x100
 /*外部时钟 slave模式 */
/* Clock ID for Primary I2S EBIT */
#define Q6AFE_LPASS_CLK_ID_PRI_MI2S_EBIT                          0x101
/* Clock ID for Secondary I2S IBIT */
#define Q6AFE_LPASS_CLK_ID_SEC_MI2S_IBIT                          0x102
/* Clock ID for Secondary I2S EBIT */
#define Q6AFE_LPASS_CLK_ID_SEC_MI2S_EBIT                          0x103
/* Clock ID for Tertiary I2S IBIT */
#define Q6AFE_LPASS_CLK_ID_TER_MI2S_IBIT                          0x104
/* Clock ID for Tertiary I2S EBIT */
#define Q6AFE_LPASS_CLK_ID_TER_MI2S_EBIT                          0x105
/* Clock ID for Quartnery I2S IBIT */
#define Q6AFE_LPASS_CLK_ID_QUAD_MI2S_IBIT                         0x106
/* Clock ID for Quartnery I2S EBIT */
#define Q6AFE_LPASS_CLK_ID_QUAD_MI2S_EBIT                         0x107

        如连接上 MI2S 后端的PCM流打开或者关闭,操作函数startup和shutdown被调用,会根据当前主从模式和mi2s_rx_cfg重新计算afe_clk_set,并调用q6afe.c的afe_set_lpass_clk_cfg发送给ADSP。

vendor/qcom/opensource/audio-kernel/asoc/sm6150.c

static struct afe_clk_set mi2s_clk[MI2S_MAX] = {
...
    {
        AFE_API_VERSION_I2S_CONFIG,
        Q6AFE_LPASS_CLK_ID_TER_MI2S_IBIT,
        Q6AFE_LPASS_IBIT_CLK_1_P536_MHZ,
        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
        0,
    },
...

};

static int msm_mi2s_snd_startup(struct snd_pcm_substream *substream)
{
..    
           if (!intf_conf->msm_is_mi2s_master) {  //从模式
            mi2s_clk[index].clk_id = mi2s_ebit_clk[index];
            fmt = SND_SOC_DAIFMT_CBM_CFM;
        }
        ret = msm_mi2s_set_sclk(substream, true);
        if (ret < 0) {
            dev_err(rtd->card->dev,
                "%s: afe lpass clock failed to enable MI2S clock, err:%d\n",
                __func__, ret);
            goto clean_up;
        }
  ...


}
计算公式为:
+msm_mi2s_set_sclk
    +update_mi2s_clk_val
        +mi2s_clk[dai_id].clk_freq_in_hz =
  mi2s_rx_cfg[dai_id].sample_rate * 2 * bit_per_sample;
        
static void msm_mi2s_snd_shutdown(struct snd_pcm_substream *substream)
{
...
ret = msm_mi2s_set_sclk(substream, false);
..

} 

2.7 MCLK配置和解析

SM6125支持两路MCLK,分别为:

■ MCLK1 GPIO_119

■ MCLK2 GPIO_118

        MCK与MI2S没有绝对的对应关系,可以自由组合使用,主要为有些没有晶振的外置codec提供时钟源。MCLK的GPIO配置和调用流程与MI2S类似:

arch\arm64\boot\dts\qcom\trinket-pinctrl.dtsi

i2s_mclk1 {
    i2s_mclk1_sleep: i2s_mclk1_sleep {
        mux {
            pins = "gpio119";
            function = "mclk1";
        };

        config {
            pins = "gpio119";
            drive-strength = <2>;   /* 2 mA */
            bias-pull-down;         /* PULL DOWN */
        };
    };
    i2s_mclk1_active: i2s_mclk1_active {
        mux {
            pins = "gpio119";
            function = "mclk1";
        };

        config {
            pins = "gpio119";
            drive-strength = <16>;   /* 8 mA */
            bias-pull-up;         /* PULL UP */
            output-high;
        };
    };
};

arch/arm64/boot/dts/qcom/msm-audio-lpass.dtsi

    tdm_tert_rx: qcom,msm-dai-tdm-tert-rx {
...
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&ter_mi2s_tdm_auxpcm_active &ter_mi2s_tdm_auxpcm_din_active
            &ter_mi2s_tdm_auxpcm_dout_active &i2s_mclk1_active>;
    pinctrl-1 = <&ter_mi2s_tdm_auxpcm_sleep &ter_mi2s_tdm_auxpcm_din_sleep
            &ter_mi2s_tdm_auxpcm_dout_sleep &i2s_mclk1_sleep>;
...
}

        先在设备树中增加一个属性qcom,msm-mi2s-ext-mclk,用来配置当前MI2S是否带有MCLK。qcom,msm-mi2s-ext-mclk设置为1表示带有MCLK,<>排位号对应MI2S设备号。

arch/arm64/boot/dts/qcom/trinket-audio.dtsi
sm6150_auto_snd{
    ..
    qcom,msm-mi2s-ext-mclk = <0>, <0>, <1>, <0>, <0>;
    ..
}

修改Machine driver,解析和存储qcom,msm-mi2s-ext-mclk属性值。

vendor/qcom/opensource/audio-kernel/asoc/sm6150.c
@@ -81,6 +81,7 @@ struct mi2s_conf {
     struct mutex lock;
     u32 ref_cnt;
     u32 msm_is_mi2s_master;
        +    u32 msm_is_ext_mclk;    
 };

  @@ -7559,6 +7655,19 @@ static void msm_i2s_auxpcm_init(struct platform_device *pdev)
                 mi2s_master_slave[count];
         }
     }
+
+    ret = of_property_read_u32_array(pdev->dev.of_node,
+            "qcom,msm-mi2s-ext-mclk",
+            mi2s_ext_mclk, MI2S_MAX);
+    if (ret) {
+        dev_dbg(&pdev->dev, "%s: qcom,msm-mi2s-ext-mclk in DT node\n",
+            __func__);
+    } else {
+        for (count = 0; count < MI2S_MAX; count++) {
+            pdata->mi2s_intf_conf[count].msm_is_ext_mclk =
+                mi2s_ext_mclk[count];
+        }
+    }
 } 
 
 static void msm_i2s_auxpcm_deinit(struct platform_device *pdev)
@@ -7571,6 +7680,7 @@ static void msm_i2s_auxpcm_deinit(struct platform_device *pdev)
         mutex_destroy(&pdata->mi2s_intf_conf[count].lock);
         pdata->mi2s_intf_conf[count].ref_cnt = 0;
         pdata->mi2s_intf_conf[count].msm_is_mi2s_master = 0;
+        pdata->mi2s_intf_conf[count].msm_is_ext_mclk = 0;
     }
 }

        同样的,MCLK的实际是由ADSP进行控制的,我们需要将MCLK的信息传递给ADSP。修改Machine driver,增加mi2s_mclk afe_clk_set,用于定义和修改时钟设置信息。

vendor/qcom/opensource/audio-kernel/asoc/sm6150.c
struct auxpcm_conf {
@@ -96,6 +97,49 @@ static u32 mi2s_ebit_clk[MI2S_MAX] = {
     Q6AFE_LPASS_CLK_ID_QUI_MI2S_EBIT
 };
 
+static struct afe_clk_set mi2s_mclk[MI2S_MAX] = {
+    {
+        AFE_API_VERSION_I2S_CONFIG,
+        Q6AFE_LPASS_CLK_ID_MCLK_3,
+        Q6AFE_LPASS_OSR_CLK_9_P600_MHZ,
+        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
+        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
+        0,
+    },
+    {
+        AFE_API_VERSION_I2S_CONFIG,
+        Q6AFE_LPASS_CLK_ID_MCLK_1,
+        Q6AFE_LPASS_OSR_CLK_12_P288_MHZ,
+        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
+        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
+        0,
+    },
+    {
+        AFE_API_VERSION_I2S_CONFIG,
+        Q6AFE_LPASS_CLK_ID_MCLK_1,
+        Q6AFE_LPASS_IBIT_CLK_12_P288_MHZ,
+        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
+        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
+        0,
+    },
+    {
+        AFE_API_VERSION_I2S_CONFIG,
+        Q6AFE_LPASS_CLK_ID_MCLK_1,
+        Q6AFE_LPASS_OSR_CLK_9_P600_MHZ,
+        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
+        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
+        0,
+    },
+    {
+        AFE_API_VERSION_I2S_CONFIG,
+        Q6AFE_LPASS_CLK_ID_QUI_MI2S_OSR,
+        Q6AFE_LPASS_OSR_CLK_9_P600_MHZ,
+        Q6AFE_LPASS_CLK_ATTRIBUTE_COUPLE_NO,
+        Q6AFE_LPASS_CLK_ROOT_DEFAULT,
+        0,
+    }
+};
+

        在打开和关闭PCM流时,MCLK可以被使能或者关闭。

vendor/qcom/opensource/audio-kernel/asoc/sm6150.c
+static int msm_mi2s_set_mclk(struct snd_pcm_substream *substream, bool enable)
+{
+    int ret = 0;
+    struct snd_soc_pcm_runtime *rtd = substream->private_data;
+    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
+    int port_id = 0;
+    int index = cpu_dai->id;
+
+    port_id = msm_get_port_id(rtd->dai_link->id);
+    if (port_id < 0) {
+        dev_err(rtd->card->dev, "%s: Invalid port_id\n", __func__);
+        ret = port_id;
+        goto err;
+    }
+
+    mi2s_mclk[index].enable = enable;
+    
+    ret = afe_set_lpass_clock_v2(port_id,
+                     &mi2s_mclk[index]);
+    if (ret < 0) {
+        dev_err(rtd->card->dev,
+            "%s: afe lpass clock failed for port 0x%x , err:%d\n",
+            __func__, port_id, ret);
+        goto err;
+    }
+
+err:
+    return ret;
+}
+

@@ -5015,6 +5093,12 @@ static int msm_mi2s_snd_startup(struct snd_pcm_substream *substream)
             goto clk_off;
         }
 
+        if (intf_conf->msm_is_ext_mclk) {
+            pr_debug("%s: Enabling mclk, clk_freq_in_hz = %u\n",
+                __func__, mi2s_mclk[index].clk_freq_in_hz);
+            msm_mi2s_set_mclk(substream, true);
+        }
+

@@ -5061,6 +5149,14 @@ static void msm_mi2s_snd_shutdown(struct snd_pcm_substream *substream)
             pr_err("%s:clock disable failed for MI2S (%d); ret=%d\n",
                 __func__, index, ret);
 
+
+        if (intf_conf->msm_is_ext_mclk) {
+            pr_debug("%s: Disabling mclk, clk_freq_in_hz = %u\n",
+                 __func__, mi2s_mclk[index].clk_freq_in_hz);
+            msm_mi2s_set_mclk(substream, false);
+
+        }
+

Tips:部分外置codec mclk需要常开,也有些codec mclk起来后才能够通过i2c设置寄存器,因此有时候还需修改ASOC流程,使MCLK启动时序与codec要求符合。

2.8 波形实测

test_audio.wav左声道数据为0,右声道数据为0xaaaa

tinymix 'TERT_MI2S_RX Audio Mixer MultiMedia1' 1

tinymix "TERT_MI2S_RX Format" S16_LE

tinyplay /sdcard/test_audio.wav

如上图,可以看出左声道一直为0,右声道实际数据不固定为0xAAAA,通过QXDM抓音频dump数据看出,数据经过ADSP COPP阶段算法处理过了

右声道数据COPP处理前0x1531节点
hexdump *.hdf.0x1531.pcm.0x150.0x100.0x2.rx.48k.raw
0000000 0000 0000 0000 0000 0000 0000 0000 0000

00012c0 aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa
*
0129300

右声道数据COPP处理后0x1586节点
hexdump *.hdf.0x1586.pcm.0x12.0x1104.0x2.rx.48k.wav
0003f30 cbcd cbcd cbcd cbcd cbcd cbcd cbcd cb32
0003f40 cb32 cb32 cb32 cb32 cb32 cb32 cb32 ca98
0003f50 ca98 ca98 ca98 ca98 ca98 ca98 ca98 ca02
0003f60 ca02 ca02 ca02 ca02 ca02 ca02 ca02 c970
0003f70 c970 c970 c970 c970 c970 c970 c970 c8df

猜你喜欢

转载自blog.csdn.net/Q_Lee/article/details/131406375