前言:
串口通訊對于所有的嵌入式工程師十分常見,對于一個與外界交互的系統必須依賴一些手段,比如串口、USB、紅外、GPRS之類的數據通訊傳輸方式。而串口作為一種廉價的短距離可靠的通訊方式得到了廣泛應用。
廢話少說了,就此打住,進入正題。
本文主要從軟件結構上講解如何在資源比較缺乏的系統上實現通訊協議的串口通訊編程,以及如何優化程序效率,從而使系統更快、更穩定運行。
正文:
我們以51單片機為例。51中一般針對串口通訊編程,通常采取中斷接受查詢發送的方式。中斷函數在接受數據到達時被重復調用,其實是個重復入棧的過程,所以不宜將函數寫的太長,函數太長一般會導致棧太深占用系統資源,二是處理時間過長,可能導致通訊出錯。為了防止在處理數據過程中不受干擾,通常在處理接受數據前關閉中斷,處理完后再開。
通常的的編程方式如下:
staticvoidUartInterruptService(void)interrupt4
{
ES=0;
RI=0;
uart_process(SBUF);
ES=1;
}
下面重點介紹數據處理函數uart_process(SBUF);
其實很多時候,對于通訊傳輸的數據處理才是關鍵,尤其對于設計通訊協議而言。筆者在剛剛做的一個系統上就碰到這樣的問題,當系統龐大了,資源十分有限的情況下,數據處理一旦占用資源太多,效率太低將導致系統崩潰而無法運行。
到了這里,很多工程師可能會考慮開個大的緩沖區FIFO將接收到的數據保存在緩沖區,然后對其進行解析、判斷進行下一步程序編寫,當然這在系統資源比較豐富的情況下是沒有問題的,ARM上采取的就是這樣的方式。但如何系統龐大呢,留給的資源缺乏則不行。這樣做的一個很大缺點必須是將數據幀接收完了才能夠判斷,降低了效率和運行速度。
其實還有另外的方式,可以采取在每接收一個字節就對其解析,解析完判斷轉到下一個狀態,并將其中的有用數據存儲在相應的數據結構中去,可以采取狀態機實現。
將狀態機設計為兩個控制狀態,一是串口狀態——uart_state,一是命令類型狀態——cmd_state.
(1)狀態機開始狀態:串口狀態為CMD_NO
(2)接受到STX_CMD,狀態變為CMD_START.
(3)接下來將自動進入接受命令幀的狀態,再開啟命令狀態的狀態機,對發送來的有用數據進行解析,保存,校驗等。處理完畢后將uart_state設為CMD_END狀態進行下一步的接受完畢判斷,將cmd_state設置為初始的NO_CMD狀態。
(4)最后進行ETX_CMD判斷,判斷數據接收是否完畢。
voiduart_process(U8u8)
{
if(uart_state==CMD_NO)
{
if(u8==STX_CMD)
{
uart_state=CMD_START;
}
}
elseif(uart_state==CMD_START)
{
switch(cmd_state)
{
caseNO_CMD:
cmd_state=u8;
break;
caseCOST_CMD:
//解析存儲有用數據到相應數據結構中
//進行CRC校驗
……
uart_state=CMD_END;
cmd_state=NO_CMD;
CRC=0;
break;
……
}
……
}
elseif(uart_state==CMD_END)
{
uart_state=CMD_NO;
if(u8==ETX_CMD)
{
//接受完畢
//可以考慮拋出一個消息main函數循環中進行響應處理。
}
}
}
接下來我們要討論解析后我們數據存儲的問題,其實在資源比較足夠的情況下或者能夠擠出data區的情況下可以考慮用結構體,我們構造好相應結構體,將接收到的數據存儲進去,要應用的時候就十分方便。但這也有個矛盾,一般c51定義的結構體都被存儲在data區,一般通訊的字節量大空間必然不夠,存在一個矛盾,可以采用聯合體union進行存儲效果會好一點。當然也可以在保存數據時采用定義在xdata區(片外)的buffer來存儲。這樣在一定程序上優化了程序的執行效率,在程序處理立即拋出消息處理,提高了通訊數據的處理速度。對于通常資源比較豐富的系統,比如ARM上一般采取的做法是這樣的,將數據存在緩沖區,接收完一幀數據后再轉換成相應的數據結構,再進行分析、校驗。
總體來說,這種采取狀態機實時解析串口通訊數據的方式在一定程序提高了程序運行效率,使軟件架構清晰明了,程序可擴展性大,有利于后續開發。以上是筆者的一點愚見,歡迎指教。
網友評論:學習啦.
網友評論:在我發出STX_CMD命令時,uart-state狀態機將進入CMD_START狀態,等待下一個命令的輸入,而此時命令狀態機cmd_state是NO_CMD狀態,好了,現在外面發過來一個COST_CMD的命令,由于此時cmd_state正處在NO_CMD的狀態,將執行caseNO_CMD的語句,也就是將cmd_state的狀態進入COST_CMD,然后退出switch語句,這樣就相當于沒有去執行caseCOST_CMD狀態的指令,這個命令就無效了,不知道我說清楚沒有
網友評論:這種只能適合一些情況,不能適合所有~~
內存小就換個大點的嘛,現在的單片機差型號那么多,肯定能找到合適的.
網友評論:你沒有想清楚,我可以將cmd_state默認設置為NO_CMD,來個COST_CMD也就是cmd_state=u8,此時cmd_state=COST_CMD,接下來對命令幀有用數據進行解析.仔細分析下就很清楚的.
30樓說的很對,不是適合所有的情況,但大多數情況下還是合適的,如果通信協議加上上層的GUI設計的,通訊數據傳輸速度將影響整個設備的響應速度.因為不僅僅要傳輸數據還要解析,刷屏幕等,速度稍微快點響應上將好多了.
網友評論:誰需要直接可以工作的C51程序,可以來交流一下,QQ28591262
網友評論:我在使用modbusRTU模式時候遇到超時的問題,也用的是狀態機站好功能碼啥的都接受好了到了接受數據那一步了然后假如突然對方不發了怎么辦啊,不就一直停在那個狀態了啊,另一個消息來了就沒法收了呵呵,雖然一般不會出現但是假如出現了喃然后就要用到modbus里面的間隔判斷了然后那個間隔時間太短就1.5個字符,這要怎么搞啊,用定時器是不是可以?有沒其他的方法喃!因為設備內部要跟DSP通信,還要跟外部的設備通信所以就要用到兩個串口,如果兩個都用定時器的話,那怎么搞一會定時器還給他們兩用完了.
網友評論:可以仿下TCP/IP的超時處理機制.
協議每執行到一個狀態,進行一次檢查.
網友評論:開8個BYTE的緩沖足夠了~中斷函數只負責接收,在主程序循環中處理.
staticvoidUartInterruptService(void)interrupt4
{
ES=0;
RI=0;
cSerialReciveBuffer[cSerialWriteBufferPos]=cData;
if(cSerialWriteBufferPos>=UART0_RX_BUFFER_SIZE)
{
cSerialWriteBufferPos=0;
}
cSerialReciveNum++;
ES=1;
}
//主函數循環處理
while(cSerialReciveNum)
{
}
網友評論:你這種方式極其不好,程序效率低下,不能夠老在while(1)循環處理一些東西,降低了程序消息,采取消息機制會好多了,關于你所說的緩沖處理我不做評價.
網友評論:這個也是收到數據馬上處理,如果收到多個就處理多個,設置緩沖就是為了提高效率啊~~數據不在中斷中處理是好事~
網友評論:while(1)一直被循環調用,一般處理是放在外面再通過消息機制發消息到while(1)里判斷處理,如果連串口數據處理也放在while(1)里程序將寫的很長,我是比較反對這種將main弄的很長的做法:)
網友評論:可以編寫一個串口處理的函數,把串口數據處理放在一個函數里面,這樣你就會覺得舒服多了~呵呵~~
我一直認為提高效率的方法是中斷函數盡量精簡~~
網友評論:大家寫串口的時候都是怎么搞的,開緩沖的多還是不開的多,還是剛開始不開,以后成為高手后就開了.
網友評論:是不開緩沖的少,我只是探討下一個特殊情況下的處理以及如何優化程序.我不是什么高手.
網友評論:因為能夠多仔細考慮一些東西,在平臺相互移植時候就會避免一些問題:)
網友評論:你的狀態機是很高明的
網友評論:中間數據解析的時候就用了結構體做緩沖存儲數據.我見到有個網友在我的基礎上改了下狀態機用于他的項目的一個文檔,他針對他的應用自己設置了下狀態的切換我覺得挺好的.我這個只是提供一個架構,具體應用大家仔細修改下就可以了.另外我絕對對于資源比較缺乏的ARM芯片比如LPC的就可以采用我的方法.
網友評論:說白了就是把數據處理放在中斷里運行,這樣不利于程序移植和維護,如果不是迫不得已不要這樣干。
網友評論:受教了
其實只要我們在處理手里的案子,多注意下,像樓主說說的狀態機之類的處理手
法已經應用很普遍了,在臺灣過來的程式中特普遍
關鍵融會貫通,自己也能熟練的用這種處理方式,這個是難點
網友評論:其實我對TCP/IP理解在串口通訊之前,因為出現了一些問題才去思考一些解決的方法。過陣子貼個TCP/IP協議中實現上層的HTTP協議中的狀態機,里面對于狀態的校驗和重發就做的很好。還是一句老話:“由儉入奢易,由奢入儉難”,很多一些從ARM上跑的好的程序到其他平臺出問題都可能出這樣那樣的問題,這樣一個好的架構是很有必要的。當然不是說我這個東西有什么高明的地方,只是平時寫code的時候多想想一些問題可能會有意想不到的收獲!
網友評論:這樣寫的話得保證發來的第二、第三、、、、等等數不別的器件當成地址或別的啊
網友評論:我在這個壇子另外個帖子里面提到避免這點,比如我將STX和ETX設為HEX,中間的數據幀為ASCII就不避免了……
網友評論:跟著大家學習一下^_^
網友評論:幫頂
網友評論:頂...以后有這樣的文章多發一下..呵呵
網友評論:狀態機的方式我也常用,應該是比較好的方法,但是我反對把解析等操作放在中斷種進行。36樓中斷中只接受數據放到FIFO中也許是不錯的方法,樓下說while(1)效率低,可能他沒有考慮到,確實不能這樣傻等,效率太低,可以改進一下,采用輪詢,每隔1mS或其他時間查詢一下,同時可以
計算超時。不過我建議將解析幀頭放到接收中斷中,用一個緩沖區來放數據,中斷中幀頭同步之后設定標志并依次接收存儲每一個數據,中途有奇偶校驗錯誤或者緩沖區益處則等待超時,主程序輪詢并設定超時,超時后關中斷,開始解析數據。把命令結構體直接指向緩沖區即可取得所需的數據。
現在的這種常用的通信協議都是幀頭同步,幀尾超時來處理,所以我常用上面的方法。不過需要注意避免主程序和中斷競爭數據。
網友評論:采用多機器通訊做同步最好了,無論小系統,還是大系統都統統可以~
采用緩沖方式效率高啊,放在主函數中檢測,經過實際應用,效果非常好,不是傻等啊.
網友評論:不懂將所有的活都放到串口中斷中去完成,會不會影響通訊質量和占用系統資源阿請教
網友評論:SIGNAL(SIG_USART_RECV)
{
unsignedcharUARTemp;
UARTemp=UDR0;
UARTTimer=0x2;
if(!(UARTStatu&_BV(HeadIn)))
{
if(UARTemp!=HeadChar)
return;
else
{
UARTStatu|=_BV(HeadIn);
return;
}
}
if(!(UARTStatu&_BV(LengtIn)))
{
DataLong=UARTemp;
if(DataLong>MAXLength)
{
UARTStatu=0;
return;
}
UARTbuffer[0]=UARTemp;
ByteOfRead=0;
UARTStatu|=_BV(LengtIn);
return;
}
if(ByteOfRead<DataLong)
{
UARTbuffer[ByteOfRead+1]=UARTemp;
if(ByteOfRead<MAXLength)
ByteOfRead++;
if(ByteOfRead==DataLong)
{
if(UARTemp==Crc8fun(UARTbuffer,DataLong))
UARTStatu|=_BV(PackageOk);
else
UARTStatu=0;
}
}
}
網友評論:在大量數據交換通訊應用中,使用狀態機確實會拉低系統實時性,使用超時解析對于大量數據突發訪問會帶來溢出風險,視不同應用采取不同處理方法,實時性要求很強的通訊我一般在協議上下功夫,定義一個結束符,一旦判斷到結束符在中斷中解析完并發送消息到隊列中,保證不丟失消息,但需要評估解析需要消耗的CPU運行時間必須小于一個字節的接收時間。
網友評論:不錯,讀這樣的文章如飲甘露。
網友評論:“為了防止在處理數據過程中不受干擾,通常在處理接受數據前關閉中斷,處理完后再開。
static void UartInterruptService(void) interrupt 4
{
ES = 0;
RI = 0;
uart_process(SBUF);
ES=1;
}”
--------51的串口中斷函數里就不用再關閉串口中斷允許了吧,因為自己是不能搶占自己的。
“其實很多時候,對于通訊傳輸的數據處理才是關鍵,尤其對于設計通訊協議而言。筆者在剛剛做的一個系統上就碰到這樣的問題,當系統龐大了,資源十分有限的情況下,數據處理一旦占用資源太多,效率太低將導致系統崩潰而無法運行!
---------這很有可能是沒有充足的堆?臻g造成的。一般至少得留下十幾個字節的RAM做堆棧。
網友評論:不錯,值得一看
網友評論:做個記號,以后用的時候方便!
網友評論:來一下!
網友評論:陳明計和周航慈有詳細的介紹,陳明計的在《嵌入式實時操作系統SMALL RTOS51原理及應用》,周航慈在那本介紹ucos的書里面
網友評論:呵呵,我做串口都怎么做的!
網友評論:if(RI)
{
RI=0;
//P22=0;
Incept_data=SBUF;
if(Incept_Status==0x00) //中斷處于接收起始幀階段
{
if(Incept_data==Frame_Head)
{ Incept_Status=0x01;//更新接收狀態標志為接收命令碼
//P23=0;
}
else
Incept_Status=0x00;
}
if(Incept_Status==0x01)
{
if(Incept_data==Frame_SetSimulate_Orde) //如果是設置幀命令,則立設置標志
{ //P34=0;
command_marker=Frame_SetSimulate_Orde;
Incept_Status=0x02;//更新接收狀態標志為接收幀長碼
}
else if(Incept_data==Frame_SetDigital_Orde)
{ command_marker=Frame_SetDigital_Orde;
Incept_Status=0x02; // P11=0;
}
else if(Incept_data==Frame_Gather_Orde) //實時數據采集幀命令,則立設置標志
{
command_marker=Frame_Gather_Orde;
Incept_Status=0x02;
}
syscheckData=0; //準備進行數據和校驗
return;
}
if(Incept_Status==0x02) //接收幀長
{// P35=0;
FrameLen=Incept_data;
syscheckData+=FrameLen; //更新累加和校驗信息
Incept_Status=0x03; //更新接收狀態標志為接收數據碼
return;
}
if(Incept_Status==0x03)
{
Incept_DataStream[RecCount]=Incept_data;
syscheckData+=Incept_DataStream[RecCount]; //更新累加和校驗信息
RecCount++;
if(command_marker==Frame_SetSimulate_Orde)
{
if(RecCount==FrameLen)
{ // P36=0;
RecCount=0;
Incept_Status=0x04; //更新接收狀態為接收校驗碼信息
}
}
if(command_marker==Frame_SetDigital_Orde)
{
if(RecCount==FrameLen)
{
RecCount=0;
Incept_Status=0x04; //更新接收狀態為接收校驗碼信息
}
}
if(command_marker==Frame_Gather_Orde)
{
if(RecCount==FrameLen)
{
RecCount=0;
Incept_Status=0x04 ; ////更新接收狀態為接收校驗碼信息
}
}
return;
}
if(Incept_Status==0x04)
{
checkData=Incept_data;
if(syscheckData!=checkData) //如校驗出錯
Incept_Status=0x00;
else
{Incept_Status=0x0537=0;} //P15=0;
syscheckData=0;
return;
}
if(Incept_Status==0x05)
{
if(Incept_data!=Frame_End)//錯誤幀標志
b_valid_Frame=0;
else
{ b_valid_Frame=1;} //P16=0;
Incept_Status=0x00;
}
}
}
網友評論:編程序每個人的思路都不同,感覺收發都在中斷里做會更好,這個程序進來就關中斷不是太好吧?
網友評論:挖墳?
網友評論:正遇到這個問題,不過我現在也是樓主這么做的,
網友評論:頂一個,學習一下!
網友評論:
我總感覺樓主的程序是垃圾程序的典范!
嗨!
看出來了,俺水平太差,看了半天我也沒有理解樓主是什么意思!
哪位給我指點指點!
其實很多時候,對于通訊傳輸的數據處理才是關鍵,尤其對于設計通訊協議而言。筆者在剛剛做的一個系統上就碰到這樣的問題,當系統龐大了,資源十分有限的情況下,數據處理一旦占用資源太多,效率太低將導致系統崩潰而無法運行。
到了這里,很多工程師可能會考慮開個大的緩沖區FIFO將接收到的數據保存在緩沖區,然后對其進行解析、判斷進行下一步程序編寫,當然這在系統資源比較豐富的情況下是沒有問題的,ARM上采取的就是這樣的方式。但如何系統龐大呢,留給的資源缺乏則不行。這樣做的一個很大缺點必須是將數據幀接收完了才能夠判斷,降低了效率和運行速度。
其實還有另外的方式,可以采取在每接收一個字節就對其解析,解析完判斷轉到下一個狀態,并將其中的有用數據存儲在相應的數據結構中去,可以采取狀態機實現。
這段意思是不是這樣:樓主以為先判斷后接收效率高、省資源。 先接收后判斷就“降低了效率和運行速度”。
我就沒有看出到底哪里省資源了?
效率怎么就高了?
誰能指點一下?多謝!
網友評論:直接把處理函數寫在中斷處理函數中不利于代碼移植重用和維護,在大程序中這種架構不應提倡
網友評論:NO,NO
要用FIFO
要用CRC
網友評論:小頂一下
網友評論:頂頂
網友評論:不錯,挺好的
網友評論:static void UartInterruptService(void) interrupt 4
{
ES = 0;
RI = 0;
uart_process(SBUF);
ES=1;
}
LZ的開篇確實值得質疑,上面66樓網友講的正根:“ES不可能自己搶占自己!贝谥袛
硬件內部已經設計好了這種關系:(Intel硬件設計師是干什么的?如果我是硬件設計師,我怎么會把這種關系留給軟件設計員?)——首次ES中斷發生并進入ISR之后,必然阻斷后面的同級中斷(如若有的話)并使其進入中斷隊列排序等待,只有當前ISR完成,RETI 退出串口中斷程序之后,此前紀錄的、位于隊列中的ES=1才會再次觸發串口中斷。所以,LZ的開篇程序是不是一開始就沒寫好?后面.....
static void UartInterruptService(void) interrupt 4
{
//ES = 0;
RI = 0;
uart_process(SBUF);
// ES=1;
}
其實我最近一直不好處理的問題是:你在一幀數據接收過程中,如果半中間突然應該來的數據中止不來了,怎么辦?你的while(RI !=1) ; RI=0;該如何處理才不至于死等?才可能盡快退出當前狀態?——你如何盡快感知到通信失效?我知道是利用T0超時,可是怎么寫才簡單?接收100字節一幀,就要設置100次T0初值?太呆板了吧。(當然可以用宏指令)如若每接收到第50字節時就發生中止或錯誤接收現象,只好一幀重新來過,以前的都白干!
如何才能提高通信系統可靠性?
網友評論:mark下串口通信,最近正在搞這個。