开发语言: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)除了完成过程控制量的数据输入和输出外(使用已有的功能代码完成),还需要完成以下几个功能:
操作系统: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()函数返回响应,同时在控制台输出请求、响应的内容。
整个程序的流程图如下:
01与02,03与04相似,此处不再贴图。
/*
* 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
#include
//#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); //关闭串口
}
/*
* File_name:respond.c
* Author:dahu
* Description:定义了不同功能码的响应函数
* Time:2021-04-21
* encoding:UTF-8
* Version:1.0
*/
#include
#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;
}
}
#pragma once
#include
#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