前几天一个朋友在群里问:RTL8201F在STM32F407上死活ping不通,MDIO读回来的PHY ID全是0xFFFF。这个场景我太熟悉了,前两年调这块芯片时,光MDIO不通就卡了差不多两天。今天干脆把从芯片手册解读到LWIP驱动移植的完整思路写出来,顺便把那些藏在“正常操作”背后的坑都摊开讲。
RTL8201F是一颗非常典型的10/100M以太网PHY芯片,瑞昱家的,常见于工业控制板、电池管理主控、物联网网关这种要“上网”但成本敏感的嵌入式产品里。它和STM32的ETH外设配合非常常见,但恰恰因为“看起来很好调”,很多人上来就照着别人的代码抄,结果连寄存器都读不通。这篇文章适合正在调RTL8201F的硬件工程师、嵌入式软件工程师,也适合第一次碰PHY芯片、不知道从哪下手的朋友。我会从芯片手册怎么读开始,一直讲到MDIO硬件调试、自协商状态判断、STM32CubeMX生成代码的适配,以及LWIP挂上之后怎么验证。全程按我自己实际调试时的顺序来写,不是理论复述。
1. 为什么是RTL8201F:这块PHY能做什么、常被用在哪儿
先说清楚PHY在以太网链路里的位置。以太网通信分成两层来看:MAC(媒体访问控制)负责组帧、寻址、CRC校验,STM32内部继承了ETH外设,干的就是这个活;PHY则是物理层收发器,负责把MAC发过来的并行数据变成差分模拟信号送到网线上,同时把网线收到模拟信号解调成数字信号交还给MAC。RTL8201F就是连接STM32 MAC和RJ45插座之间的那座桥。
RTL8201F 支持MII和RMII两种MAC接口。MII需要7根数据线加时钟,速率10/100M自适应;RMII把数据线砍到4根,但需要一个50MHz参考时钟。STM32F407、F429这类芯片的ETH外设两种接口都支持,CubeMX里可以直接选。实际项目里绝大多数人图省引脚选RMII,RTL8201F在RMII模式下也跑得挺稳,关键是要把CLK来源理清楚:RMII的REF_CLK由谁提供,是MAC出,还是PHY出,还是外部有源晶振直接供?这个在后面的硬件调试章节我会专门讲。
相比大家更熟悉的LAN8720A,RTL8201F 的优势是封装选择多、供货稳定,而且 MDIO 寄存器实现比较标准,调试起来没那么“妖”。它内部也集成了 LDO,单 3.3V 供电就能跑,外围器件主要是网络变压器和几个去耦电容。真正让很多人卡住的反而不是芯片本身,而是三个点:一是 PHY 地址没对上,二是 MDIO 时序不对导致读寄存器全是 0xFF,三是 LwIP 移植后不知道如何判断 link up/link down。这三个问题,本质上都能通过手册和寄存器值定位。
需要说明的是,RTL8201F 和常见的 PHY 一样,遵循 IEEE 802.3 定义的基本寄存器结构,但也保留了不少厂商私有扩展寄存器。这些扩展寄存器在自协商、省电、中断配置时可能会用到,所以芯片手册别只翻基础部分,后面的寄存器描述页也得看。
2. 芯片手册解读顺序:先看引脚再看寄存器,别一上来就翻移植代码
我见过不少人拿到一颗PHY,第一件事是打开别人的STM32工程复制粘贴。这个做法风险很高,因为PHY的引脚定义、PHY地址配置、参考时钟要求都直接影响硬件和软件行为。手册解读的顺序应该是:引脚定义、参考电路、寄存器空间、时序参数,最后才是采样代码。
2.1 引脚定义与参考电路图才是硬件调试的第一手资料
RTL8201F 的引脚大致分几组:
- MAC接口组:TXD[3:0]、TX_EN、TX_CLK、RXD[3:0]、RX_DV、RX_CLK、CRS、COL。MII模式全用,RMII模式只用TXD[1:0]、TX_EN、RXD[1:0]、RX_DV,外加50MHz REF_CLK。
- 控制组:MDC、MDIO、PHYAD[4:0]。PHYAD引脚用来配置 PHY 地址,一般4位做地址、1位做速率或模式选择,具体要看手册。
- 时钟组:XI/XO,外接25MHz无源晶振,也可以通过XI直接输入时钟。
- 指示组:LED_0/LED_1/LED_2,用来指示link状态、收发活动、速率。
- 电源和地:AVDD、DVDD、AVDD_25、数字IO电源等。
调试时首先要确认的,就是PHYAD[4:0] 的具体接法。这颗芯片的PHY地址不是软件写死的,而是上电复位时通过PHYAD引脚的电平锁存的。很多STM32开发板把LAN8720A 的 PHY 地址设为0x00,RTL8201F 的参考设计则可能把地址设成0x01或者0x04。如果你照着某个开发板的代码读0x01,而你的硬件实际是0x00,那么MDIO读出来的寄存器全部是0xFFFF,因为总线上根本没设备应答。
再一个容易被坑的点是LED引脚的多功能复用。某些PHY的LED引脚在复位后同时承担配置功能,比如LED_2可能用来选择MII/RMII模式,或者选择是否从晶体输入。 RTL8201F 的参考电路图里通常会有具体的“NC”或”L”处理说明,硬件工程师画板子前就应该核对。
推荐做法:拿到硬件原理图,先画一张引脚清单,把PHYAD、MDC/MDIO、RMII/MII接口信号、LED、复位引脚全部列出来,标记连接到STM32的哪个引脚。之后在调试软件里遇到问题,先回到这张表核对,往往能省出大半天时间。
2.2 基础寄存器与扩展寄存器:该记住的地址就那几个
MDIO总线通过MDC时钟,访问PHY内部寄存器,地址空间0到31,其中0到6是IEEE 802.3定义的标准寄存器,7到31是厂商自定义寄存器。调试RTL8201F初期,真正要记住的核心寄存器其实不超过6个。
| 寄存器地址 | 名称 | 关键位 | 调试中的作用 |
|---|---|---|---|
| 0x00 | BMCR(基本模式控制) | Bit15软复位、Bit12自协商使能、Bit13速率、Bit8全双工 | 配置PHY工作模式,软复位后确认Bit15是否自清零 |
| 0x01 | BMSR(基本模式状态) | Bit2链接状态、Bit5自协商完成、Bit0~Bit4能力 | 判断link是否建立、自协商是否完成 |
| 0x02 | PHYIDR1(PHY ID高16位) | OUI | 验证MDIO通信是否正常 |
| 0x03 | PHYIDR2(PHY ID低16位) | OUI+厂商型号修订 | 验证MDIO通信是否正常 |
| 0x04 | ANAR(自协商广播) | 速率/半全双工能力 | 确认PHY向外广播的能力集 |
| 0x05 | ANLPAR(链路伙伴能力) | 对端能力 | 确认对端网卡/交换机支持什么速率 |
调试时最常用的手法是:上电后先读0x02和0x03,确认PHY ID。只要这两个寄存器能读出稳定值,就说明MDIO通信链路已经通了,剩下的事情基本都在寄存器级。读0x01的Bit2看链接状态,读Bit5看自协商是否完成;如果没有link,再读0x04和0x05,对比本地和对端能力,判断是速率不匹配还是对方没开机。
RTL8201F 的寄存器扩展机制很多,比如通过寄存器0x1F选择页、寄存器0x1E写数据,和瑞昱其他PHY类似。这些高级功能在常规调试中不常用,真要用到的时候再翻手册对应页。有一点要注意,不少PHY寄存器里的状态位是“锁存”的,比如链接状态发生变化后Bit2会一直保持低电平,即使恢复链接也不自动变高,必须通过读取一次寄存器或执行软件复位来清除。这一点经常让人误判“Link没起来”,实际上网线早就插好了。
3. 硬件调试环节:MDIO总线读写不通时,我踩过的坑
MDIO是PHY调试的“第一道门”。门都进不去,后面寄存器和LWIP全都白搭。这个环节的问题通常表现为:用读取函数读PHY ID,返回0xFFFF,或者返回0x0000。前者说明总线没设备应答,后者可能是时序或者地址溢出问题。
3.1 MDC/MDIO时序与上拉电阻:为什么读出来全是0xFF
MDC由MAC主机输出,是时钟;MDIO是双向数据线。在MDC上升沿,PHY锁存数据;在MDC下降沿附近,数据切换。通信帧由主机发起,包括一段32bit的1作为前导码(Preamble),然后是控制码、PHY地址、寄存器地址和读写位。
读操作时,主机输出前导码、控制码和地址后,需要在TA(Turnaround)阶段将MDIO引脚转为输入,等待PHY驱动数据。如果硬件上MDIO开漏没有上拉电阻,或者引脚配置不对,高电平就浮空,读到的一串数据很可能全是0xFF。
排查思路按下面顺序走:
- 用万用表或示波器确认PHY的3.3V和内部LDO电源已经建立,芯片没有进入复位状态。很多设计直接用RC复位,上电后如果MCU跑得太快,PHY还没准备好,MDIO就永远读不到值。
- 检查MDC和MDIO是否漏接了上拉电阻。MDIO一般要求4.7k到10k欧姆上拉到3.3V,MDC通常也有上拉要求,但具体看手册。没有上拉,通信稳定性会很差。
- 示波器抓MDC和MDIO的波形。看MDC是不是有规则脉冲,MDIO在MDC上升沿时是不是稳定有效。如果MDIO没有波形,多半是软件层根本没发起读写,或者引脚复用配置被CubeMX覆盖了。
- 确认PHY地址。把MDC/MDIO连续读几个地址,如果只有某个PHYAD能读到有效ID,说明地址配置和原理图不完全一致。可以用枚举方式,从0到31逐个读,很多人这么干,见效快。
还有一个比较隐蔽的坑是MDC频率太高。MDC的最高频率受PHY限定,RTL8201F一般支持最高25MHz,但STM32 HAL的默认初始化可能会配置到某个分频值。如果时钟过快且PCB走线较长,信号完整性不好,读寄存器就会出现随机错误,时好时坏。这种情况下把MDC频率降下来,问题立刻消失。
3.2 用GPIO模拟MDIO协议,把读寄存器变成一行一行能看懂的打印
很多调试环境里,HAL库的PHY读函数出了问题,你根本不知道它是卡在时序上还是卡在地址上。这时候我习惯的做法是直接用两个GPIO手动模拟MDIO协议,把每一位都拉出来打印。这个方法看起来很原始,但排查问题非常有效。
下面这段代码的思路是:把MDC对应的GPIO配置为推挽输出,把MDIO对应的GPIO配置为开漏输出,读数据时切换为输入模式读取,然后打印每一位值。
#define MDC_PIN GPIO_PIN_1 #define MDC_PORT GPIOA #define MDIO_PIN GPIO_PIN_2 #define MDIO_PORT GPIOA static void mdio_clk(void) { HAL_GPIO_WritePin(MDC_PORT, MDC_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(MDC_PORT, MDC_PIN, GPIO_PIN_SET); HAL_Delay(1); } static void mdio_write_bit(uint8_t bit) { HAL_GPIO_WritePin(MDIO_PORT, MDIO_PIN, bit ? GPIO_PIN_SET : GPIO_PIN_RESET); mdio_clk(); } static uint8_t mdio_read_bit(void) { GPIO_InitTypeDef gpio = {0}; gpio.Pin = MDIO_PIN; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(MDIO_PORT, &gpio); HAL_GPIO_WritePin(MDC_PORT, MDC_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(MDC_PORT, MDC_PIN, GPIO_PIN_SET); HAL_Delay(1); uint8_t bit = HAL_GPIO_ReadPin(MDIO_PORT, MDIO_PIN); // 恢复为开漏输出 gpio.Mode = GPIO_MODE_OUTPUT_OD; HAL_GPIO_Init(MDIO_PORT, &gpio); return bit; } static void mdio_preamble(void) { for (int i = 0; i < 32; i++) { mdio_write_bit(1); } } static uint16_t mdio_read_reg(uint8_t phyaddr, uint8_t regaddr) { mdio_preamble(); // 读操作控制码: 01 mdio_write_bit(0); mdio_write_bit(1); // PHY 地址 5 bit for (int i = 4; i >= 0; i--) { mdio_write_bit((phyaddr >> i) & 1); } // 寄存器地址 5 bit for (int i = 4; i >= 0; i--) { mdio_write_bit((regaddr >> i) & 1); } // TA: 读取时先释放总线,由PHY驱动 // 这里的实现是读一位Z状态,再做第二个时钟读取有效数据 // 实际调试时可以忽略第一个Z位 mdio_read_bit(); uint16_t value = 0; for (int i = 15; i >= 0; i--) { value |= ((uint16_t)mdio_read_bit() << i); } return value; }注意,这段代码只是个调试原型,不是正式驱动。它的好处是精简到每一步都可以加串口打印,一旦读到某个bit异常,你能立刻知道是PHY没应答,还是时序不对。我还会配合串口调试助手,用printf把读到的寄存器值打出来,一边看波形一边看打印,比盲目信任库函数直观得多。
4. 从寄存器值到link up:复位时序、自协商与LED状态机的观察
MDIO能读到PHY ID之后,下一步就是让PHY建立物理链路。这个阶段最容易出问题的不是寄存器配置本身,而是复位时序和自协商机制的理解不到位。很多人上电后立刻读link状态,读不到就以为PHY坏了,其实只是时序没等够。
4.1 复位时序到底该延时多久
RTL8201F支持硬件复位和软件复位。硬件复位引脚低有效,复位脉宽不能太短,工业级PHY一般要求至少10ms,有些资料甚至建议拉低20ms以上。硬件复位完成之后,芯片内部还要做PLL锁定、校准等动作,所以实际等待时间最好给到50ms到100ms再开始访问寄存器。
软件复位是通过写BMCR寄存器的Bit15为1实现的,写完后该位会自动清零,表示复位完成。需要注意的是,软复位期间不能对寄存器做其他读写,否则可能产生不可预期行为。我一般会写个循环,读BMCR等到Bit15变成0,再延时20ms,然后再读取PHY ID和状态。
上电瞬间的问题更常见。很多板子的PHY复位引脚由RC电路控制,有的甚至直接连到MCU的GPIO上。如果MCU在PHY之前已经跑起来,GPIO默认输出低电平,把PHY按在复位状态,那后面怎么读都读不到。还有的设计把复位引脚悬空,靠内部上电复位,这个依赖芯片内部的复位电路,不等上电稳定就开始读写,也可能失败。
4.2 自协商失败时,怎么通过寄存器值缩小范围
自协商是PHY和对端设备通过脉冲序列交换能力信息的过程。RTL8201F默认开启自协商,如果你强制设置了速率和双工,它就不协商,直接按强制模式连接。正式调试中,建议先保持自协商,再读寄存器判断状态。
判断链路状态的步骤:
- 读BMCR,确认Bit12是否为1,即自协商使能。
- 读BMSR的Bit5,看自协商是否完成。如果Bit5为0,说明还在协商,等待后再读。
- 读BMSR的Bit2,看链接是否建立。这里的“链接”是物理层检测到对端信号并能正常交换数据。Bit2为0,就是没有link。
- 读ANAR和ANLPAR,对比双方支持的能力。如果ANLPAR是0,说明对端没有回应,可能是网线问题、对端没开机、或者对端网口自协商失败。
- 读BMCR的Bit13和Bit8,确认当前配置的速率和双工模式,再看实际协商结果是否一致。
如果自协商失败,最快速的做法是手动把PHY强制成100M全双工,看能不能link。强制方法:关闭自协商位、关闭回环、设置速率位和双工位为对应模式,然后重新初始化PHY。强制后如果能link,说明问题出在对端设备或者网线;强制后还不能link,那很大概率是硬件电路问题了。
硬件层面的常见元凶包括:网线内部线对接错、RJ45的TX/RX差分对接反、变压器中心抽头处理不当、PHY芯片到变压器之间的串阻或共模电感选错。这些不是软件寄存器能解决的,只能用前面的方法定位到“非自协商问题”,然后回到硬件去查。
板子上的LED也有用。RTL8201F的LED输出可以提供速率、link、活动三种指示。调试时把LED对应的寄存器配置好,看灯的状态和寄存器值是否一致。如果寄存器显示link up但LED不亮,先别急着怀疑LED配置,先查LED限流电阻和引脚有没有虚焊;如果LED亮但寄存器读不到link up,那基本可以断定MDIO读取出了问题。
5. STM32CubeMX下ETH外设与LWIP的配置:适配RTL8201F的驱动写法
到了这一步,MDIO能读能写、link也起来了,剩下的重头戏就是把LWIP挂到STM32上。大多数人的习惯是先用STM32CubeMX生成基础工程,然后在生成的代码上改。这也是最容易出问题的地方:CubeMX默认生成的PHY驱动并不是给RTL8201F用的。
5.1 CubeMX默认生成的PHY驱动为什么跑不起来
STM32CubeMX在启用ETH外设和LWIP时,会引用一个名为lan8742.c或者dp83848.c的PHY驱动。以常见的LAN8742为例,它内部有一个初始化函数,会读取PHY ID,并且和自定义宏做比对。如果你的硬件是RTL8201F,PHY ID肯定和LAN8742不一样,于是初始化直接返回错误,LWIP也就没法启动。
CubeMX生成的代码里,还有几处和具体PHY强相关:
stm32f4xx_hal_conf.h中的PHY_ADDRESS宏定义,比如定义成0x00,如果你硬件是0x01,所有HAL层PHY读写都会落空。stm32f4xx_hal_eth.c中的HAL_ETH_Init会调用HAL_ETH_ReadPHYRegister,如果PHY ID校验失败,返回错误。ethernetif.c中的low_level_init会配置MAC地址、DMA描述符等,并调用PHY初始化。
解决思路不是去改lan8742.c适配RTL8201F,而是把PHY驱动“降级”成最简单可靠的寄存器读写接口,直接交给LWIP的底层接口去调用。因为LWIP根本不关心PHY芯片型号,它只关心底层有没有帮你把数据包收发进来、有没有正确上报link状态。
5.2 PHY地址、常用寄存器重映射与lwip底层接口的对接
先要确认硬件上的PHY地址,然后修改宏定义。假设RTL8201F的PHYAD配置为0x00,则:
#define PHY_ADDRESS 0x00接着在ethernetif.c里,去掉对LAN8742驱动库的依赖,直接使用HAL的PHY读写接口封装成自己的函数:
static uint16_t phy_read(uint16_t reg) { return HAL_ETH_ReadPHYRegister(&heth, PHY_ADDRESS, reg); } static void phy_write(uint16_t reg, uint16_t value) { HAL_ETH_WritePHYRegister(&heth, PHY_ADDRESS, reg, value); }RTL8201F的寄存器0和1能提供绝大部分状态。low_level_init里可以这样处理link状态:
static uint8_t ethernet_check_link(void) { uint16_t bmsr = phy_read(0x01); return (bmsr & 0x0004) ? 1 : 0; }如果要用CubeMX的MX_LWIP_Init接口,一般流程是:
- 调用
MX_ETH_Init,里面会配置PHY地址和MAC地址。 - 调用
MX_LWIP_Init,里面会注册底层收发函数。 - 在主循环里调用
lwip_pkt_handle或者ethernetif_input轮询收包。
还有一个关键点是ETH外设的DMA描述符。LWIP收发依赖DMA描述符链表,这个在CubeMX生成代码里已经初始化好了。不要自己手动改描述符数量和数据缓冲区大小,除非你清楚LWIP的内存池怎么配。改不好就会出现“能ping通但TCP很慢”或者“收发一段时间后死机”的情况。
RMII参考时钟的问题也要在这里强调。如果使用RMII,STM32的ETH外设支持三种时钟来源:外部50MHz有源晶振、PHY提供50MHz输出、MCU的MCO引脚输出50MHz。RTL8201F通常由外部25MHz晶振工作,再通过内部的RMII时钟输出引脚提供50MHz给STM32,硬件接法一定要和CubeMX选择的时钟配置一致。如果时钟不对,物理层一样是瞎的。
LWIP运行起来后,建议先关闭DHCP,使用静态IP,减小变量。先在PC上ping板卡,ping 192.168.1.10,通了之后再用网络调试助手建立一个TCP Server,板子作为TCP Client主动连过来。这种逐层验证的方法,能帮你快速区分是PHY问题、MAC/DMA问题,还是LWIP TCP协议栈配置问题。
6. 应用层调试与性能验证:串口助手、网络调试助手一起上
LWIP跑通不代表完事,接下来还有实际收发测试。很多人在这一步又会被坑:ping通了,但TCP传输不稳定;UDP能发不能收;长时间运行后网口死掉。这些问题一部分是LWIP内存和线程配置问题,另一部分其实还是PHY物理层问题。
6.1 网络不通先分四层查
遇到网络不通,我习惯按以下四层排查:
- 物理层:看PHY link状态寄存器,确认Link up,速率和双工协商正确。
- MAC层:看ETH DMA发送和接收中断计数,确认有帧从PHY收到DMA缓冲区。如果接收中断一直不触发,多半是DMA描述符没配好或者接收缓冲区地址异常。
- IP层:在板卡端打开打印,确认LWIP初始化成功,网卡IP地址设置正确,ARP能解析到PC的MAC地址。可以在PC端用arp -a看板卡IP对应的MAC是否在列表里。
- 应用层:用网络调试助手建立TCP连接,发送数据后查看板卡能否收到并回复。如果TCP握手都完成不了,回到前面三层继续查。
这里有个实用技巧:在LWIP里临时加一个UDP echo任务,板卡收到UDP包后原样返回。用PC端的网络调试助手发一串测试数据,看回包是否一致。UDP比TCP简单,不涉及连接状态,很适合拿来验证底层收发通路是否正常。
串口调试助手在整个过程中也特别有用。我习惯在板卡端把所有关键事件用串口打印出来,包括:PHY ID读取结果、link up/down事件、DHCP获取到的IP、收到的ARP请求、TCP连接建立等。这样即使手上没有网线分线器,也能通过串口日志判断问题发生在哪一层。使用串口助手时注意波特率和打印缓冲,不要在高频调试时把串口输出当成依赖,它可能会拖慢以太网中断响应。
vofa+这个上位机我也用过,它除了串口示波器功能,还能直接和调试助手配合,把寄存器变化曲线打出来。不过大部分场景下,sscom这类串口调试助手加一个网络调试助手就足够了。
6.2 温度、布局、静电:RTL8201F在真实产品中的隐蔽坑
RTL8201F在很多产品里属于“裸奔”状态——没有外壳屏蔽、没有散热处理、电源只靠几个普通电容。这种情况下一旦网络速率跑满,或者连续工作几小时,就可能出现偶发断链、收包错误增多。
我最常遇到的两个“隐蔽坑”:
第一个是隔离变压器选型不当。以太网PHY必须通过变压器连接RJ45,变压器不仅承担电平转换,还提供共模抑制和浪涌保护。如果变压器中心抽头接法错误,或者没有共模电感,长网线场景下信号质量会很差。RTL8201F参考电路里对中心抽头怎么接、串多大电阻、并多大电容都有推荐值,画板时不要随便省略。
第二个是散热与布局。RTL8201F封装比较小,底下散热焊盘一定要按要求接到地,不要只走一个过孔。PHY芯片到变压器之间差分走线要保持等长、阻抗控制,跨分割区域会严重影响信号质量。我有一次排查“百兆正常,千兆交换机互连偶尔丢包”,最后发现是PHY芯片下方地平面被一条走线切开,导致噪声耦合进接收通道。
静电防护也很重要。RJ45的金属壳接地处理、TVS管选型、对地电容位置,都影响整个系统的EMC性能。产品如果要做工业级应用,这些不上量测试根本发现不了,等到现场批量出问题再来查就晚了。
调试完RTL8201F之后,我最大的感受是:这颗芯片本身不难,难的是它和周围一大堆模拟器件共同组成了一条完整链路,PHY只是其中一个环节。遇到问题先别急着怀疑芯片,按物理层、数据链路层、协议栈的顺序一层层往下排,用寄存器值说话,比瞎猜靠谱得多。最后再分享一个小小的偏好:每个新板子第一次上电,我会先写一个只读PHY ID的裸机程序,不跑任何协议栈,就用串口打印寄存器。这一步如果能稳定通过,后面LWIP移植的绝大多数坑就已经被提前填掉了。