Libmodbus 源码总体分析:框架、数据结构与核心函数详解

第一部分:理论模型回顾

在深入代码之前,我们必须先理解 libmodbus 要实现的理论模型。这两张图是整个库设计的灵魂。

1. ADU 与 PDU:协议的分层

  • 图片内容:此图展示了 Modbus 协议的数据帧结构。

  • 核心概念

    • PDU (协议数据单元) :这是 Modbus 协议的核心,与具体网络无关。它只有两部分:功能码 + 数据。功能码(如0x01读线圈)指定操作,数据区提供具体参数(如地址、数量)。

    • ADU (应用数据单元):这是在实际网络上传输的完整数据包。它在 PDU 的基础上,根据网络类型增加了"包装"。

      • 串口 (RTU) :在 PDU 前面加从站地址 ,后面加CRC校验码

      • 以太网 (TCP) :在 PDU 前面加一个更复杂的MBAP头(包含事务ID、协议标识、长度等)。

    • 软件库的任务:libmodbus 的核心工作之一,就是帮我们自动完成 PDU 到 ADU 的"打包"和"解包",这样我们程序员就不用自己处理这些琐碎的字节拼接了。

2. 事务处理模型:通信的流程

  • 图片内容:清晰地展示了 Modbus "请求-响应"的完整流程。

  • 核心概念

    • 这是一个典型的客户端/服务器(主站/从站) 模型。

    • 主站(客户端):发起"请求"(包含地址、功能码、数据)。

    • 从站(服务器):接收请求 -> 执行操作(如读取线圈)-> 返回"响应"(包含地址、功能码、结果数据)。

    • 异常处理:如果从站处理出错(比如地址不存在),它会返回一个"异常响应"(功能码最高位置1,并附带错误码)。

    • 软件库的任务:libmodbus 为我们封装了建立连接、发送请求、接收并解析响应的整个流程。


第二部分:代码框架与初始化

现在,我们进入代码世界。这几张图展示了如何使用 libmodbus 库,以及库内部如何初始化。

3. 库的初始化与连接建立

  • 图片内容 :这几张图都是 C 代码片段,展示了如何创建 Modbus 上下文 (modbus_new_*)、配置参数,并建立连接 (modbus_connect)。它们非常相似,是不同应用场景的例子。

  • 保姆级解析

    1. 选择后端 :libmodbus 支持多种通信方式(RTU串口、TCP网络)。你需要先告诉库你想用哪一种。代码中通过 if...else 分支来选择。

      cs 复制代码
      if (use_backend == TCP) {
          ctx = modbus_new_tcp("192.168.1.100", 502); // 创建TCP连接上下文
      } else {
          ctx = modbus_new_rtu("/dev/ttyUSB0", 115200, 'N', 8, 1); // 创建RTU串口上下文
      }
      • modbus_new_tcp:用于网络通信,参数是 IP 地址和端口号(通常是502)。

      • modbus_new_rtu:用于串口通信,参数是串口设备名(如COM1、/dev/ttyS0)、波特率、校验位、数据位、停止位。

      • 返回值 ctx :这是一个非常重要的 modbus_t 类型的指针。你可以把它理解为一个 "Modbus连接句柄"或"遥控器" 。后续所有的操作(读、写、设置)都需要通过这个 ctx 来进行。

    2. 配置参数:创建上下文后,可以进行一些配置。

      • modbus_set_debug(ctx, TRUE);:打开调试模式。库会把发送和接收的原始字节数据打印出来,调试神器

      • modbus_set_slave(ctx, 1);(仅RTU模式) 设置从站地址。当你作为主站时,这个函数设置的是你将要访问的从站设备的地址

    3. 建立连接 :配置完成后,调用 modbus_connect(ctx)。对于TCP,这个函数会去连接目标服务器;对于RTU,它会打开串口设备。如果失败,会返回-1。

  • 一句话总结展示了使用 libmodbus 编程的标准开头------"三板斧":1) modbus_new_* 创建上下文;2) modbus_set_* 配置参数;3) modbus_connect 建立连接。


第三部分:核心数据结构

理解了如何创建连接,现在看看 libmodbus 内部是如何组织和管理这些连接信息的。

