4. Gadget驱动中的DMA映射:dma_map_single与dma_map_sg的使用、DMA地址与CPU地址转换、一致性DMA映射
在USB Gadget驱动中,DMA(直接内存访问)是消除数据搬运CPU开销的核心手段。然而,CPU与DMA控制器对内存的访问视角存在根本差异:CPU通过MMU(内存管理单元)使用虚拟地址,而DMA控制器直接操作物理地址。此外,硬件DMA引擎通常要求缓冲区在物理上连续,且对Cache一致性有严格约束。本章将深入剖析Gadget驱动中DMA映射的三种核心操作:流式DMA映射(dma_map_single / dma_map_sg)、地址转换机制以及一致性DMA映射。
4.1 DMA地址与CPU地址的转换模型
理解DMA映射的前提是厘清三种地址空间的关系:CPU虚拟地址(VA)、物理地址(PA)和DMA总线地址(DMA Addr)。在大多数支持IOMMU/SMMU的平台上,DMA地址并非直接等于物理地址,而是经过IOMMU二次映射后的总线地址。
- 驱动持有CPU虚拟地址(如kmalloc或从用户空间传来的指针)。
- 通过virt_to_phys()或dma_map_single()获得物理地址。
- IOMMU将物理地址映射为DMA总线地址,该地址被写入DMA控制器的描述符。
- DMA传输完成后,调用dma_unmap_*()解除映射,使CPU重新获得对缓冲区的安全访问权。
4.2 流式DMA映射:dma_map_single 与 dma_map_sg
流式映射(Streaming DMA Mapping)是最常用的DMA映射方式,适用于一次性的、方向明确的传输(如USB OUT/IN端点传输)。它要求驱动在每次传输前映射缓冲区,传输完成后立即解除映射。
4.2.1 dma_map_single:单缓冲区映射
当传输缓冲区在物理上连续时,使用dma_map_single()。该函数返回一个DMA总线地址,驱动将其填入USB请求(URB)的描述符中。
#include <linux/dma-mapping.h>
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);
| 参数 | 说明 | 典型值 |
|---|---|---|
dev |
DMA设备结构体指针,通常为gadget->dev.parent | gadget->dev.parent |
cpu_addr |
CPU虚拟地址,必须指向kmalloc或dma_alloc_coherent分配的缓冲区 | req->buf |
size |
传输字节数 | req->length |
dir |
数据传输方向 | DMA_TO_DEVICE / DMA_FROM_DEVICE |
4.2.2 dma_map_sg:分散/聚集映射
对于USB批量传输或高吞吐场景,数据可能分布在多个物理不连续的缓冲区中。dma_map_sg()将一组struct scatterlist条目映射为DMA可访问的分散/聚集列表,DMA引擎通过硬件SG能力自动遍历这些条目。
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
映射后,每个SG条目中的dma_address和dma_length字段被填充为DMA总线地址和长度。驱动需遍历SG列表,将每个条目的地址写入DMA描述符链。
4.3 一致性DMA映射
一致性映射(Consistent DMA Mapping)用于CPU和DMA控制器频繁共享数据的场景,如USB控制端点、状态寄存器或描述符环。它保证映射后的缓冲区在CPU和DMA视角下始终一致,无需手动调用Cache刷新函数。
dma_addr_t dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);
| 特性 | 流式映射 (dma_map_single/sg) | 一致性映射 (dma_alloc_coherent) |
|---|---|---|
| 分配时机 | 每次传输前映射,传输后解除 | 驱动初始化时分配,卸载时释放 |
| Cache一致性 | 需手动调用dma_sync_*或依赖映射/解除时的隐式同步 | 硬件保证一致性(通常通过非Cacheable或硬件Cache Coherent) |
| 适用场景 | 大数据块传输(批量/中断端点) | 控制端点、描述符环、状态共享区 |
| 内存类型 | 必须使用kmalloc分配的缓冲区 | 由API自行分配,返回CPU虚拟地址和DMA地址 |
| 性能开销 | 映射/解除有开销,但适合高频传输 | 分配开销大,但访问延迟低,无同步开销 |
- 端点0(控制端点): 使用dma_alloc_coherent分配setup包缓冲区,保证CPU写setup包后DMA立即看到最新数据。
- 批量端点(BULK): 使用dma_map_single映射每个USB请求的缓冲区,传输完成后立即解除映射。
- 等时端点(ISO): 使用dma_map_sg处理分散的音频/视频帧数据,利用硬件SG能力减少中断次数。
4.4 映射方向与Cache同步
流式映射要求驱动明确指定数据传输方向,内核根据方向执行不同的Cache维护操作:
- DMA_TO_DEVICE(CPU写,DMA读):映射时Cache写回(flush),确保DMA看到最新数据。
- DMA_FROM_DEVICE(DMA写,CPU读):映射时Cache失效(invalidate),解除映射时再次失效,防止CPU读到脏Cache。
- DMA_BIDIRECTIONAL:双向传输,映射时写回并失效,开销最大,应尽量避免。
/* 示例:Gadget BULK OUT请求的DMA映射 */
static int gadget_ep_queue(struct usb_ep *ep, struct usb_request *req, gfp_t gfp_flags)
{
struct device *dev = ep->gadget->dev.parent;
dma_addr_t dma_addr;
/* 映射缓冲区到DMA地址 */
dma_addr = dma_map_single(dev, req->buf, req->length, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed\n");
return -ENOMEM;
}
req->dma = dma_addr; /* 将DMA地址存入请求 */
/* 提交到硬件... */
/* 传输完成后在完成回调中解除映射 */
}
static void gadget_ep_complete(struct usb_ep *ep, struct usb_request *req)
{
struct device *dev = ep->gadget->dev.parent;
dma_unmap_single(dev, req->dma, req->length, DMA_FROM_DEVICE);
/* 现在CPU可以安全读取req->buf */
}