DP83867千兆PHY驱动移植与RGMII延时调试实战

简介:面向嵌入式以太网驱动开发的学习资料包,聚焦德州仪器dp83867以太网控制器芯片,适合已具备C语言基础、希望打通芯片手册阅读与驱动代码实现的开发者参考。包内共2个文件,包含1份PDF技术手册和1份C语言源码,压缩包约1.35MB;手册可用于查阅引脚配置、寄存器描述、工作模式与电气特性,源码则对应初始化、数据收发、中断处理等模块,便于理论对照实践并理解底层通信流程。已有435人学习下载,说明其在相关芯片驱动入门与调试场景中具有一定参考价值。读者可从中梳理寄存器读写流程、MAC配置思路、DMA收发机制及错误处理要点,并结合具体项目理解低功耗与硬件协同设计,适合作为快速上手dp83867开发与排查以太网通信问题的辅助材料。

1. 一块千兆 PHY 的落地现场:手册加驱动,缺一不可

板子贴片回来,上电,网口灯不亮;或者勉强亮了,自协商只跑到百兆;再或者千兆能起,一压大包就开始丢。这种时候最缺的其实是一份能对着寄存器一位一位查的手册,和一个能直接编进去跑起来的驱动骨架。dp83867cr.pdf加dp83867.c正好凑齐这两样:前者是这颗千兆以太网 PHY 的完整数据手册,寄存器表、RGMII 时序参数、strap 引脚定义、封装管脚一页页都在;后者把 MDIO 读写、软复位、RGMII 延时配置、自协商启动、链路状态解析串成了一条能跑的流程。做工业网关、板载交换、边缘盒子这类带千兆口的兄弟最用得上,尤其是 MAC 侧驱动已经调通、卡在 PHY 这一层的时候。

2. 资源全貌与硬件链路:从 MDIO 控制面到 RGMII 数据面

拿到这两份东西,很多人第一反应是直接打开.c编译。我吃过这个亏:驱动里一行0x0086写下去没反应,翻遍代码找不到原因,最后是在手册的寄存器表里发现那个寄存器属于扩展页,得先切页才能访问。所以顺序应该是先弄清两份材料各自管什么,再弄清硬件上到底怎么连的,最后才看代码。

2.1 两份材料的分工:手册当字典,C 文件当骨架

数据手册不是用来通读的,是用来查的。我一般只盯四块内容:寄存器定义表(每个 bit 什么含义、复位值多少、读写属性)、RGMII 时序章节(建立/保持时间、时钟到数据的偏斜要求)、strap 配置章节(上电瞬间哪些引脚被采样成什么配置)、电气特性(MDC 最高频率、MDIO 上拉要求、电源序)。

dp83867.c则是把这些纸面定义变成可执行序列。它一般包含这么几层:最底下是两个平台钩子mdio_read/mdio_write,往上是对扩展寄存器的封装(因为千兆 PHY 的配置项大量放在扩展页里,标准 16 位寄存器空间放不下),再往上是初始化序列和链路状态轮询。

两者交叉查的办法很土但有效:在.c里看到任何一个魔数宏,比如#define DP83867_RGMIIDCTL 0x0086,立刻回手册寄存器表搜这个地址,确认它的位域、复位值、以及"是否属于扩展页"。这一步花十分钟,能省掉后面几小时的抓瞎。

2.2 硬件侧的真实连接:MDC/MDIO 与 RGMII 两条线

PHY 和 MAC 之间永远是两条通路。控制面是 MDC/MDIO 两线制串行总线,一根时钟一根双向数据,所有寄存器读写都走它;数据面是 RGMII,收发各一组:TXC/TXD[3:0]/TX_CTL和RXC/RXD[3:0]/RX_CTL,双沿采样,千兆下时钟 125MHz。

