DMA是什么以及DMA的用法

DMA(Direct Memory Access,直接内存访问)是计算机体系中一种核心的硬件加速技术,其核心作用是在不依赖 CPU 干预的情况下,实现外设(如硬盘、网卡、显卡)与内存之间直接的数据传输,从而解放 CPU 资源,提升系统整体数据处理效率。在高性能计算、实时系统、嵌入式设备等场景中,DMA 是实现高吞吐量、低延迟数据传输的关键技术。

一、DMA 核心概念与工作原理

要理解 DMA,首先需要对比传统数据传输与 DMA 传输的差异,明确其技术价值。

1. 传统数据传输 vs DMA 传输

在没有 DMA 的场景中,外设与内存的数据传输必须经过 CPU 中转,流程如下:

  1. 外设向 CPU 发送“数据就绪”中断请求;
  2. CPU 暂停当前任务,保存上下文(如寄存器值);
  3. CPU 从外设寄存器读取数据,写入内存指定地址;
  4. 重复步骤 3 直到传输完成,CPU 恢复上下文,继续执行原任务。

这种方式的问题在于:CPU 被“绑定”为数据搬运工,无法执行其他计算任务,尤其在传输大量数据(如视频流、文件拷贝)时,会导致 CPU 占用率飙升,系统响应变慢。

而 DMA 传输通过独立的硬件控制器(DMA 控制器)绕过 CPU,直接完成数据搬运,流程如下:

  1. 初始化阶段:CPU 向 DMA 控制器下发“传输指令”,包含 4 个关键参数: 源地址:数据的起始位置(如外设缓冲区地址);目标地址:数据的写入位置(如内存地址);传输长度:需要传输的字节数;传输方向:外设→内存(如读硬盘)或内存→外设(如写网卡)。
  2. 传输阶段:DMA 控制器向系统总线(地址总线、数据总线)发出请求,获得总线控制权后,直接在源地址与目标地址之间传输数据,全程无需 CPU 参与;
  3. 完成阶段:数据传输结束后,DMA 控制器向 CPU 发送“传输完成”中断;
  4. CPU 响应:CPU 仅需处理中断(如标记数据可用),无需参与数据搬运,可继续执行其他任务。

2. DMA 控制器的核心组件

DMA 功能由硬件层面的 DMA 控制器(DMAC) 实现,其核心组件包括:

  • 通道寄存器组:每个 DMA 通道对应一组寄存器,存储该通道的源地址、目标地址、传输长度、传输模式(如单次传输/块传输);
  • 优先级仲裁器:当多个外设同时请求 DMA 时,按预设优先级(如固定优先级、轮询优先级)分配总线资源,避免冲突;
  • 中断控制器:传输完成/出错时,向 CPU 发送中断信号,触发后续处理(如数据校验、任务切换);
  • 总线接口单元:与系统总线(如 PCIe、AHB、AXI)对接,实现地址解码、数据读写等总线操作。

3. DMA 的关键特性

  • 无 CPU 干预:仅在初始化和传输完成阶段需要 CPU 参与,核心传输过程由硬件独立完成,降低 CPU 负载;
  • 高传输效率:避免 CPU 上下文切换和指令执行的开销,尤其适合 GB 级、TB 级大吞吐量数据传输(如 SSD 读写、网络数据包转发);
  • 多通道并行:主流 DMA 控制器支持多个独立通道(如 8 通道、16 通道),可同时为多个外设(如网卡+硬盘+显卡)提供传输服务;
  • 灵活传输模式:支持多种传输方式,满足不同场景需求: 块传输(Block Transfer):一次性传输固定长度的数据(如 4KB、1MB),传输期间独占总线,适合连续数据;分散-聚集传输(Scatter-Gather):将内存中多个不连续的缓冲区地址整合为“描述符表”,DMA 按表依次传输,无需 CPU 拆分数据,常用于网络数据包(如 TCP 分段)、文件碎片读写;循环传输(Circular Transfer):传输完成后自动重置地址,重复传输(如音频/视频流播放),避免频繁初始化;握手传输(Handshake Transfer):与外设通过握手信号(如就绪信号、应答信号)同步,确保数据传输的正确性(如低速外设 UART、I2C)。

二、DMA 的应用场景

DMA 广泛存在于计算机、嵌入式设备、服务器等各类硬件中,核心场景均围绕“高吞吐量、低 CPU 占用”的需求展开:

