VxWorks下用GPIO模拟MDC/MDIO驱动PHY:SMI总线实现与避坑指南
发布时间:2026/10/17 7:38:04 锦皓数字建站

简介面向VxWorks嵌入式开发者的GPIO模拟MDC/MDIO驱动代码包解决系统缺少专用SMI硬件接口时无法直接配置以太网PHY芯片的管理问题。实现基于普通GPIO引脚通过软件时序精确模拟MDC时钟与MDIO双向数据线完整覆盖初始化、时钟生成、命令地址发送、数据读写与错误检测等环节代码按协议步骤拆分可逐段对照MDC/MDIO规范学习适用于硬件受限或成本敏感的嵌入式项目也适合需要深入理解PHY管理接口的驱动开发者参考。压缩包仅1个C语言源文件体积约2KB代码精简无冗余重点演示了GPIO方向切换、延时控制以及PHY读写函数封装便于快速移植到不同VxWorks BSP源文件内可直接查阅寄存器读写流程。目前已有1558人学习是VxWorks下SMI软件模拟的实用参考。开发者可从中提取时序控制与状态切换思路减少协议调试时间尤其适合需要自主实现MDIO通信或排查PHY配置异常的场合。1. GPIO 模拟 MDC/MDIO从一颗 PHY 开始驱动的 SMI 总线拿到这份 gpio模拟的mdc mdio驱动代码.rar 时多数嵌入式工程师的第一反应是MDC/MDIO 不是硬件控制器现成的吗为什么还要用 GPIO 去模拟实际项目里这种需求一点也不冷门——跑 VxWorks 的老板卡上PHY 挂在专用 MDIO 控制器上却被别的外设抢占或者硬件设计图只引出了两个普通 GPIO 给 PHY 管理接口又或者板子上的 PHY 型号太老硬件 MDIO 控制器的时序兼容性反而不如软件灵活。这套资源解决的就是这一类场景用两个 GPIO 引脚按照 SMI 协议时序在纯软件层面把 MDC 时钟和 MDIO 数据线怼出来实现读写 PHY 寄存器。适合正在调试 PHY 驱动、网口死活不通、又没有多余专用接口可用的 VxWorks 开发者。接下来我按时序底细、落地实现和排错策略三层拆开讲。2. MDC/MDIO 时序底细为什么 GPIO 模拟能跑通 SMI2.1 MDC/MDIO 帧结构读、写、前导码各占多少位MDC/MDIO 是 IEEE 802.3 定义的串行管理总线管理接口叫 SMISerial Management Interface。MDC 是管理时钟MDIO 是双向数据线。所有 PHY 寄存器操作都靠一帧一帧的串行数据完成。标准时序里一帧总共 64 位按顺序拆开是这么排的字段位数读命令取值写命令取值前导码Preamble32 位全 1全 1起始码Start of Frame2 位0101操作码Opcode2 位10读01写PHY 地址5 位0~310~31寄存器地址5 位0~310~31转换位Turnaround2 位读时由三态转由 PHY 驱动10数据位16 位读回 PHY 寄存器值写入 PHY 寄存器值读操作和写操作最核心的差别就在转换位。写操作里主机在转换位期间输出两个高电平随后继续驱动 16 位数据读操作里主机在转换位的前半段释放 MDIO 线置高阻输入由 PHY 芯片接管在第二个时钟周期开始驱动数据。GPIO 模拟时序时最难处理的也就是这个总线方向切换的瞬间。有的 PHY 芯片在特定条件下支持省略前导码的短帧比如一些交换芯片内部的 PHY 管理接口。但用 GPIO 模拟时我强烈建议保留完整的 32 位前导码原因后面避坑章节会讲。MDC 时钟频率标准要求不高于 2.5MHz实际软件模拟时通常会远低于这个值只要时序稳定慢一点完全没问题。2.2 选择 GPIO 模拟而非硬件 MDIO 控制器的理由硬件 MDIO 控制器在很多 SoC 上都是集成好的配置 PHY 地址、寄存器地址、数据位控制器自己完成时序生成。但能用不代表它在任何板卡上都可用。最常见的几个限制第一个MDIO 控制器引脚被复用到其他功能上BSP 里引脚复用配置改起来牵扯甚广牵一发动全身第二个硬件控制器的时序寄存器参数在某些 PHY 上兼容性不好特别是老 PHY 的上拉/下拉要求差异大硬件控制器跟不上第三个也是最实际的一种你在调试的板卡压根就没把 MDIO 引脚引出来只有一个调试 GPIO 可以通过飞线搭到 PHY 的管理引脚上。GPIO 模拟相比硬件控制器有两个明显优势。一是时序完全由代码控制MDC 频率、采样点、转换位宽度都可以随时调整排查 PHY 异常时可以对某个时钟沿反复观测二是不依赖 BSP 里的控制器驱动只要 GPIO 能输出、能读输入这套 SMI 时序就成立。缺点是 CPU 占用高因为一帧 64 位全是 CPU 空等出来的一秒读几百次寄存器就会把任务卡得很难看。它适合调试和低频率管理操作不适合大数据量的管理帧交互。3. VxWorks 下 GPIO 模拟的落地实现初始化、读写与任务适配3.1 引脚规划与 GPIO 初始化先明确一个前提这套代码在 VxWorks 5.5/Cyclone 类板级 BSP 上跑过但核心逻辑跟操作系统绑定很弱换到 VxWorks 7 或者其他 RTOS 也能复用。引脚规划时MDC 必须是输出引脚MDIO 必须是双向引脚。有的 GPIO 控制器支持输入输出方向动态切换这是必备条件如果 MDIO 引脚固定为输入或输出那这个方案直接不成立。常见做法是为 MDC 和 MDIO 各分配一个引脚号然后在驱动入口做初始化。下面的代码示意了 VxWorks 下常见的 GPIO 读写抽象#define MDC_GPIO 0x0A /* MDC 时钟引脚 */ #define MDIO_GPIO 0x0B /* MDIO 数据引脚 */ /* 板级 GPIO 操作函数由具体 BSP 提供或自行封装 */ static void gpio_write(int pin, int val) { /* 调用 BSP 的 gpio 输出函数 */ } static int gpio_read(int pin) { /* 调用 BSP 的 gpio 输入函数 */ } static void gpio_dir(int pin, int dir) { /* 1 表示输出0 表示输入 */ } void mdio_gpio_init(void) { gpio_dir(MDC_GPIO, 1); gpio_dir(MDIO_GPIO, 0); gpio_write(MDC_GPIO, 0); gpio_write(MDIO_GPIO, 1); /* MDIO 空闲时拉高等待外部上拉生效 */ }代码里注意一个细节MDIO 初始化方向先设为输入是因为 MDIO 走的是开漏/上拉逻辑空闲时要靠外部上拉电阻维持在逻辑高PHY 才能识别总线的空闲状态。如果 MCU 侧把 MDIO 强制输出低电平总线就一直被拉死PHY 根本没机会说话。3.2 写时序实现写操作的一帧里前导码、起始码、操作码、PHY 地址、寄存器地址、转换位、数据位全部由主机驱动MDIO 始终保持输出方向代码相对简单。核心在于每个 bit 的输出都要跟随 MDC 产生一个完整的时钟周期。下面是一段扎实的写寄存器实现static void mdio_delay(void) { /* 空循环延时让 MDC 保持在 500kHz~1MHz 附近 */ volatile int i; for (i 0; i 50; i); } static void mdio_put_bit(int bit) { /* MDIO 数据在 MDC 上升沿前的低电平阶段放稳 */ gpio_write(MDIO_GPIO, bit); gpio_write(MDC_GPIO, 1); mdio_delay(); gpio_write(MDC_GPIO, 0); mdio_delay(); } static void mdio_put_bits(int val, int len) { int i; /* 高位先发与 SMI 协议要求的 MSB-first 一致 */ for (i len - 1; i 0; i--) { mdio_put_bit((val i) 0x01); } } int mdio_write(int phy_addr, int reg_addr, unsigned short val) { int i; /* 前导码32 个连续 1 */ for (i 0; i 32; i) { mdio_put_bit(1); } mdio_put_bits(0x01, 2); /* 起始码 01 */ mdio_put_bits(0x01, 2); /* 写操作码 01 */ mdio_put_bits(phy_addr 0x1F, 5); mdio_put_bits(reg_addr 0x1F, 5); mdio_put_bits(0x02, 2); /* 转换位 10 */ mdio_put_bits(val, 16); /* 数据 */ return 0; }逻辑说明前导码用循环逐位输出代码里不起变量计数以外的花活是故意为之——保证每一位的时钟周期长度完全一致。MDIO 数据的放置放在 MDC 低电平阶段是因为 SMI 协议本质上要求数据在 MDC 上升沿时稳定提前半拍放数据从 GPIO 到 PHY 引脚之间即使有几百纳秒的电容负载在上升沿到来时也已经趋于稳定。这里的 mdio_delay 决定 MDC 频率值大小需要根据实际板卡总线负载调整后文避坑处细说。3.3 读时序实现与方向切换读时序真正考验代码功底的是方向切换。前 322255 位都由主机输出转换位前 1 拍要把 MDIO 引脚从输出切到输入然后让 PHY 在接下来的时钟里把数据放在总线上。采样点在下降沿而不是上升沿是为了避开数据变化瞬间的不稳定区。int mdio_read(int phy_addr, int reg_addr, unsigned short *val) { int i; int data 0; int bit; /* 前导码 起始码 读操作码 地址段全部由主机驱动 */ for (i 0; i 32; i) { mdio_put_bit(1); } mdio_put_bits(0x01, 2); mdio_put_bits(0x02, 2); /* 读操作码 10 */ mdio_put_bits(phy_addr 0x1F, 5); mdio_put_bits(reg_addr 0x1F, 5); /* 转换位第一个时钟主机释放 MDIO置高阻 */ gpio_dir(MDIO_GPIO, 0); gpio_write(MDC_GPIO, 1); mdio_delay(); gpio_write(MDC_GPIO, 0); mdio_delay(); /* 转换位第二个时钟PHY 开始驱动数据主机只读 */ gpio_write(MDC_GPIO, 1); mdio_delay(); gpio_write(MDC_GPIO, 0); mdio_delay(); /* 读取 16 位数据在 MDC 下降沿之后、下一个上升沿之前采样 */ for (i 15; i 0; i--) { gpio_write(MDC_GPIO, 1); mdio_delay(); gpio_write(MDC_GPIO, 0); mdio_delay(); bit gpio_read(MDIO_GPIO); if (bit) { data | (1 i); } } *val data; return 0; }这段代码里采样时机是关键每个 bit 的读取放在 MDC 拉低并延时之后。为什么不在拉高时读因为 PHY 通常在 MDC 上升沿切换 MDIO 数据上升沿附近的数据线上还有过渡毛刺降低了被 env 干扰的概率。有的 GPIO 在读输入时有滞后稍微延后一拍再读反而是经验的体现。转换位第一拍方向切换后紧接着给了整整一个时钟周期的高阻空档这个空档对电容负载偏大的 PCB 走线是必要的否则 PHY 驱动数据时可能和主机残留的输出电平打架。3.4 与 VxWorks 任务环境粘合VxWorks 下任务调度是抢占式的GPIO 模拟时序最怕的事情就是读到一半任务被切走。一帧 64 位延时怎么也有几十微秒这段窗口里系统任务调度一发生时序就断成两截PHY 直接丢弃整个帧。所以读取和写入函数需要做成临界区保护。常见做法有两种。第一种是用 taskLock/taskUnlock 关调度代码简单但会阻塞其他高优先级任务几十微秒第二种是给 SMI 读写加一个互斥信号量同一时刻只允许一个任务操作 MDIO 总线同时配合 taskLock 保证单次帧内不被抢占。下面的示意代码给出了第二种方案SEM_ID mdio_sem; void mdio_sem_init(void) { mdio_sem semBCreate(SEM_Q_PRIORITY, SEM_FULL); } int mdio_read_safe(int phy_addr, int reg_addr, unsigned short *val) { int ret; semTake(mdio_sem, WAIT_FOREVER); taskLock(); /* 关任务调度保证帧内不被打断 */ ret mdio_read(phy_addr, reg_addr, val); taskUnlock(); semGive(mdio_sem); return ret; }信号量防止两个任务同时对同一 PHY 发起操作taskLock 防止帧内被抢占。有一点要注意taskLock 只关闭同优先级任务调度如果板子上有中断服务程序也去动 GPIO那就得用 intLock 级别的保护但 SMI 帧在中断里执行会让中断处理时间爆表设计上应当避免在中断里直接操作 MDIO而是通过消息队列把请求转给专用管理任务。4. 避坑GPIO 模拟 MDC/MDIO 常见的爆炸现场与排查思路4.1 现象读寄存器永远返回 0xFFFF原因一PHY 的 MDIO 引脚上拉电阻缺失。MDIO 是双向开漏线外部必须接一个 1.5k~10k 的上拉电阻没有上拉时总线浮空读回的全是 1。解决确认 PHY 芯片数据手册对 MDIO 引脚的外部电路要求补上拉电阻或用万用表量空闲时的 MDIO 电平应该稳定在 2.5V 以上。原因二PHY 地址不匹配。SMI 帧里的 phy_addr 和硬件拨码开关或者 PHY 的 ADDR 引脚配置不一致PHY 直接忽略这个帧。解决挨个遍历 0~31 地址找到能正确响应读 PHY ID寄存器 2/3的地址。原因三前导码没发够 32 个 1或者被省略。有的 PHY 对省略前导码支持不好干脆当成非法帧丢弃。解决代码里前导码循环次数改成 32 再试不要一开始就为省时间优化掉它。4.2 现象MDC 时钟太快导致帧被丢弃有的工程师觉得 GPIO 模拟嘛能快则快把延时删到极少结果发现读回来的数据有时正常有时全 FF。原因是 MDC 频率超过了 PHY 芯片允许的上限。标准是 2.5MHz软件模拟时因为 GPIO 和 PHY 之间的走线电容、PHY 内部输入滤波很容易在高频下变形。解决把 mdio_delay 里的循环次数从 0 逐步加大用示波器看 MDC 实际频率压到 1MHz 以下基本稳。血泪经验是模拟总线的频率宁可慢三倍也不要为了几百微秒的性能去和 PHY 的时序上限博弈。4.3 现象读时序里第一个数据位总是错原因出在方向切换上。转换位第一个时钟拉低后代码立刻切到输入模式但 GPIO 方向切换本身有建立时间紧跟着的下一个时钟上升沿PHY 可能还在驱动上一位的电平数据线上的电平没来得及切换。解决给方向切换后的第一个时钟留足低电平时长代码里表现为 gpio_dir 之后马上跟两个 mdio_delay 再拉高 MDC。另外确认一下 MDIO 引脚在输入模式下是否使能了内部上拉如果内部上拉太强会和 PHY 的输出形成分压读回的电平阈值反而出问题。4.4 现象跑一段时间后总线卡死读什么都超时GPIO 模拟是纯软件协议不存在硬件控制器那种总线错误状态机卡死多半是代码路径出了问题。常见原因前一次读时序因为 ISR 抢占中断MDIO 方向还停留在输出下一次调用直接开始怼数据和 PHY 的输出撞车。解决在每次读函数开头强制把 MDIO 设置成输入等一个时钟周期后再发前导码同时在写函数开头强制设为输出。这个方向恢复动作看起来多余实际能省掉大量玄学问题。4.5 调试手段逻辑分析仪比示波器好使纯看示波器去数 64 位序列几乎不可能强烈建议拉一根逻辑分析仪到 MDC/MDIO 测试点协议解码里选择 MDIO 协议。逻辑分析仪能看到完整帧的字段解析PHY 地址、寄存器地址、数据一目了然判断是哪一段出问题比瞎猜快得多。如果没有逻辑分析仪可以在代码的采样点后面加调试打印把每个 bit 的电平打出来虽然慢但足够定位方向切换错位这类问题。5. 验证与扩展回环测试、PHY ID 探测与通用 SMI 封装5.1 用 PHY ID 确认总线通了驱动写完先不急着配链路第一件事是读 PHY ID。PHY ID 是寄存器 2低 16 位和寄存器 3高 16 位合计 32 位厂家和型号信息都藏在里面。读到的值和 PHY 数据手册一致说明 GPIO 模拟的 SMI 时序已经完全打通后续所有寄存器操作都有基础。unsigned short phy_id_low; unsigned short phy_id_high; int phy_addr 0x01; mdio_read(phy_addr, 0x02, phy_id_low); mdio_read(phy_addr, 0x03, phy_id_high); printf(PHY ID 0x%04X%04X\r\n, phy_id_high, phy_id_low);正常读出来的 PHY ID 不是全 0 也不是全 F。全 0 说明 PHY 没有驱动应答去看地址和上拉全 F 说明总线上根本没设备检查引脚配置和供电。5.2 寄存器回环测试链路调试前更值得先做的是回环测试。PHY 的寄存器 0控制寄存器bit14 是回环模式使能位置 1 后 PHY 会在内部把发送数据直接环回到接收路径。这个测试可以分层验证MAC 侧发数据PHY 侧回环收回来一致说明 MAC 和 PHY 之间的数据通路没问题之后再开 normal mode 查外部链路。unsigned short ctrl; mdio_read(phy_addr, 0x00, ctrl); ctrl | (1 14); /* 开启回环 */ mdio_write(phy_addr, 0x00, ctrl); mdio_read(phy_addr, 0x00, ctrl); printf(Ctrl reg 0x%04X\r\n, ctrl);注意回环测试完成后一定要把 bit14 清掉有些 PHY 在回环模式下自动协商功能会失效忘了关会让后续外联测试出现诡异现象。5.3 封装成通用 SMI 接口底层时序和上层寄存器访问分开是我拆这套代码时比较认可的结构。底层保留 mdio_read/mdio_write 两个函数负责帧格式和方向切换上层提供 phy_read_reg/phy_write_reg 语义接口方便上层代码不清楚时序细节的人也能直接调。代码包里的功能基本就是这个分层拿到手后做两件事就够了改 GPIO 引脚号按自己的 PHY 地址手册调整地址遍历顺序。int phy_read_reg(int phy_addr, int reg) { unsigned short val; if (mdio_read(phy_addr, reg, val) ! 0) { return -1; } return val; } int phy_write_reg(int phy_addr, int reg, unsigned short val) { return mdio_write(phy_addr, reg, val); }从那以后我每次在 VxWorks 上用 GPIO 模拟 MDC/MDIO都会强制走一遍先读 PHY ID再开回环的验证流程。这套代码原来在我手里折腾过两个晚上才点亮问题都出在方向切换和延时上。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。