尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F407+LAN8720A+LwIP+FreeModbus实现Modbus TCP从机实战指南

STM32F407+LAN8720A+LwIP+FreeModbus实现Modbus TCP从机实战指南 在工业现场摸爬滚打过的朋友应该都有体会项目做到最后最头疼的不是功能实现而是对接。上位机要数据、组态软件要采集、MES系统要联网而这些系统十有八九点名要 Modbus TCP。我之前接过一个改造项目原有设备是基于串口的 Modbus RTU 协议上位机在厂区中控室距离一百多米拉 RS485 线根本不可能客户又不愿意改协议重写通信库最后定下来的方案就是把设备升级成网口做一台标准 Modbus TCP 从机。细想下来用 STM32F407 自带 MAC LAN8720A 这颗 PHY LwIP 协议栈 FreeModbus 移植是最顺手、也最省钱的路子。这篇内容就是围绕这套组合把从硬件设计、CubeMX 配置、LwIP 网络链路验证到 FreeModbus TCP 移植、寄存器映射、现场联调与排障的完整过程写成一篇可直接参考的实战指南。适合正在做工业设备联网、SCADA 数据采集、远程监控的同学无论你是第一次碰以太网还是已经在用 RTU 想升 TCP看完都能少走不少弯路。1. 为什么是 STM32F407 LAN8720A LwIP 这套组合1.1 需求从哪里来Modbus RTU 升级到 Modbus TCP先说项目场景。设备原本的做法是 MCU 通过 UART 转 RS485挂一条 9600 波特率的 Modbus RTU 总线上位机用 USB 转 485 采集卡去轮询。变电站、车间这种环境电磁干扰大现场网线资源又比 485 线好规划得多加上上位机那边一水儿的以太网口用户自然想直接走 TCP/IP。这里有两个选择一是买现成的串口服务器把 RS485 转成网络但设备本体还是走 485中间多一个盒子就多一个故障点而且延时和轮询效率都不理想二是把网口做进设备里让设备直接支持 Modbus TCP。我选了第二方案。既然 MCU 本身有以太网 MAC只是缺一颗 PHY成本上比外挂 W5500 硬件协议栈更可控而且代码层面用 LwIP FreeModbus 都是开源方案License 干净改起来也自由。Modbus RTU 和 Modbus TCP 在寄存器读写这件事上本质是一回事应用层数据模型完全复用只是传输层从串口换成了 TCP所以升级工作量比想象中小核心难点其实在网络链路和协议栈移植上。1.2 器件与协议栈的选型对比主控选 STM32F407 没什么悬念。F407 内置 10/100M 以太网 MAC支持 MII 和 RMII 两种接口模式自带的 DMA 和描述符可以减轻 CPU 负担跑 LwIP 这种轻量级协议栈绰绰有余。F1 系列没有 MACF4 里 F407 因为有 512KB~1MB Flash 和大容量 RAM跑 LwIP 加协议栈不会太紧张价格和供货也稳定国内生态资料多遇到问题随便一搜就有参考。PHY 选 LAN8720A 同样是从成本和资料两个维度考虑的。LAN8720A 是低功耗的 10/100M 以太网 PHYRMII 接口只需要 7 根信号线比 MII 少了一大半 IO这对于 MCU 引脚规划非常友好。市面上很多模块设计和开发板验证资料都基于这颗芯片出问题也容易找对照。同类还有 DP83848、YT8512 等但 LAN8720A 的资料量和踩坑笔记明显更多新手友好度更高。网络协议栈选 LwIP 是顺理成章的STM32CubeMX 直接集成生成代码就能跑不需要自己手动移植。Modbus 协议处理选 FreeModbus依然是开源的它把 RTU、ASCII、TCP 三种模式的应用层都做得很干净最关键的是寄存器回调机制写得很清楚我们只需要填充业务数据协议栈自己会构造报文、处理功能码、计算 CRCTCP 模式甚至不用 CRC。1.3 这套组合里的成本账算一笔硬件成本STM32F407VGT6 批量几十元LAN8720A 单颗十元以内再加上网口变压器集成座子整套以太网硬件成本大概在二十到三十元人民币左右。对比工业串口服务器动辄几百块这个成本优势相当明显而且设备直接具备联网能力数据链路少一层中转稳定性会更好。所以只要 MCU IO 足够、Flash 足够自己做这个网口方案长远看比外挂模块划算得多。2. RMII 硬件设计7 根信号线和 50MHz 时钟必须配对2.1 RMII 接口信号一览RMII即 Reduced Media Independent Interface把 MII 的 16 根信号线精简成 7 根换来的是 PHY 和 MAC 之间必须有一个 50MHz 的参考时钟。STM32F407 的 RMII 引脚是固定映射没办法随便换到别的引脚所以原理图设计必须先打开数据手册确认引脚再画网络。常用连接关系如下功能MCU 引脚连接 LAN8720AETH_RMII_REF_CLKPA1REF_CLK50MHz 输入ETH_RMII_CRS_DVPA7CRS_DVETH_RMII_RXD0PC4RXD0ETH_RMII_RXD1PC5RXD1ETH_RMII_TX_ENPB11TX_ENETH_RMII_TXD0PB12TXD0ETH_RMII_TXD1PB13TXD1ETH_MDCPC1MDCETH_MDIOPA2MDIO我不建议自己按记忆画原理图一定要打开 STM32CubeMX勾选 ETH 外设的 RMII 模式后它会自动把可用引脚列出来对着 CubeMX 生成的引脚分配去画原理图这样最不容易错。这里特别提醒一下TXD0 是 PB12、TXD1 是 PB13这对引脚和 SPI2 的 SCK/MOSI 复用如果板子上 SPI2 已经被占用就得提前评估是否冲突。2.2 时钟方案外部晶振输出 50MHz 给 MCU 的常见接法RMII 接口下 PHY 和 MAC 必须共享同一个 50MHz 参考时钟。工程上最常见的是 LAN8720A 外接 25MHz 无源晶振PHY 内部 PLL 倍频后把 50MHz 时钟从 REF_CLK 引脚输出给 STM32F407 的 PA1也就是让 PHY 作为时钟源。这个方案的好处是 STM32F407 侧只需要配置 ETH 的 RMII 模式PA1 作为输入接收 PHY 送来的 50MHz 时钟时钟源由外部 PHY 保证时序稳定。另一套方案是 STM32 的 MCO2 引脚输出 50MHz 给 PHY但这对时钟树配置和信号完整性的要求更高还需要 MCU 主动产生时钟新手更容易在时钟树配置上出错。我自己的项目两种都试过推荐优先用 25MHz 晶振加 PHY 输出 50MHz 的方案CubeMX 默认的 RMII 配置就能直接匹配不用额外折腾 MCO。2.3 PHY 地址、复位电路和网络变压器的细节LAN8720A 的 PHY 地址由 PHYAD0 引脚的电平决定默认内部下拉到 0所以很多模块和开发板上的 PHY 地址是 0x00。如果在原理图上给 PHYAD0 加了一个上拉电阻地址就变成 0x01那 CubeMX 里的 PHY Address 必须填 1否则 MDIO 读写返回全是 0xFF无法读取到 PHY 芯片 ID。这个坑太常见了我见过好几次工程里 PHY 地址配置不对网口死活起不来的情况。复位电路也值得说。LAN8720A 的 nRST 建议用单独的 RC 复位比如 10kΩ 上拉到 3.3V、0.1μF 电容对地保证上电后有足够的复位时间。不要直接把 MCU 的 NRST 信号拉过去因为 PHY 和 MCU 的上电时序不一样共用复位可能导致 PHY 还没准备好时MAC 已经开始访问 MDIO 了。网络变压器这里LAN8720A 的 TXP/TXN、RXP/RXN 是差分信号必须经过网络变压器再连接到 RJ45 座子。有两个做法一是用分离式网络变压器加普通 RJ45二是直接用带变压器的超五类 RJ45 集成座比如 HR911105A。小批量打样建议直接用集成座省事PCB 面积也省。需要注意把中心抽头的端接电阻和电容按手册接好否则网线插上后信号质量差可能百兆能通但不稳定甚至干脆协商不上。2.4 硬件上容易翻车的三个点第一LAN8720A 的 3.3V 供电。这颗 PHY 虽然标称低功耗但以太网变压器和 PHY 在工作时瞬态电流并不小如果供电走线太细或者和 MCU 共用一条细的电源线网络峰值会拉低电压直接表现为网口时好时坏。我习惯在 PHY 电源引脚就近放一个 10μF 钽电容加 0.1μF 陶瓷电容确保高频去耦。第二MDIO 的上拉电阻。MDC 和 MDIO 一般建议加上 2.2kΩ 到 4.7kΩ 上拉MDIO 是开漏双向数据线没有上拉会读不到数据。第三如果板子上 RJ45 是带屏蔽的金属座外壳要和系统地连接原则上单点接地避免地环路引入共模干扰。工业现场机箱接地不良的话网口变压器隔离能防浪涌但屏蔽层处理不到位雷雨天气容易损坏 PHY。3. CubeMX 配置与工程搭建把 LwIP 先跑起来3.1 时钟树和 ETH 外设配置打开 STM32CubeMX型号选 STM32F407VG或者你实际用的后缀。先在 SYS 里把 Debug 设为 Serial Wire不然烧录器第二次连接容易撞上 SWDIO 复用。然后是 RCC外部高速晶振选择 Crystal/Ceramic Resonator。ETH 外设配置这一步很关键。我们用的是 RMII 模式PHY Address 按前面的硬件设计填 0。RMII 时钟由外部 PHY 提供CubeMX 里如果选的是External PHY模式它就知道 REF_CLK 是从 PA1 进来的不会去生成 MCO。与此同时RCC 时钟树里要保证 ETH 外设时钟正确这个在 CubeMX 里一般不需要手改它会自动把 APB2 的时钟配好但你要确认时钟树里没有红色的错误提示尤其是 MCO2 没有开启的情况下ETH 的 RMII 时钟源要选对。有一个地方容易犯晕开启 ETH 外设后如果时钟树报错说 ETH 相关时钟没配好大概率是外部晶振没启用或者 APB 总线时钟分频有问题按 F407 的典型主频 168MHz 配置即可ETH 的时钟源来自 PLL 的输出CubeMX 会自动安排好。3.2 LwIP 参数建议满足 Modbus TCP 的典型内存配置在 Middleware 里勾选 LwIP 后进入 LwIP 配置页面。这里面参数很多我直接给一套经过验证的配置适应大部分 Modbus TCP 从机应用IP 版本IPv4 即可不必开 IPv6DHCP关闭使用静态 IP默认 192.168.1.10掩码 255.255.255.0网关 192.168.1.1MEM_SIZE1600堆内存大小单位字节MEMP_NUM_PBUF10PBUF_POOL_SIZE8PBUF_POOL_BUFSIZE1520TCP_MSS1460TCP_WND(4 * TCP_MSS)也就是 5840TCP_SND_BUF(4 * TCP_MSS)TCP_LISTEN_BACKLOG1如果只接受一个客户端这些参数不是拍脑袋定的。LwIP 的所有内存都是从 MEM_SIZE 和内存池里分配的Modbus TCP 一帧请求很简短最常见的读保持寄存器请求就 12 个字节响应也就几十个字节数据链路层用 GMAC 描述符收发所以 1600 字节的堆对 Modbus TCP 从站完全够用。如果你以后要扩展 HTTP 服务器、MQTT 或者其他大报文应用再把 MEM_SIZE 和 PBUF_POOL_SIZE 往大调。内存够大的 F407 不用太抠但要理解这些参数之间的依赖关系尤其 TCP_WND 和 TCP_SND_BUF 要和 TCP_MSS 成比例否则窗口小了PC 端大包发送就会很慢。3.3 生成工程后的第一件事确认 PHY 通讯正常生成代码并编译烧录后先别急着调协议栈。第一个要确认的是 HAL_ETH_Init 是否成功以及能不能读到 LAN8720A 的芯片 ID。STM32CubeMX 生成的 ETH 驱动里MX_LWIP_Init() 会调用 HAL_ETH_Init它内部会通过 MDIO 访问 PHY。打开调试器在 HAL_ETH_Init 返回后看一下返回值如果返回 HAL_OK说明 MDIO 链路没问题PHY 地址和硬件对得上。另一个快速验证方式是读 PHY 寄存器比如 LAN8720A 的 PHY ID 寄存器是 0x02 和 0x03正确值应该分别是 0x0007 和 0xC0F1。如果你用 STM32CubeMonitor 或者直接调试器内存窗口读寄存器读到这两个值就说明 PHY 时钟和 MDIO 都正常了。这里如果读出来全 0xFFFF先查 PHY 地址如果读出来全 0x0000多半是 MDIO 引脚没拉上拉或者硬件没上电。4. 先验证 TCP 链路三次握手、长连接和重连4.1 ping 通只说明 PHY 层没问题很多人在 LwIP 跑起来的标志就是ping 通了。确实能 ping 通说明 ARP 能解析、IP 层能转发、PHY 链路是活的这是网络通信的底线。但 ping 通离Modbus TCP 能通信还有一段距离因为它只能证明 ICMP 回显工作正常不能证明 TCP 端口监听、应用层协议解析、以及数据回传都正常。所以我会把 LwIP 的验证分成两步走第一步 ping 通第二步用 TCP 工具直接建立连接收发数据。只有第二步也通过才能放心地开始移植 FreeModbus。4.2 用网络调试助手完成第一次 TCP 连接PC 上打开任意一款网络调试助手选 TCP Client目标 IP 填设备的 192.168.1.10端口随意比如 8080先做个临时测试端口。连接之前先保证 PC 网卡和开发板在同一网段比如 PC 设 192.168.1.20掩码 255.255.255.0。点击连接后如果 LwIP 初始化正常PDU 层的三次握手会自动完成客户端发 SYNLwIP 回 SYNACK客户端再回 ACKTCP 连接建立。这时候你从调试助手发一串数据比如 hello设备端如果写了回环测试逻辑会把同样的数据原样发回来或者你至少能在调试助手里看到连接状态变为已连接。这一步验证的是 TCP 传输层收发是否通畅LwIP 的接收回调、DMA 描述符、以太网中断是否正常工作。在这个阶段我习惯加一个最简单的 TCP 回环测试代码收到什么就回什么err_t echo_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p ! NULL) { tcp_write(tpcb, p-payload, p-len, 1); tcp_output(tpcb); tcp_recved(tpcb, p-len); pbuf_free(p); } else { tcp_close(tpcb); } return ERR_OK; }这段代码虽然朴素但能把能不能收、能不能发一次性验证完。我自己在调 LwIP 时跑通这个回环后再做应用层移植后面出问题就可以明确甩锅给应用层排查范围会小很多。4.3 长连接、短连接和掉线重连的处理策略Modbus TCP 的典型工作方式是上位机作为 Modbus TCP 客户端主动连接设备 502 端口连上之后保持长连接按固定周期轮询寄存器数据。这样省去了频繁建连的开销上位机也方便管理设备状态。但长连接会带来半开连接问题。比如上位机崩溃、拔掉网线、或者直接把程序关掉TCP 连接并不会立刻通知设备端设备端可能一直以为连接还活着。LwIP 里可以开启 TCP keepalive 机制让协议栈在空闲一段时间后主动发探测报文判断连接是否真的存活。CubeMX 的 LwIP 配置页里没有直接暴露 keepalive 开关但可以在初始化代码里对 tcp_pcb 设置tcp_pcb-so_options | SOF_KEEPALIVE; tcp_pcb-keep_idle 30000; /* 30 秒空闲后开始探测 */ tcp_pcb-keep_intvl 5000; /* 探测间隔 5 秒 */ tcp_pcb-keep_cnt 3; /* 连续 3 次无响应则断开 */我在实际项目里测试过这个参数对应上位机异常断电的场景非常有效设备会在 40 秒左右判定连接失效然后关闭这个连接释放资源等上位机重新连上来。如果不设置 keepalive设备端的连接资源会被慢慢耗尽新客户端无法连接只能重启设备才能恢复。5. FreeModbus TCP 移植把 Modbus 帧跑在 TCP 之上5.1 FreeModbus 源码里哪些文件是给你改的先整体看一下 FreeModbus 的源码结构。它的核心代码在根目录下mb.c 是协议状态机主循环functions/ 目录里是各个功能码的处理函数比如读保持寄存器是 mbfuncholding.c读线圈是 mbfunccoils.c。这些核心代码不需要修改。你真正要动手的是 port 目录它是移植层接口是固定的需要根据不同的硬件平台实现。TCP 模式里portserial 和 porttimer 这两个文件可以不用串口和定时器只服务 RTU/ASCII 模式。你需要实现的是 port.h、portevent.c 和 porttcp.c。port.h 是移植层的公共头文件主要定义数据类型和字节序。portevent.c 实现事件通知机制FreeModbus 内部用事件来做状态机驱动比如收到一帧数据后产生 EV_FRAME_RECEIVED 事件。porttcp.c 是最关键的它要把 LwIP 的 TCP 收发能力封装成 FreeModbus 需要的接口。5.2 mbconfig.h 配置要点mbconfig.h 是 FreeModbus 的开关配置决定编译哪些模式和数据模型。我常用的配置是#define MB_ASCII_ENABLED 0 #define MB_RTU_ENABLED 0 #define MB_TCP_ENABLED 1 #define MB_TCP_PORT 502 #define MB_FUNC_HANDLING_COILS 1 #define MB_FUNC_HANDLING_DISCRETE_INPUT 1 #define MB_FUNC_HANDLING_HOLDING_REGISTERS 1 #define MB_FUNC_HANDLING_INPUT_REGISTERS 1 #define MB_FUNC_READ_COILS_ENABLED 1 #define MB_FUNC_WRITE_COILS_ENABLED 1 #define MB_FUNC_READ_HOLDING_ENABLED 1 #define MB_FUNC_WRITE_HOLDING_ENABLED 1关键点是把 RTU 和 ASCII 关掉只留 TCP这样代码更精简也避免编译器试图把串口相关代码也编进去。MB_TCP_PORT 就是标准 Modbus TCP 使用的 502 端口这个不能改否则上位机默认连不上。功能码根据项目需要打开一般保持寄存器全功能码够用。5.3 porttcp.c 核心逻辑与常见移植错误porttcp.c 要做的事情很简单初始化一个 TCP 服务器监听 502 端口接受客户端连接收到 TCP 数据后把数据交给 FreeModbus 协议栈处理当协议栈产出响应报文时通过 TCP 发送给客户端。我用 LwIP 的 RAW API 来实现因为它没有依赖操作系统裸机跑或 RTOS 里都能用。初始化部分static struct tcp_pcb *mb_tcp_listen_pcb; void eMBTCPInit(void) { mb_tcp_listen_pcb tcp_new(); tcp_bind(mb_tcp_listen_pcb, IP_ADDR_ANY, MB_TCP_PORT); mb_tcp_listen_pcb tcp_listen(mb_tcp_listen_pcb); tcp_accept(mb_tcp_listen_pcb, mb_tcp_accept_cb); }注意上面这段是把 bind 后的 pcb 又赋给了 listen_pcb这是 LwIP 官方 demo 常用的一个偷懒写法实际上 tcp_listen 返回的 pcb 是新的原 pcb 被废弃。如果是正式工程建议用两个变量分开保存避免混淆。accept 回调里要做的是给新连接注册接收、错误、轮询和发送完成回调static err_t mb_tcp_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, mb_tcp_recv_cb); tcp_err(newpcb, mb_tcp_err_cb); tcp_poll(newpcb, mb_tcp_poll_cb, 2); return ERR_OK; }收到数据时需要注意 LwIP 的 pbuf 可能是链表的要处理多段 pbuf 的情况static err_t mb_tcp_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { /* 对端关闭连接 */ tcp_close(tpcb); return ERR_OK; } if (p-tot_len MB_TCP_RX_BUFSIZE) { pbuf_free(p); return ERR_MEM; } pbuf_copy_partial(p, mb_tcp_rx_buf, p-tot_len, 0); uint16_t len p-tot_len; tcp_recved(tpcb, len); pbuf_free(p); /* 数据交给 FreeModbus 协议栈 */ xMBTCPRxBufStart mb_tcp_rx_buf; xMBTCPRxBufEnd mb_tcp_rx_buf len; (void)xMBPortEventPost(EV_FRAME_RECEIVED); return ERR_OK; }这里最容易犯的错就是忘了调用tcp_recved。LwIP 的接收窗口是协议栈自己维护的你收了多少数据就要通过tcp_recved告诉协议栈窗口可以释放。如果不做这一步窗口会逐渐变成 0客户端还能发数据但服务端处理不过来表现为连接还在但数据收发越来越慢最后卡死。另一个容易踩的坑是对pbuf的所有权误解。tcp_recv_cb里拿到的pbuf所有权属于调用者也就是你的回调用完必须pbuf_free否则会导致内存池耗尽。我在调试时就遇到过一段时间后设备彻底收不到任何数据查到最后就是这里少释放了一格 pbuf。FreeModbus 端最后要调用 eMBPoll 来驱动协议栈它会从内部缓冲区里解析帧、查功能码表、调用寄存器回调、组装响应帧。响应完成后FreeModbus 通过发送函数把数据发出去这个发送函数通常在eMBTCPPoll或者发送回调里执行。我用的是在 poll 时检查是否有待发送数据再调用 tcp_write 和 tcp_output。5.4 主循环里的初始化顺序main 函数里的初始化顺序有讲究。先是 HAL 初始化、系统时钟配置然后是 MX_LWIP_Init() 把网络协议栈拉起来然后调用eMBInit(MB_TCP, 0x01, MB_TCP_PORT, 0)注意从站地址参数在 TCP 模式下其实不参与寻址TCP 模式下 Unit ID 是报文里的一个字节但函数签名还得传随便填个 1 就行。接着调用eMBEnable()使能协议栈。主循环是这样while (1) { MX_LWIP_Process(); eMBPoll(); /* 业务逻辑 */ vApplicationTask(); }MX_LWIP_Process()是 LwIP 的周期处理函数TCP 的超时重传、ARP 老化都在里面。FreeModbus 的eMBPoll()响应不能太慢如果主循环里业务逻辑跑很久才调用一次Modbus 响应延时就会超标。建议把业务逻辑拆到中断或子状态机里保证 eMBPoll 的执行周期在几毫秒级别。6. 寄存器映射与应用代码把设备数据交给 Modbus 协议栈6.1 保持寄存器回调的实现FreeModbus 的工作方式是通过回调函数把协议栈和业务逻辑解耦。它内部解析完 Modbus 请求后会调用你在eMBInit时注册的回调函数让你去读写真正的设备数据。最常用的是保持寄存器回调eMBRegHoldingCB。我的实现思路是声明一个全局的 uint16_t 数组比如 100 个保持寄存器然后在这个回调里做数组切片拷贝static uint16_t usRegHoldingBuf[100]; eMBErrorCode eMBRegHoldingCB(UCHAR *pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode) { uint16_t i; if (usAddress usNRegs 100) { return MB_ENOREG; } if (eMode MB_REG_WRITE) { /* PC 端写寄存器TCP 报文是大端需要解析成小端存到本地 */ for (i 0; i usNRegs; i) { usRegHoldingBuf[usAddress i] ((uint16_t)pucRegBuffer[2 * i] 8) | (uint16_t)pucRegBuffer[2 * i 1]; } } else { /* PC 端读寄存器本地数据转成大端发出去 */ for (i 0; i usNRegs; i) { pucRegBuffer[2 * i] (uint8_t)(usRegHoldingBuf[usAddress i] 8); pucRegBuffer[2 * i 1] (uint8_t)(usRegHoldingBuf[usAddress i] 0xFF); } } return MB_ENOERR; }注意usAddress是从 0 开始的而 Modbus 协议里保持寄存器的地址范围通常写成 40001 开头上位机软件的地址偏移处理各家并不完全一致。Modbus Poll 里会有一个Address设置通常填 0 就对应协议地址 0。不同上位机可能会做 1 偏移联调时要先确认一下不然会出现读数整体偏移一位的问题。6.2 字节序和数据对齐Modbus TCP 报文里16 位寄存器数据是大端传输也就是高字节在前。STM32 是小端存储所以本地内存里的 uint16_t 数组在赋值给pucRegBuffer时需要手动把高字节和低字节调换上面代码里写的就是这个逻辑。如果你直接把memcpy一片内存给协议栈不处理字节序上位机读到的数据会高低字节颠倒比如读到 0x1234 变成 0x3412。32 位浮点数就更要注意。工业现场经常要传温度、压力这种浮点Modbus 协议本身没有规定 32 位浮点的字节序不同厂家有不同约定常见的有 AB CD 大端和 CD AB 小端两种。最稳妥的做法是在应用层定义一个地址分配表约定哪些寄存器是 16 位整数、哪些是 32 位浮点然后在回调里做显式转换typedef union { float f; uint32_t u32; uint16_t u16[2]; } Float32_t; /* 写入时读浮点 */ Float32_t value; value.u16[0] usRegHoldingBuf[addr]; /* 高 16 位 */ value.u16[1] usRegHoldingBuf[addr 1]; /* 低 16 位 */ float temp value.f;在上位机那边Modbus Poll 可以配置寄存器显示格式为 Float AB CD 或 Float CD AB联调时让上位机工程师试两种配置看到的数值哪个合理就是用哪个约定然后把这个约定写进通信协议文档里。6.3 多寄存器读写与地址越界保护跑批生产的时候上位机会一次读几十个寄存器一次写十几个寄存器这在usNRegs大于 1 时触发。回调里usAddress usNRegs越界检查不能省一旦越界返回 MB_ENOREG协议栈会回一个 Modbus 异常码给上位机而不是发一堆乱数据这对排查问题非常有帮助。我建议在工程里把保持寄存器数组做成一个结构体把设备参数按逻辑分组比如设备状态、控制命令、温度模拟量、累计量、参数配置每一组写入函数独立处理避免回调里堆一大坨 if-else。这样以后加参数只需要改结构体和回调映射表。7. 联调与抓包Modbus 报文怎么一步步验证7.1 Modbus Poll 连接与读写测试软件层面我推荐 Modbus Poll它自带 Modbus TCP 客户端功能界面直观。打开 Modbus Poll选择 Connection 里的 ConnectConnector 选 TCP/IPIP 填设备地址 192.168.1.10Port 填 502点击 OK。如果一切正常窗口标题会显示 Connected。然后设置要读的寄存器Setup 里的 Read/Write DefinitionFunction 选 03 Read Holding RegistersAddress 填 0Quantity 填 10。点 OK 后如果一切正常表格里会出现 10 个寄存器的实时数值Scan Rate 默认是 0 表示连续轮询你可以改到 100ms 模拟工业现场轮询频率。如果这个阶段出现Connect OK 但数据区域一直无响应问题大概率在 FreeModbus 侧。常见情况是eMBPoll()没在主循环里执行或者eMBEnable()没有被调用。也可能是porttcp.c里接收到了数据但没有触发EV_FRAME_RECEIVED事件协议栈根本不知道有数据进来。7.2 Wireshark 抓一个标准的 Modbus TCP 帧到这一步我强烈建议打开 Wireshark网卡设成混杂模式抓一下以太网包。先看三次握手客户端发 SYN、服务端回 SYNACK、客户端回 ACK一目了然。如果三次握手只有前两个包说明 ACK 没回可能是客户端网卡防火墙拦了也可能设备端回包有问题。连接建立后立刻能看到 Modbus TCP 报文。一个典型的读保持寄存器请求是这样的字段值含义Transaction ID0x0001事务标识用于配对请求响应Protocol ID0x0000协议标识Modbus 固定为 0Length0x0006后面字节数Unit ID0x01从站地址Function Code0x03读保持寄存器Start Address0x0000起始寄存器地址Quantity0x000A读 10 个寄存器响应报文里Transaction ID 会原样返回Length 是后面数据的字节数功能码还是 0x03然后是字节计数加寄存器数据。抓包的巨大价值在于你一眼就能看出是谁来谁往、报文结构对不对。比如 Transaction ID 不匹配说明协议栈的请求响应配对逻辑有问题比如 Length 字段写错上位机会直接丢弃这个响应。7.3 实测中的故障排查表我把自己在这套方案里遇到过的所有问题汇总成一张表按排查顺序排列现象排查点处理方式完全 ping 不通PHY 芯片 ID 能否读到先查 MDIO 和 PHY 地址ping 通但 TCP 连不上端口是否监听tcp_listen是否调用、eMBEnable是否执行IP 能 ping 通、TCP 能连但收不到数据网口中断是否正常检查 ETH 中断优先级确认MX_LWIP_Process有没有周期运行数据偶发丢包TCP 窗口耗尽检查tcp_recved有没有调用寄存器读出来全是 0寄存器数组未初始化或回调未写入在回调里打断点确认eMBRegHoldingCB是否被触发数值高低字节颠倒字节序处理问题检查回调里大端小端转换客户端一断设备就卡TCP 错误回调未处理tcp_err里关闭 pcb 释放资源多客户端同时连会乱多连接共享状态机冲突简单方案只保留一个有效连接8. 长期运行与工程化细节那些文档里不会写的坑8.1 DHCP 还是静态 IPModbus TCP 从机在绝大多数场景下都用静态 IP。理由很直接上位机要配置设备地址如果每次上电 IP 都不一样整个系统就没法管理了。并且 Modbus TCP 最常用的轮询配置就是按固定 IP 去连设备的 IP 必须稳定可预期。设备要支持修改 IP 怎么办我建议做一个寄存器映射表把 IP 的四个字节映射到保持寄存器里再配合一个保存并重启的寄存器这样上位机通过 Modbus 就可以在线改设备地址不需要拆机下载程序。具体实现时写 IP 寄存器后需要把参数存到 Flash比如 STM32 内部 Flash 的最后一个扇区然后调用 NVIC_SystemReset() 重启重启后 LwIP 初始化时先读这个地址再设置 IP。这个功能在现场维护时实用性极高。8.2 TCP 连接异常断开后怎么恢复前面提到 keepalive这是硬件在环长时间运行的关键。此外还要处理上位机主动断开但 pcb 没有正确释放的情况。LwIP 的 err 回调里我会记录当前活动连接的 pcb 指针如果 err 触发先把指针置空static void mb_tcp_err_cb(void *arg, err_t err) { if (active_pcb ! NULL) { tcp_abort(active_pcb); active_pcb NULL; } }如果不做这一步下一次客户端连接时 accept 还是会来但旧连接资源没释放一段时间后内存耗尽。我还习惯在 accept 回调里做判断如果当前已经有一个活动客户端就把新的连接直接拒绝掉。工业现场一般就是上位机和设备一对一通信不让第二个客户端接入可以避免两个上位机同时写控制寄存器的冲突风险。8.3 散热、电源稳定性和看门狗的配合LAN8720A 在长时间工作后明显升温这是正常的但也要注意不要让 PHY 附近还有其他发热元件。PCB 上尽量给 PHY 留出一块铺铜区域帮助散热。周围的电解电容不要用 105℃ 以下品质的工业环境温度普遍偏高用料至少要用 105℃ 级别。电源稳定性方面设备外壳是金属且接交流地的话网口的网络变压器隔离效果很好但是 PHY 的 3.3V 电源建议用独立的 LDO 单独供给或者至少用磁珠把 MCU 电源和 PHY 电源隔开防止 MCU 侧高频噪声耦合进 PHY 电源。看门狗的问题容易被人忽略。很多人在裸机程序里把MX_LWIP_Process()放在主循环里但喂狗也在主循环。如果某个 Modbus 请求处理耗时过长或者 PHY 挂死导致 LwIP 轮询卡住看门狗可能也救不回来。我建议把以太网协议栈周期处理放到定时器中断里或者用一个低优先级任务单独跑看门狗喂狗在主循环里最靠前的位置执行避免它们在同一个循环里互相拖累。8.4 现场经验小结写到最后想分享一个真实经历。我在实验室里测试这套 Modbus TCP 从机稳定运行三天没问题但一拿到现场客户第二天就反馈设备偶发失联。现场排查发现他们的交换机启用了 IGMP Snooping 和部分风暴抑制策略设备偶尔广播报文字节数异常被交换机直接丢弃。后来我在 LwIP 里把 TCP 超时重传次数调小、把 ARP 缓存条目数调大又让现场关闭了对该端口的速率限制设备再没丢过连接。类似的坑还会有很多但方向和排查路径基本是通的。如果这篇文章能让你少走我走过的这几步弯路那就值了。做个工程化设备网上那些能 ping 通就算成功的教程真的只能算第一步后面的链路稳定性、内存管理、异常恢复才是真正决定设备能不能在现场站得住的关键。
返回列表