linux音频pcm buffer的管理和DMA

linux音频pcm buffer的管理和DMA

buffer管理

​ 当app在调用snd_pcm_writei时,alsa core将app传来的数据搬到HW buffer(即DMA buffer)中,alsa driver从HW buffer中读取数据传输到硬件播放。

​ ALSA buffer是采用ring buffer来实现的。ring buffer有多个HW buffer组成。

​ HW buffer一般是在alsa driver的hw_params函数中分配的一块大小为buffer size的DMA buffer.

之所以采用多个HW buffer来组成ring buffer,是防止读写指针的前后位置频繁的互换(即写指针到达HW buffer边界时,就要回到HW buffer起始点)。

ring buffer = n * HW buffer.通常这个n比较大,在数据读写的过程中,很少会出现读写指针互换的情况。

​ 下图是ALSA buffer的实现以及读写指针更新的方法,

在这里插入图片描述

hw_ptr_base是当前HW buffer在Ring buffer中的起始位置。当读指针到达HW buffer尾部时,hw_ptr_base按buffer size移动.

hw_ptr即HW buffer的读指针。alsa driver将数据从HW buffer中读走并送到声卡硬件时,hw_ptr就会移动到新位置。

appl_ptr即HW buffer的写指针。app在调用snd_pcm_write写数据,alsa core将数据copy到HW buffer后,appl_ptr就更新。

boundary即Ring buffer边界。

hw_ofs是读指针在当前HW buffer中的位置。由alsa driver的pointer()返回。

appl_ofs是写指针在当前HW buffer中的位置。

hw_ptr的更新是通过调用snd_pcm_update_hw_ptr0完成。此函数在app写数据时会调用,也会在硬件中断时通过snd_pcm_peroid_elapsed调用。

snd_pcm_update_hw_ptr0

