24:【stm32】DMA数据搬运

DMA数据搬运

1、DMA的简介

DMA是直接存储器存取,它可以提供外设寄存器和存储器,存储器与存储器之间的高速数据的传输,无需CPU的干预,这样节省了CPU的资源。简单来说DMA就是数据的搬运工。

STM32中的存储器:
DMA的3种搬运方式:

1.存储器------>存储器(数据的拷贝)

2.存储器------>外设(将某数据写入串口寄存器TDR)

3.外设--------->存储器(将串口接收寄存器RDR的数据搬运到内存,避免数据的覆盖)

2、STM32中的DMA结构

在单片机中12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)。每个通道都支持软件触发和特定的硬件触发。而在STM32F103C8T6中只有DMA1(7个通道),且挂载在AHB总线上面。

由于Flash存储是只读存储器,所以DMA转运的数据不能存储在Flash存储器里面,这是不允许的,所以只能存储在SRAM存储器里面。

  • DMA请求

    DMA请求就是DMA触发,由如下图可知,每个通道的硬件请求都是特定的,比如:TIM2_CH1的请求不能通过DMA1的通道2对DMA进行触发。但是每个通道都有软件请求。所以每个通道都支持软件触发和特定的硬件触发。软件触发一般用在存储器------>存储器(数据的拷贝)。

  • DMA基本结构细节

    由下图所示:外设寄存器和存储器里面有起始地址,数据宽度。是否自增。

    1.起始地址:数据从哪里转运到哪里

    2.数据宽度:每次转运的数据有多大(Byte(uint8_t)/HalfWord(uint16_t)/Word(uint32_t))

    3.地址是否自增:第一次转运完成后,进行下一次转运时是否发生地址的偏移。

    4.传输计数器:总共需要几次转运,是一个自减的计数器。为0就不转运了。

    【注】传输计数器变为0后,自增的地址也会恢复到起始地址。

    5.自动重装器:传输计数器减为0后,是否恢复初值,又开始转运。如果没有开启自动重装器。

    6.M2M:触发控制,为1时就是软件触发,为0就是硬件触发。

    【注】软件触发一般用于存储器到存储器的转运,触发一次,以最快的要求计数器清0。所以,软件触发不能和自动重装器同时用

7.DMA开始转运的3大调节:

①DMA开关必须关闭

②计数器必修大于0

③必须要有触发源

如果转运完成,计数器清0 。想要进行第二次转运,则想要关闭DMA,写入计数器次数,然后给触发源,开启DMA。

3、案列

3.1、将数组DataA中的数据搬运到DataB中

与之相关的标准库编程接口:

我们先查看变量存储在哪个存储器当中?

c 复制代码
#include "stm32f10x.h"                 
#include "OLED.h"

uint8_t a = 10;
int main(void)
{
	OLED_Init();
	OLED_Clear();
	
	OLED_ShowNum(1,1,a,2);
	OLED_ShowHexNum(2,1,(uint32_t)&a,8);
	while(1)
	{
		
	}
}

OLED屏幕上面显示的是:

10

20000000/ /变量的地址为0x20000000,代表变量存储在SRAM存储器中

我们查看常量存储在哪个存储器当中?

c 复制代码
#include "stm32f10x.h"                 
#include "OLED.h"

const uint8_t a = 10;
int main(void)
{
	OLED_Init();
	OLED_Clear();
	
	OLED_ShowNum(1,1,a,2);
	OLED_ShowHexNum(2,1,(uint32_t)&a,8);
	while(1)
	{
		
	}
}

OLED屏幕上面显示的是:

10

08000E40/ /变量的地址为0x08000E40,代表常量存储在Flash存储器中

数据拷贝的代码如下:

MyDMA.c文件的代码如下:

c 复制代码
#include "stm32f10x.h"                  // Device header

uint8_t My_Size;

