搭建一个和linux开发者知识共享和学习的平台
分类: LINUX
2024-08-23 10:38:40
CPU写内存的时候有两种方式:
write through: CPU直接写内存,不经过cache。
write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。
DMA可以完成从内存到外设直接进行数据搬移。但DMA不能访问CPU的cache,CPU在读内存的时候,如果cache命中则只是在cache去读,而不是从内存读,写内存的时候,也可能实际上没有写到内存,而只是直接写到了cache。
这样一来,如果DMA从将数据从外设写到内存,CPU中cache中的数据(如果有的话)就是旧数据了,这时CPU在读内存的时候命中cache了,就是读到了旧数据;CPU写数据到内存时,如果只是先写到了cache,则内存里的数据就是旧数据了。这两种情况(两个方向)都存在cache一致性问题。例如,网卡发包的时候,CPU将数据写到cache,而网卡的DMA从内存里去读数据,就发送了错误的数据。
如何解决一致性问题
主要靠两类APIs:
一致性DMA缓存(Coherent DMA buffers)
DMA需要的内存由内核去申请,内核可能需要对这段内存重新做一遍映射,特点是映射的时候标记这些页是不带cache的,这个特性也是存放在页表里面的。
上面说“可能”需要重新做映射,如果内核在highmem映射区申请内存并将这个地址通过vmap映射到vmalloc区域,则需要修改相应页表项并将页面设置为非cache的,而如果内核从lowmem申请内存,我们知道这部分是已经线性映射好了,因此不需要修改页表,只需修改相应页表项为非cache即可。
相关的接口就是dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()会传一个device结构体指明给哪个设备申请一致性DMA内存,它会产生两个地址,一个是给CPU看的,一个是给DMA看的。CPU需要通过返回的虚拟地址来访问这段内存,才是非cache的。至于dma_alloc_coherent()的内部实现可以不关注,它是和体系结构如何实现非cache(如mips的kseg1)相关,也可能与硬件特性(如是否支持CMA)相关。
还有一个接口dma_cache_sync(),可以手动去做cache同步,上面说dma_alloc_coherent()分配的是uncached内存,但有时给DMA用的内存是其他模块已经分配好的,例如协议栈发包时,{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳终要把skb的地址和长度交给DMA,除了将skb地址转换为物理地址外,还要将CPU cache写回(因为cache里可能是新的,内存里是旧的)。
贴出一种实现:
void dma_cache_sync(struct device *dev, void *vaddr, size_t size,
enum dma_data_direction direction)
{
void *addr;
addr = __in_29bit_mode() ?
(void *)CAC_ADDR((unsigned long)vaddr) : vaddr;
switch (direction) {
case DMA_FROM_DEVICE: /* invalidate only */
__flush_invalidate_region(addr, size);
break;
case DMA_TO_DEVICE: /* writeback only */
__flush_wback_region(addr, size);
break;
case DMA_BIDIRECTIONAL: /* writeback and invalidate */
__flush_purge_region(addr, size);
break;
default:
BUG();
}
}
调用这个函数的时刻就是上面描述的情况:因为内存是可cache的,因此在DMA读内存(内存到设备方向)时,由于cache中可能有新的数据,因此要先将cache中的数据写回到内存;在DMA写内存(设备到内存方向)时,cache中可能还有数据没有写回,为了防止cache数据覆盖DMA要写的内容,要先将cache无效。注意这个函数的vaddr参数接收的是虚拟地址。
例如在发包时将协议栈的skb放进ring buffer之前,要做一次DMA_TO_DEVICE的flush。对应的,在收包后为ring buffer中已被使用的skb数据buffer重新分配内存后,要做一次DMA_FROM_DEVICE的flush(invalidate的时候要注意cache align)。
还有一种针对可cache的内存做一致性的方式,就是流式DMA映射。
流式DMA映射(DMA Streaming Mapping),
相关接口为 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。
一致性缓存的方式是内核专门申请好一块内存给DMA用。而有时驱动并没这样做,而是让DMA引擎直接在上层传下来的内存里做事情。例如从协议栈里发下来的一个包,想通过网卡发送出去。
但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。
这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。
由于协议栈下来的包的数据有可能还在cache里面,调用dma_map_single()后,CPU就会做一次cache的flush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了。
注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设:
从内存到外设: CPU会做cache的flush操作,将cache中新的数据刷到内存。
从外设到内存: CPU将cache置无效,这样CPU读的时候不命中,就会从内存去读新的数据。
还要注意,这几个接口都是一次性的,每次操作数据都要调用一次map和unmap。并且在map期间,CPU不能去操作这段内存,因此如果CPU去写,就又不一致了。
同样的,dma_map_sg()和dma_map_single()的后端实现也都是和硬件特性相关。
DMA Master: DMA 控制器,它负责执行数据传输。
DMA Slave: 外设设备,它作为数据传输的源或目的地。
dma_slave_config 结构体
为了配置 DMA_SLAVE 模式,Linux 内核定义了一个 dma_slave_config 结构体,用于描述 DMA 通道的配置信息。
c
struct dma_slave_config {
dma_addr_t src_addr;
dma_addr_t dst_addr;
enum dma_transfer_direction direction;
enum dma_slave_buswidth src_addr_width;
enum dma_slave_buswidth dst_addr_width;
uint32_t src_maxburst;
uint32_t dst_maxburst;
bool device_fc;
// 其他字段...
};
字段说明
src_addr: 源地址(外设地址)。
dst_addr: 目标地址(内存地址)。
direction: 数据传输方向,常见值包括 DMA_MEM_TO_DEV, DMA_DEV_TO_MEM。
src_addr_width 和 dst_addr_width: 源和目标地址的总线宽度。
src_maxburst 和 dst_maxburst: 源和目标的{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳大突发传输大小。
device_fc: 是否使用设备端流控制。
配置和使用示例
以下是如何配置和使用 DMA_SLAVE 模式的简化示例:
c
static int my_driver_probe(struct platform_device *pdev)
{
struct dma_chan *chan;
struct dma_slave_config slave_config;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
dma_addr_t src_dma;
void *src_buf;
size_t buf_size = 1024;
// 申请 DMA 通道
chan = dma_request_chan(&pdev->dev, "my_dma");
if (IS_ERR(chan)) {
pr_err("Failed to request DMA channel\n");
return PTR_ERR(chan);
}
// 分配并映射内存缓冲区
src_buf = dma_alloc_coherent(&pdev->dev, buf_size, &src_dma, GFP_KERNEL);
if (!src_buf) {
pr_err("Failed to allocate coherent memory\n");
dma_release_channel(chan);
return -ENOMEM;
}
// 配置 DMA_SLAVE 模式
memset(&slave_config, 0, sizeof(slave_config));
slave_config.direction = DMA_MEM_TO_DEV;
slave_config.src_addr = src_dma;
slave_config.dst_addr = 0x40000000; // 假设外设寄存器地址
slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
slave_config.src_maxburst = 16;
slave_config.dst_maxburst = 16;
dmaengine_slave_config(chan, &slave_config);
// 准备 DMA 传输
tx = dmaengine_prep_slave_single(chan, src_dma, buf_size, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx) {
pr_err("Failed to prepare DMA transfer\n");
dma_free_coherent(&pdev->dev, buf_size, src_buf, src_dma);
dma_release_channel(chan);
return -EIO;
}
// 提交并启动 DMA 传输
cookie = tx->tx_submit(tx);
if (dma_submit_error(cookie)) {
pr_err("Failed to submit DMA transfer\n");
dma_free_coherent(&pdev->dev, buf_size, src_buf, src_dma);
dma_release_channel(chan);
return -EIO;
}
dma_async_issue_pending(chan);
// 等待 DMA 传输完成(可以添加等待机制)
// ...
// 清理资源
dma_free_coherent(&pdev->dev, buf_size, src_buf, src_dma);
dma_release_channel(chan);
return 0;
}
static int my_driver_remove(struct platform_device *pdev)
{
// 清理代码
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "my,dma-device", },
{},
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_platform_driver = {
.probe = my_driver_probe,
.remove = my_driver_remove,
.driver = {
.name = "my_dma_driver",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_platform_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DMA_SLAVE Mode Example");
关键点
请求 DMA 通道: 使用 dma_request_chan 请求 DMA 通道。
分配和映射内存: 使用 dma_alloc_coherent 分配一致性内存,并获取物理地址。
配置 DMA_SLAVE 模式: 填写 dma_slave_config 结构体,然后调用 dmaengine_slave_config。
准备和提交传输: 使用 dmaengine_prep_slave_single 准备传输,并使用 tx_submit 提交传输。
启动传输: 调用 dma_async_issue_pending 启动 DMA 操作。
清理资源: 传输完成后,释放 DMA 通道和内存。
通过这些步骤,可以高效地在外设和内存之间进行数据传输,而不会占用过多的 CPU 资源。