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 硬件准备
- 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) & 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中数据中的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相互转换。感谢大家的观看!如果您对本文有任何疑问,或者希望进一步了解该产品,请随时通过私信或评论区留言,我们将尽快回复您的消息!