DMA

STM32F10x DMA 直接存储器访问原理、通道映射与标准库 M2M 转运、ADC 扫描采集示例

对应江协科技 STM32 入门教程 DMA 章节。DMA 可在不占用 CPU 逐字节搬运的情况下完成外设与存储器之间的数据传输,常与 ADC 扫描模式 配合实现多通道连续采样。


DMA 简介

  • DMA(Direct Memory Access,直接存储器访问)由专用控制器完成数据搬运,无需 CPU 参与每次读写
  • 适用场景:大批量数据、重复性传输(ADC 结果数组、串口 TX/RX 缓冲、SPI 收发、内存块拷贝)。
  • STM32F103 共有 12 个独立通道DMA1(7 通道) + DMA2(5 通道)(中容量 C8 等部分型号无 DMA2,以数据手册为准)。
  • 每个通道支持 软件触发特定硬件触发(如 ADC EOC、USART TXE)。
项目说明
总线DMA 挂在 AHB,可访问 SRAM 与外设
与 CPU 关系与 CPU 并行工作,争用总线时硬件仲裁
传输单位以「数据宽度 × 次数」计,BufferSize=4 + 字节宽 = 搬 4 字节
完成标志TC(Transfer Complete);另有 HT(半传输)、TE(传输错误)

有 DMA 与无 DMA 对比

方式CPU 负担典型场景
CPU 轮询 ADC_GetConversionValue每采样一次都要等 EOC、读 DR单通道、低速
CPU for 循环拷贝数组每个字节一次 load/store小数组、偶尔拷贝
DMA只配置一次,硬件自动搬ADC 扫描、串口大数据、音频缓冲

存储器映像

类型起始地址存储器用途
ROM0x0800 0000程序存储器 Flash存储 C 语言编译后的程序代码
0x1FFF F000系统存储器存储 BootLoader,用于串口下载
0x1FFF F800选项字节存储一些独立于程序代码的配置参数
RAM0x2000 0000运行内存 SRAM存储运行过程中的临时变量
0x4000 0000外设寄存器存储各个外设的配置参数
0xE000 0000内核外设寄存器存储内核各个外设的配置参数

DMA 可在 SRAM ↔ 外设寄存器SRAM ↔ SRAM(M2M) 之间搬运;不能直接对 Flash 写入(Flash 编程走专用接口)。


DMA 框图

DMA框图

DMA 作为 AHB 主设备,在外设数据寄存器SRAM 缓冲区 之间建立通路;CPU 只负责配置通道,搬运过程由 DMA 硬件自动完成。


DMA 基本结构

DMA基本结构

DMA 基本结构详解

这张图展示了 DMA 控制器的核心工作原理,分为数据传输路径控制触发路径两大部分:

1. 数据传输路径(上半部分)

