libmodbus 源码分析(发送请求篇)

目录

一、前言

在上一篇笔记中,我们完成了 libmodbus 库的介绍、源码获取与阅读工具的实操,为深入研读源码打下了基础。libmodbus 的核心价值在于封装了 Modbus 协议的底层细节,而发送请求是 Modbus 主设备的核心操作之一。本次笔记将以modbus_write_bits(写多个线圈)函数为例,拆解 libmodbus 发送请求的完整执行流程,深入解析每一步的源码逻辑与核心结构体作用,帮助你理解 libmodbus 的底层封装思路,为后续的二次开发与裸机移植提供理论支撑。

二、分析场景:modbus_write_bits 执行流程

本次源码分析聚焦于 Modbus 主设备发送写请求的场景,以modbus_write_bits函数为核心线索,逐步拆解从上下文创建到报文发送的完整流程,整个流程分为五大核心步骤,每一步对应 libmodbus 的关键 API 与底层实现。

补充:modbus_write_bits函数用于向 Modbus 从设备写入多个线圈状态(对应 1 位数字量输出),是工业控制中常用的写操作 API,其底层封装了 Modbus RTU 报文的构造、校验与发送逻辑。

三、第一步:创建 RTU 上下文(modbus_new_rtu)

主设备发送请求的第一步,是调用modbus_new_rtu函数创建 Modbus RTU 模式的上下文,该函数的核心作用是分配内存、初始化核心结构体并设置 RTU 后端属性,核心源码如下:

c 复制代码
// 分配modbus上下文结构体内存,用于表示整个Modbus总线环境
ctx = (modbus_t *) malloc(sizeof(modbus_t));
if (ctx == NULL) {
    return NULL;
}

// 初始化上下文公共属性
_modbus_init_common(ctx);
// 设置RTU模式后端,绑定RTU相关的操作函数集
ctx->backend = &_modbus_rtu_backend;
// 分配RTU模式专属数据结构体内存,用于保存串口相关属性
ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));
if (ctx->backend_data == NULL) {
    modbus_free(ctx);
    errno = ENOMEM;
    return NULL;
}
ctx_rtu = (modbus_rtu_t *) ctx->backend_data;

/* Device name and \0 */
// 分配内存并保存串口设备名称(如Windows的COM1、Linux的/dev/ttyUSB0)
ctx_rtu->device = (char *) malloc((strlen(device) + 1) * sizeof(char));
if (ctx_rtu->device == NULL) {
    modbus_free(ctx);
    errno = ENOMEM;
    return NULL;
}

#if defined(_WIN32)
// Windows平台下复制串口设备名称
strcpy_s(ctx_rtu->device, strlen(device) + 1, device);
#else
// 类Unix平台下复制串口设备名称
strcpy(ctx_rtu->device, device);
#endif

其中核心的结构体赋值逻辑如下,用于绑定 RTU 后端并初始化串口相关数据:

c 复制代码
// 绑定Modbus RTU模式的后端操作函数集
ctx->backend = &_modbus_rtu_backend;
// 分配RTU专属数据结构体,保存串口设备名、波特率等属性
ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));

modbus_new_rtu函数的核心流程可总结为:分配上下文内存→初始化公共属性→设置 RTU 后端→分配并初始化 RTU 专属串口数据,为后续的 Modbus 操作搭建基础环境。

四、第二步:设置从设备地址(modbus_set_slave)

创建完 RTU 上下文后,需要调用modbus_set_slave函数指定目标从设备地址,确保请求报文能够被正确的从机接收,核心代码如下:

首先是主设备调用的设置逻辑:

c 复制代码
if (use_backend == RTU) {
    // 设置目标从设备ID(SERVER_ID为预定义的从机地址)
    modbus_set_slave(ctx, SERVER_ID);
}

modbus_set_slave函数的底层实现如下:

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

    // 调用后端绑定的set_slave函数,设置从设备地址
    return ctx->backend->set_slave(ctx, slave);
}

该函数的核心作用是将传入的从设备 ID 赋值给modbus_t结构体的slave字段,完整的modbus_t结构体如下(展示核心字段):

