哈喽,我是余火,一个普通的牛马打工人,目前正在学如何使用Zephyr RTOS。
上篇用 UART 串口实现了电脑给板子发命令控制 WS2812 变色,有串口通信基础了。但到目前为止板子一直是离线状态——只能和电脑一对一有线通信。
ESP32-S3 最大的优势是自带 WiFi模块,所以这次直接在同一个工程上加 WiFi 功能,让板子连上路由器拿到 IP 地址。
💡 本篇学习目标
• net_mgmt(NET_REQUEST_WIFI_CONNECT, ...):发起 WiFi 连接请求
• net_mgmt 事件回调:监听连接结果和断开事件
• K_SEM 信号量桥接:将异步 WiFi 事件转换为同步阻塞等待
改了哪些东西
|
文件
|
改了什么
|
|
`src/main.c`
|
末尾追加 `#include "wifi.h"` + `wifi_init_and_connect()` 调用
|
|
`src/wifi.c`
|
新增:WiFi STA 连接管理模块(事件回调 + 信号量同步 + 重试)
|
|
`src/wifi.h`
|
新增:WiFi 模块接口声明
|
|
`prj.conf`
|
追加网络 Kconfig + WiFi 凭证 + 栈/堆调整
|
|
overlay
|
追加 `&wifi { status = "okay"; };`
|
UART 中断、命令解析线程、WS2812 灯效代码全部都可以保留不动。
WiFi STA 模式
WiFi 有两种基础模式:STA(Station,终端)和 AP(Access Point,热点)。手机连家里路由器就是 STA 模式,手机开热点让别的设备连就是 AP 模式。
嵌入式产品绝大多数用 STA 模式——板子作为终端连路由器,然后通过 TCP/UDP/MQTT 等协议通信。本篇就是让 ESP32-S3 以 STA 模式连上路由器。
启用网络子系统
相比 GPIO、UART 这些默认启用的驱动,Zephyr 的网络协议栈是可选组件,需要手动开启一堆 Kconfig。在 prj.conf 中追加:
/* ── 栈/堆调整(WiFi 协议栈需要更多内存)── */CONFIG_MAIN_STACK_SIZE=4096CONFIG_HEAP_MEM_POOL_SIZE=57344
/* ── 网络基础 ── */CONFIG_NETWORKING=yCONFIG_NET_IPV4=yCONFIG_NET_L2_WIFI_MGMT=yCONFIG_NET_L2_ETHERNET=yCONFIG_NET_MGMT=yCONFIG_NET_MGMT_EVENT=yCONFIG_NET_MGMT_EVENT_STACK_SIZE=2048CONFIG_NET_MGMT_EVENT_QUEUE_SIZE=16CONFIG_NET_DHCPV4=yCONFIG_NET_PKT_TX_COUNT=10CONFIG_NET_PKT_RX_COUNT=10CONFIG_NET_BUF_TX_COUNT=20CONFIG_NET_BUF_RX_COUNT=20CONFIG_NET_TX_STACK_SIZE=2048CONFIG_NET_RX_STACK_SIZE=2048
/* ── WiFi ── */CONFIG_WIFI=yCONFIG_WIFI_LOG_LEVEL_INF=y
/* ── WiFi 凭证(替换为你自己的) ── */CONFIG_WIFI_SSID="your_ssid"CONFIG_WIFI_PASSWORD="your_password"
💡 配置项看着多,其实就三组:网络基础(IP协议栈 + 缓冲区 + 收发线程)→ WiFi 驱动 → 凭证。Zephyr 的模块化设计就是不用网络就不编译进去,省 Flash 和 RAM。
overlay 追加一行显式确认 WiFi 外设(ESP32-S3 的 dtsi 默认已启用,但显式写出来更清晰):
&wifi { status = "okay";};
WiFi 连接模块
WiFi 逻辑拆成了独立的 wifi.c / wifi.h,main 只需调一个函数。这是实际项目的常见做法——WiFi、BLE 等通信模块各管各的,main 只负责初始化和业务逻辑。
wifi.h:
#ifndef WIFI_H#define WIFI_H
* @brief 初始化 WiFi STA 并阻塞等待连接完成 (或超时) * * 内部通过 K_SEM 将异步 net_mgmt 事件转换为同步等待。 * 超时时间固定为 30 秒。 * 连接成功后 IP 地址通过 DHCP 自动获取,并通过 LOG 输出。 * * @return 0 成功,负 errno 失败 */int wifi_init_and_connect(void);
#endif
wifi.c 核心代码:
#include <zephyr/kernel.h>#include <zephyr/net/net_if.h>#include <zephyr/net/net_mgmt.h>#include <zephyr/net/net_event.h>#include <zephyr/net/wifi_mgmt.h>#include <zephyr/logging/log.h>#include <string.h>LOG_MODULE_REGISTER(wifi, LOG_LEVEL_INF);#define WIFI_TIMEOUT_MS 30000 * WiFi 连接完成信号量 (初始值 0,最大值 1) * * 用于将异步的 net_mgmt 事件转换为同步等待: * wifi_init_and_connect() 中 k_sem_take() 阻塞 → * 回调中连接成功时 k_sem_give() 唤醒。 */static K_SEM_DEFINE(wifi_connected, 0, 1);static struct net_if *sta_iface;static struct net_mgmt_event_callback wifi_cb; * wifi_mgmt_handler — WiFi 网络管理事件回调 * * 这套 net_mgmt 事件机制和 UART 中断回调、GPIO 中断回调 * 本质上是同一套设计思想——硬件事件 → 驱动通知 → 用户回调, * 只是从单设备扩展到了网络子系统。 */static void wifi_mgmt_handler(struct net_mgmt_event_callback *cb, uint64_t mgmt_event, struct net_if *iface){ ARG_UNUSED(cb); switch (mgmt_event) { case NET_EVENT_WIFI_CONNECT_RESULT: { const struct wifi_status *status = (const struct wifi_status *)cb->info; if (status->status == 0) { * 连接成功:读取并打印 IPv4 地址 * * 由于 CONFIG_ESP32_WIFI_STA_AUTO_DHCPV4, * DHCP 通常已完成,但仍检查 addr->s_addr != 0 * 以处理 IP 尚未分配的边界情况。 */ const struct net_in_addr *addr = net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED); if (addr && addr->s_addr != 0) { char buf[NET_IPV4_ADDR_LEN]; net_addr_ntop(AF_INET, addr, buf, sizeof(buf)); LOG_INF("IPv4 address: %s", buf); } else { LOG_WRN("IPv4 address: (not assigned yet)"); } LOG_INF("WiFi connected"); k_sem_give(&wifi_connected); } else { * 连接失败(密码错误、AP 不在范围内等) * 仅记录日志,不释放信号量,调用者将在超时后返回 -ETIMEDOUT。 */ LOG_ERR("WiFi connect failed: %d", status->status); } break; } case NET_EVENT_WIFI_DISCONNECT_RESULT: { const struct wifi_status *status = (const struct wifi_status *)cb->info; LOG_WRN("WiFi disconnected: reason=%d", status->status); break; } default: break; }}int wifi_init_and_connect(void){ * 获取 WiFi STA 接口 * * 优先使用 net_if_get_wifi_sta() 精确获取 WiFi STA 接口。 * 如果返回 NULL,则回退到 net_if_get_default() 获取默认接口。 */ struct net_if *iface = net_if_get_wifi_sta(); if (!iface) { iface = net_if_get_default(); } sta_iface = iface; if (!iface) { LOG_ERR("No network interface found"); return -ENODEV; } net_mgmt_init_event_callback(&wifi_cb, wifi_mgmt_handler, NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_WIFI_DISCONNECT_RESULT); net_mgmt_add_event_callback(&wifi_cb); * 显式启用网络接口 + 等待硬件就绪 * * net_if_up() 激活网络接口,触发底层驱动初始化 WiFi 硬件。 * k_msleep(1000) 给驱动 1 秒时间完成硬件初始化, * 实测 500ms 偶尔不够稳定,1 秒后连接成功率显著提高。 */ net_if_up(iface); k_msleep(1000); * 构造 WiFi 连接请求参数 * * - SSID 和密码从 Kconfig (CONFIG_WIFI_SSID / CONFIG_WIFI_PASSWORD) 获取 * - 安全类型: WPA2-PSK * - 频段: 2.4GHz (ESP32-S3 不支持 5GHz) * - 信道: 任意 (由 AP 决定) */ struct wifi_connect_req_params params = { .ssid = (const uint8_t *)CONFIG_WIFI_SSID, .ssid_length = strlen(CONFIG_WIFI_SSID), .psk = (const uint8_t *)CONFIG_WIFI_PASSWORD, .psk_length = strlen(CONFIG_WIFI_PASSWORD), .security = WIFI_SECURITY_TYPE_PSK, .channel = WIFI_CHANNEL_ANY, .band = WIFI_FREQ_BAND_2_4_GHZ, }; LOG_INF("Connecting to SSID \"%s\" ...", CONFIG_WIFI_SSID); * 连接 + 重试机制 * * ESP32-S3 的 WiFi 驱动偶发首次连接时返回 reason=-1(驱动未完全就绪)。 * 重试最多 3 次,每次间隔 2 秒,覆盖驱动初始化时序不稳定的情况。 */ int ret; for (int attempt = 1; attempt <= 3; attempt++) { LOG_INF("WiFi connect attempt %d/3", attempt); * net_mgmt() 是非阻塞的,仅将请求投递到网络管理栈。 * 实际连接在底层驱动和网络栈的上下文中异步执行。 */ ret = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, ¶ms, sizeof(params)); if (ret) { LOG_WRN("WiFi connect request failed: %d", ret); k_msleep(2000); continue; } * 阻塞等待连接完成信号量(最多 30 秒) * * 由于 CONFIG_ESP32_WIFI_STA_AUTO_DHCPV4,驱动仅在 DHCP BOUND 后 * 才触发 CONNECT_RESULT 事件,所以唤醒时 IP 地址已分配完成。 */ ret = k_sem_take(&wifi_connected, K_MSEC(WIFI_TIMEOUT_MS)); if (ret == 0) { return 0; } LOG_WRN("WiFi connection timeout, retrying..."); k_msleep(2000); } LOG_ERR("WiFi connect failed after 3 attempts"); return -ETIMEDOUT;}
💡 整个 WiFi 连接流程的设计和 UART 中断一脉相承:UART 是"硬件收字节 → ISR 回调 → 信号量通知线程",WiFi 是"驱动连接 → net_mgmt 回调 → 信号量唤醒等待方"。本质都是异步事件 + 信号量同步这套范式。
main 函数改动
在原有 main.c 的末尾追加 WiFi 初始化调用,UART 和 WS2812 的初始化代码保留不动:
* 初始化 WiFi STA 并连接路由器 * * wifi_init_and_connect() 内部会阻塞等待连接完成(含 DHCP 获取 IP), * 最长等待 30 秒。连接成功后 IP 地址在 wifi 模块的回调中通过 LOG 输出。 */ int wifi_ret = wifi_init_and_connect(); if (wifi_ret == 0) { LOG_INF("WiFi ready, IP obtained"); } else { LOG_ERR("WiFi init failed: %d", wifi_ret); }
return 0;}
编译烧录后打开串口助手,可以看到完整的 WiFi 连接过程:SSID → 扫描认证 → DHCP 获取 IP → 打印 IPv4 地址。同时串口命令控灯功能正常工作,发 red、green 等命令依然可以控制 WS2812。
路由器上查询,能看到ESP32-S3确实连接到WiFi上了。
常见问题
Q1:编译报 undefined reference to net_if_get_wifi_sta?
• 原因:prj.conf 缺少网络 Kconfig
• 解决:确认 CONFIG_NETWORKING=y、CONFIG_NET_L2_WIFI_MGMT=y、CONFIG_WIFI=y 都已添加
Q2:LOG 输出 WiFi connect failed: -1?
• 原因:WiFi 驱动未完全就绪就发起了连接
• 解决:代码中已有 net_if_up() 后 k_msleep(1000) 等待 + 3 次重试机制,如果仍然失败检查 CONFIG_WIFI_SSID 和 CONFIG_WIFI_PASSWORD 是否正确
Q3:连接成功但没看到 IP 地址?
• 原因:DHCP 尚未完成分配
•解决:ESP32-S3的 WiFi 驱动在DHCP BOUND后才触发 NET_EVENT_WIFI_CONNECT_RESULT,正常情况下唤醒时 IP 已分配。如果看到 (not assigned yet) 说明 DHCP 超时,检查路由器 DHCP 服务
总结
本篇在第 9 篇工程基础上,通过 WiFi STA 模式 + net_mgmt 事件回调 + 信号量同步等待 实现了 ESP32-S3 连上路由器并获取 IP,UART 串口和 WS2812 功能完全保留。
顺带提一嘴,现在这种硬编码 SSID 和密码的方式其实只是开发调试用的,真正的 IoT 产品一般是设备先自建 AP 热点让用户填 WiFi 密码配网,等有空了我也准备去研究研究 SoftAP 配网的实现

。
希望我的笔记能对你有一点点点的帮助!欢迎关注一起学习👇