概述

在SD卡读写和framebuffer拷贝功能上用DMA实现

  1. 添加了dma.c和dma.h源码文件,定义了dma接口和实现
  2. 在sd.c的sd_readblock和sd_writeblock函数中添加了DMA相关函数调用实现数据传输。

树莓派dma简介

概述

BCM2835 中的大多数硬件流水线和外设都是总线主控器,使它们能够有效地满足自己的数据需求。这降低了 DMA 控制器对块到块内存传输的要求,并支持一些更简单的外设。此外,DMA 控制器提供只读预取模式,以允许将数据带入 L2 高速缓存以供以后使用。

注意 DMA 控制器直接连接到外围设备。因此,DMA控制器必须设置为使用外设的物理(硬件)地址。

BCM2835 DMA 控制器总共提供 16 个 DMA 通道。每个通道独立于其他通道运行,并在内部仲裁到 3 条系统总线之一。这意味着 DMA 通道可能消耗的带宽量可以通过仲裁器设置来控制。

每个 DMA 通道通过将控制块 (control block) 数据结构从内存加载到内部寄存器来运行。控制块定义所需的 DMA 操作。一旦当前控制块中描述的操作完成,每个控制块都可以指向另一个要加载和执行的控制块。以这种方式,可以构建控制块的链接列表,以便在没有软件干预的情况下执行一系列 DMA 操作。

外设到 DMA 通道的分配是可编程的。
DMA 可以处理字节对齐的传输,并通过缓冲和打包未对齐的访问来最小化总线流量。
每个 DMA 通道都可以通过顶级电源寄存器完全禁用以节省功耗。

DMA 控制器寄存器

DMA 控制器由几个相同的 DMA 通道组成,具体取决于所需的配置。每个单独的 DMA 通道都有一个相同的寄存器映射(尽管 LITE 通道的功能较少,因此寄存器较少)。
DMA 通道 0 位于地址 0x7E007000,通道 1 位于 0x7E007100,通道 2 位于 0x7E007200,依此类推。
因此相邻的 DMA 通道偏移 0x100。然而,DMA 通道 15 在物理上从其他 DMA 通道中移除,因此具有不同的地址基址 0x7EE05000。
image.png

DMA 通道寄存器地址映射

每个 DMA 通道都有一个相同的寄存器映射,只是每个通道的基地址不同。地址映射顶部有一个全局启用寄存器,可以禁用每个 DMA 以实现节能。每个通道寄存器集中只有三个寄存器是可直接写的(CS、CONBLK_AD 和 DEBUG)。其他寄存器(TI、SOURCE_AD、DEST_AD、TXFR_LEN、STRIDE 和 NEXTCONBK)从外部存储器中保存的控制块数据结构自动加载。

控制块数据结构

控制块 (CB) 的长度为 8 个字(256 位),并且必须从 256 位对齐的地址开始。
CB 数据结构在内存中的格式如下图所示。
在 DMA 传输开始时,控制块的每个 32 位字会自动加载到相应的 32 位 DMA 控制块寄存器中。这些寄存器的描述也定义了内存中CB数据结构中对应的位位置。

image.png
  通过将 CB 结构的地址写入 CONBLK_AD 寄存器然后设置 ACTIVE 位来启动 DMA。 DMA 将从该 reg 的 SCB_ADDR 字段中设置的地址获取 CB,并将其加载到下面描述的只读寄存器中。然后它将根据 CB 中的信息开始 DMA 传输。
  当它完成当前的 DMA 传输(长度 => 0)时,DMA 将使用 NEXTCONBK 寄存器的内容更新 CONBLK_AD 寄存器,从该地址获取一个新的 CB,并再次开始整个过程。
  当 DMA 完成 DMA 传输并且 NEXTCONBK 寄存器设置为 0x0000_0000 时,DMA 将停止(并清除 ACTIVE 位)。它将将此值加载到 CONBLK_AD reg 中,然后停止。
  大多数控制块寄存器不能直接写入,因为它们是从内存中自动加载的。可以读取它们以提供状态信息,并指示当前 DMA 传输的进度。加载到 NEXTCONBK 寄存器中的值可以被覆盖,以便控制块数据结构的链表可以动态更改。然而,只有在 DMA 暂停时才可以安全地执行此操作。

Register Map

dma寄存器映射到内存上的地址详情见下表:
每个channel有一组寄存器,每个通道的寄存器占0x100的内存,channel 0寄存器相对于0x7E007000的偏移是0,channel 1的寄存器相对0x7E007000的偏移是0x100,依次递增
image.png

