ALSA & ASOC

1       ALSA

1.1    架构简述

ALSA是Advanced Linux Sound Architecture 的缩写, 官网 : http://www.alsa-project.org.

在内核设备驱动层, ALSA提供了alsa-driver. 同时在应用层, ALSA为我们提供了alsa-lib, 应用程序只要调用alsa-lib提供的API, 即可以完成对底层音频硬件的控制.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.001.pnguploading.4e448015.gif转存失败重新上传取消

从数据结构的组织层面来看, ALSA用Card代表一个池子, Card下面可以有一个或多个逻辑设备Device; 每个Device下面可以有其它细分设备(例如PCM Device), 也可以不再继续细分(例如Control Device).

从硬件的角度来说, 简单来说, 可以认为一个Card对应一块板子, 一个Device则对应板子上的一路通道.

例如假设一个CPU有3个I2S, 然后外接了一块WM8920音频芯片, 该音频芯片有三路接口AIF1、AIF2、AIF3. 那这样一块电路板就有一个Card, 下面三个Device (I2S1 – AIF1, I2S2 – AIF2, I2S3 – AIF3).

当然, 一块板子上也可存在多个Card, 这种情况下一般是把一类接口抽象为一个Card. 例如所有的I2S接口抽象为一路Card, 所有的PCM接口抽象为一路Card.

一个Device也可能只是单纯的软件层面的抽象, 并没有与具体的硬件对应. 因此我们通常称Device为逻辑设备.

1.2    设备节点介绍

/dev/snd

crw-rw—-+ 1 root audio 116, 8 2011-02-23 21:38 controlC0

crw-rw—-+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0

crw-rw—-+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c

crw-rw—-+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p

crw-rw—-+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p

crw-rw—-+ 1 root audio 116, 3 2011-02-23 21:38 seq

crw-rw—-+ 1 root audio 116, 2 2011-02-23 21:38 timer

          controlC0 : 用于声卡的控制,例如通道选择,混音,麦克风的控制等

          midiC0D0 : 用于播放midi音频

          pcmC0D0c : 用于录音的pcm设备

          pcmC0D0p : 用于播放的pcm设备

          seq : 音序器

          timer : 定时器

其中, C0D0代表的是声卡0中的设备0, pcmC0D0c最后一个c代表capture, pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则.

/proc/asound

dr-xr-xr-x 6 root root 0 Oct  8 00:04 card0

-r–r–r– 1 root root 0 Oct  8 00:04 cards

-r–r–r– 1 root root 0 Oct  8 00:04 devices

-r–r–r– 1 root root 0 Oct  8 00:04 modules

dr-xr-xr-x 2 root root 0 Oct  8 00:04 oss

-r–r–r– 1 root root 0 Oct  8 00:04 pcm

dr-xr-xr-x 2 root root 0 Oct  8 00:04 seq

-r–r–r– 1 root root 0 Oct  8 00:04 timers

-r–r–r– 1 root root 0 Oct  8 00:04 version

          cards : 可显示系统中存在多少个声卡

          card0 : 代表某个声卡

          devices : 可显示系统中存在多少个逻辑设备

/proc/asound/card0

该节点提供该card的一些info.

-r–r–r– 1 root root 0 Oct  7 19:57 audiopci

dr-xr-xr-x 2 root root 0 Oct  7 19:57 codec97#0

-r–r–r– 1 root root 0 Oct  7 19:57 id

-r–r–r– 1 root root 0 Oct  7 19:57 midi0

dr-xr-xr-x 3 root root 0 Oct  7 19:57 pcm0c

dr-xr-xr-x 3 root root 0 Oct  7 19:57 pcm0p

dr-xr-xr-x 3 root root 0 Oct  7 19:57 pcm1p

/sys/class/sound/

lrwxrwxrwx 1 root root 0 Oct  7 20:27 card0 -> ../../devices/pci0000:00/0000:00:11.0/0000:02:02.0/sound/card0

……

1.3    card的创建与注册

1.3.1      card是什么

struct snd_card可以说是整个ALSA音频驱动最顶层的一个结构, 整个声卡的软件逻辑结构开始于该结构, 几乎所有与声音相关的逻辑设备都是在snd_card的管理之下, 声卡驱动的第一个动作通常就是创建一个snd_card结构体.

1.3.2      数据结构

struct snd_card

一个struct snd_card用于描述一个声卡.

头文件 : include/sound/core.h

struct snd_card

Comment

int number

声卡编号, 内核支持创建多个声卡

struct list_head devices

记录该声卡下所有逻辑设备的链表

struct list_head controls

记录该声卡下所有的控制单元的链表

struct snd_info_entry *proc_root

一个声卡可以有多个snd_info_entry, 每个entry代表/proc/asound/cardx/下面的一个节点.

这些entry以树形结构组织, proc_root是这个数的树根.

void *private_data;

声卡的私有数据,可以在创建声卡时通过参数指定数据的大小

1.3.3      API分析

snd_card_new

snd_card_new在sound/core/init.c中定义.

代码中有对参数和返回值的解释:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.003.pnguploading.4e448015.gif转存失败重新上传取消

整个代码所做的事情就是分配一个snd_card结构体, 并初始化该结构体的相关字段:

         首先调用kzalloc分配一块存储空间. 如果extra_size > 0, 说明需要创建一块额外的空间用做card->private_data.

         拷贝声卡的ID字符串.

         如果传入的声卡编号为-1,自动分配一个索引编号.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.006.pnguploading.4e448015.gif转存失败重新上传取消

         初始化snd_card结构中必要的字段.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.007.pnguploading.4e448015.gif转存失败重新上传取消

注意这里有一句card->card_dev.class = sound_class, 这意味着当把card_dev添加到sysfs框架时, 会在/sys/class/sound/下创建指向该device的符号链接.

sound_class的创建代码中还将sound_class->devnode初始化为了“snd/”, 这意味着相应的字符设备节点会存在于/dev/snd/目录下.

         建立逻辑设备: Control.

         建立proc文件中的info节点: 通常就是/proc/asound/card0. 注意, 这里只是创建了card0这个目录, 目录下的文件不是这里创建的, 文件的创建请参考《info节点》.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.009.pnguploading.4e448015.gif转存失败重新上传取消

         最后, 把准备好的结构体返回给调用者.

snd_card_register

snd_card_register在sound/core/init.c中定义.

代码中有对参数和返回值的解释:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.011.pnguploading.4e448015.gif转存失败重新上传取消

注意看代码注释, 该函数的主要目的是注册card下面挂载的所有device; 另外只有该函数成功返回后, 用户空间才能通过control  interface来访问底层.

         首先调用device_add, 将自己添加到sysfs框架:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.012.pnguploading.4e448015.gif转存失败重新上传取消

         调用snd_device_register_all()注册所有挂在该声卡下的逻辑设备.

snd_device_register_all()实际上是通过snd_card的devices链表, 遍历所有的snd_device, 并且调用snd_device的ops->dev_register()来实现各自设备的注册的.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.013.pnguploading.4e448015.gif转存失败重新上传取消

         在/proc/asound/card0下添加一些字段, 把相关的信息暴露给用户空间. 代码就不贴了.

snd_card_disconnect

snd_card_disconnect在sound/core/init.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.014.pnguploading.4e448015.gif转存失败重新上传取消

1.4    device的创建与注册

1.4.1      逻辑设备(device)是什么

逻辑设备隶属于card, 通常用来实现某一功能, 一个逻辑设备一般来说会对应用户空间的一个或多个设备节点(也有某些仅在内核使用的逻辑设备不会创建设备节点).

1.4.2      数据结构

struct snd_device

一个struct snd_device用于描述一个逻辑设备.

头文件 : include/sound/core.h

struct snd_device

Comment

struct list_head list

用于把自己挂载到snd_card->devices链表下.

struct snd_card *card

card which holds this device

enum snd_device_state state

设备的状态. 有BUILD、REGISTERED、DISCONNECTED三种.

enum snd_device_type type

设备的类型. 系统中定义了多种设备类型, 例如SNDRV_DEV_PCM、SNDRV_DEV_CONTROL等.

void *device_data

设备的私有数据.

struct snd_device_ops *ops

ops, 下文详细介绍.

struct snd_device_ops

每个逻辑设备都有一个对应的snd_device_ops, 我们在新增一个逻辑设备时, 需要为此设备准备好该数据结构.

头文件 : include/sound/core.h

struct snd_device_ops

Comment

int (*dev_free)(struct snd_device *dev)

当有人调用API snd_device_free时, 核心层代码会回调此处的dev_free

int (*dev_register)(struct snd_device *dev)

当有人调用API snd_device_register时, 核心层代码会回调此处的dev_register

int (*dev_disconnect)(struct snd_device *dev)

当有人调用API snd_device_disconnect时, 核心层代码会回调此处的dev_disconnect

1.4.3      API分析

snd_device_new

snd_device_new在source/sound/core/device.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.015.pnguploading.4e448015.gif转存失败重新上传取消

该API用于创建一个逻辑设备, 主要内容是分配一个struct snd_device空间, 初始化相关字段, 并把该逻辑设备添加到card->devices链表下.

注意在添加新设备到card->devices链表时, 采用的是有序插入方式: type小的在前、type大的在后. 细节可阅读代码.

snd_device_register

snd_device_register在source/sound/core/device.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.016.pnguploading.4e448015.gif转存失败重新上传取消

注释里面说的比较清楚了. 一般在注册声卡时(snd_card_register)会自动调用此处的API; 不过也可以在card注册完毕后, 在手动调用此API注册一个新的逻辑设备.

该API的实现很简单: 首先回调snd_device_ops->dev_register, 然后把核心层的状态标记为SNDRV_DEV_REGISTERED.

snd_device_register_all

snd_device_register_all在source/sound/core/device.c中定义.

相当于对snd_device_register的封装: 针对card下的每一个逻辑设备, 调用__snd_device_register注册此逻辑设备.

snd_device_disconnect

snd_device_disconnect在source/sound/core/device.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.018.pnguploading.4e448015.gif转存失败重新上传取消

一般在调用snd_card_disconnect时会调用此API.

实现也很简单, 回调snd_device_ops->dev_disconnect, 然后把核心层的状态标记为SNDRV_DEV_DISCONNECTED.

snd_device_disconnect_all

snd_device_disconnect_all在source/sound/core/device.c中定义.

针对每个逻辑设备, 调用snd_device_disconnect.

snd_device_free

snd_device_free在source/sound/core/device.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.019.pnguploading.4e448015.gif转存失败重新上传取消

实现比较简单, 首先把自己从card->devices链表中移除, 然后调用__snd_device_disconnect, 然后回调snd_device_ops->dev_free, 最后调用kfree释放snd_device结构.

snd_device_free_all

snd_device_free_all在source/sound/core/device.c中定义.

针对每个逻辑设备, 调用snd_device_free.

1.4.4      字符设备驱动的创建

逻辑设备与用户空间是通过字符设备驱动交互的.

一个逻辑设备可以创建一个或多个字符设备节点, 节点创建的时机如下:

当声卡注册时(snd_card_register), 会针对其下的每一个逻辑设备调用snd_device_register. 后者会回调逻辑设备的回调函数(snd_device_ops->dev_register).

逻辑设备在实现dev_register时, 一般会调用snd_register_device, 后者会通过device_add创建一个设备节点. 所以调用几次snd_register_device, 就会存在几个设备节点.

ALSA子系统里面对字符设备的架设类似于misc子系统.

所有ALSA字符设备的主设备号都是CONFIG_SND_MAJOR (116), ALSA系统在初始化时, 会在alsa_sound_init中调用register_chrdev, 定义此类字符设备的统一处理函数snd_fops.

snd_fops中只实现了snd_open函数, 当用户空间打开任一ALSA设备节点时, 都会进入到该open函数中

snd_open中, 会根据次设备号(每个逻辑设备都有自己的次设备号), 选择对应的逻辑设备的ops函数, 然后替换file->f_op, 此后用户空间的任何操作都是直接与逻辑设备的ops函数交互的.

ALSA系统中有一个全局数组snd_minors[], 数组的下标就是次设备号, 每个元素对应一个逻辑设备, snd_minor[i]-> f_ops存储的就是逻辑设备自己的ops函数.

有了它, snd_open的实现就比较容易了, 只需用次设备号作为下标, 从snd_minors[]中找到对应的元素, 然后用元素的f_ops替换file->f_op即可.

那snd_minors[]这个数组是何时被填充的呢? 答案是在snd_register_device里面. 下面我们看一下该函数的细节.

snd_register_device

