韦东山嵌入式Linux视频教程_3期项目实战之ALSA声卡_从零编写之数据传输(基于优龙FS2410开发板,UDA1341声卡)

一、实验环境

1.1 虚拟机环境

    a) Vmware版本:Vmware Workstation 12.5.7

    b) Ubuntu版本:9.10

    c) 内核版本:2.6.31.14

    d) toolchain版本:arm-linux-gcc 4.3.2

1.2 开发板

    优龙FS2410开发板,UDA1341声卡

    内核版本:3.4.2

二、声卡数据传输的原理(以播放为例)

image

(1) 驱动程序分配一个buffer:s2c2440_dma_new

(2) app不断写一个个period数据到buffer(appl_ptr以frame为单位) 。一个period包含多个frame,一个frame就是一个采样数据

(3) 驱动不断从buffer里取出一个period:load_dma_period,启动DMA传输:s3c2440_dma_start,发送给声卡

(4)传输完毕,产生中断):s3c2440_dma2_irq ,更新状态(hw_ptr,以frame为单位)

三、具体实现(s3c2440_dma.c)

注:内核中关于s3c24xx的DMA操作的代码框架非常复杂,暂时未仔细研究(可参考:李兰溪  S3C24XX DMA框架源码分析)。而我们自制的驱动,则简化了很多,但基本思想和流程是和内核一致的。     

准备工作

1. 定义好DMA操作相关的寄存器,并进行ioremap,以便后续的访问

#define DMA0_BASE_ADDR  0x4B000000
#define DMA1_BASE_ADDR  0x4B000040
#define DMA2_BASE_ADDR  0x4B000080
#define DMA3_BASE_ADDR  0x4B0000C0

struct s3c_dma_regs {
	unsigned long disrc;
	unsigned long disrcc;
	unsigned long didst;
	unsigned long didstc;
	unsigned long dcon;
	unsigned long dstat;
	unsigned long dcsrc;
	unsigned long dcdst;
	unsigned long dmasktrig;
};
static volatile struct s3c_dma_regs *dma_regs;

static int s3c2440_dma_init(void)
 {
	 dma_regs = ioremap(DMA2_BASE_ADDR, sizeof(struct s3c_dma_regs));
	 platform_device_register(&s3c2440_dma_dev);
	 platform_driver_register(&s3c2440_dma_drv);
	 return 0;
 }
 static void s3c2440_dma_exit(void)
 {
	 platform_device_unregister(&s3c2440_dma_dev);
	 platform_driver_unregister(&s3c2440_dma_drv);
	 iounmap(dma_regs);
 }

2. 实现几个基础函数,供后续s3c2440_dma_prepare、s3c2440_dma2_irq和s3c2440_dma_trigger调用

/* 数据传输: 源,目的,长度 */
static void load_dma_period(void)
{
    /* 把源,目的,长度告诉DMA */
    dma_regs->disrc      = playback_dma_info.phy_addr + playback_dma_info.dma_ofs;  /* 源的物理地址 */
    dma_regs->disrcc     = (0<<1) | (0<<0); /* 源位于AHB总线, 源地址递增 */
    dma_regs->didst      = 0x55000010;        /* 目的的物理地址 IIS fifo entry*/
    dma_regs->didstc     = (0<<2) | (1<<1) | (1<<0); /* 目的位于APB总线, 目的地址不变 */
    /*
	handshake mode
	DACK and DREQ are synchronized to PCLK
	Enable/Disable the interrupt setting for CURR_TC
	A unit transfer
	single service mode
	select I2SSDO of DCON2 as DMA request source
	hardware trigger DMA request
	datasize to be transfered: half word
	2(bytes)*1(unit)*initial transfer_count = len
	==>transfer_count=len/2
    */
    /* bit22: 1-noreload */
    //传输的长度(datasize是half word即2个字节):playback_dma_info.period_size/2
    dma_regs->dcon = (1<<31)|(0<<30)|(1<<29)|(0<<28)|(0<<27)|(0<<24)|(1<<23)|(1<<22)|(1<<20)|(playback_dma_info.period_size/2);
    /* 使能中断,单个传输,硬件触发 */
}
static void s3c2440_dma_start(void)
{
	/* 启动DMA */
	dma_regs->dmasktrig  = (1<<1);
}
static void s3c2440_dma_stop(void)
{
	/* 停止DMA */
	dma_regs->dmasktrig  &= ~(1<<1);
}

