☰
嵌入式以太网驱动开发实战:从MAC/PHY硬件到设备树与内核调优
2026/10/6 15:34:12 网站建设 项目流程

1. 嵌入式以太网驱动开发到底在搞什么

1.1 从一个真实场景说起

先聊一个我亲身经历的事。几年前接手一个工业网关项目,主控用的是Zynq系列芯片,PS端跑Linux,PL端挂了个自定义IP核,需要通过以太网把采集到的传感器数据实时传到上位机。硬件同事把板子焊好、网口灯也亮了,我这边驱动一加载,ifconfig能看到eth0,但就是ping不通,ethtool显示link up但收发包计数全是零。折腾了整整两天,最后发现是PHY芯片的地址在设备树里配错了——MDIO总线上挂了两个PHY,一个地址是0x01,另一个是0x03,我照着参考板抄成了0x00,结果驱动一直在跟一个不存在的PHY通信。

这件事让我意识到,嵌入式以太网驱动开发远不是“写个初始化函数就完事”的活儿。它横跨硬件原理、总线协议、内核网络子系统、设备树配置、性能调优等多个层面,任何一个环节出问题,表现都是“网不通”三个字,但根因可能千差万别。

这一期我们就围绕嵌入式驱动开发中的Ethernet以太网这个主题,把从硬件接口到内核驱动、从设备树配置到性能调优的完整链路拆开来讲。不管你是刚接触嵌入式Linux驱动的新手,还是已经做过几个项目但遇到瓶颈的老手,都能从中找到可以直接复用的经验和方法。

1.2 以太网驱动在嵌入式系统里的位置

要理解以太网驱动开发,先得搞清楚它在整个系统里的位置。一个典型的嵌入式Linux系统,网络数据从应用层到物理网线,大致经过这么几层:

  • 应用层:socket接口,TCP/UDP数据
  • 内核网络协议栈:IP层、ARP、路由等
  • 网络设备层:net_device结构体,NAPI机制
  • 以太网驱动:MAC控制器驱动,收发描述符管理
  • PHY驱动:通过MDIO总线管理PHY芯片
  • 硬件层:MAC控制器、PHY芯片、变压器、RJ45接口

以太网驱动开发的核心工作,就是实现中间那两层——MAC控制器驱动和PHY驱动,让内核网络协议栈能够通过硬件收发以太网帧。听起来简单,但实际做起来,每一层都有大量的细节需要处理。

1.3 为什么嵌入式以太网驱动值得单独拿出来讲

有人可能会问,PC上的网卡驱动不也是以太网驱动吗,有什么特别的?区别大了。PC上的网卡通常是PCIe接口,硬件自动枚举,驱动模型成熟,厂商提供完整的驱动源码。而嵌入式场景下:

  • MAC控制器可能是SoC内置的,寄存器手册几百页
  • PHY芯片通过MDIO/MDC两根线管理,地址需要手动配置
  • 接口类型多样:MII、RMII、GMII、RGMII、SGMII,每种时序不同
  • 时钟树复杂,参考时钟来源和频率需要仔细配置
  • 设备树需要自己写,每个板子都不一样
  • 功耗、实时性、吞吐量要求各不相同

这些差异决定了嵌入式以太网驱动开发需要一套独立的知识体系。下面我就按照实际开发的流程,从硬件接口讲到驱动实现,再讲到调试和优化。

2. 硬件接口与协议基础:搞懂MAC、PHY和接口类型

2.1 MAC和PHY的分工

以太网通信的硬件核心是两颗芯片:MAC和PHY。MAC全称Media Access Control,负责以太网帧的封装和解封装、CRC校验、地址过滤等;PHY全称Physical Layer,负责把MAC送来的数字信号转换成适合在网线上传输的模拟信号。

打个比方,MAC就像公司的收发室,负责把文件装进标准信封、写上收发地址;PHY就像邮递员,负责把信封实际送到对方手里。两者之间通过标准接口连接,最常见的是MII系列接口。

在嵌入式SoC里,MAC通常是内置的,比如STM32的ETH外设、Zynq的GEM、i.MX系列的FEC。PHY则是外挂的独立芯片,比如常见的KSZ8081、DP83848、RTL8211等。MAC和PHY之间还需要一个MDIO接口,用来读写PHY的内部寄存器,配置速率、双工模式、自协商等参数。

