ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第 16 章 Wi-Fi 基础:让芯片“上网“

第 16 章 Wi-Fi 基础:让芯片“上网“ ESP32-S3 自带 Wi-Fi。本章讲清楚①Wi-Fi 协议栈在芯片里的分层LwIP/TCP-IP/事件②STA连接路由器模式的完整流程与事件驱动模型③与前面章节的整合思路如通过手机控制电机④常见问题。目标不是让你成为网络专家而是能写出第一个能上网的程序。16.1 芯片里的网络分层先建立地图一个网络请求从应用到天线经过多层。ESP-IDF 帮你把下面这些层都封装好了你的应用HTTP 请求 / MQTT 消息 │ LwIPTCP/IP 协议栈TCP/UDP/IP 分包 │ esp_netif网络接口抽象WiFi/以太网 │ esp_wifiWiFi 驱动连接路由器、收发无线帧 │ 射频硬件天线你主要打交道的是事件esp_eventWiFi 连上/断开、拿到 IP 地址系统都会发出事件你注册回调处理即可。【硬件前提】ESP32-S3 只有 2.4GHz 的无线电。S3 的 Wi-Fi 只支持2.4GHz802.11 b/g/n不支持 5GHz。如果家里路由器是仅 5GHz的网络或 2.4G 频段被关掉/隐藏了 SSID芯片扫遍信道也找不到它症状就是日志里反复出现W (2311) wifi_sta: 断开原因码 201201 NO_AP_FOUND“没看到这个 AP”。遇到 201 先怀疑频段和 SSID不要怀疑板子坏了到路由器后台打开 2.4GHz 频段或单独给 2.4G 起一个 SSID 名或者干脆用手机热点开 2.4GHz 模式来测——这是本章所有示例的第一道关卡。【三个词先说人话】SSID路由器报家门的名字一个字符串标签区分大小写IP设备在局域网里的门牌号别人要找你就报这个号IPv4 就是四段数字如 192.168.1.105DHCP路由器自动发门牌号的流程——设备喊一声给我一个号路由器回一个空闲 IP顺带给出网关和 DNS。串成一句话凭 SSID 找到路由器 → 对暗号密码→ DHCP 领到 IP → 这才算上网。注意领到 IP只代表你和路由器之间这条链路通了不代表外网可达DNS、网关、运营商都可能出问题见 16.6 的对应行。16.2 STA 模式连接家里的路由器流程先看全局① nvs_flash_initWiFi 驱动要用 NVS 存射频校准数据。注意只是校准数据 SSID/密码默认并不存在 NVS 里要应用自己持久化见 16.6 的说明 ② esp_netif_init esp_event_loop_create_default网络与事件系统 ③ esp_netif_create_default_wifi_sta创建 STA 网络接口 ④ esp_wifi_init初始化 WiFi 驱动 ⑤ 注册事件回调WIFI_EVENT_STA_*、IP_EVENT_STA_GOT_IP ⑥ esp_wifi_set_mode(WIFI_MODE_STA) ⑦ esp_wifi_set_config(配置 SSID/密码) ⑧ esp_wifi_start → 驱动开始扫描连接 ⑨ 收到 IP_EVENT_STA_GOT_IP → 局域网这条路通了能不能出外网还要看 DNS/网关见 16.6完整示例事件驱动写法可直接抄进工程下面是一份完整的main/main.cpp新建工程、把它替换进去、改两个宏再按后面的 CMakeLists 配好就能编译烧录对照第 3 章的工程骨架。这一版比最小骨架多了三样东西全都是踩坑修正include 列表补全原骨架缺esp_log.h和 FreeRTOS 头文件编译不过SSID/密码用编译期长度校验写入不再用strcpy往固定数组里硬塞断线重连不在事件回调里睡 3 秒那是全书反复禁止的做法而是回调只投队列、由专门的任务做指数退避重连。// main/main.cpp —— ESP32-S3 Wi-Fi STA 连接示例v6.0.1 实测口径#includestring.h#includeesp_event.h#includeesp_log.h#includeesp_netif.h#includeesp_wifi.h#includeesp_wifi_default.h// esp_netif_create_default_wifi_sta() 的声明在这里#includefreertos/FreeRTOS.h#includefreertos/queue.h#includefreertos/task.h#includenvs_flash.hstaticconstchar*TAGwifi_sta;// ------- 改这两行就能连你家路由器 -------#defineWIFI_SSID你的路由器名#defineWIFI_PASS你的密码// 编译期长度校验。wifi_config_t 里 ssid 是 uint8_t[32]、password 是 uint8_t[64]// 见 esp_wifi_types_generic.h不写校验、直接 strcpy 进去一旦超长就是// 越界写内存——程序当场不崩之后在莫名其妙的地方崩最难查的那种。// sizeof 宏字符串时把结尾 \0 也算 1 字节所以 SSID 实际最长 31 字符、// 密码最长 63 字符超了直接在编译期报错根本到不了运行。// sizeof 里的 ((wifi_config_t *)nullptr)-sta.ssid 只求类型尺寸、不求值// 不会真的解引用空指针这是 C/C 老手也常用的取成员尺寸写法。static_assert(sizeof(WIFI_SSID)sizeof(((wifi_config_t*)nullptr)-sta.ssid),SSID 太长加上结尾零字节最多 32 字节);static_assert(sizeof(WIFI_PASS)sizeof(((wifi_config_t*)nullptr)-sta.password),密码太长加上结尾零字节最多 64 字节);// 回调和重连任务之间用队列传消息——第 13 章的老朋友typedefenum{WIFI_EVT_DISCONNECTED,// 断线了该重连WIFI_EVT_GOT_IP,// 连上了退避计时清零}wifi_evt_t;staticQueueHandle_t s_evt_queueNULL;staticvoidon_wifi_event(void*arg,esp_event_base_t base,int32_tid,void*data){if(baseWIFI_EVENTidWIFI_EVENT_STA_START){esp_wifi_connect();// 驱动就绪发起第一次连接}elseif(baseWIFI_EVENTidWIFI_EVENT_STA_CONNECTED){ESP_LOGI(TAG,已连上路由器等待 IP);}elseif(baseWIFI_EVENTidWIFI_EVENT_STA_DISCONNECTED){wifi_event_sta_disconnected_t*e(wifi_event_sta_disconnected_t*)data;// 原因码很有诊断价值esp_wifi_types_generic.h// 201WIFI_REASON_NO_AP_FOUNDSSID 错/信号不到、// 202WIFI_REASON_AUTH_FAIL多半密码错、203关联失败ESP_LOGW(TAG,断开原因码 %d,e-reason);// 铁律回调里绝不 vTaskDelay默认事件循环只有一个任务// 你在这里睡 3 秒全系统所有事件都停 3 秒——和第 13/15 章// 回调要快、重活交给任务的原则直接冲突。// 这里只做一件事往队列投一条消息超时给 0不等待。wifi_evt_t evtWIFI_EVT_DISCONNECTED;xQueueSend(s_evt_queue,evt,0);}}staticvoidon_ip_event(void*arg,esp_event_base_t base,int32_tid,void*data){if(baseIP_EVENTidIP_EVENT_STA_GOT_IP){ip_event_got_ip_t*e(ip_event_got_ip_t*)data;ESP_LOGI(TAG,拿到 IP: IPSTR,IP2STR(e-ip_info.ip));wifi_evt_t evtWIFI_EVT_GOT_IP;xQueueSend(s_evt_queue,evt,0);// 通知重连任务链路已恢复// 到这里局域网这条路通了能不能出外网还要看 DNS/网关16.6}}// 专门的重连任务睡在这里不阻塞任何人staticvoidwifi_manager_task(void*arg){TickType_t backoffpdMS_TO_TICKS(1000);// 初始退避 1 秒constTickType_t kMaxBackoffpdMS_TO_TICKS(60000);// 封顶 60 秒wifi_evt_t evt;while(true){// 一直等消息来一条处理一条第 13 章的阻塞收队xQueueReceive(s_evt_queue,evt,portMAX_DELAY);if(evtWIFI_EVT_GOT_IP){backoffpdMS_TO_TICKS(1000);// 连上了退避归零下次从头算continue;}ESP_LOGW(TAG,%u ms 后尝试重连,(unsigned)(backoff*portTICK_PERIOD_MS));vTaskDelay(backoff);// 睡在任务里事件循环毫无压力esp_wifi_connect();// 指数退避1s → 2s → 4s → … 最多 60s避免路由器长时间挂掉时// 设备 1 秒一次地猛 reconnect费电、刷屏、还可能挤占扫描backoff(backoff*2kMaxBackoff)?kMaxBackoff:backoff*2;}}externCvoidapp_main(void){ESP_ERROR_CHECK(nvs_flash_init());// ① NVSESP_ERROR_CHECK(esp_netif_init());// ② 网络ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();// ③ 接口// 队列和重连任务要在 esp_wifi_start() 之前就绪// 否则启动瞬间的断线事件来了却没人接s_evt_queuexQueueCreate(8,sizeof(wifi_evt_t));xTaskCreate(wifi_manager_task,wifi_mgr,4096,NULL,5,NULL);wifi_init_config_t icWIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(ic));// ④ 驱动// ⑤ 事件回调ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,on_wifi_event,NULL));ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,on_ip_event,NULL));wifi_config_t cfg{};// 先清零两个数组的剩余字节全为 0memcpy(cfg.sta.ssid,WIFI_SSID,sizeof(WIFI_SSID));// 长度已由memcpy(cfg.sta.password,WIFI_PASS,sizeof(WIFI_PASS));// static_assert 把关ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));// ⑥ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,cfg));// ⑦ESP_ERROR_CHECK(esp_wifi_start());// ⑧// 之后一切靠事件驱动 重连任务app_main 可以结束}配套main/CMakeLists.txt少了任何一项都链接不过idf_component_register( SRCS main.cpp INCLUDE_DIRS . REQUIRES nvs_flash esp_wifi esp_netif esp_event )esp_log.h和freertos/*不用写进 REQUIRESv6 里log、freertos属于公共依赖每个组件自动获得这和第 3 章说driver已拆分不矛盾——被拆掉的只是外设驱动事件循环、Wi-Fi、NVS 这些仍要显式 REQUIRES。抄完第一件事替换那两个中文占位符#define WIFI_SSID 你的路由器名和#define WIFI_PASS 你的密码里的中文只是占位符必须整对引号换成真值四个细节最容易翻车SSID区分大小写Home和home在空口上是两个不同的网络肉眼难分的字符别抄错0零≠O、1一≠l小写 L、B≠8引号内结尾不要留空格MyWiFi 和MyWiFi也是两个网络中文 SSID 能连但编码坑多下面另有一小节测试期先起个英文名最省事。改完必须重新idf.py build——宏是编译期展开进二进制的不重编等于没改。【动手框】① 在哪执行新建一个空工程第 3 章骨架或idf.py create-project wifi_sta上面的示例存成main/main.cpp、main/CMakeLists.txt换成上面那份在IDF 终端里cd到工程根目录含CMakeLists.txt的那层。② 敲什么idf.py set-target esp32s3 idf.py build idf.py-pCOM3 flash monitor③ 预期看到编译末尾文件名跟着你project(...)里起的工程名走这个示例代码没几行bin 却有 754KB0xb82d0——Wi-Fi 协议栈本身就是这么大一块对照第 5 章 l298n 工程的 172KB 就能感受到 16.5 说的那句WiFi 很吃内存/体积wifi_sta_demo.bin binary size 0xb82d0 bytes. Smallest app partition is 0x100000 bytes. 0x47d30 bytes (28%) free. Project build complete. To flash, run: idf.py flash烧录末尾Hash of data verified串口输出见下面跑起来应该看到什么。④ 没看到a) 编译报esp_netif_create_default_wifi_sta was not declared→ 去本节头文件迷宫那张表b) 报端口占用 /PermissionError(13)→ 关掉另一个监视器或串口助手2.6 那张表的对应行c) 反复原因码 201→ 回到 16.1 末尾的2.4GHz 硬件前提框。跑起来应该看到什么下面是这一路走下来该看到的片段每行的格式都按 v6.0.1 本地源码/预编译库里的日志语句核对过毫秒数、MAC、任务句柄、信道、RSSI、IP 会因环境不同而不同要对的是行的内容和先后顺序驱动起来 → 握手 → 已连上路由器 → 拿到 IPI (749) main_task: Calling app_main() ← 进入你的 app_main第 2 章 2.9 nvs_flash_init 成功时不打日志 静默通过就是正常 I (1021) wifi:wifi driver task: 3fceca64, prio:23, stack:6656, core0 ← ④ esp_wifi_init I (1035) wifi:mode : sta (68:ee:xx:xx:xx:xx) ← ⑥ 模式 STA括号里是本机 MAC I (2312) wifi:new:6,0, old:1,0, ap:255,255, sta:6,0, prof:1, snd_ch_cfg:0x0 I (2313) wifi:state: init - auth (0xb0) ← 802.11 三步握手开始对暗号 I (2320) wifi:state: auth - assoc (0x0) I (2331) wifi:state: assoc - run (0x10) I (2335) wifi:connected with myhome, aid 4, channel 6, BW20, bssid cc:bb:xx:xx:xx:xx I (2336) wifi:security: WPA2-PSK, phy: bgn, rssi: -53, cipher(pairwise:0x3, group:0x3), pmf:0 I (2340) wifi_sta: 已连上路由器等待 IP ← 我们的回调CONNECTED 事件 I (3012) esp_netif_handlers: sta ip: 192.168.1.105, mask: 255.255.255.0, gw: 192.168.1.1 I (3012) wifi_sta: 拿到 IP: 192.168.1.105 ← GOT_IPDHCP 领到门牌号带wifi_sta的两行是我们自己的ESP_LOGITAG 来自代码带wifi:的是驱动打的TAG 就叫wifi别以为是自己的日志读个关键词就行state: init - auth - assoc - run走通、并出现connected with 你的 SSID就说明 SSID 找对了、密码也对密码错会停在 auth 前并报原因码 202/203最后一行之后程序就没事了本示例只做连接监视器还在滚按Ctrl]退出监视器2.8 第 4 步的老规矩退出后才能再次烧录。想验证网络真的能用把esp_wifi_start()之后的逻辑换成 16.3 说的 HTTP 请求或者先到路由器管理页看这台设备有没有出现在客户端列表里。头文件迷宫esp_netif_defaults.h ≠ esp_wifi_default.h上例最容易抄错的是那两个长得像双胞胎的头文件这里一次讲清v6.0.1 本地源码核实头文件住在哪个组件里面有什么esp_netif_defaults.h注意复数esp_netif只有一堆默认配置宏/结构体如ESP_NETIF_DEFAULT_WIFI_STA()没有esp_netif_create_default_wifi_sta()这个函数esp_wifi_default.h单数esp_wifiesp_netif_create_default_wifi_sta()/esp_netif_create_default_wifi_ap()的真正声明在这里而且esp_wifi.h内部已经#include esp_wifi_default.h所以只写esp_wifi.h其实也能编过——但我们建议显式写上esp_wifi_default.h你用了哪个函数就 include 哪个头别依赖搭便车将来重构时不会一脸茫然。网上不少教程/旧代码把函数声明记到esp_netif_defaults.h头上报“was not declared in this scope” 时先来这里对答案。中文 SSID 小提醒SSID 支持中文但它只是一坨字节路由器用 UTF-8 还是 GBK 编码各家不一一个汉字 UTF-8 占 3 字节、GBK 占 2 字节。设备扫到乱码SSID 而连不上时先怀疑编码不是硬件坏了家里路由器起个英文名能省去 90% 的麻烦。上面抄完第一件事那段讲的是占位符替换的坑大小写、0/O、结尾空格这里讲的是路由器那边 SSID 本身是中文时的坑。两回事别混。事件驱动模型的本质不是程序顺序等到连上而是注册回调 → 系统在事件发生时调用你。这就是嵌入式/网络的通用模型不要等等会来叫你。和第 15 章ISR 通知任务是同一个思想。16.3 拿到了 IP 之后能干什么三个方向先把这节出现的四个缩写说清各一句话够用HTTP浏览器/客户端和服务器一问一答的请求协议GET /api/speed?value60就是它TCP/UDP更底层的两条管道——TCP 保证顺序和送达打电话UDP 只管扔出去广播喊话WebSocket把 HTTP 的一问一答升级成一条长期双向的管道服务器能主动推消息给设备MQTT物联网专用的订阅频道协议——设备往motor/cmd这个主题发消息订阅了它的谁都收得到协议头比 HTTP 小得多很适合带宽差的设备。三个方向HTTP 客户端esp_http_client内置组件向服务器发请求TCP/UDP直接和上位机/手机 App 通信自定义协议HTTP 服务器 / WebSocket设备自己当服务器手机浏览器访问。例把电机项目升级成手机遥控小车手机 → HTTP/WebSocket → ESP32 → 解析指令 → motor.setSpeed(…)。网络任务收指令和电机任务执行之间用队列通信第 13 章。16.4 配网设备第一次怎么知道密码真实产品不能让用户改代码。常见方案先补两个词APAccess Point热点是别人来连我的那个角色SoftAP就是用芯片的软件做出一个 AP相对硬件 AP而言也就是让开发板自己变成一个 Wi-Fi 热点。STA 和 AP 两种角色 S3 可以同时担任。方案做法SmartConfig乐鑫方案手机 App 用 UDP 广播 SSID/密码设备监听捕获SoftAP 配网设备自己开热点上面的 SoftAP手机连上后填密码提交BLE 配网手机通过蓝牙把配置发给设备S3 支持预置出厂预烧一个默认 WiFi 配置入门先做预置 代码里改密码能跑通后再学 SmartConfig。16.5 资源与注意WiFi 使用 NVS 存储射频校准数据必须先nvs_flash_init否则启动报错再强调一遍NVS 里存的是校准数据不是你写的 SSID/密码WiFi 会占用较多内存约 200KB大内存应用注意和 PSRAM 配合天线/射频天线区域不要被金属遮挡天线下方尽量留空看门狗WiFi 连接是异步的不要在 app_main 里死等断线重连回调里只投消息重连在专门任务里做指数退避——16.2 的完整示例就是这么写的别在事件回调里 vTaskDelay。16.6 常见问题速查现象原因解决nvs_flash_init失败NVS 分区没建/损坏检查分区表、擦除 Flash日志反复断开原因码 201NO_AP_FOUND怎么都扫不到网络路由器只有 5GHz、SSID 隐藏、或 SSID 名字抄错大小写/0 与 O/结尾空格先看 16.1 末尾的2.4GHz 硬件前提框开 2.4G 频段或用手机热点 2.4G 模式测再核对WIFI_SSID的真值连不上路由器SSID/密码错原因码 202 多是密码错、信道问题核对配置、看 WIFI_EVENT 日志里的原因码拿到 IP 但请求失败DNS/网关问题DNS把域名翻译成 IP 的电话簿路由器没给好或外网不通都会栽在这用 IP 直连测试检查路由器重启后又要填 WiFi 密码以为esp_wifi 默认把配置存 NVS 了误会esp_wifi 存进 NVS 的只有射频校准数据SSID/密码存在 RAM 里的wifi_config_t断电即忘。想不丢就自己持久化把凭据写进 NVS第 14 章 nvs_handle 的用法开机读出来再esp_wifi_set_config启动很慢扫描信道固定信道可加速16.7 想一想 小测验想一想为什么esp_wifi_start()之后 app_main 可以结束事件驱动设备连上路由器后靠什么确认网络真的可用手机遥控小车的消息流里队列放在哪一层小测验WiFi 初始化前必须先初始化哪个存储A. PSRAM B. NVS C. SRAM拿到 IP 的事件是A. WIFI_EVENT_STA_START B. IP_EVENT_STA_GOT_IP C. WIFI_EVENT_AP_START断线重连的正确姿势是A. 死等 B. 定时重连/退避 C. 重启事件驱动模型的本质是什么16.8 本章总结分层应用 → LwIP → esp_netif → esp_wifi → 射频硬件前提S3 只有 2.4GHz路由器不给开 2.4G 就永远扫不到原因码 201STA 流程NVS → netif → 事件 → 模式 → 配置 → start → 等 GOT_IP事件驱动注册回调别死等拿到 IP 后可用 HTTP/TCP/MQTT可把电机升级成遥控小车配网方案SmartConfig / SoftAP / BLE产品级必配。下一章把工程做大——组件化与工程进阶。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进