【STM32】DMA超详细解析·入门级教程

目录

1.  DMA概述

1.1  简介

1.2  存储器映像

1.3  DMA框图

1.4  基本结构

1.5  触发源选择

1.6  数据宽度与对齐

2.  USART实现数据发送

3.  DMA实现发送数据转运

3.1  DMA初始化

3.1.1  传输方向

3.1.2  外设与存储器参数配置

3.1.2.1  起始地址

3.1.2.2  数据宽度

3.1.2.3  地址是否自增

3.1.3  传输数据的大小

3.1.4  模式选择

3.1.5  存储器到存储器配置(M2M)

3.1.5.1  模式1:存储器到外设(M2P)

3.1.5.2  模式2:外设到存储器(P2M)

3.1.5.3  模式3:存储器到存储器(M2M)

3.1.6  优先级配置

3.1.7  DMA通道配置

3.1.8  DMA中断配置

3.2  DMA数据发送

3.3  主函数代码


1.  DMA概述

1.1  简介

         DMA,全称Direct Memory Access,即直接存储器访问。

        DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。

        如果没有不通过DMA,CPU传输数据还要以内核作为中转站,例如将ADC采集的数据转移到SRAM中。

        而如果通过DMA的话,DMA控制器将获取到的外设数据存储到DMA通道中,然后通过DMA总线与DMA总线矩阵协调,将数据传输到SRAM中,期间不需内核参与。

主要特征:

  • 同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
  • 独立数据源和目标数据区的传输宽度(字节、半字、全字);
  • 可编程的数据传输数目:最大为65535;
  • 对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。

1.2  存储器映像

        计算机系统的五大组成部分:运算器、控制器、存储器、输入设备和输出设备。

        其中运算器和控制器合在一起叫CPU。

STM32所有类型的存储器:

类型起始地址存储器用途
ROM0x0800 0000程序存储器Flash存储C语言编译后的程序代码
0x1FFF F000系统存储器存储BootLoader,用于串口下载
0x1FFF F800选项字节存储一些独立于程序代码的配置参数
RAM0x2000 0000运行内存SRAM存储运行过程中的临时变量
0x4000 0000外设寄存器存储各个外设的配置参数
0xE000 0000内核外设寄存器存储内核各个外设的配置参数

1.3  DMA框图

        在看之前我们先需要搞懂一个概念什么是寄存器,寄存器是一种特殊的存储器,寄存器的每一位后面连接着一根导线,可以操作外设电平的状态,完成如操作引脚电平,开关的打开或者关闭,切换数据选择器,当做计数器等的操作:

所以寄存器可以说是连接软件和硬件的桥梁,软件读写寄存器就相当于在控制硬件的执行。

        下面我们来看看DMA的框图:

①:DMA总线访问各个存储器;

②:DMA内部的多个通道进行独立的数据转运;

③:仲裁器用于管理多个通道,防止冲突;

④:DMA从设备用于配置DMA参数;

⑤:DMA请求用于硬件触发DMA的数据转运;

1.4  基本结构

        上面的图看不懂,没关系,我们总结一下:

        我们拆分一下,先看这部分:

        可以看出DMA的转运是后方向的,可以外设到内存,也可以内存到外设,我们可以通过函数进行控制:

#define DMA_DIR_PeripheralDST              ((uint32_t)0x00000010)
#define DMA_DIR_PeripheralSRC              ((uint32_t)0x00000000)
#define IS_DMA_DIR(DIR) (((DIR) == DMA_DIR_PeripheralDST) || \
                         ((DIR) == DMA_DIR_PeripheralSRC))

        然后再来看看二者所需的数据:

        首先是基地址,也就是两者的起始地址,这两个参数决定数据从哪里来到哪里去,所需函数:

//外设
  uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Channelx. */