snd_register_device的主要作用是填充snd_minors[]数组, 并创建一个字符设备节点.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.020.pnguploading.4e448015.gif转存失败重新上传取消

         参数 f_ops : 逻辑设备自己实现的ops函数集. 用户空间最终是与这里的ops交互.

         287行 : 创建字符设备节点, 节点名由参数device

1.5    逻辑设备中间层

在当前的系统中, 有很多逻辑设备中间层, 它们相当于对《device的创建与注册》中的步骤进行了封装, 然后对外提供了更加简洁的API来创建对应的逻辑设备.

这些中间层包括:

中间层

API

Control设备

snd_ctl_create : 调用snd_device_new在Card下创建一个SNDRV_DEV_CONTROL类型的逻辑设备.

PCM设备

snd_pcm_new : 类上

RAWMIDI设备

snd_rawmidi_new : 类上

TIMER设备

snd_timer_new

JACK设备

snd_jack_new

Info设备

snd_card_proc_new -> snd_info_create_card_entry , 后者最终会在card->proc_root这颗树下创建一个叶子节点.

除了Info设备外, 其它几个中间层都是对snd_device_new进行了一次封装. 当调用这些中间层提供的API时, 最终会在card下添加一个新的逻辑设备.

在card的注册阶段, 会扫描它下面的每一个逻辑设备, 然后调用snd_device_register来注册该逻辑设备. 当逻辑设备注册完成后, 核心层会回调snd_device_ops.dev_register函数, 如果dev_register里面调用了snd_register_device, 则会在用户空间出现字符设备节点.

下面我们着重分析下Control, PCM, info这几种常见的类型.

1.5.1      info逻辑设备

info设备与其它逻辑设备不一样, 它更应该称作info节点. 它不会创建字符设备节点, 而是会在/proc/下创建一些文件, 以便用户空间通过这些文件获取一些信息.

一个info节点用一个snd_info_entry表示, 一个entry对应/proc/asound/cardx/下的一个文件或目录.

我们可以用snd_card_proc_new -> snd_info_create_card_entry创建这些entry, 所有的entry都会以树形结构挂载在card->proc_root下, 注意这两个API只是创建了一个entry, 但用户空间还看不到这个entry, 需调用snd_info_register, 然后用户空间才会看见.

在card注册阶段, 会遍历card->proc_root下的每一个叶子节点, 然后调用snd_info_register把叶子节点展现在/proc/asound/cardx/下. 其调用流程是:

snd_card_register -> init_info_for_card -> snd_info_card_register -> snd_info_register_recursive -> snd_info_register.

1.5.2      Control逻辑设备

在ALSA系统中, 我们可以通过amixer工具配置音频的各个参数. amixer操作的就是这里的Control设备.

amixer的常见用法如:

         amixer contents : 列出所有可用的control

         amixer cget numid=5,iface=MIXER,name=’PCM Volume’ : 获取某个control的值

         amixer cset numid=5,iface=MIXER,name=’PCM Volume’ 25 : 设置某个control的值

注意这里的numid, iface, name这几个字段, 后文会详细分析.

Control设备是作为字符设备呈现给用户空间的, 其设备节点是/dev/snd/controlCx, 本节会简述这个设备节点的创建过程.

Kernel构建了一个Control设备中间层, 对应的代码是sound/core/control.c, 这个中间层对上向ALSA核心层注册, 将自己注册为一个逻辑设备, 并最终创建了字符设备节点, 负责与用户空间交互(snd_ctl_f_ops); 对下则提供API给各个具体的codec驱动, 供其添加具体的control; 对内则在中间层内部创建一些数据结构来管理所有的controls.

接下来我们先看看Control中间层内部的这些数据结构和它提供给下层的API, 然后看看一个具体的驱动如何利用这些API添加一个control, 最后在看下Control字符设备的创建过程.

1.5.2.1              数据结构

在介绍control相关的数据结构之前, 我们先回想一下熟悉的”sysfs Attribute”属性文件, 因为它俩有很强的相似之处.

当驱动想创建一个属性文件时, 会准备好name、权限、读写函数, 然后向‘属性文件中间层’注册. 然后我们就可以看到/sysfs/xxx/name这个属性文件, 并可以o/r/w/c这个文件.

control也类似, 当具体的codec驱动想添加一个control时, 它也要按照‘Control中间层’的要求准备好name、权限、查读写函数、等等, 然后向中间层注册. 注册完毕后, 我们就可以用amixer contents看到这个control, 并可以用amixer cget/cset修改它.

在这个过程中, 中间层要承担以下几方面的作用:

a)         总管作用 : 会有多个地方向中间层注册, 因为中间层内部要把所有注册的元素用链表或其它方式管理起来.

b)        定位作用 : 当用户空间访问某个元素时, 中间层要能从链表中定位到对应的元素. 因此中间层会给每个元素一个token, 以便区分不同的元素. 用户空间要访问某个元素时, 需指定此元素的token. 对于属性文件来说, token就是文件的绝对路径, 但对于control来说, 这个token更加复杂一点.

c)         桥梁作用 : 把用户空间对某元素的读写操作, 转换成该元素自定义的读写操作. 读写操作意味着元素要有一个存储空间value, 读写就是读这个value的值或修改它的值. 在这个过程中, 中间层并不会去触碰value, 因此一般来说, 中间层都不会为元素去创建value存储空间, 这个存储空间是在注册者中创建并维护的. 中间层只会创建代表这个元素的数据结构, 这个数据结构会做为参数回传给注册者的读写函数, 注册者通过container_of, 就可以找到该元素对应的存储空间, 然后修改该存储空间的内容.

d)        关于元素的存储空间, 在属性文件中, 比较简单, 一般是int或string型; 但在ALSA中要复杂一些, 因此‘Control中间层’定义了一些数据结构来描述这些存储空间, 后文详述.

理解了中间层的这几点作用, 再来看Control中间层会简单很多. 在‘Control中间层’内部, 用snd_kcontrol代表一个control元素; 用snd_ctl_elem_id作为control的token, 以便区分不同的control; 用snd_ctl_elem_info/snd_ctl_elem_value来描述value存储空间.

struct snd_kcontrol

struct snd_kcontrol : 代表一个control元素

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.021.pnguploading.4e448015.gif转存失败重新上传取消

         list : 所有的control元素都会挂载到card->controls表头下.

         id : 该元素的token

         count : 一个元素可以只有一个value, 也可以有多个value[], 这里的count代表个数. 在control系统中, value的专业术语是element, 一个value对应一个element. 因此代码后面的注释写的是/*count of same elements*/.

         info/get/put : 查/读/写函数.

         tlv : 暂不知其细节.

         private_value : 一个long型的数据, 可以通过info, get, put这几个回调函数访问. 底层codec驱动自定义其作用(例如可以把它拆分成多个位域, 又或者是一个指针, 指向某一个数据结构), ‘Control中间层’不会触碰它.

一般来说底层codec驱动会在该字段存储自身寄存器的地址, 这样当用户空间操作该control时, 底层就可在其put函数中修改寄存器的内容, 从而达到控制硬件的目的.

         private_date / private_free : 底层的私有数据, Control中间层’不会触碰它. 也许是内核开发者发现把unsigned long private_value当做指针用不好, 所以专门增加了这样一个void *的指针.

         vd[0] : 零长度数组, 它的最终大小与count有关. 因为可能有多个elements, 每个element的读写权限可能不一样, 因此每个vd[i]元素描述一个element的权限.

注意, snd_kcontrol只描述了control元素的相关特性, 并没有包含数据的存储空间.

struct snd_ctl_elem_id

struct snd_ctl_elem_id : token, 用于区分每一个kcontrol.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.022.pnguploading.4e448015.gif转存失败重新上传取消

Control系统中token规则有点复杂:

         numid : 在一个card下, 它所以的kcontrols都有一个不同的numid. 通过amixer controls可以看到所有的kcontrols以及每个kcontrol对应的numid.

因为numid在一个card下是唯一的, 因此如果amixer cget/cset时指定了numid, 那内核层就可以直接通过它找到对应的snd_kcontrol.

numid的主要作用是供用户空间区分不同的elem, 它的取值在一个card内从0开始递增.

         如果用户空间没有指定numid, 那内核层要去判断iface+device+subdevice+name+index这几个字段, 只有当它们全部匹配时才能唯一确定一个snd_kcontrol.

内核层负责通过snd_ctl_elem_id找到对应的snd_kcontrol的函数是snd_ctl_find_id, 可以看下代码, 更容易理解这里的逻辑.

         index : 因为一个kcontrol下面可能有多个elem, index用于在kcontrol内部标示不同的elem. 它的取值在一个kcontrol范围内从0开始递增.

snd_ctl_elem_id相当于一个纽带, 使得所有包含该token的结构体都可以通过它间接产生关联.

struct snd_ctl_elem_info

struct snd_ctl_elem_info : 用于描述一个elem的info信息, 当用户空间查询某个elem的info信息时(例如amixer contents), 底层驱动需要填充该结构体的内容并反馈给用户空间.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.023.pnguploading.4e448015.gif转存失败重新上传取消

         id : 当用户空间想获取某个elem的具体信息时, 用户空间会准备好snd_ctl_elem_info这个数据结构, 并把elem的token id填入该结构, 然后把该结构传递到内核. ‘Control中间层’会通过id找到对应的snd_kcontrol, 然后回调其info函数. 底层驱动在info函数中填充该结构体的内容, 然后返回给用户空间.

那用户空间是怎么知道某个elem的token id的呢? ‘Control中间层’实现了一个list方法snd_ctl_elem_list, 当用户空间调用该方法时, ‘Control中间层’会把所有elem的token id以列表的形式反馈给用户空间.

         type : 不同的elem可能有不同的类型, 已定义的类型有bool/int/enumerate/int64/byte. 具体参考snd_ctl_elem_type_t

         access : 该elem的访问权限, 可选值参考SNDRV_CTL_ELEM_ACCESS_*.

         count : 一个snd_kcontrol下面可能有多个elem, 一个elem下也可能有多个值. 在snd_ctl_elem_value中可以更加清楚该count的意义

         value : 这是一个大的联合体, 它用于向用户空间展示该elem可取值的范围. 具体是联合体中的哪一个元素有效与type有关: 假如type是bool/int型, 则用户空间会读取value.integer来获取该elem可取的最小值/最大值/步长; 假如type是int64型, 则用户空间会读取value.integer64; 假如type是enumerate, 则用户空间会读取value.enumerated来获取该elem可取哪些值. 特例是byte类型, 这种情况下数据会当做bin来处理, 因此这里无法反馈bin的细节内容.

struct snd_ctl_elem_value

struct snd_ctl_elem_value : 描述一个存储空间. 前文说过snd_kcontrol只包含读写数据的方法, 并不包含存储数据的空间, 数据存储空间是用该结构体定义的.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.024.pnguploading.4e448015.gif转存失败重新上传取消

         id : token, 当用户空间想读写某个elem的值时, 用户空间会准备好snd_ctl_elem_value这个数据结构, 并把elem的token id填入该结构, 然后把该结构传递到内核. ‘Control中间层’会通过id找到对应的snd_kcontrol, 然后回调其get/put函数. 底层驱动在get/put函数中填充该结构体的内容, 然后返回给用户空间.

         value : 这是一个大的联合体, 具体是联合体中的哪一个元素有效与snd_ctl_elem_info中指定的type信息有关 : 假如type是bool/int型, 则value.integer有效; 假如type是int64型, 则value.integer64有效; 假如type是enumerate, 则value.enumerated有效; 假如type是byte型, 则value.bytes有效.

另外, 注意每一个有效的存储空间都是一个数组类型, 例如value.integer.value[128]. 前文提到一个elem下可以有多个值, 具体个数取决于snd_ctl_elem_info.count. 这里的value.integer.value[128]意味着count不能超过128, 假如count为1, 则value.integer.value[0]代表有效数据, 假如count为3, 则value.integer.value[0, 1, 2]代表有效数据.

struct snd_kcontrol_new

struct snd_kcontrol_new : 当底层驱动想注册一个kcontrol时, 必须填充该结构体, 然后向‘Control 中间层’注册.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.025.pnguploading.4e448015.gif转存失败重新上传取消

         iface : snd_ctl_elem_iface_t类型的, 一般都是用的SNDRV_CTL_ELEM_IFACE_MIXER. 不同的类型在内核代码层面没有做什么特殊处理, 貌似这个类型定义没多大作用. 唯一一个已知作用是amixer controls命令时, “iface=”字段会显示不同的字符串.

         device / subdevice / name / index : 当底层注册一个kcontrol时, 核心层会创建一个对应的snd_kcontrol, 并创建一个对应的token snd_ctl_elem_id来唯一标示这个kcontrol. 这几个字段会赋值给snd_ctl_elem_id的相应字段.

