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 扫描、串口大数据、音频缓冲 |
存储器映像
| 类型 | 起始地址 | 存储器 | 用途 |
|---|---|---|---|
| ROM | 0x0800 0000 | 程序存储器 Flash | 存储 C 语言编译后的程序代码 |
| 0x1FFF F000 | 系统存储器 | 存储 BootLoader,用于串口下载 | |
| 0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 | |
| RAM | 0x2000 0000 | 运行内存 SRAM | 存储运行过程中的临时变量 |
| 0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 | |
| 0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
DMA 可在 SRAM ↔ 外设寄存器 或 SRAM ↔ SRAM(M2M) 之间搬运;不能直接对 Flash 写入(Flash 编程走专用接口)。
DMA 框图
DMA 作为 AHB 主设备,在外设数据寄存器与 SRAM 缓冲区 之间建立通路;CPU 只负责配置通道,搬运过程由 DMA 硬件自动完成。
DMA 基本结构
DMA 基本结构详解
这张图展示了 DMA 控制器的核心工作原理,分为数据传输路径和控制触发路径两大部分:
1. 数据传输路径(上半部分)
外设配置块(左侧)
- 起始地址:外设数据寄存器地址(如
&ADC1->DR、USART1->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_DIR | PeripheralSRC 外设→存储器;PeripheralDST 存储器→外设 |
DMA_BufferSize | 传输次数(=CNDTR 初值),非字节数(需结合宽度) |
DMA_PeripheralInc / DMA_MemoryInc | 每次传输后地址是否 +1(按宽度步进) |
DMA_PeripheralDataSize / DMA_MemoryDataSize | Byte / HalfWord / Word |
DMA_Mode | Normal 或 Circular |
DMA_Priority | 多通道同时请求时的仲裁优先级 |
DMA_M2M | 使能存储器到存储器(此时无真实外设参与) |
DMA 请求
定义哪些外设可以从哪个通道触发 DMA 传输。
STM32F103 DMA1 常用映射(节选)
| 外设 | DMA1 通道 |
|---|---|
| ADC1 | Channel 1 |
| SPI1_RX | Channel 2 |
| SPI1_TX | Channel 3 |
| USART1_TX | Channel 4 |
| USART1_RX | Channel 5 |
| I2C1_TX | Channel 6 |
| I2C1_RX | Channel 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_HalfWord 与 DMA_MemoryDataSize_HalfWord。
数据转运 + DMA(M2M 示意)
江协「数组转运」示例配置要点:
| 参数 | 值 |
|---|---|
| 传输方向 | 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 经典组合(解决「轮询四通道太慢、占 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 初始化、校准、SoftwareStartConvM2M(数组拷贝)
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)。
#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#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 标志,便于下次使用 */
}#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]。
#ifndef __AD_H
#define __AD_H
#include "stm32f10x.h"
/* 全局数组:DMA 自动写入,main 直接读 */
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif#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);
}#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_M2M | Enable | Disable |
| 外设地址 | 源数组(当作 Peripheral) | &ADC1->DR |
| 外设地址自增 | Enable | Disable |
| 存储器地址自增 | Enable | Enable |
| 数据宽度 | Byte | HalfWord |
| 模式 | Normal | Circular |
| 触发 | 软件 DMA_Cmd | ADC 硬件 每次 EOC |
| ADC 模式 | — | 连续 + 扫描 |
| CPU | Transfer() 等 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,务必与通道数/数组长度一致。
注意事项
- 先开 DMA 时钟(AHB),再
DMA_Init。 - 通道与外设映射必须查手册,ADC1 用 DMA1 Channel1。
- Normal 模式改 CNDTR 前必须先
DMA_Cmd(DISABLE)。 - Circular + 连续 ADC 成对使用;单独 Circular 而不连续触发可能只跑一轮。
- 数据宽度与 DR、数组类型一致(ADC 用半字 +
uint16_t)。 - M3 无 DCache,DMA 与 CPU 共享 SRAM 一般无需额外 cache 维护。
- 高负载时用 DMA 中断 替代
while(TC),避免阻塞。 - F103C8 等确认是否有 DMA2,避免初始化不存在的控制器。
总结
| 要点 | 说明 |
|---|---|
| 作用 | 外设 ↔ 存储器批量搬运,减轻 CPU |
| 通道 | 与外设固定映射,ADC1 = DMA1 Ch1 |
| M2M | 数组拷贝,Normal + 软件使能 |
| ADC | 连续扫描 + Circular DMA → AD_Value[] 自动刷新 |
| 完成 | DMA_FLAG_TCx、HT、TE 或中断 |
| 关键 | CNDTR=次数;改计数前 DISABLE 通道 |