引言
在嵌入式系统领域,ESP32 凭借其强大的性能、丰富的外设以及出色的无线通信能力,成为众多开发者的首选。而 LVGL(Light and Versatile Graphics Library)作为一款开源的嵌入式图形库,以其轻量级、高性能和丰富的 UI 组件,为嵌入式设备的界面开发提供了极大的便利。当 ESP32 与 LVGL 相结合,便为嵌入式设备的图形界面开发开辟了广阔的天地,尤其在无人机、无人船、无人车等领域的遥控终端界面开发中发挥着重要作用。
本文将以通俗易懂的方式,详细介绍 ESP32 下 LVGL 的相关知识,涵盖发展历史、程序重大迭代纪要、主流开发工具、适用场景等内容,并提供详细的代码示例,助力读者从零基础小白成长为 LVGL 开发专家。
一、ESP32 与 LVGL 基础概述
(一)ESP32 简介
ESP32 是 Espressif(乐鑫信息科技)推出的一款高性能、低功耗的 Wi-Fi 和蓝牙双模嵌入式处理器。它采用 Tensilica Xtensa LX6 双核处理器,主频最高可达 240MHz,拥有丰富的外设资源,如 GPIO、SPI、I2C、UART、ADC、DAC 等,同时集成了 Wi-Fi 802.11 b/g/n 和蓝牙 4.2/5.0 功能,能够满足各种物联网和嵌入式应用的需求。
ESP32 具有以下特点:
- 强大的处理能力:双核处理器,可实现复杂的计算和控制任务。
- 丰富的外设接口:支持多种通信协议和传感器连接,方便扩展。
- 出色的无线通信:支持 Wi-Fi 和蓝牙,便于设备之间的数据传输和远程控制。
- 低功耗设计:适合电池供电的便携式设备,延长设备的续航时间。
- 开源生态丰富:拥有大量的开发资料、库文件和社区支持,降低开发难度。
(二)LVGL 简介
LVGL 是一款开源的嵌入式图形库,旨在为嵌入式系统提供高效、美观的图形用户界面(GUI)解决方案。它采用模块化设计,具有良好的可移植性和可扩展性,支持多种显示设备和输入设备,如 LCD、OLED、触摸屏、按键等。
LVGL 的主要特点包括:
- 轻量级:占用资源少,适合内存和 Flash 有限的嵌入式设备。
- 高性能:采用先进的渲染算法,保证界面的流畅显示。
- 丰富的 UI 组件:提供按钮、滑块、图表、列表等多种常用 UI 组件,满足不同界面设计需求。
- 支持主题和样式定制:可以根据需求自定义界面的颜色、字体、布局等,打造个性化的界面。
- 跨平台:可在多种嵌入式处理器和操作系统上运行,如 ESP32、STM32、Linux 等。
(三)ESP32 与 LVGL 的结合优势
将 ESP32 与 LVGL 结合使用,具有以下优势:
- 强大的硬件支持:ESP32 的高性能处理器和丰富外设为 LVGL 的运行提供了坚实的硬件基础,能够流畅地渲染复杂的界面。
- 便捷的开发体验:ESP32 拥有完善的开发工具和生态系统,结合 LVGL 的开源特性,开发者可以快速搭建开发环境,进行界面开发和调试。
- 丰富的应用场景:适用于各种需要图形界面的嵌入式设备,如智能家居控制面板、工业监控终端、消费电子设备等,尤其在无人机、无人船、无人车等领域的遥控终端界面开发中表现出色。
二、ESP32 与 LVGL 的发展历史
(一)ESP32 的发展历程
时间节点 | 重要事件 |
2016 年 | Espressif 发布 ESP32 芯片,作为 ESP8266 的升级版,带来了更强大的性能和更多的功能。 |
2017 年 | ESP32 正式投入市场,凭借其优异的性能和性价比,迅速获得了开发者的青睐。 |
2018 年 | Espressif 推出了 ESP32 的多个衍生型号,如 ESP32 - S2、ESP32 - C3 等,以满足不同应用场景的需求。 |
2019 年及以后 | ESP32 系列芯片不断更新迭代,在性能、功耗、安全性等方面持续优化,同时其开源生态也不断完善,吸引了越来越多的开发者加入。 |
(二)LVGL 的发展历程
时间节点 | 重要事件 |
2016 年 | LVGL 的前身 LittlevGL 首次发布,最初只是一个简单的图形库,功能相对有限。 |
2017 - 2018 年 | LittlevGL 不断更新,增加了更多的 UI 组件和功能,如支持触摸输入、动画效果等,逐渐受到开发者的关注。 |
2019 年 | LittlevGL 更名为 LVGL,版本更新至 v7,在性能和易用性上有了较大提升,支持更多的平台和显示设备。 |
2020 年 | LVGL 发布 v8 版本,引入了新的渲染引擎和布局系统,进一步提高了界面的渲染效率和灵活性。 |
2021 年及以后 | LVGL 持续更新,不断优化功能和性能,扩大支持的硬件平台范围,成为嵌入式图形库领域的佼佼者。 |
(三)ESP32 与 LVGL 结合的发展
随着 ESP32 和 LVGL 各自的发展,两者的结合也逐渐成为趋势。早期,开发者需要自行移植 LVGL 到 ESP32 平台,过程较为繁琐。随着开源社区的努力,逐渐出现了专门针对 ESP32 的 LVGL 移植版本和开发库,简化了开发流程。如今,ESP32 与 LVGL 的结合已经非常成熟,开发者可以轻松地在 ESP32 上使用 LVGL 进行图形界面开发。
三、LVGL 程序重大迭代纪要
LVGL 自发布以来,经历了多次重大版本迭代,每一次迭代都带来了新的功能和性能提升。以下是 LVGL 主要版本的迭代纪要:
版本号 | 发布时间 | 重大更新内容 |
v1.0 | 2016 年 | 首次发布,提供基本的图形绘制功能和简单的 UI 组件,如按钮、标签等。 |
v2.0 | 2017 年 | 增加了更多的 UI 组件,如滑块、进度条、列表等,支持基本的触摸输入。 |
v3.0 | 2017 年底 | 引入了样式系统,允许开发者自定义 UI 组件的外观,优化了渲染性能。 |
v4.0 | 2018 年 | 支持动画效果,增强了界面的交互性和视觉效果,改进了内存管理。 |
v5.0 | 2018 年底 | 增加了更多高级 UI 组件,如图表、仪表盘等,支持多语言显示。 |
v6.0 | 2019 年中 | 重构了内部架构,提高了代码的可维护性和可扩展性,优化了触摸响应速度。 |
v7.0 | 2019 年底 | 更名为 LVGL,引入了新的布局系统,支持响应式设计,增强了对不同分辨率显示设备的适配性。 |
v8.0 | 2020 年 | 采用新的渲染引擎,提高了界面渲染效率,支持硬件加速,增加了更多的动画效果和过渡效果。 |
v9.0 | 2022 年 | 进一步优化了性能和内存占用,增强了对嵌入式系统的适应性,提供了更丰富的开发工具和文档。 |
四、ESP32 下 LVGL 开发的主流工具
(一)开发环境
- Arduino IDE
Arduino IDE 是一款简单易用的开源集成开发环境,支持多种嵌入式平台,包括 ESP32。它具有简洁的界面和丰富的库文件,适合初学者快速上手。
安装 ESP32 支持:
- 打开 Arduino IDE,点击 “文件”->“首选项”。
- 在 “附加开发板管理器网址” 中输入 ESP32 的开发板管理器网址:https://dl.espressif.com/dl/package_esp32_index.json。
- 点击 “工具”->“开发板”->“开发板管理器”,搜索 “esp32”,安装相应的开发板支持包。
安装 LVGL 库:
- 在 Arduino IDE 中,点击 “项目”->“加载库”->“管理库”,搜索 “lvgl”,安装最新版本的 LVGL 库。
- ESP - IDF
ESP - IDF 是 Espressif 官方推出的 ESP32 开发框架,提供了更底层、更全面的开发支持,适合有一定嵌入式开发经验的开发者。
安装 ESP - IDF:
- 按照 Espressif 官方文档的指导,下载并安装 ESP - IDF。
- 配置开发环境,包括工具链、路径等。
集成 LVGL:
- 可以通过 GitHub 等渠道获取 LVGL 的源码,并将其集成到 ESP - IDF 项目中。
- 根据 LVGL 的文档,配置相关的编译选项和宏定义。
(二)调试工具
- Serial Monitor
Arduino IDE 和 ESP - IDF 都自带 Serial Monitor 工具,可以通过串口与 ESP32 进行通信,查看程序运行过程中的调试信息。
使用方法:
- 在程序中使用 Serial.print () 等函数输出调试信息。
- 打开 Serial Monitor,设置正确的波特率,即可查看输出的信息。
- J - Link / ST - Link
对于需要进行硬件调试的场景,可以使用 J - Link 或 ST - Link 等调试器,通过 SWD 或 JTAG 接口与 ESP32 连接,实现断点调试、变量查看等功能。
使用方法:
- 将调试器与 ESP32 的相应接口连接。
- 在开发环境中配置调试器参数,启动调试模式。
(三)UI 设计工具
- LVGL Online Theme Editor
LVGL 官方提供了一款在线主题编辑器,开发者可以通过该工具自定义 LVGL 的主题,包括颜色、字体、边框等,生成相应的代码后导入到项目中。
使用地址:https://lvgl.io/tools/theme-editor
- SquareLine Studio
SquareLine Studio 是一款专门为 LVGL 设计的 UI 设计工具,支持可视化界面设计,能够快速生成 LVGL 代码,大大提高界面开发效率。
功能特点:
- 拖拽式界面设计,直观易用。
- 支持多种 UI 组件和布局方式。
- 可以直接生成适用于 ESP32 等平台的代码。
五、ESP32 与 LVGL 的适用场景
(一)无人机遥控终端界面开发
在无人机领域,遥控终端需要实时显示无人机的飞行状态、姿态、位置、电池电量等信息,同时提供控制指令的输入界面。使用 ESP32 与 LVGL 可以开发出功能丰富、界面友好的遥控终端界面。
例如,可以设计以下界面元素:
- 实时显示无人机的摄像头画面。
- 显示飞行参数,如高度、速度、航向等,可用仪表盘、图表等组件展示。
- 提供控制按钮,如起飞、降落、悬停、方向控制等。
- 显示电池电量、信号强度等状态信息。
(二)无人船遥控终端界面开发
无人船的遥控终端需要显示船体的位置、速度、航行轨迹等信息,以及各种传感器的数据,如水温、水质等。ESP32 与 LVGL 可以满足这些需求,开发出稳定可靠的界面。
界面设计要点:
- 采用地图组件显示无人船的位置和航行轨迹。
- 用列表或图表展示传感器数据。
- 设计控制按钮,实现对无人船的前进、后退、转向等操作。
(三)无人车遥控终端界面开发
无人车的遥控终端需要显示车辆的速度、里程、周围环境等信息,同时提供驾驶控制界面。ESP32 与 LVGL 结合可以开发出符合需求的界面。
主要界面元素:
- 显示车辆的实时视频画面。
- 用仪表盘显示速度、转速等参数。
- 设计虚拟方向盘、油门和刹车控件。
- 显示导航信息和障碍物提醒。
(四)其他适用场景
- 智能家居控制面板:可以通过界面控制灯光、窗帘、空调等家电设备,显示设备的运行状态。
- 工业监控终端:实时显示生产设备的运行参数、报警信息等,方便工作人员监控和管理。
- 消费电子设备:如智能手表、便携式播放器等,提供简洁美观的操作界面。
六、ESP32 下 LVGL 开发从入门到精通的代码示例
(一)入门篇:Hello World 程序
本示例将展示如何在 ESP32 上使用 LVGL 显示 “Hello World” 文本。
- 硬件准备
- ESP32 开发板
- LCD 显示屏(如 ILI9341)
- 相应的连接线
- 接线说明
将 LCD 显示屏与 ESP32 按照以下方式连接:
|LCD 引脚 | ESP32 引脚 |
| ---- | ---- |
|CS|GPIO5|
|DC|GPIO16|
|SCK|GPIO18|
|MOSI|GPIO23|
|RESET|GPIO17|
|BL|GPIO4|
- 代码实现
#include <lvgl.h>
#include <TFT_eSPI.h>
// 初始化TFT显示屏
TFT_eSPI tft = TFT_eSPI();
// 定义显示屏的宽度和高度
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
// LVGL显示缓冲区
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
// 显示刷新回调函数
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);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
void setup() {
Serial.begin(115200);
// 初始化TFT显示屏
tft.init();
tft.setRotation(0);
// 初始化LVGL
lv_init();
// 初始化显示缓冲区
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
// 注册显示设备
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
// 创建一个标签,显示Hello World
lv_obj_t *label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(label, "Hello World!");
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);
}
void loop() {
// 处理LVGL的任务
lv_task_handler();
delay(5);
}
- 代码说明
- 首先包含 LVGL 库和 TFT_eSPI 库,TFT_eSPI 库用于驱动 LCD 显示屏。
- 初始化 TFT 显示屏,设置显示屏的旋转方向。
- 初始化 LVGL,创建显示缓冲区,并注册显示设备,设置显示刷新回调函数。
- 在 setup () 函数中,创建一个标签对象,设置显示文本为 “Hello World!”,并将其居中显示。
- 在 loop () 函数中,调用 lv_task_handler () 处理 LVGL 的任务,确保界面能够正常刷新。
(二)进阶篇:按钮与事件处理
本示例将创建一个按钮,当按钮被点击时,改变标签的文本内容。
- 代码实现
#include <lvgl.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
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);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
// 按钮点击事件回调函数
static void btn_event_handler(lv_obj_t *btn, lv_event_t event) {
if (event == LV_EVENT_CLICKED) {
static int count = 0;
count++;
// 获取标签对象
lv_obj_t *label = lv_obj_get_child(btn, NULL);
// 改变标签文本
lv_label_set_text_fmt(label, "Clicked: %d times", count);
}
}
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(0);
lv_init();
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
// 创建一个按钮
lv_obj_t *btn = lv_btn_create(lv_scr_act(), NULL);
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_event_cb(btn, btn_event_handler);
// 在按钮上创建一个标签
lv_obj_t *label = lv_label_create(btn, NULL);
lv_label_set_text(label, "Click me!");
}
void loop() {
lv_task_handler();
delay(5);
}
- 代码说明
- 除了基本的初始化操作外,定义了一个按钮点击事件回调函数 btn_event_handler ()。
- 在回调函数中,当按钮被点击时(LV_EVENT_CLICKED 事件),通过 lv_obj_get_child () 函数获取按钮上的标签对象,然后使用 lv_label_set_text_fmt () 函数改变标签的文本内容,显示点击次数。
- 在 setup () 函数中,创建一个按钮对象,设置其大小和位置,并为其注册事件回调函数。然后在按钮上创建一个标签,显示初始文本 “Click me!”。
(三)高级篇:无人机遥控终端界面设计
本示例将模拟一个无人机遥控终端界面,包含飞行参数显示、控制按钮等元素。
- 代码实现
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <Wire.h>
TFT_eSPI tft = TFT_eSPI();
#define TFT_WIDTH 320
#define TFT_HEIGHT 240
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
// 模拟无人机飞行数据
float altitude = 10.5; // 高度(米)
float speed = 3.2; // 速度(米/秒)
int battery = 85; // 电池电量(%)
float heading = 90.0; // 航向(度)
// 显示刷新回调函数
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);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
// 输入设备读取回调函数(模拟触摸)
bool my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
// 这里只是模拟触摸,实际应用中需要根据触摸芯片的驱动进行修改
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
// 模拟触摸按下
if (digitalRead(0) == LOW) {
data->state = LV_INDEV_STATE_PR;
last_x = random(TFT_WIDTH);
last_y = random(TFT_HEIGHT);
data->point.x = last_x;
data->point.y = last_y;
} else {
data->state = LV_INDEV_STATE_REL;
data->point.x = last_x;
data->point.y = last_y;
}
return false;
}
// 更新飞行数据显示
void update_flight_data() {
// 模拟数据变化
altitude += 0.1;
if (altitude > 50) altitude = 10;
speed += 0.2;
if (speed > 10) speed = 3;
battery--;
if (battery < 0) battery = 100;
heading += 1;
if (heading >= 360) heading = 0;
// 更新高度显示
lv_obj_t *alt_label = lv_obj_get_child(lv_scr_act(), NULL);
lv_label_set_text_fmt(alt_label, "Altitude: %.1f m", altitude);
// 更新速度显示
lv_obj_t *speed_label = lv_obj_get_child(lv_scr_act(), alt_label);
lv_label_set_text_fmt(speed_label, "Speed: %.1f m/s", speed);
// 更新电池电量显示
lv_obj_t *batt_label = lv_obj_get_child(lv_scr_act(), speed_label);
lv_label_set_text_fmt(batt_label, "Battery: %d%%", battery);
// 更新航向显示
lv_obj_t *heading_label = lv_obj_get_child(lv_scr_act(), batt_label);
lv_label_set_text_fmt(heading_label, "Heading: %.1f°", heading);
}
// 控制按钮事件处理
static void control_btn_event_handler(lv_obj_t *btn, lv_event_t event) {
if (event == LV_EVENT_CLICKED) {
const char *text = lv_label_get_text(lv_obj_get_child(btn, NULL));
Serial.printf("Control button clicked: %s\n", text);
// 在这里添加相应的控制逻辑
}
}
void setup() {
Serial.begin(115200);
pinMode(0, INPUT_PULLUP);
tft.init();
tft.setRotation(1);
lv_init();
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
// 注册输入设备(触摸)
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
// 创建标题
lv_obj_t *title = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(title, "Drone Controller");
lv_obj_align(title, NULL, LV_ALIGN_IN_TOP_MID, 0, 10);
// 创建飞行数据显示区域
lv_obj_t *alt_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text_fmt(alt_label, "Altitude: %.1f m", altitude);
lv_obj_align(alt_label, NULL, LV_ALIGN_IN_TOP_LEFT, 10, 40);
lv_obj_t *speed_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text_fmt(speed_label, "Speed: %.1f m/s", speed);
lv_obj_align(speed_label, alt_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10);
lv_obj_t *batt_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text_fmt(batt_label, "Battery: %d%%", battery);
lv_obj_align(batt_label, speed_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10);
lv_obj_t *heading_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text_fmt(heading_label, "Heading: %.1f°", heading);
lv_obj_align(heading_label, batt_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10);
// 创建控制按钮区域
lv_obj_t *takeoff_btn = lv_btn_create(lv_scr_act(), NULL);
lv_obj_set_size(takeoff_btn, 80, 40);
lv_obj_align(takeoff_btn, NULL, LV_ALIGN_IN_BOTTOM_LEFT, 20, -20);
lv_obj_set_event_cb(takeoff_btn, control_btn_event_handler);
lv_obj_t *takeoff_label = lv_label_create(takeoff_btn, NULL);
lv_label_set_text(takeoff_label, "Takeoff");
lv_obj_t *land_btn = lv_btn_create(lv_scr_act(), NULL);
lv_obj_set_size(land_btn, 80, 40);
lv_obj_align(land_btn, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, -20);
lv_obj_set_event_cb(land_btn, control_btn_event_handler);
lv_obj_t *land_label = lv_label_create(land_btn, NULL);
lv_label_set_text(land_label, "Land");
lv_obj_t *hover_btn = lv_btn_create(lv_scr_act(), NULL);
lv_obj_set_size(hover_btn, 80, 40);
lv_obj_align(hover_btn, NULL, LV_ALIGN_IN_BOTTOM_RIGHT, -20, -20);
lv_obj_set_event_cb(hover_btn, control_btn_event_handler);
lv_obj_t *hover_label = lv_label_create(hover_btn, NULL);
lv_label_set_text(hover_label, "Hover");
// 创建一个定时器,定期更新飞行数据
lv_timer_create(update_flight_data, 1000, NULL);
}
void loop() {
lv_task_handler();
delay(5);
}
- 代码说明
- 本示例模拟了无人机遥控终端的界面,包含标题、飞行数据显示(高度、速度、电池电量、航向)和控制按钮(起飞、降落、悬停)。
- 定义了一个 update_flight_data () 函数,用于模拟飞行数据的变化,并更新界面上的显示。
- 使用 lv_timer_create () 创建一个定时器,每隔 1 秒调用一次 update_flight_data () 函数,实现数据的实时更新。
- 注册了触摸输入设备的回调函数 my_touchpad_read (),用于模拟触摸操作(实际应用中需要根据触摸芯片的驱动进行修改)。
- 为控制按钮注册了事件回调函数 control_btn_event_handler (),当按钮被点击时,在串口输出相应的信息,实际应用中可以在这里添加控制无人机的逻辑。
(四)专家篇:自定义组件与动画效果
本示例将创建一个自定义的仪表盘组件,并添加动画效果,用于显示无人机的速度。
- 代码实现
#include <lvgl.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
#define TFT_WIDTH 320
#define TFT_HEIGHT 240
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
// 自定义仪表盘组件
typedef struct {
lv_obj_t obj;
lv_style_t *style;
int min_val;
int max_val;
int current_val;
lv_color_t needle_color;
lv_color_t arc_color;
} custom_gauge_t;
// 自定义仪表盘绘制函数
static void custom_gauge_draw(lv_obj_t *obj, const lv_area_t *area, lv_style_t *style, lv_draw_mask_t *mask) {
custom_gauge_t *gauge = (custom_gauge_t *)obj;
// 计算中心坐标和半径
lv_coord_t center_x = area->x1 + (area->x2 - area->x1) / 2;
lv_coord_t center_y = area->y1 + (area->y2 - area->y1) / 2;
lv_coord_t radius = LV_MIN(area->x2 - area->x1, area->y2 - area->y1) / 2 - 5;
// 绘制圆弧
lv_draw_arc_dsc_t arc_dsc;
lv_draw_arc_dsc_init(&arc_dsc);
arc_dsc.color = gauge->arc_color;
arc_dsc.width = 5;
arc_dsc.opa = LV_OPA_COVER;
lv_draw_arc(lv_disp_get_draw_buf(obj->disp), &arc_dsc, center_x, center_y, radius, 135, 45, mask);
// 计算指针角度(范围:135°到45°,对应值:min_val到max_val)
float angle = 135.0f - (gauge->current_val - gauge->min_val) * (135.0f - 45.0f) / (gauge->max_val - gauge->min_val);
angle = LV_CLAMP(45.0f, angle, 135.0f);
// 绘制指针
lv_draw_line_dsc_t line_dsc;
lv_draw_line_dsc_init(&line_dsc);
line_dsc.color = gauge->needle_color;
line_dsc.width = 3;
line_dsc.opa = LV_OPA_COVER;
lv_point_t p1, p2;
p1.x = center_x;
p1.y = center_y;
p2.x = center_x + radius * cos(angle * LV_MATH_PI / 180.0f);
p2.y = center_y - radius * sin(angle * LV_MATH_PI / 180.0f); // Y轴向下为正,所以用减号
lv_draw_line(lv_disp_get_draw_buf(obj->disp), &line_dsc, &p1, &p2, mask);
// 绘制中心圆点
lv_draw_rect_dsc_t rect_dsc;
lv_draw_rect_dsc_init(&rect_dsc);
rect_dsc.radius = 5;
rect_dsc.bg_color = LV_COLOR_BLACK;
rect_dsc.opa = LV_OPA_COVER;
lv_area_t dot_area;
dot_area.x1 = center_x - 5;
dot_area.y1 = center_y - 5;
dot_area.x2 = center_x + 5;
dot_area.y2 = center_y + 5;
lv_draw_rect(lv_disp_get_draw_buf(obj->disp), &rect_dsc, &dot_area, mask);
// 绘制刻度
for (int i = 0; i <= 10; i++) {
float tick_angle = 135.0f - i * (135.0f - 45.0f) / 10;
lv_point_t tick_p1, tick_p2;
tick_p1.x = center_x + radius * cos(tick_angle * LV_MATH_PI / 180.0f);
tick_p1.y = center_y - radius * sin(tick_angle * LV_MATH_PI / 180.0f);
tick_p2.x = center_x + (radius - 10) * cos(tick_angle * LV_MATH_PI / 180.0f);
tick_p2.y = center_y - (radius - 10) * sin(tick_angle * LV_MATH_PI / 180.0f);
lv_draw_line_dsc_t tick_dsc;
lv_draw_line_dsc_init(&tick_dsc);
tick_dsc.color = LV_COLOR_GRAY;
tick_dsc.width = 2;
tick_dsc.opa = LV_OPA_COVER;
lv_draw_line(lv_disp_get_draw_buf(obj->disp), &tick_dsc, &tick_p1, &tick_p2, mask);
// 绘制刻度值
if (i % 2 == 0) {
int val = gauge->min_val + i * (gauge->max_val - gauge->min_val) / 10;
char val_str[10];
sprintf(val_str, "%d", val);
lv_obj_t *label = lv_label_create(obj, NULL);
lv_label_set_text(label, val_str);
lv_coord_t label_x = center_x + (radius - 20) * cos(tick_angle * LV_MATH_PI / 180.0f) - lv_obj_get_width(label) / 2;
lv_coord_t label_y = center_y - (radius - 20) * sin(tick_angle * LV_MATH_PI / 180.0f) - lv_obj_get_height(label) / 2;
lv_obj_set_pos(label, label_x, label_y);
}
}
}
// 自定义仪表盘事件处理
static void custom_gauge_event(lv_obj_t *obj, lv_event_t event) {
// 可以添加自定义事件处理逻辑
}
// 创建自定义仪表盘
static custom_gauge_t *custom_gauge_create(lv_obj_t *parent, lv_coord_t w, lv_coord_t h, int min_val, int max_val) {
custom_gauge_t *gauge = (custom_gauge_t *)lv_obj_create(parent, NULL);
lv_obj_set_size(gauge, w, h);
gauge->min_val = min_val;
gauge->max_val = max_val;
gauge->current_val = min_val;
gauge->needle_color = LV_COLOR_RED;
gauge->arc_color = LV_COLOR_GREEN;
// 设置绘制和事件回调函数
gauge->obj.draw_cb = custom_gauge_draw;
gauge->obj.event_cb = custom_gauge_event;
return gauge;
}
// 设置自定义仪表盘的值
static void custom_gauge_set_value(custom_gauge_t *gauge, int value) {
gauge->current_val = LV_CLAMP(gauge->min_val, value, gauge->max_val);
lv_obj_invalidate(&gauge->obj);
}
// 动画更新函数
static void anim_speed_update(void *gauge, int32_t value) {
custom_gauge_set_value((custom_gauge_t *)gauge, value);
}
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);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(1);
lv_init();
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
// 创建自定义仪表盘,范围0 - 20 m/s
custom_gauge_t *speed_gauge = custom_gauge_create(lv_scr_act(), 200, 200);
lv_obj_align(speed_gauge, NULL, LV_ALIGN_CENTER, 0, 0);
// 创建标题
lv_obj_t *title = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(title, "Drone Speed");
lv_obj_align(title, speed_gauge, LV_ALIGN_OUT_TOP_MID, 0, -10);
// 创建动画,让速度从0逐渐增加到15,再减少到5,循环进行
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, speed_gauge);
lv_anim_set_exec_cb(&anim, anim_speed_update);
lv_anim_set_values(&anim, 0, 15);
lv_anim_set_time(&anim, 2000);
lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE);
lv_anim_set_repeat_delay(&anim, 1000);
lv_anim_set_path_cb(&anim, lv_anim_path_ease_in_out);
lv_anim_start(&anim);
// 创建t);
lv_anim_start(&anim);
// 创建反向动画
lv_anim_t anim2;
lv_anim_init(&anim2);
lv_anim_set_var(&anim2, speed_gauge);
lv_anim_set_exec_cb(&anim2, anim_speed_update);
lv_anim_set_values(&anim2, 15, 5);
lv_anim_set_time(&anim2, 2000);
lv_anim_set_repeat_count(&anim2, LV_ANIM_REPEAT_INFINITE);
lv_anim_set_repeat_delay(&anim2, 1000);
lv_anim_set_path_cb(&anim2, lv_anim_path_ease_in_out);
lv_anim_start(&anim2);
}
void loop() {
lv_task_handler();
delay(5);
}
- 代码说明
- 本示例创建了一个自定义的仪表盘组件,用于显示无人机的速度。自定义组件通过结构体 custom_gauge_t 定义,包含了仪表盘的各种属性和回调函数。
- 实现了自定义的绘制函数 custom_gauge_draw (),用于绘制仪表盘的圆弧、指针、刻度等元素。
- 提供了创建自定义仪表盘的函数 custom_gauge_create () 和设置仪表盘值的函数 custom_gauge_set_value ()。
- 使用 LVGL 的动画功能,创建了两个动画,让仪表盘的指针在 0 - 15 m/s 和 15 - 5 m/s 之间来回移动,实现平滑的动画效果。动画通过 lv_anim_t 结构体配置,设置了动画的变量、执行函数、值范围、时间、重复次数等参数。
七、总结与展望
(一)总结
本文详细介绍了 ESP32 下 LVGL 的相关知识,包括 ESP32 与 LVGL 的基础概述、发展历史、程序重大迭代纪要、主流开发工具、适用场景以及从入门到专家的代码示例。通过本文的学习,读者可以了解 ESP32 与 LVGL 的结合优势,掌握基本的开发方法和技巧,能够开发出简单到复杂的图形界面应用。
从入门的 Hello World 程序,到进阶的按钮与事件处理,再到高级的无人机遥控终端界面设计和专家级的自定义组件与动画效果,逐步深入,帮助读者建立完整的知识体系。同时,介绍了主流的开发工具和调试方法,为读者的开发工作提供了便利。
(二)展望
随着嵌入式技术的不断发展,ESP32 和 LVGL 也将不断更新和完善。未来,ESP32 可能会在性能、功耗、集成度等方面有进一步的提升,支持更多的应用场景。LVGL 也将继续优化其功能和性能,提供更丰富的 UI 组件和更便捷的开发方式。
在无人机、无人船、无人车等领域,ESP32 与 LVGL 的结合将发挥越来越重要的作用,为这些设备的遥控终端界面开发提供更强大的支持。同时,随着物联网技术的普及,ESP32 与 LVGL 在智能家居、工业控制、消费电子等领域的应用也将更加广泛。
作为开发者,需要不断学习和掌握新的技术和工具,紧跟技术发展的步伐,以便更好地利用 ESP32 和 LVGL 进行创新开发,为嵌入式领域的发展贡献力量。