云计算百科
云计算领域专业知识百科平台

libmodbus 的 modbus_mapping_new() 函数详解:为 Modbus 服务器一次性分配四类数据区

  • 通信
  • 嵌入式
  • 物联网

【免费下载链接】libmodbus

A Modbus library for Linux, Mac OS, FreeBSD and Windows

项目地址:
https://gitcode.com/gh_mirrors/li/libmodbus

点击查看 免费下载

导读:本文以 libmodbus 官方文档 docs/modbus_mapping_new.md 为主线,深入讲解 modbus_mapping_new() 这个专为 Modbus 服务器/从站设计的映射分配函数。你将掌握它的函数原型、四类数据区的内存布局与零值语义、返回值与错误处理方式,并通过仓库内的测试服务器源码(如 tests/unit-test-server.c)理解它如何配合 modbus_reply() 完成请求应答,最终能够独立写出可运行、可释放的 Modbus 服务器数据映射代码。

modbus_mapping_new() 是 libmodbus 中用于在 Modbus 服务器/从站(server/slave)侧分配数据存储区的核心 API。它一次调用即可为 Modbus 标准的四类地址空间——线圈(Coils,0x)、离散输入(Discrete Inputs,1x)、保持寄存器(Holding Registers,4x)和输入寄存器(Input Registers,3x)——分别创建存储数组,并将指针统一封装进 modbus_mapping_t 结构体中,供后续的请求处理函数使用。

函数原型与基本用途

modbus_mapping_t* modbus_mapping_new(int nb_bits, int nb_input_bits, int nb_registers, int nb_input_registers);

该函数会分配四个数组,分别用于存储:

参数对应数据区数组元素类型地址空间
nb_bits 线圈(Coils) uint8_t 0x
nb_input_bits 离散输入(Discrete Inputs) uint8_t 1x
nb_registers 保持寄存器(Holding Registers) uint16_t 4x
nb_input_registers 输入寄存器(Input Registers) uint16_t 3x

四个数组的指针被保存在 modbus_mapping_t 结构体中。该结构体的定义位于 src/modbus.h,除数组指针外还记录了各数据区的数量(nb_*)和起始地址(start_*):

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;

所有数组中的元素都会被初始化为零。这一点在实现中由 memset() 保证:分配成功后立即将整块内存清零(见 src/modbus.c),因此新建的映射天然处于“全部为 0/0x0000”的初始状态,无需手工清空。

内部实现:它是 modbus_mapping_new_start_address() 的便捷封装

从源码看,modbus_mapping_new() 并不是独立实现的分配逻辑,而是对 modbus_mapping_new_start_address 的简单封装。其实现位于 src/modbus.c:

modbus_mapping_t *modbus_mapping_new(int nb_bits,
int nb_input_bits,
int nb_registers,
int nb_input_registers)
{
/* Reject negative counts: they would otherwise be converted to very large
unsigned dimensions and drive huge allocations. */
if (nb_bits < 0 || nb_input_bits < 0 || nb_registers < 0 ||
nb_input_registers < 0) {
errno = EINVAL;
return NULL;
}

return modbus_mapping_new_start_address(
0, nb_bits, 0, nb_input_bits, 0, nb_registers, 0, nb_input_registers);
}

