在上一篇博客中我们已经介绍了RK3399 I2S控制器内容,同时也介绍了有关声卡芯片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 ALSA框架
ALSA框架从上到下依次为应用程序、ALSA Library API、ALSA CORE、ASoC Driver;
- 应用程序:在应用层。ALSA为我们提供了alsa用户库,应用程序只要调用ALSA Library API,即可完成对底层音频设备的控制;
- ALSA Library API:alsa用户库接口,对应用程序提供统一的API接口,这样可以隐藏了驱动层的实现细节,简化了应用程序的实现难度;常见有tinyalsa、alsa-lib;
- ASoC CORE:ASoC是建立在标准ALSA Driver的基础上,为了更好支持嵌入式系统和应用于移动设备的音频codec的一套软件体系;
- ALSA Driver:音频硬件设备驱动主要由三大部分组成,分别是Machine、Platform、Codec;
1.2.1 ASCoC的由来
在AScoC出现之前,内核对于SoC的音频已经有部分的支持,不过有一些局限性;
- Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码的重复,例如,仅是wm8731的驱动,当时linux中有分别针对4个平台的驱动代码;
- 音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中非常普通的,而且通常都需要特定于机器的代码进行重新对音频路径进行配置;
- 当进行播放或录音时,驱动会让整个Codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持通过改变过取样频率和偏置电流来达到省电的目的;
ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个它必须和标准的ALSA驱动框架相结合才能工作。
1.2.2 音频硬件设备驱动
音频硬件设备驱动主要由三大部分组成,分别是Machine、Platform、Codec;三者的关系如下图所示:
1.2.3 Machine
Machine是指某一款机器,可以是某款设备、某款开发板、又或者是某款智能手机,由此可以看出Machine几乎是不可重用的。
每个Machine上的硬件实现可能不一样,SoC不一样、Codec不一样、音频的输入,输出设备也不一样,Machine为不、Codec、输入输出设备提供一个载体。
Machine Driver描述了如何控制CPU数字音频接口(DAI)和Codec,使得互相配合在一起工作;当播放声音时,通过CPU的DAI将音频数据发送给Codec进行处理,最终由Codec输出驱动耳机。
CPU DAI:在嵌入式系统里面一般指SoC的I2S,PCM总线控制器,负责把音频数据从I2S TX FIFO搬运到Codec(playback、capture则相反)。cpu_dai经过 snd_soc_register_dai()来注册。注:DAI是Digital Audio Interface的简称,分为cpu_dai和codec_dai,这二者经过I2S/PCM总线链接,嵌入式系统中通常是I2S和PCM接口。
1.2.4 Platform
Platform一般是指某一个SoC平台,比如RK3399、S3C2440等等,与音频相关的通常包括SoC中的时钟、DMA、I2S、PCN、I2C等等,只要指定了SoC,那么我们可以认为他会有一个对应的Platform,它只与SoC相关,这样我们就可以把Platform抽象出来,使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实际上,把Platforrm当做SoC更容易理解。
Platfrom Driver提供了配置/使能SoC音频接口的能力;Plaftrom驱动分为两个部分:snd_soc_platform_driver、snd_soc_dai_driver。
- snd_soc_platform_driver:负责管理音频数据,把音频数据通过DMA或其他操作传送至CPU DAI中;
- snd_soc_dai_driver:负责完成SoC一侧的DAI参数配置,同时也会通过一定的路径把必要的DMA等参数与snd_soc_platform_driver进行交互;
1.2.5 Codec
Codec字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、A/D、Mixer、PA、line-in、line-out等,通常包含多种输入(microphone input、line input、I2S等)和多个输出(line output、耳机等),Codec和Platform一样,是一个可以被多个Machine使用,嵌入式Ccodec通常通过I2C/SPI对内部的寄存器进行控制。实际上、把Codec当做声卡芯片更容易理解;
Codec Driver提供了配置/使能Codec的能力。
1.3 目录结构
linux内核将ALSA驱动相关的代码放在sound/alsa目录下,这下面的文件还是比较多的,我们大概了解一下即可。
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
其中
-
soc目录中存放是与Machine驱动相关的代码;
-
i2s目录中存放是与I2S接口相关的代码;
- i2c目录中存放的是与I2C接口相关的代码;
二、核心数据结构
学习ALSA驱动,首先要了解驱动框架涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
3.1 struct snd_card
linux内核中使用struct snd_card表示ALSA音频驱动中的声卡设备,snd_card可以说是整个ALSA音频驱动最顶层的一个数据结构。整个声卡的软件逻辑结构开始于该结构,几乎所有与声音相关的逻辑设备都是在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,通过编号可以找到对应的声卡设备;
- id:声卡设备的标识符;
- driver:驱动名称;
- shortname:设备简称;
- longname:设备名称;
- irq_descr:中断描述信息;
- mixername:混音器名称;
- components:声卡组件名称,由空格分隔;
- module:顶层模块;
- private_data:声卡的私有数据;
- private_free:释放私有数据的回调函数;
- devices:设备链表,保存该声卡下所有逻辑设备的链表;
- ctl_dev:控制设备;
- last_numid:最后使用的数字 ID;
- controls_rwsem:读写信号量;
- ctl_files_rwlock:读写自旋锁;
- controls_count:所有控制项数量;
- user_ctl_count:用户控制项数量;
- controls:所有控制项;
- ctl_files:活动控制文件;
- proc_root:与此声卡特定文件相关的根目录;
- proc_root_link:链接到实际 ID 的编号链接;
- files_list:与此声卡相关的所有文件;
- s_f_ops:关机状态下的文件操作;
- files_lock:自旋锁;
- shutdown:此声卡正在关闭;
- release_completion:释放完成;
- dev:分配给此声卡的设备;
- card_dev:sysfs中的cardX对象;
- dev_groups:分配的sysfs属性组;
- registered:声卡设备card_dev是否已注册;
- remove_sleep:等待队列头;
- power_state:电源状态;
- power_sleep:电源等待队列头;
3.1.1 struct snd_device
linux内核使用struct snd_device数据几个欧表示ALSA音频驱动中的音频设备、音频子设备或其他一些相关的设备;
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 */ };
三、ALSA驱动核心API
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文件;
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
3.1.4 snd_ctl_create
3.1.5 snd_info_card_create
3.2 注册/卸载声卡设备
3.2.1 snd_card_register
snd_card声卡设备注册是通过snd_card_register函数来完成的:
参考文章
[3] Linux音频子系统