1. 从零开始:为什么选择STM32+LAN8720这对黄金搭档?

如果你正在捣鼓一个需要联网的嵌入式设备,比如智能家居的网关、工业数据采集器,或者一个小型的网络服务器,那么STM32微控制器搭配LAN8720 PHY芯片的方案,大概率已经进入了你的备选清单。我当年第一次接触这个组合时,心里也犯嘀咕:市面上PHY芯片那么多,为什么偏偏是它俩?自己动手调过几轮之后,我才算真正明白了它的香。

简单来说,STM32内置了以太网MAC控制器,但它只是个“数字大脑”,负责处理TCP/IP协议栈里那些高层的逻辑。它需要一个“翻译官”把数字信号转换成能在网线上跑的模拟信号,这个翻译官就是PHY芯片。LAN8720就是这样一个非常经典且性价比极高的百兆PHY。而RMII接口,则是连接MAC(STM32)和PHY(LAN8720)的一种精简通信协议,它把数据线从MII的16根减少到了4根(2收2发),大大节省了宝贵的单片机引脚资源。对于STM32F4这类资源丰富但引脚仍需精打细算的芯片来说,RMII是绝配。

这套组合的优势太明显了:成本低、电路成熟、资料多。ST官方提供的HAL库和标准外设库对以太网支持得相当完善,而LAN8720作为Microchip(原SMSC)的明星产品,数据手册清晰,应用笔记丰富。这意味着你不需要从晶体管开始造轮子,可以把精力集中在应用逻辑上。但话说回来,从原理图到能稳定Ping通的开发板,中间还是有不少坑要踩的。硬件连接一个疏忽,软件配置一个参数不对,网络指示灯可能就死活不亮。接下来,我就把自己调试过程中总结的实战经验,从硬件到软件,掰开揉碎了跟你讲清楚。

2. 硬件连接:别让错误的连线成为你的第一个“坑”

硬件是软件跑起来的基础,这一步错了,后面代码调出花来也没用。STM32的以太网引脚是复用功能,而LAN8720的配置引脚又需要在复位时锁存,这里面的门道不少。

2.1 核心信号线:RMII接口的“生命线”

RMII接口的信号线比传统的MII少得多,但每一根都至关重要。下面这个表格是我根据STM32F407和LAN8720手册整理的必须连接的引脚对应关系,你可以把它当作接线检查表:

STM32引脚信号名称LAN8720引脚作用与说明
PA1ETH_RMII_REF_CLKXTAL1/CLKIN (Pin 5)50MHz参考时钟。这是RMII的“心跳”。通常由PHY输出给MAC,确保收发同步。
PA7ETH_RMII_CRS_DVCRS_DV/MODE2 (Pin 11)载波侦听/接收数据有效。合一信号,指示信道状态和数据有效性。
PC4ETH_RMII_RXD0RXD0/MODE0 (Pin 8)接收数据位0。
PC5ETH_RMII_RXD1RXD1/MODE1 (Pin 7)接收数据位1。
PG11ETH_RMII_TX_ENTXEN (Pin 16)发送使能。STM32拉高时,表示正在通过TXD线发送数据。
PG13ETH_RMII_TXD0TXD0 (Pin 17)发送数据位0。
PG14ETH_RMII_TXD1TXD1 (Pin 18)发送数据位1。
PA2ETH_MDIOMDIO (Pin 13)管理数据输入输出。用于配置PHY内部寄存器,如速度、双工模式。
PC1ETH_MDCMDC (Pin 14)管理数据时钟。由STM32 MAC产生,最高2.5MHz,用于驱动MDIO通信。
PD3 (自定义)LAN8720_RSTnRST (Pin 12)硬件复位。低电平有效,用于复位LAN8720。

