☰
嵌入式以太网驱动开发:从MDIO到DMA的实战排障指南
2026/10/27 1:01:40 网站建设 项目流程

做嵌入式驱动的时间长了,会慢慢形成一种直觉:拿到新板卡,先看的不是代码,而是datasheet和原理图。尤其是以太网这块,链路涉及CPU内部MAC、外部PHY芯片、网络变压器、RJ45座子,中间任何一环掉链子,表现都是"网口不通",但原因千奇百怪。这一期是第10期,集中聊聊Ethernet驱动开发里我亲手踩过、也帮别人排过的那些典型问题——从MDIO管理接口到DMA环形队列,从RGMII/SGMII的时钟配置到收包中断风暴,尽量把每个环节"为什么这么做"讲清楚,也把出了问题第一步该查哪里说透。适合刚接手嵌入式Linux网络驱动的同学,也适合那些已经把网口调通、但遇到性能或偶发丢帧问题的同行。

1. 拿到网卡先别急着写驱动——先回答三个问题

以太网驱动说复杂也复杂,说简单也简单。它不像USB那样要应对一堆枚举逻辑,也不像显示驱动要面对各种时序怪癖,但它的链路很长:应用层→协议栈→网络设备层→MAC→PHY→网线→对端。我接手过不少项目,很多人一上来就抱着内核源码啃,结果被macb、stmmac、fe这些驱动框架绕晕。其实写驱动之前,先把三个问题搞清楚,后面能省一半调试时间。

第一个问题:MAC是谁家的。多数嵌入式SoC(瑞芯微、全志、NXP i.MX系列、ST的MP1)都在片内集成了MAC控制器,甚至集成多个,你要做的是给这个MAC写一个platform_driver,或者把设备树节点配好,让内核里现成的MAC驱动绑定上去。这里的关键是搞清楚MAC的寄存器基址、中断号、时钟源、复位信号挂在哪个GPIO。如果是PCIe接口外接的独立网卡,那基本走现成的框架,需要改的就少了。

第二个问题:PHY是谁家的,地址是多少。PHY是数据链路层和物理线缆之间的桥梁,一般挂在MDIO总线上,地址由硬件引脚(strapping)决定。常见芯片里RTL8211F默认地址是0x05,KSZ9031是0x04,BCM5461则要看引脚配置。写驱动前把原理图上PHY的地址确认好,这是最简单的动作,但错过会很痛——因为你读出来的PHY ID可能全是0xFFFF,人直接愣住。这颗PHY还决定了你用generic PHY驱动还是厂商专用驱动,像RTL8211F用内核自带realtekPHY驱动就行,但有些芯片(特别是车载交换芯片的PHY)得自己写mdio_device驱动。

第三个问题:MAC与PHY之间用什么接口连。数据接口决定时钟拓扑。MII、RMII、GMII、RGMII、SGMII说到底是"CPU和PHY怎么把帧数据递过去"的不同物理方式,每种都有完全不同的引脚占用和时钟要求。还有一点容易忽略:MAC和内存之间是DMA还是FIFO搬运?这个决定你后面是写描述符管理,还是简单查状态寄存器轮询。我见过有人把RGMII的板子当RMII配置去调,结果PHY link都起不来,折腾了两天才发现设备树里phy-mode写错了。

提示:多数嵌入式项目里,以太网驱动快速跑通的最短路径是——内核开好对应MAC驱动,PHY驱动选对或者直接用generic-phy,设备树里配好phy-mode、phy-handle、复位GPIO和时钟。但那是"用起来",不是"写好驱动"。要能处理生产环境的问题,上面三个问题还是得逐个吃透。

2. MDIO引脚与寄存器:PHY的"控制口"到底怎么操作

MAC与PHY交互除了数据接口,还有一套低速管理接口——MDIO。它只有两根线:MDC(时钟)和MDIO(数据),很多工程师容易忽略这组引脚,但恰恰很多"网口起不来"的故障都是MDIO不通导致的。MDIO的通信逻辑其实非常简单,但我建议初学者还是把它当成一个独立协议来理解,而不是只调用phy_read/write函数。

