☰
STM32+ESP8266联网天气时钟:AT指令、NTP校时与OLED显示实战
2026/10/21 23:54:12 网站建设 项目流程

1. 为什么拿ESP8266当配角:选型逻辑与硬件接线

我最早做这类项目时,思路是拿ESP32一步到位,WiFi、蓝牙、双核全都有,省得折腾。但实际做下来发现,很多人手里已经囤了不少STM32最小系统板,尤其是F103C8T6这个型号,性能做逻辑控制绰绰有余,唯一缺的就是联网能力。与其换主控,不如给STM32加一个"外置网卡",而ESP8266-01S就是市面上最便宜、资料最多、也最容易上手的选择。

这套组合的核心价值在于:STM32负责所有逻辑和显示,ESP8266只干两件事——连WiFi、传数据。这种分工清晰的结构,哪怕以后再换WiFi模块(比如换成ESP32-C3),主控端代码都不用大改,只要把串口通信协议保持一致就行。所以标题虽然叫"STM32 + ESP8266",本质思路其实是"主控 + 透传网卡"的经典架构。

1.1 板子选型:F103C8T6和ESP8266-01S的组合

STM32F103C8T6是整个生态里最普及的型号,Flash 64KB、RAM 20KB,跑个OLED显示加串口解析绰绰有余。ESP8266-01S则是ESP8266系列里最经典的封装,板载PCB天线、2MB Flash、引出8个引脚。虽然ESP-12F模块性能更好,但01S因为是直插排针,接线方便,特别适合面包板原型搭建。

我这个项目用到的硬件清单如下:

器件型号/规格数量备注
主控板STM32F103C8T6最小系统板1蓝色板那款即可
WiFi模块ESP8266-01S1刷好AT固件版
显示屏0.96寸OLED,SSD1306,I2C接口1分辨率128x64
稳压模块AMS1117-3.3V1给ESP8266单独供电
温湿度传感器DHT11(可选)1扩展功能用
电源5V/1A USB电源或充电宝1最小系统板支持5V输入
杜邦线公对母若干-接线用

这里重点说一下电源问题。ESP8266-01S在WiFi发射瞬间的峰值电流能达到300mA以上,如果直接从STM32最小系统板上的3.3V引脚取电,稳压芯片会被拉垮,导致模块反复重启。我第一次就是因为偷懒共用电源,排查了整整半天才意识到问题。一定要给ESP8266单独供电,用AMS1117转出来的3.3V,并且在电源引脚旁边并联一个10uF和0.1uF的去耦电容。

1.2 接线表与GPIO分配

ESP8266-01S的引脚不多,但每个引脚都有讲究。模块在启动时对引脚电平有要求:GPIO0必须保持高电平(不能接地),GPIO2也必须悬空或上拉,否则模块会进入烧录模式而不是正常启动。GPIO1(TXD)和GPIO3(RXD)是串口引脚,负责和STM32通信。

我最终采用的接线分配如下:

ESP8266-01S引脚连接到STM32F103C8T6说明
VCC(3.3V)AMS1113-3.3V输出单独供电,不要用板载3.3V
GNDGND共地
TXD(GPIO1)PA3(USART2_RX)模块发送,MCU接收
RXD(GPIO3)PA2(USART2_TX)MCU发送,模块接收
GPIO0悬空/接3.3V启动时必须高电平
GPIO2悬空启动时保持悬空
ENVCC使能引脚,接3.3V
RST悬空(可选接MCU的PA1)手动复位用

从这张表能看出一个关键点:我用的不是USART1,而是USART2。原因是PA9/PA10(USART1)通常被ST-Link占用来下载程序,如果再接ESP8266,每次下载都得拔线,麻烦不说还容易接错。如果你有USB-TTL转串口模块,也可以把调试信息放USART1,数据通信放USART2,各干各的互不干扰。

STM32这边的OLED用的是I2C,我分配到PB6(SCL)和PB7(SDA),也就是I2C1的标准引脚。这样接线逻辑很清晰:一组串口管通信,一组I2C管显示,互不重叠。

