IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Modbus TCP Server端(附超全注释) -> 正文阅读

[网络协议]Modbus TCP Server端(附超全注释)

开发语言:C
开发平台:VS2019
开发工具:Visual studio 2019、Modbus/TCP Master

学习Modbus见:Modbus协议中文手册

获取Modbus TCP Master见:Modbus TCP Master

完整代码见文末,我会修改一点内容,请读者自行发现并改正,练习练习动手能力哈。

一、实验目的

熟悉并掌握Modbus协议,能用代码实现基于modbus协议的简单应用。

二、实验内容

(1)编写一个Modbus-TCP程序,用modbus Poll等模拟主站设备。从站设备具有16路DI、16路DO以及16路AI(从站可用曲线模拟)和16路AO(从站可用曲线模拟)。

(2)除了完成过程控制量的数据输入和输出外(使用已有的功能代码完成),还需要完成以下几个功能:

  1. 提供Modbus TCP的状态检查,检查系统是否正常、每个AI、AO、DI、DO通道是否正常;
  2. 自定义一个命令,实现对这些AI、AO、DI、DO通道选配,如果本次采集只采集8路DI和8路AO;
  3. 每次主从通信过程应该包括以下流程:
    (1) 首先主站轮询从站的状态;
    (2) 接着,从站发出响应,如果是OK,那么执行下一步,否则等待主站下一次轮询状态;
    (3) 主站对从站进行配置,配置DI/AI、DO /AO使能、采样周期;
    (4) 从站如果响应OK的话,执行下一步,否则等待主站的配置命令;
    (5)主站和从站进行数据交换和轮询。
    (6) 有通信的结束和退出命令。

三、实验环境

操作系统:Windows10

开发工具:Visual studio 2019、Modbus/TCP Master

四、设计方案

? ? 本次实验使用TCP/IP来进行modbus协议的传输,主要使用了windows下的winsock2及其相关静态链接库ws2_32.lib,使用socket(套接字)进行主从机的连接。

? ? 连接前,先进行从机(server)自检,检查AI、AO、DI、DO的数据是否已经准备好。我在从机部分用四个数组代替AI、AO、DI、DO,如图所示,所以自检OK。
在这里插入图片描述

? ? 自检完成后,创建从机套接字,并将其与使用的电脑当前IPV4地址和相关端口进行绑定。绑定后将绑定状态输出到控制台。

? ? 接着进入监听状态,当有主机连接进来时在控制台打印连接成功的信息。使用accept()函数阻塞程序运行,直到新的请求到来。每次将新的请求放入缓冲区(请求队列),处理完毕后再从缓冲区读取请求,并在while循环中,使用solve_all(SOCKET clnSock, byte request[])函数来判断请求的类型,并转到相应的处理函数。处理完后重置相应的缓冲区。solve_all()函数的实现如下。
在这里插入图片描述

注:由于modbus
tcp的报文帧格式如下,所以通过switch语句判断缓冲区的第8位:buff[7],即可直到功能码的类型。在这里,我只简单实现了01、02、03、04功能码,其他功能码类似,有一些在RTU中做了实现。

在这里插入图片描述

? ? 这里再简单看一个功能码的实现,完整程序见附录或者压缩包。以01功能码为例进行说明:由于我将AI、AO、DI、DO都设置为16路,而DI、DO是以位为基本单位读写,而AI、AO是以字(这里是16位)为基本单位读写的,所以对于DI、DO的byte型数组只需要2个元素,而后者则需要32个元素。

? ? 01功能码是读线圈/离散量的输出状态即ON/OFF。先通过报文的第12位(实际应该是第11、12个字节,但这里只用了16路,所以第11个字节为0x00,不予考虑)判断要读取的点的个数,再确定要返回的字节数,当读取的点数不超过8就返回一个字节,否则返回2个字节数据。相应的,还需要修改返回报文的第六位即这一位后面的字节数。除此之外,请求和响应的前8个字节是相同的。而响应报文的第九位是数据区的字节长度,即程序中的n,要说明的是在使用modbus tcp master软件测试时需要将n乘以2才能正确显示。再将第九位后面的区域填充为数据即可。最后用send()函数返回响应,同时在控制台输出请求、响应的内容。
在这里插入图片描述

