Chapter 08 · ARM 嵌入式 C 开发

DMA 直接内存访问

理解 DMA 通道仲裁与三种传输方向,掌握双缓冲技术,实现 UART/ADC/SPI 的零拷贝高性能数据传输。

课程进度80%

关键概念

DMA(直接内存访问)
Direct Memory Access,一种让外设与内存直接交换数据,绕过 CPU 的机制。例如 UART 收到数据,DMA 自动把数据从 UART 的 DR 寄存器复制到 RAM 缓冲区,无需 CPU 介入。CPU 只需配置好 DMA,等传输完成后处理结果,期间可以做其他工作。
DMA 控制器结构
STM32F4 有 2 个 DMA 控制器(DMA1 和 DMA2),每个有 8 个 Stream(通道),每个 Stream 有 8 个 Request(请求来源选择)。每个 Stream 独立工作,有自己的 FIFO 缓冲。DMA2 可以访问 AHB 总线,因此 SPI1/ADC 等挂在 APB2 的外设通常用 DMA2。
三种传输方向
P→M(外设到内存):ADC 采集结果 → RAM 数组(最常见)。M→P(内存到外设):RAM 数组 → UART 发送寄存器(批量发送)。M→M(内存到内存):RAM 区域之间的快速复制,类似 memcpy 但由硬件完成,仅 DMA2 支持。
循环模式 vs 普通模式
普通模式(Normal):传输完指定数量后停止,产生传输完成中断(TC)。循环模式(Circular):传输完后自动重置计数器重新开始,地址回到起点,适合连续采集(ADC)或连续发送(DAC 波形)。
DMA 中断标志
每个 DMA Stream 有 5 个状态标志:FEIF(FIFO 错误)、DMEIF(直接模式错误)、TEIF(传输错误)、HTIF(半传输,传输了一半)、TCIF(传输完成)。半传输中断在双缓冲场景中非常有用——前半 buffer 满时通知 CPU 处理,同时 DMA 继续填充后半。
双缓冲模式(Double Buffer)
DMA 交替使用两个缓冲区(Buffer0 和 Buffer1)。当 DMA 填充 Buffer0 时,CPU 处理 Buffer1;当 DMA 切换到 Buffer1 时,CPU 处理 Buffer0。实现真正的零延迟流式处理,适合音频、高速数据采集等场景。STM32F4 DMA 支持硬件双缓冲(DBM 位)。
DMA 与 Cache 一致性
STM32F4 的 Cortex-M4 没有数据 Cache,DMA 写入 SRAM 后 CPU 读到的直接是最新值,无需手动刷 Cache。但在 STM32H7(Cortex-M7,有 D-Cache)上,DMA 写入的缓冲区必须放在非 Cache 区域,或在读取前执行 SCB_InvalidateDCache_by_Addr(),否则 CPU 读到的是 Cache 中的旧值。

DMA 通道分配图

STM32F4 DMA 请求映射(常用部分) ═══════════════════════════════════════════════════════════════ DMA1(主要服务 APB1 外设) Stream │ CH0 │ CH1 │ CH2 │ CH3 │ CH4 ───────┼──────────┼──────────┼──────────┼──────────┼──────── Str0 │ SPI3_RX │ │ SPI3_RX │ SPI2_RX │ UART5_RX Str1 │ │ UART3_RX │ TIM7_UP │ UART3_TX │ UART3_TX Str2 │ SPI3_RX │ TIM7_UP │ │ TIM4_CH1 │ Str3 │ SPI2_RX │ TIM2_UP │ I2C3_RX │ │ UART3_TX Str4 │ SPI2_TX │ │ TIM3_CH4 │ UART4_TX │ Str5 │ │ USART2_RX│ │ TIM3_CH2 │ I2C1_RX Str6 │ │ USART2_TX│ │ TIM3_CH1 │ UART5_TX Str7 │ │ │ I2C2_TX │ UART5_TX │ TIM3_UP DMA2(可访问 AHB,支持 M→M) Stream │ CH0 │ CH3 │ CH4 │ CH5 ───────┼──────────────┼────────────┼────────────┼────────── Str0 │ ADC1 │ SPI1_RX │ ADC1 │ SPI6_TX Str1 │ SAI1_A │ USART6_RX │ SPI4_TX │ SPI6_RX Str2 │ TIM8_CH1 │ SPI1_RX │ USART1_RX │ SPI5_TX Str3 │ SAI1_A │ │ SPI1_TX │ SPI5_TX Str4 │ ADC1 │ SPI1_TX │ SPI4_RX │ Str5 │ SPI6_TX │ USART6_TX │ USART1_RX │ SPI5_RX Str6 │ TIM1_CH1 │ SDIO │ SDIO │ USART6_TX Str7 │ │ │ USART1_TX │ DCMI 使用时查 Reference Manual 的 DMA Request Mapping 表,选对 Stream+Channel!

DMA 内存到内存快速复制

/**
 * DMA2 M→M 快速内存复制(比 memcpy 更高效,CPU 可同时运行)
 * 注意:仅 DMA2 支持 M2M,且两端地址必须是 4 字节对齐(word传输时)
 */
DMA_HandleTypeDef hdma_m2m;

HAL_StatusTypeDef DMA_MemCopy(uint32_t *dst, uint32_t *src, uint32_t words) {
  hdma_m2m.Instance                 = DMA2_Stream0;
  hdma_m2m.Init.Channel             = DMA_CHANNEL_0;
  hdma_m2m.Init.Direction           = DMA_MEMORY_TO_MEMORY;
  hdma_m2m.Init.PeriphInc           = DMA_PINC_ENABLE;   /* 源地址自增 */
  hdma_m2m.Init.MemInc              = DMA_MINC_ENABLE;   /* 目标地址自增 */
  hdma_m2m.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
  hdma_m2m.Init.MemDataAlignment    = DMA_MDATAALIGN_WORD;
  hdma_m2m.Init.Mode                = DMA_NORMAL;
  hdma_m2m.Init.Priority            = DMA_PRIORITY_HIGH;
  hdma_m2m.Init.FIFOMode            = DMA_FIFOMODE_ENABLE;
  hdma_m2m.Init.FIFOThreshold       = DMA_FIFO_THRESHOLD_FULL;
  hdma_m2m.Init.MemBurst            = DMA_MBURST_INC4;
  hdma_m2m.Init.PeriphBurst         = DMA_PBURST_INC4;
  HAL_DMA_Init(&hdma_m2m);

  return HAL_DMA_Start_IT(&hdma_m2m,
                           (uint32_t)src,   /* 源地址 */
                           (uint32_t)dst,   /* 目标地址 */
                           words);           /* 传输 N 个 word */
}

UART DMA 发送(非阻塞)

/* UART1 DMA 发送:CPU 只提交请求,DMA 搬运数据,TX 完成后回调 */
volatile uint8_t uart_tx_busy = 0;

void UART_DMA_Send(uint8_t *data, uint16_t len) {
  while (uart_tx_busy);  /* 等待上次发送完成(可改为超时) */
  uart_tx_busy = 1;
  HAL_UART_Transmit_DMA(&huart1, data, len);
}

/* 发送完成回调,清除忙标志 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart->Instance == USART1) {
    uart_tx_busy = 0;
  }
}

/* 使用示例:发送 JSON 字符串 */
static uint8_t tx_buf[128];
void Report_Sensor(float temp, float humi) {
  int len = snprintf((char*)tx_buf, sizeof(tx_buf),
                     "{\"t\":%.1f,\"h\":%.1f}\r\n", temp, humi);
  UART_DMA_Send(tx_buf, len);
  /* 函数立即返回,DMA 在后台发送,CPU 可以干别的 */
}

SPI DMA 高速数据读取

/**
 * SPI1 DMA 全双工收发(同时 TX 和 RX)
 * 适合高速读取 Flash/ADC,提高总线利用率
 */
uint8_t spi_tx_buf[256];
uint8_t spi_rx_buf[256];

void SPI_DMA_Transfer(uint8_t *tx, uint8_t *rx, uint16_t len) {
  W25Q_CS_LOW();
  /* 同时启动 TX DMA 和 RX DMA */
  HAL_SPI_TransmitReceive_DMA(&hspi1, tx, rx, len);
  /* DMA 完成后触发 HAL_SPI_TxRxCpltCallback */
}

void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
  if (hspi->Instance == SPI1) {
    W25Q_CS_HIGH();
    /* 处理 spi_rx_buf 中的数据 */
  }
}

双缓冲 ADC 采集(音频级应用)

双缓冲工作流程(DMA 硬件双缓冲模式 DBM) 时间线 ────────────────────────────────────────────────────► DMA: [ 填充 buf0 ][ 填充 buf1 ][ 填充 buf0 ][ 填充 buf1 ] ↑ ↑ ↑ 切换到buf1 切换到buf0 切换到buf1 HT中断 TC中断 HT中断 │ │ CPU: [处理 buf0] [处理 buf1] [处理 buf0] 关键:CPU 处理时间必须小于 DMA 填充一个 buf 的时间! 否则 CPU 还没处理完,DMA 已经开始覆盖同一块 buf → 数据损坏 实现方式: ① HAL DBM(硬件双缓冲):配置 DMA M0AR/M1AR,设置 DBM=1 ② 软件双缓冲:DMA 循环 + HTIF/TCIF 中断,软件切换处理指针
#define BUF_SIZE  512
uint16_t adc_buf0[BUF_SIZE];   /* 双缓冲 */
uint16_t adc_buf1[BUF_SIZE];
volatile uint8_t buf_ready = 0;  /* 0=buf0就绪,1=buf1就绪 */

void ADC_DoubleBuffer_Init(void) {
  /* 启动 ADC DMA,使用 HAL 双缓冲(注:需要直接操作 DMA 寄存器) */
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf0, BUF_SIZE * 2);
  /* 使能半传输中断 */
  __HAL_DMA_ENABLE_IT(hadc1.DMA_Handle, DMA_IT_HT);
}

/* 半传输中断:DMA 已填充 buf0(前半),CPU 开始处理 buf0 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
  buf_ready = 0;  /* buf0 就绪 */
}

/* 传输完成中断:DMA 已填充 buf1(后半),CPU 开始处理 buf1 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
  buf_ready = 1;  /* buf1 就绪 */
}

void main_process(void) {
  while (1) {
    uint16_t *process_buf = (buf_ready == 0) ? adc_buf0 : adc_buf1;
    /* 在 DMA 填充另一半期间,处理当前这半 */
    Process_ADC_Data(process_buf, BUF_SIZE);
  }
}
DMA FIFO 与 Burst 传输

DMA FIFO 是 DMA 内部的缓冲,可以将多个小传输合并成一次 Burst 传输到 AHB 总线,减少总线仲裁次数,提高带宽利用率。FIFO 阈值有 1/4、1/2、3/4、Full 四种,阈值越大,延迟越高但效率越好。Burst 大小(MBURST/PBURST)必须与 FIFO 阈值匹配,否则会产生 FIFO 错误(FEIF)。若 FIFO 模式关闭(直接模式),每次外设请求触发一次单次传输。

DMA 使用最容易犯的错误

① 使用局部变量作为 DMA 缓冲:局部变量在栈上,函数返回后被回收,DMA 继续写入会破坏栈数据,导致极难定位的 HardFault。DMA 缓冲必须声明为全局或 static 变量。
② Stream/Channel 选错:必须查 Reference Manual 的 DMA Request Mapping 表,用错了 DMA 请求会导致外设触发时 DMA 无响应。
③ 缓冲区地址未对齐:字传输(Word)要求 4 字节对齐,半字(Half-Word)要求 2 字节对齐,否则触发总线错误。

