第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 读写也封装起来,不能直接暴露寄存器地址给上层。
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,软件才能读写。
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管理、中断封装串在一起,你看一眼就明白了:
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里区分优先级。我曾经把链路中断和接收中断混在一起处理,结果链路抖动时,接收性能直接掉到零。
- 缓冲区泄漏: 接收描述符的缓冲区,用完一定要及时回收。我见过一个项目,接收描述符的缓冲区只分配不释放,跑了一天内存就耗尽了。
好了,这一章的内容就到这里。硬件抽象层是协议栈移植的基石,你花时间把它做扎实了,后面移植协议栈就是水到渠成的事。