writing an alsa driver 中文
writing-an-alsa-driver(编写一个ALSA驱动)翻译稿 第一章编写一个ALSA驱动(by Takashi Iwai)0.3.6版本翻译:creator sz111@126.com翻译这篇文章主要是为了学习ALSA驱动,因为感觉ALSA是Linux音频发展方向,所以下决心仔细看看,但是中文资料太少,就想翻译一份奉献给广大初学并且英文不好的朋友。不过自己的英文
writing-an-alsa-driver(编写一个ALSA驱动)翻译稿 第一章
编写一个ALSA驱动
(by Takashi Iwai)
0.3.6版本
翻译:creator sz111@126.com
翻译这篇文章主要是为了学习ALSA驱动,因为感觉ALSA是Linux音频发展方向,所以下决心仔细看看,但是中文资料太少,就想翻译一份奉献给广大初学并且英文不好的朋友。不过自己的英文也非常不好,我也在努力学习中。
翻译的不好,有些地方也不准确,希望大家多提宝贵意见,共同维护这篇文档。
这篇文档主要描述如何写一个ALSA(Linux 高级声音体系)驱动。
目录
前言
1.目录树架构
概述
内核
core/oss
core/ioctl32
core/seq
core/seq/oss
core/seq/instr
头文件
驱动
drviers/mpu401
drviers/opl3 和 opl4
i2c
i2c/l3
synth
pci
isa
arm,ppc,和sparc
usb
pcmcia
oss
2.PCI驱动的基本流程
概要
代码示例
构造器
1)检查并增加设备索引
2)创建一个声卡实例
3)创建一个主要部件
4)设定驱动ID和名字
5)创建其他部件,如:混音器(mixer),MIDI,等
6)注册声卡实例
7)设定PCI驱动数据,然后返回零。
析构器
头文件
3.管理声卡和部件
声卡实例
部件
chip相关数据
1.通过snd_card_new()分配
2.分配其他设备
注册和释放
4.PCI资源管理
代码示例
一些必须做的事情
资源分配
设备结构体的注册
PCI入口
5.PCM接口
概述
代码示例
构造器
析构器
PCM相关的运行时数据
硬件描述
PCM配置
DMA缓冲区信息
运行状态
私有数据
中断的回调函数
操作函数(回调函数)
open
close
ioctl
hw_params
hw_free
prepare
trigger
pointer
copy,silence
ack
page
中断向量
周期中断
高频率的时钟中断
调用snd_pcm_period_elapsed()
原子
约束
6.控制接口
概述
控制接口描述
控制接口名称
通用capture和playback
Tone控制
3D控制
MIC Boost
接口标识
回调函数
info
get
put
回调函数并不是原子的
构造器
更新通知
7.AC97解码器的API函数
概述
代码示例
构造器
回调函数
驱动中更新寄存器
调整时钟
Proc文件
多个解码器
8.MIDI(MPU401-UART)接口
概述
构造器
中断向量
9.Raw MIDI接口
概述
构造器
回调函数
open
close
输出子系统的trigger
输入子系统的trigger
drain
10.杂项设备
FM OPL3
硬件相关设备
IEC958(S/PDIF)
11.缓存和内存管理
缓存类型
附加硬件缓存
不相邻缓存
通过Vmalloc申请的缓存
12.Proc接口
13.电源管理
14.模块参数
15.如何把你的驱动加入到ALS代码中
概述
单一文件的驱动程序
多个文件的驱动程序
16.一些有用的函数
snd_printk()相关函数
snd_assert()
snd_BUG()
17.致谢
前言
这篇文章主要介绍如何写一个ALSA(Advanced Linux Sound Architecture)(http://www.alsa-project.org)驱动.这篇文档主要针对PCI声卡。假如是其他类似的设备,API函数可能会是不同的。尽管如此,至少ALSA内核的API是一样的。因此,这篇文章对于写其他类型的设备驱动同样有帮助。
本文的读者需要有足够的C语言的知识和基本的linux内核编程知识。本文不会去解释一些Linux内核编码的基本话题,也不会去详细介绍底层驱动的实现。它仅仅描述如何写一个基于ALSA的PCI声卡驱动。
假如你对0.5.x版本以前的ALSA驱动非常熟悉的话,你可以检查驱动文件,例如es1938.c或maestro3.c,那些是在0.5.x版本基础上而来的,你可以对比一下他们的差别。
这篇文档仍然是一个草稿,非常欢迎大家的反馈和指正。
第一章 文件目录结构
概述
有两种方式可以得到ALSA驱动程序。
一个是通过ALSA的ftp网站上下载,另外一个是2.6(或以后)Linux源码里面。为了保持两者的同步,ALSA驱动程序被分为两个源码树:alsa-kernel和alsa -drvier。前者完全包含在Linux2.6(或以后)内核树里面,这个源码树仅仅是为了保持2.6(或以后)内核的兼容。后者,ALSA驱动,包含了很多更细分的文件,为了在2.2和2.4内核上编译,配置,同时为了适应最新的内核API函数,和一些在开发中或尚在测试的附加功能。当他们那些功能完成并且工作稳定的时候最终会被移入到alsa 内核树中。
ALSA驱动的文件目录描述如下。 alsa-kernel和alsa-drvier几乎含有相同的文件架构,除了“core”目录和alsa-drvier目录树中被称为“acore”。
示例1-1.ALSA文件目录结构
sound
/core
/oss
/seq
/oss
/instr
/ioctl32
/include
/drivers
/mpu401
/opl3
/i2c
/l3
/synth
/emux
/pci
/(cards)
/isa
/(cards)
/arm
/ppc
/sparc
/usb
/pcmcia/(cards)
/oss
core目录
这个目录包含了中间层,ALSA的核心驱动。那些本地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被设定的时候才会被编译。
core/seq/oss
包含了OSS音序器的模拟的代码。
core/seq/instr
包含了一些音序器工具层的一些模块。
include目录
这里面放的是ALSA驱动程序开放给用户空间,或者被其他不同目录引用的共同头文件。一般来说,私有头文件不应该被放到此文件目录,但是你仍然会发现一些这样的文件,那是历史遗留问题了。
Drivers目录
这个目录包含了在不同架构的系统中的不同驱动共享的文件部分。它们是硬件无关的。例如:一个空的pcm驱动和一系列MIDI驱动会放在这个文件夹中。在子目录里面,会放一些不同的组件的代码,他们是根据不同的bus和cpu架构实现的。
drivers/mpu401
MPU401和MPU401-UART模块被放到这里。
drviers/opl3和opl4
OPL3和OPL4 FM-synth相关放到这里。
i2c目录
这里面包含了ALSA的i2c组件。
虽然Linux有个i2c的标准协议层,ALSA还是拥有它关于一些card的专用i2c代码,因为一些声卡仅仅需要一些简单的操作,而标准的i2c的API函数对此显得太过复杂了。
i2c/l3
这是ARM L3 i2c驱动的子目录
synth目录
它包含了synth(合成器)的中间层模块
到目前为止,仅仅在synth/emux目录下面有Emu8000/Tmu10k1 synth驱动。
pci目录
它和它的一些子目录文件负责PCI声卡和一些PCI BUS的上层card模块。
在pci目录下面保存着一些简单的驱动文件,而一些比较复杂的,同时包含多个程序文件的驱动会被放置在pci目录下面一个单独的子目录里面(如:emu10k1,ice1712)。
isa目录
它和它的一些子目录文件是处理ISA声卡的上层card模块。
arm,ppc,和sparc目录
这里放置一些和芯片架构相关的一些上层的card模块。
usb目录
这里包含一些USB-AUDIO驱动。在最新版本里面,已经把USB MIDI驱动也集成进USB-AUDIO驱动了。
pcmcia目录
PCMCIA卡,特别是PCCard驱动会放到这里。CardBus驱动将会放到pci目录里面,因为API函数和标准PCI卡上统一的。
oss目录
OSS/Lite源文件将被放置在linux2.6(或以后)版本的源码树中。(在ALSA驱动中,它是空的:)
第二章 PCI驱动的基本流程
概要
PCI声卡的最简单的流程如下:
1).定义一个PCI Id表(参考PCI Entries章节)
2).建立probe()回调函数
3).建立remove()回调函数
4).建立包含上面三个函数入口的pci_driver表
5).建立init()函数,调用pci_register_driver()注册上面的pci_drvier表
6).建立exit()函数,调用pci_unregister_driver()函数
代码示例
代码如下所示。一些部分目前还没有实现,将会在后续章节逐步实现。如:在snd_mychip_probe()函数的注释部分的号码。
Example2-1.PCI驱动示例的基本流程
#include <sound/driver.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>
/*模块参数(参考“Module Parameters”)*/
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static int enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
/*定义和chip相关的记录*/
struct mychip{
struct snd_card *card;
//剩余的其他实现将放到这里
//”PCI Resource Management”
};
/*chip-specific destrcutor
*(see “PCI Resource Management”
*/
static int snd_mychip_free(struct mychip *chip)
{
...//后续会实现
}
/*component-destructor
*(see “Management of Cards and Components”
*/
static int snd_mychip_dev_free(struct snd_device *device)
{
return snd_mychip_free(device->device_data);
}
/*chip-specific constructor
*(see “Management of Cards and Components”)
*/
static int __devinit snd_mychip_create(struct snd_card *card,
struct pci_dev *pci,struct mychip **rchip)
{
struct mychip *chip;
int err;
static struct snd_device_ops ops = {
.dev_freee = snd_mychip_dev_free,
};
*rchip = NULL;
//check PCI avilability here
//(see "PCI Resource Management")
/*allocate a chip-specific data with zero filled*/
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
if (chip == NULL)
return -ENOMEM;
chip->card = card;
//rest of initialization here;will be implemented
//later,see "PCI Resource Management"
....
if ((err = snd_device_new(card, SNDRV_DEV_LOWLEVEL,
chip, &ops)) < 0) {
snd_mychip_free(chip);
return err;
}
snd_card_set_dev(card, &pci->dev);
*rchip = chip;
return 0;
}
/*constructor --see "Constructor"sub - section*/
static int __devinit snd_mychip_probe(struct pci_dev *pci,
const struct pci_device_id *pci_id)
{
static int dev;
struct snd_card *card;
struct mychip *chip;
int err;
/*(1)*/
if (dev >= SNDRV_CARDS)
return -ENODEV;
if (!enable[dev]) {
dev++;
return -ENOENT;
}
/*(2)*/
card = snd_card_new(index[dev], id[dev], THIS_MODULE, 0);
if (card == NULL)
return -ENOMEM;
/*(3)*/
if ((err = snd_mychip_create(card, pci, &chip)) < 0){
snd_card_free(card);
return err;
}
/*(4)*/
strcpy(card->driver,"My Chip");
strcpy(card->shortname,"My Own Chip 123");
sprintf(card->longname,"%s at 0x%1x irq %i",
card->shortname, chip->ioport, chip->irq);
/*(5)*/
....//implemented later
/*(6)*/
if ((err = snd_card_register(card)) < 0) {
snd_card_free(card);
return err;
}
/*(7)*/
pci_set_drvdata(pci, card);
dev++;
return 0;
}
/*destructor --seee"Destructor" sub-section*/
static void __devexit snd_mychip_remove(struct pci_dev *pci)
{
snd_card_free(pci_get_drvdata(pci);
pci_set_drvdata(pci,NULL);
}
构造函数
PCI驱动的真正构造函数是回调函数probe。probe函数和其他一些被probe调用组件构造函数,那些组件构造函数被放入了__devinit前缀。你不能放__init前缀,因为PCI设备是热拔插设备。
在probe函数中,下面几个是比较常用的。
1)检查并增加设备索引(device index)。
static int dev;
...
if (dev >= SNDDRV_CARDS)
return -ENODEV;
if (!enable[dev]){
dev++;
return -ENOENT;
}
enable[dev]是一个module选项。
每当probe被调用的时候,都要检查device是否可用。假如不可用,简单的增加设备索引并返回。dev也会被相应增加一。(step 7)。
2)创建一个card实例
struct snd_card *card;
....
card = snd_card_new(index[dev], id[dev], THIS_MODULE, 0);
这个函数详细展开在“管理card和组件”一节。
3)建立一个组件
在这个部分,需要分配PCI资源。
struct mychip *chip;
....
if ((err = snd_mychip_create(card, pci, &chip)) < 0){
snd_card_free(card);
return err;
}
这个函数详细展开在“PCI资源管理”一节。
4)设定驱动ID和名字。
strcpy(card->driver, “My Chip”);
strcpy(card->shortname, “My Own Chip 123”);
sprintf(card->longname, “%s at 0x%lx irq %i”,
card->shortname, chip->ioport, chip->irq);
驱动的一些结构变量保存chip的ID字串。它会在alsa-lib配置的时候使用,所以要保证他的唯一和简单。甚至一些相同的驱动可以拥有不同的ID,可以区别每种chip类型的各种功能。
shortname域是一个更详细的名字。longname域将会在/proc/asound/cards中显示。
5)创建其他类似于mixex,MIDI的组件。
你已经定义了一些基本组件如PCM,mixer(e.g.AC97),MIDI(e.g.MPU-401),还有其他的接口。同时,假如你想要一些proc文件,也可以在这里定义。
6)注册card实例
if ((err = snd_card_register(card)) < 0){
snd_card_free(card);
return err;
}
这个函数详细展开在“管理card和组件”一节。
7)设定PCI驱动数据,然后返回零
pci_set_drvdata(pci,card);
dev++;
return 0;
如上,card记录将会被保存。在remove回调函数和power-management回调函数中会被用到。
析构函数
remove回调函数会释放card实例。ALSA中间层也会自动释放所有附在这个card上的组件。
典型应用如下:
static void __devexit snd_mychip_remove(struct pci_dev *pci)
{
snd_card_free(pci_get_drvdata(pci));
pci_set_drvdata(pci,NULL);
}
上面代码是假定card指针式放到PCI driver data里面的。
头文件
对于以上的代码,至少需要下面几个头文件。
#include <sound/driver.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>
最后一个是仅仅当源文件中定义了module选项的时候才需要。加入代码被分成多个文件,那些不需要module选项的文件就不需要。
除此之外,加入你需要中断处理,你需要<linux/interrupt.h>,需要i/o接口就要加入<asm/io.h>,如果需要mdelay(),udelay()函数就需要加入<linux/delay.h>等等。
类似于PCM或控制API的ALSA接口会放到类似于<sound/xxx.h>的头文件当中。它们必须被包含并且要放到<sound/core.h>的后面。
第三章 管理card和组件
card实例
对于每个声卡,都要分配一个card记录。
一个card记录相当于一个声卡的总部。它管理着声卡中的所有设备(组件),例如PCM,mixers,MIDI,音序器等等。同时,card记录还保持着卡的ID和name字符串,管理proc文件,控制电源管理状态和热拔插。Card的组件列表用来在合适的时候释放资源。
如上,为了创建一个card实例,需要调用snd_card_new().
Struct snd_card *card;
card = snd_card_new(index, id, module, extra_size);
这个函数需要4个参数,card-index号,id字符串,module指针(通常是THIS_MODULE),extra-data空间的大小。最后一个参数是用来分配chip-specific数据card->private_data的。注意这个数据是通过snd_card_new()进行分配。
组件
card被创建之后,你可以把组件(设备)附加到card实例上面。在ALSA驱动程序中,一个组件用一个snd_devie结构体表示。可以是PCM,控制接口或raw MIDI接口等等。每个组件都有个入口函数。
通过snd_device_new()函数来创建组件。
snd_device_new(card, SNDRV_DEV_XXX, chip, &ops);
它需要card指针,device_level(SNDRV_DEV_XXX),数据指针和回调函数(&ops)。device-level定义了组件类型和注册和卸载的顺序。对于大部分的组件来说, device_level已经定义好了。对于用户自定义的组件,你可以用SNDRV_DEV_LOWLEVEL.
这个函数自己并不分配数据空间。数据必须提前分配,同时把分配好的数据指针传递给这个函数作为参数。这个指针可以作为设备实例的标识符(上述代码的chip)。
每个ALSA预定义组件,如ac97或pcm都是通过在各自的构造函数中调用snd_device_new().在一些回调函数中定义了每个组件的析构函数。因此,你不需要关心这种组件的析构函数的调用。
假如你要创建一个自己的组件,你需要在dev_free中的回调函数ops中设定析构函数。
以便它可以通过snd_card_free()自动被释放。下面的例子就会显示chip-specific数据的实现。
Chip-Specific Data
chip-specific信息,如:i/o口,资源,或者中断号就会保持在 chip-specific记录里面。
Struct mychip{
....
};
通常来说,有两种方式来分配chip记录。
1.通过snd_card_new()分配。
如上面所述,你可以通过传递extra-data-length到snd_card_new()的第四个参数。
card = snd_card_new(index[dev], id[dev], THIS_MODULE, sizeof(struct mychip));
无论struct mychip是否是chip记录类型。
分配之后,可以通过如下方式引用:
struct mychip *chip = char->private_data;
通过这种方法,你不必分配两次。这个记录将会和card实例一起被释放。
2.分配一个extra device。
当你通过snd_card_new()(第四个参数要设定为NULL)分配之后,通过调用kzalloc().
Struct snd_card *card;
struct mychip *chip;
card = snd_card_new(index[dev], id[dev], THIS_MODULE, NULL);
....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
chip记录应该至少拥有一个snd_card指针的成员变量。
Struct mychip {
struct snd_card *card;
....
};
然后,设定chip的card为snd_card_new返回的card指针。
chip->card = card;
下一步,初始化各个成员变量,使用一个含有特殊的ops的low-level设备注册chip记录。
Static struct snd_device_ops ops = {
.dev_free = snd_mychip_dev_free;
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
snd_mychip_dev_free()是作为设备的析构函数会调用真正的析构函数调用。
static int snd_mychip_dev_free(struct snd_device *device)
{
return snd_mychip_free(device->private_data);
}
snd_mychip_free是真正的设备析构函数。
注册和释放
当所有的组件都被分配之后,就可以通过snd_card_register()来注册card实例了。对于设备文件的操作也会使能。那是因为,在调用snd_card_register()调用之前,组件是不能被外界调用的。假如注册失败,必须要调用snd_card_free()来释放card。
对于释放card,你可以简单的通过snd_card_free().如前所述,所有的组件都可以通过它来自动释放。
要特别注意,这些析构函数(包括snd_mychip_dev_free和snd_mychip_free)不能被定义加入__devexit前缀,因为它们也有可能被构造函数调用(当构造失败的时候调用)。
对于一个运行热拔插的设备,你可以用snd_card_free_when_closed。它将推迟析构直到所有的设备都关闭。
第四章 PCI资源管理
代码示例
本节我们会完成一个chip-specific的构造函数,析构函数和PCI entries。先来看代码。
1.Example4-1.PCI资源管理示例
struct mychip{
struct snd_card *card;
struct pci_dev *pci;
unsigned long port;
int irq;
};
static int snd_mychip_free(struct mychip *chip)
{
/*disable hardware here if any*/
....//这篇文档没有实现
/*release the irq*/
if (chip->irq >= 0)
free_irq(chip->irq,chip);
/*释放io和memory*/
pci_release_regions(chip->pci);
/*disable the PCI entry*/
pci_disable_device(chip->pci);
/*release the data*/
kfree(chip);
return 0;
}
/*chip-specific constructor*/
static int __devinit snd_mychip_create(struct snd_card *card,
struct pci_dev *pci,
struct mychip **rchip)
{
struct mychip *chip;
int err;
static struct snd_device_ops ops ={
.dev_free = snd_mychip_dev_free,
};
*rchip = NULL;
/*initialize the PCI entry*/
if ((err = pci_enable_device(pci)) < 0)
return err;
/*check PCI availability (28bit DMA)*/
if (pci_set_dma_mask(pci, DMA_28BIT_MASK) < 0 ||
pci_set_consistent_dma_mask(pci,DMA_28BIT_MASK) < 0 ){
printk(KERN_ERR “Error to set 28bit mask DMA/n”);
pci_disable_device(pci);
return -ENXIO;
}
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
if (chip == NULL){
pci_disable_device(pci);
return -ENOMEM;
}
/*initialize the stuff*/
chip->card = card;
chip->pci = pci;
chip->irq = -1;
/*(1)PCI 资源分配*/
if ((err = pci_request_regions(pci, “My Chip”)) < 0){
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci,0);
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_SHARED, “My Chip”,chip){
printk(KERN_ERR “Cannot grab irq %d/n”,pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
/*(2)chip hardware的初始化*/
....//本文未实现
if ((err = snd_device_new(card, SNDRV_DEV_LOWLEVEL,
chip, &ops)) < 0){
snd_mychip_free(chip);
return err;
}
snd_card_set_dev(card,&pci->dev);
*rchip = chip;
return 0;
}
/*PCI Ids*/
static struct pci_device_id snd_mychip_ids[] = {
{PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID,0,0,0,},
....
{0,}
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);
/*pci_driver定义*/
static struct pci_drvier driver={
.name = “My Own Chip”,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = __devexit_p(snd_mychip_remove),
};
/*module的初始化*/
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
/*clean up the module*/
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_drvier(&drvier);
}
module_init(alsa_card_mychip_init);
module_exit(alsa_card_mychip_eixt);
EXPORT_NO_SYMBOLS /*为了和老版本的内核兼容*/
一些必须做的事情
一般在probe()函数中分配PCI资源,通常采用一个xxx_create()函数来完成上述功能。 在PCI设备驱动中,在分配资源之前先要调用pci_enable_device().同样,你也要设定合适的PCI DMA mask限制i/o接口的范围。在一些情况下,你也需要设定pci_set_master().
资源分配
利用标准的内核函数来分配I/O和中断。不像ALSA ver0.5.x那样没有什么帮助。同时那些资源必须在析构函数中被释放(如下)。在ALSA 0.9.x,你不需要像0.5.x那样还要为PCI分配DMA。
现在假定PCI设备拥有8直接的I/O口和中断,mychip的结构体可以如下所示:
struct mychip{
struct snd_card *card;
unsigned long port;
int irq;
}
对于一个I/O端口(或者也有内存区域),你需要得到可以进行标准的资源管理的资源的指针。对于中断,你必须保存它的中断号。但是你必须实际分配之前先把初始化中断号为
-1,因为中断号为0也是被允许的。端口地址和它的资源指针可以通过kzalloc()来分配初始化为null,所以你不用非要重新设定他们。
I/O端口的分配可以采用如下方式:
if ((err = pci_request_region(pci, “My Chip”)) < 0){
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci, 0);
它将会保留给定PCI设备的8字节的I/O端口区域。返回值chip->res_port在request_region函数中通过kmalloc分配。这个分配的指针必须通过kfree()函数进行释放,但是这个部分有些问题,具体将会在下面详细分析。
分配一个中断源如下所示:
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_DISABLED | IRQF_SHARED, “My Chip”,chip)){
printk(KERN_ERR “cannot grab irq %d/n”, pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
snd_mychip_interrupt()就是下面定义的中断处理函数。注意request_irq()成功的时候,返回值要保持在chip->irq里面。
在PCI bus上,中断是共享的。因此,申请中断的函数request_irq要加入IRQF_SHARED标志位。
request_irq的最后一个参数是被传递给中断处理函数的数据指针。通常来说,chip-specific记录会用到它,你也可以按你喜欢的方式用它。
我不想在这时候详细解释中断向量函数,但是至少这里出现的会解释一下。中断向量如下所示:
static irqreturn_t snd_mychip_interrup(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
return IRQ_HANDLED;
}
现在,让我们为上述的资源写一个相应的析构函数。析构函数是非常简单的:关闭硬件(假如它被激活)同时释放它的资源。到目前为止,我们没有硬件,所以关闭硬件的部分就不写了。
为了释放资源,“check-and-release”(检查然后释放)的方式是比较安全的。对于中断,采用如下的方式:
if (chip->irq >= 0)
free_irq(chip->irq,chip);
因为irq号是从0开始的,所以你必须初始化chip->irq为一个负数(例如-1),所以你可以按上面的方式检查irq的有效性。
通过pci_request_region()或pci_request_regions()申请I/O端口和内存空间,相应的,通过pci_release_region()或pci_release_regions来释放。
pci_release_regions(chip->pci);
通常可以通过request_region()或request_mem_region()来申请,也可以通过release_region()来释放。假定你把request_region返回的resource pointer保存在chip->res_port,释放的程序如下所示:
release_and_free_resource(chip->res_port);
在所有都结束的时候不要忘记了调用pci_disable_device().最后释放chip-specific记录。
kfree(chip);
再提醒一下,不能在析构函数前面放__devexit。
我们在上面没有实现关闭硬件的功能部分。假如你需要做这些,请注意析构函数可能会在chip的初始化完成之前被调用。最好设定一个标志位确定是否硬件已经初始化,来决定是否略过这部分。
如果chip-data放置在在含有SNDRV_DEV_LOWLEVEL标志的snd_device_new()函数申请的设备中,它的析构函数将会最后被调用。那是因为,它要确认像PCM和一些控制组件已经被释放。你不必显式调用停止PCM的函数,只要在low-level中停止这些硬件。
mamory-mapped的区域的管理和i/o端口管理一样。需要如下3个结构变量:
struct mychip{
....
unsigned long iobase_phys;
void __iomem *iobase_virt;
};
通过如下方式来申请:
if ((err = pci_request_regions(pci, “My Chip”)) < 0){
kfree(chip);
return err;
}
chip->iobase_phys = pci_resource_start(pci, 0);
chip->iobase_virt = ioremap_nocache(chip->iobase_phys,
pci_resource_len(pci, 0));
对应的析构函数如下:
static int snd_mychip_free(struct mychip *chip)
{
....
if (chip->iobase_virt)
iounmap(chip->iobase_virt);
....
pci_release_regions(chip->pci);
....
}
注册设备结构体
在一些地方,典型的是在调用snd_device_new(),假如你想通过udev或者ALSA提供的一些老版本内核的兼容的宏来控制设备,你需要注册chip的设备结构体。很简单如下所示:
snd_card_set_dev(card,&pci->dev);
所以它保存了card的PCI设备指针。它将会在后续设备注册的时候被ALSA内核功能调用。
假如是非PCI设备,就需要传递一个合适的设备结构指针。(假如是不可以热拔插的ISA设备,你就不需要这么做了。)
PCI Entries
到目前为止已经做得非常好了,下面让我们完成PCI的剩余的一些工作。首先,我们需要一个这个芯片组的pci_device_id表。它是一个含有PCI制造商和设备ID的表,还有一些mask。
例如:
static struct pci_device_id snd_mychip_ids[] = {
{PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID,0,0,0,},
....
{0,}
};
MODULE_DEVICE_TABLE(pci,snd_mychip_ids);
pci_device_id结构体的第一个和第二个结构变量是制造商和设备的ID。假如你没有关于设备的特别选择,你可以采用上述的。pci_device_id结构体的最后一个结构变量是一个私有变量。你可以放一些可以区别其他的值,如:可以区分每个设备ID的不同操作类型。这些例子出现在intel的驱动里面。最后一个table元素是代表结束符。必须把所有成员设定为0.
然后,我们来准备pci_driver记录:
static struct pci_drvier driver = {
.name = “My Own Chip”,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = __devexit_p(snd_mychip_remove),
};
probe和remove函数在以前的章节已经介绍过。remove应该用一个__devexit_p()宏来定义。所以,它不是为那些固定的或不支持热拔插的设备定义的。name结构变量是标识设备的名字。注意你不能用“/”字符。
最后,module入口如下:
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_driver(&driver);
}
module_init(alsa_card_mychip_init);
module_exit(alsa_card_mychip_exit);
注意module入口函数被标识为__init和__exit前缀,而不是__devinit或者__devexit.
哦,忘记了一件事,假如你没有export标号,如果是2.2或2.4内核你必须显式声明r如下:(当然,2.6内核已经不需要了。)
EXPORT_NO_SYMBOLS;
就这些了。
第五章 PCM接口
概述
PCM中间层是ALSA中作用非常大的。它是唯一需要在每个驱动中都需要实现的low-level的硬件接口。
为了访问PCM层,你需要包含<sound/pcm.h>。除此之外,如果你要操作hw_param相关的函数,还需要包含<sound/pcm_param.h>。
每个card设备可以最多拥有4个pcm实例。一个pcm实例对应予一个pcm设备文件。组件的号码限制主要是和Linux的可用的设备号多少有关。假如允许64bit的设备号,我们可以拥有更多的pcm实例。
一个pcm实例包含pcm 放音和录音流,而每个pcm流由一个或多个pcm子流组成。一些声卡支持多重播放的功能。例如:emu10k1就包含一个32个立体声子流的PCM放音设备。事实上,每次被打开的时候,一个可用的子流会自动的被选中和打开。同时,当一个子流已经存在,并且已经被打开,当再次被打开的时候,会被阻塞,或者根据打开文件的模式不同返回一个EAGAIND的错误信息。你也不需要知道你的驱动细节部分,PCM中间层会处理那些工作。
代码示例
下面的代码没有包含硬件接口程序,主要显示一个如何构建一个PCM接口的骨架。
Example5-1.PCM示例代码
#include <sound/pcm.h>
....
/*硬件定义*/
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FORMAT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/*硬件定义*/
static struct snd_pcm_hardware snd_mychip_capture_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FORMAT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/*播放open函数*/
static int snd_mychip_playback_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_playback_hw;
/*其他硬件初始化的部分在这里完成*/
return 0;
}
/*播放close函数*/
static int snd_mychip_playback_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
//硬件相关代码写在这
return 0;
}
/*录音open函数*/
static int snd_mychip_capture_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_capture_hw;
/*其他硬件初始化的部分在这里完成*/
return 0;
}
/*录音close函数*/
static int snd_mychip_capture_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
//硬件相关代码写在这
return 0;
}
/*hw_params函数*/
static int snd_mychip_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
return snd_pcm_lib_malloc_pages(substream,
params_buffer_bytes(hw_params));
}
/*hw_free函数*/
static int snd_mychip_pcm_hw_free(struct snd_pcm_substream *substream)
{
return snd_pcm_lib_free_pages(substream);
}
/*prepare函数*/
static int snd_mychip_pcm_prepare(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/*在此做设定一些硬件配置
*例如....
*/
mychip_set_sample_format(chip, runtime->format);
mychip_set_sample_rate(chip, runtime->rate);
mychip_set_channels(chip, runtime->channels);
mychip_set_dma_setup(chip, runtime->dma_addr,
chip->buffer_size,
chip->period_size);
return 0;
}
/*trigger函数*/
static int snd_mychip_pcm_trigger(struct snd_pcm_substream *substream,
int cmd)
{
switch(cmd){
case SNDRV_PCM_TRIGGER_START:
//启动一个PCM引擎
break;
case SNDRV_PCM_TRIGGER_STOP:
//停止一个PCM引擎
break;
default:
return -EINVAL;
}
}
/*pointer函数*/
static snd_pcm_uframes_t
snd_mychip_pcm_pointer(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream)
unsigned int current_ptr;
/*得到当前缓冲区的硬件位置*/
current_ptr = mychip_get_hw_pointer(chip);
return current_ptr;
}
/*操作函数*/
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_playback_open,
.close = snd_mychip_playback_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/*操作函数*/
static struct snd_pcm_ops snd_mychip_capture_ops = {
.open = snd_mychip_capture_open,
.close = snd_mychip_capture_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/*关于录音的定义在这里省略....*/
/*创建一个pcm设备*/
static int __devinit snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
if ((err = snd_pcm_new(chip->card, “My Chip”, 0 , 1, 1,&pcm) < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name,”My Chip”);
chip->pcm = pcm;
/*设定操作函数*/
snd_pcm_set_ops(pcm,SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm,SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
/*预分配缓冲区
*注意:可能会失败
*/
snd_pcm_lib_preallocate_pages_for_all(pcm,SNDRV_DMA_TYPE_DEV,
snd_dma_pci_data(chip->pci),64*1024,64*1024);
return 0;
}
构造函数
通过snd_pcm_new()来分配一个pcm实例。更好的方式是为pcm创建一个构造函数。
static int __devinit snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
if ((err = snd_pcm_new(chip->card, “My Chip”, 0 , 1, 1,&pcm) < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name,”My Chip”);
chip->pcm = pcm;
....
return 0;
}
snd_pcm_new()需要4个参数。第一个是card指针,第二个是标识符字符串,第三个是PCM设备索引。假如你创建了超过一个pcm实例,通过设备索引参数来区分pcm设备。例如:索引为1代表是第二个PCM设备。
第四个和第五个参数是表示播放和录音的子流的数目。上面的示例都是为1。当没有播放或录音可以用的时候,可以设定对应的参数为0。
假如声卡支持多个播放和录音子流,你可以分配更多的号码,但是它们必须可以在open()和close()函数中被正确处理。你可以通过snd_pcm_substream的成员变量number来知道那用到的是那个子流。如:
struct snd_pcm_substream *substream;
int index = substream->number;
pcm创建之后,必须设定pcm流的操作函数。
snd_pcm_set_ops(pcm,SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm,SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
操作函数类似如下:
/*操作函数*/
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_playback_open,
.close = snd_mychip_playback_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
每一个回调函数都会在“操作函数”一节中详细介绍。
设定完操作函数之后,大部分情况要预分配缓冲区。一般情况采用如下方式分配:
snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
snd_dma_pci_data(chip->pci),64*1024,64*1024);
默认它会分配64KB的缓冲。关于缓冲区的管理会在“缓冲区和内存管理”一章详细描述。
除此之外,你还可以为这个pcm设定一些附加的信息保存在pcm->info_flags变量中。一些可用的变量定义例如SNDRV_PCM_INFO_XXX都在<sound/asound.h>中,这些主要是为了硬件定义的(稍后会详细描述)。假如你的声卡仅仅支持半双工,你要指定如下:
pcm->info_flags = SNDDRV_PCM_INFO_HALF_DUPLEX;
到了析构部分?
一个pcm实例不是总是要求析构的。既然pcm中间层会自动的把pcm设备是否,你就不用特别的在调用析构函数了。
析构函数在一种情况下是必须的,就是你在内部创建了一个特殊的的记录,需要自己去释放他们。这种情况下,你可以把pcm->private_free指向你的析构函数。
Example5-2.含有一个析构函数的PCM实例
static void mychip_pcm_free(struct snd_pcm *pcm)
{
struct mychip *chip = snd_pcm_chip(pcm);
/*释放你自己的数据*/
kfree(chip->my_private_pcm_data);
//做其他事情
}
static int __devinit snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
/*分配你自己的数据*/
chip->myprivate_pcm_data = kmalloc(..);
/*设定析构函数*/
pcm->private_data = chip;
pcm->private_free = mychip_pcm_free;
....
}
PCM信息运行时指针
当打开一个一个PCM子流的时候,PCM运行时实例就会分配给这个子流。这个指针可以通过substream->runtime获得。运行时指针拥有多种信息:hw_params和sw_params的配置的拷贝,缓冲区指针,mmap记录,自旋锁等等。几乎你想控制PCM的所有信息都可以在这里得到。
Struct _snd_pcm_runtime {
/*状态*/
struct snd_pcm_substream *trigger_master;
snd_timestamp_t trigger_tstamp;/*触发时间戳*/、
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base /*缓冲区复位时的位置*/
snd_pcm_uframes_t hw_ptr_interrupt;/*中断时的位置*/
/*硬件参数*/
snd_pcm_access_t access; /*存取模式*/
snd_pcm_format_t format; /*SNDRV_PCM_FORMAT_* */
snd_pcm_subformat_t subformat; /*子格式*/
unsigned int rate; /*rate in HZ*/
unsigned int channels; /*通道*/
snd_pcm_uframe_t period_size; /*周期大小*/
unsigned int periods /*周期数*/
snd_pcm_uframes_t buffer_size; /*缓冲区大小*/
unsigned int tick_time; /*tick time滴答时间*/
snd_pcm_uframes_t min_align; /*格式对应的最小对齐*/
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
unsigned int info;
unsigned int rate_num;
unsigned int rate_den;
/*软件参数*/
struct timespec tstamp_mode; /*mmap时间戳被更新*/
unsigned int sleep_min; /*睡眠的最小节拍数*/
snd_pcm_uframes_t xfer_align; /*xfer的大小需要是成倍数的*/
snd_pcm_uframes_t start_threshold;
snd_pcm_uframes_t stop_threshold;
snd_pcm_uframes_t silence_threshold;/*silence填充阀值*/
snd_pcm_uframes_t silence_size; /*silence填充大小*/
snd_pcm_uframes_t boundary;
snd_pcm_uframes_t silenced_start;
snd_pcm_uframes_t silenced_size;
snd_pcm_sync_id_t sync; /*硬件同步ID*/
/*mmap*/
volatile struct snd_pcm_mmap_status *status;
volatile struct snd_pcm_mmap_control *control;
atomic_t mmap_count;
/*锁/调度*/
spinlock_t lock;
wait_queue_head_t sleep;
struct timer_list tick_timer;
struct fasync_struct *fasync;
/*私有段*/
void *private_data;
void (*private_free)(struct snd_pcm_runtime *runtime);
/*硬件描述*/
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/*中断的回调函数*/
void (*transfer_ack_begin)(struct snd_pcm_substream *substream);
void (*transfer_ack_end)(struct snd_pcm_substream *substream);
/*定时器*/
unsigned int timer_resolution; /*timer resolution*/
/*DMA*/
unsigned char *dma_area;
dma_addr_t dma_addr; /*总线物理地址*/
size_t dma_bytes; /*DMA区域大小*/
struct snd_dma_buffer *dma_buffer_p; /*分配的缓冲区*/
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
struct snd_pcm_oss_runtime oss;
#endif
};
snd_pcm_runtime 对于大部分的驱动程序操作集的函数来说是只读的。仅仅PCM中间层可以改变/更新这些信息。但是硬件描述,中断响应,DMA缓冲区信息和私有数据是例外的。此外,假如你采用标准的内存分配函数snd_pcm_lib_malloc_pages(),就不再需要自己设定DMA缓冲区信息了。
下面几章,会对上面记录的现实进行解释。
硬件描述
硬件描述(struct snd_pcm_hardware)包含了基本硬件配置的定义。如前面所述,你需要在open的时候对它们进行定义。注意runtime实例拥有这个描述符的拷贝而不是已经存在的描述符的指针。换句话说,在open函数中,你可以根据需要修改描述符的拷贝。例如,假如在一些声卡上最大的通道数是1,你仍然可以使用相同的硬件描述符,同时在后面你可以改变最大通道数。
Struct snd_pcm_runtime *runtime = substream->runtime;
....
runtime->hw = snd_mychip_playback_hw; /*通用定义*/
if (chip->model == VERY_OLD_ONE)
runtime->hw.channels_max = 1;
典型的硬件描述如下:
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FORMAT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
info字段包含pcm的类型和能力。位标志在<sound/asound.h>中定义,如:SNDRV_PCM_INFO_XXX。这里,你必须定义mmap是否支持和支持哪种interleaved格式。当支持mmap的时候,应当设定SNDRV_PCM_INFO_MMAP。当硬件支持interleaved或no-interleaved格式的时候,要设定SNDRV_PCM_INFO_INTERLEAVED或SNDRV_PCM_INFO_NONINTERLEAVED标志位。假如两者都支持,你也可以都设定。
如上面的例子,MMAP_VALID和BLOCK_TRANSFER都是针对OSS mmap模式,通常情况它们都要设定。当然,MMAP_VALID仅仅当mmap真正被支持的时候才会被设定。
其他一些标志位是SNDRV_PCM_INFO_PAUSE和SNDRV_PCM_INFO_RESUME。SNDRV_PCM_INFO_PAUSE标志位意思是pcm支持“暂停”操作,SNDRV_PCM_INFO_RESUME表示是pcm支持“挂起/恢复”操作。假如PAUSE标志位被设定,trigger函数就必须执行一个对应的(暂停按下/释放)命令。就算没有RESUME标志位,也可以被定义挂起/恢复触发命令。
更详细的部分请参考“电源管理”一章。
当PCM子系统能被同步(如:播放流和录音流的开始/结束的同步)的时候,你可以设定SNDRV_PCM_INFO_SYNC_START标志位。在这种情况下,你必须在trigger函数中检查PCM子流链。下面的章节会想笑介绍这个部分。
formats字段包含了支持格式的标志位(SNDRV_PCM_FMTBIT_XXX)。假如硬件支持超过一个的格式,需要对位标志位进行“或”运算。上面的例子就是支持16bit有符号的小端格式。
rates字段包含了支持的采样率(SNDRV_PCM_RATE_XXX)。当声卡支持多种采样率的时候,应该附加一个CONTINUOUS标志。已经预先定义的典型的采样率,假如你的声卡支持不常用的采样率,你需要加入一个KNOT标志,同时手动的对硬件进行控制(稍后解释)。
rate_min和rate_max定义了最小和最大的采样率。应该和采样率相对应。
channel_min和channel_max定义了最大和最小的通道,以前可能你已看到。
buffer_bytes_max定义了以字节为单位的最大的缓冲区大小。这里没有buffer_bytes_min字段,因为它可以通过最小的period大小和最小的period数量计算得出。同时,period_bytes_min和定义的最小和最大的period。periods_max和periods_min定义了最大和最小的periods。
period信息和OSS中的fragment相对应。period定义了PCM中断产生的周期。这个周期非常依赖硬件。一般来说,一个更短的周期会提供更多的中断和更多的控制。如在录音中,周期大小定义了输入延迟,另外,整个缓存区大小也定义了播放的输出延迟。
字段fifo_size。这个主要是和硬件的FIFO大小有关,但是目前驱动中或alsa-lib中都没有使用。所以你可以忽略这个字段。
PCM配置
OK,让我们再次回到PCM运行时记录。最经常涉及的运行时实例中的记录就是PCM配置了。PCM可以让应用程序通过alsa-lib发送hw_params来配置。有很多字段都是从hw_params和sw_params结构中拷贝过来的。例如:format保持了应用程序选择的格式类型,这个字段包含了enum值SNDRV_PCM_FORMAT_XXX。
其中要注意的一个就是,配置的buffer和period大小被放在运行时记录的“frame”中。在ALSA里,1frame=channel*samples-size。为了在帧和字节之间转换,你可以用下面的函数,frames_to_bytes()和bytes_to_frames()。
period_bytes = frames_to_bytes(runtime,runtime->period_size);
同样,许多的软件参数(sw_params)也存放在frames字段里面。请检查这个字段的类型。snd_pcm_uframes_t是作为表示frames的无符号整数,而snd_pcm_sframes_t是作为表示frames的有符号整数。
DMA缓冲区信息
DMA缓冲区通过下面4个字段定义,dma_area,dma_addr,dma_bytes,dma_private。其中dma_area是缓冲区的指针(逻辑地址)。可以通过memcopy来向这个指针来操作数据。dma_addr是缓冲区的物理地址。这个字段仅仅当缓冲区是线性缓存的时候才要特别说明。dma_bytes是缓冲区的大小。dma_private是被ALSA的DMA管理用到的。
如果采用ALSA的标准内存分配函数snd_pcm_lib_mallock_pages()分配内存,那些字段会被ALSA的中间层设定,你不能自己改变他们,可以读取而不能写入。而如果你想自己分配内存,你就需要在hw_params回调里面自己管理它们。当内存被mmap之后,你至少要设定dma_bytes和dma_area。但是如果你的驱动不支持mmap,这些字段就不必一定设定.dma_addr也不是强制的,你也可以根据灵活来用dma_private。
运行状态
可以通过runtime->status来获得运行状态。它是一个指向snd_pcm_mmap_status记录的指针。例如,可以通过runtime->status->hw_ptr来得到当前DMA硬件指针。
可以通过runtime->control来查看DMA程序的指针,它是指向snd_pcm_mmap_control记录。但是,不推荐直接存取这些数据。
私有数据
可以为子流分配一个记录,让它保存在runtime->private_data里面。通常可以在open函数中做。不要和pcm->private_data混搅了,pcm->private_data主要是在创建PCM的时候指向chip实例,而runtime->private_data是在PCM open的时候指向一个动态数据。
Struct int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct my_pcm_data *data;
data = kmalloc(sizeof(*data),GFP_KERNEL);
substream->runtime->private_data = data;
....
}
上述分配的对象要在close函数中释放。
中断函数
transfer_ack_begin()和transfer_ack_end()将会在snd_pcm_period_elapsed()的开始和结束。
操作函数
现在让我来详细介绍每个pcm的操作函数吧(ops)。通常每个回调函数成功的话返回0,出错的话返回一个带错误码的负值,如:-EINVAL。
每个函数至少要有一个snd_pcm_substream指针变量。主要是为了从给定的子流实例中得到chip记录,你可以采用下面的宏。
Int xxx(){
struct mychip *chip = snd_pcm_substream_chip(substream);
....
}
open函数
static int snd_xxx_open(struct snd_pcm_substream *substream);
当打开一个pcm子流的时候调用。
在这里,你至少要初始化runtime->hw记录。典型应用如下:
static int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_playback_hw;
return 0;
}
其中snd_mychip_playback_hw是预先定义的硬件描述。
close函数
static int snd_xxx_close(struct snd_pcm_substream *substream)
显然这是在pcm子流被关闭的时候调用。
所有在open的时候被分配的pcm子流的私有的实例都应该在这里被释放。
Static int snd_xxx_close(struct snd_pcm_substream *substream)
{
...
kfree(substream->runtime->private_data);
...
}
ioctl函数
这个函数主要是完成一些pcm ioctl的特殊功能。但是通常你可以采用通用的ioctl函数snd_pcm_lib_ioctl。
hw_params函数
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_substream *hw_params);
这个函数和hw_free函数仅仅在ALSA0.9.X版本出现。
当pcm子流中已经定义了缓冲区大小,period大小,格式等的时候,应用程序可以通过这个函数来设定硬件参数。
很多的硬件设定都要在这里完成,包括分配内存。
设定的参数可以通过params_xxx()宏得到。对于分配内存,可以采用下面一个有用的函数,
snd_pcm_lib_malloc_pages(substream,params_buffer_bytes(hw_params));
snd_pcm_lib_malloc_pages()仅仅当DMA缓冲区已经被预分配之后才可以用。参考“缓存区类型”一章获得更详细的细节。
注意这个和prepare函数会在初始化当中多次被调用。例如,OSSemulation可能在每次通过ioctl改变的时候都要调用这些函数。
因而,千万不要对一个相同的内存分配多次,可能会导致内存的漏洞!而上面的几个函数是可以被多次调用的,如果它已经被分配了,它会先自动释放之前的内存。
另外一个需要注意的是,这些函数都不是原子的(可以被调到)。这个是非常重要的,因为trigger函数是原子的(不可被调度)。因此,mutex和其他一些和调度相关的功能函数在trigger里面都不需要了。具体参看“原子操作”一节。
hw_free函数
static int snd_xxx_hw_free(struct snd_pcm_substream *substream);
这个函数可以是否通过hw_params分配的资源。例如:通过如下函数释放那些通过snd_pcm_lib_malloc_pages()申请的缓存。
snd_pcm_lib_free_pages(substream)
这个函数要在close调用之前被调用。同时,它也可以被多次调用。它也会知道资源是否已经被分配。
Prepare函数
static int snd_xxx_prepare(struct snd_pcm_substream *substream);
当pcm“准备好了”的时候调用这个函数。可以在这里设定格式类型,采样率等等。和hw_params不同的是,每次调用snd_pcm_prepare()的时候都要调用prepare函数。
注意最近的版本prepare变成了非原子操作的了。这个函数中,你要做一些调度安全性策略。
在下面的函数中,你会涉及到runtime记录的值(substream->runtime)。例如:得到当前的采样率,格式或声道,可以分别存取runtime->rate,runtime->format,runtime->channels。分配的内存的地址放到runtime->dma_area中,内存和period大小分别保存在runtime->buffer_size和runtime->period_size中。
要注意在每次设定的时候都有可能多次调用这些函数。
trigger函数
static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);
当pcm开始,停止或暂停的时候都会调用这个函数。
具体执行那个操作主要是根据第二个参数,在<sound/pcm.h>中声明了SNDRV_PCM_TRIGGER_XXX。最少START和STOP的命令必须定义的。
switch(cmd){
case SNDRV_PCM_TRIGGER_START:
//启动PCM引擎
break;
case SNDRV_PCM_TRIGGER_STOP:
//停止PCM引擎
break;
default:
break;
}
当pcm支持暂停操作(在hareware表里面有这个),也必须处理PAUSE_PAUSE和PAUSE_RELEASE命令。前者是暂停命令,后者是重新播放命令。
假如pcm支持挂起/恢复操作,不管是全部或部分的挂起/恢复支持,都要处理SUSPEND和RESUME命令。这些命令主要是在电源状态改变的时候需要,通常它们和STOP,START命令放到一起。具体参看“电源管理”一章。
如前面提到的,这个操作上原子的。不要在调用这些函数的时候进入睡眠。而trigger函数要尽量小,甚至仅仅触发DMA。另外的工作如初始化hw_params和prepare应该在之前做好。
pointer函数
static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream);
PCM中间层通过调用这个函数来获得缓冲区中的硬件位置。返回值需要以为frames为单位(在ALSA0.5.X是以字节为单位的),范围从0到buffer_size-1。
一般情况下,在中断程序中,调用snd_pcm_period_elapsed()的时候,在pcm中间层在在更新buffer的程序中调用它。然后,pcm中间层会更新指针位置和计算可用的空间,然后唤醒那些等待的线程。
这个函数也是原子的。
Copy和silence函数
这些函数不是必须的,同时在大部分的情况下是被忽略的。这些函数主要应用在硬件内存不在正常的内存空间的时候。一些声卡有一些没有被影射的自己的内存。在这种情况下,你必须把内存传到硬件的内存空间去。或者,缓冲区在物理内存和虚拟内存中都是不连续的时候就需要它们了。
假如定义了copy和silence,就可以做copy和set-silence的操作了。更详细的描述请参考“缓冲区和内存管理”一章。
Ack函数
这个函数也不是必须的。当在读写操作的时候更新appl_ptr的时候会调用它。一些类似于emu10k1-fx和cs46xx的驱动程序会为了内部缓存来跟踪当前的appl_ptr,这个函数仅仅对于这个情况才会被用到。
这个函数也是原子的。
page函数
这个函数也不是必须的。这个函数主要对那些不连续的缓存区。mmap会调用这个函数得到内存页的地址。后续章节“缓冲区和内存管理”会有一些例子介绍。
中断处理
下面的pcm工作就是PCM中断处理了。声卡驱动中的PCM中断处理的作用主要是更新缓存的位置,然后在缓冲位置超过预先定义的period大小的时候通知PCM中间层。可以通过调用snd_pcm_period_elapsed()来通知。
声卡有如下几种产生中断。
period(周期)中断
这是一个很常见的类型:硬件会产生周期中断。每次中断都会调用snd_pcm_period_elapsed()。
snd_pcm_period_elapsed()的参数是substream的指针。因为,需要从chip实例中得到substream的指针。例如:在chip记录中定义一个substream字段来保持当前运行的substream指针,在open函数中要设定这个字段而在close函数中要复位这个字段。
假如在中断处理函数中获得了一个自旋锁,如果其他pcm也会调用这个锁,那你必须要在调用snd_pcm_period_elapsed()之前释放这个锁。
典型代码如下:
Example5-3.中断函数处理#1
struct irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)){
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(chip->substream);
spin_lock(&chip->lock);
//如果需要的话,可以响应中断
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
高频率时钟中断
当硬件不再产生一个period(周期)中断的时候,就需要一个固定周期的timer中断了(例如 es1968,ymfpci驱动)。这时候,在每次中断都要检查当前硬件位置,同时计算已经累积的采样的长度。当长度超过period长度时候,需要调用 snd_pcm_period_elapsed()同时复位计数值。
典型代码如下:
Example5-4.中断函数处理#2
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)){
unsigned int last_ptr, size;
/*得到当前的硬件指针(帧为单位)*/
last_ptr = get_hw_ptr(chip);
/*计算自从上次更新之后又处理的帧*/
if (last_ptr < chip->last_ptr){
size = runtime->buffer_size + last_ptr - chip->last_ptr
}else{
size = last_ptr – chip->chip->last_ptr;
}
//保持上次更新的位置
chip->last_ptr = last_ptr;
/*累加size计数器*/
chip->size += size;
/*超过period的边界?*/
if (chip->size >= runtime->period_size){
/*重置size计数器*/
chip->size %= runtime->period_size;
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(substream);
spin_lock(&chip->lock);
}
//需要的话,要相应中断
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
在调用snd_pcm_period_elapsed()的时候
就算超过一个period的时间已经过去,你也不需要多次调用snd_pcm_period_elapsed(),因为pcm层会自己检查当前的硬件指针和上次和更新的状态。
原子操作
在内核编程的时候,一个非常重要(又很难dubug)的问题就是竞争条件。Linux内核中,一般是通过自旋锁和信号量来解决的。通常来说,假如竞争发生在中断函数中,中断函数要具有原子性,你必须采用自旋锁来包含临界资源。假如不是发生在中断部分,同时比较耗时,可以采用信号量。
如我们看到的,pcm的操作函数一些是原子的而一些不是。例如:hw_params函数不是原子的,而trigger函数是原子的。这意味着,后者调用的时候,PCM中间层已经拥有了锁。
在这些函数中申请的自旋锁和信号量要做个计算。
在这些原子的函数中,不能那些可能调用任务切换和进入睡眠的函数。其中信号量和互斥体可能会进入睡眠,因此,在原子操作的函数中(如:trigger函数)不能调用它们。如果在这种函数中调用delay,可以用udelay(),或mdelay()。
约束
假如你的声卡支持不常用的采样率,或仅仅支持固定的采样率,就需要设定一个约束条件。
例如:为了把采样率限制在一些支持的几种之中,就需要用到函数snd_pcm_hw_constraint_list()。需要在open函数中调用它。
Example5-5.硬件约束示例
static unsigned int rates[] ={4000,10000,22050,44100};
static unsigned snd_pcm_hw_constraint_list constraints_rates = {
.count = ARRAY_SIZE(rates),
.list = rates,
.mask = 0,
};
static int snd_mychip_pcm_open(struct snd_pcm_substream *substream)
{
int err;
....
err = snd_pcm_hw_constraint_list(substream->runtime,0,
SNDRV_PCM_HW_PARAM_RATE,
&constraints_rates);
if (err < 0)
return err;
....
}
有多种不同的约束。请参考sound/pcm.h中的完整的列表。甚至可以定义自己的约束条件。例如,假如my_chip可以管理一个单通道的子流,格式是S16_LE,另外,它还支持snd_pcm_hareware中设定的格式(或其他constraint_list)。可以设定一个:
Example5-6.为通道设定一个硬件规则
static int hw_rule_format_by_channels(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_params_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params,SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_mask fmt;
snd_mask_any(&fmt); /*初始化结构体*/
if (c->min < 2){
fmt.bits[0] &= SNDRV_PCM_FMTBIT_S16_LE;
return snd_mask_refine(f, &fmt);
}
return 0;
}
之后,需要把上述函数加入到你的规则当中去:
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,
hw_rule_channels_by_format,0, SNDRV_PCM_HW_PARAM_FORMAT,
-1);
当应用程序设定声道数量的时候会调用上面的规则函数。但是应用程序可以在设定声道数之前设定格式。所以也需要设定对应的规则。
Example5-7.为通道设定一个硬件规则
static int hw_rule_format_by_format(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_interval ch;
snd_interval_any(&ch);
if (f->bits[0] == SNDRV_PCM_FORMAT_S16_LE){
ch.min = ch.max = 1;
ch.integer = 1;
return snd_interval_refine(c, &ch);
}
return 0;
}
在open函数中:
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,
hw_rule_channels_by_format,0, SNDRV_PCM_HW_PARAM_CHANNELS,
-1);
这里我们不会更详细的描述,我仍然想说“直接看源码吧”。
第六章 控制接口
概要
控制接口非常广泛的应用在许多转换,变调等场合,可以从用户空间进行控制。混音器接口是一个最重要的接口。换句话说,在ALSA0.9.x版本,所有的混音器的工作都是通过控制接口API实现的(在0.5.x版本混音器内核API是独立出来的)。
ALSA有一个定义很好的AC97的控制模块。如果你的声卡仅仅支持AC97,你可以忽略这章。
控制接口定义
为了创建一个新的控制接口,需要定义三个函数:info,get和put。然后定义一个snd_kcontrol_new类型的记录,例如:
Example6-1.定义一个控制接口
static struct snd_kcontrol_new my_control __devinitdata = {
.face = 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
};
大部分情况是通过snd_ctl_new1()来创建control,这种情况下,你可以像上面一样,在定义的前面加上__devinitdata前缀.
iface字段表示control的类型,如:SNDRV_CTL_ELEM_IFACE_XXX,通常情况都是MIXER。CARD表示一个全局控制,而不是混音器的一部分。假如control和声卡设备联系的非常紧密,如HWDEP,RAMDIDI,TIMER或SEQUENCER,需要特别通过device和subdevice字段标识出。
name字段是表示名称的标识符。在ALSA0.9.X,控制单元名字是非常重要的,因为的角色就是通过它的名字进行分类。有一些预定义的标准的control名字。详细描述请参考下节的“控制单元名字”。
index字段存放这个control的索引号。假如一个名字下面有多个不同的control,就要通过index(索引)来区分了。这是当一个声卡拥有多个解码的时候才会用到。加入索引设定为零,就可以忽略定义。
access字段包括了控制接口的存取控制。提供了一些位的组合,如:SNDRV_CTL_ELEM_XXX。详细描述请参考“接口标志位”一节。
private_value字段这个记录的一个专有的的长整型变量。当调用info,get和put函数的时候,可以通过这个字段传递一些参数值。如果参数都是些很小的数,可以把它们通过移位来组合,也可以存放一个指向一个记录的指针(因为它是长整型的)。
另外三个在回调函数一节介绍。
控制接口名字
有一些control名字的标准。一个control通常根据“源,方向,功能”三部分来命名。
首先,SOURCE定义了control的源,是一个字符串,如:“Master”,“PCM”,“CD”,“Line”。已经有很多预定义好的“源”了。
第二,“方向”则为“Playbck”,“Capture”,“Bypass Playback”,“Bypass Capture”。或者,它如果省略,那就表示播放和录音双向。
第三个,“功能”。根据控制接口的功能不同有下面三个:“Switch”,“volume”,“route”。
一些控制接口名字的范例如下:“Master Capture Switch”,“PCM Playback Volume”.
也有一些不是采用“源,方向,功能”三部分来命名方式:
全局录音和播放
“Capture Source”,“Capture Switch”和“Capture Volume”用来做全局录音(输入)源,开关,和音量的控制。“Playback Switch”和“Playback Volume”用来做全局的输出开关和音量控制。
音调控制
音调开关和音量名称形式为“Tone Control-XXX”。如:“Tone Control-Switch”,“Tone Control – Bass”,“Tone Control – Center”。
3D控制
3D控制开关和音量命名形式为“3D Control – XXX”。如:“3D Control – Switch”,“3D Control – Switch”,“3D Control – Center”,“3D Control – Space”。
麦克风增益
麦克风增益命名如“Mic Boost”或“Mic Boost(6dB)”。
更精确的信息请参考文档(Documentation/sound/alsa/ControlNames.txt)
存取标志
存取标志是一个位标志,主要是区分给定的control的存取类型。缺省的存取类型是SNDRV_CTL_ELEM_ACCESS_READWRITE,意思是允许对control进行读写控制。当这个标志位被忽略的时候(为0),被认为是缺省读写(READWRITE)。
当这个control是只读的时候,需要传递SNDRV_CTL_ELEM_ACCESS_READ。这时候,可以不定义put函数。类似的,如果control是只写的话(虽然这种可能性很低),要设定为WRITE标志,也可以不必定义get函数。
加入control的值是经常改变的,应该加上VOLATILE标志,这意味着control可以不用显式通知就可以改变。应用程序应经常查询control。
当一个control是不活动的话,设定INACTIVE标志。还有LOCK和OWNER标志用来改变写权限。
回调函数
info函数
info函数可以得到对应control的详细信息。它必须存到一个给定的snd_ctl_elem_info对象中。例如,对于一个拥有一个元素的布尔型control的如下:
Example6-2.info函数示例
static int snd_myctl_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;
}
type字段表示control的类型。有如下几种:布尔型,整形,枚举型,BYTES型,IEC958,64位整形。count字段表示这个control里面的元素的数量。如:一个立体声音量拥有的count为2。value字段是一个union(联合)类型。value的存储依赖于类型。布尔型和整形是一样的。
枚举型和其他类型有一点不同,需要为当前给定的索引项设定字符串。
Static int snd_myctl_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
static char *texts[4] ={
“First”,“Second”,“Third”,“Fourth”
};
uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMRATED;
uinfo->count = 1;
uinfo->value.enumerated.items = 4;
if (uinfo->value.enumerated.item > 3)
uinfo->value.enumerated.item = 3;
strcpy( uinfo->value.enumerated.name,texts[ uinfo->value.enumerated.item]);
return 0;
}
get函数
这个函数用来读取当前control的值并返回到用户空间。
例如:
Example6-3.get函数示例
static int snd_myctl_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_ctl_elem_value *ucontrol)
{
struct mychip *chip = snd_kcontrol_chip(kcontrol);
ucontrol->value.integer.value[0] = get_some_value(chip);
return 0;
}
value字段和control类型有关,这点和info类似。例如,子驱动和用这个字段来存储一些寄存器的偏移,位的屏蔽。private_value设定如下:
.private_value = reg | (shift << 16) | (mask << 24)
可以通过以下函数重新得到:
static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
int reg = kcontrol->private_value & 0xff;
int shift = (kcontrol->private_value >> 16) & 0xff;
int mask = (kcontrol->private_value >> 24) & 0xff;
....
}
在get函数中,加入control拥有超过一个的元素。如count大于1,就必须填充所有的元素。上面的例子中,因为我们假定count为1,所以我们仅仅填充了一个元素(value.integer.value[0])。
put函数
这个函数主要是从用户空间写一个值
例如:
Example6-4.put函数示例
static int snd_myctrl_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;
}
如上,假如value被改变要返回1,没有改变返回0。假如有错误发生,通常返回一个带错误码的负数。
像get函数一样,如果control拥有超过一个的元素,在put函数中所有的元素都要比较一下。
回调函数不是原子的。
所有上面的3个回调函数都是非原子的。
构造器
当所有事情都准备好的时候,我们就可以创建一个新的control了。为了创建它,首先要调用两个函数,snd_ctl_new1()和snd_ctl_add()。
一个简单的方式如下:
if ((err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip))) < 0)
return err;
my_control是前面定义好的snd_kcontrol_new类型的对象,chip是一个指向kcontrol->private_data的指针,可以被回调函数调用。
snd_ctl_new1()分配了一个新的snd_kcontrol实例(这也是为何my_control可以带有__devinitdata前缀的原因了),snd_ctl_add会把给定的control组件添加到card里面。
更改通知
假如你需要在中断程序中改变或更新一个control,你需要调用snd_ctl_notify()。
例如:
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE,id_pointer);
这个函数需要card指针,event_mask,和control id指针作为参数。event-
mask表示notification的类型,如上述示例,是通知改变control的值。id指针是指向一个s
nd_ctl_elem_id的结构体。在es1938.c或es1968.c中关于硬件的卷的中断部分有相关示例。
更多推荐
所有评论(0)