Rockchip RK3399 - rt5651 ALSA Machine驱动

发布时间 2023-06-11 23:30:44作者: 大奥特曼打小怪兽

一、Machine驱动

Machine driver描述了如何控制CPU数字音频接口(DAI)和Codec,使得互相配合在一起工作,Machine驱动代码位于sound/soc/generic/simple-card.c文件。

1.1 设备节点rt5651-sound

我们在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加设备节点rt5651-sound;

rt5651_card: rt5651-sound {
    status = "okay";
    compatible = "simple-audio-card";
    pinctrl-names = "default";
    pinctrl-0 = <&hp_det>;

    simple-audio-card,name = "realtek,rt5651-codec";
    simple-audio-card,format = "i2s";
    simple-audio-card,mclk-fs = <256>;
    simple-audio-card,hp-det-gpio = <&gpio4 28 GPIO_ACTIVE_HIGH>;

    simple-audio-card,widgets =
        "Microphone", "Mic Jack",
        "Headphone", "Headphone Jack";
    simple-audio-card,routing =
        "Mic Jack", "MICBIAS1",
        "IN1P", "Mic Jack",
        "Headphone Jack", "HPOL",
        "Headphone Jack", "HPOR";

    simple-audio-card,cpu {
        sound-dai = <&i2s0>;
    };
    simple-audio-card,codec {
        sound-dai = <&rt5651>;
    };
};

其中:

  • status:指定设备状态为“正常”,表示该设备状态为正常运行;
  • compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
  • pinctrl-names:指定设备pinctrl配置集合,例如“default”表示默认配置;
  • pinctrl-0:设置default状态对应的引脚配置为hp_det,hp_det引脚配置节将GPIO4_D4配置为基本输入输出、电气特性为上拉配置;

接下来是simple-audio-card的各个字段设置:

  • simple-audio-card,name:指定声卡的名称为“realtek,rt5651-codec”;
  • simple-audio-card,format:指定音频编码格式为“I2S”,即使用I2S接口传输音频数据;
  • simple-audio-card,mclk-fs:指定主时钟频率MCLK与系统频率比值,例如256表示主时钟频率为系统频率的256倍;
  • simple-audio-card,hp-det-gpio:指定耳机检测功能使用GPIO4_D4引脚,并且检测到耳机连接时该引脚电平为高;
  • simple-audio-card,widgets:在ALSA驱动中,使用widget描述声卡设备的一个功能模块,这里定义了2个widget;
    • Mic Jack:代表麦克风;
    • Headphont Jack:代表3.5mm耳机座;
  • simple-audio-card,routing:将 CPU DAI 和 Codec DAI 连接起来后,还需要设置Codec的input和output路径,对应的术语就是routing;每个条目都是一对字符串,第一个是目的(sink),第二个是源(source);

最后配置cpu和codec端点,用于描述cpu dai和code cdai;

  • simple-audio-card,cpu:指定cpu接入音频编解码的dai(数字化接口);这里配置为RK3399的I2S0接口;
  • simple-audio-card,codec:指定编解码音频接入cpu的dai(数字化接口);这里配置为rt5651;

音频数据通过RK3399的I2S0接口传输到RT5651,再通过耳机、麦克风等端口输出或输入音频信号。

关于设备节点属性可以参考文档:

  • Documentation/devicetree/bindings/sound/simple-card.taml;
  • Documentation/devicetree/bindings/sound/widgets.txt;
1.1.1 引脚配置节点hp_det

在pinctrl设备节点新增hp_det引脚配置节点:

headphone {
        hp_det: hp-det {
                rockchip,pins =
                        <4 RK_PD4 RK_FUNC_GPIO &pcfg_pull_up>;
        };
};

此处配置GPIO4_D4引脚功能为GPIO,电气特性为pcfg_pull_up,表示上拉配置。

我们在前面的文章中已经介绍过RK3399 GPIO4_D4引脚连接的ALC5651的IRQ引脚,用于检测耳机的插入。

1.2 simple-audio-card驱动

我们定位到文件sound/soc/generic/simple-card.c,simple-card.c不是单板相关的东西,simple-audio-card 是一个 Machine driver。

Machine driver最重要的事情是:构造并注册struct snd_soc_card,可以认为一个snd_soc_card就代表着一个soc声卡。

