七、Stm32学习-DMA-循环模式与非循环模式

本文详细介绍了STM32单片机中的DMA(直接内存访问)技术,包括DMA的原理、不同类型的触发方式、存储器映像、DMA结构、配置过程,以及如何配合ADC实现数据搬运。通过实例展示了DMA在存储器间和外设数据传输中的应用。
摘要由CSDN通过智能技术生成

DMA东西不多,但是有点绕。并且DMA是非常重要且常用的数据搬用外设。今天简单学习下。

1.DMA简介

DMA是可以直接访问stm32的所有存储器,例如Flash,SRAM等。所以你通过CPU配置了DMA,它就可以自动去各个外设搬运数据到指定的地方,CPU就不用来做这搬运数据的活了,只需要在指定的地方读取CPU所需要的数据即可。

Flash一般是只读,如果要将程序写入Flash,还需要先擦除页,再写入,这是另外一部分内容。然后还要将Boot引脚置对应的高低电平。这些在手册中都能找到。

对于软件触发和特定硬件触发,一般软件触发用在存储器到存储器,因为我们只需要把数据搬运过去,所以软件触发就好比,一直快速地去触发DMA,让它一下就把数据都搬运完。但是硬件触发一般用在外设到存储器,因为外设的数据到来是不定时的,需要有一定的信号 ,不能软件触发一下把所有都搬运完,说不定那个时候外设数据还没采集到呢!综上,软件触发一般用在存储器到存储器;硬件触发一般用在外设到存储器。

2.存储器映像

这些地址在数据手册中都可以查找到。

ROM(只读存储器)和RAM(随机存取存储器)是两种重要的存储器类型

所以stm32单片机的存储地址0-FFFFFFFF,这是个非常大的存储空间,4GB。但是事实上,有很多的空间都是空的,stm32本身只有几kb的容量。可以看到都是有对应的地址的,外设的地址就是

0x4000 0000开始。可以在单片机程序中,对变量取地址,再强转,把他的地址拿出来看看和这个有什么联系。

int a;
const int b; 
int main(void)
{
	OLED_Init();
	while(1)
	{
		OLED_ShowHexNum(1, 1, (uint32_t)&a, 8);
		OLED_ShowHexNum(2, 1, (uint32_t)&ADC1->DR, 8);
		OLED_ShowHexNum(3, 1, (uint32_t)&b, 8);
	}
}

显示这几个变量的地址

发现和我们阅读手册的地址是吻合的。

3. DMA结构框图

虽然DMA有多个通道,但是DMA的总线却只有一条,所以所有的通道只能分时复用这个总线。然后通过总线的仲裁器来判断优先级,来决定谁先过。

4.DMA的基本结构

 起始地址就是填你需要搬用的数据的源头以及目的地的地址;数据宽度则是一次搬用多大的数据,有8位,16位,32位;地址是否自增,指的是搬用完一次之后是否自增地址;传输计数器,就是要转运数据的次数,每转运一次,计数器就会减1;而自动重装载器,就是在传输计数器变为0的时候重新赋值给传输计数器,达到自动开启下一轮的搬运;M2M就是选择硬件触发或者软件触发,所以软件触发和自动重装一起用,软件触发是一直触发DMA,这样DMA就停不下了;在修改传输计数器的值的时候必须先关闭DMA,修改完毕后再打开,手册里面规定的。

5.DMA请求

不同的通道对应不同的外设请求信号。要注意。

6.数据宽度与对齐

这个是针对两个传输站点不同的数据类型来设置的。

概括:小的转大的,高位舍弃;大的转小的,高位补零

7.程序编写-DMA数据转运-非循环模式

将数组A中的数据每秒+1后,转运到数组B中,+1后延迟一小会,方便看实验现象。

(1)开启DMA的外设时钟

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

