嵌入式Linux以太网驱动开发:从PHY调试到性能优化实战
做嵌入式驱动开发迟早都会撞上 Ethernet 以太网这关。这个《嵌入式驱动开发经验》系列来到第 10 期前面写了 GPIO、中断、DMA、串口、Flash、电源、时钟这些常用外设每一期我都尽量多写点现场攒下来的问题。这次选以太网原因很直接以太网接口在嵌入式系统里太常用了从家用路由器、工业网关到车载域控制器网口承担的任务早就不是“ping 通就行”而是要求长时间稳定、突发流量不丢包、各种干扰环境下不死机。它和普通外设驱动的最大区别在于链路长、变量多牵一发动全身一旦出问题涉及硬件、PHY、MAC、驱动和协议栈好几个层面。这篇我从实际项目出发按这个顺序展开先说硬件底座也就是 MAC、PHY 和 MII 接口怎么选这部分坑最多然后看 Linux 驱动里 net_device、DMA 描述符、NAPI 之间怎么配合接着给一个“链路能通但吞吐量不对”的完整排查链路最后聊性能优化和车载以太网带来的新问题。适合正在写嵌入式 Linux 网口驱动的朋友也适合已经能把网口点亮、但面对 PHY 调试和性能调优仍容易头疼的人。1. 网口的第一重身份是调试窗口不只是产品功能1.1 先讲一段我自己的经历早期做嵌入式 Linux 项目板子上 Flash 小串口速度也慢最常用的调试方案是bootloader 起来之后先用 tftp 把内核拉到内存再通过 NFS v3 挂载远程根文件系统整个文件系统都放在开发机上。这样反复改驱动、改应用都不需要重新烧写 Flash一条网线就把迭代速度提上来了。这个方案本身很成熟但前提是网口必须足够稳定。我记忆很深的一次是板子刚启动时就能看到内核日志说明网络文件系统挂载已经成功可跑十几分钟之后整块板子就卡住串口连命令都敲不进去。当时我以为是内存问题一直在查申请、释放、越界折腾了两天才发现是网口驱动在特定流量下中断疯了DMA 描述符也被反复踩掉。那一瞬间我才真正意识到以太网驱动在嵌入式项目里往往是最靠近系统稳定的那根弦它不稳定后面跑什么都白搭。所以我把这一期定位成“调试视角的以太网驱动经验”而不是各种控制器的寄存器手册复读。芯片型号你可以自己查但排查思路和容易踩的坑手册上往往一句都没写。1.2 不同产品形态对以太网驱动的要求完全不一样同样是写以太网驱动产品不同侧重点差别非常大。我简单分三类说方便你对号入座工业网关、PLC、边缘采集设备这类设备最怕长时间运行后链路掉线。现场环境电磁干扰大网口变压器到 PHY 之间的走线稍微差一点就会出现“抓包正常但 ping 丢包”的怪现象。这里你得多花时间在 PHY 的寄存器配置、中断处理和链路检测上。家用路由器和智能终端这类产品关注的是吞吐量和多连接稳定性。驱动要做到高负载下 CPU 占用率不高中断不能成为瓶颈DMA 环形缓冲区也要够用否则多设备同时上网就会随机卡顿。车载域控制器这是近几年增长很猛的方向车载以太网普遍用 100BASE-T1 这类非屏蔽单对双绞线物理层和处理方式跟传统以太网都有区别驱动还需要兼顾时间同步、低延迟和睡眠唤醒。搞清楚自己属于哪一种再去看驱动代码效率会高很多。很多新手一上来就背 MAC 控制器的寄存器布局却不知道 PHY 协商、DMA 映射和 NAPI 机制这就像知道油门在哪却不会判断路况开车肯定要出问题。2. 硬件底座的选型决定了驱动调试的难易程度2.1 先分清楚 MAC 和 PHY 到底各管什么很多嵌入式工程师从单片机的裸机以太网开始接触容易把 MAC 和 PHY 混在一起看。实际上这俩是两回事MAC 控制器通常集成在 SoC 内部负责数据链路层的帧封装、地址过滤、CRC 校验以及和内存之间的 DMA 搬运PHY 是物理层收发器负责把 MAC 出来的并行或串行数据调制成可以在网线上传输的模拟信号同时负责自动协商、链路状态检测。如果打一个粗糙的比方MAC 是高速公路入口的收费站负责核对车辆信息、安排车道PHY 是收费站到路面之间那段匝道负责把车辆平稳送上去并检测前方路是不是通的。两者之间还要有一条“引道”也就是 MAC 与 PHY 之间的接口这就是 MII 家族。驱动开发要访问 PHY通常不是直接读写 PHY 芯片的寄存器地址而是通过 MAC 一侧的 MDIO 总线用规范的 PHY 寄存器地址去访问。PHY 的标准寄存器主要由 IEEE 802.3 定义比如寄存器 0 是控制寄存器含自动协商开关、速度/双工选择、回环测试位寄存器 1 是状态寄存器含链路状态、自动协商完成标志。知道这些标准寄存器很多排查工作就可以脱离 datasheet 先做一轮。2.2 常见的 MAC 与 PHY 接口选型MAC 和 PHY 之间的接口主流的有 MII、RMII、GMII、RGMII、SGMII 等。选择哪种接口直接影响 PCB 走线难度、可支持的速率和驱动配置。下面这张表是我常用的归类接口数据方向典型速率引脚数说人话MII4 位并行10/100M约 16 根古老的接口时序指标相对宽松但现在新芯片不太用了RMII2 位并行10/100M约 10 根减少引脚但要提供 50MHz 参考时钟时钟源容易出问题GMII8 位并行1000M约 24 根引脚多、布线痛苦新设计很少直接用RGMII4 位 DDR1000M约 12 根双沿传输PCB 好走是目前千兆板子最常用的口SGMII1 对差分1000M/2.5G约 6 根串行差分引脚极少高速板子优选100BASE-T1单对差分100M2 根车载以太网专用物理层处理完全不同这里我特别想提醒的是项目需求是百兆还是千兆决定了你基本只能在 RMII 和 RGMII 之间做常规选择。如果你一开始选了 RMII却忽略了 PHY 芯片的 XI 时钟是从 MAC 还是外部晶振引入后面很可能出现“PHY 不工作但用示波器看引脚又像有波形”的伪故障。这两年做 2.5G 的板子越来越多凡是型号上写着 1G/2.5G Ethernet PCS/PMA or SGMII 的器件MAC 侧接口基本都落在 SGMII 这一档。SGMII 的优势是引脚少、路由干净但代价是 MAC 和 PHY 两边都要做 8b/10b 编解码PCS 层的一部分功能挪到了 PHY 里。驱动端要做的事情并没有少反而要更关注链路训练结果和扩展寄存器里的状态排查难度比 RGMII 要高一截。2.3 设备树里的 phy-mode 不是随便填的在嵌入式 Linux 项目里MAC 与 PHY 接口的配置一般在设备树里以 phy-mode或某些平台叫 phy-interface表示。很多工程师不重视这个字段结果网口不稳定找了一天电路问题最后发现只是 dts 里写错了。常见的错误演示是这样的mac { pinctrl-names default; pinctrl-0 mdio_pins rgmii_pins; phy-mode rgmii-id; phy-handle ethphy0; status okay; }; mdio { ethphy0: ethernet-phy0 { reg 0; reset-gpios gpio4 22 GPIO_ACTIVE_LOW; }; };这里的重点是rgmii-id。RGMII 是双沿采样对 RX 和 TX 两条路径上的时钟延迟非常敏感。芯片设计时一般会建议由 MAC 或 PHY 给某一侧加延迟具体加在哪一侧不同厂商甚至不同批次的 PHY 都不一样。开发板上通常有默认的“正确配置”但产品板按自己 PCB 走线长度来裁剪时你可能需要从rgmii-id改成rgmii-txid、rgmii-rxid或者直接写rgmii。如果 phy-mode 配错了现象往往是这样的百兆模式下稳定切到千兆后链路起来就会反复断开或者能用 ethtool 看到 1000M 全双工但实际跑吞吐就奇低。遇到这种问题别急着改驱动代码先把 phy-mode 和 PHY databook 里关于 delayed clock 的说明对齐。我个人的建议是硬件设计阶段就把 MAC 和 PHY 的距离算进去同时预留 PHY 端寄存器调试的余地。驱动开发时不光看 PHY 芯片型号还要看它的封装选项里是否支持通过 strapping 引脚或寄存器配置时钟延迟。这一点决定你是改两根线还是改一行 dts。3. Linux 网络驱动里最容易出问题的往往不是 net_device 本身3.1 net_device、netdev_ops 和 sk_buff 的分工Linux 网络驱动最外层是struct net_device它有点像设备的一张“身份证”里面记录网口名称、MAC 地址、支持的特性标志、当前状态。驱动要完成的主要动作都挂在netdev_ops回调里比如打开网口对应ndo_open发一个包对应ndo_start_xmit关闭网口对应ndo_stop。内核往驱动送报文统一用的是sk_buff。驱动要做的事情不是把sk_buff里的数据直接搬到 MAC而是先把这段数据映射成 DMA 能够访问的物理地址再填入 MAC 控制器维护的描述符表里。描述符里包含数据地址、长度、状态位以及下一个描述符的地址。新手第一次看网口驱动时经常迷失在大量回调函数里其实核心就两个方向发送方向协议栈把sk_buff交给ndo_start_xmit驱动填发送描述符然后告诉 MAC“有活干了”等传输完成后再回收sk_buff。接收方向MAC 收完一段数据把数据写进 DMA 指向的内存然后触发中断驱动在中断里通过 NAPI 机制把数据封装成sk_buff交给协议栈。那为什么很多初学者的网口驱动能通但一跑业务就崩大部分是因为只把收发流程跑通没有管住“描述符被耗尽”“DMA 缓冲区和缓存一致性”“NAPI 的预算粒度”这些底层问题。3.2 DMA 环形描述符是一个很容易被忽略的“水库”现在的 MAC 控制器普遍用环形描述符。你可以把环形描述符看成一条环形的流水线生产者是 DMA消费者是驱动。驱动在初始化时一次性分配若干个描述符并提前申请对应的 DMA 缓冲区。发送时拿到空描述符就填数据接收时描述符被 MAC 写入数据后驱动就在中断或轮询里把它们收走。这个环形队列最怕两件事一是生产者跑得比消费者快导致描述符不够用新到的报文只能被硬件丢弃。表现是网口还可以收包但 iperf 一跑就大量丢包ethool统计里 RX 的 dropped 或 ring_full 持续增长。二是驱动对描述符的回收逻辑写错比如没有正确推进 head/tail 指针导致 DMA 写内存时覆盖了一个还在使用的sk_buff随后内存被踩系统诡异崩溃。所以我在评审网络驱动代码时第一件事就是看 DMA 描述符的分配数量、初始化时是否预申请了足够的缓冲区、以及硬件指针和软件指针的推进逻辑。很多看似“偶发死机”的网络问题最终都定位到环形描述符这里。3.3 NAPI 机制真正的价值是给中断“降温”嵌入式平台和 PC 不一样CPU 资源很贵。如果每收一个包就进一次中断千兆口在满速小包场景下CPU 会被中断打满系统剩下的活儿就没法干了。因此现在的 MAC 驱动都会用 NAPI。NAPI 的核心思想很简单第一次中断来了之后先把中断屏蔽掉然后让轮询函数持续从 DMA 收包收到一定量之后再重新打开中断。这样收包过程从“中断驱动”变成“中断唤醒 轮询搬砖”网络压力越大每次中断搬运的包就越多单位包的中断成本就越低。驱动里经常出现napi_schedule、napi_complete_done、budget这些字样。budget表示一次轮询最多处理多少包不是越大越好。budget设得太大一次轮询占用 CPU 太久会影响其他任务设得太小频繁中断中断风暴的危险又回来了。我一般把预算设为 64 或 128再根据实际业务实测调整。这个值没有标准答案但你可以用top看中断 CPU 占用率来辅助判断。3.4 DMA 映射与缓存一致性是嵌入式平台的隐藏杀手以太网驱动虽然大家都会写但在嵌入式平台上缓存一致性问题往往最容易翻车。很多 SoC 的 CPU 和 DMA 不是天然一致的DMA 写进内存的数据CPU 可能因为它还在缓存里而读不到新值CPU 写了数据让 DMA 去发送DMA 也可能读到过期的缓存数据。标准的做法是在初始化接收缓冲区时用dma_alloc_coherent这类接口申请一致性 DMA 内存让内存区域在 CPU 和 DMA 间保持一致如果使用动态映射比如dma_map_single和dma_unmap_single则要根据数据方向做好DMA_FROM_DEVICE和DMA_TO_DEVICE的设置并在合适的时机调用 sync 函数。这个知识点不太会在网口“能通”的阶段暴露问题但一旦系统负载上来缓存路径不一致就会出现“抓包软件里能看到对端发来的数据但应用层就是收不到”这种邪门现象。排查这种问题最有效的办法不是反复看代码而是先用dma_alloc_coherent替换驱动里的动态 DMA 缓冲分配如果问题消失那基本可以断定是缓存一致性问题。4. 链路能通但吞吐量不对完整的排查链路比打补丁更重要4.1 别急着调软件先把 PHY 的状态“问”出来很多人遇到网络问题第一反应是在驱动里打断点、加打印。但以太网链路是一整条物理链路你加再多的日志也看不到电压和信号质量。我的习惯是先用最简单的工具把 PHY 状态读出来。ethtool eth0是最直观的工具它会显示当前的速度、双工模式、自动协商状态。如果显示链路是通的但速度为 10M 或半双工说明协商结果不对如果链路灯亮但ethtool显示无连接那多半是 MDIO 没访问到 PHY或者 PHY 没有正常工作。下一步是用ethtool -S eth0看驱动里的统计计数。不同驱动导出的字段不一样但核心思路相似链路失败、CRC 错误、RX 溢出、发送超时这几个计数器一旦大幅度增长就已经把问题指向对应环节了。比如 RX 溢出说明 MAC 侧接收队列来不及搬走CRC 错误说明物理信号质量不好或者网线/连接器有问题。4.2 内部回环和外部回环把问题切成两段排查链路问题有一个非常实用的手段回环测试。它可以把“MAC 到 PHY 这一侧”和“PHY 到网线这一侧”切开来看。内部回环Internal Loopback通过 PHY 寄存器打开回环模式让 PHY 发送的数据直接从接收路径回来。这时候不需要网线你就可以确认 MAC、DMA、驱动的中断接收整条路径是否正常。外部回环External Loopback把网口和测试仪或另一台设备对接用短网线做物理回环确认 PHY 的驱动能力、网线、连接器、变压器这一路是否正常。如果内部回环正常、外部回环失败问题大概率集中在 PHY 到网口连接器这一段的硬件如果内部回环也失败说明问题在网络控制器、DMA 或者驱动配置跟网线没关系。这套做法能省下大量瞎猜的时间。4.3 自动协商和主从时钟链路不稳的重灾区跟普通工程师聊天时我经常说“以太网自动协商不是万能的”。自动协商结果受 PHY 的能力寄存器、双方速度、双工模式甚至面板上手动固定配置影响。如果在设备树或者驱动里把速度/双工写死而实际对端是自动协商就可能出现“线路是通的但两边实际工作模式不一致”的问题。车载以太网的 PHY 更特殊。100BASE-T1 用的是单对双绞线它要在一个线对上同时收发信号所以 PHY 之间必须建立主从关系通过特定寄存器配置主时钟和从时钟。如果主从配置乱了链路可能能建立但噪声和时延很大跑着跑着就掉线。遇到链路反复断开我建议按这个顺序排查查 PHY 是否芯片复位后进入了正确状态查自动协商是否完成且速度/双工符合预期查主从或时钟配置查板级电源纹波和网线物理质量。物理层的问题软件能做的往往只是“更好地观察”和“调整参数”不能指望靠驱动补丁去修信号质量。4.4 吞吐量低的定位方法如果链路是通的但是吞吐量上不去工具链里最好用的是iperf3和抓包工具。先用iperf3 -c 对端地址 -t 60跑带宽同时top观察 CPU 占用。常见情况有三类CPU 被中断打满降低吞吐的瓶颈在中断处理。检查 NAPI 是否生效、预算是否太小、网卡是否支持中断合并。描述符不够用RX ring 太小突发流量时溢出统计里的 dropped 或 ring_full 会增加。可以试着加大环形队列长度。协议栈或 DMA 映射开销过大比如每个小包都动态映射和撤销导致 cache miss 严重。优化思路是尽量复用缓冲区减少映射次数。这里有一个容易忽略的细节不要只测 TCP 大包吞吐。TCP 流会把 CPU 开销分摊到协议栈处理小包场景才是驱动瓶颈的放大镜。用iperf3 -u -b 1G发一组 UDP 小包试试如果这时候 CPU 爆掉你基本能判断问题在驱动收包路径上而不是磁盘或应用层。5. 性能优化与车载以太网带来的新问题5.1 驱动层可以做哪些低成本优化在驱动已经稳定能跑的基础上性能优化有不少“性价比很高”的改动。首先是中断合并。许多 MAC 控制器支持设置一个时间窗口或者包数量阈值到达后才触发一次中断。比如原来每收一个包进一次中断改成每收 8 个包或每 20 微秒进一次。这个改动能明显降低高小包吞吐场景下的 CPU 占用但代价是单包时延稍微增加。对实时性要求高的业务要权衡后再开。其次是 NAPI 预算和环形队列大小。环形队列不是越大越好因为每个描述符都要预分配 DMA 缓冲区太大会吃掉内存带宽和缓存命中率。我把默认的 256 描述符调到 512 或 1024 时常见收益是突发流量下丢包率下降但再往上调收益就很平缓了。你需要用ethtool -g eth0看当前 ring 参数用-G临时改实测后再固化。还有一个经常被忽略的skb_recycle或者缓冲区复用。如果驱动能直接复用已经发完的sk_buff数据缓冲区而不是每次分配、释放CPU 开销会明显下降。很多消费级芯片的驱动都做了类似优化你可以把研究重点放在 DMA 缓冲区的生命周期管理上。5.2 车载以太网对驱动工程师提出了更多要求车载以太网的火热程度光看行业风向就知道了。传统以太网至少是两对差分线而 100BASE-T1 只用一对非屏蔽双绞线还要求全双工同时在一个线对上收发信号。PHY 内部要做回声消除、信号编解码复杂度远高于普通百兆 PHY。对驱动而言中间要改变的不仅是物理接口还有链路管理方式。拿唤醒机制来说传统网口一般通过外部 PHY 的 WoL 功能判断网络唤醒而车载以太网往往要考虑整个 ECU 的电源管理状态网口驱动要能配合休眠唤醒流程把 PHY 和 MAC 的状态正确迁移。时间同步也是一个大头。很多车载业务依赖 802.1AS 或 PTP 来做全网时间同步驱动需要支持硬件时间戳。如果 MAC 控制器只支持软件时间戳时间精度会受到不小影响这就成为选型层面的硬约束。做车载以太网驱动时我的经验是尽早确认 MAC 侧是否支持硬件时间戳以及 PHY 是否支持对应的辅助时间戳寄存器。5.3 高速 SGMII 和 2.5G 时代调试手段也得升级四五百兆的时代一根示波器探头可以应付很多问题。到了 1G/2.5G 时代SGMII 是高速串行差分信号普通探头已经很难看出正确与否更多要依赖 PHY 自带的内部寄存器状态、PCS 锁定状态以及链路训练结果来判断。如果你拿到一块写着 1G/2.5G Ethernet PCS/PMA or SGMII 的板子先用ethtool看速度再读 PHY 的状态寄存器看信号是否正常锁定。对 PCS/PMA 器件寄存器里通常有信号检测、锁定状态和错误计数这些比特比肉眼观察可靠得多。在驱动配置层面SGMII 往往要额外处理速率匹配问题。比如 PHY 支持 2.5G但 MAC 侧只工作在 1G两边接口速率不匹配就会造成链路训练异常。对于这种情况最好在硬件设计阶段就保持 MAC 和 PHY 的能力对齐软件层面再通过强制速率或自适应模式解决。6. 凭经验记下来的三条调试铁律6.1 先看物理层再看数据通路我见过太多人一上来就改驱动代码结果改了半天发现是网线松了或变压器虚焊。现在我的调试顺序非常固定先确认ethtool能看到 PHY再确认链路协商结果然后做一次内部回环测试。物理层没确认之前不碰任何软件逻辑。6.2 抓包之前先看统计计数抓包工具能看到的内容是已经到达 MAC 的数据。如果数据在更早的环节就丢了比如 PHY 没锁定、DMA ring 溢出、NAPI 预算耗尽抓包是抓不出真相的。正确做法是先看ethtool -S和netstat -i让计数器告诉你丢包发生在哪一段再决定要不要抓包。6.3 任何吞吐量测试都要用多包长、多线程验证只跑一个 TCP 大流看不出驱动收包路径的上限。我最常用的组合是TCP 大包测带宽上限UDP 小包打满测中断和 DMA 压力再用长 ping 或 PTP 报文测时延抖动。三个方向都过了这条网口才算可以放心交出去。以太网驱动是一个越深入越有意思的方向因为它的每层都有可玩的空间硬件上有 PHY、变压器、接口时序软件上有 DMA、NAPI、时间戳。这一期先把主线经验梳理清楚后面有机会再针对车载以太网和协议栈调优单独展开。