23.声卡驱动(移植+测试)

linux-2.6.22.6\sound\Sound_core.c

1.声音三要素

1.1采样频率

  音频采样率是指录音设备在一秒钟内对声音信号的采样次数, 常用的采样率有:

  8KHz - 电话所用采样率, 对于人的说话已经足够清除
  22.05KHz - 无线电广播所用采样率
  32KHz - miniDV 数码视频、DAT所用采样率
  44.1KHz - 音频 CD, 也常用于 MPEG-1 音频(VCD, SVCD, MP3)所用采样率
  48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率
  50KHz - 商用数字录音机所用采样率
  96K - BD-ROM(蓝光盘)音轨、和 HD-DVD (高清晰度 DVD)音轨等所用采样率
  而2440开发板的采样频率IISRCK最高可以达到96KHz,满足了很多常用的采样场合,如下图所示:
在这里插入图片描述
  其中CODECLK便是MCLK

1.2量化位数

  指每个采样点里传输的数字信号次数,如下图所示, 其中蓝线表示模拟信号,红线表示数字信号,量化位越高,数字信号就越可能接近原始信号,音质越好
在这里插入图片描述
  一般的量化位数为:

  8位: 分成 256 次;
  16位: 分为65536次, 已到 CD 标准;
  32位: 分为 4294967296次,很少用到
  2440的开发板只支持8位,16位,如下图所示:

  其中LRCK就是采样频率,当LRCK为低时,表示传输的采样数据是左声道,当LRCK为高时,表示传输的采样数据是右声道,每个采样点,SD(serial data)都可以传输8位,或16位数字信号(从低位到高位传输)
在这里插入图片描述

1.3声道数

  常有单声道和立体声之分,(有的也处理成两个喇叭输出同一个声道的声音),而立体声更能感受到空间效果,但数据量翻倍
  所以,声音的每秒数据量(字节/s)= (采样频率 × 量化位数 × 声道数) / 8;

2. WM9876声卡硬件分析

  声卡是负责录音、播音、调节音量和声音合成等的一种多媒体板卡

  本节使用的声卡是2440板上自带的WM9876声卡
在这里插入图片描述
  当我们播放声音时 ,将数字信号传入I2SDO脚,声卡便通过解码,产生模拟信号到喇叭/耳机

  录音时,声卡便获取麦克风的模拟信号,编码出数字信号到I2SDI引脚上

  WM8976接口分为两种:I2S接口(提供音频接收和发送)、控制接口(控制音量大小,使能各个输出通道等)

  IIS接口相关的引脚如下
  MCLK : 主机为编解码芯片提供的系统同步时钟 ( Master/system clock input ) ,在2440中,一般设置为256fs,或者384fs
  BCLK: 编解码芯片提供的串行时钟信号 ( Audio bit clock output ) ,也就是量化位深,比如I2SIRCK=44.1khz,量化位为32位,则BCLK=44.1khz322
  I2SLRCK: 采样频率信号,当为低电平时是采样的是左声道信号,为高电平是右声道信号,常见有44.1Khz(1fs)
  I2SDI : ADC数据输入
  I2SDO :DAC数据输出
  如下图所示:
在这里插入图片描述
  控制接口相关的引脚如下

  CSB/GPIO1: 3线 控制数据使能引脚
  SCLK: 3线/2线 时钟引脚
  SDIN: 3线/2线 数据输入输出引脚
  MODE: 3线/2线 控制选择,当MODE为高,表示为3线控制,MODE位低,表示2线控制,如下图所示:
在这里插入图片描述
  其它引脚如下:

  R/LOUT1:音频左/右输出通道1,外接耳机插孔
  R/LOUT2:音频左/右输出通道2,未接
  OUT3:单声道输出通道3,未接
  OUT4:单声道输出通道4,未接
  LIP/LIN:音频输入通道,外接麦克风
  那么3线和2线的控制引脚又有什么区别?

  3线控制:

  如下图所示,3线控制,每周期都要传输16位数据(7位寄存器地址+9位寄存器数据),传输完成后,给CSB一个上升沿便完成一次数据的传输
