基于stm32f407


一、利用DMA接受串口任意长数据

1、简介

  • 有时候,我们希望利用串口在设备间进行高速而稳定的数据通信,于是定义了一些通讯协议,包括帧头、功能码、数据码、校验码等等,DMA非常适合此类需求。利用串口空闲中断+DMA的方法,我们可以快速地读取整帧数据进行分析。
  • 在使用串口空闲中断+DMA的方法接受数据时,其流程如下
  1. 初始化DMA & USART
  2. 主机开始发送,在从机USART->DR寄存器收到数据后,DMA立刻将数据移至指定的存储buf中(此过程不需要cpu)
  3. 主机一帧数据发送完成后,串口暂时空闲,触发串口空闲中断。在这里可以计算收到数据的字节数,也可以对数据帧进行解码等操作。
  4. 清除标志位,开始下一帧接收

具体可以参考我以前的这篇文章:stm32 利用DMA+串口空闲中断接受任意长数据

2、循环模式和普通模式

在上面的文章中有一点没有讲清楚,就是DMA的循环模式(DMA_Mode_Circular)和普通模式(DMA_Mode_Normal)

在这里插入图片描述

  • DMA_Mode_Normal:在普通模式下,传输结束后(即传输计数DMA1_Streamx->NDTR变为0)将不再产生DMA操作。要开始新的DMA传输,需要3个步骤:①关闭DMA通道,②在DMA_CNDTRx寄存器中重新写入传输数目,③然后重新开启DMA
			//开启一次DMA传输
			//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7 
			//ndtr:数据传输量  
			void DMA_Transfer_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
			{
				DMA_Cmd(DMA_Streamx, DISABLE);                      //关闭DMA传输 
				while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}	//确保DMA可以被设置  
				DMA_SetCurrDataCounter(DMA_Streamx,ndtr);          //数据传输量  
				DMA_Cmd(DMA_Streamx, ENABLE);                      //开启DMA传输 
			}	  
  • DMA_Mode_Circular:在循环模式下,最后一次传输结束时, DMA_SxNDTR寄存器的内容会自动地被重新加载为其初始数值,内部的当前外设/存储器地址寄存器也被重新加载为初始基地址。

二、DMA接受数据错位问题

1、程序设置

  • 帧长8字节
  • DMA普通模式
  • 不使用FIFO进行节拍发送
  • 从USART3->DR寄存器向一个8字节的buf转移数据
//全局变量
#define USART3_RX_BUFFER_SIZE 8
#define USART3_TX_BUFFER_SIZE 5
uint8_t		USART3_Rx_Buffer[USART3_RX_BUFFER_SIZE] = {0};
uint8_t		USART3_Tx_Buffer[USART3_TX_BUFFER_SIZE] = {0};
uint8_t		USART3_Rx_DMA_Buffer[USART3_RX_BUFFER_SIZE] = {0};
uint8_t		USART3_Tx_DMA_Buffer[USART3_TX_BUFFER_SIZE] = {'1','2','3','4','\n'};


//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7
//chx:DMA通道选择,@ref DMA_channel DMA_Channel_0~DMA_Channel_7
//par:外设地址
//mar:存储器地址
//ndtr:数据传输量  
void DMA_Config(DMA_Stream_TypeDef *DMA_Streamx,uint32_t chx,uint32_t par,uint32_t mar,uint32_t dir,u16 ndtr)
{ 
	DMA_InitTypeDef  DMA_InitStructure;
	if((u32)DMA_Streamx>(u32)DMA2)//得到当前stream是属于DMA2还是DMA1
	  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能 
	else 
	  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能 
  	DMA_DeInit(DMA_Streamx);
  	while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}//等待DMA可配置 
	
  	/* 配置 DMA Stream */
	DMA_InitStructure.DMA_Channel 					= chx;  							//通道选择
  	DMA_InitStructure.DMA_PeripheralBaseAddr 		= par;								//DMA外设地址
  	DMA_InitStructure.DMA_Memory0BaseAddr 		    = mar;								//DMA 存储器0地址
  	DMA_InitStructure.DMA_DIR 						= dir;								//direction of transmit.
  	DMA_InitStructure.DMA_BufferSize 				= ndtr;								//数据传输量 
  	DMA_InitStructure.DMA_PeripheralInc				= DMA_PeripheralInc_Disable;		//外设非增量模式
  	DMA_InitStructure.DMA_MemoryInc 				= DMA_MemoryInc_Enable;				//存储器增量模式
  	DMA_InitStructure.DMA_PeripheralDataSize 		= DMA_PeripheralDataSize_Byte;		//外设数据长度:8位
  	DMA_InitStructure.DMA_MemoryDataSize 			= DMA_MemoryDataSize_Byte;			//存储器数据长度:8位
  	DMA_InitStructure.DMA_Mode 						= DMA_Mode_Normal;					//使用普通模式 
  	DMA_InitStructure.DMA_Priority 					= DMA_Priority_High;				//中等优先级
  	DMA_InitStructure.DMA_FIFOMode 					= DMA_FIFOMode_Disable;         	//不用FIFO
  	DMA_InitStructure.DMA_FIFOThreshold 			= DMA_FIFOThreshold_Full;			
  	DMA_InitStructure.DMA_MemoryBurst 				= DMA_MemoryBurst_Single;			//存储器突发单次传输
  	DMA_InitStructure.DMA_PeripheralBurst 		    = DMA_PeripheralBurst_Single;		//外设突发单次传输
  	DMA_Init(DMA_Streamx, &DMA_InitStructure);
  	DMA_Cmd(DMA_Streamx,ENABLE);
} 