2.1 帧格式与读写时序

MDIO帧由MAC侧发起,格式是:32位前导码全1,2位起始码01,2位操作码(读10、写01),5位PHY地址,5位寄存器地址,2位turnaround,16位数据。读操作时,PHY在turnaround之后占用总线返回数据;写操作则直接由MAC把16位数据写出去。搞清楚这个结构有个实际好处:拿逻辑分析仪抓MDIO波形时,你能自己解开每一位,而不是对着软件输出瞎猜。

Linux内核里,mdiobus_register之后,上层驱动通过bus->read(bus, phy_addr, regnum)来访问。很多SoC的mdio控制器同时支持clause 22和clause 45(用于10G PHY扩展寄存器),嵌入式千兆网卡普遍还是clause 22为主,但也见过用C45访问扩展寄存器的PHY。看PHY datasheet时先确认它遵循哪个协议,否则读写时序对不上。

2.2 IEEE标准寄存器踩坑实录

标准PHY寄存器是IEEE 802.3定义的,最常用的是这几个:

  • reg0(BMCR):软复位(bit15)、自动协商使能(bit12)、速率选择(bit13/bit6)、loopback(bit14)。写0x8000软复位后要等一段时间再操作。
  • reg1(BMSR):link状态(bit2)、自动协商完成(bit5)、支持的能力位。这个寄存器的link状态是"瞬态"的,不是实时电平,很多芯片在查询时才会更新。
  • reg2/reg3(PHY IDR):厂商ID和型号,调试时第一件事就是读这两个寄存器确认芯片身份。
  • reg4(ANAR)、reg5(ANLPAR):本端和对端的协商能力。
  • reg31(PHYCTRL):不少PHY厂商用它做扩展控制——比如寄存器页切换。

我踩过一次很典型的坑:板子回来,RJ45指示灯都亮着,ip link也显示UP,但就是分配不到IP。拿mdio工具读reg0发现是0x1000——速率强制千兆,没有自动协商位。再看原理图,原来PHY的strapping引脚被一颗电容影响了上电电平,PHY进入了"强制千兆全双工"模式,而对端交换机是自适应,两边能Link但实际速率和双工不匹配,数据全是错乱的。

排查过程其实就三步:一,确认MDIO能读写,读reg2/reg3拿到PHY ID,如果读到0xFFFF大概率是MDIO地址错、PHY没上电或者复位没放开;二,确认协商状态,读reg1看bit5是不是1,同时确认KM(链路能力)位;三,确认link状态,用ethtool eth0看Speed和Duplex是否和对端一致。物理层这一关没过,后面写再多协议栈代码都是白搭。

2.3 Linux里调试PHY的实用工具

开发早期我强烈建议在系统里带上mdio-tools(mii-tool的继任者),可以直接对PHY寄存器做读写。内核起来后用ethtool eth0能看协商结果,用ethtool -S eth0能看MAC驱动层计数器。如果PHY地址记错了,还可以在设备树里指到正确地址,重新适配。另外一个技巧:给PHY驱动加一个debugfs节点,把关键寄存器暴露出来,现场调试就不用总挂mdio工具了。

3. DMA描述符设计:高速收发绕不开的环形队列

协议栈和PHY都通了,接下来就是收发包的数据面。现在主流嵌入式SoC的MAC几乎都是DMA方式搬运数据,驱动工作的核心就是管理DMA描述符环形队列。很多人写驱动绕了半天,最后问题都出在"谁拥有这块buffer"没理清楚。

3.1 所有权位与环形缓冲区的运行原理

DMA描述符通常是环形队列,每个描述符指向一块数据buffer。驱动把待发送的数据地址、长度填进描述符,硬件发现描述符的OWN位为1就会执行搬运。搬运完成后硬件清OWN位,驱动轮询到OWN=0就能处理数据。接收方向反过来:驱动预分配一块buffer,把地址写进描述符并置OWN=1,硬件收到帧后DMA写入buffer,再清OWN位,驱动把数据上交协议栈后重新挂buffer。