2.2 MII、RMII、GMII、RGMII到底怎么选

这是新手最容易迷糊的地方。MII是Media Independent Interface的缩写,意思是“介质无关接口”,不管你是双绞线还是光纤,MAC和PHY之间的接口是统一的。但随着速率提升,MII的引脚数太多,于是衍生出一堆变种:

接口类型速率数据位宽时钟频率引脚数典型场景
MII10/100M4位25MHz/2.5MHz16+早期设计
RMII10/100M2位50MHz8+引脚受限场景
GMII1000M8位125MHz24+千兆设计
RGMII1000M4位125MHz DDR12+主流千兆方案
SGMII1000M串行625MHz4+背板、光模块

选型逻辑很直接:引脚够用、速率要求不高就选MII;引脚紧张选RMII;千兆优先选RGMII,因为引脚少、成本低;需要走背板或连接光模块就选SGMII。

RGMII有个坑需要注意:它用DDR方式在125MHz时钟的上下沿都传数据,所以对时序要求很严。PCB走线长度不匹配、时钟偏移没调好,就会出现丢包甚至完全不通。我在一个项目里遇到过RGMII的RX时钟延迟配置不对,导致接收方向大量CRC错误,后来在设备树里调整了rx-internal-delay-ps参数才解决。

2.3 MDIO总线和PHY地址

MDIO是Management Data Input/Output的缩写,只有两根线:MDC时钟和MDIO数据。MAC通过这两根线读写PHY的寄存器,标准寄存器有32个,前16个是IEEE定义的,后16个厂商自定义。

每个PHY在MDIO总线上有一个5位的地址,范围0到31。一个MDIO总线可以挂最多32个PHY。设备树里需要正确配置PHY地址,否则驱动找不到PHY。前面提到的我那个踩坑经历,就是PHY地址配错了。

提示:如果不确定PHY地址,可以在uboot里用mii info命令扫描MDIO总线,或者用mii dump <addr>逐个地址读取寄存器0,如果能读出0x1140或0x1040之类的值,说明这个地址上有PHY。

2.4 自协商与强制模式

PHY上电后默认会进行自协商,和対端交换能力信息,协商出双方都支持的最高速率和双工模式。大部分场景下自协商是没问题的,但在某些工业环境下,対端设备可能不支持自协商或者自协商不稳定,这时候就需要强制指定速率和双工模式。

强制模式在设备树里通过phy-mode和fixed-link来配置。比如:

&fec1 { phy-mode = "rmii"; fixed-link { speed = <100>; full-duplex; }; };

这段配置的意思是:不使用自协商,强制100M全双工。注意,强制模式下两端必须配置一致,否则会出现半双工/全双工不匹配,表现为能ping通但丢包严重、吞吐量极低。

3. 设备树配置:以太网驱动的第一道关卡

3.1 设备树里以太网节点的关键属性

设备树是嵌入式Linux驱动的“硬件说明书”,以太网驱动能不能正常工作,很大程度上取决于设备树写得对不对。一个典型的以太网节点包含这些关键属性:

