天天看点

stm32f429串口DMA接收不定长数据

写在开头:这段时间在整理modbus协议时,发现没有一个比较方便使用的串口模块,因此结合之前的一些理解,将串口驱动整理出来。此串口驱动有以下特点:

  1. 发送接收均使用DMA
  2. 串口配置不需要从刷固件便能修改,方便二次开发
  3. 数据接收有环形队列缓存,能接收不定长数据帧
  4. 使用读缓存函数能获取当前缓存帧数以及每帧的数据长度.                                                                                                     
    stm32f429串口DMA接收不定长数据

说明:

发送:数据在发送过程中,首先被压入缓存,发送计时器会严格控制每条数据的发送时间间隔,发送使用DMA,以减轻CPU的负荷。

接收:数据接收能实现不定长度接收,首先每帧数据都会通过DMA转移到接收缓存中,在每一帧数据到来时,会产生DMA空闲中断,此时会将数据帧的长度存入帧长度缓存。通过帧长度缓存,能够建立接收缓存中每一帧数据的索引,以及能得出缓存中剩余数据帧的个数。因此,在从缓存取出数据时,操作方便,避免程序主循环运行周期导致数据帧取出时周期不固定(这一点后面详细讨论)。

 二 .软件实现

硬件平台为stm32f429,移植到其他平台只需要将初始化改此对应平台即可。

1.初始化

       串口初始化一般分为以下步骤:1.时钟初始化;2.端口初始化; 3.DMA初始化;4.中断初始化。因此建立一个串口类型结构体SERIAL_INIT_TYPE。在使用串口时,定义相应串口号结构体变量,初始化时赋初值完成串口初始化。多数人习惯使用宏定义,对串口的每个引脚,波特率等使用宏定义,然后串口的打开使用宏开关,实现最大程度的消除重复代码。以定义变量的方式来操作同样是为了消除重复代码,但更为重要的一点是,参数灵活性得到提高。既然是变量,值是受控的,因此设备在运行过程中可以通过软件更改端口参数。在很多产品中,都会附带一个上位机软件,以来改变设备的配置(伺服驱动器等),驱动器本身的固件是不变的,属于“一次开发”,而上位机软件属于二次开发,目的是为了适当调整参数,令设备达到最优运行状态。宏定义虽然能减少代码量,但这种预编译处理产生的软件功能限制较死,每次增添功能需要重新刷固件。因此,二次开发也是本文强调的重点。 

以下是结构体成员:
typedef struct 
{
	    struct   
		{
			/*有关时钟的配置项*/
		}rcc_cfg;
		struct  
		{
			/*有关串口的配置项*/
		}port_cfg;
		struct  
		{
		    /*有关DMA的配置*/				
		}dma_cfg;
		struct  
		{
			/*有关中断的配置*/	
		}nvic_cfg;
}SERIAL_INIT_TYPE;
           

下面分别介绍四个内嵌结构体成员,分别是时钟结构体,串口结构体,dma结构体,中断结构体。

 a.时钟结构体

struct   /*rcc */
{
	uint32_t rxPORT;  /*Port_RCC*/                   
	uint32_t txPORT;
	uint32_t USART;   /*USART_RCC*/ 
	uint32_t rxDMA;   /*DMA_RCC*/
	uint32_t txDMA;	
}rcc_cfg;
           

串口相关的时钟一般为引脚时钟,串口时钟,dma时钟,这个需要查找硬件手册以确定其值。

b.串口结构体

struct   /*port*/
{
	 uint32_t           baud;                 /*波特率*/
	 USART_TypeDef*     USARTx;               /*串口号*/
	 GPIO_TypeDef*      rxPORT;               /*串口接收引脚端口号*/
	 GPIO_TypeDef*      txPORT;               /*串口发送引脚端口号*/
	 uint16_t           rxPIN;                /*串口接收引脚引脚号*/
	 uint16_t           txPIN;                /*串口发送引脚引脚号*/
	 uint8_t            rxAF;	              /*接收引脚复用*/
	 uint8_t            txAF;                 /*发送引脚复用*/
	 uint8_t            rxSOURCE;             /*接收源*/
	 uint8_t            txSOURCE;             /*发送源*/	
}port_cfg;
           

c.dma结构体

