【得捷电子Follow me第2期】基于lvgl构建的多任务程序框架(综合贴)
[复制链接]
本帖最后由 genvex 于 2023-10-13 10:04 编辑
项目目录
一、编程环境搭建
二、屏幕驱动及Lvgl移植
三、开机动画设计
四、主要功能展示
五、中文字体处理
六、任务5 手机远程遥控板载led灯
总结:心得体会
前言:
本项目内容使用Feather TFT 及 LIS3DH加速度传感器、AHT20 温湿度传感器 硬件,开发了一套用户UI,包括了时钟、硬件信息、温湿度、天气预报、加速度传感器使用,最后,使用blinker库完成手机远程遥控板载led灯的功能。
一、编程环境搭建
项目是基于Arduino在vscode的PlatformIO开发平台完成的。Arduio用来管理小型单功能项目还是可以的,主要是它编程环境准备相对简单,大众创客的挚爱。进阶的使用可以将战场转移到vscode,利用vscodeIDE提供的自带技能及其他优秀插件带来的便利,来提升编程能力。
(1)新建一个Feather的项目
(2)对项目的配置进行微调
vscode 编程环境就相对复杂一点,同样的内容在别人机器上好好的,在你的电脑上就可能玩不转了,因为很难保证大家的环境细节完全一致,所以调整配置文件也是件技术活,也没啥技巧就是多看别人的配置。如果编译通过但是不能上传,就需要指定 esptoolspy的版本,才能够解决这个问题。FeatherTFT 带有PSRAM在这里作了相关声明,后面才可以正常使用。另外,对flash分区也作了调整,默认情况只有1.多M的空间,把所有项目相关内容都一次放进去还是比较紧张的,这里我调整到3M左右的空间,跑起来就没有顾忌。
二、屏幕驱动及Lvgl移植
成功的嵌入式设备需要一个极具吸引力的用户界面,才能给用户留下良好的第一印象。传统的图形绘制库(例如经典TFT_eSPI,Adafruit_GFX,Lovyan_GFX等)可以帮助用户快速的入门,lvgl的出现给嵌入式开发在最终产品呈现提供了一种解决方案。但是lvgl的版本一直在演替,带来的问题是版本间的差异会给刚入门的小伙伴造成很大的困扰,即便是想运行一个helloworld测试,就要折腾好久,甚至弃坑,都是很正常的现象。
Lvgl的驱动核心驱动代码见下图所示。核心思想是用常规的作图库为lvgl提供一个刷屏的工具函数。我这里用了LovyanGFX来驱动Feather的屏幕,因为这个图形库有专门针对FeatherS3的驱动支持,得来全不费工夫,直接使用就好,到驱动底层内容去观摩了下,作者对屏幕的x,y方向做个偏移设置,就是这两个参数,自己上手就够有一阵折腾的。构建好这个工具函数后,把它传递到lvgl驱动核心,在配置上主要是把屏幕的尺寸、刷屏的缓存大小,触控驱动(本次没用上)的参数,看似长篇大论,但是一个环节也不能少,这个架构也是经过多次模仿学习,本人认为是一个较为优雅的解决方案。这里特别强调一下,这个驱动把刷屏的内存放在PSRAM里面,目前PSRAM最常见使用就是用来刷屏,这样做可以节省些常规内存,保障程序和图形界面的正常运行,这样设置下来,屏幕能保持在50~60fps的刷新率,也是相当流畅的。
static void lvgl_begin(void)
{
#define LVGL_HOR_RES (240)
#define LVGL_VER_RES (135)
// #define DISP_BUF_SIZE (LVGL_HOR_RES * 100)
lcd.init();
lcd.initDMA();
lcd.setRotation(2);
// lcd.setBrightness(100);
lv_init();
static lv_disp_draw_buf_t draw_buf;
size_t DISP_BUF_SIZE = sizeof(lv_color_t) * (LVGL_HOR_RES * LVGL_VER_RES); // best operation.
static lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
static lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
// static lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc( // most fast operation.
// DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
// static lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(
// DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/
disp_drv.hor_res = LVGL_HOR_RES;
disp_drv.ver_res = LVGL_VER_RES;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = my_disp_flush;
/*Set a display buffer*/
disp_drv.draw_buf = &draw_buf;
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p)
{
int w = (area->x2 - area->x1 + 1);
int h = (area->y2 - area->y1 + 1);
/* Start new TFT transaction */
lcd.startWrite();
/* set the working window */
lcd.setAddrWindow(area->x1, area->y1, w, h);
/* Write the buffer to the display */
// lcd.writePixels((lgfx::rgb565_t *)&color_p->full, w * h);
lcd.writePixelsDMA((lgfx::rgb565_t *)&color_p->full, w * h);
// lcd.pushImageDMA(area->x1, area->y1, w, h,&color_p->full);
// lcd.pushPixelsDMA((uint16_t *)color_p, w * h);
// lcd.pushImage(area->x1,area->y1,w,h,&color_p->full);
/* terminate TFT transaction */
lcd.endWrite();
/* tell lvgl that flushing is done */
lv_disp_flush_ready(disp);
}
三、开机动画设计
模仿Epressif demo的开机动画制作了一个开机动画,这个动画的运行机理和Lvgl的动画机制不一样。这个动画使用了一个时间计数器(count)来推动动画的前进,随便变量count的步进,三个大小不一的圆弧产生旋转的效果最后消失。主要脑洞比较大的地方是设计圆弧的起始角度和结束角度,这两个数值决定了圆弧的长度,用三角函数的数值变化区间来实现圆弧的从无到有再缩短,视觉效果就产生旋转效应。为了便于理解对计数器变化引起图形数值的变化做了一幅曲线图,观察起始角度和结束角度的变化曲线,蓝色线条是起始角度在整个周期的数值变化曲线,在开始一段时间(count<0)的阶段保持在0度(即默认的起始位置,这里需要复习一下arc的绘图语法,见代码块),在这个阶段结束角度在慢慢变大(橙色线),呈现了圆弧在伸长。下一阶段(count>0),开始角度(蓝色线)也开始递增,同时,结束角度(橙色线)也继续保持上升状态,这阶段呈现了弧形在旋转的效果,因为开始角度和结束角度同时移动;但由于开始角度增长速度比较快(斜率大),很快就与结束角度重合,这个阶段呈现了弧形在不断地缩短直到最后消失,至此,弧形的动画结束,最后是乐鑫和Adafruit公司的logo的淡入(fadein)和淡出(fadeout),利用的cos函数从第四象限到第一象限数值呈钟形的曲线效果,实现淡入淡出。红色虚线表开始角度和结束角度的距离,也就是弧形的长度变化曲线, 而真正产生旋转效果的参数是 rotation一直在匀速增加,推进了3调弧形的位置。
lv_arc_set_bg_angles(arc[i], arc_start, arc_end);
// 要设置背景弧的开始和结束角度,请使用 lv_arc_set_bg_angles(arc, start_angle, end_angle)
// 函数或 lv_arc_set_bg_start/end_angle(arc, angle)。
// 零度位于对象的中间右侧(3 点钟方向),并且度数沿顺时针方向增加。 角度应在 [0;360] 范围内。
lv_arc_set_rotation(arc[i], (count + 120 * (i + 1)) % 360);
同时,板载的led灯也跟随count变量的变化产生渐变,跟动画互相辉映,产生出来灯火绚烂的效果。最后动画结束后用回调函数钩子把主程序带动起来。
四、主要功能展示
本项目的主要功能包括时钟、主板性能测试、传感器数值显示面板(模拟),天气预报,泡泡5个板块。各板块使用相同的类模板进行构造,通过板载按键实现循环切换,理论上可以无限添加,只要flash空间够大。
(1)主程序:
主程序模块
(2)监控模块
用于检测和展示主板的硬件信息。
(3)温湿度监控模块
采集了使用AHT20 温湿度传感器的数据并采用仪表盘的形式展示出来。
void MonitorModel::init(){
aht.begin();
getAlldata();
}
void MonitorModel::getAlldata(){
sensors_event_t humid, temp;
aht.getEvent(&humid, &temp); // populate temp and humidity objects with fresh data℃
Serial.print(temp.temperature);
Serial.print(humid.relative_humidity);
this->temperature = temp.temperature;
this->humidity = humid.relative_humidity;
}
float MonitorModel::getTemperature() {
return this->temperature;
}
long MonitorModel::getHumidity() {
return this->humidity;
}
(4)时钟模块
(5)天气模块
(6)加速度传感器及泡泡球屏保
读取了Adafruit_LIS3DH加速度传感器数据并显示在屏幕上。
void BubbleModel::init()
{
if (!lis.begin(0x18))
{ // change this to 0x19 for alternative i2c address
Serial.println("Couldnt start");
while (1)
yield();
}
lis.setRange(LIS3DH_RANGE_4_G); // 2, 4, 8 or 16 G!
Serial.print("Range = "); Serial.print(2 << lis.getRange());
Serial.println("G");
lis.setDataRate(LIS3DH_DATARATE_50_HZ);
}
void BubbleModel::getAlldata()
{
lis.read(); // get X Y and Z data at once
sensors_event_t event;
lis.getEvent(&event);
/* Display the results (acceleration is measured in m/s^2) */
Serial.print("\tX: "); Serial.print(event.acceleration.x);
Serial.print("\tY: "); Serial.print(event.acceleration.y);
Serial.print("\tZ: "); Serial.print(event.acceleration.z);
this->accX = event.acceleration.x;
this->accY = event.acceleration.y;
this->accZ = event.acceleration.z;
}
五、中文字体处理
转中文字体虽然没有太多的技术难度,就是有点麻烦例如选什么字体(自己觉得好看,才是真的好看,纠结),字号(要跟屏幕大小匹配,来回要调整几次),字数(要转哪些字,多了文件大,少了不够用,导致某些不常见情况会出现乱码)。在没有其他选择的时候,大家会用lvgl官方提供的在线转化工具,稍微有点懵,用几次也可以掌握。国内民间也有好些转图转字体的好用的工具,本项目采用的就是好心人制作转字体工具LvglFontTools0.4,虽然界面有点丑,但是转出来的字体能正常使用,还要什么单车呢。
教程和参见下方链接和导图(不好用免费退款。
中文字体制作:
https://blog.csdn.net/kelleo/article/details/122686644
六、任务5 手机远程遥控板载led灯
实现遥控有很多解决方案,例如本地局域网的控制,乐鑫rainmaker方案,mqtt方案。前期测试了rainmaker的方案,没有成功,而且需要flash空间比较大,运行还不太稳定。局域网控制方案,需要设计一个网页,网页元素包括三条滑动条,用来控制灯光的三种颜色,也设计好了,就是过程较为繁琐。最后,选用了比较优雅的方案,使用了点灯科技的blinker库来实现项目要求的功能,该库的背后原理是mqtt机制,但是经过封装后,开发出来给用户使用的api非常简单,另外,还贴心提供手机客户端的app,使用起来更加方便,不足之处是只提供一个硬件的接入,但是算得上良心商家了。
void rgb1_callback(uint8_t r_value, uint8_t g_value, uint8_t b_value, uint8_t bright_value)
{
BLINKER_LOG("R value: ", r_value);
colorR = r_value;
BLINKER_LOG("G value: ", g_value);
colorG = g_value;
BLINKER_LOG("B value: ", b_value);
colorB = b_value;
BLINKER_LOG("Rrightness value: ", bright_value);
pixels.setPixelColor(0, r_value, g_value, b_value);
if (bright_value > 100)
bright_value = 100; // too high will damage the led.
brightness = bright_value;
pixels.setBrightness(brightness);
pixels.show();
lv_led_set_brightness(blinker_led, brightness);
lv_led_set_color(blinker_led, lv_color_make(colorR, colorG, colorB));
lv_label_set_text_fmt(rlabel, "R:%d", colorR);
lv_label_set_text_fmt(glabel, "G:%d", colorG);
lv_label_set_text_fmt(blabel, "B:%d", colorB);
}
rgb灯的响应回调函数
本项目的一些心得体会:
以往参加活动,针对主办方提出的要求逐一完成任务,把任务分解开来,这样比较轻松的完成单个内容。这次尝试把所有的任务整合在一起,在一次启动就可以展示所有(必做任务)内容,这是一次积极的挑战,通过面向对象的编程思想,把功能用类(class)进行统一的封装,使得不同的功能可以自由切换,而且还能保持流畅运行。理论上只要Flash够大,可以按照类的模板不断的扩展应用程序,相当做个了一个项目的架构,体现了做应用项目的思维,这种思维应该继续使用下去,因为经过简单功能程序学习,最终是需要把这些简单的功能综合起来(综合功能产品,手机),这样才能成为一个有用的工具,一个有趣的产品。
感谢主办方给予我极大的创作空间,祝愿该系列活动完满成功并持续举办下去!
主任务代码 ,主要修改自己wifi信息,高德天气api账号,用到 LIS3DH加速度传感器、AHT20 温湿度传感器 硬件。
附加任务代码 需要使用点灯科技blinker app 及硬件密钥。 预祝食用愉快,有问题,欢迎来聊。 汤半泛!
|