以太网MAC驱动:从控制器初始化到数据收发
以太网MAC驱动,说白了就是让CPU和物理层(PHY)之间能顺畅地收发数据包。我做了这么多年嵌入式网络驱动,MAC这块往往是整个系统稳定性的关键。今天咱们就把它拆开揉碎了讲清楚。
核心要点:MAC驱动负责数据链路层的帧封装/解封装、流量控制、以及通过MDIO总线管理PHY芯片。DMA描述符的设计直接影响收发性能。
MAC控制器初始化——第一步不能错
初始化MAC控制器,我习惯按这个顺序来。顺序错了,后面怎么调都调不通。
- 软复位:写复位寄存器,等待自清除。我遇到过有些芯片复位需要轮询超时,别死等。
- 配置工作模式:全双工/半双工、100M/1000M、流控使能。这些参数要和PHY协商结果一致。
- 设置MAC地址:6字节地址写入地址寄存器。注意字节序,有些芯片是大端。
- 配置帧过滤:广播帧、多播帧、混杂模式。调试阶段我通常先开混杂模式。
- 使能收发:最后才打开收发使能位。
// 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负载。但延迟会增大,需要权衡。
数据包收发流程——从应用到线缆
收发流程,我画了张图帮你理解整体脉络。
发送流程其实很简单:
- CPU把数据包放到缓冲区,填写描述符(长度、地址、控制位)
- CPU把描述符所有权交给DMA(写所有权位)
- DMA自动从缓冲区搬运数据到MAC内核,通过MII/RGMII接口发送给PHY
- 发送完成后,DMA更新描述符状态,触发中断
- CPU在中断中回收描述符,释放缓冲区
接收流程反过来:
- PHY收到数据,通过MII接口传给MAC
- MAC检查帧完整性(CRC、长度),DMA把数据写入CPU准备好的缓冲区
- DMA更新描述符状态,触发接收中断
- 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时序要对。把这些搞定了,剩下的就是调优了。我在项目中见过太多"网络不通"的问题,最后发现都是这些基础环节出了岔子。