3.1 实现s3c2440_dma_platform.pcm_new(即s3c2440_dma_new)

(参考 sound\soc\samsung\dma.c 的 dma_new)

static int s3c2440_dma_new(struct snd_soc_pcm_runtime *rtd)
{
	 struct snd_card *card = rtd->card->snd_card;
	 struct snd_pcm *pcm = rtd->pcm;
	 struct snd_pcm_substream *substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream;
	 struct snd_dma_buffer *buf = &substream->dma_buffer;
 	/* 
           snd_dma_buffer的作用:
           在hw_params阶段,snd_soc_platform_driver的ops->hw_params会被调用,通常会使用snd_pcm_set_runtime_buffer()
           把substream->dma_buffer的值拷贝到substream->runtime的相关字段中(.dma_area, .dma_addr, .dma_bytes),
           这样以后就可以通过substream->runtime获得这些地址和大小信息了。因为有播放和录音两个substream,而runtime始终指向当前使用的substream,
           所以便于跟踪substream。
        */
	 int ret = 0;

	 /* 1. 分配DMA BUFFER */
	 if (!card->dev->dma_mask)  //这段代码,是后来调试时发现,必须要加的
		 card->dev->dma_mask = &dma_mask;
	 if (!card->dev->coherent_dma_mask)
		 card->dev->coherent_dma_mask = DMA_BIT_MASK(32);

	 if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
		 playback_dma_info.virt_addr = (unsigned int)dma_alloc_writecombine(pcm->card->dev, s3c2440_dma_hardware.buffer_bytes_max,
						&playback_dma_info.phy_addr, GFP_KERNEL);
		 if (!playback_dma_info.virt_addr)
		 {
			 return -ENOMEM;
		 }
		 playback_dma_info.buf_max_size = s3c2440_dma_hardware.buffer_bytes_max;

		 buf->dev.type = SNDRV_DMA_TYPE_DEV;
		 buf->dev.dev = pcm->card->dev;
		 buf->private_data = NULL;
		 buf->area = (unsigned char *)playback_dma_info.virt_addr; //这句话是后来调试时,才发现需要加的
		 buf->bytes = playback_dma_info.buf_max_size;
		 buf->addr = playback_dma_info.phy_addr;
	 }

	 return ret;

        //为了简化,先去掉录音功能 
}

3.2 实现s3c2440_dma_platform.ops.open(即s3c2440_dma_open)

(参考 sound\soc\samsung\dma.c 的 dma_open)