//存储器
  uint32_t DMA_MemoryBaseAddr;     /*!< Specifies the memory base address for DMAy Channelx. */

        然后是数据宽度,其作用计时指定一次转运要按多大的数据宽度来进行,其可以选择字节(uint8_t),半字(uint16_t),字(uint32_t):

#define DMA_PeripheralDataSize_Byte        ((uint32_t)0x00000000)
#define DMA_PeripheralDataSize_HalfWord    ((uint32_t)0x00000100)
#define DMA_PeripheralDataSize_Word        ((uint32_t)0x00000200)
#define IS_DMA_PERIPHERAL_DATA_SIZE(SIZE) (((SIZE) == DMA_PeripheralDataSize_Byte) || \
                                           ((SIZE) == DMA_PeripheralDataSize_HalfWord) || \
                                           ((SIZE) == DMA_PeripheralDataSize_Word))

        其函数是:

//外设
  uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
                                        This parameter can be a value of @ref DMA_peripheral_data_size */

//存储器
  uint32_t DMA_MemoryDataSize;     /*!< Specifies the Memory data width.
                                        This parameter can be a value of @ref DMA_memory_data_size */

        地址是否自增,作用是决定下次转运是不是要把地址移到下一个位置去,其参数可以选择使能或者失能:

#define DMA_PeripheralInc_Enable           ((uint32_t)0x00000040)
#define DMA_PeripheralInc_Disable          ((uint32_t)0x00000000)
#define IS_DMA_PERIPHERAL_INC_STATE(STATE) (((STATE) == DMA_PeripheralInc_Enable) || \
                                            ((STATE) == DMA_PeripheralInc_Disable))

        其函数是:

//外设
  uint32_t DMA_PeripheralInc;      /*!< Specifies whether the Peripheral address register is incremented or not.
                                        This parameter can be a value of @ref DMA_peripheral_incremented_mode */

//存储器
  uint32_t DMA_MemoryInc;          /*!< Specifies whether the memory address register is incremented or not.
                                        This parameter can be a value of @ref DMA_memory_incremented_mode */

        然后我们看看另一个参数:传输计数器,这个值表示DMA需要转运几次,你可以将其理解为他是一个自减计数器,假如你初始化的值为5,那么每次转运一次计数减1,当减到0的时候,DMA就不会在进行转运了,并且当其减到0,之前自增的地址又会回到起始地址,方便新一轮的转换:

  uint32_t DMA_BufferSize;         /*!< Specifies the buffer size, in data unit, of the specified Channel. 
                                        The data unit is equal to the configuration set in DMA_PeripheralDataSize
                                        or DMA_MemoryDataSize members depending in the transfer direction. */

        那么他是怎么进行新一轮的转换呢?这就要靠自动重装器,其作用就是当转运次数归零后,询问是否将转运次数回到最初值,这样如果我们配置为循环模式,DMA计数归零回到起始地址,而自动重装器又将DMA的数据恢复,这样就可以循环:

#define DMA_Mode_Circular                  ((uint32_t)0x00000020)
#define DMA_Mode_Normal                    ((uint32_t)0x00000000)
#define IS_DMA_MODE(MODE) (((MODE) == DMA_Mode_Circular) || ((MODE) == DMA_Mode_Normal))

        函数:

  uint32_t DMA_Mode;               /*!< Specifies the operation mode of the DMAy Channelx.
                                        This parameter can be a value of @ref DMA_circular_normal_mode.
                                        @note: The circular buffer mode cannot be used if the memory-to-memory
                                              data transfer is configured on the selected Channel */

        然后就是触发机制,主要配置其使能或者失能,使能软件触发,失能硬件触发:

#define DMA_M2M_Enable                     ((uint32_t)0x00004000)
#define DMA_M2M_Disable                    ((uint32_t)0x00000000)
#define IS_DMA_M2M_STATE(STATE) (((STATE) == DMA_M2M_Enable) || ((STATE) == DMA_M2M_Disable))

这里需要注意一点软件触发不能和循环一起使用。