CS寄存器
DMA 控制和状态寄存器包含该 DMA 通道的主要控制和状态位。
image.png
最高位是RESET位,设置为1时初始化DMA
image.png
第3位是Data request位,
指示所选 DREQ(数据请求)信号的状态,即。由传输信息的 PERMAP 字段选择的 DREQ。 1 = 请求数据。这仅在 DMA 启动并且 PERMAP 字段已从 CB 加载后才有效。它将保持有效,指示选定的 DREQ 信号,直到加载新的 CB。如果 PERMAP 设置为零(无节奏传输),则该位将读回为 1。 0 = 无数据请求。
image.png
image.png
第0,1,2位分别是传输启动位和当前control block传输结束位,active位还表示所有的control _block传输结束

TI寄存器
保存了DMA 传输信息
image.png
PERMAP表示外设编号 (1-31),其就绪信号将用于控制传输速率,其紧急信号将在 DMA AXI 总线上输出。设置为 0 表示连续的无节奏传输。

image.png
这三个SRC开头的位,表示了对源数据的设置,SRC_INC表示源地址是否增长,如果为1, 则每次增长,否则不变。

SRC_WITDH表示源数据传输时地址宽度,如果是0则每次32bit,否则128bit

SRC_DREQ
如果该位是1,PER_MAP 选择的 DREQ 将选通源读取
否则DREQ位没有影响

image.png
DEST开头的三个位,与SRC含义相同,只是作用于目标数据而已

image.png
INTEN表示INT enable,用于允许CS 寄存器的INT位变化

TXFR寄存器
DMA 传输长度。这指定要传输的数据量(以字节为单位)。在正常(非 2D)模式下,这指定了要传输的字节数。在 2D 模式下,它被解释为 X 和 Y 长度,并且 DMA 将执行 Y 传输,每个长度为 X 字节,并在传输的每个 X 支路之后将跨度添加到地址上。随着传输的进行,DMA 引擎会更新长度寄存器,因此它将指示剩余要传输的数据。
image.png

ENABLE寄存器
每个通道的全局使能位,每个比特表示第几个dma channel被允许
image.png

外设到 DREQ 的映射如下
image.png)image.png

这里是TI寄存器的PERMAP位可选的值,对应SD卡的读写,选择11号外设,也就是EMMC,因为在sd.c中用的是EMMC协议完成的与sd交互

实现

dma.h

peripherals/dma.h

定义control _block数据结构

1
2
3
4
5
6
7
8
9
typedef struct {
unsigned int transfer_info;
unsigned int src_addr;
unsigned int dest_addr;
unsigned int transfer_length;
unsigned int mode_2d_stride;
unsigned int next_block_addr;
unsigned int res[2];
} dma_control_block;

定义dma通道寄存器组对应的数据结构

1
2
3
4
5
typedef struct {
unsigned int control;
unsigned int control_block_addr;
dma_control_block block;
} dma_channel_regs;

DMA通道寄存器组偏移计算,每个通道的寄存器占0x100字节宽度
image.png
其中KERNEL_MMIO_BASE被定义为

1
#define KERNEL_MMIO_BASE 0xFFFFFFFFFF000000

物理地址0x3F000000被映射到线性地址的0xffffffffff000000地址处
image.png

enable寄存器和status寄存器的宏定义
image.png
image.png

dma.h

在dma.h中定义了dma通道结构体,和channel类型。
dma_channel的channel成员表示通道序号,从0~14.
block成员是个指向dma_control_block结构体的指针
status成员是当前通道最近一次数据传输的状态

1
2
3
4
5
6
7
8
9
10
11
#include "peripherals/dma.h"
#define IO_TO_BUS(x) (void*)((unsigned int)(x) & 0x00ffffff | 0x7e000000)
typedef struct {
unsigned int channel;
dma_control_block *block;
int status;
} dma_channel;
typedef enum {
CT_NONE = -1,
CT_NORMAL = 0x81
} dma_channel_type;

暴露的dma功能函数接口,打开一个channel,关闭一个channel,dma实现内存拷贝,dma传输启动, dma等待

1
2
3
4
5
dma_channel *dma_open_channel(unsigned int channel);
void dma_close_channel(dma_channel *channel);
void dma_setup_mem_copy(dma_channel *channel, void *dest, void *src, unsigned int length, unsigned int burst_length);
void dma_start(dma_channel *channel);
int dma_wait(dma_channel *channel);

dma.c

定义控制块变量,分别用于framebuffer和sd卡读写;定义通道数组
image.png

