本文目录
- 1. 一、函数原型
- 2. 二、功能码与参数对应关系
- 3. 三、请求帧结构
- 4. 四、基础使用示例
- 5. 五、手动发送请求与接收响应
- 6. 六、自定义功能码实现
- 7. 七、RTU模式下的使用
- 8. 八、调试与抓包分析
- 9. 九、相关函数
- 10. 十、常见问题
modbus_build_request是libmodbus库中的一个底层函数,用于手动构建Modbus请求帧。在大多数情况下,开发者使用modbus_read_registers、modbus_write_bit等高层函数即可完成Modbus通讯,但在某些特殊场景(如自定义功能码、调试协议、实现非标准功能)下,需要手动构建请求帧。本文详细介绍modbus_build_request函数的原型、参数含义、使用方法以及完整的示例代码。
一、函数原型
int modbus_build_request(
modbus_t *ctx, // Modbus上下文对象
int function, // Modbus功能码
int addr, // 起始地址
int nb, // 数量/数值
uint8_t *req // 输出的请求帧缓冲区
);
1.1 参数详解
| 参数 | 类型 | 说明 |
|---|---|---|
| ctx | modbus_t * | Modbus上下文对象,由modbus_new_tcp()或modbus_new_rtu()创建 |
| function | int | Modbus功能码,如0x01(读线圈)、0x03(读保持寄存器)等 |
| addr | int | 起始地址(0-based),对于写单线圈/寄存器则是目标地址 |
| nb | int | 读取数量,或写入的值(功能码05时为0xFF00/0x0000,功能码06时为寄存器值) |
| req | uint8_t * | 输出缓冲区,用于存储构建的请求帧,大小至少为MODBUS_TCP_MAX_ADU_LENGTH(260字节) |
1.2 返回值
- 成功:返回请求帧的长度(字节数)
- 失败:返回-1,并设置errno
二、功能码与参数对应关系
不同功能码下,addr和nb参数的含义不同:
| 功能码 | 名称 | addr含义 | nb含义 |
|---|---|---|---|
| 0x01 | 读线圈 | 起始线圈地址 | 读取数量(1-2000) |
| 0x02 | 读离散输入 | 起始输入地址 | 读取数量(1-2000) |
| 0x03 | 读保持寄存器 | 起始寄存器地址 | 读取数量(1-125) |
| 0x04 | 读输入寄存器 | 起始寄存器地址 | 读取数量(1-125) |
| 0x05 | 写单个线圈 | 线圈地址 | 0xFF00=吸合,0x0000=断开 |
| 0x06 | 写单个寄存器 | 寄存器地址 | 写入的值(0-65535) |
| 0x0F | 写多个线圈 | 起始线圈地址 | 写入数量(1-1968)※需手动构建 |
| 0x10 | 写多个寄存器 | 起始寄存器地址 | 写入数量(1-123)※需手动构建 |
三、请求帧结构
modbus_build_request构建的请求帧格式取决于使用的协议模式:
3.1 Modbus TCP模式(MBAP头 + PDU)
// TCP请求帧结构(共12字节,以读保持寄存器为例)
// +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
// |事务ID高|事务ID低|协议ID高|协议ID低|长度高 |长度低 |从站地址|功能码 |地址高 |地址低 |数量高 |数量低 |
// +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
// | 0x00 | 0x01 | 0x00 | 0x00 | 0x00 | 0x06 | 0x01 | 0x03 | 0x00 | 0x00 | 0x00 | 0x0A |
// +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
// MBAP头(7字节):
// - 事务标识符(2字节):用于匹配请求和响应
// - 协议标识符(2字节):Modbus协议为0x0000
// - 长度(2字节):后续字节数(从站地址+PDU)
// - 从站地址(1字节):Unit ID
// PDU(协议数据单元):
// - 功能码(1字节)
// - 数据(N字节)
3.2 Modbus RTU模式(地址 + PDU + CRC)
// RTU请求帧结构(共8字节,以读保持寄存器为例)
// +--------+--------+--------+--------+--------+--------+--------+--------+
// |从站地址|功能码 |地址高 |地址低 |数量高 |数量低 |CRC低 |CRC高 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// | 0x01 | 0x03 | 0x00 | 0x00 | 0x00 | 0x0A | 0xC5 | 0xCD |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// RTU帧 = 从站地址(1字节) + PDU + CRC16(2字节)
// CRC低字节在前,高字节在后
四、基础使用示例
4.1 构建读保持寄存器请求
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
int main() {
modbus_t *ctx;
uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH];
int req_len;
int i;
// 创建TCP上下文
ctx = modbus_new_tcp("192.168.1.100", 502);
if (ctx == NULL) {
fprintf(stderr, "创建上下文失败\n");
return -1;
}
// 设置从站地址
modbus_set_slave(ctx, 1);
// 构建读保持寄存器请求:地址0,读取10个
req_len = modbus_build_request(ctx, 0x03, 0, 10, req);
if (req_len == -1) {
fprintf(stderr, "构建请求失败:%s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 打印请求帧(十六进制)
printf("请求帧长度:%d字节\n", req_len);
printf("请求帧内容:");
for (i = 0; i < req_len; i++) {
printf("%02X ", req[i]);
}
printf("\n");
// 输出示例(TCP模式):
// 请求帧长度:12字节
// 请求帧内容:00 01 00 00 00 06 01 03 00 00 00 0A
modbus_free(ctx);
return 0;
}
4.2 构建写单个线圈请求
// 构建写单个线圈请求:地址0,吸合(0xFF00)
req_len = modbus_build_request(ctx, 0x05, 0, 0xFF00, req);
printf("写线圈请求:");
for (i = 0; i < req_len; i++) printf("%02X ", req[i]);
printf("\n");
// 输出:00 01 00 00 00 06 01 05 00 00 FF 00
// 构建写单个线圈请求:地址0,断开(0x0000)
req_len = modbus_build_request(ctx, 0x05, 0, 0x0000, req);
// 输出:00 01 00 00 00 06 01 05 00 00 00 00
4.3 构建写单个寄存器请求
// 构建写单个寄存器请求:地址0,写入1234
req_len = modbus_build_request(ctx, 0x06, 0, 1234, req);
printf("写寄存器请求:");
for (i = 0; i < req_len; i++) printf("%02X ", req[i]);
printf("\n");
// 输出:00 01 00 00 00 06 01 06 00 00 04 D2
// 1234 = 0x04D2
五、手动发送请求与接收响应
构建请求帧后,可以使用modbus_send和modbus_receive手动发送和接收:
#include <stdio.h>
#include <modbus.h>
int main() {
modbus_t *ctx;
uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH];
uint8_t rsp[MODBUS_TCP_MAX_ADU_LENGTH];
int req_len, rsp_len;
ctx = modbus_new_tcp("192.168.1.100", 502);
modbus_set_slave(ctx, 1);
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败:%s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 1. 构建读保持寄存器请求
req_len = modbus_build_request(ctx, 0x03, 0, 10, req);
printf("请求长度:%d\n", req_len);
// 2. 发送请求
int sent = modbus_send(ctx, req, req_len);
if (sent != req_len) {
fprintf(stderr, "发送失败\n");
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
// 3. 接收响应
rsp_len = modbus_receive(ctx, rsp);
if (rsp_len == -1) {
fprintf(stderr, "接收失败:%s\n", modbus_strerror(errno));
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
// 4. 解析响应(TCP模式,跳过MBAP头7字节)
printf("响应长度:%d\n", rsp_len);
printf("功能码:0x%02X\n", rsp[7]);
if (rsp[7] == 0x03) {
// 读保持寄存器响应
// rsp[7] = 功能码
// rsp[8] = 字节数
// rsp[9...] = 寄存器数据(每个寄存器2字节,高字节在前)
int byte_count = rsp[8];
int reg_count = byte_count / 2;
printf("寄存器数量:%d\n", reg_count);
for (int i = 0; i < reg_count; i++) {
uint16_t value = (rsp[9 + i*2] << 8) | rsp[9 + i*2 + 1];
printf("寄存器%d:%d\n", i, value);
}
} else if (rsp[7] & 0x80) {
// 异常响应(功能码最高位为1)
printf("异常响应,异常码:0x%02X\n", rsp[8]);
}
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
六、自定义功能码实现
modbus_build_request的一个重要用途是实现设备厂商自定义的功能码。以下示例实现一个自定义功能码0x43(读取设备序列号):
// 自定义功能码:读取设备序列号
// 请求:功能码0x43,子功能码0x01
// 响应:功能码0x43,子功能码0x01,序列号长度,序列号数据
int read_device_serial(modbus_t *ctx, uint8_t *serial, int max_len) {
uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH];
uint8_t rsp[MODBUS_TCP_MAX_ADU_LENGTH];
int req_len, rsp_len;
// 注意:modbus_build_request只能构建标准功能码
// 对于自定义功能码,需要手动构建PDU部分
// 手动构建TCP请求帧
// MBAP头
req[0] = 0x00; req[1] = 0x01; // 事务ID
req[2] = 0x00; req[3] = 0x00; // 协议ID
req[4] = 0x00; req[5] = 0x04; // 长度(从站地址+功能码+子功能码)
req[6] = 0x01; // 从站地址(需与modbus_set_slave设置一致,libmodbus无公开getter)
// PDU
req[7] = 0x43; // 自定义功能码
req[8] = 0x01; // 子功能码:读序列号
req_len = 9;
// 发送请求
modbus_send(ctx, req, req_len);
// 接收响应
rsp_len = modbus_receive(ctx, rsp);
if (rsp_len == -1) return -1;
// 解析响应(跳过MBAP头7字节)
if (rsp[7] == 0x43 && rsp[8] == 0x01) {
int serial_len = rsp[9];
if (serial_len > max_len) serial_len = max_len;
memcpy(serial, &rsp[10], serial_len);
return serial_len;
}
return -1;
}
// 使用示例
int main() {
modbus_t *ctx = modbus_new_tcp("192.168.1.100", 502);
modbus_connect(ctx);
uint8_t serial[32];
int len = read_device_serial(ctx, serial, sizeof(serial));
if (len > 0) {
printf("设备序列号:");
for (int i = 0; i < len; i++) printf("%02X ", serial[i]);
printf("\n");
}
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
七、RTU模式下的使用
#include <modbus.h>
#include <modbus-rtu.h>
int main() {
modbus_t *ctx;
uint8_t req[MODBUS_RTU_MAX_ADU_LENGTH];
uint8_t rsp[MODBUS_RTU_MAX_ADU_LENGTH];
int req_len, rsp_len;
// 创建RTU上下文
ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
modbus_set_slave(ctx, 1);
modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS485);
modbus_connect(ctx);
// 构建读线圈请求(RTU模式会自动添加CRC)
req_len = modbus_build_request(ctx, 0x01, 0, 8, req);
printf("RTU请求长度:%d\n", req_len);
printf("RTU请求帧:");
for (int i = 0; i < req_len; i++) printf("%02X ", req[i]);
printf("\n");
// 输出(8字节):01 01 00 00 00 08 3D C9
// 最后2字节3D C9是CRC16校验
// 发送并接收
modbus_send(ctx, req, req_len);
rsp_len = modbus_receive(ctx, rsp);
// RTU响应帧:从站地址 + 功能码 + 数据 + CRC
if (rsp[1] == 0x01) {
int byte_count = rsp[2];
printf("线圈状态字节数:%d\n", byte_count);
for (int i = 0; i < byte_count; i++) {
printf("字节%d:0x%02X\n", i, rsp[3 + i]);
}
}
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
八、调试与抓包分析
modbus_build_request在协议调试中非常有用,可以打印出完整的请求帧用于分析:
// 调试函数:打印请求帧详情
void print_request_detail(modbus_t *ctx, int function, int addr, int nb) {
uint8_t req[MODBUS_TCP_MAX_ADU_LENGTH];
int len = modbus_build_request(ctx, function, addr, nb, req);
printf("=== 请求帧详情 ===\n");
printf("功能码:0x%02X\n", function);
printf("地址:%d (0x%04X)\n", addr, addr);
printf("数量/值:%d (0x%04X)\n", nb, nb);
printf("帧长度:%d字节\n", len);
// TCP模式解析MBAP头
if (len >= 7) {
uint16_t trans_id = (req[0] << 8) | req[1];
uint16_t proto_id = (req[2] << 8) | req[3];
uint16_t length = (req[4] << 8) | req[5];
uint8_t slave_id = req[6];
printf("事务ID:%d\n", trans_id);
printf("协议ID:%d\n", proto_id);
printf("长度字段:%d\n", length);
printf("从站地址:%d\n", slave_id);
}
printf("原始数据:");
for (int i = 0; i < len; i++) printf("%02X ", req[i]);
printf("\n\n");
}
// 使用
print_request_detail(ctx, 0x03, 0, 10); // 读保持寄存器
print_request_detail(ctx, 0x05, 0, 0xFF00); // 写线圈
print_request_detail(ctx, 0x06, 5, 100); // 写寄存器
九、相关函数
| 函数 | 功能 |
|---|---|
| modbus_send() | 发送请求帧 |
| modbus_receive() | 接收响应帧 |
| modbus_reply() | 从站模式下回复请求 |
| modbus_read_registers() | 高层函数:读保持寄存器(内部调用build_request) |
| modbus_write_bit() | 高层函数:写单个线圈 |
| modbus_write_register() | 高层函数:写单个寄存器 |
| modbus_set_slave() | 设置从站地址 |
| modbus_strerror() | 获取错误描述 |
十、常见问题
- 返回-1:检查ctx是否有效,功能码是否受支持,缓冲区是否足够大
- 请求帧长度不对:确认协议模式(TCP/RTU),TCP有7字节MBAP头,RTU有2字节CRC
- 设备无响应:检查从站地址是否正确,功能码和地址是否在设备支持范围内
- CRC错误:RTU模式下modbus_build_request会自动计算CRC,不要手动添加
- 事务ID不匹配:TCP模式下每次调用会自动递增事务ID,无需手动管理
- 自定义功能码:modbus_build_request不支持自定义功能码,需手动构建PDU
- 0x0F/0x10功能码:modbus_build_request仅支持01-06功能码,写多个线圈/寄存器需手动构建完整帧(含数据字节)
- modbus_get_slave不存在:libmodbus无公开的从站地址getter函数,需自行记录设置的值
modbus_build_request是libmodbus库中理解Modbus协议帧结构的关键函数。通过它,开发者可以深入了解Modbus请求的构建过程,实现自定义功能码、协议调试和底层优化。在实际开发中,建议优先使用高层函数(如modbus_read_registers),只有在特殊需求下才使用modbus_build_request手动构建请求帧。
发表回复