在simple-card.c文件中定义了platform driver:

static const struct of_device_id simple_of_match[] = {  // 用于匹配设备树
        { .compatible = "simple-audio-card", },
        { .compatible = "simple-scu-audio-card",
          .data = (void *)DPCM_SELECTABLE },
        {},
};
MODULE_DEVICE_TABLE(of, simple_of_match);

static struct platform_driver asoc_simple_card = {
        .driver = {
                .name = "asoc-simple-card",
                .pm = &snd_soc_pm_ops,
                .of_match_table = simple_of_match,
        },
        .probe = simple_probe,
        .remove = simple_remove,
};

module_platform_driver(asoc_simple_card);

在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是执行simple_probe函数。

        struct asoc_simple_priv *priv;
        struct device *dev = &pdev->dev;
        struct device_node *np = dev->of_node;
        struct snd_soc_card *card;
        struct link_info li;
        int ret;

        /* Allocate the private data and the DAI link array */
        priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
        if (!priv)
                return -ENOMEM;

        card = simple_priv_to_card(priv);
        card->owner             = THIS_MODULE;
        card->dev               = dev;
        card->probe             = simple_soc_probe;

        memset(&li, 0, sizeof(li));
        simple_get_dais_count(priv, &li);
        if (!li.link || !li.dais)
                return -EINVAL;

        ret = asoc_simple_init_priv(priv, &li);
        if (ret < 0)
                return ret;

        if (np && of_device_is_available(np)) {

                ret = simple_parse_of(priv);
                if (ret < 0) {
                        if (ret != -EPROBE_DEFER)
                                dev_err(dev, "parse error %d\n", ret);
                        goto err;
                }

        } else {
                struct asoc_simple_card_info *cinfo;
                struct snd_soc_dai_link_component *codecs;
                struct snd_soc_dai_link_component *platform;
                struct snd_soc_dai_link *dai_link = priv->dai_link;
                struct simple_dai_props *dai_props = priv->dai_props;

                int dai_idx = 0;

                cinfo = dev->platform_data;
                if (!cinfo) {
                        dev_err(dev, "no info for asoc-simple-card\n");
                        return -EINVAL;
                }
                if (!cinfo->name ||
                    !cinfo->codec_dai.name ||
                    !cinfo->codec ||
                    !cinfo->platform ||
                    !cinfo->cpu_dai.name) {
                        dev_err(dev, "insufficient asoc_simple_card_info settings\n");
                        return -EINVAL;
                }

                dai_props->cpu_dai      = &priv->dais[dai_idx++];
                dai_props->codec_dai    = &priv->dais[dai_idx++];

                codecs                  = dai_link->codecs;
                codecs->name            = cinfo->codec;
                codecs->dai_name        = cinfo->codec_dai.name;

                platform                = dai_link->platforms;
                platform->name          = cinfo->platform;

                card->name              = (cinfo->card) ? cinfo->card : cinfo->name;
                dai_link->name          = cinfo->name;
                dai_link->stream_name   = cinfo->name;
                dai_link->cpu_dai_name  = cinfo->cpu_dai.name;
                dai_link->dai_fmt       = cinfo->daifmt;
                dai_link->init          = asoc_simple_dai_init;
                memcpy(dai_props->cpu_dai, &cinfo->cpu_dai,
                                        sizeof(*dai_props->cpu_dai));
                memcpy(dai_props->codec_dai, &cinfo->codec_dai,
                                        sizeof(*dai_props->codec_dai));
        }

        snd_soc_card_set_drvdata(card, priv);

        asoc_simple_debug_info(priv);

        ret = devm_snd_soc_register_card(dev, card);
        if (ret < 0)
                goto err;

        return 0;
err:
        asoc_simple_clean_reference(card);

        return ret;
}

二、Platform驱动

Platfrom driver提供了配置/使能SoC音频接口的能力;Plaftrom驱动分为两个部分:snd_soc_platform_driver、snd_soc_dai_driver。

  • snd_soc_platform_driver:负责管理音频数据,把音频数据通过DMA或其他操作传送至CPU DAI中;
  • snd_soc_dai_driver:负责完成SoC一侧的DAI参数配置,同时也会通过一定的路径把必要的DMA等参数与snd_soc_platform_driver进行交互;

驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。

2.1 设备节点i2s0

