手把手教你用VSCode+PIO给ESP32-C3做双屏仪表盘:基于LVGL和0.96寸ST7735S
2026/6/13 1:27:57 网站建设 项目流程

ESP32-C3双屏仪表盘开发实战:基于LVGL与TFT_eSPI的完整指南

当两块0.96寸ST7735S屏幕在ESP32-C3上完美拼接,动态数据流畅切换的那一刻,每个硬件开发者都能体会到这种"看得见的成就感"。本文将带你从零开始,用VSCode+PlatformIO构建一个专业级双屏仪表盘,避开那些官方文档没写的坑,直接交付可落地的解决方案。

1. 开发环境与硬件准备

工欲善其事,必先利其器。我们选择的工具链组合——VSCode+PlatformIO,已经成为物联网开发的事实标准。这套组合不仅支持代码智能补全和一键烧录,更重要的是能轻松管理各种库依赖。

硬件清单

  • ESP32-C3开发板(推荐型号:Seeed Studio XIAO-ESP32C3)
  • ST7735S驱动的0.96寸IPS屏幕 ×2(分辨率160×80)
  • 杜邦线若干(建议使用彩色线区分功能)
  • 微型面包板(可选,用于临时接线测试)

接线配置是第一个关键点。由于ESP32-C3只有一个硬件SPI接口,我们需要巧妙利用片选信号(CS)实现双屏控制:

引脚功能主屏连接副屏连接备注
VCC3.3V3.3V避免直接并联供电
GNDGNDGND确保共地
SCLKGPIO1GPIO1共享时钟信号
MOSIGPIO0GPIO0共享数据线
CSGPIO9GPIO5关键区分点
DCGPIO19GPIO19可共享
RSTGPIO18GPIO7独立控制硬件复位

提示:实际接线前建议先用万用表检查线路通断,特别是GND连接是否可靠。我曾在一个雨天的调试中,花了三小时才发现是GND接触不良导致屏幕闪烁。

PlatformIO环境配置只需两步:

  1. 在VSCode中安装PlatformIO插件
  2. 创建新项目时选择"Espressif 32"平台和"ESP32-C3"开发板
; platformio.ini关键配置 [env:seeed_xiao_esp32c3] platform = espressif32 board = seeed_xiao_esp32c3 framework = arduino lib_deps = lvgl/lvgl@^8.3.4 bodmer/TFT_eSPI@^2.5.0

2. TFT_eSPI库的双屏魔改实战

官方TFT_eSPI库默认不支持双屏显示,我们需要进行深度定制。这个过程就像给汽车加装涡轮增压——需要精准调整引擎内部结构。

关键修改点

  1. TFT_eSPI/User_Setup.h中添加双屏引脚定义
  2. 修改库核心代码实现屏幕切换逻辑

首先在User_Setup.h末尾添加:

// 双屏专用配置 #define TFT_CS1 9 // 主屏片选 #define TFT_RST1 18 // 主屏复位 #define TFT_CS2 5 // 副屏片选 #define TFT_RST2 7 // 副屏复位 #define TFT_DC 19 // 共享数据/命令选择

接着需要修改TFT_eSPI库的核心文件。找到TFT_eSPI.cpp中的初始化函数,添加屏幕选择逻辑:

