Linux DMA子系统(1):DMA Engine体系结构

本文详细介绍了DMA(直接内存访问)的概念、原理及其实现方法。涵盖了DMA传输过程、所需参数、DMA控制器与CPU如何分时使用内存等内容,并探讨了DMA带来的缓存一致性问题及其解决方案。

 

目录

1. 前言

2. DMA简介

2.1 DMA概念 

2.2 DMA原理和实现 

2.3 DMA传输过程

2.4 DMA传输需要的参数

2.4.1 transfer size

2.4.2 transfer width

2.4.3 burst size

2.4.4 DMA传输方式

3. Linux DMA子系统体系结构

4. DMA控制器与CPU怎样分时使用内存

4.1 停止CPU访问内存

 4.2 周期挪用

4.3 DMA与CPU交替访问内存

5. DMA 导致的问题

5.1 关于流式 DMA 和一致性 DMA 的区别

6. 参考文章


1. 前言

最近在学习Linux DMA相关的内容,会从DMA控制器驱动(provider,提供DMA服务)和使用DMA控制器提供的服务的外设驱动(consumer)这两个方面,来分析介绍DMA Engine的体系结构。

2. DMA简介

2.1 DMA概念 

DMA(direct memory access),即不经过CPU,直接访问内存。因为不管是memory和memory之间还是memory和设备之间进行数据搬运,对CPU来说都是枯燥乏味的,且非常浪费CPU的时间,造成CPU无法及时处理一些实时的事件。因此,为了解放CPU,可以让CPU在搬运数据的这段时间可以去做更有意义的事情,工程师们就设计出来一种专门用来搬运数据的器件,即DMA控制器,让它来协助CPU进行数据搬运工作,下图是DMA的硬件示意图。

2.2 DMA原理和实现 

DMA 的原理:即CPU将需要迁移的数据的位置告诉给DMA,包括源地址,目的地址以及需要迁移的长度,然后启动DMA设备,DMA设备收到命令之后,就去完成相应的操作,最后通过中断反馈给CPU。

DMA的实现:在实现DMA传输时,是DMA控制器掌控着总线,也就是说,这里会有一个控制权转让的问题,我们当然知道,计算机中权限最大的就是CPU,这个DMA暂时掌管的总线控制权当前也是CPU赋予的,在DMA完成传输之后,会通过中断通知CPU收回总线控制权。

2.3 DMA传输过程

DMA传输的过程:一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束这四个阶段。

DMA 请求:CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。
DMA 响应:DMA控制器对DMA请求判别优先级以及屏蔽位,向总线裁决逻辑提出总线请求,当CPU执行完成当前的总线周期之后即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经就绪,通过 DMA 控制器通知I/O接口开始DMA传输。
DMA 传输:在DMA控制器的引导下,在存储器和外设之间进行数据传送,在传送过程中不需要CPU的参与。
DMA 结束:当完成既定操作之后,DMA控制器释放总线控制权,并向I/O接口发出结束信号,当I/O接口收到结束信号之后,一方面停止I/O设备的工作,另一方面向CPU提出中断请求,使CPU从不介入状态解脱,并执行一段检查本次DMA传输操作正确性的代码。最后带着本次操作的结果以及状态继续执行原来的程序。

2.4 DMA传输需要的参数

2.4.1 transfer size

在最简单的DMA传输中,只需要为DMA controller提供一个参数,即transfer size它就可以进行数据传输了:在每一个时钟周期,DMA controller将1byte的数据从一个buffer搬到另一个buffer,直到搬完transfer size个bytes即可停止。

2.4.2 transfer width

不过在实际应用场景中,只有一个transfer size参数是不够的,因为有些设备可能需要在一个时钟周期中,传输指定bit的数据,例如:

memory之间传输数据的时候,希望能以总线的最大宽度为单位(32-bit、64-bit等),以提升数据传输的效率。

而在音频设备中,需要每次写入精确的16-bit或者24-bit的数据;等等。