这里有几个极易出错的点,我踩过坑:

  1. REF_CLK时钟方向:最常见的问题。LAN8720默认配置(nINTSEL引脚拉低)是输出模式,即它产生50MHz时钟通过XTAL1引脚给STM32的PA1。所以,是LAN8720的Pin5接到STM32的PA1,而不是反过来。很多新手会在这里搞反。
  2. 引脚复用:STM32的这些引脚必须配置为复用功能AF11(以太网)。在代码初始化时,千万别忘了调用GPIO_PinAFConfig函数,否则引脚就是个普通IO,信号根本出不去。
  3. 电源与去耦:LAN8720的模拟和数字电源(VDD1A, VDD2A, VDDIO)要处理好。每个电源引脚附近都必须放置一个0.1uF的陶瓷电容到地,用于高频去耦。这是保证信号质量、减少丢包的关键,别偷懒。

2.2 关键配置引脚:决定PHY的“性格”

LAN8720有几个引脚在硬件复位(nRST上升沿)时会采样其电平,并锁存为工作模式。这意味着你必须在电路板上通过上下拉电阻固定好它们,软件无法在运行时更改。

  • PHYAD0 (Pin 10):通过一个下拉(到地)或上拉(到VDDIO)电阻设置PHY的SMI地址。如果板子上只有一个PHY,下拉设置为地址0即可。如果你的STM32要管理多颗PHY,就需要用不同地址区分。
  • MODE[2:0] (Pin 11,7,8):这三个引脚共同决定PHY的初始工作模式。最常用的配置是MODE[2:0]=111(全部通过电阻上拉),这将使能自动协商(Auto-Negotiation),让PHY自动和交换机协商出最佳的速度(10M/100M)和双工模式。这是推荐设置,省心。
  • nINTSEL (Pin 15):这个引脚决定上面提到的REF_CLK时钟方向。拉低(接地)时,LAN8720使用25MHz晶振并内部倍频产生50MHz时钟输出给MAC。拉高(接VDDIO)时,LAN8720期待外部输入50MHz时钟。我们通常采用前者,即接低电平。
  • REGOFF (Pin 9):内部1.2V稳压器使能。拉低(接地)时,使能内部LDO,此时VDDCR引脚(Pin 2)可以作为1.2V输出给其他电路使用。拉高时,禁用内部LDO,需要从外部给VDDCR引脚提供1.2V电源。除非有特殊需求,通常接地使能内部稳压器即可。

硬件检查清单:画好原理图后,务必核对:1) RMII数据线是否一一对应;2) REF_CLK方向是否正确;3) 配置引脚(PHYAD0, MODE, nINTSEL, REGOFF)的上下拉电阻是否焊对;4) 每个电源引脚是否有0.1uF去耦电容;5) 25MHz晶振的负载电容(通常20pF左右)是否匹配。

3. 软件驱动:深入理解HAL库背后的配置逻辑

硬件准备妥当,我们就进入了软件世界。原始文章给出的代码是基于标准外设库的,非常底层,能让我们看清每一个寄存器。但现在ST主推HAL库,它封装得更好,但理解其配置项同样重要。我们结合两者来看。

3.1 引脚与时钟初始化:搭建通信的“高速公路”

首先,我们需要开启所有相关的外设时钟,并把GPIO配置为以太网复用功能。这就像给高速公路接通电源并开放入口。

// 示例代码 (基于HAL库思路,非直接编译)
void ETH_GPIO_Config(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 1. 开启GPIO和SYSCFG时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_SYSCFG_CLK_ENABLE();

    // 2. 配置RMII引脚为复用推挽输出,高速模式
    GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7; // PA1, PA2, PA7
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // PC1, PC4, PC5 配置类似...
    GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    // PG11, PG13, PG14 配置类似...
    GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_13 | GPIO_PIN_14;
    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

    // 3. 配置复位引脚为普通输出
    GPIO_InitStruct.Pin = GPIO_PIN_3; // PD3
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

    // 4. 硬件复位LAN8720
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_RESET); // 拉低复位
    HAL_Delay(50); // 保持低电平至少100us,这里给50ms更稳妥
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);   // 释放复位
    HAL_Delay(10); // 等待PHY稳定

    // 5. 选择RMII接口模式(关键!)
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    SYSCFG->PMC |= SYSCFG_PMC_MII_RMII_SEL; // 设置RMII模式
}