因为软件触发就是想将传输计数器清零,但是循环模式我们上面也说了清零后会进行自动重装,因此不能一起使用。

        最后就是,使能DMA,也就是开启DMA:

1.5  触发源选择

        对于硬件触发我们需要根据不同的触发源,选择不同的通道,软件触发就随便了:

        其不同的外设,需要对应不同的通道,可以参考上下图:

        这里我们需要考虑一个问题:当多个DMA请求同时到来时,是如何工作呢?

        STM32的DMA仲裁器处理多请求时,其优先级判定分为两个明确的阶段:

软件优先级:这是你可以在程序中配置的。每个数据流(Stream)或通道(Channel)都可以被设置为以下四个等级之一:

  • 非常高(Very High)
  • 高(High)
  • 中(Medium)
  • 低(Low)

硬件优先级:当两个或更多请求的软件优先级相同时,仲裁器会转而依靠硬件规则来裁决:编号较小的数据流或通道,拥有更高的优先级。例如,DMA1的优先级高于DMA2。

1.6  数据宽度与对齐

        根据下表,简单来说就是右对齐,要是目标宽度不够,取最低位(可以参考第四行),要是目标宽度比源端宽度大则高位补零(可以参考第二或者三行):

2.  USART实现数据发送

        对于串口的原理这里不进行过多表述,这里为了方便寻找资源,我直接使用江协/江科大的串口代码,在这里演示一下不使用DMA转运出现的效果。

        程序设计:通过USART1发送5000个A的数据,同时通过GPIO0口电亮一颗LED灯进行循环亮灭,看看会出现什么情况。其中工程模版,我用的之前移植好的ZET6的代码:

STM32f103ZET6移植工程模版_freertos菜鸟教程资源-优快云下载

        首先对于串口,可以自己去复制江协的代码,这里我对初始化函数名称进行了一个简单的修改,其他的都没有动:

#include "stm32f10x.h"                
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_TxPacket[4];				//定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];				//定义接收数据包数组
uint8_t Serial_RxFlag;					//定义接收数据包标志位

void Usart_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA10引脚初始化为上拉输入
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	//模式,发送模式和接收模式均选择
	USART_InitStructure.USART_Parity = USART_Parity_No;		//奇偶校验,不需要
	USART_InitStructure.USART_StopBits = USART_StopBits_1;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行	
}


/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i ++)		//根据数字长度遍历数字的每一位
	{
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');	//依次调用Serial_SendByte发送每位数字
	}
}

/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}

/**
  * 函    数:串口发送数据包
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,Serial_TxPacket数组的内容将加上包头(FF)包尾(FE)后,作为数据包发送出去
  */
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

/**
  * 函    数:获取串口接收数据包标志位
  * 参    数:无
  * 返 回 值:串口接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
  */
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}

/**
  * 函    数:USART1中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;	//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		
		/*使用状态机的思路,依次处理数据包的不同部分*/
		
		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == 0xFF)			//如果数据确实是包头
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;	//将数据存入数据包数组的指定位置
			pRxPacket ++;				//数据包的位置自增
			if (pRxPacket >= 4)			//如果收够4个数据
			{
				RxState = 2;			//置下一个状态
			}
		}
		/*当前状态为2,接收数据包包尾*/
		else if (RxState == 2)
		{
			if (RxData == 0xFE)			//如果数据确实是包尾部
			{
				RxState = 0;			//状态归0
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}



#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

void Usart_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);

#endif

        来到主函数,我们创建一个宏定义,循环在串口发送缓冲区写入5000个A,然后进行发送,为了方便观察实验效果,我将该函数写在while循环之外,这样发送完成后LED灯就能正常工作,前后可以做个对比:

#define Sum_Duff_Size 5000

for(int i = 0;i<Sum_Duff_Size;i++)
{
    Serial_TxPacket[Sum_Duff_Size] = 'P';
}

Serial_SendArray(Serial_TxPacket, Sum_Duff_Size);

        完整main函数:

