大家好,我是小志。我们的口号是:学习、成长、应对未来风险。
什么是 DMA( (Direct Memory Access )?为什么需要它?
想象一下,如果没有 DMA,当你的 BMC 需要从网络接收一个大数据包,或者从 Flash 芯片读取固件镜像时,会发生什么?
CPU 需要像一个勤劳的搬运工,亲自一个字节一个字节地从外设(比如网卡)读取数据,然后写入到内存中。这个过程被称为可编程 I/O (PIO)。对于大数据量传输,这会极大地消耗 CPU 资源,导致系统整体性能下降,CPU 无法专注于更重要的任务,比如监控系统状态、处理 IPMI 命令等。
DMA 就是为了解决这个问题而生的。
DMA 的核心思想是解放 CPU。它通过一个专门的硬件模块——DMA 控制器 (DMAC),让外设能够不经过 CPU,直接与系统内存进行高速数据交换。
一个生动的比喻:
DMA的工作原理
一个典型的 DMA 传输过程可以分为以下几个步骤:
初始化:
CPU 配置 DMA 控制器,告诉它三个关键信息:
启动传输: CPU 发出启动命令,然后就可以“脱身”去处理其他任务了。
数据传输: 外设(如网卡)准备好数据后,向 DMA 控制器发出请求。DMA 控制器接管系统总线,直接在内存和外设之间进行数据搬运。
完成通知: 当所有数据搬运完毕后,DMA 控制器会向 CPU 发送一个中断信号,通知它传输已完成。
DMA 缓存一致性
在配备缓存的CPU上面,最近访问的内存区域的副本被缓存,甚至为DMA映射的内存区域也会被缓存。现实情况是两个独立设备之间的共享内存通常是产生缓存一致性问题的根源,缓存不一致源于其他设备可能不知道另一个设备的更新写入。另外,缓存一致性确保每个写操作似乎是即时发生的,这意味着共享同一内存区域的所有设备将看到完全相同的更改序列。
假设一个CPU配备了缓存以及一个可以通过DMA直接访问设备的外存,当CPU访问内存位置X时,当前值会存储在缓存中,对X的后续操作将更新X的缓存副本,但不会更新X的外存版本(假设是写回内存),如果在下一次设备试图访问X之前没有将缓存刷新到内存,则设备将收到X的旧值。同样,如果在设备写入新值到内存时,X的缓存副本没有被无效化,CPU将操作X的旧值。
这个问题有两种解决方案。
🟠 硬件解决方案。这些系统是一致性系统。
🟠 软件解决方案,其中操作系统负责确保缓存一致性。这些系统是非一致性系统。
Linux 内核中的 DMA 框架
Linux 内核为 DMA 操作提供了一套完善的子系统,以屏蔽不同硬件的差异,方便驱动开发者使用。其中两个最核心的概念是 DMA Mapping 和 DMA Engine。
为DMA目的分配的内存缓冲区必须相应地被映射。DMA映射包括为DMA分配内存缓冲区,并为此缓冲区生成总线地址。为DMA分配内存缓冲区不是简单用kmalloc分配普通内存,而是分配满足 DMA 控制器苛刻要求的内存 —— 普通内存可能存在 “物理地址离散、缓存干扰、DMA 不可访问” 等问题,必须专门分配。DMA 控制器是独立于 CPU 的硬件,它访问内存的规则和 CPU 完全不同:
🟠 多数 DMA 控制器只认物理连续的地址(无法像 CPU 一样通过 MMU 访问离散物理页);
🟠 DMA 访问内存时绕开 CPU 缓存(直接读物理内存),缓冲区需禁用缓存或保证缓存一致性;
🟠 部分架构中,DMA 只能访问特定范围的物理地址(比如 ARM 的低端内存区)。
总线地址≠CPU物理地址,DMA控制器访问内存时用的地址由总线桥(如PCIE ,AXI)转换而来。
CPU 通过「内存总线」直接访问内存,用的是 “物理地址”;DMA 控制器通常挂在「PCIe/AXI/AMBA 总线」上,通过「总线桥」中转访问内存 —— 总线桥会对地址做 “重映射”(比如 CPU 物理地址 0x40000000,经过 PCIe 桥后,DMA 看到的总线地址是 0x80000000)。
1. DMA Mapping (DMA 映射)
它的主要任务是解决 CPU 和外设“看到的”内存地址不一致的问题。
DMA Mapping API 的作用就是为驱动分配或映射一块内存,并返回一个外设能够直接访问的地址(DMA 地址)。它主要分为两种类型:
2. DMA Engine (DMA 引擎)
这是一个更高层的框架,位于 drivers/dma/ 目录下。它提供了一套统一的 API,让驱动开发者可以方便地请求和使用 DMA 通道进行数据搬运,而无需深入了解底层 DMA 控制器的寄存器细节。
典型应用场景
网络通信 (Ethernet MAC):
这是 DMA 最经典的应用。当 OpenBMC 作为服务器管理控制器时,需要处理大量的网络请求(如 Redfish, IPMI over LAN)。
网卡(MAC)控制器会利用 DMA,将接收到的网络数据包直接写入到由驱动程序通过 dma_map_single 或 dma_alloc_coherent 准备的内存缓冲区中,然后才通过中断通知 CPU 进行处理。发送数据包的过程则相反。
Flash 存储读写 (FMC/QSPI Controller):
传感器数据采集 (ADC / I2C / SPI):
视频捕获 (LPC/eSPI 总线):
基础篇:DMA Mapping (内存管理)
在任何 DMA 传输发生之前,驱动程序必须分配一块内存,并将其映射为设备可见的物理地址(DMA 地址)。
场景:为 SPI Flash 驱动分配一致性缓冲区
在 OpenBMC 中,SPI 控制器通常需要一块固定的内存区域来存放命令和状态描述符。
#include<linux/dma-mapping.h>#include<linux/device.h>struct my_spi_driver_data { void *cpu_addr; // CPU 看到的虚拟地址 dma_addr_t dma_addr; // 设备(DMA)看到的物理地址 size_t size;};intmy_spi_probe(struct platform_device *pdev){ struct device *dev = &pdev->dev; struct my_spi_driver_data *ddata; ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); if (!ddata) return -ENOMEM; ddata->size = 4096; // 4KB 缓冲区 // 【核心代码】分配一致性内存 // 1. 分配内存 // 2. 确保 CPU 和设备看到的是一致的 (不需要手动 flush cache) ddata->cpu_addr = dma_alloc_coherent(dev, ddata->size, &ddata->dma_addr, GFP_KERNEL); if (!ddata->cpu_addr) { dev_err(dev, "DMA memory allocation failed\n"); return -ENOMEM; } dev_info(dev, "Allocated DMA mem: CPU Addr=0x%p, DMA Addr=0x%pad\n", ddata->cpu_addr, &ddata->dma_addr); // 将 dma_addr 写入硬件寄存器,告诉 DMA 控制器去哪里搬运数据 // writel(ddata->dma_addr, base_addr + DMA_START_ADDR_REG); return 0;}// 释放资源voidmy_spi_remove(struct platform_device *pdev){ struct my_spi_driver_data *ddata = platform_get_drvdata(pdev); // 必须配对释放 dma_free_coherent(&pdev->dev, ddata->size, ddata->cpu_addr, ddata->dma_addr);}
场景:网络包或大数据的流式映射 (Streaming)
当处理大数据块(如一次读取 64KB 的固件日志)且内存已经分配好时,使用流式映射更高效。
#include<linux/dma-mapping.h>inttransfer_large_data(struct device *dev, void *buffer, size_t len, int direction){ dma_addr_t dma_handle; int ret; // 【核心代码】映射现有的内存 // direction 可以是 DMA_TO_DEVICE (写Flash) 或 DMA_FROM_DEVICE (读Flash) dma_handle = dma_map_single(dev, buffer, len, direction); if (dma_mapping_error(dev, dma_handle)) { dev_err(dev, "Failed to map DMA memory\n"); return -EIO; } // 1. 启动硬件传输 (使用 dma_handle) // start_hardware_transfer(dma_handle, len); // 2. 等待传输完成 (这里简化为同步等待,实际通常用中断) // wait_for_completion(); // 【核心代码】手动同步缓存 (仅针对流式映射) // 如果是设备写入内存,CPU 读取前必须失效(invalidate)缓存,确保读到新数据 if (direction == DMA_FROM_DEVICE) { dma_sync_single_for_cpu(dev, dma_handle, len, DMA_FROM_DEVICE); } // 处理数据... // 【核心代码】解除映射 dma_unmap_single(dev, dma_handle, len, direction); return 0;}
进阶篇:DMA Engine (提交传输任务)
这是现代 Linux 驱动(包括 OpenBMC 中的 aspeed 系列驱动)最常用的方式。它让驱动无需操作具体的 DMA 寄存器,而是通过通用 API 提交任务。
场景:使用 DMA Engine 进行 I2C/SPI 数据搬运
假设我们要通过 DMA 将一块数据从内存搬运到外设(TX 传输)。
#include<linux/dmaengine.h>#include<linux/dma-mapping.h>struct my_dma_client { struct dma_chan *chan; // DMA 通道 struct device *dev; struct completion cmp; // 用于等待传输完成};// DMA 传输完成后的回调函数 (在中断上下文中执行)voiddma_transfer_complete(void *param){ struct my_dma_client *client = param; complete(&client->cmp); // 唤醒等待的进程 dev_info(client->dev, "DMA Transfer Completed!\n");}intstart_dma_tx(struct my_dma_client *client, void *tx_buf, size_t len){ struct dma_device *dma_dev = client->chan->device; struct dma_async_tx_descriptor *tx; dma_cookie_t cookie; dma_addr_t dma_src; int ret; // 1. 映射源内存 (流式映射) dma_src = dma_map_single(client->dev, tx_buf, len, DMA_TO_DEVICE); if (dma_mapping_error(client->dev, dma_src)) return -EINVAL; // 2. 准备 DMA 传输请求 (Slave SG 模式) // 这里假设是内存 -> 外设 (Slave) tx = dmaengine_prep_slave_single(client->chan, dma_src, len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK); if (!tx) { dev_err(client->dev, "Failed to prepare DMA TX\n"); ret = -EIO; goto err_unmap; } // 3. 设置回调函数 tx->callback = dma_transfer_complete; tx->callback_param = client; // 4. 提交事务 cookie = dmaengine_submit(tx); if (dma_submit_error(cookie)) { dev_err(client->dev, "Failed to submit DMA TX\n"); ret = -EIO; goto err_unmap; } // 5. 启动 DMA 引擎 (Issue pending requests) dma_async_issue_pending(client->chan); // 6. 等待完成 (实际驱动中可能使用异步等待或超时等待) wait_for_completion(&client->cmp); // 7. 检查状态 ret = dma_async_is_tx_complete(client->chan, cookie, NULL, NULL); if (ret == DMA_COMPLETE) { dev_info(client->dev, "DMA Success\n"); } // 8. 清理 dma_unmap_single(client->dev, dma_src, len, DMA_TO_DEVICE); return 0;err_unmap: dma_unmap_single(client->dev, dma_src, len, DMA_TO_DEVICE); return ret;}
场景:分散-聚集 (Scatter-Gather) 传输
在 OpenBMC 的网络驱动或高性能存储驱动中,数据往往不是连续的。DMA Engine 支持一次性搬运多个不连续的物理页。
// 假设我们有一个 scatterlist 数组,指向多个物理上不连续的内存页intstart_sg_dma(struct my_dma_client *client, struct scatterlist *sg, int nents, int direction){ struct dma_async_tx_descriptor *tx; dma_cookie_t cookie; // 1. 映射 SG 列表 int mapped_nents = dma_map_sg(client->dev, sg, nents, direction); if (mapped_nents == 0) return -ENOMEM; // 2. 准备 SG 传输 // DMA Engine 会自动处理链式传输 tx = dmaengine_prep_slave_sg(client->chan, sg, mapped_nents, direction, DMA_PREP_INTERRUPT); if (!tx) { dma_unmap_sg(client->dev, sg, nents, direction); return -EIO; } // 3. 提交并启动 tx->callback = dma_transfer_complete; tx->callback_param = client; cookie = dmaengine_submit(tx); if (dma_submit_error(cookie)) { dma_unmap_sg(client->dev, sg, nents, direction); return -EIO; } dma_async_issue_pending(client->chan); return 0;}
调试技巧
如果你发现 DMA 不工作,可以按照以下思路排查:
检查 IOMMU: 虽然嵌入式 通常不使用复杂的 IOMMU,但如果开启了,必须确保 dma_map 成功。
缓存一致性:
这是最常见的 Bug 来源。
地址转换: 打印 virt_to_phys() 和 dma_map_single() 返回的地址。在没有 IOMMU 的简单系统中,它们通常是一样的;在有 IOMMU 的系统中,它们是不同的。
设备树: 确保 dmas 属性正确指向了系统中存在的 DMA 控制器节点。