关键点:第5步SYSCFG_PMC_MII_RMII_SEL这个操作非常重要,它告诉STM32内部的MAC,我们使用的是RMII接口而不是MII。这个配置必须在以太网外设初始化之前完成,且只需要执行一次。很多同学调不通,就是因为漏了这一步。

3.2 MAC与DMA配置:设定数据交通的“交规”

接下来是重头戏,配置STM32内部的以太网MAC和DMA控制器。这部分配置决定了数据包如何被接收、发送、存储和管理。原始文章中的ETH_MACDMA_Config函数做了很多设置,我们用HAL库的视角来解读其中几个核心项。

// 简化版的ETH初始化结构体配置
ETH_HandleTypeDef heth;
ETH_MACConfigTypeDef MACConf;
ETH_DMAConfigTypeDef DMAConf;

// MAC层配置
MACConf.DuplexMode = ETH_FULLDUPLEX_MODE; // 全双工,通常由自动协商决定
MACConf.Speed = ETH_SPEED_100M;           // 100M速率,通常由自动协商决定
MACConf.ChecksumOffload = ETH_CHECKSUM_OFFLOAD_ENABLE; // **硬件校验和卸载**:强烈建议开启!
MACConf.InterFrameGap = ETH_INTERFRAMEGAP_96BIT; // 帧间间隔

// DMA层配置(关乎性能与稳定性)
DMAConf.DropTCPIPChecksumErrorFrame = ETH_DROPTCPIPCHECKSUMERRORFRAME_ENABLE; // 丢弃校验错误的帧
DMAConf.ReceiveStoreForward = ETH_RECEIVESTOREFORWARD_ENABLE; // **接收存储转发**:必须开启!
DMAConf.TransmitStoreForward = ETH_TRANSMITSTOREFORWARD_ENABLE; // **发送存储转发**:必须开启!
DMAConf.FlushReceivedFrame = ETH_FLUSHRECEIVEDFRAME_DISABLE;
DMAConf.TransmitThresholdControl = ETH_TRANSMITTHRESHOLDCONTROL_64BYTES;
DMAConf.ForwardErrorFrames = ETH_FORWARDERRORFRAMES_DISABLE; // 不转发错误帧

// 将配置应用到HAL句柄
heth.Init.MACInit = &MACConf;
heth.Init.DMAInit = &DMAConf;
heth.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE; // 使能自动协商
heth.Init.PhyAddress = LAN8720_PHY_ADDRESS; // PHY地址,与硬件PHYAD0对应(0或1)

// 调用HAL初始化函数
if (HAL_ETH_Init(&heth) != HAL_OK) {
    Error_Handler();
}

这里我特别想强调**存储转发(Store and Forward)**模式。在缓冲(Cut-Through)模式下,DMA收到帧头就开始转发,延迟低,但无法进行帧校验。而存储转发模式下,DMA会等整个帧都进入缓冲区后,再进行CRC校验等操作,无误后才交给应用程序。这对于可靠性要求高的嵌入式网络应用是必须的。开启硬件校验和卸载(Checksum Offload)能让STM32的MAC硬件计算IP、TCP、UDP的校验和,大大减轻CPU负担。

3.3 PHY寄存器读写与状态获取:与“翻译官”对话

STM32通过SMI(MDC/MDIO)接口管理PHY。我们需要读取PHY的状态寄存器,来了解当前的连接状态、速度和双工模式。LAN8720的状态寄存器(Register 31)包含了这些信息。

// 读取LAN8720连接状态和速度的函数(基于HAL)
uint32_t LAN8720_GetLinkState(ETH_HandleTypeDef *heth) {
    uint32_t phyreg = 0;
    uint32_t linkState = 0;

    // 读取PHY状态寄存器(31)
    if (HAL_ETH_ReadPHYRegister(heth, LAN8720_PHY_ADDRESS, 31, &phyreg) != HAL_OK) {
        return ETH_LINK_UNKNOWN;
    }

    // 检查连接状态位(Bit 2: Link Status)
    if (phyreg & (1 << 2)) {
        linkState |= ETH_LINK_UP;
        // 检查速度位(Bit 1: Speed Status)
        if (phyreg & (1 << 1)) {
            linkState |= ETH_SPEED_100M;
        } else {
            linkState |= ETH_SPEED_10M;
        }
        // 检查双工模式位(Bit 0: Duplex Status)
        if (phyreg & (1 << 0)) {
            linkState |= ETH_DUPLEX_FULL;
        } else {
            linkState |= ETH_DUPLEX_HALF;
        }
    } else {
        linkState = ETH_LINK_DOWN;
    }
    return linkState;
}

