系列文章目录
Modbus通讯开发随记1——LibModbus库的学习;
Modbus通讯开发随记2——基于LibModbus库的读取写入测试;
源文件资源
Modbus主机类.cpp文件
Modbus主机类.hpp文件
前言
由于LibModbus库的直接使用为面向过程的,本文使用C++类和模板方法,将Modbus主机开发对象化,实现了Modbus通讯的关键功能,并定义数据缓存接口,以方便Modbus通讯数据的前后端分离。本文主要作为自己的开发随记文档,相关代码只考虑到了功能实现,健壮性和安全性有所欠缺,随着后续开发的推进会进行尽量补全,若对路过的您有所帮助也请指出文中的不足之处。另外,本文中的实现只进行了最简单的测试,很可能存在实现有误的情况,若您需要克隆使用请谨慎。
一、主机类的主要功能
- 根据从机的IP和端口新建一个主机对象;
- 读取和写入从机的线圈寄存器;
- 读取输入线圈寄存器;
- 支持以不同数值类型读取和写入从机的保持寄存器;
- 支持以不同数值类型读取输入寄存器;
- 对于使用过程中出现的
二、设计实现的必要数据结构
2.1.数据格式
Modbus开发中会遇到不同的设备采用不同的字节序,这个变素要求我们在开发过程中考虑多种字节序的解析,以保证接收到的数据不会出现解析错误,避免因解析错误导致程序的数据异常。以下为Modbus设备可能出现的4种字节序:
/*数据格式枚举类型;
用于指定寄存器读写的数据格式,包括大端模式、小端模式、单字反转模式等;
*/
enum class DataFormat :uint8_t{
ABCD = 0, //大端模式,高位字节存储在低地址,低位字节存储在高地址
BADC, //大端单字反转
CDAB, //小端单字反转
DCBA //小端模式,高位字节存储在高地址,低位字节存储在低地址
};
2.2.便于数据类型转换的“联合体”
这个类型的设计,我们需要使用union的特性:同一段内存可用多种数值类型来解析,这将便于我们将多个16位寄存器数据(目前常见单个寄存器保存16bits)转换成32位或64位的整型、浮点数或双精度浮点数。
上一节中提到了Modbus通讯开发中常见的字节序类型,在这个类型中我们需要根据字节序格式进行内存中字节的重排列,以保证数据解析的正确性。
/*数值类型转换联合体;
用于将不同类型的数据转换为uint16_t数组,以便Modbus协议传输;
将uint16_t数组看做存储单元的话,下标更小的单元存储低位字节,一般PC端为小端存储。
*/
template<typename T>
union MutData{
uint16_t val16i[sizeof(T)/sizeof(uint16_t)];
T tval;
void endiantrans(DataFormat src_fmt); //字节序转换函数
};
2.3.“16位寄存器”输入或输出缓存模板类
在我的预先考虑中,单个数据的访问直接返回单个数据的值,这种情况不需要缓存,
而多个数据同时读写使用堆缓存更为合适。
在程序运行阶段,有多个线程会对堆缓存进行访问,一个是访问堆缓存数据的GUI控件(这个线程可能对保存寄存器缓存或线圈寄存器缓存写入),一个是Modbus主机线程(这个线程会对缓存进行读取和写入),另一个是Modbus寄存器数据记录线程(这个线程会对缓存进行读取),需要明确的是当一个线程对缓存进行写入时缓存是需要禁止被读取的(即对缓存上锁)。
/*modbus寄存器缓存类模板;
用于缓存寄存器读写的数据,避免重复读写;
LVCache:locked value cache, 带锁数据缓存;
*/
template<typename T>
class LVCache{
public:
LVCache(int size, DataFormat fmt=DataFormat::ABCD):size_(size),fmt_(fmt),
cache_ptr_(new MutData<T>[size]){}
~LVCache(){delete[] cache_ptr_;}
LVCache(const LVCache&)=delete;
LVCache& operator=(const LVCache&)=delete;
void endiantrans();
void lock(){lock_ = true;}
void unlock(){lock_ = false;}
bool is_locked() const{return lock_;}
uint16_t *get_16i_ptr()const{return &(cache_ptr_[0].val16i[0]);}
int get_16i_size() const{return (size_*sizeof(T)/sizeof(uint16_t));}
T *get_T_ptr()const{return &(cache_ptr_[0].tval);}
int size() const{return size_;}
private:
bool lock_ = false;
DataFormat fmt_;
int size_ = 0;
MutData<T> *cache_ptr_=nullptr;
};
2.4. 线圈寄存器缓存类
多个线圈寄存器的同时读写页同样使用堆缓存。
/*线圈寄存器缓存类*/
class CoilCache{
public:
CoilCache(int size):size_(size),cache_ptr_(new uint8_t[size]){}
~CoilCache(){delete[] cache_ptr_;}
CoilCache(const CoilCache&)=delete;
CoilCache& operator=(const CoilCache&)=delete;
void lock(){lock_ = true;}
void unlock(){lock_ = false;}
bool is_locked() const{return lock_;}
uint8_t *get_ptr(){return cache_ptr_;}
int size() const{return size_;}
private:
bool lock_ = false;
int size_ = 0;
uint8_t *cache_ptr_=nullptr;
};
三、ModbusTCP主机类
/*Modbus主机类;
实现了TCP/IP协议的读写功能,提供了异常信息,并提供了不同类型数据(float、int、unsigned int、long、unsigned long)读写的接口;
*/
class SunMdbMst{
public:
SunMdbMst(const string ip, int port);
~SunMdbMst(){
disconnect();
modbus_free(modbus_ctx_);
}
void connect();
void disconnect();
/*线圈读写*/
bool read_coil(int start_addr);
void read_coils(int start_addr, CoilCache& cache);
bool read_inputCoil(int start_addr);
void read_inputCoils(int start_addr, CoilCache& cache);
void write_coil(int start_addr, bool value);
void write_coils(int start_addr, CoilCache& cache);
/*寄存器高级读数据模板实现*/
template<typename T>
T read_reg_value(int start_addr, DataFormat fmt=DataFormat::ABCD){
MutData<T> data;
if(modbus_read_registers(modbus_ctx_,start_addr,sizeof(T)/sizeof(uint16_t),&(data.val16i[0]))==-1){
string err_msg=string{"Failed to read registers: "}+string{modbus_strerror(errno)};
throw runtime_error(err_msg);
}
//字节序转换
data.endiantrans(fmt);
return data.tval;
}
template<typename T>
void read_reg_values(int start_addr, LVCache<T>& cache){
if(cache.size()<1)
throw runtime_error("Invalid number of registers to read");
cache.lock();
if(modbus_read_registers(modbus_ctx_,start_addr,cache.get_16i_size(),cache.get_16i_ptr())==-1){
string err_msg=string{"Failed to read registers: "}+string{modbus_strerror(errno)};
cache.unlock();
throw runtime_error(err_msg);
}
cache.endiantrans();
cache.unlock();
//字节序转换
return;
}
template<typename T>
T read_inputreg_value(int start_addr, DataFormat fmt=DataFormat::ABCD){
MutData<T> data;
if(modbus_read_input_registers(modbus_ctx_,start_addr,sizeof(T)/sizeof(uint16_t),&(data.val16i[0]))==-1){
string err_msg=string{"Failed to read registers: "}+string{modbus_strerror(errno)};
throw runtime_error(err_msg);
}
//字节序转换
data.endiantrans(fmt);
return data.tval;
}
template<typename T>
void read_inputreg_values(int start_addr, LVCache<T>& cache){
if(cache.size()<1)
throw runtime_error("Invalid number of registers to read");
cache.lock();
if(modbus_read_input_registers(modbus_ctx_,start_addr,cache.get_16i_size(),cache.get_16i_ptr())==-1){
string err_msg=string{"Failed to read registers: "}+string{modbus_strerror(errno)};
cache.unlock();
throw runtime_error(err_msg);
}
cache.endiantrans();
cache.unlock();
//字节序转换
return;
}
/*寄存器高级写数据模板实现*/
template<typename T>
void write_value(int start_addr, T value, DataFormat fmt=DataFormat::ABCD){
MutData<T> data(value);
data.endiantrans(fmt);
if(modbus_write_registers(modbus_ctx_,start_addr,sizeof(T)/sizeof(uint16_t),&(data.val16i[0])==-1)){
string err_msg=string{"Failed to write registers: "}+string{modbus_strerror(errno)};
throw runtime_error(err_msg);
}
return;
}
template<typename T>
void write_values(int start_addr, LVCache<T>& cache){
cache.endiantrans();
if(modbus_write_registers(modbus_ctx_,start_addr,cache.get_16i_size,cache.get_16i_ptr())==-1){
string err_msg=string{"Failed to write registers: "}+string{modbus_strerror(errno)};
throw runtime_error(err_msg);
}
return;
}
private:
modbus_t *modbus_ctx_=nullptr;
bool is_connected_ = false;
};
四、类库中的接口说明
4.1. 创建对象
//通过所连接从机的IP地址与端口号来创建主机对象
//实参1:从机IP;实参2:从机端口号
//创建失败会抛出std::runtime error,并提示失败原因
SunMdbMst 对象名(const string ip, int port);
4.2. 建立连接
//无返回值
//无实参
//连接失败会抛出std::runtime error,并提示失败原因
对象名.connect();
4.3. 断开连接
//无返回值
//无实参
对象名.disconnect();
4.4. 读单个线圈
//返回值:bool
//实参:读取的从机线圈地址
//读取失败会抛出std::runtime error,并提示失败原因
bool 对象名.read_coil(int start_addr);
4.5. 读连续多个线圈
//无返回值
//实参1:从机连续多个线圈的起始地址;实参2:线圈数据缓存的引用
//读取失败会抛出std::runtime error,并提示失败原因
对象名.read_coils(int start_addr, CoilCache& cache);
4.6. 读输入线圈
//返回值:bool
//实参1:读取的从机线圈地址
//读取失败会抛出std::runtime error,并提示失败原因
bool 对象名.read_inputCoil(int start_addr)
4.7. 写单个线圈
//无返回值
//实参1:从机写入线圈的地址;实参2:写入的bool值
//写入失败会抛出std::runtime error,并提示失败原因
对象名.write_coil(int start_addr, bool value);
4.7. 写多个连续线圈
//无返回值
//实参1:从机写入连续多个线圈的起始地址;实参2:写入的bool值缓存
//写入失败会抛出std::runtime error,并提示失败原因
对象名.write_coils(int start_addr, CoilCache& cache);
4.8. 读单个保持寄存器的数值
//返回值:T类型的一个数值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:读取的保持寄存器的起始地址;实参2:从机的字节序,默认为大端字节序
//读取失败会抛出std::runtime error,并提示失败原因
T 对象名.read_reg_value<T>(int start_addr, DataFormat fmt=DataFormat::ABCD);
4.9. 读保持寄存器的连续多个数值
//无返回值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:读取的保持寄存器的起始地址;实参2:寄存器缓存对象的引用
//读取失败会抛出std::runtime error,并提示失败原因
对象名.read_reg_values<T>(int start_addr, LVCache<T>& cache);
4.10. 读单个输入寄存器的数值
//返回值:T类型的一个数值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:读取的保持寄存器的起始地址;实参2:从机的字节序,默认为大端字节序
//读取失败会抛出std::runtime error,并提示失败原因
T 对象名.read_inputreg_value<T>(int start_addr, DataFormat fmt=DataFormat::ABCD);
4.11. 读输入寄存器的连续多个数值
//无返回值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:读取的输入保持寄存器的起始地址;实参2:寄存器缓存对象的引用
//读取失败会抛出std::runtime error,并提示失败原因
对象名.read_inputreg_values<T>(int start_addr, LVCache<T>& cache);
4.12. 写入保持寄存器单个数值
//无返回值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:写入保持寄存器的起始地址;实参2:写入的数值;实参3:从机字节序
//写入失败会抛出std::runtime error,并提示失败原因
对象名.write_value<T>(int start_addr, T value, DataFormat fmt=DataFormat::ABCD);
4.13. 写入保持寄存器连续多个数值
//无返回值
//模板参数T为所要读取数据的类型,如int,long, float, double
//实参1:写入连续多个数值的保持寄存器起始地址;实参2:寄存器缓存对象的引用
//写入失败会抛出std::runtime error,并提示失败原因
对象名.write_values<T>(int start_addr, LVCache<T>& cache)
4.14. 线圈缓存的创建
//无返回值
//实参:线圈数量
CoilCache ccache(int size);
4.15. 寄存器数据缓存的创建
//无返回值
//实参1:数值的数量;实参2:数据源(从机)的字节序
LVCache lcache(int size, DataFormat fmt=DataFormat::ABCD)
五、简单示例
#include <SunModbus.hpp>
#include <iostream>
#include <windows.h>
int main() {
try {
SunMdbMst master("127.0.0.1", 1502);
master.connect();
short val1;
LVCache<short> icache(8,DataFormat::DCBA);
while (true) {
val1=master.read_reg_value<short>(0x0f,DataFormat::DCBA);
master.read_reg_values(0x0f,icache);
for(int i=0;i<icache.size();i++){
cout << "icache[" << i << "]: " << icache.get_T_ptr()[i] << endl;
}
Sleep(1000);
}
}
catch(const std::runtime_error& e) {
std::cerr << "Exception occurred:" << e.what() << std::endl;
return 1;
}
return 0;
}
六、遇到的问题
编译时,模板类中的方法和模板函数找不到。
解决办法:模板类和模板函数直接内联定义在头文件类,以避免出现编译时找不到函数实例的情况,这也是模板变差的一般做法。
总结
本文作为Modbus开发过程的随记文档,本人第一次使用泛型编程,大大减短了代码行数,集合了Modbus开发中常用的功能,后续将根据需要持续更新