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, &regval); /* 检查链接状态 */

netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernetif_input);
netif_set_default(&gnetif);
netif_set_up(&gnetif);

验证方法

  1. 编译烧录:将代码编译后烧录至STM32F407,确保PHY芯片LAN8720A的时钟(50MHz)与复位引脚配置正确。
  2. 链接检测:用网线连接开发板与PC,观察PHY链接状态寄存器(寄存器1的bit2)是否为1,同时开发板LED(如有)应指示链接建立。
  3. Ping测试:在PC命令行执行ping 192.168.1.10(假设开发板IP为此地址)。若收到回复,说明发送与接收通路均正常。
  4. 判定标准: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无空闲描述符时会丢弃新到达的数据帧。