资讯动态

VxWorks下GPIO模拟MDC/MDIO实现PHY寄存器读写的驱动方案解析

发布时间:2026/10/21 21:21:53 来源:尧图企业网站定制
简介面向VxWorks平台的GPIO模拟MDC/MDIO驱动源码专门用于解决嵌入式开发中缺乏专用SMI管理接口、无法直接配置以太网PHY芯片的难题。代码包仅含1个C源文件压缩后大小2KB结构紧凑适合直接阅读、移植到其他BSP或驱动项目中。驱动完整覆盖MDC时钟生成、MDIO双向数据收发、命令与地址发送、数据读取以及错误重试逻辑基于VxWorks延时函数精确模拟2.5MHz管理时钟每个周期400ns并严格遵循上升沿写入、下降沿读取的协议时序。通过源码可深入理解GPIO引脚方向切换、按位打包发送操作码/寄存器地址/16位数据、以及PHY管理寄存器的读写流程对从事VxWorks底层驱动开发、BSP适配或以太网接口调试的工程师极具参考价值。目前已有1561人学习下载这一实现方案在资源受限的小型化设备中尤为实用。1. 这个 GPIO 模拟 MDC/MDIO 的驱动解决的是 VxWorks 下没有 MDIO 控制器时的 PHY 寄存器读写难题做嵌入式网络开发的人大概都遇到过这种尴尬芯片本身没有独立 MAC或者 SoC 的 MDIO 控制器被复用掉了但你还得通过 SMI 总线去配置 PHY 的寄存器——速率、双工、auto-negotiation、link 状态全指望这个。这个 rar 包里的代码就是用两根普通 GPIO 分别模拟 MDC 时钟和 MDIO 数据线在 VxWorks 下从零实现了一个软件 SMI 控制器直接操作 PHY 寄存器行为和一个硬件 MDIO 控制器完全一致。它适合两类人一类是板子上的 MDIO 控制器不可用、正在找替代方案的驱动开发另一类是想搞清楚 MDC/MDIO 时序到底怎么翻转 GPIO准备自己动手写位操作协议的嵌入式学习者。我拆这个包的时候第一感觉就是它把 PHY 管理这块黑匣子给透明化了代码拿过来改改引脚就能跑。2. 协议原理先行MDC/MDIO 时序与 GPIO 位拆分的对应关系2.1 SMI 接口的帧格式一个周期一个周期地看MDC/MDIO 也叫 SMISerial Management Interface是 MII 管理总线。MDC 是时钟由主机产生规范上最高 2.5MHzMDIO 是双向数据线。要模拟它首先得把一帧完整的读写事务拆成 bit 序列。标准帧格式是32 个 1 的 preamble前导码、2 bit start01、2 bit op读是 10写是 01、5 bit PHY 地址、5 bit 寄存器地址、2 bit turnaround最后是 16 bit 数据。读和写的区别就在 turnaround 阶段写操作时主机继续驱动 MDIO 输出 10然后发送 16 bit 数据读操作时主机在 turnaround 的第一个 bit 释放总线改成输入方向由 PHY 在第 2 个 turnaround bit 开始驱动数据。从 GPIO 模拟的角度看整个帧就是一组连续的电平变化序列。写帧时MDIO 线上每个 bit 的电平在 MDC 上升沿被采样所以我的做法是先把 MDIO 设置成输出然后按位把电平写出去每写一个 bit 就翻转一次 MDC。读帧时前导码、start、op、地址部分照常由主机驱动但到 turnaround 阶段就要把 MDIO 引脚从输出切到输入在后续的 16 个 MDC 时钟上升沿去读引脚电平。#define MDIO_READ 0x02 /* op code: 10 */ #define MDIO_WRITE 0x01 /* op code: 01 */ /* 一次 MDIO 事务返回 16bit 数据 */ unsigned short mdio_transfer(unsigned char phy_addr, unsigned char reg_addr, unsigned short data, unsigned char op) { unsigned short val 0; int i; gpio_mdc_set(0); /* preamble: 32 个 1 */ for (i 0; i 32; i) { gpio_mdio_set(1); gpio_mdc_toggle(); } /* start: 01 */ gpio_mdio_set(0); gpio_mdc_toggle(); gpio_mdio_set(1); gpio_mdc_toggle(); /* op code: 读 10 / 写 01 */ for (i 1; i 0; i--) { gpio_mdio_set((op i) 1); gpio_mdc_toggle(); } /* PHY 地址 5 bit从高到低 */ for (i 4; i 0; i--) { gpio_mdio_set((phy_addr i) 1); gpio_mdc_toggle(); } /* 寄存器地址 5 bit从高到低 */ for (i 4; i 0; i--) { gpio_mdio_set((reg_addr i) 1); gpio_mdc_toggle(); } if (op MDIO_WRITE) { /* turnaround: 10 */ gpio_mdio_set(1); gpio_mdc_toggle(); gpio_mdio_set(0); gpio_mdc_toggle(); /* 16 bit 数据从高到低 */ for (i 15; i 0; i--) { gpio_mdio_set((data i) 1); gpio_mdc_toggle(); } } else { /* turnaround: 主机释放总线 */ gpio_mdio_set_input(); gpio_mdc_toggle(); /* TA bit1: PHY 还没驱动忽略 */ gpio_mdc_toggle(); /* TA bit2: PHY 开始驱动 */ /* 16 bit 数据MDC 上升沿采样 */ for (i 15; i 0; i--) { gpio_mdc_set(1); if (gpio_mdio_get()) val | (1 i); gpio_mdc_set(0); } gpio_mdio_set_output(); } gpio_mdc_set(0); return val; }这段代码是模拟 SMI 的核心骨架。逻辑说明所有 bit 都是高位先出地址和数据都按 MSB 顺序逐位移出读操作在 turnaround 之后才切输入避免和 PHY 驱动总线打架。参数说明phy_addr是 PHY 的 5 位地址取决于硬件上 PHY 的 AD[4:0] 引脚接法reg_addr是寄存器地址比如常见的 0x00BMCR 控制寄存器、0x01BMSR 状态寄存器、0x04ANAR、0x05ANLPARop用来区分读还是写。时序上每次gpio_mdc_toggle()内部要保证高低电平各保持足够时间才能让 PHY 正常采样。2.2 为什么不能用 taskDelay 做延时MDC 频率怎么控制MDC 时钟最高 2.5MHz对应最小周期 400ns。也就是一个完整的 MDC 周期里高电平和低电平各至少要 200ns。在 VxWorks 里taskDelay()的最小粒度是一个 system tick通常 1ms 到 16ms 不等这比 200ns 大了几个数量级根本没法用。正确做法是空循环或者读硬件定时器做精确延时。/* 用空循环做纳秒级延时频率可调 */ static void mdc_delay(unsigned int ns) { volatile unsigned int i; unsigned int loops ns / 10; /* 按主频粗略折算 */ for (i 0; i loops; i) ; } static void gpio_mdc_set(int val) { gpio_write_pin(MDC_GPIO, val); mdc_delay(250); /* 半周期 250nsMDC 频率约 2MHz */ } static void gpio_mdc_toggle(void) { gpio_write_pin(MDC_GPIO, 1); mdc_delay(250); gpio_write_pin(MDC_GPIO, 0); mdc_delay(250); }这里的mdc_delay参数是按主频粗略估的实际必须在目标板上用示波器量一下 MDC 引脚。常见做法是先给一个偏大的延时量出来周期偏慢再逐步往下调。我一般会把半周期定在 250ns也就是 MDC 跑 2MHz既满足规范的小于 2.5MHz又给 GPIO 翻转和电平稳定留了余量。参数说明ns是期望延时纳秒数loops是空循环次数这个折算系数和 CPU 主频、流水线深度强相关换板子必须重新标定。另一个更稳的思路是直接用 VxWorks 的sysTimestamp或者板级定时器但空循环在这个场景下够用还不用开额外驱动。2.3 从 GPIO 到 PHY寄存器读写如何被封装成上层接口有了mdio_transfer这个原语PHY 的寄存器读写就很直接了。读寄存器就是构造一个读帧写寄存器就是构造一个写帧。我在拆包时注意到代码里在mdio_transfer之上又封了一层phy_read和phy_write用来屏蔽掉每次传 op 码的麻烦同时把错误处理收拢到一处。unsigned short phy_read(unsigned char phy_addr, unsigned char reg_addr) { return mdio_transfer(phy_addr, reg_addr, 0, MDIO_READ); } int phy_write(unsigned char phy_addr, unsigned char reg_addr, unsigned short data) { mdio_transfer(phy_addr, reg_addr, data, MDIO_WRITE); return 0; }这两行封装的价值在于上层代码再也不用关心 SMI 协议的细节拿到 PHY 地址和寄存器地址就能直接访问。比如要读 BMSR 看 link 状态调用phy_read(phy_addr, 0x01)就行要发起自协商往 BMCR 写 0x1000 即可。我还注意到驱动里给phy_read加了返回值容错——如果 MDC/MDIO 引脚配置错了读出来往往是 0xFFFF 或者全 0这个特征在后面排查章节会重点讲。参数说明phy_addr要和硬件跳线对上reg_addr是 IEEE 802.3 标准定义的 MII 寄存器地址不同 PHY 厂商扩展寄存器在 0x10 之后用法一样。3. 驱动代码拆解从文件结构到 GPIO 底层操作3.1 rar 包里的文件布局一个驱动该有的东西它都有打开这个 rar 包文件比我预想的完整。有用的是 4 个文件gpio_mdio.h是对外接口声明gpio_mdio.c是协议实现主体gpio_hw.c是 GPIO 硬件操作层mdio_test.c是一个自测工具。另外还有一个Makefile和一份简单的 README说明编译方式和引脚定义。这个分层的思路是对的协议和硬件操作分离。gpio_mdio.h声明phy_read/phy_write/gpio_mdio_init以及 MDC/MDIO 引脚宏定义gpio_mdio.cmdio_transfer帧收发、phy_read/phy_write封装gpio_hw.cgpio_write_pin/gpio_read_pin/gpio_mdio_set_input/gpio_mdio_set_outputmdio_test.c扫描全部 PHY 地址读取寄存器 0x02 和 0x03PHY ID并打印这个文件划分很典型。协议层完全不知道 GPIO 是怎么翻转的换平台只改gpio_hw.c协议层一行不用动。测试工具单独放一个文件也方便在板子上快速验证驱动有没有通。3.2 GPIO 硬件操作层方向切换是关键MDIO 是双向引脚读的时候要输入写的时候要输出。这个方向切换在 VxWorks 里怎么做决定了驱动能不能跑通。gpio_hw.c里用的是直接操作 GPIO 控制器寄存器的方式这个思路在多数 SoC 上都成立。/* 以某 SoC 的 GPIO 控制器为例基地址和寄存器偏移按平台调整 */ #define GPIO_BASE 0xE0000000 #define GPIO_DIR_OFF 0x00 #define GPIO_DATA_OFF 0x04 static void gpio_write_pin(unsigned int pin, int val) { unsigned int tmp; if (val) *(volatile unsigned int *)(GPIO_BASE GPIO_DATA_OFF) | (1 pin); else *(volatile unsigned int *)(GPIO_BASE GPIO_DATA_OFF) ~(1 pin); } static int gpio_read_pin(unsigned int pin) { return (*(volatile unsigned int *)(GPIO_BASE GPIO_DATA_OFF) pin) 1; } static void gpio_mdio_set_input(void) { unsigned int tmp; tmp *(volatile unsigned int *)(GPIO_BASE GPIO_DIR_OFF); tmp ~(1 MDIO_GPIO); *(volatile unsigned int *)(GPIO_BASE GPIO_DIR_OFF) tmp; } static void gpio_mdio_set_output(void) { unsigned int tmp; tmp *(volatile unsigned int *)(GPIO_BASE GPIO_DIR_OFF); tmp | (1 MDIO_GPIO); *(volatile unsigned int *)(GPIO_BASE GPIO_DIR_OFF) tmp; }这个实现里最关键的就是gpio_mdio_set_input和gpio_mdio_set_output。读帧的 turnaround 阶段必须切输入如果这个切换没做MDIO 引脚一直保持输出那读到的一定是自己写出去的电平永远不是 PHY 的反馈。参数说明pin是 GPIO 编号注意是 SoC 的引脚编号而不是物理引脚GPIO_BASE要按具体芯片手册改有些 SoC 的输入数据寄存器和输出数据寄存器是分开的读要用GPIO_EXT_PIN类似的寄存器写要用GPIO_SWP_DAT这点移植的时候容易踩坑。我在写的时候习惯在gpio_mdio_set_input()之后加一个空的mdc_delay()让电平稳定再采数据实测能降低误码率。3.3 mdio_test扫描 PHY 地址的自测工具PHY 地址这个东西很多人都会被它坑一次。板子上 PHY 的地址是通过硬件引脚拉高拉低决定的可能是 0x00也可能是 0x1F。mdio_test.c的做法很实用把 0 到 31 个地址全扫一遍每个地址读寄存器 2 和 3PHY ID 高 16 位和低 16 位只要能读出非 0xFFFF 的值就认为这个地址上挂着 PHY。void mdio_scan(void) { int addr; unsigned short id_hi, id_lo; for (addr 0; addr 32; addr) { id_hi phy_read(addr, 2); id_lo phy_read(addr, 3); if (id_hi ! 0xFFFF id_hi ! 0x0000) { printf(PHY found at addr 0x%02X: ID0x%04X%04X\n, addr, id_hi, id_lo); } } }这个扫描逻辑虽然简单但非常解决问题。0xFFFF 通常表示总线上没有设备响应0x0000 可能是 PHY 还没上电或者复位没释放。参数说明寄存器 2 存储 PHY ID 的高位寄存器 3 存储低位IEEE 规定 OUI 分布在两个寄存器里printf在 VxWorks 的 shell 里可以直接打印到控制台。我把这个mdio_scan编译进内核后在 boot 阶段跑一遍就知道当前板子的 PHY 挂在哪省去了翻原理图对地址的功夫。4. 在 VxWorks 环境下编译、挂载与验证4.1 编译方式直接编进内核镜像还是做成可加载模块VxWorks 下驱动有两种存在方式一种是编进 bootrom 或 VxWorks 内核镜像一种是编成 .o 文件用ld命令动态加载。这个包的 Makefile 两种都支持。我的经验是调试阶段用动态加载改代码重新编译一个 .o在 shell 里 load 一下就行不用反复烧 Flash调通了再编进内核镜像做产品发布。CC ccppc CFLAGS -mcpu750 -I./include -DVXWORKS -D_GNU_TOOL OBJS gpio_mdio.o gpio_hw.o all: gpio_mdio.o gpio_hw.o mdio_test.o %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f *.o这段 Makefile 是典型的交叉编译写法。逻辑说明CC指向风河编译器的前缀ccppc是 PowerPC 目标CFLAGS里的-mcpu750按目标 CPU 调ARM 平台要改成-mcpucortex-a9之类。参数说明-DVXWORKS用来控制代码里#ifdef VXWORKS的分支有些平台相关的头文件切换就靠它。编出来的.o文件可以直接通过 FTP 传到目标机在 shell 里用ld gpio_mdio.o加载。加载成功后用mdio_scan验证驱动。如果没反应先别怀疑协议代码先查gpio_hw.c里的基地址和引脚号对不对这个我后面避坑章还会重点说。4.2 在 VxWorks shell 里手动测试不写 app 也能验证驱动VxWorks 的 shell 是驱动调试的利器它可以直接调用内核里的导出符号。动态加载.o文件之后phy_read和mdio_scan这些函数自动变成 shell 命令不需要额外写测试程序。# 在 VxWorks shell 里执行 - ld /ata0a/gpio_mdio.o value 0 0x0 - gpio_mdio_init() value 0 0x0 - mdio_scan() PHY found at addr 0x1F: ID0x2000A240 value 4 0x4 - phy_read(0x1f, 0x01) value 0x796D 31085这段 shell 交互记录展示的是典型的一次成功验证。ld加载模块gpio_mdio_init()初始化引脚方向和默认电平mdio_scan()扫描到地址 0x1F 有一颗 PHYID 是 0x2000A240然后直接读 BMSR 得到 0x796D说明 link 已经建立、自协商完成。逻辑说明phy_read返回的值是value 后面的数字VxWorks 的 shell 会把函数返回值打印出来。参数说明寄存器 1BMSR的 bit 2 是 link 状态0x796D 的 bit 2 是 1表示 link up如果读出来一直是 0xFFFF就要往硬件层查了。4.3 寄存器读写正确性验证用 ID 寄存器和 loopback 双重确认驱动能跑和跑得对是两回事。我在验证时用两个手段确认时序正确第一是读寄存器 2/3 的 PHY ID这个值每颗 PHY 是固定的能对上就说明帧格式、地址、数据位顺序全对了第二是开 PHY 的 loopback 模式写 BMCR 的 bit 14把发送数据环回确认 PHY 的收发通路也通。/* 使能 PHY 内部 loopback验证 SMI 之外的数据通路 */ phy_write(0x1F, 0x00, 0x4000); /* BMCR: loopback 100M full duplex */ usDelay(100000); /* 等 PHY 完成切换 */ /* 读回 BMCR 确认写入生效 */ unsigned short bmcr phy_read(0x1F, 0x00); if ((bmcr 0x4000) 0x4000) printf(loopback enable OK\n);这个 loopback 验证的意义在于它不只是验证 MDIO 能读写还验证了 PHY 芯片本身对寄存器的响应。逻辑说明写 0x4000 到 BMCR 后读回来校验如果回读值和写入一致说明 SMI 读写路径是稳定的不是碰巧读对了一次。参数说明0x4000在 BMCR 里是 loopback 位置 1 后 PHY 会把 MAC 发来的数据直接环回usDelay在 VxWorks 下可以用sysUsDelay或者taskDelay补一个足够长的延时等 PHY 内部状态机稳定。这个测试跑通了驱动就基本可信了。5. 排查与避坑GPIO 模拟 MDC/MDIO 的典型翻车现场5.1 现象扫描不到 PHY读出来全是 0xFFFF现象mdio_scan()跑完 32 个地址一个 PHY 都没找到读任何寄存器都是 0xFFFF。原因GPIO 配置不对的概率最大。MDC/MDIO 引脚复用、上拉电阻没接、或者 GPIO 本身被别的驱动占用了。解决先用示波器量 MDC 引脚看有没有 2MHz 左右的方波有波形说明时钟正常再量 MDIO 在读帧 turnaround 阶段有没有被拉低或拉高的动作。都没有就去查gpio_hw.c里的引脚编号和基地址是不是和硬件原理图一致。我调第一块板子时就是 MDIO 引脚被寄存器复用地当成了 UART 的 TX驱动怎么改都读不到数据最后查 SoC 的 pinmux 才找到问题。5.2 现象MDC 频率跑到 5MHz 以上PHY 直接不响应现象示波器一看 MDC 频率超标了PHY 偶发能读到数据但大部分时间返回 0xFFFF。原因空循环延时标定得不对或者编译器优化把延时优化掉了。解决把mdc_delay的循环变量改成volatile防止被优化再用示波器实测 MDC 频率按比例调ns / 10这个折算系数。规范要求 MDC 最大 2.5MHz我习惯留余量跑到 2MHz 左右。血泪经验VxWorks 的编译器在不同优化级别下对空循环的处理完全不同-O2下不加volatile的空循环可能整个被删掉MDC 直接变成直流电平。5.3 现象读出来的寄存器值不稳定偶尔多一位或者少一位现象同一个寄存器连续读 10 次有 8 次是对的2 次是错的错误值像是位偏移了一位。原因MDIO 方向切换的时机不对或者 MDC 高电平保持时间不够PHY 在电平还没稳定的时候采样了。解决在gpio_mdio_set_input()之后强制加一个mdc_delay(250)让总线电平稳定后再采另外检查gpio_mdio_set_output()是不是在写操作完成后及时恢复否则下一次读帧的 preamble 会被自己残留的输出电平污染。我后来在gpio_mdc_set(0)和gpio_mdc_toggle里都强制加了半周期延时误码率就降为零了。5.4 现象VxWorks 中断里调用 phy_read 导致系统卡死现象在中断服务程序里调用phy_read结果整个系统挂住连 shell 都没反应。原因SMI 时序是靠空循环延时的一个读帧要做几十上百次 GPIO 翻转和延时总共耗时几百微秒在中断上下文里这么长时间占用 CPU会阻塞其他中断。解决不要在中断里直接调用 MDIO 读写。我一般是开一个专门的任务用信号量或者消息队列把 MDIO 操作串行化中断只负责置位一个 pending 标志最低优先级任务去真正执行读写。注意这也顺便解决了另一个隐患——如果底层 GPIO 操作不是原子的高优先级任务抢占也可能打断一帧的时序。5.5 现象加载 .o 后调用函数系统报 undefined symbol现象ld加载gpio_mdio.o报错提示gpio_write_pin等符号未定义。原因gpio_hw.o没一起加载或者 Makefile 里两个目标文件的加载顺序反了。解决先加载gpio_hw.o再加载gpio_mdio.o最后加载mdio_test.o因为后面的文件依赖前面的符号。更省事的做法是把三个.o打包成一个.a或者重新编进内核镜像避免符号解析的麻烦。6. 二次开发把驱动移植到新板子的三步走拿到这份代码真正要落地到自己的项目里核心就是替换gpio_hw.c。我在移植到一款 ARM 平台时踩了个有意思的坑SoC 的 GPIO 输入寄存器和输出寄存器是分开的读取引脚电平要用专门的输入寄存器而原代码直接读数据寄存器结果在写输出后再读每次都读到上一次写入的值。所以移植时第一步永远是打开芯片手册对照 GPIO 控制器的寄存器布局逐个确认GPIO_BASE、方向寄存器、输出寄存器和输入寄存器的偏移。第二步是确认引脚复用很多 SoC 的引脚默认不是 GPIO 功能比如 MDC 要用的引脚可能被默认成 PWM 了必须在初始化里先通过 pinmux 寄存器把它切到 GPIO 模式。第三步才是标定延时用示波器量 MDC 频率调整mdc_delay的空循环次数。有一个小技巧可以省很多时间移植后先不急着接 PHY直接在 MDC/MDIO 引脚上接一个逻辑分析仪跑一段mdio_transfer看抓出来的波形是不是符合预期帧格式。如果波形正确但 PHY 不回数据问题大概率在方向切换时序量 turnaround 阶段 MDIO 是不是真的变成了高阻输入。我在做某跨平台系统的时候把这三个步骤固化成了 checklist每次换板子都强制走一遍从那以后再也没有因为移植 GPIO 驱动而翻车。这个驱动框架最值得借鉴的地方就是把协议和硬件操作彻底分层希望你把它拿去改的时候也能保持这个分层的习惯——希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