void MyDMA_Init(uint32_t ADDrA,uint32_t ADDrB,uint8_t Size)
{
	My_Size = Size;
	//1.开启时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//2.DMA1的通道1的初始化,软件触发非自动重装
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_PeripheralBaseAddr = ADDrA;//外设站点的起始地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//数据宽度,8位
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//是否自增,这里选择自增
	
	DMA_InitStruct.DMA_MemoryBaseAddr = ADDrB;//存储器的起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//数据宽度
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//是否自增
	
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//外设站点的选择。外设站点是存储器,还是寄存器。这里选择存储器
	DMA_InitStruct.DMA_BufferSize = Size;//传输计数器
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;//是否自动重装,这里选择不自动重装
	DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;//是否软件触发
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;//优先级
	
	DMA_Init(DMA1_Channel1,&DMA_InitStruct);//DMA1的通道1
	
	DMA_Cmd(DMA1_Channel1,DISABLE);
}

void My_DMA_TransFer(void)//DMA开启搬运的函数
{
	DMA_Cmd(DMA1_Channel1,DISABLE);//关闭DMA
	DMA_SetCurrDataCounter(DMA1_Channel1,My_Size);//写入传输计数器的值
	DMA_Cmd(DMA1_Channel1,ENABLE);//开启DMA
	
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待搬运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);//手动清除标志位
}

主程序文件的代码如下:

c 复制代码
/*
	存储器------>存储器(数据的拷贝)DMA的使用
*/

#include "stm32f10x.h"                 
#include "OLED.h"
#include "Delay.h"
#include "MyDMA.h"

uint8_t DataA[] = {01,02,03,04};
uint8_t DataB[] = {0,0,0,0};

int main(void)
{
	OLED_Init();
	OLED_Clear();
	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);//显示DataA的地址
	OLED_ShowHexNum(3,8,(uint32_t)DataB,8);//显示DataB的地址
	
//	OLED_ShowNum(2,1,DataA[0],2);
//	OLED_ShowNum(2,4,DataA[1],2);
//	OLED_ShowNum(2,7,DataA[2],2);
//	OLED_ShowNum(2,10,DataA[3],2);
	
//	OLED_ShowNum(4,1,DataB[0],2);
//	OLED_ShowNum(4,4,DataB[1],2);
//	OLED_ShowNum(4,7,DataB[2],2);
//	OLED_ShowNum(4,10,DataB[3],2);
	while(1)
	{
		OLED_ShowNum(2,1,DataA[0],2);
		OLED_ShowNum(2,4,DataA[1],2);
		OLED_ShowNum(2,7,DataA[2],2);
		OLED_ShowNum(2,10,DataA[3],2);
		Delay_ms(1000);
		
		My_DMA_TransFer();//开始搬运
		
		OLED_ShowNum(4,1,DataB[0],2);
		OLED_ShowNum(4,4,DataB[1],2);
		OLED_ShowNum(4,7,DataB[2],2);
		OLED_ShowNum(4,10,DataB[3],2);
		
		DataA[0]++;
		DataA[1]++;
		DataA[2]++;
		DataA[3]++;
		
		Delay_ms(1000);
	}
}

3.2、ADC扫描模式+DMA


ADC单次扫描+DMA不自动重装模式

ADC.c文件的代码如下:

代码如下:

