简介:本资源面向使用STM32F429IGT6进行以太网开发的嵌入式工程师与学习者,聚焦HAL库下LAN8720A PHY芯片的驱动与LWIP协议栈移植,解决无操作系统环境下网络通信与PING测试的完整实现问题。压缩包共1300个文件,约31.16MB,以678个c源文件与450个h头文件为主体,辅以78个s汇编启动文件、46个icf链接脚本及lib、a等库文件,并包含ioc、uvprojx等工程配置,覆盖从底层驱动到协议栈的完整代码结构。资源基于KEIL MDK_ARM_5.27、STM32CubeMX 6.6.1、HAL V1.27.1与LWIP 2.1.2搭建,清晰展示了LAN8720A通过RMII接口与ETH外设通信、借助SMI接口读写PHY寄存器的实现方式。目前已有1989人学习下载,适合希望掌握STM32以太网裸机开发、理解LWIP移植流程与网络调试排错思路的读者参考借鉴。
1. 从一块“不亮灯”的板子说起:STM32 裸机以太网到底难在哪
手头有一块 STM32 的板子,PHY 用的是 LAN8720A,需求很朴素:不跑操作系统,用 HAL 库把 LWIP 协议栈跑起来,能 ping 通就行。听起来像是入门级任务,但真正动过手的人都知道,从 PHY 地址、RMII 时钟到 LWIP 的ethernetif.c移植,每一步都埋着雷。我见过太多人卡在“网口灯不亮”或者“能收不能发”上,最后不了了之。
这份资源就是围绕这个场景来的:STM32 + HAL 库 + LAN8720A + LWIP + 无操作系统 + PING 通。它解决的不是“以太网协议是什么”这种教科书问题,而是“我照着 CubeMX 配完了,为什么 ping 不通”这种一线问题。适合已经会点 C 语言、用过 STM32 HAL 库、想从串口/GPIO 跨到网络通信的嵌入式开发者。如果你正对着一个不响应的网口发愁,下面的内容应该能帮你把链路一层层打通。
2. 硬件链路与 CubeMX 配置:先把 PHY 和 RMII 钉死
2.1 LAN8720A 的地址与 RMII 时钟,选错一个就全盘皆输
LAN8720A 支持 RMII 接口,硬件上最容易被忽略的是 PHY 地址。LAN8720A 的 PHY 地址由PHYAD0引脚在上电时决定:该引脚悬空或拉高时地址为 1,拉低时为 0。很多开发板为了省事直接悬空,于是地址就是 1。如果你在代码里写PHY_ADDRESS = 0,MDIO 读出来的寄存器全是 0xFFFF,链路永远起不来。
另一个坑是 RMII 的 50MHz 时钟。LAN8720A 有两种时钟输出模式:一种是从外部晶振输入 25MHz,内部 PLL 倍频到 50MHz 从REF_CLK输出给 STM32;另一种是 STM32 的 MCO 引脚输出 50MHz 给 PHY。常见做法是让 LAN8720A 自己产生 50MHz,STM32 端把ETH_RMII_REF_CLK配置为输入。CubeMX 里如果选错了时钟方向,PHY 和 MAC 之间就没有同步时钟,现象是网口灯亮但收不到任何包。
配置时还要注意:RMII 模式下,ETH_MDC和ETH_MDIO必须接对,且 MDC 时钟不能超过 2.5MHz。HAL 库默认分频是ETH_MDC_HCLK_DIV42,在 168MHz 主频下约 4MHz,有些 PHY 会不稳定,改成ETH_MDC_HCLK_DIV62更稳妥。
2.2 CubeMX 里 ETH 外设的参数怎么填
在 CubeMX 的 Connectivity 里选 ETH,Mode 选 RMII,下面几个参数需要手动确认:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| PHY Address | 1 | 对应 PHYAD0 悬空 |
| Auto Negotiation | Enable | 让 PHY 自动协商速率和双工 |
| Speed | 100Mbps | 协商失败时的默认值 |
| Duplex Mode | Full Duplex | 同上 |
| Rx Mode | Interrupt | 裸机下用中断收包 |
| TX/RX Descriptor | 4/4 | 描述符数量,够用即可 |
| Checksum Offload | Disable | 先关掉,减少变量 |
配置完成后生成代码,HAL 会初始化 ETH 外设并调用HAL_ETH_Init。但此时 LWIP 还没接进来,MX_LWIP_Init需要自己补。CubeMX 如果勾选了 LWIP 中间件,会自动生成lwip.c和ethernetif.c,但无操作系统模式下需要把NO_SYS设为 1,并且把LWIP_NETCONN和LWIP_SOCKET关掉,否则会引入一堆用不上的依赖。
2.3 验证 PHY 是否活着的第一个测试
在写任何 LWIP 代码之前,先确认 MDIO 能读到 PHY 的 ID。LAN8720A 的 ID 寄存器地址是 2 和 3,读出来应该是0x0007和0xC0F1。如果读不到,后面全是白费。
/* 在 main 里 HAL_ETH_Init 之后调用 */ uint32_t phy_id1 = 0, phy_id2 = 0; HAL_ETH_ReadPHYRegister(&heth, 1, 2, &phy_id1); HAL_ETH_ReadPHYRegister(&heth, 1, 3, &phy_id2); printf("PHY ID: 0x%04X 0x%04X\r\n", phy_id1, phy_id2); /* 正常应输出 0x0007 0xC0F1,否则检查 PHY 地址和 MDIO 接线 */这段代码的逻辑很直接:通过HAL_ETH_ReadPHYRegister读 PHY 的 ID 寄存器。参数依次是 ETH 句柄、PHY 地址、寄存器地址、存放结果的指针。如果输出不是0x0007 0xC0F1,先别往下走,回头查 PHYAD0 引脚和 MDC/MDIO 的 GPIO 配置。常见问题是 MDIO 没有上拉电阻,或者 GPIO 复用选错了。
3. LWIP 无操作系统移植:ethernetif.c 是主战场
3.1 NO_SYS=1 时 LWIP 的初始化顺序
无操作系统意味着 LWIP 的所有定时器和协议处理都要在主循环里手动调用。lwipopts.h里必须定义NO_SYS=1,同时把LWIP_NETCONN、LWIP_SOCKET、LWIP_NETIF_API都设为 0。初始化顺序不能乱:
HAL_Init()和时钟配置MX_ETH_Init()初始化 MAC 和 DMAMX_LWIP_Init()初始化 LWIP 和 netif- 主循环里调用
MX_LWIP_Process()
MX_LWIP_Init里会调用lwip_init(),然后手动设置 netif 的 IP、掩码、网关,最后netif_add并netif_set_up。如果顺序反了,比如先netif_set_up再设 IP,netif 会以 0.0.0.0 启动,ping 自然不通。
/* lwip.c 中的 MX_LWIP_Init 关键片段 */ IP4_ADDR(&ipaddr, 192, 168, 1, 100); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input); netif_set_default(&gnetif); netif_set_up(&gnetif); /* 注意:netif_set_up 之后 PHY 才开始协商,不要在这之前发 ping */参数说明:netif_add的第五个参数是状态指针,裸机下传 NULL 即可;ethernetif_init是网卡初始化回调,里面会调用low_level_init配置 MAC 地址和 DMA 描述符;ethernet_input是 LWIP 自带的 IP 层输入函数。netif_set_up会触发 PHY 自动协商,协商完成前网口是不通的,所以 ping 测试要等几秒。
3.2 low_level_output 和 low_level_input 的收发闭环
ethernetif.c里最核心的是两个函数:low_level_output负责把 LWIP 的 pbuf 通过 ETH DMA 发出去,low_level_input负责从 DMA 接收缓冲区里取包并封装成 pbuf。无操作系统下,接收通常用中断方式:ETH 接收中断触发后,在中断服务函数里调用ethernetif_input,它会调用low_level_input然后交给netif->input。
/* low_level_output 的关键逻辑 */ 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; /* 把 pbuf 链里的数据拷贝到连续的 DMA 发送缓冲区 */ for (q = p; q != NULL; q = q->next) { memcpy(buffer + framelen, q->payload, q->len); framelen += q->len; } /* 调用 HAL 发送,注意超时时间别设太长 */ if (HAL_ETH_Transmit(&heth, framelen, buffer, 1000) != HAL_OK) { return ERR_IF; } return ERR_OK; }这里有个血泪经验:DMA 发送缓冲区必须是 32 字节对齐的,否则在某些 STM32 型号上会出现“发送完成但对方收不到”的玄学问题。另外HAL_ETH_Transmit的超时参数如果设成HAL_MAX_DELAY,一旦 DMA 描述符耗尽就会死等,主循环直接卡死。我一般设 1000ms,超时后返回错误让上层重试。
接收侧的中断处理要注意:ETH 中断里不要做耗时操作,只置个标志或者直接调用ethernetif_input。如果low_level_input里分配 pbuf 失败,直接丢弃该包并重新挂接 DMA 描述符,不要试图阻塞等待。
3.3 主循环里的 MX_LWIP_Process 到底做了什么
无操作系统下,LWIP 的定时器(ARP 超时、TCP 重传等)需要周期性调用sys_check_timeouts()。CubeMX 生成的MX_LWIP_Process通常长这样:
void MX_LWIP_Process(void) { ethernetif_input(&gnetif); /* 处理接收到的包 */ sys_check_timeouts(); /* 处理 LWIP 超时 */ }ethernetif_input会检查 DMA 接收描述符是否有新包,有就调用low_level_input并递交给 LWIP。sys_check_timeouts依赖sys_now()提供毫秒计数,所以sys_now必须正确实现,通常直接返回HAL_GetTick()。如果sys_now返回 0 或者不递增,ARP 永远超时,ping 不通。
主循环里调用MX_LWIP_Process的频率建议在 1ms 到 10ms 之间。太快浪费 CPU,太慢影响响应。我一般放在while(1)里不加延时,靠HAL_GetTick的节拍自然控制。
4. 避坑与排查:ping 不通时先看这五条
4.1 网口灯不亮,MDIO 读不到 PHY
现象:上电后网口指示灯不亮,串口打印 PHY ID 为0xFFFF或0x0000。原因:PHY 地址错误、MDIO 没上拉、或者 RMII 参考时钟没起来。解决:先用示波器量REF_CLK引脚有没有 50MHz 波形。没有的话检查 LAN8720A 的晶振和nRST引脚是否被拉低。有波形但 MDIO 读不到,换 PHY 地址 0 和 1 都试一遍,同时确认 MDIO 线上有 1.5k 到 4.7k 的上拉电阻。
4.2 能收 ARP 但 ping 不回
现象:用 Wireshark 抓包能看到开发板发出 ARP 请求,对方也回了 ARP 应答,但 ping 请求发过去没有 ICMP 应答。原因:low_level_output里 pbuf 拷贝越界,或者 DMA 发送描述符没有正确释放。解决:检查framelen是否超过了 ETH_TX_BUF 的大小。另外确认HAL_ETH_Transmit返回HAL_OK后,DMA 描述符的OWN位是否被硬件清掉。如果没清,说明 DMA 没真正发出去,通常是缓冲区地址不对齐导致的。
4.3 ping 通几包后死机
现象:刚开始能 ping 通,几十包之后串口无输出,网口灯还在亮但完全没响应。原因:pbuf 内存池耗尽,或者 ETH 接收中断里发生了优先级反转。解决:在lwipopts.h里把PBUF_POOL_SIZE从默认的 16 调到 32 或 64。另外检查 ETH 中断优先级是否高于SysTick,如果 ETH 中断里调用了HAL_GetTick而 SysTick 优先级更低,就会卡在中断里等 tick 递增。把 ETH 中断优先级设成比 SysTick 低一级即可。
4.4 只有第一次 ping 通,之后全超时
现象:上电后第一次 ping 能通,后面无论怎么 ping 都不通。原因:ARP 表项老化后没有重新解析,或者 netif 的linkoutput指针被意外修改。解决:检查ethernetif_init里是否正确设置了netif->linkoutput = low_level_output。另外确认sys_check_timeouts在主循环里被周期性调用,否则 ARP 表项永远不老化,但新请求也进不来。
4.5 换一台电脑就 ping 不通
现象:在自己的笔记本上能 ping 通,换到同事的电脑上就不行。原因:IP 地址冲突,或者对方电脑的 ARP 缓存里有旧的 MAC 地址。解决:先确认开发板的 IP 没有和局域网里其他设备冲突。然后在对方电脑上执行arp -d清除 ARP 缓存,或者把开发板和电脑用网线直连,排除交换机的影响。直连时注意有些网卡需要交叉线,不过现在大多数网卡都支持自动翻转。
5. 进阶验证:用 Wireshark 看 ARP 和 ICMP 的完整交互
把 ping 跑通只是第一步,真正要确认协议栈工作正常,得看包。在电脑上装 Wireshark,选对网卡,过滤条件写arp or icmp,然后 ping 开发板。正常的交互应该是:
- 电脑发 ARP 请求:“谁是 192.168.1.100?”
- 开发板回 ARP 应答:“192.168.1.100 在 xx:xx:xx:xx:xx:xx”
- 电脑发 ICMP Echo Request
- 开发板回 ICMP Echo Reply
如果第 2 步没有,说明 LWIP 的 ARP 层没响应。检查ethernetif_input是否把包正确递交给netif->input,以及etharp_input是否被调用。如果第 4 步没有,说明 ICMP 应答没发出去,回头查low_level_output的发送计数。
我习惯在low_level_output里加一个全局计数器,每发一包加一,然后在主循环里打印。ping 的时候看计数器有没有涨,涨了说明发送路径通,没涨说明 LWIP 根本没调用发送函数。这个笨办法比单步调试快得多。
还有一个验证技巧:把开发板的 MAC 地址设成和电脑网卡一样的,然后 ping 自己。如果能通,说明协议栈的环回路径没问题,问题出在物理层或 ARP。不过这个操作会干扰局域网,测完记得改回来。
从那以后我每次调以太网,都强制先读 PHY ID,再抓 ARP,最后才看 ICMP。这三步走完,基本没有调不通的网口。希望帮到你。
本文还有配套的精品资源,点击获取