static int snd_pcm_update_hw_ptr0(struct snd_pcm_substream *substream,
				  unsigned int in_interrupt)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	snd_pcm_uframes_t pos;
	snd_pcm_uframes_t old_hw_ptr, new_hw_ptr, hw_base;
	snd_pcm_sframes_t hdelta, delta;
	unsigned long jdelta;
	unsigned long curr_jiffies;
	struct timespec curr_tstamp;
	struct timespec audio_tstamp;
	int crossed_boundary = 0;

	old_hw_ptr = runtime->status->hw_ptr;
	
	/*
	 * group pointer, time and jiffies reads to allow for more
	 * accurate correlations/corrections.
	 * 校正hw_ptr位置后,这些值将存储在routine的末尾
	 */
	pos = substream->ops->pointer(substream);//获取hw_ptr在当前HW buffer中的偏移
	curr_jiffies = jiffies;
	if (runtime->tstamp_mode == SNDRV_PCM_TSTAMP_ENABLE) {//获取当前的time stamp
		if ((substream->ops->get_time_info) &&
			(runtime->audio_tstamp_config.type_requested != SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)) {
			substream->ops->get_time_info(substream, &curr_tstamp,
						&audio_tstamp,
						&runtime->audio_tstamp_config,
						&runtime->audio_tstamp_report);
	
			/* 重新测试以防硬件中不支持tstamp类型并将其降级为DEFAULT */
			if (runtime->audio_tstamp_report.actual_type == SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT)
				snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
		} else
			snd_pcm_gettime(runtime, (struct timespec *)&curr_tstamp);
	}
	
	if (pos == SNDRV_PCM_POS_XRUN) { //发生XRUN
		__snd_pcm_xrun(substream);
		return -EPIPE;
	}
	if (pos >= runtime->buffer_size) { //pos大于buffer size,出现异常
		if (printk_ratelimit()) {
			char name[16];
			snd_pcm_debug_name(substream, name, sizeof(name));
			pcm_err(substream->pcm,
				"invalid position: %s, pos = %ld, buffer size = %ld, period size = %ld\n",
				name, pos, runtime->buffer_size,
				runtime->period_size);
		}
		pos = 0;
	}
	pos -= pos % runtime->min_align;
	trace_hwptr(substream, pos, in_interrupt);
	hw_base = runtime->hw_ptr_base; //当前的hw_base
	new_hw_ptr = hw_base + pos; ;//当前的hw_ptr
	if (in_interrupt) {
		/* 我们知道一个period已被处理 */
		/* delta = "expected next hw_ptr" for in_interrupt != 0 */
		delta = runtime->hw_ptr_interrupt + runtime->period_size;
        //如果本次通过中断位置加上period_size计算出来的hw_ptr比当前hw_ptr大的话,
        //则说明上一次中断没处理,有可能hw_base需要更新到下一个HW buffer的基地址。
		if (delta > new_hw_ptr) {
			/* check for double acknowledged interrupts */
			hdelta = curr_jiffies - runtime->hw_ptr_jiffies;
            //距离上一次的jiffies大于整个buffer 的jiffies的一半。
			if (hdelta > runtime->hw_ptr_buffer_jiffies/2 + 1) { 
				hw_base += runtime->buffer_size;
				if (hw_base >= runtime->boundary) {
					hw_base = 0;
					crossed_boundary++;
				}
				new_hw_ptr = hw_base + pos;
				goto __delta;
			}
		}
	}
	/* 如果指针越过ring buffer的末端,则new_hw_ptr可能低于old_hw_ptr */
    //hw_base需要更新到下一个HW buffer的基地址。hw_ptr也要同步更新。
	if (new_hw_ptr < old_hw_ptr) {
		hw_base += runtime->buffer_size;
        //如果hw_base > boundary,那hw_base回跳到ring Buffer起始位置。
		if (hw_base >= runtime->boundary) { 
			hw_base = 0;
			crossed_boundary++;
		}
		new_hw_ptr = hw_base + pos;
	}
	  __delta:
	delta = new_hw_ptr - old_hw_ptr;
    //如果当前的hw_ptr任然比上一的hw_ptr小,说明hw_ptr走完了ring buffer一圈。
	if (delta < 0)
		delta += runtime->boundary;
 
	//如果硬件处理完一个peroid数据后不产生中断,就不更新hw_ptr
	if (runtime->no_period_wakeup) {
		snd_pcm_sframes_t xrun_threshold;
		/* 如果没有定期中断,我们必须检查the elapsed time以检测xruns. */
		jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
		if (jdelta < runtime->hw_ptr_buffer_jiffies / 2)
			goto no_delta_check;
		hdelta = jdelta - delta * HZ / runtime->rate;
		xrun_threshold = runtime->hw_ptr_buffer_jiffies / 2 + 1;
		while (hdelta > xrun_threshold) {
			delta += runtime->buffer_size;
			hw_base += runtime->buffer_size;
			if (hw_base >= runtime->boundary) {
				hw_base = 0;
				crossed_boundary++;
			}
			new_hw_ptr = hw_base + pos;
			hdelta -= runtime->hw_ptr_buffer_jiffies;
		}
		goto no_delta_check;
	}
	
	/* something must be really wrong */
    //如果当前hw_ptr比较上一次相差buffer size + peroid size,说明有错误。
	if (delta >= runtime->buffer_size + runtime->period_size) {
		hw_ptr_error(substream, in_interrupt, "Unexpected hw_ptr",
			     "(stream=%i, pos=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
			     substream->stream, (long)pos,
			     (long)new_hw_ptr, (long)old_hw_ptr);
		return 0;
	}
	
	/* Do jiffies check only in xrun_debug mode */
	if (!xrun_debug(substream, XRUN_DEBUG_JIFFIESCHECK))
		goto no_jiffies_check;
	
	/* Skip the jiffies check for hardwares with BATCH flag.
	 * Such hardware usually just increases the position at each IRQ,
	 * thus it can't give any strange position.
	 */
	if (runtime->hw.info & SNDRV_PCM_INFO_BATCH)
		goto no_jiffies_check;
	hdelta = delta;
	if (hdelta < runtime->delay)
		goto no_jiffies_check;
	hdelta -= runtime->delay;
	jdelta = curr_jiffies - runtime->hw_ptr_jiffies;
	if (((hdelta * HZ) / runtime->rate) > jdelta + HZ/100) {
		delta = jdelta /
			(((runtime->period_size * HZ) / runtime->rate)
								+ HZ/100);
		/* move new_hw_ptr according jiffies not pos variable */
		new_hw_ptr = old_hw_ptr;
		hw_base = delta;
		/* use loop to avoid checks for delta overflows */
		/* the delta value is small or zero in most cases */
		while (delta > 0) {
			new_hw_ptr += runtime->period_size;
			if (new_hw_ptr >= runtime->boundary) {
				new_hw_ptr -= runtime->boundary;
				crossed_boundary--;
			}
			delta--;
		}
		/* 将hw_base与buffer_size对齐 */
		hw_ptr_error(substream, in_interrupt, "hw_ptr skipping",
			     "(pos=%ld, delta=%ld, period=%ld, jdelta=%lu/%lu/%lu, hw_ptr=%ld/%ld)\n",
			     (long)pos, (long)hdelta,
			     (long)runtime->period_size, jdelta,
			     ((hdelta * HZ) / runtime->rate), hw_base,
			     (unsigned long)old_hw_ptr,
			     (unsigned long)new_hw_ptr);
		/* 将值重置为正确状态 */
		delta = 0;
		hw_base = new_hw_ptr - (new_hw_ptr % runtime->buffer_size);
	}

 no_jiffies_check:
    //interupt丢失,delta(如果当前hw_ptr比较上一次之差)>1.5个peroid size
	if (delta > runtime->period_size + runtime->period_size / 2) {
		hw_ptr_error(substream, in_interrupt,
			     "Lost interrupts?",
			     "(stream=%i, delta=%ld, new_hw_ptr=%ld, old_hw_ptr=%ld)\n",
			     substream->stream, (long)delta,
			     (long)new_hw_ptr,
			     (long)old_hw_ptr);
	}

 no_delta_check:
	if (runtime->status->hw_ptr == new_hw_ptr) { //hw_ptr没变化
		update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
		return 0;
	}

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
	    runtime->silence_size > 0)
		snd_pcm_playback_silence(substream, new_hw_ptr); //播放silence
	
	if (in_interrupt) { //更新hw_ptr_interrupt
		delta = new_hw_ptr - runtime->hw_ptr_interrupt;
		if (delta < 0)
			delta += runtime->boundary;
		delta -= (snd_pcm_uframes_t)delta % runtime->period_size;
		runtime->hw_ptr_interrupt += delta;
		if (runtime->hw_ptr_interrupt >= runtime->boundary)
			runtime->hw_ptr_interrupt -= runtime->boundary;
	}
	runtime->hw_ptr_base = hw_base;  //将更新后的值保存到runtime中
	runtime->status->hw_ptr = new_hw_ptr;
	runtime->hw_ptr_jiffies = curr_jiffies;
	if (crossed_boundary) {
		snd_BUG_ON(crossed_boundary != 1);
		runtime->hw_ptr_wrap += runtime->boundary;
	}
	
	update_audio_tstamp(substream, &curr_tstamp, &audio_tstamp);
    
	//通过本次更新的 件指针,检查是否XRUN,或唤醒等待在他上面的队列如poll,lib_write1
	return snd_pcm_update_state(substream, runtime);
}

