《Linux驱动:使用音频设备驱动框架-OSS构建音频设备驱动》

一,前言

OSS(Open Sound System(开放声音系统)),是unix平台上一个统一的音频接口。ALSA (Advanced Linux Sound Architecture(高级Linux声音体系)) 是为声卡提供驱动的Linux内核组件,替代OSS。在这里先来分析简单的OSS,后面有时间再研究下ALSA。分析OSS的基本框架,最后通过一个例子总结下OSS框架下实现一个音频设备驱动的一般步骤。

二,框架

内核目录下sound/sound_core.c文件是OSS的核心层,其向上提供了应用程序访问音频设备统一的入口,向下为不同音频设备提供了向上的注册接口。音频驱动开发针对特定的音频设备和系统硬件资源使用总线-设备-驱动模型构建音频设备驱动,在驱动probe中调用OSS提供的接口注册音频设备,并由OSS进行管理。OSS架构是基于文件系统的访问方式,对声音的操作通过对设备节点文件进行open、read、write等操作完成。
在这里插入图片描述
在这里插入图片描述

三,OSS实现

3.1 OSS初始化

在内核中配置,将sound_core.c编译进内核,内核初始化时加载,调用init_soundcore。
make menuconfig
-> Device Drivers -> Sound
-> <*> Sound card support

// linux-2.6.22.6/include/linux/major.h
#define SOUND_MAJOR		14
// linux-2.6.22.6/sound/sound_core.c
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;
	}
	// 创建一个设备类。/sys/class/sound
	sound_class = class_create(THIS_MODULE, "sound");
	if (IS_ERR(sound_class))
		return PTR_ERR(sound_class);

	return 0;
}

3.2 向OSS注册音频设备

OSS标准中有2个最基本的音频设备:mixer(混音器)和DSP(数字信号处理器),mixer的作用将多个信号组合或者叠加到一起,可用来调整音量大小和选择音源。对于不同声卡来说,其混音器的作用可能各不相同。向OSS核心层注册音频接口时创建的/dev/mixer设备节点文件是应用程序对mixer进行操作的软件接口。DSP也称编解码器,主要实现录音和放音的操作,其对应的设备节点文件是/dev/dsp,向该设备节点文件写数据即意味着激活声卡上的D/A 转换器进行放音,而向该设备读数据则意味着激活声卡上的A/D 转换器进行录音。
除了mixer和dsp音频设备外,还有多个不同的音频设备。OSS针对这些设备各下层提供了不同的注册接口。

int register_sound_mixer(const struct file_operations *fops, int dev);
int register_sound_midi(const struct file_operations *fops, int dev);
int register_sound_dsp(const struct file_operations *fops, int dev);
int register_sound_special(const struct file_operations *fops, int unit);

这些接口的本质都是调用sound_insert_unit函数向OSS管理的不同设备链表中插入音频设备,并为不同的音频设备创建不同的音频设备设备节点。参数fops是各种音频设备的操作函数,dev一般传入-1,自动分配设备号。

3.3 OSS管理音频设备

OSS通过一个指针数组管理多个不同类型的音频设备链表。驱动层下OSS注册音频设备时,即向对应的链表中插入一个sound_unit结构。

/*
 *	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
 */
static struct sound_unit *chains[SOUND_STEP];

比如注册一个dsp音频设备

register_sound_dsp(&smdk2410_audio_fops, -1) -> 
    sound_insert_unit(&chains[3], fops, dev, 3, 131,"dsp", S_IWUSR | S_IRUSR, NULL) -> 
        __sound_insert_unit(s, list, fops, index, low, top) -> 
            s->unit_minor=n;
        	s->unit_fops=fops;
        	s->next=*list;
        	*list=s;
    	// 以14为主设备号,以s->unit_minor为次设备号创建音频设备,创建设备节点文件
    	device_create(sound_class, dev, MKDEV(SOUND_MAJOR, s->unit_minor),
		      s->name+6);
            

