libmodbus 源码分析(接收请求篇)

目录

一、前言

在上一篇笔记中,我们深入拆解了 libmodbus 主设备发送请求的完整源码流程,掌握了 Modbus 报文的构造、校验与发送逻辑。而 Modbus 通信是双向交互的,从设备的核心职责是接收主设备的请求报文、解析并执行对应操作、返回响应结果。本次笔记将聚焦 libmodbus 从设备接收请求的场景,拆解从初始化到报文接收、完整性校验的完整流程,重点解析核心接收函数与超时机制,帮助你完整掌握 libmodbus 的双向通信逻辑,为后续实现完整的 Modbus 主从通信打下基础。

二、从设备接收请求完整执行流程

从设备接收并响应主设备请求的完整代码执行流程如下,其中初始化流程与主设备发送请求完全一致:

modbus_new_rtumodbus_set_slavemodbus_connectmodbus_mapping_new_start_address→ 循环执行(modbus_receivemodbus_send_raw_request/modbus_reply)→modbus_mapping_freemodbus_closemodbus_free

其中,初始化阶段(modbus_new_rtumodbus_set_slavemodbus_connect)的逻辑与主设备完全相同,核心是创建 RTU 上下文、设置自身从设备地址、打开并配置串口,为接收报文做好准备。

支撑整个 RTU 模式操作的核心后端结构体依然是_modbus_rtu_backend,完整定义如下:

c 复制代码
const modbus_backend_t _modbus_rtu_backend = {
    _MODBUS_BACKEND_TYPE_RTU, // 后端类型:RTU模式
    _MODBUS_RTU_HEADER_LENGTH, // RTU报文头部长度
    _MODBUS_RTU_CHECKSUM_LENGTH, // RTU报文校验码长度
    MODBUS_RTU_MAX_ADU_LENGTH, // RTU报文最大允许长度
    _modbus_set_slave, // 设置从设备地址
    _modbus_rtu_build_request_basis, // 构造请求报文基础部分
    _modbus_rtu_build_response_basis, // 构造响应报文基础部分
    _modbus_rtu_prepare_response_tid, // 准备响应报文事务ID
    _modbus_rtu_send_msg_pre, // 发送报文预处理(计算CRC)
    _modbus_rtu_send, // 实际发送报文
    _modbus_rtu_receive, // 接收报文核心逻辑(从设备入口)
    _modbus_rtu_recv, // 底层串口读取数据
    _modbus_rtu_check_integrity, // 校验报文完整性
    _modbus_rtu_pre_check_confirmation, // 预校验响应报文
    _modbus_rtu_connect, // 建立串口连接
    _modbus_rtu_is_connected, // 检查串口连接状态
    _modbus_rtu_close, // 关闭串口连接
    _modbus_rtu_flush, // 刷新串口缓冲区
    _modbus_rtu_select, // 串口超时等待选择
    _modbus_rtu_free // 释放RTU相关资源
};

补充:从设备与主设备共用同一个 RTU 后端结构体,仅在函数调用的场景与msg_type标志位上存在差异,这是 libmodbus 模块化设计的高效体现。

三、接收核心函数与 RTU 后端解析

从设备接收请求的入口函数是modbus_receive,其核心作用是调用后端绑定的receive函数,完成报文的接收逻辑,源码如下:

c 复制代码
int modbus_receive(modbus_t *ctx, uint8_t *req)
{
    // 校验Modbus上下文是否有效
    if (ctx == NULL) {
        errno = EINVAL;
        return -1;
    }

    // 调用RTU后端的receive函数,执行实际接收逻辑
    return ctx->backend->receive(ctx, req);
}

该函数最终会调用_modbus_rtu_receive,这是 RTU 模式下接收报文的核心逻辑实现,源码如下:

c 复制代码
static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req)
{
    int rc;
    modbus_rtu_t *ctx_rtu = ctx->backend_data;

    // 若存在需要忽略的确认报文,先接收并忽略
    if (ctx_rtu->confirmation_to_ignore) {
        _modbus_receive_msg(ctx, req, MSG_CONFIRMATION);
        /* Ignore errors and reset the flag */
        ctx_rtu->confirmation_to_ignore = FALSE;
        rc = 0;
        if (ctx->debug) {
            printf("Confirmation to ignore\n");
        }
    } else {
        // 接收指示报文(主设备发送的请求报文)
        rc = _modbus_receive_msg(ctx, req, MSG_INDICATION);
        if (rc == 0) {
            /* The next expected message is a confirmation to ignore */
            ctx_rtu->confirmation_to_ignore = TRUE;
        }
    }
    return rc;
}

