libmodbus 源码分析

目录


一、前言

大家好,这里是 Hello_Embed

上一篇我们分析了 Modbus RTU 协议------帧格式、四种寄存器、功能码。本篇进入源码层:libmodbus 是怎么组织代码的?modbus_tmodbus_mapping_t 是什么关系?发送一帧数据的完整调用链是怎样的?

我们从三个经典情景出发------主机发请求、从机收请求、从机构造回应------用代码走读的方式把 libmodbus 核心流程串起来。


二、libmodbus 的文件地图

复制代码
Middlewares/Third_Party/libmodbus/
├── modbus.h               ← 公共 API + 功能码宏 + modbus_mapping_t
├── modbus.c               ← 核心引擎:帧收发、超时、主机/从机逻辑
├── modbus-private.h        ← 内部结构:_modbus_backend vtable, sft_t
├── modbus-rtu.h            ← RTU 模式声明 + modbus_new_rtu()
├── modbus-rtu.c            ← RTU 后端(POSIX 版,读写串口)
├── modbus-rtu-private.h    ← modbus_rtu_t 结构定义
├── modbus-st-rtu.c         ← ★ STM32 移植版:用 UART_Device 替代 POSIX 串口
├── modbus-data.c           ← 寄存器映射的创建/释放
├── modbus-tcp.h / .c       ← TCP 后端(本项目不用)
└── errno.h / errno.c       ← 错误码

重点关注modbus-st-rtu.c------这是我们移植到 STM32H5 的关键文件。它把 libmodbus 原本的 POSIX read()/write() 替换成了 pdev->RecvByte()pdev->Send()


三、核心数据结构

3.1 modbus_t ------ 主上下文

c 复制代码
// modbus-private.h
struct _modbus {
    int slave;                          // 从站地址
    int s;                             // socket/file descriptor
    int debug;
    int error_recovery;
    struct timeval response_timeout;    // 响应超时
    struct timeval byte_timeout;        // 字节间超时
    const modbus_backend_t *backend;    // ★ 函数指针表
    void *backend_data;                 // ★ 后端私有数据 (modbus_rtu_t *)
};

backend 指向一张虚函数表 ,所有底层 I/O 操作通过它分发。backend_data 持有 RTU 或 TCP 的私有参数。

3.2 modbus_backend_t ------ 虚函数表

c 复制代码
// modbus-private.h
typedef struct _modbus_backend {
    unsigned int backend_type;
    int (*set_slave)(...);
    int (*build_request_basis)(...);   // 构架请求帧头
    int (*build_response_basis)(...);  // 构架响应帧头
    ssize_t (*send)(...);              // ★ 发送
    ssize_t (*recv)(...);             // ★ 接收一个字节
    int (*receive)(...);              // 接收完整帧
    int (*check_integrity)(...);      // CRC 校验
    int (*connect)(...);              // ★ 初始化
    int (*flush)(...);
    void (*free)(...);
    // ... 更多函数指针
} modbus_backend_t;

ST 移植版定义了 _modbus_rtu_backend_uart 实例,其中 send_modbus_rtu_send(内部调 pdev->send),recv_modbus_rtu_recv(内部调 pdev->RecvByte)。

3.3 modbus_rtu_t ------ RTU 私有数据

c 复制代码
// modbus-rtu-private.h
typedef struct _modbus_rtu {
    char *device;          // 设备名: "uart4", "usb"
    int baud;
    uint8_t data_bit;
    uint8_t stop_bit;
    char parity;           // 'N', 'E', 'O'
    int confirmation_to_ignore;
    struct UART_Device *dev;  // ★ OOP 设备指针
} modbus_rtu_t;

3.4 modbus_mapping_t ------ 寄存器映射

c 复制代码
typedef struct _modbus_mapping_t {
    int nb_bits;                   // 线圈数量
    int start_bits;                // 线圈起始地址
    int nb_input_bits;             // 离散输入数量
    int start_input_bits;
    int nb_input_registers;        // 输入寄存器数量
    int start_input_registers;
    int nb_registers;              // 保持寄存器数量
    int start_registers;
    uint8_t  *tab_bits;            // 线圈位表
    uint8_t  *tab_input_bits;      // 离散输入位表
    uint16_t *tab_input_registers; // 输入寄存器值表
    uint16_t *tab_registers;       // 保持寄存器值表
} modbus_mapping_t;

这就是从站的"内存镜像"------四张表对应四种 Modbus 寄存器。modbus_reply 根据请求的功能码读写对应的表。

3.5 数据结构关系总图

