Modbus Write File Record 功能实现与串口通信测试
目录
- 一、前言
- 二、Write File Record 核心逻辑:大文件分块传输设计
- 三、Modbus 数据包格式:帧结构与长度限制
- 四、通信应答机制:数据校验与异常反馈
- 五、功能扩展:自定义文件传输协议(Record0)
- 六、硬件连接与任务设计:串口通信测试架构
- 七、核心代码实现:分块发送与数据接收
- 八、测试验证:串口通信与调试打印
- 九、总结
- 十、结尾
一、前言
在工业传感器互联系统中,当需要传输多个大体积文件(如固件 bin 文件)时,Modbus 标准的单包传输能力无法满足需求。本次笔记聚焦 Modbus 的Write File Record(0x15 功能码)功能实现,通过分块传输解决大文件发送问题,同时基于 STM32H5 中控设计串口通信任务,完成该功能的硬件实测,验证分块传输的稳定性与可靠性。
二、Write File Record 核心逻辑:大文件分块传输设计
针对多文件、大文件的传输需求,Write File Record采用分块(Record)发送策略,每个数据包需明确 4 个核心信息,确保数据传输的有序性:
- 文件编号(File Number):标识当前发送的是哪个文件;
- 数据包序号(Record Number):标识当前发送的是该文件的第几个数据包;
- 数据包长度(Record length):标识当前数据包的有效数据大小;
- 数据本身:当前分块的实际文件数据。
传输限制:Modbus RTU 单包最大传输 256 字节,扣除协议头、校验等固定字节后,单包可传输的文件数据(Record)存在明确范围,是分块设计的核心依据。
三、Modbus 数据包格式:帧结构与长度限制
1. 完整帧结构(字节级)
Write File Record的单个数据包包含以下固定组成部分(按顺序):
| 字段 | 字节数 | 说明 |
|---|---|---|
| 设备地址 | 1 | 目标设备的 Modbus 地址 |
| 功能码 | 1 | 固定为 0x15(Write File Record) |
| 数据长度 len | 1 | 取值范围 0x09~0xFB,代表后续数据总长度 |
| 固定格式 | 1 | 固定为 0x06(子请求类型) |
| 文件编号 | 2 | 区分不同文件的唯一编号 |
| Record Number | 2 | 数据包序号,可用于指定接收对象(中控 / 传感器) |
| Record length | 2N | 当前数据包的有效数据长度(2 倍 N,N 为字数) |
| CRC 校验 | 2 | 整包数据的校验码 |
2. 长度计算规则
-
数据长度
len计算公式:len = 7 + N * 2(7 为 “固定格式 + 文件编号 + Record Number+Record length” 的总字节数,N*2 为数据本身的字节数); -
最大 / 最小数据范围:
- 单包最大可用字节:256 - 1(设备地址) - 2(CRC) - 1(功能码) - 1(数据长度) = 0xFB(251 字节);
- 最大 Record 数据:251 - 7 = 244 字节;
- 最小 Record 数据:9 - 7 = 2 字节(对应 len 最小值 0x09);
- 单次传输的 Record 数据最少为 2 字节。
四、通信应答机制:数据校验与异常反馈
Modbus 传感器接收Write File Record数据包后,应答逻辑简洁且直接:
- 正常应答:原样返回接收到的数据包,表明数据接收无误;
- 异常应答:将功能码 0x15 与 0x80(用户原文为 “或上 1”,实际为 0x80)按位或,得到 0x95 并返回,直接标识数据接收 / 解析出错,无需额外异常码,反馈方式 “简单粗暴”。
五、功能扩展:自定义文件传输协议(Record0)
Write File Record仅实现文件分块发送,无法传输文件的元信息(文件名、大小、CRC 等),因此需对其进行改造:
- 在正式分块传输前,先发送
Record0(序号为 0 的数据包); - 在
Record0中通过自定义协议定义元信息:前 N 字节为文件名、后续 M 字节为文件大小、最后 K 字节为 CRC 校验值; - 接收端解析
Record0获取文件元信息后,再接收后续分块数据,拼接为完整文件,实现 “文件本身 + 元信息” 的完整传输。
六、硬件连接与任务设计:串口通信测试架构
1. 硬件连接