在实际项目中,我通常会创建一个任务或者在一个定时器中断里周期性地调用这个函数,打印或处理网络连接状态的变化。当检测到ETH_LINK_DOWN时,可以尝试重新初始化PHY或者给出故障指示。

4. 中断与内存管理:保证数据不丢包的“守护神”

网络数据是随时可能到来的,采用轮询的方式会大量占用CPU。因此,使用中断来接收数据是标准做法。同时,如何高效地管理DMA描述符和缓冲区内存,是影响网络性能的关键。

4.1 接收中断服务函数:数据的“快递通知”

当DMA接收到一个完整的帧后,会触发接收中断。我们需要在中断服务函数(ISR)里快速地将数据从以太网DMA的缓冲区搬运到协议栈(如LWIP)的缓冲区中。

// 以太网全局中断服务函数
void ETH_IRQHandler(void) {
    HAL_ETH_IRQHandler(&heth); // 调用HAL库的中断处理函数
}

// HAL库的中断回调函数(你在主程序中重写它)
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) {
    // 1. 获取接收到的帧长度和缓冲区地址
    // 2. 将数据传递给LWIP的底层输入函数,例如:
    //    ethernetif_input(&gnetif);
    // 3. 通知操作系统(如果用了RTOS)有网络事件发生
    //    osSemaphoreRelease(s_xSemaphore);
}

在裸机环境下,你可以在HAL_ETH_RxCpltCallback中直接调用LWIP的ethernetif_input。如果使用了FreeRTOS等RTOS,更常见的做法是在回调函数中释放一个信号量或发送一个消息给一个专用的网络处理任务,由该任务来执行耗时的数据包处理逻辑,避免在中断中停留过久。

4.2 DMA描述符与缓冲区:规划数据的“临时仓库”

这是驱动中最核心也最容易内存越界的地方。DMA描述符是一个链表结构,每个描述符指向一块物理内存缓冲区(Rx_Buff/Tx_Buff)。DMA硬件不断地在这个链表中循环,将网卡收到的数据放到空闲的Rx缓冲区,或者将待发送的数据从Tx缓冲区送出去。

// 内存分配示例(使用静态数组,更安全)
#define ETH_RX_BUF_SIZE   1524 // 最大帧长度+CRC等
#define ETH_RXBUFNB       4    // 接收描述符数量
#define ETH_TX_BUF_SIZE   1524
#define ETH_TXBUFNB       2    // 发送描述符数量

// 定义在非缓存内存区(如果用了Cache,需注意一致性)
__attribute__((section(".RxDecripSection"))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB];
__attribute__((section(".TxDecripSection"))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB];
__attribute__((section(".RxArraySection"))) uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE];
__attribute__((section(".TxArraySection"))) uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE];

// 在ETH初始化后,需要将这些缓冲区链接到描述符
void ETH_DMA_Desc_Init(ETH_HandleTypeDef *heth) {
    // 初始化接收描述符链表
    for (int i = 0; i < ETH_RXBUFNB; i++) {
        HAL_ETH_DescAssignMemory(&heth, i, Rx_Buff[i], NULL); // 关联缓冲区
        HAL_ETH_PrepareRxDescriptors(&heth, ETH_RX_FRAME_INFERRING); // 准备接收
    }
    // 初始化发送描述符链表类似...
}