因此,为了满足这些多样的需求,我们需要为DMA controller提供一个额外的参数,即transfer width。

/**
 * enum dma_slave_buswidth - defines bus width of the DMA slave
 * device, source or target buses
 */
enum dma_slave_buswidth {
	DMA_SLAVE_BUSWIDTH_UNDEFINED = 0,
	DMA_SLAVE_BUSWIDTH_1_BYTE = 1,
	DMA_SLAVE_BUSWIDTH_2_BYTES = 2,
	DMA_SLAVE_BUSWIDTH_3_BYTES = 3,
	DMA_SLAVE_BUSWIDTH_4_BYTES = 4,
	DMA_SLAVE_BUSWIDTH_8_BYTES = 8,
	DMA_SLAVE_BUSWIDTH_16_BYTES = 16,
	DMA_SLAVE_BUSWIDTH_32_BYTES = 32,
	DMA_SLAVE_BUSWIDTH_64_BYTES = 64,
};

2.4.3 burst size

另外,当传输的源或者目的地是memory的时候,为了提高效率,DMA controller不愿意每一次传输都访问memory,而是在内部开一个buffer,将数据缓存在自己buffer中:

memory是源的时候,一次从memory读出一批数据,保存在自己的buffer中,然后再一点点(以时钟为节拍),传输到目的地;

memory是目的地的时候,先将源的数据传输到自己的buffer中,当累计一定量的数据之后,再一次性的写入memory。

这种场景下,DMA控制器内部可缓存的数据量的大小,称作burst size。

2.4.4 DMA传输方式

DMA传输方式分为两种,即block DMA方式和scatter-gather DMA方式。

在DMA传输数据的过程中,要求源物理地址和目标物理地址必须是连续的。但是在某些计算机体系中,如IA架构,连续的存储器地址在物理上不一定是连续的,所以DMA传输要分成多次完成。

如果在传输完一块物理上连续的数据后引起一次中断,然后再由主机进行下一块物理上连续的数据传输,那么这种方式就为block DMA方式。

Scatter-gather DMA使用一个链表描述物理上不连续的存储空间,然后把链表首地址告诉DMA master。DMA master在传输完一块物理连续的数据后,不用发起中断,而是根据链表来传输下一块物理上连续的数据,直到传输完毕后再发起一次中断。

很显然,scatter-gather DMA方式比block DMA方式效率高。

3. Linux DMA子系统体系结构

Linux DMA体系结构主要分为三个部分:DMA provider、DMA consumer以及DMA核心层(DMA Engine)。

DMA provider:即DMA控制器驱动。

DMA consumer:主要是用于消费(申请)DMA请求,对于SPI、I2C等控制器驱动,会去申请DMA通道并使能DMA传输;

DMA核心层(DMA Engine):提供公共接口函数。

4. DMA控制器与CPU怎样分时使用内存

外围设备可以通过DMA控制器直接访问内存,与此同时,CPU可以继续执行程序逻辑,通常采用三种方法实现DMA控制机与CPU分时使用内存,分别是:停止CPU访问内存、周期挪用以及DMA与CPU交替访问内存。

4.1 停止CPU访问内存

当外围设备要求传送一批数据时,由DMA控制器发一个停止信号给CPU,要求CPU放弃对地址总线、数据总线和有关控制总线的使用权,DMA控制器获得总线控制权之后,开始进行数据传输,在一批数据传输完毕之后,DMA控制器通知CPU可以继续使用内存,并把总线控制权交还给CPU。下图就是这样的一个传输图,很显然,在这种 DMA 传输过程中,CPU基本处于不工作状态或者保持状态。

优点:控制简单,他是用于数据传输率很高的设备进行成组的传输。

缺点:在DMA控制器访问内存阶段,内存效能没有充分发挥,相当一部分的内存周期是空闲的,这是因为外围设备传送两个数据之间的间隔一般大于内存存储间隔,即使是再告诉的I/O存储设备也是如此(就是内存读写速率比SSD等都快)。

 4.2 周期挪用