关于name字段, 官方规定了命名规则 : 源–方向–功能. 但是代码层面貌似没对这个名字做什么强制检查, 貌似随便取都无所谓. 也许这个规则只是一个软性约束吧.

         access : 访问权限, 可选值参考SNDRV_CTL_ELEM_ACCESS_*.

         count : 同snd_kcontrol中count的意义.

         info / get / put : 底层需要实现这三个函数.

         tlv : 所谓tlv, 就是Type-Lenght-Value的意思, 是一种元数据. 此处的具体意义暂不知.

         private_value : 同snd_kcontrol中private_value的意义. 底层驱动通常在该字段中存储该kcontrol对应的codec寄存器地址.

当底层准备好该数据结构后, 就可以调用‘Control中间层’提供的API注册一个kcontrol了. 下面看看这些API.

1.5.2.2              API

‘Control中间层’提供给底层使用的API. 主要包括两个 : snd_ctl_new1 和 snd_ctl_add.

snd_ctl_new1

snd_ctl_new1在sound/core/control.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.026.pnguploading.4e448015.gif转存失败重新上传取消

当底层准备好snd_kcontrol_new数据结构后, 可以调用此API构建一个snd_kcontrol. 随后会调用snd_ctl_add把返回的这个snd_kcontrol注册到‘Control中间层’.

函数的实现逻辑比较简单, 首先对输入参数做一些必要的检查, 然后分配一个snd_kcontrol存储空间, 然后用输入参数初始化该存储空间的相应字段, 最后返回该snd_kcontrol.

snd_ctl_add

snd_ctl_add在source/sound/core/control.c中定义.

该函数的主体逻辑分为3段:

首先, 把这个kcontrol结构体挂载到card-> controls链表头下. 这样中间层就可以通过这个表头管理所有的kcontrols.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.028.pnguploading.4e448015.gif转存失败重新上传取消

其次, kcontrol->id这个token, 在snd_ctl_new1阶段已经初始化了大部分元素, 但还有一个未初始化, 它是kcontrol->id. numid. 这里完成对它的初始化:

这个numid在某个card下是唯一的, 通过numid, 我们可以快速定位到card下的某个snd_kcontrol. 代码参考snd_ctl_find_numid.

最后, 通知用户空间, 有count个elem注册进了内核, 并告知用户空间每一个elem的token.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.030.pnguploading.4e448015.gif转存失败重新上传取消

这个事件通知机制是靠字符设备驱动的read/poll函数完成的, 后文详述.

1.5.2.3              添加一个kcontrol

通过前面的介绍, 我们已经掌握了添加一个kcontrol的方法, 下面简单演示下:

Step1 : 定义snd_kcontrol_new结构体.

static struct snd_kcontrol_new my_control __devinitdata = {

    .iface = SNDRV_CTL_ELEM_IFACE_MIXER,

    .name = “PCM Playback Switch”,

    .index = 0,

    .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,

    .private_value = 0xffff,

    .info = my_control_info,

    .get = my_control_get,

    .put = my_control_put

};

Step2 : 实现info/get/put方法.

static int my_control_info(struct snd_kcontrol *kcontrol,

    struct snd_ctl_elem_info *uinfo)

{

    uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;

    uinfo->count = 1;

    uinfo->value.integer.min = 0;

    uinfo->value.integer.max = 1;

    return 0;

}

alsa已经为我们实现了一些通用的info回调函数, 例如:snd_ctl_boolean_mono_info(), snd_ctl_boolean_stereo_info()等等, 有需要可以直接使用它们.

static int my_control_get(struct snd_kcontrol *kcontrol,

    struct snd_ctl_elem_value *ucontrol)

{

    struct mychip *chip = snd_kcontrol_chip(kcontrol);

    ucontrol->value.integer.value[0] = get_some_value(chip);

    return 0;

}

如果snd_ctl_elem_info的count字段大于1, 表示elem有多个元素单元, get回调函数也应该为value[]填充多个元素.

static int my_control_put(struct snd_kcontrol *kcontrol,

    struct snd_ctl_elem_value *ucontrol)

{

    struct mychip *chip = snd_kcontrol_chip(kcontrol);

    int changed = 0;

    if (chip->current_value !=

        ucontrol->value.integer.value[0]) {

        change_current_value(chip,

        ucontrol->value.integer.value[0]);

        changed = 1;

    }

    return changed;

}

如上述例子所示, 当control的值被改变时, put回调必须要返回1, 如果值没有被改变, 则返回0. 如果发生了错误, 则返回一个负数的错误号.

和get回调一样, 当count大于1时, put回调也要处理value[]中的多个元素值.

Step3 : 注册kcontrol.

err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip));

if (err < 0)

    return err;

1.5.2.4              Control字符设备

每一个声卡下都有一个Control设备, 它是随着声卡的创建自动生成的, 只要我们在系统中新增了一个声卡, 那这个声卡下属的Control设备就被同时创建了.

我们从代码层面看下Control设备是如何被自动创建的:

         首先, snd_card_new -> snd_ctl_create -> snd_device_new, 也就是说每当new一个声卡时, 就会自动在此声卡下添加一个Control逻辑设备.

         然后, 在snd_card_register阶段, 会针对card下属的每一个逻辑设备调用snd_device_register. 这个注册动作最终会回调逻辑设备的snd_device_ops.dev_register. 对Control设备来说, 最终就是snd_ctl_dev_register被回调. snd_ctl_dev_register继而调用snd_register_device, 创建了Control字符设备节点.

用户空间对Control字符设备节点的操作最终会转给snd_ctl_f_ops:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.031.pnguploading.4e448015.gif转存失败重新上传取消

这个ops的功能可分为两大块 : read / poll 负责向用户空间报告snd_ctl_event事件; ioctl则负责响应用户空间对kcontrol的查/读/写操作.

snd_ctl_event事件

event事件包括element add/remove/change这几类, 详见SNDRV_CTL_EVENT_XXX.

event事件的意义是当内核空间新增一个kcontrol时, 内核需要告知用户空间有kcontrol.count个element被添加进了内核, 然后用户空间就可查/读/写这些elements. 同理, 当kcontrol被删除, 或者element自身的内容发生变化时, 都需要通知用户空间.

内核用struct snd_ctl_event描述一个event事件:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.032.pnguploading.4e448015.gif转存失败重新上传取消

         type : 代表事件类型.

         data.elem.id则是此elem的token.

用户空间获取event事件的方式是: 用户空间open设备节点后, 可以read或poll此节点. 在内核层的read/poll函数中, 会wait在change_sleep这个wait_queue_head上. 当有具体的事件发生时, 例如调用snd_ctl_add添加一个kcontrol时, 会调用snd_ctl_notify. 在notify函数中会调用wake_up(&ctl->change_sleep)唤醒等待队列, 从而使得read/poll从wait状态唤醒, 进而使得用户空间收到event事件.

ioctl

当用户空间通过amixer或其它方式查/读/写elem信息时, 其实是通过ioctl与内核交互的.

内核的snd_ctl_ioctl负责处理ioctl命令, 所有可用的命令列表参见SNDRV_CTL_IOCTL_XXX. 常用的几个命令如下:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.033.pnguploading.4e448015.gif转存失败重新上传取消

这些命令基本都是自明的, 例如:

         LIST是查询card下所有的elem;

         INFO是获取某个elem的具体信息, 最终会导致底层的info函数被调用;

         READ/WRITE是读写elem的value, 最终会导致底层的get/put函数被调用;

         ADD/REMOVE则是在用户空间新增/删除一个elem, 这也会导致内核空间创建一个snd_kcontrol去hold这个elem.

         REPLACE则代表用户空间修改一个已存在的elem的信息.

1.5.3      PCM逻辑设备

PCM逻辑设备与系统中其它逻辑设备类似:

         PCM逻辑设备也是以字符设备的形式呈现给用户空间;

         内核也实现了一个‘PCM中间层’, 该中间层对上向ALSA核心层注册, 将自己注册为一个逻辑设备, 并最终创建了字符设备节点, 负责与用户空间交互(snd_pcm_f_ops); 对下则提供API给底层驱动, 供底层驱动注册一个PCM设备.

从功能的角度来说, PCM逻辑设备的作用是供用户空间播放/录制PCM音频. PCM音频本质上来说就是一块Buffer, ‘PCM中间层’的作用就是从用户空间接收这块Buffer, 然后把Buffer的数据转给底层驱动播放. 底层驱动一般对应的是I2S控制器, 它收到数据后, 会通过I2S总线把数据传给codec, 然后codec经过DA转换后送到喇叭播放.

除了Buffer的传输, 我们还要解决配置的问题, 也就是配置I2S控制器和Codec芯片, 告诉它们应该按照什么样的clock、采样深度、通道数等来播放Buffer中的数据. 因此‘PCM中间层’也提供了一些ioctl给用户空间, 供它们设置这些参数.

从分工的角度来说, PCM中间层和底层驱动各自应该专注于哪些事情?

PCM中间层主要完成一些公共性的事情, 这些事情一般与具体的硬件无关(例如存储音频数据时, 肯定需要一个Buffer, 这个Buffer很显然用环形缓冲区描述比较好, 怎么管理这个环形缓冲区的读写指针? 另外, 用户空间设置的音频参数要检查其合法性. 还有, 用户空间可能启停音频播放/录制, 这意味着我们最好实现一个状态机, 来管理用户空间的切换操作. 等等). 对于与具体硬件相关的操作, PCM中间层会pass给底层驱动去处理(这就意味着PCM中间层要定义interface, 让底层驱动去实现这些interface).

底层驱动则处理与具体硬件相关的事情, 例如I2S控制器的配置, Codec芯片的配置, 时钟的初始化等等.

在ASOC架构引入之前, 底层驱动的实现都是放在一个文件里面, 没有对它再进行更进一步的架构设计, 这导致代码的复用性很差. 例如不同的I2S控制器外接了同一颗Codec芯片, 那就需要两份底层驱动, 在这两份驱动中, 都需要包含对Codec的控制逻辑, 这段逻辑是重复的. 后来内核引入了ASOC架构, 划分了Machine、Platform、Codec这几个模块, 增加了代码的复用性. 在《ASOC》一章中我们会专门讨论这个架构, 因此本小节不会过多的描述底层驱动的细节, 只是在必要的时候简单提一下.

本小节将专注于‘PCM中间层’的实现, 也就是上面提到的环形缓冲区管理、状态机管理、interface的定义等.

下面, 开启旅程吧!

1.5.3.1              基础知识

在开始深入代码之前, 我们先介绍一些必要的基础知识, 这些知识非常有助于我们理解后面的代码.

名词解释

这里介绍几个与音频采样相关的名词, 后续在理解音频Buffer的管理时, 这几个名词很重要.

         Sample : 样本长度. 代表一个声道的音频数据, 大小取决于采样深度, 常见的有8位和16位.

         Channel : 声道数. 常见的有单声道、双声道(立体声)、5.1声道、7.1声道等.

         Frame : 帧, 构成一个完整的声音单元. Frame = Channel * Sample, 例如对于单声道, 它的大小是1*Sample, 对于5.1声道, 它的大小是6*Sample.

这里帧的概念类似于LCD中的帧, 它是硬件传送的基本单位. 例如I2S每次传输一个完整的帧, 当一帧传输完毕后, 会产生一个硬件中断.

         Rate : 采样率, 指采样一帧数据所需的时间. 例如44.1KHz代表采样一帧数据需要1s/44100 = 0.022ms.

         Period : 周期. 当用户空间把音频数据写入RAM后, 底层是用DMA把数据从RAM搬移到I2S的FIFO. DMA每搬移完一段数据后就会产生一次中断,. 我们把DMA搬移这段数据的过程称为一个周期. 这段数据的大小是可以配置的, 因此用户空间可以设置Period的大小, 如果周期设定得较大, 则单次搬移的数据较多, 这意味着单位时间内硬件中断的次数较少, CPU 也就有更多时间处理其他任务, 功耗也更低, 但这样也带来一个显著的弊端——数据处理的时延会增大.

         period_size : 一个周期内的帧数. 它间接决定了周期的大小. 用户空间在设定周期大小时, 给定的参数也是这个帧数.

         period_bytes : 对于DMA硬件来说, 它只关心数据具体有多少字节. period_bytes = period_size * Channels * Sample / 8.

         Buffer : 代表一块RAM, 用户空间与内核通过这块RAM交换数据. DMA也是从这块RAM搬移数据. 一块Buffer内包含多个Period.

         buffer_size : 一块Buffer内帧数, 这块Buffer内包含多个period.

         buffer_bytes : Buffer以字节为单位的大小, 通常在分配RAM时需要该数据. buffer_bytes = buffer_size * Channels * Sample / 8.