#include "stm32f10x.h" 
#include "LED.h"
#include "Usart_DMA.h"
            
#include "Delay.h"

#define Sum_Duff_Size 5000

int main(void)
{
	LED_GPIO_Config();
	Usart_Init();

	for(int i = 0;i<Sum_Duff_Size;i++)
	{
		Serial_TxPacket[i] = 'P';
	}

	Serial_SendArray(Serial_TxPacket, Sum_Duff_Size);

	while (1)
	{
		GPIO_SetBits(GPIOB,GPIO_Pin_0);
		Delay_ms(500);
		GPIO_ResetBits(GPIOB,GPIO_Pin_0);
		Delay_ms(500);
	}
}

        此时我们下载程序,会发现串口持续打印数据,但是此时LED灯被卡着无法进行闪烁,需要等待数据发送完成才能进行下一步的操作:

        由于每发送或接收一个字节,串口都会产生一个中断。CPU需要停下当前的工作,保存现场,跳转到中断服务程序,处理这一个字节的数据(例如,将数据从接收缓冲区复制到用户数组,或从用户数组加载下一个要发送的字节),然后恢复现场,继续之前的工作。这么做就会导致CPU大量时间浪费在数据的传输上,阻塞后续功能的实现,并且对于115200的波特率,每秒就有115200个字节,意味着CPU每秒要被中断11.5万次,负担极重。

        为了减轻CPU的负担,我们引入DMA进行数据转运。DMA模式下CPU只需要在传输开始前,对DMA控制器进行配置,告诉它数据的源地址、目标地址和传输长度。之后,整个数据块的传输(比如1024个字节)就完全由DMA控制器接管。在整个块传输完成或达到半传输、传输完成时,DMA才会产生一个中断通知CPU。CPU在此期间可以完全不受打扰地执行其他任务,或者进入低功耗模式。

3.  DMA实现发送数据转运

        在开始编写代码前我们进行一些准备工作,首先找到这张图,我们根据这张图进行DMA代码的配置:

        为了方便代码的阅读,我们将上方串口的代码部分,除了串口初始化的代码,其余全部删除:

#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

void Usart_Init(void)
{
    /*开启时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);    //开启USART1的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //开启GPIOA的时钟
    
    /*GPIO初始化*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);                    //将PA9引脚初始化为复用推挽输出
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);                    //将PA10引脚初始化为上拉输入
    
    /*USART初始化*/
    USART_InitTypeDef USART_InitStructure;                    //定义结构体变量
    USART_InitStructure.USART_BaudRate = 9600;                //波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;    //硬件流控制,不需要
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;    //模式,发送模式和接收模式均选择
    USART_InitStructure.USART_Parity = USART_Parity_No;        //奇偶校验,不需要
    USART_InitStructure.USART_StopBits = USART_StopBits_1;    //停止位,选择1位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;        //字长,选择8位
    USART_Init(USART1, &USART_InitStructure);                //将结构体变量交给USART_Init,配置USART1
    
    /*中断输出配置*/
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);            //开启串口接收数据的中断

    /*NVIC中断分组*/
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);            //配置NVIC为分组2
    
    /*NVIC配置*/
    NVIC_InitTypeDef NVIC_InitStructure;                    //定义结构体变量
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;        //选择配置NVIC的USART1线
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;            //指定NVIC线路使能
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;        //指定NVIC线路的抢占优先级为1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;        //指定NVIC线路的响应优先级为1
    NVIC_Init(&NVIC_InitStructure);                            //将结构体变量交给NVIC_Init,配置NVIC外设
    
    /*USART使能*/
    USART_Cmd(USART1, ENABLE);                                //使能USART1,串口开始运行    
}

        添加头文件:

#include "Usart_DMA.h"

3.1  DMA初始化

        首先常规的初始化配置,开启DMA的时钟,定义结构体变量:

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

    DMA_InitTypeDef DMA_InitStructure;  // 定义结构体变量

        对于结构体变量的参数:

