记录之前项目中的一个部分,因为要在windows系统的PC上控制两个Modbus从站,所以做了一个Modbus协议的串口通信,并且把他封装成了库,方便后期调用、修改和维护。
目录
一、Modbus协议简要介绍理解
(非小白请直接跳过该部分)
具体详细的Modbus协议内容和规定自行搜索,内容太多,就不在此处粘贴复制了,我只简要讲我的理解。
简单来讲,Modbus是一个标准化的通信协议,是软件层面上的,通信双方均需要遵循Modbus的协议规定才可正常通信。举一个具体例子来帮助理解:
假如我要控制一个从机(或者说其他第三方的设备),那么我就要去参考三方提供的技术手册,上面规定了我要如何使用该产品,大多会提供一个SDK,要根据SDK中的函数去操控其产品。这也是许多功能复杂的产品设备会用到的方式。但是,对于一个功能没那么复杂的产品(比如直线电机),我只需要实现控制它的位置,或者调节它的速度等简单的功能,如果再去调用SDK就显得太过于复杂了。而且当你的系统中有过多的从机设备,如果每个第三方的设备都有独立的SDK的话,那么不管是开发还是后期的维护都显得太过于复杂了。
于是为了解决这种情况,我们就做出一个约定:
我的主机可以向所有从机发送一组数据,所有的从机接收到以后按照一定的规则去解析这个数据并执行对应的功能,规则是这样的:
1.从机接收的数据(也是发送端发送的数据):
从机编号(你找谁) | 寄存器地址(找我干嘛) | 功能码(干多少) |
例如主机发送了一串数据:
01 | 02 | 03 |
在从机(以直线电机为例)的眼中他将被翻译成:
主机要找1号电机,并让他的位移距离(假定02寄存器和位移功能绑定,这个具体取决于三方厂商的设定)设置为3mm(也就是03的内容,这一部分也是需要三方提前告知设定的)。
2.从机的回复:
从机编号(我是谁) | 寄存器地址(我向你反映我的一个状态) | 功能码(状态是多少) |
这样,通过一问一答的形式主机便可以实现对从机的控制和状态的查询。当然,上面所讲的规则只是为了方便理解,它并不是Modbus协议规定的内容,Modbus规定的数据帧结构如下:
从站编号 | 功能码 | 数据位 | 校验码 |
具体Modbus协议内容可以参考链接:
二、Modbus协议串口通信实现
2.1 Windows下串口通信设置
2.1.1 Windows下和串口通信相关的几个函数
1.CreatFile函数,用于打开Windows下的串口。
CreatFile 是一个 Windows API 函数,用于在Windows操作系统上创建或打开一个文件。该函数位于 Windows API 的核心库 kernel32.dll 中,使用时需要包含头文件Windows.h头文件。
函数原型:
HANDLE CreateFile(
LPCSTR lpFileName,//串口名称
DWORD dwDesiredAccess,//读写模式设置
DWORD dwShareMode,//串口共享设置
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//串口安全属性
DWORD dwCreationDisposition,//创建文件的性质
DWORD dwFlagsAndAttributes,//文件属性和标志
HANDLE hTemplateFile//一个指向模板的句柄,一般为null
);
2.SetCommTimeouts函数,用于设置串口的超时。
SetCommTimeouts函数是Windows API中用于设置通讯端口(如串口)超时参数的函数。它的功能是配置一个串口的超时设置,包括读取和写入操作的超时时间限制。该函数被广泛用于串口通信的应用程序中。其函数原型如下:
BOOL SetCommTimeouts(
HANDLE hFile,//要设置的串口,或者其他句柄
LPCOMMTIMEOUTS lpCommTimeouts//有关配置参数的结构体
);
在它的参数中,LPCOMMTIMEOUTS结构体有如下定义:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;//指定两个连续字符之间的最大计时间隔
DWORD ReadTotalTimeoutMultiplier;//读操作的总超时时间计算公式的倍率
DWORD ReadTotalTimeoutConstant;//读操作的总超时时间计算公式的常数
DWORD WriteTotalTimeoutMultiplier;//写操作的总超时时间计算公式的倍率
DWORD WriteTotalTimeoutConstant;//写操作的总超时时间计算公式的常数
} COMMTIMEOUTS, *LPCOMMTIMEOUTS;
3.GetCommState函数,用于配置串口。
GetCommState函数是Windows API中用于获取通讯端口(如串口)的状态参数的函数。它的功能是获取指定串口的状态参数,包括波特率、数据位、停止位、校验位等,其原型如下:
BOOL GetCommState(
HANDLE hFile,//要获取状态参数的通讯端口句柄
LPDCB lpDCB//指向 DCB 结构体的指针,结构体用于存放串口的状态参数
);
其中,LPDCB的结构体的定义如下:
typedef struct _DCB {
DWORD DCBlength;
DWORD BaudRate;//波特率
DWORD fBinary : 1;
DWORD fParity : 1;
DWORD fOutxCtsFlow : 1;
BYTE Parity;//奇偶校验位
BYTE StopBits;//停止位
// 更多成员
WORD wReserved;
WORD XonLim;
WORD XoffLim;
// 更多成员
} DCB,*LPDCB;//该结构体的成员太多,省略了一部分
该结构体的成员太多,省略了许多,后面可以根据需要更改指定的配置选项,后文会有对应的代码。
4.WriteFile函数,向串口(或者其他句柄)写入数据。
WriteFile 是 Windows API 中用于向文件、管道、套接字等句柄对象中写入数据的函数。它的操作对象是一个句柄(类似于Linux中称为文件描述符),是HANDLE类型的变量。其函数原型为:
BOOL WriteFile(
HANDLE hFile,//要写入数据的文件句柄
LPCVOID lpBuffer,//指向包含要写入文件的数据的缓冲区的指针
DWORD nNumberOfBytesToWrite,//要写入文件的字节数
LPDWORD lpNumberOfBytesWritten,//用于接收实际写入文件的字节数,可以为 NULL
LPOVERLAPPED lpOverlapped//用于支持异步 I/O 操作。可以为 NULL,表示同步 I/O
);
5.ReadFile函数,从串口(或者其他句柄)中读取数据。
ReadFile函数是Windows API中用于从文件、管道、套接字或其他I/O设备中读取数据的函数。其函数原型如下:
BOOL ReadFile(
HANDLE hFile,//要读取数据的文件句柄或其他设备句柄
LPVOID lpBuffer,//指向存放读取数据的缓冲区的指针
DWORD nNumberOfBytesToRead,//要读取的字节数
LPDWORD lpNumberOfBytesRead,//用于存放实际读取的字节数的指针。可以为NULL
LPOVERLAPPED lpOverlapped//指向OVERLAPPED结构的指针,用于异步I/O操作
);
2.1.2 串口具体的配置代码
我们可以将初始化配置写在一个函数中,将其封装起来,方便后续调用,同时也会让代码显得更加整洁,具体可以如下:
#include <Windows.h>
#include<iostream>
HANDLE Initial(const char * PortName,int baud)
{
HANDLE myport = CreateFile(PortName,//串口名称
GENERIC_READ | GENERIC_WRITE,//读写模式
0,//串口共享模式,这里不允许共享,设置为0
NULL,//串口安全属性,设置为0,表示不可被子程序继承
OPEN_EXISTING,//创建文件的性质,这里为打开文件且将文件清空
0,//文件属性和标志,异步通讯为FILE_FLAG_OVERLAPPED,设置为0则为同步通讯
0);//一个指向模板的句柄,一般为null
//该函数失败返回 INVALID_HANDLE_VALUE
if (myport == INVALID_HANDLE_VALUE)
{
std::cout << "串口打开失败" << std::endl;
return myport;
}
else {
std::cout << "打开串口成功" << std::endl;
}
SetupComm(myport, 4096, 4096);//设置缓冲区,输入和输出缓冲区大小都为4096
COMMTIMEOUTS timeouts;//创建一个超时句柄
timeouts.ReadIntervalTimeout = 100;//读取两个字符之间的时间间隔
timeouts.ReadTotalTimeoutConstant = 5000;//读取操作的固定超时
timeouts.ReadTotalTimeoutMultiplier = 500;//读取操作再读取每个字符时的超时
timeouts.WriteTotalTimeoutMultiplier = 500;//写操作在写每个字符时的时间间隔
timeouts.WriteTotalTimeoutConstant = 2000;//写操作的固定超时
/*总超时=每个字符的超时*字符数+固定超时*/
SetCommTimeouts(myport, &timeouts);//设置超时
DCB dcb;//设置一个句柄用来配置串口
GetCommState(myport, &dcb);//获取设备的配置参数赋给dcp
dcb.BaudRate = baud;//波特率
dcb.ByteSize = 8;//数据位,为5~8
dcb.Parity = NOPARITY;//奇偶校验位,这里为无
dcb.StopBits = ONESTOPBIT;//停止位,1位停止位
SetCommState(myport, &dcb);//完成串口配置
return myport;
}
注意,在上述函数中我只对外提供了比特率设置的接口,因为大部分串口通信中,都会将数据位设置成8,停止位设置为1,无奇偶校验位。但是波特率不一定,有115200或者9600或者其他。如果需要额外指定配置选项或者更改默认选项请重新设置bcd成员的值。通过上述代码,我们就可以通过Initial函数来完成串口的配置。在读写串口之前,我们还要对我们的数据格式做出具体要求,所以接下俩就是有关于Modbus协议Host端的实现部分。
2.2 Modbus数据帧的实现
2.2.1字符串与16进制数据的转换
Modbus协议规定的数据帧格式都是16进制的,而我们从键盘向控制台输入的数据大多是以字符串的形式输入的且长度不定,所以我们需要将数据以字符串形式输入并在程序中将其转换成16进制。转换的方法有多种,这里给出一种以供参考:
void string_to_hex(const string& str, vector<uint8_t> &temp);
//将字符串数据转换成16进制数据,
//参数一:带转换的字符串,
//参数二:转换后16进制的数组
.
.
.
void string_to_hex(const string& str, vector<uint8_t> &temp)//把字符型数据转换成16进制数据
{
uint8_t data[20];
string str_temp;
for (int i = 0; i < str.length(); i += 2)
{
str_temp = str.substr(i, 2);//字符串裁剪
sscanf_s(str_temp.c_str(), "%hhx", &data[i / 2]);//将字符类型转换成16进制
temp.push_back(data[i/2]);
}
}
要注意的是容器的数据类型,这里设置为uint8_t是为了在进制转换过程中数据大小尽可能的小,而且每一个元素刚好是一个字节,这样在分割解析协议时会更加方便,输出数据时格式也更好处理。
当我们发出请求报文时,会收到一个回复报文,我们可以将收到的报文存储在uint8_t类型的数组中,并将其按照Modbus的报文答复格式输出到控制台,同时要注意的是输出到控制台时最好以字符串的形式,这样不容易显示乱码,下面给出参考:
void hex_to_string(uint8_t *buffer,int length, std::string &str);
//用于将16进制的数据固定成字符串,每个16进制的数的宽度为2,不足的用0补齐
//参数一为保存16进制的数组,
//参数二为要转换数据的个数
//参数三为转换后的字符串
...
...
...
void hex_to_string(uint8_t* buffer ,int length, std::string &str)
{
for (int i = 0; i < length; i++)
{
std::stringstream stream;//创建一个字符流
stream << std::setfill('0') << std::setw(2) << std::hex << int(buffer[i]);// 将数据以十六进制形式输出,并确保每个字节都用两位表示
str += stream.str();
}
return;
}
2.2.2 CRC校验码的实现
Modbus协议是靠CRC校验码来验证数据的正确性的,校验码不需要人为计算,它根据其前面报文的内容由程序自动生成并附加在原报文之后形成完整的报文。接收方收到报文后会验证CRC校验码是否正确,只有正确的CRC校验码才能让报文被正确解析,关于CRC16校验码的生成规则可自行查阅,参考链接:详述循环冗余校验CRC(C代码)_循环冗余码crc校验方法-CSDN博客
这里直接给出CRC的实现:
unsigned short CRC16_Modbus1(vector<uint8_t> &temp, int iDatalen);
//该函数用于生成CRC16校验码,并将其附加到原报文后,返回一个crc校验码
//参数1:原报文(16进制数据)
//参数2:原报文长度
unsigned short CRC16_Modbus1(vector<uint8_t> &temp, int iDatalen)
{
unsigned short crc = 0xFFFF;
for (int j = 0; j < iDatalen; j++)
{
crc = crc ^ temp[j];
for (int i = 0; i < 8; i++)
{
if ((crc & 0x0001) > 0)
{
crc = crc >> 1;
crc = crc ^ 0xa001;
}
else
crc = crc >> 1;
}
}
temp.push_back(crc & 0x00ff);
temp.push_back(crc >> 8);
return crc;
}
2.3 串口数据的读取与发送
前面我们已经介绍过WriteFile函数和ReadFile函数,再结合上节的Modbus数据帧的实现,就可以正确的通过串口接收和发送Modbus数据了。这里我将发送功能和接收功能封装成了两个函数,主要实现指定串口的发送和读取功能。发送报文函数示例如下:
bool WritePort(HANDLE& myport,string &str);
//将字符串str写入串口
...
...
bool WritePort(HANDLE& myport,string& str)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
vector<uint8_t> temp;
string_to_hex(str, temp);
CRC16_Modbus1(temp, temp.size());
if (WriteFile(myport,
temp.data(),//指向vector容器元素首地址的指针,注意:不可以用temp代替
8,
NULL,
NULL))
{
cout << "发送成功,输入报文为:" << endl;
for (int i = 0; i < temp.size(); i++)
{
std::cout <<std::hex<< int(temp[i])<<" ";
}
std::cout << std::endl;
return true;
}
else {
cout << "发送失败" << endl;
return false;
}
}
类似的,从串口接收报文的函数示意如下:
bool ReadPort(HANDLE& myport, uint8_t * buff,int &length);
//从串口中读出数据并记录读出的长度
...
...
...
bool ReadPort(HANDLE& myport, uint8_t* buff,int &length)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
uint8_t readbuffer[100];
unsigned long realnum;
if (ReadFile(myport,
readbuffer,
30,
&realnum,
NULL) == 1)
{
length = realnum;
memcpy(buff, readbuffer, realnum);
return true;
}
else
{
std::cout << "failed" << std::endl;
return false;
}
}
至此,已经可以通过上述几个函数实现Modbus通信Host端的基本功能了,当Modbus数据帧被发出后,Modbus从机会在内部自动解析数据并执行对应的功能,下面利用上述函数做一个简单的例子:
int main()
{
HANDLE myport = Initial("COM5", 115200);
int flag = 0;
uint8_t* buffer = new uint8_t[30];
int bufflength = 0;
while (1)
{
std::cout << "请输入报文:" << std::endl;
string str;
cin >> str;
WritePort(myport, str);
ReadPort(myport, buffer , bufflength);
string str2;
hex_to_string(buffer, bufflength, str2);
std::cout << str2 << std::endl;
PurgeComm(myport, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT);
cout << "是否继续?" << endl;
cout << "1-继续,0-退出" << endl;
cin >> flag;
cin.ignore();//清理一下cin缓冲区
if (flag == 0)
{
break;
}
}
CloseHandle(myport);
system("pause");
return 0;
}
这段代码用于将串口指定为COM5,波特率设置为115200,并持续向控制台输入报文并接受从机返回的值。
三、Windows平台下将程序打包成动态库
3.1 动态库和静态库基本概念
在许多程序开发的过程中,很多时候都会有大量重复的功能,如果每此都将实现这些功能的代码复制到项目中然后重新编译,这无疑会降低开发效率,增大开发难度,而且后期维护也不方便。如果说对于相同的工能,其代码可以重用或者共享就可以避免这个问题。而动态库和静态库都是在软件开发中用于代码重用和共享的概念。
静态库(Static Library)是一组已经编译好的目标代码文件的集合,这些文件可以被链接到应用程序中以提供特定的功能。应用程序在编译链接时,静态库的代码会被整个复制到生成的可执行文件中。因此,静态库会增加最终可执行文件的大小,但优点是在程序运行时不再需要额外的文件。
动态库(Dynamic Link Library,DLL)也是一个编译好的目标代码文件的集合,但在程序编译后不会被整个复制到可执行文件中。相反,程序在运行时需要动态库的支持,动态库的代码将在程序启动时被加载到内存中。这样可以减小可执行文件的大小,但需要保证动态库在运行环境中是可用的。
静态库的优点是便于部署和分发,因为它们不依赖于运行环境。但每个使用该库的程序都会包含整个库的副本,这可能增加程序的大小。动态库则允许多个程序共享相同的库,因此可以减小整体磁盘空间占用,但需要确保库文件在运行环境中存在并能够被正确加载。
在选择使用动态库还是静态库时,通常需要权衡可执行文件大小、内存使用、易用性以及可部署性等因素。
在Windows系统中,静态库是通过连接.lib文件实现的,动态库是通过连接.lib和.dll实现的。对于一些大型的项目一般会大量采用动态库链接的方案,不仅仅是为了节省空间,更多的是为了方便后期对软件的维护。举个例子:我有一个大型应用程序,包含许多功能A,B,C,D...如果我后期想更改功能A的具体实现,我只需要单独修改生成功能A的动态库程序然后重新发布A的动态库文件来替换原来的文件即可,而不需要变更B,C,D以及原应用程序。这样一来,对用户来说,更新应用的时候不需要把整个程序重新下载,节省了时间;对于发布者来说,不需要重新编译整个程序就完成的新旧功能的替换,提高了开发效率。
对于该Modbus串口通信的功能,我也推荐使用该方法。因为可以用到该功能应用的地方有很多,我们不仅可以将他集成在QT界面中做一个串口调试助手,还可以将其嵌入在一个自动化程序中作为主从通信的一个模块,而且后续如果需要对通信功能进行修改的话也更加方便。
3.2 封装动态库的实现
这里以Visual Studio为集成开发环境完成动态库的生成。下面给出相应步骤:
1.创建文件:
在VS中新建一个动态链接库的项目,如下图:
新建项目完成后你会看到你的工程目录下有这些文件:
其中pch.h文件主要包含一些在工程中会用到的头文件以实现预编译,加快编译速度。如果项目比较小的话此处不用做任何更改。
framework.h是用于包含项目的特定框架的头文件。在创建工程时,Visual Studio 会自动生成该文件,用于包含项目的标准头文件、常用库以及可能的预编译指令。在生成动态库的过程中,framework.h可能包含一些项目级别的设置,编译选项或者全局定义。
dllmain.cpp文件是程序的入口文件,在dllmain.cpp文件中,你会定义一个名为DllMain的函数,并在其中执行 DLL 所需的初始化和清理工作。在本次项目中我们不对该函数做任何更改。
根据上面讲到的代码,可以直接在项目中添加CRC16.h和modbusport.h的文件,然后在CRC16.cpp和moudbusport.cpp中实现。
CRC16.h和CRC16.cpp示例:
//CRC16.h
#pragma once
#include<vector>
#include<string>
using namespace std;
unsigned short CRC16_Modbus1(vector<uint8_t>& temp, int iDatalen);
void string_to_hex(const string& str, std::vector<uint8_t>& temp);
//将字符串数据转换成16进制数据,参数一:带转换的字符串,参数二:转换后16进制的数组
void print_hex(const uint8_t* readbuffer, unsigned long length);
//打印输出16进制数据,参数一:存放16进制数据的数组,参数二:数组长度
//CRC16.cpp
#include"pch.h"
#include"CRC16.h"
#include<string>
#include<iostream>
#include <sstream>
unsigned short CRC16_Modbus1(vector<uint8_t>& temp, int iDatalen)
{
unsigned short crc = 0xFFFF;
for (int j = 0; j < iDatalen; j++)
{
crc = crc ^ temp[j];
for (int i = 0; i < 8; i++)
{
if ((crc & 0x0001) > 0)
{
crc = crc >> 1;
crc = crc ^ 0xa001;
}
else
crc = crc >> 1;
}
}
temp.push_back(crc & 0x00ff);
temp.push_back(crc >> 8);
return crc;
}
void string_to_hex(const string &str , vector<uint8_t> &temp)//数据转换成16进制数据
{
uint8_t data[20];
string str_temp;
for (int i = 0; i < str.length(); i += 2)
{
str_temp = str.substr(i, 2);//字符串裁剪
sscanf_s(str_temp.c_str(), "%hhx", &data[i / 2]);//将字符类型转换成16进制
temp.push_back(data[i / 2]);
}
return ;
}
void print_hex(const uint8_t* readbuffer, unsigned long length)
{
for (unsigned int i = 0; i < length; i++)
{
printf("%02X ", int(readbuffer[i]));
}
cout << endl;
}
在modbusport.h文件中需要将打包函数进行声明,一般要用_declspec(dllexport)进行函数的修饰,或者可以设置一个宏来代替。
modbusport.h和modbusport.cpp文件如下:
//modbusport.h
#pragma once
#ifdef MODBUSDLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif //
#include <Windows.h>
#include<string>
DLL_API HANDLE Initial(const char* PortName, int baud);//初始化串口
//参数一:串口名称,参数二:波特率。数据位,奇偶校验位,停止位等按照最常用的设置
//函数返回一个myport句柄,需要一个handle变量接收
DLL_API bool WritePort(HANDLE& myport);
DLL_API bool WritePort(HANDLE& myport, std::string& str);
//写入串口的的重载,将字符串str写入串口
DLL_API bool ReadPort(HANDLE& myport , uint8_t* buff, int& length);
//读出串口的的重载,从串口中读出数据并记录读出的长度
DLL_API bool ReadPort(HANDLE& myport);
DLL_API void hex_to_string(uint8_t* buffer, int length, std::string& str);
//用于将16进制的数据固定成字符串,每个16进制的数的宽度为2,不足的用0补齐
注意,如果使用上面的.h文件进行声明,需要在编译器中预处理器那一栏添加MODBUSDLL_EXPORT这一行!
//modbusport.cpp
#pragma execution_character_set("utf-8")
#include"pch.h"
#include"modbusport.h"
#include<string>
#include<thread>
#include<iostream>
#include"CRC16.h"
#include <iomanip>
#include <sstream>
using namespace std;
HANDLE Initial(const char* PortName, int baud)
{
HANDLE myport = CreateFile(PortName,//串口名称
GENERIC_READ | GENERIC_WRITE,//读写模式
0,//串口共享模式,这里不允许共享,设置为0
NULL,//串口安全属性,设置为0,表示不可被子程序继承
OPEN_EXISTING,//创建文件的性质,这里为打开文件且将文件清空
0,//文件属性和标志,异步通讯为FILE_FLAG_OVERLAPPED,设置为0则为同步通讯
0);//一个指向模板的句柄,一般为null
//该函数失败返回 INVALID_HANDLE_VALUE
std::cout << PortName << std::endl;
if (myport == INVALID_HANDLE_VALUE)
{
cout << "Failed to open serial port" << endl;
return myport;
}
else {
cout << "Open serial port successfully" << endl;
}
SetupComm(myport, 4096, 4096);//设置缓冲区,输入和输出缓冲区大小都为1024
COMMTIMEOUTS timeouts;//创建一个超时句柄
timeouts.ReadIntervalTimeout = 100;//读取两个字符之间的时间间隔
timeouts.ReadTotalTimeoutConstant = 5000;//读取操作的固定超时
timeouts.ReadTotalTimeoutMultiplier = 500;//读取操作再读取每个字符时的超时
timeouts.WriteTotalTimeoutMultiplier = 500;//写操作在写每个字符时的时间间隔
timeouts.WriteTotalTimeoutConstant = 2000;//写操作的固定超时
/*总超时=每个字符的超时*字符数+固定超时*/
SetCommTimeouts(myport, &timeouts);//设置超时
DCB dcb;//设置一个句柄用来配置串口
GetCommState(myport, &dcb);//获取设备的配置参数赋给dcp
dcb.BaudRate = baud;//波特率
dcb.ByteSize = 8;//数据位,为5~8
dcb.Parity = NOPARITY;//奇偶校验位,这里为无
dcb.StopBits = ONESTOPBIT;//停止位,1位停止位
SetCommState(myport, &dcb);//完成串口配置
return myport;
}
bool WritePort(HANDLE& myport)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
vector<uint8_t> temp;
//uint8_t temp[8] = {};
string str;
cout << "Please enter telegram:" << endl;
getline(cin, str);
string_to_hex(str, temp);
cout << temp.size() << endl;
CRC16_Modbus1(temp, temp.size());
if (WriteFile(myport,
temp.data(),//指向vector容器元素首地址的指针,注意:不可以用temp代替
8,
NULL,
NULL))
{
cout << "Sent successfully" << endl;
return true;
}
else {
cout << "Send Failure" << endl;
return false;
}
}
bool WritePort(HANDLE& myport, string& str)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
vector<uint8_t> temp;
string_to_hex(str, temp);
cout << temp.size() << endl;
CRC16_Modbus1(temp, temp.size());
if (WriteFile(myport,
temp.data(),//指向vector容器元素首地址的指针,注意:不可以用temp代替
8,
NULL,
NULL))
{
cout << "Sent successfully, the input message is:" << endl;
for (int i = 0; i < temp.size(); i++)
{
std::cout << std::hex << int(temp[i]) << " ";
}
std::cout << std::endl;
return true;
}
else {
cout << "Send Failure" << endl;
return false;
}
}
bool ReadPort(HANDLE& myport)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
uint8_t readbuffer[100];
unsigned long realnum;
if (ReadFile(myport,
readbuffer,
30,
&realnum,
NULL) == 1)
{
cout << "Telegram response:" << endl;
print_hex(readbuffer, realnum);
return true;
}
else
{
return false;
}
}
bool ReadPort(HANDLE& myport, uint8_t* buff, int& length)
{
if (myport == INVALID_HANDLE_VALUE)
{
return false;
}
uint8_t readbuffer[100];
unsigned long realnum;
if (ReadFile(myport,
readbuffer,
30,
&realnum,
NULL) == 1)
{
length = realnum;
std::cout << "Telegram response:" << std::endl;
memcpy(buff, readbuffer, realnum);
print_hex(buff, realnum);
return true;
}
else
{
std::cout << "Read failed" << std::endl;
return false;
}
}
void hex_to_string(uint8_t* buffer, int length, std::string& str)
{
for (int i = 0; i < length; i++)
{
std::stringstream stream;//创建一个字符流
stream << std::setfill('0') << std::setw(2) << std::hex << int(buffer[i]);// 将数据以十六进制形式输出,并确保每个字节都用两位表示
str += stream.str();
}
return;
}
上述代码可能有部分内容有所变动,但是大体与之前所讲的函数相同,可以根据需要自由更改。
注意:完成上述代码编辑后,点击菜单栏的生成按键,选择生成解决方案,一定不要点击本地Windows调试器去运行该程序!该项目是生成.lib和.dll文件的,不能够生成exe文件去运行。动态库生成完毕后会有如下显示:
之后,在你生成的目录中(一般是在x64的Release或者Debug)会看到这些文件:
该文件夹中的.lib和.dll文件就是我们需要的目标文件,调用时需要将前面的modbusport.h文件一起打包。注意:生成该动态库的方式与调用该动态库的目标程序的方式最好一致,如果项目是Release的,动态库最好是Release的;如果项目是Debug的,则动态库也最好是Debug的。
有关更多更详细的动态库生成和调用内容可以参考:C++动态库封装及调用_c++ 调用动态库-CSDN博客
以上就是本次博客的内容,有关该库的使用我后面会专门出一个例程。如果对此博客有疑问可以留言,看到会回复的。
已经打包好的动态库文件以及Qt制作的Modbus-Host小程序链接:
【免费】Modbus-串口通讯极简版小程序资源-CSDN文库
Modbus串口通信动态库生成站内源码如下:
【免费】Modbus-Host端串口通讯动态库源码资源-CSDN文库
GitHub地址: