ESP8285 自制 WiFi 天气时钟 — 从零到固件的全过程


项目简介


一块 ESP-01 模块 + 一块 0.96 寸 OLED,用 PlatformIO 手写固件,实现了一个全功能 WiFi 天气时钟:


- NTP 网络时间(自动校准,180° 旋转显示)

- 实况天气(温度、体感温度、湿度、风力、AQI 空气质量、PM2.5)

- 三日预报(日期、星期、天气图标、温度区间)

- 逐 8 小时预报(横排时间轴 + 图标 + 温度 + 当前降水概率)

- 自定义文字(网页配置,自动换行)

- 动态天气图标(太阳光线旋转、雨滴下落、雪花飘落、闪电闪烁)

- Web 配置面板(WiFi、坐标搜索/定位/手填、页面开关、停留时长、静态 IP)

- OTA 无线更新(两跳桥接模式,突破 1MB flash 限制)


硬件总成本约 ¥15(ESP-01 + CH340 + OLED + 面包板)。


系统总体架构


---


硬件清单


部件型号价格说明
主控ESP8285N08(ESP-01 模块)~¥5内置 1MB Flash,ESP8266 变体
USB 转串口CH340G 模块~¥3用于烧录和调试
显示屏0.96" OLED SSD1306 I2C~¥54 引脚:VCC/GND/SCL/SDA
电源外部 3.3V 稳压~¥2CH340 的 3.3V 不够 WiFi 峰值电流
面包板 + 杜邦线—~¥3

接线


串口接线图 — CH340 ↔ ESP-01 TX/RX 交叉



ESP-01          OLED           CH340
─────────────────────────────────────
GPIO2 (pin2) → SCL
GPIO0 (pin3) → SDA
VCC          → VCC            3.3V
GND          → GND            GND
                          TXD → RXD (交叉)
                          RXD → TXD (交叉)

关键注意事项:

- CH340 的 3.3V 输出电流不足 WiFi 峰值需求,必须外接 3.3V 稳压

- GPIO0 在烧录时需接地(低电平进入 Download 模式),烧录后断开

- EN 引脚必须拉高(接 VCC)


---


软件架构


技术栈


- 框架: Arduino for ESP8266

- 构建: PlatformIO (espressif8266, esp01_1m)

- 显示: U8g2(I2C 软件驱动)

- 字体: wqy12_t_gb2312a(文泉驿点阵,覆盖 GB2312 一级汉字 3755 个)

- 配置存储: LittleFS 纯文本 key=value(无 JSON 库)

- 天气 API: 实况用 suyanw.cn,预报+逐小时+体感用 Open-Meteo,AQI 用 Open-Meteo Air Quality


源码结构



src/
├── main.cpp          (45行)   入口:setup/loop
├── config.h/cpp      (30+85行) 配置结构体 + LittleFS 读写
├── net.h/cpp         (10+80行) WiFi AP/STA + NTP + OTA
├── weather.h/cpp     (46+520行) 天气数据获取 + JSON 解析
├── display.h/cpp     (8+530行) OLED 渲染 + 手绘矢量图标
├── web.h/cpp         (3+280行) Web 配置面板 + REST API
└── main_bridge.cpp.bak       最小桥接固件模板(OTA 用)

总代码量约 60KB(10 个文件),编译后固件 530KB,占 1MB flash 的 55%。


数据流



[Open-Meteo API] ──→ fetchForecast() ──→ g_fc (3日预报)
                   │                     g_hour[8] (逐8小时)
                   │                     g_cur.feelTemp (体感)
                   │
[suyanw API] ─────→ fetchCurrent() ───→ g_cur (实况天气)
                   │
[Open-Meteo AQI] ─→ fetchAQI() ──────→ g_aqi (空气质量)
                   │
                   ↓
            dispTick() 1Hz ──→ render() ──→ u8g2 OLED
                   │
            webTick() ──→ HTTP Server (配置面板)
                   │
             cfgSave() ──→ /config.txt (LittleFS)

天气数据获取与解析管线


---


核心技术挑战与解决方案


1. 1MB Flash 限制下的 OTA


ESP-01 只有 1MB flash。编译后的完整固件约 530KB,加上 WiFi + HTTP 栈开销,OTA 上传区(958KB - 当前固件)不足以容纳新固件。


解决方案:两跳桥接模式



完整固件 (530KB) ──→ 桥接固件 (390KB) ──→ 新完整固件 (530KB)
                      先腾出空间            再写入目标

1. 第一跳:上传最小桥接固件(仅 WiFi + OTA,无显示/天气功能)