当I/O设备没有DMA请求时,CPU按照程序要求访问内存;一旦I/O设备有 DMA 请求,则由I/O设备挪用一个或者几个内存周期,这种传送方式如下图所示:

I/O设备要求DMA传送时可能遇到两种情况:

第一种:此时CPU不需要访问内存,例如,CPU正在执行乘法指令,由于乘法指令执行时间长,此时I/O访问与CPU访问之间没有冲突,则I/O设备挪用一两个内存周期是对CPU执行没有任何的影响。

第二种:I/O设备要求访问内存时,CPU也需要访问内存,这就产生了访问内存冲突,在这种情况下I/O设备优先访问,因为I/O访问有时间要求,前一个I/O数据必须在下一个访问请求来到之前存储完毕。显然,在这种情况下,I/O设备挪用一两个内存周期,意味着CPU延缓了对指令的执行,或者更为明确的讲,在CPU执行访问指令的过程中插入了 DMA 请求。周期挪用内存的方式与停止CPU访问内存的方式对比,这种方式,既满足了I/O数据的传送,也发挥了CPU和内存的效率,是一种广泛采用的方法,但是I/O设备每一周期挪用都有申请总线控制权,建立总线控制权和归还总线控制权的过程,所以不适合传输小的数据。这种方式比较适用于I/O设备读写周期大于内存访问周期的情况。

4.3 DMA与CPU交替访问内存

如果CPU的工作周期比内存的存储周期长很多,此时采用交替访问内存的方法可以使DMA传送和CPU同时发挥最高效率。这种传送方式的时间图如下:

此图是DMA和CPU交替访问内存的消息时间图,假设CPU的工作周期为1.2us,内存的存储周期小于0.6us,那么一个CPU周期可以分为C1和C2两个分周期,其中C1专供DMA控制器访问内存,C2专供CPU访问内存。这种方式不需要总线使用权的申请、建立和归还的过程,总线的使用权是通过C1和C2分时机制决定的。CPU和DMA控制器各自有自己的访问地址寄存器、数据寄存器和读写信号控制寄存器。在C1周期中,如果DMA控制器有访问请求,那么可以将地址和数据信号发送到总线上。在C2周期内,如果CPU有访问内存的请求,同样将请求的地址和数据信号发送到总线上。事实上,这是用C1、C2控制的一个多路转换器,这种总线控制权的转移几乎不需要什么时间,所以对于DMA来讲是很高效率的。

这种传送方式又称为“透明DMA”方式,其由来是由于DMA传送对于CPU来讲是透明的,没有任何感觉。在透明DMA方式下CPU不需要停止主程序的运行,也不进入等待状态,是一种很高效率的工作方式,当然,相对应的硬件设计就会更加复杂。

以上是传统意义的DMA:是一种完全由硬件执行I/O交换的工作方式。这种方式中,DMA控制器和CPU完全接管对总线的控制,数据交换不经过CPU,而直接在内存和I/O设备之间进行。DMA工作时,由DMA控制器向内存发起地址和控制信号,进行地址修改,对传输的数据进行统计,并以中断的形式通知CPU数据传输完成。

5. DMA 导致的问题

DMA不仅仅只会带来效率的提升,同样,他也会带来一些问题,最明显的就是缓存一致性问题。想象一下,现代的CPU都是自带一级缓存、二级缓存甚至是三级缓存,当CPU访问内存某个地址时,暂时先将新的值写入到缓存中,但是没有更新外部内存的数据,假设此时发生了DMA请求且操作的就是这一块在缓存中更新了而外部内存没有更新的内存地址,这样DMA读到的就是非最新数据;相同的,如果外部设备通过DMA将新值写入到内存中,但是CPU访问得到的确实缓存中的数据,这样也会导致拿到的不是最新的数据。

