STM32F107+LAN8720实现Modbus TCP从站全流程解析 📅 发布时间:2026/9/2 2:39:32 👁 浏览次数: 简介以STM32F107微控制器搭配LAN8720以太网物理层收发器实现TCP Modbus通信的完整工程包面向嵌入式网络开发者和工业自动化设计人员。工程完整覆盖了STM32F107的以太网接口配置、LAN8720芯片驱动、TCP Modbus从站协议栈移植以及DHCP自动获取IP地址等实用功能能够帮助开发者快速搭建基于该硬件组合的网络通信应用。压缩包共741个文件以C语言源文件和头文件为主体辅以MDK工程配置、编译生成的axf与hex固件、说明文档以及调试辅助文件整体21.5MB便于直接导入软件开发环境进行分析和二次开发。内容预览中包含实时操作系统内核、STM32标准外设库、套接字接口和DHCP协议源码说明项目已经集成完整的网络协议栈适合用于物联网网关或工业控制设备开发。已有642人学习下载对于想要深入研究TCP Modbus协议具体实现、解决嵌入式以太网驱动与通信问题的开发者具有较高的参考价值。 STM32F107 LAN8720 的 Modbus TCP 方案我去年实际做过一轮这里把整个工程从选型到调试的关键点完整梳理一遍。压缩包命名里带日期 20191114看起来是当时固化下来的一个版本里面应该包含 LWIP 协议栈移植、硬件驱动和应用层协议处理三大部分。如果你正准备在 F107 上跑 Modbus TCP这篇文章基本能把从零到通的路线讲清楚。1. 方案选型为什么是 F107 LAN8720 Modbus TCP1.1 主控为什么挑 STM32F107STM32F107 在 ST 的 F1 系列里属于“互联型”产品最大的特点就是内置了 10/100M 以太网 MAC 控制器。F103 虽然量大便宜但片内没有 MAC想要上网只能外挂 SPI 接口的 ENC28J60 这类方案——那个东西吞吐量上不去而且驱动还要单独维护一套稳定性也一般。F107 自带的 MAC 支持 MII 和 RMII 两种接口模式配合外部的 PHY 芯片就能直接接网线。这意味着协议栈的负担主要在 MCU 端数据通路是完整的内存映射访问效率和 SPI 网卡完全不在一个量级。当年选它还有一个现实原因STM32F407 虽然性能更强但封装和引脚排布对两层板不太友好而 F107 的 LQFP100 封装手工焊接压力小BOM 成本也更低做中小批量的工业采集设备足够用。1.2 PHY 芯片为什么是 LAN8720LAN8720A 是 Microchip 的低功耗 10/100M Ethernet PHYRMII 接口最核心的优势是外围电路极简。它只需要一个 50MHz 晶振而且芯片本身可以把这个时钟通过 REF_CLK 引脚直接送给 STM32F107省掉了 MAC 侧的独立时钟源。另外一个关键点LAN8720 支持 Auto-MDIX网线直连和交叉线都能自适应现场调试省了很多排查线序的麻烦。相比 DP83848 那种经典但笨重的 PHYLAN8720 的封装是 QFN-24体积小一圈在 F107 的板子上紧凑布局非常合适。低功耗特性对工业现场 24V 供电转 3.3V 的电源压力也小实测常温下整机温升很可控。1.3 通信协议选 Modbus TCP 而不是 Modbus RTUModbus RTU 在串行链路上运行一个主站轮询多个从站波特率通常 9600 或 19200瓶颈非常明显。而 Modbus TCP 只是把标准 Modbus 的帧封装在 TCP/IP 里端口固定 502PDU 部分和 RTU 几乎一致从 RTU 迁移过来的代码改造成本很低。现场场景里Modbus TCP 最大的价值是能直接跑在现有的以太网基础设施上。PLC、触摸屏、上位机组态软件只要支持 Modbus TCP 驱动就能通过交换机同时访问多个设备不用像 RS485 那样考虑地址和拓扑限制。更重要的是TCP 协议自带可靠传输和重传机制不需要应用层再手写校验和重发逻辑开发量小一个档次。2. 硬件连接与电路设计要点2.1 RMII 接口关键信号与接线STM32F107 和 LAN8720 之间走 RMII 接口信号线比 MII 少不少。RMII 一共是 7 根信号TX_EN、TXD0、TXD1、RXD0、RXD1、CRS_DV、MDC/MDIO。时钟用 50MHz REF_CLK由 PHY 侧提供。这里有一个特别容易踩的坑LAN8720 的 REF_CLK 方向问题。LAN8720A 内部可以配置为时钟源模式把外部 50MHz 晶振产生的时钟从 REF_CLK 引脚输出给 STM32F107 的 MAC。也就是说时钟是 PHY 向 MCU 方向流的不是 MCU 给 PHY。很多人第一次画板子习惯性地从 MCU 引时钟到 PHY结果要么不工作要么必须改寄存器配置才能用。用 LAN8720 做从模式也可以但需要 MCU 提供 50MHz实际项目里强烈建议直接用 PHY 做主时钟源省一个有源晶振。接线对照可以参考 ST 官方的评估板原理图标准的对应关系如下STM32F107 引脚LAN8720 引脚说明PA1 (ETH_RMII_REF_CLK)REF_CLK50MHz 时钟输入PA2 (ETH_MDIO)MDIO管理接口数据PC1 (ETH_MDC)MDC管理接口时钟PB11 (ETH_RMII_TX_EN)TX_EN发送使能PB12 (ETH_RMII_TXD0)TXD0发送数据 0PB13 (ETH_RMII_TXD1)TXD1发送数据 1PC4 (ETH_RMII_RXD0)RXD0接收数据 0PC5 (ETH_RMII_RXD1)RXD1接收数据 1PA7 (ETH_RMII_CRS_DV)CRS_DV载波侦听/数据有效2.2 时钟、复位与地址配置LAN8720 的 PHY 地址是硬件引脚配置的默认是 0。PHYAD0 引脚也就是 RXER 引脚内部有下拉如果悬空PHY 地址就是 0x00如果外部拉高地址变成 0x01。STM32 的 ETH 驱动里eth_arch或者phy_address宏要跟硬件保持一致我第一次移植的时候默认写成 1结果 MDIO 总线上怎么都读不到 PHY 的 ID 寄存器排查了大半天才发现是这个引脚状态引起的。复位方面LAN8720 的 nRST 引脚低电平有效手册要求复位脉冲宽度不少于 25 个 REF_CLK 周期实际工程里建议直接用 GPIO 控制拉低至少 20ms 再释放。上电后不能立刻去读 PHY 寄存器要等 PHY 内部上电复位完成一般的做法是在主芯片初始化里加一个 100ms 级别的延时再开始 MDIO 通信实测下来非常稳。还有一个细节LAN8720 的 REG 1BCR里有一个软件复位位上电初始化时建议把软件复位也执行一遍确保 PHY 处于已知状态。这个步骤不是必需的但做了之后反复上下电测试时 PHY 的稳定性明显更好。2.3 电源、滤波和网络变压器的处理LAN8720 的供电是 3.3V 单电源内部集成了 1.2V 稳压器。但这个稳压器需要外接滤波电容推荐在 VDDCR 引脚放一个 10uF 和一个 0.1uF 的电容组合。LAYOUT 时PHY 的电源引脚旁边必须就近放置去耦电容不然在传输大数据帧时可能出现随机丢包。网络变压器方面常见的选择是 HR911105A 这种带 RJ45 座的集成变压器工业级选带屏蔽的版本更好或者用单独的变压器比如汉仁的 HR601680 加分立 RJ45。这里有个需要注意的点变压器中心抽头的接法。LAN8720 是 3.3V 电平的 PHY中心抽头一般建议通过 0.1uF 电容接地个别方案会接 3.3V具体要看 PHY 的驱动能力。ST 的官方评估板原理图里用的是通过电容接地的方案抄作业直接照这个来最靠谱。网线插拔的瞬态干扰对 PHY 的伤害比较大layout 时 RJ45 的金属外壳和信号地之间的处理要按参考设计来不要自作聪明把外壳直接大面积接地容易形成地环路。3. 软件架构与协议栈实现3.1 LWIP 移植与内存池配置F107 上用最广泛的协议栈是 LWIP 1.4.1 或者 2.x 版本。LWIP 的移植工作主要在三块网卡底层驱动、操作系统抽象层、内存配置。如果跑裸机LWIP 的NO_SYS要设成 1然后要把sys_now()这个时间戳函数实现为基于 SysTick 的毫秒计数。F107 的 ETH 驱动可以从 ST 标准外设库的例程里复制但要注意中断处理方式。推荐使用中断接收 主循环轮询处理的方式也就是收包时在中断里只置标志位主循环调用ethernetif_input()进行 pbuf 解析这样能避免在中断上下文里执行耗时操作。内存池是 LWIP 性能的关键。MEMP_NUM_PBUF、PBUF_POOL_SIZE和PBUF_POOL_BUFSIZE这几个参数直接影响并发能力。1024 字节的 pbuf pool 建议至少分配 16-24 个默认的 10 个在 Modbus TCP 轮询频率高的时候很容易出现 pbuf 耗尽导致的丢包。F107 有 64KB RAM这是够用的。3.2 Modbus TCP 从站应用层实现Modbus TCP 帧结构和 RTU 不同它有一个 7 字节的 MBAP 报文头事务处理标识符2 字节、协议标识符2 字节必须为 0、长度2 字节、单元标识符1 字节。长度字段是从单元标识符开始算到帧末尾的字节数。收到报文后先解析 MBAP然后取出 PDU 交给标准的 Modbus 功能码处理逻辑。从站应用层建议写成独立模块不要和 LWIP 回调搅在一起。核心数据结构是一个环形接收缓冲区TCP 收到的原始数据先塞进环形缓冲然后在主循环里按 MBAP 长度字段做帧界定。因为 TCP 是流协议没有报文边界一次 recv 回调可能只收到半个请求帧也可能一次收到两个完整的请求帧没有帧界定逻辑是肯定要出问题的。功能码按常用程度建议先实现 03读保持寄存器、06写单个寄存器、16/0x10写多个寄存器这三个。00 到 02读线圈、读离散输入和 04读输入寄存器如果位操作类变量多再加。帧格式上03 和 04 的应答是“功能码 字节数 寄存器数据”06 应答就是请求原样回传0x10 应答是“功能码 起始地址 寄存器数量”。寄存器映射表建议用结构体数组存每个寄存器带一个读写属性标志。这样读写函数可以统一处理越界和非法地址判断Modbus 错误响应 02非法数据地址就在这里产生代码逻辑一目了然。3.3 TCP 多连接管理与关闭处理Modbus Poll 这类上位机工具在测试时会同时开多个连接窗口端口 502 上必须支持多 TCP 连接。LWIP 的tcp_listen()默认限制连接数量具体数值由MEMP_NUM_TCP_PCB_LISTEN和MEMP_NUM_TCP_PCB决定。建议把MEMP_NUM_TCP_PCB设成 4 到 6这样至少能同时挂 3-4 个调试窗口和 1 个 PLC 连接。连接关闭的处理是最容易出 bug 的地方。客户端主动断开时LWIP 会在tcp_err回调里通知应用层这时候必须把对应的tcp_pcb指针置空同时检查是否还有数据包没发完。如果开了 KeepAlive 或没有正确处理关闭事件服务端会残留半开连接最终导致 PCB 资源耗尽新客户端再也连不进来。我调试时遇到过 Modbus Poll 反复开关连接窗口一分钟左右设备就完全无法建立新连接后来检查就是tcp_err回调里没有正确释放资源。4. 调试工具与联调实测4.1 Modbus Poll 与 Modbus Slave 的配合用法Modbus Poll 是主站模拟工具Modbus Slave 是从站模拟工具。实际开发中这两个配合用效率非常高先用 Modbus Slave 在 PC 上模拟一个从站验证整个链路和上位机配置是否正确然后再把 PC 端的 Slave 关掉让 Modbus Poll 直接连开发板测试自己的从站实现。需要注意Modbus Poll 未注册版本打开时会弹试用提示功能上基本可用只是有些高级功能锁定。正常调试场景不追求那些扩展功能也够用。另外Modbus 工具软件还会提示“Illegal Data Address”这种异常码这正是协议栈返回的 Modbus 异常响应能直接帮助定位寄存器地址是否越界。配置连接时关键参数是远程 IP、端口 502、从站单元 ID以及功能码和寄存器起始地址。轮询周期建议从 100ms 开始测通了再逐步降到 10ms看看设备的处理能力极限在哪。实测在 10ms 周期连续读写 20 个寄存器时F107 的 CPU 占用率还能接受但中断和主循环的时间分配得更仔细。4.2 用 Wireshark 抓包看完整交互流程联调时最好开 Wireshark 抓包一眼就能看清 TCP 握手和 Modbus 帧结构。正常连接流程是客户端发 SYN服务端回 SYNACK客户端再回 ACK三次握手完成。然后客户端发 Modbus TCP 请求帧服务端回响应帧。这两轮报文的结构完全透明对排查问题帮助极大。抓包时最应该关注的是 TCP 窗口和重传。如果出现大量 TCP Dup ACK 或者 Retransmission说明链路质量有问题或者对端处理不过来。常见的原因有两个一是缓冲区和 pbuf 配置太小二是 Nagle 算法和延迟 ACK 互相影响导致的小包延迟。实时性要求高的场景可以在tcp_accept回调里调用tcp_nagle_disable()关闭 Nagle 算法实测响应时间能降低 30ms 以上。另外抓包还能验证大小端问题。Modbus TCP 的数据是网络字节序大端而 STM32 的内核是小端寄存器数值只要直接按(buf[0] 8) | buf[1]来拼就不会错。用 Wireshark 的过滤表达式modbus能直接解析出功能码和寄存器地址不用自己十六进制掰着算效率高很多。4.3 常见 Modbus 异常码与实际处理策略Modbus 异常响应的功能码是把请求功能码的最高位置 1异常码在数据字段里。开发中常见的有三个01 非法功能码、02 非法数据地址、03 非法数据值。01 一般是对端请求了未实现的功能码比如只实现了 03、06、0x10但上位机发了个 0502 是寄存器地址越界03 是写入的数据超出了合法范围。嵌入式端的处理策略是即使不知道该怎么办也必须回一个标准的异常帧不能直接丢包不回。上位机看到响应超时会反复重试反而把网络搞得更乱。我在程序里把这个逻辑写得很明确——收到的请求帧如果解析失败也要按 Modbus 规范拼一个异常响应帧发出去。这样现场联调时问题出在应用层还是协议栈一眼就能分辨。5. 实际调试踩坑记录与经验总结5.1 网线插上后灯亮但 Ping 不通先查 PHY 地址和 CRS_DV这类问题我在两个项目里各遇到过一次根源完全不同。第一次是 PHY 地址引脚被误拉高MDIO 访问不到 PHYMAC 侧完全不知道链路状态。用逻辑分析仪抓 MDIO 波形可以看到读操作永远返回 0xFFFF。第二次是 CRS_DV 信号布线受到串扰导致接收数据被截断用示波器看波形能发现边沿不干净。排查时先读 PHY 的 BSR 寄存器地址 1确认 Link 状态位为 1、Speed 位为 100M这是网络物理层最基础的体检。5.2 Modbus Poll 能连上但读写无响应十有八九是帧解析问题TCP 连上了不代表应用层正常了。Modbus Poll 连接成功只是完成了三次握手如果发的请求帧没有被正确解析从站是不会回包的。这种问题优先打开 Wireshark看请求帧是否到达设备设备是否发了 ACK。如果设备收到数据但没回 Modbus 响应那就是应用层处理卡住了重点检查帧边界判定逻辑和环形缓冲区的读写指针管理。我自己的代码里出现过只取到了一个完整帧的前半段就开始处理直接导致功能码解析错误后来在帧边界判定里加了个状态机才彻底解决。5.3 从站重启后上位机要等很久才能重连看看 TIME_WAIT 状态TCP 四次挥手中主动关闭方会进入 TIME_WAIT 状态要等 2MSL 才能完全释放端口。如果从站作为主动关闭方比如程序中主动断开连接那 502 端口会被占用一段时间。严格来说等几十秒再试是能连上的但现场调试没人愿意等。解决办法有两个一是尽量让客户端主动断开连接二是调整 LWIP 的TCP_MSL宏把默认的 60 秒改小比如改为 5000ms。需要注意改小 MSL 在严谨的 TCP 语义上是有一点妥协的但工业现场局域网环境下影响几乎可以忽略很多设备厂商都是这么干的。5.4 寄存器读写偶尔出现值跳变先别怀疑算法看看布局和优化F107 的 Modbus 从站如果只在主循环里被动处理数据寄存器值被外部逻辑并发修改时可能出现不一致。比如主循环在处理 Modbus 读请求的同一时刻定时器中断里更新了寄存器值读到的可能就是新旧混合的数据。解决方法是读操作时先关中断或使用临界区保护写操作时用 volatile 修饰寄存器数组防止编译器优化导致变量被缓存。这个坑特别隐蔽因为问题不是必现的往往要跑几个小时才出现一次。我在实际项目里的经验是代码写完先裸机直接连 PC 端工具做 24 小时老化测试把轮询周期从 100ms 压到 10ms连续读写所有寄存器地址段如果这一步能稳定通过再去现场部署基本就稳了。另外每版固件留一个版本号寄存器比如放在寄存器地址 0x0000通过 Modbus 就能远程识别固件版本这在以后维护现场设备时是真的救过命的。最后分享一个小技巧F107 的以太网驱动初始化顺序很关键正确做法是先配置 GPIO 复用为 ETH 功能再初始化 MAC然后复位并配置 PHY最后使能接收中断。顺序反了可能出现 PHY 读不到 ID 或者能读到 ID 但收不到包的情况而且这种问题不是每次都必现属于最难排查的那一类。按这个顺序来能避开一大半初始化坑。本文还有配套的精品资源点击获取