Wi-Fi 联网基础
2026/7/8 6:39:53 网站建设 项目流程

前七章我们一直在"本地"打转——控制GPIO、读传感器、刷屏幕、播放音频。这些能力很实用,但ESP32-S3真正的灵魂在于它那颗Wi-Fi + 蓝牙的无线芯。

回想我第一次让ESP32连上Wi-Fi的场景:串口打印出获取到的IP地址那一刻,我意识到这不再是普通的单片机了——它是一台能上网的微型计算机。从那以后,我的IoT项目再也没用过有线。

本章我们系统掌握ESP32-S3的Wi-Fi联网能力,从Station模式到SoftAP,从TCP Socket到HTTP请求,最后实战获取NTP网络时间。


一、Wi-Fi 硬件与协议栈

1.1 无线规格

ESP32-S3 集成了完整的 Wi-Fi 基带和射频前端:

参数规格
标准IEEE 802.11 b/g/n
频段2.4 GHz(2400~2483.5 MHz)
信道带宽20 MHz / 40 MHz
最高速率150 Mbps(802.11n, HT40, MCS7)
安全协议WPA / WPA2 / WPA3 / WPA-Enterprise
天线板载 PCB 天线或外部 IPEX 天线
工作模式Station、SoftAP、Station+AP 共存

1.2 软件协议栈

从物理层到应用层,ESP-IDF 提供了完整的网络软件栈:

应用层 HTTP / MQTT / WebSocket / NTP 传输层 TCP / UDP(由 lwIP 协议栈实现) 网络层 IPv4 / IPv6 链路层 802.11 MAC(Wi-Fi 驱动) 物理层 802.11 PHY(射频硬件)

ESP-IDF 内部集成了lwIP(lightweight IP)协议栈——这是嵌入式领域最流行的 TCP/IP 实现。我们不需要自己处理 TCP 握手、IP 分片等底层细节,只需调用 socket API 即可。


二、Wi-Fi 事件驱动编程模型

ESP32 的 Wi-Fi 编程有一个核心理念:事件驱动。你不会写一个"阻塞等待连接"的循环,而是注册事件回调函数,让系统在 Wi-Fi 状态变化时通知你。

2.1 关键事件

事件含义
WIFI_EVENT_STA_STARTStation 模式启动完成
WIFI_EVENT_STA_CONNECTED已连接到 AP(路由器)
WIFI_EVENT_STA_DISCONNECTED与 AP 断开连接
IP_EVENT_STA_GOT_IPStation 获取到 IP 地址
WIFI_EVENT_AP_STARTSoftAP 启动完成
WIFI_EVENT_AP_STACONNECTED有设备连接到了热点

WIFI_EVENT_xxxIP_EVENT_xxx是两类不同的事件组,分别由 Wi-Fi 驱动和 IP 协议栈触发,注册时注意区分。

2.2 事件循环

ESP-IDF 提供了一个全局的事件循环库esp_event),任何组件都可以通过它发布和监听事件。我们不需要自己实现观察者模式——系统已经帮你做好了。

// 创建默认事件循环esp_event_loop_create_default();// 注册 Wi-Fi 和 IP 事件处理函数esp_event_handler_instance_register(WIFI_EVENT,// 事件基:Wi-Fi 事件ESP_EVENT_ANY_ID,// 所有 Wi-Fi 事件&wifi_event_handler,// 回调函数NULL,// 回调参数NULL);esp_event_handler_instance_register(IP_EVENT,// 事件基:IP 事件IP_EVENT_STA_GOT_IP,// 获取到 IP&ip_event_handler,NULL,NULL);

三、实战一:Station 模式连接路由器

这是 Wi-Fi 最常用模式——ESP32-S3 作为客户端连上你家的路由器,获取 IP 地址后访问互联网。

3.1 完整代码

