1、环境
①环境硬件:
主板:ESP32-S3 开发板 ESP32 SuperMini

TFT屏幕:2.0寸TFT模块串口屏240x320液晶显示屏ST7789V
温湿度传感器:AHT20+BMP280温湿度气压模块
②软件环境:
ArduinoIDE2.3.7 + esp32 2.0.14
2、功能
①连接Wifi显示星期、日期、时间(秒数每秒变化一次)
②连接Wifi请求和风天气API显示天气(晴,温度,风向,风级),根据天气显示图标
③连接温湿度传感器,显示室内温度,湿度,气压
3、主要解决的问题
①七段数字格式显示
②字体的设置,字体文件的制作
③图片的显示,图片 .h文件的制作
④和风天气API返回GZIP数据的处理
⑤开发板的安装
⑥各种库的安装
cpp
// ===================== 核心优化:内置轻量级Gzip解压核心代码【无需任何库】✅ 最关键瘦身点 =====================
#include <zlib.h>
#define GZIP_WINDOW_BITS 16 + MAX_WBITS
#define DECOMPRESS_BUFFER_SIZE 512
#define COMPRESS_BUFFER_SIZE 512
#define COLOR_1B2B3A 0x0000//黑色
uint32_t gzipDecompress(uint8_t *inData, uint32_t inLen, uint8_t *outData, uint32_t outLen) {
z_stream strm;
memset(&strm, 0, sizeof(z_stream));
strm.next_in = inData;
strm.avail_in = inLen;
strm.next_out = outData;
strm.avail_out = outLen;
if (inflateInit2(&strm, GZIP_WINDOW_BITS) != Z_OK) return 0;
int ret = inflate(&strm, Z_FINISH);
inflateEnd(&strm);
return (ret == Z_STREAM_END) ? strm.total_out : 0;
}
// 获取网络时间相关库
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
// TFT显示库
#include <TFT_eSPI.h>
// 网络请求+JSON解析库
#include <HTTPClient.h>
#include <ArduinoJson.h>
// 仅保留核心I2C库 零依赖
#include <Wire.h>
#define I2C_SDA 7 // SDA引脚,固定接7
#define I2C_SCL 6 // SCL引脚,固定接6
#define READ_DELAY 1000 // 数据读取间隔
// AHT20 寄存器定义
#define AHT20_ADDR 0x38
// BMP280 寄存器定义
#define BMP280_ADDR 0x76
#define BMP280_RESET 0xE0
#define BMP280_CTRL_MEAS 0xF4
#define BMP280_CONFIG 0xF5
#define BMP280_PRESS_MSB 0xF7
#define BMP280_TEMP_MSB 0xFA
// 导入字库
#include "font/noto20.h"
#include "font/SeverSegmentNumber28.h"
#include "font/SeverSegmentNumber68.h"
#include "font/WeatherFont30.h"
#include "font/tianqi28.h"
#include "font/WeatherFont25.h"
#include "WeatherIcon/sun.h"
#include "WeatherIcon/cloud.h"
#include "WeatherIcon/duoyun.h"
#include "WeatherIcon/yin.h"
#include "WeatherIcon/rain.h"
// ===================== 配置项 无修改 =====================
const char *ssid = "ah-student";
const char *password = "AIhuaWx2024";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.aliyun.com");
const char *WEATHER_API_KEY = "c049f972cfb44a55afecdcd77fc64580";
const char *CITY_ID = "101120501";
const int WEATHER_REFRESH_INTERVAL = 10 * 60 * 1000;
const int NTP_UPDATE_INTERVAL = 60 * 1000;
// TFT相关定义
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);
// ===================== 全局变量 精简冗余 + 初始时间默认值 =====================
const char weekdays_cn[8][10] = { "0", "周一", "周二", "周三", "周四", "周五", "周六", "周日" };
// ✅ 初始默认时间:防止NTP首次获取失败时显示0
int currentYear = 2026;
int currentWeekDay = 1;
int currentMonth = 1;
int currentMonthDay = 1;
int currentHour = 0;
int currentMin = 0;
int currentSec = 0;
String weatherText = "未知";
int temp = 25;
int humidity = 50;
unsigned long lastWeatherUpdate = 0;
unsigned long lastNtpUpdate = 0;
int weatherIconCode = 100;
bool showClockStyle = true;
unsigned long lastUpdateTime = 0;
unsigned long lastStyleSwitch = 0;
const int STYLE_SWITCH_INTERVAL = 10 * 1000;
float pressure_hpa = 1013.25;
float altitude_m = 0.0;
#define SEA_LEVEL_PRESSURE 1013.25
uint16_t dig_T1, dig_P1;
int16_t dig_T2, dig_T3, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;
// ========== ✅ 数字补零格式化【传参缓冲区 线程安全版】 ==========
void formatTwoDigits(int num, char *buf) {
buf[0] = (num < 10) ? '0' : (num / 10) + '0';
buf[1] = (num % 10) + '0';
buf[2] = '\0';
}
// I2C基础驱动
void I2C_Write(uint8_t addr, uint8_t reg, uint8_t data) {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.write(data);
Wire.endTransmission();
}
void I2C_Read(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t len) {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(addr, len);
for (uint8_t i = 0; i < len; i++) buf[i] = Wire.read();
}
// AHT20温湿度读取
void readAHT20() {
uint8_t data[6] = { 0 };
Wire.beginTransmission(AHT20_ADDR);
Wire.write(0xAC);
Wire.write(0x33);
Wire.write(0x00);
Wire.endTransmission();
delayMicroseconds(80000);
Wire.requestFrom(AHT20_ADDR, 6);
if (Wire.available() == 6)
for (uint8_t i = 0; i < 6; i++) data[i] = Wire.read();
if ((data[0] & 0x80) == 0 && (data[0] & 0x08) == 0x08) {
long humi = ((long)data[1] << 12) | ((long)data[2] << 4) | (data[3] >> 4);
long temp_raw = (((long)data[3] & 0x0F) << 16) | ((long)data[4] << 8) | (long)data[5];
humidity = constrain((int)(humi * 100.0 / 0x100000), 0, 100);
temp = constrain((int)(temp_raw * 200.0 / 0x100000 - 50), -20, 80);
}
}
// BMP280初始化
void BMP280_Init() {
uint8_t buf[24];
I2C_Write(BMP280_ADDR, BMP280_RESET, 0xB6);
delay(100);
I2C_Read(BMP280_ADDR, 0x88, buf, 24);
dig_T1 = (buf[1] << 8) | buf[0];
dig_T2 = (buf[3] << 8) | buf[2];
dig_T3 = (buf[5] << 8) | buf[4];
dig_P1 = (buf[7] << 8) | buf[6];
dig_P2 = (buf[9] << 8) | buf[8];
dig_P3 = (buf[11] << 8) | buf[10];
dig_P4 = (buf[13] << 8) | buf[12];
dig_P5 = (buf[15] << 8) | buf[14];
dig_P6 = (buf[17] << 8) | buf[16];
dig_P7 = (buf[19] << 8) | buf[18];
dig_P8 = (buf[21] << 8) | buf[20];
dig_P9 = (buf[23] << 8) | buf[22];
I2C_Write(BMP280_ADDR, BMP280_CTRL_MEAS, 0x77);
I2C_Write(BMP280_ADDR, BMP280_CONFIG, 0x00);
}
// BMP280气压海拔读取
void readBMP280() {
int32_t temp_raw, press_raw;
int32_t t_fine;
uint8_t data[6] = { 0 };
I2C_Read(BMP280_ADDR, BMP280_TEMP_MSB, data, 6);
temp_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
press_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
int64_t var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11;
int64_t var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
t_fine = var1 + var2;
int64_t p = t_fine - 128000;
p = (((press_raw >> 13) - (((int32_t)dig_P1) << 1)) - (((p * p) >> 12) * ((int32_t)dig_P2)) >> 11) * (((((p * p) >> 13) * ((int32_t)dig_P3)) >> 16) + (((int32_t)dig_P4) * p) >> 15);
p = p + (((int32_t)dig_P5) << 4);
p = p - (((((p >> 15) * (p >> 15)) >> 3) * ((int32_t)dig_P6)) >> 4);
p = p + (((int32_t)dig_P7) * p) >> 15;
p = p + (((int32_t)dig_P8) << 10);
p = p + dig_P9;
p = p >> 8;
if (p > 0) pressure_hpa = (float)p / 256.0;
if (pressure_hpa > 300 && pressure_hpa < 1200) altitude_m = 44330.0 * (1.0 - pow(pressure_hpa / SEA_LEVEL_PRESSURE, 0.1903));
}
// 读取传感器数据
void readSensorData() {
readAHT20();
readBMP280();
}
// WiFi断线自动重连
void checkWiFiReconnect() {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("WiFi重连中...");
WiFi.disconnect();
WiFi.begin(ssid, password);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 20) {
delay(500);
Serial.print(".");
retry++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi重连成功");
getYantaiWeather();
} else Serial.println("\nWiFi重连失败");
}
}
// ========== Gzip解压和风天气【轻量级内置解压,无库依赖】 ==========
void getYantaiWeather() {
checkWiFiReconnect();
if (WiFi.status() != WL_CONNECTED) {
weatherText = "无网络";
return;
}
HTTPClient http;
String url = "https://mw6apx8fr5.re.qweatherapi.com/v7/weather/now?location="
+ String(CITY_ID) + "&key=" + String(WEATHER_API_KEY);
http.begin(url);
http.addHeader("Accept", "application/json");
http.addHeader("User-Agent", "ESP32");
http.addHeader("Accept-Encoding", "gzip");
http.setTimeout(5000);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
uint8_t compressedBuf[COMPRESS_BUFFER_SIZE] = { 0 };
uint8_t decompressedBuf[DECOMPRESS_BUFFER_SIZE] = { 0 };
size_t compressLen = http.getStreamPtr()->readBytes(compressedBuf, COMPRESS_BUFFER_SIZE);
if (compressLen > 0) {
size_t decompressLen = gzipDecompress(compressedBuf, compressLen, decompressedBuf, DECOMPRESS_BUFFER_SIZE);
if (decompressLen > 0) {
DynamicJsonDocument doc(256);
DeserializationError error = deserializeJson(doc, (char *)decompressedBuf);
if (!error) {
weatherText = doc["now"]["text"].as<String>() + ","
+ doc["now"]["temp"].as<String>() + "°" + ","
+ doc["now"]["windDir"].as<String>() + doc["now"]["windScale"].as<String>()+"级";
weatherIconCode = doc["now"]["icon"].as<int>();
} else weatherText = "本地气象";
} else weatherText = "解压失败";
} else weatherText = "数据为空";
} else weatherText = "请求失败";
http.end();
lastWeatherUpdate = millis();
}
// 根据天气编码显示对应图标
void showWeatherIcon(int x, int y) {
switch(weatherIconCode){
//case 100: for(int i = 0;i<frames;i++){tft.pushImage(x,y,32,32,sun_32x32[i]);delay(100)};break;// 晴→太阳(黄色)
case 100: sprite.pushImage(x,y,60,60,sun);break;// 晴→太阳(黄色)
case 101: sprite.pushImage(x,y,60,60,duoyun);break;// 多云→云朵(白色)
case 102: sprite.pushImage(x,y,60,60,duoyun);break; break;// 少云→云朵
case 104: sprite.pushImage(x,y,60,60,cloud);break; // 阴→阴云
case 305: sprite.pushImage(x,y,60,60,rain);break; // 小雨→雨滴
case 306: sprite.pushImage(x,y,60,60,rain);break; // 中雨→雨滴
case 307: sprite.pushImage(x,y,60,60,rain);break; // 大雨→雨滴
//case 401: sprite.pushImage(x,y,60,60,snow);break; // 大雨→雨滴
default: sprite.pushImage(x,y,60,60,sun);break; // 默认太阳
}
}
// ========== ✅ 新增核心函数:将NTP时间同步到全局变量【初始化专用+updateTime内部调用】 ==========
void syncNtpTimeToGlobal() {
unsigned long epochTime = timeClient.getEpochTime();
struct tm *ptm = localtime((time_t *)&epochTime);
currentYear = ptm->tm_year + 1900;
currentMonth = ptm->tm_mon + 1;
currentMonthDay = ptm->tm_mday;
currentWeekDay = (ptm->tm_wday == 0) ? 7 : ptm->tm_wday;
currentHour = ptm->tm_hour;
currentMin = ptm->tm_min;
currentSec = ptm->tm_sec;
// 串口打印同步的真实时间,方便调试
Serial.printf("✅ NTP时间同步成功:%04d/%02d/%02d %02d:%02d:%02d %s\n", currentYear, currentMonth, currentMonthDay, currentHour, currentMin, currentSec, weekdays_cn[currentWeekDay]);
}
// ✅ 修复核心逻辑+时区BUG:NTP时间更新 精准走时 无延迟
void updateTime() {
unsigned long currentMillis = millis();
if (currentMillis - lastNtpUpdate >= NTP_UPDATE_INTERVAL) {
lastNtpUpdate = currentMillis;
if (timeClient.update()) {
syncNtpTimeToGlobal(); // ✅ 调用同步函数,直接赋值全局变量
return;
}
}
// 秒数本地自增 精准无延迟 进位逻辑完善
currentSec++;
if (currentSec >= 60) {
currentSec = 0;
currentMin++;
if (currentMin >= 60) {
currentMin = 0;
currentHour++;
if (currentHour >= 24) currentHour = 0;
}
}
Serial.printf("%02d:%02d:%02d | %s | %d℃ %d%% | %.1fhPa\n", currentHour, currentMin, currentSec, weatherText.c_str(), temp, humidity, pressure_hpa);
}
// ========== 样式1:标准时钟样式【带冒号 你修改的全部保留+排版美化】 ==========
void clockStyle() {
sprite.fillScreen(COLOR_1B2B3A);
char numBuf[3]; // 临时缓冲区,用于补零格式化
// ✅ 星期+日期 同一行 统一字体 无错位
sprite.setTextColor(TFT_GREEN);
sprite.setCursor(25, 30);
sprite.loadFont(noto20);
sprite.print(weekdays_cn[currentWeekDay]);
sprite.print(" ");
formatTwoDigits(currentMonth, numBuf);
sprite.print(String(currentYear) + "/" + numBuf + "/");
formatTwoDigits(currentMonthDay, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 时分 青蓝色 TFT_CYAN 大号字体 居中显示
sprite.setTextColor(TFT_BLUE);
sprite.setCursor(30, 80);
sprite.loadFont(SeverSegmentNumber68);
formatTwoDigits(currentHour, numBuf);
sprite.print(numBuf);
sprite.setCursor(105, 70);
sprite.print(":");
sprite.setCursor(120, 80);
formatTwoDigits(currentMin, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 秒数 绿色 TFT_GREEN 实时跳动
sprite.setTextColor(TFT_GREEN);
sprite.setCursor(195, 100);
sprite.loadFont(SeverSegmentNumber28);
formatTwoDigits(currentSec, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 烟台天气: + 右侧对齐
sprite.setTextColor(TFT_BLUE);
sprite.setCursor(20, 160);
sprite.loadFont(WeatherFont30);
sprite.print("烟台天气:");
sprite.setTextColor(TFT_MAGENTA);
sprite.setCursor(20, 210);
sprite.loadFont(WeatherFont25);
sprite.print(weatherText);
sprite.unloadFont();
showWeatherIcon(160, 140);//图标
sprite.setTextColor(TFT_OLIVE);
sprite.setCursor(20, 250);
sprite.loadFont(tianqi28);
sprite.print("温湿度: ");
sprite.setCursor(120, 255);
sprite.loadFont(SeverSegmentNumber28);
sprite.print(String(temp) + " C " + String(humidity) + "%");
sprite.unloadFont();
sprite.setTextColor(TFT_ORANGE);
sprite.setCursor(20, 285);
sprite.loadFont(tianqi28);
sprite.print("气压: ");
sprite.setCursor(100, 290);
sprite.loadFont(SeverSegmentNumber28);
sprite.print(String(pressure_hpa, 1) + "hPa");
sprite.unloadFont();
sprite.pushSprite(0, 0);
}
// ========== 样式2:简约时钟样式【无冒号 配色统一】 ==========
void moonClockStyle() {
sprite.fillScreen(COLOR_1B2B3A);
char numBuf[3]; // 临时缓冲区,用于补零格式化
// ✅ 星期+日期 同一行
sprite.setTextColor(TFT_GREEN);
sprite.setCursor(25, 30);
sprite.loadFont(noto20);
sprite.print(weekdays_cn[currentWeekDay]);
sprite.print(" ");
formatTwoDigits(currentMonth, numBuf);
sprite.print(String(currentYear) + "/" + numBuf + "/");
formatTwoDigits(currentMonthDay, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 时分 青蓝色 无冒号
sprite.setTextColor(TFT_BLUE);
sprite.setCursor(30, 80);
sprite.loadFont(SeverSegmentNumber68);
formatTwoDigits(currentHour, numBuf);
sprite.print(numBuf);
sprite.setCursor(105, 70);
sprite.print(":");
sprite.setCursor(120, 80);
formatTwoDigits(currentMin, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 秒数 绿色 实时跳动
sprite.setTextColor(TFT_GREEN);
sprite.setCursor(195, 100);
sprite.loadFont(SeverSegmentNumber28);
formatTwoDigits(currentSec, numBuf);
sprite.print(numBuf);
sprite.unloadFont();
// ✅ 烟台天气: + 右侧对齐
sprite.setTextColor(TFT_YELLOW);
sprite.setCursor(20, 160);
sprite.loadFont(WeatherFont30);
sprite.print("烟台天气:");
sprite.setTextColor(TFT_MAGENTA);
sprite.setCursor(20, 210);
sprite.loadFont(WeatherFont25);
sprite.print(weatherText);
sprite.unloadFont();
showWeatherIcon(160, 140);//图标
sprite.setTextColor(TFT_OLIVE);
sprite.setCursor(20, 250);
sprite.loadFont(tianqi28);
sprite.print("温湿度: ");
sprite.setCursor(120, 255);
sprite.loadFont(SeverSegmentNumber28);
sprite.print(String(temp) + " C " + String(humidity) + "%");
sprite.unloadFont();
sprite.setTextColor(TFT_ORANGE);
sprite.setCursor(20, 285);
sprite.loadFont(tianqi28);
sprite.print("气压: ");
sprite.setCursor(100, 290);
sprite.loadFont(SeverSegmentNumber28);
sprite.print(String(pressure_hpa, 1) + "hPa");
sprite.unloadFont();
sprite.pushSprite(0, 0);
}
void setup() {
Serial.begin(115200);
delay(1000);
// WiFi连接
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi已连接");
// NTP初始化 + ✅ 强制获取时间(烧录后立即拿到真实时间)
timeClient.begin();
timeClient.setTimeOffset(28800); // 东八区
Serial.print("正在获取NTP时间...");
// 循环强制获取,直到成功
while (!timeClient.forceUpdate()) {
delay(500);
Serial.print(".");
}
Serial.println("\nNTP时间获取成功!");
syncNtpTimeToGlobal(); // ✅ 核心修复:获取成功后立即同步到全局变量
// TFT初始化
tft.begin();
tft.setRotation(0);
sprite.setColorDepth(16);
sprite.setSwapBytes(true);
// 屏幕初始化填充该颜色
tft.fillScreen(COLOR_1B2B3A);
sprite.createSprite(240, 320);
// 传感器初始化
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(400000);
BMP280_Init();
readSensorData(); // 读取初始温湿度气压
// 获取初始天气
getYantaiWeather();
// ✅ 烧录后立即绘制屏幕,无需等待loop的1秒延迟
clockStyle();
Serial.println("初始化完成,屏幕已显示!");
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - lastWeatherUpdate >= WEATHER_REFRESH_INTERVAL) getYantaiWeather();
// 每秒刷新 秒数实时变化
if (currentMillis - lastUpdateTime >= 1000) {
lastUpdateTime = currentMillis;
readSensorData();
updateTime();
showClockStyle ? clockStyle() : moonClockStyle();
}
// 10秒切换样式
if (currentMillis - lastStyleSwitch >= STYLE_SWITCH_INTERVAL) {
lastStyleSwitch = currentMillis;
showClockStyle = !showClockStyle;
}
}