----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板eMMC :16GBLPDDR3 :4GB显示屏 :15.6英寸HDMI接口显示屏u-boot :2023.04linux :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引脚连接的ALC5651的IRQ引脚,用于检测耳机的插入;当耳机插入时。耳机插头的金属会碰到检测引脚,使得检测引脚的电平发生改变,从而触发中断。这样就可以在中断处理函数中读取GPIO口的值,进一步判断是耳机插入还是拔出。
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输入引脚
因此可以在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)中介绍。
音频数据通过RK3399的I2S0接口传输到ALC5651,再通过耳机、麦克风等端口输出或输入音频信号。
关于设备节点属性可以参考文档:
- 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_