AT32 DMA — Flash 到 SRAM 数据搬运(CPU 完全不参与)

AT32F403A DMA 内存到内存搬运,中断通知完成。理解 DMA 的核心概念。

DMA — Flash 到 SRAM 数据搬运

难度: ⭐⭐(理解 DMA 概念即可)
固件库: AT32F403A_407_Firmware_Library V2.2.2
源文件: project/at_start_f403a/examples/dma/flash_to_sram/src/main.c


这个实验是干嘛的?

DMA(Direct Memory Access,直接存储器访问)是 MCU 里最强大的"搬运工"。

没有 DMA 时:CPU 自己一个字节一个字节地从 Flash 复制到 SRAM,复制 128 字节就要执行 128 次循环,CPU 什么都干不了。

有了 DMA 时:你告诉 DMA “把这块数据从这里搬到那里”,DMA 硬件自动完成,CPU 空闲了。搬运完成后 DMA 发一个中断通知 CPU。

本实验用 DMA 的内存到内存模式,把 Flash 中的 32 个 32 位数据搬到 SRAM,搬运完成后触发中断,然后比对数据。


硬件准备

不需要任何外部硬件! 这是纯内部操作,Flash → SRAM。


DMA 核心概念

DMA 的三种搬运方向

方向 说明 典型用途
外设→内存 从外设读数据存到内存 ADC 采样、串口接收
内存→外设 从内存取数据发到外设 串口发送、DAC 输出
内存→内存 内存之间复制数据 数据搬运、缓冲区拷贝

DMA 的关键参数

参数 含义
源地址 数据从哪来
目标地址 数据搬到哪去
数据宽度 每次搬运多少字节(字节/半字/字)
地址递增 搬完一个后地址是否 +1
传输数量 总共搬多少个
循环模式 搬完后是否自动重新开始

完整代码

 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
92
93
94
95
96
97
#include "at32f403a_407_board.h"
#include "at32f403a_407_clock.h"

#define BUFFER_SIZE  32

/* 源数据:存在 Flash 中(const 修饰) */
const uint32_t src_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
};

/* 目标缓冲区:存在 SRAM 中 */
uint32_t dst_buffer[BUFFER_SIZE];

dma_init_type dma_init_struct = {0};
__IO uint32_t data_counter_end = 0x01;  // 初始值不能为 0!
error_status transfer_status = ERROR;

/* DMA 中断服务函数 */
void DMA1_Channel1_IRQHandler(void)
{
  if(dma_interrupt_flag_get(DMA1_FDT1_FLAG) != RESET)
  {
    data_counter_end = 0;  // 标记搬运完成
    dma_flag_clear(DMA1_FDT1_FLAG);  // 必须清标志位!
  }
}

/* 缓冲区比对 */
error_status buffer_compare(const uint32_t* p1, const uint32_t* p2, uint16_t len)
{
  while(len--)
  {
    if(*p1 != *p2) return ERROR;
    p1++;
    p2++;
  }
  return SUCCESS;
}

int main(void)
{
  system_clock_config();
  at32_board_init();

  /* 第一步:使能 DMA1 时钟 */
  crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);

  /* 第二步:配置 DMA1 通道 1 */
  dma_reset(DMA1_CHANNEL1);

  dma_init_struct.direction = DMA_DIR_MEMORY_TO_MEMORY;  // 内存到内存
  dma_init_struct.peripheral_base_addr = (uint32_t)src_const_buffer;  // 源地址(Flash)
  dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_WORD;  // 32 位
  dma_init_struct.peripheral_inc_enable = TRUE;  // 源地址递增

  dma_init_struct.memory_base_addr = (uint32_t)dst_buffer;  // 目标地址(SRAM)
  dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_WORD;  // 32 位
  dma_init_struct.memory_inc_enable = TRUE;  // 目标地址递增

  dma_init_struct.buffer_size = BUFFER_SIZE;  // 搬 32 个
  dma_init_struct.priority = DMA_PRIORITY_MEDIUM;
  dma_init_struct.loop_mode_enable = FALSE;  // 单次搬运

  dma_init(DMA1_CHANNEL1, &dma_init_struct);

  /* 第三步:使能传输完成中断 */
  dma_interrupt_enable(DMA1_CHANNEL1, DMA_FDT_INT, TRUE);

  /* 第四步:配置 NVIC 中断优先级 */
  nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);
  nvic_irq_enable(DMA1_Channel1_IRQn, 1, 0);

  /* 第五步:启动 DMA */
  dma_channel_enable(DMA1_CHANNEL1, TRUE);

  /* 等待 DMA 搬运完成(中断会把 data_counter_end 清零) */
  while(data_counter_end != 0) {}

  /* 第六步:比对数据 */
  transfer_status = buffer_compare(src_const_buffer, dst_buffer, BUFFER_SIZE);

  if(transfer_status == SUCCESS)
  {
    at32_led_on(LED2);
    at32_led_on(LED3);
    at32_led_on(LED4);  // 3 个 LED 全亮 = 成功
  }

  while(1) {}
}

