4. 以太网驱动接口设计
任务目标
在LwIP协议栈移植中,以太网驱动接口是连接底层硬件与协议栈的桥梁。本案例基于STM32F407 + LAN8720A(RMII接口)平台,完成LwIP所需的以太网驱动接口设计,实现数据包从硬件到协议栈的接收、以及从协议栈到硬件的发送通路。
环境依赖
- 硬件:STM32F407开发板 + LAN8720A PHY芯片(RMII模式)
- IDE:Keil MDK-ARM 或 STM32CubeIDE
- 基础代码:STM32 HAL库的ETH驱动(
stm32f4xx_hal_eth.c) - LwIP版本:2.1.2或更高(已配置
NO_SYS=1,即裸机运行)
驱动接口设计
LwIP通过netif结构体与底层驱动交互,核心需要实现4个函数:low_level_init(初始化)、low_level_output(发送)、low_level_input(接收)以及中断处理中的ethernetif_input。以下代码直接使用HAL库的ETH句柄完成DMA描述符管理,无需额外实现MAC寄存器操作。
关键完整代码
/* ethernetif.h */
#ifndef ETHERNETIF_H
#define ETHERNETIF_H
#include "lwip/netif.h"
err_t ethernetif_init(struct netif *netif);
void ethernetif_input(struct netif *netif);
#endif
/* ethernetif.c - 核心驱动接口实现 */
#include "ethernetif.h"
#include "lwip/etharp.h"
#include "stm32f4xx_hal_eth.h"
extern ETH_HandleTypeDef heth;
extern struct netif gnetif;
/* 1. 接口初始化:设置MAC地址、链接回调、底层发送/接收函数 */
err_t ethernetif_init(struct netif *netif)
{
netif->name[0] = 'e';
netif->name[1] = 't';
netif->output = etharp_output;
netif->linkoutput = low_level_output;
netif->mtu = 1500;
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
netif->hwaddr_len = ETH_HWADDR_LEN;
memcpy(netif->hwaddr, heth.Init.MACAddr, ETH_HWADDR_LEN);
netif->state = &heth;
return ERR_OK;
}
/* 2. 底层发送:将pbuf链中的数据写入DMA描述符 */
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
ETH_HandleTypeDef *eth = (ETH_HandleTypeDef *)netif->state;
struct pbuf *q;
uint8_t *buffer = (uint8_t *)(eth->TxDesc->Buffer1Addr);
uint32_t len = 0;
/* 将pbuf链中的数据线性拷贝到DMA发送缓冲区 */
for (q = p; q != NULL; q = q->next) {
memcpy(buffer + len, q->payload, q->len);
len += q->len;
}
/* 填充描述符并请求发送 */
eth->TxDesc->ControlBufferSize = len & ETH_DMATXDESC_FLAG_TDES1;
eth->TxDesc->Status |= ETH_DMATXDESC_TXDESC_OWN;
eth->TxDesc = (ETH_DMADescTypeDef *)(eth->TxDesc->Buffer2Addr); /* 指向下一个描述符 */
/* 触发DMA发送 */
__HAL_ETH_ENABLE_DMA_TX(eth);
return ERR_OK;
}
/* 3. 底层接收:从DMA接收描述符中取出数据并封装为pbuf */
static struct pbuf *low_level_input(struct netif *netif)
{
ETH_HandleTypeDef *eth = (ETH_HandleTypeDef *)netif->state;
struct pbuf *p = NULL;
uint32_t len = 0;
uint8_t *buffer;
/* 检查是否有接收完成的数据帧 */
if ((eth->RxDesc->Status & ETH_DMARXDESC_OWN) != 0) {
return NULL; /* DMA仍拥有描述符,无新数据 */
}
len = ((eth->RxDesc->Status >> ETH_DMARXDESC_FRAME_LENGTH_SHIFT) & ETH_DMARXDESC_FRAME_LENGTH_MASK);
buffer = (uint8_t *)(eth->RxDesc->Buffer1Addr);
/* 分配pbuf并拷贝数据 */
p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
if (p != NULL) {
pbuf_take(p, buffer, len);
}
/* 将描述符归还给DMA */
eth->RxDesc->Status |= ETH_DMARXDESC_RXDESC_OWN;
eth->RxDesc = (ETH_DMADescTypeDef *)(eth->RxDesc->Buffer2Addr);
__HAL_ETH_ENABLE_DMA_RX(eth);
return p;
}
/* 4. 中断入口:在ETH中断回调中调用,将数据递交给协议栈 */
void ethernetif_input(struct netif *netif)
{
struct pbuf *p;
while ((p = low_level_input(netif)) != NULL) {
if (netif->input(p, netif) != ERR_OK) {
pbuf_free(p);
}
}
}
/* 5. HAL库ETH中断回调 - 在stm32f4xx_it.c的ETH_IRQHandler中调用 */
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
ethernetif_input(&gnetif);
}
初始化与使能
/* main.c 中的调用顺序 */
ETH_MACDMAConfig(&heth); /* HAL库ETH参数配置 */
HAL_ETH_Start(&heth); /* 启动DMA传输 */
HAL_ETH_ReadPHYRegister(&heth, PHY_BSR, ®val); /* 检查链接状态 */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernetif_input);
netif_set_default(&gnetif);
netif_set_up(&gnetif);
验证方法
- 编译烧录:将代码编译后烧录至STM32F407,确保PHY芯片LAN8720A的时钟(50MHz)与复位引脚配置正确。
- 链接检测:用网线连接开发板与PC,观察PHY链接状态寄存器(寄存器1的bit2)是否为1,同时开发板LED(如有)应指示链接建立。
- Ping测试:在PC命令行执行
ping 192.168.1.10(假设开发板IP为此地址)。若收到回复,说明发送与接收通路均正常。 - 判定标准:ping通即表示驱动接口设计正确——数据包从PC到达PHY后,经DMA接收描述符进入
low_level_input,再通过netif->input进入LwIP协议栈;回包则经low_level_output写入DMA发送描述符发出。
常见问题
- ping不通且链接灯不亮:检查PHY的RMII接口时钟(REF_CLK)是否由外部50MHz晶振提供,以及PHY地址(LAN8720A通常为0)是否与HAL配置一致。
- 能收到包但无法发送:确认
low_level_output中描述符的Buffer2Addr是否正确指向下一个描述符,否则DMA会反复使用同一描述符导致发送停滞。 - 接收丢包:检查
low_level_input中是否在拷贝数据后立即将描述符归还DMA,若归还过晚,DMA无空闲描述符时会丢弃新到达的数据帧。