DMA内存申请--dma_alloc_coherent 及 寄存器与内存

在项目驱动过程中会经常用到dma传输数据,而dma需要的内存有自己的特点,一般认为需要物理地址连续,并且内存是不可cache的,在linux内核中提供一个供dma所需内存的申请函数dma_alloc_coherent. 如下所述:
dma_alloc_coherent()
 

dma_alloc_coherent() -- 获取物理页,并将该物理页的总线地址保存于dma_handle,返回该物理页的虚拟地址

DMA映射建立了一个新的结构类型---------dma_addr_t来表示总线地址。dma_addr_t类型的变量对驱动程序是不透明的;唯一允许的操作是将它们传递给DMA支持例程以及设备本身。作为一个总线地址,如果CPU直接使用了dma_addr_t,将会导致发生不可预期的后果!

一致性DMA映射存在与驱动程序的生命周期中,它的缓冲区必须可同时被CPU和外围设备访问!因此一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的!

                                                                              (以上红色文字摘录在LDD3)

void *
dma_alloc_coherent(struct device *dev,        size_t size,
                   dma_addr_t    *dma_handle, gfp_t gfp)
{
    void *ret;
    if (!dev || *dev->dma_mask >= 0xffffffffUL)
        gfp &= ~GFP_DMA;
    ret = (void *)__get_free_pages(gfp, get_order(size)); //(1)
    if (ret) {
        memset(ret, 0, size);
        *dma_handle = virt_to_bus(ret);                   //(2)
    }
    return ret;
}

(1) 将size转换成order, 即2^order
(2) 将虚拟地址ret转换成总线地址

这个函数是一个平台相关的函数,以上是在x86平台的实现细节,从这里我们可以看到该函数返回值为linux 内核线性地址,所以对于驱动开发过程的mmap函数实现提供了便利。
但是在powerpc平台却不是这样,笔者就曾经遇到在将pci驱动从x86平台移植到powerpc平台时出现问题。
首先我们来先看一下两个平台对于dma内存的处理。
x86:
     linux内存区域分为DMA区域,Normal内存区域与高端内存区域,高端内存区域为当物理内存高于768M时使用,一般DMA区域为16M,这段空间由操作系统预留。DMA区域与Normal区域全部使用线性映射,采用逻辑地址使用,高端内存使用内核虚拟地址。其中内核空间的分部为:
物理区--8M隔离--vmalloc区--8k隔离--4M的高端映射区--固定映射区--128k

powerpc:
    本节采用freescale的mpc5121芯片为例,内核没有采用Normal内存区域,只使用ZONE_DMA和ZONE_HIMEM两种类型的空间,其中ZONE_DMA存放低端内存, ZONE_HIMEN存放高端内存,整个内存不在采用逻辑地址这一概念。所以基于逻辑地址的操作没有可移植性。