4. 核心数据结构:struct _modbus

  • 图片内容 :这两张图展示了 libmodbus 库中最核心的数据结构 struct _modbus(在代码中通常用 modbus_t 这个别名)的定义。它就像一个"连接信息档案袋"。

  • 保姆级解析:我们拆开这个结构体,看看里面都装了些什么宝贝:

    cs 复制代码
    struct _modbus {
        int slave;       // 【从站地址】对于主站,它表示要访问的从站ID;对于从站,它表示自己的ID。
        int s;           // 【套接字/文件描述符】这是操作系统级别的通信句柄。TCP对应网络socket,RTU对应串口文件。
        int debug;       // 【调试开关】就是我们前面用 `modbus_set_debug` 设置的那个。
        const modbus_backend_t *backend; // 【后端指针】这是**灵魂所在**!指向一个结构体,里面全是函数指针。
        void *backend_data; // 【后端私有数据】给后端函数自己用的数据,比如TCP的IP信息,RTU的串口参数。
        // ... 还有一些超时时间等字段
    };
    • backend 指针是关键! 它指向一个 modbus_backend_t 结构体。这个结构体里定义了一大堆函数指针,比如:

      • send:指向 modbus_rtu_sendmodbus_tcp_send

      • receive:指向 modbus_rtu_receivemodbus_tcp_receive

      • build_request_basis:指向构建请求报文的函数(区分TCP和RTU的报文头)。

    • 为什么这样设计? 这种设计模式叫 "策略模式""插件架构"struct _modbus 是通用框架,而 backend 是具体的实现插件。当你用 modbus_new_rtu 创建上下文时,backend 就被赋值为指向 RTU 后端的函数集。这样,上层代码(比如 modbus_read_bits)只需要调用 ctx->backend->send(...),实际执行的就是对应的 RTU 或 TCP 发送函数。这实现了代码的完美解耦和复用。

  • 一句话总结struct _modbus 是 libmodbus 库的心脏,它用 backend 指针实现了多协议支持,让同一套上层API可以操作不同的底层网络。


第四部分:核心工作流程 (图片5, 6, 9)

现在,我们来看库具体是怎么工作的。这几张图揭示了主站发送请求和从站处理请求的内部循环。

5. 主站:发送请求与接收响应的流程

  • 图片内容 :这张图展示了一个典型的主站"写入单个位"(modbus_write_bit)函数内部的执行流程。

  • 保姆级解析(跟着代码走)

    1. 构建请求ctx->backend->build_request_basis(ctx, function, addr, value, req);

      • 调用 backend 里的函数,根据是RTU还是TCP,把功能码、地址、数据等参数,组装成正确的 ADU 报文 ,存到 req 缓冲区里。
    2. 发送报文rc = send_msg(ctx, req, req_length);

      • 调用 send_msg 函数(下图 ),它最终会调用 ctx->backend->send,通过socket或串口把数据发出去。
    3. 接收响应rc = modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);

      • 等待并接收从站返回的数据,存到 rsp 缓冲区。
    4. 检查确认rc = check_confirmation(ctx, req, rsp, rc);

      • 校验响应报文:地址对不对?功能码对不对(异常响应?)?数据对不对?CRC校验对不对?
    5. 返回结果:把成功或失败的结果返回给用户。

  • 流程总结构建 -> 发送 -> 接收 -> 校验 -> 返回 。这是所有读/写函数(modbus_read_bits, modbus_write_register等)的通用模板。

6. send_msg 函数详解

  • 图片内容 :这是上上图中 send_msg 函数的内部实现。

  • 保姆级解析

    cs 复制代码
    static int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length) {
        // 第一步:可能做一些发送前的预处理,比如RTU模式下计算并添加CRC
        msg_length = ctx->backend->send_msg_pre(msg, msg_length);
        // 第二步:如果打开了调试,把要发送的字节流打印出来(就是我们抓包看到的样子!)
        if (ctx->debug) {
            for (i = 0; i < msg_length; i++) printf("[%.2X]", msg[i]);
        }
        // 第三步:调用后端真正的发送函数,可能循环发送直到成功(如果设置了错误恢复)
        do {
            rc = ctx->backend->send(ctx, msg, msg_length);
        } while (rc == -1 && (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK));
        return rc;
    }
    • 这个函数是连接"通用逻辑"和"具体后端实现"的桥梁。它处理了调试输出链接错误恢复 等通用逻辑,然后把真正的发送任务派发给 backend->send

