目录
- 一、前言
- [二、RTU 与 TCP 的帧格式差异](#二、RTU 与 TCP 的帧格式差异)
- [三、Write File Record 请求格式](#三、Write File Record 请求格式)
- [四、modbus_write_file_record 实现](#四、modbus_write_file_record 实现)
- 五、响应长度计算函数解析
- 六、总结
- 七、结尾
一、前言
大家好,这里是 Hello_Embed。上篇完成了 Socket 状态检测与断线重连机制,Modbus TCP 通信已经具备了基本的容错能力。本篇来实现**写文件(Write File Record)**功能。
建议:本篇涉及 Modbus 文件记录的概念,建议先阅读设备互联系列笔记,再阅读本文,以便更好地理解上下文。
在 Modbus RTU 项目中已经实现过写文件功能,但当时只考虑了 RTU 协议的帧格式。要在 TCP 上复用同一套逻辑,需要对帧头部分做适配------这正是本篇改造的核心。
二、RTU 与 TCP 的帧格式差异
Modbus RTU 和 TCP 的帧结构有明显差别,主要体现在头部长度:
| 字段 | RTU | TCP |
|---|---|---|
| 从机地址 / 单元标识符 | 1 字节 | 1 字节(MBAP 末位) |
| 事务处理标识符 | --- | 2 字节 |
| 协议标识符 | --- | 2 字节 |
| 长度字段 | --- | 2 字节 |
| 头部合计 | 1 字节 | 7 字节 |
| 功能码 | 1 字节 | 1 字节 |
| 数据 | N 字节 | N 字节 |
解析 :RTU 帧头只有 1 字节(从机地址),而 TCP 帧头有 7 字节(MBAP Header)。原来的 RTU 写文件代码中,头部偏移量是硬编码的
1;要支持 TCP,必须把它改成动态计算的offset,从ctx->backend->header_length取得。
三、Write File Record 请求格式
Write File Record(功能码 0x15)的完整请求帧结构如下:
| 字段 | 大小 | 说明 |
|---|---|---|
| 从机地址 | 1 字节 | RTU 帧头,TCP 中位于 MBAP 内 |
| Function Code | 1 字节 | 固定为 0x15 |
| Request Data Length | 1 字节 | 后续数据总字节数(= 7 + 数据字节数) |
| Reference Type | 1 字节 | 固定为 0x06 |
| File Number | 2 字节 | 文件编号 |
| Record Number | 2 字节 | 记录编号 |
| Record Length | 2 字节 | 记录数据的字数(注意:单位是字,非字节) |
| Record Data | N×2 字节 | 实际数据,长度必须为偶数 |
其中,Request Data Length = 7 + 数据字节数,即固定的子请求包头(Reference Type + File Number + Record Number + Record Length = 7 字节)加上实际数据。
四、modbus_write_file_record 实现
将 RTU 版本的写文件函数改造为同时支持 RTU 和 TCP,关键改动是用 offset 替代硬编码的头部偏移量:
c
int modbus_write_file_record(modbus_t *ctx,
uint16_t file_no,
uint16_t record_no,
uint8_t *buffer,
uint16_t len)
{
int rc;
int i;
int req_length;
uint8_t req[MAX_MESSAGE_LENGTH];
int offset;
/* 长度必须为偶数(Record Length 单位是字) */
len = (len + 1) & ~0x1;
/* 数据长度合法性检查
* RTU ADU 最大 256 字节,减去 1 字节地址、2 字节 CRC = 253 字节有效载荷
* 其中功能码+包头占 9 字节,剩余最多 244 字节用于传输数据
*/
if (len < 2 || len > 244)
return -1;
/* 动态计算头部偏移量:RTU 为 0(从从机地址开始),TCP 为 6(跳过 MBAP) */
offset = ctx->backend->header_length - 1;
/* 填充请求帧头部 */
req[offset++] = ctx->slave; /* 从机地址 / 单元标识符 */
req[offset++] = MODBUS_FC_WRITE_FILE_RECORD; /* 功能码 0x15 */
req[offset++] = 7 + len; /* Request Data Length */
req[offset++] = 0x06; /* Reference Type */
req[offset++] = file_no >> 8; /* File Number 高字节 */
req[offset++] = file_no & 0x00FF; /* File Number 低字节 */
req[offset++] = record_no >> 8; /* Record Number 高字节 */
req[offset++] = record_no & 0x00FF; /* Record Number 低字节 */
req[offset++] = (len / 2) >> 8; /* Record Length 高字节(字数) */
req[offset++] = (len / 2) & 0x00FF; /* Record Length 低字节(字数) */
req_length = offset;
/* 填充数据 */
for (i = 0; i < len; i++)
req[req_length++] = buffer[i];
/* 发送请求并等待响应 */
rc = send_msg(ctx, req, req_length);
if (rc > 0)
{
uint8_t rsp[MAX_MESSAGE_LENGTH];
rc = _modbus_receive_msg(ctx, rsp, MSG_CONFIRMATION);
if (rc < 0)
return -2;
rc = check_confirmation(ctx, req, rsp, rc);
}
return rc;
}
解析 :
offset = ctx->backend->header_length - 1是整个改造的核心。header_length在 RTU 中为1(含从机地址),在 TCP 中为7(含 MBAP Header)。减去1后,offset指向req数组中从机地址所在的位置,后续字段依次填入。由此,同一套代码在两种协议下均能正确构造帧结构。
五、响应长度计算函数解析
库内部通过 compute_response_length_from_request 函数预先计算期望的响应帧长度,以便校验接收到的数据是否完整。Write File Record 对应的分支如下:
c
static unsigned int compute_response_length_from_request(modbus_t *ctx, uint8_t *req)
{
int length;
const int offset = ctx->backend->header_length;
switch (req[offset]) {
/* ... 其他功能码 ... */
case MODBUS_FC_WRITE_FILE_RECORD:
length = req[offset + 1] + 2;
break;
default:
length = 5;
}
return offset + length + ctx->backend->checksum_length;
}
这里 req[offset + 1] 取到的是 Request Data Length 字段(即 7 + 数据字节数),再加上 2 是因为要把**功能码(1 字节)和 Request Data Length 自身(1 字节)**也计入总长度。
以一次包含 N 字节数据的请求为例推导:
| 字段 | 字节数 |
|---|---|
| 功能码 | 1 |
| Request Data Length(值 = 7 + N) | 1 |
| Sub-Request 包头(Reference Type + File/Record No. + Record Length) | 7 |
| Record Data | N |
| 有效载荷合计 | 9 + N |
所以 length = req[offset + 1] + 2 = (7 + N) + 2 = 9 + N,与上表吻合。
最终函数返回:
总字节数 = offset(协议头) + length(有效载荷) + checksum_length(校验码)
RTU 中 offset = 1,checksum_length = 2(CRC);TCP 中 offset = 7,checksum_length = 0(无校验码)。同一个表达式,自动适配两种协议。
解析 :这个计算方式的巧妙之处在于:Write File Record 的响应帧 与请求帧 有效载荷结构完全一致(Server 原样回显),所以用请求帧的
Request Data Length字段直接推算响应长度,无需额外字段。
六、总结
| 改造点 | 改造前(RTU) | 改造后(RTU + TCP) |
|---|---|---|
| 头部偏移量 | 硬编码为 1 |
动态计算:ctx->backend->header_length - 1 |
| 响应长度计算 | 固定公式 | 通用公式:offset + req[offset+1] + 2 + checksum_length |
| 适用协议 | 仅 RTU | RTU 和 TCP 均支持 |
经过本篇改造,写文件功能已经完全协议无关------无论底层使用 RTU 还是 TCP,上层调用接口保持不变,协议差异由 ctx->backend 封装层透明处理。
七、结尾
本篇完成了写文件功能从 RTU 到 TCP 的适配改造,核心是用动态 offset 替代硬编码头部偏移,实现了协议无关的帧构造。
下一篇将改进 H5 开发板程序,增加更完善的容错机制,敬请期待~
Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~