MDC 频率是有上限的,手册里给的最大值通常在 25MHz 量级,但实际项目里我一般压到 2.5MHz 以下。原因很简单:MDIO 是开漏/准双向结构,靠上拉拉高,边沿本来就软,跑太快在长走线或者多 PHY 挂同一条总线的板子上很容易采错。上拉电阻常见取值 1.5k 到 2.2k,缺了它读回来就是全 1。

RGMII 最容易翻车的是延时。125MHz 下周期只有 8ns,时钟和数据之间必须有一个大约 1.5 到 2ns 的偏斜才能满足采样窗口。这个偏斜有三个来源:PCB 走线本身(RGMII 规范要求时钟线比数据线长一截)、MAC 侧内部延时、PHY 侧内部延时。三者的和必须落在窗口内,而且只能有一方提供延时——两边都开或者都不开,都会出问题。这颗 PHY 内部延时是可配的,通过扩展寄存器使能并调节步进,这也是.c里最核心的几行代码之一。

复位和 strap 也得说清楚。RESET_N拉低要保持足够时间(手册给的是微秒级最小值,实际我按毫秒级给),释放后芯片内部还要完成自举,这段时间内访问 MDIO 是无效的。而 PHY 地址、自协商开关、RGMII 模式这些配置,是在复位释放瞬间从特定引脚上采样锁存的,也就是 strap。strap 引脚上通常挂上下拉电阻,如果这些引脚同时又接了 LED 或者被别的电路拉动,采样值就可能和设计意图不符——这类问题表现出的现象特别像"驱动写错了"。

2.3 驱动在协议栈里的位置:谁管协商,谁管收发

把职责划清楚,调试思路才不会乱。MAC 只管帧的收发、DMA 描述符、CRC 校验;PHY 管物理层的编码解码、自协商、链路建立。协商结果(速率、双工)由 PHY 决定,MAC 得读回来再按对应速率配置自己的时钟和接口模式。

所以一个典型的启动流程是:复位 PHY → 读 ID 寄存器确认器件在位 → 配置 RGMII 延时和时钟模式 → 启动自协商 → 轮询链路状态 → 链路起来后读速率双工 → 通知 MAC 侧调整。.c里的函数基本就是按这个顺序组织的,缺任何一步都可能表现为"灯亮但通不了"或者"通一会儿就断"。

3. 核心代码拆解:一次寄存器读写要走完的四层

这份 C 程序的价值不在代码量,在于它把"平台相关"和"器件相关"分开了。下面按调用层次从底往上拆,每一层我都标了移植时要动的地方。

3.1 最底层:MDIO 读写原语与扩展寄存器寻址

最先要落地的是两个钩子函数。它们不该包含任何器件逻辑,只负责把一次 16 位寄存器事务发到总线上:

/* 移植时只需要实现这两个,其余代码可以原样复用 */ int mdio_read (uint8_t phy_addr, uint16_t reg, uint16_t *val); int mdio_write(uint8_t phy_addr, uint16_t reg, uint16_t val);

phy_addr是 strap 决定的 5 位地址(0~31),同一条 MDIO 总线上挂多颗 PHY 时靠它区分;reg是 5 位寄存器地址空间。返回值约定 0 为成功,非 0 为失败——这个约定很重要,因为上层初始化序列需要靠返回值判断要不要中止。

麻烦在于这颗 PHY 的大量配置项不在标准 16 位地址空间里,而在扩展页。访问方式遵循标准的 MMD 机制:先往0x0D写目标地址,再往0x0E读写数据。

