程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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);
View Code
复制代码

可以看到在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

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
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-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
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-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(1495)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示