一、什么是 DMA?——先用生活比喻理解

🏪 快递站的比喻

没有 DMA 的世界(CPU 搬运数据):
┌─────────┐
│  老板(CPU) │ ——> 老板亲自去仓库取货,送到柜台,再回来继续工作
│           │     每搬一次货就要停下手头的工作
└─────────┘
⚠️ 老板很累,效率低!

有 DMA 的世界:
┌─────────┐     ┌─────────┐
│  老板(CPU) │     │ 搬运工(DMA)│ ——> 搬运工负责搬货
│  专心算账  │     │  自动搬运   │     老板可以专心做别的事
└─────────┘     └─────────┘
✅ 老板轻松,效率高!

📖 正式定义

DMA = Direct Memory Access(直接存储器访问)

核心思想:数据搬运不需要 CPU 参与
         DMA 控制器自动在 "外设" 和 "内存" 之间搬运数据

二、DMA 的基本概念

2.1 数据搬运的三个要素

┌──────────────────────────────────────────────────┐
│                 DMA 搬运数据三要素                    │
├──────────────────────────────────────────────────┤
│                                                    │
│   1️⃣ 从哪里搬?(源地址 Source)                      │
│      - 可以是外设的数据寄存器(如 UART 的 DR 寄存器)       │
│      - 也可以是内存中的数组                           │
│                                                    │
│   2️⃣ 搬到哪里?(目的地址 Destination)               │
│      - 可以是内存中的数组                             │
│      - 也可以是外设的数据寄存器                        │
│                                                    │
│   3️⃣ 搬多少?(数据长度 Length)                      │
│      - 要传输的数据个数                               │
│                                                    │
└──────────────────────────────────────────────────┘

2.2 DMA 的传输方向(3 种)

方向 1:外设 → 内存(最常用)
┌──────────┐    DMA     ┌──────────┐
│  UART 接收 │ ────────→ │ 内存数组   │
│  ADC 采集  │           │ buffer[] │
└──────────┘            └──────────┘
例子:串口接收数据存到数组、ADC 采集结果存到数组

方向 2:内存 → 外设
┌──────────┐    DMA     ┌──────────┐
│ 内存数组   │ ────────→ │  UART 发送 │
│ buffer[] │           │  SPI 发送  │
└──────────┘            └──────────┘
例子:把数组中的数据通过串口发送出去

方向 3:内存 → 内存
┌──────────┐    DMA     ┌──────────┐
│ 数组 A     │ ────────→ │ 数组 B     │
│ src[]    │           │ dst[]    │
└──────────┘            └──────────┘
例子:快速复制一块内存数据

2.3 DMA 的工作模式

模式 1:普通模式(Normal)
┌─────────────────────────────────────┐
│ 搬完指定数量的数据后 → 自动停止         │
│ 想再搬一次 → 需要重新启动              │
│                                      │
│ 数据: [1][2][3][4][5] → 搬完 → 停止! │
└─────────────────────────────────────┘
适用场景:发送一段固定数据

模式 2:循环模式(Circular)
┌─────────────────────────────────────┐
│ 搬完指定数量后 → 自动从头开始继续搬     │
│ 不需要重新启动,一直循环               │
│                                      │
│ 数据: [1][2][3] → [1][2][3] → [1]...│
│       不停地循环搬运                   │
└─────────────────────────────────────┘
适用场景:ADC 连续采集、持续接收数据

2.4 地址是否自增

┌─────────────────────────────────────────────────┐
│ 外设地址自增?                                     │
│   通常 ❌ 不自增                                   │
│   因为外设数据寄存器地址是固定的                      │
│   比如 UART 的数据寄存器永远是那一个地址              │
│                                                    │
│ 内存地址自增?                                      │
│   通常 ✅ 要自增                                    │
│   因为要把数据存到数组的不同位置                      │
│   buffer[0], buffer[1], buffer[2]...              │
└─────────────────────────────────────────────────┘

图示说明:
外设(固定地址)          内存(地址自增)
┌──────┐              ┌──────┐
│ 0x4001│ ──→ 数据 1 ──→│ [0]  │  地址+1
│ 0x4001│ ──→ 数据 2 ──→│ [1]  │  地址+1  
│ 0x4001│ ──→ 数据 3 ──→│ [2]  │  地址+1
└──────┘              └──────┘

三、STM32 中的 DMA 资源

3.1 DMA 控制器和通道

以 STM32F103 为例:
┌───────────────────────────────────────┐
│              DMA1(7 个通道)             │
├───────────────────────────────────────┤
│ 通道 1: ADC1                           │
│ 通道 2: SPI1_RX / USART3_TX / TIM1_CH1 │
│ 通道 3: SPI1_TX / USART3_RX / TIM1_CH2 │
│ 通道 4: SPI2_RX / USART1_TX / I2C2_TX  │  ← 每个通道可以
│ 通道 5: SPI2_TX / USART1_RX / I2C2_RX  │    连接多个外设
│ 通道 6: USART2_RX / I2C1_TX            │    但同一时间
│ 通道 7: USART2_TX / I2C1_RX            │    只能用一个
└───────────────────────────────────────┘

以 STM32F4 为例:
┌───────────────────────────────────────┐
│  DMA1(8 个数据流 Stream,每个流 8 个通道)    │
│  DMA2(8 个数据流 Stream,每个流 8 个通道)    │
├───────────────────────────────────────┤
│  比 F1 更灵活,用 Stream + Channel 组合    │
└───────────────────────────────────────┘

⚠️ 重点:不是随便选通道的!每个外设对应固定的 DMA 通道,要查数据手册或 CubeMX 自动分配。


