Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1006727
  • 博文数量: 153
  • 博客积分: 4195
  • 博客等级: 上校
  • 技术积分: 2631
  • 用 户 组: 普通用户
  • 注册时间: 2009-06-22 11:32
文章存档

2012年(7)

2010年(35)

2009年(111)

分类: LINUX

2009-06-22 12:40:53

为了实现mp3播放,我们最近在sep4020上完成了i2s的驱动,主要经验总结如下:

 

1.       首先是要在probe函数里进行一系列的初始化,这些初始化对于i2s是很重要的,而且很多

     配置操作codecL3gpio口线;

L3接口相对于一个混音器控制接口,也就是对应在驱动中的mixer结构体,在这里我们需要利用3gpio口线实现对L3的控制,以下是初始化代码:

*(volatile unsigned long*)(GPIO_PORTD_DIR_V) &= ~(0xd<<1);       //GPB[4:1]=00_0 Output(L3CLOCK):Output(L3DATA):Output(L3MODE)  

 

*(volatile unsigned long*)(GPIO_PORTD_SEL_V) |= (0xd<<1);      

 //GPD[4:1] 1 1010

     配置端口为放音功能,因为sep4020只支持单独放音和录音,不能全双工,因此我们在这里配置为放音,是通过一个口线置高置低实现的,具体代码:

       *(volatile unsigned long*)(GPIO_PORTG_DIR_V) &= ~(0x1<<11);

*(volatile unsigned long*)(GPIO_PORTG_SEL_V) |= 0x1<<11;

       *(volatile unsigned long*)(GPIO_PORTG_DATA_V) |= 0x1<<11;

     配置pwm,实现对codec时钟的供给:

*(volatile unsigned long*)PWM4_CTRL_V =0x00;

*(volatile unsigned long*)PWM4_DIV_V =0x4; //88MHz/(4*2)=11Mhz 11M/256fs=42.96k

 *(volatile unsigned long*)PWM4_PERIOD_V =0x2;   //计数时钟为总线的DIV分频

 *(volatile unsigned long*)PWM4_DATA_V =0x1;     //周期为两个计数时钟

 *(volatile unsigned long*)PWM_ENABLE_V =0x1<<3;     //高电平为一个计数时钟

     初始化codecUDA1341,实际这一步是和第一步配置控制L3口线一起的,配置好口线后,通过这些口线将codec的参数配置好,当然具体codec的参数要看uda1341的手册,其中的uda1341_l3_addressuda1341_l3_data是单独为其编写的函数:

*(volatile unsigned long*)(GPIO_PORTD_DATA_V) &= ~(L3M|L3C|L3D);

*(volatile unsigned long*)(GPIO_PORTD_DATA_V) |= (L3M|L3C); //Start condition : L3M=H, L3C=H

//以下配置可能需要修改 marked at 11-08

uda1341_l3_address(0x14 + 2);

uda1341_l3_data(0x61);               //1110 dc-filtering开不开无所谓 不能像三星的选成MSB

uda1341_l3_address(0x14 + 2);

uda1341_l3_data(0x21);

uda1341_l3_address(0x14 + 2);

uda1341_l3_data(0xc1);        //Status 1,Gain of DAC 6 dB,Gain of ADC 0dB,ADC non-inverting,DAC non-inverting,Single speed playback,ADC-Off DAC-On

              uda1341_l3_address(0x14 + 0);

uda1341_l3_data(0x0f);        //00,00 ffff  : Volume control (6 bits)  -14dB

              uda1341_l3_address(0x14 + 0);

uda1341_l3_data(0x7b);        //01,11 10,11 : Data0, Bass Boost 18~24dB, Treble 6dB

              uda1341_l3_address(0x14 + 0);

uda1341_l3_data(0x83);

     配置dma,主要实现了对dma通道的使能,清除中断标志位,具体对dma的缓冲区分配等会在使用dma之前的一个dmasetup函数中实现,并且有对应的dmaclear清除缓冲区。

 

2.       音频驱动的audio结构体,和mixer结构体

