STM32F107 + LAN8720A + LwIP 实现 Modbus TCP 从站全解析

发布时间:2026/9/1 7:45:37
STM32F107 + LAN8720A + LwIP 实现 Modbus TCP 从站全解析 简介基于STM32F107与LAN8720 PHY芯片实现的TCP MODBUS通信工程源码包面向嵌入式网络开发者和工业自动化学习者可用于快速搭建基于Modbus TCP协议的设备通信方案。包内共741个文件约21.5MB核心代码包含154个C源文件与152个头文件覆盖以太网驱动、DHCP客户端、MODBUS协议栈及HAL库接口另含uvprojx、uvoptx等MDK工程配置以及hex、axf等编译输出便于直接打开工程或烧录验证。工程完整实现了LAN8720通过MII/RMII接口与STM32F107的连接并处理MDIO/MDC配置、PHY状态检测、自适应速率协商等细节MODBUS部分则支持读写寄存器、线圈、输入寄存器等常用功能并包含从MODBUS RTU到TCP的转换逻辑。此外工程还集成了LCD显示、传感器采集等应用层逻辑配合modbuspool等调试工具的用法说明能够帮助理解从PHY芯片配置到TCP报文交互再到业务控制的完整链路。已有642人学习下载适合需要移植或参考STM32以太网通信实现的开发者可节省大量底层调试时间。 从同事手里接过来一个压缩包文件名就叫“stm32f107 tcp modbuslan872020191114.rar”看到这个命名我基本就明白里面是什么了一块带以太网功能的MCU、一颗PHY芯片、以及一套跑Modbus TCP从站的完整工程。这种压箱底方案在工业设备改造、数据采集网关、智能仪表项目里非常常见很多产线设备至今还在跑着类似的代码。这篇就以这套组合为线索把STM32F107 LAN8720A LwIP Modbus TCP从站的全链路拆开讲清楚。内容覆盖硬件连接、驱动点亮、协议栈移植、报文格式、功能码实现以及联调阶段那些容易让人血压升高的坑。无论你是刚接触以太网应用层开发的新手还是准备在老平台上快速实现Modbus TCP通信的工程师这篇都可以作为一份可落地的参考笔记。1. 这套固件包的硬件底座STM32F107和LAN8720A是怎么配合的1.1 三个器件各司其职先把这个板上最核心的三颗料说清楚。STM32F107是意法半导体的互联型芯片Cortex-M3内核主频72MHz它最特别的点是内置了10/100M以太网MAC控制器这是它在当时能和普通F103区分开来的核心卖点。也就是说MAC层的事情交给F107但物理层信号收发需要一颗PHY芯片来完成这里选的LAN8720A就是PHY的角色。这三者的关系可以类比成一个“网口收发室”STM32F107的MAC是写文档和读文档的人LAN8720A是收发室负责把文档封装成信件寄出去、把收到的信件拆开送进来的员工而中间的RMII接口就是两个人之间的办公桌通道。实际应用中RJ45座子里还藏着一个网络变压器负责电平隔离和共模抑制这在后面调试时会成为隐性麻烦来源。STM32F107提供MII和RMII两种MAC与PHY的连接方式。这个项目用的RMII因为它只需要7根信号线节省引脚是百兆以太网低成本方案里的主流选择。RMII的时钟固定要求50MHz这是后面软件配置和硬件时钟设计绕不开的一个坑。1.2 RMII接口上的关键信号RMII接口上发送方向有TX_EN和TXD[1:0]接收方向有RXD[1:0]和CRS_DV有的芯片标为CRSDV再加上一个REF_CLK参考时钟一共7根信号线。这些信号直接对应到STM32F107的ETH_*引脚硬件设计时必须对照参考手册核对引脚复用功能不能随便接。软件初始化时要留意PHY地址问题。LAN8720A的PHY地址由PHYAD0引脚的电平决定默认是0x00也有板子把它拉高变成0x01。如果驱动初始化后读不到PHY ID首先就要检查这个地址配置。我记得以前接过一块板子因为硬件把PHYAD0通过10K电阻上拉了寄存器读出来全是0xFF折腾了整整一个下午才发现是地址对不上。这种低级问题往往比复杂协议更耗时间。1.3 时钟链路50MHz从哪里来FMII接口的REF_CLK必须是50MHz但50MHz从哪来有很多种做法。LAN8720A支持外接25MHz晶振内部通过PLL倍频到50MHz从CLK_OUT引脚输出给MCU的ETH_RMII_REF_CLK也可以直接用外部有源晶振产生50MHz时钟给PHY的REF_CLK输入。两种方案各有拥趸前者少一路有源晶振成本低后者时钟路径更干净。STM32F107还支持从MCO引脚输出时钟给PHY但在RMII模式下这个方案在时序上容易踩坑相位裕量不好控制。我在调试中实测下来最简单可靠的做法还是25MHz晶振接LAN8720A再让PHY自己输出50MHz时钟给MCU。这样MCU侧的以太网时钟和PHY完全同源不存在两个时钟源不同步的问题联调时少一类疑难杂症。2. 把网口点亮PHY驱动、RMII和链接状态的命门2.1 寄存器读写是第一步在跑协议栈之前首先要确认PHY芯片能被正常访问。LAN8720A的寄存器通过MDIO/MDC接口读写STM32F107的MAC自带MDIO控制器软件上只需操作ETH_MACMDIOAR和ETH_MACMIODR寄存器。一个常见的自检流程是先读PHY的标识寄存器LAN8720A的PHYIDR1应该返回0x0007PHYIDR2返回0x130F不一定每个版本完全一致但大致是这个范围。如果你读到的值全是0或0xFFFF先别急着怀疑代码检查硬件连接更实际。MDIO上拉电阻、PHY地址引脚电平、还有RMII接口的REF_CLK有没有起来这三项占据了80%的“读不到PHY ID”案例。我自己调试时习惯先写一个只有MDIO读写功能的最小测试程序把PHY ID读出来打印到串口这一步过了再往下走LwIP。2.2 链接状态检测是稳定性命门很多人的以太网程序在开机能通信运行一段时间后掉线就再也回不来问题大多出在链接状态检测上。PHY芯片的寄存器1是基本状态寄存器其中bit2是链接状态位0表示链接断开1表示链接正常。LwIP中通常用ETHERSENTHREAD或定时任务轮询这个寄存器一旦发现链接断开就调用etharp_tmr、netif_set_down之类的接口清理协议栈状态重新建立链接时再做全套初始化。有一台设备曾经出现“每次上电几分钟后上位机就ping不通”的诡异故障排查到最后发现是网线被老鼠咬到只有一对线还能用而这一对线正好能维持100Mbps协商失败后的10Mbps半双工——这种故障用万用表量不出来只有把PHY状态寄存器实时打印出来才能看到链接在反复up/down之间横跳。所以强调一点链接状态检测不是可选项是必须项。2.3 复位时序不能省LAN8720A的复位时序也是老司机容易翻车的地方。有些板子的NRST引脚直接连到MCU的复位输出看起来好像没问题但MCU主频起来之后PHY的启动时序其实还没有完全结束。如果立刻去访问PHY寄存器大概率失败。标准做法是给PHY单独一个GPIO做软复位控制或者至少在做MDIO访问之前延时至少100ms。很多代码里用delay_ms(50)就往下走这在实际芯片上不够保险。我习惯延时200ms用osDelay或简单循环都行这个时间成本在整个系统启动流程里微不足道但能换来很高的初始化和驱动稳定性。3. LwIP之上搭建Modbus TCP从站代码架构的取舍3.1 内存管理的铁律STM32F107只有64KB RAM部分型号到256KB跑LwIP必须精打细算。在F107平台上常用的方案是让LwIP使用内存池PBUF_POOL把PBUF_POOL_SIZE设置在1224个之间每个PBUF的大小由PBUF_POOL_BUFSIZE控制一般是需要容纳一个完整的TCP段再加协议头。这里有一条铁律以太网中断里只做数据接收和转发到tcpip_thread绝对不要做协议解析更不能在中断里调用malloc或者netconn_write。LwIP虽然不是完全禁止这么做但一旦数据量大内存碎片和重入问题会非常难查。老老实实采用tcpip_thread mailbox机制把数据从ETH中断通过信号量送到协议栈线程这是稳定性前提。3.2 我采用的架构独立TCP服务器进程Modbus TCP从站在应用层的实现有两种常见套路一种是直接使用LwIP的raw API在tcp_accept、tcp_recv等回调函数里写业务逻辑另一种是用netconn API把TCP服务器做成一个独立线程阻塞在accept上连接建立后在一个循环里轮询netconn_recv。我实测下来在F107这种资源有限的平台上netconn API更直观代码可读性也更好内存开销在可接受范围内。主流程大致是这样创建一个netconn绑定502端口调用netconn_listen进入监听状态。然后循环调用netconn_accept接收连接每来一个连接就创建一个客户端处理线程。F107的RAM同时挂3个客户端连接已经没有问题不过工业现场通常只有一个上位机在访问所以更简单的做法是只处理单连接新连接到来时把旧的连接关闭这样能大大降低资源占用。3.3 多线程还是裸机轮询这个工程如果跑的是FreeRTOS任务划分上至少要有tcpip_threadLwIP协议栈线程、ethernetif_thread以太网接口层消息处理、modbus_tcp_server_threadModbus TCP应用线程。如果只有一个Modbus连接甚至可以省掉独立的modbus线程直接在tcpip_thread的接收回调里做解析响应但这样会让tcpip_thread的负载变大TCP层面的延迟会受影响。我个人的习惯是保持独立线程哪怕这个线程大部分时间都在阻塞等待。原因是后续扩展Modbus功能时比如增加写多寄存器、批量读写独立线程的代码结构更容易维护不至于把tcpip_thread变成一个大杂烩。4. Modbus TCP报文到底长什么样从MBAP到功能码4.1 MBAP头逐个字段说清楚Modbus TCP协议和串口上的Modbus RTU最关键的区别在于TCP模式下没有CRC校验因为TCP/IP本身已经提供了可靠传输和校验而MBAP头承担了RTU中地址码和CRC的部分功能。MBAP头固定7个字节布局如下字段长度说明事务处理标识符2字节客户端每次请求递增服务器原样返回用于匹配请求和响应协议标识符2字节0表示Modbus协议目前基本固定为0x0000长度2字节后面所有字节的数量即单元标识符PDU的长度单元标识符1字节类似RTU中的从站地址用于网关路由到不同下游设备举个例子读取保持寄存器起始地址0x0000、数量2个的请求报文十六进制是00 01 00 00 00 06 FF 03 00 00 00 02。逐个拆开看00 01是事务ID00 00是协议ID00 06表示后面6个字节的数据FF是单元标识符03是功能码后面4个字节是请求数据。4.2 栈上的报文解析函数收到一帧数据后不急着找功能码第一件事就是校验MBAP头。至少要确认协议标识符为0x0000长度字段和实际收到的数据长度一致否则直接丢弃。为什么不校验单元标识符因为在典型的单设备应用中单元标识符一般填0xFF或任意值服务器侧可以宽松处理。确认MBAP头没问题之后从第7个字节开始取功能码再根据功能码决定后面如何解析。实际项目中最常用的是03读保持寄存器、06写单个寄存器、16写多个寄存器另外辅以04读输入寄存器。处理完数据后组织响应报文要求事务ID和单元标识符必须原样返回这是上位机判断请求是否被正确响应的基础。4.3 寄存器地址映射的策略Modbus是世界语但设备是各说各话的所以寄存器地址映射决定了你的设备在别人眼里好不好用。一个老生常谈但依然被忽略的问题是1-based和0-based的差异。很多上位机组态软件显示的地址从40001开始这是Modicon的习惯而设备内部实际存储从0x0000开始两者之间差1导致“能看到数据但永远差一个地址”。我的做法是定义统一的寄存器抽象层内部用一个结构体数组维护寄存器变量地址和属性只读/读写Modbus协议解析只负责找到对应的内存地址不关心变量具体含义。这样升级固件、更换变量表的时候上位机配置不用动。还可以顺手在这个抽象层实现“非法地址异常码02”的返回避免上位机查不到数据时直接卡死。4.4 功能码实现中容易忽略的边界写多个寄存器的功能码16请求里会带字节数计数例如写2个寄存器就是4字节。解析时一定要拿这个字节数和实际剩余缓冲区长度做对比防止异常报文把程序搞飞。同理读取时如果请求的寄存器数量乘2后和响应长度对不上也要主动报异常码03非法数据值。这些防御性写法在Modbus RTU时代没有太大必要因为串口一帧就那么长但TCP连接可以被任意设备访问数据边界和长度校验就成了底线。5. 上位机联调实录端口占用、连接闪断和异常码问题5.1 能ping通但Modbus连不上联调时最常见的现象就是PC能ping通设备但Modbus Poll连接失败或超时。如果设备是网线直连主机先确认PC防火墙有没有放行502端口Windows防火墙在默认情况下会拦截入站的TCP连接但ICMP的ping报文不受影响这就造成“能ping通但业务端口不通”的假象。排查顺序是先在本机telnet 设备IP 502看端口通不通不通就重点查防火墙和监听端口。如果telnet是通的但Modbus Poll依然不工作下载一个网络抓包工具看一下。抓包后你会发现大概率是设备端根本没有回应Modbus请求这种情况就要查设备侧的程序TCP server是否真的创建成功、bind的端口是不是写成了别的值、netconn_accept是否在正常运行。我在调试阶段习惯把PHY状态、TCP状态、收到的报文长度都通过串口打印出来能有效缩小排查范围。5.2 端口占用bind 502失败跑在Windows上位机常见的一个报错是“ports are not available: exposing port tcp 0.0.0.0:502”这通常是Docker或某些开发工具在尝试绑定502端口但被占用。如果用Modbus Poll做测试也可能遇到“端口被占用”的提示因为有些工具默认会占用本地端口。解决方法是先用netstat -ano查看502端口是被哪个进程占着或者在测试时让上位机的本地端口映射到其他随机端口不一定要本地502。设备侧同样可能遇到这个问题。如果开发板上还跑了其他网络服务占用了502端口或者上次程序退出后socket没有关闭重启后bind会失败。调试期可以做一个“软重启”逻辑设备上电后如果502端口被占用自动关闭旧连接重新bind这在长时间联调测试时能省不少重启的时间。5.3 连接只有重启才能连上一分钟热搜里有一条“西门子tcp只有每次重启的时候才能连上一分钟”这种问题在Modbus TCP设备上也比较常见。现象是设备重启后能正常通信大约一分钟内必然断开之后无论怎么重新连接都回不来。这种“每隔一定时间就断开”的规律优先怀疑是协议栈里的某个超时定时器或者看门狗没喂狗。对于基于LwIP的方案排查点有两个一个是tcpip_thread是否持续运行另一个是ethernetif_link状态检查线程是否在正常调度。如果用了RTOS可能是某个优先级较高的任务堵住了tcpip_thread的调度导致协议栈无法及时响应TCP保活超时后连接就被系统回收。实际解决手法是降低Modbus业务线程的优先级或者给tcpip_thread一个更高的中断优先级。5.4 异常码02和功能码不识别Modbus Poll连接成功但读不到正确数据时工具界面会显示异常码最常见的是02 Illegal Data Address。这个异常码的意思是功能码支持但请求的数据地址超出了设备映射范围。排查思路就是核对设备寄存器映射表和上位机请求地址。可以在设备侧把每次请求的起始地址和数量打印出来对比上位机的配置问题一目了然。如果是地址差1的问题就说明设备端和上位机的地址基准不同统一约定后再去沟通。还要注意一种情况是上位机读取寄存器数量超过了你定义的最大值比如你只实现了32个寄存器请求一次读50个也应该返回异常码02或03而不是用内存越界来回答。6. 接手老固件前我最想确认的三件事拿到这个rar包之后如果你和我一样是实际要用起来的人我建议先不要急着去烧录先打开工程看一眼代码结构确认三件事。第一件是PHY地址到底配的几。打开系统初始化代码搜索0x01或者0x00相关的MDIO地址配置和硬件原理图对照一遍。这是一个3分钟能解决的问题错过了可能要排查很久。第二件是LwIP的内存池配置和FreeRTOS的heap配置是否留了余量。如果堆不够Modbus通信会时好时坏而且很容易出现“连上一段时间后无响应”这类间歇性故障。看配置时重点确认MEM_SIZE、PBUF_POOL_SIZE和RTOS的TOTAL_HEAP_SIZE三者的关系。第三件是看门狗。工业设备几乎都会开独立看门狗但喂狗的位置很讲究。如果喂狗放在了Modbus请求处理的回调里那么当上位机不轮询设备时系统会不断复位。正确做法是喂狗放在一个固定的RTOS tick任务里和业务流量彻底解耦。我看到过太多老工程的看门狗喂狗位置有问题跑着跑着莫名其妙重启问题不在Modbus协议在于喂狗逻辑绑定错了事件。工程代码是20191114的日期到现在的设备环境里跑大概率会有一些兼容性小问题但核心方案本身非常成熟。把底层时钟、PHY地址、看门狗、链接检测这四件事确认好这套Modbus TCP从站就能稳定地跑很久。我在现场摸爬滚打的经验是Modbus TCP的技术难度不在于协议本身而在于底层以太网链路的稳定性和应用层的防御式编程。硬件链路调好了协议栈配置合理了Modbus TCP就是几个函数调用的事。本文还有配套的精品资源点击获取