:讓芯片“上網(wǎng)“)
ESP32-S3 自帶 Wi-Fi。本章講清楚①Wi-Fi 協(xié)議棧在芯片里的分層LwIP/TCP-IP/事件②STA連接路由器模式的完整流程與事件驅(qū)動模型③與前面章節(jié)的整合思路如通過手機控制電機④常見問題。目標不是讓你成為網(wǎng)絡(luò)專家而是能寫出第一個能上網(wǎng)的程序。16.1 芯片里的網(wǎng)絡(luò)分層先建立地圖一個網(wǎng)絡(luò)請求從應(yīng)用到天線經(jīng)過多層。ESP-IDF 幫你把下面這些層都封裝好了你的應(yīng)用HTTP 請求 / MQTT 消息 │ LwIPTCP/IP 協(xié)議棧TCP/UDP/IP 分包 │ esp_netif網(wǎng)絡(luò)接口抽象WiFi/以太網(wǎng) │ esp_wifiWiFi 驅(qū)動連接路由器、收發(fā)無線幀 │ 射頻硬件天線你主要打交道的是事件esp_eventWiFi 連上/斷開、拿到 IP 地址系統(tǒng)都會發(fā)出事件你注冊回調(diào)處理即可?!居布疤帷縀SP32-S3 只有 2.4GHz 的無線電。S3 的 Wi-Fi 只支持2.4GHz802.11 b/g/n不支持 5GHz。如果家里路由器是僅 5GHz的網(wǎng)絡(luò)或 2.4G 頻段被關(guān)掉/隱藏了 SSID芯片掃遍信道也找不到它癥狀就是日志里反復(fù)出現(xiàn)W (2311) wifi_sta: 斷開原因碼 201201 NO_AP_FOUND“沒看到這個 AP”。遇到 201 先懷疑頻段和 SSID不要懷疑板子壞了到路由器后臺打開 2.4GHz 頻段或單獨給 2.4G 起一個 SSID 名或者干脆用手機熱點開 2.4GHz 模式來測——這是本章所有示例的第一道關(guān)卡。【三個詞先說人話】SSID路由器報家門的名字一個字符串標簽區(qū)分大小寫IP設(shè)備在局域網(wǎng)里的門牌號別人要找你就報這個號IPv4 就是四段數(shù)字如 192.168.1.105DHCP路由器自動發(fā)門牌號的流程——設(shè)備喊一聲給我一個號路由器回一個空閑 IP順帶給出網(wǎng)關(guān)和 DNS。串成一句話憑 SSID 找到路由器 → 對暗號密碼→ DHCP 領(lǐng)到 IP → 這才算上網(wǎng)。注意領(lǐng)到 IP只代表你和路由器之間這條鏈路通了不代表外網(wǎng)可達DNS、網(wǎng)關(guān)、運營商都可能出問題見 16.6 的對應(yīng)行。16.2 STA 模式連接家里的路由器流程先看全局① nvs_flash_initWiFi 驅(qū)動要用 NVS 存射頻校準數(shù)據(jù)。注意只是校準數(shù)據(jù) SSID/密碼默認并不存在 NVS 里要應(yīng)用自己持久化見 16.6 的說明 ② esp_netif_init esp_event_loop_create_default網(wǎng)絡(luò)與事件系統(tǒng) ③ esp_netif_create_default_wifi_sta創(chuàng)建 STA 網(wǎng)絡(luò)接口 ④ esp_wifi_init初始化 WiFi 驅(qū)動 ⑤ 注冊事件回調(diào)WIFI_EVENT_STA_*、IP_EVENT_STA_GOT_IP ⑥ esp_wifi_set_mode(WIFI_MODE_STA) ⑦ esp_wifi_set_config(配置 SSID/密碼) ⑧ esp_wifi_start → 驅(qū)動開始掃描連接 ⑨ 收到 IP_EVENT_STA_GOT_IP → 局域網(wǎng)這條路通了能不能出外網(wǎng)還要看 DNS/網(wǎng)關(guān)見 16.6完整示例事件驅(qū)動寫法可直接抄進工程下面是一份完整的main/main.cpp新建工程、把它替換進去、改兩個宏再按后面的 CMakeLists 配好就能編譯燒錄對照第 3 章的工程骨架。這一版比最小骨架多了三樣?xùn)|西全都是踩坑修正include 列表補全原骨架缺esp_log.h和 FreeRTOS 頭文件編譯不過SSID/密碼用編譯期長度校驗寫入不再用strcpy往固定數(shù)組里硬塞斷線重連不在事件回調(diào)里睡 3 秒那是全書反復(fù)禁止的做法而是回調(diào)只投隊列、由專門的任務(wù)做指數(shù)退避重連。// 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 進去一旦超長就是// 越界寫內(nèi)存——程序當場不崩之后在莫名其妙的地方崩最難查的那種。// sizeof 宏字符串時把結(jié)尾 \0 也算 1 字節(jié)所以 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 太長加上結(jié)尾零字節(jié)最多 32 字節(jié));static_assert(sizeof(WIFI_PASS)sizeof(((wifi_config_t*)nullptr)-sta.password),密碼太長加上結(jié)尾零字節(jié)最多 64 字節(jié));// 回調(diào)和重連任務(wù)之間用隊列傳消息——第 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();// 驅(qū)動就緒發(fā)起第一次連接}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關(guān)聯(lián)失敗ESP_LOGW(TAG,斷開原因碼 %d,e-reason);// 鐵律回調(diào)里絕不 vTaskDelay默認事件循環(huán)只有一個任務(wù)// 你在這里睡 3 秒全系統(tǒng)所有事件都停 3 秒——和第 13/15 章// 回調(diào)要快、重活交給任務(wù)的原則直接沖突。// 這里只做一件事往隊列投一條消息超時給 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);// 通知重連任務(wù)鏈路已恢復(fù)// 到這里局域網(wǎng)這條路通了能不能出外網(wǎng)還要看 DNS/網(wǎng)關(guān)16.6}}// 專門的重連任務(wù)睡在這里不阻塞任何人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);// 睡在任務(wù)里事件循環(huán)毫無壓力esp_wifi_connect();// 指數(shù)退避1s → 2s → 4s → … 最多 60s避免路由器長時間掛掉時// 設(shè)備 1 秒一次地猛 reconnect費電、刷屏、還可能擠占掃描backoff(backoff*2kMaxBackoff)?kMaxBackoff:backoff*2;}}externCvoidapp_main(void){ESP_ERROR_CHECK(nvs_flash_init());// ① NVSESP_ERROR_CHECK(esp_netif_init());// ② 網(wǎng)絡(luò)ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();// ③ 接口// 隊列和重連任務(wù)要在 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));// ④ 驅(qū)動// ⑤ 事件回調(diào)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{};// 先清零兩個數(shù)組的剩余字節(jié)全為 0memcpy(cfg.sta.ssid,WIFI_SSID,sizeof(WIFI_SSID));// 長度已由memcpy(cfg.sta.password,WIFI_PASS,sizeof(WIFI_PASS));// static_assert 把關(guān)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());// ⑧// 之后一切靠事件驅(qū)動 重連任務(wù)app_main 可以結(jié)束}配套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已拆分不矛盾——被拆掉的只是外設(shè)驅(qū)動事件循環(huán)、Wi-Fi、NVS 這些仍要顯式 REQUIRES。抄完第一件事替換那兩個中文占位符#define WIFI_SSID 你的路由器名和#define WIFI_PASS 你的密碼里的中文只是占位符必須整對引號換成真值四個細節(jié)最容易翻車SSID區(qū)分大小寫Home和home在空口上是兩個不同的網(wǎng)絡(luò)肉眼難分的字符別抄錯0零≠O、1一≠l小寫 L、B≠8引號內(nèi)結(jié)尾不要留空格MyWiFi 和MyWiFi也是兩個網(wǎng)絡(luò)中文 SSID 能連但編碼坑多下面另有一小節(jié)測試期先起個英文名最省事。改完必須重新idf.py build——宏是編譯期展開進二進制的不重編等于沒改?!緞邮挚颉竣?在哪執(zhí)行新建一個空工程第 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③ 預(yù)期看到編譯末尾文件名跟著你project(...)里起的工程名走這個示例代碼沒幾行bin 卻有 754KB0xb82d0——Wi-Fi 協(xié)議棧本身就是這么大一塊對照第 5 章 l298n 工程的 172KB 就能感受到 16.5 說的那句WiFi 很吃內(nèi)存/體積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串口輸出見下面跑起來應(yīng)該看到什么。④ 沒看到a) 編譯報esp_netif_create_default_wifi_sta was not declared→ 去本節(jié)頭文件迷宮那張表b) 報端口占用 /PermissionError(13)→ 關(guān)掉另一個監(jiān)視器或串口助手2.6 那張表的對應(yīng)行c) 反復(fù)原因碼 201→ 回到 16.1 末尾的2.4GHz 硬件前提框。跑起來應(yīng)該看到什么下面是這一路走下來該看到的片段每行的格式都按 v6.0.1 本地源碼/預(yù)編譯庫里的日志語句核對過毫秒數(shù)、MAC、任務(wù)句柄、信道、RSSI、IP 會因環(huán)境不同而不同要對的是行的內(nèi)容和先后順序驅(qū)動起來 → 握手 → 已連上路由器 → 拿到 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 ← 我們的回調(diào)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 領(lǐng)到門牌號帶wifi_sta的兩行是我們自己的ESP_LOGITAG 來自代碼帶wifi:的是驅(qū)動打的TAG 就叫wifi別以為是自己的日志讀個關(guān)鍵詞就行state: init - auth - assoc - run走通、并出現(xiàn)connected with 你的 SSID就說明 SSID 找對了、密碼也對密碼錯會停在 auth 前并報原因碼 202/203最后一行之后程序就沒事了本示例只做連接監(jiān)視器還在滾按Ctrl]退出監(jiān)視器2.8 第 4 步的老規(guī)矩退出后才能再次燒錄。想驗證網(wǎng)絡(luò)真的能用把esp_wifi_start()之后的邏輯換成 16.3 說的 HTTP 請求或者先到路由器管理頁看這臺設(shè)備有沒有出現(xiàn)在客戶端列表里。頭文件迷宮esp_netif_defaults.h ≠ esp_wifi_default.h上例最容易抄錯的是那兩個長得像雙胞胎的頭文件這里一次講清v6.0.1 本地源碼核實頭文件住在哪個組件里面有什么esp_netif_defaults.h注意復(fù)數(shù)esp_netif只有一堆默認配置宏/結(jié)構(gòu)體如ESP_NETIF_DEFAULT_WIFI_STA()沒有esp_netif_create_default_wifi_sta()這個函數(shù)esp_wifi_default.h單數(shù)esp_wifiesp_netif_create_default_wifi_sta()/esp_netif_create_default_wifi_ap()的真正聲明在這里而且esp_wifi.h內(nèi)部已經(jīng)#include esp_wifi_default.h所以只寫esp_wifi.h其實也能編過——但我們建議顯式寫上esp_wifi_default.h你用了哪個函數(shù)就 include 哪個頭別依賴搭便車將來重構(gòu)時不會一臉茫然。網(wǎng)上不少教程/舊代碼把函數(shù)聲明記到esp_netif_defaults.h頭上報“was not declared in this scope” 時先來這里對答案。中文 SSID 小提醒SSID 支持中文但它只是一坨字節(jié)路由器用 UTF-8 還是 GBK 編碼各家不一一個漢字 UTF-8 占 3 字節(jié)、GBK 占 2 字節(jié)。設(shè)備掃到亂碼SSID 而連不上時先懷疑編碼不是硬件壞了家里路由器起個英文名能省去 90% 的麻煩。上面抄完第一件事那段講的是占位符替換的坑大小寫、0/O、結(jié)尾空格這里講的是路由器那邊 SSID 本身是中文時的坑。兩回事別混。事件驅(qū)動模型的本質(zhì)不是程序順序等到連上而是注冊回調(diào) → 系統(tǒng)在事件發(fā)生時調(diào)用你。這就是嵌入式/網(wǎng)絡(luò)的通用模型不要等等會來叫你。和第 15 章ISR 通知任務(wù)是同一個思想。16.3 拿到了 IP 之后能干什么三個方向先把這節(jié)出現(xiàn)的四個縮寫說清各一句話夠用HTTP瀏覽器/客戶端和服務(wù)器一問一答的請求協(xié)議GET /api/speed?value60就是它TCP/UDP更底層的兩條管道——TCP 保證順序和送達打電話UDP 只管扔出去廣播喊話WebSocket把 HTTP 的一問一答升級成一條長期雙向的管道服務(wù)器能主動推消息給設(shè)備MQTT物聯(lián)網(wǎng)專用的訂閱頻道協(xié)議——設(shè)備往motor/cmd這個主題發(fā)消息訂閱了它的誰都收得到協(xié)議頭比 HTTP 小得多很適合帶寬差的設(shè)備。三個方向HTTP 客戶端esp_http_client內(nèi)置組件向服務(wù)器發(fā)請求TCP/UDP直接和上位機/手機 App 通信自定義協(xié)議HTTP 服務(wù)器 / WebSocket設(shè)備自己當服務(wù)器手機瀏覽器訪問。例把電機項目升級成手機遙控小車手機 → HTTP/WebSocket → ESP32 → 解析指令 → motor.setSpeed(…)。網(wǎng)絡(luò)任務(wù)收指令和電機任務(wù)執(zhí)行之間用隊列通信第 13 章。16.4 配網(wǎng)設(shè)備第一次怎么知道密碼真實產(chǎn)品不能讓用戶改代碼。常見方案先補兩個詞APAccess Point熱點是別人來連我的那個角色SoftAP就是用芯片的軟件做出一個 AP相對硬件 AP而言也就是讓開發(fā)板自己變成一個 Wi-Fi 熱點。STA 和 AP 兩種角色 S3 可以同時擔任。方案做法SmartConfig樂鑫方案手機 App 用 UDP 廣播 SSID/密碼設(shè)備監(jiān)聽捕獲SoftAP 配網(wǎng)設(shè)備自己開熱點上面的 SoftAP手機連上后填密碼提交BLE 配網(wǎng)手機通過藍牙把配置發(fā)給設(shè)備S3 支持預(yù)置出廠預(yù)燒一個默認 WiFi 配置入門先做預(yù)置 代碼里改密碼能跑通后再學 SmartConfig。16.5 資源與注意WiFi 使用 NVS 存儲射頻校準數(shù)據(jù)必須先nvs_flash_init否則啟動報錯再強調(diào)一遍NVS 里存的是校準數(shù)據(jù)不是你寫的 SSID/密碼WiFi 會占用較多內(nèi)存約 200KB大內(nèi)存應(yīng)用注意和 PSRAM 配合天線/射頻天線區(qū)域不要被金屬遮擋天線下方盡量留空看門狗WiFi 連接是異步的不要在 app_main 里死等斷線重連回調(diào)里只投消息重連在專門任務(wù)里做指數(shù)退避——16.2 的完整示例就是這么寫的別在事件回調(diào)里 vTaskDelay。16.6 常見問題速查現(xiàn)象原因解決nvs_flash_init失敗NVS 分區(qū)沒建/損壞檢查分區(qū)表、擦除 Flash日志反復(fù)斷開原因碼 201NO_AP_FOUND怎么都掃不到網(wǎng)絡(luò)路由器只有 5GHz、SSID 隱藏、或 SSID 名字抄錯大小寫/0 與 O/結(jié)尾空格先看 16.1 末尾的2.4GHz 硬件前提框開 2.4G 頻段或用手機熱點 2.4G 模式測再核對WIFI_SSID的真值連不上路由器SSID/密碼錯原因碼 202 多是密碼錯、信道問題核對配置、看 WIFI_EVENT 日志里的原因碼拿到 IP 但請求失敗DNS/網(wǎng)關(guān)問題DNS把域名翻譯成 IP 的電話簿路由器沒給好或外網(wǎng)不通都會栽在這用 IP 直連測試檢查路由器重啟后又要填 WiFi 密碼以為esp_wifi 默認把配置存 NVS 了誤會esp_wifi 存進 NVS 的只有射頻校準數(shù)據(jù)SSID/密碼存在 RAM 里的wifi_config_t斷電即忘。想不丟就自己持久化把憑據(jù)寫進 NVS第 14 章 nvs_handle 的用法開機讀出來再esp_wifi_set_config啟動很慢掃描信道固定信道可加速16.7 想一想 小測驗想一想為什么esp_wifi_start()之后 app_main 可以結(jié)束事件驅(qū)動設(shè)備連上路由器后靠什么確認網(wǎng)絡(luò)真的可用手機遙控小車的消息流里隊列放在哪一層小測驗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. 重啟事件驅(qū)動模型的本質(zhì)是什么16.8 本章總結(jié)分層應(yīng)用 → LwIP → esp_netif → esp_wifi → 射頻硬件前提S3 只有 2.4GHz路由器不給開 2.4G 就永遠掃不到原因碼 201STA 流程NVS → netif → 事件 → 模式 → 配置 → start → 等 GOT_IP事件驅(qū)動注冊回調(diào)別死等拿到 IP 后可用 HTTP/TCP/MQTT可把電機升級成遙控小車配網(wǎng)方案SmartConfig / SoftAP / BLE產(chǎn)品級必配。下一章把工程做大——組件化與工程進階。