STM32学习之串口采用DMA收发数据(第二周任务其二,需要利用状态机加DMA加串口)

写在前面
在学习这一节知识点的时候,真的是感觉太抽象了,没有一个合适的视频讲的我有那种豁然开朗的感觉,直到我看到了这篇文章,大家可以去看看,里面的描述特别形象。
链接:https://blog.csdn.net/gdjason/article/details/51019219

是什么

DMA —- Directional Memory Access,直接存储器存取用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
其中最重要的就是节省了CPU资源。

串口直接收发和DMA结合串口收发比较

串口直接收发数据过程:
接受:
数据通过串口发送到CPU,CPU然后把数据存储到数组里面
发送:
数据存储到数组里面, CPU把数组里面的数据发送到串口
CPU在这一串操作的过程中是会被占用的,不能进行别的操作

DMA和串口结合起来收发数据过程:
发送:
内存中的数据,通过DMA发送到外设,发送完毕,DMA的发送完毕标志清除。
接受:
串口发送数据,DMA把数据取走,然后把数据存储到数组
在这一串操作的过程中,CPU都是空闲的,可以去干别的事情,可以不用管串口的收发
并且不需要担心串口的收发,串口接收DMA在初始化的时候就处于开启状态,然后需要配置一个空闲中断一直等待数据的到来,也就是只要串口接收数据完毕,就会有一个电平变化,然后就可以进入中断,然后我们不需要在软件上无需做任何事情,只要在初始化配置的时候设置好配置就可以了。 也就是只要你打开了DMA通道,探测到电平变化,进入空闲中断,就可以自动进行收发

没听懂?那用一种更清晰的方法来描述

(来自前方声明的链接博客的描述)
对于DMA,打个比方就很好理解:
角色预设: 淘宝店主 —- STM32 MCU
快递员 —- 外设(如UART,SPI)
发货室 —- DMA
1、首先你是一个淘宝店主,如果每次发货收货都要跟快递沟通交涉会很浪费时间和精力。
2、然后你就自己建了一个发货室,发货室里有好多个货柜箱子,每个箱子上都写着快递名字(如果申通快递,顺丰快递等)。
3、每次发什么快递,你就找到对应的货柜箱子,把货物放进去即可,然后跟快递通知一声。
4、快递取走快件。
5、如果是收货,快递直接把快件放到对应的柜子,然后通知你一下。
6、你过来提取货物。

通过上面的方式,你可以不需要直接跟快递打交道,就可以轻松发货成功,DMA处理方式跟上面例子是一样的。
如果下图:
在这里插入图片描述
那么DMA在STM32上是具体怎么实现的呢? 我们先了解一下STM32关于DMA的相关配置。
1、两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道)
ps:对应我们例子,就是有两个大的发货室,一个有7个货柜,另个有5个货柜。

2、在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推)
ps: 店主可以跟每个快递公司签订协议,可以在货柜前贴上加急(很高),很急(高),急(中),一般(低), 如果同时有几个快递员过来取货,优先根据上面的优先级先取件。

3、独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
ps: 指的是货件大小

4、支持循环的缓冲器管理(会把原来的数据覆盖)

5、每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
ps: 送快递出现的异常情况(送到了一半,送完,快递出错)

在配置之前

相信听了前面这么多的讲解,你一定懂了串口采用DMA的过程,如果没懂…那还可以去查查别的文章
一定要去了解一下空闲中断!!!
下面来讲一下关于寄存器的一些知识点
1.STM32最多有两个DMA控制器,大容量有两个,DMA1有7个通道,DMA2有5个通道。用通俗的话来讲,就是STM32最多有2个快递员,第一个快递员最多有7家人需要收发快递,第二个快递员最多有5家人需要收发快递,一个快递员不能同时收发两家人的快递。
2.因为有这么多家人,当两家同时喊一个快递员来收快递的时候,这时候,就需要有一个先后顺序,这就是DMA的顺序。共有四级:很高、高、中等和低,在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。
3.每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。
4.可以实现存储器和存储器间的传输,外设和存储器,存储器和外设的传输
5.最多传输65536个字节
6.每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。也就是说传输一半数据或者传输完成亦或者是传输出错都可以有一个中断。

在这里插入图片描述

串口利用DMA配置过程

初始化UART1 GPIO
初始化UART1中断
初始化UART1的DMA的Tx,Rx的配置
初始化重新使能Tx,Rx
初始化发送数据 接收数据
初始化UART1空闲中断

实战代码

这是本周学习STM32任务之一,任务中少了Send_data[4]和Send_data[5],是接收方的ID
在这里插入图片描述

