十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32H7以太网ping不通排查:DHCP成功背后的MAC、Cache与PHY坑

STM32H7以太网ping不通排查:DHCP成功背后的MAC、Cache与PHY坑 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置 1void 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.MACAddruint8_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);在接收路径上则做 invalidateSCB_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 缓冲区和描述符千万不能放到 DTCM0x20000000开头的那块否则 DMA 会直接卡死。提示如果你的工程里以太网描述符用的是普通数组定义比如ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __attribute__((section(.RxDecripSection)));记得检查链接脚本是否把对应的 section 放进了 SRAM30x30040000 附近而且 MPU 配置的 BaseAddress 要和实际链接地址严格一致否则非 cacheable 配置不会生效。3.4 验证 Cache 问题的方法一个很直接的验证手段临时把 MPU 里整个 SRAM 区域都设成 non-cacheable或者干脆关掉 D-CacheSCB_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调用处加断点读出寄存器0x01PHY 状态寄存器和0x1FPHY 特殊控制/状态寄存器分别确认 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 是 400MHzMCO2 分频要配 8 才能得到 50MHz。如果配置写死的是分频 4实际输出 100MHzPHY 就完全没法正常工作。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.10PC 配 192.168.1.100然后从 PC ping 开发板。这样能排除网关 ARP 缓存、路由器风暴等外部因素。如果直连 ping 通说明问题很可能在局域网环境MAC 冲突、网关 ARP如果直连也 ping 不通问题在板子自身。5.3 第三步抓包确认 ARP 请求有没有发出去Wireshark 在 PC 上抓包然后从 PC 发起 ping。观察有没有收到板子发出的 ARP Request广播。如果收到 ARP RequestPC 会回 ARP Reply单播板子有没有后续发出 ICMP Echo Request。如果板子发出了 ICMP Echo RequestPC 回了 Echo Reply但板子没有响应说明 RX 方向的单播接收或 ICMP 处理有问题。如果板子根本没发出 ARP Request问题在 LwIP 的发送路径或 ARP 表。在我的案例里抓包结果显示PC ping 板子时板子收到了 ARP Request也回了 ARP ReplyPC 的 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 拿不到 IPPHY 初始化失败、MDIO 通信异常、时钟不对复位时序、LAN8742 驱动、MCO2 50MHzDHCP 拿到 IP但 ping 网关不通MAC 冲突、ARP 异常、Cache 一致性问题arp -a查冲突、Wireshark 抓 ARP、MPU 配置DHCP 拿到 IP直连 ping 通过路由器 ping 不通网关 ARP 缓存异常、MAC 冲突清 ARP 缓存、换唯一 MACping 时通时不通随机丢包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 如果分配在 DTCMDMA 永远读不到。检查你使用的内存堆位置确保 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 时钟这个顺序我试过很多次基本能覆盖九成以上的情况。
返回列表