9.2.2 实现 Write File Record(保姆级教学)

你好!今天我们继续学习"综合实现"课程中的 9.2.2 实现 Write File Record。上一节我们分析了 Write File Record 的功能和报文格式,这一节我们要动手在 libmodbus 库中增加这个功能,并实际测试它。我会像教小学生一样,一步一步带你理解代码和操作。


一、准备工作:硬件连接

这张图很简单,但非常重要,它告诉我们在测试时如何连接设备。我们来解读一下:

  • 两个485互连:我们需要两个串口(比如 UART2 和 UART4)通过 RS485 总线连接起来。RS485 是一种差分信号传输,通常用两根线(A、B)连接。这里"两个485互连"就是把两个设备的 RS485 接口的 A 线接一起,B 线接一起。

  • 接到PC,供电:开发板需要供电(比如通过 USB 线连接电脑供电),同时可能还需要调试串口连接到电脑,以便我们在电脑上观察打印信息。

  • 连接到PC,调试:调试串口(通常是 UART1 或专门的调试口)接到电脑,用串口工具(如 SecureCRT、Putty)查看输出。

在我们的测试中,我们将用 H5 开发板,它有多个 UART。我们计划:

  • 任务1 通过 channel1(即 UART2) 发出 Write File Record 数据包。

  • 任务2 通过 channel2(即 UART4) 接收这个数据包,验证无误后在 LCD 上显示结果。

所以硬件上,我们需要将 UART2 和 UART4 的 RS485 接口连接起来(注意共地),然后两个任务分别运行在同一个开发板的不同核心或不同进程?实际上可能是在同一个开发板上跑两个程序,或者用两块开发板。但这里我们可以简单理解:我们编写两个程序,一个发送,一个接收,运行在同一块开发板上,通过串口互联。


二、libmodbus 简介

libmodbus 是一个开源的 Modbus 协议库,支持 RTU 和 TCP。它提供了很多现成的函数,比如:

  • modbus_read_registers

  • modbus_write_register

  • modbus_write_bits

  • 等等

但是,它默认没有提供 modbus_write_file_record 函数。所以我们需要自己动手在 libmodbus 中添加这个功能。这正是本节的目标。


三、分析 modbus_write_file_record 函数

这张图片里展示了一个函数的实现,函数名是 modbus_write_file_record。我们把它抄写下来,逐行分析。

cs 复制代码
int modbus_write_file_record(modbus_t *ctx,
    uint16_t file_ne,
    uint16_t record_ne,
    uint8_t *buffer,
    uint16_t len);

这是函数的原型:

  • ctx:modbus 上下文指针,包含了串口配置、从机地址等信息。

  • file_ne:文件编号(File Number),我们要写入哪个文件。

  • record_ne:记录编号(Record Number),当前是文件的第几个数据块。

  • buffer:指向要发送的数据的指针。

  • len:要发送的数据字节数。

函数返回值:成功返回0,失败返回负数。

函数体内部
cs 复制代码
int rc;
int byte_count;
int req_length;
int bit_check = 0;
int pos = 0;
uint8_t req[MAX_MESSAGE_LENGTH];
  • rc:用来接收各种函数的返回值。

  • req_length:请求报文的总长度(字节数)。

  • req:一个数组,用来存放要发送的 Modbus 请求报文。MAX_MESSAGE_LENGTH 是 libmodbus 定义的,通常为 256。

cs 复制代码
/* 长度是2N */
len = (len + 1) & ~0x1;

这一行很关键!它把 len 调整成偶数。为什么?因为 Modbus 协议中,Write File Record 的数据部分长度必须是 2字节的倍数 (即16位字的数量)。这里 (len + 1) & ~0x1 的作用是:如果 len 是奇数,就加1变成偶数;如果已经是偶数,则保持不变。例如:

  • len=3 → (3+1)=4,4 & ~1 = 4(二进制 0100 & 1110 = 0100),变成4。

  • len=4 → (4+1)=5,5 & ~1 = 4(0101 & 1110 = 0100),还是4。

    所以这个操作确保 len 是偶数。但这样可能会丢失最后一个字节(如果原数据是奇数),所以调用者必须确保数据长度本来就是偶数?实际上协议规定数据必须是16位字的整数倍,所以我们发送的数据长度必须是偶数。如果你的数据是奇数,需要自己填充一个字节(比如0x00)。

cs 复制代码
/* ADU最大是256,256-1字节设备地址-2字节CRC=253
   功能码等包头是7字节
   传输的数据最大253-9=244
*/
if (len < 2 || len > 244)
    return -1;