//开启一次DMA传输
//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7 
//ndtr:数据传输量  
void DMA_Transfer_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{
	DMA_Cmd(DMA_Streamx, DISABLE);                      //关闭DMA传输 
	while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){}	//确保DMA可以被设置  
	DMA_SetCurrDataCounter(DMA_Streamx,ndtr);           //数据传输量  
	DMA_Cmd(DMA_Streamx, ENABLE);                       //开启DMA传输 
}	  

//配置usart3
void USART3_Init(uint32_t bound)
{
	//GPIO端口设置
 	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD,ENABLE); 
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);
 
	GPIO_PinAFConfig(GPIOD,GPIO_PinSource8,GPIO_AF_USART3); //GPIOD8复用为USART3_TX
	GPIO_PinAFConfig(GPIOD,GPIO_PinSource9,GPIO_AF_USART3); //GPIOD9复用为USART3_RX
	
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8| GPIO_Pin_9; 
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;	
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_Init(GPIOD,&GPIO_InitStructure);
	
	USART_InitStructure.USART_BaudRate = bound;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	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(USART3, &USART_InitStructure);
	
 	NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;		
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			
	NVIC_Init(&NVIC_InitStructure);	
	
	USART_ClearFlag(USART3, USART_FLAG_TC);
//	USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
	USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);	//串口空闲中断	
	USART_ITConfig(USART3, USART_IT_TC, ENABLE);	//发送完成中断
	USART_DMACmd(USART3,USART_DMAReq_Rx | USART_DMAReq_Tx,ENABLE);		
	DMA_Cmd(DMA1_Stream1,ENABLE);
	USART_Cmd(USART3, ENABLE); 
	
	//DMA for rx
	DMA_Config(DMA1_Stream1,DMA_Channel_4, 
						 (uint32_t)&(USART3->DR),
						 (uint32_t)USART3_Rx_DMA_Buffer,
						 DMA_DIR_PeripheralToMemory,  
						 USART3_RX_BUFFER_SIZE);

	//DMA for tx					 
	DMA_Config(DMA1_Stream3,DMA_Channel_4, 
						 (uint32_t)&(USART3->DR),
						 (uint32_t)USART3_Tx_DMA_Buffer,
						 DMA_DIR_MemoryToPeripheral,
						 USART3_TX_BUFFER_SIZE);
	usart3.update=0;
	usart3.locked=0;
	usart3.tx_length=0;
	usart3.rx_length=0;
	usart3.rx_size=USART3_RX_BUFFER_SIZE;
	usart3.tx_size=USART3_TX_BUFFER_SIZE;
	usart3.tx_buf=USART3_Tx_DMA_Buffer;
	usart3.rx_buf=USART3_Rx_DMA_Buffer;

	DMA_Transfer_Enable(DMA1_Stream1,USART3_RX_BUFFER_SIZE);	//开启一次DMA接收
//	DMA_Transfer_Enable(DMA1_Stream3,USART3_TX_BUFFER_SIZE);	//开启一次DMA发送
}

//中断服务函数
void USART3_IRQHandler(void)
{
	uint8_t  rc_tmp;
	uint16_t rc_len;
	//空闲中断(接收)---------------------------------------------------------------------------------------
    if(USART_GetITStatus(USART3,USART_IT_IDLE)!=RESET)
    {
      //清除IDLE标志
      rc_tmp=USART3->SR;
      rc_tmp=USART3->DR;
      DMA_Cmd(DMA1_Stream1, DISABLE);
      DMA_ClearITPendingBit(DMA1_Stream1, DMA_IT_TCIF1);	//清除发送完成标志
      DMA_ClearITPendingBit(DMA1_Stream1, DMA_IT_TEIF1);	//清除发送错误标志
      rc_len = USART3_RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Stream1);	//计算本次收到的数据帧长度
		
	  if(!usart3.locked)
	  {
		usart3.rx_length=rc_len;
		Data_Decode(USART3_Rx_DMA_Buffer);
		DMA_Transfer_Enable(DMA1_Stream1,USART3_RX_BUFFER_SIZE); 
	  }
	}  
	
	//发送完成中断(发送)---------------------------------------------------------------------------------------
	if(USART_GetITStatus(USART3,USART_IT_TC)!=RESET)
	{
	   	DMA_ClearFlag(DMA1_Stream3, DMA_FLAG_TCIF3);  	//清除DMA发送完成标志
		USART_ClearITPendingBit(USART3, USART_IT_TC);	//清除发送完成标志
		DMA_Cmd(DMA1_Stream3, DISABLE); 
	}
}