#define DP83867_REGCR 0x0D #define DP83867_ADDAR 0x0E /* REGCR 位域:bit15 选择访问数据还是地址,bit14 关闭地址自增 */ #define REGCR_FUNC_ADDR (0u << 15) #define REGCR_FUNC_DATA (1u << 15) #define REGCR_NO_POST_INC (1u << 14) #define DP83867_DEVAD 0x1Fu /* 该器件的扩展页固定用这个设备地址 */ static int dp83867_ext_write(uint8_t pa, uint16_t reg, uint16_t val) { int rc; /* 第一步:把 REGCR 配成"接下来写的是地址",并关掉自增 */ rc = mdio_write(pa, DP83867_REGCR, REGCR_FUNC_ADDR | REGCR_NO_POST_INC | DP83867_DEVAD); if (rc) return rc; /* 第二步:目标扩展寄存器地址送进 ADDAR */ rc = mdio_write(pa, DP83867_ADDAR, reg); if (rc) return rc; /* 第三步:REGCR 切到"接下来操作的是数据" */ rc = mdio_write(pa, DP83867_REGCR, REGCR_FUNC_DATA | REGCR_NO_POST_INC | DP83867_DEVAD); if (rc) return rc; /* 第四步:数据写进 ADDAR */ return mdio_write(pa, DP83867_ADDAR, val); }

读操作的差别在最后一步:切到数据模式后,mdio_read(pa, DP83867_ADDAR, &val)拿回值。这套"地址-数据"的两段式访问必须严格按顺序,中间插入任何其他寄存器的读写都可能打断状态机,导致读回旧值。这是我踩过的最隐蔽的坑,后面第 5 章会细说。

那个REGCR_NO_POST_INC位也不是可有可无的:关掉自增之后,每次访问都得重新写地址,看似啰嗦,但换来了"每次访问都是显式的",批量读连续寄存器时不容易错位。

3.2 初始化序列:复位、验 ID、配 RGMII 延时

有了扩展读写,初始化就能写成一条线性序列。顺序不能乱:

int dp83867_init(uint8_t pa) { uint16_t id1, id2, val; /* 1. 软复位,等待自清零。超时必须有,否则一颗坏片会把启动流程挂死 */ mdio_write(pa, 0x00, 0x8000); for (int i = 0; i < 100; i++) { if (mdio_read(pa, 0x00, &val) == 0 && (val & 0x8000) == 0) break; delay_ms(1); if (i == 99) return -1; } /* 2. 读 ID 确认器件在位、型号匹配。OUI 和型号号在手册 ID 表里 */ mdio_read(pa, 0x02, &id1); mdio_read(pa, 0x03, &id2); if (id1 == 0x0000 || id1 == 0xFFFF) return -2; /* 总线没通或没上拉 */ /* 3. 配置 RGMII 内部延时。硬件上 MAC 侧和走线都不提供延时的时候, 就必须由 PHY 来补,否则千兆一定出错 */ dp83867_ext_write(pa, 0x0032, 0x0003); /* bit0 使能 RX 延时,bit1 使能 TX 延时 */ dp83867_ext_write(pa, 0x0086, 0x00AA); /* 高低半字节分别是 TX/RX 延时档位 */ /* 4. 启动自协商,重启协商过程 */ mdio_write(pa, 0x00, 0x1200); /* bit12 自协商使能 + bit9 重启协商 */ return 0; }

几个参数值得展开。软复位写的是0x00寄存器的 bit15,写完必须轮询等它自清零,等不到就说明芯片没响应或者时钟没给。ID 校验只判0x0000和0xFFFF是最低要求,严谨一点应该和手册里给出的 OUI 与型号位做掩码比对,避免同总线上有别的 PHY 被误认。

0x0032是 RGMII 控制寄存器,两个低位分别使能 TX 和 RX 的内部延时;0x0086是延时档位,高 4 位管 RX、低 4 位管 TX,每档约 0.25ns。写0x00AA意味着各给 10 档、约 2.5ns——这是我在多数板子上能跑通的起点值,但它不是万能值,具体得看 MAC 侧有没有延时、走线差多少,后面第 5 章会给试配方法。

最后启动自协商的那次写,用的是标准0x00寄存器:bit12 使能自协商,bit9 重启协商过程。这两位置下去,PHY 才开始和对面发 FLP 脉冲。很多"协商不起来"的问题,根因就是初始化序列里漏了这一句,芯片一直停在强制模式。

3.3 链路状态轮询:把速率和双工读回来