上面提到OSS初始化时,向内核注册了一个字符设备,其操作函数为soundcore_fops。这里面只有一个open接口,其只是作为一个中转,应用程序访问某个音频设备(open)会根据打开的音频设备的设备节点文件的次设备号(也就是创建时使用的s->unit_minor),从音频设备链表数组(chains)中找到对应类型的音频设备链表从而找到对应的操作函数。

static const struct file_operations soundcore_fops=
{
    
    
	/* We must have an owner or the module locking fails */
	.owner	= THIS_MODULE,
	.open	= soundcore_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);
	if (s)
		new_fops = fops_get(s->unit_fops);
	if (!new_fops) {
    
    
		spin_unlock(&sound_loader_lock);
		request_module("sound-slot-%i", unit>>4);
		request_module("sound-service-%i-%i", unit>>4, chain);
		spin_lock(&sound_loader_lock);
		s = __look_for_unit(chain, unit);
		if (s)
			new_fops = fops_get(s->unit_fops);
	}
	if (new_fops) {
    
    
		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);
		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;
}

在这里插入图片描述

四,音频基本概念

4.1 采样频率

对一个声音波形记录一次声音数据的频率。频率越大,对记录的声音数据还原播放出来的声音就越真实。对于入耳采样频率不需要太大,采样率在 8KHz - 96KHz之间就行,到 96KHz 声音已经很饱满了。

4.2 采样精度

一个声音数据用多少位表示。比如说8位精度,即一个声音数据用8位也就是一个字节表示;8位精度,即一个声音数据用16位也就是两个字节表示。

4.3 左声道/右声道

左右声道数据是不同的,两种数据配合还原播放出来的声音更加立体。

4.4 IIS接口

用来传输声音数据的接口,由四条线构成,I2SSCLK – 数据位时钟 , I2SLRCK – 左右声道数据采样时钟 , I2SSDI – 声音数据输入 , I2SSDO – 声音数据输出。
在这里插入图片描述

4.5 声音录制和播放

在这里插入图片描述

4.6 控制接口

IIS接口用来传输音频数据,控制接口用来传输主控对编解码芯片的控制数据,比如设置声音音量、切换声道输出、设置MIC增益等。也就通过GPIO来对编解码芯片的某些寄存器进行数据读写操作。不同的编解码芯片使用不同的传输模式,比如WM8976G提供两种传输模式,一种是三线模式,一种是IIC模式。

五,实现WM8976G的音频设备驱动

5.1 硬件电路

5.1.1 WM8976G相关

L2/GPIO2:单声道音频输入或第二个麦克风接入或者做普通GPIO口
LRC:左右声道数据采样时钟
BCLK:声音数据位时钟
ADCDAT:IIS数据输入
DACDAT:IIS数据输出
MCLK:WM8976工作时钟,由主控提供
MICBIAS:麦克风偏置电压,通过调节该偏置电压可以改善麦克风录制失真问题
LIP:麦克风输入通道
LIN:麦克风输入通道(接地)
AUXL:音频输入左声道(外接音频)
AUXR:音频输入右声道(外接音频)
CSB/GPIO1:控制接口使用3线模式时的片选脚或者做普通GPIO口
SCLK:三线模式时钟或者IIC模式时钟
SDIN:三线模式数据或者IIC模式数据
MODE:控制接口使用三线模式或IIC模式选择脚,高电平时使用三线模式
VMID:参考电压,通过调节参考电压可以改善声音播放时的噪音问题
ROUT1:音频输出通道1,右声道,可外接耳机或喇叭
LOUT1:音频输出通道1,左声道,可外接耳机或喇叭
ROUT2:音频输出通道2,右声道,可外接耳机或喇叭
LOUT2:音频输出通道2,左声道,可外接耳机或喇叭
OUT3:音频输出通道3
OUT4:音频输出通道4
在这里插入图片描述

5.1.2 S3C2440相关

在这里插入图片描述
在这里插入图片描述