#include<stdio.h>#include<string.h>#include"freertos/FreeRTOS.h"#include"freertos/task.h"#include"freertos/event_groups.h"#include"esp_system.h"#include"esp_wifi.h"#include"esp_event.h"#include"esp_log.h"#include"nvs_flash.h"#include"lwip/err.h"#include"lwip/sys.h"#defineWIFI_SSID"YourRouterSSID"#defineWIFI_PASS"YourRouterPassword"#defineMAX_RETRY5staticconstchar*TAG="WiFi_Station";staticints_retry_count=0;staticEventGroupHandle_t s_wifi_event_group;constintWIFI_CONNECTED_BIT=BIT0;// Wi-Fi 事件处理staticvoidwifi_event_handler(void*arg,esp_event_base_tevent_base,int32_tevent_id,void*event_data){if(event_base==WIFI_EVENT&&event_id==WIFI_EVENT_STA_START){esp_wifi_connect();ESP_LOGI(TAG,"Connecting to AP...");}elseif(event_base==WIFI_EVENT&&event_id==WIFI_EVENT_STA_DISCONNECTED){if(s_retry_count<MAX_RETRY){esp_wifi_connect();s_retry_count++;ESP_LOGI(TAG,"Retry to connect (attempt %d)",s_retry_count);}else{xEventGroupSetBits(s_wifi_event_group,WIFI_CONNECTED_BIT);ESP_LOGE(TAG,"Connection failed after %d retries",MAX_RETRY);}}}// IP 事件处理staticvoidip_event_handler(void*arg,esp_event_base_tevent_base,int32_tevent_id,void*event_data){if(event_id==IP_EVENT_STA_GOT_IP){ip_event_got_ip_t*event=(ip_event_got_ip_t*)event_data;ESP_LOGI(TAG,"Got IP: "IPSTR,IP2STR(&event->ip_info.ip));s_retry_count=0;xEventGroupSetBits(s_wifi_event_group,WIFI_CONNECTED_BIT);}}voidwifi_sta_init(void){// 1. 初始化 NVS(Wi-Fi 配置存储需要)esp_err_tret=nvs_flash_init();if(ret==ESP_ERR_NVS_NO_FREE_PAGES||ret==ESP_ERR_NVS_NEW_VERSION_FOUND){ESP_ERROR_CHECK(nvs_flash_erase());ret=nvs_flash_init();}ESP_ERROR_CHECK(ret);// 2. 创建事件组s_wifi_event_group=xEventGroupCreate();// 3. 初始化网络接口ESP_ERROR_CHECK(esp_netif_init());// 4. 创建默认事件循环ESP_ERROR_CHECK(esp_event_loop_create_default());// 5. 创建默认 Station 网络接口esp_netif_create_default_wifi_sta();// 6. 初始化 Wi-Fi 驱动wifi_init_config_tcfg=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));// 7. 注册事件处理器ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,&wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,IP_EVENT_STA_GOT_IP,&ip_event_handler,NULL,NULL));// 8. 配置并启动 Wi-Fiwifi_config_twifi_config={.sta={.ssid=WIFI_SSID,.password=WIFI_PASS,.threshold.authmode=WIFI_AUTH_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG,"Wi-Fi Station initialized, SSID: %s",WIFI_SSID);}voidapp_main(void){wifi_sta_init();// 等待 Wi-Fi 连接EventBits_t bits=xEventGroupWaitBits(s_wifi_event_group,WIFI_CONNECTED_BIT,pdFALSE,pdFALSE,portMAX_DELAY);if(bits&WIFI_CONNECTED_BIT){ESP_LOGI(TAG,"Wi-Fi connected successfully!");}// 后续可在此处启动 TCP 通信等}

这是任何 Wi-Fi 项目的标准模板,学会这个,后续的 HTTP、MQTT、WebSocket 全都建立在这一套初始化的基础之上。

3.2 断线重连机制

上述代码中已经包含了一个简单的重连机制:断开后尝试重连MAX_RETRY(5次)。在实际产品中,你可能还需要:

  • 指数退避:重连间隔从 1s、2s、4s……逐渐拉长,避免频繁重连加重路由器负担
  • 保存上次成功的 SSID/密码到 NVS,掉电不丢失
  • AP 扫描 + 信号强度排序:当有多个已知网络时自动选择最佳的那个
