11-DMA.md 11 KB


title: DMA tags: [STM32, DMA, 数据传输]

created: 2026-07-06

DMA — 直接存储器访问

原理

DMA 是什么

Direct Memory Access(直接存储器访问),在不占用 CPU 的情况下在外设和存储器之间搬运数据:

传统方式 (CPU搬运):
  CPU:  读取 外设DR → CPU寄存器 → 写入 内存数组
  CPU忙 → 无法处理其他任务

DMA方式:
  CPU:  配置DMA → 继续运行其他代码
  DMA:  自动 外设DR → 内存数组 (不占用CPU)
  CPU:  DMA传输完成 → 中断通知CPU

基本结构

DMA基本传输单元:

外设地址寄存器    DMA_CPARx
     ↓
外设 ←→ DMA 通道 ←→ 存储器
              ↑
           传输方向

DMA1 (STM32F103C8T6):
  ├─ 通道1: ADC1 / TIM2_CH3 / TIM4_CH1
  ├─ 通道2: USART3_TX / SPI1_RX / TIM1_CH1
  ├─ 通道3: USART3_RX / SPI1_TX / TIM1_CH2
  ├─ 通道4: USART1_TX / SPI2_RX / TIM2_CH4
  ├─ 通道5: USART1_RX / SPI2_TX / TIM3_CH2
  ├─ 通道6: USART2_TX / SPI3_RX / TIM3_CH3
  └─ 通道7: USART2_RX / SPI3_TX / TIM3_CH4

DMA2: 仅大容量芯片有 (F103RC以上)

传输方向

内存到内存 (M2M):
  源: 数组A  目标: 数组B
  DMA直接复制,不经过CPU

外设到内存 (P2M):
  源: ADC_DR/USART_DR  目标: 用户数组
  典型:ADC多通道扫描

内存到外设 (M2P):
  源: 发送缓冲区  目标: USART_DR/SPI_DR
  典型:串口发送大批量数据

循环模式 vs 正常模式

正常模式 (Normal):
  传输 BufferSize 次后停止
  需要重新配置 BufferSize 和 Cmd

循环模式 (Circular):
  传输完 BufferSize 次后自动重置
  地址回到起点,持续传输
  适合 ADC 连续扫描 + DMA

API 表格

DMA_InitTypeDef

typedef struct {
    uint32_t DMA_PeripheralBaseAddr;   // 外设基地址 (&USART1->DR, &ADC1->DR)
    uint32_t DMA_MemoryBaseAddr;       // 内存基地址 (uint32_t)&数组
    uint32_t DMA_DIR;                  // DMA_DIR_PeripheralSRC / PeripheralDST
    uint32_t DMA_BufferSize;           // 传输次数
    uint32_t DMA_PeripheralInc;        // DMA_PeripheralInc_Enable / Disable
    uint32_t DMA_MemoryInc;            // DMA_MemoryInc_Enable / Disable
    uint32_t DMA_PeripheralDataSize;   // Byte / HalfWord / Word
    uint32_t DMA_MemoryDataSize;       // Byte / HalfWord / Word
    uint32_t DMA_Mode;                 // DMA_Mode_Normal / Circular
    uint32_t DMA_Priority;             // VeryHigh / High / Medium / Low
    uint32_t DMA_M2M;                  // DMA_M2M_Enable / Disable
} DMA_InitTypeDef;

核心函数

函数 描述
DMA_Init(DMA_Channelx, &DMA_InitStructure) 初始化 DMA 通道
DMA_Cmd(DMA_Channelx, ENABLE) 使能 DMA 通道
DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE) 使能传输完成中断
DMA_GetFlagStatus(DMA_Channelx, DMA_FLAG_TCx) 获取传输完成标志
DMA_ClearFlag(DMA_Channelx, DMA_FLAG_TCx) 清除传输完成标志
DMA_GetITStatus(DMA_Channelx, DMA_IT_TC) 获取传输完成中断状态
DMA_ClearITPendingBit(DMA_Channelx, DMA_IT_TC) 清除传输完成中断标志
DMA_SetCurrDataCounter(DMA_Channelx, uint16_t) 设置剩余传输次数

DMA 通道与硬件请求映射

通道 请求1 请求2 请求3 请求4 请求5
DMA1_CH1 ADC1 TIM2_CH3 TIM4_CH1
DMA1_CH2 USART3_TX SPI1_RX TIM1_CH1 TIM2_UP TIM3_CH3
DMA1_CH3 USART3_RX SPI1_TX TIM1_CH2 TIM2_CH1 TIM3_CH4
DMA1_CH4 USART1_TX SPI2_RX TIM1_CH3 TIM2_CH2 TIM4_CH3
DMA1_CH5 USART1_RX SPI2_TX TIM1_CH4 TIM2_CH4
DMA1_CH6 USART2_TX SPI3_RX TIM1_UP TIM3_CH1
DMA1_CH7 USART2_RX SPI3_TX TIM1_TRIG TIM3_CH2

