STM32以太网ETH外设详解:从MII/RMII到lwIP调试实战

发布时间:2026/10/5 5:24:54
STM32以太网ETH外设详解:从MII/RMII到lwIP调试实战 板子上电网线插进RJ45座电脑右下角的网络图标转了好几圈最后跳出来一个黄色感叹号。这个画面我太熟悉了——几乎所有第一次调STM32以太网的人都会在这一步卡住。串口打印一切正常GPIO翻转也能看到波形但网口就是死活ping不通。问题往往不在你写的代码而在STM32内置的以太网外设ETH本身。这颗外设并没有把整个以太网方案包圆它只负责数据链路层的MAC部分物理层还得外挂一颗PHY芯片。很多人第一次打开STM32参考手册看到ETH那一章密密麻麻的寄存器就头大其实它的工作机制并不复杂。这篇文章我打算把STM32的ETH外设从头到尾拆一遍从它内部的MAC、DMA、MII/RMII接口这些基本结构讲起再结合CubeMX的配置流程、常用PHY芯片的搭配、以及我实际调试中踩过的坑把整个通信链路讲透。不管你是准备做板载以太网通信、接入lwIP跑TCP/IP协议栈还是仅仅想把ETH外设跑通这篇都值得认真看一遍。1. 先搞清楚分工STM32的ETH外设到底管到哪一层1.1 为什么STM32不把PHY也集成进去很多初学者有个疑问STM32既然叫“以太网外设”为什么还需要外接PHY芯片不能一颗芯片全包这个问题的答案藏在芯片制造工艺里。MAC层是纯数字逻辑负责组帧、拆帧、地址过滤、CRC校验这些活用CMOS数字工艺做起来非常便宜。但PHY不一样它要把数字信号变成能在网线上跑的电平涉及数模混合电路、模拟信号调理、编码解码——MAC侧的数据是并行的、电平为3.3V的逻辑信号到了网线上要变成100BASE-TX的差分信号走MLT-3编码电平幅度只有正负1V左右。这种模拟电路对工艺要求完全不同。我打个比方MAC相当于快递分拣中心负责打包、贴单、按地址分类PHY相当于运输车队负责把包裹实实在在送到路上。分拣中心可以建在写字楼里但车队必须有车库和修车厂——芯片厂商不会为了一个车队去建修车厂直接找运输公司外包更划算。所以你在几乎所有MCU上看到的以太网方案都是这个格局MCU内部集成MAC外部接一颗PHY芯片两者之间通过MII或RMII接口连接。STM32的ETH外设本质上就是这颗MAC加上配套的DMA控制器和接口逻辑。1.2 一份数据从应用层到网线的完整旅程要理解ETH外设的职责边界最好跟一包数据走一遍完整旅程。假设你写了一个TCP客户端程序要往服务器发送“Hello”这几个字节。数据先进入lwIP协议栈依次加上TCP头、IP头、以太网头变成了一个完整的以太网帧。这个帧此刻还只是放在内存里的一段数据它的目的地是网线。这时ETH外设开始介入。首先是DMA控制器把帧数据搬运到ETH外设内部的发送FIFO然后MAC内核从FIFO取出数据做这么几件事补上前导码和帧起始定界符SFD补上CRC校验字段加上帧间隙IFG然后按照MII/RMII接口的时序把数据一位一组地送给PHY芯片。PHY收到后再把MII接口传来的4位并行数据做并串转换经过扰码、MLT-3编码变成差分信号送上网线。接收路径正好反过来PHY从网线上收模拟信号解码后变成MII接口的数字信号送给MACMAC做CRC校验、地址过滤DMA再把数据搬回内存最后lwIP协议栈从内存里把数据取出来解析。这份数据流就是整个以太网通信的主线。ETH外设的位置非常清晰它在内存和PHY之间做搬运工和质检员。搞清楚这条主线后面看任何以太网相关代码都不会犯迷糊。2. MII、RMII与PHY芯片选型接口选择是个连锁反应2.1 MII和RMII在引脚和时钟上的本质差异STM32的ETH外设支持两种MAC与PHY之间的接口MII和RMII。它们的差别不光是引脚数量还牵扯到时钟方案的根本不同。MIIMedia Independent Interface是经典接口总共需要16根信号线信号类型信号名称方向说明发送数据TXD[3:0]MAC→PHY4位并行发送数据发送使能TX_ENMAC→PHY发送有效指示发送时钟TX_CLKPHY→MAC100M时25MHz10M时2.5MHz接收数据RXD[3:0]PHY→MAC4位并行接收数据接收有效RX_DVPHY→MAC接收有效指示接收时钟RX_CLKPHY→MAC100M时25MHz10M时2.5MHz载波侦听CRSPHY→MAC载波侦听状态冲突检测COLPHY→MAC冲突检测管理接口MDC/MDIOMAC↔PHY寄存器读写管理通道RMIIReduced Media Independent Interface是精简版把数据线从4位砍到2位整体只需要7根信号线信号类型信号名称方向说明发送数据TXD[1:0]MAC→PHY2位并行发送数据发送使能TX_ENMAC→PHY发送有效指示接收数据RXD[1:0]PHY→MAC2位并行接收数据载波侦听/接收有效CRS_DVPHY→MAC合并了CRS和RX_DV参考时钟REF_CLK双向/外部固定50MHz管理接口MDC/MDIOMAC↔PHY寄存器读写管理通道RMII能用一半的线干同样的活核心秘诀是把时钟从跟随速率变化改成固定50MHz。因为数据位宽减半为了在同样的100Mbps速率下传完数据时钟必须翻倍到50MHz。但注意这个50MHz是双方向共用的参考时钟谁产生它成了一个大问题。2.2 REF_CLK 50MHz从哪来三种方案各有坑我在调试RMII时被REF_CLK折腾过一整个晚上。STM32和PHY都需要这个50MHz参考时钟但谁输出、谁输入不同PHY芯片的接法完全不同。方案一外部有源晶振直接产生50MHz同时送给STM32的ETH_REF_CLK引脚和PHY的REF_CLK引脚。这个方案最干净两边都不用配置时钟输出功能但需要一颗50MHz有源晶振成本略高PCB上还要多占一块地方。方案二STM32的MCO引脚输出50MHz给PHY。以STM32F407为例MCO1PA8可以输出PLL48CLK分频后的时钟配置成50MHz送给LAN8720的REF_CLK。这个方案要求MCU的时钟树正确生成PLL48CLK后面第3节我会详细讲这个坑。方案三PHY自己输出50MHz给STM32。像DP83848这类PHY内部带时钟源或能从25MHz晶振倍频出50MHz通过CLK_OUT引脚反送给STM32。选哪个方案不是拍脑袋决定的要看你选的PHY支持哪种模式。LAN8720的REF_CLK既可以做输入也可以做输出具体靠引脚配置和寄存器设置所以开发板十有八九用MCU输出50MHz的方案。DP83848则常用25MHz晶振配合CLK_OUT输出50MHz也有外部50MHz有源晶振的接法。2.3 网络变压器和RJ45不是随便选的PHY芯片和RJ45之间还隔着一个网络变压器。这东西看着不起眼作用却很大隔离PHY和外部网线的直流电平、抑制共模干扰、提供阻抗匹配。RJ45座子有两种一种自带变压器一种不带。很多开发板用的是带变压器的HR911105A这类网口PCB设计更省事。有一件事必须提醒不同PHY的变压器中心抽头接法不一样有的接电源有的接地具体要看PHY数据手册里的参考电路。我之前换了一款PHY没注意中心抽头差异结果10Mbps勉强能通100Mbps完全不行查了几天才发现是这里的问题。3. 时钟树与引脚规划ETH初始化的两个隐形门槛3.1 ETH外设的时钟来源与系统时钟的联动STM32F4系列里ETH外设的时钟挂在AHB总线上但它的参考时钟来源很特殊——来自PLL48CLK。这意味着ETH的正常工作高度依赖系统时钟树配置正确。我把F407的时钟树简化说明一下外部晶振HSE进入PLLPLL输出经过分频得到PLL48CLK48MHz这个48MHz时钟一路给USB、SDIO用另一路经过二分频得到24MHz就是ETH外设的MAC内核时钟。RMII接口那个50MHz的REF_CLK则不是这个来源它来自MCO输出或其他时钟源前面已经讲过。这就导致了一个隐蔽的问题如果外部晶振选的是8MHz但CubeMX里系统时钟配置没正确生成PLL48CLKUSB和ETH都会异常。反过来很多以太网开发板板载晶振是25MHz因为25MHz经过PLL后更容易配置出各种时钟同时还能兼顾以太网RMII参考时钟的生成。所以你在做硬件设计时就要想清楚如果板子上ETH和USB、SDIO共存晶振频率的选择不是随便定的。以STM32F407为例25MHz晶振几乎是标配原因之一就是为了同时满足以太网和USB对时钟的要求。3.2 RMII模式下的引脚占用与冲突排查RMII虽然比MII少了一半引脚但占用数仍然不少。以最常见的RMII配置为例引脚功能说明PA1ETH_REF_CLKRMII参考时钟PA2ETH_MDIO管理数据PA7ETH_CRS_DV载波/接收有效PC1ETH_MDC管理时钟PC4ETH_RXD0接收数据0PC5ETH_RXD1接收数据1PB11ETH_TX_EN发送使能PB12ETH_TXD0发送数据0PB13ETH_TXD1发送数据1一共9个引脚。看起来不多但仔细一看就会发现这些引脚经常和别的功能打架。我举几个真实例子。PA2和PA3是USART2的TX/RX如果你用了串口2做调试就得牺牲其中一个功能。PC4、PC5在某些型号上可能复用为ADC或定时器输入。PB12是SPI2的NSSPB13是SPI2的SCK——如果板子上还有一个SPI接口的Flash或其它外设冲突几乎不可避免。我建议你在画PCB之前先拿CubeMX把所有用到的外设都配置一遍看引脚冲突在哪里有问题趁早调整别等板子打回来再改。另外ETH的引脚没有重映射选项它的复用是固定的只有部分型号支持通过AFIO在有限范围内切换绝大多数情况你只能围着这9个引脚做文章。3.3 PHY地址和MDIO时序怎么确认PHY在跟你说话PHY芯片和MAC之间有一条管理通道MDIOMAC通过它读写PHY内部的寄存器。MDIO协议里每个PHY有一个地址总线上可以挂多个PHY靠地址区分。LAN8720的PHY地址由芯片的PHYAD0引脚决定接地为0接高电平为1。DP83848可以配置成多种地址。如果你用的是CubeMX默认配置它默认PHY地址是0但你的板子如果恰好用的PHY地址是1MAC根本访问不到PHY通信自然失败。怎么验证MAC和PHY之间通信正常最直接的办法是读PHY的ID寄存器。LAN8720的PHYIDR1寄存器2值为0x0007PHYIDR2寄存器3的值为0x0200。用HAL库可以这样读uint32_t idr1 0, idr2 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_ISFR, idr1); // 寄存器2 HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_ISFR 1, idr2); // 寄存器3 printf(PHY ID: 0x%04X 0x%04X\r\n, idr1, idr2);如果读出来全是0xFFFF或者0x0000说明MDIO通信有问题先查PHY地址对不对、MDC/MDIO引脚是否接反、PHY供电是否正常。如果读出来是对的ID恭喜MAC和PHY之间的物理通道已经通了接下来才能谈协议栈的事。4. CubeMX里的ETH配置从图形界面到能跑通的细节4.1 参数面板里那些默认值背后的含义CubeMX里配置ETH第一步是在Connectivity里找到ETH勾选RMII或MII。这里有个重要前提引脚必须在CubeMX里先分配好如果选RMII但引脚没配对它会直接报错。参数面板里有一堆配置项我挑关键的说。PHY Address默认是0必须改成你板上PHY的实际地址。LAN8720地址为0DP83848在不少板子上是1改错了连PHY都访问不了。MAC Address默认是一组随机地址。理论上随便填只要不与局域网内其他设备冲突但如果你要跑工业以太网协议这里必须按厂商分配的地址段填。Ethernet Speed、Duplex Mode这些参数CubeMX默认是自动协商。如果你确定对端设备不支持自动协商需要手动指定速率和双工模式但绝大多数场景保持默认就好。还有一组高级参数DMA描述符数量、发送/接收FIFO阈值、PBL等。这些参数HAL库会给你一个默认值一般情况下不用动。但如果你发现大数据吞吐时丢包DMA描述符数量是最值得调高的参数之一。默认4个RX描述符做大数据接收时确实偏少我后来改成8个才稳。4.2 DMA描述符lwIP能干活的前提ETH外设里的DMA控制器的设计有点特殊它不靠传统的中断寄存器列表来管理数据包而是用一套称为“描述符”的内存结构。每个以太网DMA描述符对应一个缓冲区描述符里存放着缓冲区的地址、长度、状态标志。DMA从第一个描述符开始循环处理完一个再处理下一个全部用完后又回到第一个形成一个环。这就是所谓的“描述符环”。这段描述符数组放在哪里直接影响系统稳定性。F4系列里有一块CCM内存只能CPU直接访问DMA访问不了描述符绝对不能放那里。H7系列还有D2域SRAM只有ETH的DMA能访问放描述符倒是对了。F4没有这么讲究你只要保证描述符放在普通SRAM区域就可以CubeMX生成的代码会自动分配好。一个常见的坑是你在链接脚本或启动文件里改了内存布局把描述符数组挤到了错误的位置结果DMA读写异常ETH收发数据时好时坏。排查这个问题最直接的手段是检查描述符地址是否在DMA可访问的区域内以及是否4字节对齐。4.3 lwIP的堆与内存池参数调不好ping都不通CubeMX里勾选LWIP后会生成一套默认配置。很多人直接编译下载发现能初始化但ping不通然后就开始怀疑ETH硬件有问题。其实很多时候是lwIP内存配置不合格。lwIP有两种内存管理方式内存堆heap和内存池pool。TCP/UDP报文的数据缓冲使用PBUF每种PBUF的数量和大小在lwipopts.h里配置。CubeMX生成的配置默认偏保守如果你开了TCP服务器一上来开了好几个连接PBUF不够分配协议栈直接收包失败。我的建议参数如下在内存充足的STM32F407上#define MEM_SIZE (10 * 1024) // 内存堆大小10KB起步 #define PBUF_POOL_SIZE 16 // PBUF池数量 #define PBUF_POOL_BUFSIZE 1518 // 单个PBUF大小一个以太网帧 #define TCP_MSS 1460 // 最大分段大小 #define TCP_WND (4 * TCP_MSS) // TCP接收窗口 #define TCP_SND_BUF (4 * TCP_MSS) // TCP发送缓冲这几个参数是联动的不是随便填。PBUF_POOL_BUFSIZE必须能容纳一个完整以太网帧1518字节否则大的数据包会被截断。TCP_WND和TCP_SND_BUF决定单条TCP连接的吞吐量如果做文件传输这类大流量应用它们应该设得更大一些。我见过太多人在协议栈参数上被坑调大MEM_SIZE就以为万事大吉实际跑起来发现内存碎片严重长时间运行后连接越来越不稳定。lwIP的参数调优是个系统工程建议先用默认配置跑通再根据实际业务流量逐步调整。5. 实战排查ping不通时我按这个顺序定位5.1 物理层先过PHY的link状态是第一个信号当ping不通的时候我从不先在协议栈里瞎翻。第一步永远是验证PHY有没有建立物理链路。以太网的物理层连接建立有一个分工PHY芯片通过自动协商与对端设备达成速率和双工模式的一致然后由MAC配置PHY最后链路才进入up状态。判断这个状态最简单的方法是读PHY的基本状态寄存器BSR。以LAN8720为例寄存器1的bit2是Link Status为1表示链路已建立。uint32_t bsr 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_BSR, bsr); if (bsr (1 2)) { printf(Link UP\r\n); } else { printf(Link DOWN\r\n); }如果Link DOWN先别碰代码。查网线是否插好、对端设备是否开机电口指示灯亮没亮、网口座的指示灯状态。然后查PHY供电电压对不对——LAN8720是3.3V供电但它的I/O耐压有的版本支持2.5V接线方式差别很大。再查网络变压器的中心抽头接法是否和数据手册一致。之前我碰到过一个板子LAN8720的供电滤波电容没放对位置PHY工作不稳定link时好时坏最后在靠近电源引脚的地方补了一颗电容才解决。Link UP了还是ping不通那就往下看MAC层。5.2 从Link UP到能收发中间差了一个DMALink UP只说明PHY和网线没问题但数据能不能从PHY传到MAC、再从MAC传到内存是另一回事。验证MAC和DMA这一层有个好办法直接看ETH的中断和DMA状态寄存器。HAL库里ETH的接收中断触发时会调用HAL_ETH_RxCpltCallback。在这里加一个计数变量如果收包计数不增长说明DMA根本没把数据搬进来问题在MAC层配置或DMA描述符。另一个快速方法是用Wireshark抓包。把板子接在交换机上电脑也接在同一个交换机然后往板子的IP发ARP和ICMP请求。如果在Wireshark里看不到板子的响应包说明问题在MAC或协议栈如果看得到响应包但ping还是不通问题可能在上层路由或防火墙。实际调试中我发现最常见的DMA层问题是描述符数量不够导致的丢包。高速接收时RX描述符被占满新的数据包无处存放直接被DMA丢弃。我建议把RX描述符从默认的4个改成8个同时保证接收中断优先级够高别被其他中断饿死。5.3 网段、网关和ARP表协议栈层面的三个低级错误物理层和MAC层都正常ping还是不通大概率是lwIP配置出问题。这里我列三个最常见的低级错误。第一是IP网段不一致。板子IP是192.168.1.10电脑IP是192.168.2.5两者不在一个网段路由器没有转发当然不通。这个错误几乎每个人都会犯一次检查方法很简单ifconfig看一下双方IP或者用ARP命令查一下ARP表里有没有对方。第二是MAC地址冲突。有些开发板出厂固件烧了一样的MAC地址两台设备同时上网直接冲突。局域网设备里有一台和你相同MAC地址的设备时交换机转发数据会乱表现就是时通时断。这个问题的排查很隐蔽最好在初始化时强制修改MAC地址确保每个板子都不同。第三是没有配置默认网关或网关配置错误。如果你要访问的不是同一网段的地址必须配置网关。lwIP里netif-gw字段不能填错否则跨网段通信完全不可用。之前有次我在客户现场排查问题板子和电脑明明都在同一个局域网里ARP也能互相看到但UDP数据总是发不出去。折腾了半天最后发现lwIP的netif还没有被设置为up状态netif_set_up函数根本没调。这个错误低级得很但现场一着急注意力全放在上层协议上反而忽略了基本状态。6. 性能与稳定性从能ping通到能长期跑业务6.1 中断模式还是轮询模式看你的业务类型ETH的HAL库提供了两种工作模式中断和轮询。CubeMX默认生成的是中断模式ETH的DMA收到数据后触发中断在回调函数里处理数据。轮询模式则是主循环里不断调用HAL_ETH_GetRxDataBuffer检查有没有新数据。这两种模式没有绝对的优劣取决于业务场景。如果MCU同时跑着多个任务主循环被其他任务占用的时间不确定就应该用中断模式保证以太网数据不丢。如果以太网只是其中一个功能数据量不大轮询模式反而更简单——代码好写、调试方便还不容易踩中断优先级和重入的坑。我自己的习惯是裸机程序里用轮询模式跑小数据量业务RTOS里用中断模式配合信号量唤醒协议栈处理任务。注意中断回调里千万别做耗时操作就塞一个二进制信号量或置一个标志位真正的数据处理放到主循环或专用任务里。6.2 实测数据不同工作状态下的性能表现我手头一块F407LAN8720的板子跑lwIP做了一套简单的UDP回环测试。在轮询模式下UDP小包64字节来回耗时大概0.3ms吞吐量约2Mbps大包1400字节吞吐能到7Mbps左右基本已经接近裸机轮询的上限。换成中断模式并把RX描述符调到8个以后小包来回耗时降到0.2ms以下大包吞吐能到9.5Mbps左右。这个性能跑常规的传感器数据采集、设备状态上报完全够用但如果你要拿它做视频流或者文件大传输F407的M4内核会先成为瓶颈。长时间运行稳定性方面我踩过一个典型坑UDP收发连续跑48小时以后内存占用稳步上升最后协议栈直接卡死。排查下来发现不是ETH外设的问题是代码里一个缓冲区分配了没释放产生了内存泄漏。lwIP的PBUF分配是动态的,用完了必须调用pbuf_free归还不是RTOS的free是lwIP自己的内存管理函数。写网络代码前一定要把PBUF的生命周期管理搞清楚。6.3 几个让我少加班的经验调试ETH和lwIP的过程中有几个经验是文档里不会写但特别有用的。网口变压器的中心抽头电容取值会影响信号质量。PHY工作时推荐在差分线对上并联一颗小电容具体值参考PHY数据手册不是所有PHY都一样。我换过一颗PHY后一开始没注意这一点100Mbps模式下丢包很严重加了对电容以后才恢复正常。调试中多用Wireshark不要光盯着串口打印。串口打印只能看到本端状态抓包能同时看到双方报文交互的全过程。比如ARP请求有没有收到、应答有没有发出、TCP握手卡在哪个阶段在抓包里一目了然。最后是printf的安全问题。在中断回调里用printf打印调试信息如果调用链过长可能导致重入冲突。我在RTOS里遇到过printf死锁进调试器看了半天才发现是中断里调printf触发了堆分配锁。调试阶段可以在特定标志位控制下打印生产代码把打印全部关掉。如果你正在调STM32以太网链路不通时不要死盯着协议栈代码按物理层、MAC层、协议栈这个顺序逐层排查少走弯路。希望这篇内容能帮你早点把网口点亮。