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