ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

嵌入式Linux以太网驱动开发实战:从MAC/PHY到DMA与NAPI调优

嵌入式Linux以太网驱动开发实战:从MAC/PHY到DMA与NAPI调优 1. 嵌入式以太网驱动开发到底在搞什么做嵌入式Linux驱动开发这些年以太网这块是我踩坑最多、也最有意思的方向之一。很多人刚接触嵌入式网络的时候觉得以太网不就是插上网线能通就行了吗实际上从MAC控制器到PHY芯片从DMA描述符到协议栈对接中间每一层都有大量需要自己动手配置和调试的东西。尤其是当你拿到一块新板子SoC可能是Zynq、可能是i.MX系列、也可能是STM32MP1外挂的PHY从常见的KSZ9031、RTL8211到车载级的Marvell 88Q2112每一颗的初始化时序、寄存器配置、时钟方案都可能不一样。这篇文章主要面向已经有一定嵌入式Linux基础、正在做或准备做以太网驱动移植和调试的工程师。我会从整体架构讲到具体的驱动实现细节包括设备树配置、PHY初始化、DMA描述符环的建立、收发流程、中断处理、常见问题排查等。不管你是用Zynq做UDP测试还是在车载以太网上跑TSN底层的驱动逻辑是相通的。我会尽量把每个关键环节的原理和实操都讲透让你看完能直接上手改自己的驱动。先说说嵌入式以太网的整体架构。一个典型的嵌入式以太网系统硬件上通常包含这几个部分SoC内部的MAC控制器比如Cadence GEM、Synopsys DesignWare GMAC、TI CPSW等、外部PHY芯片、变压器和RJ45接口或者车载以太网的专用连接器。MAC和PHY之间通过MII/RMII/GMII/RGMII/SGMII等接口连接软件上Linux内核已经提供了成熟的网络子系统框架包括net_device结构、NAPI机制、PHY抽象层phylib等。驱动开发者的工作说白了就是把这些硬件资源用软件串起来让内核的网络协议栈能通过你的驱动收发数据包。听起来简单但实际做起来从设备树写对第一个寄存器地址开始到最终能稳定跑满千兆吞吐量中间可能要经历无数次抓包、看寄存器、调时序的过程。2. 核心架构拆解与方案选型思路2.1 MAC与PHY的分工与接口选择理解MAC和PHY的分工是做好以太网驱动的前提。MAC层负责的是数据链路层的功能包括帧的封装和解封装、CRC校验、地址过滤、流控等。PHY层负责的是物理层处理实际的电信号编解码、链路协商、时钟恢复等。两者之间通过标准接口通信常见的有MII、RMII、GMII、RGMII、SGMII这几种。选哪种接口主要看你的速率需求和引脚资源。MII需要16根线支持10/100MbpsRMII只需要7根线但需要50MHz的参考时钟GMII是MII的千兆版本需要24根线RGMII把GMII的24根线压缩到12根通过双沿采样实现千兆速率是目前嵌入式系统里最常用的千兆接口SGMII则是串行接口只需要几对差分线常用于通过SerDes连接光模块或背板。我在Zynq平台上用得最多的是RGMII因为它引脚数量适中千兆速率也够用。但RGMII有个坑要注意TX和RX路径上通常需要延迟。有些PHY芯片内部可以配置延迟有些则需要PCB走线来保证时序。如果你发现ping通但丢包严重或者干脆链路都起不来大概率就是RGMII延迟没配对。2.2 设备树配置的关键节点在Linux内核里以太网控制器的描述完全依赖设备树。以Zynq的GEM控制器为例一个典型的设备树节点包含以下关键信息gem0: ethernete000b000 { compatible cdns,zynq-gem, cdns,gem; reg 0xe000b000 0x1000; status okay; clocks clkc 30, clkc 30, clkc 13; clock-names pclk, hclk, tx_clk; #address-cells 1; #size-cells 0; phy-mode rgmii-id; phy-handle phy0; phy0: ethernet-phy0 { reg 0; device_type ethernet-phy; }; };这里面有几个关键点需要解释。compatible属性决定了内核用哪个驱动来匹配这个设备reg是控制器的寄存器基地址和范围。clocks和clock-names描述了控制器需要的时钟源通常包括APB时钟、AHB时钟和发送时钟。phy-mode告诉驱动MAC和PHY之间的接口类型rgmii-id表示RGMII接口且PHY内部自带延迟。phy-handle指向PHY子节点PHY子节点的reg属性对应PHY在MDIO总线上的地址。这个地址通常由PHY芯片的硬件引脚决定范围是0到31。如果你不确定PHY地址是多少可以用MDIO扫描的方式枚举出来后面我会讲具体怎么做。2.3 DMA描述符环的设计考量以太网控制器收发数据靠的是DMA而DMA的核心是描述符环。每个描述符指向一块缓冲区包含数据长度、状态标志、校验信息等。驱动初始化的时候要分配一块一致性DMA内存来存放描述符环然后把环的基地址写到控制器的寄存器里。描述符环的大小直接影响性能和内存占用。太小了容易在高速率下出现描述符耗尽导致丢包太大了浪费内存而且增加缓存失效的开销。我一般设置TX和RX各256个描述符这个值在千兆速率下基本够用。如果你跑UDP高速测试发现丢包可以先把RX描述符数量翻倍试试。还有一个关键点是缓冲区大小的选择。以太网帧最大1518字节不含VLAN标签但为了对齐和性能考虑通常分配2048字节的缓冲区。这样每个缓冲区正好占半个页内存管理比较高效。如果你要支持Jumbo Frame缓冲区就要相应加大到4KB或更大。3. 驱动初始化与PHY配置实操3.1 控制器初始化的完整流程以太网控制器的初始化流程大致分为以下几个步骤我以Zynq GEM为例详细说明。第一步是时钟和复位。在probe函数里先通过clk_get获取时钟句柄然后clk_prepare_enable使能时钟。复位操作通常通过控制器的网络控制寄存器NCR的复位位来完成写1后等待硬件自动清零。第二步是配置MAC地址。从设备树或EEPROM里读取MAC地址写入控制器的特定寄存器。如果产品没有预烧MAC地址可以用随机生成的本地管理地址但要确保同一网络内不冲突。第三步是初始化DMA描述符环。用dma_alloc_coherent分配描述符内存然后逐个初始化每个描述符的地址字段和状态字段。RX描述符的状态字段要设置为“owned by hardware”这样控制器才会往这个描述符里写数据。第四步是配置网络参数。包括设置最大帧长、配置双工模式和速率通常由PHY协商结果决定、使能CRC校验、配置中断掩码等。第五步是注册网络设备。调用register_netdev把net_device注册到内核网络子系统这样ifconfig就能看到这个接口了。第六步是启动PHY。通过phylib框架连接PHY设备启动链路协商注册PHY状态变化的中断处理函数。整个流程里最容易出问题的是第三步和第六步。描述符环如果初始化不对表现为接口能起来但收发不了数据。PHY如果连不上表现为接口起不来或者链路状态始终是down。3.2 PHY芯片的识别与寄存器配置PHY芯片的识别通过MDIO总线读取两个标准寄存器寄存器2PHY ID1和寄存器3PHY ID2。这两个寄存器组合起来形成一个32位的ID内核的phylib会根据这个ID去匹配对应的PHY驱动。如果你在调试时发现PHY ID读出来是0x0000或者0xffff说明MDIO通信有问题。排查方向包括MDIO时钟频率是否过高一般不超过2.5MHz、MDIO引脚复用是否正确、PHY的供电和复位是否正常。PHY的配置分两部分标准寄存器和扩展寄存器。标准寄存器包括控制寄存器寄存器0、状态寄存器寄存器1、自动协商通告寄存器寄存器4等。扩展寄存器则是各厂商自定义的用来配置RGMII延迟、LED行为、功耗模式等。以Realtek RTL8211为例它的扩展寄存器0x1c用来配置RGMII延迟。写入不同的值可以分别控制TX和RX路径的延迟。如果你用的是rgmii-id模式但PHY没有正确配置延迟链路可能能起来但数据错误率很高。/* 配置RTL8211的RGMII延迟 */ static int rtl8211_config_rgmii_delay(struct phy_device *phydev) { int ret; /* 使能TX和RX内部延迟 */ ret phy_write(phydev, 0x1c, 0x0800); if (ret 0) return ret; return 0; }这段代码看起来简单但实际调试的时候你可能需要反复尝试不同的延迟组合。我的经验是先用示波器看一下RGMII的时钟和数据眼图确认时序余量然后再决定是否需要内部延迟。3.3 中断处理与NAPI机制以太网控制器的中断处理直接影响系统在高负载下的性能。传统的中断方式是每收到一个包就触发一次中断在千兆速率下这会导致CPU频繁进出中断上下文开销很大。NAPINew API机制通过“中断轮询”的混合方式解决了这个问题第一个包到达时触发中断中断处理函数关闭接收中断并调度NAPI轮询轮询函数一次处理多个包直到没有新包或者达到预算上限然后重新打开中断。实现NAPI的关键步骤包括在probe时调用netif_napi_add注册轮询函数在中断处理函数里调用napi_schedule触发轮询在轮询函数里循环处理RX描述符并调用napi_complete_done结束轮询。轮询预算budget的设置需要权衡延迟和吞吐量。预算太小会导致频繁重新触发中断预算太大则增加单次轮询的处理延迟。内核默认的预算是64对于千兆以太网来说我一般设置成128或256这样在满负载时能减少中断次数。中断处理里还有一个容易忽略的细节中断状态的清除。有些控制器要求先读取中断状态寄存器再写回清除有些则直接写1清除。如果清除顺序不对可能出现中断丢失或者中断风暴。我在调试Zynq GEM的时候就遇到过中断风暴的问题最后发现是中断状态清除不完整导致的。4. 数据收发流程与性能调优4.1 发送路径的完整实现发送数据的过程从协议栈调用驱动的ndo_start_xmit回调开始。这个回调的职责是把上层传下来的skb数据拷贝到DMA缓冲区然后更新TX描述符的状态最后通知控制器开始发送。具体步骤是这样的首先检查TX描述符环里是否有空闲描述符如果没有就返回NETDEV_TX_BUSY让协议栈稍后重试。然后取出一个空闲描述符把skb的数据拷贝到描述符指向的缓冲区。如果skb的数据长度超过一个缓冲区的大小需要做线性化或者使用多个描述符。拷贝完成后设置描述符的长度字段和状态字段标记为“owned by hardware”最后写控制器的发送触发寄存器。发送完成后的清理工作通常放在发送完成中断里处理。中断处理函数遍历已经发送完成的描述符释放对应的skb更新描述符状态为空闲。这里要注意内存屏障的使用确保描述符状态的更新对硬件和CPU都是可见的。有一个性能优化的技巧是使用分散-聚集Scatter-GatherDMA。如果控制器支持SG模式可以直接把skb的fragment地址填到描述符里避免数据拷贝。这在处理大包或者做转发的时候能显著降低CPU占用。4.2 接收路径与数据对齐接收路径比发送路径稍微复杂一些因为涉及到数据对齐和协议栈的交互。当控制器收到一个包后DMA把数据写入RX描述符指向的缓冲区然后触发接收中断。中断处理函数调用napi_scheduleNAPI轮询函数遍历RX描述符对每个完成的描述符构造skb并递交给协议栈。构造skb的时候要注意数据对齐。以太网帧头是14字节加上VLAN标签是18字节如果直接从这个偏移构造skbIP头就不会对齐到4字节边界影响后续协议栈的处理效率。常见的做法是在缓冲区开头预留2字节这样IP头就能对齐。内核提供了netdev_alloc_skb_ip_align函数来简化这个操作。还有一个关键点是缓存一致性。DMA写入的数据可能在CPU的缓存里还是旧值所以在读取描述符状态和缓冲区数据之前需要调用dma_rmb()确保数据可见。同样在把RX描述符重新交给硬件之前需要调用dma_wmb()确保描述符状态的更新已经写入内存。4.3 千兆吞吐量上不去的排查思路很多人做完驱动后发现吞吐量只有100Mbps左右远达不到千兆。这个问题我遇到过好几次原因各不相同下面列一个排查清单。排查项检查方法常见问题链路协商速率ethtool eth0PHY只协商到100MRGMII延迟示波器看时序延迟配置错误导致重传描述符数量查看驱动参数描述符太少导致丢包中断亲和性/proc/interrupts中断集中在单个CPUCPU频率cpufreq-infoCPU降频导致处理不过来MTU设置ip link showMTU过小导致分片流控配置ethtool -a eth0流控未开启导致丢包最常见的原因是链路只协商到了100M。这时候先确认PHY的自动协商通告寄存器是否使能了千兆模式然后检查MAC侧是否配置为千兆模式。有些PHY默认只通告100M需要手动修改寄存器4的值。另一个常见原因是RGMII延迟不对。表现是链路能起来ping小包正常但大包或者高速传输时大量丢包。用ethtool -S eth0查看统计信息如果看到大量的CRC错误或者对齐错误基本可以确定是时序问题。5. 常见问题与排查技巧实录5.1 PHY识别不到怎么办PHY识别不到是最常见的问题之一。现象是驱动加载后ifconfig -a看不到网络接口或者看到接口但ethtool显示Link detected为no。排查步骤是这样的首先确认MDIO总线是否正常工作。可以在驱动里加打印读PHY ID寄存器看返回值。如果返回0xffff或0x0000说明MDIO通信失败。这时候检查MDIO的时钟频率一般不要超过2.5MHz。如果时钟没问题检查MDIO和MDC引脚的pinmux配置是否正确。如果MDIO能读到ID但phylib匹配不上驱动可能是PHY的ID不在内核支持的列表里。这时候可以手动添加PHY ID到对应的驱动里或者使用通用PHY驱动。通用PHY驱动虽然功能少一些但基本的链路协商和收发是没问题的。还有一种情况是PHY的复位时序不对。有些PHY要求复位引脚拉低至少10ms然后拉高后等待至少50ms才能访问寄存器。如果复位时间不够PHY可能处于未就绪状态。5.2 链路能起来但ping不通链路状态显示up但ping不通这个问题涉及的面比较广。我一般按照从底层到上层的顺序排查。先看MAC地址是否正确。用ip link show eth0查看MAC地址如果是全0或者全F说明驱动没有正确设置MAC地址。MAC地址不对的话交换机可能不转发你的包。再看收发统计。用ethtool -S eth0查看TX和RX的包计数。如果TX有计数但RX为0说明包发出去了但没有收到回复可能是对端没收到或者回复被丢弃了。如果TX和RX都有计数但ping不通可能是协议栈层面的问题比如IP地址配置错误或者路由表不对。还有一个容易忽略的点是VLAN配置。如果你的网络环境有VLAN而接口没有正确配置VLAN过滤包可能被硬件丢弃。可以用ethtool -k eth0查看VLAN offload的状态必要时用vconfig或ip link命令配置VLAN接口。5.3 高速传输时丢包严重高速传输丢包的原因通常集中在DMA和中断处理上。先检查描述符环的大小如果TX或RX描述符经常处于耗尽状态就需要增大描述符数量。可以通过在驱动里加统计计数来确认。中断处理也是瓶颈之一。如果所有网络中断都落在同一个CPU上在高负载时这个CPU会成为瓶颈。可以通过/proc/interrupts查看中断分布然后用echo cpu_mask /proc/irq/irq_num/smp_affinity把中断分散到多个CPU。还有一个不太容易发现的问题是内存带宽瓶颈。如果DMA缓冲区的内存位于低速的内存区域比如某些SoC的DDR控制器有多个通道不同地址范围的带宽不同可能导致DMA传输效率低下。这种情况下可以尝试调整DMA内存的分配策略或者使用控制器支持的内存端口优化。5.4 常见问题速查表现象可能原因解决方法接口不出现驱动未匹配检查compatible属性PHY ID读取失败MDIO通信异常检查时钟和引脚复用链路始终downPHY未复位或时钟缺失检查复位时序和时钟ping不通MAC地址或VLAN配置错误检查MAC地址和VLAN过滤高速丢包描述符不足或中断集中增大描述符环分散中断CRC错误多RGMII延迟不对调整PHY延迟寄存器吞吐量只有100M链路协商速率低检查自动协商通告寄存器系统卡死中断风暴检查中断清除逻辑6. 进阶话题与实战经验分享6.1 车载以太网的特殊考量车载以太网和普通以太网在驱动层面的主要区别在于PHY芯片和物理层标准。车载以太网通常使用100BASE-T1或1000BASE-T1标准通过单对双绞线传输PHY芯片如Marvell 88Q2112、NXP TJA1100等。这些PHY的寄存器配置和链路协商流程与标准以太网PHY有所不同需要参考具体的芯片手册。另外车载以太网对EMC和温度范围的要求更严格PHY的配置里通常需要使能一些特殊的抗干扰模式。如果你在做车载项目建议先用PHY厂商提供的配置工具生成初始化脚本然后再移植到Linux驱动里。6.2 虚拟化环境下的以太网直通在虚拟化环境里跑嵌入式Linux时以太网设备的直通配置是个常见需求。以QEMU为例可以通过-device vfio-pci把物理网卡直通给虚拟机或者用-netdev tap创建虚拟网络设备。直通模式下性能最好但需要IOMMU支持。虚拟网络设备模式下配置简单但吞吐量受限于虚拟交换机的性能。如果你在VirtualBox里跑嵌入式开发环境网络连接不上通常是因为网络模式选错了。NAT模式最简单但外部访问不了虚拟机桥接模式可以让虚拟机获得独立IP但需要宿主机网络支持。我一般用桥接模式配置好之后虚拟机和开发板之间可以直接通信调试方便。6.3 从零写一个以太网驱动的建议路径如果你打算从零开始写一个以太网驱动我建议按照以下路径推进。先找一个内核里已有的类似驱动作为参考比如Zynq GEM的驱动在drivers/net/ethernet/cadence/macb_main.cDesignWare GMAC的驱动在drivers/net/ethernet/stmicro/stmmac/。这些驱动结构清晰注释也比较全适合作为模板。然后从最简单的功能开始先让驱动能加载、能识别PHY、能显示接口。这个阶段不需要收发数据只要ifconfig -a能看到接口就算成功。接下来实现基本的收发功能能ping通就行。最后再优化性能加NAPI、加SG、调中断亲和性。调试过程中善用ethtool、ifconfig、ip、tcpdump这些工具。ethtool -S看统计信息ethtool -ddump寄存器tcpdump抓包分析。这些工具能帮你快速定位问题出在驱动层还是协议栈层。6.4 几个让我印象深刻的踩坑经历有一次调试一块新板子PHY死活识别不到。查了两天最后发现是硬件工程师把MDIO的上拉电阻焊成了下拉。这种硬件问题软件层面很难发现只能通过测量引脚电平来确认。还有一次是RGMII延迟的问题。链路能起来小包ping正常但一跑iperf就大量丢包。用示波器看RGMII的时钟和数据发现建立时间余量只有不到200ps。后来在PHY的扩展寄存器里加了内部延迟余量提升到800ps以上问题解决。最诡异的一次是系统跑一段时间后网络就断了重启才能恢复。查了很久发现是中断处理里有个竞态条件在特定时序下会导致中断状态清除不完整积累到一定程度就触发中断风暴。后来加了自旋锁保护中断处理的关键区域问题再没出现过。这些经历告诉我以太网驱动调试既需要软件层面的细致分析也需要对硬件时序和信号完整性有基本的了解。很多时候问题不在代码逻辑而在硬件配置或者时序参数上。6.5 工具链与调试环境搭建建议一个顺手的调试环境能大幅提升效率。我通常会在开发板上跑一个NFS根文件系统这样修改驱动后重新编译内核通过TFTP加载新内核不需要反复烧写Flash。串口控制台是必须的用来查看内核启动日志和驱动打印。网络调试方面我会在PC上跑一个Wireshark通过交换机的端口镜像功能抓取开发板的网络流量。这样能看到驱动发出的原始帧对比协议栈的预期行为快速定位问题。内核配置里记得打开CONFIG_DYNAMIC_DEBUG这样可以在运行时动态开关驱动里的调试打印不用重新编译内核。还有CONFIG_NETDEV_STATS和CONFIG_ETHTOOL_NETLINK这些选项能提供更丰富的统计信息。我个人在实际操作中的体会是以太网驱动开发最核心的能力不是写代码而是理解整个数据通路——从协议栈到驱动、从DMA到MAC、从PHY到线缆每一层都要心里有数。遇到问题的时候按照从底层到上层的顺序逐层排查用工具获取客观数据而不是靠猜这样效率最高。另外多看看内核里成熟驱动的实现很多你遇到的问题别人已经踩过坑了源码里往往藏着答案。
返回列表