☰
libmodbus 输入寄存器读取指南:modbus_read_input_registers 的用法、协议原理与源码剖析
2026/10/4 1:43:06 网站建设 项目流程
  • 通信
  • 嵌入式
  • 物联网

【免费下载链接】libmodbus

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

项目地址:https://gitcode.com/gh_mirrors/li/libmodbus
点击查看免费下载

导读:本文以 libmodbus 官方文档 docs/modbus_read_input_registers.md 为主线,系统讲解modbus_read_input_registers()这一核心 API:如何通过 Modbus 功能码 0x04 一次批量读取从站(Slave/Server)的输入寄存器,涵盖函数签名、参数约束、返回值与错误码、RTU/TCP 两种后端下的请求帧构造、底层源码实现以及配套测试用例。读完本文,你将能在 Linux、macOS、FreeBSD 或 Windows 上编写出可靠的输入寄存器采集程序,并理解其与保持寄存器(holding registers)读取在协议语义上的差异。

函数签名与头文件

modbus_read_input_registers()声明在公共头文件 src/modbus.h 中,属于 libmodbus 公开 API 的一员:

int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);

使用前需#include <modbus.h>,并通过 modbus_new_rtu() 或 modbus_new_tcp() / modbus_new_tcp_pi() 创建modbus_t *ctx上下文,再调用 modbus_connect() 建立连接。函数在 src/modbus.c 中实现。

功能概述:一次读取多个输入寄存器

该函数读取远程设备中起始地址为addr、数量为nb的输入寄存器,并将读取结果以 16 位字(word)的形式存入dest数组。它使用 Modbus 功能码0x04(Read Input Registers),与读取保持寄存器的 0x03 相对。

在 Modbus 协议的历史语义中:

  • 输入寄存器(Input Registers):只读,通常承载设备的测量值、状态量(如温度、电压、传感器采样),由设备本身维护,客户端只能读不能写;
  • 保持寄存器(Holding Registers):可读写,用于存放可配置参数或可更新的数据区。

官方文档也明确指出:随着工业实践演进,如今更常见的是只使用保持寄存器。因此modbus_read_input_registers()主要用于对接那些仍然遵循"输入寄存器只读"传统布局的老式设备或特定仪表。

参数说明

参数含义约束
ctxlibmodbus 上下文指针不能为 NULL,否则返回 -1 并置errno = EINVAL
addr起始寄存器地址(0 起始)需在设备地址范围内;越界由从站返回异常响应
nb要读取的寄存器数量必须 ≥ 1 且 ≤MODBUS_MAX_READ_REGISTERS(125)
dest存放读取结果的uint16_t数组必须分配至少nb * sizeof(uint16_t)字节

缓冲区责任在调用方:dest数组必须由调用者预先分配且容量足够容纳全部寄存器,这是文档强调的硬性前提:

Thedestarray must be allocated with at leastnb * sizeof(uint16_t)bytes. It is the caller's responsibility to ensure the buffer is large enough to hold all the registers to be read.

即uint16_t dest[nb];或malloc(nb * sizeof(uint16_t))均可,但绝不能分配不足。

返回值

  • 成功:返回实际读取到的寄存器个数(即nb);
  • 失败:返回 -1,并通过errno报告具体错误,可用 modbus_strerror() 获取可读的错误描述。

错误码详解

官方文档列出了两种明确的错误场景,此外还有网络层面的错误会透传自底层send_msg/_modbus_receive_msg:

  • EINVAL:ctx或dest为 NULL,或nb小于 1。见 src/modbus.c 中的显式校验;
  • EMBXILVAL:请求的寄存器数量超出上限(nb > MODBUS_MAX_READ_REGISTERS)。EMBXILVAL在 src/modbus.h 中定义为MODBUS_ENOBASE + MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE,即"非法数据值"协议异常对应的本地错误码。

另外需注意:函数内部虽会对nb做本地校验,但addr是否越界属于从站侧裁决的范畴——若地址非法,从站会回送MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS(0x02)异常帧,此时函数同样返回 -1,errno被置为EMBXILADD(见 src/modbus.h),这一点在单元测试中也有覆盖(下文详述)。

