Rockchip RK3399 - ASoC Platform驱动基础(DMA框架)
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
在上一篇博客我们介绍了ALSA子系统的软件架构,同时介绍了ALSA CORE核心数据结构和相关API。本节我们将会介绍ASoC软件体系中音频三大驱动模块(Codec、Platform 和Machine)中的Platform 。
Platform driver主要是平台相关的dma操作以及音频管理,由以下两大功能模块组成:
- dma driver :先通过dma驱动将dma buffer中的音频数据搬运到 I2S tx FIFO;
- cpu dai driver:然后再通过cpu_da驱动将音频数据从I2S tx FIFO搬运codec中,数据会在codec侧进行解码的操作,最终输出到耳机/音箱中;
描述platform的最主要的几个数据结构分别是:snd_soc_dai,snd_soc_dai_driver、snd_soc_component、snd_soc_component_driver;其中:
- snd_soc_dai:描述cpu端的dai;
- snd_soc_dai_driver:与snd_soc_dai对应的驱动数据结构就是snd_soc_dai_driver;
- snd_soc_component:ASoC使用同一的数据结构来描述codec设备和platform设备,该数据结构就是snd_soc_component;也就是说,不管是platform、codec,核心层现在都统一用struct snd_soc_component来管理了, 一个component对应一个模块;
- snd_soc_component_driver:与snd_soc_component对应的驱动数据结构就是snd_soc_component_driver;
每个platform driver都必须有一个struct snd_soc_dai_driver结构体,用于定义其dai能力和操作。该结构体被导出,以便machine driver可以将其注册到ASoC CORE中。
关于snd_soc_dai,snd_soc_dai_driver数据结构具体参考:Rockchip RK3399 - ASoC Machine驱动基础 dai相关内容。
由于ASoC统一使用snd_soc_component、snd_soc_component_driver来描述codec设备和platform设备,在文章Rockchip RK3399 - ASoC Codec驱动基础中我们已经对这两个数据结构进行了介绍了,这里就不重复介绍了。
本篇博客我们重点介绍platform中涉及到的dma驱动相关的内容,当然既然是学习dma驱动,那我们就从内核dma框架开始说起。
一、DMA框架
1.1 DMA概述
什么是DMA?DMA英文全称为Direct memory access,直译过来就是直接内存访问,大概就是就是不用通过CPU就能将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。它的作用就是解决大量数据传输过度消耗CPU资源的问题。DMA节省了大量的CPU资源,使CPU专注于更加实用的操作。
1.1.1 原理介绍
DMA的本质就是实现数据的直接传输,即将内存某块内存地址的数据传输到另一块地址空间,既然是数据传输,想必就涉及到源地址、目标地址、以及传输数据的长度。
DMA传输主体包含两大类,一类是内存资源,另一类是外设资源;因此存在四种主体之间的数据传输:
- 外设到内存;
- 内存到外设;
- 内存到内存;
- 外设到外设;
在实现DMA传输时,是DMA控制器掌控着总线,也就是说,这里会有一个控制权转让的问题,我们当然知道,计算机中权限最大的就是CPU,这个DMA暂时掌管的总线控制权当前也是CPU赋予的,在DMA完成传输之后,会通过中断通知CPU收回总线控制权。
1.1.2 传输过程
一个完整的DMA传输必须经过DMA请求、DMA响应、DMA传输、DMA结束这是个阶段:
- DMA请求:初始化DMA控制器,主要包括:使能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传输操作正确性的代码;
(1) transfer width
在DMA数据传输时,除了需要指定源地址、目标地址、以及传输数据量外,还需要指定传输的位数。例如内存之间传输数据的时候,希望能以总线的最大宽度为单位(32-bit、64-bit等),以提升数据传输的效率。
而在音频设备中,需要每次写入精确的16位或者24位的数据;等等。因此,为了满足这些多样的需求,我们需要为DMA controller提供一个额外的参数,即transfer width。
(2) burst size
当传输的源或者目的地是内存的时候,为了提高效率,DMA controller不愿意每一次传输都访问内存,而是在内部开一个buffer,将数据缓存在自己buffer中:
内存是源的时候,一次从内存读出一批数据,保存在自己的buffer中,然后再一点点(以时钟为节拍),传输到目的地;
内存是目的地的时候,先将源的数据传输到自己的buffer中,当累计一定量的数据之后,再一次性的写入内存。
这种场景下,DMA控制器内部可缓存的数据量的大小,称作burst size。在数据结构struct dma_device中min_burst/max_burst分别用于指定最小/最大的burst size。
1.1.3 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则什么都不做,这样极大的提升了系统性能;
在一致性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则是将缓存失效刷新。
1.2 RK3399中的DMA
RK3399包含两个DMA控制器。DMAC0和DMAC1,这两个控制器均支持外设和内存、内存和内存之间的数据传输。
DMAC在复位后处于非安全状态,可以通过配置SGRF模块来更改安全状态。
1.2.1 DMAC0
DMAC0支持以下功能:
- 支持Trustzone技术;
- 支持12种外设请求;
- 最多可以传输64位数据;
- 同时支持6个通道;
- 最大burst size为16;
- 提供12个中断输出和1个终止输出;
这里我们需要将请求和通道区分开来,通道是执行数据传输的实体,而而请求是涉及的端点,一个通道可以用来为多个请求提供服务。
下表显示了DMAC0请求映射:
外设请求 | 源 | 极性 |
---|---|---|
请求0 | I2S0 tx | 高电平 |
请求1 | I2S0 rx | 高电平 |
请求2 | I2S1 tx | 高电平 |
请求3 | I2S1 rx | 高电平 |
请求4 | I2S2 tx | 高电平 |
请求5 | I2S2 rx | 高电平 |
请求6 | PWM | 高电平 |
请求7 | SPDIF tx | 高电平 |
请求8 | SPI5 tx | 高电平 |
请求9 | SPI4 rx | 高电平 |
请求10 | 保留 | |
请求11 | 保留 |
1.2.2 DMAC1
DMAC1支持以下功能:
- 支持Trustzone技术;
- 支持20种外设请求;
- 最多可以传输64位数据;
- 同时支持8个通道;
- 最大burst size为16;
- 提供116个中断输出和1个终止输出;
- 支持128个MFIFO深度;
下表显示了DMAC1请求映射:
外设请求 | 源 | 极性 |
---|---|---|
请求0 | UART0 tx | 高电平 |
请求1 | UART0 rx | 高电平 |
请求2 | UART1 tx | 高电平 |
请求3 | UART1 rx | 高电平 |
请求4 | UART2 tx | 高电平 |
请求5 | UART2 rx | 高电平 |
请求6 | UART3 tx | 高电平 |
请求7 | UART3 rx | 高电平 |
请求8 | UART4 tx | 高电平 |
请求9 | UART4 rx | 高电平 |
请求10 | SPI0 tx | 高电平 |
请求11 | SPI0 rx | 高电平 |
请求12 | SPI1 tx | 高电平 |
请求13 | SPI1 rx | 高电平 |
请求14 | SPI2 tx | 高电平 |
请求15 | SPI2 rx | 高电平 |
请求16 | SPI3 tx | 高电平 |
请求17 | SPI3 rx | 高电平 |
请求18 | SPI4 tx | 高电平 |
请求19 | SPI4 rx | 高电平 |
DMAC支持递增地址突发传输和固定地址突发传输。但是,在以字节或半字大小访问SPI和UART的情况下,DMAC仅支持固定地址突发传输,并且地址必须对齐到字。换句话说,对于这种情况,地址必须是4的倍数,才能进行固定地址突发传输。
1.2.3 DMA框图
DMA控制器框图如下图所示:
DMA控制器包含一个指令处理模块,使其能够处理控制DMA传输的程序代码。程序代码存储在DMAC使用AXI接口访问的系统内存区域中,DMAC将指令临时存储在缓存中。
DMAC0支持6个通道,DMAC1支持8个通道,每个通道能够支持单个并发的DMA操作线程。此外,还存在一个单独的DMA管理线程,可以用它来初始化DMA通道线程。DMAC每个AXI时钟周期最多执行一条指令。为了确保定期执行每个活动线程,它通过依次处理DMA管理线程和DMA通道线程来进行交替。在选择下一个活动的DMA通道线程执行时,它使用循环轮询的方式。
DMAC使用可变长度的指令,每条指令由1到6个字节组成。它为每个DMA通道提供单独的程序计数器(PC)寄存器。当一个线程从一个地址请求指令时,缓存进行查找。如果命中缓存,缓存会立即提供数据。否则,线程会暂停,而DMAC使用AXI接口执行缓存行填充操作。如果一条指令大于4个字节或跨越缓存行的末尾,DMAC会执行多个缓存访问来获取指令。在进行缓存行填充时,DMAC使其他线程能够访问缓存,但如果发生另一个缓存未命中,这会导致流水线暂停,直到第一个缓存行填充完成。
当DMA通道线程执行加载(load)或存储(store)指令时,DMAC将该指令添加到相应的读取或写入队列中。DMAC使用这些队列作为指令存储缓冲区,在将指令发送到AXI总线之前使用。DMAC还包含一个多FIFO(MFIFO)数据缓冲区,用于在DMA传输过程中存储读取或写入的数据。
1.3 DMA框架
在linux中,内核设计了一套DMA框架,如果想增加某一种新DMA硬件的驱动,只需要去编写芯片相关的代码,然后调用内核提供函数注册到DMA核心层即可。
DMA框架主要由以下部分组成:
- DMA控制器:硬件部分;
- DMA provider:即DMA controller driver,DMA控制器驱动主要作用是管理channel并响应client driver(consumer)的传输请求并控制DMA控制器执行传输;
- DMA核心层:为DMA controller driver以及DMA client driver(即DMA consumer)提供公共接口函数;
- DMA consumer:即DMA client driver,主要用于消费(申请)DMA请求;以SPI、I2C等控制器驱动为例,会去申请DMA通道并使能DMA传输;
1.4 目录结构
linux内核将RTC驱动相关的代码放在drivers/dma目录下,这下面的文件还是比较多的,我们大概了解一下即可。
dmaengine.c:DMA核心层的实现;
xxx-dma:大部分都是各个平台的DMA控制器驱动,比stm32-dma.c。
二、DMA controller device
我们首先从DMA控制器的角度了解DMA框架为DMA controller driver提供了哪些功能和API。
首先要了解DMA controller driver涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
2.1 struct dma_device
linux内核内使用struct dma_device数据结构来描述一个DMA设备(对应的物理设备就是一个DMA控制器),dma_device包含了字符设备,dma设备操作函数(这些操作函数会被client driver调用)等信息,定义在include/linux/dmaengine.h:
/** * struct dma_device - info on the entity supplying DMA services * @ref: reference is taken and put every time a channel is allocated or freed * @chancnt: how many DMA channels are supported * @privatecnt: how many DMA channels are requested by dma_request_channel * @channels: the list of struct dma_chan * @global_node: list_head for global dma_device_list * @filter: information for device/slave to filter function/param mapping * @cap_mask: one or more dma_capability flags * @desc_metadata_modes: supported metadata modes by the DMA device * @max_xor: maximum number of xor sources, 0 if no capability * @max_pq: maximum number of PQ sources and PQ-continue capability * @copy_align: alignment shift for memcpy operations * @xor_align: alignment shift for xor operations * @pq_align: alignment shift for pq operations * @fill_align: alignment shift for memset operations * @dev_id: unique device ID * @dev: struct device reference for dma mapping api * @owner: owner module (automatically set based on the provided dev) * @chan_ida: unique channel ID * @src_addr_widths: bit mask of src addr widths the device supports * Width is specified in bytes, e.g. for a device supporting * a width of 4 the mask should have BIT(4) set. * @dst_addr_widths: bit mask of dst addr widths the device supports * @directions: bit mask of slave directions the device supports. * Since the enum dma_transfer_direction is not defined as bit flag for * each type, the dma controller should set BIT(<TYPE>) and same * should be checked by controller as well * @min_burst: min burst capability per-transfer * @max_burst: max burst capability per-transfer * @max_sg_burst: max number of SG list entries executed in a single burst * DMA tansaction with no software intervention for reinitialization. * Zero value means unlimited number of entries. * @descriptor_reuse: a submitted transfer can be resubmitted after completion * @residue_granularity: granularity of the transfer residue reported * by tx_status * @device_alloc_chan_resources: allocate resources and return the * number of allocated descriptors * @device_router_config: optional callback for DMA router configuration * @device_free_chan_resources: release DMA channel's resources * @device_prep_dma_memcpy: prepares a memcpy operation * @device_prep_dma_xor: prepares a xor operation * @device_prep_dma_xor_val: prepares a xor validation operation * @device_prep_dma_pq: prepares a pq operation * @device_prep_dma_pq_val: prepares a pqzero_sum operation * @device_prep_dma_memset: prepares a memset operation * @device_prep_dma_memset_sg: prepares a memset operation over a scatter list * @device_prep_dma_interrupt: prepares an end of chain interrupt operation * @device_prep_slave_sg: prepares a slave dma operation * @device_prep_dma_cyclic: prepare a cyclic dma operation suitable for audio. * The function takes a buffer of size buf_len. The callback function will * be called after period_len bytes have been transferred. * @device_prep_interleaved_dma: Transfer expression in a generic way. * @device_prep_dma_imm_data: DMA's 8 byte immediate data to the dst address * @device_caps: May be used to override the generic DMA slave capabilities * with per-channel specific ones * @device_config: Pushes a new configuration to a channel, return 0 or an error * code * @device_pause: Pauses any transfer happening on a channel. Returns * 0 or an error code * @device_resume: Resumes any transfer on a channel previously * paused. Returns 0 or an error code * @device_terminate_all: Aborts all transfers on a channel. Returns 0 * or an error code * @device_synchronize: Synchronizes the termination of a transfers to the * current context. * @device_tx_status: poll for transaction completion, the optional * txstate parameter can be supplied with a pointer to get a * struct with auxiliary transfer status information, otherwise the call * will just return a simple status code * @device_issue_pending: push pending transactions to hardware * @device_release: called sometime atfer dma_async_device_unregister() is * called and there are no further references to this structure. This * must be implemented to free resources however many existing drivers * do not and are therefore not safe to unbind while in use. * @dbg_summary_show: optional routine to show contents in debugfs; default code * will be used when this is omitted, but custom code can show extra, * controller specific information. * @dbg_dev_root: the root folder in debugfs for this device */ struct dma_device { struct kref ref; unsigned int chancnt; unsigned int privatecnt; struct list_head channels; struct list_head global_node; struct dma_filter filter; dma_cap_mask_t cap_mask; enum dma_desc_metadata_mode desc_metadata_modes; unsigned short max_xor; unsigned short max_pq; enum dmaengine_alignment copy_align; enum dmaengine_alignment xor_align; enum dmaengine_alignment pq_align; enum dmaengine_alignment fill_align; #define DMA_HAS_PQ_CONTINUE (1 << 15) int dev_id; struct device *dev; struct module *owner; struct ida chan_ida; u32 src_addr_widths; u32 dst_addr_widths; u32 directions; u32 min_burst; u32 max_burst; u32 max_sg_burst; bool descriptor_reuse; enum dma_residue_granularity residue_granularity; int (*device_alloc_chan_resources)(struct dma_chan *chan); int (*device_router_config)(struct dma_chan *chan); void (*device_free_chan_resources)(struct dma_chan *chan); struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)( struct dma_chan *chan, dma_addr_t dst, dma_addr_t src, size_t len, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_xor)( struct dma_chan *chan, dma_addr_t dst, dma_addr_t *src, unsigned int src_cnt, size_t len, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_xor_val)( struct dma_chan *chan, dma_addr_t *src, unsigned int src_cnt, size_t len, enum sum_check_flags *result, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_pq)( struct dma_chan *chan, dma_addr_t *dst, dma_addr_t *src, unsigned int src_cnt, const unsigned char *scf, size_t len, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_pq_val)( struct dma_chan *chan, dma_addr_t *pq, dma_addr_t *src, unsigned int src_cnt, const unsigned char *scf, size_t len, enum sum_check_flags *pqres, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_memset)( struct dma_chan *chan, dma_addr_t dest, int value, size_t len, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_memset_sg)( struct dma_chan *chan, struct scatterlist *sg, unsigned int nents, int value, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_interrupt)( struct dma_chan *chan, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_slave_sg)( struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_transfer_direction direction, unsigned long flags, void *context); struct dma_async_tx_descriptor *(*device_prep_dma_cyclic)( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_transfer_direction direction, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_interleaved_dma)( struct dma_chan *chan, struct dma_interleaved_template *xt, unsigned long flags); struct dma_async_tx_descriptor *(*device_prep_dma_imm_data)( struct dma_chan *chan, dma_addr_t dst, u64 data, unsigned long flags); void (*device_caps)(struct dma_chan *chan, struct dma_slave_caps *caps); int (*device_config)(struct dma_chan *chan, struct dma_slave_config *config); int (*device_pause)(struct dma_chan *chan); int (*device_resume)(struct dma_chan *chan); int (*device_terminate_all)(struct dma_chan *chan); void (*device_synchronize)(struct dma_chan *chan); enum dma_status (*device_tx_status)(struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *txstate); void (*device_issue_pending)(struct dma_chan *chan); void (*device_release)(struct dma_device *dev); /* debugfs support */ void (*dbg_summary_show)(struct seq_file *s, struct dma_device *dev); struct dentry *dbg_dev_root; };
它包含了以下成员:
-
ref:引用计数,每次分配或释放通道时都会增加或减少;
-
chancnt:支持的DMA通道数量;
-
privatecnt:通过dma_request_channel请求的DMA通道数量;
-
channels:DMA通道的链表,链表中每个成员都是struct dma_chan类型;保存DMA控制器支持的所有channel的链表的链表头,在初始化时应该使用INIT_LIST_HEAD宏将其初始化为链表,随后然后调用list_add_tail将所有的channel添加到该链表头中;
-
global_node:链表节点,该DMA设备会通过该节点添加到全局变量dma_device_list中;
-
filter:设备/从设备过滤器函数和参数映射的信息;
-
cap_mask:一个bitmap,表示DMA控制器支持的传输类型,支持的传输类型见enum dma_transaction_type中的定义:
- DMA_MEMCPY:该设备能够进行内存到内存的复制;
- DMA_XOR:该设备能够对内存区域执行异或操作;
- DMA_SG:该设备支持内存到内存的分散/聚合传输;
- DMA_SLAVE:该设备可以处理设备到内存的传输,包括分散-聚集转移;
- DMA_CYCLIC:该设备可以处理循环传输;
-
desc_metadata_modes:DMA设备支持的元数据模式;
-
max_xor:最大XOR源数量,在没有此功能的情况下为 0;
-
max_pq:最大PQ源数量和PQ-continue功能;
-
copy_align:memcpy操作的对齐位移;
-
xor_align:xor 操作的对齐位移;
-
pq_align:pq 操作的对齐位移;
-
fill_align:memset操作的对齐位移;dev_id:设备ID,全局唯一;
-
dev:用于DMA映射API的 struct device 引用;
-
owner:所有者模块(根据提供的 dev 自动设置);
-
chan_ida:通道ID,全局唯一;
-
src_addr_widths::一个bitmap,表示该控制器支持哪些宽度的源地址类型,宽度类型见enum dma_slave_buswidth中的定义;
-
dst_addr_widths:一个bitmap,表示该控制器支持哪些宽度的目标地址类型,宽度类型见enum dma_slave_buswidth中的定义;
-
directions:一个bitmap,表示该控制器支持哪些传输方向,支持的传输方向见enum dma_transfer_direction中的定义;
-
min_burst:该设备支持的最小的burst传输的大小;
-
max_burst:该设备支持的最大的burst传输的大小;
-
max_sg_burst:单个 burst 执行的最大SG列表条目数;
-
descriptor_reuse:提交的传输可以在完成后重新提交;
-
residue_granularity:由 tx_status 报告的传输残留部分的粒度;
-
device_alloc_chan_resources:分配DMA channel资源并返回分配的描述符数量;当client driver调用dma_request_channel函数申请dma channel时,该函数会被回调;
-
device_router_config:DMA 路由器配置的可选回调函数;
-
device_free_chan_resources:释放资源DMA channel资源;当client driver调用dma_release_channel函数释放dma channel时,该函数会被回调;
-
device_prep_dma_memcpy:准备memcpy操作的回调函数;
-
device_prep_dma_xor:准备xor操作的回调函数;
-
device_prep_dma_xor_val:准备xor验证操作的回调函数;
-
device_prep_dma_pq:准备pq操作的回调函数;
-
device_prep_dma_pq_val:准备pqzero_sum操作的回调函数;
-
device_prep_dma_memset:准备memset操作的回调函数;
-
device_prep_dma_memset_sg:准备scatterlist上的memset操作的回调函数;
-
device_prep_dma_interrupt:准备链的结束中断操作的回调函数;
-
device_prep_slave_sg:准备从设备DMA操作的回调函数;
-
device_prep_dma_cyclic:准备适用于音频的循环DMA操作的回调函数;
-
device_prep_interleaved_dma:以通用方式传递DMA表达式的回调函数;
-
device_prep_dma_imm_data:将8字节DMA立即数据传输到目标地址的回调函数;
-
device_caps:可以使用此回调函数覆盖通道特定的DMA从属能力;
-
device_config:将新配置推送到通道,client driver调用dmaengine_slave_config配置DMA channel的时候,dmaengine会调用该回调函数,交给DMA控制器驱动处理;
-
device_pause:暂停通道上的任何传输;
-
device_resume:恢复之前暂停的通道上的任何传输;
-
device_terminate_all:中止通道上的所有传输。返回0或错误代码;
-
device_synchronize:将传输的终止与当前上下文同步;
-
device_tx_status:轮询传输完成情况的回调函数,可选地可以通过txstate参数获得附加的传输状态信息;
-
device_issue_pending:获取待处理队列中的第一个传输描述符,并开始传输。每当传输完成时,它应该移动到列表中的下一个传输。client driver调用dma_async_issue_pending启动传输的时候,会调用该回调函数;
-
device_release: dma_async_device_unregister被调用后的某个时间调用,当对此结构体没有进一步引用时。必须实现此回调函数以释放资源,然而许多现有的驱动程序并未实现,因此在使用中不安全;
-
dbg_summary_show:用于在debugfs中显示内容的可选函数。当省略时将使用默认代码,但自定义代码可以显示额外的控制器特定信息;
-
dbg_dev_root:在debugfs中为此设备的根文件夹;
device_prep_dma*:为DMA的操作函数,client driver通过dmaengine_prep API获取传输描述符的时候,damengine则会直接回调DMA控制器驱动相应的device_prep_dma*接口。需要实现的函数取决于DMA控制器支持的传输类型。
device_pause、device_resume、device_terminate_all:client driver调用dmaengine_pause、dmaengine_resume、dmaengine_terminate_*等API的时候,dmaengine会调用相应的回调函数。
2.1.1 transfer width
DMA传输宽度使用struct dma_slave_buswidth表示,定义在include/linux/dmaengine.h,内容如下:
/** * 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, DMA_SLAVE_BUSWIDTH_128_BYTES = 128, };
在数据结构struct dma_device中src_addr_widths/sdst_addr_widths分别用于指定源地址/目标支持支持的传输宽度。
2.1.2 传输方向
DMA传输方向使用struct dma_transfer_direction表示,定义在include/linux/dmaengine.h,内容如下:
/** * enum dma_transfer_direction - dma transfer mode and direction indicator * @DMA_MEM_TO_MEM: Async/Memcpy mode * @DMA_MEM_TO_DEV: Slave mode & From Memory to Device * @DMA_DEV_TO_MEM: Slave mode & From Device to Memory * @DMA_DEV_TO_DEV: Slave mode & From Device to Device */ enum dma_transfer_direction { DMA_MEM_TO_MEM, DMA_MEM_TO_DEV, DMA_DEV_TO_MEM, DMA_DEV_TO_DEV, DMA_TRANS_NONE, };
上面的enum表明,DMA传输从方向上来说分为mem2mem、mem2dev、dev2mem和dev2dev。
其中,mem2dev、dev2mem和dev2dev属于Slave DMA传输,mem2mem属于Async TX传输。
因为linux为mem2mem的DMA传输在DMA engine之上封装了更为简洁的API接口,即Async TX API,想要了解Async TX API,请参考内核文档:Documentation/crypto/async-tx-api.rst,因此,DMA Engine提供的API就是Slave DMA API。
2.1.3 传输状态
DMA传输状态使用enum dma_status表示,定义在include/linux/dmaengine.h,内容如下:
/** * enum dma_status - DMA transaction status * @DMA_COMPLETE: transaction completed * @DMA_IN_PROGRESS: transaction not yet processed * @DMA_PAUSED: transaction is paused * @DMA_ERROR: transaction failed */ enum dma_status { DMA_COMPLETE, DMA_IN_PROGRESS, DMA_PAUSED, DMA_ERROR, DMA_OUT_OF_ORDER, };
2.1.4 传输类型
我们的dma_device结构体有一个字段叫做cap_mask,用来保存支持的各种传输类型,并且你需要使用dma_cap_set函数修改这个掩码,将支持的传输类型作为参数传入。
DMA传输类型使用enum dma_transaction_type表示,定义在include/linux/dmaengine.h,内容如下:
/** * enum dma_transaction_type - DMA transaction types/indexes * * Note: The DMA_ASYNC_TX capability is not to be set by drivers. It is * automatically set as dma devices are registered. */ enum dma_transaction_type { DMA_MEMCPY, DMA_XOR, DMA_PQ, DMA_XOR_VAL, DMA_PQ_VAL, DMA_MEMSET, DMA_MEMSET_SG, DMA_INTERRUPT, DMA_PRIVATE, DMA_ASYNC_TX, DMA_SLAVE, DMA_CYCLIC, DMA_INTERLEAVE, DMA_COMPLETION_NO_ORDER, DMA_REPEAT, DMA_LOAD_EOT, /* last transaction type for creation of the capabilities mask */ DMA_TX_TYPE_END, };
目前可用的类型有:
(1) DMA_MEMCPY
- 设备能够进行内存到内存的复制;
- 不管源和目标组合缓冲区的总大小是多少,只传输最小的那个字节数。这意味着两个列表中的分散-聚集缓冲区的数量和大小不需要相同,并且操作的功能等效于strncpy,其中 count参数等于两个分散-聚集列表缓冲区的最小总大小;
- 它通常用于在主机内存和内存映射的GPU设备内存之间复制像素数据,例如现代PCI视频图形卡上的情况。最直接的例子是OpenGL API函数 glReadPielx(),它可能需要将一个巨大的帧缓冲从本地设备内存完全复制到主机内存;
(2) DMA_XOR
- 设备能够对内存区域执行异或操作;
- 用于加速异或密集型任务,例如RAID5;
(3) DMA_XOR_VAL
- 设备能够使用异或算法对内存缓冲区执行奇偶校验检查;
(4) DMA_PQ
- 设备能够执行RAID6 P+Q计算,其中P是简单的异或运算,Q是Reed-Solomon算法;
(5) DMA_PQ_VAL
- 设备能够使用RAID6 P+Q算法对内存缓冲区执行奇偶校验检查;
(6) DMA_MEMSET
- 设备能够使用提供的模式填充内存;
- 模式被视为单字节有符号值;
(7) DMA_INTERRUPT
- 设备能够触发一个虚拟传输,从而生成周期性中断;
- 客户端驱动程序使用DMA控制器中断定期调用回调函数;
(8) DMA_PRIVATE
- 设备仅支持从设备传输,因此不可用于异步传输;
(9) DMA_ASYNC_TX
- 设备不应设置此标志,如果需要,框架将设置该标志;
(10) DMA_SLAVE
- 设备可以处理设备到内存的传输,包括散射-聚集传输;
- 虽然在mem2mem情况下,我们有两种不同类型来处理单个要复制的块或它们的集合,但在这里,我们只有一种处理两者的传输类型;
- 如果您想传输单个连续的内存缓冲区,只需构建一个只包含一个项目的散射列表即可;
(11) DMA_CYCLIC
- 设备可以处理循环传输;
- 循环传输是指块集合将循环遍历自身,最后一个项指向第一个项;
- 它通常用于音频传输,其中您希望操作一个您将用音频数据填充的单个环形缓冲区;
(12) DMA_INTERLEAVE
- 设备支持交错传输;
- 这些传输可以将数据从非连续缓冲区传输到非连续缓冲区,与DMA_SLAVE相反,后者可以将数据从非连续数据集传输到连续目标缓冲区;
- 它通常用于2D内容传输,此时您希望直接将一部分未压缩数据传输到显示屏以打印;
(13) DMA_COMPLETION_NO_ORDER
- 设备不支持有序完成;
- 如果设备设置了此功能,则驱动程序应在device_tx_status中返回DMA_OUT_OF_ORDER;
- 如果设备导出此功能,则所有cookie跟踪和检查API应视为无效;
- 在这一点上,这与dmatest的轮询选项不兼容;
- 如果设置了此标志,建议用户为发送到DMA设备的每个描述符提供唯一标识符,以便正确跟踪完成情况;
(14) DMA_REPEAT
- 设备支持重复传输。重复传输由DMA_PREP_REPEAT传输标志表示,类似于循环传输,在结束时会自动重复,但可以由客户端替换;
- 此功能仅限于交错传输,因此如果未设置DMA_INTERLEAVE标志,则不应设置此标志。这种限制是基于DMA客户端的当前需求,如果将来有需要,应添加对其他传输类型的支持;
(15) DMA_LOAD_EOT
- 设备支持在传输结束(EOT)时通过将DMA_PREP_LOAD_EOT标志设置为新传输队列来替换重复传输;
- 根据DMA客户端的需求,将来可能会添加在其他时间点(例如在传输结束之前而不是转移结束时)替换当前运行中的传输的支持
这些不同类型也会影响随时间变化的源地址和目标地址。
指向RAM的地址通常在每次传输后递增(或递减)。在环形缓冲区的情况下,它们可能会循环(DMA_CYCLIC)。指向设备寄存器(例如FIFO)的地址通常是固定的。
2.2 struct dma_chan
linux内核使用数据结构struct dam_chan来表示DMA通道,定义在include/linux/dmaengine.h,内容如下:
/** * struct dma_chan - devices supply DMA channels, clients use them * @device: ptr to the dma device who supplies this channel, always !%NULL * @slave: ptr to the device using this channel * @cookie: last cookie value returned to client * @completed_cookie: last completed cookie for this channel * @chan_id: channel ID for sysfs * @dev: class device for sysfs * @name: backlink name for sysfs * @dbg_client_name: slave name for debugfs in format: * dev_name(requester's dev):channel name, for example: "2b00000.mcasp:tx" * @device_node: used to add this to the device chan list * @local: per-cpu pointer to a struct dma_chan_percpu * @client_count: how many clients are using this channel * @table_count: number of appearances in the mem-to-mem allocation table * @router: pointer to the DMA router structure * @route_data: channel specific data for the router * @private: private data for certain client-channel associations */ struct dma_chan { struct dma_device *device; struct device *slave; dma_cookie_t cookie; dma_cookie_t completed_cookie; /* sysfs */ int chan_id; struct dma_chan_dev *dev; const char *name; #ifdef CONFIG_DEBUG_FS char *dbg_client_name; #endif struct list_head device_node; struct dma_chan_percpu __percpu *local; int client_count; int table_count; /* DMA router */ struct dma_router *router; void *route_data; void *private; };
它包含了以下成员:
- device:指向提供此通道的DMA设备的指针;
- slave:指向使用此通道的设备的指针;
- cookie:返回给客户端的最后一个cookie值;
- completed_cookie:此通道上最后完成的cookie;
- chan_id:为DMA设备当前通道分配一个唯一id;
- dev:用于DMA通道设备的数据结构,类型为struct dma_chan_dev;
- name:sysfs中的反向链接名称;
- dbg_client_name:用于debugfs的从设备名称,例如:"2b00000.mcasp:tx";
- device_node:链表节点,用于将其添加到DMA设备(dma_device)通道链表channels中;
- local:指向 dma_chan_percpu 结构的per-cpu指针;
- client_count:使用此通道的客户端数量;
- table_count:在mem-to-mem分配表中出现的次数;
- router:指向DMA路由器结构的指针;
- route_data:用于路由器的通道特定数据;
- private:特定客户端-通道关联的私有数据;
其中dma_cookie_t本质上就是s32类型,它是一个随时间递增的DMA传输ID。
include/linux/dmaengine.h:22:typedef s32 dma_cookie_t;
2.2.1 struct dma_chan_dev
struct dma_chan_dev用于将sysfs设备节点与通道支持的设备相关联,定义如下:
/** * struct dma_chan_dev - relate sysfs device node to backing channel device * @chan: driver channel device * @device: sysfs device * @dev_id: parent dma_device dev_id * @chan_dma_dev: The channel is using custom/different dma-mapping * compared to the parent dma_device */ struct dma_chan_dev { struct dma_chan *chan; struct device device; int dev_id; bool chan_dma_dev; };
它包含以下成员:
- chan:指向DMA通道的指针;
- device:sysfs设备节点;
- dev_id:所属的DMA设备的id,即父dma_device ->dev_id;
- chan_dma_dev:该通道使用与父dma_device不同的定制/不同的 dma-mapping;
2.2.2 struct virt_dma_desc
每个DMA控制器包含多个通道,每个DMA通道可以连接到多个外设的请求,比如UART、SPI、I2C等。DMA控制器抽象出来struct dam_chan对应DMA控制器的物理channel,又抽象出来虚拟的channel,软件上可以实现多个虚拟channel对应一个物理channel。
在linux内核中,使用数据结构struct virt_dma_desc表示虚拟channel,定义在drivers/dma/virt-dma.h:
struct virt_dma_chan { struct dma_chan chan; struct tasklet_struct task; void (*desc_free)(struct virt_dma_desc *); spinlock_t lock; /* protected by vc.lock */ struct list_head desc_allocated; struct list_head desc_submitted; struct list_head desc_issued; struct list_head desc_completed; struct list_head desc_terminated; struct virt_dma_desc *cyclic; };
其中:
- chan:指向DMA channel的指针,用于和client driver打交道;
- task:一个tasklet,用于等待该虚拟channel上传输的完成(由于是虚拟channel,传输完成与否只能由软件判断);
- desc_allocated & desc_submitted & desc_issued & desc_completed:四个链表头,用于保存不同状态的虚拟channel描述符;
- cyclic:描述了一个struct dma_async_tx_descriptor的链表;
三、DMA client device
介绍完立刻了DMA controller driver中涉及到的数据结构,我们将会从DMA传输的客户端设备驱动的角度,即consumer的角度去介绍DMA框架为client driver提供了哪些功能和API。
首先要了解DMA client driver涉及到的数据结构,知道每个数据结构以及成员的含义之后,再去看源码就容易了。
3.1 struct dma_slave_config
struct dma_slave_config结构体包含了完成一次DMA传输所需要的所有可能的参数,用于配置DMA slave通道运行时参数,定义在include/linux/dmaengine.h:
/** * struct dma_slave_config - dma slave channel runtime config * @direction: whether the data shall go in or out on this slave * channel, right now. DMA_MEM_TO_DEV and DMA_DEV_TO_MEM are * legal values. DEPRECATED, drivers should use the direction argument * to the device_prep_slave_sg and device_prep_dma_cyclic functions or * the dir field in the dma_interleaved_template structure. * @src_addr: this is the physical address where DMA slave data * should be read (RX), if the source is memory this argument is * ignored. * @dst_addr: this is the physical address where DMA slave data * should be written (TX), if the destination is memory this argument * is ignored. * @src_addr_width: this is the width in bytes of the source (RX) * register where DMA data shall be read. If the source * is memory this may be ignored depending on architecture. * Legal values: 1, 2, 3, 4, 8, 16, 32, 64, 128. * @dst_addr_width: same as src_addr_width but for destination * target (TX) mutatis mutandis. * @src_maxburst: the maximum number of words (note: words, as in * units of the src_addr_width member, not bytes) that can be sent * in one burst to the device. Typically something like half the * FIFO depth on I/O peripherals so you don't overflow it. This * may or may not be applicable on memory sources. * @dst_maxburst: same as src_maxburst but for destination target * mutatis mutandis. * @src_port_window_size: The length of the register area in words the data need * to be accessed on the device side. It is only used for devices which is using * an area instead of a single register to receive the data. Typically the DMA * loops in this area in order to transfer the data. * @dst_port_window_size: same as src_port_window_size but for the destination * port. * @device_fc: Flow Controller Settings. Only valid for slave channels. Fill * with 'true' if peripheral should be flow controller. Direction will be * selected at Runtime. * @peripheral_config: peripheral configuration for programming peripheral * for dmaengine transfer * @peripheral_size: peripheral configuration buffer size * * This struct is passed in as configuration data to a DMA engine * in order to set up a certain channel for DMA transport at runtime. * The DMA device/engine has to provide support for an additional * callback in the dma_device structure, device_config and this struct * will then be passed in as an argument to the function. * * 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; void *peripheral_config; size_t peripheral_size; };
该结构体具有以下字段:
- direction:数据在DMA通道上的进行传输的方向,可以是DMA_MEM_TO_DEV(内存到设备)或DMA_DEV_TO_MEM(设备到内存)。该字段已被弃用,驱动程序应使用device_prep_slave_sg和device_prep_dma_cyclic函数中的方向参数或dma_interleaved_template结构体中的dir字段;
- src_addr:当src是设备时,即从dev2dev或者从dev2mem时,读取DMA从数据的物理地址,反之,当src是内存时,即从mem2dev或者mem2mem时,则忽略此参数;
- dst_addr:当dst是设备时,即从mem2dev或者从dev2dev时,写入DMA从数据的物理地址,反之,当dst是mem时,即从mem2dev或者mem2mem时,则忽略此参数;
- src_addr_width:读取DMA数据的源寄存器的宽度(以字节为单位)。如果源是内存,则根据架构可能会忽略此参数。合法值为1、2、3、4、8、16、32、64、128;
- dst_addr_width:与src_addr_width相同,写入DMA数据的目的寄存器的宽度(以字节为单位);
- src_maxburst:一次传输到设备的最大字数(注意:字数以src_addr_width成员作为单位,而不是字节)。通常是I/O外设FIFO深度的一半,以防止溢出。这可能适用于内存源,也可能不适用;
- dst_maxburst:与src_maxburst相同,但适用于目的地(TX)目标;
- src_port_window_size:数据在设备端访问所需的寄存器区域的长度(以字为单位)。仅用于使用区域而不是单个寄存器接收数据的设备。通常情况下,DMA在该区域中循环以传输数据;
- dst_port_window_size:与src_port_window_size相同,但适用于目的地端口;
- device_fc:流控制器设置。仅对从设备通道有效。如果外设应该是流控制器,则填充为true。方向将在运行时选择;
- peripheral_config:用于配置外设进行DMA传输的外设配置;
- peripheral_size:外设配置缓冲区的大小;
该结构体作为配置数据传递给DMA controll driver,以在运行时设置特定通道的DMA传输。DMA controll driver必须提供对dma_device结构体中device_config函数的支持,并且该结构体将作为参数传递给该函数。
3.2 struct dma_async_tx_descriptor
struct dma_async_tx_descriptor用于描述一次DMA传输,表示DMA异步传输描述符,定义在include/linux/dmaengine.h:
/** * struct dma_async_tx_descriptor - async transaction descriptor * ---dma generic offload fields--- * @cookie: tracking cookie for this transaction, set to -EBUSY if * this tx is sitting on a dependency list * @flags: flags to augment operation preparation, control completion, and * communicate status * @phys: physical address of the descriptor * @chan: target channel for this operation * @tx_submit: accept the descriptor, assign ordered cookie and mark the * descriptor pending. To be pushed on .issue_pending() call * @callback: routine to call after this operation is complete * @callback_param: general parameter to pass to the callback routine * @desc_metadata_mode: core managed metadata mode to protect mixed use of * DESC_METADATA_CLIENT or DESC_METADATA_ENGINE. Otherwise * DESC_METADATA_NONE * @metadata_ops: DMA driver provided metadata mode ops, need to be set by the * DMA driver if metadata mode is supported with the descriptor * ---async_tx api specific fields--- * @next: at completion submit this descriptor * @parent: pointer to the next level up in the dependency chain * @lock: protect the parent and next pointers */ 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; enum dma_desc_metadata_mode desc_metadata_mode; struct dma_descriptor_metadata_ops *metadata_ops; #ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH struct dma_async_tx_descriptor *next; struct dma_async_tx_descriptor *parent; spinlock_t lock; #endif };
该结构体具有以下字段:
- cookie: 用于跟踪此传输的标识符,如果此传输正在依赖列表上等待,则设置为-EBUSY;
- flags: 用于增强操作准备、控制完成和传递状态的标志;
- phys: 描述符的物理地址;
- chan: 目标通道,用于执行该操作;
- tx_submit: 接受描述符并分配有序的标识符,并标记描述符为挂起状态。在调用.issue_pending()时推送;
- callback: 操作完成后要调用的函数;
- callback_param: 传递给回调函数的参数;
- desc_metadata_mode: 核心管理的元数据模式,以保护对DESC_METADATA_CLIENT或DESC_METADATA_ENGINE的混合使用。否则,为DESC_METADATA_NONE;
- metadata_ops: DMA驱动程序提供的元数据模式操作,如果支持描述符的元数据模式,则DMA驱动程序需要设置它;
- next: 在完成后提交此描述符;
- parent: 指向依赖链中的上一级的指针;
- lock: 保护parent和next指针的自旋锁。
3.3 ASoC中的DMA
由于在ASoC的platform驱动中,会使用DMA将音频数据从内存(dma buffer)拷贝到 I2S tx FIFO,因此这一小节我们介绍一下音频驱动中涉及到的与DMA相关的数据结构。这些数据结构定义在定义在include/sound/dmaengine_pcm.h。
我们之前介绍过音频驱动的两大任务:
- playback :如何把用户空间的应用程序发过来的PCM数据,转化为人耳可以辨别的模拟音频;
- capture :把mic拾取到得模拟信号,经过采样、量化,转换为PCM数据送回给用户空间的应用程序;
而这两大任务是通过ALSA CORE的PCM驱动来完成的,每一个pcm实例下面有一个playback stream和capture stream,分别用于音频播放和录音。
3.3.1 struct dmaengine_pcm
struct dmaengine_pcm用来定义音频传输的配置和管理。它包含了DMA通道数组、DMA配置数据、ASoC组件以及一些标志位。
通过这个结构体,可以配置和管理PCM的DMA传输通道;
struct dmaengine_pcm { struct dma_chan *chan[SNDRV_PCM_STREAM_LAST + 1]; // 数组长度为2 const struct snd_dmaengine_pcm_config *config; struct snd_soc_component component; unsigned int flags; };
其中:
-
chan:数组长度为2,分别存放音频数据发送和接收使用的DMA通道;
-
config:PCM配置数据;
-
component: struct snd_soc_component,可以看出来为DMA driver单独搞了一个struct snd_soc_component;
-
flags:一些标志位;比如:
- SND_DMAENGINE_PCM_FLAG_COMPAT:兼容性标志位,表示如果无法通过设备树来进行请求DMA通道,则尝试使用compat_request_channel或compat_filter_fn函数来请求DMA通道;
- SND_DMAENGINE_PCM_FLAG_NO_DT:非设备树标志位,表示不尝试通过设备树来请求DMA通道。只有在设置了SND_DMAENGINE_PCM_FLAG_COMPAT标志位时,该标志才有意义;
- SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX:半双工标志位,表示PCM是半双工的,DMA通道在捕获和播放之间共享;
3.3.2 struct snd_dmaengine_pcm_config
struct snd_dmaengine_pcm_config是基于DMA框架上的用于PCM配置的数据结构,实际上就是在DMA框架的基础上做了一层封装,适用于PCM设备;
/** * struct snd_dmaengine_pcm_config - Configuration data for dmaengine based PCM * @prepare_slave_config: Callback used to fill in the DMA slave_config for a * PCM substream. Will be called from the PCM drivers hwparams callback. * @compat_request_channel: Callback to request a DMA channel for platforms * which do not use devicetree. * @process: Callback used to apply processing on samples transferred from/to * user space. * @compat_filter_fn: Will be used as the filter function when requesting a * channel for platforms which do not use devicetree. The filter parameter * will be the DAI's DMA data. * @dma_dev: If set, request DMA channel on this device rather than the DAI * device. * @chan_names: If set, these custom DMA channel names will be requested at * registration time. * @pcm_hardware: snd_pcm_hardware struct to be used for the PCM. * @prealloc_buffer_size: Size of the preallocated audio buffer. * * Note: If both compat_request_channel and compat_filter_fn are set * compat_request_channel will be used to request the channel and * compat_filter_fn will be ignored. Otherwise the channel will be requested * using dma_request_channel with compat_filter_fn as the filter function. */ struct snd_dmaengine_pcm_config { int (*prepare_slave_config)(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params, struct dma_slave_config *slave_config); struct dma_chan *(*compat_request_channel)( struct snd_soc_pcm_runtime *rtd, struct snd_pcm_substream *substream); int (*process)(struct snd_pcm_substream *substream, int channel, unsigned long hwoff, void *buf, unsigned long bytes); dma_filter_fn compat_filter_fn; struct device *dma_dev; const char *chan_names[SNDRV_PCM_STREAM_LAST + 1]; const struct snd_pcm_hardware *pcm_hardware; unsigned int prealloc_buffer_size; };
其中:
- prepare_slave_config:用于为PCM substream填充DMA slave_config的回调函数。将在PCM驱动程序的hw_params回调中调;
- compat_request_channel:用于请求DMA通道的回调函数,适用于不使用设备树的平台;
- process:用于对从/向用户空间传输的样本应用处理的回调函数;
- compat_filter_fn:在请求没有使用设备树的平台的通道时将用作过滤函数。filter参数将是DAI的DMA数据;
- dma_dev:如果设置了,将在此设备上请求DMA通道,而不是DAI设备;
- chan_names:如果设置了,将会根据这些DMA通道的名字来分配DMA通道;
- pcm_hardware:snd_pcm_hardware结构,用于PCM;
- prealloc_buffer_size:预分配音频缓冲区的大小;
3.3.3 struct snd_dmaengine_dai_dma_data
struct snd_dmaengine_dai_dma_data结构体定义了DAI DMA配置数据的信息;
/** * struct snd_dmaengine_dai_dma_data - DAI DMA configuration data * @addr: Address of the DAI data source or destination register. * @addr_width: Width of the DAI data source or destination register. * @maxburst: Maximum number of words(note: words, as in units of the * src_addr_width member, not bytes) that can be send to or received from the * DAI in one burst. * @filter_data: Custom DMA channel filter data, this will usually be used when * requesting the DMA channel. * @chan_name: Custom channel name to use when requesting DMA channel. * @fifo_size: FIFO size of the DAI controller in bytes * @flags: PCM_DAI flags, only SND_DMAENGINE_PCM_DAI_FLAG_PACK for now * @peripheral_config: peripheral configuration for programming peripheral * for dmaengine transfer * @peripheral_size: peripheral configuration buffer size */ struct snd_dmaengine_dai_dma_data { dma_addr_t addr; enum dma_slave_buswidth addr_width; u32 maxburst; void *filter_data; const char *chan_name; unsigned int fifo_size; unsigned int flags; void *peripheral_config; size_t peripheral_size; };
它包含以下成员变量:
- addr:DAI数据源或目的寄存器的地址;
- addr_width:DAI数据源或目的寄存器的宽度;
- maxburst:在一个突发传输中可以发送到或从DAI接收的最大字(注:这里的字指的是根据src_addr_width成员的单位,而不是字节)数;
- filter_data:自定义的DMA通道过滤器数据,通常在请求DMA通道时使用;
- chan_name:在请求DMA通道时使用的自定义通道名称;
- fifo_size:DAI控制器的FIFO大小(以字节为单位);
- flags:PCM_DAI标志,目前只有SND_DMAENGINE_PCM_DAI_FLAG_PACK;
- peripheral_config:用于dmaengine传输的外设配置;
- peripheral_size:外设配置缓冲区的大小。
四、注册DMA controller device
DMA controller driver的编写,实际上就是根据SoC的DMA controller信息去构建dma_device以及dma_chan,然后去根据SoC DMAcontroller寄存器去编写dma_device的操作函数,最后将其注册到内核即可。
编写一个dma controller driver的基本步骤包括(不考虑虚拟channel的情况):
(1)定义一个struct dma_device变量,并根据实际的硬件情况,填充其中的关键字段。
(2)根据controller支持的channel个数,为每个channel定义一个struct dma_chan变量,进行必要的初始化后,将每个channel都添加到struct dma_device变量的channels链表中;
(3)根据硬件特性,实现struct dma_device变量中必要的回调函数(device_alloc_chan_resources/device_free_chan_resources、device_prep_dma_xxx、device_config、device_issue_pending等等);
(4)调用dma_async_device_register将struct dma_device变量注册到kernel中;
(5)当client driver申请dma channel时(例如通过device tree中的dma节点获取),dmaengine core会调用dma controller driver的device_alloc_chan_resources函数,controller driver需要在这个接口中将该channel的资源准备好;
(6)当client driver配置某个dma channel时,dmaengine core会调用dma controller driver的device_config函数,controller driver需要在这个函数中将client想配置的内容准备好,以便进行后续的传输;
(7)client driver开始一个传输之前,会把传输的信息通过dmaengine_prep_slave_xxx接口交给controller driver,controller driver需要在对应的device_prep_dma_xxx回调中,将这些要传输的内容准备好,并返回给client driver一个传输描述符;
(8)然后,client driver会调用dmaengine_submit将该传输提交给controller driver,此时dmaengine会调用controller driver为每个传输描述符所提供的tx_submit回调函数,controller driver需要在这个函数中将描述符挂到该channel对应的传输队列中;
(9)client driver开始传输时,会调用dma_async_issue_pending,controller driver需要在对应的回调函数(device_issue_pending)中,依次将队列上所有的传输请求提交给硬件;
(10)其他需要的步骤。
4.1 注册DMA设备
dma_async_device_register用于注册DMA设备,该函数需要传入一个struct dma_device数据结构的指针;函数定义在drivers/dma/dmaengine.c,
/** * dma_async_device_register - registers DMA devices found * @device: pointer to &struct dma_device * * After calling this routine the structure should not be freed except in the * device_release() callback which will be called after * dma_async_device_unregister() is called and no further references are taken. */ int dma_async_device_register(struct dma_device *device) { int rc; struct dma_chan* chan; if (!device) // 1. 参数校验 return -ENODEV; /* validate device routines */ if (!device->dev) { pr_err("DMAdevice must have dev\n"); return -EIO; } device->owner = device->dev->driver->owner; if (dma_has_cap(DMA_MEMCPY, device->cap_mask) && !device->device_prep_dma_memcpy) { // 内存拷贝传输类型,需要实现device_prep_dma_memcpy dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_MEMCPY"); return -EIO; } if (dma_has_cap(DMA_XOR, device->cap_mask) && !device->device_prep_dma_xor) { // 异或传输类型需要实现device_prep_dma_xor dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_XOR"); return -EIO; } if (dma_has_cap(DMA_XOR_VAL, device->cap_mask) && !device->device_prep_dma_xor_val) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_XOR_VAL"); return -EIO; } if (dma_has_cap(DMA_PQ, device->cap_mask) && !device->device_prep_dma_pq) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_PQ"); return -EIO; } if (dma_has_cap(DMA_PQ_VAL, device->cap_mask) && !device->device_prep_dma_pq_val) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_PQ_VAL"); return -EIO; } if (dma_has_cap(DMA_MEMSET, device->cap_mask) && !device->device_prep_dma_memset) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_MEMSET"); return -EIO; } if (dma_has_cap(DMA_INTERRUPT, device->cap_mask) && !device->device_prep_dma_interrupt) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_INTERRUPT"); return -EIO; } if (dma_has_cap(DMA_CYCLIC, device->cap_mask) && !device->device_prep_dma_cyclic) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_CYCLIC"); return -EIO; } if (dma_has_cap(DMA_INTERLEAVE, device->cap_mask) && !device->device_prep_interleaved_dma) { dev_err(device->dev, "Device claims capability %s, but op is not defined\n", "DMA_INTERLEAVE"); return -EIO; } if (!device->device_tx_status) { dev_err(device->dev, "Device tx_status is not defined\n"); return -EIO; } if (!device->device_issue_pending) { dev_err(device->dev, "Device issue_pending is not defined\n"); return -EIO; } if (!device->device_release) dev_dbg(device->dev, "WARN: Device release is not defined so it is not safe to unbind this driver while in use\n"); kref_init(&device->ref); // 2. 初始化引用计数 /* note: this only matters in the * CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH=n case */ if (device_has_all_tx_types(device)) dma_cap_set(DMA_ASYNC_TX, device->cap_mask); rc = get_dma_id(device); // 为DMA设备分配唯一ID if (rc != 0) return rc; ida_init(&device->chan_ida); /* represent channels in sysfs. Probably want devs too */ list_for_each_entry(chan, &device->channels, device_node) { // 遍历DMA通道链表,注册每一个通道 rc = __dma_async_device_channel_register(device, chan); if (rc < 0) goto err_out; } mutex_lock(&dma_list_mutex); // 获取互斥锁 /* take references on public channels */ if (dmaengine_ref_count && !dma_has_cap(DMA_PRIVATE, device->cap_mask)) list_for_each_entry(chan, &device->channels, device_node) { /* if clients are already waiting for channels we need * to take references on their behalf */ if (dma_chan_get(chan) == -ENODEV) { /* note we can only get here for the first * channel as the remaining channels are * guaranteed to get a reference */ rc = -ENODEV; mutex_unlock(&dma_list_mutex); goto err_out; } } list_add_tail_rcu(&device->global_node, &dma_device_list); // 将当前DMA设备添加到全局链表dma_device_list中 if (dma_has_cap(DMA_PRIVATE, device->cap_mask)) device->privatecnt++; /* Always private */ dma_channel_rebalance(); mutex_unlock(&dma_list_mutex); // 释放互斥锁 dmaengine_debug_register(device); return 0; err_out: /* if we never registered a channel just release the idr */ if (!device->chancnt) { ida_free(&dma_ida, device->dev_id); return rc; } list_for_each_entry(chan, &device->channels, device_node) { if (chan->local == NULL) continue; mutex_lock(&dma_list_mutex); chan->dev->chan = NULL; mutex_unlock(&dma_list_mutex); device_unregister(&chan->dev->device); free_percpu(chan->local); } return rc; }
这段函数主要执行流程如下:
(1) 首先对DMA设备的成员进行校验:
- 检查传入的device指针是否为空,如果为空则返回-ENODEV错误码;
- 检查device结构体中的dev字段,如果为空则输出错误信息并返回-EIO错误码;
- 函数会将device->dev->driver->owner赋值给device->owner,用于记录设备所属的驱动程序的拥有者;
- 检查设备是否支持各种DMA传输类型,如DMA_MEMCPY、DMA_XOR等。如果设备声明支持某个传输类型但对应的函数指针未定义,则输出错误信息并返回-EIO错误码;
- 检查device->device_tx_status和device->device_issue_pending是否定义,如果未定义则输出错误信息并返回-EIO错误码;
(2) 进行DMA设备成员的初始化;
- 函数初始化device->ref;
- 并调用get_dma_id函数为DMA设备分配ID,并赋值给device->dev_id;
- 函数初始化device->chan_ida;
- 遍历device->channels链表,为每个通道调用__dma_async_device_channel_register函数进行注册;
- 获取dma_list_mutex互斥锁,并在配置了公共通道且没有DMA_PRIVATE能力的情况下递增引用计数以防止其他客户端等待通道;
- 调用list_add_tail_rcu将设备添加到dma_device_list链表中;
- 调用dma_channel_rebalance函数进行通道的重新平衡;
- 注册设备的调试信息;
4.1.1 get_dma_id
在dma_ida树中为DMA设备分配id,并返回这个唯一的ID,ida是基于idr(redix树)实现的,这里就不深究了;
static int get_dma_id(struct dma_device *device) { int rc = ida_alloc(&dma_ida, GFP_KERNEL); if (rc < 0) return rc; device->dev_id = rc; return 0; }
全局变量dma_ida定义如下:
static DEFINE_IDA(dma_ida);
4.1.2 ida_init
ida_init函数定义在include/linux/idr.h,用于初始化ida:
static inline void ida_init(struct ida *ida) { xa_init_flags(&ida->xa, IDA_INIT_FLAGS); }
4.1.3 __dma_async_device_channel_register
__dma_async_device_channel_register定义在drivers/dma/dmaengine.c,用于注册 DMA通道,函数如下:
static int __dma_async_device_channel_register(struct dma_device *device, struct dma_chan *chan) // dma通道 { int rc; chan->local = alloc_percpu(typeof(*chan->local)); // 分配用于保存每个CPU核心上通道本地数据的内存空间 if (!chan->local) return -ENOMEM; chan->dev = kzalloc(sizeof(*chan->dev), GFP_KERNEL); // 动态分配内存,指向struct dma_chan_dev数据结构 if (!chan->dev) { rc = -ENOMEM; goto err_free_local; } /* * When the chan_id is a negative value, we are dynamically adding * the channel. Otherwise we are static enumerating. */ chan->chan_id = ida_alloc(&device->chan_ida, GFP_KERNEL); // 为当前DMA设备的该通道分配一个唯一id if (chan->chan_id < 0) { pr_err("%s: unable to alloc ida for chan: %d\n", __func__, chan->chan_id); rc = chan->chan_id; goto err_free_dev; } chan->dev->device.class = &dma_devclass; // dma_devclass,从而当注册chan->dev->device.设备后,会在/sys/class/dma下创建以chan->dev->device设备名命名的文件夹 chan->dev->device.parent = device->dev; chan->dev->chan = chan; // 初始化当前通道 chan->dev->dev_id = device->dev_id; dev_set_name(&chan->dev->device, "dma%dchan%d", // 设置设备名称为dma%dchan%d,第一个%d为DMA设备的id,第二个当前通道通道的id device->dev_id, chan->chan_id); rc = device_register(&chan->dev->device); // 注册DMA通道设备,会在sysfs文件系统为DMA通道设备创建目录 if (rc) goto err_out_ida; chan->client_count = 0; device->chancnt++; // 统计DMA设备通道个数 return 0; err_out_ida: ida_free(&device->chan_ida, chan->chan_id); err_free_dev: kfree(chan->dev); err_free_local: free_percpu(chan->local); chan->local = NULL; return rc; }
其中class dma_devclass定义如下:
static struct class dma_devclass = { .name = "dma", .dev_groups = dma_dev_groups, .dev_release = chan_dev_release, };
其注册实际为dmaengine模块的入口函数:
static int __init dma_bus_init(void) { int err = dmaengine_init_unmap_pool(); if (err) return err; err = class_register(&dma_devclass); // 注册class if (!err) dmaengine_debugfs_init(); return err; }
如下是rk3399开发板内核启动后 /sys/class/dma/的目录结构:
root@rk3399:/# ll /sys/class/dma/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan0 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan0/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan1 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan1/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan10 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan10/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan11 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan11/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan2 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan2/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan3 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan3/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan4 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan4/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan5 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan5/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan6 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan6/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan7 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan7/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan8 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan8/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma0chan9 -> ../../devices/platform/ff6d0000.dma-controller/dma/dma0chan9/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan0 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan0/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan1 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan1/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan10 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan10/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan11 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan11/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan12 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan12/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan13 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan13/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan14 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan14/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan15 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan15/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan16 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan16/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan17 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan17/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan18 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan18/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan19 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan19/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan2 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan2/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan3 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan3/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan4 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan4/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan5 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan5/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan6 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan6/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan7 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan7/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan8 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan8/ lrwxrwxrwx 1 root root 0 Mar 15 23:04 dma1chan9 -> ../../devices/platform/ff6e0000.dma-controller/dma/dma1chan9/
从上面输出的信息可以看出DMAC0对应12个通道,也就是说将DMAC0支持的每一种外设请求注册成一个通道,一共12个通道,DMAC1同理。
4.1.4 dma_list_mutex
互斥锁dma_list_mutex定义如下:
static DEFINE_MUTEX(dma_list_mutex);
4.1.5 dma_device_list
全局变量dma_device_list定义如下:
static LIST_HEAD(dma_device_list);
4.2 卸载DMA设备
dma_async_device_unregister用于注册DMA设备,该函数需要传入一个struct dma_device数据结构的指针;函数定义在drivers/dma/dmaengine.c,
/** * dma_async_device_unregister - unregister a DMA device * @device: pointer to &struct dma_device * * This routine is called by dma driver exit routines, dmaengine holds module * references to prevent it being called while channels are in use. */ void dma_async_device_unregister(struct dma_device *device) { struct dma_chan *chan, *n; dmaengine_debug_unregister(device); list_for_each_entry_safe(chan, n, &device->channels, device_node) __dma_async_device_channel_unregister(device, chan); // 卸载DMA通道 mutex_lock(&dma_list_mutex); /* * setting DMA_PRIVATE ensures the device being torn down will not * be used in the channel_table */ dma_cap_set(DMA_PRIVATE, device->cap_mask); dma_channel_rebalance(); ida_free(&dma_ida, device->dev_id); // 释放DMA设备id dma_device_put(device); mutex_unlock(&dma_list_mutex); }
该函数与注册过程是相反的,就不介绍了。
4.3 cookie相关API
4.3.1 dma_cookie_init
dma_cookie_init函数用于初始化DMA通道的cookie,定义在drivers/dma/dmaengine.h:
/** * dma_cookie_init - initialize the cookies for a DMA channel * @chan: dma channel to initialize */ static inline void dma_cookie_init(struct dma_chan *chan) { chan->cookie = DMA_MIN_COOKIE; // DMA_MIN_COOKIE = 1 chan->completed_cookie = DMA_MIN_COOKIE; // DMA_MIN_COOKIE = 1 }
4.3.2 dma_cookie_assign
dma_cookie_assign用于给DMA描述符分配一个唯一的cookie,定义在drivers/dma/dmaengine.h:
/** * dma_cookie_assign - assign a DMA engine cookie to the descriptor * @tx: descriptor needing cookie * * Assign a unique non-zero per-channel cookie to the descriptor. * Note: caller is expected to hold a lock to prevent concurrency. */ static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx) { struct dma_chan *chan = tx->chan; // 获取当前DMA通道 dma_cookie_t cookie; cookie = chan->cookie + 1; // if (cookie < DMA_MIN_COOKIE) cookie = DMA_MIN_COOKIE; tx->cookie = chan->cookie = cookie; return cookie; }
该函数的目的是为DMA描述符分配一个唯一的、非零的cookie。通道cookie是DMA传输时使用的标识符,用于跟踪和标识正在进行的DMA传输。
4.3.3 dma_cookie_complete
dma_cookie_complete用于完成一个描述符。通过更新通道completed_cookie来标记此描述符完成。将描述符cookie清零以防止意外重复完成,函数定义在drivers/dma/dmaengine.h:
/** * dma_cookie_complete - complete a descriptor * @tx: descriptor to complete * * Mark this descriptor complete by updating the channels completed * cookie marker. Zero the descriptors cookie to prevent accidental * repeated completions. * * Note: caller is expected to hold a lock to prevent concurrency. */ static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx) { BUG_ON(tx->cookie < DMA_MIN_COOKIE); tx->chan->completed_cookie = tx->cookie; tx->cookie = 0; }
4.3.4 dma_status
dma_cookie_status用于报告cookie状态,函数接受一个DMA通道的指针chan、待查询的cookie和一个非空的 dma_tx_state结构体指针 state。定义在drivers/dma/dmaengine.h:
/** * dma_cookie_status - report cookie status * @chan: dma channel * @cookie: cookie we are interested in * @state: dma_tx_state structure to return last/used cookies * * Report the status of the cookie, filling in the state structure if * non-NULL. No locking is required. */ static inline enum dma_status dma_cookie_status(struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *state) { dma_cookie_t used, complete; used = chan->cookie; complete = chan->completed_cookie; // 获取completed_cookie标志 barrier(); if (state) { state->last = complete; state->used = used; state->residue = 0; state->in_flight_bytes = 0; } return dma_async_is_complete(cookie, complete, used); // 判断指定的cookie是否已完成 }
dma_async_is_complete函数定义在include/linux/dmaengine.,内容如下:
/** * dma_async_is_complete - test a cookie against chan state * @cookie: transaction identifier to test status of * @last_complete: last know completed transaction * @last_used: last cookie value handed out * * dma_async_is_complete() is used in dma_async_is_tx_complete() * the test logic is separated for lightweight testing of multiple cookies */ static inline enum dma_status dma_async_is_complete(dma_cookie_t cookie, dma_cookie_t last_complete, dma_cookie_t last_used) { if (last_complete <= last_used) { if ((cookie <= last_complete) || (cookie > last_used)) return DMA_COMPLETE; } else { if ((cookie <= last_complete) && (cookie > last_used)) return DMA_COMPLETE; } return DMA_IN_PROGRESS; }
五、为DMA client driver提供API
下面我们介绍一下DMA client driver如何使用Slave DMA:
(1) 请求一个DMA从通道;
(2) 配置DMA slave通道运行时参数;
(3) 获取一个用于识别本次传输的描述符;
(4) 提交传输并启动传输;
(5) 发出待处理的请求并等待传输结束后的回调通知;
需要注意的是:我们下面提到的从设备(slave device)指的就是client device。
5.1 请求一个DMA从通道
在从设备的DMA上下文中,通道的分配与主设备的DMA上下文略有不同。DMA client driver通常只需要从特定的DMA控制器中获取一个通道。为了请求一个DMA通道,可以使用dma_request_chan API接口:
struct dma_chan *dma_request_chan(struct device *dev, const char *name);
该接口会查找并返回与dev设备关联的名称为name的DMA通道。该关联关系是通过DT、ACPI或基于板级文件的dma_slave_map匹配表完成的。
dma_request_chan函数定义如下:
/** * dma_request_chan - try to allocate an exclusive slave channel * @dev: pointer to client device structure * @name: slave channel name * * Returns pointer to appropriate DMA channel on success or an error pointer. */ struct dma_chan *dma_request_chan(struct device *dev, const char *name) { struct dma_device *d, *_d; struct dma_chan *chan = NULL; /* If device-tree is present get slave info from here */ if (dev->of_node) // 如果使用了设备树 chan = of_dma_request_slave_channel(dev->of_node, name); /* If device was enumerated by ACPI get slave info from here */ if (has_acpi_companion(dev) && !chan) // 忽略 chan = acpi_dma_request_slave_chan_by_name(dev, name); if (PTR_ERR(chan) == -EPROBE_DEFER) return chan; if (!IS_ERR_OR_NULL(chan)) goto found; /* Try to find the channel via the DMA filter map(s) */ 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); if (IS_ERR(chan)) return chan; if (!chan) return ERR_PTR(-EPROBE_DEFER); found: #ifdef CONFIG_DEBUG_FS chan->dbg_client_name = kasprintf(GFP_KERNEL, "%s:%s", dev_name(dev), name); #endif chan->name = kasprintf(GFP_KERNEL, "dma:%s", name); // DMA通道的名称:dma:%s if (!chan->name) return chan; chan->slave = dev; // 设置为DMA client device if (sysfs_create_link(&chan->dev->device.kobj, &dev->kobj, DMA_SLAVE_NAME)) dev_warn(dev, "Cannot create DMA %s symlink\n", DMA_SLAVE_NAME); if (sysfs_create_link(&dev->kobj, &chan->dev->device.kobj, chan->name)) dev_warn(dev, "Cannot create DMA %s symlink\n", chan->name); return chan; }
5.1.1 of_dma_request_slave_channel
我们重点关注如何使用设备请求DMA从通道的,具体实现是of_dma_request_slave_channel函数,根据参数name并解析device tree 来请求具体的DMA 通道,函数实现位于drivers/dma/of-dma.c:
/** * of_dma_request_slave_channel - Get the DMA slave channel * @np: device node to get DMA request from * @name: name of desired channel * * Returns pointer to appropriate DMA channel on success or an error pointer. */ struct dma_chan *of_dma_request_slave_channel(struct device_node *np, // 设备节点 比如RK3399 i2s0设备节点 const char *name) // DMA通道名称,比如tx、rx { 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); } /* Silently fail if there is not even the "dmas" property */ if (!of_find_property(np, "dmas", NULL)) // 判断是否设置dmas属性 return ERR_PTR(-ENODEV); count = of_property_count_strings(np, "dma-names"); // 获取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++) { // 遍历dmas属性值,查找名字为name对应的dmas的值 if (of_dma_match_channel(np, name, (i + start) % count, &dma_spec)) // 存放第i个属性值的信息 continue; mutex_lock(&of_dma_lock); ofdma = of_dma_find_controller(&dma_spec); // 获取DMA控制器 if (ofdma) { chan = ofdma->of_dma_xlate(&dma_spec, ofdma); // 这里应该就是根据DMA控制器以及通道编号获取到DMA通道的 } 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); }
我们以i2s0设备节点为例:
i2s0: i2s@ff880000 { dmas = <&dmac_bus 0>, <&dmac_bus 1>; dma-names = "tx", "rx"; ...... };
每个需要使用DMA client都会通过 dmas来引用DMA控制器和通道,通过dma-names实现name的匹配。最终调用ofdma->of_dma_xlate。
比如名字为tx的DMA通道对应的DMA通道为<&dmac_bus 0>;
5.1.2 of_dma_find_controller
of_dma_find_controller函数是用于在设备树的DMA助手列表中查找匹配的DMA控制器的函数。它接收一个指向设备树中DMA specifier的指针作为参数,比如我们上面说的<&dmac_bus 0>;
/** * of_dma_find_controller - Get a DMA controller in DT DMA helpers list * @dma_spec: pointer to DMA specifier as found in the device tree * * Finds a DMA controller with matching device node and number for dma cells * in a list of registered DMA controllers. If a match is found a valid pointer * to the DMA data stored is retuned. A NULL pointer is returned if no match is * found. */ static struct of_dma *of_dma_find_controller(struct of_phandle_args *dma_spec) { struct of_dma *ofdma; list_for_each_entry(ofdma, &of_dma_list, of_dma_controllers) // 遍历of_dma_list链表,获取每一个DMA控制器 if (ofdma->of_node == dma_spec->np) // 设备节点匹配 return ofdma; pr_debug("%s: can't find DMA controller %pOF\n", __func__, dma_spec->np); return NULL; }
5.1.3 struct of_dma
struct of_dma定义在include/linux/of_dma.h:
struct of_dma { struct list_head of_dma_controllers; struct device_node *of_node; struct dma_chan *(*of_dma_xlate) (struct of_phandle_args *, struct of_dma *); void *(*of_dma_route_allocate) (struct of_phandle_args *, struct of_dma *); struct dma_router *dma_router; void *of_dma_data; };
这里应该与通过设备树注册DMA控制器相关的数据结构,咱们就不具体深究了,不然本节内容太过于发散了。
5.2 配置DMA slave通道运行时参数
接下来的步骤是向DMA controller driver传递一些特定的信息。DMA client device可以使用的大多数通用信息都在struct dma_slave_config中。如DMA方向、DMA地址、总线宽度、DMA突发长度等等。
使用dmaengine_slave_config来设置DMA通道的具体参数:
int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)
dmaengine_slave_config函数定义在include/linux/dmaengine.h:
static inline int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config) { if (chan->device->device_config) return chan->device->device_config(chan, config); return -ENOSYS; }
可以看到这里回调了DMA设备的device_config函数,用于配置DMA通道chan运行时参数。
5.3 获取描述符
对于DMA client deivice使用DMA,DMA engine支持的各种slave传输模式有:
- slave_sg:用于在scatter gather buffers列表和总线设备之间进行DMA传输;
- dma_cyclic:用于执行循环DMA操作,直到操作明确停止,常应用与音频等场景中;
- interleaved_dma:用于不连续的、交叉的DMA传输,常应用与图像处理等场景中;
使用以下API来获取不同传输模式的描述符:
static inline struct dma_async_tx_descriptor *dmaengine_prep_slave_sg( struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_transfer_direction dir, unsigned long flags) { if (!chan || !chan->device || !chan->device->device_prep_slave_sg) return NULL; return chan->device->device_prep_slave_sg(chan, sgl, sg_len, dir, flags, NULL); } static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_transfer_direction dir, unsigned long flags) { if (!chan || !chan->device || !chan->device->device_prep_dma_cyclic) return NULL; return chan->device->device_prep_dma_cyclic(chan, buf_addr, buf_len, period_len, dir, flags); } static inline struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma( struct dma_chan *chan, struct dma_interleaved_template *xt, unsigned long flags) { if (!chan || !chan->device || !chan->device->device_prep_interleaved_dma) return NULL; return chan->device->device_prep_interleaved_dma(chan, xt, flags); }
在调用dmaengine_prep_slave_sg之前,DMA client driver应该已经对scatterlist进行了DMA操作的映射,并且必须保持scatterlist mapped在DMA操作完成之前一直被映射。scatterlist 必须使用DMA struct device进行映射。如果稍后需要同步映射,则必须使用DMA struct device调用dma_sync_*_for_*。常的设置应该如下所示:
struct device *dma_dev = dmaengine_get_dma_device(chan); nr_sg = dma_map_sg(dma_dev, sgl, sg_len); if (nr_sg == 0) /* error */ desc = dmaengine_prep_slave_sg(chan, sgl, nr_sg, direction, flags);
5.4 提交传输并启动传输
准备好描述符并添加回调信息后,必须将其放置在DMA controller driver的传输队列中,通过调用dmaengine_submit API来将准备好的描述符放到传输队列上,之后调用dma_async_issue_pending API来启动传输;
static inline dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc) { return desc->tx_submit(desc); }
此操作返回一个cookie,可以与dma_async_is_tx_complete函数搭配使用来检查DMA传输是否已完成。
dmaengine_submit不会启动DMA操作,它只是将其添加到传输队列上,之后调用dma_async_issue_pending API来启动传输。如果通道空闲,则会启动队列中的第一个传输,并将后续的传输排队等待。
static inline void dma_async_issue_pending(struct dma_chan *chan) { chan->device->device_issue_pending(chan); }
注意:在调用dmaengine_submit之后,提交的传输描述符(struct dma_async_tx_descriptor)属于DMA controller driver。因此,DMA client driver必须将指向该描述符的指针视为无效。
5.5 等待传输完成
每次完成DMA操作时,都会启动队列中的下一个传输,并触发一个tasklet。然后,tasklet会调用DMA client driver的完成回调函数进行通知(如果已设置)。
这种机制确保在每次DMA操作完成后,会自动启动下一个传输,并通过回调通知DMA client driver。
5.6 其它API
此外,还可以调用dmaengine_pause、dmaengine_resume、dmaengine_terminate_*等API来暂停传输、恢复传输、终止传输等操作。
5.6.1 终止传输
以下是三个终止DMA操作的函数:
/** * dmaengine_terminate_sync() - Terminate all active DMA transfers * @chan: The channel for which to terminate the transfers * * Calling this function will terminate all active and pending transfers * that have previously been submitted to the channel. It is similar to * dmaengine_terminate_async() but guarantees that the DMA transfer has actually * stopped and that all complete callbacks have finished running when the * function returns. * * This function must only be called from non-atomic context and must not be * called from within a complete callback of a descriptor submitted on the same * channel. */ static inline int dmaengine_terminate_sync(struct dma_chan *chan) { int ret; ret = dmaengine_terminate_async(chan); if (ret) return ret; dmaengine_synchronize(chan); return 0; } /** * dmaengine_terminate_async() - Terminate all active DMA transfers * @chan: The channel for which to terminate the transfers * * Calling this function will terminate all active and pending descriptors * that have previously been submitted to the channel. It is not guaranteed * though that the transfer for the active descriptor has stopped when the * function returns. Furthermore it is possible the complete callback of a * submitted transfer is still running when this function returns. * * dmaengine_synchronize() needs to be called before it is safe to free * any memory that is accessed by previously submitted descriptors or before * freeing any resources accessed from within the completion callback of any * previously submitted descriptors. * * This function can be called from atomic context as well as from within a * complete callback of a descriptor submitted on the same channel. * * If none of the two conditions above apply consider using * dmaengine_terminate_sync() instead. */ static inline int dmaengine_terminate_async(struct dma_chan *chan) { if (chan->device->device_terminate_all) return chan->device->device_terminate_all(chan); return -EINVAL; } /** * dmaengine_terminate_all() - Terminate all active DMA transfers * @chan: The channel for which to terminate the transfers * * This function is DEPRECATED use either dmaengine_terminate_sync() or * dmaengine_terminate_async() instead. */ static inline int dmaengine_terminate_all(struct dma_chan *chan) { if (chan->device->device_terminate_all) return chan->device->device_terminate_all(chan); return -ENOSYS; }
调用这些函数将停止DMA通道上的所有活动,并有可能丢弃尚未完全传输的数据。每个函数的行为略有不同,请根据具体需求和情况选择适当的函数。
这个函数有两个变种可用:
- dmaengine_terminate_async:可能不会等待DMA完全停止或任何正在运行的完成回调函数执行完毕。但是可以从原子上下文或完成回调函数内部调用 dmaengine_terminate_async。在安全释放DMA传输所访问的内存或释放完成回调函数内访问的资源之前,必须调用 dmaengine_synchronize;
- dmaengine_terminate_sync:将等待传输和任何正在运行的完成回调函数完成后再返回。但是此函数不能从原子上下文或完成回调函数内部调用;
- dmaengine_terminate_all:已经被弃用,在新的代码中不应使用;
具体选择哪个函数取决于您的需求和上下文。如果需要立即停止DMA并等待操作完成,可以使用 dmaengine_terminate_sync。如果需要在某些条件下异步终止DMA,可以使用 dmaengine_terminate_async。
请注意,在使用 dmaengine_terminate_async后,必须调用 dmaengine_synchronize来确保在释放相关资源之前没有访问冲突。
5.6.2 暂停传输
dmaengine_pause这个操作可以在DMA通道上暂停活动,而不会丢失数据;
static inline int dmaengine_pause(struct dma_chan *chan) { if (chan->device->device_pause) return chan->device->device_pause(chan); return -ENOSYS; }
5.6.3 恢复传输
dmaengine_resume恢复先前暂停的DMA通道。尝试对当前未暂停的通道进行恢复是无效的操作;
static inline int dmaengine_resume(struct dma_chan *chan) { if (chan->device->device_resume) return chan->device->device_resume(chan); return -ENOSYS; }
5.6.4 检查传输完成情况
dma_async_is_tx_complete函数用于检查DMA通道的状态;可以与dmaengine_submit返回的cookie一起使用,来检查特定DMA传输是否已完成;
/** * dma_async_is_tx_complete - poll for transaction completion * @chan: DMA channel * @cookie: transaction identifier to check status of * @last: returns last completed cookie, can be NULL * @used: returns last issued cookie, can be NULL * * If @last and @used are passed in, upon return they reflect the driver * internal state and can be used with dma_async_is_complete() to check * the status of multiple cookies without re-checking hardware state. */ static inline enum dma_status dma_async_is_tx_complete(struct dma_chan *chan, dma_cookie_t cookie, dma_cookie_t *last, dma_cookie_t *used) { struct dma_tx_state state; enum dma_status status; status = chan->device->device_tx_status(chan, cookie, &state); if (last) *last = state.last; if (used) *used = state.used; return status; }
注意:并非所有DMA controller driver都能为正在运行的DMA通道提供可靠的信息。建议在使用此API之前,DMA引擎用户先暂停或停止通道。
5.6.5 同步终止
dmaengine_synchronize用于将DMA通道的终止与当前上下文进行同步;
/** * dmaengine_synchronize() - Synchronize DMA channel termination * @chan: The channel to synchronize * * Synchronizes to the DMA channel termination to the current context. When this * function returns it is guaranteed that all transfers for previously issued * descriptors have stopped and it is safe to free the memory associated * with them. Furthermore it is guaranteed that all complete callback functions * for a previously submitted descriptor have finished running and it is safe to * free resources accessed from within the complete callbacks. * * The behavior of this function is undefined if dma_async_issue_pending() has * been called between dmaengine_terminate_async() and this function. * * This function must only be called from non-atomic context and must not be * called from within a complete callback of a descriptor submitted on the same * channel. */ static inline void dmaengine_synchronize(struct dma_chan *chan) { might_sleep(); if (chan->device->device_synchronize) chan->device->device_synchronize(chan); }
此函数应在dmaengine_terminate_async后使用,以将DMA通道的终止与当前上下文进行同步。该函数将等待传输和任何正在运行的完成回调函数完成后再返回。
如果使用dmaengine_terminate_async停止DMA通道,则必须在安全释放先前提交的描述符所访问的内存或释放先前提交的描述符的完成回调函数内部访问的任何资源之前调用此函数。
如果在dmaengine_terminate_async和此函数之间调用了dma_async_issue_pending,则此函数的行为是未定义的。
这个函数必须只能从非原子上下文中调用,并且不能在同一通道上提交的描述符的完成回调函数内部调用。
六、 ASoC中的DMA client driver
在ASoC中提供了函数devm_snd_dmaengine_pcm_register,从函数前缀devm可以看出这是一个带有资源管理的函数,用于注册一个dmaengine pcm,该函数是I2S DMA client driver的入口函数。
函数有三个参数,参数1一般传入ASoC platform driver中的平台设备的的dev成员,参数2就是PCM配置了,参数3就是标志位;函数定义在sound/soc/soc-devres.c;
/** * devm_snd_dmaengine_pcm_register - resource managed dmaengine PCM registration * @dev: The parent device for the PCM device * @config: Platform specific PCM configuration * @flags: Platform specific quirks * * Register a dmaengine based PCM device with automatic unregistration when the * device is unregistered. */ int devm_snd_dmaengine_pcm_register(struct device *dev, // 比如:pdev->dev,pdev为i2s0设备节点对应的平台设备 const struct snd_dmaengine_pcm_config *config, unsigned int flags) // config传入NULL,flags传入0 { struct device **ptr; int ret; ptr = devres_alloc(devm_dmaengine_pcm_release, sizeof(*ptr), GFP_KERNEL); // 动态申请内存,指向一个struct *device if (!ptr) return -ENOMEM; ret = snd_dmaengine_pcm_register(dev, config, flags); // 申请DMA通道 if (ret == 0) { *ptr = dev; devres_add(dev, ptr); } else { devres_free(ptr); } return ret; }
这个函数内容主要完成了两个操作:
- 调用devres_alloc动态分配一块内存,数据结构类型为struct device *;同时指定了释放资源的回调函数,当设备被注销时会调用devm_dmaengine_pcm_release函数释放相关资源;
- 调用snd_dmaengine_pcm_register注册 dmaengine PCM,如果注册成功,则将dev指针保存在ptr所指向的内存中;并调用devres_add将该内存块与dev相关联,以便在设备注销时自动释放。
这里我们重点研究一下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, // 比如:pdev->dev,pdev为i2s0设备节点对应的平台设备 const struct snd_dmaengine_pcm_config *config, unsigned int flags) { const struct snd_soc_component_driver *driver; struct dmaengine_pcm *pcm; int ret; pcm = kzalloc(sizeof(*pcm), GFP_KERNEL); // 动态分配一个dmaengine_pcm结构 if (!pcm) return -ENOMEM; #ifdef CONFIG_DEBUG_FS pcm->component.debugfs_prefix = "dma"; #endif if (!config) config = &snd_dmaengine_pcm_default_config; // 走这里 pcm->config = config; // 初始化pcm成员 pcm->flags = flags; ret = dmaengine_pcm_request_chan_of(pcm, dev, config); // 请求DMA通道 if (ret) goto err_free_dma; if (config->process) driver = &dmaengine_pcm_component_process; else driver = &dmaengine_pcm_component; // 走这里 ret = snd_soc_component_initialize(&pcm->component, driver, dev); // 初始化pcm->component if (ret) goto err_free_dma; ret = snd_soc_add_component(&pcm->component, NULL, 0); // 注册pcm component if (ret) goto err_free_dma; return 0; err_free_dma: dmaengine_pcm_release_chan(pcm); kfree(pcm); return ret; }
函数执行流程如下:
- 此处分配一个dmaengine_pcm结构,然后根据传入的config和flag设置pcm,config被设置为了snd_dmaengine_pcm_default_config;
- 调用dmaengine_pcm_request_chan_of函数请求DMA通道,根据传输的是否是半双工,设置pcm的通道;
- 调用snd_soc_add_platform函数注册platform component,实际上snd_soc_component_initialize和snd_soc_add_platform合在一起就是函数snd_soc_register_component;
snd_dmaengine_pcm_default_config定义如下:
static const struct snd_dmaengine_pcm_config snd_dmaengine_pcm_default_config = { .prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config, };
6.1 dmaengine_pcm_request_chan_of
dmaengine_pcm_request_chan_of函数根据DMA通道的名称去请求DMA通道。定义如下:
static int dmaengine_pcm_request_chan_of(struct dmaengine_pcm *pcm, struct device *dev, const struct snd_dmaengine_pcm_config *config) { unsigned int i; const char *name; struct dma_chan *chan; if ((pcm->flags & SND_DMAENGINE_PCM_FLAG_NO_DT) || (!dev->of_node && // 未指定该标志位,不会进入 !(config->dma_dev && config->dma_dev->of_node))) return 0; if (config->dma_dev) { // 不会进入 /* * If this warning is seen, it probably means that your Linux * device structure does not match your HW device structure. * It would be best to refactor the Linux device structure to * correctly match the HW structure. */ dev_warn(dev, "DMA channels sourced from device %s", dev_name(config->dma_dev)); dev = config->dma_dev; } for_each_pcm_streams(i) { // i=0,1 if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) name = "rx-tx"; else name = dmaengine_pcm_dma_channel_names[i]; // 走这里,名字依次为tx、rx if (config->chan_names[i]) // 如果指定了DMA通道名称,会走这里,很显然没有指定 name = config->chan_names[i]; chan = dma_request_chan(dev, name); // 请求一个DMA通道 if (IS_ERR(chan)) { /* * Only report probe deferral errors, channels * might not be present for devices that * support only TX or only RX. */ if (PTR_ERR(chan) == -EPROBE_DEFER) return -EPROBE_DEFER; pcm->chan[i] = NULL; } else { pcm->chan[i] = chan; // 保存DMA通道 } if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) break; } if (pcm->flags & SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX) // 如果指定了半双工,发送/接收使用同一个通道 pcm->chan[1] = pcm->chan[0]; return 0; }
其中dmaengine_pcm_dma_channel_names数组定义如下:
static const char * const dmaengine_pcm_dma_channel_names[] = { [SNDRV_PCM_STREAM_PLAYBACK] = "tx", // 播放流 [SNDRV_PCM_STREAM_CAPTURE] = "rx", // 录音流 };
6.2 dmaengine_pcm_component
这里我们需要说一下dmaengine_pcm_component,类型为 struct snd_soc_component_driver,定义在sound/soc/soc-generic-dmaengine-pcm.c:
static const struct snd_soc_component_driver dmaengine_pcm_component = { .name = SND_DMAENGINE_PCM_DRV_NAME, // snd_dmaengine_pcm .probe_order = SND_SOC_COMP_ORDER_LATE, // 1 .open = dmaengine_pcm_open, .close = dmaengine_pcm_close, .hw_params = dmaengine_pcm_hw_params, .trigger = dmaengine_pcm_trigger, .pointer = dmaengine_pcm_pointer, .pcm_construct = dmaengine_pcm_new, };
我们之前说过为DMA client driver如何使用Slave DMA,包含5个步骤,其中第一个步骤由devm_snd_dmaengine_pcm_register函数实现了,因此我们不难猜到其它几个步骤应该会在dmaengine_pcm_component的其它回调函数中实现了。
6.2.1 dmaengine_pcm_open
通过名字我们应该可以猜测出这个函数应该在播放音频/录音的时候会被调用,定义在sound/soc/soc-generic-dmaengine-pcm.c:
static int dmaengine_pcm_open(struct snd_soc_component *component, struct snd_pcm_substream *substream) // PCM substream { struct dmaengine_pcm *pcm = soc_component_to_pcm(component); // 获取struct dmaengine_pcm struct dma_chan *chan = pcm->chan[substream->stream]; // 获取DMA通道 tx/rx通道 int ret; ret = dmaengine_pcm_set_runtime_hwparams(component, substream); // 初始化PCM运行时硬件参数 if (ret) return ret; return snd_dmaengine_pcm_open(substream, chan); }
dmaengine_pcm_set_runtime_hwparams函数,用于初始化初始化PCM运行时硬件参数;
static int dmaengine_pcm_set_runtime_hwparams(struct snd_soc_component *component, struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); struct dmaengine_pcm *pcm = soc_component_to_pcm(component); struct device *dma_dev = dmaengine_dma_dev(pcm, substream); struct dma_chan *chan = pcm->chan[substream->stream]; struct snd_dmaengine_dai_dma_data *dma_data; struct snd_pcm_hardware hw; if (rtd->dai_link->num_cpus > 1) { // 跳过 dev_err(rtd->dev, "%s doesn't support Multi CPU yet\n", __func__); return -EINVAL; } if (pcm->config->pcm_hardware) // 未设置 跳过 return snd_soc_set_runtime_hwparams(substream, pcm->config->pcm_hardware); dma_data = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); // 获取rtd->dais[0]->stream[substream].dma_data memset(&hw, 0, sizeof(hw)); hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED; hw.periods_min = 2; hw.periods_max = UINT_MAX; hw.period_bytes_min = dma_data->maxburst * DMA_SLAVE_BUSWIDTH_8_BYTES; if (!hw.period_bytes_min) hw.period_bytes_min = 256; hw.period_bytes_max = dma_get_max_seg_size(dma_dev); hw.buffer_bytes_max = SIZE_MAX; hw.fifo_size = dma_data->fifo_size; // DAI控制器的FIFO大小 if (pcm->flags & SND_DMAENGINE_PCM_FLAG_NO_RESIDUE) hw.info |= SNDRV_PCM_INFO_BATCH; /** * FIXME: Remove the return value check to align with the code * before adding snd_dmaengine_pcm_refine_runtime_hwparams * function. */ snd_dmaengine_pcm_refine_runtime_hwparams(substream, dma_data, &hw, chan); return snd_soc_set_runtime_hwparams(substream, &hw); }
snd_soc_set_runtime_hwparams定义在sound/soc/soc-pcm.c:
/** * snd_soc_set_runtime_hwparams - set the runtime hardware parameters * @substream: the pcm substream * @hw: the hardware parameters * * Sets the substream runtime hardware parameters. */ int snd_soc_set_runtime_hwparams(struct snd_pcm_substream *substream, const struct snd_pcm_hardware *hw) { substream->runtime->hw = *hw; return 0; }
snd_dmaengine_pcm_open定义在sound/core/pcm_dmaengine.c:
/** * snd_dmaengine_pcm_open - Open a dmaengine based PCM substream * @substream: PCM substream * @chan: DMA channel to use for data transfers * * The function should usually be called from the pcm open callback. Note that * this function will use private_data field of the substream's runtime. So it * is not available to your pcm driver implementation. * * Return: 0 on success, a negative error code otherwise */ int snd_dmaengine_pcm_open(struct snd_pcm_substream *substream, struct dma_chan *chan) { struct dmaengine_pcm_runtime_data *prtd; int ret; if (!chan) return -ENXIO; ret = snd_pcm_hw_constraint_integer(substream->runtime, SNDRV_PCM_HW_PARAM_PERIODS); if (ret < 0) return ret; prtd = kzalloc(sizeof(*prtd), GFP_KERNEL); if (!prtd) return -ENOMEM; prtd->dma_chan = chan; substream->runtime->private_data = prtd; return 0; }
6.2.2 dmaengine_pcm_hw_params
dmaengine_pcm_hw_params函数中回调用了pcm->config->prepare_slave_config,不难猜测这里应该就是配置DMA slave通道运行时参数;
static int dmaengine_pcm_hw_params(struct snd_soc_component *component, struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct dmaengine_pcm *pcm = soc_component_to_pcm(component); struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream); struct dma_slave_config slave_config; int ret; if (!pcm->config->prepare_slave_config) // 定义了该函数 return 0; memset(&slave_config, 0, sizeof(slave_config)); ret = pcm->config->prepare_slave_config(substream, params, &slave_config); if (ret) return ret; return dmaengine_slave_config(chan, &slave_config); // 配置DMA slave通道运行时参数 }
prepare_slave_config被设置成了snd_dmaengine_pcm_prepare_slave_config,定义在sound/soc/soc-generic-dmaengine-pcm.c;
/** * snd_dmaengine_pcm_prepare_slave_config() - Generic prepare_slave_config callback * @substream: PCM substream * @params: hw_params * @slave_config: DMA slave config to prepare * * This function can be used as a generic prepare_slave_config callback for * platforms which make use of the snd_dmaengine_dai_dma_data struct for their * DAI DMA data. Internally the function will first call * snd_hwparams_to_dma_slave_config to fill in the slave config based on the * hw_params, followed by snd_dmaengine_set_config_from_dai_data to fill in the * remaining fields based on the DAI DMA data. */ int snd_dmaengine_pcm_prepare_slave_config(struct snd_pcm_substream *substream, // pcm substream struct snd_pcm_hw_params *params, // pcm硬件配置参数 struct dma_slave_config *slave_config) // DMA slave通道运行时参数 { struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); // pcm runtime struct snd_dmaengine_dai_dma_data *dma_data; int ret; if (rtd->dai_link->num_cpus > 1) { dev_err(rtd->dev, "%s doesn't support Multi CPU yet\n", __func__); return -EINVAL; } dma_data = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); // 获取rtd->dais[0]->stream[substream].dma_data ret = snd_hwparams_to_dma_slave_config(substream, params, slave_config); // 将硬件参数params转换为slave_config if (ret) return ret; snd_dmaengine_pcm_set_config_from_dai_data(substream, dma_data, slave_config); // 根据DAI DMA数据初始化slave_config return 0; }
snd_hwparams_to_dma_slave_config用于将硬件参数(hw_params)转换为dma_slave_config结构体。它接收一个指向snd_pcm_substream结构体的指针(pcm substream)、一个指向snd_pcm_hw_params结构体的指针(params)和一个指向dma_slave_config结构体的指针(slave_config)作为参数,定义在sound/core/pcm_dmaengine.c;
/** * snd_hwparams_to_dma_slave_config - Convert hw_params to dma_slave_config * @substream: PCM substream * @params: hw_params * @slave_config: DMA slave config * * This function can be used to initialize a dma_slave_config from a substream * and hw_params in a dmaengine based PCM driver implementation. * * Return: zero if successful, or a negative error code */ int snd_hwparams_to_dma_slave_config(const struct snd_pcm_substream *substream, // pcm substream const struct snd_pcm_hw_params *params, // pcm硬件配置参数 struct dma_slave_config *slave_config) // DMA slave通道运行时参数 { enum dma_slave_buswidth buswidth; int bits; bits = params_physical_width(params); // 获取硬件参数中的物理宽度(bits),并根据其值来确定总线宽度(buswidth) if (bits < 8 || bits > 64) // 8~64之间 return -EINVAL; else if (bits == 8) buswidth = DMA_SLAVE_BUSWIDTH_1_BYTE; // 1字节 else if (bits == 16) buswidth = DMA_SLAVE_BUSWIDTH_2_BYTES; // 2字节 else if (bits == 24) buswidth = DMA_SLAVE_BUSWIDTH_3_BYTES; // 3字节 else if (bits <= 32) buswidth = DMA_SLAVE_BUSWIDTH_4_BYTES; // 4字节 else buswidth = DMA_SLAVE_BUSWIDTH_8_BYTES; // 8字节 if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { // pcm播放流 slave_config->direction = DMA_MEM_TO_DEV; // 设置传输方向mem->dev slave_config->dst_addr_width = buswidth; // 目的寄存器的宽度,这里配置的的就是音频的采样位数/8 } else { // pcm录音刘 slave_config->direction = DMA_DEV_TO_MEM; // dev->mem slave_config->src_addr_width = buswidth; // 源寄存器的宽度,这里配置的的就是音频的采样位数/8 } slave_config->device_fc = false; return 0; }
snd_dmaengine_pcm_set_config_from_dai_data函数用于根据DAI DMA数据初始化dma_slave_config。它接收一个指向snd_pcm_substream结构体的指针(substream)、一个指向snd_dmaengine_dai_dma_data结构体的指针(dma_data)和一个指向dma_slave_config结构体的指针(slave_config)作为参数,定义在sound/core/pcm_dmaengine.c;
/** * snd_dmaengine_pcm_set_config_from_dai_data() - Initializes a dma slave config * using DAI DMA data. * @substream: PCM substream * @dma_data: DAI DMA data * @slave_config: DMA slave configuration * * Initializes the {dst,src}_addr, {dst,src}_maxburst, {dst,src}_addr_width * fields of the DMA slave config from the same fields of the DAI DMA * data struct. The src and dst fields will be initialized depending on the * direction of the substream. If the substream is a playback stream the dst * fields will be initialized, if it is a capture stream the src fields will be * initialized. The {dst,src}_addr_width field will only be initialized if the * SND_DMAENGINE_PCM_DAI_FLAG_PACK flag is set or if the addr_width field of * the DAI DMA data struct is not equal to DMA_SLAVE_BUSWIDTH_UNDEFINED. If * both conditions are met the latter takes priority. */ void snd_dmaengine_pcm_set_config_from_dai_data( const struct snd_pcm_substream *substream, const struct snd_dmaengine_dai_dma_data *dma_data, struct dma_slave_config *slave_config) { if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { // pcm数据流类型为播放流 mem->dev slave_config->dst_addr = dma_data->addr; // 写入DMA数据的物理地址 slave_config->dst_maxburst = dma_data->maxburst; // burst size if (dma_data->flags & SND_DMAENGINE_PCM_DAI_FLAG_PACK) // 未设置 slave_config->dst_addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED; if (dma_data->addr_width != DMA_SLAVE_BUSWIDTH_UNDEFINED) // !=0 slave_config->dst_addr_width = dma_data->addr_width; } else { // pcm数据流类型为录音流 dev->mem slave_config->src_addr = dma_data->addr; // 读取DMA数据的物理地址 slave_config->src_maxburst = dma_data->maxburst; // burst size if (dma_data->flags & SND_DMAENGINE_PCM_DAI_FLAG_PACK) // 未设置 slave_config->src_addr_width = DMA_SLAVE_BUSWIDTH_UNDEFINED; if (dma_data->addr_width != DMA_SLAVE_BUSWIDTH_UNDEFINED) // != 0 slave_config->src_addr_width = dma_data->addr_width; } slave_config->peripheral_config = dma_data->peripheral_config; slave_config->peripheral_size = dma_data->peripheral_size; }
以RK3399 I2S0接口plfatform dma driver为例,音频播放时:
struct snd_dmaengine_dai_dma_data { .addr = res->start + I2S_TXDR, // I2S0寄存器基地址0xff880000, 偏移0x0024为发送FIFO数据寄存器 .addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, //音频采样位宽/8,单位字节 4字节 .maxburst = 8, // FIFO深度 } struct dma_slave_config slave_config { .direction = DMA_MEM_TO_DEV, // 传输方向 .dst_addr_width = 4, // 音频采样位宽/8,单位字节 .dst_addr = res->start + I2S_TXDR, // 发送FIFO数据寄存器地址 .dst_maxburst = 8, // 发送FIFO深度 };
录音时:
struct snd_dmaengine_dai_dma_data { .addr = res->start + I2S_RXDR; // 偏移0x0028为接收FIFO数据寄存器 .addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; // 音频采样位宽/8,单位字节 .maxburst = 8; // FIFO深度 } struct dma_slave_config slave_config { .direction = DMA_DEV_TO_MEM, .src_addr_width = 4, .src_addr = res->start + I2S_RXDR, .dst_maxburst = 8, };
6.2.3 dmaengine_pcm_trigger
snd_dmaengine_pcm_trigger函数从名字就可以看出,该函数应该是用来进行启动/暂停/终止DMA传输等操作;
static int dmaengine_pcm_trigger(struct snd_soc_component *component, struct snd_pcm_substream *substream, int cmd) { return snd_dmaengine_pcm_trigger(substream, cmd); }
snd_dmaengine_pcm_trigger定义在sound/core/pcm_dmaengine.c:
/** * snd_dmaengine_pcm_trigger - dmaengine based PCM trigger implementation * @substream: PCM substream * @cmd: Trigger command * * This function can be used as the PCM trigger callback for dmaengine based PCM * driver implementations. * * Return: 0 on success, a negative error code otherwise */ int snd_dmaengine_pcm_trigger(struct snd_pcm_substream *substream, int cmd) { struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream); struct snd_pcm_runtime *runtime = substream->runtime; int ret; switch (cmd) { case SNDRV_PCM_TRIGGER_START: // 开始 ret = dmaengine_pcm_prepare_and_submit(substream); if (ret) return ret; dma_async_issue_pending(prtd->dma_chan); break; case SNDRV_PCM_TRIGGER_RESUME: // 恢复 case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: // 释放暂停状态 dmaengine_resume(prtd->dma_chan); break; case SNDRV_PCM_TRIGGER_SUSPEND: // 挂起 if (runtime->info & SNDRV_PCM_INFO_PAUSE) dmaengine_pause(prtd->dma_chan); else dmaengine_terminate_async(prtd->dma_chan); break; case SNDRV_PCM_TRIGGER_PAUSE_PUSH: // 暂停 dmaengine_pause(prtd->dma_chan); break; case SNDRV_PCM_TRIGGER_STOP: // 停止 dmaengine_terminate_async(prtd->dma_chan); break; default: return -EINVAL; } return 0; }
这里我们以音频播放为例,会执行函数dmaengine_pcm_prepare_and_submit:
static int dmaengine_pcm_prepare_and_submit(struct snd_pcm_substream *substream) { struct dmaengine_pcm_runtime_data *prtd = substream_to_prtd(substream); struct dma_chan *chan = prtd->dma_chan; struct dma_async_tx_descriptor *desc; enum dma_transfer_direction direction; unsigned long flags = DMA_CTRL_ACK; direction = snd_pcm_substream_to_dma_direction(substream); if (!substream->runtime->no_period_wakeup) flags |= DMA_PREP_INTERRUPT; prtd->pos = 0; desc = dmaengine_prep_dma_cyclic(chan, // 获取DMA异步传输描述符,用于执行循环DMA操作 substream->runtime->dma_addr, snd_pcm_lib_buffer_bytes(substream), snd_pcm_lib_period_bytes(substream), direction, flags); if (!desc) return -ENOMEM; desc->callback = dmaengine_pcm_dma_complete; // DMA传输完成后要调用的函数 desc->callback_param = substream; prtd->cookie = dmaengine_submit(desc); // 提交DMA传输 return 0; }
参考文章
[2] 一文掌握DMA技术原理
[3] Linux DMA子系统(1):DMA Engine体系结构(部分转载)
[6] Linux DMA子系统(2):DMA控制器驱动(provider)
[7] Linux DMA子系统(3):DMA设备驱动(consumer)
[9] Documentation/driver-api/dmaengine/provider.rst
[10] Documentation/driver-api/dmaengine/client.rst