ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

libmodbus 3.1.6在QT上位机中的集成与Modbus通信实战

libmodbus 3.1.6在QT上位机中的集成与Modbus通信实战 简介libmodbus-3.1.6-master 是一套面向嵌入式开发者的 Modbus 通信协议库源码包重点解决 libmodbus 在 ARM A7 架构 imx6ull 平台上的交叉编译与定制集成问题。资源共 158 个文件以 C 源文件、头文件、configure 配置脚本和 Makefile 工程文件为主体辅以大量文本说明、国际化翻译文件及少量文档压缩包仅 776KB结构紧凑便于直接导入 Linux 环境编译。目前已有 399 人学习下载。该包完整涵盖 Modbus RTU 与 TCP/IP 两种模式支持服务器端与客户端双重角色并针对嵌入式场景提供了从交叉编译工具链安装、./configure --hostarm-linux-gnueabihf 配置到 make install 部署的实践路径。结合 imx6ull 的 Cortex-A7 平台开发者可基于此包实现与 PLC、变频器、温控器等设备的工业级通信并根据项目需求进行功能扩展、性能优化或错误处理增强是嵌入式工业通信开发的实用参考资料。 开头先交代一下背景。我是在做上位机的时候接触到这个包的。当时项目里要用Modbus协议跟一块温控仪表通信GitHub上直接点了个仓库主页的Download ZIP落下来的文件名就是ewhales-libmodbus-3.1.6-master.zip。一眼扫过去大概就明白了libmodbus是源码包3.1.6是版本号master是分支名。这种命名方式在开源项目里其实很常见就是某个维护者基于libmodbus官方仓库做的fork打包成zip供人直接下载使用。如果你也是做PLC通信、数据采集、嵌入式网关这一类的东西或者正准备在QT里接入Modbus从站这个包值得好好研究一下。它解决的是工业现场中最基础、也最绕不开的那个问题用可靠、高效、跨平台的方式完成Modbus协议的主站/从站通信。1. 拿到ewhales-libmodbus-3.1.6-master.zip之后先搞清楚这是个什么东西1.1 版本与分支的含义别被文件名骗了很多人拿到这种zip包第一反应是解压、编译、跑demo结果不是configure报错就是链接失败。我建议你先花两分钟看一下文件名里的信息。libmodbus是法国开发者Stéphane Raimbault发起的老牌开源库用纯C写的专门实现Modbus协议。3.1.6是它的一个稳定版本号发布周期大概在2021年前后修复了此前版本中的一批bug尤其是串口通信超时处理和TCP连接复用方面的老问题。master后缀表示这是主分支的代码意味着它可能比官方打tag的版本新一些也可能夹带了fork作者本人的本地修改。ewhales这个前缀大概率是GitHub用户名的仓库名也就是说这个zip不是官方sourceforge或libmodbus官网发布的原始包而是某个开发者fork后打包的。这带来一个实际影响里面的代码结构、编译脚本、甚至个别源文件都可能和官方3.1.6版本存在差异。稳妥的做法是先看README和ChangeLog再决定是直接使用还是对照官方源码做一次diff。1.2 解压后的目录结构说明解压后你会看到典型的autotools工程布局configure.ac和Makefile.am这是autotools体系的构建脚本src/目录里面是modbus.c、modbus-rtu.c、modbus-tcp.c、modbus-data.c等核心源码tests/目录提供单元测试和集成测试的样例docs/目录包含API文档配置UNIT_TESTING/或tests/unit-testing子目录需要配合unity测试框架这一步值得特别说明libmodbus从诞生至今一直使用autotools作为官方构建系统官方并没有提供CMakeLists.txt。如果你习惯CMake工程要么自己写一个CMakeLists把src下的源文件包进去要么去找社区维护的libmodbus-cmake分支。相比之下autotools在Linux下非常顺手但Windows的Visual Studio工程没法直接吃这套构建体系这也是很多初学者卡住的地方。1.3 它到底能干什么一个讲人话的协议说明Modbus协议是工业自动化领域的事实标准定义了一套主从架构的请求-应答模型。主站发起请求从站响应。数据对象主要分4类线圈Coil、离散输入Discrete Input、保持寄存器Holding Register、输入寄存器Input Register。libmodbus把这个协议完整地实现了包括CRC校验、RTU帧格式、TCP帧格式、广播地址0、异常应答处理等。这意味着你不需要自己拼报文、算CRC、解析ACK直接调函数就能读写一个从站的寄存器数值。我在项目里最常用的就是读保持寄存器得到温度值再写保持寄存器修改PID设定参数整个通信链路的底层细节全交给libmodbus去处理。2. 从zip到能跑编译方式和集成选型的完整逻辑2.1 Linux下的标准编译流程如果你在Ubuntu或CentOS这类系统上开发编译流程很固定。先安装基础依赖sudo apt-get install autoconf automake libtool然后在源码根目录执行以下命令./autogen.sh ./configure --prefix/usr/local make -j4 sudo make install默认安装后头文件在/usr/local/include/modbus/目录下动态库是libmodbus.so静态库是libmodbus.a。注意configure阶段有几个可选参数值得关注--without-documentation跳过文档构建省时间--enable-static同时生成静态库--disable-shared只生成静态库适合嵌入式交叉编译如果你的板子是ARM架构的需要交叉编译那就在configure时指定--host参数例如./configure --hostarm-linux-gnueabihf --prefix/opt/arm-libs这一步我在做全志和瑞芯微平台的时候都试过整体流程很流畅唯一要注意的是交叉编译器必须提前加入到PATH环境变量里不然configure检测编译器会失败。2.2 Windows和QT环境下的集成选择我的项目是QT加MSVC编译链直接在Windows上跑没法享用autotools。经过实测最省心的接入方式是源码级集成把src/目录下的modbus.c、modbus-rtu.c、modbus-tcp.c、modbus-data.c以及对应的头文件直接拷到QT工程里然后在.pro文件里追加INCLUDEPATH $$PWD/third_party/libmodbus/src SOURCES \ $$PWD/third_party/libmodbus/src/modbus.c \ $$PWD/third_party/libmodbus/src/modbus-rtu.c \ $$PWD/third_party/libmodbus/src/modbus-tcp.c \ $$PWD/third_party/libmodbus/src/modbus-data.c这种方式有几个好处不需要额外编译动态库调试时可以单步进入Libmodbus源码查看收发细节也没有运行时DLL依赖问题。代价是编译时间稍微多了一点但对日常项目完全可以接受。如果你更喜欢动态链接库也可以用MinGW编译生成libmodbus.dll不过MSVC环境下调用MinGW编译的DLL会涉及到运行时库不一致的坑我建议MSVC工程就老老实实编译源码不折腾DLL。2.3 源码集成与动态库的取舍对照我自己在两种模式之间切换过不少次整理一下各自的适用情况集成方式适用场景优点缺点源码集成上位机软件、QT工程、非标准交叉编译环境调试方便、链接简单、可定制内部行为编译时间略长、源代码污染工程目录动态库多个程序共用一套通信库、快速原型部署灵活、不影响主工程构建需要单独处理链接路径和DLL分发静态库单机部署、需求稳定独立可执行、不依赖外部文件每次参数调整都要重新编库个人建议如果是做设备配套的专用上位机源码集成最省心如果是做通用型网关程序或者平台型软件建议编译成动态库方便后续独立升级通信模块。3. 核心API的使用逻辑从一段Hello World级别的代码入手3.1 建立上下文RTU和TCP两条路libmodbus的使用方式非常固定先创建一个modbus_t类型的上下文句柄然后基于这个句柄去设置参数和执行读写。串口RTU模式用modbus_new_rtu函数TCP模式用modbus_new_tcp函数。// RTU模式设备挂在/dev/ttyUSB0波特率9600无校验8数据位1停止位 modbus_t *ctx modbus_new_rtu(/dev/ttyUSB0, 9600, N, 8, 1); if (ctx NULL) { fprintf(stderr, Unable to create RTU context: %s\n, modbus_strerror(errno)); return -1; } modbus_set_slave(ctx, 1); // 从站地址TCP模式更简单只需要IP地址和端口号modbus_t *ctx modbus_new_tcp(192.168.1.100, 502);这里有个很容易忽略的细节modbus_new_tcp创建的是服务器模式也就是作为Modbus主机连接远端从站modbus_new_tcp_pi是面向IPv6的版本。如果你的设备支持IPv6网络优先用_pi版本。3.2 连接管理connect、close和超时设置创建上下文之后需要建立连接if (modbus_connect(ctx) -1) { fprintf(stderr, Connection failed: %s\n, modbus_strerror(errno)); modbus_free(ctx); return -1; }连接成功后可以设置超时时间这一步直接影响通信稳定性。默认超时其实比较长在工业现场如果从站响应慢程序就会傻等。建议设置一个合理的秒数和微秒数struct timeval response_timeout; response_timeout.tv_sec 1; response_timeout.tv_usec 0; modbus_set_response_timeout(ctx, response_timeout);同时调用modbus_set_error_recovery(ctx, MODBUS_ERROR_RECOVERY_LINK | MODBUS_ERROR_RECOVERY_PROTOCOL)让库在通信异常时自动做连接重置。这条我强烈建议加上尤其是串口环境不稳定的时候能省掉很多重启设备的麻烦。3.3 读写寄存器的几个常用函数读写寄存器是项目里最常用的操作。读保持寄存器用modbus_read_registers写单个保持寄存器用modbus_write_register写多个保持寄存器用modbus_write_registers。线圈读写对应modbus_read_bits和modbus_write_bits。一个简单的读取示例uint16_t tab_reg[32]; int rc modbus_read_registers(ctx, 0, 10, tab_reg); if (rc -1) { fprintf(stderr, Read failed: %s\n, modbus_strerror(errno)); } else { for (int i 0; i rc; i) { printf(reg[%d] %d\n, i, tab_reg[i]); } }这里有个容易踩坑的返回值理解modbus_read_registers返回的是成功读取的寄存器数量不是0表示成功。如果读取失败返回值是-1并且errno被设置。很多新手习惯用if (rc)判断错误这在读取0个寄存器时会有逻辑问题。实测中libmodbus是不会返回0的——要么返回读取数量要么返回-1但如果你的代码逻辑习惯不好迟早会翻车。3.4 从一帧报文的视角理解整个调用链我习惯把整个调用过程对应到Modbus协议的报文上去理解。比如读取从站地址为1的设备的保持寄存器0到9主站发送请求帧01 03 00 00 00 0A C5 CD01从站地址03功能码0000起始地址000A寄存器数量C5CD是CRClibmodbus内部封装了整帧的组装和CRC计算从站返回响应帧01 03 14 数据区共20个字节...libmodbus负责解析和校验返回帧的数据长度及CRC理解这一层后调试的时候你就会看报文而不是瞎猜。比如返回的异常码是02非法数据地址你就知道请求的寄存器超出了从站映射范围不需要去怀疑串口线松了。4. QT项目接入libmodbus的一段真实经历4.1 为什么不用QModbusDevice偏要选libmodbus我承认QT自带的QModbusDevice在Qt SerialBus模块里用起来也很方便尤其是信号槽机制让异步编程非常舒服。但在我的实际场景里要么是旧项目从纯C代码迁移过来要么是需要自定义功能码和特殊报文格式QModbusDevice的封装反而成了限制。libmodbus更底层灵活性更强而且对内存和引脚级别的控制更直接。更关键的一点是QModbusDevice在Qt5.14之后才默认编译进Qt SerialBus模块如果你的QT版本较老或者裁剪过模块它就不可用。libmodbus没有这个依赖问题。4.2 .pro工程文件组织方式我的工程结构是这样的my_project/ ├── main.cpp ├── modbus_worker.h ├── modbus_worker.cpp └── third_party/ └── libmodbus/ └── src/在.pro文件里添加QT core TARGET modbus_demo CONFIG console CONFIG - app_bundle TEMPLATE app SOURCES \ main.cpp \ modbus_worker.cpp \ third_party/libmodbus/src/modbus.c \ third_party/libmodbus/src/modbus-rtu.c \ third_party/libmodbus/src/modbus-tcp.c \ third_party/libmodbus/src/modbus-data.c HEADERS \ modbus_worker.h \ third_party/libmodbus/src/modbus.h \ third_party/libmodbus/src/modbus-rtu.h \ third_party/libmodbus/src/modbus-tcp.h \ third_party/libmodbus/src/modbus-version.h INCLUDEPATH \ $$PWD/third_party/libmodbus/srcWIN32下还必须留意一点libmodbus源码里用到了strcasecmp函数MSVC不认这个需要在win环境下加一个宏映射。我是在modbus_worker.h里加了#ifdef _WIN32 #define strcasecmp _stricmp #endif不然编译100%报错这是QT Windows接入libmodbus的经典坑之一。4.3 串口通信不能阻塞UI线程QT的UI线程必须保持响应而libmodbus的读写函数默认是阻塞的。如果在QMainForm里直接调用modbus_read_registers界面会卡死。解决的思路有几种开独立QThread用QtConcurrent异步执行或者配合QSocketNotifier做事件驱动。我测试下来最稳的是开一个QThread专门负责modbus轮询线程内做阻塞读写通过信号把结果传回UI线程。下面是线程的核心结构class ModbusWorker : public QThread { Q_OBJECT public: void run() override { modbus_t *ctx modbus_new_rtu(COM3, 9600, N, 8, 1); modbus_set_slave(ctx, 1); modbus_connect(ctx); while (!m_stop) { uint16_t data[16]; int rc modbus_read_registers(ctx, 0, 16, data); if (rc 0) { emit dataReady(data, rc); } QThread::msleep(500); } modbus_close(ctx); modbus_free(ctx); } };这种方式的好处是逻辑清晰、容易控制轮询周期坏处是要注意线程退出时不能还在阻塞的modbus_read里。我在stop函数里用了一个m_stop标志但在串口阻塞时线程可能停不下来。稳妥的做法是先把serial连接关闭让阻塞的调用返回-1再让线程退出。4.4 实测数据QT调用libmodbus的性能表现在一个实际的小项目里我用QT加libmodbus读取一个224位保持寄存器的设备波特率96008位数据无校验。实测连续读取1000轮单轮耗时大约25毫秒无超时、无丢包。换成TCP方式连接同一台设备的以太网网关单轮读取时间降到2毫秒左右。这说明TCP模式性能比RTU高出很多如果现场条件允许优先用TCP链路。5. 那些容易翻车的坑编译报错、时序异常和线程安全5.1 Windows下编译时第二常见的问题socket链接告警除了strcasecmp问题Windows下链接modbus-tcp.c时还会报unresolved external symbol __imp__...这类socket相关错误。原因很简单libmodbus的TCP代码依赖Windows的Winsock库但autotools构建环境下只对Linux平台自动链接socket库Windows下手动集成时需要在.pro里加LIBS -lws2_32或者在你的工程预编译头文件里加#pragma comment(lib, ws2_32.lib)这一步漏掉的话编译会卡在链接阶段报错信息非常不直观我第一次遇到时排查了很久。5.2 RTU模式下死亡时间与粘包问题RTU模式使用串口传输数据而串口没有TCP的帧边界概念。libmodbus通过Modbus标准规定的3.5个字符时间间隙来区分不同的数据帧。如果你设置的波特率很低比如1200波特率3.5字符时间大约是30毫秒通信轮询周期必须大于这个间隙。实测中还遇到过另一个粘包问题当程序连续发起多个寄存器读取请求设备响应特别快时主板串口缓冲区里可能同时堆了两帧数据后端只能解析出第一帧。解决办法是在读取数据后清空一次串口缓冲区。libmodbus提供了modbus_flush函数modbus_flush(ctx);我在每次轮询调用modbus_read_registers之后主动调用modbus_flush忽略掉残余字节实测粘包率大幅下降。但也要注意flush用得太勤可能把合法响应末尾的帧间隔也给清了要根据波特率和帧长辩证对待。5.3 线程安全的真面目libmodbus的上下文结构体modbus_t在源码里是这样设计的只允许单线程内使用一个上下文。也就是说同一个modbus_t指针不能同时在两个线程里调用读写函数否则CRC计算和接收缓冲区的状态会被互相污染出现的现象是读到的寄存器值随机错乱有时候还会报header错误。我的处理方案有两种第一种是加互斥锁把整个modbus调用的临界区锁住QMutex modbus_mutex; QMutexLocker locker(modbus_mutex); int rc modbus_read_registers(ctx, 0, 10, tab_reg);第二种是每个线程创建独立的modbus_t实例各自连接串口或网络。这种方式天然并行但会占用多个串口或TCP连接。如果你的设备只允许一个连接就得用锁。我在实际网关项目里用的是第二种四个采集线程分别读四台串口设备每个线程创建自己的RTU上下文指向不同的串口设备文件。这样既避免了锁竞争也让单个设备故障只影响对应的独立线程。5.4 数据校验与返回值判断的坑libmodbus在接收到从站响应后自动完成CRC校验和功能码匹配如果校验失败会返回-1并设置errno为EBADMSG或EINVAL。但有些场合我们需要自己判断数据合法性比如从站设备型号特殊返回的字节序跟广茂旧称汇川或施耐德的寄存器高低字节顺序不一致。这里有个实际操作建议在库里遇到返回错误时先打印modbus_strerror(errno)再配合串口调试工具看原始字节流。去年我花了一整天排查一个偶发性的读取错误最后发现是仪表在特定寄存器地址上返回了超过预期长度的数据触发libmodbus的长度检查。换用modbus_set_quirks配置兼容模式后问题解决。6. 最后的两个使用心得这个库用久了你会发现它的很多默认配置其实是为稳定和完整而设计的不一定适合你的现场。比如默认RTU字节序是大端模式符合Modbus规范但某些国产设备偏偏用小端。好在3.1.6版本的libmodbus提供了字节序设置的函数modbus_set_byte_order实测可以直接支持乱序调整。如果你遇到的寄存器数据高低字节互换不用自己写掩码转换直接调这个函数就行。再分享一个关于串口参数的细节在接入新的从站设备时先不要急着在程序里写死串口参数先在终端里用modbus通信调试工具比如Modbus Poll验证一下设备的波特率、数据位、校验位和停止位确认无误后再把参数填进libmodbus的modbus_new_rtu调用里。这个习惯让我省去了大量排错时间因为从站设备对串口参数异常通常不会有明确指示只是响应超时或应答内容一团乱码。ewhales-libmodbus-3.1.6-master.zip这个包本质上就是libmodbus官方版本的一个镜像或再分发但它让我第一次真正认识到一个成熟的通信库该有的设计长什么样。无论是应付PLC通信、自动化仪表采集还是在QT上位机里构建稳定的数据通道libmodbus都像是那个你一开始觉得多余、后面却离不开的基础设施。拿到这个zip后你完全可以按我上面的步骤在二十分钟内跑通第一个寄存器读取然后你就会发现Modbus协议的那点复杂细节其实已经被处理得相当干净了。本文还有配套的精品资源点击获取
返回列表