4、网络接口层移植:MAC层驱动适配、PHY芯片初始化、MDIO总线操作、网络中断处理

好,咱们进入第四讲。这一章是协议栈移植里最「硬」的部分——网络接口层。说白了,就是让MCU的以太网外设真正跑起来,能和外面的网线对上话。

我刚开始做这块的时候,总觉得不就是配几个寄存器嘛,结果一调就是三天。后来发现,MAC层、PHY芯片、MDIO总线、中断处理,这四个东西环环相扣,一个没弄好,整个网络就趴窝。咱们一个一个来拆。

4.1 MAC层驱动适配——让硬件开始工作

MAC层,全称Media Access Control,是MCU内部集成的以太网控制器。不同厂商的MAC差异很大,但核心要做的事差不多:初始化DMA描述符、配置MAC地址、设置帧过滤规则。

我个人习惯,先把MAC的寄存器映射表搞清楚。比如STM32的ETH模块,它的DMA描述符是链式的,你得在内存里建一个描述符链表,每个描述符指向一个数据缓冲区。这个链表一旦建好,数据收发就全靠硬件自动完成了。

核心要点:MAC层驱动适配,本质上是把协议栈的底层接口函数,挂接到你的硬件寄存器操作上。

举个例子,lwIP协议栈里有个叫low_level_init的函数,你得在里面完成:

  • 使能MAC时钟和GPIO时钟
  • 复位MAC控制器
  • 配置MAC地址寄存器
  • 初始化DMA描述符环
  • 设置接收和发送描述符数量(我一般各设4个,够用)

我在项目中遇到过一个问题:DMA描述符的地址没对齐。ARM Cortex-M系列要求描述符地址按4字节对齐,我当时偷懒没检查,结果数据收发总是丢包。嗯,这种坑踩一次就记住了。

4.2 PHY芯片初始化——让物理层活过来

PHY芯片,就是那个负责把数字信号转成模拟信号的小家伙。常见的型号有LAN8720、DP83848、RTL8201等。PHY芯片的初始化,说白了就是通过MDIO总线给它写寄存器,让它进入工作状态。

PHY初始化通常包括:

  • 软件复位(写寄存器0的bit15)
  • 配置自动协商(Auto-Negotiation)
  • 等待链接建立(Link Up)
  • 读取PHY ID确认芯片型号

小技巧:PHY芯片的复位时序很重要。有些芯片需要先拉低复位引脚至少10ms,再拉高。我见过有人只等了1ms,结果PHY死活不工作。

这里有个关键点:PHY芯片的地址。一般通过硬件引脚配置,常见地址是0x00或0x01。你写驱动时最好先扫描一下总线,确认PHY地址对不对。我曾经因为原理图和实际板子对不上,PHY地址写错了,调了一整天。

自动协商这块,我建议开启。它能让PHY自动匹配对端设备的速度和双工模式。但要注意,有些老旧交换机不支持自动协商,你得手动强制设置成10M半双工。嗯,这种场景在工业现场还挺常见的。

4.3 MDIO总线操作——管理PHY的命脉

MDIO,全称Management Data Input/Output,是MAC和PHY之间的管理接口。它只有两根线:MDC(时钟)和MDIO(数据)。通过它,你可以读写PHY芯片内部的所有寄存器。

MDIO的时序其实很简单,但实现起来有讲究。标准MDIO协议规定:

  • 先发送32个1作为前导码(Preamble)
  • 然后发送起始码(01)
  • 接着是操作码(读为10,写为01)
  • 然后是PHY地址(5位)和寄存器地址(5位)
  • 最后是数据(16位)

我一般会封装两个函数:phy_read(phy_addr, reg_addr)phy_write(phy_addr, reg_addr, data)。这样上层代码就干净多了。

注意:MDIO操作有时会失败,尤其是PHY芯片正在复位或链接状态变化时。我建议加一个超时重试机制,比如连续读3次,取多数值。

有些MCU的MAC外设自带MDIO控制器,你只需要配置时钟分频和操作模式就行。但有些低端MCU没有硬件MDIO,你得用GPIO模拟。模拟的时候要注意时序,MDC频率不能超过2.5MHz,否则PHY芯片可能不响应。

我记得有一次,客户反映板子偶尔连不上网。排查了半天,发现是MDIO总线上有干扰,导致PHY寄存器读写出错。后来在MDIO线上加了个上拉电阻,问题就解决了。这种硬件问题,软件再怎么写也绕不过去。

4.4 网络中断处理——别让CPU累死

网络数据收发,全靠中断驱动。MAC收到一个包,触发接收中断;发送完成,触发发送完成中断。中断处理函数里,你要做的事很明确:把数据从硬件缓冲区搬到协议栈的缓冲区。

中断处理的几个原则:

  • 快进快出:中断里只做最必要的事,比如复制数据、清中断标志。复杂的协议处理放到任务级去做。
  • 避免阻塞:不要在中断里调用延时函数或等待信号量。
  • 注意优先级:网络中断的优先级不能太低,否则高优先级中断会频繁打断网络收发,导致丢包。

以lwIP为例,接收中断的处理流程一般是:

void ETH_IRQHandler(void)
{
    // 检查中断源
    if (ETH_GetFlagStatus(ETH_FLAG_RX) == SET)
    {
        // 清除中断标志
        ETH_ClearFlag(ETH_FLAG_RX);
        
        // 通知协议栈有数据到达
        tcpip_input(pbuf, netif);
    }
    
    // 发送完成中断
    if (ETH_GetFlagStatus(ETH_FLAG_TX) == SET)
    {
        ETH_ClearFlag(ETH_FLAG_TX);
        // 释放已发送的pbuf
    }
}

这里有个细节:接收中断里调用了tcpip_input,这个函数会把数据包交给lwIP的tcpip线程去处理。这样中断函数就能快速返回,不会影响其他中断的响应。

避坑指南:我曾经在中断里直接调用了pbuf_free来释放内存,结果因为优先级问题导致了死锁。后来改成用消息队列把释放操作放到任务里做,问题就解决了。

发送中断的处理相对简单。你只需要检查发送描述符的状态,如果发送完成,就把描述符标记为空闲,然后释放对应的数据缓冲区。但要注意,有些MAC控制器支持多个发送描述符,你得循环检查,直到所有发送都完成。

嗯,网络中断处理这块,说白了就是「别让CPU闲着,也别让CPU累死」。合理的中断设计,能让你的网络吞吐量提升30%以上。

小结

这一章的内容比较杂,但都是实打实的硬功夫。MAC层驱动适配是基础,PHY芯片初始化是前提,MDIO总线操作是工具,网络中断处理是效率。四者缺一不可。

我个人建议,调试的时候先搞定PHY芯片的初始化,确保能通过MDIO正常读写寄存器。然后测试MAC层的DMA收发,用回环模式验证。最后再挂接协议栈,进行实际网络通信。一步一步来,别想一口吃成胖子。

下一章,咱们聊聊数据链路层的封装和解封装,看看以太网帧到底是怎么在协议栈里流转的。