實驗專案名稱 Modbus TCP實驗
- 一、實驗目的
- 二、實驗內容
- 三、實驗環境
- 四、設計方案
- 五、實驗結果及分析(或設計總結)
- 六、完整代碼
- 6.1 server.c
- 6.2 respond.c
- 6.3 respond.h
開發語言: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
/*
* 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/291006.html
標籤:其他
上一篇:配置JAVA的環境變數
下一篇:微信小程式學習筆記