? ? 整个程序的流程图如下:

在这里插入图片描述

五、实验结果及分析(或设计总结)

(1)先查看本机IP地址,在程序中修改为今天的IP。
在这里插入图片描述在这里插入图片描述

(2)功能码测试
未连接时:
在这里插入图片描述
在这里插入图片描述

02功能码:
在这里插入图片描述
在这里插入图片描述

03功能码:
在这里插入图片描述
在这里插入图片描述

01与02,03与04相似,此处不再贴图。

六、完整代码

6.1 server.c

/*
* File_name:server.c
* Author:dahu
* Description:modbus tcp socket主要实现源文件
* Time:2021-04-21
* encoding:UTF-8
* Version:1.0
*/


/*********************************************************************
*                          Modbus-TCP报文帧格式
* |-----------------MBAP报头------------------|-----PDU-----|
* 
* 事务处理标识箱    协议表示符    长度    单元标识符   功能码   数据     
* 
    2Bytes          2Bytes    2Bytes    1Bytes    1Bytes  nBytes
exp:00 00         00  00      00 06     01         03     00 00   00 0A
**********************************************************************/


#include<stdio.h>
#include<winsock2.h>
//#include"scom.h"
#include"respond.h"

#pragma comment(lib,"ws2_32.lib")                //使用静态链接库

#define BUF_SIZE 20                              //缓冲区大小
#define Request_Queue_len 20                     //请求队列最大长度 

const char *server_addr = "172.20.10.3";       //服务器IP地址
short port = 502;                                //服务器端口号  

void TCP_server(short port,const char *p);       //TCP服务器函数声明
void STM32_IO(void);                             //与STM32通信,有点问题,没使用到,用的模拟方法实现
void init();                                     //从站状态检查,本来应该是STM32开机自检,由于用的是模曲线拟,基本上没什么用



int main() {
	//初始化 DLL
	WSADATA wsaData;   
	WSAStartup(MAKEWORD(2, 2), &wsaData);        //初始化,以指明 WinSock 规范的版本

	init();
	TCP_server(port,server_addr);
	//STM32_IO();

	WSACleanup();                                 //终止 DLL 的使用
	return 0;
}