/*------------------------------------------------------
下面的就是实现串口和DMA结合的程序
包含了利用DMA实现串口的收发
-------------------------------------------------------*/
#include "sys.h"
#include "usart.h"	
#include "led.h"
#include "string.h"
#include <stdarg.h>
#include "crc.h"
////////////////////////////////////////////////////////////////////////////////// 	 
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用	  
#endif
 

//加入以下代码,支持printf函数,而不需要选择use MicroLIB	  
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
_sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

/*------------------------------------------------------
下面的就是实现串口和DMA结合的程序
包含了利用DMA实现串口的收发
-------------------------------------------------------*/

/*--------------------数据包类型-----------------------*/
enum//枚举
{
    WAITING_FF1,  		//等待1(包头)
    WAITING_FF2,			//等待2(包头)
    RECEIVE_ID,				//接收方id
		RECEIVE_ExtraID,	//接收方扩展ID
    RECEIVE_LEN,			//数据长度
    RECEIVE_PACKAGE,	//数据包
    RECEIVE_CHECK		//接收校验位
}receive_state_;

u8   DMA_Rece_Buf[DMA_Rec_Len];	   //DMA接收串口数据缓冲区
u16  Usart1_Rec_Cnt=0;             //本帧数据长度	

u8   DMA_Tx_Buf[DMA_Tx_Len];	   	//DMA接收串口数据缓冲区
u16  Usart1_Tx_Cnt=0;             //本帧数据长度	

u8 DataBag[3],PWMValue;	
u8 CRCCODE,CRCCODEdaima;											//自己计算的CRC校验码

const CRC_8 crc_8_MAXIM = {0x31,0x00,0x00,TRUE,TRUE};
//UART1的GPIO初始化
void UART1_GPIO_Init(void)
{
	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure; 
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
	
	//USART1_TX   PA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA9

	//USART1_RX	  PA.10
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
	GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化PA10
}

//UART1的中断优先级初始化
void UART1_NVIC_Init(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//串口1接收中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
}

//UART1_RX的DMA初始化
void UART1_DMA_RX_Init(void)
{
	DMA_InitTypeDef DMA_UART1_RX;
	
	//相应的DMA配置
	DMA_DeInit(DMA1_Channel5);   //将DMA的通道5寄存器重设为缺省值  串口1对应的是DMA通道5
	DMA_UART1_RX.DMA_PeripheralBaseAddr = (u32)&USART1->DR;  //DMA外设ADC基地址
	DMA_UART1_RX.DMA_MemoryBaseAddr = (u32)DMA_Rece_Buf;  //DMA内存基地址
	DMA_UART1_RX.DMA_DIR = DMA_DIR_PeripheralSRC;  //数据传输方向,从外设读取发送到内存
	DMA_UART1_RX.DMA_BufferSize = DMA_Rec_Len;  //DMA通道的DMA缓存的大小
	DMA_UART1_RX.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //外设地址寄存器不变
	DMA_UART1_RX.DMA_MemoryInc = DMA_MemoryInc_Enable;  //内存地址寄存器递增
	DMA_UART1_RX.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  //数据宽度为8位
	DMA_UART1_RX.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
	DMA_UART1_RX.DMA_Mode = DMA_Mode_Normal;  //工作在正常缓存模式
	DMA_UART1_RX.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 
	DMA_UART1_RX.DMA_M2M = DMA_M2M_Disable;  //DMA通道x没有设置为内存到内存传输
	
	DMA_Init(DMA1_Channel5, &DMA_UART1_RX);  //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
		
	DMA_Cmd(DMA1_Channel5, ENABLE);  //正式驱动DMA传输
}

//UART1_TX的DMA初始化
void UART1_DMA_TX_Init(void)
{
	DMA_InitTypeDef DMA_UART1_TX;
	
	//相应的DMA配置
	DMA_DeInit(DMA1_Channel4);   //将DMA的通道5寄存器重设为缺省值  串口1对应的是DMA通道5
	DMA_UART1_TX.DMA_PeripheralBaseAddr = (u32)&USART1->DR;  //DMA外设ADC基地址
	DMA_UART1_TX.DMA_MemoryBaseAddr = (u32)DMA_Tx_Buf;  //DMA内存基地址
	DMA_UART1_TX.DMA_DIR = DMA_DIR_PeripheralDST;  //数据传输方向,从内存读取发送到外设
	DMA_UART1_TX.DMA_BufferSize = DMA_Tx_Len;  //DMA通道的DMA缓存的大小
	DMA_UART1_TX.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //外设地址寄存器不变
	DMA_UART1_TX.DMA_MemoryInc = DMA_MemoryInc_Enable;  //内存地址寄存器递增
	DMA_UART1_TX.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  //数据宽度为8位
	DMA_UART1_TX.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
	DMA_UART1_TX.DMA_Mode = DMA_Mode_Normal;  //工作在正常缓存模式
	DMA_UART1_TX.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级 
	DMA_UART1_TX.DMA_M2M = DMA_M2M_Disable;  //DMA通道x没有设置为内存到内存传输
	DMA_Init(DMA1_Channel4, &DMA_UART1_TX);  //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
		
	DMA_Cmd(DMA1_Channel4, ENABLE);  //正式驱动DMA传输
}

