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收发,用回环模式验证。最后再挂接协议栈,进行实际网络通信。一步一步来,别想一口吃成胖子。
下一章,咱们聊聊数据链路层的封装和解封装,看看以太网帧到底是怎么在协议栈里流转的。