/*********************************************************************************************
* 名称:main(short port,const char* p)
* 功能:创建tcp服务器,与client通信
* 入口参数:port:端口号;p:ip地址,本机ipv4地址
* 返回参数:无
* 说明:无
**********************************************************************************************/
void TCP_server(short port,const char* p) {
	/*Windows下的socket函数原型:
	SOCKET socket(int af, int type, int protocol);
	Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄,如:
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	*/
	SOCKET server_socket;                                     //创建一个套接字,参数(地址簇,socket类型,使用的协议) 
	server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);


	struct sockaddr_in server_addr;                            //sockaddr_in结构体,可在ws2def.h中查看原型
	/*memset()函数原型是:extern void* memset(void* buffer, int c, int count)
	buffer:为指针或是数组, c:是赋给buffer的值,count:是buffer的长度.
	这个函数在socket中多用于清空数组.如:原型是memset(buffer, 0, sizeof(buffer))*/
	memset(&server_addr, 0, sizeof(server_addr));              //每个字节都用0填充
	server_addr.sin_family = AF_INET;                          //使用IPv4地址
	server_addr.sin_addr.s_addr = inet_addr(p);                //具体的IP地址,inet_addr是比较老的定义,要取消SDL检查。或者用inet_pton()函数
	server_addr.sin_port = htons(port);                        //端口

	/*Windows下的bind()函数原型:
	int bind(SOCKET sock, const struct sockaddr* addr, int addrlen);
	*/
	int bind_status = -1;
	bind_status = bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));           //将套接字与特定的IP地址和端口绑定起来

	if (bind_status == -1) {                                     //绑定状态监测,成功返回0,失败返回-1
		printf("绑定失败\n");
		exit(1);
	}
	else printf("套接字与IP地址、端口绑定成功\n");

	/*Windows下listeb()函数原型:
	int listen(SOCKET sock, int backlog);
	backlog 为请求队列的最大长度当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。
	如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列。
	*/
	int listen_status = -1;
	listen_status = listen(server_socket, Request_Queue_len);      //让套接字处于监听状态(并没有接收请求。接收请求需要使用 accept() 函数)

	if (listen(server_socket, 5) == -1) {                          //监听状态监测
		printf("监听失败\n");
		exit(1);
	}
	else printf("创建TCP服务器成功\nTCP连接正常,开始监听\n\n");

	/*Windows下accept()函数原型:
	SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
	accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
	accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来
	*/
	SOCKADDR clntAddr;
	int nSize = sizeof(SOCKADDR);
	//char buff[BUF_SIZE] = { 0 };                                                   //缓冲区

	byte buff[BUF_SIZE] = {0};
	SOCKET clntSock = accept(server_socket, (SOCKADDR*)&clntAddr, &nSize);          //接收客户端请求
	printf("%d Link successfull\n", clntSock);


	while (1) {     

		//int recv(SOCKET sock, char *buf, int len, int flags);
		//int send(SOCKET sock, const char *buf, int len, int flags);

		int strLen = recv(clntSock, buff, BUF_SIZE, 0);         //接收客户端发来的数据,recv返回的是字节数

		if (strLen = 0)
			printf("客户端连接关闭\n\n");
		else if (strLen < 0) 
			printf("Linking Error");

		printf("\n\n**************************************************************************************************\n");
		//int length = buff[5];                                  //MBAP报文头,2+2+2+1   功能码1+起始地址2+数量2
		printf("收到报文为:\n");
		for (int i = 0;i < 12;i++)printf("0x0%x ", buff[i]);     //显示主站的请求报文
		printf("\n");

		solve_all(clntSock,buff);                                //根据功能码进行相应处理,从站返回相应报文

		memset(buff, 0, BUF_SIZE);                               //重置缓冲区
		//Sleep(1000);
	}

	closesocket(server_socket);                                   //关闭套接字
}




/*********************************************************************************************
* 名称:init()
* 功能:服务器端(从机)开机自检,但我没连接stm32,所以这个函数暂时是个空壳子
* 入口参数:无
* 返回参数:无
* 说明:无
**********************************************************************************************/
void init(void) {
	printf("从站状态检查...");
	printf("\n从站状态正常\n");
}




/*********************************************************************************************
* 名称: STM32_IO()
* 功能:与32通信,实现modbus tcp的AI、AO等
* 入口参数:无
* 返回参数:无
* 说明:调试不成功,暂时没使用
**********************************************************************************************/
void STM32_IO(void) {
	int data[9];  //
	HANDLE hcom;  //声明串口操作句柄
	hcom = open_scom("COM3", 9600, NOPARITY, 8, 1);//打开串口
	while (1)
	{
		read_scom(hcom, data);  //读串口
		// do someting;
		//break
		//printf("%d\n", data);
	}
	
	close_scom(hcom);         //关闭串口
}

6.2 respond.c

/*
* File_name:respond.c
* Author:dahu
* Description:定义了不同功能码的响应函数
* Time:2021-04-21
* encoding:UTF-8
* Version:1.0
*/


#include<stdio.h>
#include"respond.h"



/*********************************************************************************************
* 功能:模拟16路DI、DO、AI、AO
* 说明:使用的Modbus/TCP master ,使用了01 02 03 04功能码
*      前两个以bit为基本单位,16路即2bytes;后两个以word为基本单位,16路即32bytes(16words)。
* 注意:连续两个寄存器之间存在字节序和大小端的问题,需注意
*********************************************************************************************/
byte coil[2] = {0x00,0x00};    //读线圈/离散量输出状态(ON/OFF)   01    bit   16路DO:0000 0000 0000 0000(高到低)
byte relay[2] = {0x0f,0x06};   //读离散量输入值(ON/OFF)         02    bit   16路DI:0000 0110 0000 1111(高到低)
byte holding_reg[32] = {0x01,0x01,0x01,0x02,0x00,0x03,0x00,0x04,0x00,0x05,0x00,0x06,0x00,0x07,0x00,0x08,
						0x00,0x01,0x00,0x02,0x00,0x03,0x00,0x04,0x00,0x05,0x00,0x06,0x00,0x07,0x00,0x08,};     
                               //读保持寄存器值                   03    word  (响应报文按字节来的,这里不用word定义了)
