DMA功能用于执行内存读取和写入操作而不占用CPU周期。当需要传输数据块时,CPU向DMA控制器提供原地址和目的地址以及总字节数,然后DMA控制器会将数据从源传输到目标,而不占用CPU周期。
一、设置DMA映射
在外设DMA时,根据传输方向指定内存源或目的地址,这里的地址是总线地址,并设置缓存一致性。
所有的DMA传输都要进行适当的内存映射,DMA映射包括分配DMA缓冲区和为其生成总线地址。DMA设备使用的总线地址,总线地址是dma_addr_t{}类型的实例。
DMA映射分为两种类型:一致性DMA映射和流式DMA映射。一致性DMA映射可以用于多次DMA传输,它会自动解决缓存一致性的问题。一致性DMA映射通常存在于驱动程序的整个生命周期,而流式映射则通常在DMA传输完成就被取消。
1. 一致性DMA映射
/* 分配的dma物理内存一定是连续的,其中flags是分配内存的方式GFP_KERNEL或GEP_ATOMIC
*/
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flags);
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t *dma_handle);
2. 流式DMA映射
流式DMA映射必须使用已分配的缓冲区;在映射时需要指定传输方向,只能基于该方向使用该DMA映射。
映射分为两种形式:单缓冲映射(只允许单页映射),分散/聚集映射(允许传递多个缓冲区);
单缓冲区映射:
enum dma_data_direction {
DMA_BIDIRECTIONAL = 0,
DMA_TO_DEVICE = 1,
DMA_FROM_DEVICE = 2,
DMA_NONE = 3,
};
// 其中ptr是内核虚拟地址
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum
dma_data_direction direction);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,
enum dma_data_direction direction);
// 单页流式映射
dma_addr_t dma_map_page(struct device *dev, struct page *page,
offset_t offset, size_t size, enum dma_data_direction direction);void dma_unmap_page(struct device *dev, dma_addr_t dma_addr, size_t size,
enum dma_data_direction direction);
分散/聚集映射
struct scatterlist {
unsigned long page_link; // 指向page指针struct page *page;
uint offset;
uint length;
dma_addr_t dma_address;
uint dma_length;
};
int dma_map_sg(struct device *dev, struct scatterlist *sg, uint nents,
enum dma_data_direction direction);
void dma_unmap_sg(struct device *dev, struct scatterlist *sg, uint nents,
enum dma_data_direction direction);
/*dmap sg 使用示例*/
u32 wrbuf1 = kzalloc(SDMA_SIZE, GFP_DMA);
u32 wrbuf2 = kzalloc(SDMA_SIZE, GFP_DMA);
u32 wrbuf3 = kzalloc(SDMA_SIZE/2, GFP_DMA);
struct scatterlist sg[3];
sg_init_table(sg, 3);
sg_set_buf(&sg[0], wrbuf1, SDMA_SIZE);
sg_set_buf(&sg[1], wrbuf2, SDMA_SIZE);
sg_set_buf(&sg[2], wrbuf3, SDMA_SIZE/2);
ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM);
// 在DMA使用完后临时交给CPU使用,使用完后又返回给CPU
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, uint nents,
enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, uint nents,
enum dma_data_direction direction);
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_addr, size_t size,
enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction direction);
二、DMA引擎API
DMA引擎是开发DMA控制器驱动的通用内核框架。DMA的主要任务是复制内存时减轻CPU的负担,使用通道将事物(IO传输)委托给DMA引擎。DMA引擎则通过DMA控制器驱动程序/API提供一组可供其他设备(DMA从设备)使用的框架。下面是从设备DMA使用DMA引擎的用法。
1. 分配DMA从通道
// 分配释放dma从通道
struct dma_chan *dma_request_channel(const dma_cap_mask *mask,
dma_filter_fn fn, void *fn_param);
void dma_releas_channel(struct dma_chan *chan);
enum dma_transaction_type {
DMA_MEMCPY,... , DMA_PRIVATE/*内存不可再memcpy访问*/,
DMA_SLAVE/*设备内存之间*/, DMA_CYCLIC, /*设备能够处理循环传输*/
};
// 示例:
dma_cap_mask my_dma_cap_mask;
struct dma_chan *chan;
dma_cap_zero(my_dma_cap_mask);
dmap_cap_set(DMA_MEMCPY, my_dma_cap_mask);
chan = dma_request_channel(my_dma_cap_mask, NULL, NULL);
2. 设置从设备和控制器的特定参数
DMA从通道的运行时配置struct dma_slave_config{},可以设置DMA方向、DMA地址、总线宽度、DMA突发长度(DMA burst length)
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;
[...]
};
//其中dma_slave_buswidth的定义如下:
enum dma_slave_buswidth {
DMA_SLAVE_BUSWIDTH_UNDEFINED = 0,
DMA_SLAVE_BUSWIDTH_1_BYTE = 1,
DMA_SLAVE_BUSWIDTH_2_BYTE = 2,
DMA_SLAVE_BUSWIDTH_3_BYTE = 3,
DMA_SLAVE_BUSWIDTH_4_BYTE = 4,
DMA_SLAVE_BUSWIDTH_8_BYTE = 8,
DMA_SLAVE_BUSWIDTH_16_BYTE = 16,
DMA_SLAVE_BUSWIDTH_32_BYTE = 32,
DMA_SLAVE_BUSWIDTH_64_BYTE = 64,
};
/* 其中src_maxburst和dst_maxburst,一次突发中可以发送到设备的最大字数(这里的字指的是dma_slave_buswidth)。通常是从设备的FIFO大小的一半。*/
// 设置dmaslave 配置
int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config);
//示例如下:
struct dma_chan *my_dma_chan;
dma_addr_t dma_src, dma_dst;
struct dma_slave_config my_dma_cfg = {0};
my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL);
my_dma_cfg.direction = DMA_MEM_TO_MEM;
my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES;
dmaengine_slave_config(my_dma_chan, &my_dma_cfg);
3. 获取事务描述符
在获取的dma_chan{}中都包含一个dma_device{}的指针,而dma_device{}代表一个DMC(DMA控制器),它提供了一系列获取DMA事务的函数:
// 函数的原型为:
struct dma_async_tx_descriptor *(*device_prep_dma)(struct dma_chan *chan,
dma_addr_t dst, dma_addr_t src, size_t len, ulong flags);
device_prep_dma_memcpy() // 准备memcpy操作
device_prep_dma_sg() // 准备s/g memcpy操作
device_prep_dma_xor()
device_prep_dma_xor_val()
device_prep_dma_pq()
device_prep_dma_pq_val()
device_prep_dma_memset() // 准备memset操作
device_prep_dma_memset_sg() // 准备memset s/g操作
device_prep_dma_slave_sg() // 准备从设备DMA操作
device_prep_dma_interleaved_dma()
// 这些函数会返回struct dma_async_tx_descriptor{}指针
// 示例:
struct dms_device *dma_dev = my_dma_chan->device;
struct dma_async_tx_descriptor *tx = NULL;
char *dst_data, src_data;
dma_addr_t dma_src, dma_dst;
src_data = kzalloc(BUFFER_SIZE, GFP_DMA);
dst_data = kzalloc(BUFFER_SIZE, GFP_DMA);
dma_src = dma_map_single(NULL, src_data, BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_dst = dma_map_single(NULL, dst_data, BUFFER_SIZE, DMA_MEM_TO_MEM);
tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst,
dma_src, BUFFER_SIZE, 0);
4. 提交事务
要将事务放入驱动程序的等待队列中,需要调用dmaengine_submit(),该函数返回一个cookie,可以用来查看DMA活动的进度。注意:dmaengine_submit()不会启动DMA,只是将其放入待处理队列中。
// 提交事物的函数原型
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
// 示例:
struct completion transfer_ok;
init_completion(&transfer_ok);
tx->callback = my_dma_callback;
dma_cookie_t cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) { printk(KERN_ERR "failed to start dma.");
}
// 对应的callback
static void my_dma_callback() {
complete(&transfer_ok);
return;
}
5. 发出挂起的请求并等待回调通知
在通道上调用dma_async_issue_pending()来激活通道等待队列中的事务。在DMA操作完成时,队列中的下一个事务自动启动,并触发设置的软中断(tasklet),它负责调用用户的回调函数。
// 启动dma操作的函数
void dma_async_issue_pending(struct dma_chan *chan);
// 使用举例:
dma_async_issue_pending(my_dma_chan);
wait_for_completion(&transfer_ok);
dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, BUFFER_SIZE, DMA_MEM_TO_MEM);
dma_unmap_single(my_dma_chan->device->dev, dma_dst_addr, BUFFER_SIZE, DMA_MEM_TO_MEM);
// 接下来通过tx_data和rx_data处理数据
三、DMA DT绑定
DMA通道的DT绑定取决于DMA控制器节点(取决于SoC),某些参数(如DMA单元)可能因SoC而异。
下面是绑定示例:
// DT文件中的DMA通道:
uart1 : serial&02020000 {
compatible = "fsl,imx6sx-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6SX_CLK_UART_IPG>, <&clks IMX6SX_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
// 对应提取DMA的代码
static int imx_uart_dma_init(struct imx_port *sport) {
struct dma_slave_config slave_config = {};
struct device *dev = sport->port.dev;
int ret;
/*dma rx,注意此处的dma_request_slave_channel()
会调用of_dma_request_slave_channel()解析DT设备节点,
收集通道设置。*/
struct dma_chan_rx = dma_request_slave_channel(dev, "rx");
/*...*/
};