1. 嵌入式网络硬件MAC与PHY的“黄金搭档”搞嵌入式开发尤其是做物联网、智能硬件网络功能几乎是标配。但很多刚入行的朋友一看到网卡驱动就头大感觉比字符设备和块设备驱动复杂太多了。其实拆开来看核心硬件就两块MAC和PHY。你可以把它们想象成一对默契的搭档一个主内一个主外。MACMedia Access Control是媒体访问控制器它通常集成在SoC内部就像I2C控制器、SPI控制器一样是CPU的一个外设。它的职责是处理数据链路层的事情比如组帧、寻址、错误检测。当一款芯片的数据手册说自己“支持网络”十有八九指的是它内置了MAC控制器。但光有MAC还上不了网它需要一个翻译官和物理世界的网络打交道这就是PHYPhysical Layer芯片。PHY负责最底层的模拟信号处理比如把MAC传来的数字信号变成能在网线上跑的差分信号或者反过来。所以一个典型的嵌入式有线网络方案就是SoC内置MAC 外置PHY芯片。这里有个坑我踩过有些老旧的或者低成本的SoC比如早些年三星的S3C2440内部没有MAC。这时候怎么办那就得用“外置MACPHY一体芯片”的方案比如经典的DM9000。这种芯片对SoC提供一个类似SRAM的并行接口SoC像访问内存一样操作它。这种方案的优点是让不支持网络的SoC也能上网缺点是效率通常不如内置MAC高因为缺少专用的网络DMA等加速引擎而且成本也高一些。所以选型的时候如果SoC有内置MAC优先用“内置MAC外置PHY”的方案这是性价比和性能的最佳平衡点。我们后面要聊的驱动开发也主要围绕这种主流架构展开。2. 硬件接口详解MII、RMII、RGMII到底怎么选MAC和PHY之间要通信需要一套物理接口和协议。这套接口决定了你的网络是百兆还是千兆。最常见的有这么几组MIIMedia Independent Interface这是元老级的标准接口一共16根线包括数据、时钟、控制信号。它能支持10M和100M。但缺点很明显线太多了对PCB布线是个噩梦现在新设计已经很少用了。RMIIReduced MII顾名思义精简版的MII。信号线直接从16根砍到了7根布线压力小了很多。它同样支持10M/100M。时钟设计上和MII有个关键区别MII的发送和接收时钟都由PHY提供而RMII需要一个外部的50MHz参考时钟REF_CLK供双方共用。画原理图的时候这个时钟源千万别忘了。GMIIGigabit MII和RGMIIReduced GMII这俩是千兆网络的接口。GMII数据位宽是8位时钟125MHz所以能达到1000Mbps的速率。但它的线也不少24根。于是就有了精简版的RGMII把线数减到14根不算管理接口。RGMII的“魔法”在于它在时钟的上升沿和下降沿都采样数据。也就是说虽然数据位宽从8位减到了4位但一个时钟周期内能传输的数据量没变从而保证了千兆速率。在实际项目中我绝大部分时候用的都是RGMII接口。因为它兼顾了千兆性能和布线的便利性。正点原子的STM32MP157开发板MAC和PHY之间就是用的RGMII。这里有个关键点RGMII的时钟延迟。有时为了时序对齐需要在PCB上或通过芯片配置对发送或接收数据通道加入延迟。这就是设备树里phy-mode rgmii-id、rgmii-rxid、rgmii-txid这些配置的由来它们分别表示由PHY来提供接收延迟、发送延迟或两者。除了这些数据接口别忘了还有MDIOManagement Data Input/Output接口。它通常就两根线MDC时钟线和MDIO数据线。你可以把它理解成I2C专门用来配置PHY芯片内部的寄存器或者读取PHY的状态比如连接速度、双工模式。一个MDIO接口可以管理多个PHY靠不同的PHY地址区分。3. PHY芯片探秘以RTL8211F和YT8511C为例PHY芯片看起来是个黑盒子但其实驱动它的大部分工作Linux内核已经帮我们做好了。这得益于IEEE的标准化它规定了PHY芯片前16个寄存器的功能。所以不管你是用Realtek的RTL8211F还是裕太微电子的YT8511C只要你是进行基本的网络通信内核的通用PHY驱动就能搞定。当然每款PHY也有自己的“绝活”会通过扩展寄存器来实现这就需要厂商提供特定的驱动了。我们来看看这两款常用芯片的要点。RTL8211F-CGV1.2版核心板使用PHY地址设置通过PHYAD[2:0]三个引脚配置地址范围0x1~0x7。原理图上需要根据这些引脚的上拉/下拉状态来确定地址驱动里要写对。中断它有个INTB引脚当连接状态、速度等发生变化时会拉低中断通知MAC。这比轮询的方式高效多了。自动协商这是现代以太网必备功能双方通过“协商”来自动选择最高共通的速率和双工模式。一般我们只需要使能它配置BMCR寄存器剩下的交给硬件。RGMII电压通过CFG_EXT等引脚配置支持3.3V、2.5V等电平。务必和MAC侧的电平匹配否则通信不了。YT8511C/HV1.3版及以后核心板使用基本寄存器和RTL8211F兼容所以通用驱动也能用。特殊配置它有些功能通过特定引脚控制比如RXD3引脚控制低功耗模式LED_1000引脚控制工作模式。这些都在原理图里定好了。一个关键点STM32MP157的千兆网络需要PHY提供一个125MHz的时钟。YT8511C默认这个时钟输出是关闭的需要手动配置它的一个扩展寄存器地址0x0C将bit[2:1]设置为11来使能125MHz时钟输出。这个坑我debug了好久最后查数据手册才解决。所以更换PHY芯片后驱动移植的工作主要就是1. 确认PHY地址2. 检查并配置特殊的引脚功能和寄存器3. 确认接口电平和时钟。大部分情况下你只需要改设备树。4. Linux网络驱动核心net_device与sk_buff终于来到软件部分。Linux网络驱动的核心是struct net_device。它代表一个网络设备是驱动和内核协议栈之间的桥梁。你可以把它类比为字符设备驱动里的file_operations但更庞大。申请一个以太网设备通常用alloc_etherdev或alloc_etherdev_mqs函数。它会帮你分配net_device结构体并做一些以太网相关的通用初始化比如设置MTU为1500。驱动要做的就是填充这个结构体的各个成员最重要的就是netdev_ops操作集。netdev_ops里全是函数指针驱动开发者需要实现它们。几个最关键的函数ndo_open网卡激活时调用比如ifconfig eth0 up。在这里你要初始化硬件、申请资源、启动PHY、使能数据队列。ndo_stop网卡关闭时调用。做的事情和open相反。ndo_start_xmit这是数据发送的入口当上层有数据要发送时最终会调用到这里。你会拿到一个struct sk_buff *skb参数里面就装着要发送的数据包。你的任务就是把skb里的数据搬移到硬件的发送缓冲区并启动发送。ndo_tx_timeout发送超时处理。如果网络拥堵或硬件异常发送可能卡住这个函数会被调用通常需要重启一下发送队列或硬件模块。说到sk_buff这是Linux网络子系统的“血液”所有网络数据包都用它来承载。它是个很复杂的结构体但驱动层面主要关注这几个指针skb-data指向数据包有效负载的起始位置。skb-len有效负载的长度。skb-tail指向有效负载的末尾。内核提供了一组辅助函数来操作sk_buffskb_put(skb, len)在skb的尾部扩展len字节的空间用于存放要发送的数据。skb_push(skb, len)在skb的头部扩展len字节通常用于添加协议头。skb_pull(skb, len)从skb的头部移除len字节用于剥离协议头。skb_reserve(skb, len)在skb的缓冲区头部预留len字节通常用于对齐或给后续的skb_push留空间。数据接收的流程通常是硬件收到数据包产生中断在中断处理函数中分配一个skb从硬件FIFO读取数据到skb里然后设置skb-protocol通过eth_type_trans函数最后调用netif_rx或netif_receive_skb把skb提交给上层协议栈。5. 驱动实战STM32MP1网络驱动源码走读理论说再多不如看代码。我们以STM32MP157的平台为例看看一个真实的、SoC内置MAC外置PHY的驱动是怎么工作的。驱动源码主要位于drivers/net/ethernet/stmicro/stmmac/dwmac-stm32.c。ST的驱动已经写得非常完善了我们做移植的时候大部分是配置工作而不是重写。首先看设备树。这是现代Linux驱动开发的核心。STM32MP1的网络节点在stm32mp157d-atk.dtsi中会有类似这样的补充ethernet0 { status okay; pinctrl-0 ethernet0_rgmii_pins_a; pinctrl-1 ethernet0_rgmii_pins_sleep_a; pinctrl-names default, sleep; phy-mode rgmii-id; max-speed 1000; phy-handle phy0; mdio { #address-cells 1; #size-cells 0; phy0: ethernet-phy0 { reg 0; }; }; };这段设备树信息非常关键phy-mode rgmii-id指定了RGMII接口模式且由PHY提供内部延迟。max-speed 1000告诉驱动PHY支持千兆。phy-handle指向具体的PHY节点。mdio子节点定义了MDIO总线里面的phy0子节点指定了PHY芯片的地址这里是0。这个地址必须和硬件上PHYAD引脚设置的地址一致驱动初始化流程在stm32_dwmac_probe函数中调用stmmac_probe_config_dt解析设备树把上面那些phy-mode、max-speed、phy-handle等信息读出来填充到plat_stmmacenet_data结构体里。获取并初始化时钟、复位等硬件资源。调用stmmac_dvr_probe这个函数是ST通用MAC驱动的主入口。它会做很多事情初始化MAC控制器硬件。注册MDIO总线并且通过MDIO总线去探测和注册PHY设备。这里就是调用我们前面提到的mdiobus_register和phy_device_register。最终调用register_netdev把我们准备好的net_device注册到内核。这样ifconfig命令就能看到我们的网卡了。关于PHY驱动的匹配内核在注册MDIO总线时会去读取PHY芯片的ID寄存器地址2和3。这个ID是全世界唯一的。内核会用这个ID去匹配已编译进内核的PHY驱动。比如RTL8211F它的驱动是drivers/net/phy/realtek.c。如果匹配成功这个特定的驱动就会被绑定到PHY设备上如果没找到特定驱动就会fallback到通用的PHY驱动 (drivers/net/phy/phy_device.c)通用驱动足以保证基础通信。所以整个驱动链条是这样的应用层Socket - 内核协议栈 - 网络设备接口层(net_device) - STM32 MAC驱动(dwmac-stm32.c) - MDIO总线 - PHY芯片驱动(realtek.c或通用驱动) - 物理网线。6. 数据收发与NAPI机制在早期的网络驱动中每收到一个数据包就产生一个中断。这在低速网络下没问题但在千兆网络下如果数据包很小且密集中断风暴会彻底压垮CPU。Linux内核引入了NAPINew API机制来解决这个问题。NAPI的核心思想是“中断轮询”第一个数据包到达触发硬件中断。在中断处理函数中屏蔽接收中断然后调度NAPI的轮询函数。内核在合适的时机通常在软中断上下文调用驱动提供的轮询函数poll在这个函数里驱动轮询硬件接收缓冲区把所有等待的数据包一口气全部取出来提交给上层。处理完所有数据包后再使能接收中断等待下一次中断触发。这种方式大大减少了中断次数在高负载下性能提升非常明显。STM32MP1的驱动就使用了NAPI。在驱动代码中你会在stmmac_dvr_probe调用路径中看到netif_napi_add函数它注册了两个poll函数stmmac_napi_poll_tx和stmmac_napi_poll_rx分别用于轮询处理发送完成和接收数据。对于驱动开发者如果你要自己实现NAPI需要在驱动数据结构里定义一个struct napi_struct。在ndo_open中调用netif_napi_add注册你的poll函数并调用napi_enable。在中断处理函数中调用napi_schedule来调度你的poll函数。在你的poll函数里循环读取硬件数据直到读完或达到预算(budget)。最后调用napi_complete标记本轮处理完成。7. 调试技巧与常见坑点网络驱动调试起来比较麻烦因为涉及硬件、驱动、协议栈多个层面。分享几个我常用的方法和踩过的坑1. 确认硬件连接和供电用万用表量一下PHY芯片的供电电压通常是3.3V、2.5V、1.0V等几路是否正常。检查晶振是否起振。PHY一般需要外部25MHz晶振。用示波器或逻辑分析仪抓一下RGMII的时钟和数据线。看看125MHz千兆或25MHz百兆时钟有没有数据线上有没有波形。这是判断MAC和PHY物理层是否通信的最直接方法。2. 利用内核打印和proc文件系统在驱动里多加点printk尤其是在probe、open、ndo_start_xmit和中断处理函数里。cat /proc/interrupts可以看到你的网卡中断有没有被触发触发了多少次。dmesg | grep eth或dmesg | grep phy可以过滤出网络相关的内核日志经常能发现PHY初始化失败、链接状态变化等信息。3. 链路状态不对现象ifconfig显示网卡是UP的但LINK灯不亮或者没有RUNNING标志。排查首先ethtool eth0查看PHY的详细状态比如链接、速度、双工模式。如果这里显示“no link”问题就在物理层或PHY配置。检查设备树的phy-mode是否正确。rgmii和rgmii-id可能导致时钟时序不对无法建立链接。检查PHY芯片是否需要特殊配置比如前面说的YT8511C的125MHz时钟输出。4. 能ping通自己但ping不通别人这通常说明数据发送路径可能有问题或者接收路径虽然能收到数据但上层没处理。在ndo_start_xmit函数里加打印看看上层是不是有数据发下来。如果没有可能是上层协议栈或路由配置问题。如果有数据发下来但发送失败检查MAC的发送描述符DMA是否配置正确硬件发送状态寄存器有没有错误标志。5. 关于MAC地址如果设备树里没有指定MAC地址内核可能会用一个随机的或者全零的地址。最好在设备树里通过local-mac-address属性设置一个唯一的地址。驱动里可以通过ndo_set_mac_address来实现MAC地址修改功能。6. 性能调优如果网络吞吐量上不去可以尝试调整DMA描述符环的大小。在设备树里可以配置snps,pblProgrammable Burst Length等属性。确保内核配置了网络相关的优化选项比如CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS。最后网络驱动调试是个需要耐心的过程。很多时候问题不是出在驱动代码本身而是硬件焊接、时钟配置、设备树参数这些“外围”因素。我的经验是准备好原理图、数据手册和示波器从物理层到驱动层一层一层地隔离和排查总能找到问题所在。当你第一次看到ping通的那一刻那种成就感绝对是驱动开发中最美妙的体验之一。