一.DMA概念
STM32 的 DMA (Direct Memory Access,直接存储器访问)是一种无需 CPU 干预,直接在存储器(如 RAM 、Flash )与外设(如 UART 、SPI、ADC 等)之间或存储器之间传输数据的技术,能显著减轻 CPU 负担,提高数据传输效率,尤其适合高速、大数据量的场景(如传感器数据采集、通信数据收发等)。
- 独立于 CPU:数据传输由 DMA 控制器直接完成,CPU 可同时执行其他任务。
- 多通道支持 :STM32 不同系列的 DMA 控制器通道数量不同(如 STM32F1 有 2 个 DMA 控制器,共 12 个通道;F4/F7/H7 等系列有更多通道和更复杂的仲裁机制)。
- 灵活的传输方向 :
- 外设→存储器(如 ADC 采集数据到 RAM);
- 存储器→外设(如 RAM 数据通过 UART 发送);
- 存储器→存储器(如 RAM 内部数据复制)。
- 传输模式 :
- 单次传输:传输完成后停止,需重新配置启动;
- 循环传输:传输完成后自动重新开始,适合周期性数据(如 ADC 连续采样)。
- 数据宽度 :支持 8 位、16 位、32 位数据传输,可匹配外设和存储器的数据格式。
- 中断与标志:传输完成、半传输、错误等事件可触发中断或通过标志位查询状态。
二、DMA 控制器结构(以 STM32F1 为例)
- 2 个 DMA 控制器(DMA1 和 DMA2),DMA2 仅在大容量型号中存在。
- DMA1有7个通道,DMA2有5个通道,每个通道对应特定的外设请求(如 DMA1_CH1 可对应 TIM2_UP、ADC1 等),通道与外设的映射关系由芯片手册定义。
- 仲裁器:当多个通道同时请求时,通过优先级(软件设置 + 硬件固定)决定传输顺序。