在这里插入图片描述
  2线控制:

  如下图所示,2线控制就是I2C通信方式
在这里插入图片描述
  本节的WM8976的MODE脚接的高电平,所以是3线控制

3.接下来便来分析linux内核的声卡系统

  在linux声卡中存在两种声卡系统,一种是OSS(开放声音系统),一种是ALSA(先进Linux声音架构)。本节系统以OSS(Open Sound System)为例 ,

  内核以linux-2.6.22.6版本为例,位于:linux-2.6.22.6\sound\Sound_core.c

3.1首先进入入口函数

3.1.1init函数开始看起 注册平台

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

3.1.2probe调用

static struct device_driver s3c2410iis_driver = {
	.name = "s3c2410-iis",
	.bus = &platform_bus_type,
	.probe = s3c2410iis_probe,
	.remove = s3c2410iis_remove,
};

3.1.3probe函数

static int s3c2410iis_probe(struct device *dev) 
{
	struct platform_device *pdev = to_platform_device(dev);
	struct resource *res;
	unsigned long flags;

	printk ("s3c2410iis_probe...\n");
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res == NULL) {
		printk(KERN_INFO PFX "failed to get memory region resouce\n");
		return -ENOENT;
	}

	iis_base = (void *)S3C24XX_VA_IIS ;
	if (iis_base == 0) {
		printk(KERN_INFO PFX "failed to ioremap() region\n");
		return -EINVAL;
	}

	iis_clock = clk_get(dev, "iis");
	if (iis_clock == NULL) {
		printk(KERN_INFO PFX "failed to find clock source\n");
		return -ENOENT;
	}
	/*使能时钟*/
	clk_enable(iis_clock);

	local_irq_save(flags);
	/*配置GPIO管脚*/
	
	/* 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);
	/*iis总线初始化*/
	init_s3c2410_iis_bus();
	/*使用L3接口初始化芯片*/
	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;
	}

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

	printk(AUDIO_NAME_VERBOSE " initialized\n"); 

	return 0;

}

3.1.4看下init_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;
//        uda_sampling = DATA2_DEEMP_NONE;
//        uda_sampling &= ~(DATA2_MUTE);


	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));
#if 1
	/* 对于MINI2440,  */
	uda1341_l3_data(EXTDATA(EXT2_MIC_GAIN(0x6)) | EXT2_MIXMODE_CH2);//input channel 2 select
#else
	/* 对于TQ2440 */
	uda1341_l3_data(EXTDATA(EXT2_MIC_GAIN(0x6)) | EXT2_MIXMODE_CH1);//input channel 1 select(input channel 2 off)
#endif
}

3.1.5uda1341_l3_address

static void uda1341_l3_address(u8 data)
{
	int i;
	unsigned long flags;

	local_irq_save(flags);

// write_gpio_bit(GPIO_L3MODE, 0);
	s3c2410_gpio_setpin(S3C2410_GPB2,0);
// write_gpio_bit(GPIO_L3CLOCK, 1);
	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);
}

  如下图所示:
在这里插入图片描述
  入口函数里,只注册了一个主设备号为(SOUND_MAJOR)14的“sound”字符设备和class类,这里为什么没有创建设备节点?

是因为, 当注册声卡系统的驱动后,才会有设备节点,此时这里的代码是没有驱动的,后面会分析到

3.2 再来看看“sound”字符设备的file_perations:

在这里插入图片描述
  这里只有个.open,为什么没有.read,.write函数?

  显然在.open函数里做了某些处理,我们进入soundcore_open()来看看

3.3 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; //定义一个新的file_operations
       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[chain]数组里找到sound_unit结构体
                                             //一个sound_unit对应一个声卡驱动

       if (s)
              new_fops = fops_get(s->unit_fops);    //通过sound_unit,获取对应的file_operations
              ... ...

       if (new_fops) {                                           //当找到file_operations
              int err = 0;
              const struct file_operations *old_fops = file->f_op;//设上次的file_operations等于当前的

              file->f_op = new_fops;                //设置系统的file_operations等于s-> unit_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;
}

  通过上面的代码和注释分析到,系统声卡之所以只有一个open(),里面是通过次设备号来调用__look_for_unit()函数,找到chains[chain]数组里的驱动声卡sound_unit结构体,然后来替换系统声卡的file_operations,实现偷天换日的效果。

