简介:面向嵌入式与物联网驱动开发者的DP83867以太网控制器学习资料,聚焦TI芯片的寄存器操作与底层通信实现,适合需要快速理解以太网接口配置、收发流程及中断处理的初中级工程师参考。压缩包为rar格式,共2个文件,包含1份PDF技术手册和1份C语言源码,整体约1.35MB,体积轻便便于随项目查阅。技术手册覆盖功能特性、电气参数、引脚配置、寄存器描述与工作模式,源码则给出初始化、数据发送接收、DMA传输、错误处理及中断响应等核心模块的实现思路。已有435人学习下载,可帮助读者把手册中的寄存器读写说明与代码实践对应起来,掌握MAC地址设置、速率双工配置、数据帧收发和中断处理等关键环节,并理解硬件中断与网络协议处理的基本流程,为DP83867驱动开发与调试提供可直接借鉴的参考。
1. 板子首次上电、MDIO 读回全 0xFF 时,这份 dp83867cr.pdf 与 dp83867.c 能提供最快落地的路径
千兆 PHY 调试最让人困扰的一点是:MAC 侧看起来一切正常,时钟有、复位有、电源也测量过,但 MDIO 就是读不到 ID;或者读到了 ID,自协商也能完成,可一跑流量就丢包。这类问题的原因大多不在“代码没写对”,而在于手册里几个关键细节没有被读透。dp83867cr.pdf 是这款千兆以太网 PHY 的完整手册,寄存器映射、strap 引脚定义、RGMII 时序门限都写在里面;配套的 dp83867.c 则是一份可以直接嵌进裸机工程或 RTOS 工程的驱动实现,涵盖 MDIO 收发、扩展寄存器访问、RGMII 延时配置以及链路状态解析。
这份资料适合正在做千兆网口 bring-up 的嵌入式工程师、需要把 PHY 驱动移植到自研板上的驱动开发者,以及被“link 不亮”“千兆丢包”困扰、想系统过一遍 PHY 配置流程的人。它不教你以太网协议栈,只解决一件事:让这颗 PHY 在你的板子上稳定跑起来。
2. 拆解资源包:手册的五个章节和 dp83867.c 的四层骨架
拿到 PDF 之后常见的错误是从第一页开始顺序阅读,读到第 300 页已经忘了第 40 页的 strap 表写了什么。这份手册的用法应当按“查”来设计,而不是按“读”来设计。C 程序也一样,如果一上来就看配置函数,很容易被一堆魔数绕进去。
2.1 手册里真正要反复翻的五个章节
第一块是引脚描述与 strap 功能表。这颗 PHY 在复位上升沿会把若干多功能引脚的电平锁存成配置项,包括 PHY 地址、MAC 接口类型(RGMII 还是 SGMII)、自协商使能、时钟输出模式等等。硬件同事画的上下拉电阻,最终就是通过这张表变成芯片内部的行为。板子回来之后 link 行为诡异,第一件事应该是拿万用表和这张表对一遍,而不是改代码。
第二块是寄存器映射总表。clause 22 的标准寄存器只有 0x00 到 0x1F 三十二个,这颗 PHY 把大量功能放到了扩展寄存器空间里,靠 0x0D 和 0x0E 两个窗口寄存器去间接访问。总表的作用是让你在写代码之前就知道:我要配的这个功能,究竟在标准空间还是扩展空间,偏移是多少,是读写还是只读,有没有自清零位。
第三块是 RGMII 时序章节。这里给的是建立时间、保持时间、时钟到数据的偏斜门限,以及内部延时调节的步进值。很多人把 RGMII 当成“接上就能跑”的总线,实际上 125MHz 下 8ns 一个周期,PCB 走线差 500mil 就可能把眼图吃掉一半,内部延时档位没配对,表现就是千兆不通、百兆能通,或者反过来。
第四块是 strap 状态回读寄存器。手册里有一对专门用来回读复位瞬间 strap 采样结果的寄存器,偏移在寄存器总表里能查到。这个功能的价值在于:你不需要拿示波器去戳引脚,直接读寄存器就知道硬件到底把 PHY 配成了什么,能省掉大量“到底是我代码错了还是电阻贴错了”的扯皮。
第五块是上电时序与复位要求。复位脉宽最小值、时钟稳定到复位释放的间隔、复位后多久寄存器可访问,这些数字看起来枯燥,但读 ID 全 0xFF 的场景里,有一半是复位还没走完就开始发 MDIO 帧。
2.2 C 程序的四层骨架:从总线回调到状态机
dp83867.c 的结构可以按四层来理解,移植的时候也按这四层去替换,比整份拷贝进去再改要干净得多。
最底下是总线层,只做一件事:把“读一个 clause 22 寄存器”和“写一个 clause 22 寄存器”抽象成两个函数指针。裸机工程里通常是 GPIO 模拟 MDIO,或者用 MAC 自带的 MDIO 控制器;Linux 下则是 phy 框架提供的读写回调。这一层不关心 PHY 是什么型号,换芯片也不用动。
往上是寄存器访问层,负责把“标准寄存器”和“扩展寄存器”统一成同一个调用接口。对外暴露的应该是phy_read(addr)和phy_write(addr, val);当 addr 大于 0x1F 时,内部自动走 REGCR/ADDAR 的握手流程。这一层是整份代码里最值得抄的部分,因为它是纯逻辑,跟平台无关。
再往上是配置层,负责软复位、RGMII 延时、时钟输出、中断屏蔽、LED 行为这些一次性设置。这一层的函数通常只在初始化的地方调一次,参数来自板级配置,不应该硬编码在驱动里。
最上面是状态层,负责读链路状态、速率、双工模式、自协商结果,以及清中断标志。这一层会被上层轮询或者中断驱动反复调用,所以必须足够轻,不能每次都去访问扩展寄存器。
2.3 手册到代码的对照表
把两者的对应关系列清楚,改代码的时候就不用两边来回翻了。下面这张表是移植时我一般会先填一遍的东西,填完基本就知道这块板子有哪些地方需要特别处理。
| 手册位置 | 内容 | 代码里对应 | 移植时要确认的参数 |
|---|---|---|---|
| 引脚与 strap 表 | 复位锁存的配置项 | board_strap_check() | PHYAD、RGMII/SGMII、自协商使能 |
| 寄存器总表 0x00~0x1F | clause 22 标准寄存器 | phy_read/phy_write | 无需改,通用 |
| 寄存器总表 扩展区 | 延时、strap 回读、时钟 | ext_read/ext_write | 窗口寄存器偏移、bit14 语义 |
| RGMII 时序章节 | 延时步进与门限 | set_rgmii_delay() | 步进值、TX/RX 各几档 |
| 上电时序章节 | 复位脉宽、就绪时间 | phy_hw_reset() | 延时毫秒数、轮询次数 |
表格最后一行那个“延时毫秒数”看着最不起眼,但它是最容易在上板第一天就遇到问题的地方。复位释放之后立刻发 MDIO,芯片内部还没准备好,读回来就是 0xFFFF,然后你会去怀疑上拉电阻、怀疑时钟、怀疑 GPIO 方向,绕一大圈回到原点。
提示:移植前先把这张表填满,凡是有空格的地方都去手册里找答案,不要靠猜。猜出来的参数在实验室常温下往往能跑,一到高低温循环就原形毕露。
3. 打通 MDIO 通路:clause 22 帧格式与扩展寄存器的两写两读
所有 PHY 调试的第一步都是让 MDIO 通路跑起来。这一步不通,后面所有配置都是空谈。很多工程里 MDIO 是 GPIO 模拟的,因为 MAC 自带的 MDIO 控制器有时候时序参数不可调,遇到长走线或者多 PHY 挂同一条总线时反而不好调。
3.1 先把 clause 22 的 32 位帧拆成五段
clause 22 的管理帧结构是固定的:32 位前导码、2 位起始位01、2 位操作码(读10、写01)、5 位 PHY 地址、5 位寄存器地址、2 位 turnaround,最后 16 位数据。总共 64 个 MDC 周期。
前导码的作用是让总线上的所有 PHY 同步时钟。有些 MAC 控制器支持省略前导码,但在多 PHY 共享总线的场景下,保留 32 位前导是最稳的做法,代价只是多 32 个时钟周期,换算下来几微秒,完全值得。
turnaround 是读写方向切换的地方,也是模拟 MDIO 最容易写错的地方。读操作时,主机在送完寄存器地址后要立刻把 MDIO 切成输入并释放总线,第一个 TA 周期 PHY 输出高阻,主机读到的值要丢掉;第二个 TA 周期 PHY 才开始驱动数据。写操作则相反,主机连续输出10两个 TA 位,然后继续输出 16 位数据。少读这一位,整个数据就会错位一格,读出来的值永远是实际值左移一位,这种 bug 用逻辑分析仪看波形才能发现。
MDC 频率是另一个要盯的参数。手册会给一个上限,实际工程里我一般把它压到 2.5MHz 到 12.5MHz 之间。理由是:MDC 是主机输出的,MDIO 是双向的,读的时候 PHY 要在 MDC 上升沿之后把数据摆稳,走线长了、加上拉电阻阻值偏大,边沿就缓,频率一高就采错。
3.2 扩展寄存器:REGCR 与 ADDAR 的握手
clause 22 只有 32 个寄存器地址,这颗 PHY 显然不够用,所以它做了一套间接访问机制:0x0D 是窗口控制寄存器,0x0E 是窗口数据寄存器。所有超出 0x1F 的寄存器,都要先告诉芯片“我要访问哪个地址”,再从这个数据窗口读写。
写一个扩展寄存器的完整流程是四步:先把 0x0D 写成0x001F,这一步的意思是“接下来的数据口指向 ADDAR”;再把目标地址写进 0x0E;然后把 0x0D 写成0x401F,其中 bit14 置 1 是关键,它表示禁止地址自增,把窗口锁死在刚才写的那个地址上;最后把数据写进 0x0E。
读的流程前 三步完全一样,第四步变成从 0x0E 读。区别在于不能有别的 MDIO 访问夹在中间,因为窗口是全局状态,不是每个调用私有的。这一点在多任务环境下特别要命:一个任务正在配延时,另一个任务来读链路状态,窗口被改掉,第一个任务的第四步就写到了错误的地址上。血的教训是,这一层必须加锁,哪怕是裸机也要关中断。
3.3 可编译的读写实现
先看 GPIO 模拟 MDIO 的最底层,这一段是整个驱动的地基。
/* mdio_gpio.c —— 两个 GPIO 模拟 MDIO/MDC,时序参数按需调 */ #include <stdint.h> #define MDC_PIN 38 #define MDIO_PIN 39 #define MDC_DELAY_NS 40 /* 半周期 40ns,约 12.5MHz,保守取值 */ static inline void delay_ns(uint32_t ns) { /* 平台相关:空循环或硬件定时器 */ } static void mdc_high(void) { gpio_set(MDC_PIN); delay_ns(MDC_DELAY_NS); } static void mdc_low (void) { gpio_clr(MDC_PIN); delay_ns(MDC_DELAY_NS); } static void mdio_dir_out(void) { gpio_dir(MDIO_PIN, 1); } static void mdio_dir_in (void) { gpio_dir(MDIO_PIN, 0); } static void mdio_wbit(int b) { gpio_set_val(MDIO_PIN, b); mdc_high(); mdc_low(); } static int mdio_rbit(void) { int b; mdc_high(); b = gpio_get(MDIO_PIN); /* 上升沿之后采样,留足建立时间 */ mdc_low(); return b; }MDC_DELAY_NS这个值是整个文件里唯一需要跟硬件强相关的参数。它决定了 MDC 的频率,半周期 40ns 对应 12.5MHz。板子上拉电阻用 1.5k 还是 10k,走线有多长,都会影响这个值能开多高。调试期先取大一点,等链路稳定了再往上提,别一上来就拉满。
接下来是完整的 clause 22 读写帧。
/* clauses 22 帧:PRE(32) + ST(01) + OP(10读/01写) + PHYAD(5) + REGAD(5) + TA + DATA(16) */ int mdio22_read(uint8_t phyad, uint8_t regad, uint16_t *val) { int i; uint16_t data = 0; mdio_dir_out(); for (i = 0; i < 32; i++) mdio_wbit(1); /* 前导码,多 PHY 总线必须保留 */ mdio_wbit(0); mdio_wbit(1); /* ST = 01 */ mdio_wbit(1); mdio_wbit(0); /* OP = 10,读 */ for (i = 4; i >= 0; i--) mdio_wbit((phyad >> i) & 1); for (i = 4; i >= 0; i--) mdio_wbit((regad >> i) & 1); mdio_dir_in(); /* 释放总线,交给 PHY 驱动 */ mdio_rbit(); /* 第一个 TA 位高阻,丢弃 */ for (i = 15; i >= 0; i--) data |= (uint16_t)mdio_rbit() << i; *val = data; return 0; } int mdio22_write(uint8_t phyad, uint8_t regad, uint16_t val) { int i; mdio_dir_out(); for (i = 0; i < 32; i++) mdio_wbit(1); mdio_wbit(0); mdio_wbit(1); /* ST = 01 */ mdio_wbit(0); mdio_wbit(1); /* OP = 01,写 */ for (i = 4; i >= 0; i--) mdio_wbit((phyad >> i) & 1); for (i = 4; i >= 0; i--) mdio_wbit((regad >> i) & 1); mdio_wbit(1); mdio_wbit(0); /* TA = 10,主机继续驱动 */ for (i = 15; i >= 0; i--) mdio_wbit((val >> i) & 1); mdio_dir_in(); /* 写完释放,避免总线上拉过热 */ return 0; }读完那一位mdio_rbit()是整段代码里最容易被删掉的。删掉之后读出来的数据整体左移一位,最低位恒为 0,看波形完全正常,看数据就是不对。类似地,mdio22_write结尾那行mdio_dir_in()也经常被忽略;如果 MDIO 上拉比较强、而主机一直推低电平,长时间运行会让驱动管发热,虽然不至于烧,但温度上来后时序余量就没了。
有了 clause 22 的读写,扩展寄存器就是纯组合。
/* 扩展寄存器访问:REGCR 是窗口控制,ADDAR 是窗口数据 */ #define REGCR 0x0D #define ADDAR 0x0E int dp83867_ext_write(uint8_t phy, uint16_t addr, uint16_t val) { mdio22_write(phy, REGCR, 0x001F); /* 窗口指向 ADDAR */ mdio22_write(phy, ADDAR, addr); /* 写入目标扩展地址 */ mdio22_write(phy, REGCR, 0x401F); /* bit14=1:禁止地址自增,锁死窗口 */ mdio22_write(phy, ADDAR, val); /* 写数据 */ return 0; } int dp83867_ext_read(uint8_t phy, uint16_t addr, uint16_t *val) { mdio22_write(phy, REGCR, 0x001F); mdio22_write(phy, ADDAR, addr); mdio22_write(phy, REGCR, 0x401F); return mdio22_read(phy, ADDAR, val); }这四个 MDIO 事务必须作为一个原子单元执行。裸机上关中断,RTOS 上拿互斥量,Linux 上靠 phy 框架的总线锁。0x401F里的 bit14 是“禁止自增”,如果误写成0x001F,部分实现会在每次读写后把地址加一,导致你连着写两个相邻寄存器时看起来没问题,隔几个寄存器再写就错位了——这类 bug 复现率极低,排查成本极高。
3.4 参数怎么改、读不到时看哪里
phyad这个参数不是随便填的。它由硬件 strap 引脚在复位瞬间决定,取值范围 0 到 31 中的一部分。如果板子上只挂一颗 PHY,通常 strap 成全 0 或者全 1;挂多颗时按顺序编。代码里我一般写一个扫描函数,从 0 到 31 逐个读 0x02 和 0x03,看看哪个地址能返回合理的值。
判断“合理”的标准是:0x02 不能是 0x0000 或 0xFFFF,0x03 的低 4 位是型号、高 6 位是版本号,通常不会是 0。扫到多个地址有响应也正常,有些 PHY 会响应广播地址。
读不到的时候按这个顺序排:先量 MDC 有没有波形,再看 MDIO 上拉电阻在不在位,然后确认复位是否已经释放、释放后等了多久,最后才是降 MDC 频率。这个顺序不是随便排的,它按“排查成本从低到高”排列。降频率放最后,是因为它往往是上面三项都正常时的补救手段,而不是首选。
4. RGMII 延时与 strap 配置:复位那一刻就决定了一半
总线通了、ID 也读对了,接下来才是真正的硬骨头。这一章的两个主题——strap 和 RGMII 延时——决定了这块板子在物理层能不能跑满千兆,而它们恰恰是最容易被“代码能跑就行”糊弄过去的部分。
4.1 strap 引脚在 RESET_N 上升沿锁存了什么
这颗 PHY 有一组多功能引脚,在正常工作状态下它们是数据线、时钟线或者 LED 驱动,但在复位上升沿的那一瞬间,芯片会把这些引脚上的电平采样下来,写进内部配置寄存器。这就是 strap 机制。
被 strap 决定的配置项通常包括:PHY 地址的低几位、MAC 接口是 RGMII 还是 SGMII、自协商是使能还是强制、时钟输出使能与频率、以及 RGMII 内部延时的使能位。硬件工程师在画原理图的时候,通过上下拉电阻把这些引脚定死;软件工程师在写驱动的时候,必须知道这些默认值是什么,否则你配的延时可能和 strap 配的延时叠加,得到一个完全错误的偏斜。
举个具体的例子:假设硬件用 4.7k 下拉把某个 RX 数据线拉低,strap 结果就是“内部 RX 延时关闭”;而你的驱动里又往延时寄存器写了一个非零值。这时候内部延时到底是开还是关?取决于寄存器和 strap 的优先级——通常是寄存器可以覆盖 strap,但你得先确认这一点,再决定是改电阻还是改代码。
我一般会在初始化流程里加一步:读 strap 状态回读寄存器,把结果打印出来,和原理图上的电阻接法逐一对照。查对照的过程只有十分钟,但能省掉后面几小时的“为什么延时配了没用”。
| Strap 引脚 | 采样功能 | 典型接法 | 回读确认方式 |
|---|---|---|---|
| RX_D0 | PHYAD0 | 下拉=0,上拉=1 | 扩展区 strap 状态寄存器 bit0 |
| RX_D1 | PHYAD1 | 同上 | bit1 |
| RX_D2 | RGMII/SGMII 选择 | 下拉=RGMII | bit 对应位 |
| RX_D3 | 自协商使能 | 上拉=使能 | bit 对应位 |
| RX_CTRL | 内部延时使能 | 依 PCB 走线定 | bit 对应位 |
表格里的“回读确认方式”那一列,具体偏移和位定义要去手册的寄存器总表里查,不同版本会有差异,代码里最好用宏把地址和掩码单独定义出来,改的时候只改一处。
4.2 RGMII 内部延时的档位怎么算
RGMII 的本质是一组源同步总线:发送方向 MAC 输出 125MHz 时钟和 4 位数据,接收方向 PHY 输出 125MHz 时钟和 4 位数据。问题在于,数据是在时钟的哪个沿有效?RGMII 规范要求时钟相对数据有一个约 1.5ns 到 2ns 的偏斜,这个偏斜可以加在 PCB 上(把时钟线走长一点),也可以加在芯片内部(调延时寄存器)。
PCB 上加延时的问题是:走线长度和板材介电常数有关,温度和批次都会影响,一旦板子量产就改不动了。所以现在主流做法是 PCB 上时钟和数据等长走,延时全部放在 PHY 内部用寄存器调。这颗 PHY 的延时寄存器把 TX 和 RX 分成两个四位字段,每档大约 0.25ns。
算一下:125MHz 周期 8ns,四分之一周期 2ns。取 2ns 除以 0.25ns,得到 8 档。这是个很自然的起点。但 0.25ns 是典型值,实际步进会随工艺和温度漂移,所以真正靠谱的做法是“从 8 档开始,上下各扫两档,看哪个范围内误码率最低”。
扫描的方法不复杂:让对端持续打流,记录 CRC 错误计数,每次改一档延时,观察一分钟。把误码为零的档位区间记下来,取区间的中点作为最终值。中点的意义在于留出温漂余量——取区间边缘的话,温度一变化就可能掉出窗口。
TX 和 RX 要分开扫。很多人一次改两个,结果通了也不知道是哪边起了作用。分开扫还有个好处:如果发现 TX 延时怎么调都不影响误码,那大概率是对端 MAC 自己带了延时配置,两边叠加了,这时候应该把其中一边的延时关掉。
4.3 延时的配置与回读代码
延时寄存器在扩展空间里,操作步骤是“读-改-写”,不能整字覆盖,因为同一个寄存器里还有别的位。
#define RGMIIDCTL 0x0086 /* 偏移以手册寄存器总表为准 */ /* tx_step / rx_step 取值 0~15,每档约 0.25ns */ int dp83867_set_rgmii_delay(uint8_t phy, uint8_t tx_step, uint8_t rx_step) { uint16_t v; if (tx_step > 15 || rx_step > 15) return -1; if (dp83867_ext_read(phy, RGMIIDCTL, &v) != 0) return -2; v &= (uint16_t)~0x00FF; /* 只清低 8 位,高位原样保留 */ v |= (uint16_t)(tx_step & 0x0F); /* bit[3:0] TX 延时 */ v |= (uint16_t)((rx_step & 0x0F) << 4); /* bit[7:4] RX 延时 */ return dp83867_ext_write(phy, RGMIIDCTL, v); }那个v &= ~0x00FF是这段代码的关键。如果偷懒直接v = tx | (rx << 4),会把高 8 位一起清零,而高 8 位里可能放着 FIFO 深度或者其他时序相关的配置,结果就是延时调好了、别的功能坏了。
配套还要一个软复位后的重配流程,因为复位会把延时寄存器打回默认值。
#define BMCR 0x0000 int dp83867_soft_reset(uint8_t phy) { uint16_t v; int i; mdio22_write(phy, BMCR, 0x8000); /* bit15 自清零,触发软复位 */ for (i = 0; i < 100; i++) { /* 最多等 100ms */ delay_ms(1); if (mdio22_read(phy, BMCR, &v) == 0 && !(v & 0x8000)) return 0; } return -3; /* 复位不返回,先查时钟是否在跑 */ }软复位之后必须重新写一遍延时、时钟输出这些扩展配置,因为复位只保证标准寄存器回到默认状态,扩展寄存器的行为要看具体实现——有的会一起复位,有的不会。不管是哪种,重写一遍都是最省心的做法。
4.4 用 strap 回读寄存器验证硬件接法
前面反复提到 strap 回读,这里说清楚它怎么用。流程是:上电、等待复位释放、等一小段时间让芯片完成 strap 采样、读回读寄存器、和原理图比对。
发现不一致的时候不要急着改代码,先怀疑三件事。第一,复位脉宽够不够,脉宽太窄会导致采样不稳定。第二,strap 引脚上有没有别的电路在拉,比如 LED 限流电阻、测试点、保护二极管漏电。第三,采样时刻引脚的驱动能力够不够,有些引脚在复位期间是弱驱动,外面挂个大电容就会把电平拖慢。
实际调试中我遇到过一例:某块板子读出来的 PHYAD 和原理图对不上,反复查电阻都是对的,最后发现是复位信号从 MAC 那边过来,MAC 的复位输出是开漏的,上拉电阻又选得偏大,上升沿特别缓,PHY 在阈值附近的抖动导致采样到了错误值。换了个 2.2k 上拉就正常了。这种事光看代码永远看不出来。
5. 排查记录:五个让 link 起不来的坑
下面这五条都是在上板阶段真实遇到过的,按从“最容易误判”到“最难复现”的顺序排。每条都按现象、原因、解决的顺序写,方便对照自己的现场。
5.1 读 PHY ID 全 0xFFFF,但波形看起来正常
现象:MDIO 扫地址,所有地址都返回 0xFFFF,逻辑分析仪抓 MDC 和 MDIO 波形,边沿整齐,前导码、起始位、操作码全对。
原因:波形对不代表时序对。常见的是 MDIO 在读操作时释放得太晚,主机一直在驱动高电平,PHY 那边的驱动器抢不过,读回来的自然全是 1。另一种是 MDC 频率太高,PHY 在上升沿之后还没来得及把数据摆上去,主机就采样了。
解决:先把 MDC 频率降到 2.5MHz 以下试一次,如果通了,就说明是时序余量问题,再慢慢往上提。如果还不通,用示波器同时看 MDC 和 MDIO,重点看 TA 那两个周期里 MDIO 有没有从高电平变成 PHY 驱动的低电平。如果一直保持高,说明主机方向切换没生效,检查 GPIO 的方向寄存器有没有写对。
5.2 自协商能完成,但一 ping 就丢包
现象:读 BMSR 显示链路建立、速率 1000M、全双工,但 ping 大包必丢,小包偶尔通。
原因:九成是 RGMII 延时没配好。自协商只走 MDIO,跟 RGMII 数据通路无关,所以链路能建立;一旦真的开始传数据,建立保持时间不够,MAC 侧收到的是错包。
解决:先把 TX 和 RX 的延时都设成 0,然后固定一边、扫另一边。用对端持续发大包,同时读 MAC 的 CRC 错误计数和接收错误计数,找到误码为零的档位区间后取中点。如果两边都扫过还是不行,去确认对端 MAC 有没有开自己的内部延时,两边都开会叠加成四倍偏斜。
5.3 扩展寄存器写进去读不回
现象:写延时寄存器返回成功,立刻读回来发现还是旧值。写标准寄存器完全正常。
原因:REGCR 的 bit14 没置。如果只写了地址、没锁窗口,某些实现会在下一个 MDIO 事务里把窗口指针复位到默认值,导致最后那一次数据写落到了别的地址上。
解决:确认写窗口控制的时候用的是带 bit14 的值。另外要确认这四次 MDIO 事务之间没有被中断打断,裸机上看中断优先级,RTOS 上看有没有加锁。
5.4 软复位之后所有配置都丢了
现象:初始化流程跑完一切正常,中间执行一次软复位,然后再读延时寄存器,回到了默认值,链路质量明显下降。
原因:软复位只保证标准寄存器回到 IEEE 定义的默认值,扩展寄存器是否复位取决于芯片实现。多数情况下扩展配置会被清零,只是文档里写得不显眼。
解决:把“软复位 → 等待复位完成 → 重写所有扩展配置”封装成一个函数,凡是需要复位的地方都调它,不要单独调复位。我一般还会在重配之后做一次回读校验,把关键寄存器的值和期望值比一遍,不一致就报错,别让它默默跑起来。
5.5 百兆通、千兆不通
现象:强制百兆一切正常,切到千兆就链路抖动或者完全起不来。
原因:千兆对时钟质量比百兆敏感得多。常见的是 PHY 的参考时钟频偏超限、抖动太大,或者是 RGMII 时钟走线经过了噪声区。另一个高频原因是内部延时的档位在千兆下不够,百兆周期 40ns,余量大得多,同样的偏斜在百兆下看不出来。
解决:先用频率计测参考时钟,看频偏是否在手册给的范围内,再看电源纹波,尤其是 PHY 的模拟电源。时钟和电源都没问题,再回头把延时重新扫一遍,这次按千兆的误码标准来判。还有一个容易被忽略的点:千兆下自协商要确认是对端和本端都通告了 1000BASE-T 全双工,有时候交换机端口被手动配成了百兆。
6. 上板联调的验证顺序:先证总线,再证时序,最后证流量
这套顺序是我踩过几次“跳步”的坑之后定下来的。跳步的代价是:问题出现时你不知道是哪一层的错,只能从头再来一遍,时间反而更长。
第一步证总线。扫描 PHY 地址,读 ID,回读几个标准寄存器,确认读写的值能对得上。这一步的判据很简单:能读到非 0x0000、非 0xFFFF 的 ID,且写入再读出的值一致。做不到就别往下走,先把 MDIO 的时序和方向切换彻底搞定。
第二步证复位与 strap。执行软复位,确认复位位能自清零;读 strap 回读寄存器,和原理图逐位比对。这一步能提前暴露硬件接错、上拉选错、复位脉宽不够这些“代码改不动”的问题。
第三步证配置。写延时、写时钟输出、写中断掩码,每写一个立刻回读,做成一张自检表。
| 阶段 | 操作 | 期望结果 | 失败先查 |
|---|---|---|---|
| 总线 | 扫 0~31 读 0x02/0x03 | 唯一地址返回有效 ID | MDC 频率、方向切换 |
| 复位 | 写 BMCR bit15 | 100ms 内自清零 | 参考时钟、供电 |
| strap | 读 strap 状态寄存器 | 与原理图逐位一致 | 上拉阻值、复位脉宽 |
| 配置 | 写延时后回读 | 读回值等于写入值 | REGCR bit14、总线锁 |
| 链路 | 读 BMSR 两次 | 两次都报 link up | 对端端口配置 |
| 流量 | 持续打流 10 分钟 | 误码计数为零 | 延时档位、时钟质量 |
第四步才是打流。打流的时候不要只看通不通,要看计数:CRC 错误、对齐错误、接收溢出,这三个计数在多数 MAC 里都有。只要有一个在缓慢增长,就说明延时余量不够,哪怕 ping 一次都没丢。
最后留一个习惯:每次改完延时或 strap 相关的配置,我都会把整块板子断电、重新上电、再跑一遍十分钟的打流。热复位和冷启动的表现经常不一样,因为 strap 只在冷启动的复位上升沿采样,热复位有时候不会重新锁存。这个动作花十分钟,但能挡掉那种“实验室好了、客户现场挂了”的返工。
那份 dp83867cr.pdf 和 dp83867.c 建议就放在工程目录下,改一次参数就回去翻一次寄存器表,别凭记忆写地址。希望帮到你。
本文还有配套的精品资源,点击获取