思維導圖
Modbus RTU(先學一點理論)
概念
與Modbus TCP區別
與modbus TCP不同的是RTU沒有報文頭MBAP字段,但是在尾部增加了兩個CRC檢驗字節(CRC16),因為網絡協議中自帶校驗,所以在TCP協議中不需要使用CRC校驗碼。
RTU和TCP的總體使用方法基本一致,只是在創建modbus對象時有所不同,TCP需要傳入網絡socket信息;而RTU需要傳入串口相關信息。
特點
1.遵循主從問答的通信方式
2.采用串口的方式進行通信
設置串口參數時要求:(了解,后續還會用)
波特率為9600(波特率是指每秒鐘傳輸的比特數)
8位數據位 (數據位是指每個字符中包含的比特數)
1位停止位 (停止位是指在每個字符傳輸結束后添加的比特數)
無流控 (流控是指在數據傳輸過程中控制數據流量的一種機制,無流控表示在該設置下沒有額外的控制機制來控制數據流量)
modbus rtu協議格式
地址碼 功能碼 數據 校驗碼
地址碼(1字節):從機ID
功能碼(1字節):和modbus tcp一樣(01 02 03 04 05 06 0f 10H)
數據:起始地址、地址、數量、數據、字節計數;和modbus tcp一樣。
校驗碼(2字節):對地址碼、功能碼、數據進行校驗,由函數生成,循環冗余校驗 (低字節在前)
其實modbus rtu協議的格式和modbus tcp是很像,就是把tcp的MBAP報文頭去掉,只保留了一個字節的主機ID,最后結尾加上了兩個字節的校驗碼。(校驗碼沒有實際意義,是函數生成的不用管)
?以01發送的數據格式為例,可以看到數據位是一樣的,上圖就是tcp和rtu協議格式的區別。數據接收也是和tcp一樣的,所以就不再講了。
modbus 庫
官方文檔:libmodbus
1庫的安裝
第一步和第二步都要運行,第一步是為了安裝配置,第二步是為了讓你使用這個庫更方便,把它放在你的C語言庫里。?????
1.1庫的安裝配置(共四步)
通過網盤分享的文件:sqlite-autoconf-3460000.tar.gz
鏈接: https://pan.baidu.com/s/1ro8-xbsFitDSEEK6mSYzwQ?pwd=3521 提取碼: 3521(直接復制命令,別手打,按順序)
1. (先下載壓縮包,CtrlC+V復制到虛擬機任意路徑下)在linux中解壓壓縮包
tar -xvf libmodbus-3.1.7.tar.gz
2. 進入源碼目錄
cd libmodbus-3.1.7
3.創建文件夾(存放頭文件、庫文件)
mkdir install
4.執行腳本configure,進行安裝配置(指定安裝目錄)
./configure --prefix=$PWD/install
5. 執行make
make????????????????????????//編譯
6.執行make install
make install????????????????//安裝
執行完成后會在install文件夾下生產對應的頭文件、庫文件件夾install,用于存放產生的頭文件、庫文件等
1.2.庫的使用
要想編譯方便,可以將頭文件和庫文件放到系統路徑下(直接復制命令,別手打,按順序)
sudo cp include/modbus/*.h /usr/include
sudo cp lib/* -r /lib -d
后期編譯時,可以直接gcc xx.c -lmodbus(和編譯有關線程代碼一樣)
頭文件默認搜索路徑:/usr/include 、/usr/local/include(之前文章庫里的內容)
庫文件默認搜索路徑:/lib、/usr/lib
2.函數接口
在上面的官方文檔里包含所有的函數接口,以下是常用的modbus tcp函數接口,上個文章嘗試自己寫函數,這里就是使用這些別人寫好的庫函數(更方便)。
modbus_t* modbus_new_tcp(const char *ip, int port)
功能:以TCP方式創建Modbus實例,并初始化
參數:ip :ip地址
? ? ? ? ? ?port:端口號
返回值:成功:Modbus實例
失敗:NULL
int modbus_set_slave(modbus_t *ctx, int slave)
功能:設置從機ID
參數:ctx :Modbus實例
? ? ? ? ? ?slave:從機ID
返回值:成功:0
? ? ? ? ? ? ? ?失敗:-1
int modbus_connect(modbus_t *ctx)
功能:和從機(slave)建立連接
參數:ctx:Modbus實例
返回值:成功:0
???????????????失敗:-1
void modbus_free(modbus_t *ctx)
功能:釋放Modbus實例
參數:ctx:Modbus實例
void modbus_close(modbus_t *ctx)
功能:關閉套接字
參數:ctx:Modbus實例
int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:讀取線圈狀態,可讀取多個連續線圈的狀態(對應功能碼為0x01)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :寄存器起始地址
? ? ? ? ? ?nb :寄存器個數
? ? ???????dest :得到的狀態值
返回值:成功:讀到的數量
???????????????失敗:-1
int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:讀取輸入狀態,可讀取多個連續輸入的狀態(對應功能碼為0x02)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :寄存器起始地址
???????????nb :寄存器個數
???????????dest :得到的狀態值
返回值:成功:返回nb的值
? ? ? ? ? ? ? ?失敗:-1
int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:讀取保持寄存器的值,可讀取多個連續保持寄存器的值(對應功能碼為0x03)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :寄存器起始地址
? ? ? ? ? ?nb :寄存器個數
? ? ? ? ? ?dest :得到的寄存器的值
返回值:成功:讀到寄存器的個數
? ? ? ? ? ? ? ?失敗:-1
int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest)
功能:讀輸入寄存器的值,可讀取多個連續輸入寄存器的值(對應功能碼為0x04)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :寄存器起始地址
? ? ? ? ? ?nb :寄存器個數
? ? ? ? ? ?dest :得到的寄存器的值
返回值:成功:讀到寄存器的個數
? ? ? ? ? ? ? ?失敗:-1
int modbus_write_bit(modbus_t *ctx, int addr, int status);
功能:寫入單個線圈的狀態(對應功能碼為0x05)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :線圈地址
? ? ? ? ? ?status:線圈狀態
返回值:成功:1
? ? ? ? ? ? ? ?失敗:-1
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
功能:寫入多個連續線圈的狀態(對應功能碼為15)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :線圈地址
? ? ? ? ? ?nb :線圈個數
? ? ? ? ? ?src :多個線圈狀態
返回值:成功:寫入的數量
? ? ? ? ? ? ? ?失敗:-1
int modbus_write_register(modbus_t *ctx, int addr, int value);
功能: 寫入單個寄存器(對應功能碼為0x06)
參數: ctx :Modbus實例
? ? ? ? ? ? addr :寄存器地址
? ? ? ? ? ? value :寄存器的值
返回值:成功:1
? ? ? ? ? ? ? ?失敗:-1
int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);
功能:寫入多個連續寄存器(對應功能碼為16)
參數:ctx :Modbus實例
? ? ? ? ? ?addr :寄存器地址
? ? ? ? ? ?nb :寄存器的個數
? ? ? ? ? ?src :多個寄存器的值
返回值:成功:寫入的數量
? ? ? ? ? ? ? ?失敗:-1
有關讀取浮點的就不全舉例了,感興趣可以去查看官方文檔
float modbus_get_float_dcba(const uint16_t *src)
功能:讀取浮點類型的數據
參數:src:讀到數據的存放數組
返回值:轉換后的浮點類型
編程
從上往下寫就可以連接到Modbus Slave,ip要寫主機的IP地址,不要寫成虛擬機的IP地址。,你要是在虛擬機運行的Modbus Slave,那就可以寫虛擬機地址。
編程步驟
1.創建實例
2.設置從機ID
3.建立連接
4.寄存器操作(按需選擇)
5.關閉套接字
6.釋放實例
編程實現
1.基礎步驟實現:
#include <modbus.h>
#include <stdio.h>int main(int argc, char const *argv[])
{char ip[128] = {"192.168.50.224"}; //IPint port = 502; //端口號int slave = 1; //從機地址int addr = 0x0000; //寄存器地址int nb = 0x0002; //寄存器數uint16_t dest[12] = {0}; //接收數組// 創建實例// IP與端口號可作為命令行傳參modbus_t *modbus = modbus_new_tcp(ip, port);if (modbus == NULL){perror("err\n");return -1;}//設置從機IDmodbus_set_slave(modbus, slave); //建立連接int con = modbus_connect(modbus);if (con == -1){perror("con:\n");return -1;}//寄存器操作int read = modbus_read_registers(modbus, addr, nb, dest);if (read == -1){perror("read:\n");return -1;}for (int i = 0; i < read; i++){printf("%d ", dest[i]);}putchar(10);//關閉套接字modbus_close(modbus);//釋放實例modbus_free(modbus);return 0;
}
2.數據采集小項目:
編程實現采集傳感器數據和控制硬件設備(傳感器和硬件通過slave模擬)
傳感器:2個,光線傳感器、加速度傳感器(x\y\z)
硬件設備:2個,led燈、蜂鳴器
要求:
1.多任務編程:建議多線程
2.循環1s采集一次數據,并將數據打印至終端
3.同時從終端輸入指令控制硬件設備
????????0 1:led燈打開
????????0 0:led燈關閉
????????1 1:蜂鳴器開
????????1 0:蜂鳴器關
#include <stdio.h>
#include <modbus.h>
#include <unistd.h>
#include <pthread.h>void *handler1(void *arg){ //內不含阻塞,相當于后臺運行uint8_t dest1[32] = {0};uint16_t dest2[32] = {0};modbus_t *modbusid = (modbus_t *)arg;int size = 0;while(1){ //讀取線圈狀態size = modbus_read_bits(modbusid,0,2,dest1);if(size == -1){ //容錯判斷perror("modbus_read_registers err");break;}printf("LED:%02x 蜂鳴器:%02x\n",dest1[0],dest1[1]);//查詢寄存器數值size = modbus_read_registers(modbusid,0,2,dest2);if(size == -1){ //容錯判斷perror("modbus_read_registers err");break;}printf("溫度傳感器:%02x 加速度傳感器:%02x\n",dest2[0],dest2[1]);sleep(5); //5秒打印一次}
}void *handler2(void *arg){ //用于執行寫操作,需要輸入指令modbus_t *modbusid = (modbus_t *)arg;int addr,nb,status;int a = 0; //標志操作05,06while(1){scanf("%d",&a); //選擇操作if(a == 5){ //操作單個線圈scanf("%d %d",&addr,&status);modbus_write_bit(modbusid,addr,status);}else if(a == 6){ //操作單個寄存器scanf("%d %d",&addr,&nb);modbus_write_register(modbusid,addr,nb);}if(a == -1)break;}
}int main(int argc, const char *argv[])
{//創建實例modbus_t *modbusid = modbus_new_tcp("192.168.43.148",502);if(modbusid == NULL){perror("modbus_new_tcp err");return -1;}//設置從機IDint slave = 1;if(modbus_set_slave(modbusid,slave) == -1){perror("modbus_set_slave err");return -1;}//建立連接if(modbus_connect(modbusid) == -1){perror("modbus_connect err");return -1;}//寄存器操作pthread_t ptid1;pthread_create(&ptid1,NULL,handler1,modbusid); //創建第一個線程pthread_detach(ptid1);pthread_t ptid2;pthread_create(&ptid2,NULL,handler2,modbusid); //創建第二個線程pthread_join(ptid2,NULL);//關閉套接字modbus_close(modbusid);//釋放實例modbus_free(modbusid);return 0;
}
五秒打印一次,終端還可以輸入命令取改變寄存器和線圈值
?