Chinaunix首页 | 论坛 | 博客
  • 博客访问: 309398
  • 博文数量: 101
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 774
  • 用 户 组: 普通用户
  • 注册时间: 2018-10-15 14:13
个人简介

搭建一个和linux开发者知识共享和学习的平台

文章分类

全部博文(101)

文章存档

2024年(15)

2023年(24)

2022年(27)

2019年(8)

2018年(27)

分类: LINUX

2024-08-23 10:38:40

CPU写内存的时候有两种方式:


write through: CPU直接写内存,不经过cache

write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。


DMA可以完成从内存到外设直接进行数据搬移。但DMA不能访问CPUcacheCPU在读内存的时候,如果cache命中则只是在cache去读,而不是从内存读,写内存的时候,也可能实际上没有写到内存,而只是直接写到了cache


这样一来,如果DMA从将数据从外设写到内存,CPUcache中的数据(如果有的话)就是旧数据了,这时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(如mipskseg1)相关,也可能与硬件特性(如是否支持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_DEVICEflush。对应的,在收包后为ring buffer中已被使用的skb数据buffer重新分配内存后,要做一次DMA_FROM_DEVICEflushinvalidate的时候要注意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就会做一次cacheflush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了。


注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设:

从内存到外设: CPU会做cacheflush操作,将cache中新的数据刷到内存。

从外设到内存: CPUcache置无效,这样CPU读的时候不命中,就会从内存去读新的数据


还要注意,这几个接口都是一次性的,每次操作数据都要调用一次mapunmap。并且在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 资源。


阅读(264) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~