void TFT_eSPI::init(uint8_t tc) { if(TFT_choice == 1) { // 主屏初始化 pinMode(TFT_CS1, OUTPUT); pinMode(TFT_RST1, OUTPUT); digitalWrite(TFT_CS1, HIGH); digitalWrite(TFT_RST1, HIGH); // ...其余初始化代码 } else { // 副屏初始化 pinMode(TFT_CS2, OUTPUT); pinMode(TFT_RST2, OUTPUT); digitalWrite(TFT_CS2, HIGH); digitalWrite(TFT_RST2, HIGH); // ...其余初始化代码 } }

测试双屏基础功能时,可以使用这个简单示例:

#include <TFT_eSPI.h> TFT_eSPI tft; void setup() { // 初始化主屏 TFT_choice = 1; tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLUE); tft.setTextColor(TFT_WHITE); tft.drawString("MAIN SCREEN", 10, 30); // 初始化副屏 TFT_choice = 2; tft.init(); tft.setRotation(3); tft.fillScreen(TFT_RED); tft.setTextColor(TFT_BLACK); tft.drawString("SUB SCREEN", 10, 30); } void loop() {}

3. LVGL引擎的双屏适配技巧

LVGL作为轻量级GUI库,其默认配置不直接支持拼接屏幕。我们需要定制显示驱动,让LVGL认为两块屏幕是一个逻辑显示器。

显示缓冲区配置

#define SCREEN_WIDTH 320 // 双屏总宽度 #define SCREEN_HEIGHT 80 // 单屏高度 static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[SCREEN_WIDTH * 10]; // 行缓冲模式

关键是要实现自定义的flush_cb函数,将LVGL的绘图指令分发到两个物理屏幕:

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = (area->x2 - area->x1 + 1); uint32_t h = (area->y2 - area->y1 + 1); // 左屏区域 (0-159) if(area->x1 < 160) { uint16_t left_width = min(160 - area->x1, w); TFT_choice = 1; tft.startWrite(); tft.setAddrWindow(area->x1, area->y1, left_width, h); tft.pushColors((uint16_t*)color_p, left_width * h, true); tft.endWrite(); } // 右屏区域 (160-319) if(area->x2 >= 160) { uint16_t right_width = min(area->x2 - 159, w); uint16_t x_start = max(area->x1, 160) - 160; TFT_choice = 2; tft.startWrite(); tft.setAddrWindow(x_start, area->y1, right_width, h); tft.pushColors((uint16_t*)color_p + (160-area->x1), right_width * h, true); tft.endWrite(); } lv_disp_flush_ready(disp); }

setup()中初始化LVGL时,需要特别注意设置正确的显示尺寸:

lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = SCREEN_WIDTH; disp_drv.ver_res = SCREEN_HEIGHT; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv);

4. 仪表盘UI设计与性能优化

有了基础显示框架后,我们来构建一个实用的双屏仪表盘。典型布局可以是:左屏显示实时数据图表,右屏展示关键参数数值。

创建复合UI组件

// 创建带背景框的数值显示组件 lv_obj_t* create_value_panel(lv_obj_t* parent, const char* title, int x, int y) { lv_obj_t* panel = lv_obj_create(parent); lv_obj_set_size(panel, 150, 60); lv_obj_set_pos(panel, x, y); lv_obj_set_style_bg_color(panel, lv_color_hex(0x333333), 0); lv_obj_t* label = lv_label_create(panel); lv_label_set_text(label, title); lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 5); lv_obj_t* value = lv_label_create(panel); lv_label_set_text(value, "0"); lv_obj_align(value, LV_ALIGN_BOTTOM_MID, 0, -5); lv_obj_set_style_text_font(value, &lv_font_montserrat_24, 0); return value; // 返回数值标签用于后续更新 }

性能优化技巧

  1. 使用局部刷新而非全屏刷新
  2. 合理设置LVGL的刷新周期(建议20-30ms)
  3. 启用LVGL的异步渲染特性
// 在setup()中添加 lv_tick_set_cb([](){ static uint32_t last_tick = 0; uint32_t curr_tick = millis(); return curr_tick - last_tick; }); // 主循环优化 void loop() { static uint32_t last_update = 0; if(millis() - last_update > 30) { lv_timer_handler(); last_update = millis(); } // 其他任务... }

高级技巧- 实现跨屏动画:

// 创建从右屏向左屏滑动的动画 void create_cross_screen_animation(lv_obj_t* obj) { lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, obj); lv_anim_set_values(&a, 320, -100); // 从右屏外到左屏外 lv_anim_set_time(&a, 2000); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x); lv_anim_start(&a); }

5. 项目实战:物联网数据仪表盘

现在我们将所有知识整合,构建一个显示真实物联网数据的仪表盘。假设我们从MQTT服务器获取环境传感器数据。

数据模型设计

struct SensorData { float temperature; float humidity; uint16_t pm25; uint16_t co2; time_t timestamp; }; SensorData current_data; lv_obj_t* temp_label; lv_obj_t* humi_label; lv_obj_t* chart; void setup() { // ...之前的初始化代码 // 创建UI temp_label = create_value_panel(lv_scr_act(), "Temperature", 10, 10); humi_label = create_value_panel(lv_scr_act(), "Humidity", 180, 10); // 创建图表 chart = lv_chart_create(lv_scr_act()); lv_obj_set_size(chart, 300, 150); lv_obj_align(chart, LV_ALIGN_BOTTOM_MID, 0, -10); lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 50); lv_chart_set_point_count(chart, 24); } void update_display() { // 更新数值显示 lv_label_set_text_fmt(temp_label, "%.1f°C", current_data.temperature); lv_label_set_text_fmt(humi_label, "%.1f%%", current_data.humidity); // 更新图表 static uint8_t point_cnt = 0; lv_chart_set_next_value(chart, NULL, current_data.temperature); if(++point_cnt >= 24) { lv_chart_refresh(chart); point_cnt = 0; } }

MQTT数据接收处理

#include <WiFi.h> #include <PubSubClient.h> WiFiClient espClient; PubSubClient client(espClient); void callback(char* topic, byte* payload, unsigned int length) { if(strcmp(topic, "sensor/data") == 0) { // 解析JSON数据 DynamicJsonDocument doc(256); deserializeJson(doc, payload, length); current_data.temperature = doc["temp"]; current_data.humidity = doc["humi"]; current_data.timestamp = time(nullptr); update_display(); } } void reconnect() { while (!client.connected()) { if (client.connect("esp32c3-dashboard")) { client.subscribe("sensor/data"); } else { delay(5000); } } } void loop() { if (!client.connected()) { reconnect(); } client.loop(); lv_timer_handler(); }

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

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

立即咨询