3.4__look_for_unit()函数如下图所示:

在这里插入图片描述
  其中chains[]数组定义如下所示:
在这里插入图片描述
  其中, chains[0]存放的Mixers,实现调节音量,高音等,就是我们VM8976的控制接口chains[3]存放的DSP,用来实现音频输入输出,就是我们VM8976的I2S接口

  显然VM8976的驱动有2个,需要将2个file_operations放入chains[0]和chains[3]数组里,供给系统的open()来调用

3.5我们以DSP为例,搜索chains[3]来看看

在这里插入图片描述
  如上图所示,显然register_sound_dsp()函数就是被我们声卡驱动调用的,用来注册dsp设备节点,继续进入sound_insert_unit()函数看看

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);   //分配个新的sound_unit
       int r;
       if (!s)
              return -ENOMEM;
       
       spin_lock(&sound_loader_lock);

// __sound_insert_unit()里主要实现:将分配的新的s插入到chains[3]里,然后并放入fops操作结构体
       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);         //s->name="sound/dsp"
       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);      
                           //s->name+6="dsp",也就是在/dev下创建"dsp"的设备节点
       return r;

 fail:
       kfree(s);
       return r;
}

  所以,register_sound_dsp()函数用来创建/dev/dsp 设备节点,同时将dsp相关的file_operations放入chains[3]里面

3.6 同样, Mixers的驱动流程也是这样,它的函数是register_sound_mixer(),如下图所示

在这里插入图片描述
  也是创建/dev/mixer设备节点, 同时将dsp相关的file_operations放入chains[0]里面

3.7 接下来,我们便搜索register_sound_dsp()函数,看看被哪些声卡驱动调用

  如下图所示,找到一个支持s3c24xx板卡的声卡驱动uda1341
在这里插入图片描述
  uda1341声卡和WM8976声卡非常相似,音频都是I2S接口,就只有控制部分不一样

  uda1341声卡的硬件,如下图所示:
在这里插入图片描述
  它的控制引脚只有3个:
  L3MODE:0地址模式,1 数据模式
  L3CLK:每一个CLK传输1位
  L3DAT: 地址信号 数据信号
在这里插入图片描述
  下图为uda1341ts关于L3MODE的一些说明:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  和WM8976的控制时序完全不一样,WM8976控制时序如下所示:
在这里插入图片描述
  所以接下来,便修改S3c2410-uda1341.c的控制部分,来移植为wm8976驱动

4.移植wm8976驱动

4.1 首先进入uda1341的probe函数

static int s3c2410iis_probe(struct device *dev)
{
       struct platform_device *pdev = to_platform_device(dev);
       struct resource *res;
       unsigned long flags;
       printk ("s3c2410iis_probe...\n");
       /*获取资源*/
       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
       if (res == NULL) {
              printk(KERN_INFO PFX "failed to get memory region resouce\n");
              return -ENOENT;
       }

       iis_base = (void *)S3C24XX_VA_IIS ;
       if (iis_base == 0) {
              printk(KERN_INFO PFX "failed to ioremap() region\n");
              return -EINVAL;
       }

/*获取I2S时钟,并使能*/
       iis_clock = clk_get(dev, "iis");
       if (iis_clock == NULL) {
              printk(KERN_INFO PFX "failed to find clock source\n");
              return -ENOENT;
       }
       clk_enable(iis_clock);

       /*进入临界区, 禁止中断,并保存中断状态*/
       local_irq_save(flags);

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

       /*退出临界区,使能中断,并恢复之前保存的flags中断状态*/
       local_irq_restore(flags);

       /*设置2440的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;
       }

    /*设置DMA输入通道,用来接收声音*/
       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;
       }
   

  /*创建/dev/dsp,/dev/mixer两个设备节点,
并将smdk2410_audio_fops和smdk2410_mixer_fops 两个file_operations放入chains[0]和chains[3]里,供给内核的声卡系统调用*/
       audio_dev_dsp = register_sound_dsp(&smdk2410_audio_fops, -1);
       audio_dev_mixer = register_sound_mixer(&smdk2410_mixer_fops, -1);

       printk(AUDIO_NAME_VERBOSE " initialized\n");

       return 0;
}

  从上面的代码来看, uda1341的管脚和wm8976的管脚连接都是一样的,只有init_uda1341()不一样,里面是初始化uda1341的控制引脚接口,所以需要屏蔽,然后自己来写个init_wm8976()函数