这个"所有权"概念是整个DMA设计的地基。如果驱动在硬件还在使用时把buffer提走,轻则数据错乱,重则DMA写坏已释放的内存。我见过一个顽疾:某型号网卡在发送频繁时偶发系统崩溃,排查很久发现是tx_complete处理时没有正确判断skb的ownership,导致释放了还在DMA读的skb——高速缓存不一致带来了内存损坏。

3.2 一致映射还是流式映射?

DMA buffer管理有两条路,选择时要想清楚场景:

  • 一致DMA映射:用dma_alloc_coherent分配,内存物理连续且未经过cache,CPU和DMA看到同一份数据。适合收发环形队列这种长期复用的buffer。缺点是分配慢、容量受限。
  • 流式DMA映射:用dma_map_single/dma_unmap_single,按收发操作动态映射和同步cache。适合skb这种短暂存活的buffer。问题是cache同步做不好就会踩坑。

我早期用非coherent配置开发时遇到过一个诡异现象:第一次发包成功,第二次发的是"脏数据",抓包发现MAC发出去的帧内容被截断或反复发旧数据。最后发现是没有在发送前调用dma_sync_single_for_device把cache里的数据flush到内存。后来习惯是:在ndo_start_xmit里做完dma_map_single之后立即做一次dma_sync_single_for_device,确保DMA看到的永远是新鲜数据。一致DMA映射能省心,但tx方向一次性大包吞吐高时,分配也慢;生产环境多数是混合用。

3.3 描述符数量与NAPI weight怎么定

描述符环形队列的大小直接决定高负载下的丢包表现。太小了高吞吐丢帧,太大了低负载时收包中断扫描一堆空描述符浪费CPU。我的一般起步值是256,收发都先开256,跑负载测试看rx_dropped和rx_missed计数再调整。千兆满载、CPU占比从35%调到20%时,我把收包ring开到了512,NAPI weight从64调到128,中断次数几乎减半但收包吞吐不掉。

需要提醒的是,描述符数量加大不代表性能一定变好。ring太深时,中断合并的效果反而差,因为在同一个softirq周期内扫描了过多的空描述符。嵌入式产品建议做一个监控:描述符使用率超过80%持续几秒就告警,别等丢帧了才去查水线。VA(接收描述符深度)和DA(发送描述符深度)在设备树里通常有配置选项,改之前看驱动是否支持动态调整。

4. 接口模式与时钟拓扑:RGMII/SGMII链路调试实战

这是让很多人崩溃的环节。协议栈配好、MAC驱动也配好,可网口就是"link up但跑不通"。数据接口的物理模式和时钟配置没对齐是高频原因。

4.1 单口/多口接口模式速览

  • MII:数据位宽4bit或8bit,发送和接收各有独立的时钟,信号多,基本只在老设计里见。
  • RMII:50MHz单时钟,4bit数据宽度,收发共享,引脚大幅减少,常用在低成本方案。
  • GMII:8bit数据+125MHz时钟,信号数量多,适合片内短距离,板上不常见。
  • RGMII:用DDR双沿采样在GMII基础上实现收发各4bit,需要125MHz时钟,时序要求高,是板上最常见的千兆接口。
  • SGMII:串行,1.25Gbps差分层,通过SerDes的PCS/PMA发送,引脚很少,适合板级和背板连接。

4.2 时钟拓扑和RGMII延时

每种模式的时钟来源不一样,设计时必须搞清楚:RMII的50MHz时钟是MAC提供还是PHY提供?RGMII的125MHz参考时钟从哪来?SGMII的参考时钟是内外部还是晶振直接给?少了任何一个时钟,Link都不可能起来。

RGMII有一个著名的"坑":发送方向时钟需要约2ns的delay才能满足采样窗口。有的PHY通过strapping引脚配置这个delay,有的必须在寄存器里打开TX delay和RX delay。如果delay没配对,典型现象是协商正常、link正常,但抓包全是FCS错误的长帧,或者干脆是"能收不能发"。我调试一块客制化板卡时遇到的现象就是这样:MAC和PHY之间时序看起来都满足,但ethtool eth0显示有大量rx_crc_errors,后来在PHY寄存器里把RX内部delay打开,故障立刻消失。这类问题建议一开始就在驱动初始化时主动配置RGMII delay,不要默认靠strap。

