嵌入式以太网驱动开发:EMAC与MDIO架构、配置与调试实战

📅 2026/7/20 12:21:38 👁️ 阅读次数 📝 编程学习
嵌入式以太网驱动开发:EMAC与MDIO架构、配置与调试实战

1. 项目概述与核心价值

在嵌入式系统开发,尤其是工业控制、物联网网关或网络设备领域,实现稳定可靠的以太网通信是基本功。很多开发者初次接触时,往往把重点放在协议栈和应用层,却对底层硬件控制器与物理层芯片(PHY)如何“握手”、如何高效协同工作感到困惑。今天,我们就来深入拆解以太网控制器(EMAC)及其管理接口(MDIO)这个组合,它们正是连接软件驱动与物理网络的桥梁。理解这套机制,不仅能帮你写出更稳定、高效的底层驱动,更能让你在调试网络不通、PHY配置异常等问题时,思路清晰,直击要害。

简单来说,EMAC负责数据链路层的“粗活累活”:按照IEEE 802.3标准封装和解封装以太网帧,进行地址过滤、CRC校验、流量控制等。但它是个“近视眼”,看不见实际的物理信号。PHY则是个“翻译官”和“信号兵”,负责将EMAC的数字信号转换成能在网线上跑的模拟信号,并处理载波侦听、冲突检测等物理层事务。MDIO就是连接这两位“同事”的一条专用“内部电话线”,EMAC通过它来给PHY下达指令(配置寄存器)和询问状态(读取寄存器)。我们以德州仪器(TI)的典型EMAC/MDIO模块为蓝本,这套架构在众多ARM Cortex系列处理器中非常常见,其设计思路具有普适性。

2. EMAC/MDIO模块整体架构解析

一个完整的以太网子系统,硬件上通常由三大部分构成:CPU及系统总线、EMAC控制器模块、以及外部的PHY芯片。EMAC模块内部又可以清晰地划分为数据通路和控制通路。

2.1 数据通路:高速传输的流水线

数据通路是网络数据的“高速公路”,追求高吞吐量和低延迟。它核心包含发送和接收两条独立的路径。

发送路径(Transmit Path)工作流如下:

  1. CPU准备数据:应用程序或网络协议栈将待发送的数据包放入系统内存的缓冲区中。
  2. 描述符(Descriptor)驱动:CPU不会直接搬运数据,而是准备一个称为“描述符”的数据结构。这个描述符就像快递单,里面记录了数据包在内存中的地址(缓冲区指针)、长度、以及一些控制信息(如是否由硬件添加CRC)。
  3. 触发DMA:CPU将这个描述符的地址写入EMAC的发送通道头指针寄存器(TXnHDP)。这个动作相当于按下了流水线的启动按钮。
  4. DMA搬运数据:EMAC内部的发送DMA引擎看到新的头指针后,开始工作。它根据描述符中的地址,直接从系统内存中将数据包内容读取出来。这个过程完全不需要CPU参与,极大地解放了CPU。
  5. 数据进入FIFO:DMA读取的数据被送入发送FIFO(先入先出缓冲区)。FIFO在这里起到关键的缓冲和速率匹配作用。因为DMA从内存读取数据是突发式的(burst),而MAC发送数据到PHY是连续、平稳的比特流。FIFO平滑了这种速率差异。
  6. MAC层封装与发送:当FIFO中的数据达到设定的阈值(例如,64字节的整数倍)或一个完整的数据包已就绪时,MAC发送器开始工作。它从FIFO中取出数据,为其添加上前导码、帧起始定界符,并根据配置决定是否计算并附加帧校验序列(CRC),最后通过MII/RMII接口的并行信号线,将数据一位一位地交给PHY。
  7. PHY处理:PHY接收到MAC发来的并行数据,将其转换成串行的差分信号,并通过RJ-45接口发送到网线上。