byte input_reg[32] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};       
                               //读输入寄存器值                   04    word    全为0x0101,即257




/*********************************************************************************************
* 名称:solve_01(SOCKET clntSock, byte request[], byte coil[])
* 功能:01功能码,读线圈/离散量输出状态
* 入口参数:clnSock:客户端socket,request:接收数据缓冲区,coil:事先准备好的线圈/离散量状态输出值
* 返回参数:无
* 说明:收到报文的关键部分为:01(1byte)+ 起始地址(2bytes)+ 寄存器数(2bytes)。
*      例如: 02 00 00 00 06表示读取0x0000开始的6个线圈的状态(1个线圈占用1位,共1个字节)
*      返回报文关键部分为:02(1byte)+ N(1byte)+ n=N/N+1字节状态(n bytes)
*      对上诉例子的响应:02 01 01
*上面只是关键部分报文分析,完整报文符合server.c中我写的 Modbus TCP报文格式
**********************************************************************************************/
void solve_01(SOCKET clntSock, byte request[], byte coil[]) {
	int len, n;
	len = request[11];                                                     //需要读取的点的个数
	n = (len % 8) == 0 ? (len / 8) : (len / 8 + 1);                        //对应的的字节数
	byte send_buff[8 + 3] = { 0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x02 };   //返回数据缓冲区,前八个字节不变(除了长度位),后面最多3个字节
	send_buff[5] = 2 + 1 + n;                                              //确定响应报文第六位,即后续报文长度:01+02+n+n bytes data
	send_buff[8] = 2 * n;                                                  //我用的是Modbus/TCP这个软件来测试的,软件有点小问题,n要乘以2,正确的报文这里不需要乘以2  
	for (int i = 0;i < n;i++) {
		send_buff[i + 9] = coil[i];
	}
	printf("\n该指令为读线圈/离散量状态输出即DO,返回内容应该为:\n");
	for (int i = 0;i < 8 + 1 + n;i++)printf("0x0%x ", send_buff[i]);
	printf("\n**************************************************************************************************");
	send(clntSock, send_buff, 8 + 1 + n, 0);                               //响应请求
}




/*********************************************************************************************
* 名称:solve_02(SOCKET clntSock, byte request[], byte relay[])
* 功能:02功能码,读离散量输入
* 入口参数:clnSock:客户端socket,request:接收数据缓冲区,relay:事先准备好的离散量输入值
* 返回参数:无
* 说明:同01功能码
**********************************************************************************************/
void solve_02(SOCKET clntSock, byte request[], byte relay[]) {
	int len,n;
	len = request[11];                                                   //需要读取的点的个数
	n = (len % 8) == 0 ? (len/8):(len/8+1);                              //对应的的字节数
	byte send_buff[8 + 3] = { 0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x02 }; //返回数据缓冲区,前八个字节不变(除了长度位),后面最多3个字节
	send_buff[5] = 2+1+n;                                                //确定响应报文第六位,即后续报文长度:01+02+n+n bytes data
	send_buff[8] = 2*n;                                                  //我用的是Modbus/TCP这个软件来测试的,软件有点小问题,n要乘以2,正确的报文这里不需要乘以2  
	for (int i = 0;i < n;i++) {
		send_buff[i + 9] = relay[i];
	}
	printf("\n该指令为读离散量输入即DI,返回内容应该为:\n");
	for (int i = 0;i < 8+1+n;i++)printf("0x0%x ", send_buff[i]);
	printf("\n**************************************************************************************************");
	send(clntSock, send_buff, 8+1+n, 0);                                 //响应请求
}