链路是否建立,看标准0x01寄存器的 bit2(Link Status)。这个位有个坑:它是锁存低有效的,读一次会把状态清掉,下次再读才是当前真实值,所以轮询时要读两次或者接受一次延迟。

速率和双工不在标准寄存器里,得从扩展状态寄存器解析:

#define DP83867_PHYSTS 0x001F int dp83867_get_link(uint8_t pa, int *speed, int *duplex) { uint16_t bmsr, sts; if (mdio_read(pa, 0x01, &bmsr)) return -1; mdio_read(pa, 0x01, &bmsr); /* 再读一次,拿到实时的链路位 */ if (!(bmsr & (1u << 2))) { *speed = 0; return 0; } if (dp83867_ext_read(pa, DP83867_PHYSTS, &sts)) return -1; /* 位序按手册寄存器表核对,不同批次的文档编排略有差异 */ *duplex = (sts & (1u << 13)) ? 1 : 0; /* 1=全双工 */ switch ((sts >> 14) & 0x3) { case 0: *speed = 10; break; case 1: *speed = 100; break; default: *speed = 1000; break; } return 0; }

DP83867_PHYSTS这个名字和地址来自手册的扩展寄存器表,0x001F是我手上这版手册里的编址。位域部分我在注释里留了话:速率和双工的具体 bit 位置请拿手册的寄存器描述页对一遍,因为同一颗芯片不同修订版的手册在编排上会有出入,照抄别人的代码而不对表,最后出错都不知道错在哪。

读出速率之后,逻辑上还要通知 MAC 侧把接口时钟切到对应频率(千兆 125MHz、百兆 25MHz、十兆 2.5MHz)。这一步在裸机里通常是自己写寄存器,在内核态则由 PHY 框架回调adjust_link完成。

4. 编译与上板跑通:裸机和内核态两条路

同一份.c,在裸机工程和带 PHY 子系统的内核里,接法完全不同。前者你自己管一切,后者你只需要提供设备树描述,框架会替你调初始化。

4.1 裸机移植:把三个钩子填掉

裸机下没有现成的总线驱动,得自己用 GPIO 模拟 MDIO 时序。核心是前导码、起始位、操作码、地址、 turnaround 这几段:

static void mdio_bit(int b) { gpio_set(MDIO_PIN, b); gpio_set(MDC_PIN, 0); mdio_delay(); /* MDC 低电平期间准备数据 */ gpio_set(MDC_PIN, 1); mdio_delay(); /* 上升沿由对端采样 */ } int mdio_write(uint8_t pa, uint16_t reg, uint16_t val) { for (int i = 0; i < 32; i++) mdio_bit(1); /* 前导码 32 个 1 */ mdio_bit(0); mdio_bit(1); /* ST = 01 */ mdio_bit(0); mdio_bit(1); /* OP = 01 表示写 */ for (int i = 4; i >= 0; i--) mdio_bit((pa >> i) & 1); /* 5 位 PHY 地址 */ for (int i = 4; i >= 0; i--) mdio_bit((reg >> i) & 1); /* 5 位寄存器地址 */ mdio_bit(1); mdio_bit(0); /* TA:先释放一个周期再驱动 */ for (int i = 15; i >= 0; i--) mdio_bit((val >> i) & 1); /* 16 位数据 */ return 0; }

mdio_delay()决定 MDC 频率,用空循环或硬件定时器都行,目标是压到 2.5MHz 以下。注意TA那两个周期:写操作里是1,0,读操作里要先把 MDIO 切成输入方向,再读两个周期,第二个周期开始才是数据。方向切换漏了,读回来永远是全 1。

移植时还要处理一个硬件细节:MDIO 是双向的,GPIO 必须在发送和接收之间切换方向,切换时机要卡在 TA 之前。有些平台用硬件 MAC 自带的 MDIO 控制器,那就更省事,只要把mdio_read/write转成对控制器寄存器的读写即可,时序问题交给硬件。

4.2 内核态挂载:设备树描述 + PHY 框架接管