2. 设备重启进入桥接模式 → 空闲空间 958KB - 390KB = 568KB ≥ 530KB

3. 第二跳:上传完整固件


每次部署双击 deploy.bat,全程自动化,约 60 秒完成。


OTA 双跳升级流程


2. JSON 解析(无 ArduinoJson)


为节省 flash 空间(ArduinoJson 约 30KB),手写了轻量级 JSON 解析器:



// 提取 "key":"string_value"
static bool arrStr(const String& s, const char* key, int idx, char* out, size_t n);

// 提取 "key":[array] 中第 idx 个数值
static float arrNum(const String& s, const char* key, int idx);

// 提取 "section":{...} 顶层对象
static String sectionOf(const String& s, const char* sect);

踩过的坑:

- arrStr 最初复制了 JSON 引号字符,导致 sscanf 解析日期失败 → 所有预报显示相同星期

- Open-Meteo 响应中 hourly_units 和 daily_units 排在数据前面,用 indexOf 需精确匹配 "daily":{ 而非 "daily"

- suyanw API 使用 chunked 编码 + 冒号后空格不一致,需要容错解析


3. OLED 180° 旋转 + 中文显示


用户要求屏幕倒装(180° 旋转),U8g2 一行搞定:



U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R2, 2, 0, U8X8_PIN_NONE);
//                                                     ↑  ↑
//                                               SCL=GPIO2  SDA=GPIO0

中文字体用文泉驿点阵 u8g2_font_wqy12_t_gb2312a,覆盖 GB2312 一级汉字,完美显示天气/星期/配置界面。


4. 手绘矢量天气图标


不用位图(太占空间),用 U8g2 的画线/画圆 API 手绘 9 种天气图标:



☀️ 晴   ⛅ 多云   ☁️ 阴   🌧️ 雨   ⛈️ 雷阵雨
❄️ 雪   🌨️ 冻雨   🧊 冰雹   🌫️ 雾

每个图标在 24×24 网格上设计,运行时缩放到任意尺寸。最终加入帧动画:太阳光线旋转、雨滴下落、雪花飘落、闪电闪烁。


天气主页效果


逐8小时预报效果


5. 配置系统


去掉 ArduinoJson 后,配置用纯文本存储:



ssid=Zyzjt
pass=***
city=惠州
text=今天记得带伞
flags=1,1,1,1,1
dwell=6
lat=23.11147
lon=114.41523

读取时用 sscanf + 局部 int 变量(非指针),避免 LittleFS 字符串对齐问题导致的崩溃。


---


API 数据源


数据API刷新频率说明
实况天气suyanw.cn5 分钟chunked JSON,免费无 Key
三日预报Open-Meteo60 分钟WMO 天气代码 + 温度极值
逐 8 小时Open-Meteo60 分钟含降水概率
体感温度Open-Meteo60 分钟与预报同请求,零额外开销
AQI 空气质量Open-Meteo Air Quality30 分钟US AQI + PM2.5 + PM10
地理编码Open-Meteo Geocoding按需城市名 → 坐标

---


踩坑记录(血泪教训)


坑1:CH340 供电不足,WiFi 一开就重启


ESP-01 的 WiFi 启动瞬间峰值电流约 300mA,而 CH340 模块的 3.3V 引脚最大输出只有 ~50mA。


症状: 设备上电后串口有输出,WiFi 一连就掉电重启,反复循环。串口日志看到 [MAIN] boot 反复打印。


原因: CH340 的 3.3V 是 LDO 稳压,电流不够 WiFi 射频用。ESP8266 系列对电源要求严格,峰值电流不足会导致 brownout 复位。


解决: 外接 3.3V 稳压模块(AMS1117-3.3 或类似),直接给 ESP-01 的 VCC 和 GND 供电。CH340 只负责 TXD/RXD 信号线。


供电方案对比



CH340 3.3V ──✗── 不接 ESP-01 VCC
外部稳压 3.3V ──→ ESP-01 VCC + OLED VCC
CH340 GND ──→ 共地(ESP-01 GND + OLED GND + 外部稳压 GND)

坑2:TX/RX 接反,烧录无反应


ESP-01 和 CH340 之间是交叉连接:ESP 的 TXD 接 CH340 的 RXD,ESP 的 RXD 接 CH340 的 TXD。


症状: esptool 报 Timed out waiting for packet header 或 A fatal error occurred: Wrong boot mode detected,烧录完全不成功。


原因: 串口通信是交叉的——一端的发送(TX)必须接另一端的接收(RX)。如果 TX 接 TX、RX 接 RX,信号完全对不上。