(2)DMA的配置

	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;						//存储器地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte ;    //数据8位
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  			//地址是否自增
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;					//外设地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_Byte; //数据8位
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;		//地址是否自增
	DMA_InitStructure.DMA_BufferSize = Size;							//计数器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;					//传输方向
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable ;						//选择硬件触发或软件触发,打开软件触发
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;						//选择DMA的传输模式为不连续,连续or单次
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 				//DMA优先级

	DMA_Init(DMA1_Channel1,&DMA_InitStructure);

上面6个为存储器的配置,分别配置地址;数据位为8位;地址自增;

再配置BufferSize,就是计数器,初始化数组有4个数据,所以配置为4;再配置传输的方向,选择外设为源端,即外设传递数据到存储器;配置为软件触发;DMA的模式选择为不连续模式;到这DMA的配置就完成了。

(3)DMA的使能

DMA_Cmd(DMA1_Channel1, ENABLE);

当使能DMA后,立刻开始数据转运,转运完成后(计数器为0后)停止。所以需要我们再次写入BufferSize值。

(4)编写修改DMA的BufferSize值(计数器值)函数

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);					//DMA失能,在写入传输计数器之前,需要DMA暂停工作
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);	//写入传输计数器,指定将要转运的次数
	DMA_Cmd(DMA1_Channel1, ENABLE);						//DMA使能,开始工作
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);	//等待DMA工作完成
	DMA_ClearFlag(DMA1_FLAG_TC1);						//清除工作完成标志位
}

先关闭DMA,调用函数写入计数器值;然后再开启DMA;然后检查DMA传输完成标志位(还可以检查传输一半等标志位),最后清除标志位等待下一次。

所以每调用一次函数,就转运一次数据。

8.程序编写-DMA数据转运-循环模式

循环模式的话,是不需要判断标志位,因为循环模式会自动重新装载计数器的值。所以不需要调用函数来开启转运,直接修改DMA的配置即可。

void DMA_User_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//开启DMA的时钟
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;						//存储器地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte ;    //数据8位
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  			//地址是否自增
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;					//外设地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_Byte; //数据8位
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;		//地址是否自增
	DMA_InitStructure.DMA_BufferSize = Size;							//计数器
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;					//传输方向
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable ;						//选择硬件触发或软件触发,打开软件触发。硬件触发一般和外设配合
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular ;						//选择DMA的传输模式为不连续,连续or单次
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 				//DMA优先级

	DMA_Init(DMA1_Channel1,&DMA_InitStructure);
	DMA_Cmd(DMA1_Channel1, ENABLE);  									//打开DMA,转运后等待DMA关闭,重新写入计数器
}

修改DMA的模式为循环模式既可。开启一次DMA即可,不需要反复地去开启关闭修改BufferSize值。

9.程序编写-DMA配合ADC数据转运-循环模式

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//开启DMA1的时钟
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	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);
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	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);
	
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_ContinuousConvMode=ENABLE;
	ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;
	ADC_InitStructure.ADC_NbrOfChannel=4;
	ADC_InitStructure.ADC_ScanConvMode=ENABLE;
	ADC_Init(ADC1,&ADC_InitStructure);
	
		/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;									
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//外设基地址,ADC1储存数据的地址
	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_Cmd(DMA1_Channel1,ENABLE);
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	ADC_DMACmd(ADC1,ENABLE);
	
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//软件触发AD转换一次

ADC正常配置,我想要硬件全自动,那我就选择开启连续转换,并且我4路的ADC通道,我再开启扫描模式,一次扫描4个ADC通道,再进行转换,再扫描,再转换。

DMA配置要注意,外设的地址自增应该关闭,这样才能保持我们的数据源头一直是ADC1,这样印证了ADC的数据覆盖问题,我们需要DMA一直不断地把数据搬走,不然就覆盖了。然后要开启数据目的地地地址自增,这样才能分开存入我们想要存入地地方。然后开启循环模式。关闭软件触发,开启硬件触发,硬件地触发源为ADC1,这边没有特定参数配置DMA的触发源。

但是可以开启ADC到DMA的通道,这样ADC一有数据过来,DMA就搬用,一有数据过来DAM就搬运。

然后我们调用目的地的数组就可以查看ADC的数据了。

  • 24
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值