第2章 硬件抽象层设计:MAC/PHY芯片接口抽象、DMA描述符管理、中断处理机制封装

说实话,很多做协议栈移植的朋友,上来就啃协议栈源码,结果卡在最底层——硬件跑不通。我早年也踩过这个坑,调了三天发现是DMA描述符的地址对齐没处理好。所以这一章,咱们先把硬件抽象层(HAL)的底子打牢。

硬件抽象层,说白了就是给上层协议栈一个统一的接口,让它不用关心底下是哪个厂家的MAC、哪种PHY芯片。你想想看,今天用STM32+LAN8720,明天换成NXP+DP83848,如果代码里到处是寄存器操作,那移植起来得多痛苦?

核心原则: 硬件抽象层要做到“上层无感切换”。上层调用的函数名、参数、行为完全一致,底层实现可以随时替换。

2.1 MAC/PHY芯片接口抽象

MAC和PHY是网口的两大核心器件。MAC负责数据链路层,PHY负责物理层。它们之间通常通过MII/RMII/GMII/RGMII等接口连接。我习惯把接口抽象分成三层:

  • PHY驱动层: 负责PHY芯片的复位、初始化、链路状态检测、速率/双工模式配置。
  • MAC驱动层: 负责MAC内核的初始化、MAC地址设置、帧过滤配置、流控使能。
  • DMA管理层: 负责描述符的分配、初始化、收发调度。

来看一个典型的PHY接口抽象结构:

/* phy_driver.h */
typedef struct {
    uint8_t  phy_addr;       /* PHY地址 */
    uint8_t  phy_id_hi;      /* PHY厂商ID高字节 */
    uint8_t  phy_id_lo;      /* PHY厂商ID低字节 */
    
    /* 标准操作接口 */
    int32_t (*init)(void);                    /* 初始化PHY */
    int32_t (*reset)(void);                   /* 复位PHY */
    int32_t (*get_link_status)(uint8_t *up);  /* 获取链路状态 */
    int32_t (*get_speed_duplex)(uint8_t *speed, uint8_t *duplex);
    int32_t (*auto_negotiate)(void);          /* 启动自协商 */
} phy_driver_t;

我在项目中遇到过一个问题:不同PHY芯片的寄存器地址映射不一样。有的PHY用 Clause 22 格式,有的用 Clause 45。所以抽象层里一定要把 MDIO 读写也封装起来,不能直接暴露寄存器地址给上层。

我的习惯: 每个PHY芯片单独写一个 .c/.h 文件,实现 phy_driver_t 结构体里的函数指针。主控代码里只维护一个 phy_driver_t 的实例指针,切换芯片时改一下指针指向就行。

2.2 DMA描述符管理

DMA描述符,是MAC和内存之间的“快递单”。每个描述符记录了一个数据缓冲区的地址、长度、状态标志。以太网DMA通常使用描述符环(Descriptor Ring),收发各一个环。

嗯,这里要注意:描述符的结构和布局,不同芯片差异很大。但抽象层可以统一成下面这种形式:

/* dma_desc.h */
typedef struct {
    uint32_t buf_addr;       /* 数据缓冲区物理地址 */
    uint16_t buf_len;        /* 缓冲区长度 */
    uint16_t flags;          /* 状态标志:OWN、EOP、FS等 */
    uint32_t next_desc;      /* 下一个描述符的物理地址(链式结构) */
} dma_desc_t;

/* 描述符环管理结构 */
typedef struct {
    dma_desc_t *desc_ring;   /* 描述符环基地址 */
    uint32_t    ring_size;   /* 描述符数量 */
    uint32_t    prod_idx;    /* 生产者索引(DMA硬件使用) */
    uint32_t    cons_idx;    /* 消费者索引(软件使用) */
    uint8_t    *buf_pool;    /* 数据缓冲区池 */
} dma_ring_t;

描述符管理有几个关键点,我吃过亏的地方:

  • 地址对齐: 描述符本身和缓冲区地址,通常要求4字节或8字节对齐。有些芯片要求描述符环必须放在特定的内存区域(如非Cache区域)。
  • Cache一致性: 如果CPU和DMA共享内存,一定要处理好Cache刷新。我当年在Cortex-A7上移植时,忘了刷Cache,结果收上来的数据全是乱码。
  • 描述符所有权: 硬件和软件通过OWN位来交接描述符。硬件置位OWN表示描述符归硬件所有,软件不能动;硬件处理完后清除OWN,软件才能读写。
