基于STM32F4实现串口1+DMA中断+环形队列的数据收发处理

基于STM32F4实现串口1+DMA中断+环形队列的数据收发处理

用法简介

串口加环形队列基本满足普通单片机的数据收发处理,实现起来简单可靠,但实际项目中会出现多个串口连用同时又希望数据收发过程不要占用太多CPU时间,这时DMA就有大用了。
最近在做项目时同时使用STM32F407的6路串口作为485通信的接口,由于通信中断太多导致收发耦合,同外部485自动收发电路产生了兼容问题,外部485电路会因为两端同时有收发数据导致数据异常,从而MCU处理错误数据引发一系列问题。因此本次使用DMA来减少中断以及中断中数据的处理时间。

总体方案

总体方案为:开启STM32F4的串口1收发DMA功能,接收采用DMA传输完成中断函数将每一个字节压入环形队列。main函数进程中实现对数据的DMA发送,DMA发送完成队列实现对中断标志的清空。
查参考手册选用DMA2的数据流5/7的通道4即可。

串口1RX
接收DMA
缓存字节
接收队列
应用程序1
应用程序2
发送队列
发送缓存
发送DMA
串口1TX

环形队列的实现

为便于移植采用了C++的形式进行了封装,类的定义如下:

class CQueue   
{
    
    
	public:
	CQueue(uint8_t *puch_DATA, uint16_t uin_BufLen);
		CQueue(){
    
    };
		~CQueue(){
    
    };	
		
		//data
public:
		BOOL   b_ModSetEn;
		INT32U uin_front;//队首
		INT32U uin_rear;//队尾
		INT32U uin_length;//数据长度
		INT32U uin_MaxLength;//

		INT8U  *queueBuf;//队列数组地址			

private:
		INT16U ul_ErrorChk;	//异常记录				
				
		//Function
public:
		BOOL IsEmpty(void);//检查是否为空队列
		BOOL IsFull(void);//检查是否为满队列 
		BOOL PopData(INT8U * BUF);//出队操作
		BOOL PushData(INT8U DATA);// 入队操作
		BOOL SetQueue(uint8_t *puch_DATA, uint16_t uin_BufLen);// 队列设置

};

构造函数:

CQueue::CQueue(uint8_t *puch_DATA, uint16_t uin_BufLen)// 
{
    
    
		queueBuf = puch_DATA;//队列数组地址	
		uin_length = 0;//数据长度
		uin_MaxLength =uin_BufLen;//数据长度
		uin_front = uin_MaxLength-1;//队首
		uin_rear = uin_MaxLength-1;//队尾
	
		memset(puch_DATA,0,uin_MaxLength);
}

入队操作:

BOOL CQueue::PushData(INT8U DATA)//入队操作
{
    
    
	if(IsFull()) return false;
    queueBuf[uin_rear] = DATA;
	uin_rear = (uin_rear +1 )% uin_MaxLength;//转圈圈起来
	uin_length++;
	return true;
}	

出队操作:

BOOL CQueue::PopData(INT8U * BUF)//出队操作
{
    
    
	if(IsEmpty()) return false;
	*BUF = queueBuf[ uin_front ] ;
	uin_front = (uin_front +1 )% uin_MaxLength;//转圈圈起来
	uin_length--;
	return true;
}

我采用的是最简单的方式,网上也有很多,参考一下就行了。

串口配置

使用串口1进行配置,波特率可设置。