外设配置块(左侧)

  • 起始地址:外设数据寄存器地址(如 &ADC1->DRUSART1->DR
  • 数据宽度:8 / 16 / 32 位
  • 地址是否自增:外设寄存器通常 不增(固定 DR)

存储器配置块(右侧)

  • 起始地址:数组或缓冲区首地址
  • 数据宽度:需与源/目的匹配规则一致
  • 地址是否自增:数组缓冲通常 自增

方向控制

  • 外设 → 存储器:ADC、USART RX 等
  • 存储器 → 外设:USART TX、SPI TX 等
  • M2M:存储器 → 存储器(DMA_M2M_Enable,双端地址均在 SRAM)

2. 控制与触发路径(下半部分)

传输计数器 CNDTR

  • 记录剩余传输次数,每传一个「数据宽度单位」减 1
  • 减到 0 置位 TC(传输完成)

模式(DMA_Mode)

模式行为典型用途
Normal传完停止,CNDTR 不归零重装M2M 单次拷贝、一次性缓冲
Circular传完自动重装 CNDTR,从头再搬ADC 连续扫描、环形音频缓冲

触发源

  • 硬件触发:ADC 每转换完成一次、USART 发送寄存器空等
  • 软件触发:M2M 模式下 DMA_Cmd(ENABLE) 即开始

DMA_InitTypeDef 字段说明

标准库 DMA_Init() 各成员含义(配置前建议对照此表):

成员含义
DMA_PeripheralBaseAddr外设端地址(M2M 时作数组地址)
DMA_MemoryBaseAddr存储器端地址(M2M 时作目的数组地址)
DMA_DIRPeripheralSRC 外设→存储器;PeripheralDST 存储器→外设
DMA_BufferSize传输次数(=CNDTR 初值),非字节数(需结合宽度)
DMA_PeripheralInc / DMA_MemoryInc每次传输后地址是否 +1(按宽度步进)
DMA_PeripheralDataSize / DMA_MemoryDataSizeByte / HalfWord / Word
DMA_ModeNormal 或 Circular
DMA_Priority多通道同时请求时的仲裁优先级
DMA_M2M使能存储器到存储器(此时无真实外设参与)

DMA 请求

定义哪些外设可以从哪个通道触发 DMA 传输。

DMA请求

STM32F103 DMA1 常用映射(节选)

外设DMA1 通道
ADC1Channel 1
SPI1_RXChannel 2
SPI1_TXChannel 3
USART1_TXChannel 4
USART1_RXChannel 5
I2C1_TXChannel 6
I2C1_RXChannel 7

DMA2(中高密度)节选:ADC3 Ch2、TIM1_CH1 Ch6、USART3_TX Ch2 等。

配置前必须查参考手册 DMA 请求映射表一个通道在同一时刻只服务一个外设请求,不可与手册冲突。


数据宽度与对齐

数据宽度与对齐
规则说明
低 → 高低位数据迁入高位,高位自动补 0
高 → 低高位数据迁入低位,多出的高位舍弃
最佳实践源、目的、外设寄存器宽度一致

示例:

外设 HalfWord(16bit) → 存储器 Byte(8bit):高 8 位被丢弃 ❌
外设 HalfWord         → 存储器 HalfWord:ADC DR → uint16_t 数组 ✅

ADC 数据寄存器为 16 位有效(12 位 ADC),DMA 应设 DMA_PeripheralDataSize_HalfWordDMA_MemoryDataSize_HalfWord


数据转运 + DMA(M2M 示意)

数据转运+DMA

江协「数组转运」示例配置要点:

参数
传输方向M2M Enable,逻辑上「外设端=源数组、存储器端=目的数组」
源/目的DataA[]DataB[]
地址自增源、目的均 Enable
计数器数组元素个数(字节宽时等于字节数)
模式Normal(传完即停)
触发软件DMA_Cmd(ENABLE) 启动

一次 M2M 传输时序:

CPU 调用 MyDMA_Transfer()
  → DISABLE 通道
  → SetCurrDataCounter(4)     重装次数
  → ENABLE 通道               立即开始
  → 硬件:DataA[0..3] 逐个写入 DataB[0..3]
  → TC 置位
  → CPU 轮询 TC / 或中断

ADC 扫描模式 + DMA

ADC扫描模式+DMA

多通道 ADC 经典组合(解决「轮询四通道太慢、占 CPU」问题):

ADC:连续转换 + 扫描模式 + 软件触发一次
DMA:外设(ADC1->DR) → 存储器(AD_Value[]) + 循环模式

一轮扫描过程(4 通道):

通道0 转换完成 → DMA 搬 DR → AD_Value[0]
通道1 转换完成 → DMA 搬 DR → AD_Value[1]
通道2 转换完成 → DMA 搬 DR → AD_Value[2]
通道3 转换完成 → DMA 搬 DR → AD_Value[3]
CNDTR 减到 0 → Circular 模式自动重装 → 下一轮扫描

main直接读 AD_Value[] 即可,无需 while(EOC)。详见 ADC


标准库 DMA 初始化流程

外设 → 存储器(以 ADC 为例)

1. RCC_AHBPeriphClockCmd(DMA1)              /* 开 DMA 时钟 */
2. 填写 DMA_InitTypeDef
3. DMA_Init(DMA1_Channel1, &init)
4. DMA_Cmd(ENABLE)
5. ADC_DMACmd(ADC1, ENABLE)                  /* 允许 ADC 触发 DMA */
6. ADC 初始化、校准、SoftwareStartConv

M2M(数组拷贝)

1. 开 DMA1 时钟
2. DMA_Init(M2M_Enable,Normal,双端 Inc)
3. DMA_Cmd(DISABLE)                         /* 先不启动 */
4. 需要时:Transfer() 里重装 CNDTR 再 ENABLE */

重复启动 Normal 模式(修改 CNDTR 前必须先 DISABLE 通道):

DMA_Cmd(DMA1_Channel1, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1, Size);
DMA_Cmd(DMA1_Channel1, ENABLE);

DMA 标志与中断(了解)

标志含义
DMA_FLAG_TCx传输完成(CNDTR=0)
DMA_FLAG_HTx半传输完成(适合双缓冲)
DMA_FLAG_TEx传输错误

可用 DMA_ITConfig + NVIC 在中断里处理,避免主循环 while 阻塞。


代码示例

DMA 数据转运(M2M)

两个 SRAM 数组 之间拷贝,演示 DMA 基本配置。使用 DMA1 通道 1(M2M 不占用外设映射,但教程统一用 Ch1)。

MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H

#include "stm32f10x.h"

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);