5.2 构建驱动

通过总线-设备-驱动模型来构建音频设备驱动,在驱动的probe中初始化硬件资源、向OSS核心层注册dsp音频设备和mixer音频设备。

5.2.1 注册平台设备

系统启动初始化时,注册s3c_device_iis平台设备。加载注册过程在前面的文章中已经分析过多次,这里不再赘述。

// linux-2.6.22.6/arch/arm/plat-s3c24xx/devs.c
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
	}
};

// linux-2.6.22.6/arch/arm/mach-s3c2440/mach-smdk2440.c
static struct platform_device *smdk2440_devices[] __initdata = {
    
    
	&s3c_device_usb,
	&s3c_device_lcd,
	&s3c_device_wdt,
	&s3c_device_i2c,
	&s3c_device_iis,
    &s3c2440_device_sdi,
};

static void __init smdk2440_machine_init(void)
{
    
    
	s3c24xx_fb_set_platdata(&smdk2440_lcd_cfg);

	platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
	smdk_machine_init();
}

MACHINE_START(S3C2440, "SMDK2440")
	/* Maintainer: Ben Dooks <[email protected]> */
	.phys_io	= S3C2410_PA_UART,
	.io_pg_offst	= (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
	.boot_params	= S3C2410_SDRAM_PA + 0x100,

	.init_irq	= s3c24xx_init_irq,
	.map_io		= smdk2440_map_io,
	.init_machine	= smdk2440_machine_init,
	.timer		= &s3c24xx_timer,
MACHINE_END

5.2.2 注册平台驱动

// linux-2.6.22.6/sound/soc/s3c24xx/s3c2440-wm8976.c
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,
};

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);
}

5.2.3 probe函数分析

总线-设备-驱动模型的设备和驱动的匹配过程,以及匹配成功后调用驱动的probe函数的过程在之前的文章中已经分析过多次,这里不再赘述。直接看驱动的probe函数,即s3c2410iis_probe。

5.2.3.1 初始化硬件资源

......
iis_base = (void *)S3C24XX_VA_IIS ;

// 使能IIS模块
iis_clock = clk_get(dev, "iis");
clk_enable(iis_clock);

// 设置相关引脚功能
/* 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);

// 初始化IIS模块
init_s3c2410_iis_bus();

......

5.2.3.2 初始化并设置音频编解码芯片

.......
init_wm8976();
.......

static void init_wm8976(void)
{
    
    
	uda1341_volume = 57;
	uda1341_boost = 0;

	/* software reset */
	wm8976_write_reg(0, 0);

	/* OUT2的左/右声道打开
	 * 左/右通道输出混音打开
	 * 左/右DAC打开
	 */
	wm8976_write_reg(0x3, 0x6f);
	
	wm8976_write_reg(0x1, 0x1f);//biasen,BUFIOEN.VMIDSEL=11b  
	wm8976_write_reg(0x2, 0x185);//ROUT1EN LOUT1EN, inpu PGA enable ,ADC enable

	wm8976_write_reg(0x6, 0x0);//SYSCLK=MCLK  
	wm8976_write_reg(0x4, 0x10);//16bit 		
	wm8976_write_reg(0x2B,0x10);//BTL OUTPUT  
	wm8976_write_reg(0x9, 0x50);//Jack detect enable  
	wm8976_write_reg(0xD, 0x21);//Jack detect  
	wm8976_write_reg(0x7, 0x01);//Jack detect 
}    

5.2.3.3 初始化音频输入和输出的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;
}
.......

5.2.3.4 向OSS核心层注册音频设备

注册之后由OSS核心层管理并操作音频设备,就是第三节分析的一样。

......
audio_dev_dsp = register_sound_dsp(&smdk2410_audio_fops, -1);
audio_dev_mixer = register_sound_mixer(&smdk2410_mixer_fops, -1);
......

六,应用程序播放和录制

待定

猜你喜欢

转载自blog.csdn.net/qq_40709487/article/details/127594053