__snd_pcm_lib_xfer [pcm_lib.c] 【重点】

snd_pcm_lib/kernel_write/writev/read/readv 八个函数都是对这个函数的封装。

/* the common loop for read/write data */
snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
				     void *data, bool interleaved,
				     snd_pcm_uframes_t size, bool in_kernel)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	snd_pcm_uframes_t xfer = 0;
	snd_pcm_uframes_t offset = 0;
	snd_pcm_uframes_t avail;
	pcm_copy_f writer;
	pcm_transfer_f transfer;
	bool nonblock;
	bool is_playback;
	int err;

	err = pcm_sanity_check(substream);/* sanity-check for read/write methods */
	if (err < 0)
		return err;
	
	is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; //确认是播放流
	if (interleaved) {
		if (runtime->access != SNDRV_PCM_ACCESS_RW_INTERLEAVED &&
		    runtime->channels > 1)//数据为交错,且通道数>1 
			return -EINVAL;
        //指定writer,后面会调用。interleaved_copy主要做了frames_to_bytes
		writer = interleaved_copy;  
	} else {
		if (runtime->access != SNDRV_PCM_ACCESS_RW_NONINTERLEAVED)
			return -EINVAL;
		writer = noninterleaved_copy;
	}
	
	if (!data) {
		if (is_playback)
			transfer = fill_silence;//如果数据为空且在播放,则填充静音而不是复制数据
		else
			return -EINVAL;
	} else if (in_kernel) {
		if (substream->ops->copy_kernel)
			transfer = substream->ops->copy_kernel;
		else
			transfer = is_playback ?
				default_write_copy_kernel : default_read_copy_kernel;
	} else {
		if (substream->ops->copy_user)
			transfer = (pcm_transfer_f)substream->ops->copy_user;
		else
			transfer = is_playback ?
				default_write_copy : default_read_copy;
	}
	
	if (size == 0)
		return 0;
	
	nonblock = !!(substream->f_flags & O_NONBLOCK);
	
	snd_pcm_stream_lock_irq(substream);
	err = pcm_accessible_state(runtime);
	if (err < 0)
		goto _end_unlock;
	
	if (!is_playback &&
	    runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
	    size >= runtime->start_threshold) { //录音流,状态处于PREPARED,size大于阈值
		err = snd_pcm_start(substream);
		if (err < 0)
			goto _end_unlock;
	}
	
	runtime->twake = runtime->control->avail_min ? : 1;
	if (runtime->status->state == SNDRV_PCM_STATE_RUNNING)
		snd_pcm_update_hw_ptr(substream);//更新hw_ptr【分析在buffer管理部分】
	//若substream为播放流,调用snd_pcm_playback_avail
	//获取可用(可写)的播放空间
	//avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;
	//hw_ptr指向硬件已经处理过的数据位置,appl_ptr为用户程序已经处理过的数据位置。appl_ptr之后到buffer末尾这段,是可以写入的,hw_ptr之前的空间也是可写的(其中的数据已被播放)。 appl_ptr比hw_ptr靠后。
    avail = snd_pcm_avail(substream);
	while (size > 0) {
		snd_pcm_uframes_t frames, appl_ptr, appl_ofs;
		snd_pcm_uframes_t cont;
		if (!avail) {//当无可写空间时
			if (!is_playback &&
			    runtime->status->state == SNDRV_PCM_STATE_DRAINING) {
                //处于录音流且状态为DRAINING,暂停
				snd_pcm_stop(substream, SNDRV_PCM_STATE_SETUP); 
				goto _end_unlock;
			}
			if (nonblock) {
				err = -EAGAIN;
				goto _end_unlock;
			}
			runtime->twake = min_t(snd_pcm_uframes_t, size,
					runtime->control->avail_min ? : 1);
			err = wait_for_avail(substream, &avail);//等待avail_min数据可用
			if (err < 0)
				goto _end_unlock;
			if (!avail)
				continue; /* draining */
		}
		frames = size > avail ? avail : size; //frames取size和avail中较小的值
        //READ_ONCE可保证在多线程下被其它函数调用变量时不会出错
		appl_ptr = READ_ONCE(runtime->control->appl_ptr);//HW buffer的写指针
		appl_ofs = appl_ptr % runtime->buffer_size; //写指针在当前HW buffer中的位置。
		cont = runtime->buffer_size - appl_ofs; //当前HW buffer中还未被写过的空间数
		if (frames > cont)
			frames = cont; //令frames不超出count
		if (snd_BUG_ON(!frames)) {
			runtime->twake = 0;
			snd_pcm_stream_unlock_irq(substream);
			return -EINVAL;
		}
		snd_pcm_stream_unlock_irq(substream);
		err = writer(substream, appl_ofs, data, offset, frames,
			     transfer);//调用前面的interleaved_copy 主要是frames_to_bytes
        					//将数据写入DMA缓冲区,分析在下面。
		snd_pcm_stream_lock_irq(substream);
		if (err < 0)
			goto _end_unlock;
		err = pcm_accessible_state(runtime);
		if (err < 0)
			goto _end_unlock;
		appl_ptr += frames; //向fifo写入了frames大小,appl_ptr指针后移
		if (appl_ptr >= runtime->boundary)
			appl_ptr -= runtime->boundary;
        //更新给定的appl_ptr,在需要时调用ack callback,返回错误时,恢复为原始值。
		err = pcm_lib_apply_appl_ptr(substream, appl_ptr);
		if (err < 0)
			goto _end_unlock;
	
		offset += frames; //偏移量增加frames,用于interleaved_copy frames_to_bytes
		size -= frames; //要写的数据减少frames
		xfer += frames; //用于出错时的返回值
		avail -= frames; //有效空间减少frames
		if (is_playback &&
		    runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
		    snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) { //当处于播放流,状态为PREPARED,有效数据大于启动阈值时
			err = snd_pcm_start(substream); //这里开始DMA传输
			if (err < 0)
				goto _end_unlock;
		}
	}

 _end_unlock:
	runtime->twake = 0;
	if (xfer > 0 && err >= 0)
		snd_pcm_update_state(substream, runtime);
	snd_pcm_stream_unlock_irq(substream);
	return xfer > 0 ? (snd_pcm_sframes_t)xfer : err;
}
EXPORT_SYMBOL(__snd_pcm_lib_xfer);

