首页 / 资讯中心 / 文章详情

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

嵌入式Linux以太网驱动开发实战:从MAC/PHY到NAPI与性能优化 ★ FEATURED ARTICLE
1. 嵌入式以太网驱动开发到底在搞什么做嵌入式Linux驱动开发这些年以太网这块我踩的坑比任何一个外设都多。你说它难吧协议栈都是现成的内核里TCP/IP栈跑了几十年了你说它简单吧从MAC控制器到PHY芯片从设备树配置到DMA描述符环每一层都能让你调半天。这篇内容我把自己在多个平台上折腾以太网驱动的经验整理出来从硬件接口到驱动框架从调试手段到性能优化尽量把这件事讲透。以太网驱动在嵌入式系统里属于那种平时不出事一出事就是大事的模块。工业控制场景下网口断了可能整条产线停摆车载以太网出问题直接影响行车安全。所以搞这块东西不能只会改改设备树、抄抄参考代码就完事得把整个数据通路搞清楚。这篇文章适合有一定嵌入式Linux基础、正在做或准备做以太网驱动开发的工程师也适合那些想从单片机裸机网络转向Linux驱动开发的朋友。我会从整体架构讲到具体实操把每个关键环节的为什么说清楚。2. 以太网驱动的整体架构与设计思路2.1 从硬件到软件的分层模型嵌入式以太网的数据通路可以粗略分成四层物理层PHY芯片、MAC控制器、DMA引擎、协议栈接口。PHY负责把差分信号转换成数字信号MAC负责帧的封装和解封装DMA负责在内存和MAC之间搬运数据协议栈接口就是net_device那一套。很多新手容易把PHY和MAC搞混简单说MAC是大脑PHY是嘴巴和耳朵。MAC发号施令PHY负责实际收发。在Linux内核里以太网驱动遵循net_device框架。你的驱动要做的事情就是分配net_device结构体、初始化硬件、注册中断、实现ndo_start_xmit发送函数、在中断或NAPI轮询中处理接收、维护统计信息。听起来不复杂但每个环节都有讲究。2.2 为什么选择NAPI而不是纯中断早期驱动用纯中断方式收包每个包来一个中断小包多的时候CPU直接被打爆。NAPINew API的思路是第一个包触发中断然后关中断改用轮询方式批量收包收完了再开中断。这样在高负载下能显著降低CPU占用。我实测过一个场景千兆网口跑iperf纯中断模式下CPU占用率能到70%以上换成NAPI之后降到30%左右。所以现在写驱动NAPI基本是标配。但NAPI也不是银弹低负载场景下轮询会引入额外延迟所以内核提供了netif_napi_add的权重参数来平衡。2.3 设备树与驱动匹配的设计考量现在ARM平台基本都用设备树来描述硬件。以太网节点的compatible属性决定了哪个驱动来probe。比如常见的snps,dwmac对应Synopsys DesignWare MACti,am335x-cpsw对应TI的CPSW。写驱动的时候of_match_table里要列出支持的compatible字符串。设备树里还要描述PHY的连接方式是MDIO总线还是固定连接PHY地址是多少时钟从哪来这些信息驱动probe的时候都要解析。我见过不少人设备树写错了导致PHY识别不到然后花几天时间查硬件最后发现是mdio节点没写对。3. 核心细节解析与实操要点3.1 MAC控制器初始化的关键步骤MAC初始化是整个驱动的基础顺序不能乱。一般流程是使能时钟、复位MAC、配置DMA描述符环、设置MAC地址、配置过滤模式、使能收发。每一步都有坑。时钟这块很多SoC的MAC有多个时钟域总线时钟、收发时钟、PHY参考时钟。少使能一个寄存器读写可能正常但数据收发就是不通。我建议在probe函数里把所有相关时钟都clk_get一遍用clk_prepare_enable统一使能出错时逐个disable。复位操作要注意等待时间。有些MAC复位需要几十毫秒你如果复位完立刻读写寄存器可能读到全0或全F。稳妥的做法是复位后轮询复位完成位或者至少mdelay(10)再继续。DMA描述符环的分配要用dma_alloc_coherent保证物理地址连续。描述符的数量一般是2的幂次比如256或512。太少容易丢包太多浪费内存。我一般用256个接收描述符、256个发送描述符大部分场景够用了。3.2 PHY芯片的识别与配置PHY通过MDIO总线访问地址一般是0到31。驱动需要扫描MDIO总线找到PHY读取ID寄存器确认型号然后根据PHY的特性配置。这里有个常见问题有些PHY上电后需要一段时间才能响应MDIO你如果probe太快可能读不到。解决办法是在扫描前加个延时或者重试几次。PHY的配置包括协商模式自协商还是强制、速率10/100/1000M、双工模式、流控。自协商是默认选项但有些工业场景需要强制千兆全双工来避免协商抖动。配置通过phy_device结构体和phy_driver来完成内核的PHY库已经封装好了大部分操作。注意修改PHY配置后一定要重启自协商或者软复位PHY否则新配置可能不生效。我踩过这个坑改了寄存器但没重启协商结果速率一直是100M。3.3 中断处理与NAPI轮询的配合中断处理函数要做的事情很明确确认中断源、关中断、调度NAPI。不要在中断里做太多事情否则会影响系统实时性。NAPI的poll函数里轮询收包收够了预算或者没包了就往上报。发送完成中断也要处理用来释放已经发送完的skb。有些驱动用单独的中断有些和接收共用一个。共用的时候要在中断处理里判断中断状态寄存器。中断合并是个值得调的参数。包多的时候每个包一个中断太浪费合并太多又增加延迟。我一般设置接收中断合并为收到8个包或超时100us触发一次发送中断合并为4个包或超时50us。具体值要根据场景调。3.4 设备树配置的常见陷阱设备树里以太网节点最容易出问题的地方PHY地址写错、mdio节点缺失、时钟属性不对、pinmux没配。我整理了一个检查清单mdio节点是否存在bus频率是否合理一般2.5MHzPHY的reg属性是否和硬件一致phy-mode是否正确rgmii、rmii、sgmii等时钟属性是否指向正确的时钟源pinctrl是否配置了正确的引脚复用phy-mode特别容易搞错。RGMII和RMII的时序完全不同配错了要么不通要么丢包严重。有个简单判断方法看原理图上MAC和PHY之间连了几根数据线RGMII是4根数据线双沿采样RMII是2根。4. 实操过程与核心环节实现4.1 从零搭建一个以太网驱动框架假设我们用一个虚构的SoCMAC兼容vendor,fastmacPHY是通用的千兆PHY。先写设备树节点ethernet10000000 { compatible vendor,fastmac; reg 0x10000000 0x1000; interrupts 0 32 4; clocks clk_mac, clk_mac_ptp; clock-names mac, ptp; phy-mode rgmii; phy-handle phy0; mdio { #address-cells 1; #size-cells 0; phy0: ethernet-phy1 { reg 1; }; }; };驱动侧先定义of_match_tablestatic const struct of_device_id fastmac_of_match[] { { .compatible vendor,fastmac }, { } }; MODULE_DEVICE_TABLE(of, fastmac_of_match);probe函数里按顺序做获取寄存器基地址、使能时钟、复位、分配描述符、注册netdev、连接PHY、请求中断。4.2 DMA描述符环的初始化细节描述符环是驱动和硬件之间的契约。以接收为例每个描述符包含缓冲区物理地址、状态标志、长度。初始化的时候要把所有描述符的缓冲区分配好把物理地址写进描述符状态标志设为owned by DMA。for (i 0; i RX_DESC_NUM; i) { rx_ring[i].buffer dma_alloc_coherent(dev, BUF_SIZE, rx_ring[i].phys, GFP_KERNEL); rx_ring[i].desc-buf_addr rx_ring[i].phys; rx_ring[i].desc-status DMA_OWN; }发送描述符类似但缓冲区是动态分配的skb。发送的时候把skb的数据物理地址填进去设置好长度和标志然后通知硬件。提示dma_alloc_coherent分配的缓冲区要保证cache一致性。如果你的平台不支持硬件cache一致性收包后需要invalidate cache发包前需要flush cache。这个搞错了会出现数据错乱而且很难查。4.3 收发流程的完整实现发送流程上层调用ndo_start_xmit驱动检查发送队列是否有空位有的话把skb映射成DMA地址填充描述符更新发送索引通知硬件。发送完成后在中断里释放skb和DMA映射。接收流程NAPI poll函数里检查接收描述符的状态如果是owned by CPU说明有包到了。把数据从DMA缓冲区拷贝到skb或者用build_skb零拷贝设置skb的协议类型调用napi_gro_receive往上送。然后重新分配缓冲区把描述符还给DMA。这里有个性能优化点小包多的时候拷贝开销很大。可以用build_skb把DMA缓冲区直接做成skb省掉一次拷贝。但这样缓冲区管理会复杂一些要确保skb释放后缓冲区能回到驱动手里。4.4 中断与轮询的代码实现static irqreturn_t fastmac_interrupt(int irq, void *dev_id) { struct fastmac_priv *priv dev_id; u32 status readl(priv-base INT_STATUS); if (status RX_INT) { writel(RX_INT, priv-base INT_MASK_CLR); napi_schedule(priv-napi); } if (status TX_INT) { writel(TX_INT, priv-base INT_MASK_CLR); fastmac_tx_clean(priv); } return IRQ_HANDLED; } static int fastmac_poll(struct napi_struct *napi, int budget) { struct fastmac_priv *priv container_of(napi, struct fastmac_priv, napi); int received 0; while (received budget) { if (!fastmac_rx_packet(priv)) break; received; } if (received budget) { napi_complete(napi); writel(RX_INT, priv-base INT_MASK_SET); } return received; }这段代码看起来简单但有几个细节中断确认要在关中断之前做否则可能丢中断napi_complete之后才能重新使能中断顺序反了会出问题。5. 常见问题与排查技巧实录5.1 网口不通的排查思路网口不通是最常见的问题排查要按层次来。先看PHY有没有识别到读PHY ID寄存器如果是0xFFFF或0x0000说明MDIO通信有问题。检查mdio时钟、PHY地址、上电时序。PHY识别到了但不通看自协商状态。读BMSR寄存器看link状态和协商结果。如果link up了但速率不对检查phy-mode配置。MAC层面看寄存器读写是否正常。如果读写都正常但DMA不工作检查描述符环的基地址寄存器是否写对DMA使能位是否置位。最后看协议栈层面ifconfig看统计信息有没有RX errors、TX errors、dropped。这些计数器能告诉你问题出在哪个环节。5.2 丢包问题的定位方法丢包分两种接收丢包和发送丢包。接收丢包看RX dropped和RX fifo errors。如果fifo errors多说明DMA搬运不过来可能是描述符太少或者总线带宽不够。如果dropped多可能是协议栈处理不过来看CPU占用和softirq统计。发送丢包看TX dropped和TX carrier errors。carrier errors一般是PHY链路问题dropped可能是发送队列满了。可以适当增加发送队列长度或者用flow control让对端慢点发。我遇到过一个典型案例千兆网口跑UDP大包吞吐量只有100Mbps。查下来是DMA描述符的缓冲区大小设成了1536字节但实际包是1518字节加上描述符开销刚好超了导致每个包都要分片。改成2048之后吞吐量直接上到900Mbps。5.3 常见问题速查表现象可能原因排查方法PHY ID读不到MDIO时钟太快、PHY地址错、上电时序降低MDIO频率、扫描所有地址、加延时Link up但ping不通phy-mode错、MAC地址全0、防火墙检查设备树、ifconfig看MAC、关防火墙丢包严重描述符太少、缓冲区太小、中断合并过度增加描述符、调大缓冲区、调整合并参数吞吐量低DMA未使能、cache一致性、CPU瓶颈查DMA寄存器、检查cache操作、看CPU占用网口时通时断时钟不稳定、电源纹波、连接器接触不良示波器看时钟、测电源、换线5.4 调试工具与手段ethtool是必须会的工具。ethtool eth0看链路状态ethtool -S eth0看统计ethtool -i eth0看驱动信息。调试PHY的时候ethtool --phy-statistics很有用。ifconfig和ip命令看接口状态和统计。/proc/net/dev看收发包计数。dmesg看驱动打印probe阶段的log能告诉你很多信息。硬件层面示波器看MDIO时钟和数据线逻辑分析仪抓MDIO帧。如果MDIO通信有问题抓波形是最直接的。我一般先确认MDIO时钟频率标准是2.5MHz有些PHY支持到12.5MHz但频率太高线长了容易出错。6. 性能优化与进阶话题6.1 零拷贝收发的实现思路零拷贝的核心思想是让数据在内存里只搬一次。接收方向用build_skb把DMA缓冲区直接变成skb省掉memcpy。发送方向用skb的碎片机制把用户数据直接映射成DMA地址省掉拷贝。但零拷贝不是没有代价的。缓冲区管理变复杂skb释放后要确保缓冲区回到驱动。而且不是所有场景都适合零拷贝小包多的时候管理开销可能比拷贝还大。我一般在大包场景下开零拷贝小包场景还是老老实实拷贝。6.2 多队列与RSS高端SoC的MAC支持多队列每个队列有独立的中断和NAPI实例。配合RSS接收侧缩放可以把不同流的包分发到不同CPU核上处理大幅提升多核平台的网络性能。实现多队列驱动每个队列一套描述符环和NAPI中断可以共享也可以分开。关键是队列和CPU的绑定一般用irq affinity把中断绑到特定核然后该核处理对应队列的包。6.3 时间同步与PTP工业场景经常需要PTP精确时间协议来做时间同步。支持PTP的MAC有硬件时间戳单元能在包收发时打上精确的时间戳。驱动需要实现PTP clock的注册和时间戳的读取。这块比较复杂涉及SO_TIMESTAMPING选项、PTP硬件时钟、PHC子系统。如果只是做普通网络通信可以跳过但如果做工业控制或音视频同步PTP是绕不开的。6.4 车载以太网的特别之处车载以太网和普通以太网有几个不同一是速率100BASE-T1和1000BASE-T1是单对双绞线不是普通的四对二是PHY配置主从模式是固定的不能自协商三是EMC要求高线束和连接器都有特殊要求。驱动层面车载以太网PHY的寄存器操作和普通PHY类似但初始化流程不同。主从模式要在设备树里指定不能靠自协商。另外车载以太网通常跑AVB或TSN协议对延迟和抖动要求很高中断合并参数要调得更激进。7. 我个人的一些经验体会以太网驱动开发最忌讳的就是抄代码不改。每个平台的MAC和PHY都不一样参考代码只能给你框架细节必须自己调。我见过太多人把参考驱动的设备树直接复制过来结果PHY地址不对、时钟属性不对然后花几天时间怀疑硬件。调试的时候一定要有层次感。先确认PHY能不能识别再确认link能不能up再确认能不能收发最后才看性能和稳定性。不要一上来就抓包分析协议底层都没通抓包也看不出什么。还有一点统计信息是你的朋友。RX errors、TX errors、dropped、fifo errors这些计数器能告诉你很多信息。养成习惯每次调试先看统计比盲目抓包效率高得多。最后说个小心得MDIO总线上可以挂多个PHY但地址不能冲突。如果你板子上有交换芯片或者多个网口扫描PHY的时候要注意地址分配。有些交换芯片的PHY地址是固定的你如果不知道扫描的时候可能会干扰到它。我一般先看原理图确认PHY地址再写驱动省得后面排查。
阅读完成 · 觉得有帮助?
咨询建站