STM32--------DMA

一.DMA概念

STM32 的 DMA (Direct Memory Access,直接存储器访问)是一种无需 CPU 干预,直接在存储器(如 RAMFlash )与外设(如 UARTSPI、ADC 等)之间或存储器之间传输数据的技术,能显著减轻 CPU 负担,提高数据传输效率,尤其适合高速、大数据量的场景(如传感器数据采集、通信数据收发等)。

  1. 独立于 CPU:数据传输由 DMA 控制器直接完成,CPU 可同时执行其他任务。
  2. 多通道支持 :STM32 不同系列的 DMA 控制器通道数量不同(如 STM32F1 有 2 个 DMA 控制器,共 12 个通道;F4/F7/H7 等系列有更多通道和更复杂的仲裁机制)。
  3. 灵活的传输方向
    • 外设→存储器(如 ADC 采集数据到 RAM);
    • 存储器→外设(如 RAM 数据通过 UART 发送);
    • 存储器→存储器(如 RAM 内部数据复制)。
  4. 传输模式
    • 单次传输:传输完成后停止,需重新配置启动;
    • 循环传输:传输完成后自动重新开始,适合周期性数据(如 ADC 连续采样)。
  5. 数据宽度 :支持 8 位、16 位、32 位数据传输,可匹配外设和存储器的数据格式。
  6. 中断与标志:传输完成、半传输、错误等事件可触发中断或通过标志位查询状态。

二、DMA 控制器结构(以 STM32F1 为例)

  • 2 个 DMA 控制器(DMA1 和 DMA2),DMA2 仅在大容量型号中存在。
  • DMA1有7个通道,DMA2有5个通道,每个通道对应特定的外设请求(如 DMA1_CH1 可对应 TIM2_UP、ADC1 等),通道与外设的映射关系由芯片手册定义。
  • 仲裁器:当多个通道同时请求时,通过优先级(软件设置 + 硬件固定)决定传输顺序。

三、DMA 基本工作流程

  1. 配置 DMA 通道
    • 设定外设地址(如 UART 的数据寄存器 DR、ADC 的数据寄存器 DR);
    • 设定存储器地址(如 RAM 中的数组);
    • 设定传输数据量(字节数、半字数、字数);
    • 设定传输方向、数据宽度、是否循环模式、优先级等。
  2. 使能外设 DMA 请求:外设需开启 DMA 模式(如 UART 的 DMAT 位、ADC 的 DMA 位)。
  3. 启动 DMA 传输:使能 DMA 通道,当外设产生请求(如 UART 发送缓冲区空、ADC 转换完成)时,DMA 自动开始传输。
  4. 传输完成处理:通过中断或查询标志位,确认传输完成后进行后续操作(如处理数据、关闭 DMA 等)

四.关于基地址的解释

"外设基地址" 和 "存储器基地址" 是广义上的概念 ,尤其是在 "存储器到存储器(M2M)" 模式下,它们的含义需要结合 DMA 的工作机制来理解,并非严格对应硬件上的 "外设"(如 GPIO、USART 等)。

  1. **外设基地址(DMA_PeripheralBaseAddr)**在 STM32 的 DMA 架构中,"外设" 是一个相对概念:

    • 当 DMA 用于 "外设到存储器"(如 ADC 采集数据到内存)或 "存储器到外设"(如内存数据发送到 USART)时,"外设基地址" 确实对应硬件外设的寄存器地址(如 ADC 的数据寄存器、USART 的发送寄存器)。
    • 但在存储器到存储器(M2M)模式 下(代码中通过**DMA_M2M_Enable**使能),DMA 的传输发生在两个内存区域之间,此时并没有实际的硬件外设参与。这种情况下,代码中将 "源地址(原数组首地址AddrA)" 定义为 "外设基地址",仅仅是因为 DMA 的逻辑框架要求区分 "源端" 和 "目的端",这里的 "外设" 只是作为 "源端" 的代称,并非真正的硬件外设。
  2. **存储器基地址(DMA_MemoryBaseAddr)**同样是广义概念:

    • 在常规的 "外设到存储器" 模式中,"存储器基地址" 通常指内存中的缓冲区(如数组、变量地址),用于存放从外设读取的数据。
    • 在 M2M 模式中,"存储器基地址" 对应 "目的地址(目标数组首地址AddrB)",即数据最终要写入的内存区域。这里的 "存储器" 是相对于 "源端(被当作外设的内存区域)" 而言的,本质上两者都是内存空间。