/** 
  * @brief  DMA 初始化结构体定义
  */

typedef struct
{
  uint32_t DMA_PeripheralBaseAddr; /*!< 指定DMAy通道x的外设基地址 */

  uint32_t DMA_MemoryBaseAddr;     /*!< 指定DMAy通道x的存储器基地址 */

  uint32_t DMA_DIR;                /*!< 指定外设是源还是目的地。
                                        此参数可以是 @ref DMA_data_transfer_direction 中的值 */

  uint32_t DMA_BufferSize;         /*!< 指定通道的缓冲区大小,以数据单元为单位。 
                                        数据单元等于在DMA_PeripheralDataSize或DMA_MemoryDataSize
                                        成员中设置的配置,具体取决于传输方向 */

  uint32_t DMA_PeripheralInc;      /*!< 指定外设地址寄存器是否递增。
                                        此参数可以是 @ref DMA_peripheral_incremented_mode 中的值 */

  uint32_t DMA_MemoryInc;          /*!< 指定存储器地址寄存器是否递增。
                                        此参数可以是 @ref DMA_memory_incremented_mode 中的值 */

  uint32_t DMA_PeripheralDataSize; /*!< 指定外设数据宽度。
                                        此参数可以是 @ref DMA_peripheral_data_size 中的值 */

  uint32_t DMA_MemoryDataSize;     /*!< 指定存储器数据宽度。
                                        此参数可以是 @ref DMA_memory_data_size 中的值 */

  uint32_t DMA_Mode;               /*!< 指定DMAy通道x的操作模式。
                                        此参数可以是 @ref DMA_circular_normal_mode 中的值。
                                        @注意:如果在所选通道上配置了存储器到存储器数据传输,
                                              则不能使用循环缓冲区模式 */

  uint32_t DMA_Priority;           /*!< 指定DMAy通道x的软件优先级。
                                        此参数可以是 @ref DMA_priority_level 中的值 */

  uint32_t DMA_M2M;                /*!< 指定DMAy通道x是否将用于存储器到存储器传输。
                                        此参数可以是 @ref DMA_memory_to_memory 中的值 */
}DMA_InitTypeDef;

3.1.1  传输方向

        根据框图我们知道,DMA数据转运的方向有三种:外设到存储器、存储器到外设、存储器到存储器。

        而我们想要通过芯片向电脑传输数据,就是存储器到外设,因此配备:

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;                      // 数据传输方向:存储器到外设

        对于DMA_DIR的参数,可以参考:

/** @defgroup DMA_data_transfer_direction 
  * @{
  */

#define DMA_DIR_PeripheralDST              ((uint32_t)0x00000010)//存储器→外设
#define DMA_DIR_PeripheralSRC              ((uint32_t)0x00000000)//外设→存储器
#define IS_DMA_DIR(DIR) (((DIR) == DMA_DIR_PeripheralDST) || \
                         ((DIR) == DMA_DIR_PeripheralSRC))

        对于存储器到存储器的设置,又一个具体的参数下面会涉及到,可以通过目录直接跳转过去。

3.1.2  外设与存储器参数配置

        我们需要对如下参数进行配置:

3.1.2.1  起始地址

        首先对于起始地址,对于外设的起始地址,由于我们使用的是串口1进行数据传输,所以这里我们需要采用其数据寄存器(DR)的地址,这里我们创建一个宏定义,使串口1指向DR寄存器:

#define USART_DR_ADDRESS   ((uint32_t)&(USART1->DR))

        对于存储器的起始地址,我们创建一个发送缓冲区(也就是一个数组即可):

#define USART_TX_BUFFER_SIZE 5000 // 定义发送缓冲区大小

uint8_t Usart_Tx_Buf[USART_TX_BUFFER_SIZE];

        此时的起始地址配置:

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART_DR_ADDRESS;  // 外设基地址:串口1数据寄存器


    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Usart_Tx_Buf;         // 存储器基地址:发送缓冲区
