ESP32-S3 打造桌面天气站(屏幕 UI)
用 ESP32-S3 打造一台会“呼吸”的桌面天气站 🌤️
你有没有过这样的时刻?清晨坐在书桌前,端起咖啡杯,顺手瞥一眼角落里那块小小的屏幕——它正安静地告诉你今天北京多云转晴、气温23°C、湿度58%……不需要打开手机,也不用唤醒电脑,一切信息就在眼前,像一位老朋友轻声提醒。
这,就是我们今天要做的东西: 一台真正属于你的桌面天气站 。不是那种只能显示数字的电子钟,而是一个有设计感、能联网、会动、甚至带点“情绪”的小设备。它不贵,但看起来很值;它小巧,却装得下整个城市的天气。
核心芯片?选的是乐鑫最新的 ESP32-S3 。这块芯片这几年在创客圈火得不行,不只是因为便宜,而是它真的“啥都能干”。Wi-Fi 能连,蓝牙能通,CPU 够快,还能直接驱动彩色屏幕。最关键的是——它能让嵌入式开发变得有点“意思”,比如让你的温度数字慢慢滑动变化,或者让一朵云图标随着天气阴晴缓缓飘过。
为什么是 ESP32-S3?因为它不只是个“单片机”了
说实话,以前做这种项目,我总会纠结:要不要加个外置 Wi-Fi 模块?图形处理会不会卡?内存够不够塞下一个好看的图标?但现在,这些问题在 ESP32-S3 面前,基本都成了“伪问题”。
先看几个硬核参数:
-
双核 Xtensa LX7,主频高达 240MHz
这意味着你可以把网络请求丢给一个核,UI 渲染交给另一个核,互不打扰。想象一下你在刷网页的时候后台还在下载文件——这就是多任务的感觉。 -
支持外部 PSRAM 和 Flash,最高可达 16MB
别小看这个。你要显示一张 JPEG 图标?LVGL 要缓存一整屏内容?没问题,PSRAM 就是干这个的。没有它,你可能连一个平滑动画都跑不起来。 -
原生 LCD 控制器 + JPEG 硬件解码
对,你没看错,它可以直接输出 RGB 信号到屏幕,不用靠 SPI 一点一点“喂”数据。而且如果你用了 JPG 格式的天气图标,S3 可以硬件解码,CPU 几乎不怎么参与。 -
USB OTG 支持
这个很多人忽略,但它意味着你可以用 USB 当调试口、当串口、甚至将来做成 HID 设备(比如模拟键盘上报环境数据)。
换句话说, ESP32-S3 已经不是一个传统意义上的 MCU 了 ,它更像是一个微型 SoC 系统,专为视觉交互类 IoT 设备而生。
实际开发中那些“爽点”
举个例子:我想让屏幕每 10 分钟自动刷新一次天气数据。在过去,如果主控性能弱,你就得牺牲帧率或关闭动画来腾出 CPU 时间。但在 S3 上,我可以这么做:
xTaskCreatePinnedToCore(
fetch_weather_task, // 单独任务获取天气
"FetchWeather",
4096,
NULL,
3,
NULL,
0 // 绑定到 CPU0
);
xTaskCreatePinnedToCore(
lvgl_update_task, // LVGL 主循环
"LVGLUpdate",
8192,
NULL,
2,
NULL,
1 // 绑定到 CPU1
);
两个任务各司其职,互不影响。一个去网上抓 JSON 数据,另一个专心画画。结果就是:界面丝滑如德芙,后台更新悄无声息。
屏幕怎么选?别再只盯着 ILI9341 了!
说到显示,很多教程还停留在“接个 2.4 寸 ILI9341 屏就行”的阶段。没错,它便宜、资料多,但真要用起来体验并不好——SPI 接口速率有限,刷个全屏都要几十毫秒,更别说想搞点渐变、阴影、圆角这些现代 UI 元素了。
我的建议是: 直接上 RGB 接口的 TFT 屏,分辨率至少 480×320 。
比如市面上常见的 RM67162 或 ST7701S 驱动的 3.5” 屏,支持 16-bit RGB 并行输入,配合 ESP32-S3 的 LCD 控制器,可以实现接近 60fps 的刷新率(当然取决于 DMA 效率)。更重要的是,它可以摆脱“SPI 发送命令+写数据”的低效模式,进入真正的“显存映射”时代。
我是怎么配置屏幕驱动的?
这里分享一段我在项目中实际使用的初始化代码(基于 ESP-IDF):
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_rgb.h"
static esp_lcd_panel_handle_t panel_handle;
void init_rgb_display() {
const esp_lcd_rgb_panel_config_t panel_config = {
.data_width = 16,
.pins = {
.clk = 40,
.de = 41,
.vsync = 42,
.hsync = 43,
.data = {10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25},
},
.timings = {
.pclk_hz = 10 * 1000 * 1000,
.h_res = 480,
.v_res = 320,
.hsync_back_porch = 40,
.hsync_front_porch = 40,
.hsync_pulse_width = 10,
.vsync_back_porch = 15,
.vsync_front_porch = 15,
.vsync_pulse_width = 10,
.flags.pclk_active_neg = true,
},
.bounce_buffer_size_px = 480 * 50, // 50 行缓冲
.flags.fb_in_psram = true
};
ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
// 启动流式传输
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
}
关键点有几个:
-
bounce_buffer_size_px设置了一个“跳动缓冲区”(也叫双行缓冲),避免一次性申请整个 framebuffer(那可是 480×320×2 ≈ 300KB!) - 使用 PSRAM 存储帧缓冲,释放宝贵的 IRAM 给实时任务
- PCLK 设置为 10MHz,实测足够稳定且能满足基本刷新需求
这样一来,屏幕就不再是“拖后腿”的部分,反而成了系统的亮点。
LVGL:让嵌入式界面也能“卷”起来
如果说硬件决定了下限,那软件决定了上限。在这个项目里, LVGL 就是我的上限突破器 。
你可能会问:“干嘛不用 Arduino 自带的 TFT_eSPI 直接画图?”
答案很简单: 可维护性太差,扩展性为零 。
试想一下,你现在画了个温度标签,明天想加个湿度进度条,后天还想做个切换页面的按钮……很快你的 loop() 函数就会变成一堆 tft.fillRect() 和坐标计算的地狱。
而 LVGL 不一样。它是组件化的,就像 Web 前端里的 React 或 Vue,每个元素都是对象,有自己的样式、事件和生命周期。
我的设计思路:构建一个“活”的天气面板
我不想做一个静态仪表盘,而是希望它有点生命力。比如:
- 温度变化时,数字从旧值滑动到新值(动画过渡)
- 天气状况改变时,图标淡入淡出
- 背景颜色随时间微调(白天偏蓝,傍晚泛橙)
这一切,LVGL 都能轻松实现。
创建基础 UI 结构
lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, lv_color_hex(0x1E1E1E), 0);
// 温度标签
lv_obj_t *label_temp = lv_label_create(screen);
lv_label_set_text(label_temp, "--°C");
lv_obj_set_style_text_font(label_temp, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(label_temp, lv_color_white(), 0);
lv_obj_align(label_temp, LV_ALIGN_CENTER, 0, -60);
// 天气图标
lv_obj_t *img_weather = lv_img_create(screen);
lv_img_set_src(img_weather, &icon_clear_day); // LVGL image declare
lv_obj_align(img_weather, LV_ALIGN_CENTER, 0, 0);
// 湿度条
lv_obj_t *bar_humidity = lv_bar_create(screen);
lv_bar_set_value(bar_humidity, 60, LV_ANIM_ON);
lv_obj_set_size(bar_humidity, 200, 10);
lv_obj_align(bar_bar, LV_ALIGN_CENTER, 0, 80);
是不是比一堆 drawXxx() 调用清晰多了?
加点动画让它“动”起来
LVGL 内建了强大的动画系统。比如我想让温度数字平滑变化:
static float current_temp = 0;
void animate_temperature_to(float target) {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, label_temp);
lv_anim_set_values(&a, (int)(current_temp * 10), (int)(target * 10));
lv_anim_set_exec_cb(&a, [](void *var, int32_t v) {
char buf[16];
sprintf(buf, "%.1f°C", v / 10.0);
lv_label_set_text((lv_obj_t *)var, buf);
});
lv_anim_set_time(&a, 800);
lv_anim_set_playback_delay(&a, 100);
lv_anim_set_playback_time(&a, 200);
lv_anim_start(&a);
current_temp = target;
}
效果如何?当你从 22°C 更新到 26°C 时,屏幕上会看到数字像老式计数器一样滚动上升,最后稳稳停住。用户感知到的是“正在更新”,而不是“突然变了”。
数据从哪来?OpenWeatherMap + HTTPS 安全请求
天气数据不能凭空生成,得找 API。目前最稳定的免费方案之一是 OpenWeatherMap ,注册后能拿到每天 1000 次免费调用额度,完全够个人使用。
请求地址长这样:
http://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=YOUR_KEY&units=metric
返回 JSON 示例:
{
"main": {
"temp": 23.5,
"humidity": 58
},
"weather": [
{
"main": "Clear",
"description": "clear sky"
}
],
"name": "Beijing"
}
如何安全地发起 HTTPS 请求?
⚠️ 注意:不要用 HTTP!API Key 明文传输非常危险。
正确做法是使用 HTTPClient 配合 MbedTLS,验证服务器证书。ESP-IDF 默认已经集成了这套机制,只需启用相关组件即可。
#include <HTTPClient.h>
String fetch_weather_json(const String& city) {
if (!WiFi.isConnected()) return "";
HTTPClient http;
String url = "https://api.openweathermap.org/data/2.5/weather?q=";
url += city;
url += "&appid=" + SECRET_API_KEY;
url += "&units=metric";
http.begin(url);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
http.end();
return payload;
} else {
Serial.printf("HTTP request failed, code: %d\n", httpCode);
http.end();
return "";
}
}
但等等—— API Key 写死在代码里也很危险!
更好的做法:用 NVS 存储配置项
NVS(Non-Volatile Storage)是 ESP32 提供的一种键值存储系统,类似小型数据库,数据掉电不丢失。
我们可以这样做:
- 第一次运行时提示用户通过串口或 BLE 输入城市名和 API Key
- 存入 NVS
- 后续启动自动读取
nvs_handle_t handle;
nvs_open("config", NVS_READWRITE, &handle);
// 保存
nvs_set_str(handle, "api_key", key.c_str());
nvs_set_str(handle, "city", city.c_str());
nvs_commit(handle);
// 读取
char buf[64];
size_t len = sizeof(buf);
nvs_get_str(handle, "api_key", buf, &len);
String saved_key = String(buf);
这样即使别人拿到固件,也无法轻易提取敏感信息。
性能优化实战:如何让小设备跑出大体验?
资源有限是现实,但我们可以通过技巧弥补。
1. 内存分配策略:哪些放 IRAM,哪些扔 PSRAM?
- IRAM(~192KB) :留给中断服务程序、DMA 描述符、高频调用函数
- PSRAM(8~16MB) :存放 framebuffer、图片资源、JSON 缓冲区
示例:创建 LVGL 的绘图缓冲区时指定位置
static lv_color_t *draw_buf_dbl = (lv_color_t*)ps_malloc(sizeof(lv_color_t) * 480 * 100);
lv_disp_draw_buf_init(&draw_buf, draw_buf_dbl, NULL, 480 * 100);
ps_malloc 是 ESP-IDF 提供的 API,优先在 PSRAM 分配。
2. 图标管理:用 C 数组还是外部文件?
很多人喜欢把 PNG 图标烧进 Flash,然后动态加载。但其实对于固定尺寸的小图标(如 64x64), 预编译成 C 数组更高效 。
工具推荐: LVGL Image Converter
设置输出格式为 “C array”,颜色深度 RGB565,然后复制到代码中:
LV_IMG_DECLARE(icon_rain_64);
优点:
- 零解码开销
- 直接位于 Flash,运行时不占 RAM
- LVGL 可直接引用
3. 减少网络压力:缓存 + 定时轮询
频繁请求不仅耗电,还容易触发限流。我的策略是:
- 首次启动立即获取
- 成功后每隔 15 分钟 自动刷新
- 失败时指数退避重试(1min → 2min → 4min…)
- 添加本地缓存机制,断网时显示上次有效数据
void start_weather_update_loop() {
while (1) {
String json = fetch_weather_json(current_city);
if (json != "") {
parse_and_update_ui(json);
last_success_time = millis();
save_to_cache(json); // 写入 NVS
retry_delay = 15 * 60 * 1000; // 下次 15 分钟后再查
} else {
retry_delay = min(retry_delay * 2, 60 * 60 * 1000); // 最大 1 小时
}
delay(retry_delay);
}
}
中文字体怎么办?别怕,LVGL 支持 UTF-8!
默认情况下,LVGL 字体只包含 ASCII 字符。想显示“北京”、“晴”、“湿度”这些中文?必须自己生成字体。
步骤如下:
- 使用 lvgl-font-addons 工具
- 输入想要的 TTF 字体(推荐思源黑体、站酷酷黑)
- 指定字符范围:
--range=U+4E00-U+9FFF,U+30,U+20(覆盖常用汉字+数字+空格) - 输出为 C 文件并包含进工程
npx lv_font_conv -r latine-3,greek-cyrillic,chinese-1 \
--font "ZCOOLKuaiLe-Regular.ttf" \
-s 24 \
--format c-array \
-o font_kuaile_24.c
然后注册字体:
lv_font_t* font_chinese = &my_font_kuaile_24;
lv_obj_set_style_text_font(label_city, font_chinese, 0);
虽然生成后的字体文件可能有几百 KB,但放在 PSRAM 或压缩后存储都没问题。毕竟,谁不想自己的天气站说“你好,北京”而不是冷冰冰的“Hello Beijing”呢?
功耗与休眠:让它陪你更久一点
虽然是插电设备,但我还是希望它节能。特别是在夜间,没必要全亮度运行。
方案一:PWM 调节背光
几乎所有 TFT 屏都有 BLK(Backlight)引脚,接 PWM 就能调亮度。
ledcSetup(0, 5000, 8); // 通道 0,5kHz,8位精度
ledcAttachPin(TFT_BL, 0); // 绑定引脚
ledcWrite(0, 128); // 50% 亮度
可以结合 RTC 时间判断是否进入“夜间模式”:
if (hour >= 22 || hour <= 6) {
ledcWrite(0, 64); // 降低至 25%
} else {
ledcWrite(0, 255); // 全亮
}
方案二:浅睡眠模式(Light-sleep)
ESP32-S3 支持多种低功耗模式。虽然屏幕开着没法深度睡眠,但可以让 CPU 在空闲时暂停。
esp_sleep_enable_timer_wakeup(15 * 60 * 1000 * 1000); // 15分钟后唤醒
esp_light_sleep_start();
注意:进入 Light-sleep 前需暂停 LVGL 任务,醒来后再恢复。
扩展性:下一步你能加什么?
这个项目最迷人的地方在于它的可塑性。一旦基础框架搭好,后续功能几乎是“即插即用”。
✅ 已实现
- 实时温度/湿度显示
- 天气图标动态切换
- 自动定时刷新
- 支持中文界面
- 安全 HTTPS 请求
🔧 正在规划
- 触摸屏支持 → 实现翻页(当前天气 / 未来预报 / AQI)
- NTP 时间同步 → 显示精准时间 + 日出日落
- SD 卡记录 → 存储历史数据生成趋势图
- BLE 配网 → 手机 App 设置城市和主题
- 动态壁纸 → 根据天气切换背景图(晴天蓝天,雨天窗边雨滴)
🚀 黑科技彩蛋(已验证)
我还试过一个有趣的玩法: 用 ESP32-S3 的 USB OTG 模拟成一个 HID 设备 ,把当前温度作为“键盘输入”发送给电脑。这样只要按一个快捷键,就能把“23°C”打到文档里——纯属好玩,但足够惊艳。
遇到过哪些坑?这是我踩出来的经验清单
❌ 屏幕花屏?检查时序参数!
刚开始调试 RGB 屏时满屏雪花,后来发现是 pclk_active_neg 没设对。有些屏幕需要下降沿采样,就得开启负极性。
💡 建议:先用最低 PCLK(如 5MHz)测试能否点亮,再逐步提高。
❌ LVGL 卡顿?别忘了定期调用 lv_timer_handler()
这个函数是 LVGL 的“心跳”,必须在某个任务中持续运行:
void lvgl_update_task(void *pvParameter) {
while (1) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(5)); // 每 5ms 一次
}
}
否则动画不会动,点击无响应。
❌ HTTPS 报错 -0x7200 ?缺少根证书!
MbedTLS 默认不会信任所有 CA,你需要手动添加 OpenWeatherMap 使用的证书链。
解决方法:导出 api.openweathermap.org 的 PEM 证书,编译进代码:
http.setCACert(weather_ca_pem_start); // 来自 certificates bundle
否则永远连接失败。
❌ 内存溢出崩溃?监控 heap 使用情况!
尤其是在 PSRAM 初始化失败的情况下,很容易误以为分配成功。
建议加入监控:
void print_memory_usage() {
size_t free_iram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
Serial.printf("IRAM: %d KB, PSRAM: %d KB\n", free_iram / 1024, free_psram / 1024);
}
每分钟打印一次,帮助定位泄漏点。
写在最后:技术的意义,是让人生活得更舒服一点
做这个项目的过程中,我常常在想:我们折腾这么多,到底图个啥?
是为了炫技吗?不是。
是为了省几块钱买成品吗?也不是。
真正打动我的,是某天晚上加班回家,看到它静静地亮着,显示“室内 24°C,室外小雨”,然后顺手把窗户关上了。
那一刻我知道,这不是一块会发光的塑料板,而是一个懂得关心你的小东西。
而这一切,始于一块开源芯片、一段自由代码、和一颗愿意动手的心。
所以,如果你也在寻找一个既能练手又有温度的项目,不妨试试这个天气站吧。
不一定完美,但一定值得。
毕竟, 最好的技术,从来都不是冷冰冰的参数表,而是藏在日常里的温柔瞬间 。✨
更多推荐


所有评论(0)