PCM的基本架构

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.034.pnguploading.4e448015.gif转存失败重新上传取消

         一个pcm实例(例如pcm0)是card下的一个逻辑设备, 这个逻辑设备会在用户空间创建两个设备节点.

         一个pcm实例包含两个stream : playback & capture. 每个stream对应一个设备节点.

         每个stream下可包含多个substream.

在内核层, 每个substream都有一块自己的Buffer来与用户空间交换音频数据. 从这个角度来看, substream存在的意义貌似是为了分时复用底层的音频硬件.

在用户空间, 每个设备节点可以被open多次, 每次open内核层都会找到一个空闲的substream与之对应, 如果内核层的substream被用完了, 则此次open操作会失败. 这样看来, 用户空间的读、写、控制操作都是针对substream进行的, 这也进一步说明substream可以用来分时复用底层音频硬件.

PCM环形缓冲区管理

前文多次提到用户空间与内核空间通过一块Buffer交换音频数据, 这到底是怎样的一块Buffer?

一般来说, 这块Buffer是一段物理上连续的内存(更准确的说, 是一块可被DMA访问的内存), 这块内存由内核空间创建.

针对playback, 用户空间往这块Buffer写入数据, 然后DMA把数据从Buffer搬移到I2S的FIFO。

针对capture, DMA把数据从I2S FIFO搬移到这块Buffer, 然后用户空间从Buffer读取数据.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.035.pnguploading.4e448015.gif转存失败重新上传取消

很明显, 这块Buffer涉及到一方生产, 另一方消费的问题. 因此用环形缓冲区管理这块Buffer是最合理的.

在环形缓冲区机制中, 当读/写指针增长到Buffer的边界时, 需要从0开始重新计数. 还要考虑读写指针相等时, Buffer是满还是空的问题. 环形缓冲区可以有多种实现方式, 不同的实现方式有不同的办法来解决这些问题. 有的实现方式非常巧妙, 例如内核的kfifo, 它利用2的幂次方这个特性, 使得解决上述问题变得很简单, 有兴趣可以去了解下.

在ALSA中, 它有自己的一套方法来实现环形缓冲区, 与kfifo比较相似, 但个人感觉没有kfifo易用. 也许ALSA这样做是为了解决kfifo对空间的浪费问题, kfifo要求分配的空间必现是2的幂次方而不管你实际使用的大小是多少, 但是ALSA的这套逻辑没有这个限制.

ALSA实现的环形缓冲区细节如下:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.036.pnguploading.4e448015.gif转存失败重新上传取消

红框代表实际Buffer的大小, 绿框代表逻辑Buffer的大小. 我们以Playback为例, 介绍几个主要的参数的意义:

         hw_ptr : 读指针, 代表DMA搬移数据的位置.

         appl_ptr : 写指针, 代表用户空间写入数据的位置.

         buffer_size : Buffer的实际大小.

         runtime->boundary : Buffer的逻辑大小, 它一般是实际大小的整数倍. 当读写指针超过boundary时, 会从0开始重新计数.

我们先看下写指针appl_ptr, 写指针是在逻辑Buffer上递增的, 但我们不能说往逻辑位置写入数据, 需要先找到实际的写位置才行. 那如何通过逻辑位置得到实际的写位置?

         appl_ofs = appl_ptr % buffer_size : 这个取模运算, 巧妙的得到了写指针在实际Buffer中的偏移量.

         real_appl_ptr = real_buffer_base + appl_ofs : 用偏移量+实际Buffer的起始地址, 就可以得到实际的写入地址了. 在分配Buffer时会把实际Buffer的起始位置存储起来.

再来看看读指针hw_ptr. 先想想何时会更新hw_ptr? 一般是当DMA搬移完一个period的数据后, 会产生一个中断, 在中断处理函数中更新hw_ptr比较合适. 那怎么更新咧?

         hw_ofs = ops->pointer() : 首先, 通过底层驱动获取读指针在实际Buffer中的偏移量.

         hw_ptr = hw_ptr_base + hw_ofs : hw_ptr_base是Buffer在逻辑位置上的基址, 基址+偏移量, 就可以得到读指针在逻辑地址上的位置了. 代码在运行过程中会适时更新hw_ptr_base的值.

在读写指针的更新过程中, 内核会保证它们满足一定的约束条件:

          首先, 因为appl_ptr和hw_ptr是逻辑上的指针, 因此理论上来说appl_ptr – hw_ptr可以大于实际Buffer的大小buffer_size. 但内核会保证appl_ptr与hw_ptr的距离不会大于buffer_size, 因为等于buffer_size意味着即将发生overrun事件, 也就是用户空间写入数据的速度太快了, DMA来不及搬移它们, 最终没有空闲的buffer给用户空间写入了. 此时内核不允许用户空间继续写入, 因此也不会继续更新appl_ptr了.

          其次, hw_ptr在增长的过程中, 不会超越appl_ptr. 因为当hw_ptr增长至与appl_ptr相等时, 意味着即将发生underrun事件, 也就是DMA搬移速度太快了, 用户空间写入太慢了, 没有新数据可以搬移了. 此时内核一般会暂停DMA搬移, 然后我们就可能听到短暂的爆破音(当然也可配置内核去搬移一段旧数据, 避免爆破音). 当发生这种问题时内核不会继续增加hw_ptr了, 因此它不会越过appl_ptr.

为何要引入逻辑Buffer这个东东? 其目的是为了方便计算空闲buffer大小:

         avail = hw_ptr + buffer_size – appl_ptr : (逻辑读位置 + 实际buffer的大小)– 逻辑写位置就是空闲buffer的大小.

         avail -= runtime->boundary (if avail>runtime->boundary) : 什么时候会出现avail > runtime->boundary ? 当appl_ptr越过boundary被重新赋值为0, hw_ptr非常接近boundary但还未越过boundary, 此时计算出来的avail就大会大于boundary. 但实际的空闲buffer没这么大, 需要减去boundary. 如果想不明白自己在纸上画一下就清楚了.

         avail += runtime->boundary (if avail<0) : 什么时候会出现avail < 0 ? 貌似非常罕见. 只有当boundary设置得非常大, 使得hw_ptr+buffer_size发生了整型数据溢出的问题, 溢出后由于最高位被丢弃, 使得其变成了一个很小的数, 此时hw_ptr+buffer_size – appl_ptr就会小于0. 不过就算发生这种问题, avail += runtime->boundary也可得到实际的空闲空间.

关于环形缓冲区代码层面的细节, 我们将在《struct snd_pcm_runtime》一节介绍它们.

1.5.3.2              数据结构

先来张全图, 方便查看各个数据结构间的隶属关系.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.037.pnguploading.4e448015.gif转存失败重新上传取消

struct snd_pcm

struct snd_pcm : 代表一个pcm实例, 也是代表一个pcm逻辑设备.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.038.pnguploading.4e448015.gif转存失败重新上传取消

         list : 把自己挂载到card-> devices表头下.

         streams : 指向它下属的playback stream和capture stream.

struct snd_pcm_str

struct snd_pcm_str : 代表一个pcm stream.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.039.pnguploading.4e448015.gif转存失败重新上传取消

         dev : 一个steam对应一个字符设备节点. 这的dev与创建字符设备节点有关.

         substream_count : 该stream下属的substream的个数.

         substream_opened : 有多少个substream已经被用户空间open了. 用户空间可以针对同一个设备节点open多次, 每次open内核层都会选一个空闲的substream与之对应. 如果所有的substream都被opened, 则新的open会失败.

         substream : 用链表的形式串联多个substream.

struct snd_pcm_file

struct snd_pcm_file : 每个被open的substream对应一个snd_pcm_file.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.040.pnguploading.4e448015.gif转存失败重新上传取消

struct snd_pcm_substream

struct snd_pcm_substream : 代表一个pcm substream. substream的一个重要功能就是要准备一块DMA buffer, 以便与用户空间交换数据.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.041.pnguploading.4e448015.gif转存失败重新上传取消

         buffer_bytes_max : buffer_bytes的最大值, buffer_bytes代表substream创建的那块DMA buffer的实际大小.

         dma_buffer: 用于描述substream创建的那块DMA buffer. 详见《struct snd_dma_buffer》.

         dma_max : 貌似没多大作用, 一般与buffer_bytes_max相等.

         ops : 需要底层驱动实现的接口函数, 详见《struct snd_pcm_ops》.

         runtime : 保存substream在运行过程中的一些runtime信息, 所谓运行过程就是指该substream被open之后、被close之前的这段时间. 详见《struct snd_pcm_runtime》.

         next : 指向下一个substream, 以便把所有的substream用链表的形式串起来.

struct snd_dma_buffer

struct snd_dma_buffer : 用于描述一块DMA buffer.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.042.pnguploading.4e448015.gif转存失败重新上传取消

         area : buffer的虚拟地址, 供CPU访问buffer时使用.

         addr : buffer的物理地址, 供DMA访问buffer时使用.

         bytes : buffer的大小.

struct snd_pcm_runtime

struct snd_pcm_runtime : 保存了一个substream在运行过程中的各种参数, 主要包括这几个方面: 音频格式、采样率等相关的参数, 这些参数与我们在《名称解释》中介绍的概念密切相关; 环形缓冲区管理相关的参数; 当出现overrun/underrun事件后, 指示如何处理该事件的参数.

这一小节很长, 也很复杂, 涉及到的知识很多. 但是搞明白它们, 就基本理解了‘PCM中间层’的核心逻辑了. 下面分别介绍下这几方面的参数:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.043.pnguploading.4e448015.gif转存失败重新上传取消

音频格式、采样率等相关的参数 (可结合《名称解释》一节理解它们的意义):

         unsigned int sample_bits : 代表采样深度, 例如8bit、16bit等.

         snd_pcm_format_t format : 音频格式, snd_pcm_format_t定义了所有内核支持的格式. 用户空间一般不直接设置采样深度, 而是设置音频格式. 音频格式和采样深度的转换关系见pcm_formats[], pcm_formats[x].phys代表的就是某格式对应的采样深度.

         snd_pcm_subformat_t subformat : 这个参数暂时没啥用.

         unsigned int channels : 声道数, 例如单声道、立体声、5.1声道等.

         unsigned int frame_bits : 一帧数据占多少个bits, 计算公式是 channels * sample_bits.

         snd_pcm_uframes_t period_size : 代表一个period内的帧数.

         unsigned int rate : 代表采样频率.

         snd_pcm_uframes_t buffer_size : 代表一块Buffer内的帧数. 这块Buffer由内核分配, 可被DMA访问, 用于与用户空间交换音频数据.

         unsigned int periods : 代表Buffer内的periods的数目, 这个参数与buffer_size的意义有点重复, 它俩可以相互转换, buffer_size = periods * period_size.

         snd_pcm_sframes_t delay : 从代码注释来看, 貌似代表I2S的FIFO的大小, 以帧为单位. 把它命名为delay的原因是如果我们知道I2S FIFO的大小, 同时知道采样频率, 那我们就可计算出I2S传送完这段数据所需的时间.

         snd_pcm_uframes_t min_align ;  size_t byte_align : 这两个参数都与对齐有关, 一个是以帧为单位, 一个是以byte为单位.

这个对齐是指内核分配的那块Buffer要对齐到这个边界上, 由前述可知, 内核分配的这块Buffer一定是frame的整数倍, 不是已经很‘整齐’了吗, 还有什么好对齐的? 没错, 如果sample_bits是8的整数倍, 分配的这块Buffer既是frame的整数倍, 又可对齐到字节边界, 此时没有什么好调整的. 但假如sample_bits很奇怪, 不是8的整数倍, 比如是14bit, 那内核分配的这块Buffer虽然是frame的整数倍, 但很可能无法对齐到字节边界, 这样不规整的buffer不好管理.

所以内核有一段逻辑来计算这个min_align和byte_align, 代码在snd_pcm_hw_params中, 摘录如下:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.044.pnguploading.4e448015.gif转存失败重新上传取消

         最后, 内核提供了几个与上述格式相关的函数, 简单列一下:

bytes_to_samples : 字节数转换为sample数.

bytes_to_frames : 字节数转换为帧数.

samples_to_bytes : samples转字节数.

frames_to_bytes : 帧数转字节数.

snd_pcm_lib_buffer_bytes : 获取一个substream分配的那块Buffer的字节数.

snd_pcm_lib_period_bytes : 获取一个substream的一个period的字节数.

环形缓冲区管理相关的参数:

         unsigned char *dma_area

         dma_addr_t dma_addr

         size_t dma_bytes

         struct snd_dma_buffer *dma_buffer_p