三、DMA 基本工作流程
- 配置 DMA 通道 :
- 设定外设地址(如 UART 的数据寄存器 DR、ADC 的数据寄存器 DR);
- 设定存储器地址(如 RAM 中的数组);
- 设定传输数据量(字节数、半字数、字数);
- 设定传输方向、数据宽度、是否循环模式、优先级等。
- 使能外设 DMA 请求:外设需开启 DMA 模式(如 UART 的 DMAT 位、ADC 的 DMA 位)。
- 启动 DMA 传输:使能 DMA 通道,当外设产生请求(如 UART 发送缓冲区空、ADC 转换完成)时,DMA 自动开始传输。
- 传输完成处理:通过中断或查询标志位,确认传输完成后进行后续操作(如处理数据、关闭 DMA 等)
四.关于基地址的解释
"外设基地址" 和 "存储器基地址" 是广义上的概念 ,尤其是在 "存储器到存储器(M2M)" 模式下,它们的含义需要结合 DMA 的工作机制来理解,并非严格对应硬件上的 "外设"(如 GPIO、USART 等)。
-
**外设基地址(DMA_PeripheralBaseAddr)**在 STM32 的 DMA 架构中,"外设" 是一个相对概念:
- 当 DMA 用于 "外设到存储器"(如 ADC 采集数据到内存)或 "存储器到外设"(如内存数据发送到 USART)时,"外设基地址" 确实对应硬件外设的寄存器地址(如 ADC 的数据寄存器、USART 的发送寄存器)。
- 但在存储器到存储器(M2M)模式 下(代码中通过**
DMA_M2M_Enable
**使能),DMA 的传输发生在两个内存区域之间,此时并没有实际的硬件外设参与。这种情况下,代码中将 "源地址(原数组首地址AddrA
)" 定义为 "外设基地址",仅仅是因为 DMA 的逻辑框架要求区分 "源端" 和 "目的端",这里的 "外设" 只是作为 "源端" 的代称,并非真正的硬件外设。
-
**存储器基地址(DMA_MemoryBaseAddr)**同样是广义概念:
- 在常规的 "外设到存储器" 模式中,"存储器基地址" 通常指内存中的缓冲区(如数组、变量地址),用于存放从外设读取的数据。
- 在 M2M 模式中,"存储器基地址" 对应 "目的地址(目标数组首地址
AddrB
)",即数据最终要写入的内存区域。这里的 "存储器" 是相对于 "源端(被当作外设的内存区域)" 而言的,本质上两者都是内存空间。
STM32 的 DMA 控制器设计时,需要兼容多种传输场景(外设↔存储器 、存储器↔外设 、存储器↔存储器 ),因此采用了 "外设端" 和 "存储器端" 的通用框架来描述传输的两端。在 M2M 模式下,这种框架仍然适用,只是 "外设端" 被复用为其中一个内存区域的地址,并非实际的硬件外设。
五.DMA_InitStructure.DMA_M2M
在 STM32 的 DMA 配置中,DMA_InitStructure.DMA_M2M用于设置是否启用 存储器到存储器(Memory-to-Memory,简称 M2M)传输模式 ,其 Enable 和Disable的含义如下:
1. DMA_M2M_Enable(使能存储器到存储器模式)
- 含义:允许 DMA 直接在两个存储器地址之间传输数据(例如:从一个数组复制到另一个数组,或从内存的一块区域复制到另一块区域)。
- 特点 :
- 无需外部硬件外设(如 ADC、USART、SPI 等)触发,完全由软件启动传输(通过
DMA_Cmd
使能 DMA 即可开始)。 - 传输的两端都是内存地址(代码中用
AddrA
和AddrB
分别表示源和目的地址),此时 DMA 的 "外设端" 和 "存储器端" 本质上都是内存空间(如前所述的 "广义概念")。 - 适用于批量数据的快速复制(例如:缓存数据迁移、大数据块搬运),效率远高于 CPU 逐字节复制(CPU 只需启动传输,后续由 DMA 硬件自动完成)。
- 无需外部硬件外设(如 ADC、USART、SPI 等)触发,完全由软件启动传输(通过
2. DMA_M2M_Disable(禁用存储器到存储器模式,默认值)
- 含义:DMA 传输需要由外部硬件外设的事件触发,此时传输方向为 "外设←→存储器"(而非两个存储器之间)。
- 特点 :
- 传输的一端是硬件外设的寄存器(如 ADC 的数据寄存器、USART 的发送 / 接收寄存器),另一端是内存地址。
- 传输由外设的特定事件触发(例如:ADC 转换完成、USART 收到数据、定时器溢出等),无需 CPU 主动干预启动。
- 适用于外设与内存之间的数据交互(例如:ADC 采集数据自动存入内存、内存数据自动发送到 USART)。
总结
- DMA_M2M_Enable:用于 内存到内存 的数据传输,软件启动,无需外设参与。
- DMA_M2M_Disable:用于 外设与内存之间 的数据传输,由外设事件触发,是默认且更常用的模式(适配大多数外设场景)。
六.两数组DMA传输
1.MyDMA.h
cpp
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
2.MyDMA.c
cpp
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size; //定义全局变量,用于记住Init函数的Size,供Transfer函数使用
/**
* 函 数:DMA初始化
* 参 数:AddrA 原数组的首地址
* 参 数:AddrB 目的数组的首地址
* 参 数:Size 转运的数据大小(转运次数)
* 返 回 值:无
整个流程分为 "初始化" 和 "传输触发" 两部分:
初始化:配置 DMA 的源 / 目的地址、数据宽度、自增模式、传输方向等参数,保存传输大小,为传输做准备。
传输触发:重新设置计数器,启动 DMA,等待传输完成并清除标志,实现内存到内存的高效数据转运
(无需 CPU 干预,仅在开始和结束时占用 CPU)。
*/
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size; //将Size写入到全局变量,记住参数Size
/*STM32 的外设必须开启对应时钟才能工作。
DMA1 挂载在 AHB 总线上,因此通过RCC_AHBPeriphClockCmd函数开启DMA1的时钟。*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
// 外设基地址,给定形参AddrA 这里的 "外设" 是广义的:
// 在 "存储器到存储器(M2M)" 模式下,源地址(原数组)被当作 "外设端" 处理,
// 因此AddrA(原数组首地址)作为源地址。
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
/*配置每次传输的数据大小为 "字节(8 位)"。可选值还有半字(16 位)、字(32 位),
需与传输的数据类型匹配*/
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
/* 使能后,每次传输完成后,源地址(AddrA)会自动递增(递增步长 = 数据宽度,这里为 1 字节),
实现连续传输数组的下一个元素。若禁用,会一直传输源地址的同一个数据。*/
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
/*"存储器" 指目的地址(目标数组),因此AddrB(目标数组首地址)作为目的地址。 */
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;
/*与外设数据宽度保持一致(均为字节),
避免数据截断或错位(例如:源传 1 字节,目的按 4 字节接收会导致数据错误)。 */
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
//存储器地址自增,选择使能
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
//数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
/*配置需要传输的总次数(每次传输 1 字节,因此Size即总字节数)。
DMA 会通过计数器递减计数,当计数器归零时表示传输完成。 */
DMA_InitStructure.DMA_BufferSize = Size;
/*正常模式:传输完成后 DMA 自动停止,计数器归零,需重新配置计数器才能再次传输。
若选循环模式(DMA_Mode_Circular),传输完成后会自动重启,计数器恢复初始值,适合连续重复传输。 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
/*使能 M2M 模式:表示传输在两个存储器(内存数组)之间进行,无需外设触发(如 ADC、USART 等),直接由软件启动。
若禁用(默认),则 DMA 传输需由外设事件触发(如 USART 接收到数据时)。*/
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
/*当多个 DMA 通道同时请求传输时,优先级高的通道先执行。
可选:低(Low)、中(Medium)、高(High)、极高(VeryHigh)。*/
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1(一共7个通道)
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}
/**
* 函 数:启动DMA数据转运
* 参 数:无
* 返 回 值:无
*/
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA 在运行时无法修改计数器(BufferSize),因此需先禁用,确保修改安全。
/*由于 DMA 在 "正常模式" 下传输完成后计数器会归零,
因此下次传输前需用全局变量MyDMA_Size重新设置计数器值(即传输大小)。*/
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
/*DMA1_FLAG_TC1是 DMA1 通道 1 的 "传输完成标志位":
当传输完成(计数器归 0)时,该标志位会被硬件置 1;未完成时为 0。
循环等待标志位为 1,确保 CPU 在传输完成后再执行后续操作。*/
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
/*标志位被置 1 后不会自动清零,需手动清除,避免下次传输时误判为 "已完成"。*/
DMA_ClearFlag(DMA1_FLAG_TC1);
}
3.main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; //定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0}; //定义测试数组DataB,为数据目的地
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //DMA初始化,把源数组和目的数组的地址传入
/*显示静态字符串*/
OLED_ShowString(1, 1, "DataA");
OLED_ShowString(3, 1, "DataB");
/*显示数组的首地址*/
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
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); //显示数组DataA
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); //延时1s,观察转运前的现象
MyDMA_Transfer(); //使用DMA转运数组,从DataA转运到DataB
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
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); //延时1s,观察转运后的现象
}
}
4.程序现象

