libmodbus 源码分析_0

libmodbus源码分析

libmodbus 是一个免费的跨平台支持 RTU 和 TCP 的 Modbus 库,遵循 LGPL V2.1+ 协议。libmodbus 支持 Linux、Mac Os X、FreeBSD、QNX 和 Windows 等操作系统。libmodbus 可以向符合 Modbus 协议的设备发送和接收数据,并支持通过串口或者 TCP 网络进行连接。

如何下载、使用和移植libmodbus请参考百问网提供的全场景工业互联设备管理系统解决方案

各文件作用如下:

  • win32: 定义在Windows下使用Visual Studio编译时的项目文件和工程文件以及相关配置选项等。其中,modbus-9.sln默认使用Visual Studio 2008。
  • Makefile.am: Makefile.am是Linux下AutoTool编译时读取相关编译参数的配置文件,用于生成Makefile文件,因为用于Linux下开发,所以在这里暂时忽略
  • modbus.c: 核心文件,实现Modbus协议层,定义共通的Modbus消息发送和接收函数各功能码对应的函数。
  • modbus.h: libmodbus对外暴露的接口API头文件。
  • modbus-data.c: 数据处理的共通函数,包括大小端相关的字节、位交换等函数。
  • modbus-private.h: libmodbus内部使用的数据结构和函数定义。
  • modbus-rtu.c: 通信层实现,RTU模式相关的函数定义,主要是串口的设置、连接及消息的发送和接收等。
  • modbus-rtu.h: RTU模式对外提供的各API定义。
  • modbus-rtu-private.h: RTU模式的私有定义。
  • modbus-tcp.c: 通信层实现,TCP模式下相关的函数定义,主要包括TCP/IP网络的设置连接、消息的发送和接收等。
  • modbus-tcp.h: 定义TCP模式对外提供的各API定义
  • modbus-tcp-private.h: TCP模式的私有定义。
  • modbus-version.h.in: 版本定义文件。

我们主要分析的就是 modbus.c modbus-rtu.cmodbus-data.c 这三个文件

由于我们采用的物理层通信协议是串行通信接口 RS-485 标准协议进行传输,因此应用层(这里不涉及传输层和网络层可以直接理解为在数据链路层对应用层 Modbus 的一帧数据进行帧分装)的 Modbus 协议使用 Modbus RTU(Remote Terminal Unit) 模式。

在源码中我们可以看到 Modbus TCP 和 TCPPI 涉及传输层和网络层的 IPv4 以及 IPv6 的模式,这是将应用层的 Modbus 数据封装到 ip 包中通过标准的网络设备(如交换机和路由器)在局域网或广域网中进行传输,不受串口物理距离的限制。

Modbus RTU 使用二进制格式传输数据,相比于 ASCII 模式(另一种 Modbus 协议实现)具有更高的数据传输效率。Modbus ASCII 采用可读字符表示数据,这使得它的效率较低。而 Modbus RTU 数据帧更紧凑,传输效率更高。

libmodbus 作为一个优秀且免费开源的跨平台支持 RTU 和 TCP 模式的Modbus 开发库,非常值得大家借鉴和学习。下面对 libmodbus 源代码进行阅读和分析。

unit-test-client.c

我们从源码的单元测试客户端(主机)libmodbus-3.1.10\tests\unit-test-client.cmain 函数开始分析源码执行流和所用的数据结构。推进学习百问网提供的全场景工业互联设备管理系统解决方案,其中有对 libmodbus 源码详细的视频课程讲解。

modbus_t *ctx = NULL;

这里重点关注 modbus_t 结构体,它用来针对不同传输协议生成对应的传输实例。

由于 Modbus 是一主多从,一个连接对应一个传输实例的context,即传输上下文用来记录整个连接传输过程中的信息。

例如源码 modbus-rtu.h 中通过 modbus_new_rtu 函数生成一个 modbus_t 实例,该实例用于一个 Modbus RTU 模式的传输。

MODBUS_API modbus_t *
modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit);

可以看到参数都是与串口协议相关的。