这几个参数用来描述内核分配的那块用于交换音频数据的Buffer. 这块Buffer有可能在substream初始化过程已经分配好了, 保存在snd_pcm_substream->dma_buffer中, 称为pre-allocated(例如atmel pdc就有pre allocated), 此时只需把这块Buffer赋值给snd_pcm_rumtime的这几个参数即可. 如果没有pre-allocate, 则substream在运行过程中会分配这块Buffer, 并填充这几个字段. 这一段逻辑的代码细节参见snd_pcm_lib_malloc_pages.

         snd_pcm_uframes_t hw_ptr_base : 见《PCM环形缓冲区管理》一节对它的解释.

         struct snd_pcm_mmap_status *status : status->hw_ptr代表硬件DMA的读(playback)/写(capture)位置.

         struct snd_pcm_mmap_control *control : control->appl_ptr代表用户空间的写(playback)/读(capture)位置.

         snd_pcm_uframes_t hw_ptr_interrupt / unsigned long hw_ptr_jiffies / unsigned long hw_ptr_buffer_jiffies / u64 hw_ptr_wrap : 这几个具体意义暂时不知, 但应该与hw_ptr有关.

         snd_pcm_uframes_t boundary : 见《PCM环形缓冲区管理》一节对它的解释.

管理环形缓冲区的几个数据结构都有了, 那缓冲区的读/写指针是何时更新的呢? 我们以Playback为例, 来看看写指针appl_ptr和读指针hw_ptr的更新时机:

         appl_ptr的第一种更新时机是当用户空间调用write函数往缓冲区中写入数据时, 在内核层snd_pcm_write -> snd_pcm_lib_write -> __snd_pcm_lib_xfer函数会计算appl_ptr的新位置, 并最终调用pcm_lib_apply_appl_ptr更新该参数.

         appl_ptr的第二种更新时机是当用户空间通过mmap的方式往缓冲区中写入数据时, 在mmap方式下, 内核并不知道用户空间何时完成写入了, 因此用户空间完成写入时需要通过某种方式告知内核. alsa提供了ioctl SNDRV_PCM_IOCTL_SYNC_PTR, 供用户空间通知内核更新appl_ptr, 例如tinyalsa中的pcm_sync_ptr采用的就是这种方式. 在内核层, snd_pcm_common_ioctl -> snd_pcm_sync_ptr -> pcm_lib_apply_appl_ptr 会最终更新该参数.

         hw_ptr的更新时机是当DMA搬移完一个period的数据后, 此时会产生一个硬件中断, 在中断处理函数中会调用snd_pcm_period_elapsed -> snd_pcm_update_hw_ptr0来更新hw_ptr.

除了硬件中断外, 用户空间也可通过ioctl SNDRV_PCM_IOCTL_HWSYNC / SNDRV_PCM_IOCTL_REWIND / SNDRV_PCM_IOCTL_FORWARD或是在ioctl SNDRV_PCM_IOCTL_SYNC_PTR指定flag为SNDRV_PCM_SYNC_PTR_HWSYNC来通知内核更新hw_ptr (ioctl -> … -> do_pcm_hwsync -> …. -> snd_pcm_update_hw_ptr0). 内核层也可通过主动调用snd_pcm_update_hw_ptr -> … -> snd_pcm_update_hw_ptr0来更新hw_ptr.

不论是哪种情形, 最终都是在snd_pcm_update_hw_ptr0中计算并更新hw_ptr.

最后, 内核还提供了几个获取缓冲区空闲空间的函数:

snd_pcm_playback_avail : playback模式下用户空间可写入的空间有多少.

snd_pcm_capture_avail : capture模式下用户空间可读取的数据有多少.

snd_pcm_playback_hw_avail : playback模式下DMA可搬移的数据有多少.

snd_pcm_capture_hw_avail : capture模式下DMA可写入的空间有多少.

overrun/underrun事件相关的参数:

以playback模式为例, 说明下它们的意义.

         snd_pcm_uframes_t start_threshold : 缓冲区的数据超过该值时, 硬件开始启动数据传输. 如果太大, 从开始播放到声音出来时延太长, 甚至可导致太短促的声音根本播不出来; 如果太小, 又可能容易导致Xrun.

         snd_pcm_uframes_t stop_threshold : 缓冲区空闲区大于该值时, 硬件停止传输. 默认情况下, 这个数为整个缓冲区的大小, 即整个缓冲区空了, 就停止传输. 但偶尔的原因导致缓冲区空, 如CPU忙, 增大该值, 继续播放缓冲区的历史数据, 而不关闭再启动硬件传输(一般此 时有明显的声音卡顿),可以达到更好的体验.

当stop_threshold大于整个缓冲区大小时, 当DMA搬移完所有有效数据时, 读写指针就重叠了. 如果不停止DMA, 继续搬移, 就意味着会搬移一些写指针之后的旧数据, 此时我们就会听到一些旧声音. 如果不想听到旧声音, 我们可以把写指针之后的这段旧数据清零, 这就是所谓的silence模式.

         snd_pcm_uframes_t silence_threshold : 当DMA可搬移的数据小于该值时, 意味着马上就要搬完了, 此时内核开始把旧数据清零.

         snd_pcm_uframes_t silence_size : 每次清零动作最多会清零多大的空间, 如果它被设为0, 则内核不会去做清零动作, 也就是说silence模式无效.

         snd_pcm_uframes_t silence_start : 从哪个位置开始清零.

         snd_pcm_uframes_t silence_filled : 已经清零了多少空间.

内核中的snd_pcm_playback_silence函数负责清零动作, 阅读代码可了解更多细节.

其它一些比较重要的参数:

         unsigned int info : 与SNDRV_PCM_INFO_XXX有关, 貌似是用来表示底层硬件的一些特性. 由底层硬件对此参数进行初始化.

         snd_pcm_access_t access : snd_pcm_access_t定义了用户空间访问内核分配的那块用于交换音频数据的Buffer的方式, 包括:

          SNDRV_PCM_ACCESS_MMAP_INTERLEAVED / SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED / SNDRV_PCM_ACCESS_MMAP_COMPLEX :

mmap表示把Buffer map到用户空间, 然后在访问.

interleaved与noninterleaved是指Buffer中声道的排放方式, 以双声道为例: interleaved是指左右声道交替排放, noninterleaved是指先把所有的左声道排放在一起、然后接着在把所有的右声道排放在一起.

          SNDRV_PCM_ACCESS_RW_INTERLEAVED / SNDRV_PCM_ACCESS_RW_NONINTERLEAVED : RW代表用户空间用write/read的方式访问Buffer. interleaved与noninterleaved意义同上.

         struct snd_pcm_hardware hw : 详见《struct snd_pcm_hardware》.

         struct snd_pcm_hw_constraints hw_constraints : 详见《struct snd_pcm_hw_constraints》.

struct snd_pcm_ops

struct snd_pcm_ops : PCM中间层定义的需要底层驱动实现的接口函数, 相当于interface. 中间层在恰当的时候会回调这些接口函数. 从底层驱动的角度来说, 绝大部分工作就是实现这个ops定义的函数(一般只需实现部分, 其它的中间层都有默认实现. 除非中间层的实现在自己的硬件上用不了, 才需要我们自己实现.), 然后向PCM中间层‘注册’即可.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.045.pnguploading.4e448015.gif转存失败重新上传取消

         open : 当用户空间open某个substream时, 会回调这里的open函数, 并把substream作为参数传进来.

         close : 同上

         ioctl : 同上

         hw_params : 在《名称解释》一节我们介绍了一些参数, 这些参数被内核称为hw params. 用户空间可以对这些参数进行配置, 比如设置音频的格式、通道数、period大小等. 当用户空间对这些参数进行配置时, 会回调这里的hw_params函数. 该函数的参数snd_pcm_hw_params相当于一个参数集, 包含需要设置的各个参数. 详见《hw params description》一节对该结构体的介绍.

         hw_free : 前文说过substream会准备一块用户交换音频数据的Buffer, 当hw_free被回调时, 我们需要在该函数中释放掉这块Buffer.

既然有释放, 就应该有分配. substream的这块Buffer是在哪里分配的呢? 底层驱动在open或hw_params回调函数中, 会调用snd_pcm_lib_malloc_pages, 该函数会检查是否有pre_allocated Buffer, 如果有就直接使用它; 否则就会调用PCM中间层提供的函数snd_dma_alloc_pages来分配Buffer.

         prepare : 当用户空间把数据写入Buffer后, 会通知底层做prepare动作, 此时一般是对DMA进行相关配置, 使得其处于待命状态.

         trigger : 用于启动或暂停DMA传输. 参数cmd表示到底是启还是停.

         pointer : 获取DMA此刻正在从Buffer的哪个位置搬移数据. 可结合《PCM环形缓冲区管理》一节理解它.

         get_time_info : 暂时不知.

         fill_silence : 当发生Xrun事件时, 我们可以把旧数据清零, 这样用户听到的就是‘静音’. 该函数执行具体的清零动作.

         copy_user : 当用户空间读写音频Buffer时, 会回调此函数. 在该函数里面一般会用copy_from_user / copy_to_user.

         copy_kernel : 当内核空间读写音频Buffer时, 会回调此函数. 在该函数里面使用memcpy即可.

         page : 给定虚拟机制, 返回该地址对应的物理页面. 参数offset是指在音频Buffer内的偏移量, substream->runtime->dma_area + offset就能得到一个虚拟地址. 一般都没有实现该函数, 即使实现了也是用的中间层提供的snd_pcm_lib_get_vmalloc_page.

         mmap : 用户空间可以把音频Buffer先map到用户空间, 然后在访问. 当用户空间执行map操作时, 该函数会被回调. 因为Buffer一般已经分配好了, 该函数只需修改页表建立映射即可, 一般也无需实现.

         ack : 当PCM中间层更新环形缓冲区的指针appl_ptr时, 会回调该函数, 只有当它的范围值大于等于0时, 更新动作才成功. 例如pcm_lib_apply_appl_ptr里面就回调了该函数.

hw params description

我们在《名称解释》一节介绍了很多参数, 这些参数被称作hw params. 在struct snd_pcm_runtime中, 有对这些参数的定义:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.046.pnguploading.4e448015.gif转存失败重新上传取消

PCM中间层提供了一套不太好理解的机制来描述这些参数, 我们对这套机制做个基本介绍.

首先, 内核定义了snd_pcm_hw_param_t来代指这些参数, 例如SNDRV_PCM_HW_PARAM_ACCESS代表access参数, SNDRV_PCM_HW_PARAM_SAMPLE_BITS代表sample_bits参数. 可以把它理解成数组的下标, 不同的下标代表不同的参数, 然后数组里面存的就是参数值.

好像很好理解嘛, 有什么奇怪的? 麻烦就麻烦在这个‘数组’, 如果所有的参数都是int型, 那我们用一个int params[]数组就行了, 这到简单. 但这里的参数类型不一, 普通的数组无法满足要求, 只能设计一个结构体数组. 这个结构体数组就是struct snd_pcm_hw_params, 它里面包含了所有的hw params.

要理解这个结构体数组, 我们要先了解参数的类型. 内核把参数的分为两种类型:

         第一种是mask类型的. 所谓mask类型就是说一个bit代表一个意义, 多个bit可以相或, 例如formats = SNDRV_PCM_FMTBIT_U8 | SNDRV_PCM_FMTBIT_S16_LE, 详见《struct snd_mask》. 内核定义access, format, subformat这三种参数是mask类型的.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.047.pnguploading.4e448015.gif转存失败重新上传取消

         第二种是interval类型的. 这种类型有点奇怪, 它可能是个整数, 也可能是个范围[min, max]. 详见《struct snd_interval》. 内核定义了下面这些参数都是interval类型的.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.048.pnguploading.4e448015.gif转存失败重新上传取消

内核用struct snd_mask和struct snd_interval分别描述这两种参数. 它们的细节如下.

struct snd_mask

struct snd_mask : 描述一个mask类型的参数.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.049.pnguploading.4e448015.gif转存失败重新上传取消

          SNDRV_MASK_MAX 256代表一个snd_mask最多可以存储256个bit.

          一个u32可以存储32个bit, 因此数组 __u32 bits[(SNDRV_MASK_MAX+31)/32]就代表可以存储SNDRV_MASK_MAX个bit.

实际上用不了那多, 最大的也只用了53个bit, 用u64就可存放了, 不知内核这样设计是不是为了考虑以后的扩展性. 内核几个mask参数占用的bit数如下:

         access : #defineSNDRV_PCM_ACCESS_LASTSNDRV_PCM_ACCESS_RW_NONINTERLEAVED (5个bit)

         format : #defineSNDRV_PCM_FORMAT_LAST SNDRV_PCM_FORMAT_DSD_U32_BE (53个bit)

         subformat : #defineSNDRV_PCM_SUBFORMAT_LASTSNDRV_PCM_SUBFORMAT_STD (1个bit)