// 指数退避重连示例staticvoidwifi_reconnect_with_backoff(void){staticintbackoff=1;ESP_LOGI(TAG,"Reconnecting in %d seconds...",backoff);vTaskDelay(pdMS_TO_TICKS(backoff*1000));esp_wifi_connect();if(backoff<32)backoff*=2;// 最多 32 秒}

四、实战二:SoftAP 模式创建热点

有时候我们希望 ESP32-S3 自身作为热点,让手机或电脑连接上来——比如在配网模式、或作为独立服务器时。

4.1 代码实现

voidwifi_ap_init(void){ESP_ERROR_CHECK(nvs_flash_init());ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());// 创建 SoftAP 网络接口esp_netif_create_default_wifi_ap();wifi_init_config_tcfg=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));// 配置热点wifi_config_twifi_config={.ap={.ssid="ESP32S3_AP",.ssid_len=strlen("ESP32S3_AP"),.password="12345678",.max_connection=4,// 最大连接设备数.authmode=WIFI_AUTH_WPA2_PSK,.channel=6,// 默认信道},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP,&wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG,"SoftAP started! SSID: ESP32S3_AP, Password: 12345678");ESP_LOGI(TAG,"SoftAP IP: 192.168.4.1");// 默认网关}

ESP32 SoftAP 默认 IP 为192.168.4.1,DHCP 范围为192.168.4.2192.168.4.101。连上热点后,在浏览器输入192.168.4.1就能访问 ESP32 上的网页服务。

4.2 Station + AP 共存模式

ESP32-S3 支持Station 和 SoftAP 同时运行,这意味着:

  • ESP32 一边连接路由器上网(访问 NTP、MQTT 服务器)
  • 一边作为热点,让手机直连查看设备状态
// 配置共存模式wifi_config_tsta_config={.sta={.ssid="RouterSSID",.password="RouterPass"}};wifi_config_tap_config={.ap={.ssid="ESP32S3_AP",.password="12345678"}};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));// 共存模式ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&sta_config));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP,&ap_config));ESP_ERROR_CHECK(esp_wifi_start());

共存模式下,Station 和 AP 使用同一个射频,不能同时收发数据。实际测试中,AP 的数据吞吐量大约会下降 30%~50%。


五、实战三:TCP Socket 通信(HTTP GET)

有了 IP 地址,接下来就是 TCP/IP 通信了。lwIP 提供了标准的Berkeley Socket API,和 Linux/桌面平台的 socket 编程几乎一致。

5.1 HTTP GET 请求

#include"lwip/sockets.h"#include"lwip/netdb.h"voidhttp_get_example(void){constchar*host="httpbin.org";constchar*request="GET /ip HTTP/1.1\r\n""Host: httpbin.org\r\n""Connection: close\r\n""\r\n";// 1. DNS 解析structhostent*h=gethostbyname(host);if(!h){ESP_LOGE(TAG,"DNS lookup failed");return;}structin_addraddr=*(structin_addr*)h->h_addr_list[0];ESP_LOGI(TAG,"DNS resolved: %s -> "IPSTR,host,IP2STR(&addr));// 2. 创建 Socketintsock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(sock<0){ESP_LOGE(TAG,"Socket creation failed");return;}// 3. 连接服务器structsockaddr_indest={.sin_family=AF_INET,.sin_port=htons(80),.sin_addr=addr,};if(connect(sock,(structsockaddr*)&dest,sizeof(dest))!=0){ESP_LOGE(TAG,"Connection failed");close(sock);return;}ESP_LOGI(TAG,"Connected to %s:%d",host,80);// 4. 发送 HTTP 请求intsent=send(sock,request,strlen(request),0);ESP_LOGI(TAG,"Sent %d bytes",sent);// 5. 接收响应charrecv_buf[1024];intlen;do{len=recv(sock,recv_buf,sizeof(recv_buf)-1,0);if(len>0){recv_buf[len]='\0';printf("%s",recv_buf);// 打印 HTTP 响应}}while(len>0);// 6. 关闭连接close(sock);ESP_LOGI(TAG,"Connection closed");}