复制代码
modbus_t (协议栈上下文)
  ├── backend → _modbus_rtu_backend_uart (虚函数表, 编译期常量)
  │     ├── send    → _modbus_rtu_send    → pdev->send(...)
  │     ├── recv    → _modbus_rtu_recv    → pdev->RecvByte(...)
  │     ├── connect → _modbus_rtu_connect → pdev->Init(...)
  │     └── flush   → _modbus_rtu_flush   → pdev->Flush(...)
  │
  └── backend_data → modbus_rtu_t
        ├── device = "uart4"
        ├── baud   = 115200
        └── dev    → g_uart4_dev (UART_Device *)

modbus_mapping_t (寄存器镜像, 独立分配)
  ├── tab_bits[16]            ← 线圈 Coil
  ├── tab_input_bits[3]       ← 离散输入 DI
  ├── tab_registers[5]        ← 保持寄存器 HR
  └── tab_input_registers[4]  ← 输入寄存器 IR

四、ST 移植版:modbus-st-rtu.c 全景

这是整个移植的核心。标准 libmodbus 用 POSIX read()/write() 操作 /dev/ttyS0。我们要把它改成用 UART_Devicesend/RecvByte

4.1 创建 RTU 上下文:modbus_new_st_rtu()

c 复制代码
modbus_t *modbus_new_st_rtu(const char *device, int baud,
                             char parity, int data_bit, int stop_bit)
{
    // ① 分配 modbus_t
    modbus_t *ctx = pvPortMalloc(sizeof(modbus_t));
    _modbus_init_common(ctx);        // 初始化默认超时、错误恢复等

    // ② 绑定虚函数表
    ctx->backend = &_modbus_rtu_backend_uart;

    // ③ 查找 OOP 设备 (★ 关键桥接)
    struct UART_Device *pdev = GetUARTDevice((char *)device);
    if (!pdev) { modbus_free(ctx); return NULL; }

    // ④ 分配 RTU 私有数据, 存设备指针和串口参数
    modbus_rtu_t *ctx_rtu = pvPortMalloc(sizeof(modbus_rtu_t));
    ctx_rtu->dev = pdev;            // ★ 把 UART_Device 挂到上下文
    ctx_rtu->baud = baud;
    ctx_rtu->parity = parity;
    // ...

    ctx->backend_data = ctx_rtu;
    return ctx;
}

4.2 核心 I/O 函数的 ST 实现

c 复制代码
// 发送: 调用 OOP 的 send()
static ssize_t _modbus_rtu_send(modbus_t *ctx, const uint8_t *req, int req_length)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    return pdev->send(pdev, (uint8_t *)req, req_length, 1000);
}

// 接收一个字节: 调用 OOP 的 RecvByte()
static ssize_t _modbus_rtu_recv(modbus_t *ctx, uint8_t *rsp, int rsp_length, int timeout)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    return pdev->RecvByte(pdev, rsp, timeout);
}

// 初始化: 调用 OOP 的 Init()
static int _modbus_rtu_connect(modbus_t *ctx)
{
    modbus_rtu_t *ctx_rtu = ctx->backend_data;
    struct UART_Device *pdev = ctx_rtu->dev;
    pdev->Init(pdev, ctx_rtu->baud, ctx_rtu->parity,
               ctx_rtu->data_bit, ctx_rtu->stop_bit);
    return 0;
}

五、情景一:主机发送请求

场景:PC 上位机作为主站,通过 modbus_read_registers 读从站。

复制代码
用户调用: modbus_read_registers(ctx, addr=0, nb=1, dest)
  │
  ▼
modbus.c: send_msg()           ← 组装请求帧
  │ ① build_request_basis()    ← 填地址+功能码+起始地址+数量
  │ ② send_msg_pre()           ← 追加 CRC
  │ ③ backend->send()          ← ★ 调 _modbus_rtu_send
  │                                → pdev->send(pdev, req, len, 1000)
  │                                → HAL_UART_Transmit_DMA(&huart2, ...)
  │                                → xSemaphoreTake(TX_Semaphore)
  │
  ▼
发送完成, 等待响应:
  │ receive_msg(ctx, rsp, MSG_CONFIRMATION)
  │   while (未收完) {
  │     backend->recv()         ← 逐字节收
  │       → pdev->RecvByte(pdev, &byte, timeout)
  │       → xQueueReceive(RX_Queue, &byte, timeout)
  │   }
  │   check_integrity()         ← CRC 校验
  ▼
返回给用户: dest = 读到的寄存器值

六、情景二:从机接收请求

复制代码
用户调用 (while 循环中):
  modbus_receive(ctx, query)
    │
    ▼
  receive_msg(ctx, req, MSG_INDICATION)
    │
    ├─ while (未收完一帧) {
    │     backend->recv()       ← _modbus_rtu_recv
    │       → pdev->RecvByte(pdev, &byte, BYTE_TIMEOUT)
    │       → xQueueReceive(RX_Queue, &byte, timeout)
    │
    │     if (超时) return -1
    │     if (帧间超时 && 已收到一些字节) break  ← IDLE 自然触发帧边界
    │  }
    │
    └─ check_integrity()        ← CRC 校验
         if (CRC 不匹配 && 开启了错误恢复)
           flush()               ← 清空缓冲, 准备收下一帧