struct snd_interval

struct snd_interval : 描述一个interval类型的参数.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.050.pnguploading.4e448015.gif转存失败重新上传取消

          openmin置位, 代表min有效, 此时代表参数最小值是多少.

          openmax置位, 代表max有效, 此时代表参数最大值是多少.

          openmin, openmax都置位, 代表参数的范围是[min, max].

          integer置位, 代表参数是整形, 此时min == max.

struct snd_pcm_hw_params

struct snd_pcm_hw_params : 用于描述一个硬件参数集. 当用户空间想修改某hw params时, 准备好该参数集, 然后把它设置到内核即可. 反之, 当用户空间想获取该参数集时, 请求内核返回该参数集即可.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.051.pnguploading.4e448015.gif转存失败重新上传取消

          flags : 可以设置一些condition标志, 与snd_pcm_hw_constraints相关, 后文详述.

          masks[] : struct snd_mask类型的数组, 大小为3. 用于描述access、format、subformat这几个参数.

          intervals[] : struct snd_interval类型的数组, 用于描述内核定义的所有interval参数.

          rmask : 由用户空间设置. 当用户空间想修改hw params时, 并不一定会修改所有的参数, 该标志表示用户空间想修改哪些参数, 例如 SNDRV_PCM_HW_PARAM_ACCESS | SNDRV_PCM_HW_PARAM_SAMPLE_BITS代表想修改access和sample_bits这两个参数. 内核会检测该rmask, 只关心需要修改的参数, 忽略其它参数.

          cmask : 由内核空间设置. 当用户空间通过rmask告知内核修改一些参数时, 内核空间会在修改成功后置位cmask中对应的bit. 如果对应的bit没有被置位, 则代表参数修改没有成功.

          info / msbits / rate_num / rate_den / fifo_size : 这几个参数都由内核空间设置. 当用户空间请求内核返回snd_pcm_hw_params参数集时, 内核会设置这几个参数, 然后返回.

顺便提一下, 用户空间通过ioctl SNDRV_PCM_IOCTL_HW_PARAMS来设置/获取该参数集.

struct snd_pcm_hardware

struct snd_pcm_hardware : 它的意义与snd_pcm_hw_params非常相似, 也是用来描述各种hw params. 不同的是它是用来描述底层硬件具备哪些能力的, 例如可支持哪些format, 哪些采用频率等. 它一般是在底层驱动中定义的, 由工程师查阅相关硬件手册后填写该结构体.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.052.pnguploading.4e448015.gif转存失败重新上传取消

每个字段的意思都很清楚, 不在赘述了.

sw params description

当发生Xrun事件时, 我们需要一些参数控制内核的行为(是继续播放还是停止播放, 如果继续的话, 播放旧声音还是静音数据), 这些参数称为sw params.

在struct snd_pcm_runtime中, 有对sw params的定义:

struct snd_pcm_sw_params

struct snd_pcm_sw_params : 用于描述一个软件参数集. 当用户空间想修改某sw params时, 准备好该参数集, 然后把它设置到内核即可. 反之, 当用户空间想获取该参数集时, 请求内核返回该参数集即可.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.054.pnguploading.4e448015.gif转存失败重新上传取消

有关参数的意思在《struct snd_pcm_runtime》一节都描述过了, 这里不在赘述.

顺便提一下, 用户空间通过ioctl SNDRV_PCM_IOCTL_SW_PARAMS来设置/获取该参数集.

hw params constraints

constraints的意思是我们可以在内核里面设置一些rule, 每个rule有一个检查函数, rule可以指定自己关心哪些参数. 然后把所有的rules用链表串起来.

当用户空间对某个参数进行设置时, 内核会遍历所有的rules, 如果发现某个rule关心的参数集里面包含当前正在被修改, 则会调用该rule的检查函数, 只有当检测函数通过时, 才允许修改此参数. 这就是所谓constraints(限制)的意义.

内核用struct snd_pcm_hw_rule描述一个rule, 用struct snd_pcm_hw_constraints串联所有的rules.

struct snd_pcm_hw_rule

struct snd_pcm_hw_rule : 代表一个rule.

         cond : 设置一些条件. 如果某个rule设置了cond, 那么只有当snd_pcm_pcm_hw_params.flags也设置了相同的cond时, 才会进行rule.func检查. 否则忽略该rule. 代码详见这个判断.

         deps[4] : 代表该rule所关心的参数集, 当用户空间修改参数集中的任何一个参数时, 都会触发此rule被执行. 貌似最大只能关心5个参数?

         func : 检查函数, 在检查函数中, 一般会更新var所指向的参数. 意思就是说, 如果用户空间想修改deps[]中的某个参数时, 内核空间要先看看能否修改var所指向的参数, 如果不行, 那也不允许用户空间修改.

struct snd_pcm_hw_constraints

struct snd_pcm_hw_constraints : 相当于一个池子, 串联所有的rules.

         *rules : 链表, 串联所有的rules.

         rules_num : 该池子中实际有多少个rules.

         rules_all : 该池子的容量有多少, 当池子容量不够时, 系统会自动扩容. 每次扩容16个rule空间.

添加与检查rules的时机

添加rules的时机:

         snd_pcm_open_substream -> snd_pcm_hw_constraints_init -> snd_pcm_hw_rule_add添加了很多系统默认的rules.

         另外, 在必要的时候, 我们也可调用snd_pcm_hw_rule_add添加一些自定义rule.

检查rules的时机:

         ioctl -> … -> snd_pcm_hw_refine -> constrain_params_by_rules会扫描并检查所有的rules

1.5.3.3              API

这里只介绍PCM中间层提供给底层驱动使用的API.

ALSA内核代码中还有很多有用的工具函数, 例如分配环形缓冲区的snd_pcm_lib_malloc_pages, 获取缓冲区空闲空间的snd_pcm_playback_avail等, 我们都已经在《数据结构》的相关小节描述了, 这里暂时不赘述了. 后续如果有问题可以把这些函数专门整理了放入此节.

snd_pcm_new

snd_pcm_new在source/sound/core/pcm.c中定义.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.057.pnguploading.4e448015.gif转存失败重新上传取消

该函数主要作用是创建一个PCM实例, 具体包括:

          构建一个struct snd_pcm数据结构来代表一个PCM实例

          调用snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)构建playback stream, 并创建playback_count个substream.

          调用snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)构建capture stream, 并创建capture_count个substream.

          最后, 调用snd_device_new把这个实例作为一个逻辑设备添加到card->devices链表下.

请注意, 在card被注册之前, 我们需要调用snd_pcm_set_ops为此PCM实例设置回调函数, 因为当用户空间通过设备节点与PCM中间层交互时, PCM中间层需要回调底层驱动实现的ops函数.

snd_pcm_set_ops

snd_pcm_set_ops是在source/sound/core/pcm_lib.c中定义的.

底层驱动实现好struct snd_pcm_ops后, 调用此API分别为playback stream和capture stream设置回调函数.

1.5.3.4              底层驱动向PCM中间层注册

底层驱动最主要的工作是实现struct snd_pcm_ops结构体, 然后调用中间层提供的API注册即可.

关于如何实现snd_pcm_ops, 暂时不多说了, 看《数据结构》一节的描述应该能理解的差不多了.

注册步骤如下, 抄一张网上的图片:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.059.pnguploading.4e448015.gif转存失败重新上传取消

1.5.3.5              PCM字符设备的创建

当card被注册时, 会扫描下属的每个逻辑设备并注册它们, 这里创建的PCM逻辑设备也会在那时进行注册. 当PCM逻辑设备被注册时, ALSA系统层会回调逻辑设备的snd_device_ops. dev_register函数, 也就是snd_pcm_dev_register. 在该回调函数中, 会针对每一个stream调用snd_register_device, 进而在用户空间创建对应的设备节点.

PCM中间层的snd_pcm_f_ops会负责与用户空间交互, 其主要功能包括:

         open / release : 打开或者关闭某substream.

         read / write / mmap : 用户空间与PCM中间层交换音频数据.

         ioctl : 提供各种各样的控制接口.

1.5.3.6              PCM中间层的状态机

下图是PCM的状态的转换图:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.060.pnguploading.4e448015.gif转存失败重新上传取消

除XRUN状态之外, 其它的状态大多都由用户空间的ioctl()显式的切换. 例如:

1)         用户空间open, 内核层open一个substream, 处于OPEN状态.

2)         用户空间ioctl(SNDRV_PCM_IOCTL_HW_PARAMS)设定runtime的hw params, substream切换到SETUP状态.

3)         用户空间ioctl(SNDRV_PCM_IOCTL_PREPARE)后, substream切换到PREPARE状态. 此时代表DMA硬件已经准备好了, 可以开始搬移数据了.

4)         用户空间ioctl(SNDRV_PCM_IOCTL_START)后, substream切换到RUNNING状态. 此时DMA开始搬移数据.

5)         用户空间ioctl(SNDRV_PCM_IOCTL_DROP)后, substream切换回SETUP状态.

XRUN状态又分有两种: 在播放时, 用户空间没及时写数据导致缓冲区空了, 硬件没有可用数据播放导致UNDERRUN; 录制时, 用户空间没有及时读取数据导致缓冲区满后溢出, 硬件录制的数据没有空闲缓冲可写导致OVERRUN.

1.5.3.7              hrtimer 模拟 PCM 周期中断

在《struct snd_pcm_runtime》中讲述DMA Buffer相关的代码时, 我们提到‘当DMA搬移完一个period的数据后, 此时会产生一个硬件中断, 在中断处理函数中会调用snd_pcm_period_elapsed -> snd_pcm_update_hw_ptr0来更新hw_ptr’, 同时会调用DMA开始下一轮数据传输.

但有些platform可能存在一些设计缺陷, 当DMA搬移完成后不会产生硬件中断, 这样系统如何知道什么时候传送完了一个period的数据呢? 在已知sample_rate的情况下, I2S消耗一个period的数据是可以计算出来的, 这个时间也就是DMA搬移数据的时间. 因此我们可以用定时器来模拟这种硬件中断:

         首先触发DMA搬运数据, 同时启动定时器开始计时.

         当定时到 1 * period_size / sample_rate, 这时 I2S 已传输完一个period的音频数据了, 进入定时器中断处理: 调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经处理完毕了, 同时准备下一次的数据搬运.

为了更好保证数据传输的实时性, 建议采用高精度定时器 hrtimer. 例如fsl的imx-pcm-fiq.c就实现了snd_hrtimer_callback.

1.5.3.8              用户空间播放PCM音频

在用户空间, 我们可以用alsa-lib提供的API来播放PCM音频, 用法很简单, 基本步骤就是“打开->设置参数->读写音频数据”, 可以参考这个示例.

另外, Android自己开发了一套tinyalsa用于替代alsa-lib, 如果想用tinyalsa播放音频, 可参考tinyplay.c

用户空间与内核交换音频数据的方式目前已知的有三种:

         用户空间直接调用write / read.

         用户空间mmap内核Buffer, 然后写入/读取数据, 操作完成后用ioctl(SNDRV_PCM_IOCTL_SYNC_PTR)通知内核读写已完毕.

         用户空间直接用ioctl SNDRV_PCM_IOCTL_WRITEI_FRAMES / SNDRV_PCM_IOCTL_READI_FRAMES / SNDRV_PCM_IOCTL_WRITEN_FRAMES / SNDRV_PCM_IOCTL_READN_FRAMES 读写数据. 其中I代表interleaved, N代表noninterleaved.

2       ASoC

2.1    简介

从硬件的角度来说, 典型的音频设计是: 一块电路板上一颗CPU的I2S接口外挂一颗Codec芯片, Codec在外接耳机或功放等. 如下图所示:

以播放为例, 在这样一个硬件结构下, 涉及到几个模块:

         DMA : 负责把用户空间的音频数据搬移至I2S的FIFO.

         I2S : 负责以某个采样频率、采样深度、通道数发送音频数据, 也叫dai (Digital Audio Interface).

         AFIx : 负责以某个采样频率、采样深度、通道数接收音频数据, 也称作dai.

         DAC : 并把数据通过DAC转换后送给耳机等播放.

虽然说我们可以用第一章介绍的ALSA架构, 实现一个PCM逻辑设备, 并在逻辑设备的代码内控制上述几个模块, 但这样做会使得代码的复用性很差.

