免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1461 | 回复: 0
打印 上一主题 下一主题

SEP4020的Linux音频驱动设计开发 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2009-06-22 12:40 |只看该作者 |倒序浏览

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

1.       首先是要在probe函数里进行一系列的初始化,这些初始化对于i2s是很重要的,而且很多
●     配置操作codec的L3的gpio口线;
L3接口相对于一个混音器控制接口,也就是对应在驱动中的mixer结构体,在这里我们需要利用3根gpio口线实现对L3的控制,以下是初始化代码:
*(volatile unsigned long*)(GPIO_PORTD_DIR_V) &= ~(0xd       //GPB[4:1]=00_0 Output(L3CLOCK):Output(L3DATA):Output(L3MODE)   

*(volatile unsigned long*)(GPIO_PORTD_SEL_V) |= (0xd      
//GPD[4:1] 1 1010
●     配置端口为放音功能,因为sep4020只支持单独放音和录音,不能全双工,因此我们在这里配置为放音,是通过一个口线置高置低实现的,具体代码:
       *(volatile unsigned long*)(GPIO_PORTG_DIR_V) &= ~(0x1
*(volatile unsigned long*)(GPIO_PORTG_SEL_V) |= 0x1
       *(volatile unsigned long*)(GPIO_PORTG_DATA_V) |= 0x1
●     配置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     //高电平为一个计数时钟
●     初始化codec(UDA1341),实际这一步是和第一步配置控制L3口线一起的,配置好口线后,通过这些口线将codec的参数配置好,当然具体codec的参数要看uda1341的手册,其中的uda1341_l3_address,uda1341_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接受数据,并传递给上层的功能(录音)。这部分中又以write,read函数最为重要,ioctl可以沿用别人的,因此我们的主要工作也是集中在write,read函数上。

而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 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_fragsize的buffer环,

●信号量的思想:
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 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)
         *(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的独特编码(包含了心理声学模型)但是听出来的效果像是有空白间隔一样。


本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u3/99423/showart_1972927.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP