STM32 DMA — 直接存储区访问(存储器到存储器)

野火 EBF STM32F103 Mini HAL 库:用 DMA1 把一段常量数组自动搬运到内存数组,演示"不占用 CPU"的数据传输本质。

STM32 DMA — 直接存储区访问(存储器到存储器)

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程15-DMA—直接存储区访问 / DMA—存储器到存储器模式(并补充"存储器到外设"思路)
参考源码User/main.c(存储器到存储器)

DMA(Direct Memory Access,直接存储器访问)允许外设与内存、内存与内存之间直接搬运数据,全程无需 CPU 参与。CPU 只需"下发搬运指令",搬运由 DMA 控制器在后台完成,大幅解放内核。本文从最直观的"内存→内存"搬运入手,再延伸到"内存→外设"。


一、功能简介

例程准备两个数组:

  • 源数组 aSRC_Const_Buffer:32 个 32 位常量(已初始化);
  • 目标数组 aDST_Buffer:32 个 32 位空位。

用 DMA 把源数组整块搬到目标数组,搬运完成后比较两数组是否一致:一致则点亮 LED2(成功),否则点亮 LED1(失败)。

核心知识点:

  1. DMA 的方向、地址递增、数据宽度、传输模式、优先级;
  2. STM32F1 的 DMA 通道与触发源(存储器到存储器必须用 DMA1);
  3. 用标志位轮询传输完成。

二、硬件准备

项目 说明
开发板 野火 EBF STM32F103 Mini
DMA 控制器 DMA1(F103 仅 1 个 DMA 控制器,7 个通道)
本例通道 DMA1_Channel6(软件触发,M2M 模式专用)
指示 LED1 → PC2(失败)、LED2 → PC3(成功)

存储器到存储器(M2M)模式是 DMA 的特例:它不依赖外设硬件请求,而是软件触发一次就自动连续搬完,非常适合做 memcpy 加速。


三、完整代码(带中文注释)

1. main.c(M2M 搬运 + 校验)

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include "main.h"
#include "stm32f1xx.h"
#include "./led/bsp_led.h"

/* 存储器到存储器传输必须使用 DMA1 */
DMA_HandleTypeDef DMA_Handle;

#define DMA_STREAM          DMA1_Channel6
#define DMA_STREAM_CLOCK()  __HAL_RCC_DMA1_CLK_ENABLE();
#define BUFFER_SIZE         32

/* 源数据:常量数组(放在 Flash,DMA 把它读进 SRAM) */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE] = {
  0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
  0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
  0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
  0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
  0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
  0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
  0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
  0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};

/* 目标数组:等待被填充 */
uint32_t aDST_Buffer[BUFFER_SIZE];

static void DMA_Config(void);
uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength);

int main(void)
{
  uint8_t TransferStatus;
  SystemClock_Config();
  LED_GPIO_Config();

  DMA_Config();    // 配置并启动 DMA 搬运

  /* 轮询等待 DMA 传输完成标志(TCIF6) */
  while(__HAL_DMA_GET_FLAG(&DMA_Handle, DMA_FLAG_TC6) == RESET)
  {
  }

  /* 比较源与目标是否一致 */
  TransferStatus = Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
  if (TransferStatus == 0)
    LED1_ON;       // 不一致 => 失败灯
  else
    LED2_ON;       // 一致 => 成功灯

  while (1);
}

/* DMA 配置:存储器 -> 存储器 */
static void DMA_Config(void)
{
  HAL_StatusTypeDef DMA_status = HAL_ERROR;
  DMA_STREAM_CLOCK();

  DMA_Handle.Instance                 = DMA_STREAM;                 // DMA1_Channel6
  DMA_Handle.Init.Direction          = DMA_MEMORY_TO_MEMORY;        // 存储器到存储器
  DMA_Handle.Init.PeriphInc          = DMA_PINC_ENABLE;             // 外设(源)地址递增
  DMA_Handle.Init.MemInc             = DMA_MINC_ENABLE;             // 内存(目标)地址递增
  DMA_Handle.Init.PeriphDataAlignment= DMA_PDATAALIGN_WORD;         // 源数据 32 位
  DMA_Handle.Init.MemDataAlignment   = DMA_MDATAALIGN_WORD;         // 目标数据 32 位
  DMA_Handle.Init.Mode               = DMA_NORMAL;                 // 普通模式(一次)
  DMA_Handle.Init.Priority           = DMA_PRIORITY_MEDIUM;         // 中等优先级
  HAL_DMA_Init(&DMA_Handle);

  /* 启动传输:源地址 -> 目标地址,共 BUFFER_SIZE 个数据项 */
  DMA_status = HAL_DMA_Start(&DMA_Handle,
                             (uint32_t)aSRC_Const_Buffer,
                             (uint32_t)aDST_Buffer,
                             BUFFER_SIZE);
  if (DMA_status != HAL_OK)
  {
    while (1)   // 启动失败,闪烁报警
    {
      LED1_ON; Delay(0xFFFFFF); LED1_TOGGLE; Delay(0xFFFFFF);
    }
  }
}

/* 比较两个数组是否完全一致 */
uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength)
{
  while(BufferLength--)
  {
    if(*pBuffer != *pBuffer1) return 0;   // 有差异立即返回 0
    pBuffer++;   pBuffer1++;
  }
  return 1;   // 全部相等返回 1
}

四、代码分段讲解

1. DMA 到底省了什么?

普通 for 循环拷贝数组,每个字节都要 CPU 取指、读源、写目标——CPU 全程被占。DMA 接管总线后,CPU 在 HAL_DMA_Start 之后就可以去干别的事(本例只是 while 等标志,实际项目里可以并行运算)。对大块数据 / 高速外设(ADC、SPI、USART、LCD)而言,DMA 是性能关键。

2. 四个关键配置

字段 本例 含义
Direction DMA_MEMORY_TO_MEMORY 存储器→存储器(软件触发)
PeriphInc / MemInc ENABLE 源、目标地址都递增(否则会反复写同一格)
PeriphDataAlignment / MemDataAlignment WORD(32位) 数据宽度要一致,避免错位
Mode DMA_NORMAL 普通模式:搬完一次就停(对比循环 DMA_CIRCULAR

注意:这里"Periph"一方是源数组,在 M2M 模式下 HAL 把源/目标统称为外设/内存,只需保证两者都递增、宽度相同即可。

3. 通道与触发源

F103 的 DMA1 有 7 个通道,每个通道固定对应某些外设请求(如 ADC1→CH1、SPI1→CH3、USART1_TX→CH4)。只有 DMA1 支持 M2M 软件触发,所以本例用 DMA1_Channel6。换成"内存→USART"时,就要选 USART 对应的专属通道,并由外设硬件触发。

4. 传输完成怎么知道?

__HAL_DMA_GET_FLAG(&DMA_Handle, DMA_FLAG_TC6) 读取传输完成标志(TCIF)。本例用轮询等待;实际中更常用传输完成中断DMA_IT_TC),在 HAL_DMA_XferCpltCallback 里接着处理,彻底不阻塞 CPU。

5. 补充:存储器到外设(如 USART 发大报)

野火例程还有"DMA—存储器到外设模式":把 Direction 设为 DMA_MEMORY_TO_PERIPH,把外设地址填成 &USART1->DR,源填发送缓冲区,再 HAL_UART_Transmit_DMA()。这样 CPU 一次性把几百字节交给 DMA,串口慢慢发、CPU 同时翻转 LED——直观展示了"DMA 让 CPU 腾出手"。


五、实验现象

  1. 下载程序,DMA 把 32 个常量从源数组搬进目标数组;
  2. 比较两数组完全一致 → LED2 点亮(成功灯);
  3. 若把源/目标数据宽度设错(比如一方 WORD 一方 BYTE),拷贝错位导致比较失败 → LED1 点亮。

六、常见问题

Q1:传输后数据错位/不一致?

  • 检查源和目标数据宽度是否一致(都 WORD 或都 HALFWORD);
  • 检查 PeriphInc/MemInc 是否都 ENABLE(单地址重复写会出错)。

Q2:DMA 一直不完成(标志位不来)?

  • M2M 模式确认用的是 DMA1;
  • 确认 HAL_DMA_Start 返回的 DMA_status == HAL_OK,且传输长度不为 0。

Q3:为什么我的外设 DMA 没动?

  • 外设 DMA 需要对应的专属通道外设硬件触发,不能随便选通道;
  • 外设自身要先使能(如 USART 要先 HAL_UART_Init)。

Q4:NORMAL 和 CIRCULAR 怎么选?

  • 单次传输(如发一帧数据)用 NORMAL
  • 连续流(如 ADC 持续采集、DAC 持续输出波形)用 CIRCULAR,搬完自动从头再来。

七、小结

  • DMA 是"数据搬运工",让 CPU 从繁琐的拷数据中解放;
  • M2M 配置要点:方向 M2M、双端递增、宽度一致、软件触发
  • 掌握 DMA 后,ADC、SPI、USART、DAC 等高速外设才能真正发挥性能。

八、拓展应用

  1. 大数组 memcpy 加速:把 for 循环拷贝换成 DMA M2M,批量搬运 SRAM/Flash 数据更快。
  2. ADC + DMA 连续采样:如本博客 ADC 章节,DMA 把转换结果循环搬进数组,零 CPU 占用。
  3. USART/SPI 大块发送:用 HAL_UART_Transmit_DMA / HAL_SPI_Transmit_DMA,发送几百 KB 时 CPU 仍可并行处理。
  4. 双缓冲(Double Buffer):配合 DMA 半传输中断,实现"一边 DMA 填半区、CPU 处理另半区"的无缝流水线,适合音频、图像流。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计