void UART1_DMAConfig(INT32U Buart)
{
    
    
  GPIO_InitTypeDef GPIO_InitStructure;
  USART_InitTypeDef USART_InitStructure;

  RCC_AHB1PeriphClockCmd(PC_USART_RX_GPIO_CLK | PC_USART_TX_GPIO_CLK, ENABLE);
  /* 使能 USART 时钟 */
  RCC_APB2PeriphClockCmd(PC_USART_CLK, ENABLE);
  /* GPIO初始化 */
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  /* 配置Tx引脚为复用功能  */
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin = PC_USART_TX_PIN;
  GPIO_Init(PC_USART_TX_GPIO_PORT, &GPIO_InitStructure);
  /* 配置Rx引脚为复用功能 */
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin = PC_USART_RX_PIN;
  GPIO_Init(PC_USART_RX_GPIO_PORT, &GPIO_InitStructure);

  /* 连接 PXx 到 USARTx_Tx*/
  GPIO_PinAFConfig(PC_USART_RX_GPIO_PORT, PC_USART_RX_SOURCE, PC_USART_RX_AF);
  /*  连接 PXx 到 USARTx__Rx*/
  GPIO_PinAFConfig(PC_USART_TX_GPIO_PORT, PC_USART_TX_SOURCE, PC_USART_TX_AF);

  /* 配置串PC_USART 模式 */
  /* 波特率设置:PC_USART_BAUDRATE */
  USART_InitStructure.USART_BaudRate = Buart;
  /* 字长(数据位+校验位):8 */
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  /* 停止位:1个停止位 */
  USART_InitStructure.USART_StopBits = USART_StopBits_1;
  /* 校验位选择:不使用校验 */
  USART_InitStructure.USART_Parity = USART_Parity_No;
  /* 硬件流控制:不使用硬件流 */
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  /* USART模式控制:同时使能接收和发送 */
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  /* 完成USART初始化配置 */
  USART_Init(PC_USART, &USART_InitStructure);

  /* 使能串口接收中断 */
  UART_DMA_RxConfig(DMA2_Stream5,DMA_Channel_4,(uint32_t)&USART1->DR,(uint32_t)&auch_RxData,1);          
  UART_DMA_TxConfig(DMA2_Stream7,DMA_Channel_4,(uint32_t)(&USART1->DR),(uint32_t)auch_TxData,1);
  /* 使能串口收发DMA */
  USART_DMACmd(USART1, USART_DMAReq_Rx , ENABLE);                                       //串口DMA开启
  USART_DMACmd(USART1, USART_DMAReq_Tx , ENABLE);                                       //串口DMA开启	
  /* 使能串口 */
  USART_Cmd(PC_USART, ENABLE);
}

串口的配置很常规,野火教程上抄一段就行了。下边还有一些宏定义:

#define PC_USART                             USART1
#define PC_USART_CLK                         RCC_APB2Periph_USART1
#define PC_USART_BAUDRATE                    115200  //串口波特率

#define PC_USART_RX_GPIO_PORT                GPIOA
#define PC_USART_RX_GPIO_CLK                 RCC_AHB1Periph_GPIOA
#define PC_USART_RX_PIN                      GPIO_Pin_10
#define PC_USART_RX_AF                       GPIO_AF_USART1
#define PC_USART_RX_SOURCE                   GPIO_PinSource10

#define PC_USART_TX_GPIO_PORT                GPIOA
#define PC_USART_TX_GPIO_CLK                 RCC_AHB1Periph_GPIOA
#define PC_USART_TX_PIN                      GPIO_Pin_9
#define PC_USART_TX_AF                       GPIO_AF_USART1
#define PC_USART_TX_SOURCE                   GPIO_PinSource9

#define PC_USART_IRQHandler                  USART1_IRQHandler
#define PC_USART_IRQ                 				USART1_IRQn

#define DMA_USART_RX_IRQ 						 DMA2_Stream5_IRQn
#define DMA_USART_Rx_IRQHandler                  DMA2_Stream5_IRQHandler
#define UART_RX_DMA_STREAM						DMA2_Stream5
#define UART_RX_DMA_CHANNEL						DMA_Channel_4

#define DMA_USART_TX_IRQ 						DMA2_Stream7_IRQn
#define DMA_USART_Tx_IRQHandler                 DMA2_Stream7_IRQHandler
#define UART_TX_DMA_STREAM						DMA2_Stream7
#define UART_TX_DMA_CHANNEL						DMA_Channel_4

串口接收DMA配置

接收DMA采用传输完成中断即可,中断标志DMA_IT_TCIF5
在这里插入图片描述
具体配置代码UART_DMA_RxConfig(DMA2_Stream5,DMA_Channel_4,(uint32_t)&USART1->DR,(uint32_t)&auch_RxData,1)如下:

