> 技术文档 > STM32+W5500实现Modbus RTU与Modbus TCP相互转换_modbus tcp rtu stm32

STM32+W5500实现Modbus RTU与Modbus TCP相互转换_modbus tcp rtu stm32

目录

1 前言

2 项目环境

2.1 硬件准备

2.2 软件准备

2.3 方案图示

2.4 硬件连接

3 Modbus Slave

3.1 软件介绍

3.2 功能介绍

3.2.1 建立连接

3.2.2 串口配置

3.2.3 配置窗口信息

3.2.4 窗口操作

4 Modbus Poll

4.1 软件介绍

4.2 功能介绍

4.2.1 建立连接

4.2.2 服务器参数配置

4.2.3 配置窗口信息

5 Modbus RTU 转 Modbus TCP 数据转换方式

5.1 Modbus RTU 帧结构

5.2 Modbus TCP 帧结构

5.3 Modbus RTU 转 Modbus TCP 的方法

5.3.1 去掉 CRC 校验

5.3.2 添加 MBAP 头

5.3.3 组合成 TCP 帧

5.4 Modbus TCP 转 Modbus RTU 的方法

5.4.1 去除 MBAP 头

5.4.2 计算 CRC 校验并附加到末尾

6 例程修改

6.1 修改wiz_platform.c

6.2 修改wiz_platform.h

6.3 修改loopback.c

7 功能验证

8 总结


1 前言

Modbus RTU

基于串行通信(如RS-485),采用主从问答模式。数据以二进制格式传输,每字节包含1位起始位、8位数据、1位奇偶校验(可选)和1位停止位,无固定帧头/尾,依赖时间间隔(通常3.5字符时间)区分帧边界。其优势是硬件成本低、抗干扰强,适合短距离、低速率场景(如传感器网络),但多从机时需严格时序控制。

Modbus TCP

基于以太网(TCP/IP协议),将Modbus报文封装在TCP帧中,通过IP地址+端口号标识设备。数据以明文二进制传输,包含MBAP报文头(事务标识、协议标识等),无需校验位(依赖TCP可靠性)。其优势是传输速率高、支持远程访问,适合分布式系统(如工厂自动化),但需网络基础设施支持。

W5500io-M 是炜世推出的高性能SPI转以太网模块,具有以下特点:

  • 极简设计:集成MAC、PHY、32KB缓存及RJ45网口,通过4线SPI接口直连主控,3.3V供电,紧凑尺寸适配嵌入式场景 。
  • 简单易用:用户无需再移植复杂的TCP/IP协议栈到MCU中,可直接基于应用层数据做开发。
  • 资料丰富:提供丰富的MCU应用例程和硬件参考设计,可直接参考使用,大大缩短研发时间,硬件兼容W5100Sio-M模组,方便方案开发与迭代。
  • 应用广泛:在工业控制、智能电网、充电桩、安防消防、新能源、储能等地方都有广泛应用。

产品链接:商品详情

2 项目环境

2.1 硬件准备

  1. W5500io-M模块
  2. STM32F103VCT6开发板
  3. 一根网线
  4. 杜邦线若干

2.2 软件准备

  1. 例程链接:w5500.com/w5500.html
  2. 开发环境:keil uvision 5
  3. 飞思创串口助手
  4. 网络调试助手
  5. Modbus Slave
  6. Modbus Poll

2.3 方案图示

2.4 硬件连接

1. //W5500_SCS--->STM32_GPIOD7/*W5500的片选引脚*/2. //W5500_SCLK--->STM32_GPIOB13/*W5500的时钟引脚*/3. //W5500_MISO--->STM32_GPIOB14/*W5500的MISO引脚*/ 4. //W5500_MOSI--->STM32_GPIOB15/*W5500的MOSI引脚*/ 5. //W5500_RESET--->STM32_GPIOD8/*W5500的RESET引脚*/ 6. //W5500_INT--->STM32_GPIOD9/*W5500的INT引脚*/

3 Modbus Slave

3.1 软件介绍