从源码中可以发现,从设备与主设备共用同一个_modbus_receive_msg函数完成报文接收:

c 复制代码
int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type)

其中,msg_type_t msg_type是区分主设备与从设备的关键标志位:MSG_INDICATION表示从设备接收主设备的请求报文,MSG_CONFIRMATION表示主设备接收从设备的响应报文,通过该标志位,实现了同一个函数支撑双向通信的接收需求。

四、_modbus_receive_msg 接收流程拆解

_modbus_receive_msg是 libmodbus 接收报文的核心函数,采用状态机模式逐步读取报文数据,完整流程分为四步,具体如下:

第零步:确认初始读取字节数

首先初始化接收状态与需要读取的初始字节数,核心是读取报文头部与功能码,源码如下:

c 复制代码
/* We need to analyse the message step by step.  At the first step, we want
 * to reach the function code because all packets contain this
 * information. */
step = _STEP_FUNCTION;
// 初始读取长度:头部长度 + 1字节功能码
length_to_read = ctx->backend->header_length + 1;

第一步:等待数据到达

通过循环等待串口数据到达,支持超时配置,源码如下:

c 复制代码
while (length_to_read != 0) {
    // 等待串口数据,超时时间由p_tv指定
    rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);
    if (rc == -1) {
        _error_print(ctx, "select");
        if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {

补充:p_tv是指向超时时间结构体的指针,后续会详细解析其配置逻辑,这是实现接收超时机制的核心。

第二步:读取底层原始数据

当检测到串口有数据后,调用底层recv函数读取原始字节数据,源码如下:

c 复制代码
// 从串口读取数据,写入msg缓冲区(偏移msg_length确保数据不覆盖)
rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);
if (rc == 0) {
    // 读取失败,设置连接重置错误
    errno = ECONNRESET;
    rc = -1;
}

只有当返回值rc不为 - 1 时,说明读取到了有效原始数据,才能进入后续的状态机处理流程。

第三步:状态机分阶段读取完整报文(核心)

这一步是接收流程的核心,通过状态机判断当前报文接收阶段,计算后续需要读取的字节数,循环读取直到获取完整报文,源码如下:

c 复制代码
/* Sums bytes received */
msg_length += rc;
/* Computes remaining bytes */
length_to_read -= rc;

if (length_to_read == 0) {
    switch (step) {
    case _STEP_FUNCTION:
        /* Function code position */
        // 根据功能码计算后续需要读取的元数据长度
        length_to_read = compute_meta_length_after_function(
            msg[ctx->backend->header_length], msg_type);
        if (length_to_read != 0) {
            step = _STEP_META;
            break;
        } /* else switches straight to the next step */
    case _STEP_META:
        // 根据元数据计算后续需要读取的业务数据长度
        length_to_read = compute_data_length_after_meta(ctx, msg, msg_type);
        if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
            errno = EMBBADDATA;
            _error_print(ctx, "too many data");
            return -1;
        }
        step = _STEP_DATA;
        break;

整个状态机分为三个阶段,对应 Modbus 报文的结构:

  1. _STEP_FUNCTION:读取设备地址与功能码,这是所有 Modbus 报文的必备字段;
  2. _STEP_META:读取元数据(起始地址、寄存器数量等),根据功能码计算所需长度;
  3. _STEP_DATA:读取业务数据与校验码,根据元数据计算所需长度。

第四步:校验报文完整性

当报文读取完成后,调用后端的check_integrity函数校验报文的完整性(核心是校验 CRC16 校验码),源码如下:

c 复制代码
// 校验报文完整性,返回校验结果
return ctx->backend->check_integrity(ctx, msg, msg_length);

接收流程总结

Modbus 报文接收的核心逻辑可概括为:通过状态机分三阶段读取数据,先读取设备地址与功能码,再根据功能码确定元数据长度,最后根据元数据确定业务数据与校验码长度,最终校验报文完整性。无论是从设备接收请求,还是主设备接收响应,都遵循这一统一流程。

五、接收超时机制详解

libmodbus 的接收流程中设计了两层超时机制,分别对应不同的接收阶段,确保在无数据传输时不会无限阻塞,提升程序的健壮性,核心源码与解析如下:

1. 第一阶段超时:indication_timeout(请求等待超时)

该超时时间是从设备等待主设备发送请求的总超时时间,源码如下:

c 复制代码
if (msg_type == MSG_INDICATION) {
    /* Wait for a message, we don't know when the message will be
     * received */
    if (ctx->indication_timeout.tv_sec == 0 && ctx->indication_timeout.tv_usec == 0) {
        /* By default, the indication timeout isn't set */
        p_tv = NULL;
    } else {
        /* Wait for an indication (name of a received request by a server, see schema)
         */
        tv.tv_sec = ctx->indication_timeout.tv_sec;
        tv.tv_usec = ctx->indication_timeout.tv_usec;
        p_tv = &tv;
    }
  • indication_timeout:从机等待主机请求的超时时间,若为主机调用,则对应等待从机响应的超时时间;
  • 配置为 0(秒与微秒均为 0):采用查询方式,不阻塞,直接返回是否有数据;
  • 配置为非 0 值:采用阻塞方式,等待指定时间,超时后返回错误,该方式在 FreeRTOS 中对其他任务的执行影响较小。

该超时主要作用于第一阶段(设备地址与功能码)的等待,是整个报文接收的总超时控制。

2. 后续阶段超时:byte_timeout(字节传输超时)

当读取到设备地址与功能码后,后续元数据、业务数据的传输采用byte_timeout超时机制,源码如下:

c 复制代码
tv.tv_sec = ctx->byte_timeout.tv_sec;
tv.tv_usec = ctx->byte_timeout.tv_usec;
p_tv = &tv;
  • byte_timeout:单个字节传输的超时时间,仅作用于报文内部的后续字节读取;
  • 该超时时间通常配置得较短,仅等待单个字节的传输耗时,若超时则判定报文传输异常,返回错误。

补充:两层超时机制的设计,既保证了整个请求接收的灵活性(总超时),又保证了报文内部传输的高效性(字节超时),有效提升了 Modbus 通信的健壮性,避免了无效的长时间阻塞。

六、总结

  1. 从设备接收请求流程:初始化(同主设备)→ 循环接收(modbus_receive)→ 解析响应 → 释放资源,共用 RTU 后端结构体;
  2. 核心接收函数_modbus_receive_msg采用状态机分三阶段读取报文,最终校验 CRC 完整性,主从设备通过msg_type区分;
  3. 两层超时机制:indication_timeout(总请求超时)与byte_timeout(单个字节超时),支持查询与阻塞两种模式,提升通信健壮性。

七、结尾

本次我们完整拆解了 libmodbus 从设备接收请求的源码流程,掌握了状态机报文读取与双层超时机制的核心逻辑,至此 libmodbus 的主从双向通信核心流程已全部解析完毕。这些底层逻辑不仅是使用 libmodbus 开发的基础,更是实现 Modbus 协议二次开发与裸机移植的关键。吃透这些内容,能够大幅提升你在工业通信领域的开发能力与故障排查效率。感谢各位的阅读,持续关注本系列笔记,一起深耕 Modbus 与嵌入式开源库开发,解锁更多实战技能与工业应用场景!

相关推荐
lxl13076 小时前
学习C++(5)运算符重载+赋值运算符重载
学习
ruxshui6 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
慾玄7 小时前
渗透笔记总结
笔记
AutumnorLiuu7 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习
CS创新实验室7 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
嵌入小生0077 小时前
单向链表的常用操作方法---嵌入式入门---Linux
linux·开发语言·数据结构·算法·链表·嵌入式
峥嵘life7 小时前
Android EDLA CTS、GTS等各项测试命令汇总
android·学习·elasticsearch
千谦阙听7 小时前
数据结构入门:栈与队列
数据结构·学习·visual studio
.小墨迹7 小时前
C++学习——C++中`memcpy`和**赋值拷贝**的核心区别
java·linux·开发语言·c++·学习·算法·机器学习
望忆7 小时前
关于《Cold & Warm Net: Addressing Cold-Start Usersin Recommender Systems》
学习