VxWorks下GPIO模拟MDIO:SMI时序驱动开发与PHY调试实战

发布时间:2026/10/26 10:35:26
VxWorks下GPIO模拟MDIO:SMI时序驱动开发与PHY调试实战 简介这份资源面向在 VxWorks 环境下开发以太网 PHY 驱动的嵌入式工程师聚焦于缺少专用 SMI 硬件接口时如何用普通 GPIO 引脚模拟 MDC 时钟与 MDIO 双向数据线完成对 PHY 芯片寄存器的读写配置。压缩包内共 1 个 C 源文件约 2KB核心实现涵盖 GPIO 方向初始化、MDC 时钟周期生成、命令与地址位发送、下降沿数据采样及错误检测等环节可作为移植与调试的参考样例。已有 1561 人学习下载说明该实践在无专用管理接口的板卡上具有普遍需求。读者可从中了解软件模拟时序的精度控制方法、读写函数的组织方式以及如何结合系统延时函数逼近 2.5MHz 时钟进而对照自身 BSP 快速搭建可用的 MDIO 访问通道减少从协议理解到代码落地之间的反复试错。1. 用两根普通 GPIO 把 MDIO 时序“掰”出来手上有一块板子PHY 芯片的 MDC/MDIO 引脚没接到主控的专用 MAC 控制器上而是随手连到了两个普通 GPIO。硬件已经定版飞线不现实这时候要么改板要么用软件把 SMI 时序一位一位地“掰”出来。这份gpio模拟的mdc mdio驱动代码.rar干的就是这件事在 VxWorks 下用 GPIO 翻转模拟 MDC 时钟和 MDIO 双向数据线实现符合 IEEE 802.3 的 SMI 读写让上层还能像操作真 MAC 一样去读 PHY 寄存器。适合正在做 VxWorks BSP 移植、PHY 调试、或者被“MAC 没引出 MDIO”卡住的嵌入式驱动开发者尤其是需要读 PHY ID、配自协商、查链路状态的那批人。2. 先看清结构GPIO 模拟 SMI 的驱动分层与文件组织拿到一个压缩包别急着编译。先搞清楚它到底由哪几块组成、每块归谁管后面改起来才不会牵一发动全身。这个项目的本质是把“硬件 SMI 控制器”换成“软件时序发生器”所以它的分层思路和标准 MAC 驱动是一致的只是最底下那层从寄存器写变成了 GPIO 翻转。2.1 驱动分层应用接口、SMI 时序、GPIO 抽象三层从职责上切代码大致分三层。最上面是对外的 PHY 访问接口通常就是phyRead/phyWrite这类函数供 BSP 初始化、链路检测、自协商配置调用中间是 SMI 帧的组装与解析负责把寄存器地址、PHY 地址、操作码拼成 32 位帧再按位驱动时钟最底下是 GPIO 读写抽象把“拉高 MDC”“读 MDIO”“切 MDIO 方向”封装成几个小函数。这样分层的好处很直接换一块板子只要改 GPIO 抽象层里的引脚号和方向切换方式SMI 时序和上层接口一行不用动。我见过不少人图省事把 GPIO 操作直接写进时序循环里结果换板子时满文件找gpioPinSet血泪经验就是别这么干。常见的文件组织大概是这样具体文件名以包内为准这里按这类项目的通用做法描述gpio_mdio/ ├── gpioMdioSmi.c # SMI 帧组装、时序、phyRead/phyWrite ├── gpioMdioSmi.h # 对外接口与配置结构体 ├── gpioMdioHw.c # GPIO 引脚初始化、方向切换、电平读写 └── gpioMdioHw.h # 引脚宏、板级配置如果包里的文件命名不同按“时序”和“硬件抽象”两个关键词去找对应文件即可逻辑归属不会变。2.2 关键数据结构把引脚和延时参数收进一个配置块这类驱动最怕参数散落一地。合格的做法是定义一个配置结构体把 MDC 引脚、MDIO 引脚、半周期延时、以及可选的“MDIO 是否需要手动切方向”都收进去。上层初始化时填一次后面时序函数只读这个结构体。/* gpioMdioSmi.h —— 板级配置集中管理 */ typedef struct { int mdcPin; /* MDC 时钟引脚编号 */ int mdioPin; /* MDIO 双向数据引脚编号 */ int halfPeriodUs; /* MDC 半周期延时单位微秒 */ int phyAddr; /* 默认 PHY 地址0~31 */ BOOL mdioBidir; /* MDIO 是否为双向引脚需要切方向 */ } GPIO_MDIO_CFG;halfPeriodUs是最容易被忽略的参数。IEEE 802.3 规定 MDC 最高 25MHz也就是半周期 20ns但 GPIO 翻转加函数调用开销根本到不了这个速度实际工程里通常压到 1MHz 以下半周期 500ns 以上反而更稳。mdioBidir决定读操作时要不要把 MDIO 从输出切成输入有些 SoC 的 GPIO 支持开漏自动方向有些必须手动切这个标志就是给两种硬件留的口子。2.3 编译接入把源文件挂进 BSP 的 MakefileVxWorks 的 BSP 一般用Makefile或Makefile.prj管理源文件。把.c文件加进SRCS或对应的对象列表头文件路径加进CFLAGS的-I里是最基础的一步。# 片段示意把驱动源文件编进 BSP SRCS gpioMdioSmi.c gpioMdioHw.c CFLAGS -I$(WIND_BASE)/target/h/drv/gpioMdio改完 Makefile 后建议先只编驱动、不链接整个镜像用make gpioMdioSmi.o这类目标单独验证语法和头文件依赖比整包编译失败后再回头找错快得多。编译通过只是第一步真正的坑在时序和方向切换上下一章细说。3. 核心时序拆解一帧 SMI 是怎么被 GPIO 一位位送出去的SMI 帧结构是固定的32 位里前 32 个时钟周期依次是前导码32 个 1、起始码01、操作码读 10 / 写 01、PHY 地址5 位、寄存器地址5 位、 turnaround2 位、数据16 位。用 GPIO 模拟就是把“每个时钟周期在 MDC 上升沿采样 MDIO”这件事用代码复现出来。这一章按写操作、读操作、延时控制三块拆。3.1 写寄存器从帧组装到逐位输出的完整链路写操作的时序相对简单因为 MDIO 全程由主控驱动。核心是把 32 位帧按高位先出的顺序逐位放到 MDIO 上每放一位翻转一次 MDC。/* gpioMdioSmi.c —— SMI 写寄存器 */ STATUS gpioMdioWrite(UINT32 phyAddr, UINT32 regAddr, UINT16 data) { UINT32 frame; int i; /* 组装 32 位帧前导码 起始 写操作码 PHY 寄存器 turnaround 数据 */ frame 0xFFFFFFFF; /* 前导码 32 个 1实际发送时截取 */ frame (0x5 28); /* 起始码 01 写操作码 01合起来 0101 */ frame | ((phyAddr 0x1F) 23); /* 5 位 PHY 地址 */ frame | ((regAddr 0x1F) 18); /* 5 位寄存器地址 */ frame | (0x2 16); /* turnaround 写操作为 10 */ frame | (data 0xFFFF); /* 16 位数据 */ gpioMdioSetDirOutput(); /* 写操作 MDIO 全程输出 */ /* 先发 32 个前导 1 */ for (i 0; i 32; i) { gpioMdioDataSet(1); gpioMdcPulse(); } /* 再发 32 位有效帧高位先出 */ for (i 31; i 0; i--) { gpioMdioDataSet((frame i) 0x1); gpioMdcPulse(); } return OK; }这段代码有两个点值得说。第一前导码单独发 32 个 1而不是塞进frame里是因为frame只有 32 位塞不下前导码加有效帧共 64 个周期分开写更清晰。第二gpioMdcPulse()内部完成“拉高—延时—拉低—延时”的完整周期保证 MDIO 在 MDC 上升沿前已经稳定。参数上phyAddr和regAddr都做了 0x1F掩码防止上层传越界值导致帧错位这种防御性写法在驱动里很常见。3.2 读寄存器turnaround 阶段的方向切换是成败关键读操作比写多一个坑turnaround 阶段 MDIO 要从主控输出切到 PHY 输出主控必须及时把引脚切成输入否则会和 PHY 抢总线。很多“读出来全是 0xFFFF 或 0x0000”的问题都出在这里。/* gpioMdioSmi.c —— SMI 读寄存器 */ STATUS gpioMdioRead(UINT32 phyAddr, UINT32 regAddr, UINT16 *data) { UINT32 frame; UINT16 value 0; int i; frame (0x6 28); /* 起始码 01 读操作码 10合起来 0110 */ frame | ((phyAddr 0x1F) 23); frame | ((regAddr 0x1F) 18); gpioMdioSetDirOutput(); for (i 0; i 32; i) { /* 前导码 */ gpioMdioDataSet(1); gpioMdcPulse(); } for (i 31; i 16; i--) { /* 先发帧的高 16 位含 turnaround 前段 */ gpioMdioDataSet((frame i) 0x1); gpioMdcPulse(); } gpioMdioSetDirInput(); /* turnaround 后切输入交给 PHY 驱动 */ for (i 15; i 0; i--) { /* 读 16 位数据高位先入 */ gpioMdcPulse(); value | (gpioMdioDataGet() 0x1) i; } *data value; return OK; }这里frame只组装到寄存器地址为止turnaround 和读数据交给后面的循环处理。切输入的位置在发完寄存器地址之后、读第一个数据位之前正好对应 turnaround 的第一个周期。gpioMdcPulse()在读循环里先调用再采样是因为 MDIO 在 MDC 上升沿有效先产生上升沿再读才符合时序。如果读出来数据整体偏移一位八成是这里采样点放错了。3.3 延时控制为什么 GPIO 模拟不能盲目追高时钟gpioMdcPulse()是整个驱动的节奏心脏它的实现直接决定时序是否稳定。/* gpioMdioHw.c —— MDC 时钟脉冲与延时 */ void gpioMdcPulse(void) { gpioPinSet(mdcPin, 1); /* MDC 上升沿 */ udelay(gCfg.halfPeriodUs); /* 保持高电平 */ gpioPinSet(mdcPin, 0); /* MDC 下降沿 */ udelay(gCfg.halfPeriodUs); /* 保持低电平 */ }udelay的精度依赖系统时钟节拍VxWorks 下通常能到微秒级。halfPeriodUs取 1 意味着 MDC 约 500kHz取 0 则完全靠 GPIO 翻转和函数调用本身的开销速度不可控容易在长线或干扰环境下翻车。我一般会把半周期设到 1~2 微秒牺牲一点速度换稳定毕竟 PHY 寄存器读写不是高频操作一次链路检测也就读几个寄存器慢一点完全无感。提示如果板子上 MDC/MDIO 走了长排线或经过电平转换芯片适当加大halfPeriodUs并考虑在 MDIO 上加弱上拉能明显减少读数据偶发错误。4. 跑起来从引脚配置到读通 PHY ID 的验证步骤代码编过不等于能用。GPIO 模拟 SMI 的验证要一层层往上走先确认引脚能翻转再确认帧能发出最后用读 PHY ID 做端到端验收。这一章按这个顺序给可执行的步骤。4.1 引脚初始化与方向控制上电后第一件事是把 MDC 配成输出、MDIO 配成可切换方向并给一个确定的初始电平。MDC 空闲时为低MDIO 空闲时靠上拉为高。/* gpioMdioHw.c —— 引脚初始化 */ STATUS gpioMdioHwInit(GPIO_MDIO_CFG *cfg) { gCfg *cfg; /* 保存板级配置 */ gpioPinConfigOutput(cfg-mdcPin); /* MDC 固定输出 */ gpioPinSet(cfg-mdcPin, 0); /* 空闲低电平 */ if (cfg-mdioBidir) { gpioPinConfigBidir(cfg-mdioPin); /* 双向引脚运行中切方向 */ } else { gpioPinConfigOutput(cfg-mdioPin); } gpioPinSet(cfg-mdioPin, 1); /* 空闲高电平配合外部上拉 */ return OK; }gpioPinConfigBidir的具体实现依赖 SoC 的 GPIO 驱动有的平台叫gpioPinConfigOpenDrain有的要单独配方向寄存器。这一步没配对后面读操作切输入时引脚可能仍处于输出态直接表现为读回值恒定。初始化完成后建议先用调试器或点灯的方式确认 MDC 引脚真的在翻转别急着跑完整读写。4.2 用读 PHY ID 做端到端验收PHY 的寄存器 2 和 3 是 ID 寄存器值固定且可预期是验证 SMI 通不通的最佳探针。/* 验证片段读 PHY ID 并打印 */ void gpioMdioSelfTest(void) { UINT16 idHi 0, idLo 0; if (gpioMdioRead(gCfg.phyAddr, 2, idHi) ! OK || gpioMdioRead(gCfg.phyAddr, 3, idLo) ! OK) { logMsg(SMI read failed\n, 0, 0, 0, 0, 0, 0); return; } logMsg(PHY ID 0x%04x%04x\n, idHi, idLo, 0, 0, 0, 0); }读出来的 ID 应该和 PHY 芯片手册上的 OUI 加型号对得上。如果读到0xFFFF通常是 MDIO 没切输入或上拉缺失读到0x0000多半是 MDC 没真正翻转或 PHY 地址填错。这一步过了说明时序、方向、地址全对后面配自协商、读链路状态就是顺水推舟。4.3 常见失败现象与对应检查点把验证过程中最容易出现的几种现象列成表出问题时按行排查比盲改快。现象优先检查说明读回恒为 0xFFFFMDIO 方向切换、上拉电阻引脚仍为输出或总线无上拉读回恒为 0x0000MDC 是否翻转、PHY 地址时钟没动或地址不匹配数据整体偏移一位读循环采样点上升沿前后采样顺序错偶发读错、重试才好halfPeriodUs太小加大半周期或加延时写成功读失败turnaround 切输入时机切早了或切晚了这张表不是万能但覆盖了 GPIO 模拟 SMI 八成以上的翻车场景。真遇到表里没有的回到“先确认引脚翻转、再确认帧、最后确认方向”这条主线基本都能定位。5. 避坑与排查GPIO 模拟 MDIO 最容易踩的五个坑这一章单独拎出来是因为下面这几条几乎每个用 GPIO 模拟 SMI 的人都撞过写在这里省得重复交学费。每条按“现象 → 原因 → 解决”给。5.1 读 PHY 寄存器全是 0xFFFF现象gpioMdioRead返回成功但数据恒为0xFFFF。原因通常是 MDIO 在读阶段没有真正切成输入主控仍在驱动高电平或者外部上拉缺失导致总线浮空被读成高。解决确认gpioMdioSetDirInput()在 turnaround 后确实生效用示波器看读阶段 MDIO 是否由 PHY 驱动硬件上补 1.5k~10k 上拉。5.2 写进去的值读出来不对现象写寄存器后立刻读回值不一致。原因多半是写帧的 turnaround 位或操作码拼错也可能是phyAddr越界没掩码导致帧错位。解决对照 802.3 帧格式逐位核对frame的移位量确认写操作码是01、turnaround 是10并检查所有地址参数都做了 0x1F。5.3 换一块板子就完全不通现象同一份代码在原板子正常换板子后读不到 PHY。原因是 GPIO 引脚号、方向切换方式、甚至 MDC 空闲电平在不同板子上不一样而代码里写死了。解决把所有板级差异收进GPIO_MDIO_CFG引脚号和mdioBidir由 BSP 初始化时传入时序代码不出现任何具体引脚号。5.4 高时钟下偶发错误、降速就好现象halfPeriodUs设得很小大部分时候正常偶尔读错。原因是 GPIO 翻转加函数调用开销叠加实际半周期远小于设定值加上线路容性负载导致边沿变缓。解决把半周期提到 1~2 微秒别盲目追 25MHz长线场景再加 MDIO 上拉和串联电阻。5.5 多 PHY 场景下地址冲突现象板上有多个 PHY读某个地址总是读到另一个 PHY 的数据。原因是 PHY 地址由硬件 strapping 决定软件里phyAddr填错或总线上有地址重复。解决逐个地址扫描读 ID 寄存器确认每个 PHY 的实际地址再在配置里分别指定别假设地址从 0 连续排布。注意排查时优先用示波器看 MDC 和 MDIO 的实际波形比在代码里加打印快得多。GPIO 模拟的时序问题波形一照基本无所遁形。6. 二次开发把这份驱动改成可复用模块的几个手法这份代码拿来直接用没问题但要在多个项目间复用得做点改造。我一般从三个方向下手配置外置、接口对齐、加一层薄封装。第一个手法是把板级配置彻底外置。GPIO_MDIO_CFG不要在任何.c里写死初值而是由 BSP 的sysHwInit或类似入口填充后调用gpioMdioHwInit。这样同一份驱动源码可以同时服务多块板子靠不同的配置块区分。改造后验证方法很简单把配置里的phyAddr故意改错读 ID 应该失败改回来应该成功说明配置真的生效了。第二个手法是接口对齐标准 MAC 驱动的phyRead/phyWrite签名。很多上层代码自协商、链路监控是按标准接口写的如果你的gpioMdioRead参数顺序或返回类型不一致移植时就得改上层。包一层薄适配即可/* 适配标准 PHY 访问接口 */ STATUS phyRead(int phyAddr, int regAddr, UINT16 *data) { return gpioMdioRead((UINT32)phyAddr, (UINT32)regAddr, data); } STATUS phyWrite(int phyAddr, int regAddr, UINT16 data) { return gpioMdioWrite((UINT32)phyAddr, (UINT32)regAddr, data); }第三个手法是加超时和重试。GPIO 模拟在干扰环境下偶发错误是常态读操作失败重试两三次往往就好了。可以在gpioMdioRead外层包一个带重试的版本重试仍失败再上报避免上层因为一次偶发错误就判定链路断开。重试次数别设太多三次足够多了会拖慢链路检测周期。改造完怎么验证我的习惯是准备一个“已知答案”的测试读 PHY ID 两次比对、写一个厂商专用寄存器再读回、拔掉网线看链路状态寄存器是否变化。这三步过了基本可以放心往上层交。从那以后我每次改这类时序驱动都强制先跑一遍读 ID 加写读回环再谈其他功能。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询