2、数据缓冲错位问题

(1)问题描述:

发送数据部分没什么问题,之前我设置的接收缓冲buf比传输数据长度多一点,接收也没问题,但今天调DMA的时候我把二者长度设成一样了(都是8字节),于是遇到以下问题

  • 发送数据“12345678”,第一次发送后,只在buf[0]位置受到一个 ‘1’
  • 再次发送数据“12345678”,buf[0]~buf[7]收到 “81234567”
  • 再发送其他8字节数据,如“87654321”,buf[0]~buf[7]收到 “18765432”

基本就是第一次只收到第一个数据,以后都是数据错位,最后的数据跑到第一位了

(2)调试过程

注意,DMA是不受cpu控制的,一旦设置好后就会自动搬运数据,在debug过程中它导致的赋值操作不会在断点停下,所以对DMA进行debug要特别注意

  • 打开debug查看,第一次发送数据“12345678”后,DMA1_Stream1的EN位为1,表明数据流处于使能状态;NDTR值为7,表明本帧数据还有7个字节待接收,看起来没有问题。
  • 进一步查看USART3->DR寄存器,其值为‘1’,说明第一个数据正常DMA正常传送,奇怪的是为何只发了一个数就进入中断了,这说明串口在发了一个数后就处于空闲状态。
  • debug单步调试,发现进刚进空闲中断的时候缓冲buf其实没收到值,此时USART3->DR中已经是‘1’了,buf[0]处的‘1’是在重设接收数据长度,重新使能DMA后才收到的,这就说明第一次DMA实质没有启动
  • USART3_Init()函数最后一句DMA_Transfer_Enable(DMA1_Stream1,USART3_RX_BUFFER_SIZE);处设置断点,发现进入DMA_Transfer_Enable函数前数据长度实质已经设好了,而且EN位也是1(流已使能);而出此函数后,EN位变成0了(流处于禁止状态)
  • 进入DMA_Transfer_Enable函数单步调试,发现失能、重设两步都正常,但是最后使能流失败,看了看使能函数,里面就一个位操作,是在不知道为啥不能使能。于是查看数据手册,发现如下内容:
    在这里插入图片描述
  • 按手册查看DMA1->LISRDMA->HISR,发现在DMA_Transfer_Enable函数中DMA_Cmd(DMA_Streamx, DISABLE)一句执行的瞬间,DMA1->LISR立刻变为0x800,这标志 数据流1传输完成,正因为这个标志没有清除导致使能流失败
    在这里插入图片描述
    在这里插入图片描述
  • 为什么会出现这个置位呢,进一步查看数据手册,有如下内容:
    在这里插入图片描述
  • 这样一来就真相大白了!就是因为串口初始化后进行了多余的重新设置数据传输量长度的操作,导致数据流失能,这样一来串口DR寄存器的数据就不能转移,因此串口被阻塞,触发空闲中断。接下来在中断服务函数USART3_IRQHandler中清除了所有传输完成传输错误标志,使得中断函数最后重新设置数据传输量长度时数据流可以使能成功(EN成功置位)。DMA使能后立即将DR寄存器中的 '1’转移至buf[0],同时NDTR值由8减一变为7,这就出现了第一次发送“12345678”后的情形
  • 第二次点击发送时,由于NDTR值为7,还可以发送“1234567”,它们被DMA转移到buf[1]~buf[7],然后DMA传输完成。此时发送数据的最后一个字节‘8’实际上已经处于USART3->DR中了,但是由于由一轮传输已经结束,故暂时无法转移到buf中。重新设置数据传输量NDTR=8后,DMA再次打开,立即把DR中的剩余’8‘搬运到buf[0],NDTR-1=7。这就产生了第二次发送“1234567”后的情况
  • 此后每次发送8字节数据的情况都和上面一样了,如果不单步debug观察,宏观上看起来数据总是错位的。

(3)解决方案

  • 如果不改程序想恢复正常,只要发送长度少于8字节的数据即可,这也是为什么以前缓冲buf较大时程序没有出现异常
  • 删除USART3_Init()函数最后一句DMA_Transfer_Enable(DMA1_Stream1,USART3_RX_BUFFER_SIZE)即可从根本上解决此问题

(4)后记

出现这种错位,本质上还是在于对底层寄存器了解太少,又没怎么看数据手册。我以前只是简单地顾名思义把DMA_Transfer_Enable函数当成转移使能了,还以为每次传输都要加,导致了这种错位。
所以说知其然还要知其所以然,做技术还是要踏实一点,急于求成不关注细节总会出问题,这次也算给自己提个醒吧

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