接收路径(Receive Path)则是反向过程:

  1. PHY接收信号:PHY从网线上接收到模拟信号,将其还原成数字比特流,再转换成并行数据,通过MII/RMII接口提交给EMAC。
  2. MAC层处理:MAC接收器首先识别前导码和帧起始,然后进行地址匹配(判断是否是发给本机的帧)、帧校验(检查CRC错误、短帧、巨帧等)。只有通过检查的帧才会被接受。
  3. 数据进入接收FIFO:被接受的帧数据被写入接收FIFO进行缓冲。
  4. DMA写入内存:接收DMA引擎从接收FIFO中取出数据,并根据接收描述符(由CPU预先准备的空缓冲区描述符)的指示,将数据直接写入系统内存的指定位置。
  5. 通知CPU:当一个帧接收完成,DMA更新对应的描述符状态(如标记帧有效、长度等),并可选地产生一个中断通知CPU:“有新的数据包到了,请处理”。

注意:这里的DMA(直接内存访问)是EMAC模块内部的专用DMA引擎,它与芯片内可能存在的通用DMA控制器是独立的。它的唯一任务就是在EMAC的FIFO和系统内存之间搬运网络数据,因此效率极高。

2.2 控制通路:MDIO与寄存器配置

如果说数据通路是“高速公路”,那么控制通路就是“交通指挥中心”。它不直接处理数据包,但负责管理整个系统的运行状态。其核心是两类寄存器访问和MDIO接口。

EMAC控制寄存器:这些是内存映射的寄存器,CPU通过读写这些寄存器来配置和控制EMAC本身的行为。例如:

  • 设置MAC地址。
  • 使能/禁用发送接收功能。
  • 配置中断掩码(决定哪些事件能触发中断)。
  • 设置流控参数、工作模式(全双工/半双工)等。

MDIO模块与PHY管理:这是本文的重点之一。MDIO是一个两线(MDC时钟线和MDIO数据线)的串行接口,遵循IEEE 802.3标准。EMAC通过它来管理外接的PHY芯片。PHY芯片内部有数十个寄存器,用于配置其工作模式(10M/100M/1000M、全双工/半双工)、自协商参数、查询链路状态、获取错误统计等。MDIO模块就是CPU配置和查询这些PHY寄存器的“代理”和“自动化工具”。

MDIO模块内部有几个关键子模块:

  • MDIO时钟发生器:从EMAC的外设时钟分频产生MDC时钟(通常为1-2.5 MHz)。
  • 全局PHY检测与链路监控:这是一个后台自动化任务。MDIO模块上电后,会自动轮询所有32个可能的PHY地址(0-31),探测哪些地址上有PHY设备存在,并持续监控它们的链路状态(是否已连接网线并协商成功)。结果会实时更新在ALIVE(PHY存活状态)和LINK(PHY链路状态)寄存器中,软件只需读取这两个寄存器就能快速掌握网络物理层的状态。
  • 活跃PHY监控:当软件选定一个PHY作为当前使用的设备后,MDIO模块会透明地(无需软件干预)持续读取该PHY的链路状态寄存器。一旦链路发生变化(通断),可以触发中断通知CPU,避免了软件频繁轮询带来的开销。
  • PHY寄存器用户访问:当软件需要主动读写PHY寄存器时(如初始化配置),通过USERACCESSn寄存器提交请求。MDIO模块会接管后续的所有时序操作,完成后通过状态位或中断通知CPU。这同样将CPU从低速的串行总线操作中解放出来。

3. MDIO模块深度剖析与实操指南

MDIO接口看似简单,但理解其工作机制对于调试PHY相关问题至关重要。很多网络不通的故障,根源都在MDIO通信或PHY配置上。

3.1 MDIO时钟配置:稳定通信的基础

MDC时钟由EMAC的外设时钟分频而来。TI的文档指出,MDC最高可运行在2.5MHz,但典型操作频率是1.0MHz。配置不当会导致通信失败。