1. 存储设备(硬盘、SSD、U盘)

  • 场景:文件拷贝、系统启动、数据库读写等;
  • 作用:SSD 与内存之间的读写速度可达 GB/s 级,若依赖 CPU 中转,会导致 CPU 占用率接近 100%(尤其大文件拷贝);通过 DMA,CPU 仅需下发“读/写指令”,数据传输由 SSD 控制器与 DMA 直接完成,CPU 可同时处理其他任务(如浏览器浏览、文档编辑)。
  • 典型协议:SATA、NVMe(基于 PCIe)协议中均内置 DMA 功能,NVMe SSD 甚至支持多队列 DMA(Multi-Queue DMA),进一步提升并行性。

2. 网络设备(网卡、无线模块)

  • 场景:网络数据包接收/发送(如下载文件、视频通话、服务器转发);
  • 作用:千兆网卡每秒需处理数百万个数据包(每个数据包约 1.5KB),若 CPU 逐个接收,会因“中断风暴”(频繁处理数据包中断)导致性能瓶颈;通过 DMA 的“分散-聚集传输”,网卡可直接将多个数据包写入内存的不连续缓冲区,CPU 仅需批量处理已传输完成的数据包,大幅提升网络吞吐量(如服务器可同时处理万级并发连接)。
  • 典型技术:网卡的 DMA 功能常与“中断 coalescing(中断聚合)”结合,减少中断次数(如每接收 100 个数据包触发一次中断),进一步降低 CPU 负载。

3. 多媒体设备(显卡、声卡、摄像头)

  • 显卡(GPU):GPU 与内存(显存)之间的数据传输(如 3D 模型加载、视频渲染)依赖 DMA,部分高端显卡支持“PCIe DMA 直通”,可直接访问系统内存,避免数据二次拷贝;
  • 声卡:音频流(如音乐播放)需持续传输到扬声器,通过 DMA 循环传输模式,可实现“无卡顿”播放,CPU 无需反复写入音频数据;
  • 摄像头:实时视频采集(如直播、监控)需将摄像头的图像数据(如 YUV 格式)快速写入内存,DMA 可避免 CPU 因“逐帧处理”导致的延迟。

4. 嵌入式与实时系统(MCU、工业控制)

  • 场景:传感器数据采集(如温度、加速度)、电机控制、工业总线(如 CAN、EtherCAT)通信;
  • 作用:嵌入式设备(如 MCU)的 CPU 性能有限,若需同时处理传感器数据、控制逻辑、通信任务,易出现实时性不足;通过 DMA 可自动采集传感器数据(如 ADC 采样值)并写入内存,CPU 专注于控制算法,确保任务在毫秒/微秒级完成。

三、DMA 的使用方式(软件视角)

DMA 的使用需结合硬件平台(如 x86、ARM、RISC-V)和软件层次(驱动、操作系统、应用),核心是通过配置 DMA 控制器寄存器调用操作系统 API 实现数据传输。以下分“驱动开发”和“应用开发”两个层面说明:

1. 底层驱动开发(直接操作硬件)

在无操作系统(裸机)或需要深度定制驱动的场景(如嵌入式 Linux 内核驱动),需直接操作 DMA 控制器的寄存器,步骤如下:

步骤 1:使能 DMA 时钟与外设时钟

硬件层面,DMA 控制器和目标外设(如 UART、ADC)需先上电使能时钟(如 ARM 芯片的 RCC 寄存器、x86 的 PCIe 配置空间),否则无法操作。

// 示例(ARM Cortex-M 裸机):使能 DMA1 和 UART2 时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;  // 使能 DMA1 时钟
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;// 使能 UART2 时钟

步骤 2:配置 DMA 通道参数

根据传输需求,设置 DMA 通道的源地址、目标地址、传输长度、传输方向等,需注意地址对齐(如 32 位传输需地址按 4 字节对齐,避免总线错误)。

// 示例:配置 DMA1 通道4,实现 UART2 接收数据(外设→内存)
DMA1_Channel4->CPAR = (uint32_t)&USART2->DR;  // 源地址:UART2 数据寄存器
DMA1_Channel4->CMAR = (uint32_t)rx_buffer;    // 目标地址:内存接收缓冲区
DMA1_Channel4->CNDTR = 1024;                  // 传输长度:1024 字节
DMA1_Channel4->CCR |= DMA_CCR_DIR;            // 传输方向:外设→内存(DIR=1)
DMA1_Channel4->CCR |= DMA_CCR_MINC;           // 内存地址自增(MINC=1)
DMA1_Channel4->CCR |= DMA_CCR_PSIZE_0;        // 外设数据宽度:16位
DMA1_Channel4->CCR |= DMA_CCR_MSIZE_0;        // 内存数据宽度:16位