设备节点i2s0定义在arch/arm64/boot/dts/rockchip/rk3399.dts文件:

i2s0: i2s@ff880000 {
        compatible = "rockchip,rk3399-i2s", "rockchip,rk3066-i2s";
        reg = <0x0 0xff880000 0x0 0x1000>;
        rockchip,grf = <&grf>;
        interrupts = <GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH 0>;
        dmas = <&dmac_bus 0>, <&dmac_bus 1>;
        dma-names = "tx", "rx";
        clock-names = "i2s_clk", "i2s_hclk";
        clocks = <&cru SCLK_I2S0_8CH>, <&cru HCLK_I2S0_8CH>;
        pinctrl-names = "default";
        pinctrl-0 = <&i2s0_8ch_bus>;
        power-domains = <&power RK3399_PD_SDIOAUDIO>;
        #sound-dai-cells = <0>;
        status = "disabled";
};

这是 Rockchip RK3399 SoC中I2S0设备节点描述。它包括以下属性:

  • compatible: 指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
  • reg: 指定I2S0控制器的基地址和地址空间大小,从 0xff880000 到 0xff881000 共有 0x1000 个字节的寄存器空间,其中0xff880000 为I2S0寄存器基地址为;
  • rockchip,grf:设置为grf设备节点,用于定位并访问与I2S0控制器相关的GRF寄存器;
  • interrupts: 指定I2S控制器的中断号为GIC_SPI 39,并且取值方式为IRQ_TYPE_LEVEL_HIGH,意味着中断信号为高电平触发;
  • dmas: 指定数据传输时使用的DMA控制器,第一个表示TX数据使用的DMA控制器,第二个表示 RX 数据使用的 DMA 控制器;
  • dma-names: 分别对应 "tx" 和 "rx" 的 DMA 名称;
  • clock-names: 指定I2S0时钟名称,"i2s_clk"表示I2S0控制器时钟,"i2s_hclk" 表示I2S0 BUS时钟;
  • clocks: 指定I2S0控制器时钟使用SCLK_I2S0_8CH,BUS时钟使用 HCLK_I2S0_8CH;
  • pinctrl-names: 指定设备pinctrl配置集合,例如“default”表示默认配置;
  • pinctrl-0: 设置default状态对应的引脚配置为i2s0_8ch_bus,这里主要配置I2S0相关引脚复用为I2S功能;
  • power-domains: 指定设备隶属于的电源域,这里是 RK3399_PD_SDIOAUDIO;
  • #sound-dai-cells: 表示定义这个节点的sound DAI数据单元格的数量,这里为0表示没有单元格;
  • status: 表示设备状态,这里 "disabled" 表示该设备当前是禁用状态;

我们需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加如下属性,启用I2S0控制器:

&i2s0 {
        status = "okay";
};

关于设备节点属性可以参考文档Documentation/devicetree/bindings/sound/rockchip-i2s.txt。

而RK3399 I2S控制器驱动代码位于sound/soc/rockchip/rockchip_i2s.c文件。

2.1.1 引脚配置节点i2s0_8ch_bus

引脚配置节点i2s0_8ch_bus定义在pinctrl设备节点下:

i2s0 {
        i2s0_2ch_bus: i2s0-2ch-bus {
                rockchip,pins =
                        <3 RK_PD0 1 &pcfg_pull_none>,
                        <3 RK_PD1 1 &pcfg_pull_none>,
                        <3 RK_PD2 1 &pcfg_pull_none>,
                        <3 RK_PD3 1 &pcfg_pull_none>,
                        <3 RK_PD7 1 &pcfg_pull_none>,
                        <4 RK_PA0 1 &pcfg_pull_none>;
        };

        i2s0_8ch_bus: i2s0-8ch-bus {
                rockchip,pins =
                        <3 RK_PD0 1 &pcfg_pull_none>,
                        <3 RK_PD1 1 &pcfg_pull_none>,
                        <3 RK_PD2 1 &pcfg_pull_none>,
                        <3 RK_PD3 1 &pcfg_pull_none>,
                        <3 RK_PD4 1 &pcfg_pull_none>,
                        <3 RK_PD5 1 &pcfg_pull_none>,
                        <3 RK_PD6 1 &pcfg_pull_none>,
                        <3 RK_PD7 1 &pcfg_pull_none>,
                        <4 RK_PA0 1 &pcfg_pull_none>;
        };
};

