4. 车载以太网PHY概述

好,咱们进入第四章。这一章是动手调试PHY芯片的基本功。你想想看,PHY芯片就像个黑盒子,它为什么连不上?为什么丢包?为什么协商速率不对?

答案全在它的寄存器里。而读写这些寄存器,靠的就是MDIO/MDC总线。我刚开始做车载以太网时,总觉得这玩意儿简单,不就是两根线嘛。直到有一次在实车上死活读不到PHY的ID,排查了一整天,最后发现是MDC时钟极性反了……嗯,从那以后我再也不敢小看这条总线了。

4.1 MDIO/MDC总线协议——物理层与数据链路层的“对话通道”

MDIO(Management Data Input/Output)和MDC(Management Data Clock)是IEEE 802.3定义的管理接口。说白了,就是MAC层(或者处理器)用来读写PHY内部寄存器的专用通道。

  • MDC:时钟线,由主控端(MAC/CPU)驱动。最高频率通常为2.5MHz,但有些PHY支持到12.5MHz。
  • MDIO:数据线,双向。主控发命令时输出,PHY回复数据时输入。

协议本身不复杂,但有几个关键点我提醒你注意:

MDIO帧结构(读操作示例)

整个帧共64个时钟周期:

  • Preamble(32个1):同步头
  • Start(2位):固定为01
  • OP Code(2位):读为10,写为01
  • PHY Address(5位):支持32个PHY
  • Register Address(5位):支持32个寄存器
  • Turnaround(2位):读操作时,主控释放总线,PHY驱动数据
  • Data(16位):寄存器值

这里有个坑——Turnaround周期。我曾经遇到过一块PHY,它的Turnaround时序比标准慢了半个时钟周期,结果用通用控制器死活读不到数据。后来在示波器上抓到波形才发现,是PHY的驱动延迟偏大。解决办法?要么降MDC频率,要么在软件里加一个时钟周期的等待。

4.2 PHY寄存器地址空间——基本寄存器与扩展寄存器

IEEE 802.3定义了基本寄存器集,共32个(地址0x00~0x1F)。但说实话,这32个寄存器对于车载PHY来说远远不够。所以各厂商都扩展了自己的扩展寄存器空间。

寄存器类型 地址范围 访问方式 典型用途
基本寄存器 0x00 ~ 0x1F 直接通过MDIO帧访问 控制/状态/协商/链路检测
扩展寄存器(Page方式) 0x00 ~ 0x1F(分页) 先写Page选择寄存器,再访问 厂商特定功能:EEE、TDR、温度传感器
扩展寄存器(MMD方式) 0x0000 ~ 0xFFFF 通过MDIO帧中的DEVAD字段 10GBASE-T、车载多千兆PHY

我个人习惯,拿到一块新PHY,第一件事就是读寄存器0x02和0x03(PHY ID寄存器)。这两个寄存器共32位,包含了OUI(厂商代码)和型号版本信息。如果读出来全是0xFFFF或0x0000,那基本可以断定MDIO通信有问题。

避坑指南:我曾经调试一款国产PHY,它的基本寄存器0x00(控制寄存器)的复位值不是标准的0x3100,而是0x3000。这意味着它的自动协商默认是关闭的。如果你按标准PHY的初始化流程走,链路永远起不来。所以,永远不要假设PHY的复位状态符合标准——读一遍寄存器再说。

4.3 Linux下mdio-tools使用——调试PHY的瑞士军刀

在Linux环境下,最常用的PHY调试工具就是mdio-tools。它包含两个主要命令:mdio和mdio-raw。

安装很简单:

# 从源码编译
git clone https://github.com/wkz/mdio-tools
cd mdio-tools
make
sudo make install

# 或者使用包管理器(部分发行版)
sudo apt install mdio-tools   # Debian/Ubuntu
sudo yum install mdio-tools   # RHEL/CentOS

4.3.1 基本用法

先看看系统里有哪些MDIO总线:

