新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設計應用 > 搞定單片機多字節(jié)串口接收

搞定單片機多字節(jié)串口接收

作者: 時間:2016-11-19 來源:網(wǎng)絡 收藏
    工作了一年多,寫了不少單片機串口程序。感覺串口多字節(jié)接收部分的邏輯相對于配置寄存器跟串口來說,是有點難度的——寄存器配置基本上都是死的,串口多字節(jié)跟一字節(jié)只是多了一個循環(huán)。

串口接收程序是基于串口中斷的,單片機的串口每次接收到一字節(jié)數(shù)據(jù)產(chǎn)生一次中斷,然后再讀取某個寄存器就可以得到串口接收的數(shù)據(jù)了。然而在實際應用當中,基本上不會有單字節(jié)接收的情況。一般都是基于一定串口通信協(xié)議的多字節(jié)通信。在422或者485通信中,還可能是一個主機(一般是計算機)帶多個從機(相應的有單片機的板卡)。這就要求我們的單片機能夠在連續(xù)接收到的串口數(shù)據(jù)序列中識別出符合自己板卡對應的通信協(xié)議,來進行控制操作,不符合則不進行任何操作。簡而言之就是,單片機要在一串數(shù)據(jù)中找到符合一定規(guī)律的幾個字節(jié)的數(shù)據(jù)。

本文引用地址:http://butianyuan.cn/article/201611/318290.htm

先來說下怎樣定串口協(xié)議吧。這個協(xié)議指的不是串口底層的協(xié)議,而是前面提到的數(shù)據(jù)幀協(xié)議。一般都是有幀頭(2~3個字節(jié)吧),數(shù)據(jù)(長度根據(jù)需要),結束位(1位,有時候設計成校驗字節(jié),最簡單的校驗也就是前面所有數(shù)據(jù)求和)。

比如0xaa 0x55 +(數(shù)據(jù)部分省略)+校驗和(除了aa 55 之外數(shù)據(jù)的和),如果要是多板卡的話有時候還要在幀頭后面加一個板選字節(jié)(相當于3字節(jié)幀頭了)。

第一次寫串口接收程序的時候,我首先想到的就是定義一個全局變量(實際上最好是定義局部靜態(tài)變量),初始值設置為0,然后每進一次中斷+1,然后加到串口通信協(xié)議的長度的時候再清零。然后判斷幀頭、校驗。寫完了之后我自己都覺得不對,一旦數(shù)據(jù)錯開了一位,后面就永遠都接收不到數(shù)了。無奈看了一下前輩們的代碼,跟我的思路差不多,只不過那個計數(shù)值跟接收到的數(shù)據(jù)時同時判斷的,而且每次中斷都要判斷,一旦不對計數(shù)的那個變量就清零。

廢話少說,直接上一段代碼讓大家看看就明白了。(通信協(xié)議姑且按照簡單的aa 55 一個字節(jié)數(shù)據(jù) 一個字節(jié)校驗,代碼是基于51單片機的)。接收成功則在中斷程序中把串口接收成功標志位置1。

下面是全局變量定義

1 unsigned char receive[4]={0,0,0,0};//接收緩存2 3 bit uart_flag;//串口接收成功標志

然后串口中斷部分