这里我们只关注i2s0_8ch_bus引脚配置节点,这里定义了9个引脚,管脚与具体的功能和电气特性如下:

  • GPIO3_PD0:功能复用为I2S0_SCLK,电气特性配置为pcfg_pull_none;
  • GPIO3_PD1:功能复用为I2S0_LRCK_RX,电气特性配置为pcfg_pull_none;
  • GPIO3_PD2:功能复用为I2S0_LRCK_TX,电气特性配置为pcfg_pull_none;
  • GPIO3_PD3:功能复用为I2S0_SDI0,电气特性配置为pcfg_pull_none;
  • GPIO3_PD4:功能复用为I2S0_SDI1/I2S0_SDO3,电气特性配置为pcfg_pull_none;
  • GPIO3_PD5:功能复用为I2S0_SDI2/I2S0_SDO2,电气特性配置为pcfg_pull_none;
  • GPIO3_PD6:功能复用为I2S0_SDI3/I2S0_SDO1,电气特性配置为pcfg_pull_none;
  • GPIO3_PD7:功能复用为I2S0_SDO0,电气特性配置为pcfg_pull_none;
  • GPIO4_PA0:功能复用为I2S0_MCLK,电气特性配置为pcfg_pull_none;

2.2 I2S控制器驱动

 

三、Codec驱动

Codec driver提供了配置/使能Codec的能力,驱动代码位于sound/soc/codecs/rt5651.c文件、

3.1 设备节点rt5651

我们在arch/arm64/boot/dts/rockchip/rk3399-evb.dts文件添加rt5651设备节点,该节点位于i2c1节点下:

 

&i2c1 {
    status = "okay";
    i2c-scl-rising-time-ns = <150>;
    i2c-scl-falling-time-ns = <30>;
    clock-frequency = <200000>;

    rt5651: rt5651@1a {
        #sound-dai-cells = <0>;
        compatible = "rockchip,rt5651";
        reg = <0x1a>;
        clocks = <&cru SCLK_I2S_8CH_OUT>;
        clock-names = "mclk";
        pinctrl-names = "default";
        pinctrl-0 = <&i2s_8ch_mclk>;
        hp-det-gpio = <&gpio4 28 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

 其中:

  • status :指定设备状态为“正常”,表示该设备状态为正常运行;
  • i2c-scl-rising-time-ns:定义了SCL信号上升时间的最小值,单位是纳秒;
  • i2c-scl-falling-time-ns:定义了SCL信号下降时间的最小值,单位是纳秒;
  • clock-frequency:定义了I2C总线的时钟频率,单位是赫兹;

接着定义rt5651音频编解码器的设备树节点,其名称为 rt5651,内部的设备地址为0x1a;

  • #sound-dai-cells :指定DAI的单元格数量,这里为0表示不需要添加额外的 DAI 单元格(比如,不需要配置 SAI、PCM 等标志);
  • compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
  • reg:指定了rt5651设备在I2C控制器上的地址;
  • clocks:定义了音频编解码器使用的时钟,这里使用了由 PPL(Phase Locked Loop)产生的 SCLK_I2S_8CH_OUT 时钟信号;
  • clock-names:定义了音频编解码器使用的时钟的名称,这里为mclk,表示主时钟;
  • pinctrl-names:指定设备pinctrl配置集合,例如“default”表示默认配置;
  • pinctrl-0:设置default状态对应的引脚配置为i2s0_8ch_mclk,这里主要配置I2C1相关引脚复用为I2C功能;
  • status :指定设备状态为“正常”,表示该设备状态为正常运行;
3.1.1 引脚配置节点i2s0_8ch_mclk

在arch/arm64/boot/dts/rockchip/rk3399.dts文件添加引脚配置节点i2s0_8ch_bus,定义在pinctrl设备节点下:

i2s0 {
        i2s0_8ch_mclk: i2s0-8ch-mclk {
                rockchip,pins =
                        <3 RK_PC1 2 &pcfg_pull_none>;
        };
        
        ....
}

此处配置GPIO3_C1引脚功能为MAC_TXCLK,电气特性为pcfg_pull_none。

我们在前面的文章中介绍的ALC565接线中并没有使用GPIO3_C1,所以这个我认为是多余的。

3.2 Codec驱动