DMA映射

​ 一个DMA映射就是分配一个 DMA 缓冲区并为该缓冲区生成一个能够被设备访问的地址的组合操作。一般情况下,简单地调用函数virt_to_bus 就设备总线上的地址,但有些硬件映射寄存器也被设置在总线硬件中。映射寄存器(mappingregister)是一个类似于外围设备的虚拟内存等价物。在使用这些寄存器的系统上,外围设备有一个相对较小的、专用的地址区段,可以在此区段执行DMA。通过映射寄存器,这些地址被重映射到系统 RAM。映射寄存器具有一些好的特性,包括使分散的页面在设备地址空间看起来是连续的。但不是所有的体系结构都有映射寄存器,特别地,PC 平台没有映射寄存器。
​ 在某些情况下,为设备设置有用的地址也意味着需要构造一个反弹(bounce)缓冲区。例如,当驱动程序试图在一个不能被外围设备访问的地址(一个高端内存地址)上执行 DMA 时,反弹缓冲区被创建。然后,按照需要,数据被复制到反弹缓冲区,或者从反弹缓冲区复制。
​ 根据 DMA 缓冲区期望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:

​ 一致 DMA 映射:它们存在于驱动程序的生命周期内。一个被一致映射的缓冲区必须同时可被 CPU 和外围设备访问,这个缓冲区被处理器写时,可立即被设备读取而没有cache效应,反之亦然,使用函数pci_alloc_consistent建立一致映射。_