代码示例

1. 内存到内存搬运 (数组A → 数组B)

#include "stm32f10x.h"

#define BUFFER_SIZE 32

const uint32_t src_buf[BUFFER_SIZE] = {
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
    20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    30, 31
};
uint32_t dst_buf[BUFFER_SIZE] = {0};

void DMA_M2M_Init(void)
{
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_InitTypeDef DMA_InitStructure;

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)src_buf;
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)dst_buf;
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize         = BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Enable;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Word;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Enable;  // 内存到内存
    DMA_Init(DMA1_Channel6, &DMA_InitStructure); // CH6 可用于 M2M

    DMA_Cmd(DMA1_Channel6, ENABLE);

    // 等待传输完成
    while (DMA_GetFlagStatus(DMA1_Channel6, DMA1_FLAG_TC6) == RESET);

    // 此时 dst_buf 内容应与 src_buf 相同
}

int main(void)
{
    DMA_M2M_Init();
    while (1);
}

2. ADC + DMA 多通道连续采集

#include "stm32f10x.h"

#define ADC_CH_COUNT 4

uint16_t adc_values[ADC_CH_COUNT]; // PA0–PA3

void ADC1_DMA_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_DMA1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // ADC 配置:扫描+连续
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CH_COUNT;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);

    // DMA 配置
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)adc_values;
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize         = ADC_CH_COUNT;
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    ADC_DMACmd(ADC1, ENABLE);
    DMA_Cmd(DMA1_Channel1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1));

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    // DMA 自动将 4 通道结果循环写入 adc_values[]
}

// 读取最新值
// adc_values[0]=PA0, [1]=PA1, [2]=PA2, [3]=PA3
// DMA 在 Circular 模式下自动更新

3. DMA + USART 发送

#include "stm32f10x.h"

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

void USART1_DMA_Init(void)
{
    // ... USART1 初始化同上节课内容 ...

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)tx_buffer;
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize         = sizeof(tx_buffer) - 1;
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority           = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel4, &DMA_InitStructure); // CH4: USART1_TX

    DMA_Cmd(DMA1_Channel4, ENABLE);
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
    // DMA 自动将 tx_buffer 数据发送出去
}

常见坑点

  1. DMA_BufferSize 是传输次数:不是字节数。HalfWord 模式下传输 10 次 = 20 字节,Word 模式下传输 10 次 = 40 字节

  2. 外设地址不要配错:外设地址必须使用 (uint32_t)&外设->DR 形式。例:ADC 是 &ADC1->DR,USART 是 &USART1->DR

  3. DMA 通道与硬件请求对应:ADC1 只能使用 DMA1_Channel1。USART1_TX 只能用 DMA1_Channel4。通道选错不会报错但也不会传输

  4. 内存到内存(M2M)模式限制

    • M2M 模式使用 DMA1,且仅通道 2/6/7 可用于 M2M(不同芯片有差异)
    • M2M 模式下 DMA_PeripheralBaseAddr 指向源地址,DMA_MemoryBaseAddr 指向目标地址
    • M2M 模式下 DMA_DIR 必须是 DMA_DIR_PeripheralSRC
  5. 循环模式(Circular)注意:使能循环后传输永不停,不触发正常传输完成中断(只有半传输和错误中断)。想要周期性处理数据时,用半传输中断(DMA_IT_HT)或定时器中断

  6. 传输完成中断:正常模式下每传输 BufferSize 次触发一次 TC 中断;使用 DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE) 使能;中断服务函数中先清除标志再处理数据

  7. DMA 使能顺序:先配置 DMA,再使能外设的 DMA 请求。如果先使能外设再使能 DMA,外设可能在 DMA 未就绪时产生第一次请求

  8. DMA 半传输中断DMA_IT_HT 在传输到一半时触发,适合双缓冲(乒乓缓冲)场景

  9. ADC 连续模式 + DMA 循环:ADC 设置 ContinuousConvMode=ENABLE、DMA 设 Mode=Circular,可做到全程无 CPU 参与采集。但注意 ADC_CLK 越快,DMA 带宽占用越高

  10. DMA 不会自动停止:正常模式传输完成后自动停止(DMA_Cmd 自动变 DISABLE),再次传输前需重新使能 DMA_Cmd(DMA_Channelx, ENABLE);循环模式不会停止,即使重新配置也需要先 Disable 再 Enable