MAC层调优:DMA描述符环与数据包收发路径

各位好,欢迎来到第三章。今天咱们聊聊MAC层最核心的两个东西:DMA描述符环和数据包收发路径。

说实话,很多做U-Boot移植的工程师,往往把MAC驱动调通就跑,很少有人会去深挖DMA描述符环的细节。但我个人习惯是,只要板子网络性能不达标,第一件事就是打开描述符环的配置看一眼。为什么?因为这里藏着太多性能瓶颈了。

3.1 DMA描述符环:收发数据的“传送带”

先说说DMA描述符环是什么。说白了,它就是一块内存区域,里面放了一串描述符。每个描述符指向一个数据缓冲区,告诉DMA控制器:“数据从这里拿”或者“数据放这里”。

我见过不少新手,以为描述符环就是个简单的数组。其实不然。它更像一条环形传送带——数据包从网口进来,DMA自动把数据填到描述符指向的缓冲区,然后CPU去处理。处理完再把描述符还给DMA,循环使用。

核心要点:描述符环的大小、对齐方式、缓存一致性策略,直接决定了收发包的吞吐量和延迟。

3.1.1 描述符结构

不同MAC控制器(比如DesignWare、Freescale的FEC、TI的CPSW)的描述符格式略有差异,但核心字段大同小异。我以最常见的DesignWare GMAC为例:

struct dw_desc {
    u32 des0;   // 控制/状态字:数据长度、错误标志、OWN位
    u32 des1;   // 扩展状态/控制:校验和、时间戳
    u32 des2;   // 缓冲区地址(低32位)
    u32 des3;   // 下一个描述符地址 / 缓冲区地址(高32位)
};

这里最关键的字段是OWN位(在des0的最高位)。OWN位决定了描述符归谁管:

  • OWN=1:描述符归DMA控制器所有,CPU不能碰
  • OWN=0:描述符归CPU所有,DMA不会动它

嗯,这里要注意。很多驱动bug就出在这个OWN位的切换时机上。我曾经调试过一个丢包问题,查了两天才发现是CPU在DMA还没写完数据时就去读了缓冲区——说白了就是OWN位没等DMA清掉就抢了。

3.1.2 环的大小与对齐

描述符环的大小,我建议至少配置64个描述符。太小了容易丢包,太大了浪费内存。对于U-Boot这种轻量级环境,64~128个是比较合理的范围。

对齐要求呢?大多数MAC控制器要求描述符环4字节对齐,有些甚至要求缓存行对齐(32字节或64字节)。你想想看,如果描述符环跨了缓存行,DMA和CPU来回刷缓存,性能直接打对折。

我的经验:分配描述符环时,用cache_aligned_alloc()或者手动做对齐。别偷懒用malloc,万一不对齐,调试起来很痛苦。

3.2 数据包收发路径:从网口到协议栈

理解了描述符环,咱们再来看数据包怎么从网口一路跑到协议栈。我画了一张流程图,把整个路径串起来:

U-Boot MAC层数据包收发路径 U-Boot MAC层数据包收发路径 ① 网口接收数据 ② DMA写入缓冲区 ③ CPU轮询/中断 ④ 解析以太网头 ⑤ 递交协议栈 ① 协议栈构造包 ② CPU填充描述符 ③ 触发DMA发送 ④ DMA读取缓冲区 ⑤ 数据从网口发出 描述符环 OWN位控制权交接 缓存一致性维护

3.2.1 接收路径详解

接收路径上,最耗时的环节其实是CPU轮询描述符。U-Boot默认用轮询模式(polling),因为中断在U-Boot里处理起来太麻烦。但轮询有个问题:CPU一直在空转,浪费电不说,还拖慢了其他任务。

我建议的做法是:在收包函数里加一个超时机制。比如轮询1000次还没收到包,就主动退出,别死等。代码大概长这样:

int eth_rx_poll(struct eth_device *dev, int timeout_ms)
{
    int retry = timeout_ms * 100;  // 假设每次轮询10us
    while (retry--) {
        if (desc_has_data(rx_ring)) {
            process_packet(rx_ring);
            return 0;
        }
        udelay(10);
    }
    return -ETIMEDOUT;
}

注意:轮询间隔不能太短,否则CPU占用率飙升。10~20微秒是比较合理的值。我曾经见过有人写1微秒轮询,结果整个系统卡成幻灯片。

3.2.2 发送路径详解

发送路径相对简单,但有个坑:DMA发送完成通知。有些MAC控制器在DMA发送完成后,会清掉描述符的OWN位。但有些不会,需要CPU去轮询状态寄存器。

我个人习惯是,发送前先检查描述符环里有没有空闲的描述符。如果没有,就等一会儿再试。千万别直接往满的环里塞数据,会覆盖还没发完的包。

int eth_tx_packet(struct eth_device *dev, void *buf, int len)
{
    struct dw_desc *desc = get_tx_desc(tx_index);
    
    // 检查描述符是否空闲
    if (desc->des0 & OWN_BIT) {
        printf("TX ring full, dropping packet\n");
        return -1;
    }
    
    // 填充描述符
    desc->des2 = (u32)buf;
    desc->des0 = (len & LEN_MASK) | OWN_BIT;
    
    // 触发DMA发送
    writel(POLL_DEMAND, &mac_regs->tx_poll);
    
    return 0;
}

3.3 调优实战:描述符环参数优化

好了,理论讲完了,咱们来点实战的。下面这张表是我在实际项目中总结的描述符环调优参数:

参数 默认值 推荐值 说明
描述符数量 32 64~128 太少丢包,太多浪费内存
缓冲区大小 1518字节 2048字节 留点余量给VLAN标签
描述符对齐 4字节 64字节(缓存行) 避免伪共享问题
轮询间隔 1微秒 10~20微秒 平衡CPU占用和延迟
OWN位检查方式 轮询寄存器 直接读描述符内存 减少寄存器访问开销

避坑指南:我曾经在一个ARM Cortex-A7平台上调U-Boot网络,发现收包速度上不去。查了半天,原来是DMA描述符环分配在DDR里,而DMA和CPU的缓存一致性没处理好。解决方案很简单:在分配描述符环时,用dma_alloc_coherent()确保缓存一致。就这么一行代码,吞吐量翻了一倍。

3.4 缓存一致性问题

说到缓存一致性,这是个绕不开的话题。CPU有缓存(L1/L2),DMA直接读写内存。如果CPU修改了描述符,但数据还在缓存里没写回内存,DMA读到的就是旧数据。反过来,DMA写完了描述符,CPU从缓存读到的也可能是旧数据。

解决办法有两种:

  • 使用一致性内存:dma_alloc_coherent()分配的内存,硬件保证缓存一致。简单但性能稍差。
  • 手动刷缓存:在切换OWN位前后,调用flush_dcache_range()和invalidate_dcache_range()。性能好,但代码复杂。

我个人偏向第二种。为什么?因为U-Boot里网络流量不大,手动刷缓存的开销可以忽略,而且能更精细地控制一致性。但如果你用的是多核CPU,或者DMA和CPU频繁交互,那还是用一致性内存更稳妥。

核心原则:OWN位切换前,CPU必须确保描述符数据已经写回内存;OWN位切换后,CPU必须从内存重新读取描述符,不能使用缓存中的旧副本。

3.5 小结

这一章咱们把DMA描述符环和数据包收发路径拆开揉碎了讲了一遍。你想想看,其实MAC层的调优,本质上就是在做三件事:描述符环的配置、OWN位的正确切换、缓存一致性的维护。这三件事做好了,网络性能基本不会差。