​ 流式 DMA映射:流式DMA映射是为单个操作进行的设置。它映射处理器虚拟空间的一块地址,以致它能被设备访问。应尽可能使用流式映射,而不是一致映射。这是因为在支持一致映射的系统上,每个 DMA 映射会使用总线上一个或多个映射寄存器。具有较长生命周期的一致映射,会独占这些寄存器很长时间――即使它们没有被使用。使用函数dma_map_single建立流式映射。

DMA管理

任何一个设备驱动想要使用DMA共享缓冲区,就必须为缓冲区的生产者或者消费者。

如果驱动A想用驱动B创建的缓冲区,那么我们称B为生成者,A为消费者。

生产者:

实现和管理缓冲区的操作函数;
允许其他消费者通过dma-buf接口函数共享缓冲区;
实现创建缓冲区的细节;
决定在什么存储设备上申请内存;
管理scatter list的迁徙;

消费者:

作为一个缓冲区的消费者;
无需担心缓冲区是如何/在哪里创建的;
需要一个可以访问缓冲区scatterlist的机制,将其映射到自己的地址空间,这样可以让自己可以访问到内存的同块区域,实现共享内存。

外设的dma-buf操作函数

dma_buf共享缓冲区接口的使用具体包括以下步骤:

1.生产者发出通知,其可以共享一块缓冲区;
2.用户空间获取与该共享缓冲区关联的文件描述符,将其传递给潜在的消费者;
3.每个消费者将其绑定在这个缓冲区上;
4.如果需要,缓冲区使用者向消费者发出访问请求;
5.当使用完缓冲区,消费者通知生产者已经完成DMA传输;
6.当消费者不再使用该共享内存,可以脱离该缓冲区;

kernel 4.19 DMA

dma_cookie_t

/**
 * typedef dma_cookie_t - an opaque(不透明) DMA cookie
 * if dma_cookie_t is >0 it's a DMA request cookie, <0 it's an error code
 */
	typedef s32 dma_cookie_t;

struct data_chunk

/**
 * Interleaved Transfer Request 交错传输请求
 * ----------------------------
 * chunk是要传输的连续字节的集合。
 * 两个chunk之间的间隙(以字节为单位)称为“块间间隙inter-chunk-gap”(ICG)。
 * ICG在chunks之间可能会或可能不会更改。
 * FRAME是连续的{chunk,icg}对的最小系列,当重复整数次时,指定传输。
 * 传输模板是:帧的规格,要重复的次数以及其他每个传输属性。
 *
 * 实际上,客户端驱动程序将在其生命周期内为每种类型的传输准备好模板,
 * 并在提交请求之前仅设置“ src_start”和“ dst_start”。
 *
 * |      Frame-1        |       Frame-2       | ~ |       Frame-'numf'  |
 * |====....==.===...=...|====....==.===...=...| ~ |====....==.===...=...|
 *
 * ==  Chunk size
 * ... ICG
 */

/**
 * struct data_chunk - Element of scatter-gather list that makes a frame.组成框架的分散聚集列表的元素。
 * @size: 要从源读取的字节数。
 * size_dst := fn(op, size_src), so doesn't mean much for destination.
 * @icg: 此块的最后一个src / dst地址之后,下一个块的第一个src / dst地址之前要跳转的字节数。
 *   如果dst_inc为true而dst_sgl为false,则忽略dst(假定为0)。
 *   如果src_inc为true而src_sgl为false,则忽略src(假定为0)。
 * @dst_icg: 此块的最后一个dst地址之后,下一个块的第一个dst地址之前要跳转的字节数。
 *   如果dst_inc为true且dst_sgl为false则忽略。
 * @src_icg: 此块的最后一个src地址之后,下一个块的第一个src地址之前要跳转的字节数。
 *   如果src_inc为true且src_sgl为false则忽略。
 */
	struct data_chunk {
	size_t size;
	size_t icg;
	size_t dst_icg;
	size_t src_icg;
	};

dma_async_tx_descriptor 异步传输描述符

/**
 * struct dma_async_tx_descriptor - 异步传输描述符
 * ---dma generic offload fields---
 * @cookie:本次传输的跟踪cookie,如果本次传输位于独立的链表,则设置为-EBUSY
 * @flags: 标志用于增强操作准备,控制完成和通信状态
 * @phys: 该描述符的物理地址
 * @chan: 本次操作的目标通道
 * @tx_submit: 接受描述符,分配有序的cookie并将描述符标记为pending.在.issue_pending()调用中被推送
 * @callback: 此操作完成后要调用的routine
 * @callback_param: 传递给回调routine的常规参数
 * ---async_tx api specific fields---
 * @next: 完成时提交此描述符
 * @parent: 指向依赖关系链中更高层次的指针
 * @lock: 保护父指针和下一个指针
 */