/*********************************************************************************************
* 名称:solve_03(SOCKET clntSock, byte request[], byte holding_reg[])
* 功能:03功能码,读保持寄存器
* 入口参数:clnSock:客户端socket,request:接收数据缓冲区,holding_reg:事先准备好的保持寄存器
* 返回参数:无
* 说明:类似01功能码,不在赘述
**********************************************************************************************/
void solve_03(SOCKET clntSock,byte request[],byte holding_reg[]) {
	int len, n;
	len = request[11];                                                       //需要读取的寄存器的个数
	n = 2 * len;                                                             //对应的的字节数
	byte send_buff[8 + 33] = { 0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x03 };    //返回数据缓冲区,前八个字节不变(除了长度位),后面最多33个字节
	send_buff[5] = 2 + 1 + n;                                                //确定响应报文第六位,即后续报文长度:01+02+n+n bytes data
	send_buff[8] = n;                                                        //我用的是Modbus/TCP这个软件来测试的,软件有点小问题,n要乘以2,正确的报文这里不需要乘以2  
	for (int i = 0;i < n;i++) {
		send_buff[i + 9] = holding_reg[i];
	}
	printf("\n该指令为读保持寄存器,返回内容应该为:\n");
	for (int i = 0;i < 8 + 1 + n;i++)printf("0x0%x ", send_buff[i]);
	printf("\n**************************************************************************************************");
	send(clntSock, send_buff, 8 + 1 + n, 0);                                 //响应请求
}




/*********************************************************************************************
* 名称:solve_04(SOCKET clntSock, byte request[], byte input_reg[])
* 功能:04功能码,读输入寄存器
* 入口参数:clnSock:客户端socket,request:接收数据缓冲区,relay:事先准备好的离散量输入值
* 返回参数:无
* 说明:同01功能码
**********************************************************************************************/
void solve_04(SOCKET clntSock, byte request[], byte input_reg[]) {
	int len, n;
	len = request[11];                                                       //需要读取的寄存器的个数
	n = 2 * len;                                                             //对应的的字节数
	byte send_buff[8 + 33] = { 0x00,0x00,0x00,0x00,0x00,0x06,0x01,0x04 };    //返回数据缓冲区,前八个字节不变(除了长度位),后面最多33个字节
	send_buff[5] = 2 + 1 + n;                                                //确定响应报文第六位,即后续报文长度:01+02+n+n bytes data
	send_buff[8] = n;                                                        //我用的是Modbus/TCP这个软件来测试的,软件有点小问题,n要乘以2,正确的报文这里不需要乘以2  
	for (int i = 0;i < n;i++) {
		send_buff[i + 9] = input_reg[i];
	}
	printf("\n该指令为读输入寄存器,返回内容应该为:\n");
	for (int i = 0;i < 8 + 1 + n;i++)printf("0x0%x ", send_buff[i]);
	printf("\n**************************************************************************************************");
	send(clntSock, send_buff, 8 + 1 + n, 0);                                 //响应请求
}






/*********************************************************************************************
* 名称:solve_all(SOCKET clnSock,byte request[])
* 功能:对 功能码进行判别,并转到相应的功能码的处理函数
* 入口参数:clnSock:客户端socket,request:接收数据缓冲区
* 返回参数:无
* 说明:无
**********************************************************************************************/
void solve_all(SOCKET clnSock,byte request[]) {
	switch (request[7]) {
		case 1:solve_01(clnSock,request,coil);break;
		case 2:solve_02(clnSock,request,relay);break;    //DI 读离散量输入
		case 3:solve_03(clnSock,request, holding_reg);break;
		case 4:solve_04(clnSock,request,input_reg);break;
	}
}

6.3 respond.h

#pragma once
#include<winsock2.h>

#ifndef _RESPOND_H_
#define _RESPOND_H_


void solve_01(SOCKET clntSock, byte request[], byte coil[]);
void solve_02(SOCKET clntSock, byte request[], byte relay[]);
void solve_03(SOCKET clntSock, byte request[], byte holding_reg[]);
void solve_04(SOCKET clntSock, byte request[], byte input_reg[]);


void solve_all(SOCKET clnSock, byte request[]);  


#endif
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-30 13:06:15  更:2021-07-30 13:06:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/4 1:30:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码