这里有两个值得注意的实现细节:

  • 等价性:该函数等价于以全部起始地址为 0 调用 modbus_mapping_new_start_address(0, nb_bits, 0, nb_input_bits, 0, nb_registers, 0, nb_input_registers),因此各数组的第一个元素即对应地址 0,数组下标与 Modbus 地址一一对应。
  • 负数防护:由于本函数采用 int 型参数(而 modbus_mapping_new_start_address() 使用 unsigned int),负数会被转换成极大的无符号数,从而触发超大内存分配。源码在转发前显式检查并返回 EINVAL 拒绝负值,这是一个面向不可信配置的安全设计。
  • 零值语义:跳过不需要的数据区

    Modbus 服务器不一定需要暴露全部四类数据区。例如纯输出型设备可能没有输入寄存器,此时可将对应参数传 0,该数组不会被分配,对应的指针为 NULL:

    /* 只分配线圈与保持寄存器,离散输入、输入寄存器不分配 */
    mb_mapping = modbus_mapping_new(500, 0, 500, 0);
    /* 此时 mb_mapping->tab_input_bits 与 tab_input_registers 均为 NULL */

    这种零值语义在源码中体现为“按区分配”的结构:modbus_mapping_new_start_address() 对每个数据区单独判断 nb_* == 0,为零时指针置 NULL 且不执行 malloc,否则分配并清零(见 src/modbus.c)。仓库的随机测试服务器正是这么做的:

    mb_mapping = modbus_mapping_new(500, 500, 500, 500);

    见 tests/random-test-server.c。

    尺寸上限:65536 与 MODBUS_MAX_TABLE_SIZE

    由于 Modbus 地址空间为 16 位,每个数据区最多只能寻址 65536 个条目。nb_bits、nb_input_bits、nb_registers、nb_input_registers 四个参数均不得超过 65536;更大的值会被拒绝,以避免由不可信配置驱动的超大内存分配。

    这一约束在实现中通过 MODBUS_MAX_TABLE_SIZE 宏(定义于 src/modbus.c,值为 65536)落实:

    if (nb_bits > MODBUS_MAX_TABLE_SIZE || nb_input_bits > MODBUS_MAX_TABLE_SIZE ||
    nb_registers > MODBUS_MAX_TABLE_SIZE ||
    nb_input_registers > MODBUS_MAX_TABLE_SIZE) {
    errno = EINVAL;
    return NULL;
    }

    见 src/modbus.c。按最大尺寸估算内存开销:线圈与离散输入每个元素占 1 字节(uint8_t),64K 位约 64 KB;保持与输入寄存器每个元素占 2 字节(uint16_t),64K 寄存器约 128 KB。实践中通常远小于此上限。

    另外需要注意,单次 Modbus 读请求的数量另有约束:MODBUS_MAX_READ_BITS(2000)与 MODBUS_MAX_READ_REGISTERS(125)定义于 src/modbus.h,映射数组只需覆盖设备实际使用的地址范围即可,无需与请求上限对齐。

    返回值与错误处理

    • 成功时:返回新分配并已清零的 modbus_mapping_t * 结构体。
    • 失败时:返回 NULL 并设置 errno。

    可能的错误码如下:

    errno含义触发条件
    EINVAL 无效参数 任一请求的数组维度超过 65536 条目(modbus_mapping_new() 中还包括负数参数)
    ENOMEM 内存不足 malloc 分配任一数组或结构体失败

    值得注意的是,源码对分配失败做了渐进式回滚:例如保持寄存器分配失败时,会依次释放已分配的输入寄存器、离散输入、线圈数组和结构体本身,避免内存泄漏(见 src/modbus.c),调用方无需在失败路径上手工清理部分数组。

    官方文档给出的标准错误处理模式如下(见 docs/modbus_mapping_new.md):

    /* The first value of each array is accessible from the 0 address. */
    mb_mapping = modbus_mapping_new(
    BITS_ADDRESS + BITS_NB,
    INPUT_BITS_ADDRESS + INPUT_BITS_NB,
    REGISTERS_ADDRESS + REGISTERS_NB,
    INPUT_REGISTERS_ADDRESS + INPUT_REGISTERS_NB
    );
    if (mb_mapping == NULL) {
    fprintf(
    stderr, "Failed to allocate the mapping: %s\\n",
    modbus_strerror(errno)
    );
    modbus_free(ctx);
    return -1;
    }

    注意该示例中使用了 modbus_strerror(errno) 将 errno 转换为可读字符串,并在失败时释放此前已创建的 Modbus 上下文 ctx(由 modbus_free(ctx) 完成),随后以 -1 退出,这是 libmodbus 服务器程序的典型启动序列。

    在服务器中的实战用法:填充映射并应答请求

    modbus_mapping_new() 的设计初衷是“方便在 Modbus 服务器/从站中处理请求”。分配好映射后,服务器需要:

  • 填充数据:直接向数组写入值(线圈/离散输入为 0 或 1,寄存器为 uint16_t);
  • 应答请求:把映射指针传给 modbus_reply(),由它根据请求中的功能码与地址自动读写映射数组并构造响应(原型见 src/modbus.h)。
  • 仓库的单元测试服务器 tests/unit-test-server.c 演示了完整流程(它使用带起始地址的变体,但填充与应答逻辑完全相同):

    mb_mapping = modbus_mapping_new_start_address(UT_BITS_ADDRESS,
    UT_BITS_NB,
    UT_INPUT_BITS_ADDRESS,
    UT_INPUT_BITS_NB,
    UT_REGISTERS_ADDRESS,
    UT_REGISTERS_NB_MAX,
    UT_INPUT_REGISTERS_ADDRESS,
    UT_INPUT_REGISTERS_NB);
    if (mb_mapping == NULL) {
    fprintf(stderr, "Failed to allocate the mapping: %s\\n", modbus_strerror(errno));
    modbus_free(ctx);
    return -1;
    }

    /* 服务器侧填充只读输入值 */
    modbus_set_bits_from_bytes(
    mb_mapping->tab_input_bits, 0, UT_INPUT_BITS_NB, UT_INPUT_BITS_TAB);

    for (i = 0; i < UT_INPUT_REGISTERS_NB; i++) {
    mb_mapping->tab_input_registers[i] = UT_INPUT_REGISTERS_TAB[i];
    }

    其中 UT_BITS_ADDRESS(0x130)、UT_REGISTERS_ADDRESS(0x160)等测试地址定义在 tests/unit-test.h.in。服务器主循环随后反复调用 modbus_receive() 接收请求,并将映射交给 modbus_reply(ctx, query, rc, mb_mapping) 处理(见 tests/unit-test-server.c),退出循环后调用 modbus_mapping_free(mb_mapping) 释放映射(见 tests/unit-test-server.c)。

    带宽测试服务器则展示了“零值跳过 + 请求上限”的组合用法(见 tests/bandwidth-server-one.c 与 tests/bandwidth-server-many-up.c):

    modbus_mapping_new(MODBUS_MAX_READ_BITS, 0, MODBUS_MAX_READ_REGISTERS, 0);

    即:按单次请求可读取的最大位数(2000)与最大寄存器数(125)分配线圈和保持寄存器,其余两类数据区不分配。

    与 modbus_mapping_new_start_address() 的关系与选型

    两个函数的行为对比如下:

    特性modbus_mapping_new()modbus_mapping_new_start_address()
    起始地址 固定全部为 0 每类数据区可独立指定
    参数类型 int unsigned int
    地址映射关系 数组下标 == 地址 数组下标 + start == 地址

    modbus_mapping_new() 适合地址从 0 开始的常规设备。而带起始地址的变体可以把映射“挂”到任意高位地址而不浪费内存——例如只暴露寄存器 340~349,可用 modbus_mapping_new_start_address(0, 0, 0, 0, 340, 10, 0, 0),此时 tab_registers 只分配 10 个元素,客户端访问地址 340 对应数组下标 0(详见 docs/modbus_mapping_new_start_address.md)。

    释放映射:modbus_mapping_free()

    与分配配对的是释放函数(原型见 docs/modbus_mapping_free.md):

    void modbus_mapping_free(modbus_mapping_t *mb_mapping);

    它依次释放四个数组(tab_input_registers、tab_registers、tab_input_bits、tab_bits),最后释放 modbus_mapping_t 结构体本身;传入 NULL 时安全返回,无返回值(见 src/modbus.c)。

    务必区分两组释放函数:

    • modbus_mapping_free(mb_mapping) —— 释放数据映射;
    • modbus_free(ctx) —— 释放 Modbus 上下文。

    两者相互独立,服务器退出时都需要调用,顺序通常为先释放映射再释放上下文(见 tests/unit-test-server.c)。

    小结

    modbus_mapping_new() 是 libmodbus 服务器端编程的入口级 API:一次调用完成四类数据区的分配与清零,零值参数跳过不需要的数据区,65536 上限与负数检查提供了防御性保护,失败时 errno 区分参数错误(EINVAL)与内存不足(ENOMEM)。结合 modbus_reply() 与 modbus_mapping_free(),即可搭建完整可用的 Modbus 服务器。相关文档与源码可继续阅读:

    • 官方文档:docs/modbus_mapping_new.md、docs/modbus_mapping_new_start_address.md、docs/modbus_mapping_free.md
    • 核心实现:src/modbus.c(分配与释放)、src/modbus.h(结构体定义)
    • 参考实现:tests/unit-test-server.c(完整服务器流程)、tests/unit-test.h.in(地址常量定义)

    赞

    分享

    • 通信
    • 嵌入式
    • 物联网

    【免费下载链接】libmodbus

    A Modbus library for Linux, Mac OS, FreeBSD and Windows

    项目地址:
    https://gitcode.com/gh_mirrors/li/libmodbus

    点击查看 免费下载

    上一篇:
    CANN/asc-devkit核间同步到达API

    下一篇:
    CANN ops-nn 图融合 Pass 深度解析:WeightQuantBatchMatmulV2 Transpose 融合机制、约束与源码实现

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » libmodbus 的 modbus_mapping_new() 函数详解:为 Modbus 服务器一次性分配四类数据区
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!