struct dma_async_tx_descriptor {
	dma_cookie_t cookie; /**/
	enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
	dma_addr_t phys;
	struct dma_chan *chan;
	dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
	int (*desc_free)(struct dma_async_tx_descriptor *tx);
	dma_async_tx_callback callback;
	dma_async_tx_callback_result callback_result;
	void *callback_param;
	struct dmaengine_unmap_data *unmap;
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
	struct dma_async_tx_descriptor *next;
	struct dma_async_tx_descriptor *parent;
	spinlock_t lock;
#endif
};

dma_request_chan[dmaengine.c]

​ dma_request_slave_channel_reason()根据参数name 并解析device tree 来申请具体的DMA 通道。一个DMA控制器有8个channel,每两个作为一组,既可以输出也可以输入,但它们的控制代码是一样的,并且在DMA注册过程中,已经使DMA处于可用状态了,这里根据传入的name 参数以及在device tree 中的描述申请具体的通道。

/**
 * dma_request_chan - 尝试分配一个专有的从属通道
 * @dev:	pointer to client device structure
 * @name:	slave channel name
 *
 * 成功返回指向适当DMA通道的指针,或返回错误指针。
 */
struct dma_chan *dma_request_chan(struct device *dev, const char *name)
	{
	struct dma_device *d, *_d;
	struct dma_chan *chan = NULL;

	/* 如果存在设备树,请从此处获取slave info */
	if (dev->of_node)
		chan = of_dma_request_slave_channel(dev->of_node, name);

	/* 如果设备由ACPI枚举,请从此处获取slave info */
	if (has_acpi_companion(dev) && !chan)
		chan = acpi_dma_request_slave_chan_by_name(dev, name);

	if (chan) {
		/* 找到有效通道或需要推迟请求者 */
		if (!IS_ERR(chan) || PTR_ERR(chan) == -EPROBE_DEFER)
			return chan;
	}

	/* 尝试通过 DMA filter map(s)找到channel */
	mutex_lock(&dma_list_mutex);
	list_for_each_entry_safe(d, _d, &dma_device_list, global_node) {
		dma_cap_mask_t mask;
		const struct dma_slave_map *map = dma_filter_match(d, name, dev);

		if (!map)
			continue;
		
		dma_cap_zero(mask);
		dma_cap_set(DMA_SLAVE, mask);
		
		chan = find_candidate(d, &mask, d->filter.fn, map->param);
		if (!IS_ERR(chan))
			break;

	}
	mutex_unlock(&dma_list_mutex);

	return chan ? chan : ERR_PTR(-EPROBE_DEFER);
}
EXPORT_SYMBOL_GPL(dma_request_chan);

of_dma_request_slave_channel[of-dma.c]

每个需要使用DMA的client都会通过 dmas来引用DMA控制器和通道,通过dma-names实现name的匹配。

/**
 * of_dma_request_slave_channel - Get the DMA slave channel
 * @np:		device node to get DMA request from
 * @name:	name of desired channel
 *
 * 成功返回指向适当DMA通道的指针,或返回错误指针。
 */
struct dma_chan *of_dma_request_slave_channel(struct device_node *np,
					      const char *name)
	{
	struct of_phandle_args	dma_spec;
	struct of_dma		*ofdma;
	struct dma_chan		*chan;
	int			count, i, start;
	int			ret_no_channel = -ENODEV;
	static atomic_t		last_index;

	if (!np || !name) {
		pr_err("%s: not enough information provided\n", __func__);
		return ERR_PTR(-ENODEV);
	}

	/* 如果根本没有“dmas”属性,则无提示地失败 */
	if (!of_find_property(np, "dmas", NULL))
		return ERR_PTR(-ENODEV);

	count = of_property_count_strings(np, "dma-names");
	if (count < 0) {
		pr_err("%s: dma-names property of node '%pOF' missing or empty\n",
			__func__, np);
		return ERR_PTR(-ENODEV);
	}

	/*
	 * approximate an average distribution across multiple
	 * entries with the same name
     *估计多个具有相同名称的条目的平均分布
	 */
		start = atomic_inc_return(&last_index);
		for (i = 0; i < count; i++) {
		if (of_dma_match_channel(np, name,
					 (i + start) % count,
					 &dma_spec))
			continue;

		mutex_lock(&of_dma_lock);
		ofdma = of_dma_find_controller(&dma_spec);

		if (ofdma) {
			chan = ofdma->of_dma_xlate(&dma_spec, ofdma);
		} else {
			ret_no_channel = -EPROBE_DEFER;
			chan = NULL;
		}

		mutex_unlock(&of_dma_lock);

		of_node_put(dma_spec.np);

		if (chan)
			return chan;
		}

	return ERR_PTR(ret_no_channel);
}
EXPORT_SYMBOL_GPL(of_dma_request_slave_channel);

dma_slave_config

