简介:
本文主要分析uda1341音频设备驱动的框架,通过分析这个框架了解如何操作音频设备。而本文将分为两部分,第一部分总体介绍uda1341音频设备的框架,而第二部分会结合详细代码分析设备操作流程。
Linux内核:linux-2.6.22.6
所用开发板:JZ2440 V3(S3C2440A)
音频芯片:uda1341
所用总线:IIS
声明:
本文是看完韦东山老师的视频并看过一些文章后所写的学习总结,所以文中可能会有些地方会与其他人的文章有些相似的地方,如果您觉得我的文章有什么不对的地方,请您指出,我会改正。同时我也会将一些重要的知识点在文中指出,并将一些其他人可能忽略的知识点指出,例如总时钟与采样频率,采样频率类型以及预分频系数的关系。而在本文中可能会用到S3C2440A中的一些硬件或者寄存器的信息,所以希望你可以了解2440中IIS总线,并知道如何设置他。而这在我们后面分析代码时会用到。我将这部分内容翻译在:S3C2440A 第二十一章:IIS总线接口 。
第一部分:uda1341音频设备框架
其实在每次介绍框架信息的时候我都想讲解一下内核的思想,即内核框架很多时候是采用分层的思想设计的,将有共性的东西提出到一个抽象层,而将有差异性的留下来让用户编写——这层一般为物理层,而个抽象层会给下面物理层向上注册的接口来实现设备驱动的注册,而同时上层的操作函数又要回调下层的具体操作函数,实现对设备的控制。而这个分层的思想我们在前面的框架介绍中已经做了了分析。而现在我们还是用这样的思想来看这个驱动框架,这样你或许对这个整体的框架信息有一个更好的了解。
下面我先引入一张我画的uda1341的驱动框图:
图中分为上下两层,下层为sound\soc\s3c24xx\s3c2410-uda1341.c:该层主要实现2440和uda1341相关的硬件的操作。上层为:sound\sound_core.c:该层将音频设备抽象为了字符设备,并使用操作字符设备的方式操作该音频设备。图中下层通过注册函数将自己的操作函数和设备向上注册到上层,而上层会将这些注册的设备和操作函数放到一个名为chains的链表中,当上层字符设备想要操作下层设备时,他们会通过相应设备的索引号从链表中取出相应的操作函数,进数对下而实现由上层操作函层操作函数的回调。而这就是这个框架的整体信息。
第二部分:结合代码分析设备操作流程
上面我已经将这两层的关系画出,并对他们的关系做了简单的描述,而下面我们将结合代码来分析在这两层中具体做了些什么事。同时声明我在分析代码时会删除一些判断语句来使程序看上去更加简洁明了。
s3c2410-uda1341.c:
我们先分析硬件相关层,然后看他们是怎么实现向上注册的。同时我想大家在分析之前看一下下面的问题,然后大家带着几个问题去看代码会好一点:
1. 既然这层主要讲平台驱动注册,那平台设备的资源在哪里?
2. 既然要设置硬件,那么都要设置那些相关的硬件?引脚?时钟?还有吗?
3. 既然要向上注册,那我们是通过那个函数向上注册的?
下面我们带着这些问题来分析这层,如上面第一个问题所问的,我们要写uda1341我们要用到i2s总线,那么总线的资源在哪里那?
我们很多时候要知道其实对于S3C2440A他的大多数资源都是在arch\arm\plat-s3c24xx\devs.c这个文件夹下的,其中包括i2c,uart,usb,nand等等,很多的资源都在这个文件中,我们的i2s也不例外。我们看他的代码:
static struct resource s3c_iis_resource[] = {
[0] = {
.start = S3C24XX_PA_IIS,
.end = S3C24XX_PA_IIS + S3C24XX_SZ_IIS -1,
.flags = IORESOURCE_MEM,
}
};
static u64 s3c_device_iis_dmamask = 0xffffffffUL;
struct platform_device s3c_device_iis = {
.name = "s3c2410-iis",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_iis_resource),
.resource = s3c_iis_resource,
.dev = {
.dma_mask = &s3c_device_iis_dmamask,
.coherent_dma_mask = 0xffffffffUL
}
};
从上面看我们知道了i2s相关的寄存器的物理地址,以及与DMA相关的设置。
好了,我们下面开始正式分析代码,我们还是先从入口函数开始分析:
static int __init s3c2410_uda1341_init(void) {
memzero(&input_stream, sizeof(audio_stream_t));
memzero(&output_stream, sizeof(audio_stream_t));
return driver_register(&s3c2410iis_driver);
}
从上面看他注册了一个设备驱动s3c2410iis_driver,那我们就要看看这个s3c2410iis_driver有什么?
extern struct bus_type platform_bus_type;
static struct device_driver s3c2410iis_driver = {
.name = "s3c2410-iis",
.bus = &platform_bus_type,
.probe = s3c2410iis_probe,
.remove = s3c2410iis_remove,
};
从上面可以看出这是一个平台驱动,他的名字为s3c2410-iis,而我们在回答上面第一个问题时看到i2s中有一个同名的设备。我们知道在平台——设备——驱动模型中,当注册的平台设备和平台驱动的名字相同时,我们就要调用平台驱动中的probe函数。所以我们现在要进入.probe = s3c2410iis_probe中分析驱动中做了什么?
static int s3c2410iis_probe(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct resource *res;
unsigned long flags;
/* 为寄存器申请空间 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
/* IO端口重映射 */
iis_base = (void *)S3C24XX_VA_IIS ;
/* 获得i2s的时钟,并使能他 */
iis_clock = clk_get(dev, "iis");
clk_enable(iis_clock);
local_irq_save(flags); /* 禁止本地中断 */
/* 设置L3控制引脚和i2s的引脚 */
/* GPB 4: L3CLOCK, OUTPUT */
s3c2410_gpio_cfgpin(S3C2410_GPB4, S3C2410_GPB4_OUTP);
s3c2410_gpio_pullup(S3C2410_GPB4,1);
/* GPB 3: L3DATA, OUTPUT */
s3c2410_gpio_cfgpin(S3C2410_GPB3,S3C2410_GPB3_OUTP);
/* GPB 2: L3MODE, OUTPUT */
s3c2410_gpio_cfgpin(S3C2410_GPB2,S3C2410_GPB2_OUTP);
s3c2410_gpio_pullup(S3C2410_GPB2,1);
/* GPE 3: I2SSDI */
s3c2410_gpio_cfgpin(S3C2410_GPE3,S3C2410_GPE3_I2SSDI);
s3c2410_gpio_pullup(S3C2410_GPE3,0);
/* GPE 0: I2SLRCK */
s3c2410_gpio_cfgpin(S3C2410_GPE0,S3C2410_GPE0_I2SLRCK);
s3c2410_gpio_pullup(S3C2410_GPE0,0);
/* GPE 1: I2SSCLK */
s3c2410_gpio_cfgpin(S3C2410_GPE1,S3C2410_GPE1_I2SSCLK);
s3c2410_gpio_pullup(S3C2410_GPE1,0);
/* GPE 2: CDCLK */
s3c2410_gpio_cfgpin(S3C2410_GPE2,S3C2410_GPE2_CDCLK);
s3c2410_gpio_pullup(S3C2410_GPE2,0);
/* GPE 4: I2SSDO */
s3c2410_gpio_cfgpin(S3C2410_GPE4,S3C2410_GPE4_I2SSDO);
s3c2410_gpio_pullup(S3C2410_GPE4,0);
local_irq_restore(flags); /* 释放本地中断 */
/* 初始化S3C2440A中i2s总线 */
init_s3c2410_iis_bus();
/* 初始化uda1341 */
init_uda1341();
/* 设置并注册DMA */
output_stream.dma_ch = DMACH_I2S_OUT;
if (audio_init_dma(&output_stream, "UDA1341 out")) {
audio_clear_dma(&output_stream,&s3c2410iis_dma_out);
printk( KERN_WARNING AUDIO_NAME_VERBOSE
": unable to get DMA channels\n" );
return -EBUSY;
}
input_stream.dma_ch = DMACH_I2S_IN;
if (audio_init_dma(&input_stream, "UDA1341 in")) {
audio_clear_dma(&input_stream,&s3c2410iis_dma_in);
printk( KERN_WARNING AUDIO_NAME_VERBOSE
": unable to get DMA channels\n" );
return -EBUSY;
}
/* 注册设备dsp和mixer */
audio_dev_dsp = register_sound_dsp(&smdk2410_audio_fops, -1);
audio_dev_mixer = register_sound_mixer(&smdk2410_mixer_fops, -1);
return 0;
}
通过看上面代码我们大致将上面的工作分为了7部分:
1. 为寄存器申请空间,IO端口重映射
2. 获得i2s的时钟,并使能他
3. 设置L3控制引脚和i2s的引脚
4. 初始化S3C2440A中i2s总线
5. 初始化uda1341
6. 设置并注册DMA
7. 注册设备dsp和mixer
对于第一和第二部分我们在上面已经讲得很清楚了,我们现在说第三部分,而要说这部分就要先看下uda1341 的电路图了,
从上面三张图片可以看出,2440和uda1341的L3控制引脚以及i2s引脚的连接。而在2440中对于i2s有专门的引脚:
所以我们要将2440的i2s的引脚设为i2s,而L3我们是用2440控制而不接收数据,所以设置为输出就可以了 。
分析完第三部分,我们来分析第四部分:初始化S3C2440A中i2s总线
static void init_s3c2410_iis_bus(void){
__raw_writel(0, iis_base + S3C2410_IISPSR);
__raw_writel(0, iis_base + S3C2410_IISCON);
__raw_writel(0, iis_base + S3C2410_IISMOD);
__raw_writel(0, iis_base + S3C2410_IISFCON);
clk_disable(iis_clock);
}
而这部分就是对2440中寄存器的操作了,其主要是向IIS预分频寄存器(IISPSR),IIS控制寄存器(IISCON),IIS模式寄存器(IISMOD)以及IIS FIFO控制寄存器(IISFCON)中写入0 。并关闭i2s时钟。而具体关于寄存器的操作就要大家去看2440关于i2s这章了。
下面我们讲第五部分:初始化uda1341
static void init_uda1341(void)
{
/* GPB 4: L3CLOCK */
/* GPB 3: L3DATA */
/* GPB 2: L3MODE */
unsigned long flags;
uda1341_volume = 62 - ((DEF_VOLUME * 61) / 100);
uda1341_boost = 0;
local_irq_save(flags);
s3c2410_gpio_setpin(S3C2410_GPB2,1);//L3MODE=1
s3c2410_gpio_setpin(S3C2410_GPB4,1);//L3CLOCK=1
local_irq_restore(flags);
uda1341_l3_address(UDA1341_REG_STATUS);
uda1341_l3_data(0x40 | STAT0_SC_384FS | STAT0_IF_MSB|STAT0_DC_FILTER); // reset uda1341
uda1341_l3_data(STAT1 | STAT1_ADC_ON | STAT1_DAC_ON);
uda1341_l3_address(UDA1341_REG_DATA0);
uda1341_l3_data(DATA0 |DATA0_VOLUME(0x0)); // maximum volume
uda1341_l3_data(DATA1 |DATA1_BASS(uda1341_boost)| DATA1_TREBLE(0));
uda1341_l3_data((DATA2 |DATA2_DEEMP_NONE) &~(DATA2_MUTE));
uda1341_l3_data(EXTADDR(EXT2));
uda1341_l3_data(EXTDATA(EXT2_MIC_GAIN(0x6)) | EXT2_MIXMODE_CH1);//input channel 1 select(input channel 2 off)
}
而关于uda1341初始化这部分你就要自己对着uda1341的数据手册来对这些命令进行分析了,我在这里就不细说了,而我要说的是这里的两个函数:uda1341_l3_address和uda1341_l3_data。他们对应的函数分别为:
static void uda1341_l3_address(u8 data)
{
int i;
unsigned long flags;
local_irq_save(flags);
s3c2410_gpio_setpin(S3C2410_GPB2,0);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
udelay(1);
for (i = 0; i < 8; i++) {
if (data & 0x1) {
s3c2410_gpio_setpin(S3C2410_GPB4,0);
s3c2410_gpio_setpin(S3C2410_GPB3,1);
udelay(1);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
} else {
s3c2410_gpio_setpin(S3C2410_GPB4,0);
s3c2410_gpio_setpin(S3C2410_GPB3,0);
udelay(1);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
}
data >>= 1;
}
s3c2410_gpio_setpin(S3C2410_GPB2,1);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
local_irq_restore(flags);
}
static void uda1341_l3_data(u8 data)
{
int i;
unsigned long flags;
local_irq_save(flags);
udelay(1);
for (i = 0; i < 8; i++) {
if (data & 0x1) {
s3c2410_gpio_setpin(S3C2410_GPB4,0);
s3c2410_gpio_setpin(S3C2410_GPB3,1);
udelay(1);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
} else {
s3c2410_gpio_setpin(S3C2410_GPB4,0);
s3c2410_gpio_setpin(S3C2410_GPB3,0);
udelay(1);
s3c2410_gpio_setpin(S3C2410_GPB4,1);
}
data >>= 1;
}
local_irq_restore(flags);
}
而这两个函数是对应着uda1341在I2S模式时的时序图:
上面主要是用IO引脚来模拟时序,希望大家可以了解并学习,因为我们在以后可能会遇到开发板没有内置总线协议模块的时候,那么我们就要用这种方法来模拟各种协议了。
而第六部分关于DMA的设置和注册我还有些知识点不够了解,所以在这里我就不做介绍,不然可能讲错了误导大家,而第七部分注册设备dsp和mixer要在下面sound_core.c层讲。
sound_core.c:
下面我们要讲sound_core.c这层了,而在这层中主要讲两点。第一点:字符驱动的注册。第二点:dsp和mixer在chains链表中的存取。而我们先讲第二点的部分即dsp和mixer在chains链表中的存取,然后再讲如何通过字符驱动将存好的dsp和mixer设备取出并调用他们的操作函数。
那么我们就正好接着上面所讲的,开始介绍第七部分注册设备dsp和mixer。我们先讲注册设备dsp:
int register_sound_dsp(const struct file_operations *fops, int dev)
{
return sound_insert_unit(&chains[3], fops, dev, 3, 131,
"dsp", S_IWUSR | S_IRUSR, NULL);
}
而从上面看sound_insert_unit主要就是将dsp注册到chains链表中。而他们的具体是如何实现的我这里写出了他们简略的关系:
register_sound_dsp(&smdk2410_audio_fops, -1);
int register_sound_dsp(const struct file_operations *fops, int dev)
sound_insert_unit(&chains[3], fops, -1, 3, 131, "dsp", S_IWUSR | S_IRUSR, NULL);
static int sound_insert_unit(struct sound_unit **list, const struct file_operations *fops, int index, int low, int top, const char *name, umode_t mode, struct device *dev)
struct sound_unit *s = kmalloc(sizeof(*s), GFP_KERNEL);
r = __sound_insert_unit(s, list, fops, -1, 3, 131);
static int __sound_insert_unit(struct sound_unit * s, struct sound_unit **list, const struct file_operations *fops, int index, int low, int top)
while (*list && (*list)->unit_minor<n)
list=&((*list)->next);
while(n<top)
{
/* Found a hole ? */
if(*list==NULL || (*list)->unit_minor>n)
break;
list=&((*list)->next);
n+=SOUND_STEP;
}
/*
* Fill it in
*/
s->unit_minor=n;
s->unit_fops=fops;
/*
* Link it
*/
s->next=*list;
*list=s;
上面分析中缩进内容为上面函数中的内容。而从上面看就是将相应的设备放到了chains中设备所对应的位置,而设备所对应的位置为:
/*
* Allocations
*
* 0 *16 Mixers
* 1 *8 Sequencers
* 2 *16 Midi
* 3 *16 DSP
* 4 *16 SunDSP
* 5 *16 DSP16
* 6 -- sndstat (obsolete)
* 7 *16 unused
* 8 -- alternate sequencer (see above)
* 9 *16 raw synthesizer access
* 10 *16 unused
* 11 *16 unused
* 12 *16 unused
* 13 *16 unused
* 14 *16 unused
* 15 *16 unused
*/
从上面我们就知道了dsp在chains[3]中存放,而mixer在chains[0]中存放。而这与mixer的注册函数相对应:
int register_sound_mixer(const struct file_operations *fops, int dev)
{
return sound_insert_unit(&chains[0], fops, dev, 0, 128,
"mixer", S_IRUSR | S_IWUSR, NULL);
}
介绍完注册dsp和mixer,我们就开始介绍注册字符驱动,然后介绍如何从chains链表中获得他们相应的操作函数来实现通过回调函数对设备操作。
我们先从入口函数开始看起:
#define SOUND_MAJOR 14
static int __init init_soundcore(void)
{
if (register_chrdev(SOUND_MAJOR, "sound", &soundcore_fops)==-1) {
printk(KERN_ERR "soundcore: sound device already in use.\n");
return -EBUSY;
}
sound_class = class_create(THIS_MODULE, "sound");
if (IS_ERR(sound_class))
return PTR_ERR(sound_class);
return 0;
}
从上面看我们知道他做了两件事情:
1. 注册了一个注册设备号为14,名字为sound,而操作函数为soundcore_fops的字符设备。
2. 创建了一个名为sound的类。
而我们都知道在我们以前学的字符设备驱动中,注册完类紧接着就是在内核空间使用udev机制创建相应的字符设备。那么这里为什么没有看到创建设备的函数那?
这是因为在这一层中我们是抽象来的字符设备,而我们在没有真实的设备存在时我们这个字符设备是没有意义的,而当我们有一个真实的设备注册上来时我们再使用创建设备函数来创建这个设备,所以我们在上面sound_insert_unit函数中可以看到
spin_lock(&sound_loader_lock);
r = __sound_insert_unit(s, list, fops, index, low, top);
spin_unlock(&sound_loader_lock);
if (r < 0)
goto fail;
else if (r < SOUND_STEP)
sprintf(s->name, "sound/%s", name);
else
sprintf(s->name, "sound/%s%d", name, r / SOUND_STEP);
device_create(sound_class, dev, MKDEV(SOUND_MAJOR, s->unit_minor),
s->name+6);
上面代码的意思就是当向chains链表中注册了设备时,我们就会用device_create函数创建这个设备了。这样我们的字符设备驱动就完整了。那么我们现在就要来看看在这个字符设备的file_operations中做了什么:
static const struct file_operations soundcore_fops=
{
/* We must have an owner or the module locking fails */
.owner = THIS_MODULE,
.open = soundcore_open,
};
在这里我们看到只有一个open函数,即当用户要打开这个设备时调用,但是并没有与字符设备相关的处理函数,这是为什么那?
其实,这也与我们上面说的一样:因为在这一层中我们是抽象来的字符设备,在没有真实的设备存在时这个字符设备是没有意义的。所以我们要在open函数中找到对应的设备,并用他的操作函数代替已有的操作函数。而更清楚的说明我们就要看代码了:
int soundcore_open(struct inode *inode, struct file *file)
{
int chain;
int unit = iminor(inode);
struct sound_unit *s;
const struct file_operations *new_fops = NULL;
chain=unit&0x0F;
if(chain==4 || chain==5) /* dsp/audio/dsp16 */
{
unit&=0xF0;
unit|=3;
chain=3;
}
spin_lock(&sound_loader_lock);
s = __look_for_unit(chain, unit); /* 从chains中找相应的设备 */
if (s)
new_fops = fops_get(s->unit_fops); /* 并获得相应设备的操作函数 */
if (!new_fops) { /* 如果没有或者操作函数 */
························
}
if (new_fops) { /* 获得了操作函数 */
/*
* We rely upon the fact that we can't be unloaded while the
* subdriver is there, so if ->open() is successful we can
* safely drop the reference counter and if it is not we can
* revert to old ->f_op. Ugly, indeed, but that's the cost of
* switching ->f_op in the first place.
*/
int err = 0;
const struct file_operations *old_fops = file->f_op;
file->f_op = new_fops; /* 将获得操作函数赋值给字符设备的操作函数 */
spin_unlock(&sound_loader_lock);
if(file->f_op->open)
err = file->f_op->open(inode,file); /* 回调获得设备的操作函数open */
if (err) {
fops_put(file->f_op);
file->f_op = fops_get(old_fops);
}
fops_put(old_fops);
return err;
}
spin_unlock(&sound_loader_lock);
return -ENODEV;
}
下面我们再跳回到s3c2410-uda1341.c层,看open函数具体做了什么?
static int smdk2410_audio_open(struct inode *inode, struct file *file)
{
int cold = !audio_active;
DPRINTK("audio_open\n");
if ((file->f_flags & O_ACCMODE) == O_RDONLY) {
if (audio_rd_refcount || audio_wr_refcount)
return -EBUSY;
audio_rd_refcount++;
} else if ((file->f_flags & O_ACCMODE) == O_WRONLY) {
if (audio_wr_refcount)
return -EBUSY;
audio_wr_refcount++;
} else if ((file->f_flags & O_ACCMODE) == O_RDWR) {
if (audio_rd_refcount || audio_wr_refcount)
return -EBUSY;
audio_rd_refcount++;
audio_wr_refcount++;
} else
return -EINVAL;
if (cold) {
audio_rate = AUDIO_RATE_DEFAULT;
audio_channels = AUDIO_CHANNELS_DEFAULT;
audio_fragsize = AUDIO_FRAGSIZE_DEFAULT;
audio_nbfrags = AUDIO_NBFRAGS_DEFAULT;
if ((file->f_mode & FMODE_WRITE)){
init_s3c2410_iis_bus_tx(); /* 初始化i2s为发送 */
audio_clear_buf(&output_stream);
}
if ((file->f_mode & FMODE_READ)){
init_s3c2410_iis_bus_rx(); /* 初始化i2s为接收 */
audio_clear_buf(&input_stream);
}
}
return 0;
}
而这其中最主要的就是init_s3c2410_iis_bus_tx()和init_s3c2410_iis_bus_rx()函数了。我们分析init_s3c2410_iis_bus_tx:
static void init_s3c2410_iis_bus_tx(void)
{
unsigned int iiscon, iismod, iisfcon;
char *dstr;
//Kill everything...
__raw_writel(0, iis_base + S3C2410_IISPSR);
__raw_writel(0, iis_base + S3C2410_IISCON);
__raw_writel(0, iis_base + S3C2410_IISMOD);
__raw_writel(0, iis_base + S3C2410_IISFCON);
clk_enable(iis_clock);
iiscon = iismod = iisfcon = 0;
//Setup basic stuff
iiscon |= S3C2410_IISCON_PSCEN; // Enable prescaler
iiscon |= S3C2410_IISCON_IISEN; // Enable interface
// iismod |= S3C2410_IISMOD_MASTER; // Set interface to Master Mode
iismod |= S3C2410_IISMOD_LR_LLOW; // Low for left channel
iismod |= S3C2410_IISMOD_MSB; // MSB format
iismod |= S3C2410_IISMOD_16BIT; // Serial data bit/channel is 16 bit
iismod |= S3C2410_IISMOD_384FS; // Master clock freq = 384 fs
iismod |= S3C2410_IISMOD_32FS; // 32 fs
iisfcon|= S3C2410_IISFCON_RXDMA; //Set RX FIFO acces mode to DMA
iisfcon|= S3C2410_IISFCON_TXDMA; //Set TX FIFO acces mode to DMA
iiscon |= S3C2410_IISCON_TXDMAEN; //Enable TX DMA service request
iiscon |= S3C2410_IISCON_RXIDLE; //Set RX channel idle
iismod |= S3C2410_IISMOD_TXMODE; //Set TX Mode
iisfcon|= S3C2410_IISFCON_TXENABLE; //Enable TX Fifo
dstr="TX";
//setup the prescaler
audio_set_dsp_speed(audio_rate);
//iiscon has to be set last - it enables the interface
__raw_writel(iismod, iis_base + S3C2410_IISMOD);
__raw_writel(iisfcon, iis_base + S3C2410_IISFCON);
__raw_writel(iiscon, iis_base + S3C2410_IISCON);
}
上面就是通过设置2440中i2s的寄存器来让他们为发送模式,而我要讲的是其中预分频系数,上面代码中预分频是通过audio_set_dsp_speed(audio_rate)函数算出来的。但是这个预分频与PCLK,主机时钟频率,以及采样频率到底是怎么样的那?
其实是PCLK = 主机时钟频率*采样频率*(预分频系数+1);
好了关于uda1341音频设备驱动框架就分析完了,希望对你有所帮助。
参考文章:
S3C2440 UDA1341声卡驱动分析(oos)
31.Linux-wm9876声卡驱动(移植+测试)
Linux下的I2S驱动学习
声卡驱动移植uda1341和wm8976:
基于mini2440的UDA1341音频驱动架构分析