&mac { pinctrl-names = "default"; pinctrl-0 = <&mac_pins>; phy-mode = "rgmii-id"; phy-handle = <&phy0>; status = "okay"; mdio { #address-cells = <1>; #size-cells = <0>; phy0: ethernet-phy@1 { reg = <1>; reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>; reset-assert-us = <10000>; reset-deassert-us = <50000>; }; }; };

逐个解释:

  • phy-mode:指定MAC和PHY之间的接口类型,常见值有mii、rmii、gmii、rgmii、rgmii-id、sgmii。rgmii-id表示RGMII接口且内部延迟,MAC和PHY各自处理自己的时钟延迟。
  • phy-handle:指向MDIO总线下的PHY节点。
  • reg:PHY在MDIO总线上的地址。
  • reset-gpios:PHY的复位引脚,有些PHY需要硬件复位才能正常工作。
  • reset-assert-us和reset-deassert-us:复位拉低和拉高的持续时间,单位微秒。

3.2 phy-mode的坑:rgmii、rgmii-id、rgmii-txid、rgmii-rxid

这四个值看起来差不多,但含义完全不同:

  • rgmii:MAC和PHY都不加内部延迟,需要PCB走线来保证时序
  • rgmii-id:MAC和PHY都加内部延迟
  • rgmii-txid:只在发送方向加内部延迟
  • rgmii-rxid:只在接收方向加内部延迟

选哪个取决于硬件设计。如果PCB走线等长做得很好,可以用rgmii;如果走线有偏差,就需要用内部延迟来补偿。我一般先用rgmii-id试,不通再换其他模式。实测下来,大部分国产PHY配合国产SoC,rgmii-id的成功率最高。

3.3 时钟配置:容易被忽略的关键点

以太网需要时钟,而且不止一个。以RGMII为例:

  • MAC需要125MHz的发送时钟和接收时钟
  • PHY需要25MHz或50MHz的参考时钟
  • MDIO需要MDC时钟,通常由MAC分频产生

参考时钟的来源有两种:外部晶振直接提供给PHY,或者由SoC的时钟控制器输出。设备树里需要配置时钟树,确保每个时钟都正确使能。

&mac { clocks = <&clks IMX6QDL_CLK_ENET>, <&clks IMX6QDL_CLK_ENET_REF>; clock-names = "ipg", "ptp"; assigned-clocks = <&clks IMX6QDL_CLK_ENET_REF>; assigned-clock-rates = <125000000>; };

这段配置的意思是:MAC的IPG时钟和PTP时钟来自时钟控制器,并且把ENET_REF时钟设置为125MHz。如果时钟频率不对,PHY可能无法正常建立链路,或者链路能建立但数据错误率极高。

3.4 引脚复用配置

SoC的引脚通常是多功能的,以太网引脚需要配置为正确的复用功能。这部分在设备树的pinctrl节点里完成:

&iomuxc { mac_pins: macgrp { fsl,pins = < MX6QDL_PAD_ENET_MDIO__ENET_MDIO 0x1b0b0 MX6QDL_PAD_ENET_MDC__ENET_MDC 0x1b0b0 MX6QDL_PAD_RGMII_TXC__RGMII_TXC 0x1b0b0 MX6QDL_PAD_RGMII_TD0__RGMII_TD0 0x1b0b0 /* ... 其他引脚 ... */ >; }; };

每个引脚的配置值包含驱动能力、上下拉、速率等参数。这些值通常参考SoC手册和参考板设计,不要随意改动。我曾经因为把某个引脚的驱动能力从0x1b0b0改成了0x1b0b1,导致RGMII发送时钟信号质量下降,千兆模式下丢包率飙升。

4. 驱动代码实现:从probe到收发

4.1 驱动框架概览

Linux内核的以太网驱动遵循标准的网络设备驱动框架。核心结构体是struct net_device,驱动需要实现的主要回调函数包括:

  • ndo_open:打开设备,初始化硬件,启动收发
  • ndo_stop:关闭设备,停止收发
  • ndo_start_xmit:发送数据包
  • ndo_set_mac_address:设置MAC地址
  • ndo_get_stats:获取统计信息
  • ndo_do_ioctl:ioctl命令处理
  • ndo_tx_timeout:发送超时处理

驱动的初始化流程通常是:平台驱动probe -> 分配net_device -> 初始化硬件 -> 注册net_device -> 等待用户ifconfig up。

4.2 probe函数里做了什么

probe函数是驱动的入口,主要工作包括:

  1. 获取设备树信息:读取寄存器基地址、中断号、时钟、PHY句柄等
  2. 映射寄存器:devm_ioremap_resource把物理地址映射到虚拟地址
  3. 使能时钟:clk_prepare_enable使能MAC和PHY的时钟
  4. 复位MAC:写MAC控制寄存器,软复位
  5. 配置MAC:设置MAC地址、接口模式、速率等
  6. 连接PHY:通过of_phy_connect或phy_connect连接PHY设备
  7. 注册中断:申请收发中断
  8. 分配描述符:分配DMA描述符环
  9. 注册net_device:register_netdev注册网络设备

每一步都可能出问题。比如时钟没使能,读写寄存器会返回全0或全F;PHY连接失败,phy_connect会返回NULL;中断申请失败,收发中断无法响应。

4.3 收发描述符与DMA

现代以太网MAC都支持DMA,驱动需要管理描述符环。以发送为例:

  • 驱动分配一组发送描述符(通常256或512个)
  • 每个描述符指向一个数据缓冲区
  • 驱动把要发送的数据填入缓冲区,更新描述符状态
  • MAC读取描述符,通过DMA把数据发送出去
  • 发送完成后,MAC更新描述符状态,触发中断
  • 驱动在中断处理中回收缓冲区

接收方向类似,只是方向相反。描述符的管理是驱动性能的关键,描述符数量太少会导致丢包,太多会浪费内存。

/* 发送描述符结构体示例 */ struct tx_desc { __le32 status; __le32 length; __le32 buf_addr; __le32 next; };

描述符的status字段包含OWN位(表示描述符归属MAC还是CPU)、错误标志、校验和状态等。驱动和MAC通过OWN位来同步描述符的归属。

4.4 NAPI与中断处理

Linux内核从2.6版本开始引入NAPI(New API)机制,用于在高负载下减少中断开销。NAPI的核心思想是:中断触发后,关闭接收中断,改用轮询方式处理数据包,直到没有数据包或达到预算上限,再重新打开中断。

以太网驱动的中断处理函数通常这样写:

static irqreturn_t mac_interrupt(int irq, void *dev_id) { struct net_device *ndev = dev_id; struct mac_priv *priv = netdev_priv(ndev); u32 status; status = readl(priv->base + MAC_ISR); writel(status, priv->base + MAC_ISR); if (status & MAC_ISR_RX) { if (napi_schedule_prep(&priv->napi)) { writel(0, priv->base + MAC_IER); __napi_schedule(&priv->napi); } } if (status & MAC_ISR_TX) { mac_tx_clean(priv); } return IRQ_HANDLED; }

NAPI的poll函数负责实际的数据包接收:

static int mac_poll(struct napi_struct *napi, int budget) { struct mac_priv *priv = container_of(napi, struct mac_priv, napi); int work_done = 0; while (work_done < budget) { struct sk_buff *skb = mac_rx(priv); if (!skb) break; napi_gro_receive(napi, skb); work_done++; } if (work_done < budget) { napi_complete(napi); writel(MAC_IER_RX, priv->base + MAC_IER); } return work_done; }

注意:NAPI的budget参数通常设为64或128,表示一次poll最多处理多少个数据包。设太小会导致中断频繁,设太大会增加延迟。实测下来,64到128之间是比较平衡的值。

4.5 PHY驱动与自协商流程

PHY驱动通常不需要自己写,内核已经包含了大部分常见PHY的驱动。驱动开发者需要做的是在设备树里正确描述PHY,然后调用phy_connect连接。

PHY连接后,内核的PHY状态机会自动处理自协商。自协商完成后,PHY会触发一个中断或通过轮询通知MAC,MAC再调整自己的速率和双工模式。

如果PHY驱动不支持你的PHY芯片,就需要自己写一个。PHY驱动的主要工作是:

  • 实现config_init回调,配置PHY的初始状态
  • 实现read_status回调,读取链路状态
  • 实现config_aneg回调,配置自协商参数
  • 注册到phy_driver链表
static struct phy_driver my_phy_driver = { .phy_id = 0x12345678, .name = "My PHY", .phy_id_mask = 0xffffffff, .features = PHY_GBIT_FEATURES, .config_init = my_phy_config_init, .config_aneg = my_phy_config_aneg, .read_status = my_phy_read_status, };

5. 调试与问题排查:网不通到底卡在哪

5.1 调试工具和方法

以太网驱动调试有一套成熟的工具链:

  • ifconfig/ip link:查看接口状态、MAC地址、统计计数
  • ethtool:查看链路状态、速率、双工、自协商参数
  • ethtool -S:查看详细的收发统计,包括错误计数
  • mii-tool:查看和设置PHY状态
  • mii dump:在uboot里读写PHY寄存器
  • tcpdump:抓包分析
  • ping/iperf:连通性和性能测试

调试的基本思路是自底向上:先确认PHY链路是否建立,再确认MAC是否正常收发,最后确认协议栈是否正常。

5.2 常见问题速查表

现象可能原因排查方法
网口灯不亮供电、复位、时钟、PHY地址量电压、查时钟、扫MDIO
link up但ping不通MAC地址、IP配置、防火墙ip addr、tcpdump
丢包严重双工不匹配、RGMII延迟、描述符不足ethtool -S、调整延迟
吞吐量低中断开销、描述符数量、CPU占用iperf、top、调整NAPI
千兆协商失败时钟质量、PCB走线、PHY配置降速测试、查信号完整性
热插拔后不通PHY状态机、中断丢失重新ifconfig up、查中断计数

5.3 一个典型的排查案例

回到开头我提到的那个Zynq项目。现象是:ifconfig能看到eth0,ethtool显示link up,但ping不通,ethtool -S显示rx_packets和tx_packets都是0。

排查步骤:

  1. 确认PHY地址:在uboot里用mii info扫描,发现地址0x01和0x03上有PHY,0x00上没有。设备树里配的是0x00,改成0x01。
  2. 重新加载驱动,ethtool显示link up,但ethtool -S仍然全是0。
  3. 检查中断:cat /proc/interrupts发现以太网中断计数为0。说明中断没有触发。
  4. 检查中断配置:设备树里中断号写的是<0 29 4>,但Zynq的GEM中断号应该是<0 57 4>。改过来。
  5. 重新加载,中断计数开始增长,ethtool -S显示收发包正常,ping通。

这个案例说明,以太网驱动的问题往往不是单一原因,而是多个配置错误叠加。排查时要一层一层剥,先解决最底层的硬件问题,再往上查。

5.4 性能调优的几个方向

网通了之后,下一步就是调优。嵌入式以太网的性能瓶颈通常在这几个地方:

  • 描述符数量:发送和接收描述符各增加到512或1024,可以减少丢包
  • NAPI预算:从64增加到128或256,减少中断次数
  • 中断合并:如果MAC支持,开启中断合并,减少CPU占用
  • CPU亲和性:把以太网中断绑定到特定CPU核心,避免缓存颠簸
  • TCP窗口:调整内核的TCP窗口参数,提升大流量吞吐
# 把eth0的中断绑定到CPU1 echo 2 > /proc/irq/57/smp_affinity # 调整TCP缓冲区 sysctl -w net.core.rmem_max=16777216 sysctl -w net.core.wmem_max=16777216

实测下来,在Zynq平台上,经过这些调优,千兆以太网的TCP吞吐量可以从600Mbps提升到900Mbps以上。

6. 几个容易踩坑的细节和经验

6.1 PHY复位时序不能省

很多PHY芯片需要在上电后拉低复位引脚至少10ms,然后拉高,再等待至少50ms才能访问寄存器。如果复位时间不够,PHY可能处于不确定状态,表现为MDIO读写失败或链路不稳定。设备树里的reset-assert-us和reset-deassert-us就是干这个的,不要图省事删掉。

6.2 MAC地址的读取和设置

嵌入式设备的MAC地址通常存在EEPROM、OTP或Flash的某个分区里。驱动需要在probe时读取MAC地址并写入MAC寄存器。如果读不到,内核会随机生成一个,但随机MAC在局域网里可能冲突。建议在uboot里就把MAC地址设置好,传给内核。

6.3 中断和轮询的取舍

NAPI是中断和轮询的混合体,但不是所有场景都适合。在低负载场景下,纯中断模式延迟更低;在高负载场景下,NAPI能显著降低CPU占用。有些MAC支持自适应中断合并,可以根据负载动态调整,这是比较理想的方案。

6.4 别忘了看SoC手册的勘误表

SoC的以太网MAC可能有硬件bug,比如某个版本的芯片在特定条件下会丢包、DMA会卡死等。这些通常在勘误表(Errata)里有说明,驱动里需要加workaround。我遇到过一个SoC的MAC在千兆模式下发送描述符超过256个时会卡死,后来在驱动里限制发送队列长度为128才解决。

6.5 测试要覆盖异常场景

不要只测正常ping通就完事。要测:网线热插拔、对端设备重启、强制双工不匹配、大流量持续传输、CPU高负载下的收发、休眠唤醒后的恢复。这些异常场景才是真正暴露问题的地方。

嵌入式以太网驱动开发是一个需要耐心和细心的活儿,硬件、设备树、驱动、协议栈,每一层都可能出问题。但只要掌握了正确的调试方法和排查思路,大部分问题都能在可控时间内解决。希望这一期的内容能帮你在下一个以太网项目里少走一些弯路。

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

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

立即咨询