Rockchip RK3399 - ALSA子系统
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
在上一篇博客中我们已经介绍了RK3399 I2S/PCM控制器内容,同时也介绍了有关声卡芯片ALC5651的一些内容,这一节我们将正式来介绍声卡驱动。
一、ALSA框架
音频设备接口包括PCM、I2S、AC97等,分别适用于不用的应用场合。针对音频设备,linux内核中包含了两类音频设备驱动框架;
- OSS:开放声音系统,包含dsp和mixer字符设备接口,应用访问底层硬件是直接通过sound设备节点实现的;
- ALSA:先进linux声音架构,以card和组件(PCM、mixer等)为组件,应用是通过ALSA提供的alsa-lib库访问底层硬件的操作,不再访问sound设备节点了;本篇博客以ALSA为例,来介绍音频驱动;
1.1 ALSA概述
ALSA表示先进linux声音架构(Advanced Linux Sound Archiecture),它由一系列的内核驱动、应用程序编程接口(API)以及支持linux下声音的应用程序组成、
ALSA项目发起的原有是linux下的声卡驱动(OSS)没有获得积极的维护,而且落后于新的声卡技术。Jaroslav Kysela早先写了一个声卡驱动,并由此开始了ALSA项目,随后,更多的开发者加入到开发队伍中,更多的声卡获得支持,API的结构也获得了重组。目前已经成为了linux的主流音频体系结构。
1.2 音频硬件设备
在上一篇博客中我们介绍了RK3399与ALC5651之间的接线原理图啊,涉及到的硬件设备包括RK3399、ALC5651。在ALSA子系统中:
- 使用Platform来描述RK3399;
- 使用Codec来描述ALC5651;
- 使用Machine来描述RK3399与ALC5651之间的关系。
接下来我们会从硬件和软件的角度来阐述Platform、Codec、Machine。
1.2.1 Machine
Machine是指某一款机器,可以是某款设备、某款开发板、又或者是某款智能手机,由此可以看出Machine几乎是不可重用的。
每个Machine上的硬件实现可能不一样,SoC不一样、Codec不一样、音频的输入,输出设备也不一样,Machine为CPU、Codec、输入输出设备提供一个载体。
1.2.1 Platform
Platform一般是指某一个SoC平台,比如RK3399、S3C2440等等,与音频相关的通常包括SoC中的时钟、DMA、I2S、PCN、I2C等等,只要指定了SoC,那么我们可以认为他会有一个对应的Platform,它只与SoC相关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platform当做SoC更容易理解。
1.2.3 Codec
Codec字面上的意思就是编解码器,Codec里面包含了D/A、A/D、AIF、Mixer、PA、Line-in、Line-out等,通常包含多种输入(Microphone input、Line-in、I2S、PCM等)和多个输出(Line-out、耳机等),Codec和Platform一样,是一个可以被多个Machine使用,嵌入式Codec通常通过I2C/SPI对内部的寄存器进行控制。实际上、把Codec当做声卡芯片更容易理解;
- A/D:把麦克风拾取的模拟信号转换为数字信号;
- D/A:把音频I2S/PCM接口传送过来的数字信号转换为模拟信号;
- AIF:音频数字接口,用于Codec与SoC之间的数据传输;
- Mixer:混音器,把多路输入信号混合成单路输出;
- DRC:动态分为调节;
- LHPF:高低通滤波;
- PA:功率放大器;
1.3 ALSA框架
我们来看一下ALSA子系统的软件架构:
ALSA框架从上到下依次为应用程序、ALSA Library API、ALSA CORE、ASoC CORE、硬件驱动程序、硬件设备;
- 应用程序:tinyplay/tinycap/tinymix,这些用户程序直接调用alsa用户库接口来实现放音、录音、控制;
- ALSA Library API:alsa用户库接口,对应用程序提供统一的API接口,这样可以隐藏了驱动层的实现细节,简化了应用程序的实现难度;常见有tinyalsa、alsa-lib;
- ALSA CORE:alsa核心层,向上提供逻辑设备(PCM/CTL/MIDI/TIMER/…)系统调用,向下驱动硬件设备(Machine/I2S/DMA/CODEC);
- ASoC CORE:建立在标准ALSA CORE基础上,为了更好支持嵌入式系统和应用于移动设备的音频Codec的一套软件体系,它将音频硬件设备驱动划分为Codec、Platform 和 Machine;
- Hardware Driver:音频硬件设备驱动,由三大部分组成,分别是 Machine、Platform、Codec;
以前我们总提到,写上层应用程序的人,不应该关系硬件的实现,所以我们会给硬件写一个驱动,然后写应用程序的人直接调用驱动就可以访问硬件了,具体如下:
简单的应用程序是没有问题的,但是一些复杂的硬件还是不行,应用程序需要了解很多驱动的接口,于是就进行了改进。
在驱动和APP之间添加一些库,APP去访问那些库,进而访问驱动。这些库封装了声卡复杂的使用方法,对于音频系统,库lib有两种选择,一种是alsa-lib,以及alsa-lib的简化版本tinyalsa。
1.4 ASoC
如果第一次接触ALSA子系统,我们看到上面的架构图,你可能会和我有一样的疑问,既然有了ALSA CORE、为啥又整出来ASoC CORE;实际上ASoC CORE就是在标准的ALSA CORE基础上做了一层封装,至于初衷是什么,在内核文档 Documentation/sound/soc/overview.rst中详细列出了,在ASoC子系统之前,内核中对SoC音频有一些支持,但它具有一些限制:
- 标准的ALSA驱动框架里面Codec驱动往往与SoC耦合过于紧,不利于在多样化的平台/机器上移植复用,例如,仅是wm8731的驱动,当时linux中有分别针对4个平台的驱动代码;
- 音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中非常普通的,而且通常都需要特定于机器的代码进行重新对音频路径进行配置;
- 当进行播放或录音时,驱动会让整个Codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的;
ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树中:sound/soc;
- 独立的Codec驱动,允许将编解码器驱动程序重新使用在其他平台和设备上;
- 简单的I2S/PCM音频接口设置,使Codec和SoC之间的连接便于配置。每个SoC接口和Codec都会将其音频接口能力与核心进行匹配和配置,这些信息都会在应用程序硬件参数确定后使用;
- 动态音频电源管理DAPM,使得Codec任何时候都工作在最低功耗状态,同时负责音频路由的创建;
- POPs和click 音抑制弱化处理,在 ASoC 中通过正确的音频部件上下电次序来实现;
- Machine驱动的特定控制,比如耳机、麦克风的插拔检测,外放功放的开关;
为了实现以上功能,ASoC将音频系统按照硬件组成拆分成多个可重用的组件驱动程序:分别是 Machine、Platform、Codec。三者的关系如下图所示:
1.4.1 Machine驱动
Machine driver描述了如何控制platform、codec、cpu dai(Digital Audio Interface,数字音频接口)和codec dai,使得互相配合在一起工作;比如播放声音时,通过SoC的DAI将音频数据发送给Codec进行处理,最终由Codec输出驱动耳机。
Machine driver通过配置dai_link把cpu_dai、codec_dai各个音频接口给链结成一条条音频数据链路,然后注册snd_soc_card。
dai_link:Machine driver定义的音频数据链路,它指定链路用到的platform、codec、codec_dai、cpu_dai,这四者就构成了一条音频数据链路用于多媒体声音的回放和录制。
比如对于goni_wm8994平台的 media 链路:
- codec="wm8994-codec";
- codec_dai="wm8994-aif1";
- cpu_dai="samsung-i2s";
- platform="samsung-audio";
这四者就构成了一条音频数据链路用于多媒体声音的回放和录制。一个系统可能有多个音频数据链路,比如media和voice,因此可以定义多个dai_link 。如 WM8994 的典型设计,有三个 dai_link,分别是:
- AP<>AIF1 的 “HIFI”(多媒体声音链路);
- BP<>AIF2 的 “Voice”(通话语音链路);
- 以及 BT<>AIF3(蓝牙SCO语音链路);
更多信息参考:Documentation/sound/soc/machine.rst,https://www.kernel.org/doc/html/v6.3/sound/soc/machine.html。
1.4.2 Platform驱动
Platfrom driver驱动分为三个部分:。
(1) cpu dai driver:在嵌入式系统里面通常指SoC的 I2S、PCM 总线控制器,负责把音频数据从 I2S tx FIFO 搬运Codec(这是回放的情形,录制则方向相反)。每个cpu dai driver必须提供以下功能:
- DAI描述信息;
- DAI配置信息;
- PCM描述信息;
- 系统时钟配置;
- 挂起和恢复(可选);
更多信息参考: Documentation/sound/soc/dai.rst。
(2) DMA driver :负责把dma buffer中的音频数据搬运到 I2S tx FIFO。值得留意的是:某些情形下是不需要 dma 操作的,比如Modem和Codec直连,因为Modem本身已经把数据送到 FIFO 了,这时只需启动codec_dai 接收数据即可;DMA driver可以参考soc/pxa/pxa2xx-pcm.c;
(3) DSP driver
每个DSP driver通常提供以下功能:
- DAPM拓扑;
- Mixer controls;
- DMA IO to/from DSP buffers (if applicable);
- Definition of DSP front end (FE) PCM devices;
更多信息参考: Documentation/sound/soc/platform.rst,https://www.kernel.org/doc/html/v6.3/sound/soc/platform.html。
1.4.3 Codec驱动
Codec driver它不应包含任何特定于目标平台或设备的代码。所有特定于平台和设备的代码应分别添加到平台和机器驱动程序中,Codec driver提供了配置编解码器、FM、MODEM、BT或外部DSP,以提供音频捕获和播放功能。
每个Codec driver必须提供以下功能:
- Codec DAI和PCM的配置信息;
- Codec的控制接口,如I2C/SPI;
- Mixer和其它音频控件;
- Codec的音频操作;
- DAPM描述信息;
- DAPM事件处理程序;
可选地,Codec driver还可以提供:
- DAC数字静音控制;
更多信息参考: Documentation/sound/soc/codec.rst;https://www.kernel.org/doc/html/v6.3/sound/soc/codec.html。
1.5 目录结构
1.5.1 源码
linux内核将ALSA驱动相关的代码放在sound/目录下,这下面的文件还是比较多的,我们大概了解一下即可。
root@zhengyang:/work/sambashare/rk3399/linux-5.2.8# ls sound/ ac97 arm core hda Kconfig mips oss pcmcia soc spi x86 ac97_bus.c atmel drivers i2c last.c modules.builtin parisc ppc sound_core.c synth xen aoa built-in.a firewire isa Makefile modules.order pci sh sparc usb
其中:
- i2s:存放的是ALSA自己的I2S控制代码;
- i2c:存放的是ALSA自己的I2C控制代码;
- core:是ALSA驱动的中间层,它是整个ALSA驱动的核心层;
- drivers:存放一些与CPU、BUS架构无关的公共代码;
- pci:pci声卡的顶层目录,子目录包含各种pci声卡的代码;
- isa:isa声卡的顶层目录,子目录包含各种isa声卡的代码;
- soc:针对ASoC体系的中间层代码;
- soc/codecs:针对SoC体系的各种Codec代码,与平台无关;
更多目录结构信息可以参考:https://www.kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html。
ALSA API可以分解成以下几个主要的接口:
- 声卡和设备管理接口,提供管理声卡注册和请求可用设备的通用接口;
- PCM接口:管理数字音频回放(playback)和录音(capture)的接口;
- Raw MIDI接口:支持MIDI(Musical Instrument Digital Interface),标准的电子乐器,这些API提供对声卡上MIDI的访问;
- Proc信息接口(Proc Info API);
- 定时器(Timer)接口,为同步音频事件提供对声卡上时间处理硬件的访问;
- 混音器(Mixer)接口;
1.5.2 ALSA设备文件
Linux下可以看到音频设备文件结构,可以看到这些字符设备的主设备号都是116;
root@zhengyang:~# ll /dev/snd crw-rw----+ 1 root audio 116, 6 Jul 6 18:39 controlC0 crw-rw----+ 1 root audio 116, 5 Jul 6 18:39 midiC0D0 crw-rw----+ 1 root audio 116, 3 Jul 6 18:39 pcmC0D0c crw-rw----+ 1 root audio 116, 2 Jul 6 18:39 pcmC0D0p crw-rw----+ 1 root audio 116, 4 Jul 6 18:39 pcmC0D1p crw-rw----+ 1 root audio 116, 1 Jul 6 18:39 seq crw-rw----+ 1 root audio 116, 33 Jul 6 18:39 timer
其中:
- controlC0:用于声卡的控制,例如通道选择,混音,麦克控制,音量加减,开关等;
- midiC0D0:用于播放midi音频;
- pcmC0D0c:用于录音的pcm设备;
- pcmC0D0p:用于播放的pcm设备;
- pcmC0D1p:用于播放的pcm设备;
- seq:音序器;
- timer:定时器;
C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。
从上面的列表可以看出,声卡0下挂了5个设备,根据声卡的实际能力,驱动实际上可以挂载更多种类的设备,我们通常更关心的是pcm和control这两种设备,默认一个声卡对应一个Control设备。
二、ALSA核心数据结构
学习ALSA驱动,首先要了解驱动框架涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
由于ASoC是建立在标准ALSA CORE上的一套软件体系,因此本篇博客重点介绍的都是ALSA CORE中的数据结构,并不涉及到ASoC CORE中的数据结构。ALSA CORE中的数据结构大部分定义在include/sound/core.h、以及sound/core/目录下的文件中。
3.1 struct snd_card
linux内核中使用struct snd_card表示ALSA音频驱动中的声卡设备,snd_card可以说是整个ALSA音频驱动最顶层的一个数据结构,比如我们就会为我们使用的声卡设备AL5651分配一个struct snd_card数据结构。
整个声卡的软件逻辑结构开始于该结构,几乎所有与声音相关的逻辑设备都是在snd_card的管理之下,声卡驱动的第一个动作通常就是创建一个snd_card结构体;数据结构定义在include/sound/core.h文件中:
/* main structure for soundcard */ struct snd_card { int number; /* number of soundcard (index to snd_cards) */ char id[16]; /* id string of this card */ char driver[16]; /* driver name */ char shortname[32]; /* short name of this soundcard */ char longname[80]; /* name of this soundcard */ char irq_descr[32]; /* Interrupt description */ char mixername[80]; /* mixer name */ char components[128]; /* card components delimited with space */ struct module *module; /* top-level module */ void *private_data; /* private data for soundcard */ void (*private_free) (struct snd_card *card); /* callback for freeing of private data */ struct list_head devices; /* devices */ struct device ctl_dev; /* control device */ unsigned int last_numid; /* last used numeric ID */ struct rw_semaphore controls_rwsem; /* controls list lock */ rwlock_t ctl_files_rwlock; /* ctl_files list lock */ int controls_count; /* count of all controls */ int user_ctl_count; /* count of all user controls */ struct list_head controls; /* all controls for this card */ struct list_head ctl_files; /* active control files */ struct snd_info_entry *proc_root; /* root for soundcard specific files */ struct proc_dir_entry *proc_root_link; /* number link to real id */ struct list_head files_list; /* all files associated to this card */ struct snd_shutdown_f_ops *s_f_ops; /* file operations in the shutdown state */ spinlock_t files_lock; /* lock the files for this card */ int shutdown; /* this card is going down */ struct completion *release_completion; struct device *dev; /* device assigned to this card */ struct device card_dev; /* cardX object for sysfs */ const struct attribute_group *dev_groups[4]; /* assigned sysfs attr */ bool registered; /* card_dev is registered? */ wait_queue_head_t remove_sleep; #ifdef CONFIG_PM unsigned int power_state; /* power state */ wait_queue_head_t power_sleep; #endif #if IS_ENABLED(CONFIG_SND_MIXER_OSS) struct snd_mixer_oss *mixer_oss; int mixer_oss_change_count; #endif };
其中:
- number: 声卡设备编号,通常从0开始,通过编号可以在snd_cards指针数组中找到对应的声卡设备;
- id:声卡设备的标识符;
- driver:驱动名称;
- shortname:设备简称,更多地用于打印信息;
- longname:设备名称,会在具体驱动中设置,主要反映在/proc/asound/cards中;
- irq_descr:中断描述信息;
- mixername:混音器名称;
- components:声卡组件名称,由空格分隔;
- module:顶层模块;
- private_data:声卡的私有数据;
- private_free:释放私有数据的回调函数;
- devices:保存该声卡下所有逻辑设备的链表;链表中存放的数据类型为struct snd_device;
- ctl_dev:声卡Control设备内核设备结构体,其parent为card_dev,class为sound_class,主设备号为116;
- last_numid:存储注册snd_control时为其分配的编号;
- controls_rwsem:读写信号量,用于并发操作controls链表;
- ctl_files_rwlock:读写自旋锁,用于并发操作ctl_files链表;
- controls_count:controls链表的长度;
- user_ctl_count:用户控制设备的数量;
- controls:保存该声卡下所有控件(controls)的链表;该链表中存放的数据类型为struct snd_kcontrol;
- ctl_files:用于管理该card下的active的control设备;链表中存放的数据类型为struct snd_ctl_file;
- proc_root:声卡设备在proc文件系统的根目录;即/proc/asound/card%d目录;
- proc_root_link:指向/proc/asound/card%d的链接文件,文件名为id;
- files_list:保存此声卡相关的所有文件的链表;链表中存放的数据类型为struct snd_monitor_file;
- s_f_ops:关机状态下的文件操作;
- files_lock:自旋锁;
- shutdown:此声卡正在关闭;
- release_completion:释放完成;
- dev:分配给此声卡的设备,一般为平台设备的device;
- card_dev:声卡设备的内核设备结构体,card用于在sys中显示,用于代表该card;把snd_card看做是device的子类,其parent为dev,class为sound_class,未设置设备号devt(默认就是0);
- dev_groups:分配的sysfs属性组;
- registered:声卡设备card_dev是否已注册;
- remove_sleep:等待队列头;
- power_state:电源状态;
- power_sleep:电源等待队列头;
每一个声卡设备的创建都是通过snd_card_new函数实现的,声卡设备被注册后都会被添加到全局snd_cards指针数组中;
static struct snd_card *snd_cards[SNDRV_CARDS];
3.2 struct snd_device
我们知道声卡设备一般包含许多功能模块,比如PCM(录音和播放)、Control(声卡控制),因此ALSA将声卡的功能模块又抽象为一个逻辑设备,与之对应的数据结构就是struct snd_device,定义在sound/core/device.c;
struct snd_device { struct list_head list; /* list of registered devices */ struct snd_card *card; /* card which holds this device */ enum snd_device_state state; /* state of the device */ enum snd_device_type type; /* device type */ void *device_data; /* device structure */ struct snd_device_ops *ops; /* operations */ };
其中:
- list:用于构建双向链表节点,该节点会添加到声卡设备snd_card的devices链表中;
- snd_card:表示当前声卡逻辑设备所属的声卡设备;
- state:表示当前声卡逻辑设备的状态;
- type:表示当前声卡逻辑设备的类型,比如pcm、control设备;
- device_data:一般用于存放具体的功能模块逻辑设备的结构,比如对于pcm逻辑设备存放的就是snd_pcm实例;
- ops:声卡逻辑设备的操作集;
每一个声卡逻辑设备的创建最终会调用snd_device_new来生成一个snd_device实例,并把该实例链接到snd_card的devices链表中。通常,linux内核已经提供了一些常用的功能模块逻辑设备的创建函数,而不必直接调用snd_device_new,比如: snd_pcm_new、snd_ctl_create。
需要注意的是:声卡逻辑设备注册后会在/dev/snd目录下生成对应的字符设备文件。
3.2.1 struct snd_device_ops
linux中使用snd_device_ops来表示声卡逻辑设备的操作集;定义在include/sound/core.h文件;
struct snd_device_ops { int (*dev_free)(struct snd_device *dev); int (*dev_register)(struct snd_device *dev); int (*dev_disconnect)(struct snd_device *dev); };
其中:
- dev_free:声卡逻辑设备释放函数,在卸载声卡设备时被调用;
- dev_register:声卡逻辑设备注册函数,在注册声卡设备被调用;
- dev_disconnect:声卡逻辑设备断开连接函数,在关闭声卡设备时被调用。
3.2.2 enum snd_device_state
linux内核使用snd_device_state表示声卡逻辑设备的状态,定义在include/sound/core.h文件;
enum snd_device_state { SNDRV_DEV_BUILD, // 构建中 SNDRV_DEV_REGISTERED, // 已经准备并准备就绪 SNDRV_DEV_DISCONNECTED, // 已断开连接 };
3.2.3 enum snd_device_type
linux内核使用snd_device_state表示声卡逻辑设备的类型,定义在include/sound/core.h文件;
/* device allocation stuff */ /* type of the object used in snd_device_*() * this also defines the calling order */ enum snd_device_type { SNDRV_DEV_LOWLEVEL, SNDRV_DEV_INFO, SNDRV_DEV_BUS, SNDRV_DEV_CODEC, SNDRV_DEV_PCM, SNDRV_DEV_COMPRESS, SNDRV_DEV_RAWMIDI, SNDRV_DEV_TIMER, SNDRV_DEV_SEQUENCER, SNDRV_DEV_HWDEP, SNDRV_DEV_JACK, SNDRV_DEV_CONTROL, /* NOTE: this must be the last one */ };
其中:
- SNDRV_DEV_LOWLEVEL:低级别硬件访问接口;
- SNDRV_DEV_INFO:信息查询接口;
- SNDRV_DEV_BUS:总线接口,如USB、PCI等;
- SNDRV_DEV_CODEC:编解码器设备;
- SNDRV_DEV_PCM:PCM 设备,包括输入输出设备以及混音器等;
- SNDRV_DEV_COMPRESS:压缩和解压缩设备;
- SNDRV_DEV_RAWMIDI:原始MIDI设备;
- SNDRV_DEV_TIMER:定时器设备;
- SNDRV_DEV_SEQUENCER:序列器设备;
- SNDRV_DEV_HWDEP:硬件依赖设备;
- SNDRV_DEV_JACK:JACK音频连接设备;
- SNDRV_DEV_CONTROL:control设备,此项必须放在最后;
3.3 struct snd_minor
linux内核使用snd_minor表示声卡逻辑设备上下文信息,它在调用snd_register_device函数注册声卡逻辑设备时被初始化,在声卡逻辑设备被使用时就可以从该结构体中得到相应的信息。定义在include/sound/core.h文件;
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 */ };
其中:
- type:设备类型,取值为 SNDRV_DEVICE_TYPE_XXX;
- card:声卡逻辑设备所属的声卡设备的编号;
- device:设备索引;
- f_ops:文件操作集。
- private_data:用户提供给 f_ops->open函数的私有数据指针;
- dev:声卡逻辑设备对应的 struct device 结构体指针;
- card_ptr:指向所属声卡设备;
每一个snd_minor的创建都是通过snd_register_device函数实现的,并被添加到全局snd_minors指针数组中;
static struct snd_minor *snd_minors[SNDRV_OS_MINORS];
3.4 数据结构关系
为了更加形象的表示struct snd_card、struct snd_device、struct snd_minor 之间的关系,我们绘制了如下关系框图:
三、声卡的创建和注册
3.1 创建声卡设备
linux内核提供了snd_card_new函数创建和初始化snd_card数据结构,函数定义在sound/core/init.c:
/** * snd_card_new - create and initialize a soundcard structure * @parent: the parent device object * @idx: card index (address) [0 ... (SNDRV_CARDS-1)] * @xid: card identification (ASCII string) * @module: top level module for locking * @extra_size: allocate this extra size after the main soundcard structure * @card_ret: the pointer to store the created card instance * * Creates and initializes a soundcard structure. * * The function allocates snd_card instance via kzalloc with the given * space for the driver to use freely. The allocated struct is stored * in the given card_ret pointer. * * Return: Zero if successful or a negative error code. */ 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); // 动态分配snd_crad数据结构,以及额外空间 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); // 用于为声卡设备分配一个索引,索引编号范围为0~SNDRV_CARDS-1 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 将指定索引idx对应的位位置1 */ if (idx >= snd_ecards_limit) snd_ecards_limit = idx + 1; /* increase the limit */ mutex_unlock(&snd_card_mutex); // 释放互斥锁 card->dev = parent; // 初始化成员变量 card->number = idx; card->module = module; 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); // 初始化双向链表头节点 #ifdef CONFIG_PM init_waitqueue_head(&card->power_sleep); #endif init_waitqueue_head(&card->remove_sleep); // 初始化等待队列头 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); // 设置名称card%d 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); // 创建control设备 if (err < 0) { dev_err(parent, "unable to register control minors\n"); goto __error; } err = snd_info_card_create(card); // 创建声卡的proc文件 if (err < 0) { dev_err(parent, "unable to create card info\n"); goto __error_ctl; } *card_ret = card; // 保存声卡设备 return 0; __error_ctl: snd_device_free_all(card); __error: put_device(&card->card_dev); return err; }
函数的输入参数包括:
- parent:父设备对象,一般为platform设备;
- idx:声卡的索引;
- xid:声卡设备的标识;
- module:用于锁定的顶层模块;
- extra_size:分配给驱动程序额外使用的空间大小,该空间用于存储声卡的私有数据private_data;
- card_set:指向存储创建的snd_card实例的指针;
函数执行流程大致如下:
- 调用kzalloc动态分配snd_card数据结构,同时申请extra_size大小的额外空间,用于保存声卡设备的私有数据;
- 为声卡设备分配一个编号(或者说索引),范围在为0~SNDRV_CARDS-1之间;
- 调用set_bit标记 snd_cards_lock 中的一个位,以表示对应的声卡设备已被占用。具体来说,它会将idx对应的位设置为 1,标记这个声卡设备已经被实例化使用了;
- 初始化声卡设备成员;
- card->dev= parent;
- card->number = idx;
- card->module - module;
- 初始化各种锁、以及各种双向链表头节点;
- 调用device_initialize进行设备card->card_dev初始化;
- 初始化card->card_dev成员变量;
- parent设置为入参参数parent;
- class设置为sound_class;
- 初始化release、groups等;
- 调用snd_ctl_create创建control设备(这是一个逻辑设备);
- 调用snd_info_card_create在proc文件系统/proc/saound下创建card%d文件夹,同时在card%d文件夹下创建id文件;
3.1.1 snd_cards_lock
snd_card_lock是一个位图类型的变量,位图是一种用于紧凑表示二进制状态的数据结构;
static DECLARE_BITMAP(snd_cards_lock, SNDRV_CARDS); static struct snd_card *snd_cards[SNDRV_CARDS];
在此处,该位图被用于跟踪哪些声卡已经被其他驱动程序实例化使用,从而避免出现冲突。
声明时采用了 DECLARE_BITMAP 宏,这意味着变量会被分配到内核代码段中。除此之外,还需要指定该位图的大小,这里的大小为 SNDRV_CARDS(默认为8),即支持的声卡的最大数量。
比如:snd_card_lock第6位被设置为1,就表示id为6的声卡设备已经被分配了,对应的声卡设备存储在snd_cards指针数组中,每一个元素指向一个snd_crad。
3.1.2 snd_ecards_limit
snd_ecards_limit定义在sound/core/sound.c文件中:
/* this one holds the actual max. card number currently available. * as default, it's identical with cards_limit option. when more * modules are loaded manually, this limit number increases, too. */ int snd_ecards_limit; EXPORT_SYMBOL(snd_ecards_limit);
这个变量的作用是跟踪已经分配给各个驱动程序实例的声卡设备编号,以便在创建新的声卡设备时分配未被使用的编号。
3.1.3 sound_class
sound_class是在init_soundcore函数中进行的初始化,位于sound/sound_core.c文件中:
struct class *sound_class; EXPORT_SYMBOL(sound_class); MODULE_DESCRIPTION("Core sound module"); MODULE_AUTHOR("Alan Cox"); MODULE_LICENSE("GPL"); static int __init init_soundcore(void) { int rc; rc = init_oss_soundcore(); if (rc) return rc; sound_class = class_create(THIS_MODULE, "sound"); //会在/sys/class/目录下创建一个新的文件夹,/sys/class/sound; if (IS_ERR(sound_class)) { cleanup_oss_soundcore(); return PTR_ERR(sound_class); } sound_class->devnode = sound_devnode; return 0; } subsys_initcall(init_soundcore);
可以看到在init_soundcore函数中创建了sound_class,名称为"sound",并设置devnode指向了sound_devnode;
static char *sound_devnode(struct device *dev, umode_t *mode) { if (MAJOR(dev->devt) == SOUND_MAJOR) // 如果主设备号为14,就不会创建设备节点 return NULL; return kasprintf(GFP_KERNEL, "snd/%s", dev_name(dev)); // /设置dev下设备节点名称 /dev/snd/xxx }
那么udev就会根据devnode的返回值来决定创建的设备节点文件的相对路径。同时,udev还会为这些设备节点文件设置相应的权限、所属用户和组等信息,以确保用户可以正确访问这些设备节点文件。
3.1.4 创建声卡proc文件
snd_info_card_create函数会在proc文件系统下/proc/asound目录下创建card%d文件夹,函数定义在函数定义在sound/core/info.c;
/* * create a card proc file * called from init.c */ int snd_info_card_create(struct snd_card *card) { char str[8]; struct snd_info_entry *entry; if (snd_BUG_ON(!card)) return -ENXIO; sprintf(str, "card%i", card->number); entry = create_subdir(card->module, str); // 创建crd%d目录 if (!entry) return -ENOMEM; card->proc_root = entry; // 保存card entry return snd_card_ro_proc_new(card, "id", card, snd_card_id_read); // 在card%d目录下下创建id文件,并设置文件的read函数(返回card->id) }
snd_info_card_create函数接受一个指向struct snd_card 结构体的指针作为参数,用于表示需要创建proc文件的声卡设备。该函数:
- 首先通过sprintf 函数生成一个字符数组,命名为"card%d",其中%d为该声卡设备的编号;
- 接着,使用create_subdir 函数在/prco/asound目录下创建一个名为"card%d"的文件夹,并返回对应的snd_info_entry结构体指针。如果创建失败,则函数返回 -ENOMEM 错误码;
- 之后,该函数调用snd_card_ro_proc_new函数在/proc/asound/card%d目录下创建一个名为 "id" 的RO 类型 proc 文件,并设置id文件的read回调函数为snd_card_id_read;
(1) create_subdir
create_subdir函数接受一个 struct module结构体指针和一个字符数组指针作为参数,用于表示需要创建子目录的模块对象和子目录名称。
static struct snd_info_entry *create_subdir(struct module *mod, const char *name) { struct snd_info_entry *entry; entry = snd_info_create_module_entry(mod, name, NULL); if (!entry) return NULL; entry->mode = S_IFDIR | 0555; if (snd_info_register(entry) < 0) { snd_info_free_entry(entry); return NULL; } return entry; }
该函数:
- 先通过 snd_info_create_module_entry函数创建一个名为card%d的proc文件系统入口结构体snd_info_entry,并将其mode属性设置为S_IFDIR | 0555,表示该entry对象为一个文件夹。如果创建失败,则返回NULL;
- 接着,该函数使用snd_info_register函数将entry对象注册到proc文件系统中,并返回该entry对象的指针。如果注册失败,则通过snd_info_free_entry(函数释放entry对象占用的内存,并返回 NULL;
(2) snd_card_ro_proc_new
snd_card_ro_proc_new函数定义在include/sound/info.h:
/** * snd_card_ro_proc_new - Create a read-only text proc file entry for the card * @card: the card instance * @name: the file name * @private_data: the arbitrary private data * @read: the read callback * * This proc file entry will be registered via snd_card_register() call, and * it will be removed automatically at the card removal, too. */ static inline int snd_card_ro_proc_new(struct snd_card *card, const char *name, void *private_data, void (*read)(struct snd_info_entry *, struct snd_info_buffer *)) { return snd_card_rw_proc_new(card, name, private_data, read, NULL); }
该函数在/proc/asound/card%d目录下创建一个名为"id"的RO类型proc文件,并设置id文件的read函数为snd_card_id_read;
/** * snd_card_rw_proc_new - Create a read/write text proc file entry for the card * @card: the card instance * @name: the file name * @private_data: the arbitrary private data * @read: the read callback * @write: the write callback, NULL for read-only * * This proc file entry will be registered via snd_card_register() call, and * it will be removed automatically at the card removal, too. */ int snd_card_rw_proc_new(struct snd_card *card, const char *name, void *private_data, void (*read)(struct snd_info_entry *, struct snd_info_buffer *), void (*write)(struct snd_info_entry *entry, struct snd_info_buffer *buffer)) { struct snd_info_entry *entry; entry = snd_info_create_card_entry(card, name, card->proc_root); // 在/proc/asound/card%d目录下创建id文件 if (!entry) return -ENOMEM; snd_info_set_text_ops(entry, private_data, read); // 设置id文件的read函数为snd_card_id_read if (write) { entry->mode |= 0200; entry->c.text.write = write; } return 0; }
(3) snd_card_id_read
snd_card_id_read定义在sound/core/info.c文件中,用于在proc文件系统中读取声卡设备id的信息,并将其输出到用户空间程序中。
static void snd_card_id_read(struct snd_info_entry *entry, struct snd_info_buffer *buffer) { struct snd_card *card = entry->private_data; snd_iprintf(buffer, "%s\n", card->id); }
3.2 注册声卡设备
snd_card声卡设备注册是通过snd_card_register函数来完成的,函数定义在sound/core/init.c;
/** * snd_card_register - register the soundcard * @card: soundcard structure * * This function registers all the devices assigned to the soundcard. * Until calling this, the ALSA control interface is blocked from the * external accesses. Thus, you should call this function at the end * of the initialization of the card. * * Return: Zero otherwise a negative error code if the registration failed. */ int snd_card_register(struct snd_card *card) { int err; if (snd_BUG_ON(!card)) return -EINVAL; if (!card->registered) { // 如果没有注册,则将card_dev注册到内核 err = device_add(&card->card_dev); if (err < 0) return err; card->registered = true; // 设备标志位 } if ((err = snd_device_register_all(card)) < 0) // 遍历devices链表,注册声卡所有的逻辑设备 return err; mutex_lock(&snd_card_mutex); if (snd_cards[card->number]) { // 由于声卡设备未注册,所以这里为NULL /* already registered */ mutex_unlock(&snd_card_mutex); return snd_info_card_register(card); /* register pending info */ } if (*card->id) { // 如果设置了声卡设备的标识 /* make a unique id name from the given string */ char tmpid[sizeof(card->id)]; memcpy(tmpid, card->id, sizeof(card->id)); snd_card_set_id_no_lock(card, tmpid, tmpid); } else { /* create an id from either shortname or longname */ const char *src; src = *card->shortname ? card->shortname : card->longname; snd_card_set_id_no_lock(card, src, retrieve_id_from_card_name(src)); } snd_cards[card->number] = card; // 保存声卡设备 mutex_unlock(&snd_card_mutex); err = snd_info_card_register(card); if (err < 0) return err; #if IS_ENABLED(CONFIG_SND_MIXER_OSS) if (snd_mixer_oss_notify_callback) snd_mixer_oss_notify_callback(card, SND_MIXER_OSS_NOTIFY_REGISTER); #endif return 0; }
主要完成了如下操作:
- 调用device_add将声卡设备card_dev注册到linux内核系统的设备驱动程序模型;会在/sys/class/sound类文件下创建card%d链接文件,但是由于未设置设备号devt(默认就是0),不会在文件系统创建设备节点/dev/snd/card%d;
- 调用snd_device_register_all注册所有的逻辑设备;
- 将当前的声卡设备保存到指针数组snd_cards中;
- 调用snd_info_card_register注册声卡设备的proc文件;在/proc/asound/下创建card%d的链接文件,文件名为声卡设备的标识id;
比如,我们可以在/proc/asound/目录下看到realtekrt5651co指向了声卡设备在proc文件系统的根目录/proc/asound/card1:
root@rk3399:/# ll /proc/asound/ dr-xr-xr-x 5 root root 0 Aug 6 16:43 card0/ dr-xr-xr-x 5 root root 0 Aug 6 16:55 card1/ -r--r--r-- 1 root root 0 Aug 6 15:10 cards -r--r--r-- 1 root root 0 Aug 6 16:55 devices lrwxrwxrwx 1 root root 5 Aug 6 16:55 hdmisound -> card0/ -r--r--r-- 1 root root 0 Aug 6 16:55 pcm lrwxrwxrwx 1 root root 5 Aug 6 16:55 realtekrt5651co -> card1/ dr-xr-xr-x 6 root root 0 Aug 6 16:55 seq/ -r--r--r-- 1 root root 0 Aug 6 16:55 timers -r--r--r-- 1 root root 0 Aug 6 16:55 version
3.2.1 snd_device_register_all
snd_device_register_all函数定义在sound/core/device.c文件,该函数实际上就是遍历声卡设备的逻辑设备链表devices,依次调用__snd_device_register函数;
/* * register all the devices on the card. * called from init.c */ int snd_device_register_all(struct snd_card *card) { struct snd_device *dev; int err; if (snd_BUG_ON(!card)) return -ENXIO; list_for_each_entry(dev, &card->devices, list) { // 遍历每一个声卡逻辑设备 err = __snd_device_register(dev); if (err < 0) return err; } return 0; }
__snd_device_register函数本质上就是调用声卡逻辑设备操作集中的dev_register函数,以control设备为例,其dev_register被设置为snd_ctl_dev_register,snd_ctl_dev_register函数最终调用snd_register_device函数来实现声卡逻辑设备的注册;
/* * registration of the control device */ static int snd_ctl_dev_register(struct snd_device *device) { struct snd_card *card = device->device_data; return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1, // 注册声卡逻辑设备 &snd_ctl_f_ops, card, &card->ctl_dev); }
3.2.2 snd_info_card_register
snd_info_card_register函数定义在sound/core/info.c文件;
/* * register the card proc file * called from init.c * can be called multiple times for reinitialization */ int snd_info_card_register(struct snd_card *card) { struct proc_dir_entry *p; int err; if (snd_BUG_ON(!card)) return -ENXIO; err = snd_info_register(card->proc_root); // 注册proc_root if (err < 0) return err; if (!strcmp(card->id, card->proc_root->name)) // 在调用snd_card_new创建声卡设备时会将card->id设置为参数xid,card->proc_root->name会被设置为card%d return 0; if (card->proc_root_link) return 0; p = proc_symlink(card->id, snd_proc_root->p, card->proc_root->name); // 创建card->id指向card->proc_root->name的链接 if (!p) return -ENOMEM; card->proc_root_link = p; return 0; }
在Linux系统中,proc文件系统是一种虚拟文件系统,它提供了一种机制,可以通过文件的方式来访问内核和进程中的数据结构和信息。
该函数接受一个指向 struct snd_card 结构体的指针card作为参数,用于指定需要注册的声卡设备;
- 首先该函数调用snd_info_register函数来注册该设备对应的proc根目录;。如果注册失败,则返回相应的负数错误码;
- 接着,该函数会检查该声卡设备的标识id是否与proc_root的名称相同,如果是,则直接返回0。否则,该函数会创建一个proc符号链接(proc symlink),将声卡设备的id指向proc_root的名称,并保存符号链接的指针到设备结构体中。如果符号链接创建失败,则返回相应的负数错误码;
proc_symlink函数用于创建一个proc符号链接(proc symlink),该函数接受三个参数,分别为:
- name:需要创建的符号链接文件的名称;
- parent:指定需要创建的链接文件位于该目录下;
- dest:指定需要为哪个文件创建链接文件;
它在用户空间等效ln -s dest name,即创建name链接文件,指向dest。
四、声卡逻辑设备的创建和注册
我们知道声卡设备一般包含许多功能模块,比如PCM(录音和播放)、Control(声卡控制),因此ALSA将声卡的功能模块又抽象为一个逻辑设备,这个逻辑设备在ALSA中使用struct snd_device数据结构表示。
ALSA为我们提供了snd_device_new函数可以为声卡逻辑设备分配一个snd_device数据结构,并将该实例链接到 snd_card 的 devices 链表中,同时也提供了逻辑设备的注册函数snd_register_device。
根据声卡功能模块的种类不同,ALSA封装了一些常用的部件的创建函数,而不必直接调用snd_device_new,常见的如下:
PCM -- snd_pcm_new() RAWMIDI -- snd_rawmidi_new() CONTROL -- snd_ctl_create() TIMER -- snd_timer_new() INFO -- snd_card_proc_new() JACK -- snd_jack_new()
4.1 创建声卡逻辑设备
snd_device_new函数用于创建一个新的snd_device实例,并将实例注册到声卡设备card的的逻辑设备链表devices中;函数定义在sound/core/device.c;
/** * snd_device_new - create an ALSA device component * @card: the card instance * @type: the device type, SNDRV_DEV_XXX * @device_data: the data pointer of this device * @ops: the operator table * * Creates a new device component for the given data pointer. * The device will be assigned to the card and managed together * by the card. * * The data pointer plays a role as the identifier, too, so the * pointer address must be unique and unchanged. * * Return: Zero if successful, or a negative error code on failure. */ int snd_device_new(struct snd_card *card, enum snd_device_type type, void *device_data, struct snd_device_ops *ops) { struct snd_device *dev; struct list_head *p; if (snd_BUG_ON(!card || !device_data || !ops)) return -ENXIO; dev = kzalloc(sizeof(*dev), GFP_KERNEL); // 动态分配snd_device数据结构 if (!dev) return -ENOMEM; INIT_LIST_HEAD(&dev->list); // 初始化链表节点 dev->card = card; // 设置所属的声卡设备 dev->type = type; // 设置类型 dev->state = SNDRV_DEV_BUILD; // 设置状态 dev->device_data = device_data; // 设置额外数据 dev->ops = ops; // 设置操作集 /* insert the entry in an incrementally sorted list */ list_for_each_prev(p, &card->devices) { // 遍历声卡设备的逻辑设备链表devices,将dev插入到适当的位置,保证链表按照设备类型的顺序排列 struct snd_device *pdev = list_entry(p, struct snd_device, list); if ((unsigned int)pdev->type <= (unsigned int)type) break; } list_add(&dev->list, p); // 在p节点后插入dev_list节点 return 0; }
该函数接收四个参数:
- card:表示为声卡硬件设备所分配的的snd_card数据结构;
- type:表示该设备的类型,可以是 SNDRV_DEV_XXX 几种类型之一;
- device_data: 是一个指针,允许开发者传入设备使用的额外数据;
- ops:是一个 struct snd_device_ops结构体指针,其中包含了该设备使用的回调函数;
4.2 注册声卡逻辑设备
snd_register_device函数用于注册一个声卡设备,该函数接受六个参数:
- type:设备类型,比如control设备该参数设置为SNDRV_DEVICE_TYPE_CONTROL、pcm设备该参数设置为SNDRV_DEVICE_TYPE_PCM_PLAYBACK/SNDRV_DEVICE_TYPE_PCM_CAPTURE;
- card:声卡设备所属的snd_card结构体指针;
- dev:设备逻辑设备的编号,大多数的情况为0;
- f_ops:文件操作集指针,比如control设备该参数设置为snd_ctl_f_ops;
- private_data:用户提供给f_ops->open()函数的私有数据指针;比如pcm设备该参数设置为snd_pcm实例;
- device:声卡逻辑设备对应的struct device结构体指针;比如pcm设备该参数设置为&pcm->streams[i].dev;
函数定义在sound/core/sound.c文件中:
/** * snd_register_device - Register the ALSA device file for the card * @type: the device type, SNDRV_DEVICE_TYPE_XXX * @card: the card instance * @dev: the device index * @f_ops: the file operations * @private_data: user pointer for f_ops->open() * @device: the device to register * * Registers an ALSA device file for the given card. * The operators have to be set in reg parameter. * * Return: Zero if successful, or a negative error code on failure. */ int snd_register_device(int type, struct snd_card *card, int dev, const struct file_operations *f_ops, void *private_data, struct device *device) { int minor; int err = 0; struct snd_minor *preg; if (snd_BUG_ON(!device)) return -EINVAL; preg = kmalloc(sizeof *preg, GFP_KERNEL); // 动态分配struct snd_minor数据结构 if (preg == NULL) return -ENOMEM; preg->type = type; // 设置类型 preg->card = card ? card->number : -1; // 声卡设备的编号 preg->device = dev; // 设置逻辑设备的编号 preg->f_ops = f_ops; // 设置文件操作集 preg->private_data = private_data; // 设置私有数据 preg->card_ptr = card; // 设置声卡设备指针 mutex_lock(&sound_mutex); // 获取互斥锁 minor = snd_find_free_minor(type, card, dev); // 获取一个可用的次设备号 if (minor < 0) { err = minor; goto error; } preg->dev = device; // 设置device device->devt = MKDEV(major, minor); // 设置设备号 主设备号是静态变量,值为116 err = device_add(device); // 将声卡逻辑设备的device注册到linux内核系统的设备驱动程序模型, // 以control设备为例,会在/sys/class/sound类文件下创建controlCxx链接文件,同时会在文件系统创建设备节点/dev/snd/controlCxx; // 以pcm设备为例,会在/sys/class/sound类文件下创建pcmCxxDxxp或pcmCxxDxxc链接文件,同时会在文件系统创建设备节点/dev/snd/pcmCxxDxxp或pcmCxxDxxc; if (err < 0) goto error; snd_minors[minor] = preg; // 保存到全局变量中 error: mutex_unlock(&sound_mutex); // 释放互斥锁 if (err < 0) kfree(preg); return err; }
函数大致流程如下:
- 函数首先通过kmalloc函数申请了一块内存来存放snd_minor结构体,然后将所有相关信息填充到该结构体中,包括设备类型、所属声卡设备的编号、设备索引、文件操作集、私有数据指针、以及设备对应的 struct device 结构体指针等;
- 接着,函数调用snd_find_free_minor函数获取一个可用的次设备号并分配给该设备;
- 调用device_add将声卡逻辑设备的device注册到linux内核系统的设备驱动程序模型,以control设备为例,会在/sys/class/sound类文件下创建controlC%d链接文件,同时会在文件系统创建设备节点/dev/snd/controlC%d;
- 然后,函数将snd_minor信息存储到全局变量snd_minors的对应索引处;
4.2.1 字符设备驱动
这里我们需要说一下音频字符设备驱动的注册,其在alsa_sound_init函数中完成的,函数位于sound/core/sound.c:
/* * INIT PART */ static int __init alsa_sound_init(void) { snd_major = major; snd_ecards_limit = cards_limit; if (register_chrdev(major, "alsa", &snd_fops)) { pr_err("ALSA core: unable to register native major device number %d\n", major); return -EIO; } if (snd_info_init() < 0) { unregister_chrdev(major, "alsa"); return -ENOMEM; } #ifdef CONFIG_SND_DEBUG sound_debugfs_root = debugfs_create_dir("sound", NULL); #endif #ifndef MODULE pr_info("Advanced Linux Sound Architecture Driver Initialized.\n"); #endif return 0; }
函数内部调用register_chrdev完成字符设备驱动程序的注册,主设备号设置为major,即116,所有次设备共用一个操作集为snd_fops。
4.2.2 snd_fops
static const struct file_operations snd_fops = { .owner = THIS_MODULE, .open = snd_open, .llseek = noop_llseek, };
所有次设备共用一个open接口,这里以snd_open函数为例进行分析:
static int snd_open(struct inode *inode, struct file *file) { unsigned int minor = iminor(inode); // 获取次设备号 struct snd_minor *mptr = NULL; const struct file_operations *new_fops; int err = 0; if (minor >= ARRAY_SIZE(snd_minors)) return -ENODEV; mutex_lock(&sound_mutex); mptr = snd_minors[minor]; // 获取到具体的声卡逻辑设备上下文,即比如control、pcm设备等 if (mptr == NULL) { mptr = autoload_device(minor); if (!mptr) { mutex_unlock(&sound_mutex); return -ENODEV; } } new_fops = fops_get(mptr->f_ops); // 获取mptr的f_ops文件结构体 mutex_unlock(&sound_mutex); if (!new_fops) return -ENODEV; replace_fops(file, new_fops); // 用new_fops替换file->f_op if (file->f_op->open) err = file->f_op->open(inode, file); // 执行该次设备的文件open函数 return err; }
如上述注释所述,在snd_open函数中利用次设备号根据全局数组snd_minors找到相应的snd_minor数据结构,然后从snd_minor结构中取出文件操作集f_ops,并且把file->f_op替换为snd_minor的f_ops,紧接着直接调用file的f_ops->open,然后返回。因为file->f_op已经被替换,以后应用程序的所有read/write/ioctl调用都会进入声卡逻辑设备自己的回调函数中。
五、ALSA driver示例
有关ALSA driver的示例可以参考博客:Linux ALSA 之三:简单的 ALSA Driver 实现。
参考文章
[3] Linux音频子系统
[4] http://www.alsa-project.org/。
[6] Linux内核中的链表——struct list_head