可以看到DataA不断地运往DataB
七.ADC多通道DMA转运
1.流程
ADC 持续采集多个模拟信号,并通过 DMA 自动将结果存入内存,无需 CPU 干预。整体流程可总结为以下6 大步骤:
(1) 时钟配置(硬件工作的前提)
- 开启
ADC1
(模数转换器)、GPIOA
(ADC 输入引脚)、DMA1
(数据传输控制器)的时钟,确保三者能正常工作。 - 配置 ADC 时钟:将 APB2 总线时钟(72MHz)6 分频,得到 12MHz 的 ADCCLK(符合 ADC 最大时钟≤14MHz 的要求)。
(2)GPIO 配置(模拟信号输入通道)
- 将**
PA0~PA3
引脚配置为模拟输入模式** (GPIO_Mode_AIN
),使其与 ADC 模块的模拟通道 0~3 连接,用于接收外部模拟信号(如电压)。
(3) ADC 规则组通道配置(采样序列定义)
- 在 ADC 的 "规则组" 中,按顺序配置 4 个通道:序列 1→通道 0(PA0)、序列 2→通道 1(PA1)、序列 3→通道 2(PA2)、序列 4→通道 3(PA3)。
- 每个通道的采样时间设为 55.5 个 ADCCLK 周期(平衡采样精度和速度)。
(4)ADC 核心参数配置(工作模式设定)
- 扫描模式 (ScanConvMode = ENABLE):ADC 按规则组序列依次转换 4 个通道,而非只转换第一个。
- 连续转换模式 (ContinuousConvMode = ENABLE):一次扫描完成后自动启动下一次,实现不间断采样。
- 数据右对齐:12 位转换结果存于 16 位寄存器的低 12 位,方便直接读取。
- 软件触发:无需外部硬件信号,通过软件启动一次后持续工作。
(5)DMA 配置(自动传输桥梁)
- 传输方向 :外设(ADC 数据寄存器
ADC1->DR
)→存储器(数组AD_Value
)。 - 地址与宽度 :
- 外设地址固定为
ADC1->DR
(始终从这里读转换结果),数据宽度 16 位(半字)。 - 存储器地址为AD_Value数组,地址自增(每次存下一个元素),数据宽度 16 位(匹配数组类型)。
- 外设地址固定为
- 循环模式 (DMA_Mode_Circular):传输 4 个数据后自动重启,覆盖旧数据,保持数组始终是最新结果。
- 使能 ADC 触发 DMA:ADC 每次转换完成后,自动触发 DMA 传输数据。
(6)启动与校准(进入工作状态)
- ADC 校准:复位并启动校准,补偿硬件误差,保证采样精度。
- 启动工作 :使能 DMA 和 ADC,通过软件触发 ADC 开始转换。由于 ADC 是连续模式,触发一次后会持续扫描 4 个通道,DMA 则自动将结果存入
AD_Value
数组。
最终效果
系统会自动、持续地采集 PA0~PA3 的模拟信号,转换结果实时存于**AD_Value[0]~AD_Value[3]**中,用户直接读取该数组即可获取最新采样值,CPU 几乎无需参与采样过程,效率极高。
2.代码
(1) AD.h
cpp
#ifndef __AD_H
#define __AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
#endif
(2) AD.c
cpp
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4]; //定义用于存放AD转换结果的全局数组
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
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); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,使能,每转换一次规则组序列后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,选择半字,对应16为的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,始终以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,给定存放AD转换结果的全局数组AD_Value
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,选择半字,与源数据宽度对应
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
DMA_InitStructure.DMA_BufferSize = 4; //转运的数据大小(转运次数),与ADC通道数一致
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式,与ADC的连续转换一致
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}
(3) main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //显示转换结果第0个数据
OLED_ShowNum(2, 5, AD_Value[1], 4); //显示转换结果第1个数据
OLED_ShowNum(3, 5, AD_Value[2], 4); //显示转换结果第2个数据
OLED_ShowNum(4, 5, AD_Value[3], 4); //显示转换结果第3个数据
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
(4)现象
程序现象和上一节一样,但是速度会变快的多
八.使能 ADC 触发 DMA是如何做到的
在 STM32 中,"使能 ADC 触发 DMA" 是通过硬件机制实现的:当 ADC 完成一次转换后,会自动产生一个 DMA 请求信号,触发 DMA 控制器将转换结果从 ADC 数据寄存器传输到内存。具体实现步骤和原理如下:
核心函数:ADC_DMACmd(ADC1, ENABLE);
代码中通过这一行实现 "使能 ADC 触发 DMA",其作用是允许 ADC 在转换完成后,自动向 DMA 控制器发送请求信号,启动数据传输。
背后的工作机制:
-
硬件连接关系 STM32 的 ADC 和 DMA 控制器之间存在内部硬件连线(无需软件配置引脚)。例如,ADC1 的转换完成信号会连接到 DMA1 的通道 1(不同通道映射需参考芯片手册),形成固定的 "ADC→DMA 通道 " 触发链路。(代码中使用
DMA1_Channel1
,正是因为 ADC1 的 DMA 请求默认映射到该通道。) -
使能后的流程 当**ADC_DMACmd(ADC1, ENABLE)**使能后,整个触发 - 传输过程如下:
- 步骤 1:ADC 完成转换 ADC 按配置完成一次规则组扫描(例如转换 4 个通道),每个通道的转换结果会依次存入 ADC 的数据寄存器(
ADC1->DR
)。 - 步骤 2:ADC 自动发送 DMA 请求每次转换完成(或扫描完一组通道后),ADC 硬件会自动产生一个 "DMA 请求信号",通知 DMA 控制器 "有新数据待传输"。
- 步骤 3:DMA 响应请求并传输数据 DMA 控制器收到请求后,按预设配置(源地址
ADC1->DR
、目的地址AD_Value
、数据宽度等),将数据从 ADC 寄存器传输到内存数组。 - 步骤 4:循环触发(配合连续模式) 由于代码中 ADC 配置为**
连续转换模式
,且 DMA 配置为循环模式
**,上述过程会不断重复:ADC 转换→触发 DMA 传输→ADC 继续转换→DMA 继续传输...... 形成全自动的数据采集链路。
- 步骤 1:ADC 完成转换 ADC 按配置完成一次规则组扫描(例如转换 4 个通道),每个通道的转换结果会依次存入 ADC 的数据寄存器(
-
为何需要单独使能?
ADC_DMACmd
是一个开关:- 禁用(
DISABLE
)时,ADC 转换完成后不会发送 DMA 请求,数据需通过 CPU 手动读取(ADC_GetConversionValue
)。 - 使能(
enable
)时,才允许 ADC 触发 DMA 自动传输,解放 CPU。
- 禁用(
总结
"使能 ADC 触发 DMA" 本质是通过ADC_DMACmd
函数打开 ADC 到 DMA 的硬件请求链路。之后,ADC 每完成一次转换,就会自动向 DMA 发送信号,触发 DMA 将数据从ADC1->DR
传输到内存(如AD_Value
数组),整个过程无需 CPU 干预,实现了高效的 "ADC 采集→DMA 传输" 自动化流程。
九.关于ADC->DR的疑问
在多通道 ADC 转换中(如代码中 4 个通道的扫描模式),ADC 的数据寄存器(ADC1->DR
)是逐个覆盖存储转换结果的,而非同时存储多个数据。具体过程如下:
关键原理:ADC_DR
是单寄存器,多通道时按顺序更新
STM32 的 ADC 数据寄存器(DR
)是一个 16 位的单寄存器,无论多少个通道,每次转换完成后,当前通道的结果会直接写入DR
,并覆盖上一个通道的结果。
以代码中 4 个通道(通道 0~3)的扫描模式为例,转换流程为:
- 先转换通道 0,结果存入
ADC1->DR
; - 接着转换通道 1,结果覆盖
ADC1->DR
中原通道 0 的数据; - 再转换通道 2,结果覆盖
ADC1->DR
中原通道 1 的数据; - 最后转换通道 3,结果覆盖
ADC1->DR
中原通道 2 的数据。
为何 DMA 能正确获取所有通道数据?
虽然DR
会被逐个覆盖,但配合 DMA 的 "连续请求" 机制,可确保所有数据被正确传输到内存:
- 在扫描模式 + DMA 使能下,ADC 每完成一个通道的转换(而非等待所有通道扫描完),就会立即触发一次 DMA 请求;
- DMA 收到请求后,会立即将当前
DR
中的数据(当前通道的结果)传输到内存数组(如AD_Value
),并通过 "存储器地址自增" 功能,依次存到AD_Value[0]
→AD_Value[1]
→AD_Value[2]
→AD_Value[3]
; - 当 DMA 传输完成 4 次(对应 4 个通道)后,由于配置了 "循环模式",会重新开始下一轮传输,覆盖旧数据。
总结
多通道扫描时,ADC1->DR
会被逐个通道的结果覆盖 ,但 DMA 会在每个通道转换完成后立即 "抢读" 数据并传输到内存,因此不会丢失数据。最终AD_Value
数组中会按通道顺序保存完整的 4 个结果,实现多通道数据的连续采集与存储。
十.ADC和DMA为何能准确同步
ADC 每次获取数据后,DMA 能准确传输且时间节拍控制精准,核心依赖于 STM32 的硬件级同步机制和固定时序设计,具体原因如下:
1. 硬件触发:无软件延迟的请求响应
ADC 与 DMA 之间的触发关系是纯硬件连接,而非软件干预:
- 当 ADC 完成一个通道的转换后,硬件会自动产生一个 "DMA 请求信号"(无需 CPU 指令),这个信号直接连接到 DMA 控制器的对应通道(如代码中 ADC1→DMA1_Channel1)。
- DMA 控制器收到请求后,会立即启动数据传输(从
ADC1->DR
到AD_Value
数组),整个过程由硬件电路驱动,响应时间固定(通常在几个时钟周期内),几乎无延迟。
这种 "硬件触发 - 硬件响应" 的机制,避免了软件中断或函数调用带来的随机延迟,保证了触发的及时性。
2. ADC 转换节奏固定:传输请求的时间间隔可预测
ADC 的转换时序是严格由时钟和配置决定的,每个通道的转换时间固定,因此 DMA 请求的间隔也固定:
- 单通道转换时间 = 采样时间 + 12.5 个 ADCCLK 周期(ADC 核心转换时间,固定值)。例如代码中采样时间为 55.5 个 ADCCLK(12MHz 下约 4.625μs),则单通道转换时间 = 55.5 + 12.5 = 68 个 ADCCLK ≈ 5.67μs。
- 4 通道扫描总时间 = 4 × 单通道转换时间(约 22.67μs),且连续转换模式下,每轮扫描的间隔完全一致。
因此,ADC 触发 DMA 的时间间隔(即每个通道的转换完成时刻)是严格固定的,形成了 "周期性的 DMA 请求",节奏稳定。
3. DMA 传输速度匹配:确保数据不丢失、不错位
DMA 的传输配置与 ADC 的转换节奏完全匹配,保证数据能被及时取走:
- 传输速度 :DMA 挂载在 AHB 总线(最高 72MHz),传输一个 16 位数据(半字)仅需 1-2 个总线周期(约 14ns),远快于 ADC 的转换间隔(约 5.67μs)。因此,DMA 有充足时间在 "下一个通道转换完成前" 完成当前数据的传输,不会出现 "前一个数据未传完,新数据已覆盖
ADC1->DR
" 的情况。 - 地址自增与数量匹配 :DMA 配置为 "存储器地址自增" 且
BufferSize=4
,与 ADC 的 4 个通道一一对应。每次传输后,DMA 自动指向数组的下一个位置,确保 4 个通道的数据按顺序存入AD_Value[0]~[3]
,不会错位。
4. 同步模式设计:ADC 与 DMA 的 "闭环联动"
代码中 ADC 和 DMA 的模式配置形成了完美的同步闭环:
- ADC 配置为 "连续转换 + 扫描模式":转换完 4 个通道后,立即自动开始下一轮扫描,持续产生周期性的 DMA 请求。
- DMA 配置为 "循环模式":传输完 4 个数据后,自动复位地址指针和计数器,准备接收下一轮 ADC 的请求,与 ADC 的连续转换形成 "无缝衔接"。
这种 "ADC 连续转换→DMA 循环传输" 的联动,使得两者始终保持节奏一致,不会出现 "ADC 等待 DMA" 或 "DMA 等待 ADC" 的情况。
总结
ADC 与 DMA 的精准同步,本质是硬件触发的即时性 、ADC 转换时序的固定性 、DMA 传输速度的匹配性 以及模式配置的同步性共同作用的结果。整个过程无需 CPU 参与,完全由硬件按固定节拍执行,因此能实现极高的时间精度和可靠性。