文章目录
- Modbus起源
- 1.起源:
- 2.分类:
- 3.优势:
- 4.应用场景:
- 5.ModbusTCP特点:
- Modbus TCP协议格式
- 1.报文头
- 2.寄存器
- 3.功能码
- 工具软件使用
- .Modbus Slave&Poll
- 2.网络调试助手
- .Wireshark的使用
- Modbus RTU
- 1、与Modbus TCP的区别
- 2、Modbus RTU特点
- 3.ModbusRTU协议格式:
- 4.报文详解:
- 5.模拟器的使用
- 一、虚拟串口的安装
- 二、虚拟机绑定端口
- 三、测试通信
- 四、将Modbus Slave模拟器作为RTU设备的从机
- Modbus库
- 【1】库的安装
- 1.库的安装配置
- 2.库的使用
- 【2】函数接口
- 【3】编程流程
- 基于Modbus的工业数据采集项目
- 1.Http简介
- 2.Http特点
- 3.Http协议格式
- 客户端请求消息格式
- a. 请求行:
- b. 请求头:
- c. 空行
- d. 请求体
- WebServer
- 1.服务器源码分析:
- 2.安装使用postman
- 3.结合Modbus部分整体流程分析
Modbus起源
1.起源:
Modbus由Modicon公司于1979年开发,是一种工业现场总线协议标准。
Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本,其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种
其中Modbus TCP是在施耐德收购Modicon后1997年发布的。
2.分类:
1)Modbus RTU:
运行在串口上的协议,采用二进制表现形式以及紧凑型数据结构,通信效率高,应用广泛
2)Modbus ASCII:
运行在串口上的协议,采用ASCII码传输,并且利用特殊字符作为其字节的开始与结束标识,其传输效率要远远低于Modbus RTU协议,一般只有在通信数据量较小的情况下才考虑使用Modbus ASCII通信协议
3)Modbus TCP:
运行在以太网上的协议
3.优势:
免费、简单、容易使用
4.应用场景:
Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备
5.ModbusTCP特点:
1.采用主从问答方式进行通信
2.ModbusTCP是应用层协议,基于传输层TCP进行传输的
3.ModbusTCP端口号默认为502
Modbus TCP协议格式
ModbusTcp协议包含三部分:报文头、功能码、数据
Modbus TCP/IP协议最大数据帧长度为260字节
1.报文头
包含7个字节,分别是:
2.寄存器
包含四种:离散量输入、线圈、输入寄存器、保持寄存器
1)离散量和线圈其实就是位寄存器(每个寄存器数据占1字节),工业上主要用于控制IO设备。
线圈寄存器,类比为开关量,每一个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。
对应上面的功能码也就是:0x01 0x05 0x0f
离散输入寄存器,离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。
所以功能码也简单就一个读的 0x02
2)输入和保持寄存器是字寄存器(每个寄存器数据占2个字节),工业上主要用于存储工业设备的值。
保持寄存器,这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写
所以功能码有对应的三个:0x03 0x06 0x10
输入寄存器,这个和保持寄存器类似,但是也是只支持读而不能写。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值
对应的功能码也就一个 0x04
3.功能码
具体协议分析可参考:
http://www.360doc.com/content/20/0804/12/43769266_928452485.shtml
读数据:
主机->从机:报文头(7) + 功能码(1) + 起始地址(2)+ 数量(2)
从机->主机:报文头(7) + 功能码(1) + 字节计数(1)+ 数据(?)
写单个:
主机->从机:报文头(7) + 功能码(1) + 地址(2)+ 数据/断通标志(2)
写多个:
主机->从机:报文头(7) + 功能码(1) + 起始地址(2)+ 数量(2)+字节计数(1)+数据(?)
练习:读传感器数据,读1个寄存器数据,写出主从数据收发协议。
主机给从机:
|-事务处理标识符|-协议类型-|-字节长度-|-从机ID-|-功能码-|-起始地址-|-寄存器个数-|
0x0000 0x0000 0x0006 0x01 0x03 0x0000 0x0001
|-事务处理标识符|-协议类型-|-字节长度-|-从机ID-|-功能码-|-数据长度-|-寄存器数据-|
0x0000 0x0000 0x0005 0x01 0x03 0x02 0x0102
工具软件使用
.Modbus Slave&Poll
1)软件默认安装
2)破解
点击connection->connect,输入序列号即可
3)使用
先设置
后连接(连接时注意先开启slave端(相当于服务器),后起poll端(相当于客户端))
查询windows IP:
win r 输入cmd
输入 ipconfig
2.网络调试助手
.Wireshark的使用
捕获器选择:
windows如果连接有线网络,选择本地连接/以太网
如果连接无线网络,选择WLAN
如果只是在本机上的通信,选择NPCAP Loopback apdater
或Adapter for loopback traffic capture
过滤条件:
过滤端口:tcp.port==502
过滤IP:ip.addr == 192.168.3.11(windows 的ip)
Modbus RTU
1、与Modbus TCP的区别
在一般工业场景使用modbus RTU的场景还是更多一些,modbus RTU基于串行协议进行收发数据,包括RS232/485等工业总线协议。
与modbus TCP不同的是RTU没有报文头MBAP字段,但是在尾部增加了两个CRC检验字节(CRC16),因为网络协议中自带校验,所以在TCP协议中不需要使用CRC校验码。
RTU和TCP的总体使用方法基本一致,只是在创建modbus对象时有所不同,TCP需要传入网络socket信息;而RTU需要传入串口相关信息。
2、Modbus RTU特点
ModbusRTU也是主从问答协议,由主机发起,一问一答
设置串口参数:
设置串口参数时要求:
波特率为9600
8位数据位
1位停止位
无流控
3.ModbusRTU协议格式:
ModbusRTU协议数据帧包含四部分:地址码、功能码、数据、校验码
地址码:从机ID 1-247
功能码:同ModbusTCP协议
数据:起始地址、数量、数据
校验码:2字节,对地址码、功能码、数据部分进行校验,调用函数自动生成
4.报文详解:
03功能码:
主机->从机: 01 03 00 00 00 01 84 0A
01:从机ID
03:功能码
00 00:起始地址
00 01:数量
84 0A:校验码
从机->主机:01 03 02 00 14 b8 44
1.01:从机ID
03:功能码
02: 字节计数
00 14:数据
b8 44 :校验码
参考示例:
https://blog.csdn.net/qq153471503/article/details/124317894
5.模拟器的使用
由于实际硬件产品成本较高,我们这里可以使用Modbus软件模拟器,进行数据模拟从而分析Modbus协议。
使用工具:
- ModbusPoll(模拟主机)和ModbusSlave(模拟从机)
- vspd虚拟串口
- UartAssist串口调试工具
设置串口参数要求:波特率为9600 8位数据位 1位停止位 无流控 无校验
虚拟串口的使用:
一、虚拟串口的安装
1.将压缩包解压后,双击vspd.exe文件进行安装
2.安装完成后,找到安装目录,将Cracked下的文件复制到软件安装目录
3.打开软件,添加com1和com2端口(用完记得删除端口)
4.添加完端口后,打开设备管理器,这里出现如下图所示即可。
二、虚拟机绑定端口
- 将虚拟机在系统关机(必须是关机状态,挂起不行)状态下,点击虚拟机->设置->硬件->添加串行端口,添加COM1
2.添加完成后,第一次使用需要将电脑重启
3.重启之后,开启虚拟机,点击虚拟机->可移动设备->串行端口->连接
4.当连接上虚拟串口后,在终端输入dmesg | grep tty,可以查看到对应的设备文件,其中默认的会有ttyS0文件,剩下的就是虚拟串口对应的设备文件
三、测试通信
1.Windows打开串口调试工具,选择好串口COM2->COM1,设置对应的波特率
2.在虚拟机运行minicom
在虚拟机安装minicom软件
sudo apt-get install minicom
在终端执行sudo minicom -s
1)选择serial port setup,回车
2)设置设备文件,波特率,关闭流控,按如下图设置(文件改成自己的)
3)修改完成后,回车,保存修改,选择save setup as dfl,敲回车,再次选择exit回车
4)退出后就可以和windows下的串口调试工具进行通信测试
5)也可以在这个界面输入字符,查看串口助手的显示情况。
6)退出:ctrl+A、Z,在弹出的界面里输入X 回车,即可退出。
四、将Modbus Slave模拟器作为RTU设备的从机
虚拟机绑定COM1端口,slave连接COM2端口,虚拟机通过编程测试串口通信
Modbus Slave端的配置如下:
Modbus库
【1】库的安装
1.库的安装配置
1.在linux中解压压缩包
tar -xvf libmodbus-3.1.7.tar.gz
2.进入源码目录,创建文件夹(存放头文件、库文件)
cd libmodbus-3.1.7
mkdir install
3.执行脚本configure,进行安装配置(指定安装目录)
./configure --prefix=$PWD/install
4.执行make和make install
make//编译
make install//安装
执行完成后会在install文件夹下生产对应的头文件、库文件件夹。install用于存放产生的头文件、库文件等
2.库的使用
要想编译方便,可以将头文件和库文件放到系统路径下
sudo cp install/include/modbus/.h /usr/include
sudo cp install/lib/ -r /lib -d
后期编译时,可以直接gcc xx.c -lmodbus
头文件默认搜索路径:/usr/include 、/usr/local/include
库文件默认搜索路径:/lib、/usr/lib
【2】函数接口
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 :得到的状态值
int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest)
功能:读取输入状态,可读取多个连续输入的状态(对应功能码为0x02)
参数:ctx :Modbus实例addr :寄存器起始地址nb :寄存器个数dest :得到的状态值
返回值:成功:返回nb的值
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:线圈状态
返回值:成功:0失败:-1
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);
功能:写入多个连续线圈的状态(对应功能码为15)
参数:ctx :Modbus实例addr :线圈地址nb :线圈个数src :多个线圈状态
返回值:成功:0失败:-1
int modbus_write_register(modbus_t *ctx, int addr, int value);
功能: 写入单个寄存器(对应功能码为0x06)
参数: ctx :Modbus实例addr :寄存器地址value :寄存器的值
返回值:成功:0失败:-1
int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);
功能:写入多个连续寄存器(对应功能码为16)
参数:ctx :Modbus实例addr :寄存器地址nb :寄存器的个数src :多个寄存器的值
返回值:成功:0失败:-1
【3】编程流程
1.创建实例
modbus_new_tcp
2.设置从机ID
modbus_set_slave
3.连接
modbus_connect
4.读写操作
调用功能码对应函数
5.关闭套接字
modbus_close
6.释放示例
modbus_free
基于Modbus的工业数据采集项目
1.Http简介
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于Web Browser(浏览器)到Web Server(服务器)进行数据交互的传输协议。
HTTP是应用层协议
HTTP是一个基于TCP通信协议传输来传递数据(HTML 文件, 图片文件, 查询结果等)
HTTP协议工作于B/S架构上,浏览器作为HTTP客户端通过URL主动向HTTP服务端即WEB服务器发送所有请求,Web服务器根据接收到的请求后,向客户端发送响应信息。
HTTP默认端口号为80,但是你也可以改为8080或者其他端口
2.Http特点
HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
3.Http协议格式
客户端请求消息格式
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
a. 请求行:
请求行是由请求方法字段、url字段、http协议版本字段3个部分组成。请求行定义了本次请求的方式,格式如下:GET /example.html HTTP/1.1(CRLF)。
http的请求方式:
http协议中共定义了八种数据的请求方法。分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT;我们在实际应用中常用的也就是 get 和 post,其他请求方式也都可以通过这两种方式间接的来实现。
(增POST 删DELETE 改PUT 查GET)
GET方法和POST方法的区别?
GET通常用来从服务器上获得数据,而非修改信息;POST用来向服务器传递数据。
1.请求数据带参数,;GET请求的数据会附加在URL之后,以?分割URL和传输数据,多个参数用&连接。POST请求会把请求的数据放置在HTTP请求包的包体中。
因此,GET请求的数据会暴露在地址栏中,而POST请求则不会。
2.传输数据的大小;在HTTP规范中,没有对URL的长度和传输的数据大小进行限制。但是在实际开发过程中,对于GET,特定的浏览器和服务器对URL的长度有限制。因此,在使用GET请求时,传输数据会受到URL长度的限制。对于POST,由于不是URL传值,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,Apache、IIS都有各自的配置
3.GET请求返回的内容可以被浏览器缓存起来。而每次提交的POST,浏览器在你按 下F5的时候会跳出确认框,浏览器不会缓存POST请求返回的内容
4.GET对数据进行查询,POST主要对数据进行增删改!简单说,GET是只读,POST是写
5.对于参数的数据类型,get只接受ASCII字符,而post没有限制。
b. 请求头:
也被称作消息报头,请求头是由一些键值对组成,每行一对,关键字和值用英文冒号“:”分隔。允许客户端向服务器发送一些附加信息或者客户端自身的信息,典型的请求头如下:
Accept:作用:描述客户端希望接收的 响应body 数据类型;示例:Accept:text/html
Accept-Charset:作用:浏览器可以接受的字符编码集;示例:Accept-Charset:utf-8
Accept-Language:作用:浏览器可接受的语言;示例:Accept-Language:en
Connection:作用:表示是否需要持久连接,注意HTTP1.1默认进行持久连接;示例:Connection:close
Content-Length:作用:请求的内容长度:示例:Content-Length:348
Content-Type:作用:描述客户端发送的 body 数据类型
c. 空行
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。
d. 请求体
请求数据:请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。
WebServer
简单TCP服务器示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>#define PORT 8080
#define BUFFER_SIZE 1024void handle_request(int client_socket) {char buffer[BUFFER_SIZE];char response[] = "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<html><body><h1>Hello, World!</h1></body></html>";// 从客户端读取请求ssize_t bytes_read = read(client_socket, buffer, BUFFER_SIZE - 1);if (bytes_read == -1) {perror("读取请求失败");return;}buffer[bytes_read] = '\0';// 打印请求内容printf("收到请求:\n%s\n", buffer);// 发送响应给客户端ssize_t bytes_written = write(client_socket, response, strlen(response));if (bytes_written == -1) {perror("发送响应失败");}
}int main() {int server_socket, client_socket;struct sockaddr_in server_address, client_address;socklen_t client_address_len;//创建套接字if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("创建套接字失败");exit(1);}//设置地址重用int reuse = 1;if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {perror("设置地址重用失败");exit(1);}//初始化绑定地址server_address.sin_family = AF_INET;server_address.sin_port = htons(PORT);server_address.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {perror("绑定地址失败");exit(1);}//启动监听if (listen(server_socket, 10) == -1) {perror("启动监听失败");exit(1);}printf("服务器已启动,监听端口 %d\n", PORT);// 接受连接并处理请求while (1) {client_address_len = sizeof(client_address);if ((client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_len)) == -1) {perror("接受连接失败");continue;}printf("接受新连接\n");// 处理请求handle_request(client_socket);// 关闭客户端套接字close(client_socket);printf("连接已关闭\n");}// 关闭服务器套接字close(server_socket);return 0;
}
1.服务器源码分析:
1.初始化服务器
2.循环等等待连接,连接后创建线程,调用线程函数msg_request,再函数中调用
handler_msg函数分析请求
3.handler_msg函数中,先查看请求内容,其次获取请求方法、URL、参数,判断请求方法是什么,对need_handler赋值,确定请求资源路径,如果请求地址没有携带任何资源,则默认返回index.html文件,如果资源不存在,返回404,如果需要处理(get请求带参数,post请求)调用handle_request函数,如果不需要(get请求不带参数且资源存在),调用echo_www函数,直接返回资源
4.handle_request函数主要用来获取post请求正文数据,调用parse_and_process函数处理正文内容(需要自己额外添加函数)