带 PHY 子框架的内核里,.c大部分内容不用自己调,框架会在扫描到 PHY 后调用你的初始化回调。你要做的是把硬件连接关系描述清楚:

&mac0 { phy-handle = <&ethphy0>; phy-mode = "rgmii-id"; /* 关键:谁提供延时 */ mdio { ethphy0: ethernet-phy@0 { reg = <0>; /* MDIO 上的 PHY 地址,必须与 strap 一致 */ }; }; };

phy-mode这一行是重灾区,四种取值的含义必须分清:

取值谁提供 TX 延时谁提供 RX 延时
rgmii都不提供都不提供
rgmii-idPHYPHY
rgmii-txidPHYMAC 或走线
rgmii-rxidMAC 或走线PHY

写rgmii而硬件上两边都没做延时,千兆必然出错;写rgmii-id而 PHY 内部的延时寄存器没被使能,效果和rgmii一样。这两件事必须配对:设备树声明了-id,驱动里就得真的把0x0032的两个使能位打开。

另外reg = <0>这个地址来自 strap,不是随便填的。板上 PHY 地址引脚怎么挂的,得翻原理图确认,填错了框架扫不到器件,表现就是"PHY 一直不 ready"。

4.3 验证:三级手段逐层排除

跑通不靠感觉,靠分层验证。我一般按这个顺序走:

第一级看寄存器。初始化完成后立刻回读0x02/0x03,ID 对得上说明 MDIO 链路通了;回读0x0032和0x0086,值和你写进去的一致,说明扩展页访问没问题。这一步不过,后面全白搭。

第二级看协商。读0x01的 bit2 和 bit5,链路位和自协商完成位都置起来,说明物理层握手成功了。再读扩展状态寄存器,确认速率是 1000 而不是 100。

第三级压流量。协商到千兆不代表能跑满。用打流工具跑双向大包,同时看 MAC 侧的 CRC 错误计数和丢包计数。错误计数在涨,八成是 RGMII 时序余量不够;计数干净但吞吐上不去,那要往 MAC 的 DMA 和缓冲区配置上查。

有一点容易忽略:不要只测单向。RGMII 的 TX 和 RX 是两套完全独立的延时,TX 方向没问题不代表 RX 方向也没问题,双向都压一遍才能定位到底是哪一侧的偏斜不对。

5. 避坑与排查:五个让我返工的现象

下面这几条都是我在实际板子上撞过的,按"现象 → 原因 → 解决"记下来,下次遇到能省时间。

5.1 读 ID 回来全是 0xFFFF

现象:初始化第一步读0x02,返回值0xFFFF,驱动直接返回错误。

原因有三个层次:MDIO 数据线没有上拉电阻,线被拉不住;PHY 地址和 strap 实际值对不上,事务发出去没人应答;复位还没释放完就发起访问,芯片内部状态机没准备好。

解决:先拿万用表量 MDIO 对电源的上拉,正常应该有 1.5k 到 2.2k;再翻原理图核对地址引脚的上下拉,和代码里的phy_addr对一遍;最后确认复位释放到第一次 MDIO 访问之间留了足够延时,我一般给 10ms 以上。

5.2 千兆掉百兆,灯是亮的

现象:链路能起来,但速率一直是 100M,怎么重启协商都不变。

原因:最常见的是 RGMII 延时没配对——设备树写rgmii但硬件上没人提供延时,或者写rgmii-id但0x0032的使能位没打开。千兆对时序窗口的要求比百兆严得多,百兆能凑合,千兆就掉下来。其次是变压器或网口只支持百兆,或者网线是超五类以下的劣质线。

解决:先把phy-mode和0x0032/0x0086的配置对齐,再换一根确认过能跑千兆的成品线,最后才怀疑硬件。顺序反了会浪费大量时间在换线换板上。

5.3 小包正常,大包一压就丢

现象:ping 一直通,一跑大包吞吐,丢包率就上去了,MAC 侧 CRC 错误计数同步增长。

