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.c
和 modbus-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.c
的 main
函数开始分析源码执行流和所用的数据结构。推进学习百问网提供的全场景工业互联设备管理系统解决方案,其中有对 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 上下文信息的关键数据结构。
int slave
在 Modbus 协议中,主机发送请求时,会指定目标从机的地址;从机响应时也会包含自己的地址。slave
字段保存了当前上下文中配置的从机地址。
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)
int debug
用于控制是否启用调试输出:
- 如果
debug
为1
,libmodbus 会打印详细的调试信息(例如发送和接收的数据包)。- 如果
debug
为0
,则不打印调试信息。
在源码中modbus.c
#1968 行通过如下函数进行初始化。int modbus_set_debug(modbus_t *ctx, int flag)
int error_recovery
用于控制在通讯错误发生时是否尝试恢复连接:
- 如果
error_recovery
为1
,libmodbus 会尝试自动恢复连接。- 如果
error_recovery
为0
,则不进行恢复,直接返回错误。
在源码modbus.c
#1796 行通过如下函数进行初始化。int modbus_set_error_recovery(modbus_t *ctx, modbus_error_recovery_mode error_recovery)
int quirks
用于启用或禁用某些特定设备的特殊行为(quirks):
- 某些 Modbus 设备可能不符合标准协议,
quirks
字段可以用于兼容这些设备。- 具体行为取决于后端实现。
struct timeval response_timeout
用于设置等待从机响应的最大时间。struct timeval
包含两个字段:
tv_sec
:秒数。tv_usec
:微秒数。- 例如,如果设置为
{2, 500000}
,表示等待 2.5 秒。struct timeval { long tv_sec; long tv_usec; };
struct timeval byte_timeout
用于设置等待两个字节之间的最大时间。如果超过这个时间仍未收到新字节,则认为通讯超时。
struct timeval indication_timeout
用于设置等待从机发送指示(indication)的最大时间。这个字段在某些特殊情况下使用。
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;
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
释放内存。