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

目录

[1. DMA概述](#1. DMA概述)

[1.1 简介](#1.1 简介)

[1.2 存储器映像](#1.2 存储器映像)

[1.3 DMA框图](#1.3 DMA框图)

[1.4 基本结构](#1.4 基本结构)

[1.5 触发源选择](#1.5 触发源选择)

[1.6 数据宽度与对齐](#1.6 数据宽度与对齐)

[2. USART实现数据发送](#2. USART实现数据发送)

[3. DMA实现发送数据转运](#3. DMA实现发送数据转运)

[3.1 DMA初始化](#3.1 DMA初始化)

[3.1.1 传输方向](#3.1.1 传输方向)

[3.1.2 外设与存储器参数配置](#3.1.2 外设与存储器参数配置)

[3.1.2.1 起始地址](#3.1.2.1 起始地址)

[3.1.2.2 数据宽度](#3.1.2.2 数据宽度)

[3.1.2.3 地址是否自增](#3.1.2.3 地址是否自增)

[3.1.3 传输数据的大小](#3.1.3 传输数据的大小)

[3.1.4 模式选择](#3.1.4 模式选择)

[3.1.5 存储器到存储器配置(M2M)](#3.1.5 存储器到存储器配置(M2M))

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

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

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

[3.1.6 优先级配置](#3.1.6 优先级配置)

[3.1.7 DMA通道配置](#3.1.7 DMA通道配置)

[3.1.8 DMA中断配置](#3.1.8 DMA中断配置)

[3.2 DMA数据发送](#3.2 DMA数据发送)

[3.3 主函数代码](#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所有类型的存储器:

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

1.3 DMA框图

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

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

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

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

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

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

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

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

1.4 基本结构

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

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

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

cpp 复制代码
#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))

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

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

cpp 复制代码
//外设
  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):

cpp 复制代码
#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))

其函数是:

cpp 复制代码
//外设
  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 */

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

cpp 复制代码
#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))

其函数是:

cpp 复制代码
//外设
  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,之前自增的地址又会回到起始地址,方便新一轮的转换:

cpp 复制代码
  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的数据恢复,这样就可以循环:

cpp 复制代码
#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))

函数:

cpp 复制代码
  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 */

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

cpp 复制代码
#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菜鸟教程资源-CSDN下载

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

cpp 复制代码
#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);		//清除标志位
	}
}
cpp 复制代码
#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灯就能正常工作,前后可以做个对比:

cpp 复制代码
#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函数:

cpp 复制代码
#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代码的配置:

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

cpp 复制代码
#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,串口开始运行    
}

添加头文件:

cpp 复制代码
#include "Usart_DMA.h"

3.1 DMA初始化

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

cpp 复制代码
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // 开启DMA时钟

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

对于结构体变量的参数:

cpp 复制代码
/** 
  * @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数据转运的方向有三种:外设到存储器、存储器到外设、存储器到存储器。

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

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

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

cpp 复制代码
/** @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寄存器:

cpp 复制代码
#define USART_DR_ADDRESS   ((uint32_t)&(USART1->DR))

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

cpp 复制代码
#define USART_TX_BUFFER_SIZE 5000 // 定义发送缓冲区大小

uint8_t Usart_Tx_Buf[USART_TX_BUFFER_SIZE];

此时的起始地址配置:

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


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

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

cpp 复制代码
/** @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_Byte 8位 1字节 串口、SPI字节传输
DMA_PeripheralDataSize_HalfWord 16位 2字节 ADC12位数据、16位外设
DMA_PeripheralDataSize_Word 32位 4字节 32位外设、内存批量操作

这里全设成字节:

cpp 复制代码
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节

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

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

cpp 复制代码
/** @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数据寄存器,串口硬件会自动将数据移出,不需要改变寄存器地址:

cpp 复制代码
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;        // 外设地址不自增

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

cpp 复制代码
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;                // 存储器地址自增

3.1.3 传输数据的大小

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

cpp 复制代码
#define SENDBUFF_SIZE 5000 // 一次发送的数据量

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

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

cpp 复制代码
// 示例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 模式选择

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

cpp 复制代码
#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))

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

cpp 复制代码
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;                          // 普通模式(发送完成停止)

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

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

cpp 复制代码
  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 */

其参数如下:

cpp 复制代码
/** @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)
cpp 复制代码
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;  // 存储器→外设
3.1.5.2 模式2:外设到存储器(P2M)
cpp 复制代码
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  // 禁用M2M模式
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 外设→存储器
3.1.5.3 模式3:存储器到存储器(M2M)
cpp 复制代码
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;   // 使能M2M模式
// 注意:M2M模式下DMA_DIR参数通常被忽略

3.1.6 优先级配置

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

cpp 复制代码
#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 不重要的后台传输
cpp 复制代码
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;                // 最高优先级

3.1.7 DMA通道配置

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

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

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

cpp 复制代码
    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的各通道标志位,其实就更改一下后缀的数字:

通道 全局标志 传输完成 半传输 传输错误
Channel1 DMA1_FLAG_GL1 DMA1_FLAG_TC1 DMA1_FLAG_HT1 DMA1_FLAG_TE1
Channel2 DMA1_FLAG_GL2 DMA1_FLAG_TC2 DMA1_FLAG_HT2 DMA1_FLAG_TE2
Channel3 DMA1_FLAG_GL3 DMA1_FLAG_TC3 DMA1_FLAG_HT3 DMA1_FLAG_TE3
Channel4 DMA1_FLAG_GL4 DMA1_FLAG_TC4 DMA1_FLAG_HT4 DMA1_FLAG_TE4
Channel5 DMA1_FLAG_GL5 DMA1_FLAG_TC5 DMA1_FLAG_HT5 DMA1_FLAG_TE5
Channel6 DMA1_FLAG_GL6 DMA1_FLAG_TC6 DMA1_FLAG_HT6 DMA1_FLAG_TE6
Channel7 DMA1_FLAG_GL7 DMA1_FLAG_TC7 DMA1_FLAG_HT7 DMA1_FLAG_TE7

3.1.8 DMA中断配置

首先使能传输完成中断:

cpp 复制代码
    DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);// 使能传输完成中断

其中对于DMA的中断类型:

cpp 复制代码
DMA_IT_TC    // Transfer Complete - 传输完成中断
DMA_IT_HT    // Half Transfer - 半传输完成中断  
DMA_IT_TE    // Transfer Error - 传输错误中断

配置NVIC:

cpp 复制代码
    // 配置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,等待数据准备好后再使能:

cpp 复制代码
DMA_Cmd(DMA1_Channel4, DISABLE);

3.2 DMA数据发送

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

cpp 复制代码
volatile uint8_t dma_busy = 0; // DMA发送状态标志

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

cpp 复制代码
void USART_WaitDMAFinish(void)
{
    while (dma_busy)
    {
        // 等待DMA发送完成
    }
}

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

cpp 复制代码
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,进行下一次数据传输,这里我们只进行发送,不进行后续处理,因此没有后续代码,想要进行一些处理的可以自行添加:

cpp 复制代码
// 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的发送:

cpp 复制代码
#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资源-CSDN下载

STM32学习笔记_时光の尘的博客-CSDN博客
FreeRTOS实战(四)·USART串口实现DMA数据转运(江协/江科大代码移植)_freertos dma-CSDN博客

相关推荐
chao1898442 小时前
基于TMS320F28069 DSP开发板实现RS485通信
单片机·嵌入式硬件
朱嘉鼎3 小时前
消费级MCU如何管理内存
单片机·嵌入式硬件
R6bandito_4 小时前
STM32 HAL库原子操作编译问题解决指南
c语言·ide·经验分享·stm32·单片机·嵌入式硬件·mcu
Jerry丶Li4 小时前
三十、STM32的USART (串口发送+接收)
stm32·单片机·嵌入式硬件
从零点4 小时前
STM32连接以太网失败自动重新连接
stm32·单片机·嵌入式硬件
dlwlrma_5164 小时前
NUCLEO-G0B1RE STM32G0B1RET6的学习(3)——SPI从DMA HAL库到应用层回调函数CallBack的定义
stm32
python百炼成钢5 小时前
13.RTC实时时钟
linux·stm32·单片机·嵌入式硬件·实时音视频
国科安芯5 小时前
FreeRTOS 在 AS32系列RISC-V 架构MCU电机驱动中的应用实践与优化
单片机·嵌入式硬件·安全·架构·压力测试·risc-v·安全性测试
染予5 小时前
GPIO中断实现流程
单片机·嵌入式硬件