STM32学习笔记(8_2)- DMA直接存储器存取代码

无人问津也好,技不如人也罢,都应静下心来,去做该做的事。

最近在学STM32,所以也开贴记录一下主要内容,省的过目即忘。视频教程为江科大(改名江协科技),网站jiangxiekeji.com

本期开始学习DMA,直接存储器存取。DMA是一个数据转运小助手,它主要是用来协助CPU,完成数据转运的工作。

const是c语言的关键字,表示的是常量的意思,被const修饰的变量,在程序中,只能读,不能写。在STM32中,使用const定义的变量,是存储在Flash里面的。当然这里就不应该说是“变量"了,而应该说是“常量"。也就是说,被const修饰的变量,它的值只能在定义的时候赋值,不能在后面的程序中再次赋值。

什么时候需要用const定义常量呢?这个是当我们程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省SRAM的空间。比如查找表、字库数据等等。如果你有一个很大的查找表或者字库,最好加一个const。

DMA基本结构

DMA常用函数

打开dma.h,拉到最后。

Delnit:恢复缺省配置

void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);

DMA_Init:初始化

void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);

DMA_StructInit:结构体初始化

void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);

DMA_Cmd:使能

void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);

DMA_ITConfig:中断输出使能

void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);

DMA设置当前数据寄存器:就是给这个传输计数器写数据的,

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 

 DMA获取当前数据寄存器:这个函数就是返回传输计数器的值,如果你想看看,还剩多少数据没有转运,就可以调用这个函数,获取一下传输计数器,这样就行了。

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

最后四个函数,获取标志位状态、清除标志位、获取中断状态、清除中断挂起位。

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
void DMA_ClearFlag(uint32_t DMAy_FLAG);
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
void DMA_ClearITPendingBit(uint32_t DMAy_IT);

两个程序现象 

DMA数据转运

在这个程序里,我们将使用DMA,进行存储器到存储器的数据转运。也就是把一个数组里面的数据,复制到另一个数组里。这里先定义了一个数组DataA,里面存的是1、2、3、4,作为待转运的源数据。然后下面再定义一个数组DataB,里面存的是4个0,作为转运数据的目的地。之后我们将会写一个模块,叫MyDMA。然后MyDMA初始化,把源数组和目的数组的地址传进去,再传入转运数据的长度4,接着执行主循环的流程。

第一步,自增,变化一下源数组DataA的测试数据。

第二步,显示一下DataA和DataB,然后延时一秒,方便观看。

第三步,调用一下MyDMA_Transfer函数,使用DMA进行数据转运。和主程序里直接使用for循环,使用CPU一个个手动地转运数据,效果是一样的。

接着最后,再显示一下DataA和DataB,看一下数据是不是从DataA转运到了DataB。

这里第一行是DataA,右边是DataA数组的地址。第二行就是DataA的源数据了,每隔两秒变一次。第三行是DataB,右边是DataB数组的地址。最后一行,是DataB的目的地数据。

可以看到,DataA每变一次,Delay1秒后,数据就转运到了DataB。这个转运过程,就是由DMA来完成的。你也可以定义100个、1000个等等数据,然后使用DMA来进行转运,都是可以的。这是第一个程序的现象。

接线图

和OLED显示实验接线时一样的。

初始化步骤

根据DMA基本结构图来配置即可

第一步,RCC开启DMA的时钟,DMA要开启AHB总线。

	/*开启时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);						//开启DMA的时钟

第二步,就可以直接调用DMA_Init,初始化这里的各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增,方向、传输计数器、是否需要自动重装、选择触发源,当然还有一个通道优先级,图没画出来。这些参数,通过一个结构体就可以配置好了。

	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;										//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;						//外设基地址,给定形参AddrA
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			//外设地址自增,选择使能
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;							//存储器基地址,给定形参AddrB
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;			//存储器数据宽度,选择字节
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;					//存储器地址自增,选择使能
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;						//数据传输方向,选择由外设到存储器
	DMA_InitStructure.DMA_BufferSize = Size;								//转运的数据大小(转运次数)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;							//模式,选择正常模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;								//存储器到存储器,选择使能
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;					//优先级,选择中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);							//将结构体变量交给DMA_Init,配置DMA1的通道1

这里,如果你选择的是硬件触发,不要忘了在对应的外设调用一下XXXDMA_Cmd,开启一下触发信号的输出。如果你需要DMA的中断,那就调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,然后写中断函数就行了。中断的配置各个外设都一样。

第三步,就可以进行开关控制,DMA_Cmd,给指定的通道使能,就完成了。

	/*DMA使能*/
	DMA_Cmd(DMA1_Channel1, DISABLE);	//这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始

最后,在运行的过程中,如果转运完成,传输计数器清0了,这时想再给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能,这样就行了。

DMA转运有三个条件:第一,就是开关控制,DMA_Cmd必须使能;第二,就是传输计数器必须大于0;第三,就是触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次。当传输计数器等手0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。注意一下,写传输计数器时,必须要先关闭DMA,再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。

代码展示

这里DMA不涉及外部电路,所以直接在System里添加MyDMA.c、.h文件,封装对应的驱动函数。

main函数

#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,观察转运后的现象
	}
}

MyDMA.h文件

#ifndef __MYDMA_H
#define __MYDMA_H

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);

#endif

MyDMA.c文件

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;					//定义全局变量,用于记住Init函数的Size,供Transfer函数使用

/**
  * 函    数:DMA初始化
  * 参    数:AddrA 原数组的首地址
  * 参    数:AddrB 目的数组的首地址
  * 参    数:Size 转运的数据大小(转运次数)
  * 返 回 值:无
  */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;					//将Size写入到全局变量,记住参数Size
	
	/*开启时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);						//开启DMA的时钟
	
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;										//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;						//外设基地址,给定形参AddrA
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			//外设地址自增,选择使能
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;							//存储器基地址,给定形参AddrB
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;			//存储器数据宽度,选择字节
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;					//存储器地址自增,选择使能
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;						//数据传输方向,选择由外设到存储器
	DMA_InitStructure.DMA_BufferSize = Size;								//转运的数据大小(转运次数)
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;							//模式,选择正常模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;								//存储器到存储器,选择使能
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;					//优先级,选择中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);							//将结构体变量交给DMA_Init,配置DMA1的通道1
	
	/*DMA使能*/
	DMA_Cmd(DMA1_Channel1, DISABLE);	//这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}

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

DMA+AD多通道

用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运。最终,AD转换的数据就会直接自动地跑到我们定义的数组里面来,之后我们就只需要用OLED显示一下就行了。看上去就很方便。

这个硬件电路和程序现象和上一节的AD多通道都是一模一样的,也是测量PA0~PA3这4个通道的模拟量。就只是在STM32端,使用了扫描模式,并且加了DMA转运数据。

接线图

这里DMA+AD多通道的代码有点看懵了,后面理解了在补回来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值