关键细节 :从机收帧时,依赖 IDLE 中断的天然帧边界。HAL_UARTEx_ReceiveToIdle_DMA 一次收一帧,数据已经在 Queue 里。modbus_receive 从 Queue 逐字节取,超时判断帧结束。


七、情景三:从机构造回应

复制代码
收到请求后, 用户调用:
  modbus_reply(ctx, query, query_length, mb_mapping)
    │
    ├─ 解析功能码:
    │     FC_READ_COILS        → 读 tab_bits, build 响应
    │     FC_READ_HOLDING_REGS → 读 tab_registers, build 响应
    │     FC_WRITE_SINGLE_COIL → 写 tab_bits, build 确认
    │     ...
    │
    ├─ 构造响应帧:
    │     build_response_basis() ← 填地址+功能码
    │     send_msg_pre()         ← 追加 CRC
    │
    └─ 发送:
          backend->send()       ← _modbus_rtu_send
            → pdev->send(pdev, rsp, rsp_length, 1000)

八、与 UART_Device 的桥接

回顾整个调用链,OOP 封装的价值在这里充分体现:

复制代码
libmodbus (协议层, 不知道用哪个串口)
   │
   ▼ backend->send() / backend->recv() / backend->connect()
   │
UART_Device (抽象层, 不知道底层是哪种 HAL)
   │
   ▼ pdev->send() / pdev->RecvByte() / pdev->Init()
   │
HAL + DMA + FreeRTOS (物理层)

换一个后端只改一行

c 复制代码
// 用板载 UART4 (接 RS-485)
ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);

// 用 USB CDC (虚拟串口, 接 PC)
ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);

九、常见坑

9.1 GetUARTDevice 返回 NULL

modbus_new_st_rtu 内部会检查 GetUARTDevice 的返回值。如果设备名不存在于 g_uart_devices[] 数组中,返回 NULL。检查是否在 uart_device.c 里注册了对应的 g_xxx_dev

9.2 modbus_receive 返回 0(过滤帧)

从机模式下,modbus_receive 对不匹配的从站地址返回 0(静默忽略),不是错误。上层的标准写法:

c 复制代码
do {
    rc = modbus_receive(ctx, query);
} while (rc == 0);  // 过滤不匹配的帧

9.3 CRC 校验失败

收到 CRC 错误时,如果 error_recovery 启用了 MODBUS_ERROR_RECOVERY_PROTOCOL,libmodbus 会自动调用 flush() 清空缓冲。否则需要手动处理。

9.4 malloc → pvPortMalloc

标准 libmodbus 用 malloc,ST 移植版全部替换为 pvPortMalloc(FreeRTOS 安全分配)。新增代码时注意不要混用。


十、结尾

本篇走完了 libmodbus 的三个核心情景和数据结构全景:

  • modbus_t + modbus_backend_t vtable + modbus_rtu_t 的层次设计
  • modbus-st-rtu.c 如何把 POSIX I/O 桥接到 UART_Device
  • 主机发送(③send→) / 从机接收(RecvByte←) / 从机回应(③send→) 三条调用链

学习路径回顾:

复制代码
Note 10:    DMA+IDLE (物理层基础)
Note 11:    RTOS 信号量 (多任务基础)
Note 12:    UART_Device OOP (抽象层)
Note 13:    Modbus 协议分析 (协议层)
Note 14:    libmodbus 源码分析 ← 本篇
相关推荐
12.=0.1 小时前
【stm32_8】IIC内部集成电路——IIC的时序、利用IO口模拟IIC的时序、IIC通信器件的读写使用、半导体存储器的基本概述
c语言·stm32·单片机·嵌入式硬件
05候补工程师1 小时前
【408考研】数据结构核心笔记:单链表与栈操作精髓总结
数据结构·笔记·考研·链表·c#
kdxiaojie1 小时前
U-Boot分析【学习笔记】(7)
linux·笔记·学习
加贝哥|usun1 小时前
我的Vibe Coding项目开源了:CHM转PDF批量文档转化工具
pdf·ai编程
namas88481 小时前
APLC IDE 用户手册
ide·单片机·嵌入式硬件
Huanzhi_Lin1 小时前
skynet笔记
笔记·lua·skynet·actor·actor模型
草莓熊Lotso3 小时前
【Linux网络】UDP Socket 编程全解析:从回显服务到通用字典服务,从零实现工业级代码
linux·运维·服务器·数据库·c++·单片机·udp
海石8 小时前
📱随时随地大小编:TraeSolo 移动端初体验
前端·ai编程·trae
mCell8 小时前
批判性思维:AI 时代程序员最容易忽视的能力
ai编程·claude·vibecoding