原因:CRC 错误基本可以锁定在物理层,RGMII 的采样窗口余量不足。可能是 PCB 上 RGMII 走线不等长、时钟线没按要求加长,也可能是延时档位给得不够,还可能是 MAC 侧也在提供延时造成叠加。

解决:把0x0086的延时档位一格一格往上试,每试一档压一次大包看错误计数,找到错误计数归零的那个区间后取中间值。这个笨办法比算理论值快得多,因为板子的实际走线参数你算不准。

5.4 扩展寄存器写进去读出来还是旧值

现象:0x0032写0x0003,紧接着读回来是0x0000,或者干脆读出来是别的寄存器内容。

原因:扩展页访问被打断了。0x0D→0x0E→0x0D→0x0E这四步之间插入了别的寄存器操作,状态机被重置;或者软复位之后写的,复位把扩展寄存器清回了默认值;还有一种更隐蔽的,mdio_write返回了成功但硬件上 MDC 时钟还没走完就发起下一次事务。

解决:把扩展写封装成不可打断的原子序列,中间不插任何访问;软复位之后必须重新走一遍完整配置;在mdio_write返回前加一个极短的等待,确保最后一个时钟周期走完。

5.5 复位后偶发识别不到,重上电又好了

现象:十次上电有一两次读不到 PHY,复位一次就好,无法复现。

原因:上电时序或者 strap 采样窗口的问题。复位释放的时刻如果正好落在电源还没爬升到位、或者参考时钟还没起来的区间,strap 采样就可能采错;也可能复位保持时间给得太短,芯片内部自举没完成。

解决:把复位保持时间从微秒级放宽到毫秒级,同时在复位释放后加一段固定等待再访问 MDIO。如果还偶发,就得用示波器抓上电瞬间的电源和时钟,看复位释放点是否落在时钟有效之后。这类问题不解决,量产时会变成返修率。

6. 二次开发:把寄存器配置抽成表,留一条能回滚的验证路径

这份.c直接能用,但要用在多个板子上,就得改结构。我的做法是把所有"跟板子相关的寄存器写"从初始化函数里抽出来,变成一张表:

struct phy_tweak { uint16_t reg; /* 扩展寄存器地址 */ uint16_t val; /* 目标值 */ uint16_t mask; /* 只改这些位,其余保持 */ const char *note; /* 为什么这么配,给自己和后人看 */ }; static const struct phy_tweak dp83867_tbl[] = { { 0x0032, 0x0003, 0x0003, "使能 TX/RX 内部延时" }, { 0x0086, 0x00AA, 0x00FF, "TX/RX 各约 2.5ns,按板子实测调整" }, { 0x0010, 0x0000, 0x0000, "保留位,按 strap 补偿时填" }, };

驱动初始化的时候遍历这张表,读-改-写三步走,mask保证只动目标位,不误伤同一个寄存器里的其他配置。好处很直接:换一块板子只改表,不动流程代码;出了问题是哪一项配错,一眼能看出来;note字段逼着你写下理由,半年后回来看不用重新推理一遍。

配套的验证习惯也得改。以前我是"跑通了就提交",现在改成每次改延时参数都强制走一遍三段回读:写完立刻回读确认值进去了,链路起来后打印速率双工确认协商对了,压一轮双向大包确认错误计数干净。这三段缺一段,参数就算白调——因为有些错误只在特定包长或者特定温度下才露头,当场看不出来,过两天在客户现场冒出来。

还有一个我后来加的习惯:把当前生效的寄存器配置在初始化结束时整体 dump 一遍,存进日志。板子出问题时,第一件事就是拿现场日志和出厂日志对比,哪几个寄存器被改过一目了然。这比让现场的人描述"网口不通"有用一百倍。从那以后我每次做 PHY 相关的改动,都强制走一遍"改表、回读、压包、dump"这四步,哪怕只是改一个延时档位。希望帮到你。

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