modbus_tcp协议
modbus_rtu协议和modbus库
http协议和web服务器搭建
服务器原码分析和基于WebServer的工业数据采集项目
第H5,即网页制作,项目完善
一、modbus起源
1.起源
Modbus由Modicon公司于1979年开发,是一种工业现场总线协议标准
Modbus通信协议具有多个变种,其中有支持串口,以太网多个版本
其中最著名的是Modbus RTU、Modbus ASCII和Modbus TCP三种
其中Modbus TCP是在施耐德收购Modicon后1997年发布的
2. 分类
Modbus RTU
运行在串口上的协议
采用二进制表现形式以及紧凑的数据结构,通信效率较高,应用比较广泛
Modbus ASCII
运行在串口上的协议
采用ASCII码进行传输,并且每个字节的开始和结束都有特殊字符作为标志,传输效率远远低于Modbus RTU,一般只有通讯量比较少时才会考虑它。
注:在ASCII模式下,每个8位的字节被拆分成两个ASCII字符进行发送,比如十六进制0xAF(1010 1111),会被分解成ASCII字符“A”(0100 0001)和”F”(0100 0110)进行发送,其发送量显然比RTU增加一倍。
Modbus TCP
运行在以太网上的协议
3. 优势
免费、简单、容易使用
4. 应用场景
Modbus协议是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备。
5. ModbusTCP特点(掌握)
1)采用主从问答式通信
2)Modbus TCP是应用层协议,基于传输层的TCP进行通信的
3)Modbus TCP端口号默认502
注:更好的理解网络模型的分层特点:
各层之间独立,每一层不需要知道下一层如何实现
当任何一层发生变化时,只要层间接口关系保持不变,则这层以上或以下层不受影响。
二、ModbusTCP的协议格式
ModbusTcp协议包含三部分:报文头、功能码、数据
MBAP:Modbus Application Protocol (modbus报文头)
PDU:Protocol Data Unit(协议数据单元)
Modbus TCP/IP协议最大数据帧长度为260字节
1.报文头
包含7个字节
2. 寄存器
一共有四种类型的寄存器
线圈寄存器支持读也支持写,对应3个功能码
写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。
离散输入寄存器,离散输入寄存器就相当于线圈寄存器的只读模式,也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。对应功能码1个。
比如我读取外部按键的按下还是松开。
保持寄存器,这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。功能码有对应的三个。
比如空调检测到的室温,这是不可以修改的,因为室温是根据实际的物理环境决定的。
输入寄存器,这个和保持寄存器类似,但是只支持读而不能写。一个寄存器也是占据两个byte的空间,对应的功能码就一个。
类比通过读取输入寄存器获取现在的温度传感器的值
总结:
读都是可以一个功能码读多个或单个的。而写是分为写单个和写多个,需要两个功能码。
所以对于只读的寄存器都只有一个功能码,而可读可写的寄存器都有三个功能码。
3. 功能码
功能码占一个字节
读数据:
主机→从机
报文头----功能码---起始地址---数量
从机→主机
报文头----功能码---字节计数---数据
写单个:
主机--》从机
报文头---功能码---地址--- 通/断标志 /数据
从机--》主机
原文返回
写多个
主机-->从机
报文头---功能码--起始地址--数量--字节计数--数据
从机-->主机
报文头---功能码--起始地址--数量
主机询问数据流
00 00 00 00 00 06 0103 00 6300 02
03:功能码
0x0063:起始地址 6*16+3=99 ,40100
0002:个数 40100 40101
从机应答数据流
00 00 00 00 00 07 01 030402 1330 08
03:功能码
04:字节计数
0213 :40100数据
3008: 40101数据
练习:
1. 读传感器数据,读1个寄存器数据,写出主从数据收发协议。
00 00 00 00 00 06 11 0100 0100 01
00 00 00 00 00 04 11 01 01 00
2. 写出控制IO设备开关的协议数据,操作1个线圈,置1。
00 00 00 00 00 06 11 05 00 63 ff 00
00 00 00 00 00 06 11 0500 63 ff 00
三、工具软件的使用
1. Modbus Slave/Poll
1) 软件默认安装
2) 破解 点击connection->connect ,输入序列号即可
3) 使用 先设置从机 从机相当于服务器
C/S Client /Server
B/S Browser /Server
2. 网络调试助手
02 2B 555
3. wireshark的使用
4. 网络恢复
保证虚拟机关机
点击更改设置,如果此时你可以点还原默认设置,直接点还原默认设置
点击还原默认设置
等待即可
重置完后重启虚拟机
然后进入虚拟机
如何确认网络可以了
1. ping命令,ping www.baidu.com
如果ping通了则调试成功
2. 打开浏览器看一看能不能打开网页
3. ifconfig看一看网段和windows的网段一不一样
可以对多个从站地址进行排版
03功能码实现封装
05功能码实现
整体代码
//客户端架构
#include <stdio.h>
#include <stdlib.h> // atoi
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
int sockfd;
void set_slave_id(uint8_t*p,uint8_t id);
void read_registers(uint8_t *p,uint16_t addr,uint16_t nb,uint8_t* dest);
void write_coil(uint8_t *p,uint16_t addr,int op,uint8_t *dest);
int main(int argc, char const *argv[])
{
//1.创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
socklen_t len = sizeof(struct sockaddr_in);
struct sockaddr_in s;
//2.填充结构体
s.sin_family = AF_INET;
s.sin_port = htons(atoi(argv[2]));
s.sin_addr.s_addr = inet_addr(argv[1]);
//3.连接
if (connect(sockfd, (struct sockaddr *)&s, len) < 0)
{
perror("connect err");
return -1;
}
//实现03功能码
uint8_t buf[32]={0};
uint8_t data[32]={0};
set_slave_id(buf,0x02);
// read_registers(buf,0x0000,0x0002,data);
// // buf 00 00 00 00 00 06 01 03 00 00 00 02
// for (int i = 0; i < data[8]; i++)
// {
// printf("%02x ",data[9+i]);
// }
// printf("\n");
write_coil(buf,0x0000,0,data);
close(sockfd);
return 0;
}
//buf 00 00 00 00 00 06 xx 03 00 00 00 00
// buf 00 00 00 00 00 06 xx 03 xx xx xx xx
//设置从站地址
void set_slave_id(uint8_t*p,uint8_t id)
{
p[6]=id;
}
//03功能码 读保持寄存器 发送的数组地址 指定其起始地址 指定数量 接收地址
void read_registers(uint8_t *p,uint16_t addr,uint16_t nb,uint8_t* dest)
{
p[5]=0x06;
p[7]=0x03;
p[8]=addr >> 8; // 0000 0000 0000 0001
p[9]=addr ;
p[10]=nb >> 8;
p[11]=nb ;
//主机发送请求数据
send(sockfd,p,32,0);
//接收从机的回复
recv(sockfd,dest,32,0);
}
//05功能码函数
}
四、Modbus RTU
1.与Modbus TCP的区别
在一般工业场景使用modbus RTU的场景还是更多一些,modbus RTU基于串行协议进行收发数据,包括RS232/485等工业总线协议。
与modbus TCP不同的是RTU没有报文头MBAP字段,但是在尾部增加了两个CRC检验字节(CRC16),因为网络协议中自带校验,所以在ModbusTCP协议中不需要使用CRC校验码。
RTU和TCP的总体使用方法基本一致,只是在创建modbus对象时有所不同,TCP需要传入网络socket信息;而RTU需要传入串口相关信息。
2、 Modbus RTU特点
Modbus RTU也是主从问答协议,由主机发起,一问一答
设置串口参数:
波特率为9600,8位数据位,1位停止位,无流控
当两台设备进行串口通信,假如他们对数据的处理速度不同。如果接收端数据缓冲区已满,则此时继续发送来的数据就会丢失。使用流控机制时,当接收端数据处理能力饱和时,就发出“不再接收”的信号,发送端就停止发送,直到接收端处理能力释放,发送“可以继续发送”的信号给发送端时,发送端才继续发送数据;
3、 Modbus RTU协议格式
ModbusRTU数据帧包含:地址码 功能码 数据 校验码
地址码:从机ID
功能码:同TCP
数据: 起始地址
校验码: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 B4 44
01:地址码 从机地址
03:功能码
02:字节计数
00 14:数据
B4 44:校验码
五、模拟器的使用
由于实际硬件产品成本较高,我们这里可以使用Modbus软件模拟器,进行数据模拟从而分析Modbus协议。
使用工具:
1. ModbusPoll(模拟主机)和ModbusSlave(模拟从机)服务器
2. vspd虚拟串口
3. UartAssist串口调试工具
设置串口参数要求:波特率为9600 8位数据位 1位停止位 无流控 无校验
虚拟串口的使用:
一、 虚拟串口的安装
1. 将压缩包解压后,双击vspd.exe文件进行安装
2.安装完成后,找到安装目录,将Cracked下的文件复制到软件安装目录
3. 打开软件
添加COM1和COM2端口(用完之后记得删除端口)
4.添加完端口后,打开设备管理器,这里出现如下图所示即可。
或
二、虚拟机绑定端口
1. 将虚拟机在系统关机(必须是关机状态,挂起不行)状态下,点击虚拟机->设置->硬件->添加串行端口,添加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端的配置如下:
五、可能会遇到的问题
1. 虚拟串口完成主机与vmware下虚拟机进行串口通信
https://blog.csdn.net/baidu_33232390/article/details/54954931
2. VSPD虚拟串口工具——从此告别硬件串口调试
https://blog.csdn.net/qq_34202873/article/details/88391265
3. vmware虚拟机检测不到vspd虚拟串口问题
https://blog.csdn.net/readnap/article/details/116659657
实现03功能码
#include "Crc_Calc.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
//串口实现ModbusRtu 并完成03功能码某一功能
int main(int argc, char const *argv[])
{
//1.打开串口文件
//2.初始化串口
//3.创建要发送的03功能码数组
// 01 03 00 00 00 01
//4.根据创建的数组产生CRC校验,大小两个字节
//5.把生成的CRC校验码放到要发送的03功能码数组中
//6.往串口文件中写入拼好的03功能码数组
//7.接收从机回应--即往串口文件中读取信息
//8.打印从机回应
//9.关闭文件
return 0;
}
六、 Modbus库
库的安装
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,用于存放产生的头文件、库文件等
库的使用
要想编译方便,可以将头文件和库文件放到系统路径下
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
函数接口
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 :寄存器个数
失败:-1
编程流程
1. 创建实例
modbus_new_tcp
2. 设置从机ID
modbus_set_slave
3. 建立连接
modbus_connect
4. 寄存器操作
功能码所对应的函数
5. 关闭套接字
modbus_close
6. 释放实例
modbus_free
练习:通过库函数实现03功能码采集数据
注意:编译不要忘了链接库、查看网络是否能用,查看slave端协议是否正确,查看slave端是否有对应的寄存器类型,查看slave id是否一致
gcc modbus.c -lmodbus
完成以下任务:
1.任务:编程实现采集传感器数据和控制硬件设备(传感器和硬件通过slave模拟)
传感器:2个,光线传感器、加速度传感器
硬件设备:2个,led灯、蜂鸣器
要求:
1.多任务编程:多线程
2.循环1s采集一次数据,并将数据打印至终端
3.同时从终端输入指令控制硬件设备
0 1 :led灯打开
0 0:led灯关闭
1 1:蜂鸣器开
1 0 : 蜂鸣器关
记得链接库 -lmodbus -lpthread
基于WebServer的工业数据采集项目
一、 http简介
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于Web Browser(浏览器)到Web Server(服务器)进行数据交互的传输协议。
HTTP是应用层协议,基于TCP通信协议传输来传递数据(HTML 文件, 图片文件, 查询结果等)
HTTP协议工作于B/S架构上,浏览器作为HTTP客户端通过URL主动向HTTP服务端即WEB服务器发送所有请求,Web服务器根据接收到的请求后,向客户端发送响应信息。
HTTP默认端口号为80,但是你也可以改为8080或者其他端口
二、 http特点
HTTP是短连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
三、 Http协议格式
1)客户端请求消息格式
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
请求行是由请求方法字段、url字段、http协议版本字段3个部分组成。请求行定义了本次请求的方式,格式如下:GET /example.html HTTP/1.1(CRLF)。
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。
2)服务器响应消息格式
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
状态行:由三部分组成,HTTP协议的版本号、状态码、以及对状态码的文本描述。例如:HTTP/1.1 200 OK(CRLF) 。(200表示请求已经成功)
HTML
开发环境:vscode
可以在 www/root下面新建一个文件,例如:example.html
安装库open in browser
库安装完成后,在编写文本位置右击->open in other browser->选择合适的浏览器即可在网页显示html标签内容
输入html,选择html:5或者!回车可以将框架进行搭建
2. Html简介
HTML(英文Hyper Text Markup Language的缩写)中文译为“超文本标记语言”。是用来描述网页的一种语言。
所谓超文本,因为它可以加入图片、声音、动画、多媒体等内容,不仅如此,它还可以从一个文件跳转到另一个文件,与世界各地主机的文件连接。
HTML 不是一种编程语言,而是一种标记语言 (markup language)
Web 浏览器的作用是读取 HTML 文档,并以网页的形式显示出它们。浏览器不会显示 HTML 标签,而是使用标签来解释页面的内容
3.html标签
1.标签格式:
(1)有尖括号包围的关键字,如:<html>
(2)通常成对存在,如:<body> </body>
(3)上面的标签前面是开始标签,后面是结束标签
2.标签分类:
(1)单标签:也称空标签 <标签名 /> 如:<br/>
(2)双标签:成对存在 <标签名> 内容 </标签名>
常用标签:
1)h1-h6标题标签
格式:<hn> 标题文本 </hn>
举例:
<h1>这是标题标签</h1>
<h2>这是标题标签</h2>
<h3>这是标题标签</h3>
<h4>这是标题标签</h4>
<h5>这是标题标签</h5>
<h6>这是标题标签</h6>
2)p段落标签:
一个段落中会根据浏览器窗口的大小自动换行
格式:<p> 文本内容 </p>
3)br换行标签:
格式:<br />
4)div标签:
是一个块级元素,可以把文档分割为独立的、不同的部分,可以在div中嵌套标签
举例:
<div class="news">
<h2>News headline 1</h2>
<p>some text. some text. some text...</p>
</div>
注:可以给div设置class或id,通过选择器设置属性,则内部成员具有相同属性
5)Input表单标签:
表示输入意思,是单标签
格式:<input type=”” name=”” value=”” id=””.....>
属性有多种:
当type为radio,表示是单选框
用法:<input type=”radio” name=”控件名称” value=”提交的数值” οnclick=”处理函数” checked=”checked”>
解释:name:控件名称,同一组单选框设置相同名称
//Value:必须要有,是当点击时会提交的数据
Onclick:点击时会执行双引号中的处理函数
Checked:默认选中,同一组中只设置一个即可
用‘,’拆分响应的字符串
var rep=response.split(',');
600,700
rep[0]=600
rep[1]=700
WebServer
基础服务器
#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 80
#define BUFFER_SIZE 1024
void handle_request(int client_socket) {
char buffer[BUFFER_SIZE];
char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\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() {
}
强化服务器原码分析
web服务器原码分析 (qq.com)
1. 初始化服务器
2. 循环等待连接,连接后创建线程,调用msg_request函数,并在该函数中继续调用handler_msg函数分析数据
3. handler_msg中,首先查看协议内容,其次获取请求方法、url、参数,判断方法是什么并对need_handler赋值,确定请求资源路径,如果请求地址没有携带任何资源,那么默认返回index.html,如果资源不存在就返回404.如果需要处理(post请求或get请求带参数),调用handle_request函数,如果不需要(get请求不带参数)echo_www,直接返回资源
4. handle_request主要用来获取post请求消息正文,调用parse_and_process函数处理正文内容(需要自己添加需要功能)
安装使用postman
结合Modbus部分整体流程分析
通过postman模拟浏览器,实现Modbus Slave端数据采集和设备控制
注意:
存在共享内存和消息队列数据收发问题时。
解决方案:
- 在代码中加打印语句,确保两个进程用的是同一个id
- 由于程序是强制结束,再下次运行代码时,将消息队列删除一下查看和删除共享内存和消息队列:
ipcs -m :查看共享内存
ipcrm -m shmid:删除共享内存
ipcs -q:查看消息队列
ipcrm -q semid:删除消息队列
key值的创建路径指定/目录下的某个新建文件
多使用打印语句,排查错误位置