步骤 3:使能 DMA 与外设中断

配置 DMA 传输完成/出错中断,确保传输结束后能通知 CPU 处理。

// 使能 DMA 传输完成中断和全局中断
DMA1_Channel4->CCR |= DMA_CCR_TCIE;  // 传输完成中断使能(TCIE=1)
DMA1_Channel4->CCR |= DMA_CCR_HTIE;  // 半传输中断使能(可选,用于进度监控)
NVIC_EnableIRQ(DMA1_Channel4_IRQn);  // 使能 DMA 中断向量

// 使能外设(UART2)的 DMA 请求
USART2->CR3 |= USART_CR3_DMAR;  // UART2 接收 DMA 请求使能

步骤 4:启动 DMA 传输并处理中断

启动 DMA 后,等待传输完成中断,在中断服务函数中处理数据(如校验、转发)。

// 启动 DMA 传输
DMA1_Channel4->CCR |= DMA_CCR_EN;  // 使能 DMA 通道

// DMA 传输完成中断服务函数
void DMA1_Channel4_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_TCIF4) {  // 检查传输完成标志
        DMA1->IFCR |= DMA_IFCR_CTCIF4;  // 清除中断标志
        process_rx_data(rx_buffer, 1024);  // 处理接收的数据
        DMA1_Channel4->CNDTR = 1024;      // 重置传输长度,准备下一次传输
    }
}

2. 操作系统层应用开发(调用 API)

在 Windows、Linux、macOS 等操作系统中,用户无需直接操作硬件寄存器,而是通过操作系统提供的 API 或库函数使用 DMA(本质是操作系统内核驱动已封装好 DMA 逻辑)。以下以 Linux 为例说明:

场景:用户程序通过 DMA 与外设(如自定义 PCIe 设备)传输数据

Linux 中,用户空间无法直接访问 DMA 控制器,需通过“内核驱动 + 系统调用”或“内存映射(mmap)”实现数据传输,核心流程如下:

  1. 内核驱动初始化 DMA:申请 DMA 通道(dma_request_channel);分配 DMA 可访问的内存(dma_alloc_coherent,确保内存物理地址连续,避免 DMA 地址不连续问题);配置 DMA 传输参数(源/目标地址、长度),启动传输。
  2. 用户程序与内核驱动通信:通过 ioctl 或 mmap 将内核中的 DMA 缓冲区映射到用户空间,用户程序可直接读写该缓冲区,无需数据拷贝;传输完成后,内核通过信号或中断通知用户程序。

示例代码片段(Linux 内核驱动):

#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>

struct dma_dev {
    struct dma_chan *dma_chan;  // DMA 通道
    dma_addr_t dma_addr;        // DMA 物理地址
    void *virt_addr;            // 虚拟地址(内核空间)
    size_t buf_len;             // 缓冲区长度
};

// 初始化 DMA
static int dma_dev_probe(struct platform_device *pdev) {
    struct dma_dev *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->buf_len = 4096;  // 缓冲区长度 4KB

    // 1. 申请 DMA 通道(匹配外设类型,如 "memory-to-memory")
    dev->dma_chan = dma_request_channel(DMA_MEM_TO_MEM, NULL, NULL);
    if (!dev->dma_chan) return -ENODEV;

    // 2. 分配 DMA 可访问的连续内存
    dev->virt_addr = dma_alloc_coherent(&pdev->dev, dev->buf_len, 
                                        &dev->dma_addr, GFP_KERNEL);
    if (!dev->virt_addr) return -ENOMEM;

    // 3. 配置 DMA 传输(内存→内存示例,实际可改为外设地址)
    struct dma_async_tx_descriptor *desc;
    dma_cap_mask_t mask;
    dma_cap_zero(mask);
    dma_cap_set(DMA_MEMCPY, mask);  // 传输类型:内存拷贝

    desc = dmaengine_prep_dma_memcpy(dev->dma_chan, dev->dma_addr,  // 目标地址
                                     (dma_addr_t)src_buffer,        // 源地址(如外设地址)
                                     dev->buf_len, DMA_PREP_INTERRUPT);  // 传输长度+中断
    if (!desc) return -EINVAL;

    // 4. 绑定传输完成回调函数
    desc->callback = dma_transfer_complete;
    desc->callback_param = dev;

    // 5. 提交 DMA 传输任务
    dmaengine_submit(desc);
    dma_async_issue_pending(dev->dma_chan);  // 启动传输

    return 0;
}

