简介:一套面向VxWorks嵌入式开发者的GPIO模拟MDC/MDIO驱动代码,专门解决系统缺乏专用SMI硬件接口时,用普通GPIO完成以太网PHY芯片管理通信的问题。压缩包内仅有1个C源文件,包体约2KB,虽小巧却完整覆盖驱动核心实现,包括GPIO方向配置、基于延时函数生成2.5MHz时钟、协议时序控制及MDIO读写函数等关键环节,并对数据帧格式与读取流程做了完整演示。代码清晰演示了初始化MDC/MDIO引脚、发送起始位和命令/地址(5位命令/地址与16位数据位)、在时钟下降沿接收响应数据,并带有简单错误检测逻辑,便于开发者对照协议标准理解软件模拟全过程。该驱动适用于对VxWorks BSP开发和SMI协议有基础的技术人员,可将其作为无专用MDIO硬件平台下的移植范例,或用于排查PHY通信异常。目前已有1558人学习下载,对于需要实现以太网管理接口而又受硬件资源限制的嵌入式项目,具有直接的参考价值。
1. 一块没有 MDIO 控制器的板子,怎么读 PHY 寄存器
一块没有内置 MDIO 控制器的板子,boot 起来之后 PHY 完全是个黑匣子:不知道它有没有完成硬件复位,不知道它协商到了什么速率,连寄存器都读不出来。我刚拿到这份“gpio模拟的mdc mdio驱动代码”时,第一反应就是这正好解决 vxworks 开发里最尴尬的一种场景——MDC/MDIO 引脚被复用、或者 SoC 压根没有 SMI 主控,但网口又必须得调通。整套驱动的核心思路是 bit-banging:用两个 GPIO 引脚,把 MDIO 管理总线的时序逐 bit 手搓出来,MDC 当时钟,MDIO 当数据,硬生生把一个完整的 MDIO 帧拆成几十个 GPIO 翻转动作。
适合谁用?一种是板级 BSP 开发,MDC/MDIO 被 GPIO 复用吃掉,只能靠模拟救急;另一种是内核没有对应 SMI 控制器驱动,但又想读 PHY 的协商状态、改 PHY 寄存器做调试。代码量不大,但把整条 MDIO 时序链路从头到尾实现了,读 PHY ID、读 link 状态、改 PHY 控制寄存器都能直接做。读完这套代码,你不仅能跑通驱动,还能把 GPIO 模拟同步总线的思路整个带走,换芯片、换 RTOS 都能照搬。
2. 协议与选型:MDC/MDIO/SMI 的时序规矩,和 GPIO 模拟的边界
2.1 MDC/MDIO:两根线管住 PHY 的全部寄存器
MDIO 管理接口在 PHY 手册里常叫 SMI(Serial Management Interface),IEEE 802.3 标准里的正式名字是 MDIO/MDC。物理上就两根线:MDC 是管理时钟,由 MAC 侧或者主控侧产生;MDIO 是双向数据线,主机写 PHY 寄存器时由主机驱动,读 PHY 寄存器时由 PHY 驱动。寄存器地址空间是 5 位 PHY 地址加 5 位寄存器地址,一共 32 个 PHY、每个 32 个寄存器,这也就是为什么你能通过 MDIO 把 PHY 的 ID、状态、协商结果、中断标志全部读出来。
这套接口最舒服的地方在于:它是个完全主机主导的同步串行协议。MDC 时钟谁产生?主机产生。数据什么时候有效?以 MDC 边沿为基准。所以根本没有硬实时要求,也不需要中断同步。而 GPIO 模拟恰恰擅长这种场景——只要把每个时钟边沿、每个电平变化用 GPIO 翻转复现出来,协议自然就通了。硬件控制器能做到 25MHz,我们不用,模拟时把频率压到几百 kHz,时序余量反而更大。
2.2 SMI 帧格式与采样时机:为什么 GPIO 能代替硬件控制器
一条完整的 MDIO 帧长这样:
前导码 ST OP PHYAD REGAD TA DATA 32'b1 01 10/01 5bit 5bit 2bit 16bit前导码是固定的 32 个连续 1,目的是让总线上所有 PHY 同步;ST 固定是 01,表示这是一条标准 MDIO 帧;OP 用 10 表示读、01 表示写;PHYAD 是目标 PHY 地址;REGAD 是目标寄存器地址;TA 是 turnaround 周期,读的时候主机在这两拍里释放总线,让 PHY 接管数据线,写的时候 TA 固定驱动 10;最后是 16 bit 的数据。
采样时机要看读写方向。写数据时,主机必须先把 bit 放到 MDIO 上,然后拉高 MDC,PHY 在 MDC 上升沿锁存数据。读数据时反过来,PHY 在 MDC 边沿附近驱动 MDIO,主机等电平稳定后再采样。GPIO 模拟能成,关键就是抓住这两条:写先摆数据再给时钟,读给时钟后等稳定再取电平。频率再低都行,只要每个边沿都对,PHY 不关心你是硬件控制器还是软件 GPIO。
2.3 vxworks 里这套驱动的位置:miiBus 与 PHY 驱动之间
vxworks 的网络驱动栈里,PHY 访问是经过 miiBus 层统一的。网卡驱动要读 PHY,调的是 miiBus 的回调;miiBus 底下要么是硬件 MDIO 控制器,要么就是这咱这种软件模拟总线的驱动。很多时候 BSP 里的 miiBus 驱动把 readPhy/writePhy 两个函数指针指到硬件控制器的 read/write,这个项目做的事就是把这两个函数指针换成 GPIO 模拟的版本。
搞懂这个位置很重要。因为你在 vxworks 下跑这套驱动,最终不是直接调我的mdc_mdio_gpio_read到处读,而是把它注册成 miiBus 的总线驱动,上层网卡驱动照常用miiBusReadPhy这类接口,你的模拟时序就在下层被自动调用了。这样整个网络栈、PHY 驱动、协商逻辑全都是原装的,你只是把底层的“交通方式”从高速路换成了乡间小路。
3. 源码结构拆解:驱动分层、API 与 miiBus 挂接方式
3.1 文件组织与依赖
这份代码的文件不多,但层次很干净。我拿到手第一件事就是把文件列表过一遍,确认哪些是板级相关、哪些是纯算法,这样后面要换板子时心里有数:
mdc_mdio_gpio/ ├── mdc_mdio_gpio.c /* bit-banging 核心:帧组装、读写 bit */ ├── mdc_mdio_gpio.h /* 三个对外 API 声明 */ ├── board_gpio_adapter.c /* 板级 GPIO 适配:方向、置位、读取 */ └── mdio_selftest.c /* 自检函数:读 PHY ID 和 link 状态 */依赖关系非常明确:核心文件只调board_gpio_adapter.c暴露出来的gpioSet/gpioGet/gpioDirOut/gpioDirIn这四个函数,完全不知道引脚在哪个 GPIO 控制器上。板级文件反过来只负责把引脚操作落到 BSP 的底层接口上。如果想移植到别的板子,改board_gpio_adapter.c一个文件就够了,核心时序一行都不用动。这个分层是这套代码最值得学的部分:把硬件差异隔离在最底层,上层只面对逻辑电平。
3.2 核心数据结构和对外 API
头文件里定义了一个设备结构体,用来保存引脚配置和时序校准参数:
typedef struct { int pinMdc; /* MDC 对应的 GPIO 号 */ int pinMdio; /* MDIO 对应的 GPIO 号 */ UINT32 halfCycles; /* MDC 半周期对应的 CPU cycle 数 */ UINT32 tscFreq; /* 校准后的 CPU 主频 */ BOOL inited; /* 是否已完成初始化 */ } MDC_MDIO_DEV;对外 API 就三个,语义很清楚:
int mdc_mdio_gpio_init(MDC_MDIO_DEV *dev, int pinMdc, int pinMdio, int freqKhz); int mdc_mdio_gpio_read(MDC_MDIO_DEV *dev, int phyAddr, int regAddr, int *value); int mdc_mdio_gpio_write(MDC_MDIO_DEV *dev, int phyAddr, int regAddr, int value);init负责记录引脚号、把频率换算成 CPU cycle 数、并把两个引脚初始化为空闲状态;read和write是核心操作。这里freqKhz是你要的 MDC 频率,典型值在 250~500kHz 之间,比硬件控制器慢两个数量级,但 PHY 根本不挑。真正要注意的是halfCycles的计算,这一步决定后面的忙等延时准不准,第 4 章会展开讲。
3.3 挂进 vxworks miiBus:把模拟总线伪装成标准总线驱动
代码单独提供了一个回调适配函数,作用是把自己包装成 miiBus 能认的 readPhy/writePhy 接口:
LOCAL int gpioMiiReadPhy(int unit, int phyAddr, int regAddr, UINT16 *pData) { int val = 0; if (mdc_mdio_gpio_read(&gGpioDev, phyAddr, regAddr, &val) != 0) { return ERROR; } *pData = (UINT16)(val & 0xFFFF); return OK; } LOCAL int gpioMiiWritePhy(int unit, int phyAddr, int regAddr, UINT16 data) { return mdc_mdio_gpio_write(&gGpioDev, phyAddr, regAddr, data); }然后把这些函数指针填进MII_BUS_DRV结构体,调用miiBusAdd()注册进 vxworks。结构体的具体字段在不同版本 BSP 里会不一样,但你只需要找 readPhy 和 writePhy 两个函数指针的位置,填好就完成挂接。弄完以后,上层网卡驱动、PHY 驱动、协商逻辑全部走原装流程,你这套 GPIO 模拟的时序在底层被自动调用,对上层完全透明。这也是最不容易翻车的接入方式——不要让上层直接感知到“模块总线是软件模拟的”,否则网卡驱动里到处都是特判,改起来没完没了。
4. 核心实现拆解:bit-banging 从 32 位前导到 16 位数据的完整链路
4.1 延时校准:找对 MDC 半周期的“时钟基准”
整个 GPIO 模拟最核心的命门不是协议格式,而是延时精度。vxworks 的 tickLib 是按 system tick 计时的,默认 60Hz 下一个 tick 就是 16ms,MDC 半周期 1us,拿 tick 根本没法等。sysUsDelay()在某些 BSP 里能用,但精度受实现影响很大,有的会把整个调度器卷进来。在这种微秒级、纳秒级的时序场景里,最稳的办法是用 CPU 周期计数器做忙等。
static UINT32 halfCycles; static UINT32 tscFreq; static void mdioCalibrate(int freqKhz) { tscFreq = vxTscGetFrequency(); /* 拿 CPU 主频,换成物理计数器 */ halfCycles = tscFreq / (freqKhz * 2); /* 半周期 = 总周期的一半 */ } static void mdioHalfPeriodWait(void) { UINT32 start = (UINT32)vxTscGet64(); while (((UINT32)vxTscGet64() - start) < halfCycles) ; }vxTscGet64() 在 vxWorks 7 里能直接拿时间戳计数器,如果你的 BSP 没有这个接口,换成sysTimestamp()加上主频换算也行。逻辑说明:freqKhz是你期望的 MDC 频率,半周期占用的 CPU cycle 数等于主频除以频率再除以 2。比如主频 1GHz、MDC 频率 500kHz,半周期就是 1000 个 cycle,忙等 1000 次翻转即可。参数说明:mdioCalibrate 必须在 init 阶段调用,而且要在系统时钟稳定后再校准,不要在 boot 早期跑,否则拿到的 tscFreq 可能不准。
4.2 写一个 bit:数据必须走在 MDC 上升沿之前
写 bit 的实现是整个模拟的基础动作,后面所有帧字段都是它的组合:
static void mdioWriteBit(int bit) { gpioSet(dev->pinMdio, bit); /* 1. 先摆数据,此时 MDC 为低 */ gpioSet(dev->pinMdc, 1); /* 2. MDC 上升沿,PHY 锁存数据 */ mdioHalfPeriodWait(); gpioSet(dev->pinMdc, 0); /* 3. MDC 拉低,准备下一个 bit */ mdioHalfPeriodWait(); }逻辑说明:顺序不能反。如果先拉高 MDC 再改 MDIO,PHY 在上升沿锁存到的是上一个 bit 的残留电平,数据就全错了。第一步先把 bit 放到 MDIO,MDC 还是低,这时总线电平稳定;第二步拉高 MDC,PHY 在上升沿采集到正确电平;第三步拉低 MDC,完成一个完整时钟周期。参数说明:bit只能是 0 或 1,这个函数不区分是前导码、地址还是数据,调用方只传要发送的二进制值,封装得很干净。
4.3 读一个 bit:先给时钟,等稳了再采样
读 bit 比写 bit 略麻烦一点,因为数据方向反了:
static int mdioReadBit(void) { gpioSet(dev->pinMdc, 0); mdioHalfPeriodWait(); gpioSet(dev->pinMdc, 1); /* 上升沿,PHY 驱动 MDIO 输出数据 */ mdioHalfPeriodWait(); /* 等数据稳定后再采样 */ return gpioGet(dev->pinMdio); /* 采样点:高电平中段 */ }逻辑说明:读不能像写那样“先摆数据再给时钟”。MDIO 线上的数据是 PHY 驱动的,主机能做的就是把 MDC 拉高,给 PHY 一个边沿,等电平稳定后读取。这里我把采样点放在 MDC 拉高后的半个周期中段——这是最保守的做法,不依赖 PHY 芯片内部具体的输出延迟。参数说明:返回 0 或 1,没有错误路径,因为 GPIO 读取不存在协议错误,读出来是什么就是什么。整帧的正确性靠前面的帧格式拼装保证。
4.4 完整读帧:从 32 位前导到 16 位数据的组包过程
把写 bit 和读 bit 串起来,一条完整的读帧长这样:
int mdc_mdio_gpio_read(MDC_MDIO_DEV *dev, int phyAddr, int regAddr, int *value) { int i, val = 0; gpioDirOut(dev->pinMdio); /* 切到输出,准备发帧头 */ gpioSet(dev->pinMdio, 1); for (i = 0; i < 32; i++) { mdioWriteBit(1); /* 前导码:32 个连续 1 */ } mdioWriteBit(0); mdioWriteBit(1); /* ST = 01 */ mdioWriteBit(1); mdioWriteBit(0); /* OP = 10,读操作 */ for (i = 4; i >= 0; i--) { mdioWriteBit((phyAddr >> i) & 1); /* 5bit PHY 地址,MSB 先发 */ } for (i = 4; i >= 0; i--) { mdioWriteBit((regAddr >> i) & 1); /* 5bit 寄存器地址 */ } gpioDirIn(dev->pinMdio); /* 关键:TA 期间释放总线 */ mdioHalfPeriodWait(); /* TA 第一拍:高阻 */ mdioHalfPeriodWait(); /* TA 第二拍:PHY 已开始驱动 */ for (i = 15; i >= 0; i--) { /* 16 个数据 bit */ if (mdioReadBit()) { val |= (1 << i); } } gpioDirOut(dev->pinMdio); /* 读完之后必须恢复输出能力 */ gpioSet(dev->pinMdio, 1); /* 空闲状态:MDIO 拉高 */ *value = val; return 0; }逻辑说明:TA 是整个读帧最容易出错的地方。前 32+14 个 bit 主机一直在驱动 MDIO,到了 TA 必须把引脚切成输入,让 PHY 接管数据线。如果不切,主机的输出和 PHY 的输出会直接对拉,读回来的数据要么全是 1 要么全是 0,而且长期这样操作有损伤引脚的风险。TA 之后,PHY 从第 2 个 TA 周期开始驱动 MDIO,所以数据 bit 的读取紧跟其后,正好落在 PHY 驱动的节奏上。参数说明:phyAddr有效范围 0~31,对应 PHYAD 引脚配置的地址;regAddr同样 0~31。返回值 0 表示成功,读到的 16 bit 值通过value指针返回。这个函数的时序严格对应第 2 章的帧格式图,逐字段组装,没有任何“优化”。
写帧的套路完全对称,只是 OP 换成 01、TA 换成固定的 10,主机会持续驱动 MDIO,其实更要简单:
int mdc_mdio_gpio_write(MDC_MDIO_DEV *dev, int phyAddr, int regAddr, int value) { int i; gpioDirOut(dev->pinMdio); gpioSet(dev->pinMdio, 1); for (i = 0; i < 32; i++) { mdioWriteBit(1); } mdioWriteBit(0); mdioWriteBit(1); /* ST = 01 */ mdioWriteBit(0); mdioWriteBit(1); /* OP = 01,写操作 */ for (i = 4; i >= 0; i--) { mdioWriteBit((phyAddr >> i) & 1); } for (i = 4; i >= 0; i--) { mdioWriteBit((regAddr >> i) & 1); } mdioWriteBit(1); mdioWriteBit(0); /* TA = 10,写固定如此 */ for (i = 15; i >= 0; i--) { mdioWriteBit((value >> i) & 1); } gpioSet(dev->pinMdio, 1); /* 回到空闲状态 */ return 0; }逻辑说明:写帧不需要切方向,因为整个帧都是主机在驱动。TA 在写操作里不是“释放总线”,而是发送一个固定的 10 模式,告诉 PHY “接下来是数据”。这个细节很多实现会搞错,把读的 TA 逻辑直接复制到写里,结果写出来的帧 TA 变成高阻,PHY 根本不认。参数说明:value是要写入寄存器的 16 bit 数据,建议只传低 16 位有效值,高位数据 PHY 会忽略但总线协议没有定义,传 0 比较干净。
4.5 关键时序参数速查
下表是我在这套代码里最常用的几个参数,移植到新板时需要重新校准的就是光标行:
| 参数 | 典型值 | 说明 |
|---|---|---|
| MDC 频率 | 250kHz~500kHz | 硬件控制器能跑 25MHz,GPIO 模拟压两个数量级 |
| MDC 半周期 | 1us~2us | 由 freqKhz 换算成 CPU cycle 数 |
| 前导码长度 | 32 bit | 不支持省略,换 PHY 偶发失败罪魁祸首之一 |
| 读 TA | 第 1 拍高阻,第 2 拍 PHY 接管 | 必须切 GPIO 方向 |
| 写 TA | 固定驱动 10 | 不要复用读 TA 的逻辑 |
| PHY 地址范围 | 0~31 | 由 PHYAD 引脚电平决定 |
5. 避坑记录:GPIO 模拟 MDIO 常见的五类踩坑与修复
5.1 MDC 频率飙太高,PHY 直接不回数据
现象:按硬件控制器的经验,把 freqKhz 设成 2500kHz,结果读 PHY ID 读出来全是 0xFFFF,偶尔还能读对一次,多数时间不对。
原因:GPIO 翻转本身有延迟,加上 vxworks 开着中断调度,忙等循环可能被打断,半周期 jitter 太大。PHY 要求在 MDC 上升沿附近数据要保持稳定,你的 GPIO 翻转抖动超过它的建立时间窗口,数据就采错了。硬件 MDIO 控制器是非阻塞的硬件状态机,频率高了也稳;GPIO 模拟是 CPU 一条条指令跑出来的,一被打断节奏全乱。
解决:先把 freqKhz 降到 250kHz,半周期 2us,用逻辑分析仪抓 MDC/MDIO 两个引脚的波形,确认每个 bit 都是干净的方波,再逐步往上调。GPIO 模拟不需要追求频率,正确性远比速度重要。
5.2 前导码被“优化”掉,第二片 PHY 直接消失
现象:代码里有人把前导码从 32 个 1 改成 2 个 1,理由是“测过第一片 PHY 能读”。换了一块多个 PHY 的板子,第二片 PHY 的 ID 怎么都读不出来。
原因:MDIO 标准允许省略前导码,但实际 PHY 芯片的实现千差万别。有的 PHY 支持无前导访问,有的必须在总线空闲后先看到 32 个连续 1 才重新同步。一条总线上挂多个 PHY 时,前导码还承担着让所有 PHY 对齐帧边界的职责,省了它,第二片 PHY 的帧同步就乱了。
解决:老老实实输出 32 个 1,不要在这上面做“性能优化”。32 个 bit 在 500kHz 下也就 64us 的事,这点时间换来的兼容性绝对值回票价。从那以后我每次写完模拟驱动,都会拿双 PHY 的板子跑一遍全地址扫描再收工。
5.3 读 TA 没切方向,MDIO 和 PHY 对拉
现象:read 函数的帧头、地址都对,读回来的数据却全是 0xFFFF。加了打印发现 PHY ID 寄存器 2/3 读到全 1。
原因:读帧的 TA 阶段没有调用 gpioDirIn 切方向。MDIO 引脚还保持着输出状态,GPIO 输出的推挽电平和 PHY 驱动的电平直接短路对拉。GPIO 强输出 1 的时候,PHY 输出 0,总线电压被拉在中间值,GPIO 读回来就变成 1。数据全是 1 就是这么来的。
解决:在读帧进入 TA 之前,先调gpioDirIn(dev->pinMdio),把引脚切成输入;等 16 个数据 bit 读完后,再切回输出并主动拉高,为下一帧做准备。方向切换的顺序不能反,也不能省。每次写完读函数,我都会先抓波形确认 TA 阶段 MDIO 是高阻状态,而不是被强行拉低或拉高。
5.4 vxworks 延时函数不够精确,时序周期漂移几倍
现象:用sysUsDelay(2)代替忙等,结果 MDC 周期被拉长到几十微秒,PHY 偶发读不出数据,程序跑起来像是卡死了一样。
原因:sysUsDelay的精度依赖 BSP 实现,且它不保证纳秒级粒度。tick 驱动的延时函数在低 system tick 下误差是毫秒级的;即便实现是 us 级,函数调用本身也会因为调度器介入而引入不确定性。GPIO 模拟对延时的要求是“周期稳定”,不是“周期最短”,一个系统函数因为你不知道的调度开销把周期拉长几倍,整个时序就废了。
解决:改成 CPU cycle 忙等,就是第 4 章那段 mdioHalfPeriodWait。忙等期间关中断效果更稳,但会阻塞系统调度,适合放在初始化阶段或低频率调用场合。如果 PHY 驱动会频繁跑读操作,建议保持开中断的忙等,只要半周期够短,影响可以接受。
5.5 write 完之后立刻 read,读回来还是旧值
现象:往 ANAR 寄存器写完协商能力,马上读回来验证,发现还是旧值,过了几百毫秒再读才正确。
原因:PHY 内部寄存器不是直接映射的,很多寄存器写入后要经过内部状态机才能生效。尤其像协商控制、软复位这类操作,PHY 需要几十到几百微秒去完成内部流程,期间你去读,数据还在老地方。这是 PHY 芯片的固有行为,不是 MDIO 时序的错误。
解决:写寄存器后至少延时 1ms 再读,软复位类的操作建议延时 10ms 以上。我在自检函数里加了一个简单的 sleep 调用,所有写后读的场景统一走延时,避免在业务代码里到处补 wait。这个坑不是 GPIO 模拟特有的,硬件控制器一样会遇到,但模拟驱动因为整体频率低,更容易暴露出来。
6. 跑通与改造:验证脚本、多 PHY 扫描和换板移植
6.1 编译加载:最小改动接入 vxworks
代码拿到手后,最快的跑通路径是三步:把 mdc_mdio_gpio.c、board_gpio_adapter.c 放进 BSP 的 build 目录,在板级初始化代码里调用mdc_mdio_gpio_init传入实际引脚号和期望的 MDC 频率,然后编译进内核镜像。注意 init 里要传对引脚号,这一步错了后面全是白忙。建议 init 函数内部加一个简单的 GPIO 电平自检——先把 MDIO 拉高再读回来,确认引脚配置没被复用,能读到 1 再继续。
6.2 验证方法:读 PHY 的三个关键寄存器
跑通之后别急着接协议栈,先写一个自检函数把 PHY 的基础信息打出来看看:
void mdioSelfTest(void) { int phyId1 = 0, phyId2 = 0, bmsr = 0; mdc_mdio_gpio_read(&gGpioDev, 0, 2, &phyId1); /* PHY ID 高 16 位 */ mdc_mdio_gpio_read(&gGpioDev, 0, 3, &phyId2); /* PHY ID 低 16 位 */ mdc_mdio_gpio_read(&gGpioDev, 0, 1, &bmsr); /* 状态寄存器 */ printf("PHY ID: 0x%04x 0x%04x\n", phyId1, phyId2); printf("BMSR : 0x%04x (link=%d)\n", bmsr, (bmsr >> 2) & 1); }逻辑说明:PHY ID 寄存器是 IEEE 标准定义的 2/3 号寄存器,任何 PHY 都能读;状态寄存器 1 号里的 bit2 是 link 状态。如果 PHY ID 不是 0xFFFF 也不是 0x0000,说明 GPIO 模拟时序已经通了。如果 ID 全 F,先按第 5 章的坑逐条排查,别急着改上层代码。参数说明:这里 PHY 地址先用 0 验证,如果你的 PHYAD 引脚配置成别的地址,换成对应值。
6.3 二次开发:多 PHY 扫描、读重试和换板移植
多 PHY 是实际项目中一定会碰到的需求。在 init 之后做个全地址扫描就能摸清板上有几片 PHY:
for (int i = 0; i < 32; i++) { int id1 = 0, id2 = 0; mdc_mdio_gpio_read(&gGpioDev, i, 2, &id1); mdc_mdio_gpio_read(&gGpioDev, i, 3, &id2); if ((id1 != 0xFFFF) && (id2 != 0xFFFF) && (id1 != 0x0000) && (id2 != 0x0000)) { printf("PHY found at addr %d\n", i); } }扫描的时候注意给每片 PHY 预留 TA 切换时间,频率压低一点没关系,扫描过程就几毫秒。二次开发另外两个高频修改点是读重试和换板移植。读重试可以在 mdc_mdio_gpio_read 外面包一层,失败就重读三次,必要时每次重试前重新发一次前导码,因为总线状态可能被上一次失败操作弄脏。换板移植则只动 board_gpio_adapter.c 里的四个 GPIO 函数,核心时序文件完全不用改,这也是当初分层设计的价值。
我自己最常做的一个改造是把这套 bit-banging 逻辑抽成一个不依赖 vxworks 的纯 C 模块,底层只留 dir_out/dir_in/set/get/delay 五个回调,这样同一个算法文件可以同时用在 vxworks 和 Linux 环境。从那以后我每次拿到没有 MDIO 控制器的板子,都强制自己先写一个 50 行的 bit-banging 自检,把 PHY ID 读出来再上协议栈——这套代码本身就是一个很好的起点,跑通它,后面所有 PHY 层的排查都变得有底可查。希望帮到你。
本文还有配套的精品资源,点击获取