NUCLEO-H755ZI-Q 这块板子,板载 PHY 是 LAN8742A,拿 STM32H745ZI-Q 官方的 Ethernet + LwIP + DHCP 例程直接烧进去,DHCP 能拿到 IP,但 ping 不通,这个问题我前后折腾了两天,踩了不少坑,最后定位到的问题既不在 PHY 也不在 LwIP 配置,而是分散在几个特别容易被人忽略的底层环节。
先说结论:DHCP 能成功,只能证明“板子能收发 UDP 广播帧、MAC 接收路径正常、协议栈基础调度没死”,但 ping 是 ICMP + ARP 的组合,涉及单播收发、发送方向 DMA、以及地址解析,任何一个环节有隐患都会在 ping 这里暴雷。下面我把整个排查过程、涉及的原理、还有最终改法完整拆开讲。
1. 先理清现象:DHCP 成功和 ping 通,根本不是同一件事
1.1 DHCP 成功证明了什么
DHCP 的交互看起来简单,实际藏着不少信息。客户端先发 DHCP Discover(目标地址 255.255.255.255,源地址 0.0.0.0),服务器回 DHCP Offer,客户端再发 DHCP Request,服务器最后回 DHCP ACK。这个过程里:
- 板子的发送方向必须能工作,否则 DHCP Discover 根本出不去,服务器连请求都收不到,更不可能回 Offer。
- 板子的接收方向必须能工作,否则收到不到 Offer / ACK。
- 板子的协议栈 UDP 处理路径必须能收发,因为 DHCP 是 UDP 67/68 端口。
换句话说,DHCP 能成功,说明板子的 DMA 收发、MAC、PHY、中断、LwIP 的 UDP 处理都已经转起来了。这个结论我在排查初期反复强调给自己听,因为很多人一看到 ping 不通,第一反应就是“PHY 初始化失败了”或者“中断没起来”,但 DHCP 成功会帮你把这一大坨可能性直接排除掉。
1.2 ping 链路和 DHCP 的差异在哪里
ping 一个 IP,比如 ping 局域网里的网关 192.168.1.1,流程是:
- 先查 ARP 缓存,如果没有网关的 MAC 地址,就广播发 ARP Request。
- 网关回复 ARP Reply(单播帧)。
- 板子把 ICMP Echo Request 封装成以太网帧发给网关。
- 网关回 ICMP Echo Reply,板子收到后在协议栈里统计并回复结果。
这里多了几个 DHCP 那里没覆盖的环节:
- ARP 请求和回复是否正常。
- 板子的 MAC 地址是否有效、是否和局域网里其他设备冲突。
- 板子能否正确处理发往自己单播地址的帧,这涉及 STM32 的 MAC 地址过滤器。
- 发送路径上,DMA 能不能把 CPU 构造好的 ARP / ICMP 帧完整地搬到以太网控制器里。
DHCP 成功但你 ping 不通,问题基本锁定在这些“D-H-C-P 没有覆盖到”的环节里。我在实际排雷时,找到了三个最典型的坑,下面一个个讲。
2. 第一个坑:MAC 地址冲突导致 ARP 表错乱
2.1 例程里的 MAC 地址是怎么来的
STM32 官方 Ethernet + LwIP 例程里,MAC 地址的初始值一般是个宏,比如:
#define MAC_ADDR0 0x00 #define MAC_ADDR1 0x80 #define MAC_ADDR2 0xE1 #define MAC_ADDR3 0x00 #define MAC_ADDR4 0x00 #define MAC_ADDR5 0x00或者在某些版本的例程(比如带 STM32Cube FW_H7 的 LwIP 应用模板)里,会尝试从 OTP / UID 生成一个基于芯片唯一 ID 的 MAC 地址。
坏就坏在很多复制例程的人根本不会去看这段代码。如果你用的是写着0x00, 0x80, 0xE1, 0x00, 0x00, 0x00这种固定 MAC 的例程,那你手头如果同时调试两块板子、或者局域网里已经有人用同一个例程跑过同一块板子,非常容易出现 MAC 地址冲突。
2.2 为什么 MAC 冲突时 DHCP 能成功但 ping 不通
DHCP 服务器分配 IP 时,很多实现并不严格校验客户端的 MAC 是否唯一,甚至有的服务器在收到 client MAC 相同的情况下仍然会把同一 IP 租给新请求方,或者拒绝新请求。很多时候 DHCP 成功了。
但问题出在网关上。网关(路由器)会维护一个 ARP 缓存表,记录“某个 IP 对应哪个 MAC”。当局域网里有两台设备 MAC 完全一样,网关的 ARP 表就会被两个设备反复刷新,结果是:
- 你 ping 网关时,网关回 ARP Reply 给了“MAC 相同”的某一台设备(可能是你自己,也可能是另一块板子),报文走到错误设备上。
- 如果两块的 MAC 一样,连交换机都会困惑,单播帧可能被错误转发。
我遇到过一次很典型的场景:同一块 H745 官方例程烧进 H755 板子,先在实验室的笔记本上起了一个 DHCP 服务器,拿到 IP 一切正常;拿到办公网去测,DHCP 能拿到 IP,但 ping 网关时通时不通,丢包率接近 100%。后来抓包发现网关 ARP 缓存里,192.168.x.x 对应的 MAC 和另一台已经上线的设备一模一样——就是有人也在用官方例程默认 MAC 烧了同一块型号的开发板挂在同一个网段。
2.3 怎么快速验证这个坑
这一步不需要动逻辑分析仪,先用串口把板子实际的 MAC 地址打印出来,然后在 PC 上执行:
arp -a看看局域网里是否有其他 IP 对应了相同的 MAC 地址。如果有,基本可以确诊 MAC 冲突。
另外还可以在板上和 PC 上同时抓包:板子断电情况下 ping 一下网关,看能不能通;再给板子上电后立刻 ping,看通不通。如果板子上电后网关就 ping 不通了,而板子断电后恢复正常,多半就是 MAC 或 IP 冲突。
2.4 推荐改法
官方例程里如果使用了固定 MAC,建议在 main 函数里一开始就基于 chip UID 生成一个唯一 MAC。常见做法是读取 96 位唯一 ID,取低 3 字节拼到 OUI(如0x02, 0x00, 0x00)后面,注意把本地管理位(locally administered bit)置 1:
void GetUniqueMAC(uint8_t *mac) { uint32_t uid[3]; uid[0] = HAL_GetUIDw0(); uid[1] = HAL_GetUIDw1(); uid[2] = HAL_GetUIDw2(); mac[0] = 0x02; /* locally administered, unicast */ mac[1] = 0x00; mac[2] = 0x00; mac[3] = (uint8_t)(uid[2] & 0xFF); mac[4] = (uint8_t)((uid[1] >> 8) & 0xFF); mac[5] = (uint8_t)((uid[0] >> 16) & 0xFF); }然后把生成的 MAC 地址在初始化 MAC 之前写进heth.Init.MACAddr:
uint8_t mac[6]; GetUniqueMAC(mac); heth.Init.MACAddr[0] = mac[0]; heth.Init.MACAddr[1] = mac[1]; heth.Init.MACAddr[2] = mac[2]; heth.Init.MACAddr[3] = mac[3]; heth.Init.MACAddr[4] = mac[4]; heth.Init.MACAddr[5] = mac[5];改完以后,用arp -d *清空 PC 的 ARP 缓存再 ping,很多莫名其妙的问题会直接消失。
3. 第二个坑:Cortex-M7 的 D-Cache 和 DMA 一致性问题
3.1 为什么 DHCP 能通但 TX 大流量会挂
这是 STM32H7 系列上最经典的“隐形炸弹”。
H7 的内核是 Cortex-M7,带 D-Cache(数据缓存)。DMA 和外设直接访问内存,不走 CPU 的 Cache。当 CPU 往内存里写数据时,数据可能只被写进 Cache 里,还没有真正落到 SRAM。此时如果 DMA 去读这块内存,读到的可能是 Cache 里的旧数据,而不是 CPU 刚构造的新帧。
反过来,DMA 从网卡收到的数据写入内存后,Cache 里可能残留着旧的脏数据,CPU 去读时不会从 SRAM 重新加载,导致读到的是老数据。
LwIP 收包、发包都用 DMA 描述符指向的缓冲区。官方例程在配置 MPU 的时候,通常会把以太网 DMA 描述符和缓冲区的内存区域设置为non-cacheable,或用 cache 维护函数手动刷。但如果你的工程是从别处拷的、或者修改过存储器布局,这块配置可能丢了。
诡异就诡异在:DHCP 数据量小、时序要求不高,而且很多 DHCP 过程里,帧是协议栈内部构造好、放入缓冲区之后立刻交给 DMA 发送,某些编译优化和时序下,Cache 里的数据恰好也同步到了 SRAM,所以 DHCP 能成功。但 ping 一旦跑起来,尤其是 ARP 广播触发后协议栈会同时构造多帧、CPU 写缓冲区和 DMA 读缓冲区之间发生竞争,Cache 脏数据问题就暴露了,表现就是:
- DHCP 成功。
- 板子偶尔能发出去一个包。
- 但 ping 批量发包时基本全丢,或者只有刚上电后的前几个包能通。
3.2 官方例程里为什么没问题
STM32Cube FW_H7 的官方例程,特别是带 LwIP 的模板,在main.c里有一段MPU_Config()。里面会专门给以太网 DMA 描述符和缓冲区划分一个内存区,并配置为:
MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER2; MPU_InitStruct.BaseAddress = 0x30040000; /* 以太网缓冲区,以官方模板为例 */ MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_REGION_ENABLE; MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_REGION_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);注意到IsCacheable = MPU_REGION_NOT_CACHEABLE这一行,挺关键的。如果移植时不小心改了MPU_Region_InitTypeDef,或者你压根没用 CubeMX 的默认模板而是自己搭的工程,那么“Cache 一致性问题”几乎必然出现。
3.3 正确的处理方式
如果你不想折腾 MPU,有个更简单的办法:在以太网 DMA 发送前手动做 Cache clean。比如在low_level_output()里、调用HAL_ETH_Transmit之前:
/* 确保 CPU 构造的帧数据落到了内存,而不是停留在 D-Cache */ SCB_CleanDCache_by_Addr((uint32_t *)p_buffer, len);在接收路径上,则做 invalidate:
SCB_InvalidateDCache_by_Addr((uint32_t *)p_buffer, len);不过这种做法有几个隐患:每次收发包都要刷 Cache,性能损耗大;而且如果描述符和缓冲区在同一个 cache line 上,可能把其他数据也刷掉。所以更稳妥的方案,还是从根上把以太网使用的整块 RAM 配成非 cacheable。
我在实际项目里用的是官方的思路:把ETH_RX_BUF、ETH_TX_BUF、以及 DMA 描述符数组都放到0x30040000起始的 DTCM 后的 SRAM 区(有的板子是 SRAM3),然后在 MPU 里为这块区域配置非 cacheable。注意,ST 的H7 系列里 DTCM 本身不支持 DMA 访问,所以 RX/TX 缓冲区和描述符千万不能放到 DTCM(0x20000000开头的那块),否则 DMA 会直接卡死。
提示:如果你的工程里以太网描述符用的是普通数组定义,比如
ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __attribute__((section(".RxDecripSection")));,记得检查链接脚本是否把对应的 section 放进了 SRAM3(0x30040000 附近),而且 MPU 配置的 BaseAddress 要和实际链接地址严格一致,否则非 cacheable 配置不会生效。
3.4 验证 Cache 问题的方法
一个很直接的验证手段:临时把 MPU 里整个 SRAM 区域都设成 non-cacheable,或者干脆关掉 D-Cache(SCB_DisableDCache()),然后重新编译烧录测试。如果 ping 立刻通了,或者丢包率显著下降,那问题基本就在 Cache 一致性。
但关掉 D-Cache 只是验证手段,不是最终方案,因为 D-Cache 对 H7 性能影响太大了,LwIP 高负载时关 Cache 会导致 CPU 占用率飙升。正确做法还是把以太网相关内存隔离出来配成 non-cacheable。
4. 第三个坑:PHY 状态机、link 事件和 LAN8742 驱动细节
4.1 LAN8742 和 LAN8720 的差异
很多人搜 STM32H7 以太网问题时,看到最多的资料是 LAN8720(正点原子等板子用的多),但 NUCLEO-H755ZI-Q / H745ZI-Q 板载 PHY 是 LAN8742A。两者硬件引脚兼容,但寄存器定义、勘误表还是有差别。如果你拿 LAN8720 的驱动代码来调 LAN8742,或者反过来,大概率出现“PHY 读到 ID 不对”、“自动协商失败”、“link 状态永远 down”这种问题。
H745 官方例程里本身就带了 LAN8742 的驱动文件lan8742.c,所以正常情况下 PHY 型号这块不会出错。但要注意路径:如果你从旧版 FW 拷的例程,有可能带的还是 LAN8742 的老版驱动,建议确认LAN8742_PHY_ID是否匹配。
4.2 最容易忽略的 link 回调
LwIP 的ethernetif.c里面有个ethernetif_set_link函数,它会根据 PHY 的 link 状态调用netif_set_link_up/netif_set_link_down。如果底层 PHY 的 link 状态没有正确上报,LwIP 的 netif 标志位(NETIF_FLAG_LINK_UP)可能一直没被置位。
正常情况下 DHCP 能成功,说明 link 应该是 up 的。但如果你用外部 PHY 或者修改了 PHY 地址,有可能出现“DHCP 成功是一瞬间 link up 了,之后又掉下去”的情况。检查方法:在HAL_ETH_ReadPHYRegister或lan8742_ReadPHYRegister调用处加断点,读出寄存器0x01(PHY 状态寄存器)和0x1F(PHY 特殊控制/状态寄存器),分别确认 link status 位和 auto-negotiation 完成位。
在我这个场景里,PHY 本身没毛病,但我发现 H745 双核例程里,PHY 的复位引脚、中断引脚在 H755 板子上虽然一样,可复位 GPIO 的初始化被放在了MX_GPIO_Init()里,而MX_ETH_Init()可能在 GPIO 初始化之前就执行了,导致 PHY 复位时序不对。这属于初始化顺序的坑,官方模板理论上不会犯,但如果手动调整过 CubeMX 生成顺序就可能踩到。
简单排查方法是在 PHY 复位之后延时 200ms 以上,再读 PHY 寄存器,确认能正确读到0x0007左右的 PHY ID。如果读到全 0 或全 F,说明 MDIO 通信没建立起来,优先排查 PHY 地址和复位引脚。
4.3 以太网时钟:50MHz REF_CLK 必须稳定
LAN8742A 工作在 RMII 模式时,REF_CLK 是 50MHz。在 NUCLEO 板上,这个时钟可以由 STM32 的 MCO2 引脚(PA1 或 PC9,视板子版本而定)输出,也可以由外部晶振提供。H745/H755 例程中,一般是在SystemClock_Config()里配置RCC_MCO2输出 50MHz。
如果 MCO2 输出频率不对,DHCP 这种小包可能还能勉强跑,但 ping 这种持续收发就会因为时钟抖动大而丢包甚至完全不通。验证方式:用示波器看 PHY 的 XI/CLKOUT 引脚有没有 50MHz 稳定时钟,幅度是否够。没有示波器的话,可以在main.c里确认HAL_RCC_MCOConfig(RCC_MCO2, RCC_MCO2SOURCE_SYSCLK, RCC_MCODIV_...)的配置是否和系统时钟匹配。
举例,如果 SYSCLK 是 400MHz,MCO2 分频要配 8 才能得到 50MHz。如果配置写死的是分频 4,实际输出 100MHz,PHY 就完全没法正常工作。
5. 实操排查步骤:我把这套流程走了一遍
5.1 第一步:确认板子的 IP、MAC、link 状态
先在串口里把关键信息打出来:
printf("IP addr : %d.%d.%d.%d\r\n", (uint8_t)(netif->ip_addr.addr >> 24), (uint8_t)(netif->ip_addr.addr >> 16), (uint8_t)(netif->ip_addr.addr >> 8), (uint8_t)(netif->ip_addr.addr)); printf("MAC addr: %02X:%02X:%02X:%02X:%02X:%02X\r\n", netif->hwaddr[0], netif->hwaddr[1], netif->hwaddr[2], netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]); printf("Link up : %d\r\n", (netif->flags & NETIF_FLAG_LINK_UP) != 0);如果 Link up 为 0,别往下查了,先解决 PHY link 问题。
5.2 第二步:ping 网关前,先 ping 同网段直连主机
用网线把开发板直连 PC(不经过路由器),在 PC 上手动配一个同网段静态 IP(比如开发板 DHCP 拿到 192.168.1.10,PC 配 192.168.1.100),然后从 PC ping 开发板。这样能排除网关 ARP 缓存、路由器风暴等外部因素。如果直连 ping 通,说明问题很可能在局域网环境(MAC 冲突、网关 ARP);如果直连也 ping 不通,问题在板子自身。
5.3 第三步:抓包,确认 ARP 请求有没有发出去
Wireshark 在 PC 上抓包,然后从 PC 发起 ping。观察:
- 有没有收到板子发出的 ARP Request(广播)。
- 如果收到 ARP Request,PC 会回 ARP Reply(单播),板子有没有后续发出 ICMP Echo Request。
- 如果板子发出了 ICMP Echo Request,PC 回了 Echo Reply,但板子没有响应,说明 RX 方向的单播接收或 ICMP 处理有问题。
- 如果板子根本没发出 ARP Request,问题在 LwIP 的发送路径或 ARP 表。
在我的案例里,抓包结果显示:PC ping 板子时,板子收到了 ARP Request,也回了 ARP Reply(PC 的 ARP 表能解析出来),但 ICMP Echo Reply 一直不出来。这说明 RX 没问题、ARP 回复没问题,出问题的是 ICMP 报文的发送路径。最终定位到就是 Cache 一致性问题——协议栈处理完 Echo Request 后会构造一个 Echo Reply 放在缓冲区,但 DMA 读出来的还是旧数据,所以 PC 上看到的是“板子似乎没回”。
5.4 第四步:逐一屏蔽可疑配置
排查顺序我建议这样:
- 关 D-Cache(临时验证)。
- 修改 MAC 地址为基于 UID 生成的唯一值。
- 检查 MPU 配置,确保以太网缓冲区和描述符区域 non-cacheable。
- 检查 PHY link 状态和时钟。
我自己最终是第 2 步 + 第 3 步组合解决的。改完 MPU 后,ping 网关从 100% 丢包变成了 0% 丢包,稳了一整天。
6. 常见问题速查表 + 一些实战心得
6.1 现象速查
| 现象 | 最可能的原因 | 重点排查方向 |
|---|---|---|
| DHCP 拿不到 IP | PHY 初始化失败、MDIO 通信异常、时钟不对 | 复位时序、LAN8742 驱动、MCO2 50MHz |
| DHCP 拿到 IP,但 ping 网关不通 | MAC 冲突、ARP 异常、Cache 一致性问题 | arp -a查冲突、Wireshark 抓 ARP、MPU 配置 |
| DHCP 拿到 IP,直连 ping 通,过路由器 ping 不通 | 网关 ARP 缓存异常、MAC 冲突 | 清 ARP 缓存、换唯一 MAC |
| ping 时通时不通,随机丢包 | Cache 一致性问题、PHY 时钟不稳 | MPU 配置、MCO2 波形 |
| ping 小包通,大包不通 | DMA 描述符长度配置问题、缓冲区不足 | 检查ETH_RXBUFNB、ETH_TXBUFNB、MTU 配置 |
| 能收到 ARP 请求但回不了 ICMP | 发送路径 DMA / Cache 问题 | low_level_output加SCB_CleanDCache |
| 能 DHCP,但上电后过一会儿就不通 | link 状态翻转、PHY 热插拔处理逻辑缺失 | 检查ethernetif_set_link调用时机 |
6.2 一些经验提醒
- 别在 DTCM 里放 DMA 缓冲区。STM32H7 的 DTCM 不支持 DMA 访问,LwIP 的 pbuf 如果分配在 DTCM,DMA 永远读不到。检查你使用的内存堆位置,确保 LwIP 的内存堆没有被链接到 DTCM。
- MPU 的 BaseAddress 必须和实际链接地址一致。我曾经把 MPU 配到
0x30040000,但链接脚本把以太网缓冲区和描述符放到了0x30000000,实际测试发现ping依然不通,因为 non-cacheable 配置根本没覆盖到 DMA 访问的内存。 - H745 / H755 双核例程里,M4 核也会初始化一些外设。如果你只烧 M7 核的程序但 M4 核的程序也在跑或没跑,以太网中断可能被核间共享的资源干扰。建议如果不需要 M4 功能,先把 M4 核的代码编译成空循环,排除双核干扰。
- 不要迷信“官方例程”。官方例程在官方板子 + 官方 IDE 环境下是验证过的,但你只要改了编译器版本、优化等级(比如 O2/O3)、或改了内存布局,Cache 相关的问题就可能重新冒出来。遇到类似现象,先把优化等级调到 -O0 试试,能通的话大概率就是 Cache 或内存对齐问题。
6.3 最后的几个小技巧
- 如果 ping 网关不通,先 ping 一下板子所在网段的广播地址(比如
ping 192.168.1.255),看有没有其他设备响应,可以快速判断是不是 IP 被其他设备占用了。 - 在
lwipopts.h里打开LWIP_DEBUG、LWIP_ICMP_DEBUG、LWIP_ARP_DEBUG,把调试输出打到串口,能看到 ARP 是否发出、ICMP 是否收到、错误码是什么,省去反复抓包的麻烦。 - 在
low_level_output()里加一个计数器,每次发送自增;在eth_irq的接收中断里加一个计数器。ping 一轮之后对比两个数值,可以快速知道是发送链路崩了还是接收链路崩了。
这套排查走下来,NUCLEO-H755ZI-Q + LAN8742 的 ping 问题基本能根治。我自己的板子现在 DHCP 获取 IP 后,ping 网关和局域网内其他主机都是稳定 1ms 以内,连续 ping 一晚上也没掉一个包。遇到同样现象的,建议按顺序先查 MAC 地址,再查 MPU / Cache,最后回头看 PHY 时钟,这个顺序我试过很多次,基本能覆盖九成以上的情况。