//初始化IO 串口1 
//bound:波特率
void uart_init(u32 bound)
{
	USART_InitTypeDef USART_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//使能USART1时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//使能DMA传输
	
	//USART 初始化设置
	USART_InitStructure.USART_BaudRate = bound; //串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
	USART_Init(USART1, &USART_InitStructure); //初始化串口1
	
	UART1_GPIO_Init(); //UART1的GPIO初始化
	UART1_NVIC_Init(); //UART1的中断优先级初始化
	
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); //开启空闲中断
	USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); //使能串口1的DMA接收
	USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口1的DMA发送
	USART_Cmd(USART1, ENABLE); //使能串口 
	
	UART1_DMA_RX_Init(); //UART1_RX的EDMA功能初始化
	UART1_DMA_TX_Init(); //UART1_TX的EDMA功能初始化
}

//重新使能UART1_RX的DMA功能
void UART1_RX_DMA_Enable(void)
{ 
	DMA_Cmd(DMA1_Channel5, DISABLE ); //先停止DMA,暂停接收 
 	DMA_SetCurrDataCounter(DMA1_Channel5, DMA_Rec_Len); //DMA通道的DMA缓存的大小
 	DMA_Cmd(DMA1_Channel5, ENABLE); //使能USART1 TX DMA1 所指示的通道 
}	

//重新使能UART1_TX的DMA功能
void UART1_TX_DMA_Enable(void)
{ 
	DMA_Cmd(DMA1_Channel4, DISABLE ); //先停止DMA
 	DMA_SetCurrDataCounter(DMA1_Channel4, DMA_Tx_Len); //DMA通道的DMA缓存的大小
 	DMA_Cmd(DMA1_Channel4, ENABLE); //使能USART1 TX DMA1 所指示的通道 
}	

//发送len个字节.
//buf:发送区首地址
//len:发送的字节数
void Usart1_Send(u8 *buf,u8 len)
{
	u8 t;
  	for(t=0;t<len;t++)		//循环发送数据
	{		   
		while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);	  
		USART_SendData(USART1,buf[t]);
	}	 
	while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);		
}
/*在空闲中断服务函数里面实现状态机的功能*/
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
		u8 i = 0,Receive_OK = 0,a;