4.2写init_wm8976()函数之前需要先写一个寄存器操作函数

  参考wm8976芯片手册时序图:
在这里插入图片描述
所以,代码如下:

static void wm8976_write_reg(unsigned char reg, unsigned int data)
{
int i;
unsigned long flags;
//对于wm8976来说,数据的高七位表示寄存器地址,低9位表示寄存器的值
unsigned short val = (reg << 9) | (data & 0x1ff);

/*wm8976引脚csb,dat,clk分别对应2440芯片的GPB2,3,4引脚*/
       s3c2410_gpio_setpin(S3C2410_GPB2,1);
       s3c2410_gpio_setpin(S3C2410_GPB3,1);
       s3c2410_gpio_setpin(S3C2410_GPB4,1);
 
/*退出临界区,使能中断,并恢复之前保存的flags中断状态*/
    local_irq_save(flags);

/*把val值写入wm8976,共16位,从高到低传输*/
       for (i = 0; i < 16; i++){
              if (val & (1<<15))
              {
                     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);                   
              }
              val = val << 1;
        }
 
       //传输完成,需要让csb信号产生低脉冲,写入wm8976
       s3c2410_gpio_setpin(S3C2410_GPB2,0);     
       udelay(1);

//引脚恢复到高电平状态
       s3c2410_gpio_setpin(S3C2410_GPB2,1);
       s3c2410_gpio_setpin(S3C2410_GPB3,1);
       s3c2410_gpio_setpin(S3C2410_GPB4,1);
   
       local_irq_restore(flags);
}

4.3参考wm8976g.pdf第87页,来初始化wm8976,使能输出声道1,2,混响器等

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

static void init_wm8976(void)
{
       uda1341_volume = 57;         // wm8976的音量默认值,后面会讲到
       uda1341_boost = 0;

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

       /* BIT[6-5]:使能音频的输出左右通道2
        * BIT[3]: 使能mixer混音器的输出右通道  
        * BIT[2]: 使能mixer混音器的输出右通道  
        * BIT[1]: 使能DAC传输的右通道
     * BIT[0]: 使能DAC传输的左通道
        */
       wm8976_write_reg(0x3, 0x6f);
     /* BIT[4]: 使能输出麦克风电压
        */
       wm8976_write_reg(0x1, 0x1f);  
       wm8976_write_reg(0x2, 0x185);//ROUT1EN LOUT1EN, inpu PGA enable ,ADC enable
       wm8976_write_reg(0x6, 0x0);//SYSCLK=MCLK 
       wm8976_write_reg(0x4, 0x10); // [4:3]=10:使用I2S接口传输                   
       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
}

  wm8976初始化修改完成后,还需要修改音量控制等函数,之前就分析了uda1341的probe函数,里面会注册dsp、mixer设备节点:

/dev/dsp

  用来播发和录音,由于uda1341和wm8976都用了I2S接口,所以dsp的file_operations不需要修改,

/dev/mixer

  用来控制音量,调低音,高音等,由于wm8976的控制接口不一样,所以需要修改mixer的file_operations->ioctl函数

4.4 mixer的file_operations->ioctl函数如下所示:

在这里插入图片描述

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

