前言
modbus在上下位机数据交互时被广泛使用,因此写了这篇笔记和大家一起学习。
参考文献
Libmodbus源码分析(二)-常用接口函数分析
Libmodbus源码分析(四)-RTU相关函数分析
【Qt Modbus通信】QModbus实现modbus的主机功能 源码分享
第三方库 libmodbus
网上有一个现成的libmodbus C库,支持Linux, Mac OS X, FreeBSD, QNX 和 Win32。
下载地址为:http://libmodbus.org/download/
基于libmodbus开源的学习例程Qmodbus
亲测源码支持QT4/QT5版本,在window和Linux环境下都可以使用
官网:http://qmodbus.sourceforge.net/
源码:https://github.com/ed-chemnitz/qmodbus/
如果下载不了也可以在我的GitHub上下载
该DEMO支持 RTU/TCP/ASCII模式
下面我将DEMO中的关键代码移植出来,实现了一个运行在子线程中且包含了主机功能和从机功能的程序。
项目配置
我将需要用到的libmodbus源码放在了这个路径下 大家可以在我的GitHub上自行下载
需要用到的文件有
需要注意的地方
加上以下内容就可以在运行在window或ubuntu环境下
unix {
SOURCES += 3rdparty/qextserialport/posix_qextserialport.cpp \
3rdparty/qextserialport/qextserialenumerator_unix.cpp
DEFINES += _TTY_POSIX_
}
win32 {
SOURCES += 3rdparty/qextserialport/win_qextserialport.cpp \
3rdparty/qextserialport/qextserialenumerator_win.cpp
DEFINES += _TTY_WIN_ WINVER=0x0501
LIBS += -lsetupapi -lws2_32
}
主机功能
程序运行效果
完成功能:modbus主机在子线程中每隔一秒钟向modbus Slave 请求 40030-40035寄存器的内容。
关键代码演示
1.将modbusPollThread类放到子线程中
slaveID 指定读取从机的ID地址
为什么要子线程中运行:因为读取从机数据是一个比较耗时的操作如果放在主线程运行会导致QT的UI界面卡顿
modbusPollThread::modbusPollThread(int slaveID, QObject *parent)
{
qDebug() << "modbusPollThread" << QThread::currentThreadId();
m_slaveId = slaveID; //从机ID
isWork = false; //modbus是否连接成功
m_pollThread = new QThread();
this->moveToThread(m_pollThread);
connect(m_pollThread,SIGNAL(started()),this,SLOT(initPollThread()));
connect(m_pollThread,SIGNAL(finished()),this,SLOT(ClosePollThread()));
m_pollThread->start();
}
2.连接从机
特别注意:
modbus_register_monitor_add_item_fnc
modbus_register_monitor_raw_data_fnc 这两个回调函数可以帮助你获取更多的主从交互信息
void modbusPollThread::connnectModbusPoll(QString portName, int baud)
{
qDebug() << "connnectModbusPoll" << QThread::currentThreadId();
my_bus = modbus_new_rtu(portName.toLatin1().data(),baud,'N',8,1); //无法验证串口端口是否被占用
modbus_set_slave(my_bus,m_slaveId); //设置从机地址为1
modbus_connect(my_bus);
//寄存器map初始化
mb_mapping = modbus_mapping_new(READSTARTADDR, READSTARTADDR,
READSTARTADDR, READSTARTADDR); //依次设置 bits、input_bits、registers、input_registers寄存器的大小,他们默认起始地址均为0
if (mb_mapping == NULL) {
qDebug() << "connect fail" << (stderr, "Failed to allocate the mapping: %s\n",
modbus_strerror(errno));
modbus_free(my_bus);
return;
}
modbus_register_monitor_add_item_fnc(my_bus, modbusPollThread::stBusMonitorAddItem);
modbus_register_monitor_raw_data_fnc(my_bus, modbusPollThread::stBusMonitorRawData);
Time_one->start(1000);
isWork = true;
}
3.设置modbus回调函数
//modbus回调函数
void modbusPollThread::stBusMonitorAddItem( modbus_t * modbus, uint8_t isRequest, uint8_t slave, uint8_t func, uint16_t addr, uint16_t nb, uint16_t expectedCRC, uint16_t actualCRC )
{
Q_UNUSED(modbus);
qDebug() << "成功接受到从机回复" << "isRequest" << isRequest << "slave" << slave << "func" << func\
<< "addr" << addr << "nb" << nb << "expectedCRC" << expectedCRC << "actualCRC" << actualCRC;
}
// 送成功/接受成功会到此回掉函数
void modbusPollThread::stBusMonitorRawData( modbus_t * modbus, uint8_t * data, uint8_t dataLen, uint8_t addNewline , uint8_t direction)
{
Q_UNUSED(modbus);
QString dump;
for( int i = 0; i < dataLen; ++i )
{
dump += QString::asprintf( "%.2x ", data[i] );
}
// if(direction == 0)
// qDebug() << "串口发送" << "data" << dump;
// else
// qDebug() << "串口接受" << "data" << dump;
}
4.读取从机数据
4.1读取保持寄存器内容使用接口 modbus_read_registers
4.2读取输入寄存器使用接口 modbus_read_input_registers
4.3下面的函数的功能为 向从机ID为m_slaveId的从机读取 40030-40034的寄存器内容
如果modbus_read_registers的返回值不等于读取的寄存器长度 则说明读取失败
uint16_t modbus_hold_reg[100]; //缓存读取到的数据
void modbusPollThread::modbus_update_text()
{
int readNum = 5;
modbus_set_slave(my_bus,m_slaveId);//设置需要连接的从机地址
int ret = modbus_read_registers(my_bus,40030,readNum,modbus_hold_reg); //读取保持寄存器的第0位开始的前5位
//modbus_read_input_registers(my_bus,0,50,modbus_input_reg); //读取输入寄存器的第0位开始的前5位
QString err;
if(ret != readNum) //读取错误
{
if( ret < 0 )
{
if(
#ifdef WIN32
errno == WSAETIMEDOUT ||
#endif
errno == EIO
)
{
err += tr( "I/O error" );
err += ": ";
err += tr( "did not receive any data from slave." );
}
else
{
err += tr( "Protocol error" );
err += ": ";
err += tr( "Slave threw exception '" );
err += modbus_strerror( errno );
err += tr( "' or function not implemented." );
}
}
else
{
err += tr( "Protocol error" );
err += ": ";
err += tr( "Number of registers returned does not "
"match number of registers requested!" );
}
}
if( err.size() > 0 )
emit SIGNAL_SENDGETSLAVEINFO("读取失败:" + err);
else //读取正常
{
QString info = QString("从机ID: %1 modbus read : %2 %3 %4 %5 %6 \r\n ")\
.arg(QString::number(m_slaveId)).arg(modbus_hold_reg[0])\
.arg(modbus_hold_reg[1]).arg(modbus_hold_reg[2])\
.arg(modbus_hold_reg[3]).arg(modbus_hold_reg[4]);
emit SIGNAL_SENDGETSLAVEINFO(info);
}
}
5.UI主线程中 使用主机功能和从机功能
5.1实例化
//传入主机ID 1
m_slave = new modbusSlaveThread(1);
connect(this,&MainWindow::signal_connectSlave,m_slave,&modbusSlaveThread::connnectModbusSlave);
connect(this,&MainWindow::signal_disconnectSlave,m_slave,&modbusSlaveThread::disconnnectModbusSlave);
//传入从机ID 1
m_poll = new modbusPollThread(1);
connect(this,&MainWindow::signal_connectPoll,m_poll,&modbusPollThread::connnectModbusPoll);
connect(this,&MainWindow::signal_disconnectPoll,m_poll,&modbusPollThread::disconnnectModbusPoll);
connect(m_poll,&modbusPollThread::SIGNAL_SENDGETSLAVEINFO,this,&MainWindow::onShowSlaveInfo);
5.2 开启主机/关闭主机
void MainWindow::on_pushButton_Poll_clicked()
{
if(ui->pushButton_Poll->text() == "开启主机")
{
if(ui->comboBox_name_POLL->currentText().isEmpty()==true)
{
ui->textEdit_POLL->setText(ui->textEdit_POLL->toPlainText().append("未设置设备号\r\n"));
ui->textEdit_POLL->moveCursor(QTextCursor::End); //textedit 滚动条自动往下滚动
return;
}
QString namestring = ui->comboBox_name_POLL->currentText();
uint modbus_baud = ui->comboBox_baud_POLL->currentText().toUInt();
//发送信号 开启主机
emit signal_connectPoll(namestring,modbus_baud);
ui->pushButton_Poll->setText("关闭主机");
}else {
//发送信号关闭主机
emit signal_disconnectPoll();
ui->pushButton_Poll->setText("开启主机");
}
}
从机功能
程序运行效果
使用modbus Poll定时向程序读取 寄存器1-5的内容
关键代码演示
从机代码和主机代码差不多
1.请求连接主机
1.1 设置保持寄存器的值 修改tab_registers数组
1.2 设置输入寄存器的值 修改tab_input_bits数组
void modbusSlaveThread::connnectModbusSlave(QString portName, int baud)
{
qDebug() << "connnectModbusSlave" << QThread::currentThreadId();
my_bus = modbus_new_rtu(portName.toLatin1().data(),baud,'N',8,1);
modbus_set_slave(my_bus,m_Id); //设置从机地址为1
modbus_connect(my_bus);
//寄存器map初始化
mb_mapping = modbus_mapping_new(40099, 40099,
40099, 40099); //依次设置 bits、input_bits、registers、input_registers寄存器的大小,他们默认起始地址均为0
if (mb_mapping == NULL) {
qDebug() << "connect fail" << (stderr, "Failed to allocate the mapping: %s\n",
modbus_strerror(errno));
modbus_free(my_bus);
return;
}
mb_mapping->tab_registers[1] = 77; //设置一下hold寄存器的值
mb_mapping->tab_registers[2] = 77;
mb_mapping->tab_registers[3] = 3;
mb_mapping->tab_registers[4] = 4;
mb_mapping->tab_registers[5] = 5;
modbus_register_monitor_add_item_fnc(my_bus, modbusSlaveThread::stBusMonitorAddItem);
modbus_register_monitor_raw_data_fnc(my_bus, modbusSlaveThread::stBusMonitorRawData);
Time_one->start(300);
isWork = true;
}
2.定时轮询判断是否有主机向从机请求数据
modbus_receive 返回值大于0说明主机向从机发送了请求
void modbusSlaveThread::modbus_slave_work()
{
int rc;
//qDebug() << "modbus_slave_work" << QThread::currentThreadId();
uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
//轮询接收数据,并做相应处理
rc = modbus_receive(my_bus, query);
if (rc > 0) {
modbus_reply(my_bus, query, rc, mb_mapping);
}
}
调试工具
破解好的调试工具大家可以直接在我的GitHub上下载
在没用设备可以调试程序的时候,我们可以使用虚拟串口工具Virtual Serial Port Driver Pro
Modbus Poll 是Modbus主设备模拟软件
Modbus Slave 是Modbus从设备模拟软件
源码下载:https://github.com/jbyyy/libmodbusDemo
ModbusSlave_Poll文件夹是该文章的源码
qmodbus-master.zip是Qmodbus软件的开源程序