而在源码 modbus-tcp.h 中通过 modbus_new_tcp 函数生成一个 modbus_t 实例,该实例用于 Modbus TCP 模式的传输。

MODBUS_API modbus_t *modbus_new_tcp(const char *ip_address, int port);

可以看到参数都是与tcp协议相关的。

modbus_new_rtu

main 函数中第 #125 行就是构造这个 modbus_t 的实例

ctx = modbus_new_rtu(ip_or_device, 115200, 'N', 8, 1);

这里指定 server-ip 或者串口设备,波特率,校验位,数据位,停止位。


struct _modbus {
    /* Slave address */
    int slave;                          /* 存储 Modbus 从机(Slave)的地址 */
    /* Socket or file descriptor */
    /* 对于 TCP/IP 协议用于存储网络套接字(socket)描述符,对于串口用于存储串口文件描述符(file descriptor) */
    int s;
    int debug;                          /* 调试模式标志 */
    int error_recovery;                 /* 错误恢复模式标志 */
    int quirks;
    struct timeval response_timeout;    /* 响应超时时间 */
    struct timeval byte_timeout;        /* 字节超时时间 */
    struct timeval indication_timeout;  /* 指示超时时间 */
    const modbus_backend_t *backend;    /* 后端实现 */
    void *backend_data;
};

_modbus 结构体就是 modbus_t,它是 libmodbus 库中用于存储 Modbus 上下文信息的关键数据结构。


  1. int slave
    在 Modbus 协议中,主机发送请求时,会指定目标从机的地址;从机响应时也会包含自己的地址。slave 字段保存了当前上下文中配置的从机地址。

  2. int s
    用于存储与 Modbus 通讯相关的底层资源:

    • 对于 TCP 或 TCP_PI 后端,s 是一个网络套接字(socket)。
    • 对于 RTU 后端,s 是一个串口设备的文件描述符。
    • 这个字段是实际进行数据传输的句柄。
      以 RTU 为例,在源码 modbus-rtu.c #75 行进行初始化
    static int _modbus_set_slave(modbus_t *ctx, int slave)
    
  3. int debug
    用于控制是否启用调试输出:

    • 如果 debug1,libmodbus 会打印详细的调试信息(例如发送和接收的数据包)。
    • 如果 debug0,则不打印调试信息。
      在源码中 modbus.c #1968 行通过如下函数进行初始化。
    int modbus_set_debug(modbus_t *ctx, int flag)
    
  4. int error_recovery
    用于控制在通讯错误发生时是否尝试恢复连接:

    • 如果 error_recovery1,libmodbus 会尝试自动恢复连接。
    • 如果 error_recovery0,则不进行恢复,直接返回错误。
      在源码 modbus.c #1796 行通过如下函数进行初始化。
    int modbus_set_error_recovery(modbus_t *ctx, modbus_error_recovery_mode error_recovery)
    
  5. int quirks
    用于启用或禁用某些特定设备的特殊行为(quirks):

    • 某些 Modbus 设备可能不符合标准协议,quirks 字段可以用于兼容这些设备。
    • 具体行为取决于后端实现。
  6. struct timeval response_timeout
    用于设置等待从机响应的最大时间。struct timeval 包含两个字段:

    • tv_sec:秒数。
    • tv_usec:微秒数。
    • 例如,如果设置为 {2, 500000},表示等待 2.5 秒。
    struct timeval
    {
        long tv_sec;
        long tv_usec;
    };
    
  7. struct timeval byte_timeout
    用于设置等待两个字节之间的最大时间。如果超过这个时间仍未收到新字节,则认为通讯超时。

  8. struct timeval indication_timeout
    用于设置等待从机发送指示(indication)的最大时间。这个字段在某些特殊情况下使用。

  9. const modbus_backend_t *backend
    指向一个 modbus_backend_t 结构体,该结构体定义了与具体传输方式(TCP、RTU 等)相关的函数指针,用来实现具体的通信相关的句柄。通过这个字段,libmodbus 可以调用特定后端的实现函数(例如发送数据、接收数据等)。

    typedef struct _modbus_backend {
        unsigned int backend_type;
        unsigned int header_length;
        unsigned int checksum_length;
        unsigned int max_adu_length;
        int (*set_slave)(modbus_t *ctx, int slave);
        int (*build_request_basis)(
            modbus_t *ctx, int function, int addr, int nb, uint8_t *req);
        int (*build_response_basis)(sft_t *sft, uint8_t *rsp);
        int (*get_response_tid)(const uint8_t *req);
        int (*send_msg_pre)(uint8_t *req, int req_length);
        ssize_t (*send)(modbus_t *ctx, const uint8_t *req, int req_length);
        int (*receive)(modbus_t *ctx, uint8_t *req);
        ssize_t (*recv)(modbus_t *ctx, uint8_t *rsp, int rsp_length);
        int (*check_integrity)(modbus_t *ctx, uint8_t *msg, const int msg_length);
        int (*pre_check_confirmation)(modbus_t *ctx,
                                    const uint8_t *req,
                                    const uint8_t *rsp,
                                    int rsp_length);
        int (*connect)(modbus_t *ctx);
        unsigned int (*is_connected)(modbus_t *ctx);
        void (*close)(modbus_t *ctx);
        int (*flush)(modbus_t *ctx);
        int (*select)(modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length);
        void (*free)(modbus_t *ctx);
    } modbus_backend_t;
    
  10. void *backend_data
    用于存储后端特定的私有数据。例如:

    • 对于 TCP 后端,可能存储与网络连接相关的额外信息。
    • 对于 RTU 后端,可能存储与串口配置相关的额外信息。

