51單片機S485通訊,正真好用,講解通俗易懂。
9-串口通訊之485通訊.doc
(74.5 KB, 下載次數(shù): 75)
2017-6-14 21:48 上傳
點擊文件名下載附件
51單片機 下載積分: 黑幣 -5
第九節(jié):串口通訊之485通訊
(1) 開場白:
串口通訊有三種常用的硬件方式。
第一種是TTL方式。比如兩個單片機的RX,TX引腳直接連接上(甲的RX連接乙的TX, 甲的TX連接乙的RX),這種通訊距離最短,局限于兩三米長的距離。
第二種是232方式。兩個CPU之間都經(jīng)過了兩個MAX232等電平轉(zhuǎn)換芯片,比如單片機跟電腦的串口通訊。這種最大傳輸距離大概十米左右。
第三種是485方式。兩個CPU之間都經(jīng)過了兩個MAX485等電平轉(zhuǎn)換芯片,此種方式在工控上應用最多,尤其是距離長,要求一臺主機控制多臺設備的情況下。從地址呼叫原理上看,一臺主機應該可以控制N多臺從機,但是書上說一臺主機最多可以控制32個從機,可能主要是從電阻匹配的角度來考慮,我沒驗證過,我只搞過一臺主機控制十幾臺從機的項目。485的最大傳輸距離大概在1000米左右。
這三種方式在編程上還是會有一些差異。TTL方式與232方式的編程幾乎完全兼容,唯一需要注意的是,發(fā)送一串數(shù)據(jù)時,每個字節(jié)延時間隔的大小可能會有些差異。
485通訊跟前面兩種方式有一個最大的區(qū)別是,485通訊要多增加一個IO口來控制數(shù)據(jù)流的方向,輸出低電平時表示接受數(shù)據(jù)的狀態(tài),輸出高電平時表示發(fā)送數(shù)據(jù)的狀態(tài)。485的通訊協(xié)議一般都是用主機對從機廣播呼叫的模式。即平時所有從機處于接受數(shù)據(jù)的狀態(tài)(流控IO口處于低電平),主機發(fā)送呼叫某個從機的地址,相應的從機收到數(shù)據(jù)后,馬上切換到發(fā)送數(shù)據(jù)的狀態(tài)(流控IO口處于高電平),然后往主機返回對應的數(shù)據(jù),從機對主機發(fā)送完數(shù)據(jù)之后,從機又馬上切換到接受數(shù)據(jù)的狀態(tài)(流控IO口處于低電平)。
這節(jié)利用第八節(jié)的232通訊程序修改成485通訊程序。
(2)功能需求:
無論是單片機還是上位機,最好在固定協(xié)議前多發(fā)送一個填充的無效字節(jié)0x00,因為硬件原因,第一個字節(jié)往往容易丟失。
通訊協(xié)議:XX YY EB 00 55
其中后三位 EB 00 55就是我所說的數(shù)據(jù)尾,它的有效數(shù)據(jù)XX YY在數(shù)據(jù)尾的前面。
任意時刻,從電腦“串口調(diào)試助手”上位機收到的一堆數(shù)據(jù)中,只要此數(shù)據(jù)中包含關(guān)鍵字EB 00 55 ,并且此關(guān)鍵字前面兩個字節(jié)的數(shù)據(jù)XX YY 分別為01 02,那么就往上位機發(fā)送“eb 00 aa”表示確認,同時蜂鳴器叫一聲。否則,往上位機發(fā)送“eb 00 55”表示出錯。
(3)硬件原理:
在電腦的串口處加一個232轉(zhuǎn)485的通訊模塊。然后把單片機串口通訊的那兩個引腳經(jīng)過一個MAX485芯片,直接與電腦處的232轉(zhuǎn)485的通訊模塊相連接,多增加一根單片機的IO口,用此IO連接MAX485的第2和第3引腳,實現(xiàn)流控的功能。當此IO處于低電平時為接收數(shù)據(jù)的狀態(tài),當此IO口處于高電平時為發(fā)送數(shù)據(jù)的狀態(tài)。
(4)源碼適合的單片機:PIC18f4520,晶振為22.1184MHz,波特率115200
(5)源代碼講解如下:
#include<pic18.h> //包含芯片相關(guān)頭文件
//補充說明:吳堅鴻程序風格是這樣的,凡是輸出IO后綴都是_dr,凡是輸入的//IO后綴都//是_sr
#define beep_dr LATA2 //蜂鳴器輸出
#define rede_dr LATC5 //485通訊的流控
//補充說明:吳堅鴻程序風格是這樣的,凡是做延時計數(shù)閥值的常量
//前綴都用cnt_表示。
#define cnt_voice_time 150 //蜂鳴器響的聲音長短的延時閥值
#define cnt_send 300 //確保接收緩沖區(qū)沒有繼續(xù)接收數(shù)據(jù),是變量
//send_cnt的溢出閥值
Void usart_service(); //串口通訊服務程序,放在main函數(shù)里
unsigned char asy_recieve(); //把串口緩沖區(qū)的數(shù)據(jù)一個個提取出來
void eusart_send(unsigned char t_data); //串口發(fā)送一個字節(jié)的數(shù)據(jù)
Void Buf_clear() ; //把余下的緩沖區(qū)清零
void Delay11(unsigned int MS); //延時函數(shù)
//補充說明:吳堅鴻程序風格是這樣的,凡是計數(shù)器延時的變量
//后綴都用_cnt表示。
unsigned int voice_time_cnt; //蜂鳴器響的聲音長短的計數(shù)延時
unsigned int send_cnt=0; //一串數(shù)據(jù)從上位機發(fā)過來的時候,他們每個字節(jié)之間//的延時間隔很短,如果他們的延時間隔一旦超過了這個send_cnt變量的延時,那么就////認為他們的一串數(shù)據(jù)已經(jīng)發(fā)送完畢
//補充說明:吳堅鴻程序風格是這樣的,凡是涉及統(tǒng)計數(shù)量的變量
//后綴都用_total表示。
unsigned int RCREG_total; //統(tǒng)計串口緩沖區(qū)已經(jīng)收了多少個數(shù)據(jù)
unsigned int RCREG_read_total; //統(tǒng)計已經(jīng)從串口緩沖區(qū)讀出了多少個數(shù)據(jù)
//補充說明:吳堅鴻程序風格是這樣的,凡是用來更新的標識變量,比如液晶刷屏,或者有新接收的串口數(shù)據(jù)更新等等,后綴一律用_update表示
Unsigned char send_update=0; //一旦有數(shù)據(jù)從上位機發(fā)送過來,就會引發(fā)串口接收中////斷,在串口中斷里,我把send_update=1表示目前正在接收數(shù)據(jù),警告單片機先不要//猴急,等串口中斷把所有從上位機連續(xù)發(fā)送過來的一堆數(shù)據(jù)接收完,再處理。那么什么///時候才知道發(fā)送的數(shù)據(jù)已經(jīng)發(fā)送完畢了呢?用send_cnt識別。因為在串口中斷里,我///每次都會把send_cnt=0,而在main函數(shù)里,一旦發(fā)現(xiàn)send_update==1,send_cnt就//會開始自加,當它超過某個數(shù)值的時候,就會自動把send_update=0,表示目前已經(jīng)沒//有數(shù)據(jù)發(fā)送了。而如果有數(shù)據(jù)不斷從上位機傳來,send_cnt永遠也不會超過某個數(shù)值,//因為每個中斷它都被清零,這個原理跟看門口狗喂狗的原理很像。
//補充說明:吳堅鴻程序風格是這樣的,凡是用來接收數(shù)據(jù)的緩沖區(qū)數(shù)組后綴都用_buf表//示
Unsigned char RCREG_buf[50]; //串口接收緩沖區(qū),讀者可以根據(jù)實際項目設置大小
Unsigned char RCREG_buf_temp[50]; //臨時處理串口數(shù)據(jù)的緩沖區(qū),可以不用那么大
//補充說明:吳堅鴻程序風格是這樣的,凡是自鎖變量名, 后綴都用_lock表示。
Unsigned char send_lock=0;
//補充說明:吳堅鴻程序風格是這樣的,凡是在main函數(shù)中用的中間變量,前綴m_,后//綴用_char或者_int表示類型
Unsigned int m_int; //中間變量,只要是用在main函數(shù)里,誰都可以重復用。
//主程序
main()
{
ADCON0=0x00;
ADCON1=0x0f; //全部為數(shù)字信號
ADCON2=0xa1; //右對齊
RBPU=0; //上拉電阻
SSPEN=0; //決定RA5不作為串口
TRISA2=0; //蜂鳴器輸出
TRISC5=0; //485通訊的流控輸出
BRG16=0; //設置串口通信寄存器
BRGH=0;
SPBRGH=0x00;
SPBRG=0x02; //22.1184MHz晶振,115200波特率
SYNC=0;
SPEN=1;
TX9=0;
TXEN=1;
TXIF=1;
RX9=0;
CREN=1;
RCIE=1;
PEIE=1;
GIE=1;
T1CON=0x24; //定時器中斷配置
TMR1H=0xFE;
TMR1L=0xEF;
TMR1IF=0;
TMR1IE=1;
TMR1ON=1;
TMR1IE=1;
//補充說明,以上的內(nèi)容為寄存器配置,每種不同的單片機會有點差異,
//大家不用過度關(guān)注以上寄存器的配置,只要知道有這么一回事即可
beep_dr=0; //關(guān)蜂鳴器,上電初始化IO
rede_dr=0; //流控IO,從機處于接收數(shù)據(jù)的狀態(tài)
while(1)
{
CLRWDT(); //喂看門狗,大家不用過度關(guān)注此行
usart_service(); //串口通訊服務
}
}
//中斷
void interrupt timer1rbint(void)
{
if(RCIE==1&&RCIF==1) //串口中斷,一次只能接受一個字節(jié)
{
RCIE=0;
RCIF=0;
++RCREG_total; //以下代碼是鴻哥的在所有串口項目中用到的標準代碼
if(RCREG_total>50) //超過緩沖區(qū)
{
RCREG_total=50;
}
RCREG_buf[RCREG_total-1]=RCREG; //依次把上位機來的數(shù)據(jù)存入數(shù)組緩沖區(qū)
send_update=1; //通知單片機目前正在接收數(shù)據(jù)
send_cnt=0; //及時喂狗,雖然main函數(shù)那邊不斷在累加,但是只要串口的數(shù)//據(jù)還沒發(fā)送完畢,那么它永遠也長不大,因為每個中斷都被清零,很可憐。
RCIE=1;
}
if(TMR1IE==1&&TMR1IF==1) //定時中斷
{
TMR1IF=0; //定時中斷標志位關(guān)閉
TMR1ON=0; //定時中斷開關(guān)關(guān)閉
if(voice_time_cnt) //控制蜂鳴器聲音的長短
{
beep_dr=1; //蜂鳴器響
--voice_time_cnt; //蜂鳴器響的聲音長短的計數(shù)延時
}
else
{
Asm(“nop”); //添加此行空指令為了使else的內(nèi)容跟if的內(nèi)容對稱,意義////不大
beep_dr=0; //蜂鳴器停止
}
TMR1H=0xFe; //重新設置定時時間間隔
TMR1L=0x00;
TMR1ON=1; //定時中斷開關(guān)打開
}
}
void usart_service() //串口服務程序,在main函數(shù)里
{
if(send_update==1) //說明目前串口正在接收數(shù)據(jù),不要讀緩沖區(qū)數(shù)據(jù)
{
send_lock=1; //開自鎖標志
++send_cnt; //只要有數(shù)據(jù)接收,send_cnt每次都被串口中斷清零
if(send_cnt>cnt_send) //延時一段時間,確認緩沖區(qū)沒有繼續(xù)接受數(shù)據(jù)
{
send_cnt=0;
send_update=0;
}
}
Else //說明當前沒有繼續(xù)接收數(shù)據(jù)了
{
if(send_lock==1) //在數(shù)據(jù)已經(jīng)接收完畢,并且還沒有處理過數(shù)據(jù)的情況下
{
send_lock=0; //處理一次就鎖起來,不用每次都進來,除非有新接收的數(shù)據(jù)
m_int=0; //中間變量清零,用來統(tǒng)計處理了多少個剛剛接收的數(shù)據(jù)
while(RCREG_read_total<RCREG_total) //說明還沒有把緩沖區(qū)的數(shù)據(jù)讀取完
{
CLRWDT();
RCREG_buf_temp[m_int]=asy_recieve();
If(m_int>=4) //說明接收了5個數(shù)據(jù)以上(2個有效數(shù)據(jù)加3個數(shù)據(jù)尾)
{
if(RCREG_buf_temp[m_int-2]==0xeb&&RCREG_buf_temp[m_int-1]==0x00&&RCREG_buf_temp[m_int]==0x55) //數(shù)據(jù)尾”eb 00 55”判斷
{
if(RCREG_buf_temp[m_int-4]==0x01&& RCREG_buf_temp[m_int-3]==0x02)
//有效數(shù)據(jù)是否為01 02的判斷
{
eusart_send(0x00); //串口發(fā)送一個填充的無效字節(jié)0x00,避免第一個字節(jié)丟失而引起的問題
eusart_send(0xeb); //串口發(fā)送應答的數(shù)據(jù)
eusart_send(0x00); //串口發(fā)送應答的數(shù)據(jù)
eusart_send(0xaa); //串口發(fā)送應答的數(shù)據(jù)
voice_time_cnt= cnt_voice_time; //蜂鳴器響“滴”一聲就停
}
Else
{
eusart_send(0x00); //串口發(fā)送一個填充的無效字節(jié)0x00,避免第一個字節(jié)丟失而引起的問題
eusart_send(0xeb); //串口發(fā)送應答的數(shù)據(jù)
eusart_send(0x00); //串口發(fā)送應答的數(shù)據(jù)
eusart_send(0x55); //串口發(fā)送應答的數(shù)據(jù)
}
Break; //跳出循環(huán)
}
}
m_int++; //中間變量加1,
}
Buf_clear(); //把緩沖區(qū)的下標清零,方便下一堆數(shù)據(jù)接收與處理
}
}
}
Void Buf_clear() //把緩沖區(qū)的下標清零
{
RCREG_read_total =0;
RCREG_total=0;
}
unsigned char asy_recieve() //把串口緩沖區(qū)的數(shù)據(jù)一個個提取出來
{
unsigned char RCREG_dt;
++RCREG_read_total; //已經(jīng)讀出了多少個數(shù)據(jù)
RCREG_dt=RCREG_buf[RCREG_read_total -1];
if(RCREG_read_total >=RCREG_total) //只要把全部數(shù)據(jù)都讀完,馬上把緩沖區(qū)清零
{
RCREG_read_total =0;
RCREG_total=0;
}
return RCREG_dt;
}
void eusart_send(unsigned char t_data) //串口發(fā)送一個字節(jié)的數(shù)據(jù)
{
unsigned int error_delay;
Rede_dr=1; //流控IO,切換至發(fā)送數(shù)據(jù)的狀態(tài),準備發(fā)送數(shù)據(jù),
//以下幾個空延時是必要的。
asm("nop");
asm("nop");
asm("nop");
asm("nop");
TXREG=t_data; //發(fā)送數(shù)據(jù)
error_delay=0;//等待把數(shù)據(jù)發(fā)送完畢
while(1) //此處也可以直接用延時替代.
{
CLRWDT();
if(TXIF==1) //等待把數(shù)據(jù)發(fā)送完畢
{
break;
}
Else
{
++error_delay;
if(error_delay>200) //超時也要退出,不能死等
{
break;
}
}
}
Delay11(1); //此處最玄機,要特別注意。每發(fā)送完一個字節(jié),由于不同的項目,這//里的延時間隔都不一樣,讀者根據(jù)實際情況來改。這里最容易出問題,必須要延時,尤其是連續(xù)發(fā)送一堆數(shù)據(jù)的時候.讀者也可以改成計數(shù)延時的方式,有興趣的自己動腦筋,程序框架需要改動一點。
Rede_dr=0; //流控IO,發(fā)送完數(shù)據(jù)之后,務必馬上把從機切換回接收數(shù)據(jù)的狀態(tài),把總//線釋放
}
//延時函數(shù)
void Delay11(unsigned int MS)
{
unsigned char us,usn;
while(MS!=0) //for 12M
{
CLRWDT();
usn = 2;
while(usn!=0)
{ CLRWDT();
us=0xf5;
while (us!=0){us--;};
usn--;
}
MS--;
}
}
(6)小結(jié):
485跟TTL與232通訊唯一的編程區(qū)別就是多增加一個流控IO(本文是rede_dr)。此IO除了在發(fā)送數(shù)據(jù)的時候為高電平,發(fā)送完數(shù)據(jù)之后,其他任意時刻都必須為低電平,起到釋放總線的作用。略微要注意的細節(jié)是,在流控IO從低電平切換到高電平的時候,不要馬上發(fā)送數(shù)據(jù),中間有必要加幾個空延時指令(asm(“nop”);)。
(未完待續(xù),下節(jié)更精彩,不要走開哦)
|