本项目基于RP2040带屏12指神探在vscode_pio编程环境移植了lvgl,利用外部rtc作为时钟源,实现了手表功能和一个泡泡屏保程序,程序可通过按键切换。
项目代码思路流程图
一、编程环境搭建
项目是基于Arduino在vscode的PlatformIO开发平台完成的。Arduio用来管理小型单功能项目还是可以的,主要是它编程环境准备相对简单,大众创客的挚爱。进阶的使用可以将战场转移到vscodeIDE,利用vscodeIDE提供的自带技能及其他优秀插件带来的便利,来提升编程效果。
二、屏幕驱动及LVGL移植
成功的嵌入式设备需要一个极具吸引力的用户界面,才能给用户留下良好的第一印象。在Arduino的范畴里,传统的图形绘制库(例如经典TFT_eSPI,Adafruit_GFX,Lovyan_GFX等)可以帮助用户快速的入门,LVGL的出现给嵌入式开发在最终产品呈现提供了一种解决方案。Arduino编程环境的LVGL运行效果比在micropython环境更为流畅,可以实现多任务的切换,而且编程的难度可深可浅,根据不同的编程能力,实现同一功能有很多种解决办法。但是LVGL的版本一直在演替,带来的问题是版本间的差异会给刚入门的小伙伴造成很大的困扰,即便是想运行一个helloworld测试,就要折腾好久,甚至弃坑,都是很正常的现象,所以要多加练习。
(1)屏幕驱动
因为早前已经实现了TFT_eSPI库来驱动ST7789这个屏幕,如果不想玩LVGL,直接使用TFT_eSPI这个经典的图形库也可以玩出花来,毕竟过去这么多年,国内外的大神就是使用这些纯朴的工具开发出很多优秀的作品,但是社会变了,使用LVGL来制作用户界面成为了一种时尚,假如你在嵌入式开发领域活动,你不掌握一点LVGL相关知识,在大家津津有味地讨论起技术来,你只能在风中凌乱了。
/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp_drv, 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((uint16_t *)&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp_drv);
}
static void lv_encoder_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
data->state = LV_INDEV_STATE_RELEASED;
if (ENTER_BUTTON)
{
data->state = LV_INDEV_STATE_PR;
ENTER_BUTTON = false;
}
else if (PREV_BUTTON)
{
data->enc_diff = -1;
PREV_BUTTON = false;
}
else if (NEXT_BUTTON)
{
data->enc_diff = 1;
NEXT_BUTTON = false;
}
}
lv_timer_t *button_read_timer;
三、时钟设计
(1)RTC8563驱动
rp2040默认I2C引脚是4-5,没有被引出,要使用扩展口的引脚必须修改才能使用I2C的外置设备。在Arduino官方板卡支持库没有I2C驱动指定引脚的功能函数。所以,折中的办法是手动修改系统内部的引脚默认设置。
修改I2C默认引脚的方法 :pins_arduino.h ~/.platformio/packages/framework-arduino-mbed/variants/RASPBERRY_PI_PICO/pins_arduino.h,具体路径参见下图。
如果忘记修改了,就会花屏,不能正常启动(大家应该都知道,我为什么知道的吧--把一个主机整成砖头了)。
由于Pico没有提供网络支持,所以要实现时钟功能需要外部提供一个时钟源,这里用到的是RTC8563.这个设备在esp32上使用得挺好的,在pico上的使用却还不多。
(2)时钟设计
使用了面向对象思路来构造程序,把LVGL时钟用类来封装,这样有利于程序的进入和退出的管理。相较于面向过程的编程,代码的思路更为清晰,但工作量会增加,处理变量、函数、定时器会需要一些特殊处理。
时间指针旋转角度的更新函数:
void WatchController::update()
{
rtc.getTime(&timeStruct);
lv_img_set_angle(view.ui.img_hour, (timeStruct.hours * 300 + timeStruct.minutes * 5) % 3600);
lv_img_set_angle(view.ui.img_minute, timeStruct.minutes * 60 + timeStruct.seconds);
lv_img_set_angle(view.ui.img_second, timeStruct.seconds * 60);
}
其对应的计算公式如下
LVGL 系统里1圈是3600度
- 时针移动角度计算公式:
- 时针每小时300度,即每小时移动(3600/12)= 300度。
- 分针每分钟移动(300/60)= 5度。
- 计算公式:
hours * 300 + minutes * 5
- 分钟针移动角度计算公式:
- 分针每分钟60度,即每秒移动(3600/60)= 60度。
- 计算公式:
minutes * 60 + seconds * 1
- 秒针移动角度计算公式:
- 秒针每秒钟60度,即每秒移动(3600/60)= 60度。
- 计算公式:
seconds * 60
四、泡泡程序
完成了两个程序的切换,按拨轮的中间键可以切换两个程序。
{
// monitorController.model.speaker.beep();
switch ((++currentScreen) % 2)
// switch ((currentScreen) % 2)
{
case 0:
bubblecontroller.onViewDisappear();
watchcontroller.onViewLoad();
break;
case 1:
watchcontroller.onViewDisappear();
bubblecontroller.onViewLoad();
break;
default:
break;
}
}
void BubbleView::game_draw_event_cb(lv_event_t *e)
{
// changing the style of the rectangle.
lv_obj_t *obj = lv_event_get_target(e);
lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e); // get the draw context
lv_draw_rect_dsc_t draw_rect_dsc; // there is many draw dsc
lv_draw_rect_dsc_init(&draw_rect_dsc);
draw_rect_dsc.radius = LV_RADIUS_CIRCLE; // change the rectangle into a circle
draw_rect_dsc.border_width = 2;
draw_rect_dsc.bg_opa = LV_OPA_0; // transparent circle
draw_rect_dsc.border_color = lv_color_white(); //,LV_COLOR_MAKE(100, 150, 226);
// draw_rect_dsc.border_color = lv_color_hex(rand()*512+rand()*125);
if (vectb.size() < 50)
{
bubble_t b = {
.r = lv_rand(5, 50),
.x = lv_rand(b.r, LV_HOR_RES - b.r),
.y = lv_rand(b.r, LV_VER_RES - b.r),
.dx = (((random(2) == 1) ? -1 : 1) * lv_rand(1, 2)),
.dy = (((random(2) == 1) ? -1 : 1) * lv_rand(1, 2)),
// .color = lv_color_white(),
};
vectb.push_back(b);
}
for (auto &b : vectb)
{
// draw_rect_dsc.border_color = lv_color_white();
lv_area_t gen_rect; // the area_t is import for the drawing.
gen_rect.x1 = b.x; // giving the x1,y1,x2,y2 site for every rectangle
gen_rect.x2 = b.x + b.r; // but now it is circle here.
gen_rect.y1 = b.y;
gen_rect.y2 = b.y + b.r;
lv_draw_rect(draw_ctx, &draw_rect_dsc, &gen_rect); // the main function works here
b.move();
}
}
- 更改矩形的样式:
- 通过lv_event_get_target(e)获取目标对象obj和lv_event_get_draw_ctx(e)获取绘图上下文draw_ctx。
- 初始化绘制矩形描述符draw_rect_dsc,并设置属性,如圆角半径、边框宽度、背景透明度和边框颜色。
- 绘制气泡:
- 如果vectb(气泡向量)的大小小于50,随机生成气泡的半径、位置、速度等属性,并将气泡添加到vectb中。
- 遍历vectb中的每个气泡b,根据其属性绘制圆形(实际上是用矩形模拟圆形),并调用b.move()移动气泡。
- 绘制函数:
- lv_draw_rect函数用于绘制矩形或圆形,根据给定的绘图上下文、绘制描述符和区域进行绘制。
本项目的一些心得体会:
以往参加活动,针对主办方提出的要求逐一完成任务,把任务分解开来,这样比较轻松的完成单个内容。这次尝试把所有的任务整合在一起,在一次启动就可以展示所有(必做任务)内容,这是一次积极的挑战,通过面向对象的编程思想,把功能用类(class)进行统一的封装,使得不同的功能可以自由切换,而且还能保持流畅运行。理论上只要Flash够大,可以按照类的模板不断的扩展应用程序,相当做个了一个项目的架构,体现了做应用项目的思维,这种思维应该继续使用下去,因为经过简单功能程序学习,最终是需要把这些简单的功能综合起来),这样才能成为一个有用的工具,一个有趣的产品。
感谢主办方给予我极大的创作空间,祝愿该系列活动完满成功并持续举办下去!
核心代码:
https://gitee.com/genvex/rp2040_watch_with_rtc