void UART_DMA_RxConfig(DMA_Stream_TypeDef *DMA_Streamx,u32 channel,u32 addr_Peripherals,u32 addr_Mem,u16 Length)                                       //DMA的初始化
{
    
    
	NVIC_InitTypeDef NVIC_InitStructure;
	DMA_InitTypeDef UARTDMA_InitStructure;

	if((u32)DMA_Streamx>(u32)DMA2)//DMA1?DNA2
	{
    
    
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2 	
	}
	else 
	{
    
    
		RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1
	}                 

  DMA_ClearFlag(UART_RX_DMA_STREAM, DMA_FLAG_FEIF5 | DMA_FLAG_DMEIF5 | DMA_FLAG_TEIF5 | DMA_FLAG_HTIF5 | DMA_FLAG_TCIF5);
                                                                                                             
  /* DMA2 Stream5  or Stream6 disable */
  DMA_Cmd(UART_RX_DMA_STREAM, DISABLE);                                                     
  while (DMA_GetCmdStatus(UART_TX_DMA_STREAM) != DISABLE){
    
    }//保证DMA可设置
  /* DMA2 Stream3  or Stream6 Config */
  DMA_DeInit(UART_RX_DMA_STREAM);

  UARTDMA_InitStructure.DMA_Channel = channel;//通道
  UARTDMA_InitStructure.DMA_PeripheralBaseAddr = ( uint32_t)(addr_Peripherals) ;//外设地址
  UARTDMA_InitStructure.DMA_Memory0BaseAddr =   ( uint32_t)addr_Mem; //       缓存地址
  UARTDMA_InitStructure.DMA_DIR =  DMA_DIR_PeripheralToMemory;//方向-外设到内存
  UARTDMA_InitStructure.DMA_BufferSize = Length; //传输长度
  UARTDMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  UARTDMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  UARTDMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte ;
  UARTDMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte ;//以字节8位方式传输
  UARTDMA_InitStructure.DMA_Mode = DMA_Mode_Normal   ;//传输模式正常模式
  UARTDMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
  UARTDMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  UARTDMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  UARTDMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single ;
  UARTDMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  DMA_Init(UART_RX_DMA_STREAM, &UARTDMA_InitStructure);

  NVIC_InitStructure.NVIC_IRQChannel = DMA_USART_RX_IRQ ;                                                //中断分组
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

  DMA_ITConfig(UART_RX_DMA_STREAM, DMA_IT_TC, ENABLE);//开启传输完成中断触发   //DMA中断时能
  /* DMA2 Stream3  or Stream6 enable */
  USART_DMACmd(USART1, USART_DMAReq_Rx , ENABLE);      //串口DMA开启
  DMA_Cmd(UART_RX_DMA_STREAM, ENABLE);     //DMA开启  
}

要注意的点就是DMA数据流以及通道要确认正确,传进去的外设及内存的地址为32位,传输字长、外设及内存的自增,中断的开启标志等也是关键。
接收中断如下,进入传输中断证明全局变量auch_RxData已经拿到一个字节数据,然后是判断队列情况,压入数据队列,清除中断标志以及重新使能DMA通道。

void DMA2_Stream5_IRQHandler()
{
    
    
  if(DMA_GetITStatus(UART_RX_DMA_STREAM  , DMA_IT_TCIF5)) 
  {
    
    
    if (!Uart1RxQueue.IsFull()) //若缓冲队列未满,开始传输
      {
    
    
        //往缓冲区写入数据,如使用串口接收、dma写入等方式
         Uart1RxQueue.PushData(auch_RxData);
      }
      else
      {
    
    
        return ;
      }

    DMA_ClearITPendingBit(UART_RX_DMA_STREAM , DMA_IT_TCIF5);  //清中断标志
	DMA_SetCurrDataCounter(UART_RX_DMA_STREAM, 1);//设置数据传输长度
	DMA_Cmd(UART_RX_DMA_STREAM, ENABLE);  //重新使能DMA通道
  }
  return ;
}

这里清中断是因为手册里的要求
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
重新写传输长度以及使能也是寄存器的实用要求:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
实测这里只需要使能通道就可以了,与数据项寄存器说明一致。或者这里不进行重新使能,将传输模式改为循环模式即可UARTDMA_InitStructure.DMA_Mode = DMA_Mode_Circular

串口发送DMA配置

发送的话配置要分成两部分,配置部分和发送使能部分,因为一旦使能就开始进行发送了。
配置部分如下:
要先定义一些全局变量

