謝謝:http://blog.csdn.net/gszhy/article/details/8594433
51單片機的串口,是個全雙工的串口,發送數據的同時,還可以接收數據。
當串行發送完畢后,將在標志位 TI 置 1,同樣,當收到了數據后,也會在 RI 置
1。
無論 RI 或 TI 出現了
1,只要串口中斷處於開放狀態,單片機都會進入串口中斷處理程序。
在中斷程序中,要區分出來究竟是發送引起的中斷,還是接收引起的中斷,然后分別進行處理。
看到過一些書籍和文章,在串口收、發數據的處理方法上,很多人都有不妥之處。
接收數據時,基本上都是使用“中斷方式”,這是正確合理的。
即:每當收到一個新數據,就在中斷函數中,把
RI 清零,並用一個變量,通知主函數,收到了新數據。
發送數據時,很多的程序都是使用的“查詢方式”,就是執行 while(TI ==0);
這樣的語句來等待發送完畢。
這時,處理不好的話,就可能帶來問題。
看了一些網友編寫的程序,發現有如下幾條容易出錯:
1.有人在發送數據之前,先關閉了串口中斷!等待發送完畢后,再打開串口中斷。
這樣,在發送數據的等待期間內,如果收到了數據,將不能進入中斷函數,也就不會保存的這個新收到的數據。
這種處理方法,就會遺漏收到的數據。
2.有人在發送數據之前,並沒有關閉串口中斷,當
TI = 1 時,是可以進入中斷程序的。
但是,卻在中斷函數中,將 TI 清零!
這樣,在主函數中的while(TI
==0);,將永遠等不到發送結束的標志。
3.還有人在中斷程序中,並沒有區分中斷的來源,反而讓發送引起的中斷,執行了接收中斷的程序。
對此,做而論道發表自己常用的方法:
接收數據時,使用“中斷方式”,清除
RI 后,用一個變量通知主函數,收到新數據。
發送數據時,也用“中斷方式”,清除 TI
后,用另一個變量通知主函數,數據發送完畢。
這樣一來,收、發兩者基本一致,編寫程序也很規范、易懂。
更重要的是,主函數中,不用在那兒死等發送完畢,可以有更多的時間查看其它的標志。
實例:
求一個PC與單片機串口通信的程序,要求如下:
1、如果在電腦上發送以$開始的字符串,則將整個字符串原樣返回(字符串長度不是固定的)。
2、如果接收到1,則將P10置高電平,接收到0,P10置低電平。(用來控制一個LED)
單片機是STC89C52RC/晶振11.0592/波特率要求是9600或4800。謝謝!
問題補充:可能會將【$ABCD,123456,987654ccc,aasdasd,aaaa,sssd,4D】這樣的字符串(字符串長度約為50-150個字符)傳送給單片機,只能能原樣返回。
1 最佳答案: 2 下列程序,已經調試成功。 3 #include <REG52.H> 4 sbit LED = P1^0; 5 unsigned char UART_buff; 6 bit New_rec = 0, Send_ed = 1, Money = 0; 7 //---------------------------------------------- 8 void main (void) 9 { 10 SCON = 0x50; //串口方式1, 8-n-1, 允許接收. 11 TMOD = 0x20; //T1方式2 12 TH1 = 0xFD; [url=]//9600bps@11.0592MHz[/url] 13 TL1 = 0xFD; 14 TR1 = 1; 15 ES = 1; //開中斷. 16 EA = 1; 17 while(Money == 0); //等着交費,呵呵,等着接收$. 18 while(1) { 19 if ((New_rec == 1) && (Send_ed == 1)) { //如果收到新數據及發送完畢 20 SBUF = UART_buff; //那就發送. 21 New_rec = 0; 22 Send_ed = 0; 23 } } 24 } 25 //---------------------------------------------- 26 void ser_int (void) interrupt 4 27 { 28 if(RI == 1) { //如果收到. 29 RI = 0; //清除標志. 30 New_rec = 1; 31 UART_buff = SBUF; //接收. 32 if(UART_buff == '1') LED = 1; 33 if(UART_buff == '0') LED = 0; 34 if(UART_buff == '$') Money = 1; 35 } 36 else { //如果送畢. 37 TI = 0; //清除標志. 38 Send_ed = 1; 39 } 40 } 41 //----------------------------------------------
http://bbs.ednchina.com/BLOG_ARTICLE_3007162.HTM
串口接收程序是基於串口中斷的,單片機的串口每次接收到一字節數據產生一次中斷,然后再讀取某個寄存器就可以得到串口接收的數據了。然而在實際應用當中,基本上不會有單字節接收的情況。一般都是基於一定串口通信協議的多字節通信。在422或者485通信中,還可能是一個主機(一般是計算機)帶多個從機(相應的有單片機的板卡)。這就要求我們的單片機能夠在連續接收到的串口數據序列中識別出符合自己板卡對應的通信協議,來進行控制操作,不符合則不進行任何操作。簡而言之就是,單片機要在一串數據中找到符合一定規律的幾個字節的數據。
先來說下怎樣定串口協議吧。這個協議指的不是串口底層的協議,而是前面提到的數據幀協議。一般都是有幀頭(2~3個字節吧),數據(長度根據需要),結束位(1位,有時候設計成校驗字節,最簡單的校驗也就是前面所有數據求和)。
比如0xaa 0x55 +(數據部分省略)+校驗和(除了aa 55 之外數據的和),如果要是多板卡的話有時候還要在幀頭后面加一個板選字節(相當於3字節幀頭了)。
第一次寫串口接收程序的時候,我首先想到的就是定義一個全局變量(實際上最好是定義局部靜態變量),初始值設置為0,然后每進一次中斷+1,然后加到串口通信協議的長度的時候再清零。然后判斷幀頭、校驗。寫完了之后我自己都覺得不對,一旦數據錯開了一位,后面就永遠都接收不到數了。無奈看了一下前輩們的代碼,跟我的思路差不多,只不過那個計數值跟接收到的數據時同時判斷的,而且每次中斷都要判斷,一旦不對計數的那個變量就清零。
廢話少說,直接上一段代碼讓大家看看就明白了。(通信協議姑且按照簡單的aa 55 一個字節數據 一個字節校驗,代碼是基於51單片機的)。接收成功則在中斷程序中把串口接收成功標志位置1。
1 然后串口中斷部分 2 void ser()interrupt 4 3 { 4 static unsigned char count;//串口接收計數的變量 5 RI=0;//手動清某個寄存器,大家都懂的 6 receive[count]=SBUF; 7 if(count==0&&receive[count]==0xaa)//同時判斷count跟收到的數據 8 { 9 count=1; 10 } 11 else if(count==1&&receive[count]==0x55) 12 { 13 count=2; 14 } 15 else if(count==2) 16 { 17 count++; 18 } 19 else if(count==3&&receive[count]== receive [2])//判斷校驗和,數據多的話是求//和,或者其他的校驗方法,也可能是固定的幀尾 20 { 21 count=0; 22 uart_flag =1;//串口接收成功標志,為1時在主程序中回復,然后清零 23 ES=0; //關中斷,回復完了再ES=1; 24 } 25 else 26 { 27 count=0;//判斷不滿足條件就將計數值清零 28 } 29 }
第一次做的串口大概就按照這個方法寫完了(我后來看過其他的代碼,有人用switch語句寫的,邏輯跟這個也差不多,不過我還是感覺用if else來寫清晰一些),
不過在測試的時候發現了bug,如果數據幀發送一半,然后突然停止,再來重新發,就會丟失一幀的數據。比如先接受到aa 55,然后斷了,再進來aa 55 01 01,就不受控制了。后來我也想到一個bug,如果在多設備通信中,屬於其他設備的的幀數據最后一位是aa(或者最后兩位為aa 55 ,或者最后3位為aa 55 板選),下一次通信的數據就接收不到了。
當時對於數據突然中斷的bug,沒有想到很好的解決辦法,不過這種情況幾率極小,所以一直用這個方法寫也沒有問題。多設備通信最后一位恰好是aa的幾率也很小,出問題的可能也很小。當時項目里面的控制數據跟校驗恰好不可能出現aa,於是我把if(count==0&&receive[count]==0xaa)改成了if(receive[count]==0xaa)其他都沒變,解決了,沒有bug了。
后來我又寫了幾次單片機程序,才想到了一些解決問題的方法——不過改天再接着寫吧,太累了,明天還要上班呢。
在后來的項目中,真的遇到了數據位跟校驗位都可能出現aa的情況。我考慮到每次數據都是連續發送的(至少我們用labwindows做的上位機程序是這樣的),成功接收到了一幀數據是要有一定時間回復的,也就是說如果接收到一半,但是很長時間沒接收到數據,把計數值count清零就ok啦。涉及時間的問題自然要用定時器來實現啦。
這次的通信協議如下,串口波特率19200,2個幀頭aa 55 ,一個板選,6字節數據,一個校驗字節(除幀頭外其他數據的和)。
1 全局變量定義 2 unsigned char boardAddr;//板選地址,通過檢測幾個io引腳,具體怎么得到的就不寫了,很簡單的 3 unsigned char g_DatRev [10]={0};//接收緩存 4 bit retFlag=0;//為1代表串口接收到了一幀數據 5 6 7 串口初始化函數,晶振22.1184 8 9 void init_uart() 10 { 11 SCON = 0x50; //串口方式1允許接收 12 TMOD = 0x21; //定時器1,方式2,8位自動重載,同時配置定時器0,工作方式1 13 PCON = 0x80; // 波特率加倍 14 TH1 = 0xfa; 15 TL1 = 0xfa; //寫入串口定時器初值 16 TH0=(65536-2000)/256; //寫入定時器0初值,串口傳輸一個字節時間為(1/19200)*10,計算得0.52ms 17 TL0=(65536-2000)%256; //定時器0定時大約1ms多 18 EA=1; 19 ET0=1; //波特率:19200 22.1184M 初值:250(0xfa) 20 IE |= 0x90; 21 TR1 = 1; 22 } 23 24 串口中斷函數 25 26 void UART_INT(void) interrupt 4 27 { 28 static unsigned char count;//串口接收計數的變量 29 30 RI = 0; 31 g_DatRev[count] = SBUF; 32 if(g_DatRev[count]==0xaa&&count==0) //幀頭 33 { 34 count=1; 35 } 36 else if(count==1&&g_DatRev[count]==0x55) 37 { 38 count=2; 39 } 40 41 else if (count==2&&g_DatRev[2] == boardAddr) 42 { 43 CK = g_DatRev[count]; 44 count=3; 45 46 } 47 48 else if(count>=3&&count<9) 49 { 50 51 CK += g_DatRev[count]; 52 count ++; 53 } 54 55 else if(count == 9&&CK==g_DatRev[9]) 56 { 57 ES = 0; 58 retFlag = 1; 59 count=0; 60 } 61 else 62 { 63 count=0; 64 } 65 resettimer(); 66 67 } 68 69 //判斷count不為0的話就啟動定時器 70 void resettimer() 71 { 72 TR0=0; 73 TH0=(65536-2000)/256; 74 TL0=(65536-2000)%256; 75 if(count!=0) 76 { 77 TR0=1; 78 } 79 } 80 81 定時器中斷函數 82 void T0_time()interrupt 1 83 { 84 TR0=0; 85 TH0=(65536-2000)/256; 86 TL0=(65536-2000)%256; 87 count=0; 88 89 }
這種方法的確是本人自己想出來的,別人可能也這樣做過,但我這個絕對不是抄襲或者模仿來的。這樣寫的確可以避免前面提到過的bug,不過代價是多用了一個定時器的資源,而且中斷函數里的內容更多了,占用了更多的時間。
要是能把第一種方法改進一下就好了,主要是那個校驗不能為aa的那個bug,因為畢竟傳輸到一半突然斷了的可能性是非常小的。后來我想第一個判斷if(count==0&&receive[count]==0xaa)好像有點太嚴格了,考慮到第二字節的幀頭,跟板選地址不可能為aa,於是把這個改寫為if(count>=0&&count<=2&& receive[count]==0xaa),這樣就把bug出現的幾率降到了非常小,也只是在前一幀結尾數據恰好為 aa 55 板選 的時候才出現,幾率是多少大家自己算一下吧,呵呵。這樣我自己覺得,昨天寫的那種方法改進到這個程度,應該算可以啦,反正我是很滿意了。
實際上我還想過其他的方法,比如緩存的數組采用移位寄存的方式。拿前面的4個字節的協議為例。
1 void ser()interrupt 4 2 { 3 unsigned char i; 4 RI=0; 5 6 for(i=0;i<3;i++) 7 { 8 receive[i]=receive[i+1]; 9 } 10 receive[3]=SBUF; 11 if(reveive[0]==0xaa&&receive[1]==0x55&&receive[2]==receive[3]) 12 { 13 ret_flag=1; 14 ES = 0; 15 } 16 17 }
這段代碼看上去可是簡單明了,這樣判斷可是不錯啊,同時判斷幀頭跟校驗不會產生前面提到的bug。說實話當時我剛想出這種方法並寫出來的時候,馬上就被我給否了。那個for循環可真是很占時間的啊,延時函數都是這樣寫的。每次都循環一下,這延時太長,通信速度太快的話就不能接收到下一字節數據了。最要命的是這個時間的長度是隨着通信協議幀的字節數增加而增加的,如果一次要接收幾十個字節,肯定就玩完了。這種方法我一次都沒用過。
不過我居然又想出來了這種方法的改良措施,是前兩天剛想出來的,呵呵,還沒有實踐過呢。
下面代碼的協議就按第二段程序(定時器清零的那個協議,一共10字節)
全局變量
1 bit ret_flag; 2 unsigned char receive[256]={0}; 3 unsigned char boardaddress; 4 5 中斷函數 6 7 void ser()interrupt 4 8 { 9 10 11 static unsigned char i=0; 12 static unsigned char total=0; 13 RI=0; 14 receive[i]=SBUF; 15 total=total-receive[i-7]+receive[i-1]; 16 17 if(receive[i-9]==0xaa&&receive[i-8]==0x55 18 &&receive[i-7]==boardaddress&&receive[i]==total 19 ) 20 { 21 ret_flag=1; 22 ES = 0; 23 } 24 i++; 25 26 }
之所以要定義256個長度的數組,就是為了能夠讓數組“首尾相接”。因為0 -1 = 255 , 255+1 = 0。而且我在計算校驗的時候也改進了算法,不會因為數據長度的增加而增加計算校驗值的時間。這種方法也是我不久前才想出來的,所以還沒有經過實際的驗證。上面的代碼可能會有邏輯上的錯誤,如果真有錯誤,有網友看出來的話,請在下面留言告訴我。這個方法也是我原創的哦,別人也肯能會想到,不過我這個絕對不是抄襲別人的。
上面的代碼最大的缺點就是變量定義的太多了,太占ram資源了,編譯的時候可能會出現錯誤,畢竟51單片機才128字節的ram(有的資源也很豐富的,比如c8051系列的),這一下子就是256字節的變量。不過對於資源多一些的單片機,這樣寫還是可以的。要是能有4bit在一起的數據類型就好了,呵呵,verilog代碼里面是可以的,C語言里貌似不行啊。
要想能在例如51單片機上運行,只能按照下面的折中方式了,也就是把i相關的量都與一個0x0f
1 全局變量 2 3 bit ret_flag; 4 unsigned char receive[16]={0};// 可以考慮在定義時加上idata,畢竟還可能是32 5 //或者64長度的數組呢unsigned char idata receive[16]={0}; 6 7 unsigned char boardaddress; 8 9 中斷函數 10 11 void ser()interrupt 4 12 { 13 14 15 static unsigned char i=0; 16 static unsigned char total=0; 17 RI=0; 18 receive[i&0x0f]=SBUF; 19 total=total-receive[(i-7)&0x0f]+receive[(i-1)&0x0f]; 20 21 if(receive[(i-9)&0x0f]==0xaa&&receive[(i-8)&0x0f]==0x55 22 &&receive[(i-7)&0x0f]==boardaddress&&receive[i&0x0f]==total 23 ) 24 { 25 ret_flag=1; 26 ES = 0; 27 } 28 i++; 29 30 }