寫在開頭:這段時間在整理modbus協定時,發現沒有一個比較友善使用的序列槽子產品,是以結合之前的一些了解,将序列槽驅動整理出來。此序列槽驅動有以下特點:
- 發送接收均使用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.不定長度資料接收
程式實作:
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);
}