这段注释解释了为什么最大数据长度是244字节。我们来计算一下:

  • Modbus RTU 最大报文长度是256字节。

  • 减去1字节的从机地址,再减去2字节的 CRC,剩下253字节可用于功能码+数据。

  • Write File Record 的请求中,除了功能码(1字节),还有"请求数据长度"字段(1字节),以及每个子请求的固定开销:Reference Type(2字节)、File Number(2字节)、Record Number(2字节)、Record Length(2字节)。这些固定部分总共 1(功能码)+1(数据长度)+2+2+2+2 = 10字节?实际上注释里说"功能码等包头是7字节",可能是指子请求内的固定部分(不含功能码和请求数据长度)是7字节?我们后面再算。但最终结果是数据最大244。所以我们限制 len 必须 ≤244。

cs 复制代码
/* 包头 */
req[0] = ctx->slave;
req[1] = MODBUS_FC_WRITE_FILE_RECORD;
req[2] = 7 + len; /* Request data length */

这里开始构造报文:

  • req[0]:从机地址(即我们要发给哪个设备)。ctx->slave 保存了目标从机地址。

  • req[1]:功能码,MODBUS_FC_WRITE_FILE_RECORD 应该是预定义的宏,值为0x15

  • req[2]:请求数据长度。这个字段是后面所有子请求的总字节数。(len=2n)

cs 复制代码
req[3] = 0x06; /* Sub-Req. x, Reference Type */

req[4] = file_ne >> 8;
req[5] = file_ne & 0x00ff; /* Sub-Req. x, File Number */

req[6] = record_ne >> 8;
req[7] = record_ne & 0x00ff; /* Sub-Req. x, Record Number */

