项目介绍
LVGL是一种轻量级的图形化界面,结合RP2040微控制器可以方便地实现各种图形化界面。本项目将LVGL移植到这个带有240x240分辨率LCD,两个按键以及一个拨轮开关的调试终端上,制作了一款图形化控制终端。
硬件介绍
本项目使用的是带屏版的12指神探,它是在原板基础上,配备了一块240*240分辨率的LCD彩屏以及两个可程控按键和一个拨轮,丰富了人机交互功能,方便信息观察、界面切换。
本项目主要使用了如下硬件:
- 1个ST7789驱动的240*240分辨率LCD彩屏
- 1个NSHT30温湿度传感器(外置传感器)
- 1个拨轮开关
- 1个按键开关
软件工具
- Thonny:用于代码编写、调试及上传
- WSL2 : Windows下的Linux子系统,用于固件的编译
设计思路
本项目思路比较明确,就是将LVGL移植到现有的平台上,主要要解决的问题有:固件的编译以及程序的移植。
目前LVGL能找到资料较多的是使用C语言开发,MicroPython的资料相对较少,使用MicroPython移植到ESP32的还有较多,而使用MicroPython移植到本次使用的RP2040平台的几乎没有资料,本项目中尝试挑战使用MicroPython进行开发。
固件编译
目前没有看到现成的固件编译,但是官方的Github仓库中给出了自行编译固件的方法,在Windows电脑上通过WSL2搭建Ubuntu 20.04系统,进行固件编译,需要注意环境的配置。
git clone https://github.com/lvgl/lv_micropython.git
cd lv_micropython
git submodule update --init --recursive lib/lv_bindings
make -C ports/rp2 BOARD=PICO submodules
make -j -C mpy-cross
make -j -C ports/rp2 BOARD=PICO USER_C_MODULES=../../lib/lv_bindings/bindings.cmake
程序移植
这一步还是很痛苦的(┬┬﹏┬┬),虽然官网的文档还是比较齐全的,但都是C语言的,官方没有给出MicroPython版本的使用文档,所以所有的函数名以及用法都是只能根据给出的C语言文档及少量示例进行推测,希望官方早点推出MicroPython版的文档。
Thonny里面的实例检查很有用,可以快速的了解一下某个模块具有的属性和方法。
主要代码片段
启动文件跳转
去年直接在main.py
中修改程序,某次bug导致无法再次烧录程序,只能将固件重新刷掉,为了避免这个问题,今年参考智能车的做法,在文件中进行跳转,可以通过按键决定跳转至哪个文件,发生意外时仍然可以选择回正常的程序并重新烧录。
import time
import os
from machine import Pin
time.sleep_ms(20)
boot_select = Pin(8, Pin.IN, pull=Pin.PULL_UP)
time.sleep_ms(20)
if boot_select.value() == 1:
try:
os.chdir("/")
with open('user.py','r') as f:
exec(f.read())
except Exception as e:
print(e)
else:
pass
# 本文件通过 Thonny 保存在板子的 Flash 中
# 可以选择是否从保存在 Flash 中的user.py 启动
温度采集
因本次学习的主要目标是LVGL的移植,故温度采集选择直接使用现成的库,避免了重复造轮子,提高了学习开发效率。在主循环中不断获取测量值,并更新到控件上。
from sht30 import SHT30
sensor = SHT30(scl_pin=21, sda_pin=20, i2c_address=0x44)
while True:
temperature, _ = sensor.measure()
bar.set_value(int(temperature*10), 0)
lbl_temp.set_text("%.2f°C"%temperature)
time.sleep(0.1)
初始化
这个部分主要是导入必要的库,并且初始化屏幕和LVGL。
from machine import Pin, SPI, ADC, PWM
import time
import sys
sys.path.append('.')
from st77xx import St7789
if 0:
# with DMA, the repaints seem to be too slow? To be investigated
# we seem to be fine performance-wise without DMA with 320x240 anyway
import rp2_dma
rp2_dma=rp2_dma.DMA(0)
else: rp2_dma=None
import lvgl as lv
lv.init()
st7789_res = 0
st7789_dc = 1
st7789_cs = 4
spi_sck=Pin(2)
spi_tx=Pin(3)
spi0=SPI(0,baudrate=24000000, phase=1, polarity=1, sck=spi_sck, mosi=spi_tx)
lcd=St7789(rot=0,res=(240,240),spi=spi0,cs=st7789_cs,dc=st7789_dc,bl=None,rst=st7789_res,rp2_dma=rp2_dma,factor=8)
scr = lv.screen_active()
...
lv.screen_load(scr)
主菜单
主菜单界面中包括三个选项,分别对应跳转到下述的三个子菜单中。
menu = lv.menu(scr)
menu.set_style_bg_color(menu.get_style_bg_color(0).darken(30), 0)
menu.set_mode_root_back_button(lv.menu.ROOT_BACK_BUTTON.ENABLED)
menu.get_main_header_back_button().add_event_cb(back_event_handler, lv.EVENT.CLICKED, None)
menu.set_size(scr.get_display().get_horizontal_resolution(), scr.get_display().get_vertical_resolution())
menu.center()
root_page = lv.menu_page(menu, "Settings")
section = lv.menu_section(root_page)
cont = lv.menu_cont(section)
lv_group_t.add_obj(cont)
cont.remove_flag(lv.menu_cont.FLAG.SCROLLABLE)
cont.add_flag(lv.menu_cont.FLAG.SCROLL_ON_FOCUS)
lbl = lv.label(cont)
lbl.set_text("666")
menu.set_load_page_event(cont, sub_mechanics_page)
cont = lv.menu_cont(section)
lv_group_t.add_obj(cont)
cont.remove_flag(lv.menu_cont.FLAG.SCROLLABLE)
cont.add_flag(lv.menu_cont.FLAG.SCROLL_ON_FOCUS)
lbl = lv.label(cont)
lbl.set_text("777")
menu.set_load_page_event(cont, sub_temp_page)
cont = lv.menu_cont(section)
lv_group_t.add_obj(cont)
cont.remove_flag(lv.menu_cont.FLAG.SCROLLABLE)
cont.add_flag(lv.menu_cont.FLAG.SCROLL_ON_FOCUS)
lbl = lv.label(cont)
lbl.set_text("888")
menu.set_load_page_event(cont, sub_pic_page)
menu.set_page(root_page)
子菜单1
子菜单1中主要包括一个slider滑动条控件和一个switch开关控件,使用menu_sect和menu_cont对内容进行划分,使用label和image对内容进行标记。
sub_mechanics_page = lv.menu_page(menu, None)
sub_mechanics_page.set_style_pad_hor(lv.menu.get_main_header(menu).get_style_pad_left(0), 0)
section = lv.menu_section(sub_mechanics_page)
# subpage cont 1
cont = lv.menu_cont(section)
image = lv.image(cont)
image.set_src(lv.SYMBOL.POWER)
label = lv.label(cont)
label.set_text("666")
label.set_long_mode(lv.label.LONG.SCROLL_CIRCULAR)
label.set_flex_grow(1)
image.add_flag(lv.menu_cont.FLAG.FLEX_IN_NEW_TRACK);
image.swap(label)
slider = lv.slider(cont)
slider.set_range(0, 100)
slider.set_value(60, 0)
slider.set_flex_grow(1)
# subpage cont 2
cont = lv.menu_cont(section)
image = lv.image(cont)
image.set_src(lv.SYMBOL.POWER)
label = lv.label(cont)
label.set_text("888")
label.set_long_mode(lv.label.LONG.SCROLL_CIRCULAR)
label.set_flex_grow(1)
switch = lv.switch(cont)
子菜单2
子菜单2中主要包括一个bar控件和一个label控件,用作温度计的显示。
sub_temp_page = lv.menu_page(menu, None)
sub_temp_page.set_style_pad_hor(lv.menu.get_main_header(menu).get_style_pad_left(0), 0)
bar = lv.bar(sub_temp_page)
bar.set_style_bg_opa(lv.OPA.COVER, 0)
bar.set_style_bg_color(lv.palette_main(lv.PALETTE.RED), 0)
bar.set_style_bg_grad_color(lv.palette_main(lv.PALETTE.BLUE), 0)
bar.set_style_bg_grad_dir(lv.GRAD_DIR.VER, 0)
bar.set_size(20, 180)
# bar.center() # 没用
# bar.set_pos(20, 20) # 没用
bar.set_range(-100, 400)
lbl_temp = lv.label(sub_temp_page)
lbl_temp.set_text("")
子菜单3
子菜单3中主要包括一个image控件,实现了显示外部自定义照片的功能。
sub_pic_page = lv.menu_page(menu, None)
sub_pic_page.set_style_pad_hor(lv.menu.get_main_header(menu).get_style_pad_left(0), 0)
section = lv.menu_section(sub_pic_page)
with open('/png_decoder_test.bin', 'rb') as f:
png_data = f.read()
print(len(png_data))
png_image_header = lv.image_header_t({
'w': 200,
'h': 150,
'cf': lv.COLOR_FORMAT.RGB565A8
})
png_image_dsc = lv.image_dsc_t({
'data_size': len(png_data),
'data': png_data,
'header': png_image_header
})
image1 = lv.image(section)
image1.set_src(png_image_dsc)
image1.center()
模拟编码器输入
使用平台的拨码开关,分别模拟编码器按下、编码值+1、编码值-1,用于交互。
l_button = Pin(7, Pin.IN, pull=Pin.PULL_UP)
ok_button = Pin(8, Pin.IN, pull=Pin.PULL_UP)
r_button = Pin(9, Pin.IN, pull=Pin.PULL_UP)
def indev_encoder_read_cb(indev_drv, data):
if ok_button.value() == 0:
data.state = lv.INDEV_STATE.PRESSED
else:
data.state = lv.INDEV_STATE.RELEASED
if l_button.value() == 0:
data.enc_diff = -1
time.sleep(0.1)
if r_button.value() == 0:
data.enc_diff = 1
time.sleep(0.1)
lv_group_encoder = lv.group_create()
lv_group_encoder.add_obj(menu)
lv_group_encoder.add_obj(slider)
lv_group_encoder.add_obj(switch)
...
encoder_indev = lv.indev_create()
encoder_indev.set_type(lv.INDEV_TYPE.ENCODER)
encoder_indev.set_group(lv_group_encoder)
encoder_indev.set_read_cb(indev_encoder_read_cb)
模拟按键输入
使用平台的按键开关,模拟屏幕触摸指定区域(10,6),用于菜单的返回。
m_button = Pin(5, Pin.IN, pull=Pin.PULL_UP)
def indev_button_read_cb(indev_drv, data):
if m_button.value() == 0:
data.btn_id = 0
data.state = lv.INDEV_STATE.PRESSED
time.sleep(0.1)
else:
data.state = lv.INDEV_STATE.RELEASED
lv_group_button = lv.group_create()
button_indev = lv.indev_create()
button_indev.set_type(lv.INDEV_TYPE.BUTTON)
point_t = lv.point_t({'x':10, 'y':6})
button_indev.set_button_points([point_t])
button_indev.set_group(lv_group_button)
button_indev.set_read_cb(indev_button_read_cb)
实现功能
本项目还算成功地将LVGL移植到这个基于RP2040的带有240x240分辨率LCD,两个按键以及一个拨轮开关的调试终端上,制作了一款图形化控制终端。实现
经验总结
根据我所移植的这些模块,我也总结出了从C语言版移植到MicroPython的一些规律经验:
- C语言中没有对象一说,都是结构体,对应进行操作时都是使用某个函数,传参为该模块的结构体和设定的属性,而MicroPython中每一个模块都是一个对象,进行对应操作时应该是使用对象里面的方法,传参为设定的属性。
例如,C语言程序为
lv_obj_t * btn1 = lv_button_create(lv_screen_active());
lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -40);
对应移植的MicroPython程序为
btn=lv.button(lv.screen_active())
btn.align(lv.ALIGN.BOTTOM_MID, 0, -40)
- C语言中定义的常量命名为
LV_模块_属性_值
,而对应到MicroPython中则为LV.模块.属性.值
。
例如,C语言程序为
lv_obj_remove_flag(btn1, LV_OBJ_FLAG_PRESS_LOCK);
对应移植的MicroPython程序为
btn1.remove_flag(lv.obj.FLAG.PRESS_LOCK)
遇到主要难题及解决方法
- 固件编译时需要先在系统中安装交叉编译工具链Arm GNU Toolchain,下载pico-sdk。
- 固件编译好后,还需要在文件系统的根目录中添加
lv_utils.py
、st77xx.py
文件(lv_binding_micropython仓库中有),我一直以为是固件编译好后自带这两个文件,但后面查找后感觉应该是需要额外手动添加的。 - 菜单的返回一开始打算使用按键模拟输入keypad键值,后面发现菜单无法接受keypad传参,也无法使用encoder实现返回,只能通过按键模拟点击,把位置绑定在返回按钮那里,进而实现返回功能。
- 要输出带透明通道的png图像应使用官方的转换工具)将其转换为RGB565A8的格式,而且在程序中,应该在读入后手动为其新建
lv.image_header_t
,包括图像的宽和高以及格式,在lv.image_dsc_t
中添加图片大小、类型,以及刚刚新建的lv.image_header_t
,示例见上代码。
存在问题
- 目前将平台连接到Thonny之后,可以正常的运行程序,但第2次运行程序的时候就就会卡住,只能将平台断开重连,不知道是不是固件编译的原因,还是其他原因,暂时不明确。
- 目前程序使用过程中可能会报内存错误
MemoryError: memory allocation failed, allocating 6723 bytes
,感觉可能是显示的内容比较多了,里面有一张图片,如果把图片去掉的话就基本上是可以稳定运行,不会报错,后期可以考虑对图片进行再次压缩。 - 在菜单的menu_page中,控件无法实现定位,尝试了bar和image控件均不行。
未来计划
- 目前的移植还只是一个初步尝试,还有许多控件没有进行学习,后面将进一步进行学习,并将学习所得进行分享。
- 希望可以实现主图滑动,点击进入子页面的效果,但是好像没有找到现成的控件,一个示例是使用多个页面进行跳转,后续再进行尝试。
参考资料
- Welcome to the documentation of LVGL! — LVGL documentation
- lvgl/lv_micropython: Micropython bindings to LVGL for Embedded devices, Unix and JavaScript
- lvgl/lv_binding_micropython: LVGL binding for MicroPython
- 【正点原子】手把手教你学LVGL图形界面编程
- LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。_lvgl indev-CSDN博客