前言
在 ESP32 物联网项目中,Wi-Fi 往往是最先暴露问题的模块:设备刚上电时无法连接热点,运行一段时间后 MQTT 离线,执行舵机或电机动作时突然掉线,或者看似网络正常、业务请求却不断超时。这些现象常被统一归结为“Wi-Fi 不稳定”,于是开发者会不断调用 WiFi.begin()、缩短重连周期,甚至直接重启设备。
但 Wi-Fi 不稳定通常不是单一原因造成的。无线链路的信号强度和干扰会影响关联与重传;供电在射频发射或外设启动瞬间产生的压降,可能导致 Brownout、复位或射频异常;应用中的忙等、长回调和不合理的 FreeRTOS 任务调度,也会让网络协议栈无法及时运行。它们最终都可能表现为 MQTT 断开、HTTP 超时或频繁重连。
本文面向使用 Arduino-ESP32 或 ESP-IDF 开发 ESP32 设备的开发者,围绕“如何获得证据并定位根因”展开。文章不会把重连代码当作万能修复方案,而是从最小复现、事件日志和对照实验开始,依次检查无线环境、信号与天线、供电完整性,以及任务阻塞和看门狗问题。
建议按正文顺序执行:先确认 Wi-Fi 是否真的断开,再用靠近 AP、替换电源、拆除负载等实验隔离变量,最后审查连接状态机和任务调度。完成这些步骤后,才能针对根因做出稳定且可验证的修复。
1. 先区分故障现象
先回答下面几个问题。不同答案决定后续排查方向:
| 现象 | 优先怀疑方向 | 关键证据 |
|---|---|---|
| 根本扫描不到目标 AP | 频段、信道、SSID 配置、距离 | 扫描结果、路由器信道 |
| 能连接但很快断开 | RSSI、认证、供电、任务阻塞 | Wi-Fi 断开原因码、串口日志 |
| Wi-Fi 未断但 MQTT/HTTP 失败 | 服务端、DNS、网络可达性、应用阻塞 | IP、网关 ping、业务日志 |
| 只有电机、继电器、屏幕亮起时断网或重启 | 供电压降、电磁干扰 | Brownout 日志、电源波形 |
| 运行一段时间后随机断连 | 内存泄漏、长阻塞、重连状态机 | 堆内存、任务运行时间、事件日志 |
| 靠近路由器后明显改善 | 信号、天线、遮挡、干扰 | RSSI 与丢包率对比 |
第一原则:不要把业务连接失败直接当作 Wi-Fi 断线。 先确认 STA 是否仍关联到 AP、是否保有 IPv4 地址、默认路由和 DNS 是否仍然可用。
2. 建立最小可复现实验
在改业务代码前,准备一个只负责连接、打印状态和周期性上报 RSSI 的最小程序。测试期间先断开电机、舵机、继电器、大功率 LED 和其他外设,供电改为可靠的 USB 电源或实验室电源。
建议至少记录以下信息:
-
固件版本、开发板型号、天线类型和供电方式。
-
AP 的 2.4 GHz SSID、信道、加密方式和距离。
-
连接、获取 IP、断开、重连事件及断开原因码。
-
每次业务失败时的 RSSI、空闲堆内存、复位原因。
-
复现操作,例如“舵机转动后 3 秒内断开”或“持续上传 20 分钟后超时”。
Arduino 框架下可先使用如下骨架。它不解决问题,但能把“连接失败”和“连接后断开”区分开。
#include <WiFi.h>
constexpr char WIFI_SSID[] = "your-ssid";
constexpr char WIFI_PASSWORD[] = "your-password";
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("Wi-Fi associated with AP");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.printf("Got IP: %s, RSSI: %d dBm\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI());
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.printf("Wi-Fi disconnected, reason: %d\n",
info.wifi_sta_disconnected.reason);
break;
default:
break;
}
}
void setup() {
Serial.begin(115200);
WiFi.onEvent(onWiFiEvent);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false); // 仅用于排查,确认是否与省电策略有关
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
void loop() {
static uint32_t lastReport = 0;
if (millis() - lastReport >= 5000) {
lastReport = millis();
Serial.printf("status=%d rssi=%d heap=%u\n",
WiFi.status(), WiFi.RSSI(), ESP.getFreeHeap());
}
delay(10);
}
WiFi.setSleep(false) 会提高功耗,不应在没有验证的情况下作为永久修复。它的意义是隔离变量:若关闭省电后现象消失,再评估是否需要调整省电模式、DTIM 或业务心跳。
3. 信号与无线环境排查
3.1 正确理解 RSSI
RSSI 是接收信号强度,单位为 dBm,数值越接近 0 信号越强。它不是吞吐率,也不能单独代表无线质量,但适合做现场快速判断。
| RSSI 范围 | 现场判断 | 建议 |
|---|---|---|
-50 dBm 以上 |
很好 | 一般不应由信号导致频繁断线 |
-50 到 -67 dBm |
可用且稳定 | 继续检查干扰和供电 |
-67 到 -75 dBm |
边缘可用 | 调整位置、天线和信道后复测 |
-75 dBm 以下 |
高风险 | 优先改善覆盖,不要只靠重连 |
不要只看某一次 RSSI。应在设备静止、设备工作、距离变化和干扰源开启四种状态下各采样一段时间。若 RSSI 波动超过约 10 dB,通常说明遮挡、天线接触、供电噪声或环境干扰存在变量。
3.2 确认 2.4 GHz 兼容性
多数 ESP32 仅支持 2.4 GHz Wi-Fi。手机能连上不代表 ESP32 能连上,因为手机可能连接的是 5 GHz 或 6 GHz 频段。
检查路由器侧配置:
-
为测试创建独立的 2.4 GHz SSID,避免频段合一功能干扰判断。
-
信道先固定为
1、6或11之一,不要使用自动信道作为首轮排查条件。 -
频道带宽优先设为
20 MHz,排除 40 MHz 环境下的兼容性与拥堵因素。 -
加密方式优先使用 WPA2-PSK/AES 或 WPA2/WPA3 混合模式;旧 ESP32 Arduino Core 或 ESP-IDF 版本可能不适配仅 WPA3 的网络。
-
SSID 不要隐藏;测试时避免中文、空格和特殊字符,排除编码与配置输入问题。
-
检查 AP 是否开启了 MAC 地址过滤、客户端隔离或接入数量限制。
3.3 天线、摆放与干扰
PCB 天线附近必须留出空间。不要让金属外壳、电池、屏幕排线、USB 线束或地平面紧贴天线区域;外置天线版本还要确认 IPEX 座、天线开关和射频路径是否配置正确。
快速验证方法:
-
将 ESP32 放到距离路由器 1 到 2 米、无遮挡的位置。
-
断开所有可能产生干扰的大功率外设。
-
固定 AP 信道后连续运行 15 分钟,记录 RSSI 和断开次数。
-
仅改变距离或仅开启一个外设,再做对照。
靠近 AP 后稳定,不代表程序没有问题,但说明无线链路裕量不足是重要变量。此时优先改善安装位置、天线朝向和 AP 覆盖,而不是把重连间隔调得更短。
4. 供电与复位排查
Wi-Fi 在发射、关联、DHCP、重连时会出现瞬态电流需求。开发板可以通过 USB 稳定运行,不代表接入舵机、电机、继电器或由弱 DC-DC 供电时仍能稳定。供电压降有时不会留下明显的“掉线”日志,而是直接触发复位或让射频工作异常。
4.1 识别供电相关症状
以下现象优先检查供电:
-
串口出现
Brownout detector was triggered。 -
日志突然从启动信息重新开始,或复位原因不是软件预期的重启。
-
电机启动、舵机动作、继电器吸合、LED 全亮时网络中断。
-
USB 供电稳定,切换到电池、DC-DC 或长电源线后不稳定。
-
触摸模块、传感器或显示屏刷新时 RSSI 突变、串口乱码或设备卡死。
Arduino 中可以在启动时打印复位原因:
#include <esp_system.h>
void printResetReason() {
esp_reset_reason_t reason = esp_reset_reason();
Serial.printf("reset reason: %d\n", reason);
}
void setup() {
Serial.begin(115200);
printResetReason();
// 其余初始化...
}
该数值应结合所用 ESP32 芯片和框架版本的枚举定义解释。排查时的重点不是背诵数值,而是确认“是否发生复位”以及复位是否总与负载动作关联。
4.2 硬件检查清单
-
ESP32 的
3.3 V稳压器应有足够的瞬态供电能力;不要用传感器板的小 LDO 给 ESP32 和执行器一起供电。 -
电机、舵机、继电器等执行器使用独立电源或独立 DC-DC;与 ESP32 共地,但不要与其共用细长的正电源路径。
-
在 ESP32 供电输入附近放置合适的去耦电容;大电流负载附近也应有本地储能电容。
-
缩短电源线和地线,避免面包板细导线承载电机或舵机电流。
-
继电器线圈、电机等感性负载需有正确的驱动和续流措施,避免反冲和传导干扰。
-
用示波器观察 ESP32
3.3 V电源轨,触发条件设在电机启动、Wi-Fi 重连或业务峰值时,而不是只测静态电压。
万用表只能看到平均电压,往往抓不到毫秒级压降。若没有示波器,最有效的替代实验是:保持固件不变,改用短 USB 线和质量可靠的独立电源;再逐个断开负载做 A/B 对照。
4.3 不要关闭 Brownout 来“解决”问题
关闭欠压检测可能让日志暂时消失,但无法让电源变稳定,反而会把问题表现为随机死机、Flash 写入异常或不可重复的无线故障。只有在已确认硬件电压裕量且确有特殊产品需求时,才应讨论阈值策略;它不是常规排障手段。
5. 任务阻塞与看门狗排查
无线协议栈、事件回调、TCP/IP 处理和用户业务需要共享 CPU 时间。ESP32 虽然有双核型号,但“有双核”不等于应用可以任意阻塞:长时间不让出 CPU、在回调中做耗时工作、持锁等待网络,都会让网络表现为延迟、超时、掉线,甚至触发任务看门狗。
5.1 高风险代码模式
以下模式在 Wi-Fi 不稳定的项目里非常常见:
// 问题 1:长时间忙等,未让出 CPU
while (!sensorReady()) {
// 等待外设,可能持续数百毫秒甚至更久
}
// 问题 2:在 Wi-Fi 回调中执行网络请求、Flash 写入或大量 JSON 处理
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
uploadAllHistory();
}
}
// 问题 3:无限重连循环,占满 loop 或任务
while (WiFi.status() != WL_CONNECTED) {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
这些写法的问题不是“不能用循环”,而是循环没有超时、没有等待、没有状态边界,也没有给系统任务留出运行机会。
5.2 改为非阻塞状态机
把连接、业务上传和传感器读取拆成短步骤。每轮 loop() 或 FreeRTOS 任务只做可快速完成的一小段工作,失败后记录时间并稍后重试。
enum class NetworkState {
Disconnected,
Connecting,
Online,
};
NetworkState networkState = NetworkState::Disconnected;
uint32_t lastConnectAttempt = 0;
void networkProcess() {
const uint32_t now = millis();
switch (networkState) {
case NetworkState::Disconnected:
if (now - lastConnectAttempt >= 5000) {
lastConnectAttempt = now;
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
networkState = NetworkState::Connecting;
}
break;
case NetworkState::Connecting:
if (WiFi.status() == WL_CONNECTED) {
networkState = NetworkState::Online;
Serial.println("Wi-Fi online");
} else if (now - lastConnectAttempt >= 15000) {
WiFi.disconnect();
networkState = NetworkState::Disconnected;
}
break;
case NetworkState::Online:
if (WiFi.status() != WL_CONNECTED) {
networkState = NetworkState::Disconnected;
}
break;
}
}
void loop() {
networkProcess();
sensorProcess();
applicationProcess();
delay(5);
}
这里的重连间隔和超时只是示例,应按产品业务调整。关键约束是:networkProcess() 本身不能等待连接完成;业务代码也必须接受网络暂时不可用的状态。
5.3 FreeRTOS 项目的注意点
使用 ESP-IDF 或 Arduino-ESP32 的 FreeRTOS 接口时,重点检查:
-
任务循环内是否有
vTaskDelay()、阻塞队列接收或其他合理的让出方式。 -
是否把 CPU 密集型任务和 Wi-Fi 相关任务不合理地绑在同一核心。
-
是否在持有互斥锁时发起 HTTP、MQTT、DNS 等可能长时间等待的调用。
-
Wi-Fi 事件回调里是否只做轻量通知,再由专用任务处理耗时工作。
-
任务栈是否足够;栈溢出可能以随机网络问题的形式出现。
-
是否频繁创建和销毁任务、定时器、HTTP 客户端或大块 JSON 缓冲,导致堆碎片。
一个合适的模式是:Wi-Fi 事件回调只设置事件位或向队列发送消息;网络任务负责连接与重连;业务任务通过队列提交待上传数据;所有网络 I/O 设置明确超时。
5.4 识别看门狗与阻塞日志
串口出现 Task watchdog got triggered、Interrupt wdt timeout、长时间无心跳日志,通常说明问题已经不只是信号差。此时不要先增大看门狗超时,而要定位最后运行的任务、删除忙等、拆分大计算或把耗时 I/O 移到可控任务中。
为了缩小范围,可在关键路径加入低频、带时间戳的日志:
Serial.printf("[%lu] upload start, heap=%u, rssi=%d\n",
millis(), ESP.getFreeHeap(), WiFi.RSSI());
// 执行一次带超时的上传
Serial.printf("[%lu] upload finished\n", millis());
不要在高频循环中无节制打印。串口输出本身会改变任务时序,也会让真正的阻塞位置更难观察。
6. 重连策略:恢复服务,而不是制造风暴
可靠的重连策略需要区分三个层次:Wi-Fi 关联、IP 获取、上层服务连接。例如 STA 已恢复但 DHCP 尚未完成时,MQTT 不应立刻重连;TCP 已建立但 DNS 失败时,也不应反复执行 Wi-Fi 断开重连。
建议遵守以下规则:
-
用 Wi-Fi 事件驱动状态变化,避免在主循环中持续调用
WiFi.begin()。 -
对 Wi-Fi、MQTT、HTTP 使用独立状态和独立重试计时器。
-
失败后采用退避重试,例如 5 秒、10 秒、20 秒,上限封顶;不要每一轮循环都重连。
-
网络恢复后重新建立 MQTT 订阅、时间同步和必要的会话状态。
-
上传数据先进入本地队列或缓存,网络恢复后限速补发,避免一次性洪峰。
-
所有 DNS、TCP、HTTP、MQTT 操作设置超时,禁止无限等待。
频繁调用 WiFi.disconnect() 和 WiFi.begin() 不是通用修复方案。若 AP 端主动断开、RSSI 很差或电源正在跌落,激进重连只会增加发射与认证负载,让问题更明显。
7. 推荐排查顺序
按以下顺序执行,通常能用最少的改动定位根因:
-
刷入最小复现程序,打印连接事件、RSSI、可用堆和复位原因。
-
在距离 AP 1 到 2 米处测试,使用独立 2.4 GHz SSID、固定 20 MHz 带宽和固定信道。
-
使用可靠 USB 或实验室电源供电,拆除电机、舵机、继电器等大负载。
-
若稳定,再逐个恢复外设和真实电源路径,找出触发故障的负载或接线。
-
若仍不稳定,记录断开原因码,检查路由器加密方式、AP 容量和信道干扰。
-
若 Wi-Fi 仍在线但业务失败,检查 DNS、网关、服务器超时和 MQTT/HTTP 状态机。
-
若伴随卡死、看门狗或长时间无日志,审查忙等、回调耗时、任务优先级、锁和栈空间。
-
根因解决后,再添加退避重连、离线缓存和运行指标,验证长时间稳定性。
8. 现场记录模板
每次故障记录至少保留如下内容,后续分析会快很多:
时间:
固件版本 / 芯片型号:
供电方式 / 外设负载状态:
AP:2.4 GHz,信道 __,带宽 __,加密方式 __
距离与安装环境:
故障现象:
Wi-Fi 事件与断开原因码:
RSSI:
IP 地址是否保留:
复位原因 / 看门狗日志:
空闲堆内存:
复现步骤:
对照实验结果:
9. 结论
ESP32 Wi-Fi 不稳定不是一个单一的软件问题。信号差会提高重传与断开概率,供电压降会直接影响射频与系统稳定性,任务阻塞则会让协议栈和业务超时失去响应。正确的处理方式是先用事件、RSSI、复位原因和对照实验获得证据,再针对根因修复。
当设备能在固定无线环境、可靠供电和完整外设负载下连续运行,并且在 AP 重启、短暂断网和业务服务器异常后能够有节制地恢复服务,才算完成了 Wi-Fi 稳定性验证。