#if SYSTEM_SUPPORT_OS 		//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	
	//串口1空闲中断
	if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)  
	{
		/* 1.清除标志 */
		USART_ClearITPendingBit(USART1, USART_IT_IDLE); //清除中断标志
		
		/* 2.读取DMA */
		USART_ReceiveData(USART1); //读取数据
		Usart1_Rec_Cnt = DMA_Rec_Len - DMA_GetCurrDataCounter(DMA1_Channel5); //接收个数等于接收缓冲区总大小减已经接收的个数
		
		//检验串口是否正常工作代码
		if(DMA_Rece_Buf[0] == 0X42)
			LED1 = !LED1;
		
		/* 3.根据读取的DMA数据来进行相应的操作*/
		//状态机执行相应的命令,因为串口发送的数据是定好的16个,所以我的DMA_Rece_Buf定死了是16个数据
		
		//while循环是为了使得状态机能够验证,需要循环至少8次
		a = 16;			//初始化a值,让循环循环16次
		while(a--)
		{
			switch(receive_state_)
			{
				case WAITING_FF1:
						if(DMA_Rece_Buf[0] == 0x41)
						{
							receive_state_=WAITING_FF2;
						}
						break;
				case WAITING_FF2:
						if(DMA_Rece_Buf[1]==0x54)
							receive_state_=RECEIVE_ID;
						break;
						//以上代码判断包头
						
				case RECEIVE_ID://接收ID
						DMA_Rece_Buf[2] = DMA_Rece_Buf[2] >>1;	//发送的ID应该是C4
						DMA_Rece_Buf[3] = DMA_Rece_Buf[3] >>1;	//发送的ID应该是40
					//	如此才能匹配下方
						if(DMA_Rece_Buf[2] == 0x62 && DMA_Rece_Buf[3] == 0x20)
						//{
							receive_state_ = RECEIVE_ExtraID;
						//LED1 = !LED1;	//验证数据是否发送成功
						//}
						break;
				case RECEIVE_ExtraID://接收扩展ID
						if(DMA_Rece_Buf[4] == 0x00 && DMA_Rece_Buf[5] == 0x00)
							receive_state_ = RECEIVE_LEN;
						//以上代码验证接收方ID
						
				case RECEIVE_LEN:
						if(DMA_Rece_Buf[6] == 0x08)
						//{
							receive_state_=RECEIVE_PACKAGE;
							//LED1 = !LED1;//验证数据是否发送成功
						//}
						break;
						//以上代码验证接收的数据包长度
						
						//接收数据包
				case RECEIVE_PACKAGE:
						while(i < 3)
						{
							DataBag[i] =  DMA_Rece_Buf[7+i];	
							i++;
						}
						i = 0;	
						receive_state_=RECEIVE_CHECK;
						break;
						
						//CRC校验
				 case RECEIVE_CHECK:
						CRCCODE = DMA_Rece_Buf[15];
						CRCCODEdaima = crc8(DMA_Rece_Buf,15,crc_8_MAXIM);
					//我在网上找到的CRC代码,不知道为什么,和计算出来的值就是不匹配,导致了我后面无论如何都无法校验
						if(CRCCODE == 0x01)
						{	
								Receive_OK = 1;
								receive_state_=WAITING_FF1;
								//LED1 = !LED1;//验证数据是否发送成功
								CRCCODE = 0;
								CRCCODEdaima = 0;
						}
					default:
						receive_state_=WAITING_FF1;
			}
		}
		/* 4. 分析数据包内容*/
		if(Receive_OK)
		{
			if(DataBag[0] == 0x01)				//判断是哪一个灯
			{
				if(DataBag[1] == 0x00)			//保持当前状态
					LED0 = LED0;
				else if(DataBag[1] == 0x01)	//开灯
					LED0 = 0;
				else if(DataBag[1] == 0x02)	//关灯
					LED0 = 1;
				else if(DataBag[1] == 0x03)	//状态取反
					LED0 = !LED0;
			}
			else if(DataBag[0] == 0x02)		//判断是哪一个灯
			{	
				if(DataBag[1] == 0x00)			//保持当前状态		
					LED1 = LED1;
				else if(DataBag[1] == 0x01)	//开灯
					LED1 = 0;
				else if(DataBag[1] == 0x02)	//关灯
					LED1 = 1;
				else if(DataBag[1] == 0x03)	//状态取反
					LED1 = !LED1;
			}
			if(DataBag[2] <= 190)					//判断舵机需要转过多少度
			{
				PWMValue = DataBag[2];
			}
		}
		/* 5.搬移数据进行其他处理 */
		//memcpy函数就是把Rece_Buf里面的Cnt个数据存储到Tx_Buf里面
		memcpy(DMA_Tx_Buf, DMA_Rece_Buf, Usart1_Rec_Cnt); //将接收转移通过串口1的DMA方式发送出去测试
		UART1_TX_DMA_Enable(); //开启一次DMA发送,实现转发

		/* 6.开启新的一次DMA接收 */
		UART1_RX_DMA_Enable(); //重新使能DMA,等待下一次的接收
  } 
	
#if SYSTEM_SUPPORT_OS 	//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntExit();  											 
#endif
} 


#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "timer.h"
#include "CRC.h"

extern u8 PWMValue;			//PWM方波值
int main(void)
{		
	u16 times=0;
	/* 系统初始化 */
	delay_init(); //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	/* 自定义外设初始化 */
	LED_Init(); //LED端口初始化
	uart_init(115200); //初始化串口(DMA发送和空闲中断接收模式)
	TIM3_PWM_Init(1999,719);		//这里的arr的值和psc的值可以修改,只要在不分频情况下为72MHZ,719对应0.01ms,1999对应2000*0.01 = 20ms
	while(1)
	{
		delay_ms(10);  
		TIM_SetCompare2(TIM3, 1750 + PWMValue/0.9);
		times++;
		//闪烁LED,提示系统正在运行
//		if(times == 50)
//			{
//				LED0 = !LED0;
//				times = 0;
//			}
		}
}	


从正点原子和网上找到的代码修改
欢迎大家批评指正

猜你喜欢

转载自blog.csdn.net/scarecrow_sun/article/details/107547789