四、CubeMX 配置 DMA(图文步骤)

以"串口 1 DMA 接收"为例

步骤 1:打开 CubeMX,选好芯片,配置好 USART1
┌────────────────────────────────┐
│ Connectivity → USART1          │
│ Mode: Asynchronous             │
│ Baud Rate: 115200              │
└────────────────────────────────┘

步骤 2:在 USART1 配置页面,点击 "DMA Settings" 选项卡
┌────────────────────────────────┐
│ [DMA Settings]                 │
│                                │
│ 点击 "Add" 按钮                │
│ 选择 USART1_RX                │
└────────────────────────────────┘

步骤 3:配置 DMA 参数
┌────────────────────────────────────────────┐
│ DMA Request:    USART1_RX                   │
│ Stream/Channel: 自动分配(不用改)             │
│ Direction:      Peripheral To Memory(外设→内存)│
│ Priority:       Low/Medium/High/Very High   │
│                                              │
│ Mode:           Normal 或 Circular           │
│                                              │
│ Peripheral:                                  │
│   Increment Address: ❌ 不勾选(外设地址不自增)│
│   Data Width:        Byte(字节,8 位)         │
│                                              │
│ Memory:                                      │
│   Increment Address: ✅ 勾选(内存地址自增)    │
│   Data Width:        Byte(字节,8 位)         │
└────────────────────────────────────────────┘

步骤 4:开启 NVIC 中断(可选但推荐)
┌────────────────────────────────┐
│ [NVIC Settings]                │
│ ✅ USART1 global interrupt     │
│ ✅ DMA1 Stream/Channel interrupt│
└────────────────────────────────┘

步骤 5:生成代码

五、生成代码解析

5.1 CubeMX 生成的关键代码

/* ============= 1. DMA 句柄声明 ============= */
DMA_HandleTypeDef hdma_usart1_rx;  // DMA 句柄,代表"这个 DMA 通道"
UART_HandleTypeDef huart1;          // 串口句柄

/* ============= 2. DMA 初始化(自动生成) ============= */
void MX_DMA_Init(void)  // ⚠️ 这个函数要在外设初始化之前调用!
{
    /* DMA controller clock enable */
    __HAL_RCC_DMA1_CLK_ENABLE();  // 开启 DMA1 时钟
    
    /* DMA interrupt init */
    HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);  // 设置中断优先级
    HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);           // 使能 DMA 中断
}

/* ============= 3. 串口初始化中关联 DMA ============= */
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
    // ... GPIO 初始化 ...
    
    /* USART1 DMA Init */
    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA1_Channel5;                    // 用哪个 DMA 通道
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;       // 方向:外设→内存
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;          // 外设地址不自增
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;              // 内存地址自增
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据宽度:字节
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;   // 内存数据宽度:字节
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;                     // 普通模式
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;           // 优先级:低
    
    HAL_DMA_Init(&hdma_usart1_rx);     // 初始化 DMA
    
    __HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx);  // 🔑 关键!把 DMA 和串口关联起来
}

5.2 理解 __HAL_LINKDMA

// 这个宏做了两件事:
__HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx);

// 展开后等价于:
huart1.hdmarx = &hdma_usart1_rx;        // 串口的 DMA 接收指针 → 指向 DMA 句柄
hdma_usart1_rx.Parent = &huart1;         // DMA 的父指针 → 指向串口句柄

// 图示:
// ┌────────────┐         ┌──────────────────┐
// │  huart1     │ ──────→ │ hdma_usart1_rx   │
// │  .hdmarx ───┤         │  .Parent ────────┤──→ 指回 huart1
// └────────────┘         └──────────────────┘
// 双向关联,互相知道对方

六、DMA HAL 库常用 API 函数

6.1 函数总览

/* ========== 串口 + DMA 相关函数 ========== */

// 1. DMA 方式发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);

// 2. DMA 方式接收(固定长度)
HAL_UART_Receive_DMA(&huart1, rx_buf, len);

// 3. DMA 方式接收(不定长度)—— 非常实用!
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, buf_size);

/* ========== ADC + DMA 相关函数 ========== */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, len);

/* ========== SPI + DMA 相关函数 ========== */
HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len);
HAL_SPI_Receive_DMA(&hspi1, rx_buf, len);

/* ========== 通用 DMA 函数 ========== */
HAL_DMA_Start(&hdma, src_addr, dst_addr, len);      // 启动 DMA 传输
HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, timeout); // 轮询等待完成
HAL_DMA_Abort(&hdma);                                 // 中止 DMA 传输

6.2 回调函数(中断完成后自动调用)

/* ========== 串口 DMA 回调函数 ========== */

// 发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    // DMA 发送完成后,HAL 库自动调用这个函数
    // 你在这里写"发送完成后要做的事"
}

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // DMA 接收完指定数量数据后,自动调用
}

// 半传输完成回调(接收了一半数据时)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    // 用于双缓冲处理
}

// 空闲中断回调(接收不定长数据时用)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    // Size = 实际接收到的数据长度
}

七、实战案例

案例 1:DMA 串口发送

/* ===== 最简单的 DMA 发送 ===== */

uint8_t tx_data[] = "Hello DMA!\r\n";

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_DMA_Init();       // ⚠️ DMA 初始化必须在串口之前!
    MX_USART1_UART_Init();
    
    while (1)
    {
        // DMA 方式发送,CPU 不用等待,发完自动停止
        HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data) - 1);
        
        HAL_Delay(1000);  // CPU 可以做别的事情
    }
}

// 可选:发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &huart1)
    {
        // 发送完成,可以设置标志位或做其他操作
        // 比如点亮 LED 表示发送成功
    }
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