Modbus Slave 是一款模拟 Modbus 从站设备的软件工具。它支持 Modbus RTU/ASCII/TCP 等多种协议,可创建多个虚拟从站,方便与主站设备进行通信测试与调试,能帮助开发人员快速验证 Modbus 主站程序功能,提升开发效率。

3.2 功能介绍

3.2.1 建立连接

        点击菜单栏\"Connection\"->\"Connect...\"(或者按快捷键F3)弹出连接配置窗口。

        在连接选项那里选择\"Serial Port\",表示当前是用串口通信,如果使用的是Modbus/TCP,则选择“TCP/IP

3.2.2 串口配置

        在配置窗口中配置好端口号、波特率、数据位、校验位、停止位,在这里使用115200波特率(115200 Baud),8个数据位(8 Data bits),无校验位(None Parity),1个停止位(1 Stop Bit)。实际使用时,需依据所通信的从机设备来匹配设置。

3.2.3 配置窗口信息

点击\"Setup\"->\"Slave Definition...\",或者按快捷键F8,或者在要设置的窗口单击右键,选择\"Slave Definition...\",可以打开窗口信息配置界面。

Slave ID:可以配置从机地址

Function:可以配置寄存器/线圈类型

Address:可以配置读/写的寄存器/线圈起始地址

Quantity:可以配置读/写的寄存器/线圈个数

Rows:可以选择该窗口一列可以显示多少行,数字是对应的行数,最后一个选项\"Fit to Quantity\"是可以根据前面设置的\"Quantity\"数量自动匹配行数。

Hide Alias Columns:可以选择是否隐藏\"Alias\"列。

PLC Addresses(Base 1):可以选择通信的基地址是从0开始还是从1开始

3.2.4 窗口操作

        双击数据的位置,可修改当前地址的寄存器/线圈数值。

4 Modbus Poll

4.1 软件介绍

        Modbus Poll是一款基于 Windows 的 Modbus 主站(Master)测试工具,用于模拟和监控 Modbus RTU/TCP 通信。它支持读写从站(Slave)设备的寄存器(如线圈、输入寄存器、保持寄存器等),实时显示数据变化,并提供日志记录、数据图表和错误检测功能,广泛应用于工业自动化、PLC 调试和设备测试。

4.2 功能介绍

4.2.1 建立连接

        点击菜单栏\"Connection\"->\"Connect...\"(或者按快捷键F3)弹出连接配置窗口,在连接选项那里选择\"Modbus TCP/IP\",表示使用Modbus TCP通信

4.2.2 服务参数配置

        设置好IP及端口号,Modbus/TCP的默认端口号为502。实际根据从机设备的IP和端口号来设置。设置连接超时时间,按一般默认3000ms即可。

4.2.3 配置窗口信息

        点击\"Setup\"->\"Read/Write Definition...\",或者按快捷键F8,或者在要设置的窗口单击右键,选择\"Read/Write Definition...\",可以打开窗口信息配置界面。

Slave ID:可以配置从机地址

Function:可以配置功能码

Address:可以配置读/写的寄存器/线圈起始地址

Quantity:可以配置读/写的寄存器/线圈个数

Scan Rate:可以配置帧的扫描周期

5 Modbus RTU 转 Modbus TCP 数据转换方式

5.1 Modbus RTU 帧结构

数据帧结构:[从站地址][功能码][数据][CRC校验]

示例:01 03 00 00 00 02 C4 0B

01:从站地址(1号设备)

03:功能码(读保持寄存器)

00 01:起始寄存器地址(0001)

00 02:读取寄存器数量(2个)

C4 0B:CRC16 校验值

5.2 Modbus TCP 帧结构

数据结构[事务标识符][协议标识符][长度][单元标识符][功能码][数据]

示例00 01 00 00 00 06 01 03 00 00 00 02

00 01:事务标识符(可自定义)

00 00:协议标识符(固定)