3.1.2.2  数据宽度

        对于数据宽度的取值,可以分为:

/** @defgroup DMA_peripheral_data_size 
  * @{
  */

#define DMA_PeripheralDataSize_Byte        ((uint32_t)0x00000000)// 字节(8位)
#define DMA_PeripheralDataSize_HalfWord    ((uint32_t)0x00000100)// 半字(16位)
#define DMA_PeripheralDataSize_Word        ((uint32_t)0x00000200)// 字(32位)
#define IS_DMA_PERIPHERAL_DATA_SIZE(SIZE) (((SIZE) == DMA_PeripheralDataSize_Byte) || \
                                           ((SIZE) == DMA_PeripheralDataSize_HalfWord) || \
                                           ((SIZE) == DMA_PeripheralDataSize_Word))
宏定义数据宽度字节数适用场景
DMA_PeripheralDataSize_Byte8位1字节串口、SPI字节传输
DMA_PeripheralDataSize_HalfWord16位2字节ADC12位数据、16位外设
DMA_PeripheralDataSize_Word32位4字节32位外设、内存批量操作

        这里全设成字节:

    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节

    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;        // 存储器数据宽度:字节
3.1.2.3  地址是否自增

        这个可以简单的看出,Enable使能自增,Disable禁止自增:

/** @defgroup DMA_peripheral_incremented_mode 
  * @{
  */

#define DMA_PeripheralInc_Enable           ((uint32_t)0x00000040)
#define DMA_PeripheralInc_Disable          ((uint32_t)0x00000000)
#define IS_DMA_PERIPHERAL_INC_STATE(STATE) (((STATE) == DMA_PeripheralInc_Enable) || \
                                            ((STATE) == DMA_PeripheralInc_Disable))

        首先对于外设地址,前面我们也提到了,我们使用的是串口的数据寄存器(DR),所有数据都要写入到同一个USART数据寄存器,串口硬件会自动将数据移出,不需要改变寄存器地址:

    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        // 外设地址不自增

        对于存储器的数据,如果不自增,所有数据都会写入同一个内存位置,会进行数据的覆盖,导致数据丢失,因此需要自增:

    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;                // 存储器地址自增

3.1.3  传输数据的大小

        也就是一次性传输数据的量,我们前面举例传输的是5000字节,那么这里也设置为5000:

#define SENDBUFF_SIZE 5000 // 一次发送的数据量

    DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;                      // 传输数据大小

        注意,这个量不是代表的字节数,而是而是数据项的数量,例如:

// 示例1:字节传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_BufferSize = 100;  // 传输100个字节(100 * 1字节)

// 示例2:半字传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_BufferSize = 100;  // 传输200个字节(100 * 2字节)

// 示例3:字传输
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_BufferSize = 100;  // 传输400个字节(100 * 4字节)

3.1.4  模式选择

        模式选择可以进行单次模式或者循环模式进行数据传输:

#define DMA_Mode_Circular                  ((uint32_t)0x00000020)    // 循环模式
#define DMA_Mode_Normal                    ((uint32_t)0x00000000)    // 普通模式

#define IS_DMA_MODE(MODE) (((MODE) == DMA_Mode_Circular) || ((MODE) == DMA_Mode_Normal))

        这里我们只进行单次传输:

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;                          // 普通模式(发送完成停止)

3.1.5  存储器到存储器配置(M2M)

        对于存储器到存储器的配置有专门的参数配置:

  uint32_t DMA_M2M;                /*!< Specifies if the DMAy Channelx will be used in memory-to-memory transfer.
                                        This parameter can be a value of @ref DMA_memory_to_memory */

        其参数如下:

/** @defgroup DMA_memory_to_memory 
  * @{
  */

