目录

一、前言

在工业传感器互联系统中,当需要传输多个大体积文件(如固件 bin 文件)时,Modbus 标准的单包传输能力无法满足需求。本次笔记聚焦 Modbus 的Write File Record(0x15 功能码)功能实现,通过分块传输解决大文件发送问题,同时基于 STM32H5 中控设计串口通信任务,完成该功能的硬件实测,验证分块传输的稳定性与可靠性。

二、Write File Record 核心逻辑:大文件分块传输设计

针对多文件、大文件的传输需求,Write File Record采用分块(Record)发送策略,每个数据包需明确 4 个核心信息,确保数据传输的有序性:

  1. 文件编号(File Number):标识当前发送的是哪个文件;
  2. 数据包序号(Record Number):标识当前发送的是该文件的第几个数据包;
  3. 数据包长度(Record length):标识当前数据包的有效数据大小;
  4. 数据本身:当前分块的实际文件数据。

传输限制: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数据包时,分为三个核心状态,确保数据解析的有序性:

  1. _STEP_FUNCTION:读取前 2 字节,解析功能码(判断是否为 0x15);
  2. _STEP_META:根据功能码计算后续需读取的元数据长度(length_to_read = compute_meta_length_after_function),对于 0x15 功能码需读取 8 字节元数据(文件编号、Record Number、Record length);
  3. _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功能可稳定运行。

请添加图片描述

九、总结

  1. Write File Record通过分块传输解决大文件发送问题,需严格遵循 256 字节单包限制,核心帧结构包含设备地址、功能码、文件编号等关键字段;
  2. 通信应答机制简洁,异常通过功能码 0x95 标识,便于快速定位问题;
  3. 可通过Record0扩展自定义协议,补充文件元信息,实现完整文件传输;
  4. 基于 FreeRTOS 的串口任务验证了该功能的硬件可行性,调试打印信息可直观监控通信状态。

十、结尾

本次完成了 Modbus Write File Record功能的核心实现与硬件测试,解决了工业互联系统中大文件分块传输的核心问题,是中控与传感器之间固件升级、数据交互的关键基础。掌握该功能的帧结构设计与扩展技巧,可大幅提升工业设备通信的灵活性与可靠性。感谢各位的阅读,持续关注本系列笔记,一起深化工业互联系统的协议优化与功能落地!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