STM32+W5500实现Modbus RTU与Modbus TCP相互转换
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 硬件准备
- W5500io-M模块
- STM32F103VCT6开发板
- 一根网线
- 杜邦线若干
2.2 软件准备
- 例程链接:w5500.com/w5500.html
- 开发环境:keil uvision 5
- 飞思创串口助手
- 网络调试助手
- Modbus Slave
- 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
- 添加接收缓存声明和函数声明
#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) & 0xFF00) | (((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 TCP转RTU函数
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中数据中的CRC
uint16_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相互转换。感谢大家的观看!如果您对本文有任何疑问,或者希望进一步了解该产品,请随时通过私信或评论区留言,我们将尽快回复您的消息!
更多推荐



所有评论(0)