专栏文章目录传送门:返回专栏目录
目录
上一章节,对于Linux Audio子系统有了大概的了解,对音频的基础知识,Audio 子系统的介绍,ALSA的框架库相关知识。本章节将讲解ALSA驱动的实现原理,在应用上一些开发相关。
1. Linux ALSA 内核框架
ALSA作为音频子系统主要一个部分,从上一章节,了解主要有两个大部分ASoC Driver(纠正上一章节图片)和ALSA Soc
图1.1 Linux ALSA 内核框架
ASoc Driver:
包含三大Machine Driver、Platform Driver、Codec Driver
-
Machine Driver:
Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的代码,Machine Driver 在 ASoC 框架中扮演了平台和 Codec 之间的沟通桥梁,使得硬件设备和音频处理能够协同工作,并在不同设备之间实现通用的音频处理流程,提高了驱动的可移植性和适应性。
图1.2 Asoc内三大部分
如图1.2 Machine就是充当对Platform与Codec的一个媒介,只要这两个连接起来,就会组成一个声卡设备。通过Machine 驱动开始,对于声卡的注册,绑定平台和Codec驱动等等;图中可以看出音频数据通道,可以从Codec搬运至Platform,也可以反过来,中间的那条线可以是I2S,PCM总线。
-
Platform Driver:
Platform 驱动程序类可以分为音频DMA驱动,SoC DAI 驱动程序和 DSP 驱动程序,比较常用的是前面两个DMA驱动,SoC DAI驱动。
音频DMA : 主要负责管理数据在内存和音频硬件之间的传输的部分。它确保音频数据能够高效地从内存传输到音频硬件,以及从音频硬件传输回内存。
Soc DAI:负责Soc的音频接口与数字音频接口的连接,确保数字音频从CPU dai流向硬件接口;
-
Codec Driver:
Codec Driver意思就是音频编解码器,可以用于去解码音频或者编码音频。对于解码来说,就是讲音频数据进行解码,在Codec后经过DAC转换成模拟信号就可以通过外部设备进行播放;对于编码,模拟信号输入后通过ADC转换得到数字音频,通过Codec进行编码,用于存储或者传输。
Codec Driver功能还包括对于音频链路的控制,对音频信号做出相应的控制,比如放大音量等;
ALSA Core:
ALSA 核心层,作为ALSA最重要部分,在 ALSA Core ,指的是 Advanced Linux Sound Architecture 核心模块,它包含多个子模块,包括 PCM、MIDI、Control 和 Sequence。这些子模块一起构成了 ALSA Core,提供了在 Linux 系统中进行音频数据处理、传输和控制的基础设施。
-
PCM: 通过 PCM 子模块,声音数据可以被捕获、处理和输出;
-
MIDI:通过 MIDI 子模块,音乐合成器可以接收和响应 MIDI 命令;
-
Control :通过 Control 子模块,用户可以调整音频设备的参数;
-
Sequence :通过 Sequence 子模块,可以管理和处理音频事件序列。这些功能共同构成了 ALSA 的核心能力,使得在 Linux 系统中实现高质量的音频处理成为可能
2. Linux ALSA 代码分析
ALSA 代码目录结构,内核版本5.4:
./sound/
├── ac97
├── ac97_bus.c
├── aoa
├── arm
├── atmel
├── core
├── drivers
├── firewire
├── hda
├── i2c
├── isa
├── Kconfig
├── last.c
├── Makefile
├── mips
├── oss
├── parisc
├── pci
├── pcmcia
├── ppc
├── sh
├── soc
├── sound_core.c
├── sparc
├── spi
├── synth
├── usb
├── x86
└── xen
目录/文件名 | 作用和内容 |
---|---|
ac97 | AC'97 音频编解码器的驱动和支持 |
aoa | Apple Onboard Audio 驱动 |
arm | 针对 ARM 架构的声音驱动 |
atmel | 用于 Atmel 音频硬件的驱动和支持 |
core | 声音子系统的核心代码,提供基本的声音处理功能 |
drivers | 各种音频硬件设备的驱动程序存放位置,如 ALSA 的主要驱动程序 |
firewire | 用于 FireWire 音频设备的支持 |
hda | High Definition Audio 驱动 |
i2c | 用于 I2C 总线上音频设备的驱动和支持 |
isa | 适用于 ISA 总线上音频设备的支持 |
Kconfig | 声音子系统的配置文件 |
mips | 针对 MIPS 架构的声音驱动 |
oss | Open Sound System(OSS)的兼容性驱动 |
parisc | 适用于 PA-RISC 架构的声音驱动 |
pci | 用于 PCI 总线上音频设备的驱动和支持 |
pcmcia | 用于 PCMCIA 总线上音频设备的驱动和支持 |
ppc | 适用于 PowerPC 架构的声音驱动 |
sh | 针对 SuperH 架构的声音驱动 |
soc | 针对 SoC 架构的声音驱动,包括 ASoC 子系统 |
sparc | 适用于 SPARC 架构的声音驱动 |
spi | 用于 SPI 总线上音频设备的驱动和支持 |
synth | 音频合成器的驱动和支持 |
usb | 用于 USB 总线上音频设备的驱动和支持 |
x86 | 针对 x86 架构的声音驱动 |
xen | 用于虚拟化环境中的声音支持 |
重要的目录在于Core文件夹,实现了声音子系统的核心代码。
2.1 声卡驱动初始化
入口从ALSA 初始化开始:
vim ./sound/core/sound.c
static int __init ALSA_sound_init(void)
{
snd_major = major;
snd_ecards_limit = cards_limit;
//注册字符设备驱动,将主设备号和字符设备的操作函数关联起来
//主设备号为 116 :#define CONFIG_SND_MAJOR 116 /* standard configuration */
if (register_chrdev(major, "ALSA", &snd_fops)) {
pr_err("ALSA core: unable to register native major device number %d\n", major);
return -EIO;
}
//创建snd_proc_root ,创建相关的文件系统/proc/sound
if (snd_info_init() < 0) {
unregister_chrdev(major, "ALSA");
return -ENOMEM;
}
#ifndef MODULE
pr_info("Advanced Linux Sound Architecture Driver Initialized.\n");
#endif
return 0;
}
ALSA_sound_init主要注册字符设备驱动,将相关信息写入,并且在Linux 系统上目录/proc创建一些虚拟文件
int __init snd_info_init(void)
{
snd_proc_root = snd_info_create_entry("asound", NULL, THIS_MODULE);
if (!snd_proc_root)
return -ENOMEM;
snd_proc_root->mode = S_IFDIR | 0555;
snd_proc_root->p = proc_mkdir("asound", NULL);
if (!snd_proc_root->p)
goto error;
#ifdef CONFIG_SND_OSSEMUL
snd_oss_root = create_subdir(THIS_MODULE, "oss");
if (!snd_oss_root)
goto error;
#endif
#if IS_ENABLED(CONFIG_SND_SEQUENCER)
snd_seq_root = create_subdir(THIS_MODULE, "seq");
if (!snd_seq_root)
goto error;
#endif
if (snd_info_version_init() < 0 ||
snd_minor_info_init() < 0 ||
snd_minor_info_oss_init() < 0 ||
snd_card_info_init() < 0 ||
snd_info_minor_register() < 0)
goto error;
return 0;
error:
snd_info_free_entry(snd_proc_root);
return -ENOMEM;
}
对于字符设备驱动中有一个文件操作接口file_operations
static const struct file_operations snd_fops =
{
.owner = THIS_MODULE,
.open = snd_open,
.llseek = noop_llseek,
}
snd_open接口函数,接口通过传参的inode参数进行获取设备的次设备号,然后查找对应的snd_minior
结构体,如果没有加载就去初始化它,然后进行操作函数替换为行的操作函数表,最后打开设备。snd_open函数这里不展现出来,重点查看下snd_minior
结构体。
struct snd_minor {
int type; /* SNDRV_DEVICE_TYPE_XXX */
int card; /* card number */
int device; /* device number */
const struct file_operations *f_ops; /* file operations */
void *private_data; /* private data for f_ops->open */
struct device *dev; /* device for sysfs */
struct snd_card *card_ptr; /* assigned card instance */
ANDROID_KABI_RESERVE(1);
};
snd_minor结构体作用在于代表音频设备的次设备,每个此设备都有独特标识,声卡编号,设备编号,文件操作函数等等,这样对于一个设备包含多个音频设备就更加方便的管理。
创建设备类文件:
调用了 init_oss_soundcore()
函数来初始化 OSS(Open Sound System)相关的音频核心模块。然后,它创建了一个名为 "sound" 的设备类(sound_class
),这个类将用于管理音频设备的设备节点。
static int __init init_soundcore(void)
{
int rc;
rc = init_oss_soundcore();
if (rc)
return rc;
sound_class = class_create(THIS_MODULE, "sound");
if (IS_ERR(sound_class)) {
cleanup_oss_soundcore();
return PTR_ERR(sound_class);
}
sound_class->devnode = sound_devnode;
return 0;
}
查看设备下class 相关设备节点
这些设备节点包括 card0
、card1
、card2
等等,每个 cardX
目录下都包含了与该声卡相关的信息和控制接口。其中的 pcmCXDYZ
节点表示音频设备的不同 PCM 子设备,例如 pcmC0D0c
表示第一张声卡的第一个 PCM 播放设备。有了这些设备节点对于用户通过这些节点去获取音频的状态,配置参数,进行音频输入输出等操作,更重要的是统一了音频操作的方式。
2.2 声卡创建注册
声卡的创建主要通过./sound/core/init.c 中的snd_card_new
接口函数,主要使用声卡的设备结构体snd_card
int snd_card_new(struct device *parent, int idx, const char *xid,
struct module *module, int extra_size,
struct snd_card **card_ret)
{
struct snd_card *card;
int err;
if (snd_BUG_ON(!card_ret))
return -EINVAL;
*card_ret = NULL;
if (extra_size < 0)
extra_size = 0;
card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);
if (!card)
return -ENOMEM;
if (extra_size > 0)
card->private_data = (char *)card + sizeof(struct snd_card);
if (xid)
strlcpy(card->id, xid, sizeof(card->id));
err = 0;
mutex_lock(&snd_card_mutex);
if (idx < 0) /* first check the matching module-name slot */
idx = get_slot_from_bitmask(idx, module_slot_match, module);
if (idx < 0) /* if not matched, assign an empty slot */
idx = get_slot_from_bitmask(idx, check_empty_slot, module);
if (idx < 0)
err = -ENODEV;
else if (idx < snd_ecards_limit) {
if (test_bit(idx, snd_cards_lock))
err = -EBUSY; /* invalid */
} else if (idx >= SNDRV_CARDS)
err = -ENODEV;
if (err < 0) {
mutex_unlock(&snd_card_mutex);
dev_err(parent, "cannot find the slot for index %d (range 0-%i), error: %d\n",
idx, snd_ecards_limit - 1, err);
kfree(card);
return err;
}
set_bit(idx, snd_cards_lock); /* lock it */
if (idx >= snd_ecards_limit)
snd_ecards_limit = idx + 1; /* increase the limit */
mutex_unlock(&snd_card_mutex);
card->dev = parent;
card->number = idx;
#ifdef MODULE
WARN_ON(!module);
card->module = module;
#endif
INIT_LIST_HEAD(&card->devices);
init_rwsem(&card->controls_rwsem);
rwlock_init(&card->ctl_files_rwlock);
INIT_LIST_HEAD(&card->controls);
INIT_LIST_HEAD(&card->ctl_files);
spin_lock_init(&card->files_lock);
INIT_LIST_HEAD(&card->files_list);
mutex_init(&card->memory_mutex);
#ifdef CONFIG_PM
init_waitqueue_head(&card->power_sleep);
#endif
init_waitqueue_head(&card->remove_sleep);
card->sync_irq = -1;
device_initialize(&card->card_dev);
card->card_dev.parent = parent;
card->card_dev.class = sound_class;
card->card_dev.release = release_card_device;
card->card_dev.groups = card->dev_groups;
card->dev_groups[0] = &card_dev_attr_group;
err = kobject_set_name(&card->card_dev.kobj, "card%d", idx);
if (err < 0)
goto __error;
snprintf(card->irq_descr, sizeof(card->irq_descr), "%s:%s",
dev_driver_string(card->dev), dev_name(&card->card_dev));
/* the control interface cannot be accessed from the user space until */
/* snd_cards_bitmask and snd_cards are set with snd_card_register */
err = snd_ctl_create(card);
if (err < 0) {
dev_err(parent, "unable to register control minors\n");
goto __error;
}
err = snd_info_card_create(card);
if (err < 0) {
dev_err(parent, "unable to create card info\n");
goto __error_ctl;
et
*card_ret = card;
return 0;
__error_ctl:
snd_device_free_all(card);
__error:
put_device(&card->card_dev);
return err;
}
根据代码来看,主要做的流程就是:
-
分配内存:根据声卡对象的大小进行分配。
-
设置声卡的参数:设置声卡的ID,名称以及其他参数。
-
创建声卡接口:通过
snd_ctl_create
函数创建声卡的控制接口,使其可以被用户空间访问。 -
创建声卡信息接口:通过
snd_info_card_create
函数创建声卡的信息接口,用于提供声卡的相关信息。
创建声卡接口函数最终通过函数snd_card_register()
该函数将声卡的设备信息,控制注册,放入到声卡组中形成一个list.
2.3 PCM设备创建
3. ALSA ASoC
从ALSA 内核框架查看到三个部分,这里将从代码简要分析下工作过程。
3.1 Machine
Machine主要负责Platform和Codec之间的耦合,更加使用更多的设备,起到一个连接作用。具体代码的实现:
vim sound/soc/soc-core.c
/* ASoC platform driver */
static struct platform_driver soc_driver = {
.driver = {
.name = "soc-audio",
.pm = &snd_soc_pm_ops,
},
.probe = soc_probe,
};
这里看到platform_driver中name是soc-audio
, 与平台中的platform_device一样将触发soc_probe调用:
/* probes a new socdev */
static int soc_probe(struct platform_device *pdev)
{
struct snd_soc_card *card = platform_get_drvdata(pdev);
/*
* no card, so machine driver should be registering card
* we should not be here in that case so ret error
*/
if (!card)
return -EINVAL;
dev_warn(&pdev->dev,
"ASoC: machine %s should use snd_soc_register_card()\n",
card->name);
/* Bodge while we unpick instantiation */
card->dev = &pdev->dev;
return devm_snd_soc_register_card(&pdev->dev, card);
}
主要soc_probe
调用了devm_snd_soc_register_card
,在这个函数中将做了很多工作,查看里面最关键部分代码
int snd_soc_register_card(struct snd_soc_card *card)
{
if (!card->name || !card->dev)
return -EINVAL;
dev_set_drvdata(card->dev, card);
INIT_LIST_HEAD(&card->widgets);
INIT_LIST_HEAD(&card->paths);
INIT_LIST_HEAD(&card->dapm_list);
INIT_LIST_HEAD(&card->aux_comp_list);
INIT_LIST_HEAD(&card->component_dev_list);
INIT_LIST_HEAD(&card->list);
INIT_LIST_HEAD(&card->rtd_list);
INIT_LIST_HEAD(&card->dapm_dirty);
INIT_LIST_HEAD(&card->dobj_list);
card->instantiated = 0;
mutex_init(&card->mutex);
mutex_init(&card->dapm_mutex);
mutex_init(&card->pcm_mutex);
spin_lock_init(&card->dpcm_lock);
return snd_soc_bind_card(card);
}
在函数snd_soc_bind_card
中soc_probe_link_dais
调用调用了codec,dai和platform驱动的probe函数。
3.2 Platform
Platform 部分主要完成音频数据的管理,通过CPU的柱子接口DAI传输到Codec。在代码里面Platform改成了component说法,所以接口有所变化:
定义一个接口体snd_soc_component_driver
这个结构体内容比较多,在注册一个Platform时候需要定义一个这样的结构实例。
主要是注册了一个dai,还有一个DMA.
3.3 Codec
Codec 部分完成音频信号的转换,对音频进行控制;主要涉及的相关结构体
snd_soc_component
snd_soc_component_driver
snd_soc_dai_driver
snd_soc_dai_driver
snd_soc_dai
这些结构体都是创建后都是需要让Machine能够使用该Codec,所以对于不同Codec都需要遵从这些结构体的定义,去填充,将具体的操作函数以及数据进行赋值。注册也是在Codec代码中开始注册,注册后Machine就可以去使用。
以i.MX8MQ 中的wm8524音频芯片为例子:
wm8524采用I2C总线控制,
wm8524驱动代码:./sound/soc/codecs/wm8524.c
static int wm8524_codec_probe(struct platform_device *pdev)
{
struct wm8524_priv *wm8524;
int ret;
wm8524 = devm_kzalloc(&pdev->dev, sizeof(struct wm8524_priv),
GFP_KERNEL);
if (wm8524 == NULL)
return -ENOMEM;
platform_set_drvdata(pdev, wm8524);
wm8524->mute = devm_gpiod_get(&pdev->dev, "wlf,mute", GPIOD_OUT_LOW);
if (IS_ERR(wm8524->mute)) {
ret = PTR_ERR(wm8524->mute);
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "Failed to get mute line: %d\n", ret);
return ret;
}
ret = devm_snd_soc_register_component(&pdev->dev,
&soc_component_dev_wm8524, &wm8524_dai, 1);
if (ret < 0)
dev_err(&pdev->dev, "Failed to register component: %d\n", ret);
return ret;
}
关键在于devm_snd_soc_register_component
这个函数,里面将会调用snd_soc_register_dais
注册多个DAI设备;
static int snd_soc_register_dais(struct snd_soc_component *component,
struct snd_soc_dai_driver *dai_drv,
size_t count)
-
dev
: 传递设备节点,通常是相关的平台设备节点。 -
dai_drvs
: 指向一个snd_soc_dai_driver
数组,数组中的每个元素表示一个DAI设备的驱动信息。 -
count
: 指定dai_drvs
数组中的元素个数。