c 复制代码
#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	//1.开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//2.对ADC时钟进行分频
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//72MHZ/6 = 12MHz
	
	//3.对通道1(PA0)进行配置
	GPIO_InitTypeDef GPIOInitStruct;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,ADC的专属模式
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;

	GPIO_Init(GPIOA,&GPIOInitStruct);
	
	//4.对ADC规则组进行配置
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//ADC几,通道,序列,采样时间
	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);
	
	//5.初始化ADC 单次扫描模式
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//工作模式:独立模式/双ADC模式,这里的独立模式
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//ADC触发选择,选择软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//连续/单次模式,这里选择单次
	ADC_InitStruct.ADC_ScanConvMode = ENABLE;//扫描/非扫描,这里选择扫描
	ADC_InitStruct.ADC_NbrOfChannel = 4;//在扫描模式下的盒子数目
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,右对齐
	
	ADC_Init(ADC1,&ADC_InitStruct);
	
	//6.DMA1的通道1的初始化,硬件触发非自动重装
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址,这里选择DR寄存器
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度,16位
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//是否自增,这里选择不自增,
														  //因为是将DR数据寄存器里面的数据挪出来,始终是DR寄存器
	
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器的起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//数据宽度
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//是否自增
	
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//外设站点的选择。外设站点是存储器,还是寄存器。这里选择存储器
	DMA_InitStruct.DMA_BufferSize = 4;//传输计数器
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;//是否自动重装,这里选择不自动重装
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;//是否软件触发,这里选择硬件触发
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;//优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStruct);//DMA1的通道1
	
	//6.开启ADC电源
	ADC_Cmd(ADC1,ENABLE);
	
	//7.校准
	ADC_ResetCalibration(ADC1);//将校准复位,给CR2_RSTCAL置1,进行复位
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);//复位完成,硬件置0
	ADC_StartCalibration(ADC1);//开始校准,给CR2_CAL置1
	while(ADC_GetCalibrationStatus(ADC1) == SET);//校准完成,硬件置0
	
	//8.开启搬运
	ADC_DMACmd(ADC1 ,ENABLE);//开启ADC1硬件触发源,当一个通道转运完成后,就会自动请求DMA
	DMA_Cmd(DMA1_Channel1,ENABLE);
}

void AD_GetValue(void)//ADC触发函数并数据搬运
{
	DMA_Cmd(DMA1_Channel1,DISABLE);//关闭DMA
	DMA_SetCurrDataCounter(DMA1_Channel1,4);//写入传输计数器的值
	DMA_Cmd(DMA1_Channel1,ENABLE);//开启DMA
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发
	
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待搬运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);//手动清除标志位
}

主程序文件的代码:

c 复制代码
/*
	ADC+DMA的使用
*/

#include "stm32f10x.h"                 
#include "OLED.h"
#include "Delay.h"
#include "ADC.h"


int main(void)
{
	OLED_Init();
	OLED_Clear();
	AD_Init();

	OLED_ShowString(1,1,"AD0:");
	OLED_ShowString(2,1,"AD1:");
	OLED_ShowString(3,1,"AD2:");
	OLED_ShowString(4,1,"AD3:");
	
	while(1)
	{
		AD_GetValue();//转换并搬运,循环调用。
		
		OLED_ShowNum(1,5,AD_Value[0],4);//显示通道1采样到的数据
		OLED_ShowNum(2,5,AD_Value[1],4);
		OLED_ShowNum(3,5,AD_Value[2],4);
		OLED_ShowNum(4,5,AD_Value[3],4);
		Delay_ms(100);
	}
}

ADC连续扫描+DMA自动重装模式

ADC.c文件的代码如下:

