Rockchip RK3399 - Machine驱动(simple-audio-card)
目录
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
Machine driver描述了如何控制platform、codec、cpu dai(Digital Audio Interface,数字音频接口)和codec dai,使得互相配合在一起工作,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", "Headphones"; simple-audio-card,routing = "Mic Jack", "micbias1", "IN2P", "Mic Jack", "IN2N", "Mic Jack", "Headphones", "HPOL", "Headphones", "HPOR"; simple-audio-card,cpu { sound-dai = <&i2s0>; }; simple-audio-card,codec { sound-dai = <&rt5651>; }; };
1.1.1 常用属性
(1) status:指定设备状态为“正常”,表示该设备状态为正常运行;
(2) compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
(3) pinctrl-names:指定设备pinctrl配置集合,例如“default”表示默认配置;
(4) pinctrl-0:设置default状态对应的引脚配置为hp_det,hp_det引脚配置节将GPIO4_D4配置为基本输入输出、电气特性为上拉配置;
NanoPC-T4开发板中RK3399 GPIO4_D4引脚连接到了耳机插孔的2号引脚,用于检测耳机的插入;当耳机插入时。耳机插头的金属会碰到检测引脚,使得检测引脚的电平发生改变,从而触发中断。这样就可以在中断处理函数中读取GPIO口的值,进一步判断是耳机插入还是拔出。
此外,如果仔细观察上图,我们会发现ACL5651的IRQ引脚也连接到了RK3399 GPIO4_D4引脚;对于ALC5651来说,IRQ引脚可以用来检测麦克风的插入/输出,因此可以通过配置了ALC5651 IRQ引脚用于麦克风的检测,从而将IRQ信号传送到RK3399 GPIO4_D4引脚,从而实现RK3399对麦克风的检测。
1.1.2 simple-audio-card
接下来是simple-audio-card的各个属性设置:
(1) simple-audio-card,name:指定声卡的名称为“realtek,rt5651-codec”;
(2) simple-audio-card,format:指定数字音频接口格式为“I2S”,即使用I2S接口传输音频数据;
此外还支持的数字音频接口格式有:right_j、left_j、dsp_a、dsp_b、ac97、pdm、msb、lsb。
(3) simple-audio-card,mclk-fs:指定主时钟频率MCLK与采样频率之前的比值,例如256表示主时钟频率为系统频率的256倍;
(4) simple-audio-card,hp-det-gpio:用于指定耳机检测使用的引脚,对于NanoPC-T4开发板配置为GPIO4_D4引脚,用于检测耳机的插入和拔出;
(5) simple-audio-card,widgets:在ALSA驱动中,使用widget描述具有路径有电源管理的kcontrol,每个条目都是一对字符串:
- 第一个是widget模板名称,在Machine驱动中这个是确定的只有那么几种widget,Microphone(表示麦克风)、Headphone(表示耳机)、Speaker(表示扬声器)、Line(线路);
- 第二个是widget实例名称,可以自由定义;
"Microphone", "Mic Jack":名字为“Microphone”的widget被重命名为“Mic Jack”,该widget定义在Machine驱动中;
SND_SOC_DAPM_MIC("Microphone", NULL),
"Headphone", "Headphones":名字为“Microphone”的widget被重命名为“Headphones”(这个名字不可以随便改,因为在为ASoC声卡中创建一个带有pin的jack时候,指定了pin的名称为"Headphones"),该widget定义在Machine驱动中;
SND_SOC_DAPM_HP("Headphone", NULL)
(6) simple-audio-card,routing:配置与Codec(ALC5651)物理输入引脚、物理输出端引脚连接的路径;每个条目都是一对字符串,第一个是目的(sink),第二个是源(source);
"Mic Jack", "micbias1": 将名字为“micbias1”的widget连接到名字为“Mic Jack”的widget,其中名字为“micbias1”的widget定义在Codec驱动中;
SND_SOC_DAPM_SUPPLY("micbias1", RT5651_PWR_ANLG2, // ALC5651电源控制寄存器4 地址0x64 RT5651_PWR_MB1_BIT, 0, NULL, 0) // 位11用于MICBIAS1 Power Control电源控制,0:下电 1上电
从电路图中知道ALC5651 MICBIAS1引脚(输出引脚)连接到麦克风,并为麦克风提供偏置电压;
"IN2P", "Mic Jack":将名字为“Mic Jack”的widget连接到名字为“IN2P”的widget,其中名字为“IN2P”的widget定义在Codec驱动中; 表示将麦克风连接到音频输入IN2P引脚,这表示通过麦克风输入录制声音时使用此配置;
SND_SOC_DAPM_INPUT("IN2P") // IN2P为ALC5651麦克风2输入引脚
"IN2N", "Mic Jack":将名字为“Mic Jack”的widget连接到名字为“IN2N”的widget,其中名字为“IN2N”的widget定义在Codec驱动中; 表示将麦克风连接到音频输入IN2N引脚,这表示通过麦克风输入录制声音时使用此配置;
SND_SOC_DAPM_INPUT("IN2N") // IN2N为ALC5651麦克风2输入引脚
如果需要使用麦克风3(前提是开发板麦克3连接着耳机麦克风的话)进行录音,则配置:
"IN3P", "Mic Jack"
注意:可以同时配置多路麦克风,那么在录音的时候,多路麦克风音频信号会通过Mixer混音器进行处理。
因此可以在Machine驱动中可以构建输入端路径:micbias1 --> Mic Jack --> IN2P/IN2N;关于IN2P/IN2N之后的路径在Codec驱动中构建;
"Headphones", "HPOL":将名字为“HPOL”的widget连接到名字为“Headphones”的widget,其中名字为“HPOL”的widget定义在Codec驱动中;用于将左声道的耳机声音输出连接到耳机;
SND_SOC_DAPM_OUTPUT("HPOL") // HPOL为ALC5651耳机输出引脚
"Headphones", "HPOR": 将名字为“HPOR”的widget连接到名字为“Headphones”的widget,其中名字为“HPOR”的widget定义在Codec驱动中;用于将左声道的耳机声音输出连接到耳机;
SND_SOC_DAPM_OUTPUT("HPOR") // HPOR为ALC5651耳机输出引脚
因此可以在Machine驱动中可以构建输出端路径:HPOL/HPOR--> Headphones ;关于HPOL/HPOR之前的路径在Codec驱动中构建;
1.1.3 dai配置
最后配置cpu和codec端点,用于描述cpu dai和code cdai;
(1) simple-audio-card,cpu:指定cpu接入音频编解码的dai;这里配置为&i2s0,即i2s0设备节点的句柄;那么i2s0设备节点是什么呢,这个我们在Rockchip RK3399 - Platform驱动(DMA&i2s0)中介绍。
(2) simple-audio-card,codec:指定编解码音频接入cpu的dai;这里配置为&rt5651,即rt5651设备节点的句柄;那么rt5651设备节点是什么呢?这个我们在Rockchip RK3399 - Codec驱动( Realtek ALC5651)中介绍。
看到这里我们会思考一个问题,sound-dai和sound-dai分别配置了cpu端以及dai端的dai,然而这里配置的内容确是一个设备节点句柄。
这里以rt5651为例,rt5651有两个dai,那问题来了,在创建音频数据链路的时候会选择哪一个dai呢?
- codec驱动中定义了两个dai,其名字分别为rt5651-aif1、rt5651-aif2,默认会选择第一个dai,也就是rt5651-aif1;
- 而platform驱动中定义了一个dai,同理默认会选择第一个dai,也就是ff880000.i2s;
如果需要指定第几个dai,可以采用如下写法,其中port值默认从0开始;
"sound-dai = <&phandle port>"
通过上面的配置我们可以得到一条音频数据链路两端的dai分别为:ff880000.i2s和rt5651-aif1。音频数据通过RK3399的I2S0接口传输到ALC5651,再通过耳机、麦克风等端口输出或输入音频信号。
实际上我们在设备树中使用simple-audio-card,cpu、simple-audio-card,codec来描述音频数据链路,这种只适用于Machine中只有一条音频数据链路的情况。
对于Machine中存在多条音频数据链路音频链路,以及更多设备节点属性信息可以参考文档:
- Documentation/devicetree/bindings/sound/simple-card.yaml;
- Documentation/devicetree/bindings/sound/widgets.txt;
- Documentation/devicetree/bindings/sound/rt5651.txt;
1.2 引脚配置节点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,表示上拉配置。
NanoPC-T4开发板中RK3399 GPIO4_D4引脚连接的ALC5651的IRQ引脚,用于检测耳机的插入和拔出。
二、Machine驱动
我们定位到文件sound/soc/generic/simple-card.c,simple-card.c不是单板相关的东西,simple-audio-card 是一个Machine driver。
2.1 platform driver
Machine驱动最重要的事情是:构造并注册struct snd_soc_card。在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 = asoc_simple_probe, .remove = asoc_simple_remove, }; module_platform_driver(asoc_simple_card); // 注册平台驱动asoc_simple_card
2.2 asoc_simple_probe(重点)
在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备platform驱动匹配,会调用到platform_driver里的成员.probe,在这里也就是asoc_simple_probe函数。
static int asoc_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); // 获取成员变量&priv->snd_card card->owner = THIS_MODULE; card->dev = dev; // 设置dev,为平台设备的device card->probe = simple_soc_probe; // 设置probe 该函数会解析设备属性"simple-audio-card,hp-det-gpio",并为之申请GPIO中断
// 并在中断处理函数中检测耳机是插入还是拔出,根据判断结果调用snd_soc_jack_report向上层汇报EV_KEY、EV_SW事件
card->driver_name = "simple-card"; // 设置驱动名称 li = devm_kzalloc(dev, sizeof(*li), GFP_KERNEL); // 动态分配struct link_info数据结构,用于保存音频数据链路信息 if (!li) return -ENOMEM; ret = simple_get_dais_count(priv, li); // 解析rt5651-sound设备节点中音频数据链路相关节点,获取每条音频数据链路cpu、codec、platfrom的数量 if (ret < 0) return ret; if (!li->link) return -EINVAL; ret = asoc_simple_init_priv(priv, li); // 根据每个音频数据链路中的cpu数量、codec数量、platfom数量来初始化priv的成员 if (ret < 0) return ret; if (np && of_device_is_available(np)) { // 使用设备树,走这里 ret = simple_parse_of(priv, li); // 解析rt5651-sound设备节点的属性信息 if (ret < 0) { dev_err_probe(dev, ret, "parse error\n"); goto err; } } else { // 没有使用设备树 struct asoc_simple_card_info *cinfo; struct snd_soc_dai_link_component *cpus; 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; 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; } cpus = dai_link->cpus; cpus->dai_name = cinfo->cpu_dai.name; 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->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); // 设置snd_soc_card的私有数据drvdata为priv asoc_simple_debug_info(priv); // 输出调试信息 ret = devm_snd_soc_register_card(dev, card); // 注册ASoC声卡设备 if (ret < 0) goto err; devm_kfree(dev, li); return 0; err: asoc_simple_clean_reference(card); return ret; }
该函数的主要作用是解析设备树节点数据或用户传递进来的配置信息,初始化并注册 ASoC声卡设备。
具体来说,该函数的主要执行步骤如下:
- 分配asoc_simple_priv结构体,并将通过宏simple_priv_to_card获取其成员变量snd_card,其类型为snd_soc_card;
- 设置snd_soc_card结构体各个参数的值,包括card名称、所属设备、probe函数等;
- 调用 simple_get_dais_count,通过simple_for_each_link函数解析rt5651-sound设备节点,遍历每条音频数据链路并调用两个不同的计数回调函数simple_count_noml和 simple_count_dpcm,用于对不同类型的音频数据链路中的cpu、codec、platfrom数量进行统计,并保存在li变量中;
- 调用 asoc_simple_init_priv根据每个音频数据链路中的cpu数量、codec数量、platfom数量来初始化priv的成员,比如:
- dai_props(同时会初始化每个元素的成员cpus、num.cpus、cpu_dai、codec、num.codecs、codev_dai、platforms、num.platforms);
- dai_link(同时会初始化每个元素的成员cpus、num_cpus、codec、num_codecs、platforms、num_platforms)、dais、dlcs、codec_conf等;
- 此外设置card->dai_link = priv->dai_link;
- 需要注意的是这里仅仅是进行的动态内存分配的操作,并未对具体结构体的成员内容进行初始化;
- 由于使用了设备树,因此调用simple_parse_of函数解析rt5651-sound设备节点的属性信息,比如:
- 解析simple-audio-card,widgets 、simple-audio-card,routing、simple-audio-card,name属性,并将这些信息保存到card数据结构的成员中中;
- 解析simple-audio-card,cpu、simple-audio-card,codec、simple-audio-card,format等属性,并初始化priv->dai_link[i]成员,由于card->dai_link = priv->dai_link ,因此也就是初始化声卡card的音频数据链路dai_link[i]成员cpus、codec、dai_fmt;
- 解析simple-audio-card,mclk-fs属性,并初始化 priv->dai_props[i]->mclk_fs;
- 调用snd_soc_card_set_drvdata设置snd_soc_card的私有数据drvdata为priv;
- 调用devm_snd_soc_register_card注册 ASoC声卡设备,并返回执行结果;
- 如果执行失败,则会进行清理工作,如清理已经注册的资源,释放资源,返回错误码;
2.2.1 simple_soc_probe
ASoC声卡card的probe函数被设置为了simple_soc_probe,在注册ASoC声卡的时候会回调用card->probe函数;
static int simple_soc_probe(struct snd_soc_card *card) { struct asoc_simple_priv *priv = snd_soc_card_get_drvdata(card); int ret; ret = asoc_simple_init_hp(card, &priv->hp_jack, PREFIX); // 创建带有pin的jack,用于耳机检测 if (ret < 0) return ret; ret = asoc_simple_init_mic(card, &priv->mic_jack, PREFIX); // 创建带有pin的jack,用于麦克风检测 if (ret < 0) return ret; ret = asoc_simple_init_aux_jacks(priv, PREFIX); if (ret < 0) return ret; return 0; }
其中asoc_simple_init_hp是对耳机的初始化,asoc_simple_init_mic是对麦克风的初始化,其均定义在include/sound/simple_card_utils.h:
#define asoc_simple_init_hp(card, sjack, prefix) \ asoc_simple_init_jack(card, sjack, 1, prefix, NULL) #define asoc_simple_init_mic(card, sjack, prefix) \ asoc_simple_init_jack(card, sjack, 0, prefix, NULL)
这里都调用asoc_simple_init_jack,只是传入参数不一样。
asoc_simple_init_jack函数位于sound/soc/generic/simple-card-utils.c,下面我们以HP为例进行介绍;
int asoc_simple_init_jack(struct snd_soc_card *card, // 传入ASoC声卡设备 struct asoc_simple_jack *sjack, // 插孔,该函数会进行其成员的初始化 int is_hp, char *prefix, // is_hp为1表示耳机,为0表示麦克风 prefix="simple-audio-card," char *pin) // 传入NULL { struct device *dev = card->dev; // 获取平台设备的设备 struct gpio_desc *desc; char prop[128]; char *pin_name; char *gpio_name; int mask; int error; if (!prefix) prefix = ""; sjack->gpio.gpio = -ENOENT; // 下面通过参数来判断是hp还是mic if (is_hp) { // 这里做了字符串拼接,所以dts中要按照这样设置:simple-audio-card,hp-det-gpio snprintf(prop, sizeof(prop), "%shp-det", prefix); // prop="simple-audio-card,hp-det" pin_name = pin ? pin : "Headphones"; // "Headphones" gpio_name = "Headphone detection"; // GPIO口的名称 mask = SND_JACK_HEADPHONE; // 0x0001 } else { snprintf(prop, sizeof(prop), "%smic-det", prefix); pin_name = pin ? pin : "Mic Jack"; gpio_name = "Mic detection"; mask = SND_JACK_MICROPHONE; } // 根据dts中dev设备节点属性名prop获取属性中描述的GPIO信息,同时配置GPIO口为输入 optional表示可选的,也就是说如果没有指定prop属性,也不会报错 desc = gpiod_get_optional(dev, prop, GPIOD_IN); // 第三个参数表示配置GPIO口为输入 error = PTR_ERR_OR_ZERO(desc); if (error) return error; if (desc) { error = gpiod_set_consumer_name(desc, gpio_name); // 设置GPIO口的label为"Headphone detection" if (error) return error; sjack->pin.pin = pin_name; // "Headphones",根据该名称定为widget sjack->pin.mask = mask; // jack pin可以上报的类型,SND_JACK_HEADPHONE sjack->gpio.name = gpio_name;// GPIO的名称,也是GPIO中断的名称"Headphone detection" sjack->gpio.report = mask; // GPIO可以上报的类型,SND_JACK_HEADPHONE sjack->gpio.desc = desc; sjack->gpio.debounce_time = 150; // 延迟时间 // 创建一个带有pin的jack snd_soc_card_jack_new_pins(card, pin_name, mask, &sjack->jack, // jack可以检测的类型为SND_JACK_HEADPHONE &sjack->pin, 1); // 通过request irq申请耳机插拔中断,在中断处理函数中通过检测上升沿/下降沿判断耳机是插入还是拔出,根据判断结果调用snd_soc_jack_report发送输入事件 snd_soc_jack_add_gpios(&sjack->jack, 1, &sjack->gpio); } return 0; }
这里调用了gpiod_get_optional、snd_soc_card_jack_new_pins、snd_soc_jack_add_gpios等函数,关于jack设备我们在声卡之Jack设备已经介绍过了。
2.2.2 总结
Machine驱动主要做的就是两件事情:
- 构造一个struct snd_soc_dai_link,比如变量名为dai_link,将cpu和codec关联起来;需要初始化成员cpus、codecs、name、stream_name、dai_fmt、init;
- 构造一个struct snd_soc_card,比如变量名为card,并将其注册到ASoC中;
simple_parse_of函数通过解析设备节点rt5651-sound的属性,从而可以初始化card、以及dai_link数据结构,函数执行完毕,各个数据结构之间的关系大致如下图所示:
2.2.3 伪实现
如果我们不使用设备树的话,就可以构建一个platform device、其平台数据platform_data设置为struct asoc_simple_card_info,代码实现大致如下:
/* A Minimal ASoC Sound Card - Single DAI Link */ struct asoc_simple_card_info cinfo = { .name = "ff880000.i2s-rt5651-aif1", .card = "realtek,rt5651-codec", .codec = "rt5651", .platform = NULL, .daifmt = 1, .cpu_dai = {
.name = "ff880000.i2s",
.sysclk = xxxx,
}
.codec_dai = {
.name = "rt5651",
.sysclk = xxxx,
} };
然后asoc_simple_probe函数走通过cinfo去初始化dai_link、card的流程,最后再将card注册到ASoC代码大致如下,因此asoc_simple_probe应该是可以简化为:
/* A Minimal ASoC Sound Card - Single DAI Link */ struct snd_soc_dai_link dai_link = { .name = "ff880000.i2s-rt5651-aif1", .stream_name = "ff880000.i2s-rt5651-aif1", .dai_fmt = 1, .init = asoc_simple_dai_init, .cpus = { .dai_name = "ff880000.i2s", // 用于匹配component dai_list链表中的dai,dai_name设置pdev->dev的名称,pdev为i2s0设备节点转换得到的platform_device },
.num_cpus = 1, .codec = { .name = "rt5651", // 用于匹配全局链表component_list中的component; name设置为pdev->dev的名称,pdev为rt5651设备节点转换得到的platform_device .dai_name = "rt5651-aif1", // 用于匹配component dai_list链表中的dai;dai_name设置为codec dai的名称 },
.num_codecs = 1, .platform = { .name = NULL, }
.num_platforms = 1,
.ops = simple_ops, }; struct snd_soc_card card = { .name = "realtek,rt5651-codec", .dai_link = &dai_link, .num_links = 1,
.num_dapm_widgets = 2,
.dapm_widgers = xxx,
.num_dapm_routes = 4,
.dapm_routes = xxx, }; snd_soc_register_card(&card);
关于非设备树方式的实现,更多详情可以参考理解ALSA(三):从零写ASoC驱动。
后面我们将对asoc_simple_probe函数源码进行的分析,如果对此不感兴趣,可以忽略后面的内容。
三、相关数据结构
在分析源码之前,需要介绍一下Machine驱动中涉及到的数据结构,主要有struct asoc_simple_priv、struct link_info。
3.1 asoc_simple_priv
数据结构asoc_simple_priv定义在include/sound/simple_card_utils.h;
struct asoc_simple_priv { struct snd_soc_card snd_card; struct simple_dai_props { // 每个成员,代表一个音频数据链路,用于描述每个音频链路上所有属性 struct asoc_simple_dai *cpu_dai; // 存储当前链路上所有的cpu_dai 指向一个数组,数组长度由num.cpus决定 struct asoc_simple_dai *codec_dai; // 存储当前链路上所有的codec_dai 指向一个数组,数组长度由num.codecs决定 struct snd_soc_dai_link_component *cpus; // 存储当前链路上所有的cpu设备 指向一个数组,数组长度由num.cpus决定 struct snd_soc_dai_link_component *codecs; // 存储当前链路上的所有codec设备 指向一个数组,数组长度由num.codecss决定 struct snd_soc_dai_link_component *platforms; // 存储当前链路上所有的platform设备 指向一个数组,数组长度由num.platforms决定 struct asoc_simple_data adata; struct snd_soc_codec_conf *codec_conf; // 这个我们不会使用 struct prop_nums num; // 保存当前音频数据链路的cpu、codec、platform数量 unsigned int mclk_fs; // MCLK频率 存放simple-audio-card,mclk-fs属性的值 } *dai_props; struct asoc_simple_jack hp_jack; struct asoc_simple_jack mic_jack; struct snd_soc_jack *aux_jacks; struct snd_soc_dai_link *dai_link; struct asoc_simple_dai *dais; struct snd_soc_dai_link_component *dlcs; struct snd_soc_dai_link_component dummy; struct snd_soc_codec_conf *codec_conf; struct gpio_desc *pa_gpio; const struct snd_soc_ops *ops; unsigned int dpcm_selectable:1; unsigned int force_dpcm:1; };
其中:
- snd_card:保存ASoC中的SoC声卡设备;
- dai_props:指向动态分配得到的数组,每个元素都是一个struct simple_dai_props,保存每个音频数据链路上的属性;数组的长度等于音频数据链路的长度;
- dai_link:指向动态分配得到的数组,每个元素都一个struct snd_soc_dai_link,即描述一个音频数据链路;数组的长度等于音频数据链路的长度;
- dais:指向动态分配得到的数组,每个元素都是一个struct asoc_simple_dai,数组的长度为每个音频数据链路上的DAI个数的累计和;
- dlcs:指向动态分配得到的数组,每个元素都是一个struct snd_soc_dai_link_component,数组的长度为每个音频数据链路上的设备个数的累计和;
- dummy:虚拟的CPU/Codec设备; 例如在音频回环测试中,可以使用虚拟的设备来模拟真实的硬件设备,从而方便测试和调试;
- codec_conf:指向动态分配得到的数组,每个元素都是一个struct snd_soc_codec_conf,在这里,针对rt5651-sound设备节点,该数组长度为0;
3.2 struct asoc_simple_dai
struct asoc_simple_dai是machine driver层定义的用来描述dai的数据结构,不要和struct snd_soc_dai数据结构搞混了,其定义在include/sound/simple_card_utils.h:
struct asoc_simple_dai { const char *name; unsigned int sysclk; int clk_direction; int slots; int slot_width; unsigned int tx_slot_mask; unsigned int rx_slot_mask; struct clk *clk; bool clk_fixed; struct asoc_simple_tdm_width_map *tdm_width_map; int n_tdm_widths; };
其中:
- name:dai的名称;
- sysclck:系统时钟频率;
- clk_direction:设置时钟方向,输入or输出;SND_SOC_CLOCK_OUT表示输出,值为1;SND_SOC_CLOCK_IN表示输入,值为0;
- clk:系统时钟;
- 对于cpi dai来说系统时钟指的是RK3399 i2s_clk的时钟,即i2s0模块输出的MCLK时钟(对应的时钟名称为clk_i2s0);
- 对于codec dai来说系统时钟指的是ALC5651 i2s1接口的MCLK信号线输入的时钟,同时也是RK3399 i2s0接口的MCLK信号线输出的时钟(对应的时钟名称为clk_i2sout);
3.3 link_info
link_info定义在include/sound/simple_card_utils.h:
struct link_info { int link; /* number of link */ int cpu; /* turn for CPU / Codec */ struct prop_nums num[SNDRV_MAX_LINKS]; }; struct prop_nums { int cpus; int codecs; int platforms; };
该结构体用于保存音频设备中link、cpu 和num三个参数的信息。具体来说:
- link:表示音频数据链路的数量;
- cpu:表示当前已经处理过的CPU或Codec的数量,即正在进行处理的是第几个CPU或Codec;
- num:是一个数组,一个大小为SNDRV_MAX_LINKS的数组,用于保存每条音频数据链路所需的信息。每个数组元素是一个struct prop_nums 结构体;
四、asoc_simple_probe函数分析
4.1 simple_priv_to_card
simple_priv_to_card宏定义在include/sound/simple_card_utils.h:
#define simple_priv_to_card(priv) (&(priv)->snd_card)
可以看到这个宏内容很简单,就是获取ASoC中定义的数据类型struct snd_soc_card。
4.2 simple_get_dais_count
simple_get_dais_count函数定义在sound/soc/generic/simple-card.c,这是一个用于获取音频数据链路信息的函数。该函数接受一个指向asoc_simple_priv数据结构的指针以及一个指向link_info结构的指针。

static int simple_get_dais_count(struct asoc_simple_priv *priv, struct link_info *li) { struct device *dev = simple_priv_to_dev(priv); // 获取&priv->snd_card.dev,即获取平台设备的device struct device_node *top = dev->of_node; // 获取设备树中设备节点 ,在我们这里也就是rt5651-sound设备节点 /* * link_num : number of links. * CPU-Codec / CPU-dummy / dummy-Codec * dais_num : number of DAIs 等于CPU数量 + Codec数量 * ccnf_num : number of codec_conf 和dummy-Codec数量一致 * same number for "dummy-Codec" * * ex1) * CPU0 --- Codec0 link : 5 * CPU1 --- Codec1 dais : 7 * CPU2 -/ ccnf : 1 * CPU3 --- Codec2 * * => 5 links = 2xCPU-Codec + 2xCPU-dummy + 1xdummy-Codec * => 7 DAIs = 4xCPU + 3xCodec * => 1 ccnf = 1xdummy-Codec * * ex2) * CPU0 --- Codec0 link : 5 * CPU1 --- Codec1 dais : 6 * CPU2 -/ ccnf : 1 * CPU3 -/ * * => 5 links = 1xCPU-Codec + 3xCPU-dummy + 1xdummy-Codec * => 6 DAIs = 4xCPU + 2xCodec * => 1 ccnf = 1xdummy-Codec * * ex3) * CPU0 --- Codec0 link : 6 * CPU1 -/ dais : 6 * CPU2 --- Codec1 ccnf : 2 * CPU3 -/ * * => 6 links = 0xCPU-Codec + 4xCPU-dummy + 2xdummy-Codec * => 6 DAIs = 4xCPU + 2xCodec * => 2 ccnf = 2xdummy-Codec * * ex4) * CPU0 --- Codec0 (convert-rate) link : 3 * CPU1 --- Codec1 dais : 4 * ccnf : 1 * * => 3 links = 1xCPU-Codec + 1xCPU-dummy + 1xdummy-Codec */ if (!top) { // 如果没有设备节点才会进入,该函数我们不会进入 li->num[0].cpus = 1; // 描述第0个音频链路的cpu、codecs、platforms数量 li->num[0].codecs = 1; li->num[0].platforms = 1; li->link = 1; // 音频链路数量 return 0; } return simple_for_each_link(priv, li, simple_count_noml, simple_count_dpcm); }
该函数通过调用simple_for_each_link函数来计算音频数据链路的数量和以及每条音频数据链路中cpu、codec、platform的数量。
simple_for_each_link用于解析设备树中音频数据链路节点:
如果我们的Machine中只有一条音频数据链路,在设备树中使用simple-audio-card,cpu、simple-audio-card,codec子节点进行描述;比如我们的设备节点rt5651-sound就是single DAI link:
simple-audio-card,cpu { sound-dai = <&i2s0>; }; simple-audio-card,codec { sound-dai = <&rt5651>; };
如果存在多条音频数据链路,在设备树中使用simple-audio-card,dai-link子节点进行描述;比如:
simple-audio-card,dai-link@0 { format = "i2s"; cpu { sound-dai = <&i2s0>; }; codec { sound-dai = <&rt5651>; }; }; simple-audio-card,dai-link@1 { format = "i2s2"; cpu { sound-dai = <&i2s>; }; codec { sound-dai = <&hdmi>; }; };
此外还有多个CPU,1个Codec的复杂音频数据链路;比如:

simple-audio-card,dai-link@0 { reg = <0>; format = "left_j"; bitclock-master = <&sndcpu0>; frame-master = <&sndcpu0>; sndcpu0: cpu { sound-dai = <&rcar_sound 0>; }; codec { sound-dai = <&ak4613>; }; }; simple-audio-card,dai-link@1 { reg = <1>; format = "i2s"; bitclock-master = <&sndcpu1>; frame-master = <&sndcpu1>; convert-channels = <8>; /* TDM Split */ sndcpu1: cpu-0 { sound-dai = <&rcar_sound 1>; }; cpu-1 { sound-dai = <&rcar_sound 2>; }; cpu-2 { sound-dai = <&rcar_sound 3>; }; cpu-3 { sound-dai = <&rcar_sound 4>; }; codec { mclk-fs = <512>; prefix = "pcm3168a"; dai-tdm-slot-num = <8>; sound-dai = <&pcm3168a 0>; }; };
在文档Documentation/devicetree/bindings/sound/simple-card.yaml给出了single DAI link、 Multi DAI links、Sampling Rate Conversion、2 CPU 1 Codec (Mixing)、Multi DAI links with DPCM等不同类型音频数据链路的示例,有兴趣可以看看;
4.2.1 simple_for_each_link
simple_for_each_link函数会解析设备节点,遍历每条音频数据链路并调用两个不同的计数回调函数simple_count_noml和 simple_count_dpcm,用于对不同类型的音频数据链路中的cpu、codec、platfrom数量进行统计,并保存在li变量中。
static int simple_for_each_link(struct asoc_simple_priv *priv, struct link_info *li, int (*func_noml)(struct asoc_simple_priv *priv, struct device_node *np, struct device_node *codec, struct link_info *li, bool is_top), int (*func_dpcm)(struct asoc_simple_priv *priv, struct device_node *np, struct device_node *codec, struct link_info *li, bool is_top)) { int ret; /* * Detect all CPU first, and Detect all Codec 2nd. * * In Normal sound case, all DAIs are detected * as "CPU-Codec". * * In DPCM sound case, * all CPUs are detected as "CPU-dummy", and * all Codecs are detected as "dummy-Codec". * To avoid random sub-device numbering, * detect "dummy-Codec" in last; */ for (li->cpu = 1; li->cpu >= 0; li->cpu--) { // 设置li->cpu=1、0,然后执行__simple_for_each_link函数 ret = __simple_for_each_link(priv, li, func_noml, func_dpcm); if (ret < 0) break; } return ret; }
这里又调用了__simple_for_each_link函数:

static int __simple_for_each_link(struct asoc_simple_priv *priv, struct link_info *li, int (*func_noml)(struct asoc_simple_priv *priv, struct device_node *np, struct device_node *codec, struct link_info *li, bool is_top), int (*func_dpcm)(struct asoc_simple_priv *priv, struct device_node *np, struct device_node *codec, struct link_info *li, bool is_top)) { struct device *dev = simple_priv_to_dev(priv); // 获取&priv->snd_card.dev,即获取平台设备的device struct device_node *top = dev->of_node; // 获取设备树中设备节点 ,在我们这里也就是rt5651-sound设备节点 struct device_node *node; uintptr_t dpcm_selectable = (uintptr_t)of_device_get_match_data(dev); // 获取与设备相关的驱动程序私有数据结构,其是在设备树中由设备的compatible // 属性匹配到的of_device_id 实例中定义的。这里我们并为定义,所以获取到的为NUL。 bool is_top = 0; int ret = 0; /* Check if it has dai-link */ node = of_get_child_by_name(top, PREFIX "dai-link"); // 获取simple-audio-card,dai-link设备节点,由于rt5651-sound设备节点中并未定义该节点,所以获取到的位NULL if (!node) { // 进入,设置is_top标志位 node = of_node_get(top); is_top = 1; } /* loop for all dai-link */ do { struct asoc_simple_data adata; struct device_node *codec; struct device_node *plat; struct device_node *np; int num = of_get_child_count(node); /* get codec */ codec = of_get_child_by_name(node, is_top ? // 获取simple-audio-card,codec设备节点,即{sound-dai = <&rt5651>;}; PREFIX "codec" : "codec"); if (!codec) { // codec设备节点是必须的 ret = -ENODEV; goto error; } /* get platform */ plat = of_get_child_by_name(node, is_top ? // 获取simple-audio-card,plat设备节点,我们也未定义 PREFIX "plat" : "plat"); /* get convert-xxx property */ memset(&adata, 0, sizeof(adata)); // 全部填充0 for_each_child_of_node(node, np) // 处理simple-audio-card,convert-channels等属性,由于这些属性我们没有使用到,所以可以不用关注 simple_parse_convert(dev, np, &adata); /* loop for all CPU/Codec node */ for_each_child_of_node(node, np) { // 遍历node中每个子节点,rt5651-sound设备节点中只有两个子节点simple-audio-card,cpu、simple-audio-card,codec if (plat == np) continue; /* * It is DPCM * if it has many CPUs, * or has convert-xxx property */ if (dpcm_selectable && (num > 2 || asoc_simple_is_convert_required(&adata))) { // 不会进入 /* * np * |1(CPU)|0(Codec) li->cpu * CPU |Pass |return * Codec |return|Pass */ if (li->cpu != (np == codec)) ret = func_dpcm(priv, np, codec, li, is_top); /* else normal sound */ } else { /* * np * |1(CPU)|0(Codec) li->cpu * CPU |Pass |return * Codec |return|return */ if (li->cpu && (np != codec)) // 当np为simple-audio-card,cpu节点时进入 ret = func_noml(priv, np, codec, li, is_top); // 最终执行simple_count_noml函数 } if (ret < 0) { of_node_put(codec); of_node_put(np); goto error; } } of_node_put(codec); node = of_get_next_child(top, node); // 从top设备节中获取下一个子节点,并且该子节点在在node } while (!is_top && node); error: of_node_put(node); return ret; }
这里我们以rt5651-sound设备节点为例介绍一下这段代码流程:li->cpu=1时;
- 获取子设备节点simple-audio-card,codec;
- 获取子设备节点simple-audio-card,cpu,并调用simple_count_noml函数将当前音频数据链路信息添加到li->num[li->link]中;
当li->cpu=0时,__simple_for_each_link函数将不会做任何事情。
4.2.2 simple_count_noml
simple_count_noml函数定义在sound/soc/generic/simple-card.c:
static int simple_count_noml(struct asoc_simple_priv *priv, struct device_node *np, struct device_node *codec, struct link_info *li, bool is_top) { if (li->link >= SNDRV_MAX_LINKS) { // 链路数已经达到上限 struct device *dev = simple_priv_to_dev(priv); dev_err(dev, "too many links\n"); return -EINVAL; } li->num[li->link].cpus = 1; // cpu数量 li->num[li->link].codecs = 1; // codec数量 li->num[li->link].platforms = 1; // platform数量 li->link += 1; // 音频数据链路计数+1 return 0; }
4.3 asoc_simple_init_priv
asoc_simple_init_priv定义在sound/soc/generic/simple-card-utils.c,该函数用来初始化priv数据成员,实际就是根据每个音频数据链路中的cpu数量、codec数量、platfom数量来为priv的成员动态分配内存的过程;

int asoc_simple_init_priv(struct asoc_simple_priv *priv, struct link_info *li) { struct snd_soc_card *card = simple_priv_to_card(priv); // 获取成员变量&priv->snd_card struct device *dev = simple_priv_to_dev(priv); // 获取&priv->snd_card.dev,即获取平台设备的device struct snd_soc_dai_link *dai_link; // 用于保存音频数据链路信息,指向一个struct snd_soc_dai_link数组 struct simple_dai_props *dai_props; struct asoc_simple_dai *dais; struct snd_soc_dai_link_component *dlcs; // 用于保存音频数据链路中的设备 struct snd_soc_codec_conf *cconf = NULL; int i, dai_num = 0, dlc_num = 0, cnf_num = 0; dai_props = devm_kcalloc(dev, li->link, sizeof(*dai_props), GFP_KERNEL); // 动态申请数组,即为每一个音频数据链路分配一个struct simple_dai_props dai_link = devm_kcalloc(dev, li->link, sizeof(*dai_link), GFP_KERNEL); // 动态申请数组,即为每一个音频数据链路分配一个struct snd_soc_dai_link if (!dai_props || !dai_link) return -ENOMEM; /* * dais (= CPU+Codec) dai数量 = CPU数量 + Codec数量 * dlcs (= CPU+Codec+Platform) 音频数据链路中设备数量 = CPU数量 + Codec数量 + Platform数量 */ for (i = 0; i < li->link; i++) { // 遍历每条音频数据链路,统计dai、condec_conf等数量 int cc = li->num[i].cpus + li->num[i].codecs; // 计算当前数据链路dai数量 dai_num += cc; // 累计dai的总数量 dlc_num += cc + li->num[i].platforms; // 累计dlc的总数量 if (!li->num[i].cpus) cnf_num += li->num[i].codecs; // 累计codec_conf的总数量 } dais = devm_kcalloc(dev, dai_num, sizeof(*dais), GFP_KERNEL); // 动态申请数组,即为每一个dai分配一个struct asoc_simple_dai dlcs = devm_kcalloc(dev, dlc_num, sizeof(*dlcs), GFP_KERNEL); // 动态申请数组,即为每一个dlcs分配一个struct snd_soc_dai_link_component if (!dais || !dlcs) return -ENOMEM; if (cnf_num) { cconf = devm_kcalloc(dev, cnf_num, sizeof(*cconf), GFP_KERNEL); // 动态申请数组,即为每一个condec_conf分配一个struct snd_soc_codec_conf if (!cconf) return -ENOMEM; } dev_dbg(dev, "link %d, dais %d, ccnf %d\n", li->link, dai_num, cnf_num); /* dummy CPU/Codec 虚拟设备 */ priv->dummy.of_node = NULL; priv->dummy.dai_name = "snd-soc-dummy-dai"; priv->dummy.name = "snd-soc-dummy"; priv->dai_props = dai_props; // 设置dai_props priv->dai_link = dai_link; // 设置音频数据链路 priv->dais = dais; // 设置dai priv->dlcs = dlcs; // 设置dlcs priv->codec_conf = cconf; // 设置codec_conf card->dai_link = priv->dai_link; // 设置音频数据链路 card->num_links = li->link; // 设置音频数据链路数量 card->codec_conf = cconf; // 设置codec_conf card->num_configs = cnf_num; // 设置codec_conf数量 for (i = 0; i < li->link; i++) { // 遍历每条音频数据链路 if (li->num[i].cpus) { // 走这里 /* Normal CPU */ dai_props[i].cpus = dai_link[i].cpus = dlcs; // 设置cpu设备 dai_props[i].num.cpus = dai_link[i].num_cpus = li->num[i].cpus; // 设置cpu设备数量 dai_props[i].cpu_dai = dais; // 设置cpu_dai dlcs += li->num[i].cpus; // 指向向后移动cpus dais += li->num[i].cpus; // 指向向后移动cpus } else { /* DPCM Be's CPU = dummy */ dai_props[i].cpus = dai_link[i].cpus = &priv->dummy; dai_props[i].num.cpus = dai_link[i].num_cpus = 1; } if (li->num[i].codecs) { // 走这里 /* Normal Codec */ dai_props[i].codecs = dai_link[i].codecs = dlcs; // 设置codec设备 dai_props[i].num.codecs = dai_link[i].num_codecs = li->num[i].codecs; // 设置codec设备数量 dai_props[i].codec_dai = dais; // 设置cpu_dai dlcs += li->num[i].codecs; // 指针向后移动codecs dais += li->num[i].codecs; // 指针向后移动codecs if (!li->num[i].cpus) { // 并不会进入这里 /* DPCM Be's Codec */ dai_props[i].codec_conf = cconf; cconf += li->num[i].codecs; } } else { /* DPCM Fe's Codec = dummy */ dai_props[i].codecs = dai_link[i].codecs = &priv->dummy; dai_props[i].num.codecs = dai_link[i].num_codecs = 1; } if (li->num[i].platforms) { // 走这里 /* Have Platform */ dai_props[i].platforms = dai_link[i].platforms = dlcs; // 设置platform设备 dai_props[i].num.platforms = dai_link[i].num_platforms = li->num[i].platforms; // 设置platform设备数量 dlcs += li->num[i].platforms; // 指针向后移动platforms } else { /* Doesn't have Platform */ dai_props[i].platforms = dai_link[i].platforms = NULL; dai_props[i].num.platforms = dai_link[i].num_platforms = 0; } } return 0; }
4.4 simple_parse_of
simple_parse_of函数定义在sound/soc/generic/simple-card.c,其用来解析rt5651-sound设备节点的属性信息,主要是simple-audio-card,widgets 、simple-audio-card,routing属性,并将这些信息应用到 snd_soc_card 数据结构中;
static int simple_parse_of(struct asoc_simple_priv *priv, struct link_info *li) { struct snd_soc_card *card = simple_priv_to_card(priv); int ret; ret = asoc_simple_parse_widgets(card, PREFIX); if (ret < 0) return ret; ret = asoc_simple_parse_routing(card, PREFIX); if (ret < 0) return ret; ret = asoc_simple_parse_pin_switches(card, PREFIX); if (ret < 0) return ret; /* Single/Muti DAI link(s) & New style of DT node */ memset(li, 0, sizeof(*li)); ret = simple_for_each_link(priv, li, simple_dai_link_of, simple_dai_link_of_dpcm); if (ret < 0) return ret; ret = asoc_simple_parse_card_name(card, PREFIX); if (ret < 0) return ret; ret = snd_soc_of_parse_aux_devs(card, PREFIX "aux-devs"); return ret; }
该函数主要完成以下工作:
- 调用 asoc_simple_parse_widgets函数解析析控件(widgets)的信息(即simple-audio-card,widgets属性),并将解析得到的信息保存到snd_soc_card成员of_dapm_widgets、num_of_dapm_widgets 中;
- 调用 asoc_simple_parse_routing函数解析路由(routing)的信息(即simple-audio-card,routing属性),并将解析得到的信息保存到snd_soc_card成员of_dapm_routes、num_of_dapm_routes中;
- 调用 asoc_simple_parse_pin_switches函数解析引脚开关(pin switches)的信息(即simple-audio-card,pin-switches属性属性),并将解析得到的信息保存到snd_soc_card成员of_dapm_routes、num_of_dapm_routes中;
- 调用simple_dai_link_of函数解析simple-audio-card,cpu、simple-audio-card,codec属性指向的设备节点,并将这些信息存储在音频数据链路的snd_soc_dai_link的成员cpus、或codec;此外还会解析simple-audio-card,mclk-fs、simple-audio-card,format属性等;
- 调用 asoc_simple_parse_card_name函数解析声卡的名称,用于解析设备节点rt5651-sound中的label以及simple-audio-card,name属性,并将解析得到的信息保存到snd_soc_card成员name中;
- 调用 snd_soc_of_parse_aux_devs函数解析设备节点rt5651-sound中的lsimple-audio-card,aux-devs属性,并将解析得到的信息保存到snd_soc_card成员aux_devs、num_aux_devs中;
这里我们介绍一下simple_parse_of函数内部调用的子函数,asoc_simple_parse_widgets、asoc_simple_parse_routing、asoc_simple_parse_pin_switches、simple_for_each_link、asoc_simple_parse_card_name、snd_soc_of_parse_aux_devs。
4.4.1 asoc_simple_parse_widgets
asoc_simple_parse_widgets函数定义在sound/soc/generic/simple-card-utils.c,用于解析设备节点rt5651-sound中的simple-audio-card,widgets属性,并将解析得到的信息保存到snd_soc_card成员of_dapm_widgets、num_of_dapm_widgets 中;
simple-audio-card,widgets = "Microphone", "Mic Jack", "Headphone", "Headphones";
每个条目都是一对字符串:
- 第一个是控件模板名称,在Machine驱动中这个是确定的只有那么几种widget,Microphone(表示麦克风)、Headphone(表示耳机)、Speaker(表示扬声器)、Line(线路);
- 第二个是控件实例名称,可以自由定义,可以将其看看做控件模板名称的别名,比如“Microphone”被重命名为“Mic Jack”;
函数内容如下:
int asoc_simple_parse_widgets(struct snd_soc_card *card, char *prefix) // "simple-audio-card," { struct device_node *node = card->dev->of_node; // 获取平台设备的设备节点,即rt5651-sound char prop[128]; if (!prefix) prefix = ""; snprintf(prop, sizeof(prop), "%s%s", prefix, "widgets"); // simple-audio-card,widgets if (of_property_read_bool(node, prop)) // 如果该属性存在,进入 return snd_soc_of_parse_audio_simple_widgets(card, prop); /* no widgets is not error */ return 0; }
可以这里先是检查simple-audio-card,widgets属性是否存在,如果存在则调用snd_soc_of_parse_audio_simple_widgets函数,该函数定义在sound/soc/soc-core.c:

int snd_soc_of_parse_audio_simple_widgets(struct snd_soc_card *card, const char *propname) { struct device_node *np = card->dev->of_node; // 获取平台设备的设备节点,即rt5651-sound struct snd_soc_dapm_widget *widgets; const char *template, *wname; int i, j, num_widgets; num_widgets = of_property_count_strings(np, propname); // 获取simple-audio-card,widgets属性值中字符串的数量 if (num_widgets < 0) { dev_err(card->dev, "ASoC: Property '%s' does not exist\n", propname); return -EINVAL; } if (!num_widgets) { dev_err(card->dev, "ASoC: Property '%s's length is zero\n", propname); return -EINVAL; } if (num_widgets & 1) { dev_err(card->dev, "ASoC: Property '%s' length is not even\n", propname); return -EINVAL; } num_widgets /= 2; // 计算控件的数量 widgets = devm_kcalloc(card->dev, num_widgets, sizeof(*widgets), // 动态分配数组,为每一个控件分配一个struct snd_soc_dapm_widget GFP_KERNEL); if (!widgets) { dev_err(card->dev, "ASoC: Could not allocate memory for widgets\n"); return -ENOMEM; } for (i = 0; i < num_widgets; i++) { // 保存每一个控件的信息到snd_soc_dapm_widget int ret = of_property_read_string_index(np, propname, 2 * i, &template); // 读取设备节点属性propname的第2*i个值,保存到template if (ret) { dev_err(card->dev, "ASoC: Property '%s' index %d read error:%d\n", propname, 2 * i, ret); return -EINVAL; } for (j = 0; j < ARRAY_SIZE(simple_widgets); j++) { // 遍历simple_widgets数组 if (!strncmp(template, simple_widgets[j].name, // 根据name查找。找到则保存到widgets[i] strlen(simple_widgets[j].name))) { widgets[i] = simple_widgets[j]; break; } } if (j >= ARRAY_SIZE(simple_widgets)) { // 没有找到,即设备节点中定义了不支持的控件模板名称 dev_err(card->dev, "ASoC: DAPM widget '%s' is not supported\n", template); return -EINVAL; } ret = of_property_read_string_index(np, propname, (2 * i) + 1, &wname); // 读取设备节点属性propname的第2*i+1个值,保存到wname if (ret) { dev_err(card->dev, "ASoC: Property '%s' index %d read error:%d\n", propname, (2 * i) + 1, ret); return -EINVAL; } widgets[i].name = wname; // 设置name } card->of_dapm_widgets = widgets; // 保存音频控件 card->num_of_dapm_widgets = num_widgets; // 音频控件数量 return 0; }
其中simple_widgets是一个全局静态数组, 定义了Machine域widget,包括麦克风(MIC),耳机(HP),扬声器(SPK),线路输入接口(LINE)。
static const struct snd_soc_dapm_widget simple_widgets[] = { SND_SOC_DAPM_MIC("Microphone", NULL), SND_SOC_DAPM_LINE("Line", NULL), SND_SOC_DAPM_HP("Headphone", NULL), SND_SOC_DAPM_SPK("Speaker", NULL), };
4.4.2 asoc_simple_parse_routing
asoc_simple_parse_routing函数定义在sound/soc/generic/simple-card-utils.c,用于解析设备节点rt5651-sound中的simple-audio-card,routing属性,并将解析得到的信息保存到snd_soc_card成员of_dapm_routes、num_of_dapm_routes中;
simple-audio-card,routing = "Mic Jack", "micbias1", "IN2P", "Mic Jack", "IN2N","Mic Jack", "Headphones", "HPOL", "Headphones", "HPOR";
用于置与Codec(ALC5651)物理输入引脚、物理输出端引脚连接的路径;每个条目都是一对字符串,第一个是目的(sink),第二个是源(source):
函数内容如下:
int asoc_simple_parse_routing(struct snd_soc_card *card, char *prefix) { struct device_node *node = card->dev->of_node; // 获取平台设备的设备节点,即rt5651-sound char prop[128]; if (!prefix) prefix = ""; snprintf(prop, sizeof(prop), "%s%s", prefix, "routing"); // simple-audio-card,routing if (!of_property_read_bool(node, prop)) // 如果属性不存在,则进入 return 0; return snd_soc_of_parse_audio_routing(card, prop); }
可以这里先是检查simple-audio-card,routing属性是否存在,如果存在则调用snd_soc_of_parse_audio_routing函数,该函数定义在sound/soc/soc-core.c:

int snd_soc_of_parse_audio_routing(struct snd_soc_card *card, const char *propname) { struct device_node *np = card->dev->of_node; // 获取平台设备的设备节点,即rt5651-sound int num_routes; struct snd_soc_dapm_route *routes; int i; num_routes = of_property_count_strings(np, propname); // 获取simple-audio-card,routing属性值中字符串的数量 if (num_routes < 0 || num_routes & 1) { dev_err(card->dev, "ASoC: Property '%s' does not exist or its length is not even\n", propname); return -EINVAL; } num_routes /= 2; // 计算路由的数量 routes = devm_kcalloc(card->dev, num_routes, sizeof(*routes), // 动态分配数组,为每一个路由分配一个struct snd_soc_dapm_route GFP_KERNEL); if (!routes) { dev_err(card->dev, "ASoC: Could not allocate DAPM route table\n"); return -ENOMEM; } for (i = 0; i < num_routes; i++) { // 保存每一个路由的信息到snd_soc_dapm_route int ret = of_property_read_string_index(np, propname, 2 * i, &routes[i].sink); // 读取设备节点属性propname的第2*i个值,保存到sink if (ret) { dev_err(card->dev, "ASoC: Property '%s' index %d could not be read: %d\n", propname, 2 * i, ret); return -EINVAL; } ret = of_property_read_string_index(np, propname, (2 * i) + 1, &routes[i].source); // 读取设备节点属性propname的第2*i+1个值,保存到source if (ret) { dev_err(card->dev, "ASoC: Property '%s' index %d could not be read: %d\n", propname, (2 * i) + 1, ret); return -EINVAL; } } card->num_of_dapm_routes = num_routes; // 保存路由 card->of_dapm_routes = routes; // 路由数量 return 0; }
4.4.3 asoc_simple_parse_pin_switches
asoc_simple_parse_routing函数定义在sound/soc/generic/simple-card-utils.c,用于解析设备节点rt5651-sound中的simple-audio-card,pin-switches属性,并将解析得到的信息保存到snd_soc_card成员of_dapm_routes、num_of_dapm_routes中;比如:
simple-audio-card,pin-switches = "Speaker";
由于rt5651-sound设备节点并没有使用该属性,所以这里不做过多深究;
int asoc_simple_parse_pin_switches(struct snd_soc_card *card, char *prefix) { char prop[128]; if (!prefix) prefix = ""; snprintf(prop, sizeof(prop), "%s%s", prefix, "pin-switches"); return snd_soc_of_parse_pin_switches(card, prop); }
这里直接调用snd_soc_of_parse_pin_switches函数,该函数定义在sound/soc/soc-core.c:

int snd_soc_of_parse_pin_switches(struct snd_soc_card *card, const char *prop) { const unsigned int nb_controls_max = 16; const char **strings, *control_name; struct snd_kcontrol_new *controls; struct device *dev = card->dev; unsigned int i, nb_controls; int ret; if (!of_property_read_bool(dev->of_node, prop)) // 属性不存在,直接返回 return 0; strings = devm_kcalloc(dev, nb_controls_max, // 动态分配数组,数组长度为16个,每个元素都是一个指针 sizeof(*strings), GFP_KERNEL); if (!strings) return -ENOMEM; ret = of_property_read_string_array(dev->of_node, prop, strings, nb_controls_max); // 读取属性值,保存到strings指针数组中 if (ret < 0) return ret; nb_controls = (unsigned int)ret; controls = devm_kcalloc(dev, nb_controls, sizeof(*controls), GFP_KERNEL); // 动态分配数组,数组长度为nb_controls,每一个元素都是struct snd_kcontrol_new if (!controls) return -ENOMEM; for (i = 0; i < nb_controls; i++) { // 保存每一个control的信息到snd_kcontrol_new control_name = devm_kasprintf(dev, GFP_KERNEL, "%s Switch", strings[i]); // 设置control名称 if (!control_name) return -ENOMEM; controls[i].iface = SNDRV_CTL_ELEM_IFACE_MIXER; controls[i].name = control_name; controls[i].info = snd_soc_dapm_info_pin_switch; controls[i].get = snd_soc_dapm_get_pin_switch; controls[i].put = snd_soc_dapm_put_pin_switch; controls[i].private_value = (unsigned long)strings[i]; } card->controls = controls; // 保存controls card->num_controls = nb_controls; // controls数量 return 0; }
4.4.4 simple_dai_link_of(重要)
这个函数上面已经介绍过了,simple_for_each_link函数会解析设备树rt5651-sound设备节点,遍历每条音频数据链路并调用两个不同的计数回调函数simple_dai_link_of和simple_dai_link_of_dpcm,用于对不同类型的音频数据链路中的cpu、codec、platfrom数量进行统计,并保存在li变量中。
这里我们只需要看一下simple_dai_link_of函数即可,当li->cpu=1时,执行该函数,函数定义在sound/soc/generic/simple-card.c:
static int simple_dai_link_of(struct asoc_simple_priv *priv, struct device_node *np, // 为simple-audio-card,cpu属性指向的设备节点,即{sound-dai = <&i2s0>;}; struct device_node *codec, // 为simple-audio-card,codec属性指向的设备节点,即{sound-dai = <&rt5651>;}; struct link_info *li, // 音频数据链路信息 此时该数据结构内存全部被设置为了0 bool is_top) // 1 { struct device *dev = simple_priv_to_dev(priv); // 获取&priv->snd_card.dev,即获取平台设备的device struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); // 获取成员变量priv->dai_link[li->link] 获取第0个音频数据链路 struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); // 获取当前链路上第0个cpu设备&(dai_link)->cpus[0] struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); // 获取当前链路上第0个的codec设备 &(dai_link)->codecs[0] struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); // 获取当前链路上第0个的platform设备 &(dai_link)->platforms[0] struct device_node *cpu = NULL; struct device_node *node = NULL; struct device_node *plat = NULL; char dai_name[64]; char prop[128]; char *prefix = ""; int ret, single_cpu = 0; cpu = np; // 设置为simple-audio-card,cpu设备节点 node = of_get_parent(np); // 获取父设备节点,即rt5651-sound设备节点 dev_dbg(dev, "link_of (%pOF)\n", node); /* For single DAI link & old style of DT node */ if (is_top) prefix = PREFIX; // prefix = "simple-audio-card," snprintf(prop, sizeof(prop), "%splat", prefix); // "simple-audio-card,plat" plat = of_get_child_by_name(node, prop); // 未定义该设备节点,所以返回NULL ret = simple_parse_node(priv, cpu, li, prefix, &single_cpu); // 这个函数很重要,单独介绍,主要目的解析simple-audio-card,cpu设备节点 if (ret < 0) goto dai_link_of_err; ret = simple_parse_node(priv, codec, li, prefix, NULL); // 这个函数很重要,单独介绍,主要目的simple-audio-card,codec设备节点 if (ret < 0) goto dai_link_of_err; ret = asoc_simple_parse_platform(plat, platforms); // 初始化platforms->of_node为rt5651-sound设备节点 if (ret < 0) goto dai_link_of_err; snprintf(dai_name, sizeof(dai_name), // 得到dai_name为ff880000.i2s-rt5651-aif1 "%s-%s", cpus->dai_name, codecs->dai_name); // cpu->dai_name为"ff880000.i2s",codec->dai_name为"rt5651-aif1" asoc_simple_canonicalize_cpu(cpus, single_cpu); asoc_simple_canonicalize_platform(platforms, cpus); ret = simple_link_init(priv, node, codec, li, prefix, dai_name); // 初始化音频数据链路snd_soc_dai_link的部分成员 dai_link_of_err: of_node_put(plat); of_node_put(node); li->link++; // 自增 return ret; }
这里唯一值得关注的函数就是simple_parse_node、simple_link_init,这两个函数比较复杂,我们后面单独介绍。
当li->cpu=0时,__simple_for_each_link函数将不会做任何事情,因此simple_dai_link_of_dpcm这个函数不会执行到:

static int simple_dai_link_of_dpcm(struct asoc_simple_priv *priv, struct device_node *np, // np为simple-audio-card,cpu节点 struct device_node *codec, // simple-audio-card,codec设备节点,即{sound-dai = <&rt5651>;}; struct link_info *li, bool is_top) { struct device *dev = simple_priv_to_dev(priv); // 获取&priv->snd_card.dev,即获取平台设备的device struct snd_soc_dai_link *dai_link = simple_priv_to_link(priv, li->link); // 获取成员变量priv->dai_link[li->link] struct simple_dai_props *dai_props = simple_priv_to_props(priv, li->link); // 获取priv->dai_props[li->link] struct device_node *top = dev->of_node; // 获取设备树中设备节点 ,在我们这里也就是rt5651-sound设备节点 struct device_node *node = of_get_parent(np); // 获取父设备节点 char *prefix = ""; char dai_name[64]; int ret; dev_dbg(dev, "link_of DPCM (%pOF)\n", np); /* For single DAI link & old style of DT node */ if (is_top) prefix = PREFIX; if (li->cpu) { // 为1时进入 struct snd_soc_dai_link_component *cpus = asoc_link_to_cpu(dai_link, 0); // 获取当前链路上第0个cpu设备&(dai_link)->cpus[0] struct snd_soc_dai_link_component *platforms = asoc_link_to_platform(dai_link, 0); // 存储当前链路上第0个的platform设备 &(dai_link)->platforms[0] int is_single_links = 0; /* Codec is dummy */ /* FE settings */ // front end dai link配置 dai_link->dynamic = 1; // 动态PCM 标记 dai_link->dpcm_merged_format = 1; // 标记是否使用合并格式的 DPCM ret = simple_parse_node(priv, np, li, prefix, &is_single_links); if (ret < 0) goto out_put_node; snprintf(dai_name, sizeof(dai_name), "fe.%s", cpus->dai_name); asoc_simple_canonicalize_cpu(cpus, is_single_links); asoc_simple_canonicalize_platform(platforms, cpus); } else { // 为0时进入 struct snd_soc_dai_link_component *codecs = asoc_link_to_codec(dai_link, 0); struct snd_soc_codec_conf *cconf; /* CPU is dummy */ /* BE settings */ dai_link->no_pcm = 1; dai_link->be_hw_params_fixup = asoc_simple_be_hw_params_fixup; cconf = simple_props_to_codec_conf(dai_props, 0); ret = simple_parse_node(priv, np, li, prefix, NULL); if (ret < 0) goto out_put_node; snprintf(dai_name, sizeof(dai_name), "be.%s", codecs->dai_name); /* check "prefix" from top node */ snd_soc_of_parse_node_prefix(top, cconf, codecs->of_node, PREFIX "prefix"); snd_soc_of_parse_node_prefix(node, cconf, codecs->of_node, "prefix"); snd_soc_of_parse_node_prefix(np, cconf, codecs->of_node, "prefix"); } simple_parse_convert(dev, np, &dai_props->adata); snd_soc_dai_link_set_capabilities(dai_link); ret = simple_link_init(priv, node, codec, li, prefix, dai_name); out_put_node: li->link++; of_node_put(node); return ret; }
4.4.5 asoc_simple_parse_card_name
asoc_simple_parse_card_name函数定义在sound/soc/generic/simple-card-utils.c,用于解析设备节点rt5651-sound中的label以及simple-audio-card,name属性,并将解析得到的信息保存到snd_soc_card成员name中;
int asoc_simple_parse_card_name(struct snd_soc_card *card, char *prefix) { int ret; if (!prefix) prefix = ""; /* Parse the card name from DT */ ret = snd_soc_of_parse_card_name(card, "label"); // 获取rt5651-sound设备节点label属性 if (ret < 0 || !card->name) { // 属性不存在 char prop[128]; snprintf(prop, sizeof(prop), "%sname", prefix); // simple-audio-card,name ret = snd_soc_of_parse_card_name(card, prop); // 获取rt5651-sound设备节点simple-audio-card,name属性值"realtek,rt5651-codec",保存到card->name if (ret < 0) return ret; } if (!card->name && card->dai_link) card->name = card->dai_link->name; return 0; }
这里直接调用snd_soc_of_parse_card_name函数,该函数定义在sound/soc/soc-core.c:

/* Retrieve a card's name from device tree */ int snd_soc_of_parse_card_name(struct snd_soc_card *card, const char *propname) // "label" { struct device_node *np; int ret; if (!card->dev) { pr_err("card->dev is not set before calling %s\n", __func__); return -EINVAL; } np = card->dev->of_node; // 获取平台设备的设备节点,即rt5651-sound ret = of_property_read_string_index(np, propname, 0, &card->name); // 读取设备节点属性propname的第0个值,保存到card->name /* * EINVAL means the property does not exist. This is fine providing * card->name was previously set, which is checked later in * snd_soc_register_card. */ if (ret < 0 && ret != -EINVAL) { dev_err(card->dev, "ASoC: Property '%s' could not be read: %d\n", propname, ret); return ret; } return 0; }
4.4.6 snd_soc_of_parse_aux_devs
asoc_simple_parse_card_name函数定义在sound/soc/soc-core.c,用于解析设备节点rt5651-sound中的lsimple-audio-card,aux-devs属性,并将解析得到的信息保存到snd_soc_card成员aux_devs、num_aux_devs中;
int snd_soc_of_parse_aux_devs(struct snd_soc_card *card, const char *propname) // "simple-audio-card,aux-devs" { struct device_node *node = card->dev->of_node; struct snd_soc_aux_dev *aux; int num, i; num = of_count_phandle_with_args(node, propname, NULL); // 根据属性名和参数的要求,统计节点中符合条件的 phandle 的数量 这里找不到匹配的属性,返回负数 if (num == -ENOENT) { return 0; } else if (num < 0) { dev_err(card->dev, "ASOC: Property '%s' could not be read: %d\n", propname, num); return num; } aux = devm_kcalloc(card->dev, num, sizeof(*aux), GFP_KERNEL); // 动态分配数组,每个元素都是一个struct snd_soc_aux_dev if (!aux) return -ENOMEM; card->aux_dev = aux; card->num_aux_devs = num; for_each_card_pre_auxs(card, i, aux) { aux->dlc.of_node = of_parse_phandle(node, propname, i); if (!aux->dlc.of_node) return -EINVAL; }
}
4.5 snd_soc_card_set_drvdata
snd_soc_card_set_drvdata函数定义在include/sound/soc-card.h,用于设置snd_soc_card的私有数据drvdata为前面申请的asoc_simple_priv类型的变量priv;
/* device driver data */ static inline void snd_soc_card_set_drvdata(struct snd_soc_card *card, void *data) { card->drvdata = data; }
4.6 asoc_simple_debug_info
asoc_simple_debug_info函数定义在include/sound/simple_card_utils.h,如果定义了DEBUG模式,则该函数如下:
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2018-07-30 第八节、图片分割之GrabCut算法、分水岭算法