检查方法: 看 ESP-01 模块上的丝印:



ESP-01 引脚排列(从顶部看,天线在左):
┌─────────────┐
│  GND  TXD   │  ← TXD 是发送,接 CH340 的 RXD
│  GPIO2 RST  │
│  GPIO0 ADC0 │
│  VCC  CH_PD │  ← CH_PD(EN) 必须接 VCC
│  TXD  GND   │  ← 别搞混,模块上有两排引脚
│  RXD  GPIO0 │  ← RXD 是接收,接 CH340 的 TXD
│  RST  CH_PD │
│  GND  VCC   │
└─────────────┘

关键: ESP-01 的 CH_PD(也叫 EN)引脚必须接 VCC(拉高),否则芯片不工作。


坑3:烧录时不要按 RST 按钮


ESP-01 模块上没有 RST 按钮(只有引脚),但外接面包板上可能有人加了。烧录过程中如果意外按下 RST:


症状: 烧录到一半设备重启,flash 内容损坏 → 变砖。设备不响应任何烧录命令。


原因: esptool 烧录时通过软复位(DTR/RTS 信号线)控制 ESP8285 进入 Download 模式。手动按 RST 会打断这个流程,导致 flash 写入不完整。


烧录时的操作规范:

1. GPIO0 接地 → 上电 → 进入 Download 模式

2. pio run -t upload 开始烧录

3. 全程不要碰任何按钮和线

4. 烧录完成 → 断电 → 拔掉 GPIO0 的接地线 → 重新上电

5. 如果烧录失败,先断电再重新来,不要在线路上反复插拔


坑4:GPIO0 烧录后忘记拔,设备永远进 Download 模式


GPIO0 接地 = Download 模式。烧录完成后如果 GPIO0 还接地,设备重启会再次进入 Download 模式而不是正常运行。


症状: 串口显示 upload complete,但设备重启后没有正常工作,串口输出 waiting for download。


解决: 烧录成功后,必须断电 → 拔掉 GPIO0 接地线 → 重新上电。OTA 烧录不存在这个问题(OTA 通过 WiFi 传输,不需要 GPIO0)。


GPIO0 下载模式时序


坑5:LittleFS 字符串对齐导致崩溃


ESP8266 的 LittleFS 对内存地址有对齐要求。直接从文件缓冲区用 sscanf 解析整数可能触发异常。


症状: cfgLoad() 调用时设备 hard fault 重启,串口看到 exception 信息。


解决: 先将文件读入 char line[192] 栈缓冲区,再用 sscanf 或 atoi 解析:



// 错误:
// sscanf(f.readStringUntil('\n').c_str(), "%d", &x);  // 可能崩溃

// 正确:
char line[192];
int len = f.readBytesUntil('\n', line, sizeof(line) - 1);
line[len] = 0;
sscanf(line, "%d", &x);  // 从栈缓冲区解析,安全

坑6:Python + PowerShell 编码冲突


Windows PowerShell 默认 GBK 编码,Python 脚本中的中文和转义字符会被错误编码。


症状: Python 报 SyntaxError: invalid syntax,但语法明明正确。


解决: 用 PowerShell here-string 管道传递(@'...'@ | python -),或写入临时文件后执行。不要在 PowerShell 的 -Command 参数中嵌入复杂 Python 代码。


---


最终成果


- 530KB 固件,运行在 1MB flash 的 ESP-01 上

- 5 页轮播:时间 → 实况 → 预报 → 逐小时 → 自定义文字

- 动态图标:8 帧动画,200ms 切换

- 3 个 API 数据源,智能刷新频率(5/30/60 分钟)

- Web 配置:WiFi、坐标搜索、页面开关、静态 IP

- OTA 更新:两跳桥接,60 秒完成

- 串口调试:完整日志输出,远程故障排查


总开发时间约 2 天,从"研究一台成品天气时钟"到全自研固件,代码量 60KB,注释占比 <5%。


---


文件清单



E:\esp8233\
├── platformio.ini              项目配置
├── deploy.bat                  一键部署脚本
├── serial_listen.py            串口监听脚本
├── src/
│   ├── main.cpp                入口
│   ├── main_full.cpp.bak       完整固件备份
│   ├── main_bridge.cpp.bak     桥接固件模板
│   ├── config.h / config.cpp   配置管理
│   ├── net.h / net.cpp         网络 + OTA
│   ├── weather.h / weather.cpp 天气数据
│   ├── display.h / display.cpp OLED 渲染
│   └── web.h / web.cpp         Web 服务
└── .pio/build/esp01_1m/        编译输出