static const struct snd_pcm_hardware s3c2440_dma_hardware = {
	.info			= SNDRV_PCM_INFO_INTERLEAVED |
					SNDRV_PCM_INFO_BLOCK_TRANSFER |
					SNDRV_PCM_INFO_MMAP |
					SNDRV_PCM_INFO_MMAP_VALID |
					SNDRV_PCM_INFO_PAUSE |
					SNDRV_PCM_INFO_RESUME,
	.formats		= SNDRV_PCM_FMTBIT_S16_LE |
					SNDRV_PCM_FMTBIT_U16_LE |
					SNDRV_PCM_FMTBIT_U8 |
					SNDRV_PCM_FMTBIT_S8,
	.channels_min		= 2,
	.channels_max		= 2,
	.buffer_bytes_max	= 128*1024, //在s3c2440_dma_new里被用于指定dma_alloc_writecombine的size参数
	.period_bytes_min	= PAGE_SIZE,
	.period_bytes_max	= PAGE_SIZE*2,
	.periods_min		= 2,
	.periods_max		= 128,
	.fifo_size		= 32,
};
//目前只支持播放 
static int s3c2440_dma_open(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    int ret;
    /* 设置属性 */
    snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); //约束:periods必须是整数
    snd_soc_set_runtime_hwparams(substream, &s3c2440_dma_hardware);
    /*
        snd_soc_set_runtime_hwparams的作用: 把s3c2440_dma_hardware的各个属性赋给substream->runtime->hw,
        后续在snd_pcm_open_file==>snd_pcm_open_substream==>snd_pcm_hw_constraints_complete==>
        snd_pcm_hw_constraint_minmax里会调用诸如snd_pcm_hw_constraint_minmax(runtime, 
        SNDRV_PCM_HW_PARAM_CHANNELS, hw->channels_min, hw->channels_max);
    */
/* 注册中断 */
    ret = request_irq(IRQ_DMA2, s3c2440_dma2_irq, IRQF_DISABLED, "myalsa for playback", substream);
    if (ret)
    {
        printk("request_irq error!\n");
        return -EIO;
    }
	return 0;
}
static int s3c2440_dma_close(struct snd_pcm_substream *substream)
{
    free_irq(IRQ_DMA2, substream);
    return 0;
}

3.3 实现s3c2440_dma_platform.ops. hw_params(即s3c2440_dma_hw_params)

(参考 sound\soc\samsung\dma.c 的 dma_hw_params)

static int s3c2440_dma_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    unsigned long totbytes = params_buffer_bytes(params);

    /* 根据params设置DMA */
    /* 关于snd_pcm_set_runtime_buffer的作用,可看上文s3c2440_dma_new 关于snd_dma_buffer的注释*/
    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);

    /* 
       s3c2440_dma_new分配了很大的DMA BUFFER,而dma_bytes表明app决定使用多大
       runtime->dma_bytes被snd_pcm_lib_readv_transfer和snd_pcm_lib_writev_transfer用来和App之间传输数据
     */
    runtime->dma_bytes            = totbytes;
    playback_dma_info.buffer_size = totbytes;
    playback_dma_info.period_size = params_period_bytes(params); //记录了app在每个period里传输的数据大小(单位:byte),一个period里包含多个frame 
    return 0;
}

3.4 实现s3c2440_dma_prepare

(参考 sound\soc\samsung\dma.c 的 dma_prepare)

static int s3c2440_dma_prepare(struct snd_pcm_substream *substream)
{
    /* 准备DMA传输 */
    /* 复位各种状态信息 */
    playback_dma_info.dma_ofs = 0;
    playback_dma_info.be_running = 0;
    /* 加载第1个period */
    load_dma_period(); //仿照裸板程序的dma_init()
	return 0;
}

3.5 实现s3c2440_dma_trigger

(参考 sound\soc\samsung\dma.c 的 dma_trigger)

static int s3c2440_dma_trigger(struct snd_pcm_substream *substream, int cmd)
{
	int ret = 0;
    /* 根据cmd启动或停止DMA传输 */
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        /* 启动DMA传输 */
        playback_dma_info.be_running = 1;
        s3c2440_dma_start();
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        /* 停止DMA传输 */
        playback_dma_info.be_running = 0;
        s3c2440_dma_stop();
		break;
	default:
		ret = -EINVAL;
		break;
	}
	return ret;
}

3.6 实现s3c2440_dma2_irq

(参考 sound\soc\samsung\dma.c 的 audio_buffdone)