/**
 * struct dma_slave_config - dma slave channel runtime config
 * @direction: 现在是在该从属通道上输入还是输出数据。 DMA_MEM_TO_DEV和DMA_DEV_TO_MEM是合法值。不推荐使用,驱动程序应使用device_prep_slave_sg和device_prep_dma_cyclic函数的direction参数或dma_interleaved_template结构中的dir字段。
 * @src_addr: 这是应读取DMA从机数据(RX)的物理地址,如果源是内存,则忽略此参数。
 * @dst_addr: 这是应该写入DMA从机数据(TX)的物理地址,如果源是内存,则忽略此参数。
 * @src_addr_width: 这是应读取DMA数据的源(RX)寄存器的字节宽度。如果源是内存,则根据体系结构可以忽略。合法值:1、2、3、4、8、16、32、64。
 * @dst_addr_width: same as src_addr_width but for destination
 * target (TX) mutatis mutandis.
 * @src_maxburst: 可以一次发送到设备的最大字数(请注意:字数,以src_addr_width成员为单位,而不是字节)。通常,I / O外设的FIFO深度只有一半左右,因此您不会溢出它。这可能适用于内存源,也可能不适用。
 * @dst_maxburst: same as src_maxburst but for destination target
 * mutatis mutandis.
 * @src_port_window_size: 寄存器区域的长度,以字为单位,需要在设备侧访问数据。它仅用于使用区域而不是单个寄存器来接收数据的设备。通常,DMA在此区域中循环以传输数据。
 * @dst_port_window_size: 与src_port_window_size相同,但用于destination 端口。
 * @device_fc: 流控制器设置。仅对从属通道有效。如果外围设备应该是流量控制器,请填写“ true”。方向将在运行时选择。
 * @slave_id: 从属请求者ID。仅对从属通道有效。 dma从属外设将具有唯一的ID作为dma请求者,需要作为从属配置传递。
 *
 * 此结构作为配置数据传递给DMA引擎,以便在运行时为DMA传输设置某个通道。 DMA设备/引擎必须提供对dma_device结构,device_config中的其他回调的支持,然后该结构将作为函数的参数传入。
 *
 * The rationale for adding configuration information to this struct is as
 * follows: if it is likely that more than one DMA slave controllers in
 * the world will support the configuration option, then make it generic.
 * If not: if it is fixed so that it be sent in static from the platform
 * data, then prefer to do that.
 */
struct dma_slave_config {
	enum dma_transfer_direction direction;
	phys_addr_t src_addr;
	phys_addr_t dst_addr;
	enum dma_slave_buswidth src_addr_width;
	enum dma_slave_buswidth dst_addr_width;
	u32 src_maxburst;
	u32 dst_maxburst;
	u32 src_port_window_size;
	u32 dst_port_window_size;
	bool device_fc;
	unsigned int slave_id;
};

snd_dmaengine_pcm_register

/**
 * snd_dmaengine_pcm_register - Register a dmaengine based PCM device
 * @dev: The parent device for the PCM device
 * @config: Platform specific PCM configuration
 * @flags: Platform specific quirks
 */
int snd_dmaengine_pcm_register(struct device *dev,
	const struct snd_dmaengine_pcm_config *config, unsigned int flags)
{
	struct dmaengine_pcm *pcm;
	int ret;

	pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
	if (!pcm)
		return -ENOMEM;

#ifdef CONFIG_DEBUG_FS
	pcm->component.debugfs_prefix = "dma";
#endif
	pcm->config = config;
	pcm->flags = flags;

	ret = dmaengine_pcm_request_chan_of(pcm, dev, config);
	if (ret)
		goto err_free_dma;
	
	if (config && config->process)
		ret = snd_soc_add_component(dev, &pcm->component,
					    &dmaengine_pcm_component_process,
					    NULL, 0);
	else
		ret = snd_soc_add_component(dev, &pcm->component,
					    &dmaengine_pcm_component, NULL, 0);
	if (ret)
		goto err_free_dma;
	
	return 0;

err_free_dma:
	dmaengine_pcm_release_chan(pcm);
	kfree(pcm);
	return ret;
}
EXPORT_SYMBOL_GPL(snd_dmaengine_pcm_register);

PL330驱动分析

commit log位于https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/log/drivers/dma/pl330.c?h=linux-4.19.y 就一页。

驱动的关键数据结构:

图来自https://blog.csdn.net/xiaoaid01/article/details/54861385

有些说明很旧了,4.4中都无此结构体,仅供参考,后面有空自己画一个4.19的。

代码分析:https://blog.csdn.net/EmSoftEn/article/details/46701079 这个比较新

interlace分析

在dmaengine_mpcm_hw_params中

format = params_format(params);
frame_bytes = snd_pcm_format_size(format, params_channels(params));

得到硬件参数的frame大小,以bytes为单位。

例如2ch S16_LE,就是4 bytes。8ch S16_LE就是16 bytes。

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		maps = pcm->mdais->playback_channel_maps;
	else
		maps = pcm->mdais->capture_channel_maps;
		
	sz = snd_pcm_format_size(format, maps[i]);

