news 2026/10/21 13:05:15

STM32 HAL库驱动LAN8720A:裸机LWIP移植与ping通实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 HAL库驱动LAN8720A:裸机LWIP移植与ping通实战

简介:这份资源面向使用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 大包」这五步,少一步都可能在上线后炸出莫名其妙的问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 13:05:03

Vim办公提效配置实战:键盘流让文本处理快人一步

我每天有大量时间泡在编辑器里&#xff0c;除了写代码&#xff0c;还要改配置、写文档、处理数据、批量改文本。之前的流程是鼠标高效复制粘贴&#xff0c;后来发现真正拖慢效率的是手在键盘和鼠标之间来回切换&#xff0c;每天几百次&#xff0c;肩膀和手腕都受不了。于是我开…

作者头像 李华
网站建设 2026/10/21 13:04:39

误差反向传播法(BP算法)详解:从梯度推导到代码实践

误差反向传播法&#xff0c;很多人也叫它 BP 算法&#xff0c;是深度学习里最基础也最容易劝退的一块内容。我第一次学的时候&#xff0c;公式每个字符都认识&#xff0c;连起来就不知道在算什么。后来自己动手做了推导、写了代码、真的跑了几次模型&#xff0c;才把这条链路彻…

作者头像 李华
网站建设 2026/10/21 13:02:32

从零构建命令行效率工具 REA:剪贴板历史、提醒与本地笔记的实践指南

我最早注意到“REA”这三个字母&#xff0c;不是因为什么热门开源项目&#xff0c;而是因为我自己电脑里的脚本乱象已经到了忍无可忍的地步。前前后后我写了四五个临时脚本&#xff0c;有的负责从剪贴板里抽链接&#xff0c;有的负责定时弹窗提醒喝水&#xff0c;有的只是把文件…

作者头像 李华
网站建设 2026/10/21 13:01:04

嵌入式Linux开机黑屏终结者:psplash源码深度解析与实战改造

简介&#xff1a;psplash源码是一份面向嵌入式Linux开发者的开源启动画面工具包&#xff0c;专为ARM架构设备设计&#xff0c;可在系统启动或更新阶段接管屏幕&#xff0c;显示带进度条与动画效果的图形界面&#xff0c;常见于树莓派、开发板等平台。资源包共79个文件&#xff…

作者头像 李华
网站建设 2026/10/21 12:57:56

从技能列表到技能系统:能力盘点与项目制学习实战指南

去年年底我给自己做年度复盘&#xff0c;翻出一直在更新的技能清单&#xff0c;发现里面列了二十多项“我会的东西”&#xff1a;Python、剪辑、SQL、会议主持、PPT设计、篮球、吉他……每一项背后都有理由&#xff0c;每一项又都说不清到底到了什么水平。更尴尬的是&#xff0…

作者头像 李华
网站建设 2026/10/21 12:56:29

STM32驱动74HC595级联原理与实现:3个GPIO扩展N×8路输出

简介&#xff1a;这是一份面向STM32开发者的74HC595移位寄存器可级联驱动代码包&#xff0c;适用于需要通过串入并出移位寄存器扩展GPIO、驱动数码管或LED矩阵的常见场景&#xff0c;也适合正在学习Keil MDK下外设驱动开发的初学者。压缩包整体仅1KB&#xff0c;共包含2个文件&…

作者头像 李华