1簡述,modbus是一種工業用的多設備之間的主從通信協議。只要兩臺設備之間,是采用modbus協議的主從關系,并連接到相同網絡,即可互相通信。因為Modbus只是協議,而且只規定了數據幀,底層連接,可以是232,485或者以太網。設備一般采用232和485進行通信,因為成本低。當然要是考慮遠距離傳輸和多賣錢的話,也會采用以太網,不過應該就會相應復雜一些了。
2模式,modbus有兩種模式,一種叫RTU模式,另一種叫acsii模式,RTU模式是純二進制的,而acsii模式,一個信息中的每8位字節作為2個ascii字符傳輸的,這種模式的主要優點時允許字符之間的時間間隔長達1秒,也不會出現錯誤。而較acsii模式,RTU模式的優點是用最少的字節,表達更多的內容。但同時也要求設備必須連續傳輸。
3通訊,modbus屬于主從通訊,可以是一主一從或者一主多從。通訊的方式為主機向從機發送命令(或者叫請求)從機向主機發送響應。主機不發送,從機不返回,一發,一收,不發不收。而且一個時間,只有一個機器發送請求或者響應,否則的話,則會出錯。
4信息幀,由于項目上沒有涉及到acsii模式,所以本文只討論RTU模式,不討論acsii模式,以后如果要是用的上,肯定會繼續討論。用不上,就不討論了。RTU幀,開始時,必須要有3.5個靜止的時間,也就是時間間隔,用來區分上一幀和下一幀,如果沒有時間間隔的話,則會分辨不出哪里是幀開始,哪里是幀結束了。3.5個時間間隔依據波特率不同而不同。同樣,結束時也需要時間。除了時間以外,還有地址,功能碼,數據,crc校驗四個部分,每個部分的字節數不同,地址功能碼各1個字節,crc是2個字節其完整表達如下:
開始
|
地址
|
功能
|
數據
|
校驗
|
結束
|
3.5t
|
1字節 8b
|
1字節 8b
|
n字節 n*8b
|
2字節16b
|
3.5t
|
4.1、地址:主要用于區分從機,在下位機程序中,的宏定義中設置不同的從機地址。
#define Modbus_addr 0x01
設備響應時,第一位也是本機地址。地址的范圍是從0-247,地址0為廣播地址,所有機器均可以識別。
4.2、功能碼:表示主機要命令這個設備的什么功能,執行什么程序。我看了一下正規的modbus的功能碼多達24個,不同廠家生產的不同型號的設備,可能會支持不同的功能碼,所以買之前需要注意一下。具體功能如下:
01 讀線圈狀態 02 讀輸入狀態 03 讀保持寄存器 04 讀輸入寄存器 05 強制單個線圈
06 預置單個寄存器 07 讀不正常狀態 08 診斷 09 程序484 10 查詢484
11 通訊事件控制 12 通訊事件記錄 13 程序控制器 14 查詢控制器 15 強制多個寄存器
16 預置多個寄存器 17 報告從機id 18 程序884/M84 19 通訊鏈路復位 20 讀通用參考值
21 寫通用參考值 22 Mask Write 4X Register 23 Read/Write 4X Registers 24 Read FIFO 隊列
雖然看著功能很多,但實際上有用的,只有01 02 03 04 05 06 15 和16功能碼。
4.3、數據區,根據功能碼的不同數據的長度是不同的。
4.4、crc校驗 包含兩個字節,發送端發送時,一幀的所有數據統一計算出一個crc校驗碼,然后加在一幀的最后兩位中,然后等到發送到接收端時接收端重新計算一次除最后兩位的一幀所有數據,然后根據兩個數據的對比,來判斷接收到的數據是否正確。
5、程序,以下位機為程序對象,主要使用c語言編寫,首先,先從變量入手,既然modbus接受以幀為單位,所以就要設置兩個緩沖區,用來接收數據,我們這里使用數組來存儲接收來的數據Modbus_send_buf[Modbus_max_send_buf];//數據發送緩沖 和 Modbus_recevie_buf[Modbus_max_recevie_buf];//數據接收緩沖 ,其中Modbus_max_send_buf,和Modbus_max_recevie_buf ,為宏定義,這樣可以方便的修改一幀最大的存儲數據。有了發送接收緩沖,就可以寫中斷函數了,進入中斷后,首先做一些必要的工作,清ES ,判斷IR,清IR,做完后,就可以開始接收數據了,但有個問題?如果設備處于空閑狀態,那么接收數據后按命令執行,但如果當設備正在執行指令的時候,則不應該再繼續的接收指令,那樣的話,會讓程序進入混亂狀態。所以要在基礎工作做完后,增加一個判斷,來確定設備的忙閑。if((Modbus_cmd_flag == 0) && (Modbus_exe_flag == 0)),判斷完以后就可以繼續下面的工作了。如果通訊中包含奇偶校驗的話,那么則判斷奇偶校驗。下面就是接收數據。Modbus_recevie_buf[Modbus_recevie_count] = SBUF; ,將接收來的數據存入數組并記錄存入的數據個數Modbus_recevie_count,由于modbus是通過時間來判斷一幀的結束的,所以在程序中,必須要有一個定時器函數,這個定時器用來判斷程序是正在接受,還是已經接受完成了。所以中斷的最后所做的是計數器自加Modbus_recevie_count++;,定時器清0 Modbus_timeout_cnt = 0; ,將設備狀態轉入接收狀態Modbus_recevie_flag = 1;。此時,串口中斷的工作就完成了。
下面開始分析定時器,定時器的目的其實就1個,判斷一幀是否接收完畢,如果完畢,則進入下一步。在定時器中斷函數中,首先要對定時器值進行初始化,這個就不多說了,然后是判斷程序是否處于接受狀態if(Modbus_recevie_flag == 1),這個狀態只有在串口中斷函數中才會被置位,其他的情況不會被置位。若程序不是接收狀態,則直接跳出定時器中斷,若程序處于接收狀態,則定時計數自加Modbus_timeout_cnt++;,自加后進入判斷if(Modbus_timeout_cnt >= Modbus_max_timeout_cnt),判斷的值即為modbus接收一幀傳輸完成所需要的時間間隔。至于是多少時間,可以通過修改Modbus_max_timeout_cnt來確定?梢詫⒍〞r器終端設置為1ms1次,在9600的情況下將超時時間設為4,#define Modbus_max_timeout_cnt 4,這樣如果串口中斷不在接收數據時,定時計數將不會清0,當到達設定的超時時間后即判斷接收結束,轉向命令解析狀態。
接收來的數據可以經過一個函數來執行,同時也可以經過兩個函數,解析與執行兩步來分別執行。我喜歡后者,因為這樣可以把解析的過程和執行的過程分開來寫。程序顯得更加清晰與明朗。
在主函數中就執行1個函數,
while(1)
{
Modbus_proc();
}
這個函數是經過打包的兩個函數,進入這個函數
void Modbus_proc()
{
Modbus_cmd();
Modbus_exe();
}
可以看到,程序分為cmd解析,exe執行。
Cmd 命令解析函數
有這么幾個問題是需要判斷的,命令解析狀態,接收來的數據個數,crc,地址,這幾個問題是命令解析時需要注意的,順序可以稍做變化。但最好是這個順序。
首先判斷程序是否處于命令解析狀態if(Modbus_cmd_flag == 1)。命令解析狀態標志只有在超時后置位,其他情況下不置位。之后是判斷接收數據是否大于4字節,if(Modbus_recevie_count > 4)。當程序接收數據小于4字節則說明接收發生錯誤,拋棄它。下一步則是判斷crc校驗,由于crc在一幀的最后兩位,所以crc應該取緩沖的最后兩位
modbus_crc_h=Modbus_recevie_buf[Modbus_recevie_count-2];
modbus_crc_l = Modbus_recevie_buf[Modbus_recevie_count-1];
然后將取來的數據合并成一個16位數據,得到接收的crc
modbus_crc = ((unsigned int)(modbus_crc_h) << 8) | modbus_crc_l;
重新計算1幀的crc,得到自己的crc
modbus_crc_b = crc16(Modbus_recevie_buf,Modbus_recevie_count - 2);
最后進行對比,將自己算的crc和接收的crc進行比較,來判斷接收的數據是否正確。
if( modbus_crc_b == modbus_crc )
在crc判斷正確后,就可以判斷地址了
if(Modbus_recevie_buf[0] == Modbus_addr) // Modbus_addr為一個宏定義的本機地址,若多機可以在此處修改。
當地址,crc,等全判斷正確以后,就可以判斷最重要的功能碼了。由于功能碼很多,所以1可以用宏定義來定義功能碼增加程序的可讀性,2可以利用switch來命令的模式
#define Modbus_read_coil 0x01 //功能碼01 讀可讀寫數字量寄存器(線圈狀態):
switch (Modbus_recevie_buf[1])
{
case Modbus_read_coil:
Modbus_mode = Modbus_read_coil;
break;
……
default: //非法命令準備報異常
return ;
break;
}
Modbus_exe_flag = 1;
解析后,將執行標志置位即可。
Exe 執行函數,
執行函數在解析函數后面,而不是在里面,所以,若沒有解析,照樣可以進入執行函數,但由于執行函數中有判斷執行標志位if( modbus_crc_b == modbus_crc ),所以若標志為0,則直接退出函數。若標志為1,則執行Modbus_mode中對應的函數函數中依然用switch來選擇具體功能函數
switch(Modbus_mode) //通過判斷模式來進行對響應的發送
{
case Modbus_read_coil:
read_coil_proc();
break;
……
default:
return;
break;
}
這樣的做的話,就可以吧解析函數,執行函數和具體的實施函數分開來弄,層次多多少少要清晰一些
下面就是針對01,02,03,04,05,06,15,16幾個功能碼的執行及返回進行說明
在說明各功能函數之前,先說說響應。
上面說的那兩個函數只不過是對一幀的外圍進行解析與判斷,至于具體的參數,還需要功能函數去解析與返回,功能函數要做的事情有3個,1個是參數的解析,2是執行,3是返回響應。
先說響應,響應是有特點的,第一個字節肯定是自己的本機地址,第二個字節肯定是功能碼,最后兩個字節肯定是crc校驗,所以說,在發送緩沖中,基本上4個字節已經定死了
Modbus_send_buf[0] = Modbus_addr;
Modbus_send_buf[1] = Modbus_read_input_reg; //相應的功能碼,每個功能寒暑都不一樣
再經過執行函數最后算crc
modbus_crc = crc16(Modbus_send_buf,temp); //計算發送crc數據
Modbus_send_buf[temp] = modbus_crc >> 8; //計算
temp++;
Modbus_send_buf[temp] = modbus_crc & 0xff; //return num 高位
5.1 01 讀線圈狀態
#define Modbus_read_coil 0x01
其實表面上挺難理解的,啥線圈啥的,但你仔細看看就可以了解,就是讀輸出數字量,如果你寫下位機的話,其實就是控制讀取輸出io,說白了,就是把目前的io輸出狀態返回給主機。這些io連接的可能是繼電器,也可能是一些開關之類的東西,也就是些數字信號。讀數字輸出信號。
計算機發送命令:[設備地址] [命令號01] [起始寄存器地址高8位] [低8位] [讀取的寄存器數高8位] [低8位]
設備響應:[設備地址] [命令號01] [返回的字節個數][數據1][數據2]...[數據n][CRC校驗的低8位] [CRC校驗的高8位]
簡單的說就是返回所有的輸出io的值,放在一個或者幾個字節里,可以用判斷的方法來實現,當然,也可以用與或的方式實現。
if(P1_0 == 1)
{
temp |= (1<<8);
}
else
{
temp &= (1<<8);
}
將temp的值放入第四個緩沖區,當然這根據設備的io口,編程時就已經確定了的。接下來就可以進行crc計算了。最后發送即可。
Modbus_send_buf[3] = temp;
modbus_crc = crc16(Modbus_send_buf,4);
Modbus_send_buf[4] = modbus_crc >> 8;
Modbus_send_buf[5] = modbus_crc & 0xff; //return num 高位
5.2 02 讀只可讀數字量寄存器(輸入狀態)
基本上和01意思差不多,只不過這個功能碼返回的數據是輸入io的數據,和01的區別是01可讀可改,而02只可讀不可改。也就是輸入的狀態。數據不可由設備本身控制。程序方面和01程序一樣。
5.3 03讀可讀寫模擬量寄存器(保持寄存器)
說簡單點就是讀da,da屬于模擬量,也可以輸出,但是以模擬量的方式來進行傳輸的
計算機發送命令:[設備地址] [命令號03] [起始寄存器地址高8位] [低8位] [讀取的寄存器數高8位] [低8位] [CRC校驗的低8位] [CRC校驗的高8位]
設備響應:[設備地址] [命令號03] [返回的字節個數][數據1][數據2]...[數據n][CRC校驗的低8位] [CRC校驗的高8位]
其中返回字節個數,為讀取寄存器數乘2
寫程序時,首先要注意數據個數,temp = Modbus_recevie_buf[5];一般寄存器個數不會超過255,個數取讀取寄存器個數的低八位即可。返回即乘2,temp = temp << 1;,下面要做的就是一個循環for(i = 0;i < temp ; i += 2),把需要的數據放入發送數組。其內容是
Modbus_send_buf[i+3]=(data_v&0xff00)>>8;
Modbus_send_buf[i+4]=data_v&0x0ff;
由于幀的前面3個是地址,功能碼,和返回字節個數,所以循環從第四個數據開始存放。data_v為讀取的數據,在程序中還需要其他語句配合。比如:data_v = updateValue();
循環后就可以進入crc校驗了可以利用返回字節數來確定crc的校驗個數temp = temp + 3;,最后計算發送字節的個數
send_cnt = Modbus_recevie_buf[5]*2 + 5 ; //數據發送個數 數據+地址+命令+返回數據個數+crc低+crc高
最后將數據發送出去即可。
5.4 04讀只可讀模擬量寄存器(輸入寄存器)
和03的區別是04就是讀ad,ad輸入輸入模擬兩,只能讀,不能改,同樣也是以模擬兩的方式來進行傳輸的。其程序 與03類似
5.5 05寫數字量(線圈狀態)
05則是修改io口輸出狀態,數字量輸出。
計算機發送命令:[設備地址] [命令號05] [需下置的寄存器地址高8位] [低8位] [下置的數據高8位] [低8位] [CRC校驗的低8位] [CRC校驗的高8位]
設備響應:若執行成功,則原樣返回
寫程序時,首先確定需要修改的io口,然后根據0xff00或0x0000來置位或清零該數據位。執行完成后,將接收到的數據重新發送即可 Uart0_senddata(Modbus_recevie_buf,8);
5.6 06寫單個模擬量寄存器(保持寄存器)
06為修改設備da數據,模擬量傳輸數據。
計算機發送命令:[設備地址] [命令號06] [需下置的寄存器地址高8位] [低8位] [下置的數據高8位] [低8位] [CRC校驗的低8位] [CRC校驗的高8位]
設備響應:若執行成功,原樣返回即可
5.7 16主機設置寄存器
簡單的說,就是一次設置多個da,以一個偏移量為準,一次設置多個輸出模擬里量
計算機發送命令:[設備地址] [命令號10] [開始地址高8位] [低8位] [寄存器個數高8位] [低8位] [第一個寄存器數據高][第一個寄存器數據低][第二個寄存器數據高][第二個寄存器數據低]……[CRC校驗的低8位] [CRC校驗的高8位]
命令響應:功能碼[0x10],寄存器起始地址高字節,低字節,要寫的寄存器數量的高字節,低字節,CRC校驗低字節,高字節
在程序中,首先要獲取寄存器個數
num = Modbus_recevie_buf[6] - 2;
然后進入循環,一次把寄存器數據提取出來for(i = 0; i < num; i = i + 2)
在循環的內部提取數據temp = (((unsigned int)(Modbus_recevie_buf[i+7])<<8)|(Modbus_recevie_buf[i+8]));
以上就是我在項目中涉及到的一點modbus的通訊的下位機程序,不全,但總體的思路,接收數據并解析,解析后提取數據在設備上加載或采集,然后再按照響應的方式發送回去。
下回改進的方向,1,增加功能碼2,增加宏定義及編譯定義,3增加單片主機的程序,和pc主從機的程序。4,增加ascii的程序,和rtu同時設置。Pc機程序,采用c#號編寫。
完整的程序請參考:http://www.raoushi.com/bbs/dpj-23230-1.html