#define DMA_M2M_Enable                     ((uint32_t)0x00004000)
#define DMA_M2M_Disable                    ((uint32_t)0x00000000)
#define IS_DMA_M2M_STATE(STATE) (((STATE) == DMA_M2M_Enable) || ((STATE) == DMA_M2M_Disable))

        常见的配置有:

3.1.5.1  模式1:存储器到外设(M2P)
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;  // 存储器→外设
3.1.5.2  模式2:外设到存储器(P2M)
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 外设→存储器
3.1.5.3  模式3:存储器到存储器(M2M)
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;   // 使能M2M模式
// 注意:M2M模式下DMA_DIR参数通常被忽略

3.1.6  优先级配置

        对于优先级的配置,我们上面介绍的时候知道其优先级分为四级:

#define DMA_Priority_VeryHigh              ((uint32_t)0x00003000)    // 非常高优先级
#define DMA_Priority_High                  ((uint32_t)0x00002000)    // 高优先级
#define DMA_Priority_Medium                ((uint32_t)0x00001000)    // 中等优先级
#define DMA_Priority_Low                   ((uint32_t)0x00000000)    // 低优先级

#define IS_DMA_PRIORITY(PRIORITY) (((PRIORITY) == DMA_Priority_VeryHigh) || \
                                   ((PRIORITY) == DMA_Priority_High) || \
                                   ((PRIORITY) == DMA_Priority_Medium) || \
                                   ((PRIORITY) == DMA_Priority_Low))

        不同优先级的适用场景:

优先级级别数值适用场景
VeryHigh最高实时性要求极高的数据传输
High重要外设,如USB、以太网
Medium一般外设,如SPI、I2C
Low不重要的后台传输
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;                // 最高优先级

3.1.7  DMA通道配置

        由于我们使用的是USART1_TX,因此需要初始化通道4:

    DMA_Init(DMA1_Channel4, &DMA_InitStructure);// 初始化DMA通道 - USART1_TX使用通道4

        同时为了确保DMA从干净的状态开始工作,避免误触发中断或错误状态,这里我们还需要清除所有旧的状态标志位:

    DMA_ClearFlag(DMA1_FLAG_GL4 | DMA1_FLAG_TC4 | DMA1_FLAG_HT4 | DMA1_FLAG_TE4);// 清除DMA通道4的所有标志位

        对于这几个标志位的含义:

标志位含义说明
DMA1_FLAG_GL4全局标志通道4的全局中断标志
DMA1_FLAG_TC4传输完成数据全部传输完成时置位
DMA1_FLAG_HT4半传输完成传输完成一半数据时置位
DMA1_FLAG_TE4传输错误传输过程中发生错误时置位

        对于DMA1的各通道标志位,其实就更改一下后缀的数字:

通道全局标志传输完成半传输传输错误
Channel1DMA1_FLAG_GL1DMA1_FLAG_TC1DMA1_FLAG_HT1DMA1_FLAG_TE1
Channel2DMA1_FLAG_GL2DMA1_FLAG_TC2DMA1_FLAG_HT2DMA1_FLAG_TE2
Channel3DMA1_FLAG_GL3DMA1_FLAG_TC3DMA1_FLAG_HT3DMA1_FLAG_TE3
Channel4DMA1_FLAG_GL4DMA1_FLAG_TC4DMA1_FLAG_HT4DMA1_FLAG_TE4
Channel5DMA1_FLAG_GL5DMA1_FLAG_TC5DMA1_FLAG_HT5DMA1_FLAG_TE5
Channel6DMA1_FLAG_GL6DMA1_FLAG_TC6DMA1_FLAG_HT6DMA1_FLAG_TE6
Channel7DMA1_FLAG_GL7DMA1_FLAG_TC7DMA1_FLAG_HT7DMA1_FLAG_TE7

3.1.8  DMA中断配置

        首先使能传输完成中断:

    DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);// 使能传输完成中断

        其中对于DMA的中断类型:

