跳转至

DMA(直接内存访问)

DMA(Direct Memory Access)允许外设与内存之间直接传输数据,不需要 CPU 参与。这就像给 CPU 请了一个"搬运工",CPU 可以去做更重要的事情,数据搬运由 DMA 自动完成。


一、为什么需要 DMA?

没有 DMA 时(CPU 搬运数据)

假设我们要用 ADC 连续采集 1000 个数据:

for (int i = 0; i < 1000; i++)
{
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);  // CPU 在这里等!
    buffer[i] = HAL_ADC_GetValue(&hadc1);               // CPU 搬运数据
}

问题:CPU 大部分时间在等待搬运数据,无法做其他事情。

有 DMA 时

// 配置一次,然后启动
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buffer, 1000);
// ADC 每转换完成一次,DMA 自动把数据从 ADC->DR 搬到 buffer[i]
// CPU 完全不参与!可以去处理其他任务
graph LR
    subgraph 无 DMA
        CPU1[CPU] --> |"1.启动ADC"| ADC1[ADC]
        ADC1 --> |"2.等待完成"| CPU1
        CPU1 --> |"3.读数据"| ADC1
        CPU1 --> |"4.写入内存"| MEM1[内存]
    end
    subgraph 有 DMA
        CPU2[CPU 做其他事] -.-> |"只需初始配置"| DMA2[DMA]
        ADC2[ADC] --> |"自动搬运"| DMA2
        DMA2 --> |"自动写入"| MEM2[内存]
    end

二、DMA 基本概念

DMA 控制器概览

STM32F103 有 2 个 DMA 控制器:

DMA 控制器 通道数 总线 服务的外设
DMA1 7 个通道 AHB ADC1, USART1/2/3, SPI1/2, I2C1/2, TIM1/2/3/4
DMA2 5 个通道 AHB ADC3, USART4, SPI3, TIM5/6/7/8(仅大容量型号)

通道 ≠ 可以随意选择

每个外设对应固定的 DMA 通道。例如 ADC1 只能用 DMA1 通道 1,USART1 TX 只能用 DMA1 通道 4。需要查阅数据手册确认。

DMA1 通道对应表(常用)

通道 外设请求源
CH1 ADC1, TIM2_CH3, TIM4_CH1
CH2 USART3_TX, TIM1_CH1, TIM2_UP, SPI1_RX
CH3 USART3_RX, TIM1_CH2, TIM3_CH4, SPI1_TX
CH4 USART1_TX, TIM1_CH4, TIM4_CH2, SPI2_RX, I2C2_TX
CH5 USART1_RX, TIM1_UP, TIM2_CH1, SPI2_TX, I2C2_RX
CH6 USART2_RX, TIM1_CH3, TIM3_CH1, I2C1_TX
CH7 USART2_TX, TIM2_CH2, TIM4_CH3, I2C1_RX

三种传输方向

方向 说明 典型应用
外设 → 内存 从外设数据寄存器读取,写入内存数组 ADC 采集、USART 接收
内存 → 外设 从内存数组读取,写入外设数据寄存器 USART 发送、DAC 输出
内存 → 内存 在两个内存地址之间拷贝数据 数据缓冲区复制

三、CubeMX 中配置 DMA

在 HAL + CubeMX 工作流中,DMA 的配置非常简单——在外设配置页面的 DMA Settings 标签页中添加 DMA 通道即可。CubeMX 会自动选择正确的通道并生成所有初始化代码。

关键配置参数

在 CubeMX 的 DMA Settings 中,需要设置以下参数:

参数 含义 常用设置
Direction 传输方向 Peripheral To Memory / Memory To Peripheral
Mode 工作模式 Normal(单次)/ Circular(循环)
Increment Address - Peripheral 外设地址是否递增 通常不勾选(外设寄存器地址固定)
Increment Address - Memory 内存地址是否递增 通常勾选(依次写入数组)
Data Width - Peripheral 外设数据宽度 Byte / Half Word / Word
Data Width - Memory 内存数据宽度 与外设数据宽度匹配
Priority 优先级 Low / Medium / High / Very High

HAL 中的 DMA 使用特点

使用 HAL 库时,你通常不需要直接操作 DMA 寄存器或函数。外设的 HAL 函数(如 HAL_ADC_Start_DMA()HAL_UART_Transmit_DMA())会自动管理 DMA 的启动、传输和回调。

工作模式

传输完设定数量的数据后停止。如果需要再次传输,必须重新调用启动函数。

传输 N 次 → 停止 → 需重新调用 Start_DMA

传输完成后自动回到起始地址,重新开始传输。适合连续采集场景。

传输 N 次 → 自动回到起点 → 继续传输 → 永不停止

四、DMA 编程实战(CubeMX + HAL)

示例 1:DMA 搬运 ADC 数据

