1. 以太网驱动开发到底在折腾什么先把职责边界划清楚做嵌入式驱动开发这几年跟 Ethernet 打交道的时间越久越觉得很多人不是不会写代码而是被以太网驱动这个词本身的浓度吓住了。拿着一张开发板照着例程把网口调通ping 一下网关能通就觉得事情结束了等真正部署到现场问题接踵而至千兆协商不上、跑大包死机、跟西门子或安川设备通讯时好时坏才开始重新怀疑驱动里每一行代码。这一期就把我做嵌入式驱动开发的经验集中聊一下重点放在 Ethernet 方向上。先说一个每期都会强调的观点驱动开发的第一件事不是抄寄存器而是划清职责边界。一个完整的以太网路径从对端设备到你 CPU 里的应用程序至少有五层参与物理介质网线、变压器、PCB 走线、PHY 芯片、MAC 控制器、DMA 引擎、协议栈。我们的驱动代码覆盖的是 PHY、MAC、DMA 这三层协议栈是交接对象物理介质是边界条件。很多时候网络不通的根因根本不在驱动代码里而是在别的层但别人只会找驱动背锅。1.1 从信号到数据包的完整链路PHY 管什么MAC 管什么我用一个快递系统的类比解释这五层网线和变压器是实际运输道路PHY 芯片是司机兼装卸工它负责把 MAC 给的并行数据编成线路信号发出去也负责把收到的模拟信号解调成并列数据交回给 MAC。MAC 是快递分拣中心负责组装一帧帧快递件即以太网帧头、目的地址、源地址、类型长度、FCS 校验同时决定什么时候发、什么时候等。DMA 是传送带负责在内存和 MAC 之间搬运数据包不占 CPU 太多时间。协议栈则是调度员决定把快递件交给哪个应用IP、TCP 这些逻辑都在这一层。所以 PHY 芯片常见的型号有瑞昱 RTL8211、裕太微 YT8512/YT8531、Microchip LAN8720、TI DP83848 这些。它们的工作由 MDIO/MDC 两根线控制里面是标准寄存器由 IEEE 802.3 定义了前 16 个寄存器的基本含义。而 MAC 控制器通常集成在 SoC 内部比如 STM32 的 ETH 外设、NXP i.MX 的 FEC、全志/瑞芯微平台自带的 GMAC。MAC 侧要配置的是帧格式、流控、时钟源、接口模式、DMA 描述符等。1.2 为什么指示灯常亮但数据不通是家常便饭很多年前我第一次调板子接上安川机器人和焊机的以太网通讯双方链路指示灯都是绿的可程序一直报通讯超时。按照按寄存器排错的思路我先把 PHY 寄存器的 link status 读出来确实已经是向上的状态速率也协商成了百兆说明物理链路是通的。问题出在 MAC 到 DMA 通道之间我配置的接口模式是 MII而 PHY 侧实际工作在 RMII两边数据宽度和时钟协议不同自然会丢帧丢到通讯彻底超时。这类问题的本质就是职责边界没有划清。PHY 寄存器正常只代表司机和道路没问题不代表分拣中心和传送带正常工作。所以我后来养成了一个习惯写驱动之前先把 SoC 参考手册里 MAC、DMA、PHY 三部分各画一张简化框图标注哪部分由哪层驱动负责再开始编码。这一步节省的排查时间远比想象中多。2. PHY 芯片初始化与 MDIO 读写看似简单却是翻车重灾区2.1 PHY 地址、MDIO 时序与第一个必须避开的坑PHY 的寄存器读写接口是 MDIO一根管理时钟 MDC一根双向数据线 MDIO。绝大多数 SoC 都内置了 MDIO 控制器配置好时钟分频就可以读寄存器。但最早调试的时候我强烈建议先确认 PHY 地址。这个地址不是随便来的它由 PHY 芯片的 strapping 引脚决定一般用电阻上拉或下拉配置范围通常 0x00 到 0x1F。我遇到过两种常见翻车一是时钟分频配错导致 MDIO 时序不满足 2.5MHz 的要求读出来全是 0xFFFF二是 PHY 地址和原理图对不上读的寄存器根本不是这颗芯片的。MDIO 帧结构是这样的先是 32 个全 1 的 preamble然后 2 位起始码接着 2 位操作码读是 01写是 105 位 PHY 地址5 位寄存器地址读操作里 2 位转换态之后是 16 位数据。有个隐蔽细节读操作时PHY 芯片在转换态的第二个时钟沿后把 MDIO 驱动权交出去如果 SoC 的 MDIO 引脚没有内部上拉读到的高位会被下拉吃掉出现数据偏移一位这类诡异现象。我写过一版 GPIO 模拟的 MDIO 读函数放在下面给做低成本平台的参考static uint16_t mdio_read_gpio(uint8_t phy_addr, uint8_t reg_addr) { uint16_t val 0; int i; // preamble连续输出 32 个 1 for (i 0; i 32; i) { mdio_mdc_cycle(1); } // 起始码 操作码读操作是 01 10 mdio_write_bit(1); mdio_write_bit(0); mdio_write_bit(0); mdio_write_bit(1); // PHY 地址5 bit高位先出 for (i 4; i 0; i--) { mdio_write_bit((phy_addr i) 1); } // 寄存器地址5 bit for (i 4; i 0; i--) { mdio_write_bit((reg_addr i) 1); } // 转换态先释放总线从此时起 PHY 开始驱动 mdio_set_dir(MDIO_DIR_IN); mdio_mdc_cycle(0); mdio_mdc_cycle(0); // 读 16 bit 数据 for (i 15; i 0; i--) { mdio_mdc_half_cycle(); val | (mdio_read_level() i); mdio_mdc_half_cycle(); } mdio_set_dir(MDIO_DIR_OUT); return val; }2.2 自协商与链路检测不要只读 bit要有状态机思维PHY 初始化最核心的是自协商。基本流程是写控制寄存器地址 0x00的自动协商使能位和重启自协商位然后等待状态寄存器地址 0x01的 link status 拉高再读速度相关寄存器判断当前协商结果是千兆、百兆还是十兆是全双工还是半双工。这里我踩过一个有意思的坑link status 位在状态寄存器里的定义是短暂下降沿表示链路断开读取后清零的模式也就是边沿触发。有些 PHY 手册写得比较隐晦如果只简单轮询这个位遇到一次抖动之后寄存器会一直保持低电平导致驱动误判链路断开。正确做法是检查 0x01 寄存器 bit 2 的滞后值或者连续读取两次后一次才是当前真实状态。调试时最好把PHY ID、AN 完成、link up、速率、双工这几个值全打印出来而不是只看一个 link up。自协商里还有一个容易遗漏的点千兆模式下1000BASE-T 需要确定主从时钟关系这由控制寄存器 0x09 相关字段决定。某些 PHY 在默认主从设置下和交换机互连会出现协商结果千兆但不稳、偶尔降速到百兆的情况。遇到这种把 PHY 配成 prefer master或两边固定成一方 master 一方 slave情况往往立刻改善。3. RGMII 与 SGMII 接口选型1G/2.5G 速率背后的时钟与 PCS/PMA 逻辑3.1 三种常见 MAC 与 PHY 接口的取舍做以太网驱动迟早要面对一个问题MAC 和 PHY 之间用什么接口对接。目前主流 SoC 上最常见的三种是 RMII、RGMII、SGMII另有速率更高的例如 USXGMII 用于 10G 场景日常接触不多。RMII 是 50MHz 时钟2 根数据线双沿采样适合百兆产品缺点是带宽上限明显。RGMII 是 125MHz 时钟4 根数据线双沿采样等效 1Gbps线少是千兆板卡的主流。SGMII 则把并行数据转成一对高速差分线数据速率 1.25Gbps在连接器、长走线场景非常常见2.5G 速率的 SGMII 实际运行在 3.125Gbps 符号率上。我常跟人说接口选型一遍做对胜过写代码时反复迁就。表里列一下三种接口的差异方便做方案时直接参考接口时钟频率数据线编码方式典型速率注意事项RMII50MHz2位不编码10/100M时钟开销低适合低成本RGMII125MHz4位双沿不编码10/100/1000M时钟时序敏感需 delay 配置SGMII1.25Gbps 符号率1对差分8B/10B10/100/1000M带 PCS/PMA 层速率扩展性好2.5G SGMII3.125Gbps 符号率1对差分8B/10B100/1000/2500M需要双方都支持 2.5G 模式3.2 RGMII 的时钟相位与 txdelay 陷阱RGMII 看起来简单实际上最容易因为时钟相位搞到丢包。RGMII 的发送端要求数据与时钟同步输出接收端却要求在时钟中心采样也就是数据要相对时钟偏移半个周期。为此标准规定发送方向必须有一方加 2ns 左右的 delay接收方向同理。问题是有些 PHY 在内部加入了 TX delay有些 SoC 的 MAC 内部也有 delay 选项两边各加一次或者都没加就会导致采样边沿完全错位表现为千兆速率下 ping 不通、百兆反而正常。调试这类问题经验是先把 PHY 侧的 delay 选项找清楚。比如常用的 YT8531 和 RTL8211F寄存器里都有控制 RGMII TX delay 和 RX delay 的位不同版本芯片默认值还不一样。我的做法是把 PHY 的所有 delay 关闭依靠 MAC 内部 delay 承担全部偏移如果 MAC 不支持则把 PHY 的 RX delay 开启。然后做一轮 64 字节短包压力测试通过后再测 1518 字节大包。3.3 SGMII 里的 PCS/PMA 是干什么的1G 与 2.5G 的自协商很多人看到 SGMII 就一头雾水觉得不就是串口吗。其实 SGMII 只是外部接口形态在它内部隐藏着一整套物理编码子层也就是 PCS 和 PMA负责把 GMII 格式的数据流做 8B/10B 编码解码、时钟恢复、链路同步和自协商。PMA 是模拟收发器也就是高速 SerDesPCS 是数字逻辑层负责编码和状态机。所以当你打开 MAC 手册看到设备树里配置了 phy-mode sgmii并看到专门给 SerDes 或 PCS 配置 PLL 时钟就不要奇怪为什么没有并行数据线了。SGMII 的自协商机制和 RGMII 完全不同。传统 SGMII 是通过内部的 auto-negotiation 通告 PHY 侧工作速率交换机常见的有 1000BASE-X 自协商两者不能混为一谈。我遇到过一台工业交换机PHY 配置成 SGMII 自动协商后一直协商失败把 MAC 侧配置成无自协商、master 模式后反而稳定。这块没有统一答案必须看具体 SoC 和 PHY 手册。2.5G SGMII 更是如此它需要 MAC 和 PHY 两边的 SerDes 速率一致跑在 3.125Gbps如果一边默认还是 1.25Gbps链路起不来速率协商也根本不会发生。我建议上板之前先确认 RMII、RGMII、SGMII 对应的接口模式寄存器位千万别用默认值。4. DMA 描述符与收发路径吞吐稳定与 CPU 占用之间的矛盾怎么解4.1 环形描述符的 ownership 流转必须写清楚以太网驱动性能高低七成取决于 DMA 描述符的设计。现代 MAC 控制器普遍使用描述符环一块内存里连续放一组描述符每个描述符指向一块数据缓冲区同时记录长度、错误标志、所有权等状态。所有权规则很简单描述符里一个 ownership 位为 1 表示 DMA 硬件拥有这块缓冲区为 0 表示软件拥有。发送时驱动把待发送的数据地址、长度填进描述符然后把 ownership 置 1DMA 会自动取走并发送DMA 完成后会将 ownership 清 0并产生中断。接收则是驱动预先准备好一批空缓冲区DMA 收到包后把数据写进缓冲区再把 ownership 清 0 并通知 CPU。这个流转逻辑我在纸上画过很多次真正容易翻车的点是环绕索引的处理。描述符环通常有 8、16、32 个描述符驱动软件维护一个发送索引和一个已完成索引接收侧维护一个软件索引。如果你在中断里同时操作这两个索引又没有处理好时序很容易出现重复释放同一个描述符直接导致内存损坏。我建议把描述符环的状态打印调试函数做成独立模块凡是能出现竞争的地方加断言检查这在上板初期能省下一大笔时间。4.2 中断、轮询与 NAPI 的取舍收发路径里第二个核心问题是 CPU 占用。数据量小的时候每收一包就进一次中断完全没问题。到了满负荷千兆线速每秒钟会有接近 150 万个小包如果每个包都触发一次中断CPU 会彻底陷入中断风暴。这时候有几个常用手段。第一是中断合并MAC 可以配成每收到 N 包或攒够 T 微秒才触发中断这在吞吐要求不高但 CPU 预算紧张的场景非常好用。第二是轮询模式驱动在启动后开一个周期任务定时收包代价是延迟上行不稳定。第三是类似 Linux NAPI 的思路把中断触发 关中断 轮询收包组合起来也就是第一次中断进入后暂时屏蔽该中断在轮询循环里把描述符环中所有包收完再重新开中断。这样既能减少中断次数又能保证小包的及时性。我在嵌入式 Linux 内核里写网卡驱动时最常用的是后者在裸机 RTOS 平台则更多用中断加轮询混合具体参数要看 CPU 剩余预算。一个可以照搬的经验先用串口打印统计每秒收包数再实际看 CPU 占用率如果中断占用超过 30%就该考虑合并或 NAPI。4.3 接收缓冲区大小与超长帧别在量产时才想起接收缓冲区大小也是个看着简单、实际要命的问题。以太网最大帧通常是 1518 字节含 14 字节头部、1500 字节有效载荷和 4 字节 FCS如果 VLAN 标签则加 4 字节达到 1522 字节。很多驱动把接收缓冲区设成 1536 或 2048。可一旦对端开启了 Jumbo Frame 或使用了某些厂商自组包一个包可能到 9000 字节缓冲区不够就会丢大包。这种问题最麻烦的地方在于联调环境里没人开大帧到了现场对方设备默认开启了才突然丢包。我的建议是只要内存不紧张接收缓冲区统一按 2KB 或 4KB 对齐设计把描述符数量控制在 32 到 64 个之间。这样既兼容 Jumbo 场景也不会因为缓冲区太小导致 DMA 截断。至于发送方向如果应用层会一次发送较大数据块要提前在 lwIP 里配好 PBUF 内存池否则就会出现偶尔发送失败的玄学问题。5. 跟 lwIP 协议栈对接时的三个契约回调、内存池与校验和5.1 netif 初始化的回调链条裸机或 RTOS 场景下最常用的协议栈是 lwIP。驱动部分最重要的事是把底层收发和 lwIP 的 netif 结构体对接好。lwIP 里每个网卡对应一个 struct netif需要填上链路层类型、硬件地址、MTU 和一些回调函数。初始化时协议栈会调用 netif-init 指向的函数你的 init 函数里通常要做三件事复位 MAC、配置 PHY、注册 MAC 地址并把 netif 的 state 指针指向你的私有驱动结构体。对接本身不难难在收发路径要各就各位。发送时协议栈调用 netif-linkoutput对于以太网就是 ethernet_output指向你的 low_level_output 函数数据包 pbuf 可能是多个内存片段连成的链表驱动需要逐个把片段的物理地址拆出来组装 DMA 描述符。接收路径则反过来DMA 收到数据后驱动从私有结构体里找一块空闲缓冲区拷贝成 pbuf再调用 netif-input 把包上抛给协议栈。这里的性能关键点是尽量减少数据拷贝能做到描述符指向 pbuf 实体就比每次收包先 memcpy 到 pbuf 再上抛快得多。我接手过一套遗留代码接收路径每次包进来都要复制一次千兆实测只能跑 100 多 Mbps。改成描述符和 pbuf 复用之后吞吐直接翻倍。所以如果你发现网络吞吐总上不去先去代码里数一数数据副本有几份。5.2 内存池数量不够表现是间歇性丢包还查不出lwIP 的 pbuf 内存池配置在 lwipopts.h 里。调低内存池数量可以省 RAM但高负载下收包缓存不够用驱动会直接把新收到的包扔掉。抛开物理层调试不谈这是最典型的软件级丢包原因。具体来说PBUF_POOL_SIZE 决定协议栈可以同时缓存的包数量每个收到的包至少要占用一个 PBUF_POOL 类型的 pbuf。如果现场大量 TCP 重传或者小包特别多内存池被占满后续的包只能丢弃。我从前的判断经验是在同一固定负载下反复测试如果丢包率和内存池用量同步增长优先加大 PBUF_POOL_SIZE并适当调大 PBUF_POOL_BUFSIZE比如设成 1600 字节而不是 1518 字节。注意检查 lwIP 默认配置里 PBUF_POOL_BUFSIZE 是否覆盖整个以太网帧头如果不够收包时还得额外做拼接驱动复杂度会上升。5.3 checksum offload 与协议栈校验宏必须配对现代 MAC 支持硬件校验和也就是 IP、TCP、UDP 的 checksum 计算由 DMA/MAC 完成不占用 CPU。lwIP 侧对应一系列配置宏CHECKSUM_GEN_IP、CHECKSUM_GEN_TCP、CHECKSUM_GEN_UDP 表示发送时由谁生成校验和CHECKSUM_CHECK_IP、CHECKSUM_CHECK_TCP、CHECKSUM_CHECK_UDP 表示接收时由谁校验。如果驱动或硬件已经做了校验这些宏就要关掉否则协议栈会再算一遍或者认为数据校验是别人的事直接导致校验失败丢弃。我调试过一个问题TCP 连接可以建立但是大批量传输时小概率卡死重启就好过一阵又卡。后来发现是接收路径的 checksum offload 开启了但 lwIP 里依然开着 CHECKSUM_CHECK_TCP协议栈收到硬件已经校验过的包又自己校验一次某些描述符里保留字段被 MAC 覆盖导致校验随机失败。把宏改掉以后问题彻底消失。我的习惯是初始化驱动时把所有 checksum 能力打印出来和 lwipopts.h 的宏逐项对比确保两头一致。6. 现场排障记录从焊机通讯时好时坏反推链路问题6.1 排查顺序决定效率别一上来就怀疑驱动实践经验告诉我现场排障一定要按物理链路 - PHY 寄存器 - MAC 统计 - 协议栈收包 - 应用层这个顺序来查每层都有明确证据才可以向下走。很多人一上来就抓应用层日志或者反复改驱动代码结果问题跟当层毫无关系。具体做法先看双方网口状态链路指示灯是否常亮速度是协商到千兆还是掉到百兆再通过串口进到板子用 MII 工具或自己写的一行命令读取 PHY 寄存器把 LINK、SPEED、DUPLEX、AN 完成位打出来接着看 MAC 侧的统计寄存器重点关注 CRC 错误计数、FIFO 溢出计数、丢包计数。这些信息能让问题快速缩小到一个层。我的印象里80% 的以太网不稳定是物理层或硬件走线问题只有 20% 是驱动软件缺陷。6.2 案例一千兆协商一再掉成百兆查到最后是 PCB 走线问题有个项目现象是板子接千兆交换机时偶尔协商成功但过几分钟链路会重新协商最后稳定在百兆。单独测试时百兆也能工作所以客户一直以为是驱动 bug。我先读 PHY 状态发现自协商从来没在千兆上稳定过然后看 MAC 统计千兆尝试阶段 CRC 错误特别高。这就说明信号完整性问题而非软件问题。RGMII 千兆对时序要求严苛一旦 PCB 的 TX 时钟线没有和差分数据线做等长或者内层参考平面被切断就很容易出现 CRC 错帧。最后是改版调整走线长度和阻抗才解决的。这类问题有个典型特征干扰是随机出现偶尔还会因为温度变化改变。我总结了一条经验凡是千兆必挂、百兆正常的板子第一嫌疑是 RGMII 时钟偏移没配好第二嫌疑是 PCB 走线第三才是 PHY 芯片本身。6.3 案例二焊接电弧一起以太网就开始闪断另一个现场案例更有代表性设备是安川机器人与焊机经以太网通讯平常调整程序毫无问题但一旦焊接电弧起来网络就开始断电弧停了又恢复。从 PHY 寄存器看LINK 位频繁翻转说明物理层的接收信号质量变差已经不是驱动能控制的范围。查下来根源是焊机大电流回路和以太网线走线距离太近焊接时的高频干扰直接耦合到了网线。这个问题用软件只能缓解不能根治。我当时的处理一是把网线换成带屏蔽层的工业网线并确保屏蔽层单点接地二是调整 PHY 的 downshifting 和较强抗噪模式让链路不至于一有干扰就立刻断开三是把自适应速率从千兆固定到百兆降低信号裕量需求。这些措施都是争取时间真正解决还是改动设备布局让以太网走线远离功率线缆。嵌入式 Ethernet 现场调试就是这样很多时候驱动只是最后一道防线指望代码填硬件挖的坑结果通常是填不满的。做移动设备外接 USB 转以太网适配器的驱动时思路也是类似的常见问题是系统缺少 CDC-ECM 或 CDC-NCM 类驱动设备管理里能识别到网卡却无法正常收发本质上还是驱动和协议栈之间的接口没对齐。先看枚举出来的 USB 接口类型再确认内核配置的驱动类别通常比直接找某某驱动下载更对症。第 10 期写到这里正好覆盖了嵌入式 EEthernet 驱动最常打架的几条线PHY 初始化、接口时序、DMA 收发、协议栈对接、现场排障。每次做完一个调试周期我都习惯把当时的 PHY 寄存器快照和 MAC 统计备份留档。这个习惯帮我挺过很多次明明上次没问题这次又不行的困局。下一期打算挑其中一块展开比如 SGMII 上 PCS/PMA 寄存器的一步步分析过程这个值得单独说透。