开发语言: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)除了完成过程控制量的数据输入和输出外(使用已有的功能代码完成),还需要完成以下几个功能:
- 提供Modbus TCP的状态检查,检查系统是否正常、每个AI、AO、DI、DO通道是否正常;
- 自定义一个命令,实现对这些AI、AO、DI、DO通道选配,如果本次采集只采集8路DI和8路AO;
- 每次主从通信过程应该包括以下流程:
(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
#include<stdio.h>
#include<winsock2.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";
short port = 502;
void TCP_server(short port,const char *p);
void STM32_IO(void);
void init();
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
init();
TCP_server(port,server_addr);
WSACleanup();
return 0;
}
void TCP_server(short port,const char* p) {
SOCKET server_socket;
server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(p);
server_addr.sin_port = htons(port);
int bind_status = -1;
bind_status = bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (bind_status == -1) {
printf("绑定失败\n");
exit(1);
}
else printf("套接字与IP地址、端口绑定成功\n");
int listen_status = -1;
listen_status = listen(server_socket, Request_Queue_len);
if (listen(server_socket, 5) == -1) {
printf("监听失败\n");
exit(1);
}
else printf("创建TCP服务器成功\nTCP连接正常,开始监听\n\n");
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
byte buff[BUF_SIZE] = {0};
SOCKET clntSock = accept(server_socket, (SOCKADDR*)&clntAddr, &nSize);
printf("%d Link successfull\n", clntSock);
while (1) {
int strLen = recv(clntSock, buff, BUF_SIZE, 0);
if (strLen = 0)
printf("客户端连接关闭\n\n");
else if (strLen < 0)
printf("Linking Error");
printf("\n\n**************************************************************************************************\n");
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);
}
closesocket(server_socket);
}
void init(void) {
printf("从站状态检查...");
printf("\n从站状态正常\n");
}
void STM32_IO(void) {
int data[9];
HANDLE hcom;
hcom = open_scom("COM3", 9600, NOPARITY, 8, 1);
while (1)
{
read_scom(hcom, data);
}
close_scom(hcom);
}
6.2 respond.c
#include<stdio.h>
#include"respond.h"
byte coil[2] = {0x00,0x00};
byte relay[2] = {0x0f,0x06};
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,};
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};
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 };
send_buff[5] = 2 + 1 + n;
send_buff[8] = 2 * n;
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);
}
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 };
send_buff[5] = 2+1+n;
send_buff[8] = 2*n;
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);
}
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 };
send_buff[5] = 2 + 1 + n;
send_buff[8] = n;
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);
}
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 };
send_buff[5] = 2 + 1 + n;
send_buff[8] = n;
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);
}
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;
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
|