几个血泪教训:

  1. 缓冲区对齐:DMA缓冲区地址最好32字节对齐,可以提升存取效率。使用__attribute__((aligned(32)))来修饰缓冲区数组。
  2. 数量与大小:ETH_RXBUFNB不能太小,否则在高流量下很快被填满,导致丢包。4-8个是比较安全的起点。缓冲区大小ETH_RX_BUF_SIZE必须大于最大传输单元(MTU,通常1500字节),加上帧头等,1524字节是常用值。
  3. 内存区域:确保描述符和缓冲区所在的物理内存区域可以被以太网DMA访问(即位于总线地址空间)。使用链接脚本指定到特定的段(如.RxDecripSection)是个好习惯。
  4. Cache一致性:如果CPU有Cache(如STM32H7),而DMA直接访问内存,就会存在Cache一致性问题。你必须在使用前清理(Clean) 发送缓冲区Cache,在读取后无效化(Invalidate) 接收缓冲区Cache,或者将这部分内存配置为非缓存(Non-Cacheable)。

5. 实战调试与性能优化:从“通”到“稳”的进阶

当你的开发板第一次Ping通时,喜悦是短暂的。接下来要让它在各种情况下稳定工作,并优化其性能。

5.1 调试技巧:当网络指示灯不亮时

  1. 检查时钟:这是头号嫌疑犯。用示波器测量LAN8720的XTAL1引脚(或STM32的PA1),看是否有稳定的50MHz方波。没有时钟,一切免谈。
  2. 检查复位和配置引脚电平:用万用表测量nRST、PHYAD0、MODE等引脚的电平,确保在复位释放瞬间符合你的设计预期(上拉还是下拉)。
  3. 软件读取PHY ID:每个PHY芯片都有唯一的ID寄存器。在初始化后,尝试读取LAN8720的PHY标识寄存器(Reg 2和3)。如果能正确读出0x0007C0F1(LAN8720的ID),说明SMI管理接口(MDC/MDIO)通信正常。这是验证软件配置和硬件连接的第一步。
  4. 监听Link状态:周期性读取PHY的基本状态寄存器(Reg 1),查看Link Status位。如果一直为0,检查网线、对端设备(交换机/路由器)、以及LAN8720的自动协商是否成功。
  5. 利用环回测试:配置MAC进入内部环回模式,自己发数据给自己收。这可以排除外部PHY和线路的问题,聚焦于STM32 MAC和驱动软件本身。

5.2 性能优化:让百兆网口跑满速

  1. 中断优化:确保网络接收中断的优先级设置合理,不能太低导致数据包堆积,也不能太高影响系统关键任务。在RTOS中,中断服务例程(ISR)只做最少的搬运工作,复杂的协议解析交给任务。
  2. DMA描述符深度与缓冲区大小:增加接收描述符数量(ETH_RXBUFNB)可以有效应对网络流量突发。对于需要大量发送的应用,适当增加发送描述符数量(ETH_TXBUFNB)也有好处。
  3. 启用硬件加速:务必开启MAC的硬件校验和卸载(Checksum Offload) 和接收所有广播帧(BroadcastFramesReception) 功能。前者极大减轻CPU负担,后者对于接收ARP等广播包是必要的。
  4. 优化内存拷贝:在ETH_Rx_Packet这样的函数中,将数据从DMA缓冲区拷贝到协议栈时,使用memcpy的32位版本或DMA进行拷贝,会比单字节拷贝快得多。
  5. 协议栈调优:如果你使用LWIP,调整其内存池(MEMP_NUM_PBUF, MEMP_NUM_TCP_PCB等)参数,使其与你的驱动层缓冲区大小和数量相匹配,避免内存分配失败。

调试STM32的以太网驱动,尤其是第一次,确实会碰到各种稀奇古怪的问题。但只要你按照“时钟->复位与配置->SMI通信->Link状态->数据收发”这个顺序一步步排查,同时善用示波器、逻辑分析仪和调试打印信息,绝大多数问题都能被定位和解决。记住,稳定的硬件是基础,正确的配置是关键,而深入理解数据流(从网线到DMA缓冲区再到应用层)则是你解决复杂问题的终极武器。当你看到自己的设备稳定地跑在网络上,持续传输数据时,那种成就感就是对之前所有调试工作的最好回报。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