5.2 使用 esp_http_client(推荐)

手写 Socket 请求虽然灵活,但直接处理 HTTP 协议细节(头解析、chunked 编码、重定向等)工程量不小。ESP-IDF 提供了封装的esp_http_client

#include"esp_http_client.h"esp_err_t_http_event_handler(esp_http_client_event_t*evt){switch(evt->event_id){caseHTTP_EVENT_ON_DATA:printf("%.*s",evt->data_len,(char*)evt->data);break;default:break;}returnESP_OK;}voidhttp_get_with_lib(void){esp_http_client_config_tconfig={.url="http://httpbin.org/ip",.event_handler=_http_event_handler,.timeout_ms=5000,};esp_http_client_handle_tclient=esp_http_client_init(&config);esp_http_client_perform(client);esp_http_client_cleanup(client);}

推荐在项目中使用esp_http_client而不是裸 Socket——它帮你处理了 HTTP 协议的大多数复杂细节,代码也更简洁。


六、实战:联网获取 NTP 时间

有了 Wi-Fi 连接和基本的 HTTP 能力,我们做一个实用的功能:从 NTP 服务器获取标准时间。这是几乎所有 IoT 设备的刚需——日志打时间戳、定时任务、数据上报都需要准确的时间。

6.1 使用 ESP-IDF 的 SNTP 组件

#include"esp_sntp.h"#include"esp_netif_sntp.h"voidobtain_time(void){// 初始化 SNTPesp_sntp_config_tconfig=ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");esp_netif_sntp_init(&config);// 等待时间同步(最多 10 秒)intretry=0;constintmax_retry=10;while(sntp_get_sync_status()==SNTP_SYNC_STATUS_RESET&&retry<max_retry){vTaskDelay(pdMS_TO_TICKS(1000));retry++;}if(retry>=max_retry){ESP_LOGE(TAG,"NTP sync timeout");return;}// 获取并打印当前时间time_tnow=0;structtmtimeinfo={0};time(&now);localtime_r(&now,&timeinfo);charstrftime_buf[64];strftime(strftime_buf,sizeof(strftime_buf),"%Y-%m-%d %H:%M:%S",&timeinfo);ESP_LOGI(TAG,"NTP time synchronized: %s",strftime_buf);// 重要:将时区设为北京时间(UTC+8)setenv("TZ","CST-8",1);tzset();}

将这段代码放在wifi_sta_init()和等待WIFI_CONNECTED_BIT之后,你的 ESP32-S3 就拥有准确的北京时间了。

6.2 时区设置

// 中国标准时间(UTC+8)setenv("TZ","CST-8",1);// 日本标准时间(UTC+9)setenv("TZ","JST-9",1);// 美国东部时间(UTC-5)setenv("TZ","EST+5EDT,M3.2.0/2,M11.1.0/2",1);

setenv("TZ", "...", 1)中的1表示覆盖已有值。POSIX 时区格式中,数字表示与 UTC 的偏移,正负号的意义与常识相反:UTC+8 写作CST-8(减 8 小时),UTC-5 写作EST+5(加 5 小时)——我第一次用就在这里栽了跟头。


🧠 深度思考

Wi-Fi 编程的本质不是发数据,而是处理不确定性。

有线和无线的根本区别在于:有线通信的链路状态是确定的(线插上了就是通,掉了就是断),而无线通信的链路状态永远是一个概率问题。信号衰减、同频干扰、AP 重启、DHCP 租约过期——任何一环出问题,你的设备就断网了。

这引出了一个非常重要的设计理念:所有无线 IoT 设备都必须以"断线是常态,连上算运气"为前提来设计架构。具体来说:

  1. 所有上行数据必须本地缓存——Wi-Fi 断了不能丢数据,要等恢复后补发
  2. 重连必须有限制——无限重连会导致死循环消耗电量,也影响路由器性能
  3. 要有离线降级能力——设备断网后不能变"砖头",本地功能应继续正常运行

