前言
项目做多了,会看到很多关于DMA的操作,比如
- 音频数据传输。
- SPI数据传输(spi-oled屏幕,传输图像)。
之所以列举出上面两项,是想在之后的章节重点介绍一下。
关于DMA,其实就是数据的搬运(mem->mem, dev->mem, mem->dev)。感觉很简单,但是看代码后,发现linux提供的DMA接口并不是很简单,所以想通过一系列的文章,来打通DMA脉络,了解DMA api的使用以及背后的意义,从而在遇到DMA相关操作时,不会那么的陌生,并且很快的能融入开发当中。本系列将以全志平台为背景介绍DMA。
DMA的硬件操作
关于DMA的操作,其实就是对DMA controller的操作,在controller上,有很多独立的DMA 通道(channel)提供选择,所谓通道就是数据传输的隧道。
比如内存与设备之间的数据传输,要通过DMA其中一个通道进行传输,即DMA controller会自动从内存中取数据(源),通过选好的通道(软件指定)传输到设备上(目的地),当然这些通道都是独立的。 另外、这里涉及到了3个硬件对象,即:内存<–>DMA<—>设备(假设是SPI),三者之间的传输的配置大概是:
- 源指定:内存
指定数据所在的内存地址给DMA , 并且告诉DMA,DMA<->DRAM的DRQ的编号(关于DRQ,稍后会提到)。 - 目的地指定: SPI设备
指定SPI设备接收数据的地址(可能是某个RX寄存器,或者TX寄存器)并且告诉DMA, DMA<-> SPI之间DRQ的编号。
DRQ,是设备与DMA之间的信号线,连接到 DMA的port上。
我们来看一下 DMA controller的 block 图。
上图所示,DMA controller 上面有很多的port用于 与各个设备进行链接(物理上的),每一条链接叫做DRQ,它是一条信号线。
上面的例子中,内存与SPI之间传输数据,就要激活DMA与DRAM的DRQ,以及DMA与SPI设备之间的DRQ。
DRQ的作用是:在设备(MEM&SPI)与DMA之间的数据传输中,需要类似于TCP/IP那样的传输协议来确保数据的可靠性(比如握手协议),这些协议需要操作DRQ信号线(拉高拉低),来进行数据方面的同步。
我们在配置DMA传输时,只需要告诉DMA要用到哪个DRQ,DMA会自动去控制它。
DRQ信号线的标号,是通过 port上的硬件标号来获取到的,如下图:
我们可以看到 SDRAM链接到DMA controller的port0端口,即DRQ的type就是0,
DMA与SPI之间的是 port22, port23,即DRQ的值就是 22,23。
细心的同学可能注意到 DRQ type 有 source跟 Destination之分,其实就是传输的方向问题,不管方向是什么,都是通过唯一的port来绑定的,比如本例的传输路线如下所示:
即,此传输,port0链接端点 是 Source DRQ type的,DRQ的值为0;
port22(SPI0)链接端点 是 Destination DRQ type的, DRQ的值为22;
DMA的软件操作
软件框架(framework)
对于DMA controller的控制,在linux下有一套管理,叫做 Linux DMA Engine framework,这个 framework实际上,就是提供了一系列操作 DMA controller 的API (controller的通道的选择等),这些API提供给 client driver (SPI driver)用来操作DMA,我们会在下一节会详细展framework的结构,目前只介绍大体脉络,让我们对DMA的结构有个快速的认识。
DMA的软件框图如下(从网上拿的图片):
可以看到,CH0->CHn 对应DMA controller里的硬件通道,在driver(这里的driver是指 DMA controller driver)中每个硬件通道都被抽象成一个结构体。比如: CH0<—>DC0, CH1<—>DC1等,与 硬件通道一一对应。
接下来上层进一步封装了虚拟通道(vchan),这些虚拟通道,多个虚拟通道会对应一个真实的硬件通道DC0,从而让client driver看起来 有很多的DMA通道可以用。
但实际上,经过代码分析,不管有多少个client driver 同时用DMA,即使申请不同的DCn,也不会像想象中的那样并行执行数据传输任务,实际做法是把任务都 扔到一个等待队列给DMA framework,framework会从队列中一个一个的去处理job,即排队处理,并不会想象中的并发执行job,所以感觉vchan的设计好像没有什么用。另外,在全志平台中 一个 vchan是对应一个DC0,即虚拟通道与 物理通道是一一对应的关系。
DMA的传输方式
- DMA块传输方式
DMA数据传输可分为块传输和散列传输两种方式。在DMA传输数据的过程中,要求源物理地址必须是连续的。但是在某些情况下,连续的存储器地址在物理上不一定是连续的,所以DMA传输要分成多次完成。传输完一块物理上连续的数据后引发一次中断,然后进行下一块物理上连续的数据传输,这就是DMA块传输方式(Block DMA)。 - 散列传输
散列传输是在块传输方式上发展起来的,需要硬件的支持,它与一个传输链表相关。该链表可以是单向结构或环形结构。链表是由叫做Descriptor information(也叫DMA descriptor)的链表项组成,一个传输任务对应一项DMA descriptor,之后我们只需要指定第一个 Descriptor information项的地址, 即链表第一项的地址给DMA,DMA就会自动把所有链表项中所描述的任务都执行完。
DMA descriptor
因为全志平台用的是 散列传输模式(比较灵活),所以重点介绍一下。我们通过一个实例,来说明这个链表是如何组成并且被DMA执行的。
比如说我们想通过DMA把数据从内存给SPI,一共8K的数据,我们用vmalloc函数申请内存空间,因为vmalloc只保证虚拟地址空间是连续的,所以这些数据在内存上是不连续的,因此我们要:
- 将 8k内存所对应的连续物理空间找出来, 即2页的数据,并且这两页并不是连着的(start_phy1->end1, start_phy2->end2)
- 将上面两部分的内存范围填入 DMA descriptor结构体,组成 descriptor1, descriptor2,两个链表项。
- 将 descriptor1 的地址传给DMA,进行传输。
接下来,我们看一下 DMA Descriptror的描述(取自 datasheet)
上面的说明很详细了,我们接着上面的例子,全志平台中DMA descriptor对应的是struct sunxi_dma_lli 结构体,我们看一下如何填充此结构体,因为需要填充两个,首先是:start_phy1->end1的地址范围。
#define sunxi_slave_id(d, s) (((d)<<16) | (s))
#define GET_SRC_DRQ(x) ((x) & 0x000000ff)
#define GET_DST_DRQ(x) ((x) & 0x00ff0000)
#define DRQSRC_SDRAM 0
#define DRQDST_SPI0TX 22
#define DRQDST_SPI1TX 23
#define SUNXI_SPI_DRQ_RX(ch) (DRQSRC_SPI0RX + ch)
#define SUNXI_SPI_DRQ_TX(ch) (DRQDST_SPI0TX + ch)
struct sunxi_dma_lli *lli = malloc;
lli->src = (u32)start_phy1;
//为什么是TX
// 因为要从SPI0 HOST 的TX寄存器传数据给OLED。
// 所以DMA要把数据传给TX寄存器,TX寄存器再把数据发送给OLED
lli->dst = (u32)SPI_TX //Transmit Data;
lli->len = end1 - start_phy1;
lli->para = NORMAL_WAIT;
lli->cfg |= SRC_LINEAR_MODE
| DST_LINEAR_MODE
| GET_DST_DRQ(sconfig->slave_id); //告诉DMA传输的时候,要用到 DMA->SPI之间的DRQ,之后传输开始后,DMA会自动控制指定的DRQ信号线。
| GET_SRC_DRQ(sconfig->slave_id); //告诉DMA传输的时候,要用到 DMA->DRAM之间的DRQ,之后传输开始后,DMA会自动控制指定的DRQ信号线。
//spi driver:
//slave_id = sunxi_slave_id(SUNXI_SPI_DRQ_TX(sspi->master->bus_num), DRQSRC_SDRAM);
紧接着对于:start_phy2->end2 我们做同样的事情:
struct sunxi_dma_lli *lli2 = malloc;
...
...
接下来我们设置链表
lli->p_lln = lli2;
lli2->p_lln = 0xFFFFF800;
即第一个DMA descriptor指向第二个 DMA descriptor
第二个 指向 0xFFFFF800(意味着结束)。
之后将 lli->p_lln 写入 DMA 中
#define CHAN_START 1
#define CHAN_STOP 0
/* write the first lli address to register, and start to transfer */
//txd->lli_phys: lli的物理地址
writel(txd->lli_phys, sdev->base + DMA_LLI_ADDR(chan_num));
writel(CHAN_START, sdev->base + DMA_ENABLE(chan_num));
//sdev->base + DMA_LLI_ADDR(chan_num)
//sdev->base + DMA_ENABLE(chan_num)
DMA启动后,DMA就会自动读取链表中的第一项,传输完成后会产生一个中断,然后紧接着自动读取第二个项进行传输, 第二项传输完了,发现没有第三项(因为第二项的p_lln 是0xFFFFF800)
为了对照,我们看一个形象的图。