天天看點

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);

}

繼續閱讀