以太网MAC驱动:从控制器初始化到数据收发

以太网MAC驱动,说白了就是让CPU和物理层(PHY)之间能顺畅地收发数据包。我做了这么多年嵌入式网络驱动,MAC这块往往是整个系统稳定性的关键。今天咱们就把它拆开揉碎了讲清楚。

核心要点:MAC驱动负责数据链路层的帧封装/解封装、流量控制、以及通过MDIO总线管理PHY芯片。DMA描述符的设计直接影响收发性能。

MAC控制器初始化——第一步不能错

初始化MAC控制器,我习惯按这个顺序来。顺序错了,后面怎么调都调不通。

  1. 软复位:写复位寄存器,等待自清除。我遇到过有些芯片复位需要轮询超时,别死等。
  2. 配置工作模式:全双工/半双工、100M/1000M、流控使能。这些参数要和PHY协商结果一致。
  3. 设置MAC地址:6字节地址写入地址寄存器。注意字节序,有些芯片是大端。
  4. 配置帧过滤:广播帧、多播帧、混杂模式。调试阶段我通常先开混杂模式。
  5. 使能收发:最后才打开收发使能位。
// MAC初始化示例(简化版)
void mac_init(mac_config_t *cfg) {
    // 1. 软复位
    MAC->CR |= MAC_CR_SWR;
    while (MAC->CR & MAC_CR_SWR);  // 等待复位完成
    
    // 2. 配置模式
    MAC->CR = (cfg->duplex ? MAC_CR_DM : 0) |
               (cfg->speed << MAC_CR_PS_SHIFT) |
               MAC_CR_IPC;  // 使能IPv4校验和卸载
    
    // 3. 设置MAC地址
    MAC->MAC_ADDR_H = (cfg->addr[5] << 8) | cfg->addr[4];
    MAC->MAC_ADDR_L = (cfg->addr[3] << 24) | (cfg->addr[2] << 16) |
                      (cfg->addr[1] << 8)  | cfg->addr[0];
    
    // 4. 帧过滤
    MAC->FFR = cfg->promisc ? MAC_FFR_PM : 0;
    
    // 5. 使能收发
    MAC->CR |= MAC_CR_TE | MAC_CR_RE;
}

我的习惯:初始化完成后,读回寄存器验证一遍。有些芯片的复位位自清除时间很长,加个超时处理更稳妥。

DMA描述符配置——性能的命门

DMA描述符,说白了就是CPU和DMA控制器之间的"任务清单"。每个描述符对应一个数据缓冲区。我见过不少工程师在这上面栽跟头。

描述符通常包含:

  • 缓冲区地址:数据包在内存中的位置
  • 缓冲区长度:最大接收长度或实际发送长度
  • 控制位:是否最后一个描述符、是否产生中断
  • 状态位:由DMA写入,表示传输完成或错误
字段 偏移 说明
TDES0/TDES0 0x00 控制/状态字(所有权位、中断使能、帧结束标记)
TDES1/TDES1 0x04 缓冲区1长度、缓冲区2长度
TDES2/TDES2 0x08 缓冲区1地址
TDES3/TDES3 0x0C 下一个描述符地址(链式结构)

描述符链有两种常见结构:

  • 链式:每个描述符指向下一个,最后一个指向第一个。灵活,但遍历开销大。
  • 环形:描述符在内存中连续排列,通过索引循环。性能更好,我偏爱这种。
// 环形描述符初始化
#define DESC_NUM  64
dma_desc_t rx_desc[DESC_NUM] __attribute__((aligned(64)));
uint8_t rx_buf[DESC_NUM][2048] __attribute__((aligned(32)));

void rx_desc_init(void) {
    for (int i = 0; i < DESC_NUM; i++) {
        rx_desc[i].addr = (uint32_t)rx_buf[i];
        rx_desc[i].ctrl = DESC_OWN | DESC_INT_EN;  // 所有权给DMA,使能中断
        rx_desc[i].next = (uint32_t)&rx_desc[(i + 1) % DESC_NUM];
    }
    // 最后一个描述符不使能中断(减少中断频率)
    rx_desc[DESC_NUM - 1].ctrl = DESC_OWN;
}

我曾经踩过的坑:描述符和缓冲区的对齐要求。有些DMA要求描述符64字节对齐,缓冲区32字节对齐。不对齐的话,DMA会一直报总线错误。还有,所有权位的切换时机一定要严格——CPU写完描述符后,所有权交给DMA;DMA处理完,所有权还给CPU。谁抢谁出问题。

中断处理机制——别让CPU累死

中断处理,核心原则是"快进快出"。中断服务程序(ISR)里只做最必要的事,繁重工作交给任务上下文。

典型的中断事件:

  • 接收中断(RI):有数据包到达
  • 发送中断(TI):数据包发送完成
  • 错误中断(ERI/ETI):收发过程中出现错误
  • 过早中止(ET):传输被异常终止
