51單片機串口通信的發(fā)送與接收 字符串
當串行發(fā)送完畢后,將在標志位 TI 置 1,同樣,當收到了數據后,也會(huì )在 RI 置 1。
無(wú)論 RI 或 TI 出現了 1,只要串口中斷處于開(kāi)放狀態(tài),單片機都會(huì )進(jìn)入串口中斷處理程序。
在中斷程序中,要區分出來(lái)究竟是發(fā)送引起的中斷,還是接收引起的中斷,然后分別進(jìn)行處理。
看到過(guò)一些書(shū)籍和文章,在串口收、發(fā)數據的處理方法上,很多人都有不妥之處。
接收數據時(shí),基本上都是使用“中斷方式”,這是正確合理的。
即:每當收到一個(gè)新數據,就在中斷函數中,把 RI 清零,并用一個(gè)變量,通知主函數,收到了新數據。
發(fā)送數據時(shí),很多的程序都是使用的“查詢(xún)方式”,就是執行 while(TI ==0); 這樣的語(yǔ)句來(lái)等待發(fā)送完畢。
這時(shí),處理不好的話(huà),就可能帶來(lái)問(wèn)題。
看了一些網(wǎng)友編寫(xiě)的程序,發(fā)現有如下幾條容易出錯:
1.有人在發(fā)送數據之前,先關(guān)閉了串口中斷!等待發(fā)送完畢后,再打開(kāi)串口中斷。
這樣,在發(fā)送數據的等待期間內,如果收到了數據,將不能進(jìn)入中斷函數,也就不會(huì )保存的這個(gè)新收到的數據。
這種處理方法,就會(huì )遺漏收到的數據。
2.有人在發(fā)送數據之前,并沒(méi)有關(guān)閉串口中斷,當 TI = 1 時(shí),是可以進(jìn)入中斷程序的。
但是,卻在中斷函數中,將 TI 清零!
這樣,在主函數中的while(TI ==0);,將永遠等不到發(fā)送結束的標志。
3.還有人在中斷程序中,并沒(méi)有區分中斷的來(lái)源,反而讓發(fā)送引起的中斷,執行了接收中斷的程序。
對此,做而論道發(fā)表自己常用的方法:
接收數據時(shí),使用“中斷方式”,清除 RI 后,用一個(gè)變量通知主函數,收到新數據。
發(fā)送數據時(shí),也用“中斷方式”,清除 TI 后,用另一個(gè)變量通知主函數,數據發(fā)送完畢。
這樣一來(lái),收、發(fā)兩者基本一致,編寫(xiě)程序也很規范、易懂。
更重要的是,主函數中,不用在那兒死等發(fā)送完畢,可以有更多的時(shí)間查看其它的標志。
實(shí)例:
求一個(gè)PC與單片機串口通信的程序,要求如下:
1、如果在電腦上發(fā)送以$開(kāi)始的字符串,則將整個(gè)字符串原樣返回(字符串長(cháng)度不是固定的)。
2、如果接收到1,則將P10置高電平,接收到0,P10置低電平。(用來(lái)控制一個(gè)LED)
單片機是STC89C52RC/晶振11.0592/波特率要求是9600或4800。謝謝!
問(wèn)題補充:可能會(huì )將【$ABCD,123456,987654ccc,aasdasd,aaaa,sssd,4D】這樣的字符串(字符串長(cháng)度約為50-150個(gè)字符)傳送給單片機,只能能原樣返回。
- 最佳答案:
- 下列程序,已經(jīng)調試成功。
- #include
- sbitLED=P1^0;
- unsignedcharUART_buff;
- bitNew_rec=0,Send_ed=1,Money=0;
- //----------------------------------------------
- voidmain(void)
- {
- SCON=0x50;//串口方式1,8-n-1,允許接收.
- TMOD=0x20;//T1方式2
- TH1=0xFD;[url=]//9600bps@11.0592MHz[/url]
- TL1=0xFD;
- TR1=1;
- ES=1;//開(kāi)中斷.
- EA=1;
- while(Money==0);//等著(zhù)交費,呵呵,等著(zhù)接收$.
- while(1){
- if((New_rec==1)&&(Send_ed==1)){//如果收到新數據及發(fā)送完畢
- SBUF=UART_buff;//那就發(fā)送.
- New_rec=0;
- Send_ed=0;
- }}
- }
- //----------------------------------------------
- voidser_int(void)interrupt4
- {
- if(RI==1){//如果收到.
- RI=0;//清除標志.
- New_rec=1;
- UART_buff=SBUF;//接收.
- if(UART_buff==1)LED=1;
- if(UART_buff==0)LED=0;
- if(UART_buff==$)Money=1;
- }
- else{//如果送畢.
- TI=0;//清除標志.
- Send_ed=1;
- }
- }
- //----------------------------------------------
串口接收程序是基于串口中斷的,單片機的串口每次接收到一字節數據產(chǎn)生一次中斷,然后再讀取某個(gè)寄存器就可以得到串口接收的數據了。然而在實(shí)際應用當中,基本上不會(huì )有單字節接收的情況。一般都是基于一定串口通信協(xié)議的多字節通信。在422或者485通信中,還可能是一個(gè)主機(一般是計算機)帶多個(gè)從機(相應的有單片機的板卡)。這就要求我們的單片機能夠在連續接收到的串口數據序列中識別出符合自己板卡對應的通信協(xié)議,來(lái)進(jìn)行控制操作,不符合則不進(jìn)行任何操作。簡(jiǎn)而言之就是,單片機要在一串數據中找到符合一定規律的幾個(gè)字節的數據。
先來(lái)說(shuō)下怎樣定串口協(xié)議吧。這個(gè)協(xié)議指的不是串口底層的協(xié)議,而是前面提到的數據幀協(xié)議。一般都是有幀頭(2~3個(gè)字節吧),數據(長(cháng)度根據需要),結束位(1位,有時(shí)候設計成校驗字節,最簡(jiǎn)單的校驗也就是前面所有數據求和)。
比如0xaa 0x55 +(數據部分省略)+校驗和(除了aa 55 之外數據的和),如果要是多板卡的話(huà)有時(shí)候還要在幀頭后面加一個(gè)板選字節(相當于3字節幀頭了)。
第一次寫(xiě)串口接收程序的時(shí)候,我首先想到的就是定義一個(gè)全局變量(實(shí)際上最好是定義局部靜態(tài)變量),初始值設置為0,然后每進(jìn)一次中斷+1,然后加到串口通信協(xié)議的長(cháng)度的時(shí)候再清零。然后判斷幀頭、校驗。寫(xiě)完了之后我自己都覺(jué)得不對,一旦數據錯開(kāi)了一位,后面就永遠都接收不到數了。無(wú)奈看了一下前輩們的代碼,跟我的思路差不多,只不過(guò)那個(gè)計數值跟接收到的數據時(shí)同時(shí)判斷的,而且每次中斷都要判斷,一旦不對計數的那個(gè)變量就清零。
廢話(huà)少說(shuō),直接上一段代碼讓大家看看就明白了。(通信協(xié)議姑且按照簡(jiǎn)單的aa 55 一個(gè)字節數據 一個(gè)字節校驗,代碼是基于51單片機的)。接收成功則在中斷程序中把串口接收成功標志位置1。
- 然后串口中斷部分
- voidser()interrupt4
- {
- staticunsignedcharcount;//串口接收計數的變量
- RI=0;//手動(dòng)清某個(gè)寄存器,大家都懂的
- receive[count]=SBUF;
- if(count==0&&receive[count]==0xaa)//同時(shí)判斷count跟收到的數據
- {
- count=1;
- }
- elseif(count==1&&receive[count]==0x55)
- {
- count=2;
- }
- elseif(count==2)
- {
- count++;
- }
- elseif(count==3&&receive[count]==receive[2])//判斷校驗和,數據多的話(huà)是求//和,或者其他的校驗方法,也可能是固定的幀尾
- {
- count=0;
- uart_flag=1;//串口接收成功標志,為1時(shí)在主程序中回復,然后清零
- ES=0;//關(guān)中斷,回復完了再ES=1;
- }
- else
- {
- count=0;//判斷不滿(mǎn)足條件就將計數值清零
- }
- }
第一次做的串口大概就按照這個(gè)方法寫(xiě)完了(我后來(lái)看過(guò)其他的代碼,有人用switch語(yǔ)句寫(xiě)的,邏輯跟這個(gè)也差不多,不過(guò)我還是感覺(jué)用if else來(lái)寫(xiě)清晰一些),
不過(guò)在測試的時(shí)候發(fā)現了bug,如果數據幀發(fā)送一半,然后突然停止,再來(lái)重新發(fā),就會(huì )丟失一幀的數據。比如先接受到aa 55,然后斷了,再進(jìn)來(lái)aa 55 01 01,就不受控制了。后來(lái)我也想到一個(gè)bug,如果在多設備通信中,屬于其他設備的的幀數據最后一位是aa(或者最后兩位為aa 55 ,或者最后3位為aa 55 板選),下一次通信的數據就接收不到了。
當時(shí)對于數據突然中斷的bug,沒(méi)有想到很好的解決辦法,不過(guò)這種情況幾率極小,所以一直用這個(gè)方法寫(xiě)也沒(méi)有問(wèn)題。多設備通信最后一位恰好是aa的幾率也很小,出問(wèn)題的可能也很小。當時(shí)項目里面的控制數據跟校驗恰好不可能出現aa,于是我把if(count==0&&receive[count]==0xaa)改成了if(receive[count]==0xaa)其他都沒(méi)變,解決了,沒(méi)有bug了。
后來(lái)我又寫(xiě)了幾次單片機程序,才想到了一些解決問(wèn)題的方法——不過(guò)改天再接著(zhù)寫(xiě)吧,太累了,明天還要上班呢。
在后來(lái)的項目中,真的遇到了數據位跟校驗位都可能出現aa的情況。我考慮到每次數據都是連續發(fā)送的(至少我們用labwindows做的上位機程序是這樣的),成功接收到了一幀數據是要有一定時(shí)間回復的,也就是說(shuō)如果接收到一半,但是很長(cháng)時(shí)間沒(méi)接收到數據,把計數值count清零就ok啦。涉及時(shí)間的問(wèn)題自然要用定時(shí)器來(lái)實(shí)現啦。
這次的通信協(xié)議如下,串口波特率19200,2個(gè)幀頭aa 55 ,一個(gè)板選,6字節數據,一個(gè)校驗字節(除幀頭外其他數據的和)。
- 全局變量定義
- unsignedcharboardAddr;//板選地址,通過(guò)檢測幾個(gè)io引腳,具體怎么得到的就不寫(xiě)了,很簡(jiǎn)單的
- unsignedcharg_DatRev[10]={0};//接收緩存
- bitretFlag=0;//為1代表串口接收到了一幀數據
- 串口初始化函數,晶振22.1184
- voidinit_uart()
- {
- SCON=0x50;//串口方式1允許接收
- TMOD=0x21;//定時(shí)器1,方式2,8位自動(dòng)重載,同時(shí)配置定時(shí)器0,工作方式1
- PCON=0x80;//波特率加倍
- TH1=0xfa;
- TL1=0xfa;//寫(xiě)入串口定時(shí)器初值
- TH0=(65536-2000)/256;//寫(xiě)入定時(shí)器0初值,串口傳輸一個(gè)字節時(shí)間為(1/19200)*10,計算得0.52ms
- TL0=(65536-2000)%256;//定時(shí)器0定時(shí)大約1ms多
- EA=1;
- ET0=1;//波特率:1920022.1184M初值:250(0xfa)
- IE|=0x90;
- TR1=1;
- }
- 串口中斷函數
- voidUART_INT(void)interrupt4
- {
- staticunsignedcharcount;//串口接收計數的變量
- RI=0;
- g_DatRev[count]=SBUF;
- if(g_DatRev[count]==0xaa&&count==0)//幀頭
- {
- count=1;
- }
- elseif(count==1&&g_DatRev[count]==0x55)
- {
- count=2;
- }
- elseif(count==2&&g_DatRev[2]==boardAddr)
- {
- CK=g_DatRev[count];
- count=3;
- }
- elseif(count>=3&&count<9)
- {
- CK+=g_DatRev[count];
- count++;
- }
- elseif(count==9&&CK==g_DatRev[9])
- {
- ES=0;
- retFlag=1;
- count=0;
- }
- else
- {
- count=0;
- }
- resettimer();
- }
- //判斷count不為0的話(huà)就啟動(dòng)定時(shí)器
- voidresettimer()
- {
- TR0=0;
- TH0=(65536-2000)/256;
- TL0=(65536-2000)%256;
- if(count!=0)
- {
- TR0=1;
- }
- }
- 定時(shí)器中斷函數
- voidT0_time()interrupt1
- {
- TR0=0;
- TH0=(65536-2000)/256;
- TL0=(65536-2000)%256;
- count=0;
- }
這種方法的確是本人自己想出來(lái)的,別人可能也這樣做過(guò),但我這個(gè)絕對不是抄襲或者模仿來(lái)的。這樣寫(xiě)的確可以避免前面提到過(guò)的bug,不過(guò)代價(jià)是多用了一個(gè)定時(shí)器的資源,而且中斷函數里的內容更多了,占用了更多的時(shí)間。
要是能把第一種方法改進(jìn)一下就好了,主要是那個(gè)校驗不能為aa的那個(gè)bug,因為畢竟傳輸到一半突然斷了的可能性是非常小的。后來(lái)我想第一個(gè)判斷if(count==0&&receive[count]==0xaa)好像有點(diǎn)太嚴格了,考慮到第二字節的幀頭,跟板選地址不可能為aa,于是把這個(gè)改寫(xiě)為if(count>=0&&count<=2&& receive[count]==0xaa),這樣就把bug出現的幾率降到了非常小,也只是在前一幀結尾數據恰好為 aa 55 板選 的時(shí)候才出現,幾率是多少大家自己算一下吧,呵呵。這樣我自己覺(jué)得,昨天寫(xiě)的那種方法改進(jìn)到這個(gè)程度,應該算可以啦,反正我是很滿(mǎn)意了。
實(shí)際上我還想過(guò)其他的方法,比如緩存的數組采用移位寄存的方式。拿前面的4個(gè)字節的協(xié)議為例。
- voidser()interrupt4
- {
- unsignedchari;
- 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;
- }
- }
這段代碼看上去可是簡(jiǎn)單明了,這樣判斷可是不錯啊,同時(shí)判斷幀頭跟校驗不會(huì )產(chǎn)生前面提到的bug。說(shuō)實(shí)話(huà)當時(shí)我剛想出這種方法并寫(xiě)出來(lái)的時(shí)候,馬上就被我給否了。那個(gè)for循環(huán)可真是很占時(shí)間的啊,延時(shí)函數都是這樣寫(xiě)的。每次都循環(huán)一下,這延時(shí)太長(cháng),通信速度太快的話(huà)就不能接收到下一字節數據了。最要命的是這個(gè)時(shí)間的長(cháng)度是隨著(zhù)通信協(xié)議幀的字節數增加而增加的,如果一次要接收幾十個(gè)字節,肯定就玩完了。這種方法我一次都沒(méi)用過(guò)。
不過(guò)我居然又想出來(lái)了這種方法的改良措施,是前兩天剛想出來(lái)的,呵呵,還沒(méi)有實(shí)踐過(guò)呢。
下面代碼的協(xié)議就按第二段程序(定時(shí)器清零的那個(gè)協(xié)議,一共10字節)
全局變量
- bitret_flag;
- unsignedcharreceive[256]={0};
- unsignedcharboardaddress;
- 中斷函數
- voidser()interrupt4
- {
- staticunsignedchari=0;
- staticunsignedchartotal=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個(gè)長(cháng)度的數組,就是為了能夠讓數組“首尾相接”。因為0 -1 = 255 , 255+1 = 0。而且我在計算校驗的時(shí)候也改進(jìn)了算法,不會(huì )因為數據長(cháng)度的增加而增加計算校驗值的時(shí)間。這種方法也是我不久前才想出來(lái)的,所以還沒(méi)有經(jīng)過(guò)實(shí)際的驗證。上面的代碼可能會(huì )有邏輯上的錯誤,如果真有錯誤,有網(wǎng)友看出來(lái)的話(huà),請在下面留言告訴我。這個(gè)方法也是我原創(chuàng )的哦,別人也肯能會(huì )想到,不過(guò)我這個(gè)絕對不是抄襲別人的。
上面的代碼最大的缺點(diǎn)就是變量定義的太多了,太占ram資源了,編譯的時(shí)候可能會(huì )出現錯誤,畢竟51單片機才128字節的ram(有的資源也很豐富的,比如c8051系列的),這一下子就是256字節的變量。不過(guò)對于資源多一些的單片機,這樣寫(xiě)還是可以的。要是能有4bit在一起的數據類(lèi)型就好了,呵呵,verilog代碼里面是可以的,C語(yǔ)言里貌似不行啊。
要想能在例如51單片機上運行,只能按照下面的折中方式了,也就是把i相關(guān)的量都與一個(gè)0x0f
- 全局變量
- bitret_flag;
- unsignedcharreceive[16]={0};//可以考慮在定義時(shí)加上idata,畢竟還可能是32
- //或者64長(cháng)度的數組呢unsignedcharidatareceive[16]={0};
- unsignedcharboardaddress;
- 中斷函數
- voidser()interrupt4
- {
- staticunsignedchari=0;
- staticunsignedchartotal=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++;
- }
評論