DMA 传输错误处理

DMA 传输可能因总线错误、FIFO 溢出等原因失败,需要注册错误回调处理:

/* DMA 传输错误回调 */
void HAL_DMA_ErrorCallback(DMA_HandleTypeDef *hdma) {
  if (hdma->Instance == DMA2_Stream0) {
    uint32_t err = hdma->ErrorCode;
    if (err & HAL_DMA_ERROR_FE)   HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14); /* FIFO 错误 */
    if (err & HAL_DMA_ERROR_DME)  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15); /* 直接模式错误 */
    if (err & HAL_DMA_ERROR_TE)   HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13); /* 传输错误 */

    /* 重置 DMA 并重启 */
    HAL_DMA_DeInit(hdma);
    HAL_DMA_Init(hdma);
    /* 根据场景重新启动传输 */
  }
}

/* FIFO 错误(FEIF)最常见原因和解决方案 */
/* 原因:Burst 大小与 FIFO 阈值不匹配
 * 规则:FIFO 阈值 × 数据宽度 必须能被 Burst 大小整除
 *
 * 例:FIFO_THRESHOLD = FULL(4字), MemDataAlignment = WORD(4字节)
 *      PeriphBurst = INC4 → FIFO 需要 4×4=16 字节 = 4个 Word
 *      FIFO_THRESHOLD_FULL = 4 Word → 恰好匹配 ✓
 *
 * 简单方案:出现 FEIF 时,禁用 Burst(设为 SINGLE)和 FIFO(直接模式)*/
hdma.Init.FIFOMode  = DMA_FIFOMODE_DISABLE;    /* 关闭 FIFO,使用直接模式 */
hdma.Init.MemBurst  = DMA_MBURST_SINGLE;        /* 单次传输 */
hdma.Init.PeriphBurst = DMA_PBURST_SINGLE;

STM32H7 Cache 一致性问题

在 STM32H7(Cortex-M7,有 D-Cache)上使用 DMA 时,必须处理 Cache 与 DMA 之间的数据一致性:

/* STM32H7:DMA 写入缓冲区后,CPU 读前必须使 Cache 失效 */
static uint8_t dma_rx_buf[256] __attribute__((aligned(32)));  /* 32字节 Cache 行对齐 */

void DMA_ReceiveComplete(void) {
  /* DMA 传输完成后,使 CPU D-Cache 中对应区域失效
   * 确保 CPU 读到的是 DMA 写入的最新数据,而非 Cache 中的旧值 */
  SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf));

  /* 现在可以安全读取 dma_rx_buf */
  Process_Data(dma_rx_buf, sizeof(dma_rx_buf));
}

void DMA_PrepareTransmit(void) {
  /* CPU 写数据到 tx 缓冲区后,DMA 读前必须刷 Cache */
  static uint8_t dma_tx_buf[256] __attribute__((aligned(32)));
  memcpy(dma_tx_buf, source_data, sizeof(dma_tx_buf));
  /* 刷 Cache:将 CPU Cache 中的数据写回 RAM,让 DMA 读到最新值 */
  SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, sizeof(dma_tx_buf));
  /* 现在可以安全启动 DMA TX */
}
本章小结

DMA 让外设与内存直接交换数据,CPU 无需介入搬运,可以并行做其他工作。STM32F4 有 2 个 DMA 控制器,各 8 个 Stream,每个 Stream 有独立的优先级和 FIFO。三种传输方向:P→M(ADC/UART 接收)、M→P(UART 发送/DAC 输出)、M→M(内存复制,仅 DMA2)。循环模式 + 半传输中断实现双缓冲流水线处理。DMA 缓冲必须声明为全局/static 变量(不能是局部变量)。FEIF 错误通常由 Burst/FIFO 参数不匹配引起,解决办法是禁用 Burst 和 FIFO。STM32H7 有 D-Cache,DMA 传输前后需要 SCB_CleanDCache/SCB_InvalidateDCache 维护一致性。