RTL8201F以太网PHY调试实战:从MDIO读写到LWIP移植 干嵌入式这行的谁没被网口折磨过。你画好了板子焊上PHY满怀期待地把程序烧进去结果ping不通、寄存器读出来全是0xFF、Link状态死活起不来一通排查下来发现手册里早就写了只是你当时没细看。RTL8201F这颗10/100M以太网PHY在国产工业板、通信模块上用得特别多价格便宜、供货稳但它的坑也相当典型参考电路怎么抄、RMII时钟怎么给、PHY地址到底是几、寄存器该怎么读每一个环节都能卡住新手好几天。这篇内容我按自己实际调板子的流程来写从芯片手册怎么抓重点到MDIO读写验证再到手写PHY初始化最后完整接进LWIP协议栈。不管你是刚拿到一块带RTL8201F的开发板还是自己画的板子死活不通照着这套方法走一遍大概率能把问题定位到具体环节。1. 认识RTL8201F芯片手册应该怎么看很多人拿到几百页的芯片手册习惯从头翻我建议反过来先看引脚定义、参考电路和寄存器表再回过头看电气参数。PHY芯片本质上是MAC和网线之间的“翻译官”它不关心IP和TCP只负责把数字信号调制成差分信号发到网线上同时把收到的模拟信号解调成数字信号。所以你的第一直觉应该是“这芯片跟MCU之间怎么通信”而不是“它的DSP怎么实现的”。1.1 芯片定位与关键参数RTL8201F是瑞昱出品的一款单端口10/100M以太网PHY支持MII和RMII两种MAC接口内核电压通常工作在3.3VI/O可以直接兼容3.3V逻辑。对我做嵌入式ARM板来说最关心的是RMII模式因为只需要TXD[1:0]、RXD[1:0]、TX_EN、CRS_DV、REF_CLK这几根线比MII的16根数据线省太多IO资源而且STM32F4/H7系列千兆MAC里面的RMII控制器正好匹配。这颗芯片内部集成了均衡器、基带处理、编解码等模块但软件上你完全不用关心。真正要看的是三块内容第一是MDC/MDIO管理接口这是CPU和PHY通信的唯一通道第二是状态指示引脚比如Link/Act LED调试的时候看灯比看代码更直观第三是控制寄存器尤其是寄存器0控制、寄存器1状态、寄存器2/3芯片ID这几个是任何PHY芯片的通用基础。1.2 引脚定义与参考电路解读手册里会给你一张很详细的应用电路图不要直接抄你得先确认有没有“隐藏条件”。RTL8201F的RMII参考电路里25MHz晶振接XI/XO然后PHY内部生成50MHz的REF_CLK输出给MAC。如果你的MCU要求外部输入50MHz参考时钟一定要选支持“REF_CLK Out”模式的接法反过来如果MCU自己提供50MHz时钟PHY要配置成“REF_CLK In”模式否则两边时钟对不上数据眼图全乱。别小看这个时钟方向的差别。我见过好几个板子把PHY的CLK_REF引脚空着结果STM32侧RMII参考时钟没有输入程序怎么跑都是错。正确做法是在芯片手册里找到CLK_REF或REF_CLK相关引脚根据你的MCU规格书里ETH_RMII_REF_CLK的输入输出要求来决定。绝大多数STM32系列用的是PHY输出50MHz给MAC所以RTL8201F的接法就是让它自己出时钟MCU那边只配置RMII即可。1.3 寄存器基础与MDIO接口MDIO是IEEE 802.3定义的管理接口一共两根线MDC是管理时钟MDIO是双向数据线。每次操作发送一个帧里面包含读/写标志、PHY地址、寄存器地址和16位数据。RTL8201F的PHY地址由PHYAD引脚的电平决定手册会给出默认值有些板子把PHYAD全部接地地址就是0x00有些通过上拉电阻设成0x01。所以你拿到一块板子第一件事不是查代码而是看原理图上PHYAD引脚的接法。我建议你在初始化驱动里做一个“PHY地址扫描”从0扫到31读到芯片ID合法的那个就是PHY地址。这样即使原理图看漏了程序也能自动找到。寄存器方面寄存器0叫BMCR负责软复位、速度、双工、自动协商开关寄存器1叫BMSR里面最核心的位是bit 2表示Link状态bit 5表示自动协商完成。调试Link问题先读这两个寄存器就够了。寄存器2和3是厂家IDRTL8201F读出来有特定值可以用来确认MDIO通信是真通还是假通。2. 硬件电路设计把参考电路变成能跑通的板子你说自己做板子用官方参考电路改的为什么还是出问题因为参考电路只是一个“最小可用方案”在真实环境里电源纹波、时钟走线、地板回流都会影响PHY的工作。这个环节我不是教你怎么画PCB而是告诉你哪些点是后续软件排查的“重灾区”硬件上忽略它们后面有你折腾的。2.1 时钟25MHz晶体与50MHz RMII参考时钟RTL8201F的时钟有两种来源方式一是外部25MHz晶体二是外部直接注入25MHz或50MHz时钟。在RMII模式下MAC侧需要50MHz参考时钟这个时钟从哪里来必须要在硬件设计时定死不能留到软件里翻。通常做法是给PHY接25MHz无源晶体PHY内部锁相环输出50MHz REF_CLK给MCU。这里有个常见误区有人用STM32的MCO输出25MHz给PHY然后在RMII模式下让MAC直接使用PHY的50MHz这种做法并不是所有板子都能稳定工作因为MCO输出的信号质量和专用晶体比还是有差距的。如果PCB布线空间允许尽量用无源晶体方案少用一个MCU引脚信号更稳定。千万别忘了RMII接口有一个“收发同步”要求REF_CLK必须同源或者保证两边的频率完全一致。只要时钟源不同或者相位偏差过大调试时会看到能读到PHY寄存器但收发数据全错这属于最难排查的一类问题。2.2 复位、PHY地址与RMII控制引脚RTL8201F的复位脚是低有效。很多MCU的ETH复位并没有严格时序要求但踩过坑的人都知道如果复位脉宽不够或者复位释放后立刻去读MDIOPHY可能还没准备好就会读到全0xFF。所以驱动里上电后至少要延时10ms再开始MDIO操作如果用的是MCU的GPIO控制复位程序中先拉低、延时再拉高之后再等一下。RMII模式下还有个引脚叫PHYAD它的电平不仅决定PHY地址有些PHY还会复用做其他功能。RTL8201F设计上要仔细看手册里PHYAD[4:0]的默认值和内部上拉/下拉。如果你板子上PHYAD引脚悬空很容易得到非预期地址这也就是为什么要做地址扫描。RMII的控制引脚里TXD0/TXD1、TX_EN、RXD0/RXD1、CRS_DV、REF_CLK这7根线在MCU和PHY之间必须等长走线尤其是REF_CLK它作为同步时钟如果比数据线长太多会直接导致采样时序不正确。我甚至遇到过只在低温下丢包的板子最后查出来就是REF_CLK走线绕了一圈温度一低信号边缘变差TIMING就不够了。2.3 电源滤波与PCB走线PHY芯片的电源纹波直接影响信号质量尤其是网络变压器到PHY的这一段。RTL8201F的VDD各个引脚旁边必须放0.1uF陶瓷电容靠近引脚放置如果有1.8V或2.5V内部稳压输出脚也别忘了对应电容。我在实际调试中多次遇到“Link时好时坏”的板子示波器抓电源确实没大纹波但用近场探头一放发现PHY芯片上方有很强的时钟辐射最后是在CLK_REF输出串了一个22欧姆电阻解决。网络变压器到RJ45之间是差分走线阻抗控制在100欧姆左右PHY侧到变压器之间也是差分对但距离越短越好。如果风枪焊盘间距太近相邻引脚粘连或者沾锡板上可能没短路但MDIO信号被干扰这种现象可以通过读寄存器恢复出正确ID来判断不过能读到ID不代表硬件没问题。3. 驱动移植前的准备先把PHY点亮在碰LWIP之前我强烈建议你先做一个极小的测试工程串口打印MDIO读写延时把PHY当作一个普通I2C类的芯片来调试。这一步能排除八成的问题而且逻辑极简出错也好定位。很多人直接把ST官方的LWIP例程改一改烧进去出了问题不知道是PHY没起来还是MAC没配置对最后谁也救不了你。3.1 用串口打印日志搭一个最小调试框架最简单的方式就是用STM32CubeMX生成一个带串口和GPIO的基础工程然后把ETH的引脚在代码里手动初始化或者干脆用CubeMX把ETH外设也一起开起来但不要勾选LWIP中间件。这样你得到的是一个可以点灯、打串口的裸机工程先把RCC时钟、GPIO复用配置好。串口日志建议做一个带时间戳或者步进序号的小宏例如在每一步操作前后打印“step 1: reset PHY”和结果。别觉得土实际调试时你靠的就是这些日志判断卡在哪一步。如果你要显示结构体变量在Keil调试模式下把变量拖进Watch窗口但要确认当前编译优化级别和变量作用域否则看到的值会是“optimized away”容易被误导。这个最小工程我会编译下载后先串口打印“PHY test start”然后一颗LED每隔500ms翻转。如果你连LED都不闪那就先把基础工程调通再谈PHY。好的假设你的基础工程没问题下一步就是MDIO读写。3.2 实现MDIO读写并扫描PHY地址在STM32的HAL库中ETH外设提供了两个现成的函数HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister。它们的本质是通过ETH_MAC_MDIO寄存器配置时钟分频然后发起MDIO读写时序。注意HAL_ETH_ReadPHYRegister的第一个参数是句柄第二个参数是PHY地址第三个是寄存器地址。你可以这样扫描PHYADuint16_t id1, id2; uint8_t addr; for (addr 0; addr 32; addr) { if (HAL_ETH_ReadPHYRegister(heth, addr, 2, id1) HAL_OK HAL_ETH_ReadPHYRegister(heth, addr, 3, id2) HAL_OK) { if (id1 ! 0xFFFF id1 ! 0x0000) { printf(found phy at addr %d, ID10x%04X, ID20x%04X\r\n, addr, id1, id2); } } }如果所有地址读出来都是0xFFFF先查硬件MDC/MDIO有没有接反复位脚有没有被拉低PHY供电是不是正常。如果只有特定地址读出非FF那就记下这个地址后续LWIP配置就填它。读出来的ID1和ID2和手册值对比一下完全一致说明PHY访问链路是通的这是你后续所有调试的基础。3.3 验证基本寄存器芯片ID与模式寄存器RTL8201F的ID通常可以在寄存器2和3里读到但不是所有版本的手册都会写得很直白。你可以这样判断读到的ID值既不是0xFFFF也不是0x0000并且重复多次读结果稳定基本可以认定PHY回应你了。如果ID值偶尔变化有可能是MDIO信号质量差或者电源不稳定。接下来读寄存器0和1。寄存器0的默认值一般是0x1000也就是开启了自动协商但还没完成。我们手动设成0x3000或者0x3100开启自动协商并允许100M和10M全双工/半双工。然后反复读寄存器1看bit 5有没有从0变1bit 2有没有从0变1。Bit 5是自动协商完成bit 2是Link状态。这一步花不了多少时间但它决定了你后续LWIP能不能直接跑通。如果Link状态一直起不来别急着写网络协议栈先把自动协商搞定。4. 手写PHY初始化从软复位到Link Up当你已经能用MDIO读到ID并且稳定时就可以把PHY初始化写成一个模块了。这一节我给出一个“极简但完整”的PHY初始化序列它不依赖具体MAC库只通过MDIO操作寄存器。看懂这个序列你再去套ST的Ethernet_Link_Init或者LAN8720A的驱动就很容易了。4.1 软复位与基本配置寄存器第一步是软复位。往寄存器0写0x8000然后再读寄存器0直到bit 15清0。注意有些芯片的软复位需要一点时间循环里加个延时比如每次循环delay 10毫秒最多等2秒超时就打印错误。uint16_t bmcr; HAL_ETH_WritePHYRegister(heth, phy_addr, 0, 0x8000); for (int i 0; i 200; i) { HAL_ETH_ReadPHYRegister(heth, phy_addr, 0, bmcr); if ((bmcr 0x8000) 0) break; HAL_Delay(10); }复位以后不要急着配置速度。先设置自动协商使能同时设置默认速度为100M全双工作为回退保障HAL_ETH_WritePHYRegister(heth, phy_addr, 0, 0x1000); /* 自动协商使能 */如果希望强制100M全双工直接把寄存器0写成0x2100。但作为首次调试我建议先用自动协商模式因为网线另一头可能是交换机可能是电脑强制模式很容易不通。4.2 状态轮询与Link Status判断PHY初始化不只是“写几个寄存器”你要等它自己完成协商。最朴素的做法是周期读寄存器1观察bit 2。我一般会写一个函数int phy_wait_link(uint8_t addr, uint32_t timeout_ms) { uint16_t bmsr; uint32_t start HAL_GetTick(); do { HAL_ETH_ReadPHYRegister(heth, addr, 1, bmsr); if (bmsr 0x0004) return 0; /* Link Up */ HAL_Delay(10); } while (HAL_GetTick() - start timeout_ms); return -1; }Link Up之后再读寄存器4和5可以看到本端和协商对方的能力。如果bit 5是1说明自动协商完成链路的速率和双工模式已经确定了。此时再读寄存器0bit 13表示速度bit 8表示双工状态你就能确认它协商到了100M全双工。我在实际项目里会把“由于网线没插”、“对方不是千兆口”、“线序错误”这类原因都归结到这个阶段。你只要打印出BMSR和BMCR的值基本就能推断出物理链路是否健康。4.3 RTL8201F的特有寄存器与百兆模式确认除了标准0到6号寄存器RTL8201F还有一些扩展寄存器用于省电、环回、中断状态等。第一次调试不建议碰它们重点确认默认上电状态就是正常工作模式。有些PHY上电后会进入isolated模式这时RX信号与MAC断开Link状态依然可以起来但网卡MAC收不到包。这种情况比较诡异建议直接在寄存器0里把bit 10清0并且确认bit 6为0不是power down。如果你想测试PHY内部是否正常可以开启环回寄存器0 bit 14写1这样PHY会把发出去的数据直接收回来。你可以在不插网线的情况下做MAC环回测试。但注意这跟实际通信还不同别拿环回测试的结果当成“网口已经通了”的依据。5. 把PHY接进LWIP驱动ethernetif移植细节PHY已经Link Up了接下来才是真正的“驱动移植”把LWIP跑起来。LWIP要正常工作底层网卡驱动必须提供几个能力初始化硬件、发送一个数据包、接收一个数据包、处理中断和轮询状态。STM32的HAL库已经把MAC和DMA这部分做了封装你要做的是补上PHY地址、复位时序、资源申请和中断入口。5.1 lwIP需要网卡提供什么LWIP有一套netif接口每个网络接口对应一个struct netif。你要实现的最底层函数是static err_t low_level_init(struct netif *netif); static err_t low_level_output(struct netif *netif, struct pbuf *p); static void low_level_input(struct netif *netif);low_level_init里要做的事情包括设置MAC地址、配置DMA描述符、初始化PHY、标记netif为UP。low_level_output负责把LWIP的pbuf链表里的数据拷贝到DMA发送缓冲区或者利用零拷贝把描述符指到pbuf数据区最后触发发送。low_level_input则是在收到数据包后把DMA收到的数据组成pbuf上报到LWIP。如果你用STM32CubeMX生成LWIP中间件它会自动生成ethernetif.c大部分逻辑已经写好了。你真正要改的只有三个地方PHY地址、PHY初始化时需要的延时、以及是否需要处理额外中断。CubeMX生成的代码默认使用LAN8720A的寄存器定义把那些宏改成RTL8201F对应的地址即可或者干脆删掉用我们自己确认过的寄存器值。5.2 基于HAL库的ETH初始化和中断在裸机HAL工程里ETH初始化由HAL_ETH_Init完成它会读MAC地址、设置RMII/MII模式、设置DMA描述符。这个函数调用前你需要先填充ETH_InitTypeDef结构体包括PhyAddress、MediaInterface、AutoNegotiation、DuplexMode和Speed。CubeMX生成的代码会在SystemInit阶段调用HAL_ETH_Init但PHY靠的是外部引脚复位所以你在low_level_init里要先把PHY复位拉起来然后调用HAL_ETH_Init再调用前面写的phy_init和phy_wait_link。顺序错了很常见先初始化PHY和先初始化MAC没有绝对先后但一定要保证PHY已经能正常响应MDIO了再让MAC进入Link状态。中断处理上STM32的ETH有两个中断号ETH和ETH_WKUP。普通项目中你只需要接收DMA中断。在中断服务函数里清除标志位然后调用HAL_ETH_IRQHandlerHAL库内部会帮你清理挂起的DMA中断。如果是裸机轮询可以在主循环里周期调用HAL_ETH_GetReceivedFrame_IT然后处理low_level_input但轮询效率低建议还是开中断。5.3 low_level_output与low_level_input实现要点low_level_output的核心是遍历pbuf链表把数据按32位对齐拷贝到发送缓冲区。很多初学者直接memcpy一次性拷贝整个pbuf没问题但要注意LWIP的pbuf可能是非连续的不能用pbuf-payload直接作为起始地址拷整个长度得手动遍历。简单示例size_t framelen 0; for (struct pbuf *q p; q ! NULL; q q-next) { memcpy((uint8_t *)buff[framelen], q-payload, q-len); framelen q-len; }拷贝完成后把帧长写到DMA描述符的controlBufferSize里更新描述符状态为ETH_DMATXDESC_FIRST_SEGMENT和ETH_DMATXDESC_LAST_SEGMENT然后触发发送。接收方向low_level_input用DMA描述符里的status判断是否收到完整帧取出长度后分配一个pbuf用pbuf_alloc和pbuf_take把数据从DMA缓冲区复制到pbuf然后netif-input(pbuf, netif)把数据交给上层协议栈。这里最容易出错的是DMA描述符数量不够导致接收流程卡死。我习惯把接收描述符数量设成8个起发送描述符4个起。描述符不足时接收中断会反复触发但取不出数据看起来就像是网络卡死实际是描述符没及时回填。6. 常见问题与排查技巧最后把我在实际项目里踩过、帮别人排查过的典型问题整理成一张速查表。这些问题非常有共性哪怕你用的是别的PHY芯片排查思路也一样只是寄存器地址和位定义可能略有不同。6.1 MDIO读写异常速查表现象可能原因排查步骤所有PHY地址读出来都是0xFFFFMDIO线接反/虚焊PHY没上电复位拉住用万用表测PHY引脚电压示波器抓MDC波形只有某个地址能读到非FFPHYAD引脚电平导致地址偏移对照原理图确认PHY地址驱动里扫描0~31读出的ID值偶尔变化MDIO信号质量差电源纹波大降低MDC时钟频率检查MDIO上拉电阻补滤波电容寄存器能读但不能写MAC侧没有正确配置MDC时钟分频计算AHB时钟和MDC时钟比例HAL内设置正确分频我调试时会在主循环里周期性打印寄存器1的值观察Link状态是否来回跳变。如果Link状态一直跳多半是变压器中心抽头偏置不对、网口隔离没做好或者差分对走线太长导致眼图质量差。6.2 Link Up但网络不通Link Up只代表物理层通了不代表协议栈通。看到Link灯亮但ping不通先按顺序检查先看电脑网卡IP和设备IP是否在同一网段再查MAC地址是否冲突最后抓包看ARP有没有响应。如果ARP响应有但Ping一直超时多半是LWIP层的接收路径有问题。一个典型的坑是netif-flags里没设置NETIF_FLAG_ETHARP和NETIF_FLAG_UP或者没调用netif_set_link_up。很多从旧版LWIP移植的代码会因为宏定义变化在编译时直接通过但运行时逻辑完全错误。你可以在日志里加入网卡状态打印确认netif_is_up返回真。如果确认网卡状态正常但收不到包用Wireshark在电脑上抓包看是否发出来但PC没收到还是PC发了但板子没回应。这样可以快速把问题隔离到发送路径还是接收路径。6.3 低吞吐率与丢包吞吐率低往往不是PHY的问题而是DMA描述符和内存碎片。LWIP的PBUF_POOL_SIZE配置太小或者PBUF_POOL_BUFSIZE填的太大都会导致接收时无法申请pbuf然后DMA缓冲区被覆盖整包数据丢得莫名其妙。如果你发现UDP测试经常丢包先把LWIP的内存池调大尤其是PBUF_POOL。再检查ETH DMA的中断优先级接收中断优先级低于其他中断时频繁打断会导致DMA接收描述符耗尽。STM32的ETH中断优先级我习惯配成比SysTick低、但比普通外设高不能设成最低否则高负载下会丢帧。还有一个被忽略的点是发送描述符的TxBufferSize字段必须按描述符格式写高位表示描述符链的segment标志低位才是数据长度。一字节写错轻则发送不出去重则DMA总线错误触发hardfault。调试时可以在发送描述符回调里打印状态寄存器的值对照参考手册的位定义检查。最后再分享一个小技巧真正调通之后别忘了把PHY地址和寄存器配置整理成头文件而不是散落在主程序里。因为大多数项目不是只有一块板当你从评估板换到自己量产的板卡时PHY地址、复位脚、参考时钟方向这些都会变有一个集中管理的地方移植工作量会小很多。我自己的习惯是会把整个过程拆成三段第一段验证MDIO可读ID第二段验证Link Up第三段才跑LWIP。每一段都有一个独立的小工程互不依赖。这样如果哪一天项目又出问题我能快速定位是硬件还是软件、是物理层还是协议栈而不用把整套代码重新在线调试一遍。调试RTL8201F这件事最忌讳的就是“一把梭”把PHY和协议栈一起调到最后出了问题谁也救不了你。按这个流程来网口调通只是时间问题。