为了能够正确进行DMA操作,必须进行必要的Cache操作,Cache的操作主要分为invalidate(作废)和writeback(回写),有时候也会二者一同使用。如果DMA使用了Cache,那么Cache一致性问题是必须要考虑的,解决的最简单的办法就是禁止DMA目标地址范围的Cache功能,但是这样会牺牲掉一定的性能。因此,在DMA是否使用cache的问题上,可以根据DMA缓冲区的期望保留时间长短来决策。DMA被区分为了:一致性DMA映射和流式DMA映射。

一致性DMA映射:他申请的缓存区之后会被以非缓存的形式映射,一致性映射具有很长的生命周期,在这段时间内占用映射寄存器,即使不再使用也不会释放,一般情况下,一致性DMA的生命周期会被设计为驱动的生命周期(也就是在init里面注册,在exit里面释放)。

流式DMA映射:他的实现比较复杂,表现特征为使用周期很短,他的实现中会主动保持缓存的一致性。在使用方法上,流式 DMA 还需要指定内核数据的流向,不然会导致不可预期的后果。不过很多的现代处理能能够自己来保证CPU和DMA控制器之间的cache一致性问题,比如ARM的ACP功能,这样像dma_map_single函数只是返回物理地址,而dma_unmap_single则什么都不做,这样极大的提升了系统性能。

5.1 关于流式 DMA 和一致性 DMA 的区别

在一致性DMA映射中,它采用的是系统预留的一段DMA内存用于DMA操作,这一段内核在系统启动阶段就已经预留完毕,比如arm64平台会在dts文件中写明系统预留的DMA内存段位于何处,并且会被标志为用于DMA一致性内存申请,如果你有关注DMA的一致性映射操作API就会发现,一致性DMA不会去使用别的地方申请的内存,他都是通过dma_alloc_coherent自我申请内存,然后驱动自己填充数据最后被提交给DMA控制器。

流式DMA中,他可以是随意的内存交给DMA进行处理,你不需要从系统预留的DMA位置进行内存申请,任何普通的kmalloc申请的内存都能交给DMA控制器进行操作。

上面的两点是他们二者一个很重要的区别,我不知道为何在其他的文章中没有被写到过,那么以此衍生开的就是这二者是如何做到缓存一致性的:一致性 DMA 要做到缓存一致性很简单,在DMA内存申请的过程中,首先进行一个ioremap_nocache的映射,然后调用函数dma_cache_wback_inv 保证缓存已经刷新到位之后,后面使用这一段内存时不存在一二级缓存;而流式DMA则比较复杂,他不能直接禁止缓存,因为流式DMA可以使用系统中的任意地址范围的地址,CPU总不能将系统所有的地址空间都禁止缓存,这不科学,那么为了实现缓存一致性,流式DMA需要不断的对缓存进行失效操作,告诉CPU这一段缓存是不可信的,必须从内存中重新获取。说到这里,我想起之前的有篇文章提到过说他们二者的区别就是一个是禁止缓存,一个是刷缓存,但是他没有讲清楚,到底谁是刷缓存,谁是禁止缓存呢?现在从上面的理解来看,很明显,一致性DMA就是直接将缓存禁止,而流式DMA则是将缓存失效刷新。

6. 参考文章

\Documentation\dmaengine\provider.txt

DMA 与 scatterlist 技术简介 – 字节岛技术分享

Linux DMA Engine framework(1) - DMA Introduction_Hacker_Albert的博客-优快云博客