代码分段讲解

1. 源数据和目标缓冲区

1
2
const uint32_t src_const_buffer[BUFFER_SIZE] = { ... };  // Flash 中
uint32_t dst_buffer[BUFFER_SIZE];                         // SRAM 中
  • const 修饰 → 编译器把它放在 Flash 中(只读)
  • 没有 const → 放在 SRAM 中(可读写)
  • DMA 就是从 Flash 的地址搬到 SRAM 的地址

2. DMA 方向

1
dma_init_struct.direction = DMA_DIR_MEMORY_TO_MEMORY;

本实验是内存到内存模式。ADC+DMA 那个实验用的是外设到内存

3. 地址递增

1
2
dma_init_struct.peripheral_inc_enable = TRUE;  // 源地址递增
dma_init_struct.memory_inc_enable = TRUE;       // 目标地址递增

两个都要递增!因为我们要搬 32 个数据:

  • 第 1 个:从 src[0] 搬到 dst[0]
  • 第 2 个:从 src[1] 搬到 dst[1]
  • 第 32 个:从 src[31] 搬到 dst[31]

如果地址不递增,就会一直搬同一个位置的数据。

4. 中断通知

1
dma_interrupt_enable(DMA1_CHANNEL1, DMA_FDT_INT, TRUE);

FDT = Full Data Transfer,全部数据传输完成。

中断服务函数中:

1
2
data_counter_end = 0;  // 标记完成
dma_flag_clear(DMA1_FDT1_FLAG);  // 必须清标志位!不清会反复进中断

5. 等待完成

1
while(data_counter_end != 0) {}

data_counter_end 初始值设为 1,中断把它清零。主循环在这里等待。

注意:这个变量必须用 volatile 修饰(__IO),否则编译器可能优化掉这个循环。


实验现象

  • 3 个 LED 全部亮起 → DMA 搬运成功,数据一致
  • LED 不亮 → 数据比对失败(不应该发生)

DMA 传输速度

DMA 搬运 32 个 32 位数据(128 字节)需要多少时间?

  • DMA 时钟 = 系统时钟(240MHz)
  • 每个字需要 2 个时钟周期(读 + 写)
  • 32 个字 = 64 个时钟周期 = 64 / 240MHz ≈ 267ns

267 纳秒搬完 128 字节! 如果用 CPU 循环复制,至少需要 100+ 个时钟周期。


常见问题

Q: 数据比对失败?

  • 检查源地址和目标地址是否正确
  • 检查数据宽度是否匹配(本例用 32 位字)
  • 检查 buffer_size 是否正确

Q: 中断不触发?

  • 是否使能了 DMA 中断?
  • NVIC 是否配置了优先级?
  • 中断标志是否清除?

Q: 想用循环模式?

  • loop_mode_enable = TRUE:搬完后自动重新开始
  • 适合 ADC 连续采样:DMA 反复把 ADC 数据搬到缓冲区

小结

概念 说明
DMA 硬件自动搬运数据,不占 CPU
内存到内存 最简单的 DMA 模式
地址递增 搬多个数据时必须使能
FDT 中断 全部搬完后触发
volatile 防止编译器优化掉等待循环

学了 DMA 能做什么?

DMA 是"高性能嵌入式系统"的基石:

数据采集

项目 原理 难度
高速 ADC 采集 定时器触发 ADC + DMA 自动存储 ⭐⭐⭐
音频录音 I2S + DMA → 高速音频数据采集 ⭐⭐⭐
示波器 ADC + DMA + 双缓冲 → 连续波形采集 ⭐⭐⭐⭐
多通道同步采集 多 ADC + DMA → 同时采集多路信号 ⭐⭐⭐⭐

数据输出

项目 原理 难度
DMA 波形生成 DMA + DAC → 输出任意波形(正弦、三角) ⭐⭐⭐
LED 灯带驱动 DMA + GPIO 精确时序 → WS2812 驱动 ⭐⭐⭐
高速串口发送 DMA + USART → 大数据量快速发送 ⭐⭐
LCD 刷屏 DMA + SPI → 高速刷新屏幕 ⭐⭐⭐

性能优化

项目 原理 难度
内存拷贝加速 DMA 内存到内存 → 比 CPU 循环快 ⭐⭐
双缓冲技术 DMA 搬 A 缓冲时,CPU 处理 B 缓冲 ⭐⭐⭐⭐
零拷贝通信 DMA 直接在外设和内存之间搬运,不经过 CPU ⭐⭐⭐⭐

核心思路:DMA 的价值在于解放 CPU。没有 DMA,CPU 要亲自搬运每一个字节,什么都干不了。有了 DMA,CPU 只需要"发号施令",然后去做更重要的事(计算、控制、通信)。在高速数据采集、音频处理、显示刷新等场景,DMA 是不可或缺的。


下一步:学会了 DMA,可以学 FLASH 读写,了解如何用内部 Flash 存储用户数据。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计