void ser()interrupt 4{  static unsigned char count;//串口接收計數(shù)的變量RI=0;//手動清某個寄存器,大家都懂的receive[count]=SBUF;if(count==0&&receive[count]==0xaa)//同時判斷count跟收到的數(shù)據(jù){count=1;}else if(count==1&&receive[count]==0x55){count=2;}else if(count==2){count++;}else if(count==3&&receive[count]== receive [2])//判斷校驗和,數(shù)據(jù)多的話是求//和,或者其他的校驗方法,也可能是固定的幀尾{count=0;uart_flag =1;//串口接收成功標志,為1時在主程序中,然后清零ES=0;      //關中斷,完了再ES=1;}else{count=0;//判斷不滿足條件就將計數(shù)值清零}}

第一次做的串口大概就按照這個方法寫完了(我后來看過其他的代碼,有人用switch語句寫的,邏輯跟這個也差不多,不過我還是感覺用if else來寫清晰一些),

不過在測試的時候發(fā)現(xiàn)了bug,如果數(shù)據(jù)幀發(fā)送一半,然后突然停止,再來重新發(fā),就會丟失一幀的數(shù)據(jù)。比如先接受到aa 55,然后斷了,再進來aa 55 01 01,就不受控制了。后來我也想到一個bug,如果在多設備通信中,屬于其他設備的的幀數(shù)據(jù)最后一位是aa(或者最后兩位為aa 55 ,或者最后3位為aa 55 板選),下一次通信的數(shù)據(jù)就接收不到了。

當時對于數(shù)據(jù)突然中斷的bug,沒有想到很好的解決辦法,不過這種情況幾率極小,所以一直用這個方法寫也沒有問題。多設備通信最后一位恰好是aa的幾率也很小,出問題的可能也很小。當時項目里面的控制數(shù)據(jù)跟校驗恰好不可能出現(xiàn)aa,于是我把if(count==0&&receive[count]==0xaa)改成了if(receive[count]==0xaa)其他都沒變,解決了,沒有bug了。

后來我又寫了幾次單片機程序,才想到了一些解決問題的方法——不過改天再接著寫吧,太累了,明天還要上班呢。

在后來的項目中,真的遇到了數(shù)據(jù)位跟校驗位都可能出現(xiàn)aa的情況。我考慮到每次數(shù)據(jù)都是連續(xù)發(fā)送的(至少我們用labwindows做的上位機程序是這樣的),成功接收到了一幀數(shù)據(jù)是要有一定時間的,也就是說如果接收到一半,但是很長時間沒接收到數(shù)據(jù),把計數(shù)值count清零就ok啦。涉及時間的問題自然要用定時器來實現(xiàn)啦。

這次的通信協(xié)議如下,串口波特率19200,2個幀頭aa 55 ,一個板選,6字節(jié)數(shù)據(jù),一個校驗字節(jié)(除幀頭外其他數(shù)據(jù)的和)。

全局變量定義

unsigned char boardAddr;//板選地址,通過檢測幾個io引腳,具體怎么得到的就不寫了,很簡單的unsigned char g_DatRev [10]={0};//接收緩存bit retFlag=0;//為1代表串口接收到了一幀數(shù)據(jù)

串口初始化函數(shù),晶振22.1184

void init_uart(){SCON = 0x50;                 //串口方式1允許接收TMOD = 0x21;                //定時器1,方式2,8位自動重載,同時配置定時器0,工作方式1PCON = 0x80;                // 波特率加倍TH1 = 0xfa;TL1 = 0xfa;               //寫入串口定時器初值TH0=(65536-2)/256;    //寫入定時器0初值,串口傳輸一個字節(jié)時間為(1/19200)*10,計算得0.52msTL0=(65536-2)%256;   //定時器0定時大約1ms多EA=1;ET0=1;                  //波特率:19200    22.1184M  初值:250(0xfa)IE = 0x90;           TR1 = 1;                   }

串口中斷函數(shù)

void UART_INT(void) interrupt 4{ static unsigned char count;//串口接收計數(shù)的變量RI = 0;g_DatRev[count] = SBUF;if(g_DatRev[count]==0xaa&&count==0)             //幀頭{count=1;                                                 }else if(count==1&&g_DatRev[count]==0x55) {  count=2;          }else if (count==2&&g_DatRev[2] == boardAddr){ CK = g_DatRev[count];count=3;        }else if(count>=3&&count<9){                   CK += g_DatRev[count];count ++;}else if(count == 9&&CK==g_DatRev[9]){     ES = 0; retFlag = 1;count=0;            }            else{count=0;} resettimer();}

//判斷count不為0的話就啟動定時器void resettimer(){TR0=0;TH0=(65536-2)/256;TL0=(65536-2)%256;if(count!=0){TR0=1;}}定時器中斷函數(shù)void T0_time()interrupt 1{     TR0=0;TH0=(65536-2)/256;TL0=(65536-2)%256;count=0;}

這種方法的確是本人自己想出來的,別人可能也這樣做過,但我這個絕對不是抄襲或者模仿來的。這樣寫的確可以避免前面提到過的bug,不過代價是多用了一個定時器的資源,而且中斷函數(shù)里的內(nèi)容更多了,占用了更多的時間。

要是能把第一種方法改進一下就好了,主要是那個校驗不能為aa的那個bug,因為畢竟傳輸?shù)揭话胪蝗粩嗔说目赡苄允欠浅P〉?。后來我想第一個判斷if(count==0&&receive[count]==0xaa)好像有點太嚴格了,考慮到第二字節(jié)的幀頭,跟板選地址不可能為aa,于是把這個改寫為if(count>=0&&count<=2&& receive[count]==0xaa),這樣就把bug出現(xiàn)的幾率降到了非常小,也只是在前一幀結尾數(shù)據(jù)恰好為 aa 55 板選 的時候才出現(xiàn),幾率是多少大家自己算一下吧,。這樣我自己覺得,昨天寫的那種方法改進到這個程度,應該算可以啦,反正我是很滿意了。

實際上我還想過其他的方法,比如緩存的數(shù)組采用移位寄存的方式。拿前面的4個字節(jié)的協(xié)議為例。

void ser()interrupt 4

{

unsigned char i;

RI=0;

for(i=0;i<3;i++)

{

receive[i]=receive[i+1];

}

receive[3]=SBUF;

if(reveive[0]==0xaa&&receive[1]==0x55&&receive[2]==receive[3])

{

ret_flag=1;

ES = 0;

}

}

這段代碼看上去可是簡單明了,這樣判斷可是不錯啊,同時判斷幀頭跟校驗不會產(chǎn)生前面提到的bug。說實話當時我剛想出這種方法并寫出來的時候,馬上就被我給否了。那個for循環(huán)可真是很占時間的啊,延時函數(shù)都是這樣寫的。每次都循環(huán)一下,這延時太長,通信速度太快的話就不能接收到下一字節(jié)數(shù)據(jù)了。最要命的是這個時間的長度是隨著通信協(xié)議幀的字節(jié)數(shù)增加而增加的,如果一次要接收幾十個字節(jié),肯定就玩完了。這種方法我一次都沒用過。

不過我居然又想出來了這種方法的改良措施,是前兩天剛想出來的,,還沒有實踐過呢。

下面代碼的協(xié)議就按第二段程序(定時器清零的那個協(xié)議,一共10字節(jié))

全局變量

bit ret_flag;

unsigned char receive[256]={0};

unsigned char boardaddress;

中斷函數(shù)

void ser()interrupt 4

{

static unsigned char i=0;

static unsigned char total=0;

RI=0;

receive[i]=SBUF;

total=total-receive[i-7]+receive[i-1];

if(receive[i-9]==0xaa&&receive[i-8]==0x55&& receive[i-7]==boardaddress&&receive[i]==total)

{

ret_flag=1;

ES = 0;

}

i++;

}

之所以要定義256個長度的數(shù)組,就是為了能夠讓數(shù)組“首尾相接”。因為0 -1 = 255 , 255+1 = 0。而且我在計算校驗的時候也改進了算法,不會因為數(shù)據(jù)長度的增加而增加計算校驗值的時間。這種方法也是我不久前才想出來的,所以還沒有經(jīng)過實際的驗證。上面的代碼可能會有邏輯上的錯誤,如果真有錯誤,有網(wǎng)友看出來的話,請在下面留言告訴我。這個方法也是我原創(chuàng)的哦,別人也肯能會想到,不過我這個絕對不是抄襲別人的。

上面的代碼最大的缺點就是變量定義的太多了,太占ram資源了,編譯的時候可能會出現(xiàn)錯誤,畢竟51單片機才128字節(jié)的ram(有的資源也很豐富的,比如c8051系列的),這一下子就是256字節(jié)的變量。不過對于資源多一些的單片機,這樣寫還是可以的。要是能有4bit在一起的數(shù)據(jù)類型就好了,,verilog代碼里面是可以的,C語言里貌似不行啊。

要想能在例如51單片機上運行,只能按照下面的折中方式了,也就是把i相關的量都與一個0x0f

全局變量

bit ret_flag;

unsigned char receive[16]={0};// 可以考慮在定義時加上idata,畢竟還可能是32

//或者64長度的數(shù)組呢unsigned char idata receive[16]={0};

unsigned char boardaddress;

中斷函數(shù)

void ser()interrupt 4

{

static unsigned char i=0;

static unsigned char total=0;

RI=0;

receive[i&0x0f]=SBUF;

total=total-receive[(i-7)&0x0f]+receive[(i-1)&0x0f];

if(receive[(i-9)&0x0f]==0xaa&&receive[(i-8)&0x0f]==0x55

&&receive[(i-7)&0x0f]==boardaddress&&receive[i&0x0f]==total

)

{

ret_flag=1;

ES = 0;

}

i++;

}

這樣就可以了。等我有機會試一下吧,。我寫了這么多,想必大家都能搞定串口接收了吧。



評論


技術專區(qū)

關閉