c 复制代码
struct _modbus {
    /* Slave address */
    int slave; // 从设备地址,用于报文封装
    /* Socket or 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; // 后端专属数据(如RTU串口属性)
};

这一步的核心目的是为后续报文封装提供从设备地址,确保生成的 Modbus 报文包含正确的目标从机标识。

五、第三步:建立串口连接(modbus_connect)

设置完从设备地址后,需要调用modbus_connect函数建立与串口的连接,完成串口的打开与参数配置,核心源码如下:

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

    // 调用后端绑定的connect函数,建立串口连接
    return ctx->backend->connect(ctx);
}

modbus_connect函数最终会调用_modbus_rtu_connect函数,该函数内部包含完整的串口打开串口参数配置(波特率、数据位、停止位、校验位)逻辑。

补充:对于 Modbus RTU 模式而言,主从设备的串口物理连接(或虚拟串口连接)是通信的基础,而modbus_connect函数的核心作用是完成软件层面的串口初始化,确保数据能够通过串口正常传输。

需要注意的是,主从设备的串口是否成功建立软件连接,是后续报文能否正常发送与接收的关键前提。

六、第四步:构造写请求(modbus_write_bits)

完成前期准备工作后,进入核心的写请求构造阶段,调用modbus_write_bits函数,该函数的参数包含了写操作的核心信息:起始地址、写入位数、数据缓冲区,核心源码与逻辑如下:

首先是函数的参数定义与局部变量声明:

c 复制代码
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src)
{
    int rc;
    int i;
    int byte_count;
    int req_length;
    int bit_check = 0;
    int pos = 0;

参数说明:addr为写入的线圈起始地址,nb为写入的线圈位数,src为待写入的数据缓冲区,这三个参数明确了写操作的核心信息。

1. 构造基础请求报文

通过调用后端的build_request_basis函数,构造 Modbus 报文的基础部分,源码如下:

c 复制代码
req_length = ctx->backend->build_request_basis(
    ctx, MODBUS_FC_WRITE_MULTIPLE_COILS, addr, nb, req);

该函数最终会调用_modbus_rtu_build_request_basis,结合 Modbus 功能码表,其核心作用是构造报文的基础字段:从设备地址、功能码、起始地址(高位)、起始地址(低位)、寄存器数(高位)、寄存器数(低位)

补充:该步骤构造的基础报文字段,与此前 Modbus 报文解析中的查询报文基础结构完全一致,是 Modbus 协议标准化的体现。

2. 计算数据域字节数

根据写入的线圈位数,计算需要发送的数据域字节数,源码如下:

c 复制代码
byte_count = (nb / 8) + ((nb % 8) ? 1 : 0);

该代码的逻辑对应 Modbus 协议中位寄存器的数据字节数计算规则:当写入的位数nb为 8 的整数倍时,字节数为nb/8;当nb不为 8 的整数倍时,字节数向上取整(nb/8 + 1),多余的位填充为 0,不影响有效数据的传输。

七、第五步:发送请求报文(send_msg 系列函数)

构造完成请求报文后,通过send_msg函数完成报文的预处理与实际发送,整个过程分为两个核心步骤:报文预处理(计算 CRC 校验)与串口发送。

1. 发送报文核心函数(send_msg)

c 复制代码
static int send_msg(modbus_t *ctx, uint8_t *msg, int msg_length)
{
    int rc;
    int i;

    // 报文发送前预处理(核心:计算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]);
        printf("\n");
    }

    /* In recovery mode, the write command will be issued until to be
       successful! Disabled by default. */
    // 错误恢复模式下,重复发送直到成功(默认禁用)
    do {
        rc = ctx->backend->send(ctx, msg, msg_length);
        if (rc == -1) {
            _error_print(ctx, NULL);
            if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) {

2. 报文预处理(计算 CRC 校验)

send_msg_pre函数的底层实现为_modbus_rtu_send_msg_pre,核心作用是计算 Modbus RTU 报文的 CRC16 校验码,并追加到报文末尾,源码如下:

c 复制代码
static int _modbus_rtu_send_msg_pre(uint8_t *req, int req_length)
{
    // 计算报文的CRC16校验码
    uint16_t crc = crc16(req, req_length);

    /* According to the MODBUS specs (p. 14), the low order byte of the CRC comes
     * first in the RTU message */
    // 按照Modbus协议规范,CRC低位在前,高位在后,追加到报文末尾
    req[req_length++] = crc & 0x00FF;
    req[req_length++] = crc >> 8;

    // 返回追加校验码后的完整报文长度
    return req_length;
}

3. 实际串口发送数据

send函数的底层实现为_modbus_rtu_send,核心作用是通过串口将完整的 Modbus 报文发送出去,支持跨平台(Windows / 类 Unix),源码如下:

c 复制代码
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
#if defined(_WIN32)
    // Windows平台下,通过WriteFile函数写入串口
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    DWORD n_bytes = 0;
    return (WriteFile(ctx_rtu->w_ser.fd, req, req_length, &n_bytes, NULL))
               ? (ssize_t) n_bytes
               : -1;
#else
#if HAVE_DECL_TIOCM_RTS
    // 类Unix平台下,支持RTS信号控制
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    if (ctx_rtu->rts != MODBUS_RTU_RTS_NONE) {
        ssize_t size;

        if (ctx->debug) {
            fprintf(stderr, "Sending request using RTS signal\n");
        }

        ctx_rtu->set_rts(ctx, ctx_rtu->rts == MODBUS_RTU_RTS_UP);
        usleep(ctx_rtu->rts_delay);

        size = write(ctx->s, req, req_length);

        usleep(ctx_rtu->onebyte_time * req_length + ctx_rtu->rts_delay);
        ctx_rtu->set_rts(ctx, ctx_rtu->rts != MODBUS_RTU_RTS_UP);

        return size;
    } else {
#endif
        // 类Unix平台下,直接通过write函数写入串口
        return write(ctx->s, req, req_length);
#if HAVE_DECL_TIOCM_RTS
    }
#endif
#endif
}

补充:若需将 libmodbus 改造成裸机环境使用,核心需要修改该函数中的串口发送逻辑,替换为对应 MCU 的串口发送 API(如 STM32 的HAL_UART_Transmit),其余逻辑可保持不变。

八、RTU 后端核心结构解读

整个 Modbus RTU 发送流程的核心支撑是_modbus_rtu_backend结构体,该结构体定义了 RTU 模式下所有关键操作的函数指针,遵循固定的顺序与格式,源码如下:

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相关资源
};

该结构体是 libmodbus 模块化设计的核心体现,所有 RTU 模式的操作都通过该结构体中的函数指针完成,这也是 libmodbus 能够支持多后端(RTU/TCP)的关键所在。

九、总结

