DMA数据搬运详解:工作原理与实际应用

DMA数据搬运

  • 1、DMA的简介
  • 2、STM32中的DMA结构
  • 3、案列
  • 3.1、将数组DataA中的数据搬运到DataB中
  • 3.2、ADC扫描模式+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中


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

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

    #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存储器中

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

    #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文件的代码如下:

    #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);//手动清除标志位
    }
    

    主程序文件的代码如下:

    /*
    	存储器------>存储器(数据的拷贝)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文件的代码如下:
    代码如下:

    #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);//手动清除标志位
    }
    

    主程序文件的代码:

    /*
    	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文件的代码如下:

    #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);//手动清除标志位
    }
    

    主程序文件的代码:

    /*
    	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);
    		
    	}
    }
    

    作者:浅陌pa

    物联沃分享整理
    物联沃-IOTWORD物联网 » DMA数据搬运详解:工作原理与实际应用

    发表回复