- 中控间通过 RS485 互连,实现数据交互;
- 调试器与 USB 接口连接 PC,用于程序下载、调试与数据监控;
- 中控板载屏幕用于实时打印通信调试信息。
2. 软件任务设计
创建两个独立的 FreeRTOS 任务,基于板载串口实现Write File Record的客户端 / 服务端通信:
CH1_UART2_ClientTask(客户端):构造Write File Record请求数据包、计算校验码并发送;CH2_UART4_ServerTask(服务端):接收客户端数据包、解析并应答,同时通过屏幕打印通信状态。
七、核心代码实现:分块发送与数据接收
1. Write File Record 发送函数
// Modbus Write File Record发送函数:构造数据包并发送,完成分块数据传输
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 byte_count;
int req_length;
int bit_check = 0;
int pos = 0;
uint8_t req[MAX_MESSAGE_LENGTH];
/* 长度对齐:确保为2字节的整数倍(Record length为2N字节) */
len = (len + 1) & ~0x1;
/* 长度校验:符合2~244字节的传输限制 */
/* ADU最大是256, 256-1字节设备地址-2字节CRC=253
* 功能码等包头是7字节
* 传输的数据最大253-9=244
*/
if (len < 2 || len > 244 )
return -1;
/* 构造数据包头部 */
req[0] = ctx->slave; // 设备地址
req[1] = MODBUS_FC_WRITE_FILE_RECORD; // 功能码0x15
req[2] = 7 + len; // 数据长度len(7+数据字节数)
req[3] = 0x06; // 固定子请求类型
req[4] = file_no >> 8; // 文件编号(高8位)
req[5] = file_no & 0x00ff; // 文件编号(低8位)
req[6] = record_no >> 8; // 数据包序号(高8位)
req[7] = record_no & 0x00ff; // 数据包序号(低8位)
req[8] = (len/2) >> 8; // Record length(高8位,字数)
req[9] = (len/2) & 0x00ff; // Record length(低8位,字数)
req_length = 10; // 头部总长度10字节
/* 填充待发送数据到数据包 */
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;
}
注:CRC 校验在
send_msg函数中自动完成,无需在该函数内额外处理。
2. 数据接收状态机
服务端接收Write File Record数据包时,分为三个核心状态,确保数据解析的有序性:
_STEP_FUNCTION:读取前 2 字节,解析功能码(判断是否为 0x15);_STEP_META:根据功能码计算后续需读取的元数据长度(length_to_read = compute_meta_length_after_function),对于 0x15 功能码需读取 8 字节元数据(文件编号、Record Number、Record length);_STEP_DATA:读取剩余数据(length_to_read = compute_data_length_after_meta,值为reg[2] - 7 + 2,2 为 CRC 字节数),最后调用ctx->backend->check_integritey(ctx, msg, msg_length)完成 CRC 校验。
3. 完整任务函数实现
// CH2_UART4_ServerTask:服务端任务,接收数据包并应答,屏幕打印通信计数
static void CH2_UART4_ServerTask( void *pvParameters )
{
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
char buf[100];
int cnt = 0;
// 初始化Modbus RTU上下文(串口4、115200波特率、无校验、8数据位、1停止位)
ctx = modbus_new_st_rtu("uart4", 115200, 'N', 8, 1);
modbus_set_slave(ctx, 1); // 设置从机地址为1
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH); // 申请接收缓冲区
// 初始化Modbus映射区(各类寄存器数量均为10)
mb_mapping = modbus_mapping_new_start_address(0,
10,
0,
10,
0,
10,
0,
10);
// 初始化映射区数据:位寄存器置0,字寄存器置0x55
memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
memset(mb_mapping->tab_registers, 0x55, mb_mapping->nb_registers*2);
// 建立Modbus连接
rc = modbus_connect(ctx);
if (rc == -1) {
modbus_free(ctx);
vTaskDelete(NULL);;
}
// 循环接收并处理数据包
for (;;) {
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
/* 连接异常时跳过,不退出循环 */
if (rc < 0 ) {
continue;
}
// 应答客户端数据包
rc = modbus_reply(ctx, query, rc, mb_mapping);
if (rc == -1) {
continue;
}
// 通信计数+1,屏幕打印接收次数
cnt++;
sprintf(buf, "server recv file record cnt = %d", cnt);
Draw_String(0, 32, buf, 0xff0000, 0);
// 根据位寄存器0的值控制PC12 LED灯
if (mb_mapping->tab_bits[0])
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_RESET);
else
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_12, GPIO_PIN_SET);
// 字寄存器1自增,用于标识通信次数
mb_mapping->tab_registers[1]++;
}
// 释放资源(实际循环不会退出,仅为代码完整性)
modbus_mapping_free(mb_mapping);
vPortFree(query);
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
// CH1_UART2_ClientTask:客户端任务,循环发送Write File Record数据包,屏幕打印状态
static void CH1_UART2_ClientTask( void *pvParameters )
{
modbus_t *ctx;
int rc;
uint16_t val;
int nb = 1;
int level = 1;
char buf[100];
int cnt = 0;
int err_cnt = 0;
// 初始化Modbus RTU上下文(串口2、115200波特率、无校验、8数据位、1停止位)
ctx = modbus_new_st_rtu("uart2", 115200, 'N', 8, 1);
modbus_set_slave(ctx, 1); // 设置目标从机地址为1
// 建立Modbus连接
rc = modbus_connect(ctx);
if (rc == -1) {
modbus_free(ctx);
vTaskDelete(NULL);;
}
// 循环发送数据包(每2秒一次)
for (;;) {
memset(buf, 0x5A, 100); // 填充测试数据0x5A
// 调用Write File Record函数发送数据(文件编号1、数据包序号1、100字节数据)
rc = modbus_write_file_record(ctx, 1, 1, buf, 100);
cnt++; // 发送计数+1
if (rc < 0)
err_cnt++; // 发送失败计数+1
// 屏幕打印发送次数与失败次数
sprintf(buf, "client send file record cnt = %d, err_cnt = %d", cnt, err_cnt);
Draw_String(0, 0, buf, 0xff0000, 0);
/* 延时2秒 */
vTaskDelay(2000);
}
// 释放资源(实际循环不会退出,仅为代码完整性)
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
注:核心修改点为客户端任务中调用
modbus_write_file_record(ctx, 1, 1, buf, 100),实现向服务端串口发送分块数据。
八、测试验证:串口通信与调试打印
将上述任务部署到 STM32H5 中控后,进行串口通信测试:
- 客户端循环发送 100 字节的
Write File Record数据包,每 2 秒一次; - 服务端接收数据包并应答,同时通过板载屏幕打印接收 / 发送计数;
- 测试结果:调试打印信息正常,发送 / 接收计数同步增长,无异常报错,验证
Write File Record功能可稳定运行。

九、总结
Write File Record通过分块传输解决大文件发送问题,需严格遵循 256 字节单包限制,核心帧结构包含设备地址、功能码、文件编号等关键字段;- 通信应答机制简洁,异常通过功能码 0x95 标识,便于快速定位问题;
- 可通过
Record0扩展自定义协议,补充文件元信息,实现完整文件传输; - 基于 FreeRTOS 的串口任务验证了该功能的硬件可行性,调试打印信息可直观监控通信状态。
十、结尾
本次完成了 Modbus Write File Record功能的核心实现与硬件测试,解决了工业互联系统中大文件分块传输的核心问题,是中控与传感器之间固件升级、数据交互的关键基础。掌握该功能的帧结构设计与扩展技巧,可大幅提升工业设备通信的灵活性与可靠性。感谢各位的阅读,持续关注本系列笔记,一起深化工业互联系统的协议优化与功能落地!
更多推荐




所有评论(0)