static int smdk2410_mixer_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)
{
       int ret;
       long val = 0; 

switch (cmd) {
       case SOUND_MIXER_INFO:             //CASE : 获取声卡的描述信息
{
       mixer_info info;
       strncpy(info.id, "UDA1341", sizeof(info.id));
       strncpy(info.name,"Philips UDA1341", sizeof(info.name));
       info.modify_counter = audio_mix_modcnt;        
       return copy_to_user((void *)arg, &info, sizeof(info));  //上传用户层
}
    ... ...

case SOUND_MIXER_WRITE_VOLUME:                //CASE: 写音量,音量值为0~99
ret = get_user(val, (long *) arg);  //读用户层的数据,并放在val里
            if (ret)
                   return ret;
 			/* ioctl: val越大表示音量越大, 0-最小, 100-最大
			 * UDA1341: 寄存器的值越小音量越大
			 * WM8976: 52,53号寄存器bit[5:0]表示音量, 值越大音量越大, 0-63
			 */

     uda1341_volume = 63 - (((val & 0xff) + 1) * 63) / 100; //转换为寄存器音量值
     uda1341_l3_address(UDA1341_REG_DATA0); //写入音量的寄存器地址
     uda1341_l3_data(uda1341_volume);          //写入转换后的寄存器值数据          
     break;

case SOUND_MIXER_READ_VOLUME:                   //CASE:  读音量,音量值为0~100
     val = ((63 - uda1341_volume) * 100) / 63;  //将寄存器音量值转换为原始数据
     val |= val << 8;                  
     return put_user(val, (long *) arg);                     //上传音量值

case SOUND_MIXER_READ_IGAIN:                     //CASE: 读(in gain)混音输入增益
     val = ((31- mixer_igain) * 100) / 31;                 
     return put_user(val, (int *) arg);

case SOUND_MIXER_WRITE_IGAIN:                    //CASE:   写(in gain)混音输入增益
    ret = get_user(val, (int *) arg);
if (ret)
            return ret;
     mixer_igain = 31 - (val * 31 / 100);
     /* use mixer gain channel 1*/
     uda1341_l3_address(UDA1341_REG_DATA0);
     uda1341_l3_data(EXTADDR(EXT0));                  
     uda1341_l3_data(EXTDATA(EXT0_CH1_GAIN(mixer_igain)));     
     break;

default:
      DPRINTK("mixer ioctl %u unknown\n", cmd);
      return -ENOSYS;

    }
    return 0;
}

  从上面的代码来看,显然接下还要修改以下几个与控制接口相关的case:

case SOUND_MIXER_WRITE_VOLUME:            //写音量
case SOUND_MIXER_READ_VOLUME:             //读音量
case SOUND_MIXER_READ_IGAIN:                 //读(in gain)混音输入增益
case SOUND_MIXER_WRITE_IGAIN:                //写(in gain)混音输入增益

4.5修改“case SOUND_MIXER_WRITE_VOLUME:”和“case SOUND_MIXER_READ_VOLUME:”

  如下图所示(参考wm8976手册的P86页):
在这里插入图片描述
  其中52,53对应的输出左右通道1的音量,54,55对应的输出左右通道2的音量

  而我们耳机位于输出左右通道1,如下图所示,所以我们需要设置52,53的寄存器
在这里插入图片描述
  接下来,便来看看寄存器,如何读写音量

  我们以53通道1寄存器为例:
在这里插入图片描述
  如上图所示:

  bit8:  为1,表示每次写入音量值,即立刻更新音量
  bit7:  位1,表示通道1的左右声道都静音
  bit6: 位1,表示通道1的右声道静音
  bit5~0: 表示音量大小,默认值为57(111001),最大值为63
  所以修改的内容如下所示:

case SOUND_MIXER_WRITE_VOLUME:     //音量0~100
    ret = get_user(val, (long *) arg);          //读取应用数据,存到val里
    if (ret)
            return ret;
    uda1341_volume = (((val & 0xff)) * 63) / 100;       //最大值为63,最小值为0

wm8976_write_reg(52, (1<<8)| uda1341_volume);
wm8976_write_reg(53, (1<<8)| uda1341_volume);
    break;


case SOUND_MIXER_READ_VOLUME:
val = (uda1341_volume * 100) / 63;       //最大值为99
     return put_user(val, (long *) arg);

  4.6修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”

  参考wm8976手册的P86页,如下图所示:
在这里插入图片描述
  其中50,51对应的就是左右混音控制寄存器

  我们以50左声道混音寄存器为例:
