2015年(109)
分类: LINUX
2015-01-23 16:37:48
原文地址:SEP4020的Linux音频驱动设计开发 作者:myleeming
为了实现mp3播放,我们最近在sep4020上完成了i2s的驱动,主要经验总结如下:
1. 首先是要在probe函数里进行一系列的初始化,这些初始化对于i2s是很重要的,而且很多
● 配置操作codec的L3的gpio口线;
L3接口相对于一个混音器控制接口,也就是对应在驱动中的mixer结构体,在这里我们需要利用3根gpio口线实现对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
*(volatile unsigned long*)PWM4_PERIOD_V =0x2; //计数时钟为总线的DIV分频
*(volatile unsigned long*)PWM4_DATA_V =0x1; //周期为两个计数时钟
*(volatile unsigned long*)PWM_ENABLE_V =0x1<<3; //高电平为一个计数时钟
● 初始化codec(UDA1341),实际这一步是和第一步配置控制L3口线一起的,配置好口线后,通过这些口线将codec的参数配置好,当然具体codec的参数要看uda1341的手册,其中的uda1341_l3_address,uda1341_l3_data是单独为其编写的函数:
*(volatile unsigned long*)(GPIO_PORTD_DATA_V) &= ~(L
*(volatile unsigned long*)(GPIO_PORTD_DATA_V) |= (L
//以下配置可能需要修改 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(0x
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 < 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_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 < s->fragsize) {
up(&b->sem);
break;
}
if((ret = s
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的独特编码(包含了心理声学模型)但是听出来的效果像是有空白间隔一样。