static INT8U auch_TxData[QUEUEMAX];//DMA发送部分操作的缓存
DMA_InitTypeDef UARTDMA_TXInitStructure;//DMA发送设置结构体 设置成全局方便直接修改
void UART_DMA_TxConfig(DMA_Stream_TypeDef *DMA_Streamx,u32 channel,u32 addr_Peripherals,u32 addr_Mem,u16 Length)                                       //DMA的初始化,省略串口初始化
{
    
    
NVIC_InitTypeDef NVIC_InitStructure;
//DMA_InitTypeDef UARTDMA_TXInitStructure;//这个要设置成全局的单独定义,为发送使能使用
if((u32)DMA_Streamx>(u32)DMA2)//DMA1?DNA2
{
    
    
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2 	
}
else 
{
    
    
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1
}               
  DMA_ClearFlag(UART_TX_DMA_STREAM, DMA_FLAG_FEIF7 | DMA_FLAG_DMEIF7 | DMA_FLAG_TEIF7 | DMA_FLAG_HTIF7 | DMA_FLAG_TCIF7);
                                                                                                             
  /* DMA2 Stream5  or Stream6 disable */
  DMA_Cmd(UART_TX_DMA_STREAM, DISABLE);                                                     

  /* DMA2 Stream3  or Stream6 Config */
  DMA_DeInit(UART_TX_DMA_STREAM);
  while (DMA_GetCmdStatus(UART_TX_DMA_STREAM) != DISABLE){
    
    }//µÈ´ýDMA¿ÉÅäÖà 

  UARTDMA_TXInitStructure.DMA_Channel = channel;
  UARTDMA_TXInitStructure.DMA_PeripheralBaseAddr = ( uint32_t)(addr_Peripherals) ;
  UARTDMA_TXInitStructure.DMA_Memory0BaseAddr =   (uint32_t)(addr_Mem); // 
  UARTDMA_TXInitStructure.DMA_DIR =  DMA_DIR_MemoryToPeripheral;
  UARTDMA_TXInitStructure.DMA_BufferSize = Length; 
  UARTDMA_TXInitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  UARTDMA_TXInitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  UARTDMA_TXInitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte ;
  UARTDMA_TXInitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte ;
  UARTDMA_TXInitStructure.DMA_Mode = DMA_Mode_Normal   ;
  UARTDMA_TXInitStructure.DMA_Priority = DMA_Priority_VeryHigh;
  UARTDMA_TXInitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  UARTDMA_TXInitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  UARTDMA_TXInitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single ;
  UARTDMA_TXInitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  DMA_Init(UART_TX_DMA_STREAM, &UARTDMA_TXInitStructure);

  NVIC_InitStructure.NVIC_IRQChannel = DMA_USART_TX_IRQ ;                                                //中断分组
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  DMA_ITConfig(UART_TX_DMA_STREAM, DMA_IT_TC, ENABLE);                                           //DMA中断时能
//  DMA_Cmd(UART_TX_DMA_STREAM, ENABLE);                                                                     //DMA开启  
}

发送使能部分:
这一部分和环形队列的数据弹出接上了,要发送数据时将数据从其它程序中红压入队列,然后调用这部分即可(仅供参考,其它项目可直接发送数据)。当所有数据弹出到定义的全局数组auch_TxData[uin_cnt++]时,将传输长度以及地址赋给DMA通道,然后开启即可自动传输。

void Uart1_DMASendEnable()
{
    
    
  static uint16_t uin_cnt=0;
  while(Uart1TxQueue.IsEmpty() != 1) //若缓冲队列非空
  {
    
    
    //从缓冲区弹出字节
     Uart1TxQueue.PopData(&auch_TxData[uin_cnt++]);
  }
  {
    
    
    DMA_Cmd(UART_TX_DMA_STREAM,DISABLE);   //关闭
    UARTDMA_TXInitStructure.DMA_BufferSize = uin_cnt; //数据长度
    UARTDMA_TXInitStructure.DMA_Memory0BaseAddr =(uint32_t)(auch_TxData);//内存地址
    DMA_Init(UART_TX_DMA_STREAM, &UARTDMA_TXInitStructure);//变量写入寄存器
    DMA_Cmd(UART_TX_DMA_STREAM,ENABLE);//使能打开
    uin_cnt =0;  //长度清零
  }
  return ; 
}

传输完成就会进入中断:
中断中仅清除中断标志为下一次中断开启做准备,此部分只能由软件清除完成,不清除下一次中断进不来。

void DMA2_Stream7_IRQHandler()
{
    
    
	if(DMA_GetFlagStatus(DMA2_Stream7,DMA_FLAG_TCIF7)!=0)//必须软件清除中断才能正常下次发送
	{
    
    
		DMA_ClearFlag(DMA2_Stream7,DMA_FLAG_TCIF7);
	}
}

注意事项

  1. DMA数据流及通道要准确
  2. 收发中断数据处理以及标志位清除
  3. 一些特殊的用法要多查寄存器手册,最好边在线调试查看寄存器表现边比对寄存器使用说明
  4. 配置时外设及缓存的地址传参要正确。

总结

这次时DMA收发中断,也可尝试在项目中使用单独的收或者发中断,组合使用,实际测试此种中断DMA接收与串口中断接收丢帧及速率并无太大差别。同时若串口中断太多发送可使用查询发送,直接将数据写入发送数据寄存器效果可很棒。还可以试下网上其它方法组合串口中断。对应代码可参考这里下载,https://download.csdn.net/download/weixin_43058521/85045191

猜你喜欢

转载自blog.csdn.net/weixin_43058521/article/details/123778749