基于ESP32-S3-LCD-EV-Board的物联网多功能平台:
变色龙UI框架与LVGL游戏移植实践
作者:genvex
一、作品简介
故事过于恢宏庞大以至于不知从何讲起,说多了多眼泪,是不?
剧情需要还是讲些朋友们喜闻乐见的东西吧,我用 ESP32-S3-LCD-EV-Board 做一个lvgl的游戏合集,把目前B站流传的几个成功的lvgl游戏移植到了ev-Board上,游戏包括:羊了羊、消消乐、打砖块、植物大战僵尸等等,载体是乐鑫新推出的UI-框架(esp-brookesia) ,这个框架类似手机APP的运行界面和方式,每个程序独立管理互不影响,效果如下图所示,这种样式的框架也是所看好的发展趋势,因为随着mcu的频率不断的提升,例如乐鑫的P4 芯片的推出,无法阻挡着创客们往更加生动UI交互方向发展。因为程序可以控制进退,游戏运行起来还算流畅,这是在Arduino+pio上面所不能达到的效果,除了有游戏合集外,还专门制作了一个扩展板,这个扩展板简单地扩展了2个I2C的grove接口,一个UART的grove 接口,一个I2C的qwiic接口。grove制式的接口是M5stack和Seeed两大国内嵌入式系统创客生态的巨头,他们所生产的传感器都是基于grove接口的,那么这样EV-Board通过这样的方式就可以无缝连接地使用这两家公司所以千计的各种各样的传感器了,另外,qwiic接口则是国外嵌入系统巨无霸公司Adafruit公司所采用的传感器连接方式,作为嵌入式系统创客的鼻祖,她们家的传感器丰富至极,那么这么来一板在手,天下我有,想想都是开心呀。 故事梗概就这么多,下面我来分解动作,先解析这个框架的运行方式,然后再到单独没个程序的移植开发,坐好扶稳啦。
二、系统框图
项目以“esp32S3_ev_board”(开发板)为核心硬件,为后续的扩展和功能实现提供了物理支撑。量身定制了一块扩展板,能够扩展开发板的功能,例如增加更多的接口或者特定的硬件模块。传感器可以通过灵活的组装方式结合到ev_board上,使得ev_board成为了连接Grove传感器生态的终端,构成了一个完整的、基于Grove传感器的生态系统。成为了名副其实的 “物联网平台”,项目依托 “VSCode”(Visual Studio Code)和 “IDF”(ESP - IDF,即Espressif IoT Development Framework)编程环境,实现多种物联网功能的应用及展示。
三、各部分功能说明
(1)UI-框架(esp-brookesia)运行方式
ESP-Brookesia 是一个面向物联网设备的人机交互开发框架,基于 LVGL 构建,旨在简化用户在不同尺寸与形状屏幕上的 UI 设计及应用开发流程,该框架内置多种标准化系统 UI 和应用管理机制,允许用户灵活地修改样式、添加或删除应用 UI。ESP-Brookesia已实现所有系统 UI 统一的核心逻辑,包括 App 管理、样式表管理、事件管理等。封装了系统 UI 的通用控件,包括状态栏、导航栏、手势等。
1. Status Bar
Status Bar 用于显示时间、电量、WiFi 状态以及 App 指定图标,位于屏幕顶部,最多 6 张状态图标,并且支持自适应缩放,允许使用不同于样式表大小的图片。允许设置系统时间,格式为 HH:MM AM/PM。允许设置电量状态,包含百分比和状态图标。允许设置 WiFi 连接状态,包含状态图标。(目前时间是可以随网络同步,WIFI和电量只是装饰,因为就没有电池)
2. App Launcher
App Launcher 用于显示所有已安装 App 的图标,运行效果如下图所示,位于屏幕中间。每个 App 用一个图标展示,并且支持自适应缩放,允许使用不同于样式表大小的图片(你的图标小了,它会帮你放大,所有你想图标那么粗蛮,可以适当的留够透明的空白边距)。多页显示:通过左右滑动切换页面。页面指示器位于控件的底部,指示当前页面的位置。跟你手中的手机一毛一样。
3. Navigation Bar
提供导航按键,位于屏幕底部,导航按键:提供 "后退"、"主页"、"概览屏幕" 三种按键,通过点击控制界面切换。动态调整:支持通过样式表参数调整按键位置顺序。
4. Recents Screen
Recents Screen 用于显示正在运行中的 App,位于屏幕中间。可以显示剩余和总存储空间大小。显示当前后台 App 的 GUI 截图,并且支持自适应缩放。通过左右滑动切换 App,上下滑动清理后台 App。一键清理:支持一键清理所有后台 App。
(2)Brookesia程序移植方法
首先你得搭建好IDF编程环境(IDF编程环境不在本编内容讨论范围,反正每个人的感受和经历不一样,就像大家的成长路线不一样。),然后在乐鑫github,拉取
库内容,库包里面包含了Arduino和IDF的example,经过测试针对EV_broad,Arduino环境的屏幕驱动得不够完善,会出现屏幕诡异的划出边界,可能跟屏幕的触摸驱动和其他传感器共用I2C数据接口,它们之间会互相影响),在IDF环境下,这种情况可以有效地避免,为了追求极致流畅效果还是强忍痛苦,把这个坑继续挖下去。 针对性的我们打开esp_brookesia_phone_s3_lcd_ev_board文件夹,进行探索性测试。程序里面已经配置好了ev_board的屏幕驱动等硬件的基础实施,我们专注于我们的上层建筑即可。成功跑通代码后,我们就可以体验新鲜出炉的phone-UI, 但是一直寄人篱下也不是办法, 我们需要把这个文件拷贝出去,简历自己的项目,但从这里迁移出去后,依赖关系就发生了变化,也是一顿恶整呀。
通过修改main文件夹里的CMakeLists.txt文件(具体修改方案参见附件资料),最终完成的文件系统布局如下图所示。app里面放的些自己搭建的应用和附加增值驱动,phone里面放着的才是brookesia的APP程序系统。
第一步:框架修改
根据example里面的3个app(simple_conf,complex_conf,squareline)的运作方式来移植我们现有的lvgl程序。 顾名思义,simple_conf的设置方法最简单,后续所有APP都按这个套路进行改造,complex_conf的设置更详细些,自然修改的功能更加个性化,而squareline提供一个移植squareline项目的参考。 在simple_conf 文件里我们对应地把四个配置文件分别是: phone_app_simple_conf.hpp,phone_app_simple_conf.cpp,phone_app_simple_conf_main.h,phone_app_simple_conf_main.c 对应的修改成自己程序的名字,如flygame_conf_XXX.XXX,这样在对应的文件中引用也要修改,那么我们最好使用全部替换的手法来处理,避免无休止的报错,影响心情。
第二部:内容移植
而同级别目录下的UI文件夹放置我们的lvgl程序。这些lvgl需要归纳为一个入口函数来加载lvgl图形界面内容。 lvgl程序我们在别的地方测试好了再往这里搬,不适合在这个地方开启原始的建造工程,但是移植的工作难点却变成了修复一些由于小问题,消耗了大量时间。
第三步:修改图标
最后的步骤是修改APP在主界面显示的图标,在前面移植过程中采用默认图标即可,最后项目定型后统一更换,参见“APP图标修改指引”(修改桌面墙纸,在后面在补充)。
小结:
以上就是移植1个APP所需要开展的工作,开始的时候,觉得很繁琐,多整几个之后,这种“症状”得到缓解,成就感慢慢地飙升,于是有了前面的剧情,我把“大老虎爱3D”的lvgl最新杰作都搬了过来,这里感谢B站up主”大老虎爱3D”的无私奉献,他开创了使用lvgl开发小游戏的先河,他在lvgl动画上(animation控件)已达到了出神入化的境界,因此,一般的mcu也被折腾得上气不接下气,我们共同期待大老虎可以出品更多大众喜爱的小游戏,让我们lvgl朋友圈更加繁荣昌盛。
另外,这种移植方式是是最简单无脑的,但是文件嵌套有些多,是肯定可以继续优化的。按照,以上套路还移植了一些有用好玩的小程序,例如从esp32P4demo里拿过来的计算器小程序就优化得很成功,可以说是这些程序中最实用的一个,值得深入学习挖掘。
APP文件系统推荐布局指引
移植工作目录指引
APP图标修改指引
(3)扩展板设计:
精彩过后回到我们的主线任务,回应本次大赛的“万物互联”主题上,本次任务为ev_board做了一个扩展板,由于ev_board上的管脚资源几乎被RGB屏幕挤占得一个不剩了,还是在加了管脚扩充IC的情况下。在ev_board上11pin双排的扩充管脚上,能用的只有I2C和一路UART(跟USB接口共用)可以使用了。 所以,本次设计的扩展板就从扩展引脚引出I2C和UART接口,接口制式如前所述(grove&qwiic),加了一个bme280温度度传感器。 下图是扩展板设计图(资源一并放在附件包里头)。由于前期收藏了很多M5Stack的传感器,例如声音、光照、温湿度、二氧化碳、热成像、MIDI驱动器,这些传感器大多都是通过I2C和esp32连接的,因此这些传感器全都可以蹭上ev_board的高清屏幕。 日后可以慢慢的把这些传感器都排上用场。 这次一同购买的Adafruit 公司的SGP30 传感器是使用QWIIC接口连接的,但由于跟ev_board内部的I2C原件地址冲突,没有成功驱动,也是巧了。
PCB图案寓意:PCB布局上的卡通图案看似简单其实一点也不复杂,正面有个卡通熊猫,相传熊猫能够保佑我们的程序猿的代码稳定不bug。还有乐鑫的Logo 致敬一下这个伟大的公司。顶部的一些小图案代表了该扩展板集成了相应的硬件功能,紧扣“万物互联”的主题。背面除了些小元件之外,巧妙地利用穿孔构造了一只猫的形状,猫在人们心目中代表“灵动”与“智慧”,跟我们程序猿大型交友社区的图案相似,也是巧合了。
实物对照图
(4)程序主界面
到了最后一部分了,完成了一个多功能的控制面板,主要工作内容是ev_board的example的智能面板提取了一个页面来完成联网、天气预报、时钟、BME280温湿度传感器的数据展示。 构建 bme280_sensor类完成传感器的初始化、定时数据更新、获取当前数据等操作。
(1)BMX280 传感器的业务逻辑
使用了 `BMX280Sensor` 这个结构体来封装 BME280(或 BMX280)传感器的初始化、数据更新、数据获取等功能。整体设计考虑了传感器的初始化、数据更新以及线程安全的问题,并将数据更新和获取逻辑封装成了多个函数和任务,使得系统可以高效、稳定地工作。
1. 传感器初始化:通过 `BMX280Sensor_create` 创建传感器对象,并进行初始化配置,包括 I2C 配置和传感器初始化。
2. 数据更新:通过一个 FreeRTOS 任务(`updateTask`)每秒定时更新传感器数据,读取传感器的温度、湿度和气压值,并通过互斥锁确保数据更新的线程安全。
3. 数据获取:通过 `BMX280Sensor_getData` 和 `BMX280Sensor_getCurrentInfo` 获取当前的传感器数据。
下面将逐个分析代码中的业务逻辑。
1. 传感器创建与初始化
BMX280Sensor* BMX280Sensor_create(i2c_port_t port, bool initialized) {
BMX280Sensor* sensor = (BMX280Sensor*) malloc(sizeof(BMX280Sensor));
if (sensor == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for BMX280Sensor");
return NULL;
}
sensor->i2c_port = port;
sensor->bmx280 = NULL;
sensor->dataMutex = xSemaphoreCreateMutex();
if (sensor->dataMutex == NULL) {
ESP_LOGE(TAG, "Failed to create mutex");
free(sensor);
return NULL;
}
if (initialized) {
i2c_config_t i2c_cfg = {
.mode = I2C_MODE_MASTER,
.sda_io_num = BSP_I2C_SDA_R16,
.scl_io_num = BSP_I2C_SCL_R16,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000
};
ESP_ERROR_CHECK(i2c_param_config(port, &i2c_cfg));
ESP_ERROR_CHECK(i2c_driver_install(port, I2C_MODE_MASTER, 0, 0, 0));
}
if (BMX280Sensor_init(sensor) != ESP_OK) {
ESP_LOGE(TAG, "Sensor initialization failed");
BMX280Sensor_destroy(sensor);
return NULL;
}
BMX280Sensor_startUpdating(sensor); // 启动数据更新任务
return sensor;
}
首先使用 `malloc` 动态分配内存来创建 `BMX280Sensor` 对象。 I2C 配置:如果 `initialized` 为 `true`,则初始化 I2C 总线。但因为传感器跟屏幕的共用I2C,在板卡的bsp中已经驱动好I2C了,所以这里的初始化只有在测试阶段需要用到。 为了保持库类的完整性,继续保留了这些内容。 调用 `BMX280Sensor_init` 函数初始化传感器,如果初始化失败则销毁并释放传感器对象。初始化完成后,调用 `BMX280Sensor_startUpdating` 启动数据更新任务(通过任务创建)。通过 `xSemaphoreCreateMutex` 创建互斥锁,保证数据读取和写入的线程安全。
2. 销毁传感器对象
```c
void BMX280Sensor_destroy(BMX280Sensor* sensor) {
if (sensor != NULL) {
if (sensor->dataMutex) {
vSemaphoreDelete(sensor->dataMutex);
}
free(sensor);
}
}
```
- 资源释放:销毁传感器对象时,首先释放互斥锁,然后释放分配的内存。
3. 启动数据更新任务
void updateTask(void* param) {
// 将传入的参数(指针)转换为 BMX280Sensor 类型
BMX280Sensor* sensor = (BMX280Sensor*) param;
// 无限循环,定期更新传感器数据
while (1) {
// 定义一个用于存储传感器数据的结构体
SensorData_t data;
// 设置传感器为强制模式 (BMX280_MODE_FORCE),此模式下传感器会一次性进行测量并返回结果
if (bmx280_setMode(sensor->bmx280, BMX280_MODE_FORCE) == ESP_OK) {
// 在传感器采样时,任务延时 1 毫秒,并检查传感器是否完成采样
// 当采样完成后,跳出此循环
do {
vTaskDelay(pdMS_TO_TICKS(1)); // 延时 1 毫秒,等待传感器完成采样
} while (bmx280_isSampling(sensor->bmx280)); // 判断传感器是否在采样中
// 采样完成,读取传感器的数据 (温度、压力、湿度)
if (bmx280_readoutFloat(sensor->bmx280, &data.temperature, &data.pressure, &data.humidity) == ESP_OK) {
// 获取互斥锁,确保当前任务独占传感器数据
if (xSemaphoreTake(sensor->dataMutex, portMAX_DELAY)) {
// 将获取到的传感器数据存储到 BMX280Sensor 对象的 currentData 字段中
sensor->currentData = data;
// 释放互斥锁,允许其他任务访问传感器数据
xSemaphoreGive(sensor->dataMutex);
}
}
}
}
}
```c
void BMX280Sensor_startUpdating(BMX280Sensor* sensor) {
xTaskCreate(updateTask, "bmx280_update_task", 2048, sensor, 5, NULL);
}
```
数据更新:这个任务每 1 秒钟更新一次传感器数据。流程如下:
设置强制模式:使用 `bmx280_setMode(sensor->bmx280, BMX280_MODE_FORCE)` 设置传感器进入强制模式(每次读取时采样一次)。
等待采样完成:使用 `vTaskDelay(pdMS_TO_TICKS(1))` 等待 1 毫秒,循环检测 `bmx280_isSampling`,确保数据采样完成。
读取数据:使用 `bmx280_readoutFloat` 读取温度、湿度和气压数据。
数据保护:通过互斥锁 `xSemaphoreTake` 和 `xSemaphoreGive` 来保护共享数据 `currentData`,确保数据的安全性。
5. 获取当前传感器数据
```c
esp_err_t BMX280Sensor_getData(BMX280Sensor* sensor, SensorData_t* data) {
if (xSemaphoreTake(sensor->dataMutex, portMAX_DELAY)) {
*data = sensor->currentData;
xSemaphoreGive(sensor->dataMutex);
return ESP_OK;
}
return ESP_FAIL;
}
```
```c
esp_err_t BMX280Sensor_getCurrentInfo(BMX280Sensor* sensor, SensorData_t* info) {
if (info == NULL) {
ESP_LOGE(TAG, "Invalid argument: info is NULL");
return ESP_ERR_INVALID_ARG;
}
if (sensor->currentData.temperature != 0 || sensor->currentData.pressure != 0 || sensor->currentData.humidity != 0) {
memcpy(info, &sensor->currentData, sizeof(SensorData_t));
return ESP_OK;
}
return ESP_FAIL;
}
```
提供了两个传感器数据获取的函数,可以任选其一,其中加了检查数据有效性,这两个函数是对外联系的窗口,数据的更新自动在后台完成。在天气页面只需要调用获取数据函数即可,实现了底层与上层的轻度解耦。
6.函数调用
最后在时钟主页的lvgl版面设计中调用获取数据函数,与天气预报、时钟代码一起打包,按照APP的生成方法,建立一个独立的APP。
static void update_bme_data(void *arg)
{
// 定义存储传感器数据的结构体
SensorData_t data;
// 获取传感器数据
esp_err_t err = BMX280Sensor_getData(sensor, &data);
// 检查是否成功获取数据
if (err == ESP_OK)
{
// 输出到控制台调试信息
printf("Temperature: %.2f °C\n", data.temperature);
printf("Pressure: %.2f hPa\n", data.pressure / 1000); // 转换为 hPa 后输出
printf("Humidity: %.2f %%\n", data.humidity);
// 格式化并更新温度标签
char temp_str[32];
snprintf(temp_str, sizeof(temp_str), "%.2f℃", data.temperature); // 格式化温度数据并加入单位
lv_label_set_text(bme_temp_label, temp_str); // 设置温度标签的文本
// 格式化并更新湿度标签
snprintf(temp_str, sizeof(temp_str), "%.2f%%", data.humidity); // 格式化湿度数据并加入单位
lv_label_set_text(bme_humi_label, temp_str); // 设置湿度标签的文本
// 格式化并更新气压标签(将Pa转换为hPa)
snprintf(temp_str, sizeof(temp_str), "%.1f", data.pressure / 1000); // 格式化压力数据并转换为 hPa
lv_label_set_text(bme_press_label, temp_str); // 设置气压标签的文本
}
else
{
// 获取数据失败,输出错误信息
printf("Failed to get sensor data.\n");
}
}
PS:最后一个小问题,如何更换壁纸
跟其他电子产品一样,大家都热衷于更换壁纸,brookesia的墙纸是放在依赖里面的,根据自己屏幕的尺寸,找到对应的图片资源替换成自己的墙纸即可。
修改墙纸
components\esp-brookesia\src\systems\phone\stylesheets\480_480\assets\wallpaer
四、作品源码
项目完整代码地址:
https://download.eeworld.com.cn/detail/genvex/635122
扩展板PCB资料地址:
https://download.eeworld.com.cn/detail/genvex/635123
项目开箱贴:
https://bbs.eeworld.com.cn/thread-1296131-1-1.html
五、作品功能演示视频
https://training.eeworld.com.cn/course/68804
六、项目总结
1. 乐鑫UI框架的应用
本项目采用ESP - Brookesia框架,基于LVGL构建,简化了不同尺寸与形状屏幕的UI设计及应用开发流程,实现了系统UI统一的核心逻辑,该框架类似手机APP运行界面和方式,每个程序独立管理互不影响,在mcu频率提升背景下,极有可能物联网设备UI交互发方向,至少在今后的几年会成为比较流行的UI运行方式。
2. 多合一游戏移植
将B站流传的多个成功lvgl游戏(如羊了羊、消消乐、打砖块、植物大战僵尸等)移植到ESP32 - S3 - LCD - EV - Board上,为该平台增添了娱乐功能,丰富了其应用场景,在物联网设备上实现多种游戏功能的整合,充分体现了IDF框架的优越性,这些包含大量动画的游戏基本都可以正常运行起来,在其他环境单次只能跑通一个游戏。@老虎爱3D 大佬从睡梦中惊醒,头一回有人把的全部心血给一个锅端了。
3. 创意扩展板设计
针对EV - Board管脚资源紧张情况,设计扩展板从有限可用管脚引出特定接口(I2C和UART接口,采用grove&qwiic接口制式),并添加BME280温度传感器。这一设计使平台能无缝连接M5Stack和Seeed等公司的众多传感器,同时尝试连接国外Adafruit公司传感器,极大拓展了平台的感知能力和外设兼容性,仅以本次活动为契机,开拓了万物互联的可能性。
4. 多功能主界面设计
在程序主界面实现了联网、天气预报、时钟、BME280温湿度传感器数据展示等多功能整合。通过构建特定类和函数来管理传感器数据,在数据更新和获取过程中考虑线程安全问题,将数据更新逻辑封装成任务并利用互斥锁保护共享数据,同时实现了底层与上层的轻度解耦,方便功能调用和扩展,实现了多模块功能融合与优化。
通过这些创新实践,ESP32-S3-LCD-EV-Board不仅提升了用户体验,还增强了物联网设备的功能性和互动性,展示了其在多功能物联网解决方案平台构建中的潜力和应用前景。由于收到板卡比起其他同学几乎晚了半个月,在一个月追星赶月勤奋学习,在其他传感还没有用起来,后面有空了继续搞起来。
鸣谢:
最后感谢梅尧大佬在扩展板pcb设计的指导与帮助。
感谢重阳老师在bme280驱动与功能融合反面提供的指导和帮助。
感谢羊毛党一众党员不分昼夜的技术支撑。
感谢主办方提供深入学习的机会,不离不弃地督促我的学习进展@lightxixi ,让我把这个IDF项目硬啃下来了。
七、其他
在wifi_setting.c文件里修改自己的wifi信息,没有Bme280传感器可能无法开机,在IDF5.1-IDF5.3(不包括5.3)下运行。