struct   /*dma*/
{
	uint32_t                 rxCHANNEL;                        /*接收通道*/
	uint32_t                 txCHANNEL;                        /*发送通道*/
	uint32_t                 txFLAG;                           /*发送完成标志*/
    DMA_Stream_TypeDef*      rxSTREAM ;                        /*接收dma数据流*/
	DMA_Stream_TypeDef*      txSTREAM ;                        /*发送dma数据流*/
    USART_TypeDef*           USARTx;                           /*串口号*/
	uint8_t                  rxbuff[FIFO_SIZE];                /*接收缓存*/
	uint8_t                  txbuff[FIFO_SIZE];                /*发送缓存*/
	uint8_t                  fifo_record[FRAME_SIZE];          /*接收帧长度缓存*/
	uint16_t                 record_point;                     /*帧长度缓存指针*/
	uint16_t                 length;                           /*缓存长度*/
	uint16_t                 tail;                             /*缓存尾指针*/
	uint16_t                 head;			                   /*缓存头指针*/

}dma_cfg; 
           

d.中断结构体

struct   /*nvic*/
{
	 uint8_t   usart_channel;                /*串口中断通道*/
	 uint8_t   usart_Preemption;             /*抢占优先级*/
	 uint8_t   usart_Sub;                    /*从优先级*/
			
	 uint8_t   dma_txchannel;               /*dma中断通道*/
	 uint8_t   dma_txPreemption;            /*抢占优先级*/			
     uint8_t   dma_txSub;	                /*从优先级*/
}nvic_cfg;
           

以下是初始化串口1示例:

SERIAL_INIT_TYPE usart1=
{        .rcc_cfg.USART     			 = RCC_APB2Periph_USART1, /*时钟*/
		 . rcc_cfg.rxPORT    			 = RCC_AHB1Periph_GPIOA,
		 .rcc_cfg.txPORT     			 = RCC_AHB1Periph_GPIOA,
		 .rcc_cfg.rxDMA      			 = RCC_AHB1Periph_DMA2,
		 .rcc_cfg.txDMA      			 = RCC_AHB1Periph_DMA2, 
		 .port_cfg.USARTx    			 = USART1,               /*串口*/
		 .port_cfg.baud	     		     = 115200,
		 .port_cfg.rxPORT    			 = GPIOA,
		 .port_cfg.txPORT     		     = GPIOA,
		 .port_cfg.rxPIN     			 = GPIO_Pin_10,
		 .port_cfg.txPIN    			 = GPIO_Pin_9,
		 .port_cfg.rxAF     			 = GPIO_AF_USART1,
		 .port_cfg.txAF      			 = GPIO_AF_USART1,
		 .port_cfg.rxSOURCE  			 = GPIO_PinSource10,
		 .port_cfg.txSOURCE  			 = GPIO_PinSource9,
		 .dma_cfg.USARTx     		     = USART1,               /*dma*/
		 .dma_cfg.rxCHANNEL  		     = DMA_Channel_4,
		 .dma_cfg.txCHANNEL  		     = DMA_Channel_4,
		 .dma_cfg.txSTREAM   		     = DMA2_Stream7,
		 .dma_cfg.rxSTREAM			     = DMA2_Stream5,
		 .dma_cfg.txFLAG     		     = DMA_FLAG_TCIF4,
		 .dma_cfg.head      			 = 0,
		 .dma_cfg.tail      			 = 0,
		 .dma_cfg.length    			 = FIFO_SIZE,
		 .nvic_cfg.usart_channel    	 = USART1_IRQn,         /*中断*/
		 .nvic_cfg.usart_Preemption      = 0,
		 .nvic_cfg.usart_Sub       	     = 1, 
		 .nvic_cfg.dma_txchannel   	     = DMA2_Stream7_IRQn,
		 .nvic_cfg.dma_txPreemption	     = 0,
		 .nvic_cfg.dma_txSub       	     = 2   
};
           

定义以以上串口描述结构体,之后构建初始化函数,初始化时,只需要将以上变量传入初始化函数,即可完成不同串口的初始化。

初始化函数和串口类型结构体相对应,4个块分别初始化。

void usart_config(SERIAL_INIT_TYPE* usart)
{
	 usart_rcc_cfg(usart); 
	 usart_nvic_cfg(usart);	 
	 usart_dma_cfg(usart);
     usart_port_cfg(usart);	
}
           

具体的初始化过程详细见源码。

2.数据发送

        初始化完成后,dma发送控制已经设置为指定数据地址(txbuff【】)内容到串口,此时只需要要将数据放进发送缓存中,启动dma发送,数据就能发送出去。

        注意:在使用DMA发送时,遇到一个问题,数据只能发送一帧。产生的原因为:dma发送完成后,发送完成标志位被置一,即使在不使能发送中断的情况下,完成标志位也会影响下一帧数据的发送,因此这里使能发送中断,发送完成将标志位清零。

