简介:这份资源面向使用STM32F429IGT6进行以太网开发的嵌入式工程师与学习者,聚焦HAL库环境下LAN8720A PHY芯片的驱动与LWIP协议栈移植,解决无操作系统条件下实现网络PING通信的问题。工程基于STM32CubeMX 6.6.1与HAL V1.27.1生成,LWIP版本为2.1.2,PHY通过RMII接口与ETH外设传输数据,并由SMI接口完成寄存器读写配置,适合具备一定STM32与C语言基础、希望打通裸机网络通信链路的开发者参考。压缩包共约1300个文件,以678个c源文件与450个h头文件为主体,另含78个汇编文件、46个链接脚本、若干库文件与工程配置文档,整体约31.16MB,目录结构完整,便于直接编译与二次修改。目前已有1989人学习下载。读者可从中获得可运行的MDK工程、LAN8720A初始化与寄存器配置代码、LWIP裸机移植框架及PING联调思路,快速定位PHY地址、时钟与RMII引脚配置等常见问题。
1. 从一块 ping 不通的板子说起:STM32 HAL 库驱动 LAN8720A 跑通 LWIP 到底难在哪
手头有一块 STM32 板子,PHY 用的是 LAN8720A,想让它插上网线就能被电脑 ping 通,结果串口打印一堆超时、LWIP 初始化卡死、PHY 地址读出来是 0xFFFF——这个场景我见过太多次。这份资源就是围绕「STM32 + HAL 库 + LAN8720A + LWIP + 无操作系统 + PING」这条链路,把裸机环境下以太网通信从零跑通的完整工程。它解决的不是「以太网协议是什么」这种教科书问题,而是 RMII 时钟怎么配、PHY 地址怎么扫、LWIP 的lwipopts.h里哪几个宏不改就永远 ping 不通、MX_LWIP_Init()之后到底该在主循环里干什么。适合已经能点灯、能跑串口,但一碰以太网就翻车的嵌入式 C 开发者,也适合想把裸机网络栈吃透、不想上 FreeRTOS 的熟手。
2. 硬件链路先理清:RMII 模式、PHY 地址与复位引脚的三处硬约束
2.1 为什么 LAN8720A 必须走 RMII 而不是 MII
STM32 的 ETH 外设支持 MII 和 RMII 两种 MAC-PHY 接口。MII 需要 16 根信号线(TX/RX 各 4 位数据 + 时钟 + 控制),RMII 只要 9 根,数据位宽从 4 位降到 2 位,但参考时钟从 25MHz 提到 50MHz。选 RMII 的理由很直接:引脚省一半,PCB 好走线,LAN8720A 本身也是为 RMII 优化的低成本 PHY。代价是 50MHz 参考时钟的来源必须想清楚——LAN8720A 可以自己用 25MHz 晶振经内部 PLL 倍频出 50MHz 从 REF_CLK 引脚输出给 STM32,也可以由 STM32 的 MCO 引脚输出 50MHz 给 PHY。这两种接法决定了时钟初始化代码怎么写,接反了就是「PHY 能读 ID 但收不到包」的玄学现场。
常见做法是让 LAN8720A 外挂 25MHz 晶振,配置成 REF_CLK Output 模式,STM32 侧把 ETH_RMII_REF_CLK 配成输入。如果你板上是 STM32 出时钟,那HAL_RCC_MCOConfig()那几行就不能少,且 MCO 的分频要算准。
2.2 PHY 地址不是猜的,是扫出来的
LAN8720A 的 PHY 地址由 PHYAD0 引脚在上电时锁存决定,接下拉就是 0,接上拉就是 1。很多板子这个引脚悬空或接了 LED,导致地址不确定。与其对着原理图猜,不如写个扫描循环把 0~31 全试一遍,能读到合法 ID 的就是它。
/* 扫描 PHY 地址:LAN8720A 的 ID 寄存器 2/3 固定为 0x0007 / 0xC0F1 */ uint32_t phy_addr = 0; for (uint32_t i = 0; i < 32; i++) { uint32_t id1 = 0, id2 = 0; HAL_ETH_ReadPHYRegister(&heth, i, 2, &id1); /* PHYID1 */ HAL_ETH_ReadPHYRegister(&heth, i, 3, &id2); /* PHYID2 */ if (id1 == 0x0007 && id2 == 0xC0F1) { phy_addr = i; printf("PHY found at addr %lu\r\n", i); break; } }逻辑说明:HAL_ETH_ReadPHYRegister内部走 SMI(MDIO/MDC)时序,读寄存器 2 和 3 拼出 32 位 PHY ID。LAN8720A 的 ID 是固定的,读到就锁定地址。参数上,heth必须是已经HAL_ETH_Init过的句柄,否则 SMI 时钟都没起来,读回来全是 0xFFFF。这一步跑通,后面LAN8720_InitTypeDef里的PHYAddress才有依据。
2.3 复位引脚和时钟使能的顺序坑
LAN8720A 有个 nRST 引脚,低电平复位。有些板子把它直接接 STM32 的 GPIO,有些接 RC 电路自动复位。如果是 GPIO 控制,必须在初始化 ETH 之前先拉低至少 100us 再拉高,然后等 PHY 内部稳定(datasheet 建议 50ms 以上)再去读寄存器。顺序反了,第一次读 ID 大概率失败,但第二次又成功——这种「第一次不行第二次行」的现象,八成就是复位时序没等够。
提示:ETH 外设和 GPIO 的时钟使能要在
HAL_ETH_MspInit()里做,别丢在main()里,否则 CubeMX 重新生成代码会覆盖。
3. LWIP 裸机移植:lwipopts.h里那几个不改就 ping 不通的宏
3.1 无操作系统模式下必须关掉的宏
LWIP 默认是按有 OS 配的,裸机跑第一件事就是把NO_SYS置 1,同时把跟信号量、邮箱相关的宏全关掉。这几个宏不改,编译能过,但运行起来sys_arch相关函数找不到实现,链接阶段就报错;就算你补了空实现,协议栈内部也会因为等不到信号量而卡死。
/* lwipopts.h 裸机关键配置 */ #define NO_SYS 1 /* 无操作系统 */ #define SYS_LIGHTWEIGHT_PROT 0 /* 关掉临界区保护,裸机不需要 */ #define LWIP_NETCONN 0 /* 不用 NETCONN API */ #define LWIP_SOCKET 0 /* 不用 socket API */ #define MEM_ALIGNMENT 4 /* STM32 是 32 位,按 4 字节对齐 */ #define MEM_SIZE (16 * 1024) /* 内存池大小,太小会 malloc 失败 */ #define PBUF_POOL_SIZE 16 /* pbuf 池数量,ping 大包时不够会丢 */逻辑说明:NO_SYS=1是裸机的总开关,它会让 LWIP 走sys_check_timeouts轮询模式而不是线程阻塞。MEM_SIZE和PBUF_POOL_SIZE是最容易踩的两个值——默认值偏小,ping 小包没事,一 ping 1472 字节的大包就丢,现象是「小包通、大包不通」,很多人误以为是硬件问题,其实是内存池爆了。参数上,MEM_SIZE给 16KB 起步,PBUF_POOL_SIZE至少 16,具体看你的 RAM 余量。
3.2 网卡收发的底层对接:ethernetif.c里两个函数
LWIP 不直接碰硬件,它通过netif结构体里的low_level_output和low_level_input跟 STM32 的 ETH 驱动对接。这两个函数是移植的核心,写错了就是「能收不能发」或者「发出去没反应」。
/* 发送:把 pbuf 里的数据搬到 ETH 发送描述符 */ static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint8_t *buffer = (uint8_t *)ETH_TX_BUF; /* 发送缓冲区 */ uint32_t framelen = 0; for (q = p; q != NULL; q = q->next) { memcpy(&buffer[framelen], q->payload, q->len); framelen += q->len; } /* 把 buffer 和长度交给 HAL_ETH_TransmitFrame */ return HAL_ETH_TransmitFrame(&heth, framelen) == HAL_OK ? ERR_OK : ERR_IF; }逻辑说明:pbuf 可能是链式的(多个 pbuf 串起来),所以要用for循环逐段拷贝,不能只拷第一个。framelen累加的是实际帧长,传给HAL_ETH_TransmitFrame后由 ETH 外设自动加前导码和 CRC。参数上,ETH_TX_BUF要放在能被 DMA 访问的内存区(STM32F4/F7 上就是普通 SRAM,H7 上要注意 D2 域和 cache 一致性)。接收方向同理,low_level_input里用pbuf_alloc(PBUF_RAW, len, PBUF_POOL)申请 pbuf,再把 ETH 收到的数据拷进去,最后netif->input()递交给协议栈。
3.3 主循环里到底该放什么
裸机 LWIP 没有线程帮你跑定时器,所有超时、ARP 老化、TCP 重传都靠你手动调sys_check_timeouts()。这个函数不调,ping 的第一包能通,后面就再也不响应了——因为 ARP 表项过期没人清。
/* main 主循环 */ while (1) { ethernetif_input(&gnetif); /* 轮询收包并递交给 LWIP */ sys_check_timeouts(); /* 处理 LWIP 内部定时任务 */ /* 你的其他业务代码 */ }逻辑说明:ethernetif_input内部会调low_level_input检查 ETH 接收描述符有没有新帧,有就往上递。sys_check_timeouts必须在主循环里高频调用,间隔别超过 1ms,否则定时精度不够,TCP 重传会异常。参数上,这两个函数都不阻塞,所以主循环里放别的任务没问题,但别放HAL_Delay这种长阻塞,会把协议栈饿死。
4. 避坑与排查:ping 不通时按这个顺序查
4.1 现象:串口打印 PHY ID 为 0xFFFF
原因:SMI 时序没起来,或者 MDIO/MDC 引脚没配对,或者 PHY 没供电/没复位。解决:先量 MDC 引脚有没有 2.5MHz 左右的时钟(SMI 时钟由 ETH 外设分频得到),没有就查HAL_ETH_Init里的MDCClockDiv参数;有时钟但读不到,查 PHY 供电和 nRST 时序,用示波器看复位后 REF_CLK 有没有 50MHz 输出。
4.2 现象:PHY ID 读到了,但netif_is_link_up一直是 0
原因:自协商没完成,或者网线没插好,或者 PHY 的 BSR 寄存器没轮询。解决:在初始化后加一个循环,读 PHY 寄存器 1(BMSR)的 bit2(Link Status),等它置 1 再继续。LAN8720A 自协商一般 2~3 秒完成,别初始化完立刻判断。
4.3 现象:电脑能 ping 通板子,板子 ping 不通电脑
原因:网关或子网掩码配错,或者 LWIP 的netif没设默认网关。解决:检查IP4_ADDR那几行,确保板子 IP、电脑 IP、网关在同一网段。裸机下没有 DHCP 的话,netif_set_default和netif_set_up都要手动调,少一个都不行。
4.4 现象:小包通、大包丢
原因:PBUF_POOL_SIZE或MEM_SIZE太小,大包需要多个 pbuf 拼接,池子不够就丢。解决:把PBUF_POOL_SIZE加到 16 以上,MEM_SIZE加到 16KB 以上,同时确认ETH_RX_BUF_SIZE和ETH_TX_BUF_SIZE不小于 1524。
4.5 现象:ping 几包后彻底没响应
原因:sys_check_timeouts()没在主循环里调,或者被长阻塞任务饿死。解决:确认主循环里这个函数在跑,且循环周期小于 1ms。如果主循环里有HAL_Delay(100)这种,协议栈定时器直接停摆。
5. 进阶验证:用 Wireshark 抓包确认 ARP 与 ICMP 的完整往返
ping 通了不代表链路没问题,想确认 LWIP 真的按协议规范在跑,最直接的办法是在电脑侧用 Wireshark 抓包,看 ARP 请求/应答和 ICMP 请求/应答的完整往返。正常流程是:板子上电后主动发 ARP 请求问「谁是网关」,网关回 ARP 应答,然后电脑 ping 板子时先发 ARP 问板子 MAC,板子回 ARP,接着才是 ICMP Echo Request,板子回 Echo Reply。如果 Wireshark 里只看到 ARP 请求没有应答,说明板子的low_level_output有问题;如果 ARP 通了但 ICMP 没回,说明ethernetif_input没把包递上去,或者ip4_input里因为校验和/长度问题把包丢了。
我一般会先在low_level_input里加一句打印,把收到的帧类型和长度打出来,确认包确实进了驱动;再在ethernetif_input里打印netif->input的返回值,确认协议栈有没有接收。这两层一卡,问题范围就缩到很小了。
| 抓包现象 | 可能原因 | 排查点 |
|---|---|---|
| 只有 ARP 请求,无应答 | 发送路径断 | low_level_output、TX 描述符、ETH 发送使能 |
| ARP 正常,ICMP 无回复 | 接收路径断或 IP 层丢包 | low_level_input、pbuf 分配、IP 校验和 |
| ICMP 回复但电脑显示超时 | 回复太慢 | sys_check_timeouts调用频率、主循环阻塞 |
| 大包 ICMP 无回复 | 内存池不足 | PBUF_POOL_SIZE、MEM_SIZE |
从那以后我每次移植新的 PHY 或换板子,都强制走一遍「扫 PHY 地址 → 读 BMSR 等 Link → Wireshark 抓 ARP → ping 小包 → ping 大包」这五步,少一步都可能在上线后炸出莫名其妙的问题。希望帮到你。
本文还有配套的精品资源,点击获取