7. 从站:请求处理循环

  • 图片内容 :这张图展示了一个 Modbus 从站服务器的主循环代码。

  • 保姆级解析

    cs 复制代码
    for (;;) { // 无限循环,一直等待请求
        do {
            rc = modbus_receive(ctx, query); // 1. 接收请求报文
        } while (rc == 0); // 可能过滤掉一些非本从站的请求
        // ... 这里可能有一些特殊处理,比如模拟错误响应 ...
        rc = modbus_reply(ctx, query, rc, mb_mapping); // 2. 核心:处理请求并回复
        if (rc == -1) { break; } // 处理出错则退出
    }
    • modbus_receive:从网络或串口读取一个完整的 Modbus 请求 ADU。

    • modbus_reply:这是从站的核心处理函数。它:

      1. 解析请求报文(解包ADU,得到PDU里的功能码和地址)。

      2. 根据功能码,去一个叫 mb_mapping 的内存映射表里(这个表模拟了线圈、寄存器等区域)读取或写入数据。

      3. 根据操作结果,构建一个正常的响应报文或异常响应报文。

      4. 调用 backend->send 把响应发回去。

    • mb_mapping :这是一个重要的数据结构,由用户创建并传递进来。它包含了线圈、离散输入、保持寄存器、输入寄存器这四大区域在内存中的实际数组。从站设备的所有数据都"生活"在这个映射表里。


第五部分:应用场景与总结

8. 实际应用场景

  • 图片内容:这是一张硬件拓扑图,展示了工业场景中如何使用 Modbus。

  • 核心讲解

    • 上位机 :通常是一台电脑或工控机,运行着用 libmodbus 编写的主站程序

    • 通信链路

      • RS-485总线 :一种常见的串行通信标准,可以挂接多个设备。上位机通过 USB转485串口卡 连接到总线。

      • 以太网:有些高级设备支持 Modbus TCP,可以直接接入网络。

    • 从站设备 :各种传感器、仪表、PLC等。每个设备都有一个唯一的从站地址(1-247)。

    • 非Modbus设备 :需要额外的协议转换网关才能接入Modbus网络。

  • Libmodbus 的角色:在上位机的软件中,libmodbus 就是那个负责与下面所有 Modbus 设备"对话"的通信引擎。

最终总结:Libmodbus 框架精要

让我们把所有的知识点串起来,画一张完整的"心智图":

  1. 设计模式 :采用 "前后端分离" 的插件式架构。

    • 前端 (libmodbus API) :提供统一的、友好的函数接口给程序员使用(如 modbus_read_registers)。

    • 后端 (Backend):包含 RTU 和 TCP 两种实现,每种实现都提供一组标准的功能函数(send, receive, build_header...)。

    • 连接上下文 (modbus_t) :作为粘合剂,内部有一个 backend 指针,决定实际调用的后端函数。

  2. 工作流程

    • 主站 (客户端)

      cs 复制代码
      用户调用 modbus_write_bit() 
        -> 库函数内部调用 backend->build_request_basis() 打包报文 
        -> 调用 send_msg() (内部调用 backend->send()) 发送 
        -> 调用 modbus_receive_msg() 接收响应 
        -> 调用 check_confirmation() 验证响应 
        -> 返回结果给用户
    • 从站 (服务器)

      cs 复制代码
      无限循环 {
        调用 modbus_receive() 等待请求 
        调用 modbus_reply() {
            解析请求
            操作 mb_mapping 内存映射表
            构建响应
            调用 backend->send() 发送响应
        }
      }
  3. 学习意义

    • 理解工业通信:通过源码,你能彻底明白 Modbus 协议如何在字节流层面工作。

    • 掌握优秀设计backend 指针和 modbus_t 结构体是抽象与接口编程的典范,这种设计模式在大型软件和驱动开发中极其常见。

    • 提升调试能力 :知道了 debug 开关和 send_msg 函数,你就能理解为什么 libmodbus 能打印出原始报文,这对解决现场通信问题至关重要。

相关推荐
Remember_9932 小时前
Spring 中 REST API 调用工具对比:RestTemplate vs OpenFeign
java·网络·后端·算法·spring·php
Y_032 小时前
浅谈Java虚拟机JVM
java·开发语言·jvm
我命由我123452 小时前
JUnit - 自定义 Rule
android·java·开发语言·数据库·junit·java-ee·android-studio
电商API&Tina2 小时前
【电商API】淘宝/天猫拍立淘(按图搜索商品)API 全解析
大数据·开发语言·数据库·人工智能·json·图搜索算法
XerCis2 小时前
Python读取硬盘信息pySMART——调用smartctl
开发语言·python·硬件架构
多多*2 小时前
程序设计工作室1月28日内部训练赛 题解
java·开发语言·windows·哈希算法·散列表
2501_915921432 小时前
在没有源码的前提下,怎么对 Swift 做混淆,IPA 混淆
android·开发语言·ios·小程序·uni-app·iphone·swift
weixin_446504222 小时前
Akshare:一个实用的免费金融数据Python库
开发语言·python·金融
IT陈图图5 小时前
构建 Flutter × OpenHarmony 跨端带文本输入对话框示例
开发语言·javascript·flutter