一、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函数。
static int simple_probe(struct platform_device *pdev) { 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); // 动态分配struct asoc_simple_priv数据结构 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; }
该函数的主要作用是解析设备树节点数据或用户传递进来的配置信息,初始化并注册 ALSA SoC声卡设备。
具体来说,该函数的主要执行步骤如下:
- 分配asoc_simple_priv结构体和 snd_soc_card 结构体,并将asoc_simple_priv结构体通过simple_priv_to_card 函数转换为snd_soc_card结构体实例;
- 设置 snd_soc_card 结构体各个参数的值,包括 card 名称、所属设备、probe 函数等;
- 解析设备树节点中的数据或用户传入的配置信息,填充 dais设备接口、codecs编解码器、platforms平台相关信息,以及音频卡名称、数据格式等变量;
- 注册 ALSA SoC声卡设备;
- 调用 simple_get_dais_count 函数获取dai link 数量。
- 调用 asoc_simple_init_priv 函数初始化 priv,并根据 li 中得到的dai link 数量,分配 dai_link 数组和 dai_props 数组,然后将 dai_link 数组中的各个元素填充为不同的 dai 配置信息;
- 根据平台设备节点 np 是否存在以及平台设备是否可用,来解析 dev->platform_data 或设备树节点中音频卡的信息并填充到 card 和 dai_link 中;
- 设置 snd_soc_card 的 priv 数据字段为 asoc_simple_priv 结构体指针 priv;
- 注册 ALSA SoC声卡设备,并返回执行结果。如果执行失败,则会进行清理工作,如清理已经注册的资源,释放资源,返回错误码;
二、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,所以这个我认为是多余的。