ESP32小屏幕玩出花:用TFT_eSPI库在1.3寸ST7789上显示动态天气和传感器数据

当一块1.3寸的ST7789屏幕遇上ESP32,能碰撞出怎样的火花?这不仅仅是点亮几个像素点那么简单。想象一下,你的桌面上摆放着一个精致的小设备,实时显示着室内温湿度、室外天气状况,甚至还有动态的温度计效果——这就是我们今天要实现的创意项目。

对于已经熟悉ESP32基础开发的创客来说,如何将硬件能力转化为实际应用才是真正的挑战。本文将带你从零开始,构建一个完整的动态信息仪表盘系统。这个系统不仅会读取本地传感器数据,还能通过WiFi获取网络天气信息,最终在一块240×240像素的小屏幕上呈现专业级的可视化效果。

1. 硬件准备与环境搭建

1.1 所需硬件清单

要完成这个项目,你需要准备以下硬件组件:

  • ESP32开发板 :推荐使用ESP32-WROOM-32,它具备足够的处理能力和内存
  • 1.3寸ST7789 SPI屏幕 :240×240分辨率,带IPS面板的版本显示效果更佳
  • 温湿度传感器 :DHT22(AM2302)精度更高,DHT11更经济实惠
  • 面包板与连接线 :用于快速原型搭建
  • Micro USB数据线 :用于供电和程序烧录

硬件连接时,特别注意SPI接口的引脚分配。以下是推荐接线方式:

屏幕引脚 ESP32引脚 备注
VCC 3.3V 电源正极
GND GND 电源地
SCL GPIO18 SPI时钟线
SDA GPIO23 SPI数据线
RES GPIO17 复位引脚(低电平有效)
DC GPIO16 数据/命令选择引脚
CS GPIO5 片选引脚

1.2 软件环境配置

开发环境我们选择PlatformIO + VSCode组合,它比传统的Arduino IDE更适合管理复杂项目。

首先安装必要的库:

# 在PlatformIO项目的lib_deps中添加以下库
lib_deps = 
    bodmer/TFT_eSPI@^2.4.79
    adafruit/DHT sensor library@^1.4.4
    bblanchon/ArduinoJson@^6.19.4

TFT_eSPI库需要正确配置才能驱动ST7789屏幕。找到库目录下的 User_Setup.h 文件,进行如下修改:

#define ST7789_DRIVER     // 指定驱动芯片类型
#define TFT_WIDTH  240    // 屏幕宽度
#define TFT_HEIGHT 240    // 屏幕高度
#define TFT_MOSI 23       // SPI数据引脚
#define TFT_SCLK 18       // SPI时钟引脚
#define TFT_CS   5        // 片选引脚
#define TFT_DC   16       // 数据/命令选择引脚
#define TFT_RST  17       // 复位引脚
#define LOAD_GLCD          // 启用默认字体
#define SPI_FREQUENCY  40000000  // SPI通信频率

提示:如果屏幕显示颜色异常,尝试调整 TFT_RGB_ORDER 定义,ST7789通常需要设置为 TFT_RGB_ORDER TFT_RGB

2. 传感器数据采集与网络连接

2.1 读取DHT传感器数据

DHT系列传感器虽然使用简单,但在实际应用中需要注意读取时机和错误处理。以下是经过优化的读取代码:

#include <DHT.h>
#define DHTPIN 4      // 传感器数据引脚
#define DHTTYPE DHT22 // DHT 22 (AM2302)

DHT dht(DHTPIN, DHTTYPE);

void setup() {
  Serial.begin(115200);
  dht.begin();
}

float readTemperature() {
  float t = dht.readTemperature();
  if (isnan(t)) {
    Serial.println("Failed to read temperature!");
    return -999.0; // 特殊值表示读取失败
  }
  return t;
}

float readHumidity() {
  float h = dht.readHumidity();
  if (isnan(h)) {
    Serial.println("Failed to read humidity!");
    return -999.0;
  }
  return h;
}