c 复制代码
#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	//1.开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	//2.对ADC时钟进行分频
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//72MHZ/6 = 12MHz
	
	//3.对通道1(PA0)进行配置
	GPIO_InitTypeDef GPIOInitStruct;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,ADC的专属模式
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;

	GPIO_Init(GPIOA,&GPIOInitStruct);
	
	//4.对ADC规则组进行配置
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//ADC几,通道,序列,采样时间
	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);
	
	//5.初始化ADC,连续扫描模式
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//工作模式:独立模式/双ADC模式,这里的独立模式
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//ADC触发选择,选择软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;//连续/单次模式,这里选择连续
	ADC_InitStruct.ADC_ScanConvMode = ENABLE;//扫描/非扫描,这里选择非扫描
	ADC_InitStruct.ADC_NbrOfChannel = 4;//在扫描模式下的盒子数目
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,右对齐
	
	ADC_Init(ADC1,&ADC_InitStruct);
	
	//6.DMA1的通道1的初始化,自动重装模式
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址,这里选择DR寄存器
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//数据宽度,16位
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//是否自增,这里选择不自增,
																																//因为是将DR数据寄存器里面的数据挪出来,始终是DR寄存器
	
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器的起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//数据宽度
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//是否自增
	
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//外设站点的选择。外设站点是存储器,还是寄存器。这里选择存储器
	DMA_InitStruct.DMA_BufferSize = 4;//传输计数器
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;//是否自动重装,这里选择自动重装
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;//是否软件触发,这里选择硬件触发
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;//优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStruct);//DMA1的通道1
	
	//6.开启ADC电源
	ADC_Cmd(ADC1,ENABLE);
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件触发
	//7.校准
	ADC_ResetCalibration(ADC1);//将校准复位,给CR2_RSTCAL置1,进行复位
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);//复位完成,硬件置0
	ADC_StartCalibration(ADC1);//开始校准,给CR2_CAL置1
	while(ADC_GetCalibrationStatus(ADC1) == SET);//校准完成,硬件置0
	
	//8.开启搬运
	ADC_DMACmd(ADC1 ,ENABLE);//开启ADC1硬件触发源
	DMA_Cmd(DMA1_Channel1,ENABLE);
}

void AD_GetValue(void)//ADC触发函数并数据搬运
{
//	DMA_Cmd(DMA1_Channel1,DISABLE);//关闭DMA
//	DMA_SetCurrDataCounter(DMA1_Channel1,4);//写入传输计数器的值
//	DMA_Cmd(DMA1_Channel1,ENABLE);//开启DMA
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//等待搬运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);//手动清除标志位
}

主程序文件的代码:

c 复制代码
/*
	ADC+DMA的使用
*/

#include "stm32f10x.h"                 
#include "OLED.h"
#include "Delay.h"
#include "ADC.h"


int main(void)
{
	OLED_Init();
	OLED_Clear();
	AD_Init();

	OLED_ShowString(1,1,"AD0:");
	OLED_ShowString(2,1,"AD1:");
	OLED_ShowString(3,1,"AD2:");
	OLED_ShowString(4,1,"AD3:");
	
	while(1)
	{
		AD_GetValue();//等待转运并搬运完成
		
		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);
		Delay_ms(100);
		
	}
}
相关推荐
辰哥单片机设计2 小时前
门磁模块详解(防盗感应开关 STM32)
stm32·单片机·嵌入式硬件·传感器
夜间去看海2 小时前
基于51单片机的自动清洗系统(自动洗衣机)
嵌入式硬件·51单片机·proteus·洗衣机
yrx0203073 小时前
stm32 IIC总线busy解决方法
stm32·单片机·嵌入式硬件
YHPsophie4 小时前
ATGM331C-5T杭州中科微BDS/GNSS全星座定位授时模块应用领域
经验分享·笔记·单片机·信息与通信·交通物流
Archie_IT5 小时前
【STM32系统】基于STM32设计的SD卡数据读取与上位机显示系统(SDIO接口驱动、雷龙SD卡)——文末资料下载
arm开发·stm32·单片机·嵌入式硬件
辰哥单片机设计5 小时前
1×4矩阵键盘详解(STM32)
stm32·单片机·嵌入式硬件·矩阵·传感器
wmkswd5 小时前
CAN总线-STM32上CAN外设
stm32·单片机·嵌入式硬件
Ruohongxu5 小时前
LAN8720A-CP-TR-ABC QFN-24 以太网收发器芯片
单片机
努力看懂帖子5 小时前
关于STM32项目面试题02:ADC与DAC篇(输入部分NTC、AV:0-5V、AI:4-20mA和DAC的两个引脚)
嵌入式硬件
嵌入式大圣6 小时前
STM32 单片机最小系统全解析
stm32·单片机·嵌入式硬件