news 2026/10/17 11:19:56

GPIO模拟MDIO读写PHY寄存器的bit-banging驱动实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GPIO模拟MDIO读写PHY寄存器的bit-banging驱动实现

简介:一套面向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 层的排查都变得有底可查。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 11:19:41

Python zip包安全解压与运行:从验证到环境搭建避坑指南

简介&#xff1a;该压缩包专为海康威视网络摄像机二次开发场景设计&#xff0c;面向需要基于Python实现移动侦测报警事件的开发者。资源内封装了设备登录、报警回调等核心模块&#xff0c;其中NET_DVR_Login_V40.py负责完成SDK登录&#xff0c;MSesGCallback.py处理移动侦测消息…

作者头像 李华
网站建设 2026/10/17 11:19:32

代码化图表设计:让架构图、流程图和ER图成为可维护的代码

今天想认真聊聊 diagram-design 这个技术实践。简单说&#xff0c;它属于“用代码来画架构图、流程图、这些日常要用的图”这一大类需求。说得白话一点&#xff0c;就是把以前在画布上拖拽方框、连线的方式&#xff0c;换成写文本、跑命令、出图的方式&#xff0c;让整套流程图…

作者头像 李华
网站建设 2026/10/17 11:19:28

传送门Shader正方形背景问题:用Alpha Clipping彻底解决透明贴图残留

做传送门效果的时候&#xff0c;很多人都会卡在同一个地方&#xff1a;贴图素材本身明明是个圆形光效或者旋涡纹理&#xff0c;放进场景里却总带着一圈方方正正的底色&#xff0c;黑的或者白的&#xff0c;在Game视图里特别扎眼&#xff0c;瞬间把科幻感拉回“实习生作业”的水…

作者头像 李华
网站建设 2026/10/17 11:19:21

在VScode上写网页(html):用TaoToken统一Key打通本地预览与AI补全

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 11:17:47

寄大件/贵重物品选什么快递?快递老兵‌一招讲清楚

寄送重量超过 20 千克的大件物品时, 应该优先选择德邦以及顺丰还有京东物流, 处理单件价值在 5000 元以上的贵重物品时, 首选使用顺丰的服务并进行保价操作或者是开通京东物流的贵重件专用通道。针对普通包裹可以通过第三方平台进行价格比较, 这比直接使用官方报价能节省大约40…

作者头像 李华
网站建设 2026/10/17 11:16:47

LSTM多输入多输出时间序列预测实战指南

简介&#xff1a;本资源是一份面向深度学习初学者与时间序列建模实践者的LSTM多输入多输出预测完整实现方案&#xff0c;聚焦空气质量、污染指数等多变量时序数据的联合建模与未来多步预测任务。资源包含126个文件&#xff0c;以75个Python脚本&#xff08;含模型构建、训练、评…

作者头像 李华