static irqreturn_t s3c2440_dma2_irq(int irq, void *devid)
{
    struct snd_pcm_substream *substream = devid;
    /* 更新状态信息 */
    playback_dma_info.dma_ofs += playback_dma_info.period_size;
    if (playback_dma_info.dma_ofs >= playback_dma_info.buffer_size) // buffer_size来自于params_buffer_bytes(params),即App需要使用的缓冲区大小
        playback_dma_info.dma_ofs = 0; //如果当前DMA缓冲区中已传输的位置,超出了playback_dma_info.buffer_size,那么回零
    /* 更新hw_ptr等信息,
     * 并且判断:如果buffer里没有数据了,则调用trigger来停止DMA
     */
     snd_pcm_period_elapsed(substream);
     /*
        snd_pcm_period_elapsed为了查询当前已传输的DMA数据在ring_buffer中的位置,会调用snd_pcm_update_hw_ptr0==>substream->ops->pointer(即soc_pcm_pointer) ==>
        platform->driver->ops->pointer
        所以我们必须提供这个函数(见下文s3c2440_dma_pointer)
     */
     if (playback_dma_info.be_running)
     {
        /* 如果还有数据
         * 1. 加载下一个period
         * 2. 再次启动DMA传输
         */
        load_dma_period();
        s3c2440_dma_start();
    }
}

3.7 实现s3c2440_dma_pointer

(参考 sound\soc\samsung\dma.c 的 dma_pointer)

static snd_pcm_uframes_t s3c2440_dma_pointer(struct snd_pcm_substream *substream)
{
	unsigned long res;
	res = playback_dma_info.dma_ofs; //注:从上文s3c2440_dma2_irq可以看出,playback_dma_info.dma_ofs是以playback_dma_info.buffer_size(即App需要使用的缓冲区大小)为边界的
	/* we seem to be getting the odd error from the pcm library due
	 * to out-of-bounds pointers. this is maybe due to the dma engine
	 * not having loaded the new values for the channel before being
	 * called... (todo - fix )
	 */
	if (res >= snd_pcm_lib_buffer_bytes(substream)) {
		if (res == snd_pcm_lib_buffer_bytes(substream))
			res = 0;
	}
        //snd_pcm_update_hw_ptr0需要我们返回以frame为单位的当前DMA缓冲区中已传输的位置
	return bytes_to_frames(substream->runtime, res);
}

四、总结

1. 驱动分配DMA缓冲区

    soc_probe

        snd_soc_register_card

            snd_soc_instantiate_cards

                 snd_soc_instantiate_card

                      soc_probe_dai_link

                           soc_new_pcm

                                s3c2440_dma_platform. pcm_new (即s3c2440_dma_new)

2. app调open,最终调用到s3c2440_dma_open

    snd_soc_set_runtime_hwparams(substream, &s3c2440_dma_hardware); //设置属性

    request_irq 注册中断

3. app 调用ioctl(SNDRV_PCM_IOCTL_HW_PARAMS),最终调用s3c2440_dma_hw_params

      根据params设置DMA

4. app调ioctl(SNDRV_PCM_IOCTL_PREPARE),最终调用s3c2440_dma_prepare

    /* 复位各种状态信息 */

    /* 加载第1个period */

    load_dma_period();

5. app调用ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES)

    把数据传到DMA缓冲区,

    启动传输,最终调用s3c2440_dma_trigger(SNDRV_PCM_TRIGGER_START)启动DMA传输

6. 传输完一个period后,会触发中断,进入s3c2440_dma2_irq

   /* 更新hw_ptr等信息,

    * 并且判断:如果buffer里没有数据了,则调用trigger来停止DMA

    * 如果还有数据,则:

    * 1. 加载下一个period

    * 2. 再次启动DMA传输

    */

五、参考资料

1. 韦东山 嵌入式Linux视频教程_3期项目实战之ALSA声卡:第2课第1.1_17节_ALSA声卡10_从零编写之数据传输

2. DroidPhone 《Linux ALSA 声卡驱动》

3. 李兰溪  S3C24XX DMA框架源码分析

posted @ 2020-02-18 23:02  normalmanzhao2003  阅读(515)  评论(0编辑  收藏  举报
levels of contents