2.2 连接WiFi并获取天气数据

要让ESP32接入网络获取天气信息,我们需要完成WiFi连接和HTTP请求处理。这里使用免费的OpenWeatherMap API:

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* ssid = "your_SSID";
const char* password = "your_PASSWORD";
const String apiKey = "your_API_KEY";
const String city = "Beijing";

void connectWiFi() {
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnected!");
  } else {
    Serial.println("\nConnection failed!");
  }
}

String getWeatherData() {
  if (WiFi.status() != WL_CONNECTED) return "";
  
  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + apiKey + "&units=metric";
  
  http.begin(url);
  int httpCode = http.GET();
  
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    http.end();
    return payload;
  } else {
    http.end();
    return "";
  }
}

void parseWeatherData(String jsonStr, float &temp, float &humidity, String &conditions) {
  DynamicJsonDocument doc(1024);
  deserializeJson(doc, jsonStr);
  
  temp = doc["main"]["temp"];
  humidity = doc["main"]["humidity"];
  conditions = doc["weather"][0]["main"].as<String>();
}

注意:实际项目中应该将API密钥等敏感信息存储在单独的配置文件中,不要硬编码在代码里

3. 屏幕UI设计与动态效果实现

3.1 基础UI框架搭建

在240×240的小屏幕上设计UI需要精打细算每个像素。我们先规划几个显示区域:

  1. 顶部区域 (高度40px):显示城市名称和当前天气状况
  2. 中间左侧 (120×160):室外温度显示与趋势图
  3. 中间右侧 (120×160):室内传感器数据显示
  4. 底部区域 (高度40px):状态栏(WiFi信号、更新时间)

创建基础绘制函数:

#include <TFT_eSPI.h>
TFT_eSPI tft;

void initDisplay() {
  tft.init();
  tft.setRotation(1); // 根据屏幕实际方向调整
  tft.fillScreen(TFT_BLACK);
}

void drawHeader(String city, String weather) {
  tft.fillRect(0, 0, 240, 40, TFT_NAVY);
  tft.setTextColor(TFT_WHITE, TFT_NAVY);
  tft.setTextSize(2);
  tft.drawString(city, 10, 10);
  tft.drawString(weather, 150, 10);
}

void drawFooter(bool wifiStatus) {
  tft.fillRect(0, 200, 240, 40, TFT_DARKGREY);
  // 绘制WiFi状态图标
  if (wifiStatus) {
    tft.fillCircle(220, 220, 8, TFT_GREEN);
  } else {
    tft.fillCircle(220, 220, 8, TFT_RED);
  }
}

3.2 温度计动态效果

静态显示温度值不够直观,我们实现一个动态的温度计柱状图:

void drawThermometer(float temp, float minTemp, float maxTemp, int x, int y) {
  // 温度计边框
  tft.drawRoundRect(x, y, 40, 120, 10, TFT_WHITE);
  tft.fillRoundRect(x+15, y+120, 10, 20, 5, TFT_WHITE);
  
  // 计算温度柱高度 (0-100范围)
  int tempHeight = map(constrain(temp, minTemp, maxTemp), minTemp, maxTemp, 0, 100);
  
  // 使用渐变色 - 低温蓝色到高温红色
  uint16_t color;
  if (temp < 0) {
    color = TFT_BLUE;
  } else if (temp < 15) {
    color = tft.color565(0, 0, 255 - (temp * 17));
  } else if (temp < 30) {
    color = tft.color565((temp-15)*17, 0, 255 - (temp-15)*17);
  } else {
    color = tft.color565(255, (temp-30)*8, 0);
  }
  
  // 绘制温度柱
  tft.fillRoundRect(x+5, y+110-tempHeight, 30, tempHeight, 5, color);
  
  // 显示温度值
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(1);
  tft.drawNumber((int)temp, x+50, y+50);
  tft.drawString("C", x+70, y+50);
}