STM32 的 DMA 控制器设计时,需要兼容多种传输场景(外设↔存储器存储器↔外设存储器↔存储器 ),因此采用了 "外设端" 和 "存储器端" 的通用框架来描述传输的两端。在 M2M 模式下,这种框架仍然适用,只是 "外设端" 被复用为其中一个内存区域的地址,并非实际的硬件外设。

五.DMA_InitStructure.DMA_M2M

在 STM32 的 DMA 配置中,DMA_InitStructure.DMA_M2M用于设置是否启用 存储器到存储器(Memory-to-Memory,简称 M2M)传输模式 ,其 EnableDisable的含义如下:

1. DMA_M2M_Enable(使能存储器到存储器模式)

  • 含义:允许 DMA 直接在两个存储器地址之间传输数据(例如:从一个数组复制到另一个数组,或从内存的一块区域复制到另一块区域)。
  • 特点
    • 无需外部硬件外设(如 ADC、USART、SPI 等)触发,完全由软件启动传输(通过 DMA_Cmd 使能 DMA 即可开始)。
    • 传输的两端都是内存地址(代码中用 AddrAAddrB 分别表示源和目的地址),此时 DMA 的 "外设端" 和 "存储器端" 本质上都是内存空间(如前所述的 "广义概念")。
    • 适用于批量数据的快速复制(例如:缓存数据迁移、大数据块搬运),效率远高于 CPU 逐字节复制(CPU 只需启动传输,后续由 DMA 硬件自动完成)。

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 控制器发送请求信号,启动数据传输。

背后的工作机制:

  1. 硬件连接关系 STM32 的 ADC 和 DMA 控制器之间存在内部硬件连线(无需软件配置引脚)。例如,ADC1 的转换完成信号会连接到 DMA1 的通道 1(不同通道映射需参考芯片手册),形成固定的 "ADC→DMA 通道 " 触发链路。(代码中使用DMA1_Channel1,正是因为 ADC1 的 DMA 请求默认映射到该通道。)

  2. 使能后的流程 当**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 继续传输...... 形成全自动的数据采集链路。
  3. 为何需要单独使能?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)的扫描模式为例,转换流程为:

  1. 先转换通道 0,结果存入ADC1->DR
  2. 接着转换通道 1,结果覆盖 ADC1->DR中原通道 0 的数据;
  3. 再转换通道 2,结果覆盖 ADC1->DR中原通道 1 的数据;
  4. 最后转换通道 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->DRAD_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 参与,完全由硬件按固定节拍执行,因此能实现极高的时间精度和可靠性。

相关推荐
D.....l4 小时前
STM32学习(MCU控制)(GPIO)
stm32·嵌入式硬件·学习
lzhdim5 小时前
雷蛇(Razer)炼狱蝰蛇V2X极速版无线鼠标开箱
单片机·嵌入式硬件·计算机外设
wuk9985 小时前
基于位置式PID算法调节PWM占空比实现电机转速控制
单片机·嵌入式硬件·算法
三佛科技-134163842125 小时前
暴力风扇方案MCU控制芯片开发
单片机·嵌入式硬件·智能家居·pcb工艺
我先去打把游戏先5 小时前
ESP32学习笔记(基于IDF):SmartConfig一键配网
笔记·嵌入式硬件·mcu·物联网·学习·esp32·硬件工程
小莞尔11 小时前
【51单片机】【protues仿真】基于51单片机数字温度计数码管系统
单片机·嵌入式硬件
future141213 小时前
MCU硬件学习
单片机·嵌入式硬件·学习
GilgameshJSS14 小时前
STM32H743-ARM例程24-USB_MSC
c语言·arm开发·stm32·单片机·嵌入式硬件
小莞尔14 小时前
【51单片机】【protues仿真】基于51单片机电压测量多量程系统
c语言·单片机·嵌入式硬件·物联网·51单片机