// 中断服务程序骨架
void MAC_IRQHandler(void) {
    uint32_t status = MAC->SR;  // 读状态寄存器
    
    if (status & MAC_SR_RI) {   // 接收中断
        // 清除中断标志
        MAC->SR = MAC_SR_RI;
        // 通知接收任务(信号量/标志位)
        os_semaphore_post(rx_sem);
    }
    
    if (status & MAC_SR_TI) {   // 发送中断
        MAC->SR = MAC_SR_TI;
        os_semaphore_post(tx_sem);
    }
    
    if (status & MAC_SR_ERI) {  // 接收错误
        MAC->SR = MAC_SR_ERI;
        // 统计错误计数,必要时复位接收DMA
        error_count++;
    }
}

我建议:中断合并(Interrupt Coalescing)在高吞吐场景下很有用。让DMA攒够一定数量的包或等待一段时间再触发中断,能大幅降低CPU负载。但延迟会增大,需要权衡。

数据包收发流程——从应用到线缆

收发流程,我画了张图帮你理解整体脉络。

以太网MAC数据包收发流程 应用层数据 CPU写描述符 DMA搬运数据 PHY PHY DMA写入内存 CPU处理数据 协议栈接收 触发中断 ISR处理 发送:CPU准备→DMA搬运→PHY发送 接收:PHY接收→DMA存储→CPU处理

发送流程其实很简单:

  1. CPU把数据包放到缓冲区,填写描述符(长度、地址、控制位)
  2. CPU把描述符所有权交给DMA(写所有权位)
  3. DMA自动从缓冲区搬运数据到MAC内核,通过MII/RGMII接口发送给PHY
  4. 发送完成后,DMA更新描述符状态,触发中断
  5. CPU在中断中回收描述符,释放缓冲区

接收流程反过来:

  1. PHY收到数据,通过MII接口传给MAC
  2. MAC检查帧完整性(CRC、长度),DMA把数据写入CPU准备好的缓冲区
  3. DMA更新描述符状态,触发接收中断
  4. CPU在中断或任务中处理数据包,然后重新分配缓冲区给DMA

注意:接收描述符必须提前准备好,否则DMA没地方放数据。我见过一个案例,接收描述符用完了,DMA直接丢弃后续所有包,网络看起来就是"断断续续"的。解决方案是:在接收处理中,第一时间补充新的描述符。

PHY芯片管理接口(MDIO/MDC)——让MAC和PHY对话

MDIO(Management Data Input/Output)和MDC(Management Data Clock)是MAC管理PHY的串行总线。说白了,就是MAC通过两根线读写PHY内部的寄存器。

MDIO协议帧格式:

字段 位数 说明
PREAMBLE 32 32个连续的1,同步用
ST 2 起始码,固定为01
OP 2 操作码:01=写,10=读
PHY_ADDR 5 PHY地址(0~31)
REG_ADDR 5 寄存器地址
TA 2 转向时间:读操作时PHY输出Z,然后驱动数据
DATA 16 读写的数据
// MDIO读操作示例(GPIO模拟方式)
uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr) {
    uint16_t data = 0;
    
    // 1. 发送32个1(前导码)
    for (int i = 0; i < 32; i++) {
        MDIO_MDC_LOW();
        MDIO_MDIO_HIGH();
        MDIO_MDC_HIGH();
    }
    
    // 2. 发送起始码+操作码+地址
    uint16_t frame = 0x6200 | (phy_addr << 7) | (reg_addr << 2);
    // 0x6200 = 01 10 00000 00000 00 (ST=01, OP=10, TA=00)
    for (int i = 15; i >= 0; i--) {
        MDIO_MDC_LOW();
        MDIO_MDIO_WRITE((frame >> i) & 1);
        MDIO_MDC_HIGH();
    }
    
    // 3. 转向时间(2个周期,PHY输出数据)
    MDIO_MDC_LOW();
    MDIO_MDIO_INPUT();  // 切换为输入
    MDIO_MDC_HIGH();
    MDIO_MDC_LOW();
    MDIO_MDC_HIGH();
    
    // 4. 读取16位数据
    for (int i = 15; i >= 0; i--) {
        MDIO_MDC_LOW();
        if (MDIO_MDIO_READ()) data |= (1 << i);
        MDIO_MDC_HIGH();
    }
    
    return data;
}

我常用的PHY寄存器:

  • 寄存器0(BMCR):控制寄存器,软复位、设置速度/双工
  • 寄存器1(BMSR):状态寄存器,链接状态、协商完成
  • 寄存器4(ANAR):自动协商通告
  • 寄存器5(ANLPAR):链接伙伴能力

调试时,我习惯先读BMSR看链接是否建立,再读ANLPAR看协商结果。如果链接不稳定,多半是硬件问题——检查网口变压器、PCB走线、阻抗匹配。

总结一下:MAC驱动看似复杂,其实就几个关键点——初始化顺序别搞反、描述符所有权别抢、中断处理要快、MDIO时序要对。把这些搞定了,剩下的就是调优了。我在项目中见过太多"网络不通"的问题,最后发现都是这些基础环节出了岔子。