<think>好的,用户现在问的是dmaengine是如何注册进ALSA,以及使用的标准ops有哪些。我需要先回顾一下之前用户的问题,之前用户询问了DMA引擎在I2S驱动中的注册是否意味着DMA是I2S的组件,而我解释了DMA引擎是系统级资源,I2S驱动作为消费者使用DMA服务。现在用户进一步深入,想知道DMA引擎如何与ALSA集成,特别是注册过程和使用的操作函数。 首先,我需要确认ALSA(高级Linux声音体系)中DMA引擎的作用。ALSA负责音频设备的驱动,而DMA用于高效传输音频数据,因此DMA引擎需要与ALSA子系统交互。用户可能是在开发音频驱动或调试相关问题时遇到这个问题,需要了解如何将DMA集成到ALSA框架中。 接下来,我需要回忆Linux内核中ALSA和DMA引擎的交互机制。ALSA的PCM中间层负责管理音频数据流,而DMA引擎则处理实际的内存到外设的数据传输。因此,注册DMA引擎到ALSA可能涉及到在PCM设备创建时配置DCM参数,并设置相应的操作函数(ops)。 然后,我需要确定用户所指的“注册”具体是指哪个部分。是DMA控制器驱动注册到DMA引擎框架,还是ALSA驱动注册时使用DMA引擎的服务?根据问题,应该是后者,即ALSA驱动如何利用DMA引擎的API来注册并使用其服务。 接下来,标准ops有哪些?在ALSA的PCM接口中,ops结构体包含了一系列回调函数,比如hw_params、prepare、trigger、pointer等。当使用DMA引擎时,这些函数需要与DMA引擎的API结合,比如使用dmaengine_pcm_ops中的函数。 可能需要查阅内核源码中的相关部分,例如在sound/soc/soc-generic-dmaengine-pcm.c中,DMA引擎PCM的通用实现,里面定义了标准的ops函数,比如dmaengine_pcm_trigger,dmaengine_pcm_pointer等。 同时,用户可能需要了解注册的具体步骤,比如在ALSA驱动中如何申请DMA通道,配置从设备参数,以及如何将这些参数传递给PCM中间层。例如,使用snd_dmaengine_pcm_register()函数来注册DMA引擎PCM。 另外,需要提到不同的DMA传输方向(播放和捕获)可能需要不同的配置,比如设置DMA_SLAVE_BUSWIDTH和DMA_SLAVE_CONFIG中的参数。同时,DMA的周期大小、缓冲区大小等参数需要与音频流的格式匹配。 可能还需要考虑DMA引擎与ALSA之间的数据流管理,比如通过dmaengine_prep_dma_cyclic来准备循环缓冲区,以及通过回调函数处理传输完成的中断。 用户可能的实际需求是编写或调试一个使用DMA引擎的ALSA音频驱动,因此需要具体的代码示例和步骤说明。他们可能遇到配置错误,导致音频无法播放或录制,或者DMA传输不稳定的问题,需要知道如何正确注册和配置DMA引擎的ops。 需要确保回答结构清晰,分步骤说明注册流程,列举标准的ops函数,并提供代码示例和配置要点。同时,要强调DMA引擎作为独立子系统与ALSA的协作方式,而不是隶属于某个具体驱动,保持之前的结论一致性。 最后,验证是否有遗漏的关键点,比如DMA通道的申请和释放、错误处理、调试方法等,这些对用户实际应用可能有帮助。</think>在Linux音频子系统中,DMA引擎与ALSA(Advanced Linux Sound Architecture)的整合主要通过以下机制实现: --- ### 一、注册流程关键步骤 #### 1. **DMA控制器驱动注册** - DMA控制器驱动(如`dma-imx.c`)首先向内核的`dmaengine`框架注册,提供硬件操作函数: ```c // DMA控制器驱动示例(dma-imx.c) static struct platform_driver imx_dma_driver = { .probe = imx_dma_probe, // 初始化DMA控制器硬件 .driver = { .name = "imx-dma", // 设备树匹配标识 }, }; platform_driver_register(&imx_dma_driver); ``` - 此步骤使DMA控制器对`dmaengine`框架可见。 #### 2. **ALSA驱动关联DMA引擎** - ALSA驱动(如I2S/PCM驱动)通过`dmaengine` API申请DMA通道,并绑定到ALSA PCM操作中: ```c // ALSA驱动中申请DMA通道(如sound/soc/fsl/fsl_ssi.c) struct dma_chan *tx_chan = dma_request_slave_channel(dev, "tx"); struct dma_chan *rx_chan = dma_request_slave_channel(dev, "rx"); // 注册到ALSA PCM框架 ret = snd_dmaengine_pcm_register(&pdev->dev, &dmaengine_pcm_config, 0); ``` - `snd_dmaengine_pcm_register()`是ALSA提供的通用注册接口。 --- ### 二、核心操作函数(`struct snd_pcm_ops`) 当使用`dmaengine`时,ALSA PCM操作会映射到以下标准函数: #### 1. **硬件参数配置(`hw_params`)** ```c static int dmaengine_pcm_hw_params( struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { // 配置DMA传输参数(数据宽度、周期大小等) struct dma_slave_config config = { .direction = DMA_MEM_TO_DEV, // 或DMA_DEV_TO_MEM .dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, .src_maxburst = 8, // 突发传输长度 }; dmaengine_slave_config(dma_chan, &config); } ``` #### 2. **触发传输(`trigger`)** ```c static int dmaengine_pcm_trigger( struct snd_pcm_substream *substream, int cmd) { switch (cmd) { case SNDRV_PCM_TRIGGER_START: dma_async_issue_pending(dma_chan); // 启动DMA传输 break; case SNDRV_PCM_TRIGGER_STOP: dmaengine_terminate_async(dma_chan); // 终止传输 break; } } ``` #### 3. **获取缓冲区指针(`pointer`)** ```c static snd_pcm_uframes_t dmaengine_pcm_pointer( struct snd_pcm_substream *substream) { // 通过dmaengine_tx_status获取当前传输位置 struct dma_tx_state state; dmaengine_tx_status(dma_chan, cookie, &state); return bytes_to_frames(substream->runtime, state.residue); } ``` #### 4. **其他关键函数 - **`open`**: 初始化DMA通道和缓冲区 - **`close`**: 释放DMA资源 - **`prepare`**: 重置DMA控制器状态 --- ### 三、DMAEngine PCM配置结构体 ALSA提供通用配置模板`struct snd_dmaengine_pcm_config`: ```c // 典型配置示例 static const struct snd_dmaengine_pcm_config dmaengine_pcm_config = { .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, .pcm_hardware = &dmaengine_pcm_hardware, // PCM硬件参数(格式、速率等) .prealloc_buffer_size = 64 * 1024, // 预分配DMA缓冲区大小 }; ``` - **`prepare_slave_config`**: 从音频参数生成DMA从设备配置的通用回调。 --- ### 四、数据流交互示意图 ``` ALSA用户空间 | ↓ ALSA PCM核心层(snd_pcm_ops) | 调用dmaengine_pcm_ops ↓ DMAEngine PCM适配层(snd_dmaengine_pcm_*) | 使用dmaengine API(dmaengine_slave_config等) ↓ 具体DMA控制器驱动(如dma-imx.c) | 操作硬件寄存器 ↓ I2S/TDM等音频外设 ``` --- ### 五、调试与问题定位 1. **DMA传输卡顿**: - 检查`dmaengine_pcm_hardware`中的`periods_min`和`buffer_bytes_max`是否合理。 - 通过`dmesg | grep dma`查看DMA控制器初始化日志。 2. **无音频输出**: - 确认`dma_request_slave_channel()`是否成功返回非NULL通道。 - 使用`devmem2`工具直接读取DMA控制器的寄存器状态。 --- ### 结论 **DMA引擎通过`snd_dmaengine_pcm_register`和标准操作函数(`hw_params`/`trigger`/`pointer`)集成到ALSA框架**。ALSA驱动无需直接操作DMA控制器硬件,只需通过`dmaengine` API配置传输参数。这种设计实现了音频数据传输的硬件无关性,是Linux音频子系统的核心机制之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值