maps为playback/capture通道数,单独获取播放或录音的frame大小,间隔地访问。

if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
	chan = pcm->tx_chans[i];
	if (sz) {
		slave_config.src_interlace_size = frame_bytes - sz;
		if (slave_config.src_interlace_size)
			slave_config.dst_maxburst = sz / slave_config.dst_addr_width;
		}
	} else {
		chan = pcm->rx_chans[i];
		if (sz) {
				slave_config.dst_interlace_size = frame_bytes - sz;
				if (slave_config.dst_interlace_size)
					slave_config.src_maxburst = sz / slave_config.src_addr_width;
		}
}
/*
 * __ffs - find first bit in word.
 * @word: The word to search
 *
 * Undefined if no bit exists, so code should check against 0 first.
 */
static __always_inline unsigned long __ffs(unsigned long word)
{
	return __builtin_ctzl(word);
}

pch->burst_sz = __ffs(slave_config->dst_addr_width);

传入slave_config->dst_addr_width为4,pch->burst_sz为2

devm简介

在驱动代码中我们经常会见到一些以devm开头的函数,这一类的函数都是和设备资源管理(Managed Device Resource)相关的,驱动中提供这些函数主要是为了方便对于申请的资源进行释放,比如:irq、regulator、gpio等等。在驱动进行初始化的时候如果失败,那么通常会goto到某个地方释放资源,这样的标签多了之后会让代码看起来不简洁,devm就是为来处理这种情况。

devm相关的代码一般在各个目录的一个叫devres.c的文件中,devm核心的代码是在drivers/base/devres.c中,irq相关的代码在kernel/irq/devres.c中,regulator相关的代码在drivers/regulator/devres.c中,另外各个驱动也有相关代码,如spi的devm代码在spi.c文件中。
可参考https://blog.csdn.net/cc289123557/article/details/52137803

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux系统中,PCM(Pulse-code modulation)是一种数字音频编码方式,它将模拟音频信号转换为数字信号。PCM音频数据可以通过ALSA(Advanced Linux Sound Architecture)库进行播放。 ALSA是Linux系统中的一个音频驱动程序,它提供了一组API,用于管理音频设备和处理音频数据。在ALSA中,PCM音频数据可以通过snd_pcm_writei()函数进行播放。该函数将PCM音频数据写入到PCM设备的缓冲区中,并且在缓冲区满时自动触发音频播放。 以下是一个简单的PCM音频播放示例: ```c #include <alsa/asoundlib.h> int main() { int err; snd_pcm_t *pcm_handle; snd_pcm_hw_params_t *params; unsigned int sample_rate = 44100; unsigned int channels = 2; unsigned int buffer_size = 1024; char *buffer; // 打开PCM设备 err = snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0); if (err < 0) { printf("Error opening PCM device: %s\n", snd_strerror(err)); return -1; } // 分配PCM参数对象 snd_pcm_hw_params_alloca(&params); // 初始化PCM参数 err = snd_pcm_hw_params_any(pcm_handle, params); if (err < 0) { printf("Error initializing PCM parameters: %s\n", snd_strerror(err)); return -1; } // 设置PCM参数 err = snd_pcm_hw_params_set_access(pcm_handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); err |= snd_pcm_hw_params_set_format(pcm_handle, params, SND_PCM_FORMAT_S16_LE); err |= snd_pcm_hw_params_set_channels(pcm_handle, params, channels); err |= snd_pcm_hw_params_set_rate_near(pcm_handle, params, &sample_rate, 0); err |= snd_pcm_hw_params_set_buffer_size_near(pcm_handle, params, &buffer_size); if (err < 0) { printf("Error setting PCM parameters: %s\n", snd_strerror(err)); return -1; } // 应用PCM参数 err = snd_pcm_hw_params(pcm_handle, params); if (err < 0) { printf("Error applying PCM parameters: %s\n", snd_strerror(err)); return -1; } // 分配PCM缓冲区 buffer = (char *)malloc(buffer_size * channels * 2); // 播放PCM音频数据 while (1) { // 从文件或其他数据源读取PCM音频数据到缓冲区中 // ... // 将PCM音频数据写入到PCM设备的缓冲区中 err = snd_pcm_writei(pcm_handle, buffer, buffer_size); if (err < 0) { printf("Error writing PCM data: %s\n", snd_strerror(err)); break; } } // 关闭PCM设备 snd_pcm_close(pcm_handle); return 0; } ``` 在上面的示例中,我们首先打开PCM设备,然后初始化和设置PCM参数,接着分配PCM缓冲区,并且不断地从文件或其他数据源读取PCM音频数据到缓冲区中,最后将PCM音频数据写入到PCM设备的缓冲区中进行播放。需要注意的是,我们需要在程序结束时关闭PCM设备。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值