STM32以太网RMII接口硬件设计要点与驱动调试实战
1. 从MII到RMII先搞懂接口到底在传输什么很多人第一次接触STM32以太网时第一反应是“不就是用个网口吗”。等真正打开数据手册才发现单片机不会凭空变出网络功能它内部有一个MAC控制器负责打包、解包、地址过滤这些逻辑层工作但物理层信号的收发、电平转换、编码解码得靠一颗外部的PHY芯片来完成。而MAC和PHY之间交换数据的那条通道就是MII/RMII接口。1.1 MII和RMII的核心差异传统MII接口需要16根信号线数据线收发各4根加上时钟、控制、状态线密密麻麻一片。对于引脚资源紧张的单片机来说这几乎是一种奢侈。RMIIReduced Media Independent Interface把它砍到7根信号线TXD[1:0]发送数据、RXD[1:0]接收数据、TX_EN发送使能、REF_CLK参考时钟、CRS_DV载波侦听/数据有效。逻辑上还是那个以太网物理上引脚少了一半还多代价是时钟频率从25MHz翻倍到50MHz数据位宽从4bit降到2bit用速率换引脚。我用过STM32F407和STM32F429做产品这两颗芯片的MAC都支持MII和RMII两种模式。硬件设计上我更倾向于RMII不是因为MII不好而是四层板空间本来就紧张省下9根走线能降低干扰风险布局也从容。尤其是当你把RJ45、网络变压器、PHY芯片放在板边时RMII的走线长度更短阻抗控制更容易做。1.2 为什么外部PHY是必需品有些新手会问STM32不是有MAC吗为什么还要PHY打个比方MAC是大脑PHY是嘴巴和耳朵。大脑想说话但发出的信号是数字逻辑电平根本没法直接驱动网线这种模拟传输介质。PHY负责把MAC送来的并行数据变成串行比特流再经过编码、调制、驱动最后通过双绞线发出去。接收方向则是逆过程。另外以太网线的电压、电流、共模干扰处理都依赖PHY内部的模拟前端和DSP电路。所以板上那颗PHY芯片决定了你的网口能不能自适应10M/100M、能不能正确识别网线类型、抗不抗得住工业现场的电磁干扰。市面上常见的PHY芯片比如Microchip的LAN8720A、TI的DP83848、Realtek的RTL8201F各有特色。我后面所有讨论以LAN8720A为主因为它的RMII引脚模式配置最简单外围也最少网上资料多最适合入门。2. 硬件电路设计每一根线都不能将就2.1 引脚分配与RMII信号映射STM32F407的以太网MAC引脚是固定的但如果复用功能对了你可以把它们映射到不同的GPIO端口。RMII模式下STM32的ETH引脚分配如下PA1是ETH_RMII_REF_CLKPA2是ETH_MDIOPA7是ETH_RMII_CRS_DVPC1是ETH_MDCPC4是ETH_RMII_RXD0PC5是ETH_RMII_RXD1PG11是ETH_RMII_TX_ENPG13是ETH_RMII_TXD0PG14是ETH_RMII_TXD1。我不建议随意迁就布局把引脚换掉因为以太网信号的时序要求很严格GPIO位置一变走线就得绕绕了就容易出问题。一个容易踩的坑是REF_CLK的来源。RMII规范要求MAC和PHY必须共用一个50MHz参考时钟不能各用各的。两种实现方式第一种是外部50MHz有源晶振直接接STM32的REF_CLK引脚同时分给PHY的XI引脚第二种是PHY用25MHz晶振内部PLL倍频到50MHz然后从PHY的REF_CLK输出脚回给STM32。我推荐第一种理由很简单外部50MHz有源晶振的抖动指标更可控PHY内部的时钟输出路径会引入额外延迟在高速数字设计里时钟抖动直接影响接收误码率。注意REF_CLK在RMII模式下是双向的。如果用外部50MHz晶振它就是从晶振到MAC和PHY的单向信号如果让PHY输出时钟那STM32的REF_CLK必须配置为输入。千万别搞反搞反了MAC根本采不到数据。2.2 PHY芯片外围电路与电源设计LAN8720A的硬件电路核心就几块电源、时钟、复位、网络变压器。电源方面它内部集成了1.2V稳压器只需要一路3.3V供电比一些需要多路电源的老PHY省事。但3.3V要特别注意去耦每个电源引脚旁放一个0.1uF陶瓷电容靠近引脚摆放另外再放一个4.7uF或者10uF的钽电容做低频储能。我见过有人图省事只放一颗0.1uF结果EMC测试时辐射超标问题就出在PHY芯片瞬态电流抽不上来。时钟电路如果不用外部50MHz就用25MHz无源晶振接XI和XO引脚负载电容按datasheet要求选通常是18pF到22pF。晶振的两条走线要尽量短且远离TX/RX差分线否则晶振的谐波会耦合到网线上导致信号质量下降。复位引脚用RC复位电路即可10kΩ上拉到3.3V0.1uF电容到地复位时间大约1msSTM32上电后要延时一段时间再访问PHY寄存器这个时序细节后面再讲。网络变压器不能省。它的作用有三个电平耦合、隔离共模、抑制浪涌。LAN8720A的TX±和RX±引脚直接接网络变压器的初级次级接RJ45。选型上带内置共模扼流圈的一体化RJ45比如HanRun HR911105A最省事一颗器件就把变压器和连接器全解决了。初级侧中心抽头接3.3V次级侧中心抽头通过75Ω电阻和0.1uF电容接地这个匹配电路能改善回流损耗实测下来对于百兆以太网是必要的。2.3 完整连接拓扑电路图关键点我画一个简化的连接关系方便你对照自己画的原理图检查STM32F407 LAN8720A ----------------- ----------------- PA1 REF_CLK ---- 50MHz OSC ----- XI (或REF_CLK输出) PA2 MDIO ------------------- MDIO PC1 MDC ------------------- MDC PA7 CRS_DV ------------------- CRS_DV PC4 RXD0 ------------------- RXD0 PC5 RXD1 ------------------- RXD1 PG11 TX_EN ------------------- TX_EN PG13 TXD0 ------------------- TXD0 PG14 TXD1 ------------------- TXD1PHY的地址引脚PHYAD0默认为下拉PHYAD1默认为下拉LAN8720A的默认地址就是0x00。如果你在I2C总线上挂了别的器件或者想多颗PHY共存可以通过修改PHYAD[1:0]的上下拉来设置不同地址。我自己的板子就用默认地址省两个电阻。模式选择引脚还有一些相互矛盾的说法。LAN8720A的MODE[2:0]引脚决定工作模式它出厂默认是“全部自动协商”也就是MODE[2:0] 111对应10/100M自适应。这个配置是通过内部上拉实现的外部不需要额外处理。但如果你手动把MODE[2:0]拉低到其他组合可能会锁定成100M全双工或者10M半双工初学者很容易在这里被带偏——网线插上灯不亮排了一圈发现是模式引脚被外部下拉了。3. STM32端配置与驱动移植实操3.1 CubeMX图形化配置步骤现在用STM32CubeMX配置以太网已经非常方便。你先在Pinout视图里找到ETH把RMII接口勾上软件会自动把上面列的GPIO引脚分配好。然后到Configuration里打开ETH的参数PHY Address填0和硬件对应PHY Clock Source选择External Clock对应外部50MHz方案MAC Address随意填一个建议使用板子背面贴的MAC标签接着配置RMII的时钟。外部50MHz晶振接到PA1后这个引脚在CubeMX里会自动显示为ETH_RMII_REF_CLK。如果你用的是PHY输出25MHz/50MHz方案CubeMX里不需要额外配置时钟源但PA1必须保持浮空输入模式否则外部时钟信号进不来。生成代码后默认的ETH驱动已经初始化了GPIO和MAC你只需要自己补齐PHY的寄存器读写函数。我建议还是用正点原子或者野火的标准例程作为起点因为他们的PHY驱动写得比较完整直接帮你封装好了LAN8720A的复位、初始化、状态读取函数。自己从零写也行但需要掌握MDIO总线的操作时序后面我详细展开。3.2 PHY寄存器读写时序MDIO是灵魂MDIOManagement Data Input/Output是一个两线的管理接口MDC提供时钟MDIO传输数据。它的时序原则MDC高电平期间MDIO数据必须保持稳定MDC上升沿采样。读操作要先发送32位前导码连续的1、起始码、操作码、PHY地址、寄存器地址然后切换方向读取16位数据。写操作则不需要切换方向直接发完地址后跟16位数据。我直接给一段常用的MDIO读写代码基于STM32标准外设库用GPIO模拟时序// 宏定义用于控制MDIO引脚电平 #define MDIO_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_2) #define MDIO_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_2) #define MDC_HIGH() GPIO_SetBits(GPIOC, GPIO_Pin_1) #define MDC_LOW() GPIO_ResetBits(GPIOC, GPIO_Pin_1) #define MDIO_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) void MDIO_Start(void) { MDIO_HIGH(); for (int i 0; i 32; i) { MDC_HIGH(); MDC_LOW(); } // 起始码 01 MDIO_HIGH(); MDC_HIGH(); MDC_LOW(); MDIO_HIGH(); MDC_HIGH(); MDC_LOW(); // 操作码 10读 MDIO_HIGH(); MDC_HIGH(); MDC_LOW(); MDIO_LOW(); MDC_HIGH(); MDC_LOW(); } uint16_t PHY_ReadRegister(uint8_t phyAddr, uint8_t regAddr) { uint16_t regValue 0; MDIO_Start(); // 发送PHY地址(5bit)和寄存器地址(5bit)从高位开始 for (int i 4; i 0; i--) { if (phyAddr (1 i)) MDIO_HIGH(); else MDIO_LOW(); MDC_HIGH(); MDC_LOW(); } for (int i 4; i 0; i--) { if (regAddr (1 i)) MDIO_HIGH(); else MDIO_LOW(); MDC_HIGH(); MDC_LOW(); } // 读数据需要2个周期的 turnaround MDIO_HIGH(); MDC_HIGH(); MDC_LOW(); MDIO_HIGH(); MDC_HIGH(); MDC_LOW(); // 读取16位数据 for (int i 15; i 0; i--) { MDC_HIGH(); if (MDIO_READ()) regValue | (1 i); MDC_LOW(); } return regValue; }注意第三行的Start函数里读操作码是10二进制写操作码是01。如果你把这两个弄反PHY不会响应读回来全是0xFFFF。这类低级错误排查起来特别费时间我第一次调的时候花了半天才发现是操作码顺序写反了。3.3 PHY初始化与自协商处理复位PHY后要等待PHY内部上电稳定一般延时10ms以上然后读取寄存器0Basic Control Register和寄存器1Basic Status Register。LAN8720A的寄存器0的bit15是Soft Reset写1触发软复位复位完成后自动清零。寄存器1的bit5是Auto-Negotiation Complete置1表示自协商完成。我给的初始化流程void LAN8720A_Init(void) { // 硬复位 GPIO_ResetBits(GPIOG, GPIO_Pin_12); // 假设复位引脚接PG12 delay_ms(10); GPIO_SetBits(GPIOG, GPIO_Pin_12); delay_ms(150); // 等待PHY稳定 // 软复位 PHY_WriteRegister(0x00, 0x8000); delay_ms(50); // 启动自协商 uint16_t bcr PHY_ReadRegister(0x00); bcr | (1 12); // bit12 Auto-Negotiation Enable bcr ~(1 13); // 清除掉强制模式位 bcr ~(3 8); // 清除速度选择位 PHY_WriteRegister(0x00, bcr); // 等待自协商完成超时保护 uint32_t timeout 1000000; while (timeout--) { uint16_t bsr PHY_ReadRegister(0x01); if (bsr (1 5)) break; } }这里有个重要细节等待自协商完成的循环不能无限死等。如果网线没插或者对端设备没通电自协商永远不会完成程序就会卡死在循环里。工业现场最常见的故障就是有人只插了电源忘了插网线结果整个设备看起来“跑飞”了。正确做法是加超时退出之后通过网络状态指示灯或者串口打印提示用户。自协商完成后读取寄存器0x1FLAN8720A专有的PHY Special Control/Status Registerbit2和bit1分别表示速度和双工模式。100M全双工时这两位是10100M半双工是0010M全双工是0110M半双工是11。很多驱动不读这个寄存器直接默认100M全双工如果对端设备只支持10M就会出现频繁丢包甚至完全不通。4. 以太网通信调试与常见问题排查4.1 排查流程从物理层到协议层如果你把驱动烧进去发现网口ping不通别急着怀疑代码。我是按照这个顺序排查的先看硬件再看PHY再看MAC最后看协议栈。硬件层面检查供电、晶振、复位PHY层面用MDIO读写寄存器看能不能读出正确的IDLAN8720A的寄存器2和3应该读出0x0007和0xA000MAC层面看RMII接口的信号有示波器最好没有就通过ETH_DMARxDescToGet函数观察有没有接收到任何数据包协议层才轮到ping。现象排查方向常见原因MDIO读回全0xFFMDIO/PHY供电引脚配置错误或PHY地址不对自协商一直不完成网线/RJ45/变压器网线质量差或变压器中心抽头接错指示灯亮但ping不通MAC配置REF_CLK不稳定或ETH DMA没有启动能ping通但丢包严重信号完整性RMII走线过长或电源纹波过大4.2 几个我踩过的坑第一个坑是REF_CLK走线太长。我第一版PCB把50MHz晶振放在板子角落PA1在芯片另一侧走线绕了大半个板子结果自协商倒是能完成但数据吞吐率极低100M的链路实际只能跑到20M。后来用示波器量REF_CLK波形已经有明显的振铃和过冲。解决办法是缩短走线并且在靠近PA1引脚处串联一个22Ω的阻尼电阻。走线长度少于20mm最佳实在做不到就至少保证REF_CLK不跨越电源分割区域。第二个坑是PHY复位时序。STM32和PHY独立上电STM32的GPIO复位信号可能在上电瞬间有一个短暂的高电平脉冲导致PHY根本没复位干净。后来我加了RC延时电路确保PHY复位引脚在上电后至少保持10ms低电平。同样软件里初始化PHY之前也要加延时不能一上电就立刻读寄存器那时候PHY内部PLL还没锁住。第三个坑是MDIO的上拉电阻。MDIO是双向开漏信号必须接上拉电阻才能正常通信。有些PHY评估板会内置上拉但自己画板子时经常忘了加。表现就是MDIO读写时好时坏偶尔能读到数据稍微一变动就不行。接一个4.7kΩ上拉到3.3V就能解决要求高的场合可以换成2.2kΩ。4.3 性能优化与吞吐率提升如果你的以太网通信已经通了但吞吐率一直上不去有几个优化方向。第一是DMA描述符数量STM32的ETH驱动默认给4个发送描述符和4个接收描述符但高负载下不加大描述符数量就会频繁丢包。可以调整eth_dma.c里的ETH_TX_DESC_CNT和ETH_RX_DESC_CNT一般改成8到16个。第二是中断和轮询的取舍低速率应用用轮询就够了高速率应用必须开中断并且中断服务函数里尽量少做处理只置标志位数据解析放到主循环。第三是协议栈的接收缓冲区大小lwIP的PBUF_POOL_SIZE默认是16个如果你的应用数据包较大可以适当加大到32或64。我实际测过STM32F407在RMII模式下用lwIP做TCP服务器上传速度能到90Mbps以上瓶颈基本在协议栈和应用层而不是MAC和PHY。如果你达不到这个水平多半是驱动里还有优化空间。5. 一个小技巧用Loopback模式快速验证链路最后分享一个调试时特别实用的方法。LAN8720A支持数字环回模式配置寄存器0的bit14为1PHY就会把发送的数据直接回环到接收路径不需要外部网线。这个功能在做板级验证时非常有用——新板子贴片回来可以先不进操作系统写个最简单的裸机程序初始化PHY后开环回然后在MAC层发送一包数据看能不能在接收描述符里找到回环的数据包。能收到说明从STM32到PHY这一整条RMII通路是正常的收不到就缩小范围排查是MAC没发出去还是PHY没回环成功还是接收路径有问题。我通常会在lan8720A_Init()里加一个环回测试函数初始化完成后自动执行执行结果通过串口打印。这样每次上电开发者一眼就能知道硬件是否正常再去跑协议栈心里有底得多。等正式发布版本时再把环回测试这段代码取消编译。以太网通信这块硬件和软件是紧密咬合的任何一个环节的疏忽都会让整个系统看起来像“神秘的故障”。但只要遵循从下往上的排查思路把每一层都验证扎实STM32的以太网远没有传说的那么难调。照着上面的流程做一遍你的板子大概率能一次点亮网口。