ps:忘了原文链接了,此文为转载学习,不喜勿喷 thanks
在开始分析代码之前,先简要介绍一下DMA的基础知识。
什么是DMA
DMA,Direct Memory Access,直接内存访问。
既然叫 直接内存访问,那么相对应地,应该就有 “间接的内存访问”。
间接的内存访问,我的理解是,就是指最常见的,我们利用CPU的指令,去从一个内存地址中读出数据,然后写到另外一个内存地址中,完成对应的赋值操作。
此过程,完全都是CPU去操作的,如果是单个这样的数据读取和写入,还没啥,但是如果数据量很大,比如我们用memcpy(addr1,addr2,1024)去从地址addr1地址开始,拷贝1024个字节到内存addr2处,那么CPU这段时间,就不要干别的事情了,就一直这么的给你读取,写入数据吧,另外的还有,常见于驱动中的,尤其是涉及到和外设打交道,我们让CPU从内存一个地址,读取了一个数据,然后写入到某个设备的FIFO或者DATA寄存器中,咋写入之前,常常会等待FIFO不是满的,然后才能写入数据,要从FIFO中读取数据,要等到FIFO不是空,才能读取,这样来来回回,会比较消耗资源。
鉴于此,才出现了DMA这个硬件设备,专门设计用来处理这些相对用CPU去操作这样的事情,效率很低,换做专门的硬件的DMA来负责数据的读取和写入,释放了CPU这个苦力,可以让,在DMA忙着数据传输的过程中,CPU去忙其他更重要的事情。而专门的DMA硬件负责这样的数据传输,效率也会更高。
之所以这样,才叫做内存直接访问的。
DMA的一些基础概念
DMA传输,总的来说就是,
硬件上,会有对应的控制寄存器ctrl和配置寄存器config,
比如你想要从内存一个地址addr传输,N个字word(32bit)的数据到设备dev上,
那么你就要先去根据你的请求,去配置config寄存器,首先是传输方向,
是DMA_TO_DEVICE,然后是源地址source address是你的内存地址addr,
和目标destination address是你的dev的DATA寄存器地址,
然后要传输额transfer size是N个,每个位宽是32bit,
将源地址,目标地址,位宽,DMA传输方向设置好,
整理成一个结构,专有名称叫做LLI(Link List Item),把这个LLi设置到ctrl里面。
然后去enable DMA,DMA就可以按照你的要求,把数据传输过去了。
这样的DMA叫做single DMA传输,LLI中的next lli的域设置为空,表示就一个LLI要传输。
如果源地址或目标地址是多个分散的地址,叫做scatter/gather DMA,
就要将这些LLI组合一下,即将第一个LLI的next lli那个域,设置成下一个LLI的地址,这样一个个链接起来,最后一个LLI的nex lli的域为空,这样设置好后,将第一个LLI的值写入到ctrl中,DMA就会自动地去执行第一个LLI的数据传输,传完后,发现next lli不为空,就找到next lli的位置,
找到对应的配置,开始这个lli的数据传送,直至传完所有的数据为止。
说完了DMA的由来和基本概念后,下面来分析一下,具体的ARM的PL08x驱动是如何实现的。
*/
/*
On burst request from peripheral
Destination burst from DMAC to peripheral
Clear burst request
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int ctr_chan[2];
/*
/*
下面这个dma_device是在dmaengine.h中定义的,其就是定义了一个架构,实现了DMA驱动的与具体设备无关的部分,然后提供具体接口,然后你具体的DMA驱动,去实现对应的接口,这样的好处是,省却了你的具体驱动,去关心太多那些通用dma驱动都应该要实现的一些功能等,省却了具体驱动编程者的精力
/
struct dma_device dmac = {
.device_alloc_chan_resources = pl08x_alloc_chan_resources,
.device_free_chan_resources = pl08x_free_chan_resources,
.device_prep_dma_memcpy = pl08x_prep_dma_memcpy,
.device_prep_dma_xor = NULL,
.device_prep_dma_zero_sum = NULL,
.device_prep_dma_memset = NULL,
.device_prep_dma_interrupt = pl08x_prep_dma_interrupt,
.device_is_tx_complete = pl08x_dma_is_complete,
.device_issue_pending = pl08x_issue_pending,
.device_prep_slave_sg = pl08x_prep_slave_sg,
.device_terminate_all = pl08x_terminate_all,
};
/
如果_cctl_data,可以从DDI0196中找到对应的详细定义,下面根据datasheet内容和自己的理解,详细阐述一下:
(1)tsize:12
即TransferSize,传输大小,bit0-bit11,共12位。
表示,如果当前传输是DMA控制的情况下,当前此次DMA传输要传输的个数,其中每个具体大小是几个字节,由下面的位宽决定。
在配置完dma,开始传输后,此传输大小从你设置的值,即要传输的数,一点点减少直至0.如果读取此域,得到的值,表示当前还剩下多少个要传输。
不过特殊一点是,如果当前正在进行DMA传输,由于正在传输,所以你读取到的值,并不一定是真正的还剩多少个要传输的。一般用法是,在启用DMA传输,后来又由于传输完成后或者出错等特殊情况又禁用了DMA,此时再去读此域的值,就能真正有效地表示还多少没有传输的。
如果不是DMA控制数据流的传输,(之后会提到,关于DMA数据传输方向,除了DMA_TO_DEVICE,DMA_FROM_DEVICE之外,还有MEM_TO_MEM之类的,非DMA控制数据传输的情况)那么就应该在开始配置的时候,将此域值设置成0.
(2)sbsize:3, dbsize:3
即source burst size和destination burst size,即前面提到的,当设备发送给DMA控制器一个突发传输信号之后,DMA要传输多少个数据,就由此处设置的值决定。
支持的burst size有1,4,8,。。。,128,256。
前面已经解释过了,具体此处需要设置成多少,要根据你的硬件的datasheet中描述的你的硬件的能力去决定。
Datasheet中写的DMACxBREQ信号线,就是设备的控制器,如果支持DMA,都会有此信号线接出来的,这样,接到DMA的对应的DMACxBREQ引脚上,这样,如果设备控制器,发现当前的条件,满足DMA burst传输,比如FIFO一共36个word,发现FIFO中数据>=32个word了,快满了,就会向DMA控制器PL080,通过这个信号线发送burst read请求,然后PL080就会根据你DMA传输前此处设置的值,比如是32,去你的设备的FIFO中,一次性地读取你之前设置的32个word。
其中具体满足什么条件,才会触发DMA burst读取或写入的请求信号,都是对应你的设备的控制器的datasheet中描述的,也就是你的硬件本身的能力决定的。
(3)swidth:3, dwitch:3
即source width和destination width。
具体支持的位宽类型有,byte(8bit),half word(16bit),word(32bit)等。
关于source和destination,解释一下,
比如,你要从你的设备读取数据,即从你设备的FIFO中读取数据到内存某个位置,那么你的设备就是source,此时你的设备的FIFO的位宽,就是source width,比如你FIFO位宽是32bit的,那么此处,根据datasheet,就应该设置为word(32bit)。
(4)smaster:1,dmaster:1
即source master和destination master,当你的传输由DMA控制时,
比如上面说的DMA_FROM_DEVICE,DMA从设备的FIFO中读取数据到内存中,此时,你要设置一下,你的DMA是用哪一个,即source master是谁。因为,此处的DMA的控制者有2个,可以简单理解为,有两个dma控制器,master1和master2,你具体选哪个master来控制你的DMA传输。关于master的选择,后面的代码分析中,会再次提及。
(5)si:1,di:1
即source increment和destination increment,此处可翻译为,源地址递增和目标地址递增,
关于什么叫递增,为何要递增,可以从最开始提到的,DMA的常见应用情况中来解释,
因为常见的DMA传输,
是从某个设备读取数据到某块内存区域,即DMA_FROM_DEVICE,以DMA模式,将数据“From从”你的设备中,读取到某块内存里面;或者将某块内存区域内数据,写入到设备里面,即DMA_TO_DEVICE。
而设备往往都是只有一个DATA寄存器(或是FIFO),此地址是固定不变的,
因此:
对于DMA_FROM_DEVICE,你传输完一个数据了,再传下一个数据的时候,此时你的
Source源,还是你的设备的那个寄存器的地址,没有变化,而目标地址,往往要增加一个你的destination width,比如是一个word,32bit,4个字节,即地址要加4了,对应的就要将destination increment设置成1,表示,你的dma每次传输完,目标地址要增加的。
当然具体增加的大小,由你的设置的destination width的值决定。
相应地,如果是DMA_TO_DEVICE,那么就是DMA将数据从内存中写入到你的设备里面
,每次传输完后,就是源source地址要增加,source increment要设置为1.
同理,如果是MEM_TO_MEM类型的,内存到内存,就是两者都要设置成1了,因为DMA传输完单个数据后,源地址和目标地址都要改变,要增加相应位宽的大小的。
(6)prot
即Protection,保护位,共3bit。
一般很少用到此域,我也没有完全理解,故不多解释,仅简述其义:
Bit0: 0是普通用户模式,1是特权模式;
Bit1: 0是non-bufferable,1是bufferable;
Bit2:0是non-cacheable,1是cacheable.
(7)intr:1
是否启用计数终止中断(Terminal count interrupt)。
关于此位,之前一直很迷惑,后面终于看懂了。
就是说,对于DMA的传输中的单个LLI来说,当前传输完成了,对应的寄存器中的transfer size域的值,也就是从设置的值,递减到0,也就是此计数递减到0,即结束了,即terminal count,而Terminal Count Interrupt,即传输完了,达到了terminal count时候,发送一个中断,告诉设备此次传输完成了。
如果此位被设置为0,那么传输完当前的LLI,就不会发送这个中断了,设置为1,就发送此中断。
/
/
/*
此处之所以定义成union类型,就是方便,在设置好了之后,将此32位的值,直接写入对应的32位的寄存器中。
*/
union _cctl{
struct _cctl_data bits;
unsigned int val;
};
/*
下面这个就是DMA里面最核心的概念,LLI,Link List Item,包括了
(1)源地址;
(2)目标地址;
(3)下一个LLI的地址:如果其为0/NULL/空,说明当前只需要传输一个信息,如果非空,传完当前的LLI,就会跳转到对应的地址,执行下一个LLI的传输;
(4)对应的控制信息cctrl - channel control。
这四个值,会分别写入到对应的四个寄存器。
这样配置好了之后,再去启用DMA,DMA就会按照你的要求,把数据从源地址传送到目的地址。
下面的那个英文解释的意思是,next域,即下一个LLI的地址,其中的bit0,是bus bit,即指示当前用哪个bus,现将datasheet中相关解释截图如下:
图 1 LLI寄存器定义
如图1,对应的bit[0],LM位,就表示了,当前使用哪个AHB master,而真正的LLI的地址,是存放在bit[2-31]。
/*
/*
LLI结构体,存放LLI的dma地址(供DMA控制器访问的) bus_list,和虚拟地址(普通的地址,CPU可以访问的地址),va_list,
*/
struct _chan_lli {
dma_addr_t bus_list;
void *va_list;
};
struct pl08x_driver_data {
/*
记录对dma控制器硬件基地址ioremap之后的,驱动中可以直接访问的那个基地址
加上对应寄存器偏移量,就可以访问寄存器了
*/
void __iomem base;
/ 此pl08x的DMA控制也是一个amba设备 */
struct amba_device *dmac;
struct pl08x_platform_data pd;
/ dma内存池,供之后驱动每次的LLI的存放 /
/
* Single dma_pool for the LLIs
*/
struct dma_pool pool;
/ 记录已使用内存池的数量,当达到足够大一个值的时候,做一次清理动作 /
int pool_ctr;
/ 一个工作队列,后面可以看到,挂了一个函数,作用是清理释放dma pool */
struct work_struct dwork;
wait_queue_head_t waitq;
int max_num_llis;
/
* LLI details for each DMA channel
*/
struct _chan_lli chanllis;
/ 将DMA 通道相关的信息,打包在这个变量里 /
/ Wrappers for the struct dma_chan */
struct pl08x_dma_chan chanwrap[MAX_DMA_CHANNELS];
/ List of descriptors which can be freed */
spinlock_t lock;
};
/*
/* 暂时没有实现Linux中PM 的支持 /
/
#ifdef CONFIG_PM
#else
#endif
#ifdef MODULE
/*
a) Some devices might make use of DMA during boot
(esp true for DMAENGINE implementation)
b) Memory allocation will need much more attention
before load/unload can be supported
*/
#endif
struct pl08x_driver_data pd;
/*
/* Maximimum times we call dma_pool_alloc on this pool without freeing */
/* 后面代码中会看到,对于你的驱动提交的DMA请求,此DMA驱动内部会自动帮你转换成对应的一个个LLI,此数值就是限制一次DMA请求中,最大支持多少个LLI */
#define MAX_NUM_TSFR_LLIS (PL08X_LLI_TSFR_SIZE/sizeof(struct _lli))
#define PL08X_ALIGN 8
#define PL08X_ALLOC 0
/* 一些全局的寄存器的偏移地址,这些值都是根据datasheet中定义出来的 /
/ Register offsets /
#define PL08X_OS_ISR 0x00
#define PL08X_OS_ISR_TC 0x04
#define PL08X_OS_ICLR_TC 0x08
#define PL08X_OS_ISR_ERR 0x0C
#define PL08X_OS_ICLR_ERR 0x10
#define PL08X_OS_CFG 0x30
#define PL08X_OS_CCFG 0x10
#define PL08X_OS_ENCHNS 0x1C
#define PL08X_OS_CHAN 0x20
#define PL08X_OS_CHAN_BASE 0x100
/ DMA控制器,有很多个通道channel,每个channel都对应有自己的一些寄存器,下面就是这些寄存器的地址偏移和位域值的含义 /
/ Channel registers /
#define PL08X_OS_CSRC 0x00
#define PL08X_OS_CDST 0x04
#define PL08X_OS_CLLI 0x08
#define PL08X_OS_CCTL 0x0C
/ register masks /
#define PL08X_MASK_CFG 0xFFFFFFF1
#define PL08X_MASK_EN 0x00000001
#define PL08X_MASK_CLLI 0x00000002
#define PL08X_MASK_TSFR_SIZE 0x00000FFF
#define PL08X_MASK_INTTC 0x00008000
#define PL08X_MASK_INTERR 0x00004000
#define PL08X_MASK_CCFG 0x00000000
#define PL08X_MASK_HALT 0x00040000
#define PL08X_MASK_ACTIVE 0x00020000
#define PL08X_MASK_CEN 0x00000001
#define PL08X_MASK_ENCHNS 0x000000FF
#define PL08X_WIDTH_8BIT 0x00
#define PL08X_WIDTH_16BIT 0x01
#define PL08X_WIDTH_32BIT 0x02
/ 下面这几个宏定义,好像没用到 /
/
7 bytes must have a word alignable transfer somewhere)
/
#define PL08X_BITESIZE 0x10
/ 下面这个宏定义,好像也没用到,具体的流控制flow control的设置以及宏定义,在对应头文件中:
#define PL08X_CCFG_FCTL_MEM_TO_MEM (0)
#define PL08X_CCFG_FCTL_MEM_TO_PERI (1)
…….
/
/
#define PL08X_CODING_ERR 0xFFFFFFFF
/* 根据设置的值,解码出实际位的宽度 /
static unsigned int pl08x_decode_widthbits(unsigned int coded)
{
/
根据datasheet中的解释:
所以,目前只支持8.,16,32字节,位域的值分别是0,1, 2,所以此处判断小于3,才是有效的,然后1<
if (coded < 3)
return 1 << coded;
dev_err(&pd.dmac->dev, "%s - illegal width bits 0x%08xn", __func__,
coded);
return PL08X_CODING_ERR;
}
/* 上述函数的逆过程 */
static unsigned int pl08x_encode_width(unsigned int unencoded)
{
unsigned int retval = unencoded >> 1;
if (unencoded == (1 << retval))
return retval;
dev_err(&pd.dmac->dev, "%s - illegal width 0x%08xn", __func__,
unencoded);
return PL08X_CODING_ERR;
}