- 通信
- 嵌入式
- 物联网
【免费下载链接】libmodbus
A Modbus library for Linux, Mac OS, FreeBSD and Windows
导读:本文以 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()主要用于对接那些仍然遵循"输入寄存器只读"传统布局的老式设备或特定仪表。
参数说明
| 参数 | 含义 | 约束 |
|---|---|---|
ctx | libmodbus 上下文指针 | 不能为 NULL,否则返回 -1 并置errno = EINVAL |
addr | 起始寄存器地址(0 起始) | 需在设备地址范围内;越界由从站返回异常响应 |
nb | 要读取的寄存器数量 | 必须 ≥ 1 且 ≤MODBUS_MAX_READ_REGISTERS(125) |
dest | 存放读取结果的uint16_t数组 | 必须分配至少nb * sizeof(uint16_t)字节 |
缓冲区责任在调用方:dest数组必须由调用者预先分配且容量足够容纳全部寄存器,这是文档强调的硬性前提:
The
destarray 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;这段代码印证了两点协议行为:
- 输入寄存器数据存放在映射结构
modbus_mapping_t的tab_input_registers数组中(src/modbus.h),服务端用 modbus_mapping_new() 或 modbus_mapping_new_start_address() 创建映射时需为其分配空间; - 响应中每个寄存器占 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 但 errno 是
EMBXILADD?这表示请求已到达从站,但从站认为地址越界并回送了异常帧;属于协议级错误而非本地参数错误,可通过 modbus_strerror() 打印 "Illegal data address" 类描述。 - 如何读取超过 125 个输入寄存器?分段调用:例如读取 300 个寄存器,可拆成 3 次(125 + 125 + 50),每次用不同的
addr与nb组合,注意dest的偏移。 dest中的值是大端还是小端?已由 libmodbus 统一转换为处理器本机字节序,直接使用即可;只有当你自行构造原始帧(modbus_send_raw_request())时才需要关心线上大端序。- 串口上的从站地址:RTU 后端建帧时
assert(ctx->slave != -1),忘记调用 modbus_set_slave() 会在调试构建中直接断言失败。 - 输入寄存器与保持寄存器如何选择?优先使用 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
相关推荐
libmodbus源码架构分析:深入理解Modbus协议实现原理
libmodbus源码架构分析:深入理解Modbus协议实现原理 libmodbus是一个功能强大的开源Modbus协议库,它提供了完整的Modbus协议栈实现
通信嵌入式物联网NetworkX 地理空间网络分析实战指南:GeoPandas、PySAL、momepy 与 OSMnx 生态协同
NetworkX 地理空间网络分析实战指南:GeoPandas、PySAL、momepy 与 OSMnx 生态协同 地理空间数据(点、线、面)的网络化建模是 N
通信嵌入式物联网TiXL 上下文变量读取算子 GetVec3Var 完全指南:原理、用法与源码剖析
TiXL 上下文变量读取算子 GetVec3Var 完全指南:原理、用法与源码剖析 本篇技术指南围绕 TiXL(开源实时动态图形创作软件)算子库 Lib.flo
音视频图形学桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考