在音频驱动中主要就是实现这两个结构体的operation函数:

static struct file_operations sep4020_audio_fops = {

llseek: sep4020_audio_llseek,

write: sep4020_audio_write,

read: sep4020_audio_read,

poll: sep4020_audio_poll,

ioctl: sep4020_audio_ioctl,

open: sep4020_audio_open,

release: sep4020_audio_release

};

 

static struct file_operations sep4020_mixer_fops = {

ioctl: sep4020_mixer_ioctl,

open: sep4020_mixer_open,

release: sep4020_mixer_release

};

sep4020_audio_fops这个结构体主要实现了i2s控制器的操作,包括读写,控制,查询(poll),打开,释放等等。Audio主要实现了接受上层应用数据,并将数据传递给codec进行播放(放音);从codec接受数据,并传递给上层的功能(录音)。这部分中又以writeread函数最为重要,ioctl可以沿用别人的,因此我们的主要工作也是集中在writeread函数上。

 

sep4020_mixer_fops则主要实现了对codec参数的配置,我们也可以很清晰的看到它的operation结构体中只有控制函数,没有读写。并且由于codec的通用性,这部分的代码基本上可以沿用别人的,如2410

 

3.       关于sep4020_audio_write函数:

这个是整个驱动的核心,也是难点,牵涉了dma操作,buffer ring的思想,linux中信号量的思想。一下内容读起来会有点吃力,请好好理解代码

●关于dma

dma的操作,在这里使用了一个buffer ring的思想,这里我们来看一下建立dma缓冲环的代码来理解这种buffer ring

static int audio_setup_buf(audio_stream_t * s)

{

int frag;

int dmasize = 0;

char *dmabuf = 0;

dma_addr_t dmaphys = 0;

 

if (s->buffers)

return -EBUSY;

 

s->nbfrags = audio_nbfrags;     //音频缓冲区片数量

s->fragsize = audio_fragsize;   //音频缓冲区片大小

 

s->buffers = (audio_buf_t *)   //buffers是整体缓冲区,它是一次性分配的

kmalloc(sizeof(audio_buf_t) * s->nbfrags, GFP_KERNEL);//分配nbfrags*audio_buf_t结构体大小的空间

if (!s->buffers)

goto err;//没有分配成功,跳至err

memset(s->buffers, 0, sizeof(audio_buf_t) * s->nbfrags);//将整体缓冲区置0

for (frag = 0; frag < s->nbfrags; frag++) //初始化每个缓冲片区

{

audio_buf_t *b = &s->buffers[frag];

 

if (!dmasize) //其实这个if语句只执行了一次,

{

dmasize = (s->nbfrags - frag) * s->fragsize;

do {

dmabuf = dma_alloc_coherent(NULL, dmasize, &dmaphys, GFP_KERNEL|GFP_DMA);

if (!dmabuf)

dmasize -= s->fragsize;

} while (!dmabuf && dmasize);

if (!dmabuf)

goto err;

b->master = dmasize;//给每个缓冲区赋大小

}

 

b->start = dmabuf;

b->dma_addr = dmaphys;

sema_init(&b->sem, 1);//初始化信号量

DPRINTK("buf %d: start %p dma %d\n", frag, b->start, b->dma_addr);

 

dmabuf += s->fragsize;

dmaphys += s->fragsize;

dmasize -= s->fragsize;//这三条实现了给每个片赋物理地址,虚拟地址

}

 

s->buf_idx = 0;

s->buf = &s->buffers[0];//这两句实现了缓冲区的环形,buffers是整个缓冲区,buffer是当前使用的缓冲区

return 0;

 

err:

printk(AUDIO_NAME ": unable to allocate audio memory\n ");

audio_clear_buf(s);

return -ENOMEM;

}

这样就实现了audio_nbfrags个大小为audio_fragsizebuffer环,

 

●信号量的思想:

typedef struct {

int size; /* buffer size */

char *start; /* point to actual buffer */

dma_addr_t dma_addr; /* physical buffer address */

struct semaphore sem; /* down before touching the buffer */

int master; /* owner for buffer allocation, contain size when true */

} audio_buf_t;