void DMA1_Stream6_IRQHandler(void)   /*dma发送中断*/
{
  if(DMA_GetFlagStatus(DMA1_Stream6,DMA_FLAG_TCIF6)!=RESET)
  { 
		DMA_ClearFlag(DMA1_Stream6,DMA_FLAG_TCIF6);
  }
}

static void start_dma(SERIAL_INIT_TYPE* usart,u16 ndtr)  //启动dma
{  
		/*使能DMA*/
    DMA_Cmd(usart->dma_cfg.txSTREAM, DISABLE);	   
    while(DMA_GetFlagStatus(usart->dma_cfg.txSTREAM,usart->dma_cfg.txFLAG) != DISABLE){}	 
	DMA_SetCurrDataCounter(usart->dma_cfg.txSTREAM,ndtr);        
   /* USART1  向 DMA发出TX请求 */
   /*使能DMA*/
   DMA_Cmd(usart->dma_cfg.txSTREAM, ENABLE);		
}	  
           

       由于这里对发送时序没有严格要求,因此一启动dma数据立即会发送,但对时序有严格要求的系统,比如某些传感器会有最高频率限制要求,这时就需要加入发送计时器,以一定周期来发送,适应外部低速设备。

3.数据接收

       串口接收数据我们知道会经常用到两种中断:字节中断和帧中断。一个是每接收到一个字节的数据便产生一次中断,另一个是接收到一帧数据后产生中断。使用这两种方式配合,便能实现,串口简单的接收处理,只不过接收是实时的。在接收数据处理不及时的情况下,容易出现丢包情况。

环形缓存:

       在通讯设备中经常听到缓存一词,缓存是避免丢包的有效措施。关于环形缓存的概念,这里不提及,百度有详细的介绍。以下是环形队列的实现方法:

 a .创建队列结构体

#define FIFO_MAX_LEN  200
typedef struct
{
      uint16_t head;              //队列头
	  uint16_t tail;                //队列尾
	  uint16_t len;                //当前队列数据长度
	  uint8_t buff[FIFO_MAX_LEN];  //队列数组
}u8FIFO_TYPE;
           

b.队列初始化

void fifo_init(pu8FIFO_TYPE fifo)
{
      fifo->head=0;
	  fifo->tail=0;
	  fifo->len=0;
}   
           

c.队列判断空满

bool is_fifo_empty(pu8FIFO_TYPE fifo)
{
   if(fifo->head==fifo->tail && !fifo->len)   /*头尾相等,长度为0为空*/
	    return 1;
   else
		return 0;
}
/*判满*/
bool is_fifo_full(pu8FIFO_TYPE fifo) 
{
   if(fifo->len>=FIFO_MAX_LEN)          /*长度大于等于最大容量为满*/
		 return 1;
   else
		 return 0;
}
           

d.存数据与取数据

/*数据压入缓存*/
bool push_to_fifo(pu8FIFO_TYPE fifo , uint8_t data)
{
	if( ! is_fifo_full(fifo))
{
    	 fifo->buff[fifo->tail]=data;
		 fifo->len++;
/*存入一个字节,尾指针加一当到达最大长度,将尾指针指向0,
以此达到头尾相连的环形缓存区*/
fifo->tail=(fifo->tail+1) % FIFO_MAX_LEN; 
		  return 1;
		}
	else
		return 0;
}

/*缓存数据弹出*/
bool pop_from_fifo(pu8FIFO_TYPE fifo,uint8_t* data)
{
     *data = fifo->buff[fifo->head];    /*取出一个字节数据*/
	 /*头指针加1,到达最大值清零,构建环形队列*/
     fifo->head=(fifo->head+1) % FIFO_MAX_LEN; 
     if(!fifo->len)
	    {		 
	       fifo->len--;
		   return 1;
	    }
     else
		   return 0;	 
}
           

        以上为环形队列的实现,我们可以将压入数据进缓存发在串口中断接收中,每接收到一个数据,便存入缓存,在应用函数中调用缓存数据弹出函数,一个字节一个直接的取出数据。

       在stm32 DMA中,接收DMA能够自动构建环形缓存区,类似上面fifo->tail=(fifo->tail+1) % FIFO_MAX_LEN; 实现方式。通过配置DMA初始化结构体成员:DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  初始化完成后,用户只需要DMA_GetCurrDataCounter函数,便能获取当前缓存剩余容量,也就是队尾tail可以用以下公式获得:tail = MAX_LEN - DMA_GetCurrDataCounter();以上,实现了串口DMA接收数据,

