嵌入式上位机开发入门(二十):写文件功能的 RTU/TCP 双协议适配

目录


一、前言

大家好,这里是 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 = 1checksum_length = 2(CRC);TCP 中 offset = 7checksum_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 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
ZC跨境爬虫16 小时前
跟着 MDN 学 HTML day_9:(信件语义标记)
前端·css·笔记·ui·html
LCG元19 小时前
STM32项目实战:基于STM32F103的智能农业监控系统
stm32·单片机·嵌入式硬件
ACP广源盛1392462567319 小时前
IX8024与科学大模型的碰撞@ACP#筑牢科研 AI 算力高速枢纽分享
运维·服务器·网络·数据库·人工智能·嵌入式硬件·电脑
OBiO201319 小时前
Cell | 突破AAV载体容量限制!路中华/姜玉武/刘太安团队开发AAVLINK系统实现大基因递送
笔记
Empty-Filled19 小时前
AI生成测试用例功能怎么测:一个完整实战案例
网络·人工智能·测试用例
智者知已应修善业20 小时前
【51单片机2个按键控制流水灯运行与暂停】2023-9-6
c++·经验分享·笔记·算法·51单片机
码云数智-大飞20 小时前
本地部署大模型:隐私安全与多元优势一站式解读
运维·网络·人工智能
sakiko_20 小时前
UIKit学习笔记5-使用UITableView制作聊天页面
笔记·学习·swift·uikit
jinanwuhuaguo20 小时前
(第二十九篇)OpenClaw 实时与具身的跃迁——从异步孤岛到数字世界的“原住民”
前端·网络·人工智能·重构·openclaw
汤愈韬21 小时前
三种常用 NAT 的经典案例
网络协议·网络安全·security