#endif
MyDMA.c
#include "MyDMA.h"

/* 记住 Init 时的传输次数,Transfer 时重装 CNDTR */
static uint16_t MyDMA_Size;

/**
 * @brief  DMA1 通道 1,存储器到存储器(M2M),字节宽度
 * @param  AddrA  源数组首地址(如 (uint32_t)DataA)
 * @param  AddrB  目的数组首地址
 * @param  Size   传输次数;字节宽时 = 字节个数
 */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;

	/* 1. 开启 DMA1 时钟(DMA 在 AHB 总线) */
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	/* 2. 配置 DMA1 通道 1 */
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;                      /* M2M:外设端接源地址 */
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  /* 每次搬 1 字节 */
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;          /* 源地址每传一次 +1 */
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;                            /* 存储器端接目的地址 */
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;                   /* 目的地址每传一次 +1 */
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;                       /* 方向:外设端→存储器端(M2M 固定写法) */
	DMA_InitStructure.DMA_BufferSize = Size;                                 /* 共搬 Size 次 */
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;                            /* 传完停止,不循环 */
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;                              /* 关键:存储器到存储器 */
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);

	/* 3. 初始化后不使能,等 MyDMA_Transfer() 再启动 */
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

/**
 * @brief  启动一次 M2M 传输并阻塞等待完成
 */
void MyDMA_Transfer(void)
{
	/* 修改 CNDTR 前必须关闭通道 */
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);   /* 重装传输次数 */
	DMA_Cmd(DMA1_Channel1, ENABLE);                      /* 软件触发:使能即开始 */

	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);   /* 等待通道 1 传输完成 */
	DMA_ClearFlag(DMA1_FLAG_TC1);                        /* 清 TC 标志,便于下次使用 */
}
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};   /* 源数组 */
uint8_t DataB[] = {0, 0, 0, 0};              /* 目的数组,初始为 0 */

int main(void)
{
	OLED_Init();

	/* 把 DataA、DataB 首地址和长度 4 传给 DMA 初始化 */
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);

	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);   /* 显示数组地址,观察均在 SRAM */
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);

	while (1)
	{
		/* 修改源数组,目的数组尚未更新 */
		DataA[0]++;
		DataA[1]++;
		DataA[2]++;
		DataA[3]++;

		OLED_ShowHexNum(2, 1, DataA[0], 2);
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2);    /* 转运前 DataB 仍为旧值 */
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);

		Delay_ms(1000);

		MyDMA_Transfer();                        /* DMA 拷贝 DataA → DataB */

		OLED_ShowHexNum(2, 1, DataA[0], 2);
		OLED_ShowHexNum(2, 4, DataA[1], 2);
		OLED_ShowHexNum(2, 7, DataA[2], 2);
		OLED_ShowHexNum(2, 10, DataA[3], 2);
		OLED_ShowHexNum(4, 1, DataB[0], 2);    /* 转运后 DataB 应与 DataA 一致 */
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);

		Delay_ms(1000);
	}
}

DMA + ADC 多通道

硬件:PA0~PA3 接四路模拟电压;ADC1 连续 + 扫描DMA1 通道 1 循环搬运到 AD_Value[4]

AD.h
#ifndef __AD_H
#define __AD_H

#include "stm32f10x.h"

/* 全局数组:DMA 自动写入,main 直接读 */
extern uint16_t AD_Value[4];

void AD_Init(void);

#endif
AD.c
#include "AD.h"

/* DMA 目标缓冲;volatile 可选(DMA 写、CPU 读) */
uint16_t AD_Value[4];