1.3 烧录AT固件与验证

ESP8266-01S到手之后,第一步是确认固件是不是AT指令固件。市面上部分模块出厂预装了AT固件,但也有人买回来是NodeMCU固件或其他固件,无法直接使用。

验证方式很简单:先用USB-TTL转串口模块连接ESP8266,USB-TTL的TXD接模块RXD,RXD接模块TXD,然后在串口调试助手发送指令:

AT

如果模块正常,会返回:

OK

如果没有任何回显或者返回乱码,就需要手动烧录AT固件。方法是用USB-TTL转串口模块配合ESPFlashDownloadTool工具,把GPIO0拉低进入烧录模式,选择对应Flash大小为2MB的AT固件,擦除后下载即可。这个流程在ESP8266官方文档里有详细说明,我这里就不再展开。

这里有一个容易忽略的坑:模块的串口波特率。AT固件默认波特率大多是115200,但也有部分是9600。如果串口助手发AT没反应,先试两个波特率,别急着刷固件。我用的是115200,8位数据位,无校验,1位停止位。

固件确认后,继续发AT指令测试WiFi功能:

AT+CWMODE=1 AT+CWJAP="你的WiFi名称","你的WiFi密码"

第二次执行后如果返回WIFI CONNECTED和WIFI GOT IP,说明模块联网成功。此时再做一次AT+CIFSR能看到模块获得的IP地址,说明整个数据链路已经打通,可以进入下一步了。

2. NTP时间同步:不自己写时间协议,用AT固件帮你搞定

网络时间的获取,很多初学者第一反应是"我要在STM32上移植一个NTP客户端库"。这个方向本身没错,但放在"STM32+ESP8266 AT固件"这个场景里就重复造轮子了——ESP8266的AT固件已经内置了SNTP功能,直接用AT指令就能拿到网络时间。

2.1 两种时间获取思路的对比

获取网络时间其实有两条路:

  1. HTTP方式:向某个服务器发HTTP请求,从响应头的Date字段里解析时间。好处是服务器地址好找,缺点是HTTP报文头冗长,解析起来浪费RAM,而且时间是异步返回的,响应可能延迟。
  2. SNTP方式:用SNTP协议和NTP服务器通信,ESP8266 AT固件封装好了,直接用指令完成。SNTP报文短小,服务器响应高效,特别适合嵌入式设备。

我刚开始学的时候不知道有AT+CIPSNTPCFG这个指令,一直在折腾HTTP方式,从响应头里用strstr找Date:字段,代码写得又长又容易出错。后来在网上看别人项目笔记才发现AT固件还有这么个隐藏功能,瞬间省了至少几百行代码。

2.2 AT+CIPSNTPCFG 指令的使用细节

ESP8266 AT固件里和SNTP相关的有两条指令:

AT+CIPSNTPCFG=1,8,"ntp.xxx.com" AT+CIPSNTPTIME?

第一条指令的作用是使能SNTP并配置时区和服务器。参数解析:

  • 第一个参数1:使能SNTP(0为关闭,1为开启)
  • 第二个参数8:时区偏移,东八区填8
  • 第三个参数:NTP服务器域名,可以填你自己偏好的公共NTP服务器。我习惯填一个距离较近的公共NTP地址,不同地区延迟差别挺大

第二条指令AT+CIPSNTPTIME?用于查询当前时间。执行后,模块会返回类似这样的数据:

+CIPSNTPTIME:Fri Jan 15 10:23:45 2025

注意,这里返回的是ASCII字符串,不是Unix时间戳,所以在STM32里解析时,直接按字符串处理就行,比解析数值时间戳省事得多。

有一个细节:指令里的时区参数不是所有固件版本都支持小数时区,比如部分国家/地区的UTC+5.5时区就无法正确设置。我们国内用户几乎没有这个问题,直接填8即可。

2.3 时间戳解析与UTC时区换算