源码级原理剖析

1. 参数校验与功能码分发

入口函数在 src/modbus.c 中的逻辑非常清晰:先校验ctx、dest非空且nb在[1, MODBUS_MAX_READ_REGISTERS]范围内,随后把实际工作委托给静态函数read_registers(),并传入功能码MODBUS_FC_READ_INPUT_REGISTERS(值为 0x04,定义于 src/modbus.h):

int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest) { int status; if (ctx == NULL || dest == NULL) { errno = EINVAL; return -1; } if (nb < 1 || nb > MODBUS_MAX_READ_REGISTERS) { if (ctx->debug) { fprintf(stderr, "ERROR Too many input registers requested (%d > %d)\n", nb, MODBUS_MAX_READ_REGISTERS); } errno = EMBXILVAL; return -1; } status = read_registers(ctx, MODBUS_FC_READ_INPUT_REGISTERS, addr, nb, dest); return status; }

可见:启用调试模式(modbus_set_debug())时,越界请求会在 stderr 打印详细提示,方便排障。

2. 底层 read_registers:建帧、发送、收帧、解析

核心的read_registers()位于 src/modbus.c,是 0x03 与 0x04 两个功能码共用的实现(这正是两个 API 行为对称的原因):

static int read_registers(modbus_t *ctx, int function, int addr, int nb, uint16_t *dest) { int rc; int req_length; uint8_t req[_MIN_REQ_LENGTH]; uint8_t rsp[MAX_MESSAGE_LENGTH]; if (nb > MODBUS_MAX_READ_REGISTERS) { ... errno = EMBXILVAL; return -1; } req_length = ctx->backend->build_request_basis(ctx, function, addr, nb, req); rc = send_msg(ctx, req, req_length); if (rc > 0) { unsigned int offset; int i; rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION); if (rc == -1) return -1; rc = check_confirmation(ctx, req, rsp, rc); if (rc == -1) return -1; offset = ctx->backend->header_length; for (i = 0; i < rc; i++) { /* shift reg hi_byte to temp OR with lo_byte */ dest[i] = (rsp[offset + 2 + (i << 1)] << 8) | rsp[offset + 3 + (i << 1)]; } } return rc; }

关键点有三个:

  • 请求帧由后端(backend)构造:ctx->backend->build_request_basis是抽象接口,RTU 与 TCP 各有实现;
  • 响应校验:check_confirmation()负责比对功能码、地址与字节数是否一致,任何异常(含从站回送的协议异常帧)都会在此转化为 -1 与对应errno;
  • 字节序转换:Modbus 线上传输为大端序(高位字节在前),解析时通过(高字节 << 8) | 低字节还原为处理器本机字节序存入dest。这一点非常重要:dest中的值已经是主机序的 16 位整数,可直接参与运算,无需再手工翻转。

3. 两种后端的请求帧构造

RTU 后端(串口 / RS-485)在 src/modbus-rtu.c 中构造的 PDU 为[从站地址(1)][功能码(1)][起始地址高(1)][起始地址低(1)][数量高(1)][数量低(1)],之后由_modbus_rtu_send追加 CRC16 校验:

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; /* 0x04 */ req[2] = addr >> 8; /* 起始地址高字节 */ req[3] = addr & 0x00ff; /* 起始地址低字节 */ req[4] = nb >> 8; /* 数量高字节 */ req[5] = nb & 0x00ff; /* 数量低字节 */ return _MODBUS_RTU_PRESET_REQ_LENGTH; }

注意 RTU 后端要求先通过 modbus_set_slave() 设置从站地址(assert(ctx->slave != -1)即此约束)。

TCP 后端在 src/modbus-tcp.c 中会在 PDU 前追加 MBAP 头(事务 ID、协议 ID、长度、单元 ID),并自动递增事务 IDt_id;modbus_new_tcp_pi()创建的 PI 后端(src/modbus-tcp.c)复用同一套建帧逻辑,只是地址解析方式不同。两种 TCP 后端的 ADU 上限均为 260 字节(src/modbus.h)。