DMA_IT_TC    // Transfer Complete - 传输完成中断
DMA_IT_HT    // Half Transfer - 半传输完成中断  
DMA_IT_TE    // Transfer Error - 传输错误中断

        配置NVIC:

    // 配置DMA传输完成中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

        初始不使能DMA,等待数据准备好后再使能:

DMA_Cmd(DMA1_Channel4, DISABLE);

3.2  DMA数据发送

        首先创建一个数据发送标志位,注意使用volatile声明,防止编译器优化:

volatile uint8_t dma_busy = 0; // DMA发送状态标志

        设置一个while循环,根据标志位,等待DMA数据发送完成:

void USART_WaitDMAFinish(void)
{
    while (dma_busy)
    {
        // 等待DMA发送完成
    }
}

        首先判断一下传输的数据是否为空,或者超出缓冲区范围,如果是则直接返回,不进行数据传输,若是满足,则通过wait函数等待上一次的数据完成,完成后复制本次数据到缓冲区,配置DMA传输数据长度,然后清除标志位,设置忙状态,然后使能DMA发送中断:

void USART_SendDataDMA(uint8_t *data, uint16_t len)
{
	//若是数据为0,或者超范围直接返回,不进行数据转运
    if (len == 0 || len > USART_TX_BUFFER_SIZE) 
        return;
    
    USART_WaitDMAFinish();// 等待上一次DMA传输完成
    memcpy(Usart_Tx_Buf, data, len);// 复制数据到发送缓冲区
    DMA_SetCurrDataCounter(DMA1_Channel4, len);// 配置DMA传输数据长度
    
    DMA_ClearFlag(DMA1_FLAG_GL4 | DMA1_FLAG_TC4 | DMA1_FLAG_HT4 | DMA1_FLAG_TE4); // 清除所有DMA标志
    dma_busy = 1;// 设置忙标志
    
    DMA_Cmd(DMA1_Channel4, ENABLE); // 使能DMA
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);// 使能USART的DMA发送
}

        然后回进入到中断当中,如果数据传输完成标志位置0,进行下一次数据传输,这里我们只进行发送,不进行后续处理,因此没有后续代码,想要进行一些处理的可以自行添加:

// DMA传输完成中断服务函数
void DMA1_Channel4_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC4))
    {
        DMA_ClearITPendingBit(DMA1_IT_TC4); // 清除中断标志
        DMA_Cmd(DMA1_Channel4, DISABLE);    // 禁用DMA
        dma_busy = 0;                       // 清除忙标志
        
        // 可以在这里添加发送完成回调函数
    }
}

3.3  主函数代码

        主函数部分就是将刚刚的串口发送,更改为DMA的发送:

#include "stm32f10x.h" 
#include "LED.h"
#include "Usart_DMA.h"
            
#include "Delay.h"

#define Sum_Duff_Size 5000
extern uint8_t Usart_Tx_Buf[USART_TX_BUFFER_SIZE];

int main(void)
{
	LED_GPIO_Config();
	Usart_Init();
	DMA_Config();

	for(int i = 0;i<Sum_Duff_Size;i++)
	{
		Usart_Tx_Buf[i] = 'A';
	}

	USART_SendDataDMA(Usart_Tx_Buf,Sum_Duff_Size);

	while (1)
	{
		GPIO_SetBits(GPIOB,GPIO_Pin_0);
		Delay_ms(500);
		GPIO_ResetBits(GPIOB,GPIO_Pin_0);
		Delay_ms(500);
	}
}

        我们可以运行看一下效果:

        运行后可以发现,串口在正常打印数据的同时,LED灯也在正常的闪烁(这里没法放视频,就不进行演示了,可以自行看一下效果)。

完整工程:

基于STM32实现串口的DMA数据转运.zip资源-优快云下载

STM32学习笔记_时光の尘的博客-优快云博客

FreeRTOS实战(四)·USART串口实现DMA数据转运(江协/江科大代码移植)_freertos dma-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值