在STM32端解析这条返回数据,我写了一个简易解析函数。思路是:先判断收到的字符串里有没有+CIPSNTPTIME:前缀,有的话把冒号后面的部分切割出来,再按空格分成星期几、月份、日期、时间、年份五段,最后把月份字符串转成数字。

核心代码逻辑如下:

typedef struct { uint8_t year; // 年份后两位 uint8_t month; uint8_t day; uint8_t week; // 0代表周日 uint8_t hour; uint8_t minute; uint8_t second; } RTC_TimeTypeDef; uint8_t ParseSNTPTime(const char* buf, RTC_TimeTypeDef* time) { char weekStr[4]; const char* p = strstr(buf, "+CIPSNTPTIME:"); if (p == NULL) return 0; p += strlen("+CIPSNTPTIME:"); int yy, mo, dd, hh, mi, ss; char mon[4]; // 格式: Fri Jan 15 10:23:45 2025 sscanf(p, "%s %s %d %d:%d:%d %d", weekStr, mon, &dd, &hh, &mi, &ss, &yy); // 月份字符串转数字 static const char* months[] = {"Jan","Feb","Mar","Apr","May","Jun", "Jul","Aug","Sep","Oct","Nov","Dec"}; for (int i = 0; i < 12; i++) { if (strncmp(mon, months[i], 3) == 0) { mo = i + 1; break; } } time->year = yy % 100; time->month = mo; time->day = dd; time->hour = hh; time->minute = mi; time->second = ss; return 1; }

用sscanf直接按格式提取,简单粗暴还可靠。如果你想要的是"HTTP方式获取时间"的完整参考,思路就是向服务器发GET请求,然后从响应头找Date:字段,解析GMT时间再加8小时,但有了SNTP之后这步完全可以省略。

模块在收到AT+CIPSNTPCFG之后并不会立刻同步成功,需要等待几秒。实测下来,上电后先等大概3~5秒再查询时间比较稳妥。如果查询返回ERROR,多半是WiFi还没连上,或者SNTP服务器地址不可达,这时可以重新发一次配置指令并加延时。

从系统设计角度看,一个完整的开机时间同步流程可以定为:

  1. STM32发送AT+CWMODE=1,设置Station模式
  2. 发送AT+CWJAP连接WiFi
  3. 等待2秒,发送AT+CIPSNTPCFG=1,8,"ntp.xxx.com"
  4. 等待3秒,循环发送AT+CIPSNTPTIME?直到收到有效时间
  5. 把解析结果写入STM32内部RTC,之后整体计时交给RTC完成,不再占用ESP8266

这套流程跑通之后,你基本就拥有一个永不掉时间的时钟了,哪怕WiFi断了也不影响已同步的RTC继续跑。

3. 天气数据获取:HTTP请求与JSON解析

网络时间搞定了,接下来是天气。天气获取的核心链路是:STM32通过ESP8266向天气API服务器发起HTTP GET请求,服务器返回JSON格式的天气数据,STM32解析JSON提取温度、天气现象等信息。

3.1 天气API选型与URL构造

市面上的免费天气API接口不少,我只测试过其中几个,综合考虑稳定性和免费额度后,我选用的是国内一家天气服务商的免费版接口。免费版每天有调用次数限额,对于个人DIY项目完全够用,我实测每半小时刷新一次,一天48次,一个月也不到1500次,在免费额度范围内。

首先要做的是去官网注册账号,然后在控制台创建一个"个人版"应用,拿到API密钥(API Key)和城市ID列表。城市ID可以精确到县/区级,比如北京是"beijing",上海是"shanghai"。完整请求URL长这样:

https://api.xxxweather.com/v3/weather/now.json?key=YOUR_API_KEY&location=beijing&language=zh-Hans&unit=c

各个参数的含义:

  • key:你的API密钥,注册后自动生成
  • location:城市ID,官方文档里有对照表
  • language:显示语言,选zh-Hans返回中文天气现象
  • unit:温度单位,c代表摄氏度

URL里的v3/weather/now.json表示请求"实况天气"接口,返回的数据是当前天气状况。如果以后想加未来三天的预报,就把now改成daily,返回结构类似,解析方法通用。

这里要提醒一个坑:免费版API返回的数据格式和示例文档可能有差异,比如有的版本返回weather字段,有的返回text字段。我在写解析代码前先拿浏览器访问了一次接口URL,把返回的JSON原文保存下来,对着实际结构写解析代码,避免凭空对着文档猜字段。

3.2 AT指令发起GET请求

拿到URL之后,ESSP8266发起HTTP GET请求需要分几步走。

第一步,用AT+CIPSTART建立TCP连接。天气API通常走80端口(如果服务商支持HTTPS那就麻烦一点,AT固件原生不支持HTTPS,需要用其他方案,好在测试的这家提供HTTP明文接口):

AT+CIPSTART="TCP","api.xxxweather.com",80

如果返回CONNECT和OK,说明TCP连接建立成功。

第二步,拼接HTTP请求报文。一个最简的GET请求如下:

GET /v3/weather/now.json?key=YOUR_API_KEY&location=beijing&language=zh-Hans&unit=c HTTP/1.1 Host: api.xxxweather.com Connection: close

注意,报文最后必须有一个空行,也就是\r\n\r\n。Host头必须写,这和HTTP/1.1协议规范有关,有些服务器不校验Host,但为了保险还是加上。

第三步,使用AT+CIPSEND=<长度>发送报文长度,然后紧跟报文内容。由于报文长度是固定数字,我在STM32里先构造好报文存到缓冲数组,然后用strlen算长度。

char httpReq[256]; sprintf(httpReq, "GET /v3/weather/now.json?key=%s&location=%s&language=zh-Hans&unit=c HTTP/1.1\r\n" "Host: api.xxxweather.com\r\n" "Connection: close\r\n" "\r\n", apiKey, cityCode); uint16_t len = strlen(httpReq); sprintf(atBuf, "AT+CIPSEND=%d\r\n", len); UART_SendString(atBuf); DelayMs(100); UART_SendString(httpReq);

发送完请求后,ESP8266会陆续把服务器返回的数据通过串口推给STM32。响应报文长这个样子:

HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Length: 753 {"results":[{"location":{"id":"WX4FBXXFKE4F","name":"北京","country":"CN","path":"北京,北京,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},...

STM32要做的就是从这一坨数据里把JSON体截出来,再交给json解析库处理。

3.3 cJSON解析结构与过滤无关数据

关于JSON解析,通常有两种方案:直接用cJSON库,或者用strstr+手动切割。我用的是cJSON库,把cJSON.c和cJSON.h两个文件直接拷进工程即可。不过这个库依赖malloc/free内存管理,STM32的堆空间要提前调大,在启动文件或配置里把Heap_Size设大一些,我设为4KB足够用。

以下是我解析天气JSON的核心逻辑:

#include "cJSON.h" typedef struct { char text[16]; // 天气现象,如“晴” int temperature; // 摄氏温度 char lastUpdate[32]; // 数据更新时间 } WeatherInfo; WeatherInfo weather; void ParseWeatherJSON(const char* json) { cJSON* root = cJSON_Parse(json); if (root == NULL) return; cJSON* results = cJSON_GetObjectItem(root, "results"); cJSON* first = cJSON_GetArrayItem(results, 0); cJSON* now = cJSON_GetObjectItem(first, "now"); cJSON* text = cJSON_GetObjectItem(now, "text"); cJSON* temp = cJSON_GetObjectItem(now, "temperature"); strncpy(weather.text, text->valuestring, 15); weather.temperature = temp->valueint; cJSON_Delete(root); }

这段代码的关键是先看返回的JSON结构,results是一个数组,我们要取第0个元素(正常情况下只有这一个城市的数据),再进入now节点拿天气描述和温度。

有一个问题需要注意:cJSON解析失败时,cJSON_GetObjectItem返回的指针可能为NULL。如果服务器当时返回了错误提示(比如API调用次数超限),整个解析流程会出现空指针崩溃。所以每一层获取后都建议加一个空指针判断,或者至少在最外层判断root和now是否为NULL。

我实测解析一次完整的天气响应,耗时在20~30毫秒(72MHz主频),RAM占用大约1KB左右,完全在F103C8T6的承受范围内。

如果你不想引入cJSON依赖,也可以直接用strstr从原始JSON里找"temperature":"后的数值,再用atoi提取。这种字符串截取方式代码更短、占用更少,但灵活性差一些,一旦API返回结构变化就要跟着改。两种方案我都在项目里用过,日常DIY用strstr方式完全够用,但如果要考虑扩展(比如以后加湿度、风速、多日预报),直接上cJSON省心。

4. OLED显示与刷新策略:别让屏幕闪瞎眼

数据都拿到以后,怎么显示就决定了这个项目的使用体验。我用的是0.96寸SSD1306 OLED,128x64分辨率,I2C接口。这类屏幕在嵌入式DIY圈子里几乎是标配,驱动资料丰富,显示效果好,功耗还低。

4.1 SSD1306驱动基础与中文字库问题

SSD1306的驱动方式分硬件I2C和软件模拟I2C两种。我测试两种方式后,最终选择软件模拟I2C。原因很简单:F103的硬件I2C在一些组合下兼容性不理想,总线容易卡死,软件模拟虽然多占用两个GPIO,但时序完全可控,问题排查也方便。

OLED显示底层无非是初始化、设置坐标、写GRAM、刷新这几个动作。网上现成的SSD1306驱动代码非常多,找个注释清晰、结构简单的直接移植就行,我没做任何底层优化,因为对显示刷新率的要求不高。

真正容易踩坑的是汉字显示。天气现象返回的是"晴""多云""小雨"这种中文汉字,而SSD1306内部没有字库,必须自己提供点阵数据。处理方案有三种:

  1. 字库取模:把需要显示的汉字(晴、云、雨、雪、阴、雾等)提前用取模软件生成16x16点阵数组,做成一个小字库。最常用,我最终采用这个方案。
  2. 改用拼音/英文:解析时把中文映射成英文或拼音(Sunny/Cloudy/Rainy),这样用ASCII字库就能显示,但界面不够直观。
  3. 外挂Flash字库:用一个SPI Flash存储完整中文字库,容量充足,但电路复杂度上去了,DIY没必要。

取模时最容易被忽略的是取模方式设置。以PCtoLCD2002这个工具为例,需要设置为阴码、逐行式、逆向,这样生成的字模配合常规的显示函数才能正确显示。我第一次取模时没注意方向,显示出来全是镜像文字,排查了半天发现是取模方式选错了。如果你用的也是现成驱动函数,一定要先看驱动例程里字模数组的注释,确认取模设置和你的驱动匹配。

4.2 局部刷新方案与时间的秒级更新

OLED屏幕的显示刷新策略,直接决定用户第一眼看到的体验。最直观的例子:如果你每秒整屏全刷新一次,屏幕会明显闪烁,看着非常难受。

我采用的策略是分级刷新:

  • 时间部分每秒更新:因为时间精确到秒,所以只在上一秒位置重新绘制,不需要清屏。SSD1306的GRAM是128x64位,I2C传输一个整屏数据需要大约30ms,但如果只是改写时分秒所在的几个字节区域,速度几乎可以忽略。
  • 天气部分每30分钟更新:天气数据变化频率低,没必要频繁请求服务器,既省电也省API调用次数。每次更新前会先显示"更新中",然后清空天气区域重新绘制。
  • 日期和星期在每次时间同步或跨天时更新:跨天时把年份后两位、月份、日期、星期整体重绘一次。

局部刷新的实现方式并不复杂。SSD1306支持设置显示窗口,比如你要在指定坐标显示一个字符,只需要把光标指向该坐标,然后写入对应字模数据。例如在(32, 3)这个位置显示小时十位,就调用:

OLED_SetPos(32, 3); OLED_ShowChar('1', 16); // 显示16x8大小字符,数字用ASCII点阵

这样每次刷新只更新需要的字符位置,屏幕完全不闪,代码逻辑也更清晰。

4.3 刷新节奏与主循环设计

主循环的架构也很重要。我实际操作后的经验是,不要在主循环里做阻塞延时,而是用状态机思想组织:

static uint32_t lastTimeSecond = 0; static uint32_t lastWeatherUpdate = 0; while (1) { // 每秒更新时间显示 if (HAL_GetTick() - lastTimeSecond >= 1000) { lastTimeSecond = HAL_GetTick(); RTC_GetTime(&rtcTime); DisplayTime(&rtcTime); } // 每30分钟更新天气 if (HAL_GetTick() - lastWeatherUpdate >= 30*60*1000) { lastWeatherUpdate = HAL_GetTick(); FetchWeatherFromESP8266(); ParseWeatherJSON(rxBuffer); DisplayWeather(&weather); } // 处理串口中断收到的ESP8266返回数据 ProcessUartReceive(); }

这里的核心思想是把时间基准交给系统Tick,不在主循环里用DelayMs卡住,这样ESP8266的串口数据随时能被处理,不会因为CPU在延时而被丢弃。

OLED在I2C上还有一个细节:DHT11之类的传感器不要抢I2C总线。DHT11走的是单总线协议,如果用软件模拟I2C的两个引脚复用到了DHT11的数据脚,状态就会被搞乱。我在项目里把DHT11放在PB0上,和OLED的PB6/PB7完全隔离。

5. 断电重启后的配置持久化:Flash扇区读写

依赖串口手动输入WiFi密码和城市代码,用户用起来会非常痛苦。所以我花了点时间做了一个配置持久化:在STM32内部Flash里开辟一个专用扇区,专门存放WiFi名称、密码、城市代码、刷新间隔这些配置项,掉电不丢失。

5.1 为什么不用EEPROM或外部Flash

F103C8T6并没有I2C EEPROM,外部再挂一个AT24C02虽然可行,但多一个器件就多一份布线麻烦。直接用芯片内部Flash是最经济的选择——每个扇区可以擦除重写约1万次,对于"偶尔修改一次配置"的场景完全够用。

STM32F103C8T6的Flash容量是64KB,按扇区划分,每扇区1KB。为了不影响主程序代码区,我选用了**最后一个扇区(地址0x0800FC00,大小1KB)**存放配置。

5.2 配置结构体与读写流程

首先定义配置结构体:

typedef struct { uint32_t magic; // 魔数,判断配置是否有效,如0xA5A5A5A5 char ssid[32]; char password[32]; char city[16]; // 城市代码 uint16_t refreshInterval; // 天气刷新间隔,单位秒 } AppConfig; const AppConfig DEFAULT_CONFIG = { .magic = 0xA5A5A5A5, .ssid = "your_wifi", .password = "your_password", .city = "beijing", .refreshInterval = 1800 };

Flash写入流程分三步:

  1. 解锁Flash:调用FLASH_Unlock(),并等待操作完成
  2. 擦除目标扇区:调用FLASH_ErasePage(0x0800FC00),注意必须整个扇区擦除,没有按字节擦除的操作
  3. 写入数据:按32位字为单位写入,调用FLASH_ProgramWord(addr, data)
void Flash_WriteConfig(const AppConfig* cfg) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); FLASH_ErasePage(CONFIG_FLASH_ADDR); uint32_t* pSrc = (uint32_t*)cfg; uint32_t addr = CONFIG_FLASH_ADDR; for (uint8_t i = 0; i < sizeof(AppConfig) / 4; i++) { FLASH_ProgramWord(addr, *pSrc++); addr += 4; } FLASH_Lock(); }

读取时就简单多了,直接指针强转取出:

AppConfig* cfg = (AppConfig*)CONFIG_FLASH_ADDR; if (cfg->magic != 0xA5A5A5A5) { // 说明Flash里没有有效配置,直接用默认值 cfg = &DEFAULT_CONFIG; }

5.3 配置修改的交互逻辑

实现一个串口配置协议,用户在PC端通过串口发送一行指令就能改配置。例如:

SSID:mywifi PASS:12345678 CITY:shanghai SAVE

STM32收到SAVE后,把当前RAM中的配置结构体整体写回Flash。这样用户不用重新编译固件就能改WiFi和城市,对后续维护友好得多。

一个隐藏坑是Flash写入时间。擦除一个1KB扇区大约耗时20~40ms,期间CPU会阻塞。如果此时正好有ESP8266数据通过串口进来,很可能被丢掉,因为硬件串口FIFO只有很小的空间。所以在写入Flash前,我建议先停掉串口接收中断,写入完成后再恢复。更进一步的做法是先读取配置,之后的联网、请求、显示流程全部基于RAM中的副本,Flash只在写入时被访问一次。

6. 实测踩坑记录:三个最隐蔽的问题

这个项目从图纸到实际跑通,前后踩了不少坑。有些问题在资料里完全搜不到,得靠示波器或串口日志一步步定位。这里挑三个最典型、也最容易同样栽进去的问题说一下完整排查链路。

6.1 ESP8266-01S供电不足导致反复重启

现象:ESP8266一执行AT+CWJAP连接WiFi,模块就不断重启,串口输出乱码,有时候连AT指令都无法稳定回显。而且同一个模块在USB-TTL供电时一切正常,接到STM32板上就出问题。

排查过程:先用万用表测ESP8266的VCC电压,发现USB-TTL供电时3.3V稳定,STM32板载3.3V输出时电压跌到2.9V左右。拿起示波器看波形,清晰地看到WiFi发包瞬间的电流尖峰把电压拉低到2.3V左右,远低于ESP8266的最低工作电压。问题根源在于STM32最小系统板的3.3V稳压器最大输出电流只有几百毫安,WiFi瞬间大电流直接把它拖垮了。

解决方式:给ESP8266完全独立的供电通路——AMS1117-3.3V稳压模块直接从5V输入取电,并在ESP8266的VCC和GND之间并联一个大电容(我用的是470uF电解电容),实测电压纹波降到可忽略的程度,问题彻底消失。

6.2 天气HTTP响应不完整

现象:请求天气接口后,STM32收到的响应时有时无,经常只有一部分JSON就断了,解析自然失败。还有一些情况下,整段JSON收到了,但是末尾被截断,少了一个或多个字符。

排查过程:我先用串口调试助手直接观察ESP8266返回的原始数据,发现响应内容被拆成了多个TCP数据包推过来,中间还有+IPD前缀。问题在于ESP8266 AT固件在接收大数据时,默认的透传Buffer有限,超过部分会被丢弃,或因为STM32端串口接收处理不及时导致数据丢失。

解决方式:一共有三处要配合调整。第一,把ESP8266的串口波特率提高到115200,减少数据传输时间(虽然收到数据才几字节,但波特率低时容易积压)。第二,在STM32端的串口接收中断里,把数据先挪进一个大环形缓冲区(我开了2KB),不在中断里做任何解析,只做缓存。第三,发送完HTTP请求后,等待约500毫秒(时间取决于服务商响应速度),再一次性从缓冲区读取完整响应,因为大多数HTTP服务器响应速度都在这个范围内。

代码层面,我的串口接收中断只做一件事:

void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE)) { uint8_t ch = USART_ReceiveData(USART2); RingBuffer_Push(&rxRing, ch); // 入环形缓冲区 } }

然后主循环在空闲时调用解析函数,如果发现缓冲区里出现了HTTP/1.1 200 OK和{"results"这类关键标记,就把整段JSON切出来。如果缓冲区里还没出现,就等待或重发请求。

6.3 时间差8小时与恢复出厂时间

现象:SNTP时间同步成功后,显示的时间比本地时间快了8小时。另一个奇怪问题是,时间同步成功后过了一晚,第二天显示的时间又变成2020年1月1日这种出厂默认值。

第一个问题的原因非常直接:时区配置没有生效。ESP8266 AT固件的AT+CIPSNTPCFG第二个参数是时区偏移,但有些版本的固件把这个参数当作"GMT偏移的负数"处理,也就是说你写8,它反而减8。解决方式是查你手里的固件版本,如果是老版本就在解析时间后手动加8小时;新版本则可以正常使用。我实测过手边两个模块,一个加8一个不加,所以严谨做法是固定用同一批固件,用一个已知本地时间验证一次。

第二个问题的根源是STM32内部RTC没有后备电池。F103C8T6的RTC靠VBAT引脚供电,如果VBAT没接电池或超级电容,主电源一断电RTC就全部归零。我用一个小纽扣电池接到VBAT解决了,这样系统重启后RTC依然走时,ESP8266的SNTP只作为开机对时和每天校时使用。

顺带一提,如果不想接电池,也可以在每次开机时都做一次SNTP同步,并把同步结果写入Flash,时间复杂度也不高,只是断电期间的时间(纯靠Flash里的最后时间加上开机后的秒数)会漂移,精度不如RTC。但实际上多数DIY项目对断电时的走时精度要求不高,有Flash方案够用。

7. 还能怎么玩:从天气时钟到桌面信息终端

这个项目跑通以后,整个架构就像搭好了一块积木底座。WiFi联网、时间同步、天气获取、数据显示、配置持久化这五件事,是做任何联网型桌面设备都会遇到的公共问题。基于这套底座,可以延展出很多好玩的东西。

加DHT11温湿度传感器是最简单的扩展。在解析天气数据的同时,本地采集室内的温湿度,OLED上半屏显示本地数据,下半屏显示室外天气,对比起来挺有意思。注意DHT11的采集间隔至少要1秒以上,采集太频繁会不稳定。

加未来几天预报是把天气接口从now换成daily,JSON结构从"当前实况"变成"每日预报数组"。解析方式类似,只是要循环遍历三天的数据,OLED上做成左右翻页或上下滚动显示。

接一个扬声器做整点报时,到整点播放提示音。再配合RTC的闹钟功能,还能做闹钟。

加光敏电阻自动调亮度,OLED在晚上自动降低亮度,白天恢复高亮度,避免灯光刺眼,这个用ADC读取即可。

把ESP8266换成ESP32-C3,继续保留STM32做主控,通信协议不变,但C3模块支持BLE,可以做手机App近场配置WiFi。这样连串口配置都不用了,手机上直接发WiFi名密码过去,体验完全向商业产品靠拢。

我个人最喜欢的一个扩展是把OLED换成一块1.3寸或1.5寸彩色TFT屏,显示天气图标和实时温度的曲线图。不过这也是对STM32 RAM和Flash的一次大考验,F103C8T6的64KB Flash写代码要精打细算,20KB RAM跑图片缓冲也比较紧张,可能需要砍掉一些功能或者升级主控。

最后再分享几个我实测后的小建议

第一,整个项目最值得沉淀的不是某一段代码,而是把"串口AT指令通信"这套交互方式吃透。很多物联网模块(4G、LoRa、NB-IoT)都提供类似的AT指令接口,这套思路可以平移到很多项目上,只不过协议字段换一下。第二,虽然文章里贴了cJSON解析的代码,但如果你只做这一个项目,直接字符串截取就够了,不必为了"用库"而用库。第三,调试时一切靠串口日志,建议在STM32代码里做一个DEBUG_PRINT宏开关,调试时打开输出到USART1,发布时关掉,省得每次改代码都要删日志。

这个项目我做完之后放在桌面上跑了几个月,除了偶尔断电需要重启之外没有任何问题。最让我意外的是它的实用性——每天看一眼OLED上的温度和天气,比掏出手机解锁看天气方便多了。如果哪天WiFi账号换了,直接串口发指令即可,不用重新烧录程序。如果你手里正好有一块吃灰的STM32和ESP8266,花一个周末就能把它变成一个有用的桌面小玩意,整个过程比想象中顺利。

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

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

立即咨询