// DMA 传输完成回调函数
static void dma_transfer_complete(void *param) {
    struct dma_dev *dev = param;
    dev_info(&dev->dev, "DMA transfer completed\n");
    // 通知用户程序(如发送信号、唤醒等待队列)
}

用户程序调用(Linux):

通过 mmap 将内核 DMA 缓冲区映射到用户空间,直接读写数据:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>

#define DMA_DEV_PATH "/dev/dma_device"
#define BUF_LEN 4096

int main() {
    int fd = open(DMA_DEV_PATH, O_RDWR);
    if (fd < 0) { perror("open"); return -1; }

    // 将内核 DMA 缓冲区映射到用户空间
    void *user_buf = mmap(NULL, BUF_LEN, PROT_READ | PROT_WRITE, 
                          MAP_SHARED, fd, 0);
    if (user_buf == MAP_FAILED) { perror("mmap"); return -1; }

    // 1. 写入数据到用户缓冲区(内核会通过 DMA 传输到外设)
    memcpy(user_buf, "Hello DMA", 10);
    // 触发 DMA 传输(通过 ioctl 通知内核)
    ioctl(fd, 0x12345678);  // 自定义命令,内核收到后启动 DMA

    // 2. 等待传输完成(如通过信号或轮询)
    sleep(1);

    // 3. 读取 DMA 传输到缓冲区的数据(外设→内存)
    printf("Received data: %s\n", (char *)user_buf);

    munmap(user_buf, BUF_LEN);
    close(fd);
    return 0;
}

四、DMA 的优缺点与注意事项

1. 优点

  • 提升系统性能:解放 CPU 资源,让 CPU 专注于计算任务,而非数据搬运;
  • 降低延迟:避免 CPU 上下文切换和指令开销,尤其适合实时性要求高的场景(如工业控制、音频视频);
  • 支持高吞吐量:硬件直接传输数据,可充分利用总线带宽(如 PCIe 4.0 带宽达 8GB/s)。

2. 缺点与挑战

  • 硬件复杂度增加:需额外的 DMA 控制器硬件,增加芯片成本和设计难度(嵌入式设备需权衡成本与性能);
  • 地址对齐要求:多数 DMA 控制器要求传输地址按数据宽度对齐(如 32 位传输需地址能被 4 整除),否则会触发总线错误;
  • 内存一致性问题:部分 CPU 存在缓存(Cache),若 DMA 直接修改内存数据,CPU 缓存中的数据可能与内存不一致(需通过“缓存刷新/无效化”操作解决,如 ARM 的 dmb 指令、x86 的 sfence 指令);
  • 优先级冲突:多个外设同时请求 DMA 时,低优先级外设可能被“饿死”,需合理设计优先级策略(如实时系统采用“抢占式优先级”)。

3. 使用注意事项

  1. 地址正确性:确保 DMA 源/目标地址为物理地址(而非虚拟地址),操作系统中需通过 dma_map_single 等函数将虚拟地址转换为物理地址;
  2. 缓存处理:若传输涉及 CPU 缓存,需在 DMA 传输前刷新缓存(确保内存数据最新),传输后无效化缓存(确保 CPU 读取最新数据);
  3. 错误处理:配置 DMA 错误中断(如传输超时、总线错误),避免因硬件故障导致数据丢失或系统崩溃;
  4. 带宽分配:根据外设需求合理分配 DMA 总线带宽,避免高带宽外设(如 NVMe SSD)占用过多资源,影响其他设备(如网卡)。

五、总结

DMA 是解决“CPU 数据搬运瓶颈”的核心技术,通过硬件独立完成外设与内存的直接传输,在存储、网络、多媒体等场景中不可或缺。其使用需结合硬件平台(配置 DMA 控制器)和软件层次(驱动/API 调用),同时注意地址对齐、缓存一致性、优先级管理等细节。理解 DMA 的原理与用法,是掌握计算机硬件体系、嵌入式开发、高性能系统优化的关键基础。

全网最全面的嵌入式八股文专栏:https://www.nowcoder.com/creation/manager/columnDetail/mPZ4kk

全部评论

相关推荐

评论
2
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务