嵌入式上位机开发入门(二十):写文件功能的 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 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
LCG元2 小时前
STM32实战:基于STM32CubeMX的HAL库LED流水灯与按键中断
stm32·单片机·嵌入式硬件
AI周红伟2 小时前
Hermes Agent 工具-周红伟
linux·网络·人工智能·腾讯云·openclaw
无敌昊哥战神2 小时前
【算法与数据结构】深入浅出回溯算法:理论基础与核心模板(C/C++与Python三语解析)
c语言·数据结构·c++·笔记·python·算法
zore_c2 小时前
【C++】基础语法(命名空间、引用、缺省以及输入输出)
c语言·开发语言·数据结构·c++·经验分享·笔记
进击的小头2 小时前
第8篇:嵌入式芯片内存架构详解:SRAM_Flash_Cache与外部存储的层级设计
单片机·嵌入式硬件·架构
久违 °2 小时前
【经营管理】企业经营管理沙盘笔记(一)
笔记
平安的平安2 小时前
MCP 协议实战:用 Python 开发你的第一个 AI 工具服务
网络·人工智能·python
_李小白2 小时前
【OSG学习笔记】Day 46: CameraManipulator(相机操控器)
笔记·数码相机·学习
我登哥MVP2 小时前
【Spring6笔记】 - 13 - 面向切面编程(AOP)
java·开发语言·spring boot·笔记·spring·aop