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 headeruint8_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的通道1DMA_Cmd(DMA1_Channel1,DISABLE);
}void My_DMA_TransFer(void)//DMA开启搬运的函数
{DMA_Cmd(DMA1_Channel1,DISABLE);//关闭DMADMA_SetCurrDataCounter(DMA1_Channel1,My_Size);//写入传输计数器的值DMA_Cmd(DMA1_Channel1,ENABLE);//开启DMAwhile(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 headeruint16_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);//复位完成,硬件置0ADC_StartCalibration(ADC1);//开始校准,给CR2_CAL置1while(ADC_GetCalibrationStatus(ADC1) == SET);//校准完成,硬件置0//8.开启搬运ADC_DMACmd(ADC1 ,ENABLE);//开启ADC1硬件触发源,当一个通道转运完成后,就会自动请求DMADMA_Cmd(DMA1_Channel1,ENABLE);
}void AD_GetValue(void)//ADC触发函数并数据搬运
{DMA_Cmd(DMA1_Channel1,DISABLE);//关闭DMADMA_SetCurrDataCounter(DMA1_Channel1,4);//写入传输计数器的值DMA_Cmd(DMA1_Channel1,ENABLE);//开启DMAADC_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 headeruint16_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);//复位完成,硬件置0ADC_StartCalibration(ADC1);//开始校准,给CR2_CAL置1while(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);//开启DMAwhile(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);}
}