modbus_set_slave

if (use_backend == RTU) {
    modbus_set_slave(ctx, SERVER_ID);
}

如果使用的是 RTU 模式,则需要调用 modbus_set_slave 函数设置该连接的从机地址。

modbus_connect

调用 modbus_connect 尝试连接从机。

if (modbus_connect(ctx) == -1) {
    fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
    modbus_free(ctx);
    return -1;
}

modbus_xxx

连接成功后就可以开始测试了,#147-#166 在分配和初始化完内存后就开始 Modbus 中各种数据模型的测试程序。

#168-#180 测试读写单个线圈,#182-#209 测试读写多个线圈。

#211-#230 测试读多个离散值。后面测试读写保持寄存器,读输入寄存器等。

我们在实现应用程序时可以参考如上代码实现。

更多的 libmodbus API 都在 modbus.h 中声明。


MODBUS_API int modbus_set_slave(modbus_t *ctx, int slave);
MODBUS_API int modbus_get_slave(modbus_t *ctx);
MODBUS_API int modbus_set_error_recovery(modbus_t *ctx,
                                         modbus_error_recovery_mode error_recovery);
MODBUS_API int modbus_set_socket(modbus_t *ctx, int s);
MODBUS_API int modbus_get_socket(modbus_t *ctx);

MODBUS_API int
modbus_get_response_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec);
MODBUS_API int
modbus_set_response_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec);

MODBUS_API int
modbus_get_byte_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec);
MODBUS_API int modbus_set_byte_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec);

MODBUS_API int
modbus_get_indication_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec);
MODBUS_API int
modbus_set_indication_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec);

MODBUS_API int modbus_get_header_length(modbus_t *ctx);

MODBUS_API int modbus_connect(modbus_t *ctx);
MODBUS_API void modbus_close(modbus_t *ctx);

MODBUS_API void modbus_free(modbus_t *ctx);

MODBUS_API int modbus_flush(modbus_t *ctx);
MODBUS_API int modbus_set_debug(modbus_t *ctx, int flag);

MODBUS_API const char *modbus_strerror(int errnum);

MODBUS_API int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest);
MODBUS_API int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest);
MODBUS_API int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
MODBUS_API int
modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);
MODBUS_API int modbus_write_bit(modbus_t *ctx, int coil_addr, int status);
MODBUS_API int modbus_write_register(modbus_t *ctx, int reg_addr, const uint16_t value);
MODBUS_API int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *data);

...

modbus_write_bit 为例,该函数会返回 write_single

/* Write a value to the specified register of the remote device.
   Used by write_bit and write_register */
static int write_single(modbus_t *ctx, int function, int addr, const uint16_t value)
{
    int rc;
    int req_length;
    uint8_t req[_MIN_REQ_LENGTH];

    if (ctx == NULL) {
        errno = EINVAL;
        return -1;
    }

    req_length = ctx->backend->build_request_basis(ctx, function, addr, (int) value, req);

    rc = send_msg(ctx, req, req_length);
    if (rc > 0) {
        /* Used by write_bit and write_register */
        uint8_t rsp[MAX_MESSAGE_LENGTH];

        rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);
        if (rc == -1)
            return -1;

        rc = check_confirmation(ctx, req, rsp, rc);
    }

    return rc;
}

ctx->backend->build_request_basis

根据当前连接的模式构建基础请求头,我们使用的是 Modbus RTU 会执行如下的函数。

/* Builds a RTU request header */
static int _modbus_rtu_build_request_basis(
    modbus_t *ctx, int function, int addr, int nb, uint8_t *req)
{
    assert(ctx->slave != -1);
    req[0] = ctx->slave;
    req[1] = function;
    req[2] = addr >> 8;
    req[3] = addr & 0x00ff;
    req[4] = nb >> 8;
    req[5] = nb & 0x00ff;

    return _MODBUS_RTU_PRESET_REQ_LENGTH;
}

build_request_basis 指向这个函数 _modbus_rtu_build_request_basis 用于构建 Modbus RTU 协议的请求头部。

static int _modbus_rtu_build_request_basis(
    modbus_t *ctx, int function, int addr, int nb, uint8_t *req)
  • modbus_t *ctx: Modbus 上下文对象,包含 Modbus 通信的相关信息(如从机地址)。
  • int function: Modbus 功能码(例如读取保持寄存器、写入单个寄存器等)。
  • int addr: 寄存器或线圈的起始地址。
  • int nb: 要读取或写入的寄存器或线圈的数量。
  • uint8_t *req: 用于存储构建的请求头部的缓冲区。
  • 返回一个固定值 _MODBUS_RTU_PRESET_REQ_LENGTH,表示 RTU 请求头部的长度。

assert(ctx->slave != -1);
  • 断言 ctx->slave(从机地址)不等于 -1,确保从机地址已正确设置。
  • 如果从机地址为 -1,程序会终止并抛出错误。

req[0] = ctx->slave;
  • 将 Modbus 从机地址存储到请求头部的第一个字节。

req[1] = function;
  • 将 Modbus 功能码存储到请求头部的第二个字节。

req[2] = addr >> 8;
req[3] = addr & 0x00ff;
  • 将寄存器或线圈的起始地址拆分为高字节和低字节,并分别存储到请求头部的第三个和第四个字节。
  • addr >> 8:提取地址的高字节。
  • addr & 0x00ff:提取地址的低字节。

req[4] = nb >> 8;
req[5] = nb & 0x00ff;
  • 将寄存器或线圈的数量拆分为高字节和低字节,并分别存储到请求头部的第五个和第六个字节。
  • nb >> 8:提取数量的高字节。
  • nb & 0x00ff:提取数量的低字节。

return _MODBUS_RTU_PRESET_REQ_LENGTH;
  • 返回 RTU 请求头部的固定长度 _MODBUS_RTU_PRESET_REQ_LENGTH
  • 这个值通常是 6,表示 RTU 请求头部由 6 个字节组成(从机地址、功能码、地址高字节、地址低字节、数量高字节、数量低字节)。

send_msg

执行发送,我们会重写这个函数执行发送。

_modbus_receive_msg

执行接收,我们会重写这个函数执行接收。

check_confirmation

检查确认回复。

modbus_close modbus_free

最后执行 modbus_close 关闭连接。modbus_free 释放内存。