3.3 数据刷新策略

频繁刷新整个屏幕会导致闪烁,我们需要智能更新策略:

unsigned long lastUpdate = 0;
float lastOutTemp = -100;
float lastInTemp = -100;

void updateDisplay(float outTemp, float inTemp, float humidity, String conditions, bool wifiStatus) {
  unsigned long now = millis();
  
  // 每5秒全量刷新一次
  if (now - lastUpdate > 5000) {
    tft.fillScreen(TFT_BLACK);
    drawHeader("Beijing", conditions);
    drawFooter(wifiStatus);
    lastUpdate = now;
  }
  
  // 温度变化超过0.5度才更新
  if (abs(outTemp - lastOutTemp) > 0.5) {
    drawThermometer(outTemp, -10, 40, 30, 50);
    lastOutTemp = outTemp;
  }
  
  if (abs(inTemp - lastInTemp) > 0.5) {
    drawThermometer(inTemp, 10, 40, 150, 50);
    lastInTemp = inTemp;
  }
  
  // 湿度显示
  static int lastHumidity = -1;
  if ((int)humidity != lastHumidity) {
    tft.fillRect(150, 180, 80, 20, TFT_BLACK);
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
    tft.setTextSize(2);
    tft.drawString("Hum: " + String((int)humidity) + "%", 150, 180);
    lastHumidity = (int)humidity;
  }
}

4. 系统整合与优化

4.1 主程序逻辑

将所有功能模块整合到一个协调的工作流程中:

void setup() {
  Serial.begin(115200);
  initDisplay();
  dht.begin();
  connectWiFi();
  
  // 初始显示
  tft.fillScreen(TFT_BLACK);
  drawHeader("Loading...", "");
  drawFooter(false);
}

void loop() {
  // 读取传感器数据
  float inTemp = readTemperature();
  float inHumidity = readHumidity();
  
  // 获取天气数据
  String weatherJson = getWeatherData();
  float outTemp = 0, outHumidity = 0;
  String conditions = "";
  
  if (weatherJson != "") {
    parseWeatherData(weatherJson, outTemp, outHumidity, conditions);
  }
  
  // 更新显示
  updateDisplay(outTemp, inTemp, inHumidity, conditions, WiFi.status() == WL_CONNECTED);
  
  // 节能延迟
  delay(2000); 
}

4.2 性能优化技巧

经过实际测试,我们发现以下几个优化点能显著提升用户体验:

  1. SPI优化 :将SPI频率提升到最大稳定值(ST7789通常支持40MHz)
  2. 局部刷新 :只更新变化的部分区域,减少闪烁
  3. 内存管理
    • 使用 PROGMEM 存储静态资源
    • 预渲染复杂图形到内存缓冲区
  4. 电源管理
    • 在不活跃时降低屏幕亮度
    • 使用ESP32的深度睡眠模式
// 示例:使用双缓冲减少闪烁
TFT_eSprite spr = TFT_eSprite(&tft);

void setup() {
  // ...其他初始化...
  spr.createSprite(240, 240); // 创建与屏幕同尺寸的缓冲区
}

void drawToBuffer() {
  spr.fillScreen(TFT_BLACK);
  // 所有绘制操作改为对spr进行...
  spr.pushSprite(0, 0); // 一次性输出到屏幕
}

4.3 扩展思路

这个基础项目可以进一步扩展为:

  • 多数据源整合 :加入空气质量指数、天气预报等
  • 交互功能 :通过按钮切换显示模式
  • 云端同步 :将传感器数据上传到物联网平台
  • 低功耗设计 :使用电池供电,优化续航

在实现这些扩展功能时,建议采用模块化设计,每个功能独立成单独的代码文件,通过清晰的接口进行交互。例如,可以创建 WeatherService 类处理所有天气相关逻辑, DisplayManager 类管理屏幕输出等。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