
简介面向VxWorks嵌入式开发者的GPIO模拟MDC/MDIO驱动代码用于在缺乏专用SMI硬件接口的平台上通过通用GPIO引脚完成对以太网PHY芯片的配置与寄存器读写。资源聚焦MDC时钟生成、MDIO双向数据收发、命令与地址帧时序控制及错误检测等关键环节适合已具备一定VxWorks驱动基础、需要打通PHY管理通道的工程师参考。压缩包内共1个C源文件即驱动实现代码整体约2KB体量精简便于直接嵌入BSP工程中对照移植。目前已有1561人学习下载说明该实践场景在嵌入式网络开发中具有普遍需求。代码围绕GPIO方向配置、延时函数控制时钟周期、上升沿写与下降沿读的时序处理展开并给出读写PHY寄存器的函数示例读者可据此理解软件模拟SMI协议的完整流程快速排查时序偏差与数据异常问题为无专用MAC管理接口的板卡提供可复用的实现思路。1. 用两根普通 GPIO 把 MDIO 时序“掰”出来手上有一块板子PHY 芯片的 MDC/MDIO 引脚没接到 SoC 的专用 MAC 控制器上而是随手连到了两个普通 GPIO。硬件同事说“反正就两根线你软件模拟一下”。于是问题来了VxWorks 下没有现成的 SMI 控制器可用寄存器读写全靠自己按时序翻转电平。这份gpio模拟的mdc mdio驱动代码.rar解决的正是这个场景——用软件位操作bit-banging实现 MDC 时钟与 MDIO 双向数据线完成对 PHY 寄存器的读写进而做链路状态查询、自协商配置、PHY 初始化。适合正在 VxWorks 上做网络底层驱动、手里有“非标准接线”板子的开发者也适合想搞懂 SMI 时序到底怎么落地的人。2. 先看清这套驱动的骨架文件、接口与时序模型拿到压缩包别急着编译。先把里面几个文件的关系理顺否则后面改一个宏整个时序就崩了。这类 GPIO 模拟 SMI 的驱动结构通常很朴素一个头文件放寄存器地址宏和函数声明一个 C 文件放底层电平操作和帧收发可能再加一个测试/初始化入口。下面按“这个项目场景下合格开发最可能采用的组织方式”来讲具体文件名以包内为准逻辑是对应的。2.1 目录与文件职责划分典型的文件构成如下表读代码前先建立这张地图文件职责改动频率mdcMdioDrv.hPHY 寄存器地址宏、函数原型、GPIO 引脚宏高换板必改mdcMdioDrv.cMDC 时钟产生、MDIO 读写位、完整帧收发低phyInit.c上电初始化、自协商、链路检测调用示例中Makefile/vxworks配置片段编译规则、驱动挂载低头文件里最关键的是引脚定义和延时参数。因为 MDC 是纯输出时钟MDIO 是双向线所以引脚配置要区分方向/* mdcMdioDrv.h —— 换板子基本只动这一段 */ #define MDC_GPIO_PIN 12 /* MDC 时钟纯输出 */ #define MDIO_GPIO_PIN 13 /* MDIO 数据双向 */ /* PHY 地址与寄存器偏移按实际硬件填 */ #define PHY_ADDR 0x01 #define PHY_REG_BMCR 0x00 /* 基本模式控制寄存器 */ #define PHY_REG_BMSR 0x01 /* 基本模式状态寄存器 */ #define PHY_REG_PHYID1 0x02 #define PHY_REG_PHYID2 0x03 /* 半周期延时单位 usMDC 上限一般 2.5MHz留余量 */ #define MDC_HALF_DELAY_US 1 extern void mdcMdioInit(void); extern STATUS phyRegWrite(UINT16 phyAddr, UINT8 regAddr, UINT16 data); extern STATUS phyRegRead (UINT16 phyAddr, UINT8 regAddr, UINT16 *pData);这里MDC_HALF_DELAY_US是整个驱动的“心跳”。IEEE 802.3 规定 MDC 最高 2.5MHz也就是半周期最小 200ns。但 GPIO 翻转本身有开销加上 VxWorks 下sysUsDelay的精度实际取值要实测。我一般先给 1us 跑通再用示波器看波形往小调。PHY_ADDR尤其容易翻车——很多板子 PHY 地址由 strapping 电阻决定不是想当然的 0。2.2 SMI 帧格式32 位里每一位都有讲究MDIO 读写不是随便发几个脉冲它有一套严格的帧结构。一次完整的 SMI 帧是 32 个 MDC 周期前 32 位是前导码preamble后面才是操作码、PHY 地址、寄存器地址和数据。写操作和读操作的帧格式不同读操作还要在“turnaround”阶段把 MDIO 方向从输出切成输入。这是整个驱动最容易写错的地方先把帧拆开看读帧: Preamble(32) | ST(01) | OP(10) | PHYAD(5) | REGAD(5) | TA(Z0) | DATA(16) 写帧: Preamble(32) | ST(01) | OP(01) | PHYAD(5) | REGAD(5) | TA(10) | DATA(16)Preamble32 个连续的 1给 PHY 同步用。STStart of Frame固定01。OPOperation Code读是10写是01。PHYAD5 位 PHY 地址0~31。REGAD5 位寄存器地址0~31。TATurnaround读操作时主机先释放总线高阻PHY 拉低一个周期再给数据写操作时主机发10。DATA16 位数据高位在前。理解这张表后面看代码就不会迷路。很多新手直接把“读”写成“发完地址就读数据”漏掉 TA 阶段的方向切换结果读回来永远是0xFFFF——这是血泪经验里最常见的一条。2.3 底层电平操作与方向切换所有上层帧逻辑最终都落到两个动作拉高/拉低 MDC以及设置/读取 MDIO。因为 MDIO 双向必须能动态切方向。VxWorks 下 GPIO 方向控制一般通过芯片的 GPIO 控制器寄存器或 BSP 提供的封装函数完成这里用抽象函数表示实际替换成你板子的gpioSetDir/gpioSetValue即可/* 底层电平操作实际项目替换为 BSP 的 GPIO 接口 */ static void mdcSet(int level) { gpioSetValue(MDC_GPIO_PIN, level ? 1 : 0); } static void mdioSetDir(int isOutput) { /* 1输出0输入读 PHY 时切输入 */ gpioSetDir(MDIO_GPIO_PIN, isOutput); } static void mdioWriteBit(int bit) { gpioSetValue(MDIO_GPIO_PIN, bit ? 1 : 0); } static int mdioReadBit(void) { return gpioGetValue(MDIO_GPIO_PIN) ? 1 : 0; } /* 一个 MDC 周期先低后高数据在上升沿被采样 */ static void mdcClockPulse(void) { mdcSet(0); sysUsDelay(MDC_HALF_DELAY_US); mdcSet(1); sysUsDelay(MDC_HALF_DELAY_US); }逻辑说明SMI 协议规定数据在 MDC 上升沿被采样所以每个 bit 的流程是“先摆好 MDIO 电平 → MDC 拉低 → 延时 → MDC 拉高 → 延时”。mdcClockPulse把“拉低-拉高”封装成一个周期调用方只需在调用前把 MDIO 摆到目标位。参数上MDC_HALF_DELAY_US决定时钟频率mdioSetDir的入参决定当前是主机驱动还是 PHY 驱动总线。注意切方向前一定要保证 MDC 处于低电平否则可能在切换瞬间产生毛刺被 PHY 误采样。2.4 写寄存器从帧到位的完整落地有了底层动作写寄存器就是把 32 位帧逐位“喂”出去。写帧的 TA 是10数据段是主机输出全程 MDIO 保持输出方向STATUS phyRegWrite(UINT16 phyAddr, UINT8 regAddr, UINT16 data) { int i; UINT32 frame; mdioSetDir(1); /* 全程输出 */ /* 32 位前导码全 1 */ for (i 0; i 32; i) { mdioWriteBit(1); mdcClockPulse(); } /* 拼 32 位有效帧ST01, OP01(写), PHYAD, REGAD, TA10, DATA */ frame (0x1u 30); /* ST: 01 */ frame | (0x1u 28); /* OP: 01 写 */ frame | ((UINT32)(phyAddr 0x1F) 23); /* PHYAD 5 位 */ frame | ((UINT32)(regAddr 0x1F) 18); /* REGAD 5 位 */ frame | (0x2u 16); /* TA: 10 */ frame | (UINT32)data; /* DATA 16 位 */ for (i 31; i 0; i--) { mdioWriteBit((frame i) 0x1); mdcClockPulse(); } return OK; }逻辑说明frame的位布局严格对应 2.2 的写帧格式。ST占 bit30~31OP占 bit28~29PHYAD左移 23 位占 bit23~27REGAD左移 18 位占 bit18~22TA的10放在 bit16~17低 16 位是数据。循环从 bit31 往 bit0 发保证高位在前。参数上phyAddr和regAddr都做了 0x1F掩码防止越界污染相邻字段——这个掩码别省我见过有人传了0x20的寄存器地址结果把 OP 位改掉PHY 直接不响应。2.5 读寄存器TA 阶段的方向切换是命门读操作比写多一个坑TA 阶段主机要先释放总线PHY 拉低一个周期表示“我收到了”然后主机切输入读 16 位数据。方向切换的时机差一个周期读出来就是垃圾STATUS phyRegRead(UINT16 phyAddr, UINT8 regAddr, UINT16 *pData) { int i; UINT32 frame; UINT16 value 0; if (pData NULL) return ERROR; mdioSetDir(1); /* 先输出发前导码和地址 */ for (i 0; i 32; i) { mdioWriteBit(1); mdcClockPulse(); } /* ST01, OP10(读), PHYAD, REGAD */ frame (0x1u 30); frame | (0x2u 28); /* OP: 10 读 */ frame | ((UINT32)(phyAddr 0x1F) 23); frame | ((UINT32)(regAddr 0x1F) 18); for (i 31; i 18; i--) { /* 只发到 REGAD 结束 */ mdioWriteBit((frame i) 0x1); mdcClockPulse(); } /* TA 第一拍主机释放总线PHY 拉低 */ mdioSetDir(0); mdcClockPulse(); /* TA 第二拍PHY 释放主机准备读 */ mdcClockPulse(); /* 读 16 位数据高位在前 */ for (i 0; i 16; i) { value 1; value | (UINT16)mdioReadBit(); mdcClockPulse(); } mdioSetDir(1); /* 读完恢复输出避免总线悬空 */ *pData value; return OK; }逻辑说明发完REGADbit18后停止输出进入 TA。第一个mdcClockPulse期间主机已切输入PHY 拉低第二个脉冲 PHY 释放总线。之后 16 个周期逐位读value 1再或上新位保证高位在前。读完立刻把方向切回输出这是个好习惯——总线悬空在高阻态容易受干扰下次通信前状态不确定。参数pData做了空指针检查返回ERROR而不是让系统挂掉VxWorks 下这种防御很必要。3. 在 VxWorks 里把它跑起来初始化、编译与验证代码看懂只是第一步真正让它在板子上跑通还有初始化顺序、编译挂载、波形验证几道关。这一章按“上电到读到正确 PHY ID”的顺序走一遍每一步都给出可操作的命令和判断依据。3.1 初始化顺序GPIO 先于一切mdcMdioInit必须在任何 PHY 读写之前调用而且顺序不能乱先配 MDC 为输出并拉高空闲态再配 MDIO 为输出并拉高。如果 MDIO 初始方向是输入前导码根本发不出去void mdcMdioInit(void) { /* MDC 空闲态为高先拉高再配方向避免上电毛刺 */ gpioSetValue(MDC_GPIO_PIN, 1); gpioSetDir(MDC_GPIO_PIN, 1); /* MDIO 默认输出高空闲态 */ gpioSetValue(MDIO_GPIO_PIN, 1); gpioSetDir(MDIO_GPIO_PIN, 1); sysUsDelay(10); /* 等 PHY 上电稳定 */ }逻辑说明SMI 总线空闲时 MDC 和 MDIO 都应为高。先写值再配方向是为了避免方向切换瞬间输出一个不确定电平。sysUsDelay(10)给 PHY 内部电路一点稳定时间具体值看 PHY 手册的上电时序要求常见是几毫秒这里 10us 只是保守起步实际初始化前往往还要等 PHY 复位完成。3.2 编译与挂载到 VxWorks 镜像VxWorks 下加驱动通常是把.c加进Makefile的MACH_OBJS或对应 BSP 的源文件列表然后重编镜像。如果是可加载模块 downloadable kernel module也可以单独编译成.o再ld加载# 以 BSP 编译为例把驱动加进对象列表后重编 cd $(WIND_BASE)/target/config/bspName # 编辑 Makefile在 MACH_OBJS 中加入 mdcMdioDrv.o make clean make逻辑说明MACH_OBJS是 VxWorks BSP 编译时链接进内核的对象列表把驱动.o加进去镜像启动后函数就在符号表里。如果走可加载模块路线编译命令类似ccppc -c -o mdcMdioDrv.o mdcMdioDrv.c再在目标机 shell 里ld mdcMdioDrv.o。参数上交叉编译器前缀ccppc要和你 BSP 的TOOL变量一致别拿 x86 的编译器去编 PowerPC 的板子。3.3 用 PHY ID 寄存器做第一次自检驱动跑起来后第一个验证动作不是配自协商而是读 PHY ID 寄存器PHYID1/PHYID2地址0x02/0x03。这两个寄存器是只读的、值固定读对了说明时序、地址、方向切换全对void phySelfTest(void) { UINT16 id1 0, id2 0; mdcMdioInit(); if (phyRegRead(PHY_ADDR, PHY_REG_PHYID1, id1) ! OK || phyRegRead(PHY_ADDR, PHY_REG_PHYID2, id2) ! OK) { printf(PHY read failed\n); return; } printf(PHY ID: 0x%04X%04X\n, id1, id2); /* 对照 PHY 手册的 OUI 和型号位确认读到的不是 0xFFFF/0x0000 */ }逻辑说明读回0xFFFF通常是 MDIO 没切输入或 PHY 没响应读回0x0000多半是总线被拉死或地址错。正常应读到厂商 OUI 加型号比如某 PHY 的 ID 高 16 位是 OUI 的一部分。这一步过了再去做BMCR自协商使能、BMSR链路状态轮询才有意义。参数上PHY_ADDR如果读不到拿示波器看 MDIO 上有没有 PHY 的 ACK 拉低比盲改代码快得多。3.4 自协商与链路检测的调用节奏PHY 初始化一般流程是软复位 → 使能自协商 → 等自协商完成 → 读链路状态。这里要注意BMCR的 bit15 是软复位写 1 后要等它自动清零BMSR的 bit2 是链路状态但它有“锁存”特性读一次会清掉变化位所以轮询要用两次读或读BMSR后再读一次确认STATUS phyAutoNegStart(void) { UINT16 bmcr 0; int retry; /* 软复位 */ phyRegWrite(PHY_ADDR, PHY_REG_BMCR, 0x8000); for (retry 0; retry 100; retry) { sysUsDelay(1000); phyRegRead(PHY_ADDR, PHY_REG_BMCR, bmcr); if ((bmcr 0x8000) 0) break; /* 复位完成 */ } if (retry 100) return ERROR; /* 使能自协商 重启自协商 */ phyRegWrite(PHY_ADDR, PHY_REG_BMCR, 0x1200); return OK; }逻辑说明0x8000是软复位位写完轮询直到它清零最多等 100ms。0x1200是 bit12自协商使能加 bit9重启自协商。参数上轮询次数和延时按 PHY 手册的复位时间调整有的 PHY 要几百毫秒。链路检测则周期性读BMSR注意锁存特性别拿单次读的结果当稳定状态。4. 避坑与排查那些让波形“看起来对、读出来错”的细节这套驱动代码量不大但坑很集中。下面几条都是实际调试中反复出现的按“现象 → 原因 → 解决”记下来能省不少示波器前的时间。4.1 读回恒为 0xFFFF现象phyRegRead任何寄存器都返回0xFFFF写操作似乎也没报错。原因TA 阶段方向切换时机不对主机没真正释放总线或者mdioSetDir(0)之后没有先让 MDC 走一个完整周期就去读。解决严格按 2.5 的顺序切输入后先走两个mdcClockPulseTA 两拍再进数据循环用示波器看 MDIO 在 TA 第一拍是否被 PHY 拉低没有就是 PHY 地址错或 PHY 没上电。4.2 偶发读写失败重试就好现象大部分时候正常偶尔一次读写出错重试又好了。原因MDC_HALF_DELAY_US太小GPIO 翻转和sysUsDelay的实际开销叠加后MDC 高电平时间不够PHY 采样不稳。解决先把半周期延时加到 2~3us 跑稳再逐步往下压同时确认sysUsDelay在你的 BSP 上是忙等还是基于 tick基于 tick 的延时精度可能只有毫秒级那就得改用更细的延时接口。4.3 换块板子就读不到 PHY现象同一份代码A 板正常B 板读不到。原因PHY_ADDR由硬件 strapping 决定两块板子可能不同或者 B 板的 MDIO 上拉了但 MDC 没上拉。解决先扫 0~31 所有 PHY 地址看哪个能读回非0xFFFF的 ID再查原理图确认上拉电阻。这个扫描函数很值得写十行代码省半天。4.4 多任务并发访问总线冲突现象网络任务和调试命令同时读 PHY数据错乱。原因SMI 帧是 32 位连续时序中间被别的任务插入 GPIO 操作就断了。解决给phyRegRead/phyRegWrite加互斥信号量semMCreatesemTake/semGive把整个帧收发包在临界区里。VxWorks 下这是常规操作别省。4.5 软复位后立刻读写失败现象写完BMCR软复位马上读BMSR返回异常。原因PHY 复位期间不响应 SMI或者复位时间没等够。解决复位后轮询BMCRbit15 直到清零再加一点余量不要在复位窗口内做任何其他寄存器操作。5. 二次开发把这份驱动改成你自己的 PHY 管理模块代码跑通只是起点真正落地往往要把它嵌进更大的网络驱动框架里。这一章讲几个改造方向和一个验证习惯都是我在实际项目里踩过之后固定下来的做法。第一个改造点是抽象 GPIO 层。现在mdcSet/mdioWriteBit直接调 BSP 函数换芯片就得改驱动。更好的做法是定义一个函数指针结构体初始化时传入具体实现驱动本体只依赖接口。这样同一份 SMI 逻辑能复用到不同 SoC 上测试时还能塞一个模拟实现做单元测试。第二个是把 PHY 操作封装成对象。单 PHY 场景直接传PHY_ADDR没问题多 PHY 交换机场景就要维护 PHY 列表、每个 PHY 的链路状态和自协商结果。常见做法是定义一个PHY_DEV结构体把地址、当前速率、双工模式、链路状态打包再配一组phyDevRead/phyDevWrite包装函数。改造后调用方不再关心底层是 GPIO 模拟还是硬件 SMI 控制器。第三个是加一层寄存器访问缓存与超时。PHY 寄存器读写在链路抖动时可能失败裸调用会让上层逻辑卡死。我一般给读写加超时重试比如 3 次并对BMSR这类状态寄存器做短周期缓存避免高频轮询把总线占满。下面是一个改造后的调用示例typedef struct { UINT16 phyAddr; UINT16 lastBmsr; UINT32 lastPollTick; int linkUp; } PHY_DEV; STATUS phyDevPollLink(PHY_DEV *dev) { UINT16 bmsr 0; int retry; for (retry 0; retry 3; retry) { if (phyRegRead(dev-phyAddr, PHY_REG_BMSR, bmsr) OK) break; sysUsDelay(100); } if (retry 3) return ERROR; /* BMSR bit2 链路状态锁存特性读两次确认 */ dev-linkUp (bmsr 0x0004) ? 1 : 0; dev-lastBmsr bmsr; return OK; }逻辑说明重试 3 次、每次间隔 100us覆盖偶发总线错误linkUp取BMSRbit2。参数上lastPollTick可以用来做轮询节流比如 1 秒内不重复读。这个结构体化的改造让驱动从“一段能跑的代码”变成“能维护的模块”。验证方法上我固定用三步先读 PHY ID 确认时序再读BMSR确认链路最后用BMCR强制指定速率双工做回环测试。三步都过才认为驱动可靠。表格对比一下改造前后的差异维度原始驱动改造后GPIO 依赖直接调 BSP函数指针注入多 PHY 支持单地址宏PHY_DEV 列表错误处理无重试超时重试 缓存可测试性需真实硬件可注入模拟实现最后说个习惯。早些年我调这类时序驱动总想着“逻辑对就行”结果一次板子换 PHY 后读 ID 死活不对查了两天才发现是 TA 阶段少走了一个时钟周期——代码在旧 PHY 上“恰好”能工作。从那以后我每次改完 SMI 时序都强制先用示波器抓一帧完整波形对照 2.2 的帧格式逐位核对再上电跑自检。这个习惯帮我省下的时间远比抓波形花的多。希望帮到你。本文还有配套的精品资源点击获取