由缓存区取单字节数据。

     可以看出,每次只能从缓存取出一个字节的数据,也就是说每循环一次取一个数据。因此,取数据是和主循环周期相关的。如果我们能知道缓存中数据帧的长度即数量,那么我们就能一次取出一帧的数据。能实现这种,不定长数据接收自然也解决了。下面实现不定长度的接收。

4.不定长度数据接收

stm32f429串口DMA接收不定长数据

程序实现:

a.串口空闲中断函数

void USART1_IRQHandler(void)  
{
	u16 data;	
	static uint16_t count=0;
	static uint16_t last_tail=0;
    if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET)  /*空闲中断*/
	{
	   data = USART1->SR; 
	   data = USART1->DR;            /*清除中断标志*/
       /*计算缓存尾指针*/
       usart1.dma_cfg.tail=MAX_LEN - DMA_GetCurrDataCounter(usart1.dma_cfg.rREAM);
       /*尾指针由经过环形交接处*/
      if(usart1.dma_cfg.tail< last_tail)		
        usart1.dma_cfg.fifo_record[count] =FRAME_SIZE -last_tail  +  usart1.dma_cfg.tail;
	  else
		 usart1.dma_cfg.fifo_record[count]= usart1.dma_cfg.tail-last_tail;
       /*将计算的帧长度存入缓存*/
	   last_tail = usart1.dma_cfg.tail;
       /*构造帧长度环形缓存*/
	   count=(count+1) % FRAME_SIZE;
	}
}
           

b.从缓存中取数据

从缓存中取出一个字节:
bool get_byte(SERIAL_INIT_TYPE* usart,uint8_t* data)
{
	 if(usart->dma_cfg.tail==usart->dma_cfg.head)  //空或满
	     return 0;
	 else										  //取数据
	 {
	     *data = usart->dma_cfg.rxbuff[usart->dma_cfg.head];
		 usart->dma_cfg.head = (usart->dma_cfg.head+1)%500;
		 return 1;
	 }
}
从缓存中取出一帧数据:
bool get_str(SERIAL_INIT_TYPE* usart , uint8_t* frame, uint8_t* len)
{
	 uint8_t temp;
	 temp=usart->dma_cfg.fifo_record[usart->dma_cfg.record_point]; 
	if(temp)
	{
		   *len = temp;
		   usart->dma_cfg.fifo_record[usart->dma_cfg.record_point]=0;
			 while(temp--)
			 {
				 if(get_byte(usart,frame++)){}
				 else
				 {
	                 usart->dma_cfg.record_point= 
                         (usart->dma_cfg.record_point+1)%usart->dma_cfg.length;
					 return 0;
				 }
			 } 
             usart->dma_cfg.record_point= 
                    (usart->dma_cfg.record_point+1)%FRAME_SIZE;
	        return 1;
	 }
	else
		return 0;
	
}
           

      取出字节函数为内部函数,面向上层的为取出一帧数据bool get_str(SERIAL_INIT_TYPE* usart , uint8_t* frame, uint8_t* len)当取出数据有效时,返回1.输入串口号。frame为取出数据帧后,暂时存储区的首地址。len指向存储数据帧长度的地址。即调用函数,数据帧以及长度会传到frame和len。

三 .接口实现

我们已经实现了串口的读写,初始化函数,为了能被上层更方便的调用。应该将接口统一化。定义以下机构体:

typedef struct

{

        void(*init)(SERIAL_INIT_TYPE* usart);

        void(*write)(SERIAL_INIT_TYPE* usart,uint8_t* data, uint16_t len);

        bool(*read)(SERIAL_INIT_TYPE* usart,uint8_t* data,uint8_t* len);

}SERIAL_OPS_TYPE;

其中函数指针分别指向串口1的发送接收函数,初始化函数。当上层需要使用串口时。

按以下步骤实现(拿modbus_slave来举例):

SERIAL_OPS_TYPE  md=

{

        .init=usart_config,

        .write=usart_send,

        .read=get_str

};

void modbus_init(void)

{

   md.init(&usart2);

}

void modbus_write(uint8_t* frame,uint16_t len)

{

   md.write(&usart2,frame,len);

}

bool modbus_read(uint8_t* data,uint8_t* len)

{

   return md.read(&usart2,data,len);

}

继续阅读