dma_open_channel函数,从dma_channel结构体数组选出参数channel对应的channel,并让结构体的block指针指向对应的控制块变量。sd卡用0号channel,framebuffer使用2号channel。
通过REGS_DMA_ENABLE 指向的enable寄存器的值的变化,使能对应的dma_channel设备。
通过REGS_DMA(dma->channel)->control 指向的CS寄存器让对应的dma channel完成重置。
语句REGS_DMA(dma->channel)->control & CS_RESET 检查CS寄存器的RESET位,直至不为1表示reset过程完成。
返回对应的dma_channel结构体指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
dma_channel *dma_open_channel(unsigned int channel)
{
unsigned int _channel = allocate_channel(channel);

if (_channel == CT_NONE)
{
lfb_color_printf(0x0000ff, "INVALID CHANNEL! %d\n", channel);
return 0;
}

dma_channel *dma = (dma_channel *)&channels[_channel];
dma->channel = _channel;

if(channel == 0)
dma->block = &sd_control_block;
else if(channel == 2)
dma->block = &lfb_control_block;
dma->block->res[0] = 0;
dma->block->res[1] = 0;

REGS_DMA_ENABLE |= (1 << dma->channel);
delay(300);
REGS_DMA(dma->channel)->control |= CS_RESET;

while (REGS_DMA(dma->channel)->control & CS_RESET)
;

return dma;
}

dma_start启动dma数据传输,首先设置控制块的地址,需要将线性地址转换为物理地址。
使用ACTIVE位启动数据传输,END让当前控制块的dma传输结束时中断,INT允许中断,
image.png

dma_wait函数,判断当前dma通道的CS寄存器的active位是否为1,当所有的控制块表示的dma传输完成时,CS寄存器的active位变为0
读取CS寄存器的error位,作为本地dma传输的status状态返回
image.png

dma_setup_mem_copy的实现在dma.c中
image.png
这里与sd卡读写的dma设置有所不同,其源地址和目的地址在数据传输过程中都是要递增的,所以要给transfer_info设置TI_SRC_WIDTH,TI_SRC_INC,TI_DEST_WITDH,TI_DEST_INC属性


sd.c

在sd_init函数的最后使用dma_open_channel打开0号通道,完成初始化。
image.png

在sd_readblock函数中发送读取命令后,如果不使用DMA,那么就用循环的方法从EMMC_DATA处读取数据到buf中。
如果使用DMA的话,设置DMA的源地址和目的地址,使用TI_SRC_DREQ表示从外围设备获取数据,TI_DEST_INC, TI_DEST_WITDH表示目的地址递增,TI_PERMAP_EMMC表示外围设备类型是EMMC。
完成控制块的构造后,调用dma_start启动dma数据传输,调用dma_wait等待dma数据传输完成。
如果不用dma,则需要判断emmc的命令是否执行完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
while( c < num ) {
if(!(sd_scr[0] & SCR_SUPP_CCS)) {
sd_cmd(CMD_READ_SINGLE,(lba+c)*512);
if(sd_err) return 0;
}

if (!use_dma)
{
if ((r = sd_int(INT_READ_RDY)))
{
uart_puts("\rERROR: Timeout waiting for ready to read\n");
sd_err = r;
return 0;
}
for (d = 0; d < 128; d++)
buf[d] = *EMMC_DATA;
buf += 128;
}
c++;
}

if(use_dma)
{
dma_sd_channel->block->src_addr = IO_TO_BUS(EMMC_DATA);
dma_sd_channel->block->dest_addr = K_LIN2PHY(buf);
dma_sd_channel->block->transfer_info = TI_INTEN |
TI_WAIT_RESP |
TI_DEST_INC |
TI_DEST_WIDTH |
TI_SRC_DREQ |
TI_PERMAP_EMMC;
dma_sd_channel->block->next_block_addr = 0;
dma_sd_channel->block->transfer_length = num*512;
dma_sd_channel->block->mode_2d_stride = 0;

dma_start(dma_sd_channel);
if(dma_wait(dma_sd_channel) == 0)
{
uart_puts("ERROR in dma read\n");
while (1)
{
/* code */
}

}
}

if (!use_dma)
{
if ((r = sd_int(INT_DATA_DONE)))
{
uart_puts("\rERROR: Timeout waiting for data done\n");
sd_err = r;
return 0;
}
}
}

对于sd_writeblock函数,只需要将目的地址和源地址交换,在transfer_info中设置TI_DEST_DREQ,TI_SRC_INC,TI_SRC_WITDH, TI_PERMAP_EMMC表示是从内存到外设。
image.png

× 请我吃糖~
打赏二维码