void AD_Init(void)
{
	/* ---------- 时钟 ---------- */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);   /* ADCCLK = 72MHz/6 = 12MHz */

	/* ---------- GPIO:PA0~PA3 模拟输入 ---------- */
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	/* ---------- ADC 规则组:序列 1~4 对应通道 0~3 ---------- */
	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);

	/* ---------- ADC:连续 + 扫描,软件触发一次后自动循环 ---------- */
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;    /* 一轮扫完后立刻下一轮 */
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;          /* 按序列依次采 CH0~CH3 */
	ADC_InitStructure.ADC_NbrOfChannel = 4;
	ADC_Init(ADC1, &ADC_InitStructure);

	/* ---------- DMA1 Ch1:ADC1->DR → AD_Value[] ---------- */
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   /* DR 地址固定 */
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;           /* 依次填入 [0][1][2][3] */
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;                 /* 外设 → 存储器 */
	DMA_InitStructure.DMA_BufferSize = 4;                              /* 与通道数一致 */
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                    /* 循环,配合连续 ADC */
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                       /* 外设触发,非 M2M */
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);

	/* ---------- 使能顺序:DMA 通道 → ADC-DMA 请求 → ADC ---------- */
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);          /* 每次转换结束产生 DMA 请求 */
	ADC_Cmd(ADC1, ENABLE);

	/* ---------- ADC 校准(上电一次) ---------- */
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);

	/* ---------- 软件触发一次;连续模式下之后自动反复转换 ---------- */
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

int main(void)
{
	OLED_Init();
	AD_Init();    /* 初始化后 ADC+DMA 已在后台运行 */

	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");

	while (1)
	{
		/* 无需等待 EOC;DMA 持续刷新 AD_Value[0..3] */
		OLED_ShowNum(1, 5, AD_Value[0], 4);
		OLED_ShowNum(2, 5, AD_Value[1], 4);
		OLED_ShowNum(3, 5, AD_Value[2], 4);
		OLED_ShowNum(4, 5, AD_Value[3], 4);

		/* 可选:电压换算 (float)AD_Value[i] / 4095.0f * 3.3f */

		Delay_ms(100);   /* 仅降低显示刷新率,不影响 ADC 采样 */
	}
}

数据流:

PA0~PA3 → ADC 扫描 → DR → DMA → AD_Value[0..3] → main 显示
         ↑________________连续循环________________|

M2M 与 ADC+DMA 配置对比

项目M2M 数组转运ADC + DMA
DMA_M2MEnableDisable
外设地址源数组(当作 Peripheral)&ADC1->DR
外设地址自增EnableDisable
存储器地址自增EnableEnable
数据宽度ByteHalfWord
模式NormalCircular
触发软件 DMA_CmdADC 硬件 每次 EOC
ADC 模式连续 + 扫描
CPUTransfer() 等 TC只读数组

常见问题

Q:M2M 为什么 DMA_DIR_PeripheralSRC
A:M2M 没有真实外设,标准库约定用「外设端」作源、「存储器端」作目的,两端地址都在 SRAM。

Q:Circular 模式下 AD_Value 会被覆盖吗?
A:会不断刷新为最新一轮扫描结果;若要在「采满一轮」时再处理,可用 DMA 半传输/传输完成中断,或双缓冲。

Q:能否用 DMA2 采 ADC1?
A:不能,必须按手册映射表:ADC1 → DMA1 Channel 1

Q:BufferSize 与数组长度不一致会怎样?
A:可能只搬部分数据,或越界写 SRAM,务必与通道数/数组长度一致。


注意事项

  1. 先开 DMA 时钟(AHB),再 DMA_Init
  2. 通道与外设映射必须查手册,ADC1 用 DMA1 Channel1。
  3. Normal 模式改 CNDTR 前必须先 DMA_Cmd(DISABLE)
  4. Circular + 连续 ADC 成对使用;单独 Circular 而不连续触发可能只跑一轮。
  5. 数据宽度与 DR、数组类型一致(ADC 用半字 + uint16_t)。
  6. M3 无 DCache,DMA 与 CPU 共享 SRAM 一般无需额外 cache 维护。
  7. 高负载时用 DMA 中断 替代 while(TC),避免阻塞。
  8. F103C8 等确认是否有 DMA2,避免初始化不存在的控制器。

总结

要点说明
作用外设 ↔ 存储器批量搬运,减轻 CPU
通道与外设固定映射,ADC1 = DMA1 Ch1
M2M数组拷贝,Normal + 软件使能
ADC连续扫描 + Circular DMA → AD_Value[] 自动刷新
完成DMA_FLAG_TCx、HT、TE 或中断
关键CNDTR=次数;改计数前 DISABLE 通道