为了解决复用性问题, 内核引入了ASoC架构, 该架构存在于PCM中间层之下, 只是对PCM底层驱动的实现进行了拆分. 从用户空间的角度来说, 看不到任何区别, 用户空间面对的任然是ALSA框架提供了交互接口. 在底层ASoC抽象了如下三个模块:

         Platform : 该模块负责DMA的控制和I2S的控制, 由CPU厂商负责编写此部分代码.

         Codec : 该模块负责AFIx的控制和DAC部分的控制(也可以说是芯片自身的功能的控制), 由Codec厂商负责编写此部分代码.

         Machine : 用于描述一块电路板, 它指明此块电路板上用的是哪个Platform和哪个Codec, 由电路板商负责编写此部分代码.

从数据结构的角度来说, ASoC核心层内部定义了如下数据结构, 注意它们是由核心层内部创建和维护的:

         struct snd_soc_platform : 用于抽象一个platform, 作用是描述一个CPU的DMA设备及操作函数. 系统中可能有多个platforms, 它们都挂载在全局链表头static LIST_HEAD(platform_list)下面, 不同的platform以name区分.

         struct snd_soc_codec : 用于抽象一颗Codec. 一颗Codec可能会有多个dai接口, 该结构体的作用是描述与具体dai无关的、Codec内部的工作逻辑, 例如控件/微件/音频路由的描述信息、时钟配置、IO 控制等. 系统中可能有多个Codecs, 它们都挂载在全局链表头static LIST_HEAD(codec_list) 下面, 不同的Codec以name区分.

         struct snd_soc_dai : 用于描述一个dai, 既可以是CPU侧的dai(I2S), 也可以是Codec侧的dai(AFIx). 一颗CPU可能有多个I2S, 一个Codec也可能有多个AFIx, 因此系统中会有很多个dai, 它们都挂载在全局链表头static LIST_HEAD(dai_list)下面, 不同的dai以name区分.

从底层驱动的角度来说, ASoC定义了一些需要底层实现的interface以及相应的注册函数:

         针对DMA (platform): CPU厂商需要填充struct snd_soc_platform_driver 和struct snd_pcm_ops (《PCM逻辑设备》一节有描述该结构体), 然后调用snd_soc_register_platform向ASoC核心层注册, 例如atmel-pcm-pdc.c.

         针对I2S (cpu_dai): CPU厂商需要填充struct snd_soc_dai_driver 和 struct snd_soc_dai_ops, 然后调用snd_soc_register_dai向ASoC核心层注册, 例如atmel_ssc_dai.c.

         针对Codec (codec_dai) : Codec厂商需要填充struct snd_soc_codec_driver(用于描述Codec内部工作逻辑)和struct snd_soc_dai_driver、struct snd_soc_dai_ops(用于描述AFIx), 然后调用snd_soc_register_codec向ASoC核心层注册, 例如wm9081.c.

         针对Machine (codec): 电路板商需要填充struct snd_soc_dai_linkstruct snd_soc_ops, 然后准备一个struct snd_soc_card把dai_link包裹起来, 然后调用snd_soc_register_card注册此snd_soc_card. 例如atmel_wm8904.c.

当底层调用了snd_soc_register_card时, ASoC核心层会从全局链表中找到dai_link指定的platform、cpu_dai、codec_dai、codec, 并建立一个struct snd_soc_pcm_runtime来保存这些对应关系. 然后核心层会snd_card_new创建声卡, snd_pcm_new创建pcm逻辑设备, 最后snd_card_register注册声卡. 此后, 用户空间就可以看到设备节点了. 当用户空间访问设备节点时, 最终由ASoC核心层响应, 核心层会通过snd_soc_pcm_runtime找到对应的platform、cpu_dai、codec_dai、codec, 并根据需要回调它们实现的接口函数.

ASoC系统架构也在不停的演化, 从v4.18开始, 虽然ASoC还是划分为platform、cpu_dai、codec_dai、codec这几个模块, 但描述它们的数据结构发生了较大变化:

         首先, ASoC用统一的数据结构来描述platform和codec : 核心层用struct snd_soc_component替代原来的struct snd_soc_platform(描述platform)和struct snd_soc_codec(描述codec); 底层驱动则用struct snd_soc_component_driver替换原来的struct snd_soc_platform_driver和struct snd_soc_codec_driver.

         其次, 描述dai的数据结构虽然没有变化, 核心层依然用struct snd_soc_dai, 底层驱动依然用struct snd_soc_dai_driver、struct snd_soc_dai_ops. 但dai相关的数据结构被包裹到snd_soc_component下来统一管理了. 当底层驱动注册一个dai时, 核心层会创建一个snd_soc_component, 然后把dai挂载到这个数据结构下面.

         也就是说, 不管是platform、codec还是dai, 核心层现在都统一用struct snd_soc_component来管理了, 一个component对应一个模块. 因此原先的platform_list、codec_list、dai_list这3个表头都不存在了, 只有一个static LIST_HEAD(component_list)表头.

         最后, 核心层提供给底层的注册API也统一了, 不管是platform、codec还是dai, 现在都统一用devm_snd_soc_register_component / snd_soc_register_component注册即可.

         最最后, Machine层面没有什么变化, machine驱动还是构建一个struct snd_soc_card然后调用snd_soc_register_card注册即可. 而且核心层在注册函数的实现逻辑上也没有太大变化.

总的来说, 新的内核统一了几个数据结构和注册函数, 使之变得更加清爽了. 但不可避免的是新的数据结构相对要臃肿一些, 因为它要综合多个模块的需求, 在不同的模块驱动中, 根据需求可能只需填充新数据结构的部分即可.

本章以新的数据结构为基础来介绍它们. 下面开始旅程吧!

2.2    数据结构

先来一张数据结构的整体关系图:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.062.pnguploading.4e448015.gif转存失败重新上传取消

v4.18版本后, 内核用struct snd_soc_component来统一抽象所有的模块, 但不同的模块实现细节略有不同. 例如:

         用于描述Platform(I2S)的component, 主要靠下属的snd_soc_component_driver结构体, 该component下没有dai相关的结构体;

         但用于描述Platform(cpu_dai)的component, 则主要靠下属的snd_soc_dai、snd_soc_dai_driver、snd_soc_dai_ops结构体, snd_soc_component_driver在该component一般只有一个name, 其它callback函数都没有实现;

         用于描述Codec的component则同时包含snd_soc_component_driver和dai这两部分, 两者在该component下的地位都很重要.

系统中会有很多个component, 它们都挂载在全局链表头component_list下面.

下面分别介绍每个数据结构的细节, 这些数据结构在宏观上可分为三类: ASoC核心层内部使用的数据结构; Platform & Codec驱动中使用的数据结构; Machine驱动中使用的数据结构.

2.2.1      ASoC核心层

struct snd_soc_component

struct snd_soc_component : 由核心层内部创建和维护, 用于代表一个component. 当底层驱动注册platform、codec、dai时, 核心层都会创建一个对应的snd_soc_component.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.063.pnguploading.4e448015.gif转存失败重新上传取消

          list : 用于把自己挂载到全局链表component_list下.

          card_list : 用于把自己挂载的snd_soc_card-> component_dev_list下.

          driver : 指向下属的snd_soc_component_driver, 该结构体一般由底层驱动实现.

          dai_list : 链表头, 挂接下属的所有dais

          num_dai : 下属的dai的个数

          probe … set_bias_level : 看注释, 它们马上要被移除了.

          init : 在初始化阶段会回调它, 详见《初始化流程图》.

struct snd_soc_dai

struct snd_soc_dai : 由核心层内部创建和维护, 用于代表一个dai. 可以是cpu_dai或codec_dai.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.064.pnguploading.4e448015.gif转存失败重新上传取消

          name : 该dai的名称, ASoC核心层靠name来区分不同的dai.

          id : 应该是核心层自动分配的一个id号.

          driver : 指向下属的snd_soc_dai_driver, 该结构体一般由底层驱动实现.

          list : 用于把自己挂载到component->dai_list下.

struct snd_soc_pcm_runtime

struct snd_soc_pcm_runtime : 由核心层内部创建和维护, 当machine驱动调用了snd_soc_register_card之后, 在初始化过程中, 核心层会创建该数据结构, 详见《初始化流程图》.

一个runtime对应一个snd_soc_dai_link, 有几个dai_link就会创建几个runtime. 所有的runtime都会挂载在snd_soc_card-> rtd_list下面.

在初始化的后期, 代码会扫描rtd_list下的每一个rtd,  并针对它调用soc_new_pcm来创建一个PCM逻辑设备. 一个逻辑设备意味着用户空间可以与之交换音频数据, 从而导致播放或录制音频.

runtime这个结构体在系统运行中很重要, 它相当于一个中转站, 用户空间的所有操作都需要通过它才能转发到具体且正确的底层驱动上.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.065.pnguploading.4e448015.gif转存失败重新上传取消

         card : 对应的snd_soc_card.

         dai_link : 对应的snd_soc_dai_link.

         ops : PCM中间层需要我们实现的snd_pcm_ops, 该字段由ASoC核心层填充, 代码细节详见《回调函数被调用的时机》一节的分析.

         codec、platform : 原意是想指向Platform和Codec, 但这两个模块都统一用snd_soc_component来描述了, 因此这两个字段已经没啥用了, 后面会被删除.

         cpu_dai : 指向cpu_dai.

         codec_dai : 原意是想指向codec_dai, 但现在一个dai_link可以指定多个codec_dai, 因此必须用数组来描述了. 这个字段指向数组的第[0]个元素.

         codec_dais : 相当于一个数组, 描述所以的codec_dai.

         list : 用于把自己挂载到card->rtd_list下面.

         component_list : 链表头, 用于挂接所有隶属于该rtd的component, 这些component可能代表Platform, 也可能代表Codec.

2.2.2      Platfrom & Codec

struct snd_soc_component_driver

struct : 底层驱动需要填充该结构体, 然后向ASoC核心层注册.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.066.pnguploading.4e448015.gif转存失败重新上传取消

          name : 名字, ASoC核心层用名字区分不同的snd_soc_component_driver. 注意这个name与snd_soc_component->name不是同一个. 这里的name由驱动编写者填入, 而component->name由系统自动生成.

          Controls : 定义一些kcontrol. 参考《Control逻辑设备》一节的描述.

          dapm_widgets、dapm_routes : 与dapm相关, dapm其实是对kcontrol做了一层封装, 这里暂时不细述.

          probe : 在初始化阶段会回调它, 详见《初始化流程图》.

          pcm_new : 当向PCM中间层注册一个新的PCM逻辑设备时, 会回调此函数, 详见《初始化流程图》. 此函数中一般会针对每一个substream, 为之分配一块DMA Buffer, 也就是《PCM逻辑设备》中描述的那块环形缓冲区.

          pcm_free : 释放分配的DMA Buffer.

          set_sysclk、set_pll : 当用户空间通过ioctl设置hw params时, 这俩函数可能被回调, 详见《回调函数被调用的时机》. 这俩函数中一般设置clk和pll对应的寄存器.

          ops : snd_pcm_ops结构体, ASoC核心层在必要的时候会回调该结构体的相关函数, 详见《回调函数被调用的时机》. 每个函数的定义可参考《struct snd_pcm_ops》一节的描述.

struct snd_soc_dai_driver

struct snd_soc_dai_driver : 底层驱动在描述一个dai的时候需要准备该结构体.

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.067.pnguploading.4e448015.gif转存失败重新上传取消

         name : 唯一标示该dai.

         id : 系统自动编号.

         probe : 在初始化阶段会回调它, 详见《初始化流程图》.

         compress_new : 一般不实现它.

         pcm_new : 一般不需要实现, 因为snd_soc_component_driver中已经实现了一个pcm_new.

         ops : 一个struct snd_soc_dai_ops类型的结构, 该dai的操作函数集, 对于dai来说该结构很重要.

struct snd_soc_dai_ops

struct snd_soc_dai_ops : 底层驱动在描述一个dai的时候需要准备该结构体, 代表dai的操作函数集.

          set_xxx : 当用户空间通过ioctl设置hw params时, 会回调这些函数. 详见《回调函数被调用的时机》.

          digital_mute、mute_stream : 静音.

          startup …. delay : ASoC核心层在需要的时候会回调它们, 详见《回调函数被调用的时机》.

2.2.3      Machine

struct snd_soc_card