我在做一个远程温控器时就吃过这样的亏:产品在家里断网了,结果设备直接卡死在重连循环中,本地温控逻辑完全不执行。后来加了超时保护——30 秒连不上就跳过联网,优先保证本地控制功能。这个教训的价值远远超过了我之前学的所有 API 用法。


⚠️ 避坑指南 / 注意事项

  1. NVS 初始化必须放在 Wi-Fi 驱动之前
  • Wi-Fi 配置(SSID、密码等)存储在 NVS(非易失性存储)分区中
  • 如果nvs_flash_init()失败,后续esp_wifi_init()也会失败
  • 首次烧录新固件时,NVS 分区可能未初始化,需要先擦除再初始化
  1. 事件处理函数中不能做耗时操作
  • Wi-Fi 事件回调是在系统任务上下文中执行的
  • 不要在事件回调中调用vTaskDelay()或执行长时间阻塞操作
  • 正确做法:在回调中通过EventGroupQueuexTaskNotify通知其他任务处理
  1. Station + AP 共存模式的性能限制
  • 两个模式共享同一个射频,不能同时收发
  • 实测共存时 Station 吞吐量约下降 30%~50%
  • 如果两个模式都需要高带宽,考虑使用双核分工或分时调度
  1. SoftAP 模式下密码长度不能少于 8 位
  • WPA2-PSK 协议要求密码至少 8 个字符,设置短密码会导致连接失败
  • 如果不设密码(开放热点),将authmode设为WIFI_AUTH_OPEN且不设password字段
  1. 断线重连的陷阱
  • WIFI_EVENT_STA_DISCONNECTED在连接成功前也会触发
  • 不要在GOT_IP之前就认为连接失败
  • 重连间隔不要为零——无限快速重连会导致"Wi-Fi 风暴",影响路由器和其他设备
  1. DNS 解析可能失败
  • gethostbyname()是阻塞的,超时时间由 lwIP 配置决定(默认约 5 秒)
  • 在生产代码中,建议将 Socket 通信放在独立任务中,避免阻塞主任务
  • 如果使用esp_http_client,它内部已经处理了 DNS 和超时
  1. 时区设置的坑(必看)
  • POSIX 时区格式CST-8表示UTC+8——数字符号与常见理解相反
  • setenv("TZ", "CST-8", 1)后必须调用tzset()才能生效
  • NTP 同步返回的是 UTC 时间,需要结合时区设置转换为本地时间
  1. 天线匹配
  • 板载 PCB 天线比外部 IPEX 天线增益低约 2~3 dB
  • 如果使用外部天线,确保在 menuconfig 中选择了正确的天线类型
  • 金属外壳会严重衰减 Wi-Fi 信号,需预留天线位置或使用外置天线

总结

本章我们让 ESP32-S3 真正"上网"了:

  1. Wi-Fi 硬件与协议栈:802.11 b/g/n、lwIP TCP/IP 协议栈
  2. 事件驱动编程模型:Wi-Fi 事件循环、Station / SoftAP 模式
  3. 实战一:Station 模式——连接路由器、重连机制与指数退避
  4. 实战二:SoftAP 模式——创建热点、Station+AP 共存
  5. 实战三:TCP Socket 通信——HTTP GET 请求与 esp_http_client
  6. NTP 时间同步:获取标准时间、时区配置

从本地控制到联网通信,ESP32-S3 的能力跃升了一个维度。下一章,我们将基于 Wi-Fi 连接,进一步探索MQTT 协议与物联网云平台接入——让设备数据真正上云,实现远程监控与控制。


本文基于 ESP-IDF v5.3 编写。Wi-Fi SSID 和密码请替换为实际路由器信息。NTP 服务器使用公共pool.ntp.org,局域网内可使用路由器地址作为 NTP 服务器以降低延迟。天线类型在menuconfigComponent configESP32S3-specificWi-Fi中配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询