在对于每个单独的音频缓冲片中都定义了一个信号量sem,就是通过对这个sem信号量的获取和释放来管理缓冲区。

 

首先在初始化缓冲片的时候将信号量初始化为1,就是可用,然后对每个缓冲片使用的时候,先看是否能获得信号量,如果不能获得信号量说明这片缓冲区还在使用不能对其重新写,如果能获得信号量说明这片缓冲区已经释放可以继续使用;

缓冲区的释放是在dma的中断中实现的,在dma每次传输完成后会发出一个完成中断,在这个中断的处理函数里面释放信号量;

由于三星为对于dma的操作写了一系列标准化的接口,对于我们来说甚至有点复杂和啰嗦了,我们在这里采用了一种简便的实现方法(也可以说是偷懒,呵呵),在dma寄存器配置好后,立刻释放信号量,因为我们有8片缓冲区,这次用完了还有7片可以用,在这个时间段里面,dma肯定搬运结束了。

 

     整个写函数的实现:

先来看一下2410的写函数是怎么实现的:

整个流程可以分析为,

     上层给写函数传递一个要放音的数据包,首地址为&buffer,大小为count

     函数根据缓冲区的大小给缓冲区填充数据,如果缓冲区的大小大于传递的包,则先传输一次包的量,如果缓冲区的待续哦啊小于传输的包,则先传输一次缓冲区的数据量,无论怎样,都要保证开的缓冲区的所有空间被填满,(因为一旦没有填满,就会有空白区间,对应于音频输出就是有停顿了)。

     把上层传递过来的数据包拷贝至dma缓冲区

     将这片缓冲区放进dma队列使用,并使用下一片缓冲片

static ssize_t smdk2410_audio_write(struct file *file, const char *buffer,

size_t count, loff_t * ppos)

{

  const char *buffer0 = buffer; //此处定义buffer0,主要是判断结束条件的

  audio_stream_t *s = &output_stream;

  int chunksize, ret = 0;

  DPRINTK("audio_write : start count=%d\n", count);

  switch (file->f_flags & O_ACCMODE) {

         case O_WRONLY:

         case O_RDWR:

         break;

         default:

                return -EPERM;

  }

 

  if (!s->buffers && audio_setup_buf(s))

         return -ENOMEM;

  count &= ~0x03;

  while (count > 0) {

         audio_buf_t *b = s->buf;//注意:一下b都是s的一个缓冲片

         if (file->f_flags & O_NONBLOCK) {

                ret = -EAGAIN;

                if (down_trylock(&b->sem))

                       break;

         } else {

                ret = -ERESTARTSYS;

                //一般程序都是使用阻塞的,这句信号量的使用是为了

                //保证单独缓冲片已可以再次使用,在完成了对一片缓冲片

                //的使用之后,会up这片,来说明可以再次使用

                if (down_interruptible(&b->sem)) 

                break;

         }

//貌似audio_channel是双声道的意思?

         if (audio_channels == 2) {

                chunksize = s->fragsize - b->size;//刚开始b->size is 0 ,

                if (chunksize > count)

                chunksize = count;

                DPRINTK("write %d to %d\n", chunksize, s->buf_idx);

                if (copy_from_user(b->start + b->size, buffer, chunksize)) {

                       up(&b->sem);

                       return -EFAULT;

                }

                b->size += chunksize;//at this step,bsize is equal to the  chunksize

         } else {

                chunksize = (s->fragsize - b->size) >> 1;

                if (chunksize > count)

                chunksize = count;

                DPRINTK("write %d to %d\n", chunksize*2, s->buf_idx);

                if (copy_from_user_mono_stereo(b->start + b->size,

buffer, chunksize)) {

                up(&b->sem);

                return -EFAULT;

                }

                b->size += chunksize*2;

         }

         buffer += chunksize;//将要传的字符流往后滑动chunksize

         count -= chunksize;//将要传的总数减去已传的chunksize

 

         //如果这次填充缓冲片没有填满,会释放这一片的信号量,

         //继续对这一片进行操作,直到填满开的缓冲片大小为止,

         //保证了缓冲片内不会有空白区间,这对音频是致命的

         if (b->size < s->fragsize) {

         up(&b->sem);

         break;

         }

         if((ret = s3c2410_dma_enqueue(s->dma_ch, (void *) b, b->dma_addr, b->size))) {

                printk(PFX"dma enqueue failed.\n");

                return ret;

         }

         b->size = 0;

         NEXT_BUF(s, buf);

  }

  //buffer0是要传数据的首地址,固定不动的,buffer是现在传的地址

  //通过buffer-buffer0,可以得到传了多少,只有在返回0的时候是表明

  //这次传递的包全部传完,不然系统应该通过ret得到断点处并继续

  if ((buffer - buffer0))

  ret = buffer - buffer0;

  DPRINTK("audio_write : end count=%d\n\n", ret);

  return ret;

}