配置步骤与原理:

  1. 确定外设时钟频率:首先,你需要查阅你所使用的具体芯片的数据手册,找到EMAC/MDIO模块的外设时钟(PER_CLK)频率。这个频率通常由芯片的PLL产生,可能是几十到几百MHz。
  2. 计算分频系数:MDIO控制寄存器(CONTROL)中的CLKDIV位域用于设置分频值。分频计算公式通常为:MDC频率 = PER_CLK频率 / ((CLKDIV + 1) * 2)。例如,PER_CLK为100MHz,要得到1MHz的MDC,计算如下:CLKDIV = (PER_CLK / (2 * MDC)) - 1 = (100e6 / (2 * 1e6)) - 1 = 49。你需要将十进制49转换为二进制或十六进制,写入CLKDIV字段。
  3. 配置前导码模式CONTROL寄存器中的PREAMBLE位用于控制是否在每次MDIO帧传输前发送32个连续的“1”作为前导码。绝大多数现代PHY都要求启用前导码(设置为1)。除非你非常确定所连接的PHY不需要,否则保持启用状态。

实操心得:在驱动初始化代码中,计算CLKDIV时务必使用整数运算,并做好边界检查。如果计算出的CLKDIV值超出寄存器位域的范围,说明当前的外设时钟频率无法产生目标MDC频率,可能需要调整系统时钟或选择另一个最接近的合法频率。MDC时钟不稳定或频率过高是导致PHY寄存器读写超时或无响应的常见原因之一。

3.2 PHY的自动发现与链路监控机制

这是MDIO模块非常实用的一个功能,极大简化了驱动编写。

全局轮询流程

  1. 一旦MDIO模块被使能(设置CONTROL.ENABLE=1),其内部状态机就会自动启动一个后台任务。
  2. 该任务循环遍历32个可能的PHY地址(0-31)。对于每个地址,它执行一次“读PHY标识寄存器1(通常为地址2)”的操作。
  3. 如果某个地址有PHY响应且返回的标识符非零(非0xFFFF且非0x0000,这两个值通常表示无设备),则ALIVE寄存器中对应此PHY地址的位会被置1。
  4. 同时,模块会读取该PHY的链路状态寄存器(通常是地址1的某一位)。如果链路已建立,则LINK寄存器中对应位也会被置1。
  5. 这个轮询是持续进行的,因此ALIVELINK寄存器反映了近乎实时的物理层状态。

软件如何利用此机制:驱动初始化时,可以读取ALIVE寄存器,找到一个值为1的位,其位索引就是系统中存在的PHY的地址。然后,你可以将这个地址配置到USERPHYSELn.PHYADRMON字段,让MDIO模块开始专门监控这个PHY的链路状态。如果你使能了链路变化中断(USERPHYSELn.LINKINTENB=1),那么当网线插拔导致链路通断时,CPU会收到中断,驱动可以快速响应,更新网络接口状态。

3.3 PHY寄存器的读写操作详解

这是驱动与PHY交互的核心。MDIO模块提供了USERACCESS0USERACCESS1两个寄存器来排队处理访问请求。

写PHY寄存器流程:

  1. 检查空闲:读取目标USERACCESSn寄存器的GO位,确保其为0(表示上一次操作已完成)。如果GO为1,需要等待或返回忙状态。
  2. 组装命令:向USERACCESSn寄存器写入一个组合值。需要设置的字段包括:
    • PHYADR:目标PHY的地址(0-31)。
    • REGADR:要写的PHY内部寄存器地址(0-31)。
    • DATA:要写入的16位数据。
    • WRITE:置1,表示写操作。
    • GO:置1,启动本次操作。
  3. 提交并等待完成:写入后,MDIO模块会接管,在MDIO总线上发起标准的写帧序列。软件可以通过两种方式等待完成:
    • 轮询:循环读取GO位,直到其变为0。
    • 中断:提前配置好USERINTMASKSET寄存器,使能对应USERACCESSn的中断。操作完成后,模块会置位USERINTRAW并触发中断。
  4. 错误处理:MDIO标准规定,写操作无确认(ACK)位。因此,只要GO位清零,通常就认为操作成功。但更严谨的做法是,在重要的配置后(如重启PHY),通过一次读操作来验证配置是否生效。