00 06:后续字节数(6字节

01:单元标识符(1号设备相当于从站地址

03:功能码(读保持寄存器)

00 01 00 02:数据(同 RTU)

5.3 Modbus RTU 转 Modbus TCP 的方法

5.3.1 去掉 CRC 校验

原始数据帧:01 03 00 00 00 02 C4 0B

去掉CRC后:01 03 00 00 00 02

5.3.2 添加 MBAP 头

事务标识符(可递增):00 01

协议标识符(固定):00 00

长度(后续字节数):00 06(1+1+4)

单元标识符(同 RTU 地址):01

5.3.3 组合成 TCP 帧

最终Modbus TCP 帧: 00 01 00 00 00 06 01 03 00 00 00 02

5.4 Modbus TCP 转 Modbus RTU 的方法

5.4.1 去除 MBAP 头

原始Mosbus TCP数据帧: 00 01 00 00 00 06 01 03 00 00 00 02

去掉 MBAP 后:01 03 00 00 00 02

5.4.2 计算 CRC 校验并附加到末尾

计算 01 03 00 01 00 02 的 CRC16(结果为 C4 0B)

最终Mosbus RTU 帧:[01][03][00][01][00][02][C4][0B]

6 例程修改

本次以TCP 服务端为例:

6.1 修改wiz_platform.c

1.添加串口缓存变量

uint8_t Serial_RxPacket[MAX_VALUE];//串口的接收缓存uint8_t Index=0;//接收索引值uint8_t Serial_flag=0; //接收完成标志位

2.添加串口2初始化函数用于Modbus Slave通信

void Serial2_Init(void){/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);//开启USART1的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//将PA9引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//将PA10引脚初始化为上拉输入/*USART初始化*/USART_InitTypeDef USART_InitStructure;//定义结构体变量USART_InitStructure.USART_BaudRate = 115200;//波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//模式,发送模式和接收模式均选择USART_InitStructure.USART_Parity = USART_Parity_No;//奇偶校验,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位,选择1位USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长,选择8位USART_Init(USART2, &USART_InitStructure);//将结构体变量交给USART_Init,配置USART1//使能串口接收中断/空闲中断USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);USART_ITConfig(USART2,USART_IT_IDLE,ENABLE);//中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART2_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_Init(&NVIC_InitStructure);/*USART使能*/USART_Cmd(USART2, ENABLE);//使能USART1,串口开始运行}

3.添加串口2中断接收数据函数

void USART2_IRQHandler(void){uint8_t Clear=0;if (USART_GetITStatus(USART2, USART_IT_RXNE) == SET)//判断是否是USART2的接收事件触发的中断{uint8_t data=USART_ReceiveData(USART2);Serial_RxPacket[Index++]=data;if(Index>=MAX_VALUE)Index=0;USART_ClearITPendingBit(USART2, USART_IT_RXNE);//清除标志位}else if(USART_GetITStatus(USART2,USART_IT_IDLE)==SET)//如果产生空闲中断{Serial_flag=1;Clear=USART2->SR;Clear=USART2->DR;//清除空闲中断}}

4.添加串口2发送hex数据函数用于发送发送Modbus RTU数据帧

void USART2_SendByte(uint8_t data){ // 等待发送缓冲区空 while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); // 发送数据 USART_SendData(USART2, data);}void USART2_SendHexArray(uint8_t *data, uint8_t length){ for (uint8_t i = 0; i < length; i++) { USART2_SendByte(data[i]); }}

5.添加获取串口接收完成标志位函数,串口接收缓存清空,获取串口接收数据长度函数

//获取串口接收完成标志位函数uint8_t Serial_Receive_Flag_Complete(void){uint8_t flag=keep_live_trigger_flag; Serial_flag=0;return flag;}//串口接收缓存清空函数void Serial_Receive_Clear(void){memset(Serial_RxPacket,0,sizeof(Serial_RxPacket));Index=0;}//获取串口数据接收长度函数uint16_t Serial_Receive_len(void){return Index;}

6.2 修改wiz_platform.h

  1. 添加接收缓存声明和函数声明
#define MAX_VALUE 2048 // 串口接收缓冲区最大长度extern uint8_t Serial_RxPacket[];// 串口接收数据缓冲区void Serial2_Init(void);uint16_t Serial_Receive_len(void);uint8_t Serial_Receive_Flag_Complete(void);void Serial_Receive_Clear(void);void USART2_SendHexArray(uint8_t *data, uint8_t length);

6.3 修改loopback.c

1.添加主机序转网络序宏定义Modbus RTU 缓冲定义Modbus TCP 数据帧结构体

#define htons(x) ((uint16_t)((((x) <> 8) & 0xFF)))//主机序转网络序宏定义(16位)uint8_t rtu_buf[256]; // Modbus RTU帧缓存区typedef struct __attribute__((packed)) { uint16_t transaction_id; // 事务标识(用于请求/响应匹配) uint16_t protocol_id; // 协议标识(固定0x0000) uint16_t length; // 后续数据长度(含单元ID) uint8_t unit_id; // 设备地址(等同RTU从站地址) uint8_t data[256]; // 功能码+数据(不含CRC)} ModbusTCP_Frame;

2.添加 Modbus CRC16校验计算函数

uint16_t modbus_crc16(const uint8_t *data, uint16_t length) { uint16_t crc = 0xFFFF; for(uint16_t i = 0; i < length; i++) { crc ^= data[i]; for(uint8_t j = 0; j < 8; j++) { if(crc & 0x0001) { crc = (crc >> 1) ^ 0xA001;  } else { crc >>= 1; } } } return crc;}

3.添加打印Modbus TCP帧函数

void print_modbus_tcp_frame(const ModbusTCP_Frame *frame, uint16_t tcp_len) { printf(\"rtu_to_tcp Frame: \"); const uint8_t *p = (const uint8_t *)frame; for (uint16_t i = 0; i < tcp_len; i++) { printf(\"%02X \", p[i]); } printf(\"\\r\\n\");}

4.添加Modbus RTU转TCP函数

uint16_t rtu_to_tcp(const uint8_t *rtu_data, uint16_t rtu_len, ModbusTCP_Frame *tcp_frame, uint16_t received_tid) { if (rtu_len < 4) return 0; // 1. 计算有效数据长度(排除地址1+功能码1+CRC2) uint16_t data_len = rtu_len - 4;   // 2. 设置MBAP头(复用接收到的Transaction ID) tcp_frame->transaction_id = htons(received_tid);; // 直接使用传入的TID(需确保是网络字节序) tcp_frame->protocol_id = htons(0x0000); // 协议ID固定0 tcp_frame->length = htons(1 + 1 + data_len); // 单元ID1 + 功能码1 + 数据data_len  // 3. 复制单元ID和功能码+数据 tcp_frame->unit_id = rtu_data[0]; // 单元ID tcp_frame->data[0] = rtu_data[1]; // 功能码 memcpy(&tcp_frame->data[1], &rtu_data[2], data_len); // 数据部分  // 4. 返回总长度 = MBAP6 + 单元ID1 + 功能码1 + 数据data_len return 6 + 1 + 1 + data_len;}

5.添加Modbus TCPRTU函数

uint16_t tcp_to_rtu(const uint8_t *tcp_data, uint16_t tcp_len, uint8_t *rtu_buf) { if (tcp_len < 8) return 0; // 最小长度检查(MBAP7 + 功能码1) // 提取单元ID(RTU设备地址) rtu_buf[0] = tcp_data[6]; // MBAP第7字节是单元ID  // 复制功能码+数据(跳过MBAP头) uint16_t data_len = tcp_len - 7; memcpy(&rtu_buf[1], &tcp_data[7], data_len);  // 计算并附加CRC16校验 uint16_t crc = modbus_crc16(rtu_buf, data_len + 1); rtu_buf[data_len + 1] = crc & 0xFF; // CRC低字节 rtu_buf[data_len + 2] = crc >> 8; // CRC高字节  return data_len + 3; // 地址1 + 数据(N) + CRC2}

6.替换loopback_tcpc函数

int32_t loopback_tcps(uint8_t sn, uint8_t *buf, uint16_t port){ int32_t ret; // 函数返回值,用于错误处理 uint16_t size = 0; // 接收数据长度static uint16_t received_tid=0; // 静态变量保存事务ID(跨函数调用保持值)#ifdef _LOOPBACK_DEBUG_ uint8_t destip[4]; uint16_t destport;#endif  switch (getSn_SR(sn)) { case SOCK_ESTABLISHED: if (getSn_IR(sn) & Sn_IR_CON) // TCP连接中断表示与对端连接成功 { setSn_IR(sn, Sn_IR_CON); // 需要将中断位写\'1\'来清除#ifdef _LOOPBACK_DEBUG_ getSn_DIPR(sn, destip); destport = getSn_DPORT(sn); printf(\"%d:Connected - %d.%d.%d.%d : %d\\r\\n\", sn, destip[0], destip[1], destip[2], destip[3], destport);#endif } if ((size = getSn_RX_RSR(sn)) > 0) // 表示待接收数据长度  { if (size > DATA_BUF_SIZE) size = DATA_BUF_SIZE;  ret = recv(sn, buf, size); // 数据接收过程(从硬件接收缓冲区到用户缓冲区) buf[size] = 0x00; // 添加结束标志位  if (ret <= 0) return ret; received_tid = (buf[0] << 8) | buf[1];//接收到的TCP帧的事务标识符 uint16_t rtu_len = tcp_to_rtu(buf, size, rtu_buf); // 将TCP帧转换为RTU帧printf(\"tcp_to_rtu Frame: \");for (uint16_t i = 0; i < rtu_len; i++){printf(\"%02X \", rtu_buf[i]);} USART2_SendHexArray(rtu_buf,rtu_len); }  if(Serial_Receive_Flag_Complete()==1) {uint16_t len=Serial_Receive_len();// 获取串口数据长度ModbusTCP_Frame tcp_frame;printf(\"Modbus RTU Frame: \");for(uint16_t i=0;i<len;i++)//打印发送Modbus RTU 数据{printf(\"%02X \",Serial_RxPacket[i]);}printf(\"\\r\\n\");//提取modbus RTU中数据中的CRCuint16_t received_crc = (Serial_RxPacket[len - 1] << 8) | Serial_RxPacket[len - 2];uint16_t calculated_crc = modbus_crc16(Serial_RxPacket, len-2);//计算CRC if(received_crc==calculated_crc)//校验CRC{  // 将RTU帧转换为TCP帧(复用之前收到的事务ID)uint16_t tcp_len = rtu_to_tcp(Serial_RxPacket, len, &tcp_frame, received_tid);print_modbus_tcp_frame(&tcp_frame, tcp_len);// 打印发送的Modbus TCP数据send(sn,(uint8_t *)&tcp_frame,tcp_len);// 发送Modbus TCP数据帧} else {  printf(\"CRC validation failed \") }Serial_Receive_Clear();//清空串口缓存} break;  case SOCK_CLOSE_WAIT: #ifdef _LOOPBACK_DEBUG_ printf(\"%d:CloseWait\\r\\n\", sn);#endif if ((ret = disconnect(sn)) != SOCK_OK) return ret;#ifdef _LOOPBACK_DEBUG_ printf(\"%d:Socket Closed\\r\\n\", sn);#endif break; case SOCK_INIT:#ifdef _LOOPBACK_DEBUG_ printf(\"%d:Listen, TCP server loopback, port [%d]\\r\\n\", sn, port);#endif if ((ret = listen(sn)) != SOCK_OK) return ret; break; case SOCK_CLOSED:#ifdef _LOOPBACK_DEBUG_ printf(\"%d:TCP server loopback start\\r\\n\", sn);#endif if ((ret = socket(sn, Sn_MR_TCP, port, 0x00)) != sn) return ret;#ifdef _LOOPBACK_DEBUG_ printf(\"%d:Socket opened\\r\\n\", sn);#endif break; default: break; } return 1;}

7 功能验证

1烧录程序现象:首先,会进行 PHY 链路检测,以此确认物理层连接状态正常,保障网络通信的基础条件;检测通过后,系统会打印出已设置好的网络地址信息,用于确认网络配置是否正确;最后,监听502端口客户端连接

2使用Modbus Poll 连接W5500进行通信

3数据采集采集成功

8 总结

        本篇文章详细介绍了如何利用W5500io-M实现Modbus RTU与Modbus TCP相互转换。感谢大家的观看!如果您对本文有任何疑问,或者希望进一步了解该产品,请随时通过私信或评论区留言,我们将尽快回复您的消息!