ALSA学习笔记
扫描二维码
随时随地手机看文章
1 整体架构
Application
---------------
Alsa-lib User Space
-------------------------------------
Alsa Kernel Space
-------
sound driver
----------------------------------
Hardware
Application : 比如aplay ,它不是直接调用Kernel所提供的接口,而是调用ALSA-lib 的接口。所以应用程序只要#include "asound.h"
并链接libasound .
对于上面的架构,在某一时刻只能有一个程序打开声卡并占有它,此时其它程序打开的话,会返回busy.如要支持同时可以多个应用程序打开声卡,需要支持
混音功能,有些声卡支持硬件混音,但大部分声卡不支持硬件混音,需要软件混音。这时需要ESD,pulseAudio等,架构变为:
App1 App2
---------------
ESD , pulseaudio
--------------------
Alsa-lib User Space
-------------------------------------
Alsa Kernel Space
-------
sound driver
----------------------------------
Hardware
此时,应用程序将调用ESD,pulseaudio等混音器提供的接口。对于ESD,很多程序支持,比如mplayer . 对于pulseaudio ,有相应的patch .
Alsa本身也提供混音的plugin,dmix .
App1 App2
---------------
Alsa-lib (dmix) User Space
-------------------------------------
Alsa Kernel Space
-------
sound driver
----------------------------------
Hardware
此架构和架构1,应用程序不需要做任何修改,只需要修改asound.conf
架构1的asound.conf的例子:
pcm.!default {
type hw
card 0
}
ctl.!default {
type hw
card 0
}
架构3的asound.conf的例子:
pcm.card0 {
type hw
card 0
}
pcm.!default {
type plug
slave.pcm "dmixer"
}
pcm.dmixer {
type dmix
ipc_key 1025
slave {
pcm "hw:0,0"
period_time 0
period_size 4096
buffer_size 16384
periods 128
rate 44100
}
bindings {
0 0
1 1
}
}
关于配置,可以参考这个网站:
http://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html
对于period_size和buffer_size,要注意,我将他们修改为1024 ,8192.在我们的平台上用dmix会出现underrun!!! 信息。
2 ALSA kernel
2.1 目录
Alsa-driver包括很多在开发中的驱动,以及一些2.2,2.4 linux内核版本的支持。当这些驱动稳定后,将移入alsa-kernel中,并最终在linux kernel
的sound目录下.
sound
/core
/oss
/seq
/oss
/instr
/ioctl132
/include
/drivers
/mpu401
/op13
/i2c
/13
/synth
/emux
/pci
/(cards)
/isa
/(cards)
/arm
/ppc
/sparc
/usb
/pcmcia/(cards)
/oss
core目录
这个目录包含了中间层,ALSA的核心驱动。
core/oss
关于PCM和mixer的OSS模拟的模块保存在这个目录里面。Raw midi OSS模拟也被包含在ALSA rawmidi代码中,因为它非常小。音序器代码被保存在core/seq/oss目录里面
core/ioctl32
这个目录包含32bit-ioctl到64bit架构(如x86-64,ppc64,sparc64)的转换。对于32bit和alpha的架构,他们是不被编译的。
core/seq
它和它的子目录主要是关于ALSA的音序器。它包含了音序器的core和一些主要的音序器模块如:snd-seq-midi,snd-seq-virmidi等等。它们仅仅在内核配置中当CONFIG_SND_SEQUENCER被设定的时候才会被编译。我们在使用的ALSA驱动中也没有使用。
core/seq/oss
包含了OSS音序器的模拟的代码。
core/seq/instr
包含了一些音序器工具层的一些模块。
include目录
这里面放的是ALSA驱动程序开放给用户空间,或者被其他不同目录引用的共同头文件。
Drivers目录
这个目录包含了不同架构的系统中的不同驱动共享的文件部分。它们是硬件无关的。在子目录里面,会放一些不同组件的代码,他们是根据不同的bus和cpu架构实现的。
i2c目录
这里面包含了ALSA的i2c组件。
虽然LINUX有i2c的标准协议层,ALSA还是拥有它关于一些card的专用i2c代码,因为一些声卡仅仅需要一些简单的操作,而标准的i2c的API函数对此显得太过复杂了。
i2c/l3
这是ARM L3 i2c驱动的子目录
synth目录
它包含了synth(合成器)的中间层模块
pci目录
它和它的一些子目录文件负责PCI声卡和一些PCI BUS的上层card模块。
isa目录
它和它的一些子目录文件是处理ISA声卡的上层card模块。
arm,ppc,和sparc目录
这里放置一些和芯片架构相关的一些上层的card模块。
usb目录
这里包含一些USB-AUDIO驱动。在最新版本里面,已经把USB MIDI 驱动也集成进USB-AUDIO驱动了。
pcmcia目录
PCMCIA卡,特别是PCCcard驱动会放到这里。CardBus驱动将会放到pci目录里面,因为API函数和标准PCI卡上统一的。
oss目录
和ALSA无关。
在了解了相关的目录结构后,我们就开始来分析一下ALSA驱动了,在ALSA驱动中,我们所要从始至终要记住的是,我们的应用程序是不能直接访问驱动的,必须通过中间的ALSA层,因此我么所写的驱动,所提供的接口也是针对中间层的。
下面的内容我们从两个方面来考虑ALSA驱动,一个是驱动在操作硬件的同时,如何为中间层提供接口的,另外一个是应用程序是如何调用ALSA库API来实现对驱动的连接从而操作硬件的。
这了解这两个方面之前我们先来看一下ALSA的系统组成,也就是我们所说的中间层由哪些方面组成:
n 驱动包alsa-driver:指内核驱动程序,包括硬件相关的代码和一些公共代码,非常庞大
n 开发包alsa-libs:指用户空间的函数库,提供给应用程序使用,应用程序应包括头文件asoundlib.h。并使用共享库libasound.so
n 设置管理工具包alsa-utils:包含一些基于ALSA的用于控制声卡的应用程序,如alsaconf(侦测系统中声卡并写一个适合的ALSA配置文件),aplay(基于命令行的声音文件播放),arecord(基于命令行的声音文件录制)等
n 还包括开发包插件alsa-libplugins,其他声音相关处理小程序包alsa-tools,特殊音频固件支持包alsa-firmware,OSS接口兼容模拟层工具alsa-oss共7个子项目,其中只有驱动包是必须的
其实对于驱动包,我们用的是内核自带的,所以我们也没有安装。这个我们在前面已经详细讲解过,不多说了。
2.2 接口
Alsa kernel为上层主要提供以下接口:
1 control interface 提供灵活的方式管理注册的声卡和对存在的声卡进行查询。
2 PCM interface 提供管理数字音频的捕捉和回放。
3 原始 MIDI 接口
一种标准电子音乐指令集。 这些 API 提供访问声卡上的 MIDI 总线。这些原始借口直接工作在 The MIDI
事件上,程序员只需要管理协议和时间。
4 Timer 接口 为支持声音的同步事件提供访问声卡上的定时器。
5 音序器接口 一个比原始MIDI接口高级的MIDI编程和声音同步高层接口。它可以处理很多的MIDI协议和定时器。
6 mixer接口 控制发送信号和控制声音大小的声卡上的设备。/dsp/mixer,OSS中存在。
我们主要关心1,2接口
2.3 声卡的管理
2.3.1 卡
对于每一个声卡,一个“卡”的记录必须分配。
“卡”的记录是声卡的总部,它管理着声卡上的所有的设备(或者组件)的列表,例如PCM,Mixer,MIDI等等。
数据结构为:snd_card
其中 number : 第几个声卡,最大为SNDRV_CARDS 8个,对于我们的系统,只有一个声卡的话,number为0
id : 声卡的string
devices : 设备列表
proc_root :proc文件的根
private_data:声卡的私有数据
controls :声卡的控制接口列表
还有一些电源管理等
调用snd_card_new来创建一个声卡实体。
snd_card_new(index, id, module, extra_size);
其中extra_size为private_data内存空间的大小,在snd_card_new中分配。
2.3.2 设备(组件)
卡实例创建后,我们可以attach一个组件(设备)给一个卡的实例。在alsa驱动中,一个组件用结构snd_device对象表示。
一个组件可以是一个PCM实例,一个控制实例,一个原始MIDI接口等。它调用snd_device_new创建。
snd_device_new(card, snd_device_type_t, device_data, &ops);
在control.c ,pcm.c,info.c,Rawmidi.c以及timer.c中,都有snd_device_new的调用,分别创建类型为control,PCM等的deice.
数据结构为:snd_device
struct list_head list; /* list of registered devices */
struct snd_card *card; /* card which holds this device */
snd_device_state_t state; /* state of the device */
snd_device_type_t type; /* device type */
void *device_data; /* device structure */
struct snd_device_ops *ops; /* operations */
snd_device_ops包含了注册,unregister,free等函数。
在snd_card_new中,我们创建了一个control的device ,而snd_pcm_new创建了一个pcm的device
2.3.3 注册与释放
snd_card_register 调用它后,device 文件可以被外界访问。之前,不能安全被外界所访问。
snd_card_free 一般在退出的时候调用,这样将把所有的组件都自动释放掉。
2.4 PCM接口
ALSA PCM中间层非常强大,驱动只需要实现底层的函数以访问硬件。
每个卡最多可以有4个PCM实例。
一个PCM实例包含playback(回放)和capture(录音)流,数据结构为:snd_pcm,其中struct snd_pcm_str streams[2]; stream[0]代表
playback,stream[1]代表capture.
每一个pcm流包括一个或者多个pcm子流,
一些声卡支持多个playback功能。例如,emu10k1有一个PCM回放的32位立体声子流(substream),此时,每次打开,一个空闲的子流自动选择并
打开,同时,如果只有一个子流存在并已经打开了,接下来的打开要么被阻塞要么返回EAGAIN,但是这些在你的驱动中不需要关心,PCM中间层会
管理这些工作。
snd_pcm_new创建一个实体,
int snd_pcm_new(struct snd_card *card, char *id, int device,
int playback_count, int capture_count,
struct snd_pcm ** rpcm)
device : 第几个pcm实体,从0开始
playback_count: 回放子流数目
capture_count: 录音子流数目
流的数据结构:snd_pcm_str
其中 stream 表示方向,是playback还是capture
substream_count子流数目
struct snd_pcm_substream *substream;子流的指针,指向第一个子流,根据第一个,可以找到下一个(substream->next)
snd_pcm_new_stream创建一个流 ,在snd_pcm_new中被调用。
子流(substream)的数据结构:snd_pcm_substream
其中 stream 表示方向,是playback还是capture
buffer_bytes_max 表示最大的环形buffer大小
dma_buffer ?????
dma_max 最大
struct snd_pcm_ops *ops; 子流的操作函数,snd_pcm_set_ops会设置ops
struct snd_pcm_runtime *runtime;
next 下一个substream,对于只有一个playback的子流,为NULL
struct snd_timer *timer; ????
snd_pcm_ops 定义了一系列硬件操作函数,比如open , ioctrl , trigger等
1 open callback :
static int snd_xxx_open(struct snd_pcm_substream *substream);
这个函数在一个子流被打开时调用(snd_pcm_open_substream函数调用)
调用关系为:
在 snd_pcm_f_ops中 .open = snd_pcm_playback_open
snd_pcm_playback_open ---> snd_pcm_open_file---> snd_pcm_open_substream ---> snd_xxx_open
snd_pcm_f_ops的open ,当你对/dev/snd/PCMC0D0 open时候被调用
我们的驱动主要是分配substream->runtime,并调用request_irq把中断处理函数和runtime关联起来。
2 close callback
static int snd_xxx_close(struct snd_pcm_substream *substream);
这个函数在一个子流被关闭时调用
调用关系为:
在 snd_pcm_f_ops中 .release = snd_pcm_release
snd_pcm_release ---> snd_pcm_release_substream ---> snd_xxx_close
snd_pcm_f_ops的release ,当你对/dev/snd/PCMC0D0 close时候被调用
我们的驱动主要是调用free_irq把中断处理函数和runtime释放掉。
(是否有内存泄露,因为在open函数里对runtimer 用kmalloc了,在close函数里应该kfree(runtime)???
3 ioctl callback
一般用snd_pcm_lib_ioctl
4 hw_params callback
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params);
调用关系为:
在 snd_pcm_f_ops中 .unlocked_ioctl = snd_pcm_playback_ioctl
snd_pcm_playback_ioctl --> snd_pcm_playback_ioctl1
-->snd_pcm_common_ioctl1 ---> snd_pcm_hw_params_user ---> snd_pcm_hw_params ---> snd_xxx_hw_params
snd_pcm_f_ops的unlocked_ioctl,当你对/dev/snd/PCMC0D0 进行ioctl(int fd, int command, (char *) argstruct)调用时,文件系统的do_ioctl
会先判断unlocked_ioctl函数是否为空,不为空则调用filp->f_op->unlocked_ioctl ,其中command为 SNDRV_PCM_IOCTL_HW_PARAMS .
这个函数在 应用程序设置硬件参数时被调用,也就是说,当pcm子流的buffer大小,周期大小,格式等被定义的时候.
许多硬件的设置必须在这个回调函数中做,包括buffer的分配.buffer的分配可以调用snd_pcm_lib_malloc_pages函数.
我们的驱动好像只有buffer的分配,没做别的处理,会不会有问题 ????
5 hw_free callback
static int snd_xxx_hw_free(struct snd_pcm_substream *substream);
释放在 hw_params中分配的资源
6 prepare callback
static int snd_xxx_prepare(struct snd_pcm_substream *substream);
调用关系为:
在 snd_pcm_f_ops中 .unlocked_ioctl = snd_pcm_playback_ioctl
snd_pcm_playback_ioctl --> snd_pcm_playback_ioctl1
-->snd_pcm_common_ioctl1 ---> snd_pcm_prepare --> snd_pcm_do_prepare ---> snd_xxx_prepare
IO命令为 SNDRV_PCM_IOCTL_PREPARE.
你可以在这个函数里设置格式类型,采样率等,它和hw_params的区别在于 prepare回调函数在每次snd_pcm_prepare都会被调用
例如,underrun后的恢复等
在这个函数里,你可以通过runtime记录substream->runtime得到一些值,例如,目前的采样率,格式类型,声道数目,runtime->rate, runtime->format or runtime->channels
分配的内存设置在 runtime->dma_area,大小和周期分别为runtime->buffer_size和runtime->period_size.
我们的驱动这个函数有问题 .
7 trigger callback
static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);
调用关系为:
在 snd_pcm_f_ops中 .unlocked_ioctl = snd_pcm_playback_ioctl
snd_pcm_playback_ioctl --> snd_pcm_playback_ioctl1
-->snd_pcm_common_ioctl1 ---> snd_pcm_drain --> snd_pcm_do_drain_init ---> snd_xxx_trigger
IO命令为 SNDRV_PCM_IOCTL_DRAIN
这个callback函数是原子的,也就是不能调用会sleep的函数,trigger回调函数应当尽量小,只是triggering DMA,其他的操作在 hw_params和
perpare里面做.
我们的驱动实现了start,stop,SUSPEND 和 RESUME, 没有实现pause,unpause.还调用了msleep???? 这不对
mdelay是一个让CPU空转,一直等待到批定的时间后才退出
msleep是让当前进程休眠,让出CPU给其它进程使用,等到时间到了之后再唤醒
msleep不能用于中断上下文中
8 pointer callback
static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream)
该回调函数在PCM中间层咨询在buffer中当前硬件位置的时候被调用,位置以frames计算,范围为0到buffer_size - 1.
调用关系为:
snd_pcm_period_elapsed --> snd_pcm_update_hw_ptr_pos ---> snd_xxx_pointer
在中断处理函数中,snd_pcm_period_elapsed被调用,然后PCM中间层更新位置,并计算剩余空间,并唤醒睡着的线程.
SNDRV_PCM_POS_XRUN ????
9 其他的callback非强制的,我们驱动没实现他们.
设置好硬件操作函数后,你可以预先分配buffer,调用
snd_pcm_lib_preallocate_pages_for_all
该函数将更新子流的dma_max 以及 dma_buffer , 其中dma_buffer中的area 代表内存区域
PCM实例的释放,一般不需要,PCM中间层会自动释放其中的内存的,除非你在初始化的时候,分配了一些特殊的变量(用kmalloc),
此时在退出函数里,用kfree掉。
2.4.2. runtime
当一个PCM子流打开时,一个PCM的runtime实例被分配并赋给substream-〉runtime 。
runtime保持大多数你需要控制PCM的信息,hw_params and sw_params配置的拷贝,buffer的指针等
数据结构:snd_pcm_runtime
包括 hw :hw_params 信息
硬件描述符(struct snd_pcm_hardware)包含了基本硬件配置的定义.首先,你将在open 回调函数里定义它.
需要说明的是:runtime实例保持着描述符的copy,不是对已经的描述符的指针,也就是说,在open 回调函数中,你可以修改runtime->hw
根据你的需要.
2.4.3 中断处理
中断处理函数在声卡驱动中的作用:更新buffer的位置并在buffer位置越过以前设置的period size时,告诉PCM中间层.调用snd_pcm_period_elapsed函数
将告诉PCM中间层.
在snd_pcm_hw_params中会对period_size进行设置.
有以下几种声卡类型产生中断的方式:
1)在周期边界产生中断
最常用的方式
2)高频率的timer中断
用于那些不产生中断的芯片
我们的实现:
对于位置pcm_buf_pos ,在prepare调用的时候设为0,在中断的时候,每次会传60个bytes,每次加上60,循环.
然后调用snd_pcm_period_elapsed函数通知上层.
在中断中调用spin_lock ???? 为了多处理器吗???
PCM中的file_operation :
当调用snd_pcm_new时,
snd_pcm_new ---> snd_pcm_dev_register --> snd_register_device 此函数更新snd_minor结构,
而在 alsa_sound_init中,register_chrdev(major , "alsa",&snd_fops);当一个alsa主设备打开时,会调用snd_fops中的open函数,
也就是snd_open( 在sound.c中),而snd_open会根据snd_minor替换其file_ops,如果minor是PCM设备,将用PCM的file_operation
(snd_pcm_new中更新了)