读PHY寄存器流程:

  1. 检查空闲:同样,先检查GO位是否为0。
  2. 组装命令:向USERACCESSn寄存器写入组合值。需要设置的字段包括:
    • PHYADR:目标PHY地址。
    • REGADR:要读的PHY内部寄存器地址。
    • WRITE:置0,表示读操作。
    • GO:置1,启动本次操作。
  3. 等待完成并获取数据:操作完成后,GO位清零。此时,DATA字段中即为读回的数据。关键点ACK位也会被设置(如果PHY正常响应了读请求)。因此,完整的完成检查应该是:GO==0ACK==1
  4. 错误处理:如果GO清零后ACK为0,说明PHY没有响应或发生了总线错误。此时DATA字段的值是无效的。驱动应记录错误并可能触发PHY重新探测。

代码示例与避坑指南: 虽然TI的文档提供了一个宏定义的示例,但在实际驱动开发中,我们需要一个更健壮的实现。下面是一个基于寄存器直接操作的C函数示例,并附上关键注释:

/** * @brief 通过MDIO读取PHY寄存器 * @param phy_addr PHY地址 * @param reg_addr PHY寄存器地址 * @param data 指向存储读取数据的变量的指针 * @return 0成功,-1失败(超时或ACK错误) */ int mdio_phy_read(uint8_t phy_addr, uint8_t reg_addr, uint16_t *data) { volatile mdio_regs_t *mdio = (mdio_regs_t *)MDIO_BASE; // 假设已定义寄存器结构体 // 1. 等待USERACCESS0空闲(超时处理) uint32_t timeout = 1000; // 超时计数,根据系统时钟调整 while ((mdio->USERACCESS0 & MDIO_USERACCESS0_GO_MASK) != 0) { if (--timeout == 0) { log_error("MDIO read timeout (GO bit stuck)"); return -1; // 超时返回错误 } // 可加入微小延时 delay_us(1); } // 2. 组装并发起读命令 uint32_t cmd = (1u << MDIO_USERACCESS0_GO_SHIFT) | ((phy_addr & 0x1F) << MDIO_USERACCESS0_PHYADR_SHIFT) | ((reg_addr & 0x1F) << MDIO_USERACCESS0_REGADR_SHIFT); // WRITE位默认为0,即读操作 mdio->USERACCESS0 = cmd; // 3. 等待操作完成 timeout = 1000; while ((mdio->USERACCESS0 & MDIO_USERACCESS0_GO_MASK) != 0) { if (--timeout == 0) { log_error("MDIO read operation timeout"); return -1; } delay_us(1); } // 4. 检查ACK位,确认PHY正常响应 if ((mdio->USERACCESS0 & MDIO_USERACCESS0_ACK_MASK) == 0) { log_error("MDIO read failed (No ACK from PHY @ addr 0x%x)", phy_addr); return -1; // PHY无响应 } // 5. 提取数据 *data = (mdio->USERACCESS0 & MDIO_USERACCESS0_DATA_MASK) >> MDIO_USERACCESS0_DATA_SHIFT; return 0; // 成功 } /** * @brief 通过MDIO写入PHY寄存器 */ int mdio_phy_write(uint8_t phy_addr, uint8_t reg_addr, uint16_t data) { volatile mdio_regs_t *mdio = (mdio_regs_t *)MDIO_BASE; // 等待空闲 uint32_t timeout = 1000; while ((mdio->USERACCESS0 & MDIO_USERACCESS0_GO_MASK) != 0) { if (--timeout == 0) { log_error("MDIO write timeout (GO bit stuck)"); return -1; } delay_us(1); } // 组装并发起写命令 uint32_t cmd = (1u << MDIO_USERACCESS0_GO_SHIFT) | (1u << MDIO_USERACCESS0_WRITE_SHIFT) | // 写操作 ((phy_addr & 0x1F) << MDIO_USERACCESS0_PHYADR_SHIFT) | ((reg_addr & 0x1F) << MDIO_USERACCESS0_REGADR_SHIFT) | ((data & 0xFFFF) << MDIO_USERACCESS0_DATA_SHIFT); mdio->USERACCESS0 = cmd; // 等待写操作完成(写操作无ACK,只等GO位清零) timeout = 1000; while ((mdio->USERACCESS0 & MDIO_USERACCESS0_GO_MASK) != 0) { if (--timeout == 0) { log_error("MDIO write operation timeout"); return -1; } delay_us(1); } return 0; // 写操作完成即认为成功 }

重要提示:TI示例代码中未检查ACK位,这在PHY稳定工���时可能没问题。但在驱动初始化阶段,或处理热插拔、PHY复位等场景时,PHY可能暂时无响应。不检查ACK会导致软件误以为读操作成功,从而使用无效的寄存器数据,引发一系列难以排查的诡异问题。强烈建议在mdio_phy_read函数中始终检查ACK

4. EMAC数据通路核心机制与配置

理解了MDIO如何管理PHY后,我们再来深入EMAC的数据处理核心。配置EMAC的本质,就是搭建一条从内存到网络的高效、可控的数据管道。

4.1 描述符链表:DMA的导航图

描述符是EMAC DMA引擎工作的核心。它是一段在内存中预先定义好的数据结构(通常是16字节或32字节对齐),DMA引擎通过读取描述符来知道数据在哪里、数据有多长、以及下一步该做什么。

一个典型的发送描述符包含以下关键信息:

  • 缓冲区指针:指向存放待发送数据包内容的物理内存地址。
  • 缓冲区长度:该缓冲区中有效数据的字节数。
  • 数据包标识
    • SOP:Start Of Packet,表示这是数据包的第一个分段。
    • EOP:End Of Packet,表示这是数据包的最后一个分段。
    • 一个完整的数据包可以由一个(SOP和EOP同时置位)或多个描述符(链表)描述。
  • 硬件卸载标志:如PASSCRC。若清零,EMAC硬件会在帧尾自动计算并添加CRC;若置位,则要求软件在数据缓冲区末尾提供CRC,硬件直接发送。
  • 下一个描述符指针:指向链表中下一个描述符的地址。设置为NULL或特定值表示链表结束。

接收描述符类似,但它描述的是空闲的、可供DMA写入接收数据的缓冲区。其字段包括:

  • 缓冲区指针:指向一段空闲内存,用于存放即将到来的数据包。
  • 缓冲区长度:该缓冲区的总大小。
  • 所有权标志:通常由一个OWNER位表示。OWNER=1表示该描述符(及对应的缓冲区)由DMA硬件所有,软件不能触碰;OWNER=0表示由软件所有,可以处理其中的数据。
  • 下一个描述符指针:同样用于构成链表。

工作流程简述

  1. 初始化:驱动启动时,在内存中创建两个描述符链表池:一个发送池,一个接收池。将所有接收描述符的OWNER位置1,并将接收队列的头指针(RXnHDP)指向第一个接收描述符。这相当于告诉DMA:“这些缓冲区都是你的,有数据就往里放”。
  2. 发送数据:当上层有数据要发送,驱动从发送池取一个空闲描述符,填充缓冲区指针、长度、SOP/EOP标志等,然后将其添加到发送队列末尾,并更新EMAC的发送头指针(如果DMA空闲)。DMA会依次处理队列中的描述符,发送完成后,会将描述符的OWNER位清零并产生中断,通知软件可以回收该描述符和对应的数据缓冲区。
  3. 接收数据:当网线有数据到来,DMA使用当前OWNER=1的接收描述符指定的缓冲区存放数据。完成后,DMA将OWNER位清零,更新描述符中的实际接收帧长度、状态(如是否包含错误)等信息,并产生中断。软件中断服务程序中,遍历OWNER=0的描述符,提取数据包上交协议栈,然后将该描述符重新初始化(清空状态,OWNER置1)并放回接收队列尾部,供DMA下次使用。

4.2 流量控制:防止数据淹没的阀门

在网络通信中,接收方处理数据的速度可能跟不上发送方。如果没有流控,接收方的缓冲区会被填满,导致后续的数据包被丢弃。EMAC支持两种主要的流控机制。

半双工模式下的碰撞流控: 在半双工(共享介质,如古老的总线型以太网)模式下,流控是通过主动制造“碰撞”来实现的。当EMAC接收方的空闲缓冲区数量低于某个阈值(RXnFLOWTHRESH)时,如果RXBUFFERFLOWEN使能,EMAC会对任何新到来的数据帧立即发送一个“冲突”信号(Jam序列)。发送方检测到冲突后,会进入二进制指数退避算法,延迟重发,从而为接收方争取处理时间。这种方式简单粗暴,但会增加网络冲突。

全双工模式下的暂停帧流控: 这是现代全双工以太网的标准流控方式(IEEE 802.3x)。当接收方缓冲区紧张时,EMAC会向对端发送一个特殊的“暂停帧”。这个帧的目的地址是一个特殊的组播地址(01-80-C2-00-00-01),内部包含一个“暂停时间”字段。对端设备收到此帧后,会在指定时间内(以512位时间为单位)停止发送数据。暂停时间结束后,或收到一个暂停时间为0的“取消暂停”帧后,再恢复发送。这种方式高效且不影响其他网络设备。

配置要点

  1. 根据网络工作模式(MACCONTROL.FULLDUPLEX)选择流控类型。
  2. 合理设置RXnFLOWTHRESH阈值。设置得太高,可能过早触发流控,影响吞吐量;设置得太低,可能在流控生效前缓冲区就已溢出。通常需要根据缓冲区大小和数据包到达速率进行测试和调整。
  3. 使能流控:设置MACCONTROL.RXBUFFERFLOWEN(接收流控)和MACCONTROL.TXFLOWEN(发送流控,用于响应对方的暂停帧)。

4.3 多通道与包分类:虚拟化与QoS基础

许多高性能EMAC(如TI的这款)支持多个发送和接收通道(例如8个)。这不仅仅是多个队列,它为实现网络虚拟化和简单的服务质量(QoS)提供了硬件基础。

发送通道:可以配置为轮询调度(所有通道平等)或固定优先级调度。你可以将不同优先级或不同类型的数据包放入不同的发送通道。例如,将关键的控制报文放入高优先级通道,确保其延迟最低。

接收通道:功能更强大。每个接收通道可以绑定一个独立的MAC地址(它们共享高40位,低8位不同)。这意味着单个EMAC硬件可以模拟多个网络接口。此外,通过配置RXMBPENABLE等寄存器,可以实现灵活的包分类:

  • 单播帧:根据目的MAC地址精确匹配,送入对应的通道。
  • 组播帧:根据目的MAC地址经过哈希计算,查询MACHASH1/2寄存器中的位图,决定是过滤还是送入指定的组播通道。
  • 广播帧:可以配置为全部送入一个指定的广播通道。
  • 混杂模式:可以指定一个通道接收所有经过的帧(用于网络监控)。
  • 错误帧:可以将错误帧(如CRC错误、短帧)送入指定通道,便于统计和诊断。

这个特性在开发网关设备或需要网络隔离的应用时非常有用。

5. 驱动开发实战:初始化、收发与问题排查

理论最终要落到代码上。下面我们勾勒出一个EMAC/MDIO驱动核心部分的实现框架和关键步骤。

5.1 驱动初始化序列

一个稳健的驱动初始化流程应该像启动一台精密仪器,步骤清晰有序:

  1. 硬件与时钟使能:确保EMAC/MDIO模块所在的外设电源和时钟域已被使能(通过芯片的电源与时钟管理模块)。
  2. 软件复位:向EMAC和MDIO的控制寄存器写入复位位,等待复位完成。这确保模块从一个确定的初始状态开始。
  3. 配置MDIO
    • 根据外设时钟计算并设置CONTROL.CLKDIV,配置MDC时钟频率(通常1MHz)。
    • 使能MDIO模块(CONTROL.ENABLE=1)。
    • 等待片刻,然后读取ALIVE寄存器,探测系统中存在的PHY地址。选择一个可用的PHY地址。
    • 将该地址配置到USERPHYSELn.PHYADRMON,并可选择使能链路变化中断(LINKINTENB)。
  4. 初始化PHY(通过MDIO接口):
    • 读取PHY标识符,确认PHY型号。
    • 配置PHY工作模式(如自协商、强制速率/双工)、中断掩码等。这一步高度依赖具体的PHY芯片数据手册。
    • 软重启PHY,等待自协���完成。通过轮询PHY的链路状态寄存器,确认链路已建立。
  5. 配置EMAC MAC地址:将设备的MAC地址写入MACADDRHIMACADDRLO寄存器(需配合MACINDEX选择通道)。
  6. 初始化描述符链表
    • 在内存中分配连续的、缓存对齐的发送和接收描述符数组及数据缓冲区。
    • 初始化所有接收描述符,将OWNER位置1,并链接成环状链表。将链表首地址写入接收通道头指针寄存器(RXnHDP)。
    • 初始化发送描述符池(通常OWNER位初始为0,表示由软件所有)。
  7. 配置EMAC工作模式:设置MACCONTROL寄存器,如全双工模式、使能CRC生成与检查、使能流控等。
  8. 配置接收过滤:根据需求设置RXMBPENABLERXUNICASTSET等寄存器,配置哪些帧可以被接收,以及发往哪个通道。
  9. 配置中断:使能所需的中断源(如接收完成、发送完成、链路变化等)在EMAC和MDIO的中断掩码寄存器中。
  10. 启动EMAC:最后,设置接收控制寄存器(RXCONTROL)的RXEN位,启动接收DMA引擎。发送功能通常在需要发送第一个数据包时自动激活。

5.2 数据收发流程与中断处理

发送一个数据包

  1. 驱动从发送描述符空闲池中获取一个可用的描述符。
  2. 将数据包缓冲区的物理地址、长度、以及SOP/EOP标志填充到描述符中。
  3. 检查发送DMA引擎是否空闲(可通过状态寄存器或描述符所有权判断)。如果上一个包还没发完,可能需要等待或排队。
  4. 将准备好的描述符添加到硬件发送队列(通常是更新某个队列的尾指针,或直接写入TXnHDP触发新传输)。
  5. 硬件发送完成后,会产生发送完成中断。在中断服务程序(ISR)或下半部(如NAPI)中,遍历发送完成队列,将OWNER位已清零的描述符回收至空闲池,并释放对应的数据缓冲区内存。

接收数据包

  1. 硬件接收完成并填充缓冲区后,会产生接收完成中断。
  2. 在ISR或下半部中,软件遍历接收描述符环,找到所有OWNER位为0的描述符(表示已被硬件使用过)。
  3. 从这些描述符中提取数据包长度、状态信息。检查状态,确认无错误。
  4. 将有效的数据包(缓冲区指针和长度)传递给上层网络协议栈(如Linux内核的netif_rx或NAPI的napi_gro_receive)。
  5. 协议栈处理完毕后,驱动需要“翻新”这个描述符:为其分配一个新的空闲数据缓冲区,将OWNER位置1,并将其重新链接到接收描述符环的末尾,归还给硬件DMA,等待下一次数据接收。

5.3 常见问题排查实录

在实际开发中,你会遇到各种各样的问题。下面是一个速查表,列出了典型问题现象、可能原因和排查思路。

问题现象可能原因排查步骤与技巧
PHY无法识别,ALIVE寄存器始终为01. MDIO时钟(MDC)配置错误。
2. 硬件连接问题(MDC/MDIO线断路、上拉电阻缺失)。
3. PHY地址不对。
4. PHY芯片未复位或电源异常。
1.首要检查:用示波器测量MDC引脚是否有正确频率(~1MHz)的方波。这是最直接的证据。
2. 检查硬件原理图,确认MDIO总线上有适当的上拉电阻(通常4.7k-10kΩ)。
3. 尝试遍历所有32个PHY地址进行手动读写测试。
4. 测量PHY芯片的电源、复位引脚电平。
链路状态不稳定,时通时断1. 网线或接口质量问题。
2. PHY自协商配置错误(与对端设备不匹配)。
3. 时钟抖动或噪声干扰严重。
4. 驱动中链路状态中断处理太慢,丢失事件。
1. 更换网线,检查RJ-45接口。
2. 强制设置PHY和交换机为相同的速率和双工模式(如100M全双工),绕过自协商。
3. 检查PCB布局,MDIO、MII走线是否远离噪声源,时钟线是否包地。
4. 在链路中断服务程序中,尽快读取LINK寄存器状态并更新系统,避免多次中断嵌套导致丢失。
能收到数据包,但发送失败1. 发送描述符未正确初始化或链接。
2. 发送缓冲区地址不是物理地址或未缓存对齐。
3. 发送DMA未正确启动或触发。
4. MII_TXEN/TXD等信号线连接问题。
1. 在调试器中检查发送描述符链表的内存内容,确认OWNER、指针、长度字段正确。
2. 确保传递给DMA的缓冲区地址是物理地址(在启用MMU的系统中,需使用dma_alloc_coherent或进行地址映射)。
3. 单步调试,确认在写入TXnHDP后,硬件是否开始读取描述符。
4. 用逻辑分析仪抓取MII接口的TX相关信号,看是否有数据波形发出。
接收数据包CRC错误频繁1. PHY与MAC之间的MII/RMII接口时序不满足。
2. 时钟不同步或抖动过大。
3. 板级电磁干扰严重。
4. 接收缓冲区溢出(处理太慢)。
1. 检查MII接口的时钟(RX_CLK, TX_CLK)和数据、控制信号的时序关系,是否符合PHY和MAC芯片数据手册的要求。
2. 测量时钟信号质量,看是否有过冲、振铃或抖动。
3. 优化PCB,加强电源滤波,对高速信号进行阻抗控制。
4. 增加接收描述符环的长度,或优化驱动接收处理逻辑(如采用NAPI减少中断开销)。
网络性能低下,吞吐量不达标1. 中断处理开销太大。
2. 描述符环太小,导致频繁等待。
3. 数据拷贝开销大。
4. 流控频繁触发。
1. 采用NAPI(New API)或类似的中断+轮询混合模式,在高速流量下禁用中断,改为主动轮询收包。
2. 增大发送和接收描述符环的数量(例如从64增加到256),为DMA提供更多缓冲。
3. 使用“零拷贝”或“分散-聚集”技术,让DMA直接与协议栈的缓冲区交互,减少内存拷贝。
4. 监控流控触发统计,适当调整RXnFLOWTHRESH阈值,或优化上层协议处理速度。
驱动在长时间运行后死锁或内存泄漏1. 描述符所有权管理出现竞态条件。
2. 中断使能/禁止不配对。
3. 缓冲区未正确释放。
1.关键:访问描述符环(尤其是修改OWNER位和链表指针)时,必须使用内存屏障(Memory Barrier)指令,确保CPU和DMA看到的 memory order 是一致的。
2. 确保中断服务程序尽可能短,将耗时的处理移到下半部。仔细检查中断使能和禁用的逻辑,避免嵌套错误。
3. 在释放数据包缓冲区时,必须确保DMA传输已经完成(通过描述符状态或DMA完成回调)。

内存屏障使用示例: 在更新描述符给硬件使用前,必须确保所有对描述符的写入操作对DMA可见。在ARM架构下,通常使用DSBDMB指令。

// 准备一个新的接收描述符给硬件 desc->buffer = new_buffer_phys_addr; // 写入缓冲区地址 desc->buf_len = BUFFER_SIZE; // 写入缓冲区长度 desc->flags = DESC_FLAG_OWNERSHIP; // 最重要:将所有权交给硬件 // 在将所有权交给硬件之前,必须确保以上写入对DMA可见 __dsb(ishst); // 数据同步屏障,确保所有store操作完成 // 然后才能将描述符添加到硬件队列,或者依赖硬件轮询到OWNERSHIP标志

调试这类底层驱动,逻辑分析仪示波器是你的最佳伙伴。抓取MDIO、MII/RMII总线上的波形,可以直观地看到通信是否正常、时序是否正确,往往能快速定位是硬件问题、配置问题还是软件逻辑问题。理解EMAC/MDIO的每一个寄存器位和状态机,结合扎实的硬件调试手段,就能让复杂的嵌入式网络系统稳定运行。