req[8] = (len/2) >> 8;
req[9] = ((len/2) & 0x00ff; /* Sub-Req. x, Record Length */

注意这里赋值:

  • req[3] = 0x06;

再看 File Number 和 Record Number 的赋值:

  • file_ne >> 8 取高8位,放到 req[4]

  • file_ne & 0x00ff 取低8位,放到 req[5]

同样,Record Length 应该是记录数据的 字数 (即16位字的个数),因为 len 是字节数,且已经是偶数,所以字数 = len/2。然后这个字数用两个字节表示。代码中 (len/2) >> 8 取高8位,(len/2) & 0x00ff 取低8位.

思路是对的:我们要构造一个子请求,包含 Reference Type(2字节,值为06),File Number(2字节),Record Number(2字节),Record Length(2字节,表示后面数据的字数)。

cs 复制代码
req_length = 10;

当前已经填充了 req[0]~req[9] 共10个字节。包括:从机地址(1)、功能码(1)、请求数据长度(1)、Reference Type(1)、File Number高8位(1)、File Number低8位? (这里可能占用了1字节,实际应该是2字节)、Record Number高8位(1)、Record Number低8位? (1)、Record Length高8位(1)、Record Length低8位? (1)。如果 Reference Type 只占1字节,那么总固定部分就是 1+1+1+1+1+1+1+1+1+1=10字节。但按照标准,Reference Type 占2字节,File Number 占2,Record Number 占2,Record Length 占2,一共8字节固定,加上前面的从机地址、功能码、请求数据长度,总共 1+1+1+8=11字节。所以这里可能少了一字节,导致后续数据错位。

cs 复制代码
/* 数据 */
for (i = 0; i < len; i++)
    req[req_length++] = buffer[i];

将数据拷贝到报文中,从 req_length 开始(当前是10),每拷贝一个字节,req_length 增加。

cs 复制代码
rc = send_msg(ctx, req, req_length); // Send request

调用底层发送函数,将报文发送出去。

cs 复制代码
if (rc == 0)
    /* Used by write_bit and write_register */
    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;

如果发送成功(rc == 0),则等待接收响应(modbus_receive_msg),然后检查响应是否正确(check_confirmation)。最后返回结果。

小结

这个函数试图构造一个 Write File Record 请求并发送,然后等待确认。清晰地展示了整个流程。


四、为什么 len 是 2N?

从代码中我们看到,len 被强制转成偶数,并且后面计算 Record Length 时用了 len/2。这是因为 Modbus 协议规定:Write File Record 的记录数据 是以 16位寄存器 为单位组织的,所以数据长度必须是2字节的倍数,且记录长度字段表示的是 16位字的个数 (即 N = len/2)。因此,我们传入的 len 必须是偶数,且最大不能超过244(因为受报文长度限制)。

如果我们的原始数据是奇数个字节(比如一个文本文件,每个字符1字节),那么我们在发送前需要补一个0字节,使其变成偶数。接收方需要知道这个填充,并在重组文件时去掉它(可以通过文件大小信息来知道实际长度)。


五、如何测试?

测试分两步:

  1. 任务1(发送方) :运行一个程序,它使用我们新加的 modbus_write_file_record 函数,通过 UART2 发送一个文件(比如一个小的文本文件,内容为"Hello")。注意"Hello"是5个字节,需要补成6字节(比如补0)。文件编号可以设为1,记录编号从0开始。

  2. 任务2(接收方):运行另一个程序,它通过 UART4 接收 Modbus 请求,并解析出 Write File Record 的内容。它需要能够识别功能码0x15,然后提取出 File Number、Record Number、Record Length 和 Record Data,并将数据保存到本地文件或缓冲区。当所有记录接收完毕后,验证数据是否正确,并在 LCD 上显示结果(比如"OK"或错误信息)。

由于我们还没有实现接收方的处理,我们需要在 libmodbus 的服务器端(从机)也增加对功能码0x15的支持。通常,libmodbus 提供了一个 modbus_reply 函数,它会根据请求的功能码自动回复。但默认它不支持 0x15,所以我们也需要修改从机代码,添加对 0x15 的处理。

接收方处理思路

在从机程序中,通常有一个循环:

cs 复制代码
for (;;) {
    rc = modbus_receive(ctx, query);
    if (rc > 0) {
        // 根据 query 中的功能码处理
        modbus_reply(ctx, query, rc, mb_mapping);
    }
}

我们需要修改 modbus_reply 的内部,或者在调用 modbus_reply 之前自己解析。最简单的方法是:在从机中,收到请求后,如果功能码是 0x15,则手动解析并回复一个确认响应。

Write File Record 的成功响应很简单:它只是原样返回请求报文 (除了从机地址?实际上 Modbus 规定,对于写文件记录,成功响应就是回显请求报文(从功能码开始,包括所有数据)。所以我们可以直接发送相同的报文作为响应。

因此,接收方的伪代码:

cs 复制代码
if (query[1] == 0x15) {
    // 解析数据
    uint8_t *data_ptr = query + 2; // 跳过地址和功能码? 实际地址在query[0]
    // 注意:query[0]是地址,query[1]是功能码
    uint8_t req_data_len = query[2]; // 请求数据长度
    // 然后解析子请求...
    // 假设只有一个子请求
    uint8_t ref_type = query[3]; // 如果只有1字节,实际应该是2字节,需要调整
    // ...
    // 保存数据

    // 构造响应:直接复制请求(但地址可能需要改为自己的地址?一般不变)
    // 发送响应
    send_msg(ctx, query, rc); // rc是接收到的长度
}

注意:实际中,我们需要正确解析多字节字段。我们可以参考 libmodbus 的 modbus_reply 实现方式。


六、连线与运行

按照第一张图连接:

  • 将 H5 开发板的 UART2(作为 channel1)和 UART4(作为 channel2)通过 RS485 转接板连接起来,A-A,B-B。

  • 为开发板供电(USB 或电源适配器)。

  • 将调试串口连接到电脑,打开串口终端,以便查看两个任务的打印信息。

然后在开发板上运行两个程序(可以用两个终端,或者先后运行)。发送方先运行,发送文件;接收方后运行,等待接收。接收方接收到数据后,在 LCD 上显示结果。


七、总结

今天我们实现了 modbus_write_file_record 函数,虽然代码有些小错误,但整体流程是正确的。我们理解了:

  • 为什么要将 len 调整为偶数。

  • 如何构造请求报文。

  • 如何发送并等待响应。

  • 测试环境的搭建。

相关推荐
嵌入式×边缘AI:打怪升级日志2 小时前
综合实现:产品框架(保姆级讲解)
硬件·软件
阿成学长_Cain5 天前
R-Studio v9.5.191686 数据恢复软件中文绿色便携特别版
windows·电脑·软件
GR23423412 天前
2025年影视仓TV+手机官方版 内置地址源支持高清直播
java·智能手机·软件
芯巧电子14 天前
09. ABM器件(一)---基本知识点(01) I PSpice高级应用
cadence·pcb·软件·pspice
myloveasuka22 天前
分离指令缓存(I-Cache)和数据缓存(D-Cache)的原因
笔记·缓存·计算机组成原理·硬件
ddsoft12322 天前
在装配拆卸指导动画中如何制作螺栓批量旋出的逼真视频
composer·软件·solidworks
AUTOSAR组织25 天前
深入解析AUTOSAR框架下的TCP/IP协议栈
网络协议·tcp/ip·汽车·autosar·软件架构·软件·培训
myloveasuka25 天前
3-8 译码器(正式型号74LS138、 74HC138、74HCT138 等))
笔记·算法·计算机组成原理·硬件
私人珍藏库1 个月前
[Windows] 桌面整理 Desk Tidy v1.2.3
windows·工具·软件·win·多功能