struct snd_soc_card : Machine驱动需要填充它, 然后调用snd_soc_register_card注册一个声卡. 之后系统将创建声卡, 创建PCM逻辑设备, 并最终会生成设备节点. 该结构体很长, 我们只介绍几个常用的字段:

          probe : 在初始化阶段会回调它, 详见《初始化流程图》.

          struct snd_soc_dai_link *dai_link : 代表多个dai_link数据结构, 每个dai_link描述一个I2S到AFIx的对应关系, 这是card下最重要的一个元素.

          int num_links : 代表该card下定义了多少个dai_links. 这些link是静态定义的, 称为/* predefined links*/.

          add_dai_link : ASoC核心层会回调该函数, 在该函数中我们可以动态添加一些dai_links, 这些links称为动态定义的. 一般动态的用的很少, 本文不会讨论它们.

          struct list_head rtd_list : 用于挂载所有隶属于该card的snd_soc_pcm_runtime, card下有多少个dai_link, 就会有多少个runtime.

          controls、dapm_widgets、dapm_routes : 可以指定一些controls和dapms, 在card注册阶段, 它们会被自动添加进系统.

struct snd_soc_dai_link

struct snd_soc_dai_link : Machine驱动需要填充它, 用于描述一个桥梁, 这个桥梁从硬件电路板的角度指明了哪个cpu_dai与哪个codec_dai相连, 并且指明了电路板上用的是哪个platfrom和哪个codec.

         cpu_name、cpu_of_node、cpu_dai_name : 用于指明一个cpu_dai, 一个dai_link只能指明一个cpu_dai.

cpu_of_node和cpu_name不能同时指定, cpu_dai_name一定要指定, 详见《soc_init_dai_link》.

通过这三个name, ASoC就能找到对应的cpu_dai, 详见《snd_soc_find_dai》.

         codec_name、codec_of_node、codec_dai_name : 用于指明一个codec_dai, 一个dai_link可以指明多个codec_dais, 但这种旧的方式只能指明一个, 因此它将逐渐被废弃.

         struct snd_soc_dai_link_component *codecs、unsigned int num_codecs : 这是新的指明codec_dai的方式, 可以指定多个codec_dai. 其中struct snd_soc_dai_link_component包含name、of_node、dai_name这3个字段, 用于标示一个codec_link. 字段的名字同样要满足《soc_init_dai_link》中的约定.

         platform_name、platform_of_node : 用于指明一个platform. 名字要满足《soc_init_dai_link》中的约定. 通过扫描component_list链表, 对比着两个名字, 就能找到代表该platform的那个component.

         init : 在初始化阶段会回调它, 详见《初始化流程图》.

         struct snd_soc_ops *ops : 最重要的操作函数集

struct snd_soc_ops

struct snd_soc_ops : 一组回调函数, ASoC核心层在必要的时候会回调这些函数, 详见《回调函数被调用的时机》, 模块名是dai_link. 这个ops的作用与struct snd_soc_dai_ops很类似, 只不过它是针对Machine的.

2.2.4      回调函数被调用的时机

前文说过ASoC架构存在于PCM中间层之下, 只是对PCM底层驱动的实现进行了拆分. 因此本质上来说, 它是与PCM中间层交互的, 当用户空间对设备节点执行操作时, 最终会由PCM中间层转发给ASoC架构来处理.

根据《PCM逻辑设备》一节的知识, 我们知道PCM中间层是通过snd_pcm_ops来回调底层的. 因此, ASoC在向PCM中间层注册时, 也实现了snd_pcm_ops, 代码详见soc_new_pcm:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.071.pnguploading.4e448015.gif转存失败重新上传取消

为简便叙述, 假设使用的是红框中的几个函数. 当用户空间操作设备节点时, 这些函数最终会被PCM中间层回调(回调时机请参考《struct snd_pcm_ops》一节的描述). 这些函数继而会调用ASoC底层驱动实现的接口函数, 具体的细节总结如下: 

Note : 红字代表模块名, 篮字代表函数名.

soc_pcm_open

    • cpu_dai->driver->ops->startup
    • component->driver->ops->open
    • codec_dai->driver->ops->startup
  • rtd->dai_link->ops->startup

soc_pcm_close

    • cpu_dai->driver->ops->shutdown
    • codec_dai->driver->ops->shutdown
    • rtd->dai_link->ops->shutdown
  • soc_pcm_components_close(substream, NULL)
    • component->driver->ops->close

soc_pcm_hw_params

    • soc_dai_hw_params(substream, &codec_params, codec_dai)
        • rtd->dai_link->be_hw_params_fixup
      • dai->driver->ops->hw_params
    • soc_dai_hw_params(substream, params, cpu_dai)
        • rtd->dai_link->be_hw_params_fixup
      • dai->driver->ops->hw_params
  • component->driver->ops->hw_params

soc_pcm_prepare

    • rtd->dai_link->ops->prepare
    • component->driver->ops->prepare
    • codec_dai->driver->ops->prepare
  • cpu_dai->driver->ops->prepare

soc_pcm_trigger

    • codec_dai->driver->ops->trigger
    • component->driver->ops->trigger
    • cpu_dai->driver->ops->trigger
  • rtd->dai_link->ops->trigger

soc_pcm_hw_free

    • rtd->dai_link->ops->hw_free
    • soc_pcm_components_hw_free(substream, NULL)
      • component->driver->ops->hw_free
    • codec_dai->driver->ops->hw_free
  • cpu_dai->driver->ops->hw_free

soc_pcm_pointer

    • component->driver->ops->pointer
    • cpu_dai->driver->ops->delay
  • codec_dai->driver->ops->delay

soc_pcm_ioctl

  • component->driver->ops->ioctl

2.3    API分析

ASoC的底层驱动可分为platform、codec、cpu_dai、codec_dai、machine这5部分.

其中platform、codec、cpu_dai、codec_dai现在都统一了注册方式:

         针对platform, 底层需准备好struct snd_soc_component_driver

         针对cpu_dai, 底层需准备好struct snd_soc_component_driver(一般只包含name字段)和struct snd_soc_dai_driver

         针对codec和codec_dai, 底层需准备好struct snd_soc_component_driver和struct snd_soc_dai_driver.

然后统一调用snd_soc_register_component或devm_ snd_soc_register_component注册即可. 后者是为了方便资源回收, 相当于智能指针, 因此这里只讨论snd_soc_register_component.

machine层需准备好struct snd_soc_card数据结构, 然后调用snd_soc_register_card即可.

下面分别介绍着两个API.

devm_snd_soc_register_component

snd_soc_register_component在source/sound/soc/soc-core.c中定义:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.072.pnguploading.4e448015.gif转存失败重新上传取消

它的参数是struct snd_soc_component_driver和struct snd_soc_dai_driver, 后者如果没有可以为NULL. 代码逻辑很清晰, 分配snd_soc_component存储空间, 然后调用snd_soc_add_component, 如下:

73ed7c5c-6abf-43fd-bde7-3f6d6db9619c.073.pnguploading.4e448015.gif转存失败重新上传取消

         snd_soc_component_initialize : 利用component_driver中的字段, 对新分配的component的相关字段进行初始化.

         snd_soc_register_dais : 如果有dai_drv, 则会创建num_dai个snd_soc_dai存储空间, 并对其进行初始化, 最后把它们挂载在component->dai_list链表下.

         snd_soc_component_add : 将这个component挂载到全局链表component_list下.

         snd_soc_try_rebind_card : 针对那些处于unbind_card_list下的card, 尝试rebind. rebind其实就是重新走一遍《初始化流程图》中的步骤. 这里暂不细究.

总的来看, snd_soc_register_component的作用就是创建核心层相关数据结构的存储空间并初始化它们, 然后把所有的component挂载到component_list链表下.

当machine调用snd_soc_register_card注册时, 会遍历这个list, 找到dai_link描述的那些component. 细节请看下节.

snd_soc_register_card

snd_soc_register_card在source/sound/soc/soc-core.c中定义.

当machine层准备好相关数据结构后就可调用此API了, 它将开始整个声卡的创建流程, 并最终使得用户空间可以通过设备节点播放/录制音频数据.

它的实现比较复杂, 我们将在《初始化流程图》中描述宏观流程, 然后介绍几个重要的函数.

初始化流程图

这个宏观流程图中, 有几个重点:

       首先注意紫色字体, 它们相当于使用ALSA架构提供的API, 创建了声卡和PCM逻辑设备并最终注册了此声卡. 这也说明ASoC的上层任然是原ALSA架构. 关于这些API的细节和含义, 可参考第一章《ALSA》.

       其次红色字体代表模块名, 蓝色字体代表函数名, 它们代表ASoC核心层回调底层驱动实现的接口函数.

       最后, 加粗字体代表几个比较重要的函数, 后文会详细介绍它们. 另外用文字对流程进行了注解.

snd_soc_register_card :

    • soc_init_dai_link : 对dai_link[]指定的参数的合法性进行检查
  • snd_soc_bind_card -> snd_soc_instantiate_card
    • soc_bind_dai_link
        • rtd = soc_new_pcm_runtime : 针对每个dai_link, 创建rtd.
        • rtd->cpu_dai = snd_soc_find_dai : 找到cpu_dai, 并存储在rtd中.
        • snd_soc_rtdcom_add(rtd, rtd->cpu_dai->component)
        • codec_dais[] = snd_soc_find_dai : 找到codec_dais[], 并存储在rtd中.
        • snd_soc_rtdcom_add(rtd, codec_dais[i]->component) : 找到codec对应的component并添加到rtd->component_list链表下.
        • rtd->codec_dai = codec_dais[0]
      • for_each_component(component) : 找到platform对应component并添加到rtd->component_list链表下.

snd_soc_rtdcom_add(rtd, component)

  • soc_add_pcm_runtime : 把这个rtd添加到card->rtd_list链表下, 注意此时rtd下已经存放了我们需要的platform、codec、cpu_dai、codec_dai.
    • list_add_tail(&rtd->list, &card->rtd_list)
    • snd_card_new : 调用ALSA API创建声卡.
    • card->probe : 首先回调card的probe函数.
    • soc_probe_link_components : 针对rtd下的每个component, 回调其初始化函数.
      • soc_probe_component :
          • component->driver->probe
        • component->init
    • soc_probe_link_dais : 针对rtd下的每个dai, 回调其初始化函数.
        • soc_probe_dai(cpu_dai, order)
          • dai->driver->probe
        • soc_probe_dai(rtd->codec_dais[i], order)
          • dai->driver->probe
        • dai_link->init
        • soc_new_pcm : 针对每个rtd(也就是每个dai_link), 创建一个PCM逻辑设备
            • snd_pcm_new
            • snd_pcm_set_ops : 这里的ops负责与用户空间交互.
          • component->driver->pcm_new : 此回调函数中会分配环形缓冲区.
        • soc_link_dai_pcm_new(&cpu_dai, 1, rtd)
          • dai->driver->pcm_new(一般driver都没有实现该函数)
      • soc_link_dai_pcm_new(rtd->codec_dais, …)
        • dai->driver->pcm_new(一般driver都没有实现该函数)
    • snd_soc_add_card_controls(card, card->controls, card->num_controls);
    • snd_soc_dapm_add_routes(&card->dapm, card->dapm_routes, card->num_dapm_routes);
    • card->late_probe(card);
    • snd_soc_dapm_new_widgets(card);
  • snd_card_register(card->snd_card) : 注册声卡, 此后用户空间出现设备节点, 用户程序可与底层交互了.

soc_init_dai_link

该函数会对dai_link指定的参数的合法性进行检查, 例如dai_link中不能同时指定cpu_name和cpu_of_node, 一定要指定cpu_dai_name, 等. 逻辑很简单, 直接看代码即可 : soc_init_dai_link.

snd_soc_find_dai

snd_soc_find_dai的逻辑很简单, 遍历component_list, 根据of_node、name、dai_name这3个字串找到对应的dai.

soc_new_pcm_runtime

soc_new_pcm_runtime用于分配《struct snd_soc_pcm_runtime》中所描述的这个数据结构.

2.4    示例

Platform : atmel-pcm-pdc.c

Cpu_dai : atmel_ssc_dai.c

Codec & Codec_dai : wm8904.c

Machine : atmel_wm8904.c

2.5    综述

整完这一章, 发现ASoC架构其实没有什么特别的. 本质上来说它就是一个PCM逻辑设备, 只是对这个逻辑设备的底层实现进行了进一步的划分. 理解了《PCM逻辑设备》, 在来看它就会很容易了.

3       Alsa Tools

3.1    amixer/alsamixer, aplay, arecord…

alsa-utils工具集提供了这些工具, alsa-utils可从alsa-project官网下载.

alsamixer是amixer的图形化版本, 需要ncurses的支持. 关于这些工具的用法, 参考ALSA音频工具amixer,aplay,arecord .

发布了748 篇原创文章 · 获赞 458 · 访问量 243万+

猜你喜欢

转载自blog.csdn.net/u010164190/article/details/104988474
今日推荐