行业资讯
📅 2026/8/31 22:13:05
STM32N6 + NetX Duo DHCP获取IP失败?ACK已到却拿不到地址的排查全记录
最近在调一块 STM32N655X0HxQ 的板子主控是 STM32N6 系列软件栈用的 ThreadX NetX Duo跑了 DHCP client。现象很典型用 Wireshark 抓包能看到 DHCP Discover、Offer、Request 三步都很正常服务器也回了 ACK但板子上nx_ip_address_get查出来的 IP 始终是 0.0.0.0应用层根本拿不到地址程序逻辑直接卡死。这个问题折腾了我一整天网上相关讨论也不多所以把排查过程完整记录下来给后面踩同样坑的人一些参考。这篇内容不光是讲“怎么改”更多是讲“怎么查”。我会先拆 DHCP 在 NetX Duo 里的状态流转再按我实际调试的顺序把可能的原因一个个过最后附上几个修复案例和避坑清单。适合正在用 STM32N6 NetX Duo 做联网功能的嵌入式工程师尤其是第一次从裸机或 lwIP 切换到 ThreadX 生态的建议从头到尾读一遍。1. 问题现场复现ACK 已经到了IP 却依然是 0.0.0.01.1 先说结论这不是 DHCP 协议本身的问题你可能会觉得“收到 ACK 了怎么还没拿到 IP”这句话本身就有歧义。DHCP 的 ACK 是服务器对客户端 Request 的确认它意味着服务器已经“承诺”在租约期内把这个 IP 分配给你。但协议栈内部要做的动作还很多解析 ACK 里的 option、把 IP 绑定到网络接口、设置子网掩码和网关、启动地址冲突检测如果有的话最后才能把状态机推进到BOUND。所以“收到 ACK 但没拿到 IP”这个现象通常不是“网络不通”而是协议栈内部的某个环节没走完。我在调试时最先做的就是确认nx_dhcp_start之后DHCP 的状态到底停在哪一步。1.2 NetX Duo 的 DHCP 状态机与事件回调机制NetX Duo 的 DHCP client 状态机设计得很直接它用一个内部线程来处理收包-发包的循环。状态流转大致是NX_DHCP_STATE_INIT初始化完成准备发 DiscoverNX_DHCP_STATE_SELECTING收到 Offer准备发 RequestNX_DHCP_STATE_REQUESTING已发 Request等待 ACKNX_DHCP_STATE_BOUND收到 ACK 并完成地址绑定这是你真正能拿到 IP 的状态NX_DHCP_STATE_RENEWING/NX_DHCP_STATE_REBINDING租约续期阶段每发生一次状态变化NetX Duo 会调用通过nx_dhcp_set_state_change_callback注册的回调函数。这个回调是观察 DHCP 进度最好用的工具没有之一。我当时在回调里加了一行打印UINT dhcp_state_change(NX_DHCP *dhcp_ptr, UCHAR new_state, UCHAR old_state) { printf([DHCP] state %d - %d\r\n, old_state, new_state); if (new_state NX_DHCP_STATE_BOUND) { ULONG ip 0; nx_ip_address_get(ip_instance, ip, NULL); printf([DHCP] BOUND, ip %lu.%lu.%lu.%lu\r\n, (ip 24) 0xFF, (ip 16) 0xFF, (ip 8) 0xFF, ip 0xFF); } return NX_SUCCESS; }结果很有意思回调直接没进或者进了但打出来的状态不是BOUND而是REQUESTING。这说明 ACK 虽然是收到了但协议栈没有把状态推进下去。问题就出在这个“为什么没推进”上。2. 为什么 ACK 到了IP 却没生效五个高频原因2.1 回调函数没触发或触发时机不对这个是最容易忽略的。nx_dhcp_set_state_change_callback必须在nx_dhcp_start之前调用否则压根不会注册上。我见过不少同事把回调注册放到了nx_dhcp_start后面结果状态变化的时候回调函数指针是空的直接就跳过了。还有一种情况是回调里做了阻塞操作比如在回调里直接调用nx_ip_address_get后的打印任务里用了tx_thread_sleep或者等待信号量而 NetX Duo 的回调是在 DHCP 内部线程上下文里执行的。一旦阻塞整个 DHCP 状态机就卡住了后面的包不会继续处理。2.2 查询 API 用错了多接口和默认接口的区别STM32N6 NetX Duo 的工程里很多人会启用多接口支持。如果你初始化的时候用的是nx_ip_interface_create而不是默认的nx_ip_create后直接使用主接口那么nx_ip_address_get只查主接口的 IP而你 DHCP 跑在次接口上自然查出来是 0.0.0.0。我在调试时也犯过这个错查 API 文档才反应过来。正确做法是用nx_ip_interface_address_get并且把接口索引传对ULONG ip_address 0; ULONG netmask 0; UINT status nx_ip_interface_address_get(ip_instance, 0, ip_address, netmask);注意接口索引从 0 开始。如果你只创建了一个主接口索引就是 0。2.3 PHY 链路状态没有同步这个是 NetX Duo 特有的坑。它在处理 DHCP 包的时候会先检查链路状态。如果 PHY 的 link up/down 状态没有被正确上报给协议栈就会认为链路不可用即使收到了 ACK 也不会继续绑定地址。具体来说NetX Duo 的 DHCP 线程在处理完一个包之后会再次检查链路如果发现链路状态不是“up”就会回到INIT状态重新开始。我在 H7 上遇到过一次在 N6 上也遇到了非常典型。链路状态是通过nx_ip_link_status_check获取的底层靠 PHY 驱动和以太网驱动的回调上报。2.4 cache 和 DMA 一致性在 N6 上特别容易踩坑STM32N6 和 STM32H7 一样CPU 访问内存有 D-Cache以太网 DMA 也在访问同一块内存这就涉及 cache 一致性问题。NetX Duo 的包缓冲如果落在 cacheable 区域而驱动收发 DMA 的时候没有做 invalidate/clean 操作就会出现“内存里的数据是旧的”但 DMA 已经写入新数据的情况。具体表现就是MAC 已经收到了完整的 ACK 帧CRC 校验也通过了但协议栈解析的时候读到的是 cache 里的旧数据结果解析失败状态机不往前走。这个问题非常隐蔽因为抓包看是正常的但板子内部逻辑就是不对。2.5 内存池太小DHCP 选项解析失败NetX Duo 的 DHCP 客户端需要从 ACK 报文中解析出一串 option包括子网掩码、网关、DNS、租约时间等。这些解析结果要存到NX_DHCP结构体里如果nx_ip_create时指定的 packet pool 的 payload 太小收到的 ACK 报文可能被截断或者处理时报“packet too small”错误。我当时查的时候发现默认生成的工程里 packet payload 给了 1536 字节这理论上够用但如果开了大量 debug 打印或者 DHCP 选项特别多有些路由器会塞一堆 option还是可能出问题。保险起见我习惯把 payload 加到 1600 以上pool 大小也要够。3. 一步步排查从现象定位到根因3.1 第一步确认 DHCP 状态机真的到了 BOUND不管现象多诡异第一步永远是把状态机打印打开。这里有两个手段一个是我前面说的状态变化回调另一个是 NetX Duo 自带的 debug 打印。如果你启用了NX_ENABLE_DHCP_CLIENT和调试输出可以在工程宏里加NX_DHCP_CLIENT_DEBUG_ENABLE不同版本名字可能略有差异这样 DHCP 内部每个关键步骤都会打日志。如果你不想重新编译整个协议栈最简单的做法就是靠回调打印。我当时的输出是state 1 - 2 state 2 - 3对应INIT → SELECTING → REQUESTING然后就没有然后了。这说明 ACK 可能收到了但REQUESTING之后的处理逻辑没有跑通。3.2 第二步验证 IP 是否已经绑定到接口如果你发现状态已经到BOUND了但应用层查不到 IP那就是查询 API 用错了。我在 2.2 节已经讲了多接口的问题这里再补充一个细节nx_ip_address_get返回的是主接口地址而如果你用的是nx_ip_interface_address_get第二个参数传接口索引注意索引别传错。还有一种情况状态到了BOUNDIP 也能查到了但你把查询的逻辑放在了一个比 DHCP 线程优先级还高的线程里这个线程在 DHCP 还没有完成初始化的时候就去查了查出来自然是 0.0.0.0。这不是 DHCP 的问题是同步问题。解决方法是在状态回调里设置一个标志位然后再让业务线程等待这个标志位。3.3 第三步检查链路状态与 PHY 初始化这一步需要你熟悉自己的 PHY 芯片。我用的 PHY 是瑞昱的 RTL8201FRMII 接口初始化完成后拉高 link 中断引脚。NetX Duo 的驱动会在 link 状态变化时调用nx_ip_link_status_check来获取最新状态。你可以用下面这个函数手动轮询链路状态ULONG link_status 0; UINT status nx_ip_interface_status_check(ip_instance, 0, NX_IP_LINK_ENABLE, link_status, 500); if (status ! NX_SUCCESS) { printf([NET] link not up, status 0x%04X\r\n, status); }如果这个函数一直返回失败说明 PHY 的 link 状态没有正确上报给协议栈。这可能是因为 PHY 的中断引脚没有触发或者以太网驱动的初始化顺序不对。很多芯片要求 PHY 复位后至少等待几十毫秒才能读到正确的状态寄存器如果你在 PHY 还没 ready 的时候就去读很容易读到 0。另一个检查点是 RMII 接口的参考时钟。我用的是外部 50MHz 有源晶振如果这个时钟没起来PHY 不会报 link up但 MAC 可能仍然能收到包因为某些 PHY 在没有时钟时也能检测到信号这就会造成“能收到 ACK 但链路状态为 down”的诡异现象。3.4 第四步检查 cache / MPU / 时钟这些“底层暗礁”这一步是 STM32N6 上最容易出问题的地方。Cortex-M55 和 M7 一样D-Cache 是需要主动维护一致性的。如果你在初始化里调用了SCB_EnableDCache()那么以太网 DMA 的收发缓冲区和描述符都要注意简单做法是把所有以太网相关的 buffer 都放到非 cacheable 的 MPU 区域。CubeMX 生成的代码里通常会帮你配置一个 MPU region但有些版本只给 SRAM 配了 region没把以太网描述符 buffer 也覆盖进去。我当时的做法是在main.c里用MPU_Config专门划了一个 regionMPU_Region_InitTypeDef MPU_InitStruct {0}; MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress 0x30000000; // 根据实际 buffer 地址调整 MPU_InitStruct.Size MPU_REGION_SIZE_256KB; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec MPU_REGION_DISABLE_EXEC; 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);需要注意的是如果以太网 buffer 使用的是外部 PSRAM 或 SDRAM那 bufferable 和 shareable 属性的配置也要跟着调。不同的内存区域DMA 访问能力和 cache 策略都不一样这块没有统一答案只能逐个测试。如果你不打算改 MPU 配置还有一个备选方案每次 DMA 收发前后手动做 cache 维护。发送前调用SCB_CleanDCache_by_Addr接收后调用SCB_InvalidateDCache_by_Addr。这个方案能解决问题但代码侵入性稍强而且如果描述符本身也在 cacheable 区域维护起来很麻烦。我建议优先用 MPU region 来规划。3.5 第五步翻协议栈版本和 DHCP option 参数如果上面的步骤都排查完了还是不行就要考虑是不是协议栈版本和环境导致的问题了。我踩过一个很隐蔽的坑DHCP 服务器比如某个家用路由器发出的 ACK 里携带了 option 82中继代理信息而某些版本的 NetX Duo 对未知 option 的兼容性不好解析到一半就跳出了导致后续的关键 option如 option 1 子网掩码没有被读取。这种情况下状态机不会崩但地址绑定不完整。解决方法是抓包看看 ACK 里到底带了哪些 option。用 Wireshark 抓包然后看 DHCP option 列表。如果 option 82 是问题所在你可以尝试在 DHCP 客户端里把NX_DHCP_CLIENT_SEND_HOSTNAME等扩展选项关掉或者检查 NetX Duo 的新版本是否有相关的 bug fix。另外MAC 地址问题也值得注意。如果板子的 MAC 地址没有从 UUID 或 OTP 里读取而是写死了一个全 0 或者未烧录的地址某些 DHCP 服务器可能会认为这是一个非法客户端在 ACK 时故意不携带部分关键 option。STM32 系列每个芯片在出厂时都有唯一的 UUID可以拼出一个全球单播 MAC 地址但如果你的工程用的是默认的0x00地址很可能会触发这种情况。4. 三种典型场景的完整解决过程4.1 场景一状态回调被阻塞IP 其实已经分配我在调这块板子时第一个排查方向就是回调。现象抓包能看到 ACK但状态回调里new_state一直没有打印出BOUND而是一直停在REQUESTING。排查过程首先我在回调里加打印确认回调有被调用。然后我发现回调里我调用了业务线程的tx_event_flags_set这个事件在没有其他线程接收时确实会一直等待但tx_event_flags_set本身是异步的不会阻塞。接着我在回调里加了一个HAL_GPIO_Toggle用示波器看波形。发现回调确实被触发了但只触发到REQUESTING状态。最后我在nx_dhcp_start前后加了一堆打印发现我没有开启定时器或系统 tick 的持续运行不ThreadX 有默认 tick但如果 HCLK 配错tick 计时不准DHCP 的等待超时时间就会异常。最终修复把 CubeMX 里的系统时钟从默认的 HSI 改为外部 PLL 倍频确保 HCLK 正确ThreadX 的 tick 和 NetX Duo 的超时计算就能正常工作。其实这一步跟 DHCP 没有直接关系但 SysTick 不工作或频率错误会导致 DHCP 中“等待 ACK 超时后重发 Request”的逻辑反复触发产生“每次都是 REQUESTING”的假象。这个案例给我们的教训是不要只盯着 DHCP 本身要先确认整个系统的时钟、tick、事件机制是正常的不然很容易走到误区里。4.2 场景二D-Cache 导致脏读ACK 收到但内容解析失败另一个很典型的场景就是我在 2.4 节提到的 cache 一致性问题。现象Wireshark 能看到 ACK板子侧也能通过 DMA 中断知道“收到了一包数据”但 NetX Duo 解析这包数据时得到的结果完全不对DHCP 状态卡在REQUESTING等待超时后重发。排查过程一上来我用HAL_ETH_ReadData之类不同驱动的 API 可能不同把 DMA 接收缓冲区里的原始数据导出来核对以太网帧头、IP 头、UDP 头。发现数据是对的。然后我把这部分数据用串口发到 PC 上用 Python 脚本解析 DHCP option发现 option 也是对的。那么问题就出在 NetX Duo 的NX_PACKET拿到数据之后到 DHCP 解析之间的某个环节。打开 D-Cache 时如果接收 DMA 写入内存后协议栈读取时没有 invalidate那读到的就是 CPU cache 里的旧数据。修复方法把以太网 DMA 相关的 buffer 放到 non-cacheable 区域。这里还有一个更隐蔽的细节STM32N6 的以太网 DMA 描述符和收发 buffer 可能分散在不同的内存区有些在 DTCM有些在 SRAM有些在 AXI SRAM。DTCM 本身不在 AXI 总线上DMA 不能直接访问除非经过总线桥如果你把 buffer 放在 DTCM 里DMA 写入的数据可能根本不进你期望的 buffer这时候收到的 ACK 可能只是 DMA 中断碰巧触发了数据却是空的。我在 STM32H7 上踩过 DTCM 的坑在 N6 上同样需要留意。保险做法是把以太网相关的所有内存分配放到 AXI SRAM比如0x24000000段里并配上 non-cacheable 的 MPU region。4.3 场景三PHY 中断没接NetX Duo 不推进状态机这个场景和 2.3 节讲的链路状态问题匹配。我用的板子PHY 的中断输出引脚没有连接到 MCU而是悬空状态。虽然 MAC 初始化后 PHY 能正常协商出 link up但因为 NetX Duo 的驱动依赖 PHY 中断来触发链路状态变化回调一旦这个回调不触发协议栈就认为链路层不可用。现象DHCP 能发出去也能收到 ACK但状态机始终在REQUESTING和INIT之间循环跳转。因为每次收完包之后协议栈检查链路发现是“down”就复位回INIT。排查过程先看 PHY 芯片手册确认它的 link 状态寄存器地址。然后在以太网驱动里把phy_link_status相关的读取逻辑打开确认读取到的状态确实是 UP。再看驱动是如何上报链路状态的如果它依赖 GPIO 中断而 GPIO 没初始化和配置那就必然会失败。修复方法有两个一个是把 PHY 的中断引脚正确接到 MCU并在驱动里配置为下降沿或上升沿触发另一个是在驱动初始化后手动调用一次链路状态更新函数把 PHY 寄存器里读到的 link up 状态直接上报给 NetX Duo。后者适合硬件上已经没法改的场景。我当时选择后者在以太网驱动初始化完成后加了一段代码强制上报 link upstatus nx_ip_interface_status_check(ip_instance, 0, NX_IP_LINK_ENABLE, link_status, 0); if (status ! NX_SUCCESS) { // 手动触发一次 link up 通知 nx_ip_interface_link_up(ip_instance, 0); }这里用nx_ip_interface_link_up直接通知协议栈链路已就绪。之后 DHCP 就正常从INIT跑到BOUND了。5. 常见问题速查表与避坑清单5.1 快速对照表我把这次调试过程中遇到的各种情况、可能原因和解决思路整理成一个速查表方便你对照排查。现象可能原因检查方法解决思路抓包看到 ACK状态卡在 REQUESTINGDHCP option 解析失败 / 链路状态不对 / cache 脏读打开状态回调打印检查链路状态、检查 ACK 原始数据按 3.3 / 3.4 / 3.5 节排查状态已到 BOUND但查 IP 是 0.0.0.0查询 API 用错接口确认用的是nx_ip_interface_address_get还是nx_ip_address_get改 API传对接口索引回调从未被触发回调注册晚于nx_dhcp_start检查初始化代码顺序注册回调提到nx_dhcp_start之前链路状态始终 downPHY 中断未接 / 驱动器未上报nx_ip_interface_status_check轮询强制上报 link up / 修硬件能收到部分包但 DHCP 超时内存池太小被截断 / DMA 描述符在 DTCM抓包确认包长查看 pool 配置增大 payload把 buffer 放到 AXI SRAMACK 里 option 解析异常路由器增加了不常见 optionWireshark 抓包看 option 列表关扩展 option / 升级 NetX Duo偶发性时好时坏cache 维护时序不固定打开 cache 打印检查 MPU 配置用统一 non-cacheable region5.2 独家避坑心得最后分享几个我这次调试过程中总结出来的经验这些在官方文档里很难一次找齐。第一不要轻易关闭 D-Cache 来绕过问题。有些人遇到 cache 一致性问题第一反应是关掉 D-Cache这样确实能“解决问题”但代价是整体性能下降非常多。N6 系列的 CPU 主频很高如果 D-Cache 全关很多计算密集型任务的耗时可能会翻几倍。正确做法是把网络相关的 buffer 单独划成 non-cacheable region其他部分继续开启 cache。第二打印一定要分行、分模块、带时间戳。我在调试时发现如果打印信息写的太密比如printf([DHCP] %d - %d, ip%lu, a, b, ip)信息多了以后很容易看花眼错过关键转折点。建议把网络、协议栈、应用层三类打印分别用不同的标签前缀比如[NET]、[DHCP]、[APP]方便 grep。第三Wireshark 抓到 ACK 不代表 ACK 一定合法。有些路由器或者 DHCP 服务器在广播域里可能回复多个 ACK或者 ACK 里携带的 IP 是你之前已经过期租约的 IP。客户端必须在收到 ACK 后检查yiaddr字段与之前 Request 里的 IP 是否一致。NetX Duo 内部会做这个校验但如果它校验失败就会丢弃这个 ACK。这时候你的状态会一直停在REQUESTING超时后重新走 Discover。抓包看起来是“收到了 ACK”但协议栈认为这不是它想要的 ACK。第四用二层交换机抓包时要注意 DHCP 的广播行为。如果你的板子单独插在一个普通交换机端口上并且用的是三层路由器的 DHCP 服务广播可能会被路由器隔离导致你抓不到 ACK。这种情况建议直接在板子和路由器之间接一个带镜像功能的交换机或者直接用一根网线连到带抓包功能的树莓派上做桥接。第五考虑租约时间。如果抓包看到的是 DHCP ACK但 ACK 里的lease time非常短如 10 秒客户端可能在绑定后几秒内就进入续租流程如果你的代码在查询 IP 时刚好赶上续租间隙也可能看到 IP 被清零的瞬间。虽然这不是“没有获取到 IP”的原因但会导致你误判。第六检查线程优先级和tx_thread_sleep的使用。NetX Duo 的 DHCP 线程优先级要低于以太网接收线程否则在繁忙的网络上可能出现丢包。很多从 lwIP 转过来的人习惯把所有网络相关线程都放同一个优先级这在 ThreadX 里是可行的但必须保证preemption threshold配置正确。第七如果以上都查完了还是不行仔细看 CubeMX 生成的 NetX Duo 版本号。STM32CubeFW_N6 里的 NetX Duo 版本可能比 ThreadX 官方仓库里的旧某些 DHCP 相关 bug 已经在后续版本修复。你可以对比一下nx_dhcp.h文件里的版本宏看看是否有已知的 ACK 处理问题。6. 调试工具组合建议在整个排查过程中我几乎每天都固定使用这些工具逐一说明。串口打印是最基本的手段。但要注意串口打印本身也会占用 CPU 时间和中断如果你在 PHY 中断或 DMA 中断里加打印很可能导致中断处理时间过长反而影响以太网接收。所以我一般只在应用层或线程上下文里打印。Wireshark 抓包调试是必不可少的。没有它你无法确认“ACK 到底来了没有长什么样”。调试时建议把 DHCP 过滤表达式设置为dhcp || dhcpv6并打开 Wireshark 的 “Decode As” 功能确认是 klass 还是别的协议。SVD 文件和寄存器查看也很有用。Keil / IAR 的 debugger 可以直接查看 MAC 寄存器比如接收描述符里的status位确认 DMA 接收了没有报错。如果 DMA 报错了比如RX_OVERFLOW或DESCRIPTOR_UNAVAILABLE那说明驱动或 buffer 配置有问题。最后是逻辑分析仪或示波器在排查 PHY 时钟和中断时序时非常好用。如果你手头没有这些设备也可以用 GPIO 翻转加定时器的方式来自制一个简易时间轴。这个办法土但很有效。7. 我的最终感受这个问题其实不算特别罕见的故障但在 STM32N6 NetX Duo 这个组合下排查起来比传统 STM32H7 lwIP 要复杂一些原因主要有三个Cortex-M55 的 cache 策略需要额外关注、NetX Duo 的状态机对链路状态非常敏感、以及 CubeMX 生成的代码虽然好用但默认配置往往不是你实际硬件的配置。调试到后期我最大的体会是不要一上来就怀疑 DHCP 协议栈本身。绝大多数“收到 ACK 但拿不到 IP”的问题根源都在协议栈之下的那一层——要么是链路层状态没同步要么是内存访问的一致性出了问题要么就是调用方式不对。先把这些基础环境确认好再回头审视 DHCP 逻辑往往能省下很多时间。如果你正在调类似的问题并且手里有 Wireshark 抓包记录可以先看 ACK 报文里的option 60Vendor Class Identifier和 option 61Client Identifier。如果这两个 option 的格式和你的配置不一致某些服务器会忽略你的请求。把这两个 option 打印出来对照一下也有帮助。在结尾再分享一个小技巧在工程里留一个长按按键进入手动 IP 配置的后门。在调试 DHCP 问题的时候如果长时间卡住可以切到手动 IP让 TCP 客户端功能先跑起来验证上层逻辑是否正常。这样一来就能把问题范围精确地缩小在 DHCP 这个模块内而不是被上层业务逻辑干扰。这个后门非常有用建议所有以太网产品都留一个。