  1. libmodbus 发送请求核心流程:创建 RTU 上下文→设置从机地址→建立串口连接→构造请求报文→发送报文(含 CRC 校验);
  2. 核心支撑是_modbus_rtu_backend结构体,封装了 RTU 模式的所有操作函数,体现模块化设计思想;
  3. 报文预处理的核心是计算 CRC16 校验码,裸机移植需修改串口发送底层 API;
  4. 源码中的字节数计算、报文结构与 Modbus 协议规范完全一致,实现了协议的标准化封装。

十、结尾

本次我们深入拆解了 libmodbus 发送请求的完整源码流程,清晰看到了 Modbus 协议的标准化封装与模块化设计思想。吃透这些底层逻辑,不仅能熟练使用 libmodbus 进行开发,还能根据实际需求进行二次开发与裸机移植。libmodbus 的源码封装看似复杂,实则遵循清晰的协议逻辑与工程规范,反复研读能够大幅提升嵌入式工程开发能力。感谢各位的阅读,持续关注本系列笔记,一起深耕 Modbus 与开源库开发领域,解锁更多实用技能与实战场景!

相关推荐
上海合宙LuatOS2 小时前
LuatOS框架的使用(1)
java·开发语言·单片机·嵌入式硬件·物联网·ios·iphone
sensen_kiss2 小时前
INT301 生物计算(神经网络)Coursework 解析(知识点梳理)
人工智能·笔记·深度学习·神经网络
ziqi5222 小时前
第二十二天笔记
前端·chrome·笔记
孞㐑¥2 小时前
算法—模拟
c++·经验分享·笔记·算法
摸摸电2 小时前
DRAM结构
单片机·嵌入式硬件·设计规范
风123456789~2 小时前
【架构专栏】架构知识点
笔记·架构·考证
好好沉淀2 小时前
Java 开发环境概念速查笔记(JDK / SDK / Maven)
java·笔记·maven
杨_晨2 小时前
大模型微调训练FAQ - Loss与准确率关系
人工智能·经验分享·笔记·深度学习·机器学习·ai
Gain_chance2 小时前
25-学习笔记尚硅谷数仓搭建-DIM层其余(优惠卷、活动、地区、营销坑位、营销渠道、日期)维度表建表语句、简单分析
数据仓库·笔记·学习