连续采集 ADC 通道 0 的数据,存入缓冲区(不占用 CPU):

CubeMX 配置:

  1. Analog → ADC1 使能 IN0(PA0),Continuous Conversion = Enabled
  2. DMA Settings → Add → ADC1
    • Direction: Peripheral To Memory
    • Mode: Circular
    • Data Width: Half Word(ADC 是 12 位,16 位存储)
  3. 生成代码
/* main.c */

#define ADC_BUFFER_SIZE  100
uint16_t adc_buffer[ADC_BUFFER_SIZE];

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);                           // 校准
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);  // 启动 ADC + DMA
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
    // adc_buffer 会被 DMA 自动持续更新
    // 直接读取即可,CPU 完全不需要参与数据搬运

    uint32_t sum = 0;
    for (int i = 0; i < ADC_BUFFER_SIZE; i++)
        sum += adc_buffer[i];
    uint16_t avg = sum / ADC_BUFFER_SIZE;

    printf("ADC avg = %d\r\n", avg);
    HAL_Delay(500);
    /* USER CODE END WHILE */
}

示例 2:DMA 发送串口数据

用 DMA 发送一串字符,CPU 启动后就可以做别的事情:

CubeMX 配置:

  1. Connectivity → USART1 配置好波特率
  2. DMA Settings → Add → USART1_TX
    • Direction: Memory To Peripheral
    • Mode: Normal
    • Data Width: Byte
  3. 生成代码
/* main.c */

char tx_buffer[] = "Hello DMA!\r\n";

/* USER CODE BEGIN WHILE */
while (1)
{
    // DMA 方式发送,函数立即返回,CPU 不阻塞
    HAL_UART_Transmit_DMA(&huart1, (uint8_t*)tx_buffer, strlen(tx_buffer));
    HAL_Delay(1000);
    /* USER CODE END WHILE */
}

/* USER CODE BEGIN 4 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    // DMA 发送完成回调(可选)
    // 可以在这里设置标志位或启动下一次发送
}
/* USER CODE END 4 */

DMA 发送注意事项

DMA 发送期间,不能修改发送缓冲区的数据!因为 DMA 还在读取这块内存。等发送完成回调触发后才能安全修改。

示例 3:内存到内存拷贝

HAL 库也支持纯内存拷贝,但使用场景较少(通常用 memcpy 更简单)。如果需要,可以直接使用 HAL DMA 函数:

uint32_t src_buffer[100] = { /* 源数据 */ };
uint32_t dst_buffer[100];

/* 在 CubeMX 中配置一个 Memory To Memory 的 DMA 通道 */
/* 或者直接使用 HAL_DMA_Start() */

HAL_DMA_Start(&hdma_memtomem, (uint32_t)src_buffer, (uint32_t)dst_buffer, 100);
HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

五、DMA 回调函数

HAL 库为 DMA 提供了回调机制,通过外设的回调函数通知 CPU:

回调函数 触发时机 使用场景
HAL_ADC_ConvCpltCallback() ADC DMA 全部传输完成 处理采集完成的数据
HAL_ADC_ConvHalfCpltCallback() ADC DMA 传输到一半 双缓冲处理
HAL_UART_TxCpltCallback() UART DMA 发送完成 发送下一包数据
HAL_UART_RxCpltCallback() UART DMA 接收完成 处理接收数据
/* 以 ADC DMA 为例 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        // DMA 传输完成,数据已在 adc_buffer 中
        // 在这里处理数据或设置标志位
        data_ready = 1;
    }
}

双缓冲技巧

利用 HalfCpltCallbackCpltCallback,可以实现双缓冲:

  • 前半段数据传输完成 → CPU 处理前半段,DMA 继续传输后半段
  • 后半段数据传输完成 → CPU 处理后半段,DMA 重新传输前半段

这样 CPU 和 DMA 交替工作,不会互相等待。


六、常见问题

DMA 传输完成后数据不对?

  1. 检查数据宽度:外设和内存的 DataSize 必须匹配(如 ADC 是 16 位,要用 HalfWord)
  2. 检查地址递增:外设地址通常不递增,内存地址通常递增
  3. 检查 BufferSize:是传输的数据个数,不是字节数
  4. 检查传输方向PeripheralSRCPeripheralDST 不要搞反

DMA 只能传输一次?

检查是否设置为 Normal 模式。Normal 模式下传输完成后停止,如果要持续传输需使用 Circular 模式,或在传输完成中断中重新使能 DMA。

何时使用 DMA?

  • 大块数据传输:ADC 多通道连续采集、大量串口数据收发
  • CPU 资源紧张:需要 CPU 做复杂计算时,把数据搬运交给 DMA
  • 高速传输:DMA 的传输速度通常比 CPU 轮询更快
  • 不适合 DMA 的场景:偶尔传输一两个字节(配置 DMA 的开销大于直接搬运)