DMA 技术文档

本页系统整理直接内存访问相关的核心概念、硬件接口、环境配置与安全须知。代码块均可一键复制。

DMA 基础原理

直接内存访问(Direct Memory Access,DMA)是一种让外设子系统在不依赖 CPU 逐条执行数据搬运指令的前提下,直接与主存储器交换数据的技术。它通过独立的 DMA 控制器获得系统总线的主控权,在后台完成批量数据传输,从而释放 CPU 去执行计算或控制任务。

典型工作流程包括:外设触发 DMA 请求 → DMA 控制器向 CPU 申请总线 → 总线仲裁授权 → DMA 执行一次或多次传输 → 传输完成触发中断并释放总线。理解这一流程是排查 DMA 不工作、数据错位或中断丢失等问题的基础。

核心术语

  • 通道(Channel):DMA 控制器中的独立传输单元,通常与特定外设绑定。
  • 源地址 / 目标地址:数据从哪来、到哪去,可以是外设寄存器或内存地址。
  • 传输长度:一次 DMA 事务需要搬运的数据单元数量。
  • 突发传输(Burst):DMA 在获得总线后连续传输多个数据单元,减少总线仲裁开销。

伪代码示例

// DMA 基础配置伪代码
void DMA_Config(uint32_t src, uint32_t dst, uint16_t len) {
    DMA_Channel_TypeDef* ch = DMA1_Channel1;
    DMA_DeInit(ch);

    DMA_InitTypeDef cfg;
    cfg.PeriphAddr  = src;
    cfg.MemAddr     = dst;
    cfg.Direction   = DMA_DIR_PeriphSRC; // 外设到内存
    cfg.BufferSize  = len;
    cfg.PeriphInc   = DMA_PeriphInc_Disable;
    cfg.MemInc      = DMA_MemInc_Enable;
    cfg.PeriphDataSize = DMA_PeriphDataSize_Word;
    cfg.MemDataSize    = DMA_MemDataSize_Word;
    cfg.Mode        = DMA_Mode_Normal;
    cfg.Priority    = DMA_Priority_High;
    cfg.M2M         = DMA_M2M_Disable;

    DMA_Init(ch, &cfg);
    DMA_ITConfig(ch, DMA_IT_TC, ENABLE); // 传输完成中断
    DMA_Cmd(ch, ENABLE);
}

硬件通信方式

DMA 与硬件的交互方式取决于总线架构和外设接口。常见场景包括:外设通过专用 DMA 请求线向控制器发信号;PCIe 设备通过 MSI/MSI-X 中断或门铃机制触发主机 DMA;嵌入式 SoC 则通过 AHB / AXI 总线矩阵进行主从切换。

在 x86 平台,传统 DMA 依赖 8237 兼容控制器或现代 IOMMU 管理下的设备内存访问;在 ARM / RISC-V 平台,通常由 PL330、XDMA 或各厂商自定义 DMA IP 完成。无论哪种架构,都需关注地址对齐、缓存一致性与总线宽度匹配。

地址对齐与宽度

// 地址对齐检查示例
#define DMA_ALIGN_MASK 0x03U

bool is_dma_aligned(uint32_t addr, uint8_t width_bytes) {
    if (width_bytes == 0) return false;
    return (addr & (width_bytes - 1)) == 0;
}

// 使用示例
uint32_t buffer = 0x2000'1000;
if (!is_dma_aligned(buffer, 4)) {
    log_error("Buffer not aligned for 32-bit DMA");
}

环境搭建

学习 DMA 通常需要准备目标硬件、调试工具与交叉编译链。以 STM32 为例,推荐安装 STM32CubeIDE 或 VS Code + PlatformIO;对于 Linux 内核驱动开发,则需要配置内核源码、交叉编译器与 QEMU / 真实开发板。

推荐工具链

  • 编译器:arm-none-eabi-gcc、x86_64-linux-gnu-gcc
  • 调试器:OpenOCD、ST-Link、J-Link
  • 逻辑分析仪:Saleae、DSLogic(用于观察 DMA 时序)
  • 宿主机:Ubuntu 22.04 LTS 或 Windows + WSL2

Makefile 片段

# 最小 Makefile 示例(ARM Cortex-M)
TARGET  = dma_demo
CC      = arm-none-eabi-gcc
CFLAGS  = -mcpu=cortex-m4 -mthumb -O2 -Wall -g
LDFLAGS = -TSTM32F407VGTx_FLASH.ld -nostartfiles

SRCS = main.c dma.c startup_stm32f407xx.s
OBJS = $(SRCS:.c=.o)

$(TARGET).elf: $(OBJS)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET).elf $(TARGET).bin

驱动相关

在操作系统层面使用 DMA,通常需要内核驱动配合。驱动负责分配物理连续的 DMA 缓冲区、配置设备寄存器、处理完成中断,并通过 mmap 或 read/write 向用户态暴露数据。Linux 中常用 dma_alloc_coherentdma_map_sg 等 API。

用户态程序若直接访问 DMA 硬件,需要 root / 管理员权限,并可能绕过操作系统的内存保护机制。强烈建议在虚拟机或专用开发板上进行实验,避免损坏生产环境。

Linux DMA 缓冲区分配

// Linux 内核模块:分配一致性 DMA 缓冲区
#include 

void* dma_buf;
dma_addr_t dma_handle;
struct device* dev = &pdev->dev;

size_t size = 4096;
dma_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!dma_buf) {
    dev_err(dev, "Failed to allocate DMA buffer\n");
    return -ENOMEM;
}

// 使用 dma_buf(CPU 虚拟地址)与 dma_handle(设备总线地址)
// ...

dma_free_coherent(dev, size, dma_buf, dma_handle);

风险提示

重要安全提醒

DMA 能够绕过 CPU 直接读写物理内存,错误配置或恶意使用可能导致系统崩溃、数据丢失、隐私泄露或硬件损坏。本站所有资料仅供计算机底层技术学习与学术研究,禁止用于非法入侵、未经授权访问外部设备或任何违反法律法规的行为。

  • 在受控、隔离的实验环境中测试 DMA 代码,切勿在生产机或他人设备上运行。
  • 确保理解目标平台的内存布局与 IOMMU 配置,防止越界访问。
  • 使用 DMA 前关闭或配置好缓存一致性,避免陈旧数据或脏数据问题。
  • 遵守《网络安全法》等法律法规,所有操作后果由使用者自行承担。