4. 服务端(从站)侧如何应答 0x04

在服务端程序中,modbus_reply()对MODBUS_FC_READ_HOLDING_REGISTERS与MODBUS_FC_READ_INPUT_REGISTERS走同一分支(src/modbus.c),通过is_input区分取数来源:

case MODBUS_FC_READ_HOLDING_REGISTERS: case MODBUS_FC_READ_INPUT_REGISTERS: { unsigned int is_input = (function == MODBUS_FC_READ_INPUT_REGISTERS); int start_registers = is_input ? mb_mapping->start_input_registers : mb_mapping->start_registers; int nb_registers = is_input ? mb_mapping->nb_input_registers : mb_mapping->nb_registers; uint16_t *tab_registers = is_input ? mb_mapping->tab_input_registers : mb_mapping->tab_registers; ... int mapping_address = address - start_registers; if (nb < 1 || MODBUS_MAX_READ_REGISTERS < nb) { /* 回送 ILLEGAL_DATA_VALUE 异常 */ } else if (mapping_address < 0 || (mapping_address + nb) > nb_registers) { /* 回送 ILLEGAL_DATA_ADDRESS 异常 */ } else { rsp_length = ctx->backend->build_response_basis(&sft, rsp); rsp[rsp_length++] = nb << 1; /* 数据字节数 = 寄存器数 × 2 */ for (i = mapping_address; i < mapping_address + nb; i++) { rsp[rsp_length++] = tab_registers[i] >> 8; /* 高字节 */ rsp[rsp_length++] = tab_registers[i]; /* 低字节 */ } } } break;

这段代码印证了两点协议行为:

  1. 输入寄存器数据存放在映射结构modbus_mapping_t的tab_input_registers数组中(src/modbus.h),服务端用 modbus_mapping_new() 或 modbus_mapping_new_start_address() 创建映射时需为其分配空间;
  2. 响应中每个寄存器占 2 字节、大端序发送,与客户端解析逻辑(高字节 << 8 | 低字节)正好一一对应。

5. 上限约束的来源

MODBUS_MAX_READ_REGISTERS定义为 125(src/modbus.h),其依据是 Modbus 应用协议规范(Modbus_Application_Protocol_V1_1b.pdf第 6 章第 3 节):单次读寄存器请求的数量上限为 1~125。这一限制源自串行链路 256 字节 ADU 的约束(253 字节 PDU,见 src/modbus.h)。因此无论 RTU 还是 TCP,一次modbus_read_input_registers()最多只能取 125 个寄存器,超过即触发EMBXILVAL;读取更多数据需分段多次调用。

完整可运行示例

下面是一个 RTU 客户端完整采集输入寄存器的示例,可直接作为工程模板:

#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <modbus.h> #define NB_REGISTERS 10 int main(void) { modbus_t *ctx; uint16_t dest[NB_REGISTERS]; int rc, i; /* 1. 创建 RTU 上下文:设备、波特率、校验、数据位、停止位 */ ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); if (ctx == NULL) { fprintf(stderr, "Unable to create the libmodbus context\n"); return -1; } /* 2. 指定从站地址(RTU 必需) */ modbus_set_slave(ctx, 1); /* 3. 建立连接(打开串口) */ if (modbus_connect(ctx) == -1) { fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno)); modbus_free(ctx); return -1; } /* 4. 读取从地址 0 开始的 10 个输入寄存器 */ rc = modbus_read_input_registers(ctx, 0, NB_REGISTERS, dest); if (rc == -1) { fprintf(stderr, "modbus_read_input_registers failed: %s\n", modbus_strerror(errno)); } else { printf("Read %d input registers:\n", rc); for (i = 0; i < rc; i++) { printf("reg[%d] = 0x%04X (%u)\n", i, dest[i], dest[i]); } } /* 5. 清理 */ modbus_close(ctx); modbus_free(ctx); return 0; }

