news 2026/10/26 10:35:22

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的 Makefile

VxWorks 的 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 位)、 turnaround(2 位)、数据(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 加写读回环,再谈其他功能。希望帮到你。

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

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

UE5大作DLSS 4超分、帧生成与光追配置及性能测试指南

《黎明行者之血》&#xff08;The Blood of Dawnwalker&#xff09;是 Rebel Wolves 工作室的首款作品&#xff0c;位于波兰华沙&#xff0c;核心成员来自《巫师 3》和《赛博朋克 2077》团队。公开资料显示&#xff0c;该作基于虚幻引擎 5 开发&#xff0c;由 Bandai Namco Ent…

作者头像 李华
网站建设 2026/10/26 10:34:46

dejsonlz4安装与.jsonlz4书签备份解压校验指南

简介&#xff1a;dejsonlz4 是面向 Firefox 用户、数据恢复与脚本开发者的开源小工具&#xff0c;用于处理以 lz4 压缩存储的 .jsonlz4 书签备份文件&#xff0c;解决这类非标准格式无法直接读取、迁移或恢复书签的问题。压缩包仅 22KB&#xff0c;共 7 个文件&#xff0c;主体…

作者头像 李华
网站建设 2026/10/26 10:34:44

Qt Creator 集成 CUDA:qmake 构建链配置与排错

简介&#xff1a;面向Windows平台在Qt Creator中集成CUDA的示例工程包&#xff0c;适合具备Qt基础、希望借助NVIDIA GPU开展通用计算的开发者参考。包内共7个文件&#xff0c;以2个cpp源文件、1个ui界面文件、1个cu内核文件、1个h头文件及pro工程配置为主&#xff0c;另有user文…

作者头像 李华
网站建设 2026/10/26 10:34:08

PyAutoGUI 0.9.26 桌面自动化:坐标、图像定位与稳定交付

简介&#xff1a;PyAutoGUI 0.9.26 的源码软件包&#xff0c;面向希望用 Python 完成鼠标键盘模拟、桌面自动化与界面测试的开发者&#xff0c;也适合初学者借助示例理解跨平台 GUI 控制流程&#xff0c;解决重复性桌面操作与脚本化交互问题。包内共 37 个文件&#xff0c;以 1…

作者头像 李华
网站建设 2026/10/26 10:34:08

PyAutoGUI 0.9.26 桌面自动化:安装避坑、坐标定位与时序控制

简介&#xff1a;PyAutoGUI-0.9.26.zip 面向 Python 开发者与自动化测试人员&#xff0c;是一份 GUI 自动控制库的完整源码包&#xff1a;用脚本模拟鼠标移动、点击、拖拽、滚动与键盘输入&#xff0c;把重复的桌面操作交给程序执行&#xff0c;支持 Windows、Mac OS X 与 Linu…

作者头像 李华
网站建设 2026/10/26 10:34:06

SQL Server+Swing宾馆管理系统:事务驱动的房态账目闭环

简介&#xff1a;面向数据库课程设计与Java桌面开发入门者的宾馆管理系统项目&#xff0c;将SQL Server数据库设计与Swing图形界面整合为可运行的酒店业务案例&#xff0c;适合需要完成课设、课程作业或自学JDBC与桌面开发的学生参考。压缩包共40个文件、约2.02MB&#xff0c;含…

作者头像 李华