
最近在评估低功耗 Wi-Fi 6 方案把 Nordic 的 nRF7002 Wi-Fi 协处理器和 ST 的 NUCLEO-U5A5ZJ-Q 开发板做成了一组测试平台。两者之间用原生 Zephyr RTOS 对接没有引入厂商 SDK也没有改动内核代码纯 mainline 方式驱动。整体链路从 SPI 物理连接、设备树配置、Zephyr Wi-Fi 管理接口到最后扫描、连接、拿到 DHCP 地址整个过程跑通的体验比预想中顺畅但坑也不少。这篇文章就把完整方案、关键配置和排查记录整理出来给正在评估“MCU Wi-Fi 协处理器 Zephyr”这套组合的人一个参考。1. 项目概述与整体设计思路1.1 为什么选这对组合NUCLEO-U5A5ZJ-Q 这块板子核心是 STM32U5A5ZJCortex-M33 内核带 TrustZone 和丰富低功耗模式适合做需要兼顾算力和功耗的边缘设备。Zephyr 对 STM32U5 的 support 在 mainline 里已经很成熟板级定义、时钟树、Flash 分区这些开箱即用省去不少底层的初始化工作。nRF7002 是 Nordic 的 Wi-Fi 6 协处理器定位很明确不是独立 SoC而是通过 SPI/QSPI 接口挂在一个 host MCU 下由 host 运行协议栈和应用程序。这样选型的好处是 Wi-Fi 协议栈不用自己碰射频和基带主控只需要调用标准接口对 MCU 资源要求低很多。两个器件在 Zephyr 中都有完整的驱动支持而且都是 mainline 一部分。nRF7002 的驱动挂在 drivers/wifi/nrf700x 下支持 SPI 和 QSPI 两种模式。选用这对组合基本就是看中“两块板子 vanilla Zephyr”就能拼出一套可复现的 Wi-Fi 6 测试环境不用依赖任何厂商定制的 SDK。1.2 用原版 Zephyr 而不是厂商 SDK“Vanilla Zephyr” 的意思就是直接用上游主线代码不 per-device patch不 fork。实际开发中很多国产 Wi-Fi 模块驱动都要自己移植或者绑定在厂商 SDK 里升级 Zephyr 版本时很痛苦。而 nRF7002 的驱动从 Zephyr 3.4 左右开始逐步进入主线到 3.7、4.x 已经很稳定。这意味着 west 拉下来的代码里就已经带了驱动、设备树绑定、Kconfig 选项不需要额外打补丁。唯一要注意的是Zephyr 的 nRF700x 驱动依赖 Nordic 的 hal_nordic 模块这个通过 west 的 manifest 自动拉取。如果工程里没有这个模块驱动能编译但运行会卡在固件交互上。后面第 3 节会详细说。选择 vanilla Zephyr 还有一层考虑调试手段更统一。Zephyr 的 net_mgmt 接口、wifi shell、网络 shell 都是一套体系配合 tcpdump 抓包需要用 host 侧工具和 Zephyr 日志框架问题定位效率比厂商私有协议栈高很多。1.3 整体链路拆解硬件上 NUCLEO-U5A5ZJ-Q 做 hostnRF7002 作为 Wi-Fi 协处理器通过 SPI 连接。host 侧运行 Zephyr 网络协议栈通过 wifi_mgmt API 下发扫描、连接命令nRF7002 完成射频空口操作后把结果通过 SPI 返回。链路往下细分是应用层shell / 业务代码→ net_mgmt → Wi-Fi L2 → nrf700x 驱动 → SPI 控制器 → nRF7002 固件 → 空口。这套链路的关键点是 SPI 通信稳定性和中断响应。nRF7002 不是简单的 AT 指令模块它和 host 之间有一套基于共享内存的通信协议CS、IRQ、EN 这几根信号线的时序非常关键。所以硬件连接和设备树配置如果有一点偏差现象往往不是连不上 AP而是驱动初始化直接超时或者扫描结果为空。2. 硬件接口与连接设计2.1 nRF7002 的信号接口nRF7002 对外最主要的通信接口是 SPI 或 QSPI外加三根控制信号EN使能、IRQ中断输出、CS片选。还有一个可选信号是连到主机的“connected”相关 GPIO用来做状态同步但常规应用一般用不到。接口模式下nRF7002 作为 SPI slavehost 作为 master。SPI 速率建议初始跑 8 MHz 以下先把链路调通再尝试提高。信号连接需要关注 nRF7002 的 IO 电源域如果用 nRF7002 DK开发板做协处理器Arduino 接口上会做好电平处理直接接 3.3 V 没问题。如果用的是 nRF7002 裸模块务必确认 VDDIO 电压与 host 一致否则会出现偶发通信失败。2.2 NUCLEO-U5A5ZJ-Q 接线方案NUCLEO-U5A5ZJ-Q 的 Arduino 兼容接口可以直连 nRF7002 DK。为了测试方便我选择了 SPI1 作为通信接口并占用 D10/D11/D12/D13 四根 Arduino 引脚。控制信号则从 Zio 扩展区引出。下面是我实际使用的接线对应关系nRF7002 DK 信号NUCLEO-U5A5ZJ-Q 引脚说明VDD3.3 V供电注意共地GNDGND共地SCKD13 / PA5SPI1_SCKMOSID11 / PA7SPI1_MOSIMISOD12 / PA6SPI1_MISOCSD10 / PB6SPI1 片选低有效IRQD7 / PA8nRF7002 中断输出低有效ENZio 区 PB0芯片使能高有效需要强调不同批次 NUCLEO-U5A5ZJ-Q 的 Arduino 引脚映射可能有差异特别是 D7 和 D10。我手上这块板子 D10 对应 PB6D7 对应 PA8但其他 NUCLEO-144 板卡不一定一致。接之前务必打开 ST 官网的原理图确认 Arduino 引脚到 MCU 引脚的映射不然排查起来会非常绕。2.3 电源和信号完整性nRF7002 和 STM32U5 都是 3.3 V IO 电平共地后直接连接即可。不过有两点容易忽略第一是 nRF7002 的 EN 引脚上电时序。有些模块要求 host 的 VDD 稳定后延时一段时间再拉高 EN如果上电后立刻拉高芯片内部 LDO 可能没准备好导致 SPI 通信无响应。在 Zephyr 里可以通过 GPIO 延时控制驱动本身有处理但如果你在外部自行做了 EN 控制一定要考虑这个时序。第二是 SPI 信号尽量短而直避免和电源线、电机线长距离并行。nRF7002 的 SPI 速率即使只有 8 MHz如果杜邦线超过 20 cm并且电路中有大电流负载可能会出现偶发 CRC 错误和重传。我的实测是 10 cm 以内的杜邦线跑 8 MHz 很稳定超过 20 cm 后偶尔出现驱动 firmware load 超时。3. Zephyr 环境配置与设备树解析3.1 准备工程工作区项目基于 Zephyr mainline使用 west 管理。先确认 west workspace 已经初始化并且 zephyr、hal_nordic 等模块齐全。检查方式west list | grep -i nrf如果输出里有 hal_nordic说明模块已被 manifest 拉取。没有的话检查 west.yml 里 Zephyr 版本和模块列表。Zephyr 3.7 以上版本对 nRF7002 支持已经比较完整建议至少这个版本起步。创建自定义应用目录结构如下my_wifi_test/ ├── CMakeLists.txt ├── prj.conf ├── boards/ │ └── nucleo_u5a5zj_q.overlay └── src/ └── main.cCMakeLists.txt 内容cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(my_wifi_test) target_sources(app PRIVATE src/main.c)3.2 设备树 overlay 编写nRF7002 在 Zephyr 里绑定为“nordic,nrf7002”设备挂在 SPI 总线上。在 overlay 中需要把 SPI1 控制器、pinctrl、CS/IRQ/EN 引脚全部配置对。下面是我实测可用的 overlaypinctrl { u5a5_spi1: u5a5_spi1 { pinmux STM32_PINMUX(A, 5, AF5) /* SCK */ STM32_PINMUX(A, 6, AF5) /* MISO */ STM32_PINMUX(A, 7, AF5) /* MOSI */ ; }; }; spi1 { pinctrl-0 u5a5_spi1; cs-gpios gpiob 6 GPIO_ACTIVE_LOW; status okay; nrf7002: nrf70020 { compatible nordic,nrf7002; reg 0; spi-max-frequency 8000000; irq-gpios gpioa 8 GPIO_ACTIVE_HIGH; en-gpios gpiob 0 GPIO_ACTIVE_HIGH; status okay; }; };这里有两个容易踩的坑。第一pinctrl 节点不是必须叫 u5a5_spi1可以随意命名但 pinmux 必须对应实际使用的引脚。如果编译报undefined pinctrl node从 board 的-pinctrl.dtsi里找已经定义好的 spi1_pa5 之类节点直接引用即可。第二irq 的中断有效电平需要和设备树一致。nRF7002 的中断信号是低有效但在 Zephyr 的 nrf700x 驱动中有些版本使用 high 有效的 GPIO 配置反而更符合内部逻辑。如果扫描卡住、没有任何中断回调可以把 IRQ 的GPIO_ACTIVE_HIGH改成GPIO_ACTIVE_LOW试试这是非常值得怀疑的一个点。3.3 prj.conf 关键配置项最小化的 Wi-Fi 功能需要开启以下配置项CONFIG_WIFIy CONFIG_WIFI_NRF700Xy CONFIG_WIFI_NRF700X_SPIy CONFIG_SPIy CONFIG_GPIOy CONFIG_NETWORKINGy CONFIG_NET_L2_WIFIy CONFIG_NET_IPV4y CONFIG_NET_DHCPV4y CONFIG_NET_MGMTy CONFIG_WIFI_MGMTy CONFIG_NET_SHELLy CONFIG_WIFI_SHELLyCONFIG_WIFI_NRF700X_SPI是选择 nRF7002 的 SPI 通信模式如果你的配置走 QSPI则改成CONFIG_WIFI_NRF700X_QSPI并在设备树里改用 qspi 节点挂载。CONFIG_NET_SHELL和CONFIG_WIFI_SHELL是在串口上暴露网络和 Wi-Fi 命令调试阶段强烈建议开启这样扫描、连接、断开都不需要写应用代码直接在 shell 里操作。3.4 编译与烧录确认 Zephyr 环境变量后west build -b nucleo_u5a5zj_q -p always my_wifi_test west flash如果-b nucleo_u5a5zj_q找不到 board执行west boards | grep u5a5确认准确名称。烧录后打开串口终端默认波特率通常是 115200应该能看到 Zephyr shell 启动日志。启动日志中重点看两行是否注册了 Wi-Fi 网络接口以及 nRF7002 驱动是否初始化成功。如果看到[00:00:00.123] [INF] wifi_nrf700x: nRF7002 firmware loaded类似的日志说明固件加载成功如果卡住或报 timeout优先检查 SPI 连接和 EN/IRQ 引脚。4. Wi-Fi 应用实现与联网测试4.1 Zephyr Wi-Fi 管理接口梳理Zephyr 的 Wi-Fi 子系统的核心头文件是zephyr/net/wifi_mgmt.h对外提供wifi_scan、wifi_connect、wifi_disconnect等 API。这些 API 不是同步返回结果而是通过net_mgmt事件回调上报状态。所以应用开发需要注册一个net_mgmt_event_callback监听NET_EVENT_WIFI_SCAN_RESULT、NET_EVENT_WIFI_SCAN_DONE、NET_EVENT_WIFI_CONNECT_RESULT等事件。这种异步回调模型一开始不太直观但理解后就很简单命令发出后驱动层与 nRF7002 交互等空口完成扫描或连接回调函数携带具体结果回到应用。比如扫描结果事件里携带struct wifi_scan_result连接完成事件里携带一个int状态码。4.2 最小可用的扫描和连接代码直接在 main.c 里实现一个能跑通的流程启动后先扫描然后连接指定 AP。#include zephyr/kernel.h #include zephyr/net/wifi_mgmt.h #include zephyr/net/net_if.h #include zephyr/net/net_mgmt.h #include zephyr/net/dhcpv4.h #include zephyr/sys/printk.h #include string.h static struct net_mgmt_event_callback wifi_cb; static struct net_if *wifi_if; static K_SEM_DEFINE(scan_done_sem, 0, 1); static void wifi_mgmt_event_handler(struct net_mgmt_event_callback *cb, uint32_t mgmt_event, struct net_if *iface) { switch (mgmt_event) { case NET_EVENT_WIFI_SCAN_RESULT: { const struct wifi_scan_result *res (const struct wifi_scan_result *)cb-info; printk(SSID:%-32s RSSI:%4d CH:%2d SEC:%d\n, res-ssid, res-rssi, res-channel, res-security); break; } case NET_EVENT_WIFI_SCAN_DONE: k_sem_give(scan_done_sem); break; case NET_EVENT_WIFI_CONNECT_RESULT: { int result *((int *)cb-info); if (result 0) { printk(Wi-Fi connected, starting DHCP\n); net_dhcpv4_start(iface); } else { printk(Wi-Fi connect failed: %d\n, result); } break; } case NET_EVENT_WIFI_DISCONNECT_RESULT: printk(Wi-Fi disconnected\n); break; default: break; } } static void wifi_scan(void) { struct wifi_scan_params params {0}; printk(Scanning...\n); wifi_scan(wifi_if, params); k_sem_take(scan_done_sem, K_SECONDS(15)); } static void wifi_connect_to_ap(const char *ssid, const char *psk) { struct wifi_connect_req_params params {0}; params.ssid (uint8_t *)ssid; params.ssid_length strlen(ssid); params.psk (uint8_t *)psk; params.psk_length strlen(psk); params.channel WIFI_CHANNEL_ANY; params.security WIFI_SECURITY_TYPE_PSK; printk(Connecting to %s...\n, ssid); wifi_connect(wifi_if,