# 列出所有MDIO总线
mdio list

# 输出示例:
# mdio-bus-0:  /dev/mdio0
# mdio-bus-1:  /dev/mdio1

读一个PHY的寄存器:

# 读PHY地址0x01的寄存器0x00(控制寄存器)
mdio read /dev/mdio0 0x01 0x00

# 输出示例:
# 0x3100

写一个寄存器:

# 将PHY地址0x01的寄存器0x00写为0x3100
mdio write /dev/mdio0 0x01 0x00 0x3100

4.3.2 实战:读取PHY ID

这是我每次调试新板子必做的第一步:

#!/bin/bash
# 读取PHY ID脚本
BUS="/dev/mdio0"
PHY_ADDR=0x01

echo "=== PHY ID 读取 ==="
ID1=$(mdio read $BUS $PHY_ADDR 0x02)
ID2=$(mdio read $BUS $PHY_ADDR 0x03)
echo "寄存器0x02: $ID1"
echo "寄存器0x03: $ID2"

# 解析OUI
OUI_HIGH=$(( (ID1 >> 6) & 0x3FF ))
OUI_LOW=$(( (ID1 & 0x3F) << 6 | (ID2 >> 10) & 0x3F ))
OUI=$(( (OUI_HIGH << 6) | OUI_LOW ))
echo "OUI: 0x$(printf '%04X' $OUI)"
echo "型号: 0x$(printf '%04X' $(( (ID2 >> 4) & 0x3F )))"
echo "版本: 0x$(printf '%02X' $(( ID2 & 0x0F )))"

注意:有些PHY的MDIO地址不是通过硬件引脚配置的,而是通过I2C或SPI写入的。这种情况下,你需要在PHY上电后先通过其他接口配置它的MDIO地址,然后才能用mdio-tools访问。我遇到过一块PHY,它的默认MDIO地址是0x00,但系统把它映射到了0x01,结果读出来的数据全是错的。

4.3.3 扩展寄存器访问(Page方式)

很多车载PHY(如Marvell 88Q2112、Broadcom BCM89811)使用Page方式访问扩展寄存器。以Marvell为例:

# 先选择Page 2
mdio write /dev/mdio0 0x01 0x1E 0x0002

# 然后读扩展寄存器0x10(温度传感器值)
mdio read /dev/mdio0 0x01 0x10

# 读完后记得切回Page 0
mdio write /dev/mdio0 0x01 0x1E 0x0000

这里有个小技巧——我习惯写一个shell函数来封装Page切换:

mdio_page_read() {
    local bus=$1 phy=$2 page=$3 reg=$4
    mdio write $bus $phy 0x1E $page
    mdio read $bus $phy $reg
    mdio write $bus $phy 0x1E 0x0000
}

# 使用示例
mdio_page_read /dev/mdio0 0x01 0x02 0x10

4.4 本章知识体系

下面这张图帮你理清MDIO/MDC、寄存器空间和调试工具之间的关系:

MDIO/MDC调试知识体系 MDIO/MDC调试知识体系 MDIO/MDC物理层 MDIO帧协议 寄存器地址空间 Preamble + Start + OP PHY地址 + 寄存器地址 Turnaround + 数据 基本寄存器 (0x00~0x1F) 扩展寄存器 (Page方式) 扩展寄存器 (MMD方式) Linux调试工具:mdio-tools / mdio-raw mdio read / write Page切换封装 PHY ID解析脚本 核心思路:物理层通 → 协议层对 → 寄存器读对 → 工具用熟 → 问题定位快

嗯,这一章的内容就到这里。MDIO/MDC是PHY调试的第一道关卡。你只要把协议时序搞明白,把寄存器空间摸清楚,再把mdio-tools用熟练,大部分PHY问题都能在寄存器层面找到线索。下次遇到PHY不工作,别急着换芯片——先拿mdio-tools读一遍寄存器,答案往往就在里面。