4.3 SGMII与PCS/PMA的锁定问题

SGMII的坑更多在SerDes配置:lane速率、CDR(时钟恢复)锁定、终端电阻、ref clk是否匹配。我处理过CPU与交换芯片之间SGMII不通的案例:两边寄存器看起来都对,Tx方向也有数据,但Rx方向完全没有。查到最后是交换芯片的SGMII参考时钟没有来源——原理图上那颗共用的ref clk振荡器没贴。这就是原理图review不到位埋的雷。

另外要留意SGMII有两种工作模式:自协商模式和SGMII fixed模式。前者的协商信息通过SGMII帧中的配置字传递,后者则直接固定全双工千兆。很多交换芯片的SGMII口默认是fixed模式,而MAC侧想自协商,两边协商状态永远转不完。这类问题看ethtool eth0的Speed显示,如果你看到的是"unknown"或者协商结果反复跳变,优先检查SGMII模式而非PHY。

还有一个容易被忽略的点:PCS/PMA层初始化时,有时需要先对SerDes PLL做reset,再配置lane速率,次序错了会导致CDR锁定失败。很多SoC的serdes驱动要求phy驱动配合完成,单纯写MAC驱动的人第一次遇到会很懵。建议看SoC的serdes节点怎么配置,时钟树的parent是谁,用clk_summary逐一确认。

5. 收包中断风暴与NAPI:让CPU喘口气

千兆网口跑起来很容易遇到这个问题:ping通,iperf也能跑,但一旦同时做大量小包收发或者大面积组播,CPU占用飙升到90%以上,系统交互都卡。这就是收包中断风暴——每个包都触发一个硬件中断,硬件中断处理器把CPU占满了。

NAPI的核心思路是:第一个包到了之后,驱动关硬件中断,把网卡加到softirq的poll队列里,由软中断主动轮询描述符批量收包;直到没有包可收,再重新打开硬件中断。这个机制能大幅减少中断数量,是内核网络高性能的基础。

驱动里要做的核心事情是:

  • 注册NAPI:netif_napi_add(netdev, &priv->napi, weight);
  • 在收包中断里判断是否要切NAPI模式,调用napi_schedule(&priv->napi);
  • 实现poll回调:循环处理收包,直到配额用完;如果配额没用完说明没包了,调用napi_complete_done关闭poll模式并重新开中断。

weight就是每次poll配额,代表一次能处理的包数量。它没有黄金值:设太小(比如32)高吞吐时CPU仍频繁调度;设太大(256以上)burst流量下延迟增加。我实测过64字节小包极限收包,weight=128时能到1.4Mpps且CPU占用平滑;weight=64时CPU多出十来个点但延迟更低。路由器和NVR这类场景我会选weight=128,低延迟场景选64。

ROPS(Receive Packet Steering)可以把软中断分担到多个CPU核,但它不是免费午餐——引入cache miss和调度延迟,在高吞吐、单核的嵌入式场景里反而帮倒忙。我的建议是:多核SoC、吞吐要求高时开RPS做流表hash分发;如果是单核或低吞吐场景,关掉反而稳定。

还有个常被漏掉的优化点:硬件中断合并。很多MAC支持中断抑制/合并机制,比如在N个包到达或者经过N微秒后才触发中断。用ethtool -C eth0 rx-usecs 100这类命令可以调合并参数,能显著减小小包场景的中断次数。硬核合并适合吞吐优先,关掉适合延迟敏感。我在测量延迟需求很严的工业控制场景时,会把rx-usecs调到0并保证NAPI路径足够快;普通场景保持默认即可。

6. 排查链路三板斧:寄存器、计数器、抓包

最后这部分是方法论总结。我调试以太网问题基本按下面这个顺序递推,绝大多数问题都能在半小时内锁定到某一层。

6.1 由现象到层级的快速分诊