要点回顾:

  • nb取值 1~125,dest容量nb * sizeof(uint16_t);
  • TCP 场景只需将创建上下文换成modbus_new_tcp("192.168.1.100", 502),并去掉modbus_set_slave()的强制要求(TCP 下从站地址经 MBAP 单元 ID 传递,默认 255,见 modbus_set_slave());
  • 返回值是实际读取的寄存器数,判断成功请与nb比较或检查是否-1。

服务端配套:如何提供输入寄存器数据

若需模拟从站供上述客户端读取,服务端需在映射中初始化tab_input_registers(对应功能码 0x04 的数据源)。仓库测试 tests/unit-test-server.c 展示了这一模式:

mb_mapping = modbus_mapping_new(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); ... /* Initialize values of INPUT REGISTERS */ for (i = 0; i < UT_INPUT_REGISTERS_NB; i++) { mb_mapping->tab_input_registers[i] = UT_INPUT_REGISTERS_TAB[i]; }

随后在modbus_receive()/modbus_reply()循环中即可响应客户端的 0x04 请求。

测试用例验证

libmodbus 自带完整的单元测试,可直接验证本文所述行为(运行tests/unit-tests.sh或参考 tests/Makefile.am):

  • 正常读取:客户端 tests/unit-test-client.c 从地址0x108读取UT_INPUT_REGISTERS_NB(1 个)输入寄存器,断言返回值等于nb,并逐元素比对期望值{0x000A}(定义于 tests/unit-test.h.in):
    rc = modbus_read_input_registers( ctx, UT_INPUT_REGISTERS_ADDRESS, UT_INPUT_REGISTERS_NB, tab_rp_registers); printf("1/1 modbus_read_input_registers: "); ASSERT_TRUE(rc == UT_INPUT_REGISTERS_NB, "FAILED (nb points %d)\n", rc);
  • 越界地址:tests/unit-test-client.c 验证读取地址 0 及UT_INPUT_REGISTERS_ADDRESS + nb(超出映射范围)时返回 -1 且errno == EMBXILADD(从站回送非法数据地址异常);
  • 超量请求:tests/unit-test-client.c 验证请求MODBUS_MAX_READ_REGISTERS + 1(即 126)个寄存器时返回 -1 且errno == EMBXILVAL,与文档 Errors 一节完全对应;
  • 代理(proxy)链路:tests/proxy-test-client.c 通过modbus_proxy()转发后读取输入寄存器,验证经网关代理后功能码 0x04 的数据路径依然正确。

常见问题与注意事项

  1. 为什么返回 -1 但 errno 是EMBXILADD?这表示请求已到达从站,但从站认为地址越界并回送了异常帧;属于协议级错误而非本地参数错误,可通过 modbus_strerror() 打印 "Illegal data address" 类描述。
  2. 如何读取超过 125 个输入寄存器?分段调用:例如读取 300 个寄存器,可拆成 3 次(125 + 125 + 50),每次用不同的addr与nb组合,注意dest的偏移。
  3. dest中的值是大端还是小端?已由 libmodbus 统一转换为处理器本机字节序,直接使用即可;只有当你自行构造原始帧(modbus_send_raw_request())时才需要关心线上大端序。
  4. 串口上的从站地址:RTU 后端建帧时assert(ctx->slave != -1),忘记调用 modbus_set_slave() 会在调试构建中直接断言失败。
  5. 输入寄存器与保持寄存器如何选择?优先使用 modbus_read_registers()(0x03);仅当设备手册明确将数据放在输入寄存器区域(只读测量区)时,才使用本文的 0x04 读取。

参见

  • modbus_read_input_bits() 读取输入位(功能码 0x02)
  • modbus_read_registers() 读取保持寄存器(功能码 0x03)
  • modbus_write_register() 写单个保持寄存器
  • modbus_write_registers() 写多个保持寄存器
  • 公共 API 声明与常量定义:src/modbus.h
  • 核心实现:src/modbus.c
  • 单元测试:tests/unit-test-client.c、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
点击查看免费下载

相关推荐

上一篇:ClaudeComputerCommander 计算机健康检查:Linux 只读诊断命令集与解读指南
下一篇:Obsidian CSS自定义实战指南:3个阶段实现界面优化与效率飞跃

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询