下面看下具体的区别:
void *  __dma_alloc_coherent(size_t size, dma_addr_t *handle, gfp_t gfp)
{
          //物理空间页的申请
          page = alloc_pages(gfp, order);
         
         //对物理空间进行清零cache
         { 
                       unsigned long kaddr = (unsigned long)page_address(page);                 
                       memset(page_address(page), 0, size);
                       flush_dcache_range(kaddr, kaddr + size);
          }

         //申请虚拟空间
         c = vm_region_alloc(&consistent_head, size,   gfp &  
                                     ~(__GFP_DMA | __GFP_HIGHMEM));

       //实现虚拟地址与物理地址映射
       if (c) {
                 unsigned long vaddr = c->vm_start;
                 pte_t *pte = consistent_pte + CONSISTENT_OFFSET(vaddr);
                 struct page *end = page + (1 << order);

                 split_page(page, order);

                 /*
                  * Set the \"dma handle\"
                  */
                 *handle = page_to_bus(page);

                do {
                         BUG_ON(!pte_none(*pte));

                         SetPageReserved(page);
                         set_pte_at(&init_mm, vaddr,
                                   pte, mk_pte(page, pgprot_noncached(PAGE_KERNEL)));
                         page++;
                         pte++;
                         vaddr += PAGE_SIZE;
                 } while (size -= PAGE_SIZE);

     // 返回值为内核虚拟地址。

      return (void *)c->vm_start

 

 

1. I/O 端口和 I/O 内存

每个外设都是通过读写它的寄存器来控制. 大部分时间一个设备有几个寄存器, 并且在连续地址存取它们, 或者在内存地址空间或者在 I/O 地址空间.

在硬件级别上, 内存区和 I/O 区域没有概念上的区别: 它们都是通过在地址总线和控制总线上发出电信号来存取(即, 读写信号)[32]并且读自或者写到数据总线.

但是一些 CPU 制造商在他们的芯片上实现了一个单个地址空间, 有人认为外设不同于内存, 因此, 应该有一个分开的地址空间. 一些处理器(最有名的是 x86 家族)有分开的读和写电线给 I/O 端口和特殊的 CPU 指令来存取端口.

因为外设被建立来适合一个外设总线, 并且大部分流行的 I/O 总线成型在个人计算机上, 即便那些没有单独地址空间给 I/O 端口的处理器, 也必须在存取一些特殊设备时伪装读写端口, 常常利用外部的芯片组或者 CPU 核的额外电路. 后一个方法在用在嵌入式应用的小处理器中常见.

由于同样的理由, Linux 在所有它运行的计算机平台上实现了 I/O 端口的概念, 甚至在那些 CPU 实现一个单个地址空间的平台上. 端口存取的实现有时依赖特殊的主机制造和型号( 因为不同的型号使用不同的芯片组来映射总线传送到内存地址空间).

即便外设总线有一个单独的地址空间给 I/O 端口, 不是所有的设备映射它们的寄存器到 I/O 端口. 虽然对于 ISA 外设板使用 I/O 端口是普遍的, 大部分 PCI 设备映射寄存器到一个内存地址区. 这种 I/O 内存方法通常是首选的, 因为它不需要使用特殊目的处理器指令; CPU 核存取内存更加有效, 并且编译器当存取内存时有更多自由在寄存器分配和寻址模式的选择上.

1.1. I/O 寄存器和常规内存

不管硬件寄存器和内存之间的强相似性, 存取 I/O 寄存器的程序员必须小心避免被 CPU(或者编译器)优化所戏弄, 它可能修改希望的 I/O 行为.

I/O 寄存器和 RAM 的主要不同是 I/O 操作有边际效果, 而内存操作没有: 一个内存写的唯一效果是存储一个值到一个位置, 并且一个内存读返回最近写到那里的值. 因为内存存取速度对 CPU 性能是至关重要的, 这种无边际效果的情况已被多种方式优化: 值被缓存, 并且 读/写指令被重编排.

编译器能够缓存数据值到 CPU 寄存器而不写到内存, 并且即便它存储它们, 读和写操作都能够在缓冲内存中进行而不接触物理 RAM. 重编排也可能在编译器级别和在硬件级别都发生: 常常一个指令序列能够执行得更快, 如果它以不同于在程序文本中出现的顺序来执行, 例如, 为避免在 RISC 流水线中的互锁. 在CISC 处理器, 要花费相当数量时间的操作能够和其他的并发执行, 更快的.

当应用于传统内存时(至少在单处理器系统)这些优化是透明和有益的, 但是它们可能对正确的 I/O 操作是致命的, 因为它们干扰了那些"边际效果", 这是主要的原因为什么一个驱动存取 I/O 寄存器. 处理器无法预见这种情形, 一些其他的操作(在一个独立处理器上运行, 或者发生在一个 I/O 控制器的事情)依赖内存存取的顺序. 编译器或者 CPU 可能只尽力胜过你并且重编排你请求的操作; 结果可能是奇怪的错误而非常难于调试. 因此, 一个驱动必须确保没有进行缓冲并且在存取寄存器时没有发生读或写的重编排.

硬件缓冲的问题是最易面对的:底层的硬件已经配置(或者自动地或者通过 Linux 初始化代码)成禁止任何硬件缓冲, 当存取 I/O 区时(不管它们是内存还是端口区域).

对编译器优化和硬件重编排的解决方法是安放一个内存屏障在必须以一个特殊顺序对硬件(或者另一个处理器)可见的操作之间. Linux 提供 4 个宏来应对可能的排序需要:

#include <linux/kernel.h>

void barrier(void)

这个函数告知编译器插入一个内存屏障但是对硬件没有影响. 编译的代码将所有的当前改变的并且驻留在 CPU 寄存器的值存储到内存, 并且后来重新读取它们当需要时. 对屏障的调用阻止编译器跨越屏障的优化, 而留给硬件自由做它的重编排.

#include <asm/system.h>

void rmb(void);

void read_barrier_depends(void);

void wmb(void);

void mb(void);

这些函数插入硬件内存屏障在编译的指令流中; 它们的实际实例是平台相关的. 一个 rmb ( read memory barrier) 保证任何出现于屏障前的读在执行任何后续读之前完成. wmb 保证写操作中的顺序, 并且 mb 指令都保证. 每个这些指令是一个屏障的超集.

read_barrier_depends 是读屏障的一个特殊的, 弱些的形式. 而 rmb 阻止所有跨越屏障的读的重编排, read_barrier_depends 只阻止依赖来自其他读的数据的读的重编排. 区别是微小的, 并且它不在所有体系中存在. 除非你确切地理解做什么, 并且你有理由相信, 一个完整的读屏障确实是一个过度地性能开销, 你可能应当坚持使用 rmb.

 

  1. #define isb() __asm__ __volatile__ ("isb" : : : "memory")

  2. #define dsb() __asm__ __volatile__ ("dsb" : : : "memory")

  3. #define dmb() __asm__ __volatile__ ("dsb" : : : "memory")

  4. #define mb() do { dsb(); outer_sync(); } while (0)

  5. #define rmb() dsb()

  6. #define wmb() mb()

  7. #define smp_mb() dmb()

  8. #define smp_rmb() dmb()

  9. #define smp_wmb() dmb()

表4.27 隔离指令

指令名

功能描述

DMB

数据存储器隔离。DMB 指令保证: 仅当所有在它前面的存储器访问操作

都执行完毕后,才提交(commit)在它后面的存储器访问操作。

DSB

数据同步隔离。比 DMB 严格: 仅当所有在它前面的存储器访问操作

都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访 问操作——译者注)

ISB

指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执

行完毕之后,才执行它后面的指令。

 

 

void smp_rmb(void);

void smp_read_barrier_depends(void);

void smp_wmb(void);

void smp_mb(void);

屏障的这些版本仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用.

在一个设备驱动中一个典型的内存屏障的用法可能有这样的形式:

writel(dev->registers.addr, io_destination_address);
writel(dev->registers.size, io_size);
writel(dev->registers.operation, DEV_READ);
wmb();
writel(dev->registers.control, DEV_GO);

在这种情况, 是重要的, 确保所有的控制一个特殊操作的设备寄存器在告诉它开始前已被正确设置. 内存屏障强制写以需要的顺序完成.

因为内存屏障影响性能, 它们应当只用在确实需要它们的地方. 屏障的不同类型也有不同的性能特性, 因此值得使用最特定的可能类型. 例如, 在 x86 体系上, wmb() 目前什么都不做, 因为写到处理器外不被重编排. 但是, 读被重编排, 因此 mb() 被 wmb() 慢.

值得注意大部分的其他的处理同步的内核原语, 例如自旋锁和原子的 _t 操作, 如同内存屏障一样是函数. 还值得注意的是一些外设总线(例如 PCI 总线)有它们自己的缓冲问题; 我们在以后章节遇到时讨论它们.

一些体系允许一个赋值和一个内存屏障的有效组合. 内核提供了几个宏来完成这个组合; 在缺省情况下, 它们如下定义:

#define set_mb(var, value) do {var = value; mb();}  while 0
#define set_wmb(var, value) do {var = value; wmb();} while 0
#define set_rmb(var, value) do {var = value; rmb();} while 0

在合适的地方, <asm/system.h> 定义这些宏来使用体系特定的指令来很快完成任务. 注意 set_rmb 只在少量体系上定义. (一个 do...while 结构的使用是一个标准 C 用语, 来使被扩展的宏作为一个正常的 C 语句可在所有上下文中工作).

static int anlogic_axi_ethlite_send_data_dma(struct net_local *drvdata, u8 *data, unsigned int byte_count) { void __iomem *addr; int buffer_index = drvdata->next_tx_buf_to_use / BUFFER_SIZE; // 如果数据长度过大,进行截断 if (byte_count > ETH_FRAME_LEN) byte_count = ETH_FRAME_LEN; // 检查发送缓冲区是否空闲 if ((anlogic_axi_ethlite_readl(drvdata->base_addr + ELITE_ETXSTAT0_OFFSET) & BIT(ETXSTAT0_BUFSTAT_SHIFT)) == 0) { drvdata->next_tx_buf_to_use = ELITE_TXPINGBUF_OFFSET; } else if ((anlogic_axi_ethlite_readl(drvdata->base_addr + ELITE_ETXSTAT1_OFFSET) & BIT(ETXSTAT1_BUFSTAT_SHIFT)) == 0) { drvdata->next_tx_buf_to_use = ELITE_TXPONGBUF_OFFSET; } else { return -1; // 缓冲区已满,返回失败 } addr = drvdata->base_addr + drvdata->next_tx_buf_to_use; // 将数据复制到DMA缓冲区 memcpy(drvdata->tx_buffer[buffer_index], data, byte_count); // 同步DMA缓冲区到设备 dma_sync_single_for_device(&drvdata->ndev->dev, drvdata->tx_dma_handle[buffer_index], byte_count, DMA_TO_DEVICE); // 配置DMA传输 struct dma_async_tx_descriptor *desc = dmaengine_prep_slave_single(drvdata->dma_chan, drvdata->tx_dma_handle[buffer_index], byte_count, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); if (!desc) { dev_err(&drvdata->ndev->dev, "Failed to prepare DMA descriptor\n"); return -EINVAL; } desc->callback = dma_complete_callback; desc->callback_param = drvdata; // 提交DMA传输 dmaengine_submit(desc); dma_async_issue_pending(drvdata->dma_chan); // 设置发送请求标志 if (drvdata->next_tx_buf_to_use == ELITE_TXPINGBUF_OFFSET) { anlogic_axi_ethlite_writel(BIT(ETXSTAT0_SENDREQ_SHIFT) | (byte_count & 0xfff), drvdata->base_addr + ELITE_ETXSTAT0_OFFSE
03-15
<think>好的,我现在需要帮助用户了解如何实现基于DMA的以太网数据发送函数,具体涉及Anlogic FPGA的AXI Ethernet Lite控制器中的DMA配置、缓冲区管理和中断处理。首先,我得回忆一下DMA的基本原理和以太网控制器的相关特性。 根据用户提供的引用内容,特别是引用[1],里面提到UART控制器的FIFO结构和中断处理方式。虽然这是UART的,但DMA在以太网中的使用可能有类似之处,比如使用FIFO缓冲数据,中断状态位的处理方式。不过以太网通常涉及更复杂的数据帧结构和更高的速率,需要更详细的配置。 接下来,用户提到了Anlogic的AXI Ethernet Lite控制器,所以需要查阅相关文档,了解其寄存器配置和DMA接口。不过用户提供的引用中没有直接关于以太网控制器的信息,可能需要结合通用的DMA实现方法。 首先,缓冲区管理是关键。发送数据时需要确保数据在内存中的连续性,可能使用环形缓冲区或链表来管理多个数据包。需要分配物理上连续的内存块,因为DMA通常直接访问物理地址。引用[3]提到EG4S20开发板有64个9K BRAM,可能可以用来作为缓冲区,或者使用外部SDRAM,但用户的问题是关于DMA的,所以可能需要配置DMA描述符,每个描述符指向一个数据缓冲区。 然后是DMA配置。需要设置源地址(数据缓冲区地址)、目标地址(以太网控制器的TX FIFO地址),数据长度,传输模式(如突发传输),可能还需要设置描述符链,使DMA能够自动处理多个数据包。引用[1]中的UART控制器有64字节的FIFO,但以太网的FIFO可能更大,需要确认具体大小。 中断处理部分,当DMA传输完成或发生错误时,需要触发中断。根据引用[1]中的中断状态位管理,可能需要在中断服务程序中检查状态寄存器,清除中断标志,处理完成的事务或错误情况。比如,当DMA完成发送后,释放缓冲区资源,或者重新配置DMA进行下一次传输。 此外,需要确保数据对齐和内存屏障,防止DMA访问错误。可能还需要考虑缓存一致性问题,如果CPU有缓存,需要将缓冲区设置为非缓存或使用缓存刷新操作。 用户提到的函数anlogic_axi_ethlite_send_data_dma的具体实现可能需要以下步骤: 1. 准备发送的数据缓冲区,确保其物理地址被DMA识别。 2. 配置DMA描述符,包括数据地址、长度、下一个描述符地址等。 3. 配置DMA控制器,设置传输方向(内存到外设)、传输模式等。 4. 启动DMA传输,并启用相关中断。 5. 在中断处理函数中,检查传输状态,处理完成或错误情况。 可能遇到的问题包括DMA描述符的配置错误、缓冲区未正确对齐、中断未正确启用或清除等。需要参考具体的Anlogic FPGA文档,确认寄存器的位定义和操作流程。 最后,需要结合用户提供的引用内容,尤其是引用[2]中的FPGA引脚定义路径,可能需要在工程中正确配置以太网相关引脚,确保物理连接正确。引用[3]提到的开发板资源,如BRAM和SDRAM,可能用于缓冲区的分配和管理。</think>### 基于Anlogic AXI Ethernet Lite的DMA数据发送实现 #### 1. 缓冲区管理 - **物理连续内存分配** 使用`dma_alloc_coherent()`或专用BRAM分配物理地址连续的缓冲区,确保DMA能直接访问。例如: ```c void *tx_buffer = dma_alloc_coherent(dev, buffer_size, &dma_handle, GFP_KERNEL); ``` - **环形队列管理** 通过描述符链表实现多包缓存,每个描述符包含数据地址、长度和状态标志[^1]。例如: ```c struct dma_desc { dma_addr_t next_desc; // 下一描述符物理地址 dma_addr_t data_addr; // 数据缓冲区物理地址 u32 length; // 数据长度 u32 status; // 状态位(如OWN位) }; ``` #### 2. DMA配置 - **寄存器配置步骤** 1. 关闭DMA通道:`ETH_DMA_TX_CTRL &= ~ENABLE` 2. 设置描述符基地址:`ETH_DMA_TX_DESC = desc_phys_addr` 3. 配置传输模式(如突发长度、优先级):`ETH_DMA_TX_CTRL |= BURST_16 | PRIO_HIGH` 4. 启用DMA通道:`ETH_DMA_TX_CTRL |= ENABLE` - **关键寄存器示例** ```c // Anlogic AXI Ethernet Lite寄存器定义(参考类似IP) #define ETH_DMA_TX_DESC 0x1000 // TX描述符基地址寄存器 #define ETH_DMA_TX_CTRL 0x1004 // 控制寄存器 #define ETH_DMA_TX_STATUS 0x1008 // 状态寄存器 ``` #### 3. 中断处理 - **中断使能配置** 启用传输完成中断和错误中断: ```c ETH_DMA_INT_ENABLE |= TX_COMPLETE_INT | TX_ERROR_INT; ``` - **中断服务程序(ISR)** ```c irqreturn_t eth_dma_isr(int irq, void *dev_id) { u32 status = ETH_DMA_TX_STATUS; if (status & TX_COMPLETE) { // 释放已完成描述符的缓冲区 while (desc->status & OWN_BIT) { desc->status &= ~OWN_BIT; desc = phys_to_virt(desc->next_desc); } ETH_DMA_TX_STATUS = TX_COMPLETE; // 清除中断标志 } if (status & TX_ERROR) { // 错误恢复(如重置DMA通道) ETH_DMA_TX_CTRL |= RESET; ETH_DMA_TX_CTRL &= ~RESET; } return IRQ_HANDLED; } ``` #### 4. 数据发送流程 ```c void anlogic_axi_ethlite_send_data_dma(void *data, int len) { // 1. 获取空闲描述符 struct dma_desc *desc = get_free_tx_desc(); // 2. 填充数据到DMA缓冲区 memcpy(desc->data_virt, data, len); // 3. 更新描述符字段 desc->length = len; desc->status |= OWN_BIT; // 将控制权交给DMA // 4. 触发DMA传输 ETH_DMA_TX_POLL_DEMAND = 1; // 启动传输 } ``` #### 5. 关键优化点 - **零拷贝技术**:直接使用应用层数据物理地址,避免内存复制 - **描述符预分配**:初始化时预先生成所有描述符形成环形链 - **中断合并**:使用NAPI机制减少中断频率[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值