去年秋天,一批装在配电箱里的小设备出了个怪毛病:白天连着网一切正常,到了后半夜总有两三台自己重启,串口日志里除了几行看不出来源的乱码什么都没有。换电源、换板子、重新烧固件,折腾了整整一周,最后发现是那颗3.3V稳压芯片在低温下带不动射频峰值电流。这就是ESP8266的典型性格——它便宜、好用、资料多,但只要供电、启动模式、Flash布局、内存管理这几个地方有一处没照顾到,它就会用"玄学故障"的方式让你加班。这篇内容把ESP8266在实际开发中最常撞上的坑按类别捋一遍,从供电复位、烧录下载、Wi-Fi连接、崩溃日志、Flash文件系统到GPIO外设,每个问题都给出排查顺序和可复现的验证方法。不管你是刚拿到第一块开发板的新手,还是已经量产过几批设备的老手,下面这些场景大概率都遇到过,或者迟早会遇到。
1. 供电与复位:八成的"玄学故障"都藏在这里
ESP8266出问题,我的排查顺序永远是先量电压,再看日志,最后才怀疑代码。原因很简单:这颗芯片在Wi-Fi发射瞬间的电流能冲到300mA以上,而平均电流只有几十毫安,这种"脉冲式"的负载特性对电源的要求远高于一般MCU。很多开发板上的USB转串口芯片自带3.3V输出,标称能带500mA,实际在射频突发时就塌下去了,你万用表测出来一直是3.28V,看起来很稳,示波器一挂就现原形。
1.1 供电不足的三种典型表现与实测方法
第一种表现是启动即复位,串口打印几行启动信息后直接重来,反复循环。第二种是连接Wi-Fi时重启,日志停在wifi evt: 1或者scandone附近就断了。第三种最坑,运行几小时甚至几天才重启一次,完全随机,让人误以为是软件内存泄漏。
判断方法很直接:把示波器(没有示波器就用手持万用表的最快刷新档,虽然看不到尖峰但能看出趋势)接在芯片VCC和GND之间,探头接地弹簧一定要短,否则测到的全是干扰。让设备执行一次Wi-Fi扫描或者发送一个数据包,观察电压跌落幅度。健康的情况是峰值跌落不超过200mV,如果从3.3V掉到3.0V以下,就要从电源上找问题了。
常用对策有三条,按性价比排序:第一,换成带足够余量的LDO,输入输出压差留足1V,输出端并一颗470uF电解电容加一颗100nF陶瓷电容,电解电容负责扛住毫秒级的电流脉冲,陶瓷电容负责高频去耦,两者位置都要尽量靠近芯片VCC引脚,走线越短越好;第二,如果供电是5V,直接在前级加一个大电容做能量缓冲;第三,供电线不要用杜邦线里的细线,线径太细带来的压降在峰值电流下非常可观。
提示:判断是不是供电问题,最快的办法是用一台可调直流电源直接给芯片的3.3V引脚供电,把电流限制设到500mA,如果故障消失,基本就可以锁定是原供电电路的问题。
1.2 复位引脚、上拉电阻与复位原因的区分
ESP8266的EXT_RSTB(有的板子标RST)是低电平复位,正常工作时必须拉高到3.3V。有些模块内部已经有上拉,但为了保险,外部再并一个10k电阻到3.3V是常见做法。如果这个脚悬空或者上拉电阻虚焊,芯片会在电磁干扰下随机复位,症状和供电不足极其相似。
更有效的判断手段是读取复位原因。Arduino框架下可以用ESP.getResetReason(),返回的字符串直接告诉你上一次是什么原因重启的:
void setup() { Serial.begin(115200); Serial.println(ESP.getResetReason()); Serial.println(ESP.getResetInfo()); }常见的返回值与含义对照如下:
| 复位原因字符串 | 含义 | 优先排查方向 |
|---|---|---|
| Power on | 正常上电 | 无 |
| External system | 外部复位脚被拉低 | RST引脚、按键、干扰 |
| Hardware watchdog | 硬件看门狗超时 | 长时间阻塞、死循环 |
| Software watchdog | 软件看门狗超时 | 同上,注意delay之外的忙等 |
| Exception | 程序异常崩溃 | 空指针、数组越界、栈溢出 |
| Deep-Sleep wake | 深度睡眠唤醒 | 正常现象 |
| Software reset | 代码主动重启 | ESP.restart()调用位置 |
养成在setup()里打印复位原因的习惯,能省掉大量猜测。我曾经遇到一台设备每天重启十几次,日志显示全是External system,最后查出来是外壳装配时RST引脚的焊盘和金属壳轻微接触,温度变化时热胀冷缩导致间歇性短路。
1.3 去耦电容与布局的几个关键位置
如果是自己画板子,有几个位置的电容器千万别省:芯片VCC引脚旁边100nF,越近越好;电源入口处10uF到100uF;如果板上有继电器、电机这类感性负载,它们的电源要和ESP8266分开走线,共地单点连接。射频部分的天线净空区不要铺铜、不要走线、不要放电池,这个要求在很多模块的手册里都写了,但实际画板时被忽略的比例相当高。
还有一个小细节:某些模块把Flash和芯片放在同一块小板上,Flash的供电和芯片共用,如果滤波不好,Flash读写时会拉低电压导致取指错误,表现就是随机的Exception (2)。这类问题靠改代码永远修不好,只能在电源上下功夫。
2. 烧录卡在"Connecting...":一条完整的排查链路
如果说供电问题是最隐蔽的,那烧录失败就是最常见的。几乎每个用ESP8266的人都经历过串口监视器里一直刷点号、上传进度条走到一半报错、或者干脆提示"Failed to connect to ESP8266"。这类问题的排查其实有很清晰的顺序,关键是不要跳步,一次只改一个变量。
2.1 接线与电平:先确认最基础的三件事
第一件事,TX和RX必须交叉。模块的TX接 USB转串口的RX,模块的RX接转串口的TX,这一点新手最容易搞错,而且接反了不会烧坏,只是没反应,所以很难自我发现。第二件事,必须共地,GND不接或者接触不良,通信会时断时续,表现就是偶尔能连上偶尔连不上。第三件事,供电能力,前面说过,烧录时射频不工作,电流需求不大,但如果你的转串口模块3.3V输出只有几十毫安,依然可能不够。
我的习惯是先用一根短而粗的USB线,把模块单独接到电脑上,只接TX、RX、GND、3.3V四根线,其他外设全部断开,确认能正常烧录之后再逐个接回外设。这个"最小系统法"能排除掉一大半的外部干扰。
2.2 启动模式的三个引脚组合
ESP8266上电瞬间会采样三个引脚的电平来决定从哪儿启动,这是烧录失败最常见的软件层原因。组合关系如下:
| 工作模式 | GPIO0 | GPIO2 | GPIO15 |
|---|---|---|---|
| 正常运行(从Flash启动) | 高 | 高 | 低 |
| 串口下载模式 | 低 | 高 | 低 |
| SD卡启动 | 高 | 高 | 高 |
也就是说,要进入下载模式,必须在上电或复位的那一刻GPIO0保持低电平。很多开发板用一个按键加两个三极管做了自动下载电路,靠串口的DTR和RTS信号控制,但并不是所有板子都做得可靠。如果自动下载老是失败,就手动来:把GPIO0接到GND,按一下复位键,松开GPIO0,再点上传。这个土办法虽然笨,但成功率接近百分之百。
另外要提醒一句,GPIO15必须为低,很多模块内部已经下拉,但自己搭电路时如果忘了这个电阻,芯片会去尝试SD卡启动,同样表现为连不上。
2.3 串口驱动、波特率与Flash模式设置
驱动层面,USB转串口芯片常见的有几款,装错驱动或者装了多个版本导致端口号冲突的情况不少见。设备管理器里如果看到带黄色感叹号的未知设备,先把旧驱动彻底卸载再装。端口号建议固定成COM10以下,某些IDE对高位端口号兼容性一般。
波特率方面,上传波特率默认115200最稳,想提速可以试921600,但线材质量差、线太长的时候高波特率反而更容易失败。烧录时串口监视器必须先关掉,因为串口是独占资源,一边开着监视器一边上传,必然报错。
Flash设置里最容易出错的是Flash Mode和Flash Size。Flash Mode一般选DIO,兼容性最好;QIO速度更快但对某些Flash芯片不兼容,表现为烧录成功但运行立刻崩溃。Flash Size要和实际芯片一致,常见模块是4MB,如果误设成1MB,程序可能被截断。分区方案(Partition Scheme)也要和程序大小匹配,带OTA的方案会占用更多空间,程序一大就会提示空间不足。
# 用命令行工具确认Flash信息是个好习惯 esptool.py --port COM3 flash_id这条命令会打印Flash的厂商ID、容量和支持的模式,比猜要靠谱得多。如果读出来的容量和你在IDE里设置的不一致,那就是问题所在。
2.4 一个真实的上传失败复现过程
我记录过一次典型的排查过程,供参考。现象:上传到27%时报Failed to write to target Flash,重试三次都是同一个位置失败。
第一步,换USB线和端口,无效,排除线材。第二步,降低上传波特率到115200,失败位置变成31%,有变化但没解决,说明和时序有关。第三步,用flash_id读芯片信息,容量显示4MB,模式支持DIO和QIO,正常。第四步,怀疑供电,外接稳压电源后依然失败。第五步,换一块同型号模块,成功。把问题模块单独拿出来,用橡皮擦了一遍排针,重新焊接了Flash芯片的八个引脚,再试成功。结论是Flash芯片有个引脚虚焊,高温或者轻微形变时接触不良。
这个案例的启示是:当软件层的参数都排查完还是失败,就要怀疑硬件连接,尤其是那些看起来焊上了但实际虚焊的引脚。用手按压Flash芯片或者用热风枪补焊,往往能验证这个猜测。
3. Wi-Fi连接失败与频繁掉线:拆掉"信号满格"的假象
手机显示Wi-Fi信号满格,设备却连不上,这是ESP8266用户抱怨最多的问题之一。原因在于手机和ESP8266的射频能力完全不在一个量级,手机能收到的弱信号,ESP8266未必能解调出来;而且ESP8266只支持2.4GHz频段,对某些路由器设置天然不兼容。
3.1 频段、信道与带宽的兼容性
ESP8266只支持2.4GHz,这一点必须刻在脑子里。如果你的路由器开了5GHz同名SSID,设备有时能连上有时连不上,那大概率是被分配到了5GHz频段,或者路由器在做频段引导。解决办法是在路由器上把2.4G和5G的SSID分开命名,让设备只认2.4G那个。
信道方面,2.4GHz有1到13信道,不同区域允许的信道不一样,有些设备固件只扫描到11信道。如果路由器设置成自动选择信道,而恰好选到了12或13,部分模组就会扫描不到。这种情况在进口路由器和某些企业级AP上出现过,手动把路由器信道固定到1、6、11这三个互不重叠的信道,是最省心的做法。
带宽设置也值得一提。路由器如果强制40MHz带宽,在拥挤的环境下反而容易受干扰掉线,改成20MHz通常更稳定,代价是理论速率下降,但对ESP8266这种应用影响不大。
3.2 连上了却上不了网:DHCP与DNS的排查
有一类问题很迷惑:串口打印WL_CONNECTED,WiFi.localIP()也拿到了地址,但HTTP请求永远超时。这时候要分两步查。
第一步查网关。用WiFi.gatewayIP()打印网关地址,然后在同一网络下的电脑上ping这个网关,确认网络本身是通的。第二步查DNS。很多固件里直接写client.connect("api.example.com", 80),域名解析失败就会一直卡住,而错误码又不会明显提示。稳妥的做法是先用IP地址测试,通说明是DNS问题,再用WiFi.hostByName()手动解析域名并打印结果。
IPAddress resolved; if (WiFi.hostByName("your-server.com", resolved)) { Serial.print("DNS OK: "); Serial.println(resolved); } else { Serial.println("DNS failed"); }如果手动解析也失败,检查路由器下发的DNS地址是否正确,或者直接在代码里指定一个公共DNS。还有一个容易忽略的点:某些企业网络需要网页认证,设备端拿不到认证页面,自然上不了网,这种环境只能换网络测试。
3.3 掉线重连与睡眠模式的副作用
设备能连上,但隔几分钟掉一次,重连后又正常,这种周期性的掉线往往和睡眠模式有关。ESP8266默认开启了modem sleep,在空闲时会关闭射频以省电,某些路由器对长时间没有数据交互的客户端会主动踢掉。解决办法是关掉睡眠:
WiFi.setSleepMode(WIFI_NONE_SLEEP); WiFi.setAutoReconnect(true); WiFi.persistent(false);persistent(false)这一行值得单独说。默认情况下,ESP8266会把Wi-Fi配置写进Flash,如果代码里频繁调用连接函数,Flash会被反复擦写,几百次之后那块扇区就可能损坏,表现为配置存不住、启动异常。把它设为false之后,配置只保存在内存里,重启后重新连接,虽然慢一点,但Flash寿命有保障。
重连逻辑方面,不要依赖库的自动重连做全部工作。稳妥的写法是在主循环里检测状态,掉线超过一定时间就主动WiFi.disconnect()再WiFi.begin(),同时加一个退避机制,重试间隔从1秒逐步拉长到30秒,避免在网络确实不可用时疯狂重连把CPU占满。
3.4 天线净空与结构件的隐性影响
这一条经常被硬件工程师忽略。模块的板载天线对周围金属极其敏感,如果设备装进金属外壳,或者天线正对着电池、螺丝、走线,信号会衰减十几dB,表现就是"离路由器三米就掉线"。做产品时,天线区域下方和前方至少留出5到10毫米的净空,外壳如果是金属的,要么开窗,要么用外置天线版本。
还要注意人体影响。手持设备和放在桌面上的信号表现完全不同,测试时要模拟真实使用姿态。我曾经有个项目,样机在实验室一直很稳,装到现场立杆上就开始随机掉线,最后发现是安装支架的金属抱箍正好压在天线正下方。
4. 运行中崩溃与复位循环:读懂串口里的异常日志
代码跑着跑着突然重启,串口吐出一大段十六进制数字,这是ESP8266开发中最让人头大的场景。很多人看到这段日志直接懵掉,其实它包含的信息非常多,读懂之后定位问题会快很多。
4.1 Exception后面的数字和堆栈怎么看
崩溃日志的典型形态是这样的:
Exception (28): epc1=0x4020a1b4 epc2=0x00000000 epc3=0x00000000 excvaddr=0x00000000 depc=0x00000000 >>>stack>>> ctx: cont sp: 0x3ffef1e0 end: 0x3ffef3e0 offset: 0x01a0 3ffef380: 00000000 3ffe8f4c 3ffef3d0 40202b1c ... <<<stack<<<Exception (28)里的数字是异常类型,最常见的几个含义如下:
| 异常编号 | 含义 | 常见原因 |
|---|---|---|
| 0 | 非法指令 | 函数指针错乱、Flash内容损坏 |
| 2 | 取指错误 | Flash读取失败、代码区被覆盖 |
| 3 | 读写错误 | 访问了非法地址 |
| 9 | 软件看门狗 | 长时间阻塞 |
| 28 | 内存访问错误 | 空指针、数组越界、野指针 |
| 29 | 内存写入错误 | 向只读区或非法地址写入 |
其中28和29占了我遇到的大多数。excvaddr这个字段特别关键,它记录了出错时访问的地址。如果excvaddr=0x00000000,基本可以断定是空指针解引用;如果是个奇怪的地址比如0x3ffexxxx,可能是数组越界;如果是0x40000000附近的地址,可能是函数指针被破坏。
堆栈部分是一串地址,需要配合编译生成的.elf文件反查。Arduino IDE可以把编译输出打开,找到生成的elf文件,然后用工具把地址翻译成函数名和行号。手动做这件事很麻烦,社区有现成的异常解码工具,把整段日志粘进去就能自动解析,省事很多。我在项目里养成的习惯是每次发布固件都保留对应的elf文件,出问题时才能对得上号。
4.2 看门狗复位与阻塞式代码
看门狗是ESP8266的自我保护机制,程序卡死超过一定时间就会强制重启。硬件看门狗默认大约几秒,软件看门狗可以配置。触发看门狗的最常见原因是长时间阻塞:while循环等待某个标志位、delay()时间过长、在中断里做了耗时操作。
特别要提醒的是delay()。在Arduino框架下delay()内部会让出CPU并喂狗,所以短时间的delay()不会触发看门狗,但如果在delay()之外的忙等循环里不喂狗,几秒钟就会重启。正确做法是把长任务拆成状态机,每次循环只做一小步,主循环自然就喂狗了。
// 不推荐:忙等会触发看门狗 while (!sensorReady()) { } // 推荐:非阻塞轮询 static unsigned long lastCheck = 0; if (millis() - lastCheck > 100) { lastCheck = millis(); if (!sensorReady()) return; // 继续后续逻辑 }如果确实有无法拆分的长任务,可以在循环里手动调用ESP.wdtFeed()或者yield(),但这只是权宜之计,根本方案还是重构代码。
4.3 堆内存碎片与String的使用边界
ESP8266的可用堆内存大约40KB左右,听起来不少,但Wi-Fi协议栈本身要占掉一大块,实际留给应用的只有20多KB。在这种资源条件下,大量使用String拼接是危险动作。String每次拼接都可能重新分配内存,反复分配释放之后堆就碎掉了,最终某次分配失败导致空指针崩溃,日志显示Exception (28),但你怎么查代码都找不到明显的越界。
判断堆是否健康,可以在主循环里定期打印剩余堆和最大可分配块:
Serial.print("free heap: "); Serial.println(ESP.getFreeHeap()); Serial.print("max block: "); Serial.println(ESP.getMaxFreeBlockSize());如果getFreeHeap()还有十几KB,但getMaxFreeBlockSize()只有一两KB,说明碎片严重。解决办法是把String换成固定长度的char数组,用snprintf拼接;JSON解析用流式解析库,不要一次性构造大字符串;对象尽量在栈上分配,避免频繁new和delete。
4.4 固件版本与第三方库的兼容性
还有一种崩溃是升级之后才出现的,之前一直好好的。这种情况优先怀疑库版本。Arduino的ESP8266核心库迭代很快,不同版本之间的API行为、内存布局、中断处理都可能有变化,某个库在新版本上编译通过但运行崩溃,是很常见的。
我的做法是把项目的库版本固定下来,在工程里记录每个库的版本号和来源,升级时一次只升一个,跑够24小时稳定再升下一个。核心库的版本也要固定,不要用最新版就完事,尤其是量产项目,能跑通的版本就是最好的版本。
5. Flash、文件系统与OTA:那些"写完就坏"的坑
ESP8266的程序和文件都放在外挂的SPI Flash里,Flash的容量、分区、擦写寿命直接决定了项目的稳定性。这一块出的问题往往有延迟性,写的时候没事,跑一段时间才暴露。
5.1 分区表与Flash容量不匹配
官方核心库提供了几种预设的分区方案,从1MB到16MB不等。选错方案的表现有两种:程序编译时报"text section exceeds available space",或者烧录成功但运行时反复重启,日志显示Exception (2)取指错误。
自己算一下就很清楚:一个带Wi-Fi和HTTPS的程序,代码段轻松超过500KB,如果选了1MB Flash且带OTA的分区方案,每个程序区只有不到500KB,根本放不下。建议在项目初期就确定Flash容量,至少选4MB,分区方案选带OTA且给文件系统留出空间的。
如果需要自定义分区,可以用工具生成分区表CSV,然后在编译选项中指定。分区表里几个关键字段是名称、类型、子类型、偏移和大小,偏移必须按4KB对齐且不能重叠,改完一定要用工具校验一遍,否则烧录后可能出现程序区和文件系统区互相覆盖,表现为文件莫名损坏。
5.2 SPIFFS与LittleFS的掉电损坏
要在Flash里存配置文件或者网页资源,就得用文件系统。早期大家用SPIFFS,后来逐渐转向LittleFS,主要原因是SPIFFS在掉电时容易损坏整个文件系统,而LittleFS有日志和磨损均衡机制,掉电后恢复能力更强。
不管用哪个,有几个操作习惯必须养成:写入文件时先写临时文件,写完再重命名,避免写一半掉电导致原文件也损坏;配置文件加上校验和,读取时校验失败就回退到默认值;不要在每次循环里写文件,把写操作合并、降低频率,Flash的擦写寿命一般标称10万次,看着多,但每秒写一次的话一天就写掉八万多次。
// 配置写入的稳妥做法 void saveConfig(const Config &cfg) { File f = LittleFS.open("/cfg.tmp", "w"); if (!f) return; f.write((uint8_t*)&cfg, sizeof(cfg)); f.close(); LittleFS.remove("/cfg.bak"); LittleFS.rename("/cfg.dat", "/cfg.bak"); LittleFS.rename("/cfg.tmp", "/cfg.dat"); }这段逻辑虽然简单,但能保证任何时刻掉电,至少有一个完整可用的配置备份。
5.3 OTA升级失败与回滚设计
OTA是ESP8266很实用的功能,但失败率比有线烧录高得多。常见失败原因有三个:升级过程中断电、网络中断导致下载不完整、新固件本身有问题导致启动崩溃。
前两个可以靠校验解决。下载完成后先校验MD5或者文件大小,确认无误再调用Update.end(true)提交,中途失败就调用Update.end(false)放弃,原有固件不受影响。第三个问题最麻烦,新固件启动就崩溃,设备变砖,只能拆下来有线烧录。解决办法是在新固件启动后先做自检,比如连续稳定运行30秒、Wi-Fi能连上、关键外设能读到数据,全部通过才标记升级成功,否则主动回滚到上一个分区。
// 简化的自检与回滚思路 void checkBootOK() { if (readBootFlag() == BOOT_TRIAL) { if (!selfTest()) { ESP.restart(); // 触发回滚 } writeBootFlag(BOOT_OK); } }这套机制需要在分区表里预留两个程序区,并且引导程序支持回滚。核心库的OTA机制本身带有一定的回滚能力,但具体行为要看版本,建议在自己的代码里再加一层应用级自检,双保险。
6. GPIO与外设:引脚复用表之外的经验
引脚不够用是ESP8266项目的常态,但在想办法"挤出"引脚之前,得先知道哪些引脚能碰、哪些碰了会出事。手册上的引脚表只告诉你功能,很多使用限制是在实际调试中才体会到的。
6.1 那几个不能随便用的引脚
GPIO0、GPIO2、GPIO15这三个引脚在启动时被采样,决定了启动模式,所以它们上面接的外设必须保证上电瞬间不会把电平拉错。比如GPIO0接了一个按键到地,上电时如果按键被按住,芯片就进下载模式了,表现为程序不运行。正确做法是按键接在别的引脚上,或者用三极管隔离。
GPIO16比较特殊,它属于另一个电源域,支持深度睡眠唤醒,但内部上拉和中断能力和别的引脚不同,做输入时只能配置INPUT_PULLDOWN_16,没有内部上拉。用它接按键的话,外部必须加上拉电阻。
GPIO6到GPIO11连接着SPI Flash,绝对不能用作普通IO,用了必然崩溃。GPIO9和GPIO10在某些模块上引出,但同样有大坑,非必要不要碰。
A0是唯一的模拟输入,量程0到1.0V,大部分开发板加了分压电阻,标称能测0到3.3V,但分压比不一定精确,做精度要求高的采集时要自己校准。而且A0只有一个,多路模拟量要么用外部多路复用器,要么换带多路ADC的方案。
6.2 I2C上拉、PWM频率与中断时序
I2C在ESP8266上可以用任意GPIO模拟,但默认的SDA是GPIO4、SCL是GPIO5。必须注意的是外部上拉电阻不能省,内部上拉很弱,几十厘米的排线加上几个从设备,信号边沿会变得很缓,表现为偶尔读到0xFF或者总线锁死。一般用4.7k到10k的上拉,设备多、线长的时候取小值。
PWM方面,默认频率是1kHz、范围1023,驱动LED够用,但驱动舵机或者调光就需要改。改频率用analogWriteFreq(),改分辨率用analogWriteRange(),两个要配合着调,频率高的时候范围要相应减小,否则占空比精度不够。还有一个限制是PWM通道数量有限,同时驱动多路时要注意分配。
中断处理里不要做耗时操作,不要调用delay(),不要用Serial.print()。中断里只置一个标志位,具体处理放到主循环里做,这是嵌入式开发的通识,但在ESP8266上尤其重要,因为Wi-Fi协议栈对时序很敏感,中断里耽误太久会导致连接异常。
6.3 传感器读数漂移的排查思路
接上传感器读数不稳,先别怀疑传感器。按这个顺序查:第一,供电是否干净,传感器和ESP8266共用一路电源时,射频工作的瞬间电压波动会被传感器放大成读数跳变,给传感器单独加一颗100nF加10uF的组合电容往往立竿见影;第二,线材长度,I2C超过半米就要考虑降低速率或者用屏蔽线;第三,上拉电阻是否合适;第四,读取时序是否符合传感器手册要求,有些传感器两次读取之间需要足够的转换时间,读太快会拿到上一次的旧值。
单总线器件比如常见的温湿度传感器,对时序要求严格到微秒级,ESP8266在开启Wi-Fi时偶尔会被协议栈打断,导致时序错乱读出85或者0这样的错误值。解决办法是读取时短暂关闭中断,或者连续读三次取中间值做滤波。
7. 让设备连续跑上几个月:我自己的稳定性检查清单
前面讲的都是具体故障,最后分享一套我在项目里固定执行的检查流程。每次固件准备发布前,按这个清单过一遍,能拦下大部分会在现场暴露的问题。
| 检查项 | 具体做法 | 通过标准 |
|---|---|---|
| 复位原因 | 每次启动打印getResetReason | 连续72小时无非预期重启 |
| 堆内存 | 每小时记录空闲堆和最大块 | 无持续下降趋势 |
| Wi-Fi重连 | 手动断开路由器再恢复 | 60秒内自动重连成功 |
| 掉电恢复 | 随机时刻断电20次 | 每次都能正常启动并加载配置 |
| 长时间计时 | 用millis()做跨天测试 | 无溢出导致的逻辑错误 |
| Flash写入 | 统计单位时间写入次数 | 每小时不超过10次 |
关于millis()溢出,这个坑很多人在设备跑满49天之后才会遇到,因为millis()返回的是无符号32位,大约49.7天回绕一次。避免的方法很简单,所有时间比较都用减法:
// 错误写法:溢出后会判断失误 if (millis() > lastTime + 1000) { } // 正确写法:无符号减法自动处理回绕 if (millis() - lastTime > 1000) { }另外,设备在现场的环境适应性也要提前考虑。温度对晶振频率有影响,低温下Wi-Fi连接成功率会下降;湿度大会导致PCB表面漏电,复位引脚被轻微拉低;电源电压波动范围要按实际电网情况留足余量。这些问题在实验室里永远复现不出来,只能在设计阶段就把余量做足。
还有一个我踩过的坑:某批设备在雷雨季节集中返修,查下来是电源入口没有加 TVS 管,感应雷通过电源线打坏了稳压芯片。后来在入口加了压敏电阻和TVS,问题再没出现过。这类防护器件的成本只有几毛钱,但能省掉整批返修的麻烦。
最后再提一句调试习惯。给每台设备留一个串口日志输出口,日志里带上时间戳、复位原因、Wi-Fi状态、堆内存这几个关键字段,出问题时让现场人员把日志导出来,比远程猜测高效得多。我自己就是因为坚持打这几行日志,才在几次看似无解的现场故障里找到了线索,其中一次就是靠复位原因的统计分布,定位到了某一批次的电源芯片存在批次性问题。