在这里插入图片描述
  如上图所示:

  bit8~6: 混音输入增益,默认值为0,最大值为7

  所以修改的内容如下所示:

  1)首先修改混音输入增益的初始默认值为0,如下图所示
在这里插入图片描述
  2)修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”

case SOUND_MIXER_READ_IGAIN:   //混音输入:0~100

val = (mixer_igain* 100) / 7;
return put_user(val, (int *) arg);

 

case SOUND_MIXER_WRITE_IGAIN:
ret = get_user(val, (int *) arg);
if (ret)
return ret;

mixer_igain = val * 7 / 100;
/* use mixer gain channel 1*/
wm8976_write_reg(50, mixer_igain<<6);
wm8976_write_reg(51, mixer_igain<<6);
break;

5.配置,修改内核文件

5.1 make menuconfig 配置内核

-> Device Drivers

  -> Sound

    -> Advanced Linux Sound Architecture  // 兼容OSS

      -> Advanced Linux Sound Architecture

        -> System on Chip audio support

        <*> I2S of the Samsung S3C24XX chips              //*:将/linux-2.6.22.6/sound/soc/s3c24xx下的makefile指定的文件加入内核里

5.2 将修改好的s3c-wm8976.c放入/linux-2.6.22.6/sound/soc/s3c24xx目录下

5.3修改该目录下的makefile

obj-y += s3c2410-uda1341.o

改为:

obj-y += s3c-wm8976.o  

5.4 make uImage,如下图所示,可以看到内核已经被编译

在这里插入图片描述
  最后下载并启动内核,如下图所示,可以看到该两个设备节点
在这里插入图片描述

6.测试与运行

6.1使用wav测试声卡

  wav是属于一个未经压缩的音频文件,所以可以直接调用给我们声卡播放

播放:

 cat Windows.wav > /dev/dsp

  录音(还需要修改下驱动才行):

   cat /dev/dsp > sound.bin 

//然后对着麦克风说话

   ctrl+c    //退出
   cat sound.bin > /dev/dsp  // 就可以听到录下的声音

6.2使用madplay应用程序测试声卡

  Madplay是一个根据MAD算法写的MP3播放器,而MP3属于高压缩比(11:1)的文件,所以需要madplay解码后才能给我们声卡播放,使用之前,需要先来移植madplay

  步骤如下:

  1)首先下载并解压3个文件

libid3tag-0.15.1b.tar.gz                      //mp3的解码库
libmad-0.15.1b.tar.gz                         //madplay的文件库
madplay-0.15.2b.tar.gz                      //madplay播放器的源码

  2)先创建安装目录mkdir tmp

  3)接下来先安装2个库(./configure使用参考: http://www.cnblogs.com/lifexy/p/7866453.html )

cd   libid3tag-0.15.1b/ libmad-0.15.1b                          
./configure  --host=arm-linux --prefix=/app/tmp     //修改configure,设置编译器,设置安装路径
make               //编译
make install      //安装到/app/tmp目录下

  4)最后安装madplay-0.15.2b

cd madplay-0.15.2b
./configure --host=arm-linux --prefix=/app/tmp  CFLAGS="-I/app/tmp/include" LDFLAGS="-L/app/tmp/lib"                
                                      // CFLAGS:指定头文件, LDFLAGS:指定库文件
make                                        
make install

  5)把/app/tmp/bin下的所有文件复制到开发板nfs的bin目录下,

cd /app/tmp/bin /
cp  * /work/nfs_root/bin

  6)把/app/tmp/lib下的带so文件 复制到开发板nfs的lib目录里:

cd /app/tmp/lib/
cp  *so* /work/nfs_root/lib/  -d              //带链接复制

  7)使用madplay播放mp3

 ./madplay 1.mp3 2.mp3 3.mp3         //循环播放3首歌

  并可以使用热键来控制,常用的有以下几种:

  f   下一首
  b   下一首
  i   获取播放时间和播放歌曲名
  p   播放暂停
  s   停止
  +  音量加
  -   音量减

发布了66 篇原创文章 · 获赞 1 · 访问量 1142

猜你喜欢

转载自blog.csdn.net/qq_16933601/article/details/103811195
今日推荐