你好!今天我们继续学习"综合实现"课程中的 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(发送方) :运行一个程序,它使用我们新加的
modbus_write_file_record函数,通过 UART2 发送一个文件(比如一个小的文本文件,内容为"Hello")。注意"Hello"是5个字节,需要补成6字节(比如补0)。文件编号可以设为1,记录编号从0开始。 -
任务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 调整为偶数。
-
如何构造请求报文。
-
如何发送并等待响应。
-
测试环境的搭建。