从零构建:基于W5500与FreeModbus实现STM32与西门子S7-1200 PLC的Modbus/TCP通信
1. 项目背景与硬件选型工业控制系统中设备间的可靠通信是核心需求。最近在做一个自动化产线项目需要让STM32单片机与西门子S7-1200 PLC交换数据。经过对比多种方案最终选择了W5500FreeModbus的组合来实现Modbus/TCP通信。这个方案最大的优势是硬件协议栈稳定开发周期短。先说说硬件配置。我用的主控是STM32F103VET6野火指南者开发板它虽然没有原生以太网接口但通过SPI连接W5500模块就能快速组网。W5500是带硬件TCP/IP协议栈的以太网芯片比软件协议栈方案如LWIP更稳定特别适合工业环境。PLC端选择西门子S7-1200因为它原生支持Modbus TCP协议省去了协议转换的麻烦。这里有个关键细节MAC地址首字节必须为偶数。如果现场有多台设备每台的MAC地址需要唯一。我的配置如下uint8 mac[6] {0x00,0x08,0xdc,0x11,0x11,0x11}; // 首字节0x00是偶数 uint8 local_ip[4] {192,168,1,8}; // 与PLC同一网段 uint16 local_port 502; // Modbus TCP标准端口2. FreeModbus协议栈移植FreeModbus是一款开源协议栈但直接移植到STM32W5500平台需要些技巧。建议分三步走2.1 基础框架搭建首先从GitHub下载FreeModbus源码建议用1.5版本重点关注这几个文件port/portserial.c→ 修改为TCP通信port/portevent.c→ 事件驱动改造port/porttcp.c→ W5500适配层在mbport.h中重定义关键宏#define MB_PORT_HAS_TCP 1 // 启用TCP模式 #define MB_TCP_PORT 502 // 端口号 #define MB_TCP_ADDRESS 192.168.1.8 // 本地IP2.2 事件驱动改造Modbus本质是主从问答机制在TCP模式下需要处理连接状态。我在portevent.c中实现了事件状态机void xMBPortEventPost(eMBEventType eEvent) { switch(eEvent) { case EV_FRAME_RECEIVED: // 触发数据解析 vMBFrameReceiveCur; break; case EV_EXECUTE: // 执行功能码处理 eMBFuncWriteHoldingRegister(...); break; } }2.3 W5500适配层关键是要实现xMBTCPPortSend()和xMBTCPPortRecv()这两个函数。注意TCP通信需要维护长连接BaseType_t xMBTCPPortSend(uint8_t *pucData, uint16_t usLength) { uint16_t sent_len 0; while(sent_len usLength) { int16_t ret send(SOCK_TCPS, pucDatasent_len, usLength-sent_len); if(ret 0) return pdFALSE; sent_len ret; } return pdTRUE; }3. W5500网络层实现W5500的配置相对简单但有几个坑需要注意3.1 硬件初始化SPI时钟建议不超过30MHz初始化顺序很重要硬件复位拉低RST引脚至少500us配置PHY工作模式PHY_CONFIGURE寄存器设置MAC/IP参数void W5500_Init(void) { SPI_Config(); // SPI时钟相位/极性配置 reset_w5500(); // 硬件复位 set_w5500_mac(mac); set_w5500_ip(local_ip, subnet, gateway); printf(Ping测试: %d.%d.%d.%d\n, local_ip[0],local_ip[1],local_ip[2],local_ip[3]); }3.2 Socket状态机处理Modbus TCP需要维护Socket连接状态我的处理逻辑如下void modbus_tcps(void) { switch(getSn_SR(SOCK_TCPS)) { case SOCK_CLOSED: socket(SOCK_TCPS, Sn_MR_TCP, local_port, 0); break; case SOCK_INIT: listen(SOCK_TCPS); break; case SOCK_ESTABLISHED: if(getSn_RX_RSR(SOCK_TCPS) 0) { recv(SOCK_TCPS, ucTCPRequestFrame, sizeof(ucTCPRequestFrame)); xMBPortEventPost(EV_FRAME_RECEIVED); // 触发Modbus处理 } break; } }4. 西门子PLC端配置PLC作为Modbus主站需要做两处关键设置4.1 TIA Portal配置在OB1中调用MB_CLIENT指令块配置连接参数INTERFACE_ID硬件标识符可在设备视图查看CONNECT_DB指向连接参数DB块MB_MODE0-TCP/1-RTU4.2 数据映射建议使用DB块做数据中转。例如读取STM32的保持寄存器MB_CLIENT.REQ : TRUE; MB_CLIENT.MB_DATA_ADDR : P#DB1.DBX0.0 WORD 10; // 读取10个字 MB_CLIENT.DATA_ADDR : 40001; // Modbus起始地址5. 联调测试与排错实际调试时我遇到了三个典型问题5.1 连接超时现象PLC报错16#8180 解决方法检查防火墙是否开放502端口用Wireshark抓包确认TCP三次握手是否完成确认STM32的IP与PLC在同一子网5.2 数据错位现象收到的数据字节顺序错误 解决方法在FreeModbus中设置正确的字节序#define MB_BIG_ENDIAN 0 // 小端模式 #define MB_HIGH_LOW_ORDER 1 // 高低字节顺序5.3 性能优化当读写大量数据时可以增大W5500的Socket缓冲区#define TX_BUF_SIZE 2048 #define RX_BUF_SIZE 2048 socket_buf_init(TX_BUF_SIZE, RX_BUF_SIZE);使用Modbus功能码23读/写多个寄存器6. 完整代码结构项目最终的文件组织如下├── Drivers │ ├── W5500 │ │ ├── w5500.c // 硬件驱动 │ │ └── socket.c // Socket管理 ├── Middlewares │ ├── FreeModbus │ │ ├── port // 移植层 │ │ └── modbus // 协议栈 └── Src ├── main.c // 主循环 └── modbus_app.c // 业务逻辑关键代码片段已在文中给出完整工程可以到我的GitHub仓库获取链接见文末。在实际项目中运行超过2000小时未出现通信中断证明这个方案的可靠性。