我曾经踩过的坑: 描述符环的初始化顺序。一定要先初始化描述符,再启动DMA。如果顺序反了,DMA可能直接访问未初始化的描述符,导致总线错误。另外,描述符环的最后一个描述符要指向环首,形成闭环。

2.3 中断处理机制封装

以太网中断类型很多:接收完成、发送完成、链路状态变化、错误中断等。我建议把中断处理也抽象成回调机制,这样协议栈只需要注册自己关心的中断事件。

来看一个中断抽象的设计:

/* eth_intr.h */
/* 中断事件类型 */
typedef enum {
    ETH_INTR_RX_COMPLETE = 0x01,   /* 接收完成 */
    ETH_INTR_TX_COMPLETE = 0x02,   /* 发送完成 */
    ETH_INTR_LINK_CHANGE = 0x04,   /* 链路状态变化 */
    ETH_INTR_ERROR       = 0x08,   /* 错误中断 */
    ETH_INTR_ALL         = 0x0F    /* 所有中断 */
} eth_intr_type_t;

/* 中断回调函数类型 */
typedef void (*eth_intr_callback_t)(void *arg);

/* 注册中断回调 */
int32_t eth_intr_register(eth_intr_type_t type, 
                          eth_intr_callback_t cb, 
                          void *arg);

/* 中断处理入口(在ISR中调用) */
void eth_intr_handler(void);

中断处理函数里,我一般只做最轻量级的工作:读取中断状态寄存器,清除中断标志,然后通过回调通知上层。真正的数据搬移和协议处理,放到任务或工作队列里去做。为什么?因为中断上下文里不能调用可能引起阻塞的函数,比如内存分配、信号量等待。

我的经验: 中断回调里用“软中断”或“任务通知”机制,把数据交给优先级较低的任务去处理。这样既能保证实时性,又不会拖慢中断响应。

2.4 整体架构图

下面这张图,把MAC/PHY抽象、DMA管理、中断封装串在一起,你看一眼就明白了:

硬件抽象层整体架构 TCP/IP 协议栈(上层) 硬件抽象层(HAL)统一接口 MAC/PHY 接口抽象 MII/RMII/寄存器封装 DMA 描述符管理 描述符环/Cache一致性 中断封装 回调/软中断 具体硬件实现(STM32 MAC / LAN8720 / DP83848 / DMA 控制器) 说明:上层协议栈只与HAL接口交互,不直接操作硬件寄存器。 切换硬件平台时,只需替换最底层的具体实现,上层代码无需修改。

2.5 接口抽象对比表

不同厂家的MAC/PHY,寄存器布局和操作方式差异很大。我整理了一张对比表,方便你理解抽象的必要性:

功能项 STM32 F4/F7/H7 NXP i.MX RT TI AM335x
PHY接口 MII/RMII MII/RMII/RGMII MII/RMII/RGMII
DMA描述符格式 增强型描述符(8个Word) 简单描述符(4个Word) 三级描述符(Buffer/BD/CPPI)
描述符对齐要求 4字节对齐 8字节对齐 32字节对齐
中断向量 ETH_IRQHandler ENET_IRQHandler CPDMA_IRQHandler
Cache处理 SCB_CleanInvalidateDCache DCACHE_OP L1D_CACHE_INV

你看,光是描述符对齐要求就从4字节到32字节不等。如果没有抽象层,协议栈里到处是 #ifdef STM32 / #ifdef NXP,那代码就没法看了。

2.6 避坑指南

最后,分享几个我实际项目中遇到的坑:

  • PHY复位时序: 有些PHY芯片复位后需要等待一段时间才能访问寄存器。我遇到过复位后立刻读ID,结果读到0xFFFF的情况。加个10ms延时就好了。
  • 描述符环大小: 不是越大越好。描述符太多会占用大量内存,太少又容易丢包。我一般根据网络吞吐量和CPU频率来算,比如100Mbps下,每个描述符1.5KB,环大小设64~128个。
  • 中断嵌套: 如果MAC支持多个中断源,一定要在ISR里区分优先级。我曾经把链路中断和接收中断混在一起处理,结果链路抖动时,接收性能直接掉到零。
  • 缓冲区泄漏: 接收描述符的缓冲区,用完一定要及时回收。我见过一个项目,接收描述符的缓冲区只分配不释放,跑了一天内存就耗尽了。
重要提醒: 硬件抽象层的代码,一定要在目标硬件上逐个接口验证。不要以为仿真通过就万事大吉。我吃过这个亏——仿真环境里Cache一致性没问题,真机上一跑就崩。

好了,这一章的内容就到这里。硬件抽象层是协议栈移植的基石,你花时间把它做扎实了,后面移植协议栈就是水到渠成的事。