某次板卡连交换机,现象是link正常但ping不通。完整排查顺序是:

  1. 看协商结果:ethtool eth0,确认Speed和Duplex,比如1000M/full。两边不一致就查RGMII的delay、时钟拓扑。对端端口down也会让你单看本端什么都是好的。
  2. 看MAC统计:ethtool -S eth0 | grep -E 'rx_|tx_',快速确认有没有rx_crc_errors、alloc_rx_buff_failed、tx_dropped。CRC错误狂飙,多半是时序和时钟;alloc_rx_buff_failed说明内存分配失败,NAPI路径里的skb分配有瓶颈;tx_dropped说明发送环形队列满了或tx超时。
  3. 看PHY状态:用mdio工具读reg1、reg4、reg5,确认link状态、协商结果、对端能力。如果PHY问题确认但寄存器正常,就要怀疑PHY软件复位和硬件复位时序——比如RTL8211F有时必须等上电后100ms再做软复位。
  4. 抓包:在目标板tcpdump -i eth0 -n,在对端也抓。比较两边能否互相看到ARP请求。如果本端发出ARP,对端看不到,问题在物理通道;如果对端收到但回包丢了,问题在MAC或PHY的收方向。
  5. loopback验证:把PHY的reg0设成loopback,测试MAC能否自发自收;再把MAC loopback打开,测试DMA和中断路径。这一步能把"协议栈/MAC/DMA"和"PHY/变压器/线缆"彻底分开。

6.2 常见现象速查表

现象大概率原因优先排查动作
Link up但ping不通速率双工不匹配、RGMII delay错、VLAN配置、MTU不符ethtool对比两端,查PHY协商寄存器
能发不能收收中断没配、RX DMA描述符耗尽、PHY收方向异常、对端端口配置ethtool -S看rx_dropped,查中断映射
能收不能发TX DMA未通、PHY发方向、sedes参考时钟、对端屏蔽ethtool -S看tx_dropped,查TX ring用尽
CRC错误帧大量时钟抖动、RGMII延时、PHY输出延迟、电源噪声抓包看FCS错误,逐步开启/关闭RGMII delay
性能严重偏低NAPI weight过小、中断合并未开、DMA地址未对齐、CPU亲和性差调weight,开rx-usecs,检查cache一致性
偶发丢包重启后恢复DMA描述符内存泄漏、中断丢失、PHY复位不彻底长时间跑小包压力测试,观察tx/rx计数异常

6.3 一个真实链路排查复盘

某主控板配KSZ9031 PHY,客户反馈批量设备里约2%的板子网络性能极差,如上100Mbps拥堵会丢包到断流。起初怀疑PHY或变压器批次差异,后来抓包发现这类板子的一个共同点是tx_dropped增长很快。进一步看,TX描述符在发送很多小包时被耗尽,而驱动里ndo_start_xmit返回NETDEV_TX_BUSY时没有做有效的流控,协议栈又不断重试,形成恶性循环。最终修复分两层:一是增大TX ring size,二是检查tx_clean逻辑里对描述符回收的效率——原驱动的tx_complete做了过多中间态判断,繁忙时回收不及时。把这个逻辑改成批量回收后,出问题的板子恢复了正常。

这类问题给到我的启发是:驱动代码的性能瓶颈,往往不是注册表和中断,而是描述符回收路径的阻塞环节。排查时不要只盯着带宽上限,先看统计里的dropped和busy计数。

结尾:积累"下一块板子"的经验

做驱动的这些年,一个很深的体会是:每一次踩坑,其实都是在给"下一块板子"存经验。以太网这个领域有个好处——它有标准的PHY寄存器、有wireshark这种强力的解析工具、有清晰的协议分层,只要不情绪化地瞎试,大多数问题都能靠逻辑递推锁定到某一层。我现在拿到一块新板子,第一件事永远是翻原理图,确认PHY地址、复位脚、接口模式、时钟拓扑,把这一步做扎实了,"网口不通"至少能省下两个小时的瞎猜时间。这一期从MDIO讲到DMA,又绕了RGMII、SGMII和中antawORK优化,算是我自己对Ethernet链路的一个整理。如果能在你下一次debug时帮上一点忙,那就值了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询