对应于sep4020的操作:

这里可以看到整个函数都比较好利用并按照我们芯片的规则实现,但是在dma队列这一块由于三星2410使用了大量的dma标准操作,我们实现起来比较困难,因此我们采取我们自己的实现方式,效果同样很好,但是简单了很多。

 

首先分析:什么是dma队列,dma队列的作用就是将写函数分为了两个独立的环节,★一个是不停的从上层接受数据包,并放到空闲的缓冲片里面;

★一个是不同的通过dma通道将缓冲片的数据搬运到i2s数据通道里。

所谓的队列就是保证以上两个环节并行操作,并且让dma时刻有数据搬运。

 

那对于我们没有这种标准函数的怎么写?怎么通过其他方式实现队列?

因此我们在配置好dma寄存器后,立即进行下一片缓冲片的搬运,此时dma也在同时向i2s数据通道写数据,——实现了并行操作,无等待。

而根据实验测试,dma搬运的速度远远小于从上层拷贝到缓冲片的速度,也就是说我在准备好下一片缓冲片的时候,dma肯定还没有搬好,那我只需要等待dma搬好,并把下一片的缓冲数据给他,让他继续搬运,——实现了dma的无缝持续搬运。

 

具体代码实现:(把三星代码中红色的部分替换为一下部分)

while(test)//设置一个全局变量,在系统的第一次不用查询,以后每次都要查询

         {

//如果中断完成状态为1,表示已经搬运完,

                if(((*(volatile unsigned long*)DMAC_INTTCSTATUS_V) & 0x2 ) == 0x2) break;

         }

         //清除要用的dma通道(dma1)上的传输完成中断寄存器

         *(volatile unsigned long*)(DMAC_INTTCCLEAR_V) = 0x2;

         *(volatile unsigned long*)(DMAC_INTTCCLEAR_V) = 0x0;    

         test = 1;

//继续下一次搬运

         *(volatile unsigned long*)DMAC_C1CONTROL_V  = ((8192>>2)<<14) + (1<<12) + (2<<9) +(2<<6) + (3<<3) + 3;

         *(volatile unsigned long*)DMAC_C1SRCADDR_V  = b->dma_addr;

               *(volatile unsigned long*)DMAC_C1DESTADDR_V  = I2S_DATA ;

               *(volatile unsigned long*)DMAC_C1CONFIGURATION_V = 0x200b ; //iis

        

//同时释放信号量,让两个环节并行

         up(&b->sem); //add 1110

         wake_up(&b->sem.wait);

 

4.       关于写i2s的一些小注意点:

在写音频驱动的时候很容易碰上得到的音乐有不断的“嗒嗒”声音,一般的想肯定是认为数据传输不及时,导致有空白间隔,所以出现这种貌似停顿的声音,但事实上是如果音乐数据有数据遗漏也会出现这种声音,比如你这次给了一段连续的数据,但是紧接着跳到之后600个字节的地方,虽然数据流是连续的,但是由于mp3的独特编码(包含了心理声学模型)但是听出来的效果像是有空白间隔一样。

阅读(1941) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~