2024年寒假练 - 基于RP2040带屏12指神探及MicroPython的LVGL图形化控制终端(温度计应用)
该项目使用了带显示屏的、基于RP2040的多功能嵌入式编程学习、硬件调试平台,实现了LVGL图形化控制终端(温度计)的设计,它的主要功能为:多级菜单,温度计显示,图片显示,及部分控件demo。
标签
lvgl
RP2040
136ytr
更新2024-04-02
汕头大学
84

项目介绍

LVGL是一种轻量级的图形化界面,结合RP2040微控制器可以方便地实现各种图形化界面。本项目将LVGL移植到这个带有240x240分辨率LCD,两个按键以及一个拨轮开关的调试终端上,制作了一款图形化控制终端。

硬件介绍

本项目使用的是带屏版的12指神探,它是在原板基础上,配备了一块240*240分辨率的LCD彩屏以及两个可程控按键和一个拨轮,丰富了人机交互功能,方便信息观察、界面切换。

img

本项目主要使用了如下硬件:

  • 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里面的实例检查很有用,可以快速的了解一下某个模块具有的属性和方法。

image-20240319063240811

主要代码片段

启动文件跳转

去年直接在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)

主菜单

主菜单界面中包括三个选项,分别对应跳转到下述的三个子菜单中。

IMG_20240318_113009

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对内容进行标记。IMG_20240318_111655

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

IMG_20240318_111928

子菜单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

IMG_20240318_112006

子菜单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,两个按键以及一个拨轮开关的调试终端上,制作了一款图形化控制终端。实现

IMG_20240317_225550

经验总结

根据我所移植的这些模块,我也总结出了从C语言版移植到MicroPython的一些规律经验:

  1. 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)
  1. C语言中定义的常量命名为LV_模块_属性_值,而对应到MicroPython中则为LV.模块.属性.值

例如,C语言程序为

lv_obj_remove_flag(btn1, LV_OBJ_FLAG_PRESS_LOCK);

对应移植的MicroPython程序为

btn1.remove_flag(lv.obj.FLAG.PRESS_LOCK)

遇到主要难题及解决方法

  1. 固件编译时需要先在系统中安装交叉编译工具链Arm GNU Toolchain,下载pico-sdk。
  2. 固件编译好后,还需要在文件系统的根目录中添加lv_utils.pyst77xx.py文件(lv_binding_micropython仓库中有),我一直以为是固件编译好后自带这两个文件,但后面查找后感觉应该是需要额外手动添加的。
  3. 菜单的返回一开始打算使用按键模拟输入keypad键值,后面发现菜单无法接受keypad传参,也无法使用encoder实现返回,只能通过按键模拟点击,把位置绑定在返回按钮那里,进而实现返回功能。
  4. 要输出带透明通道的png图像应使用官方的转换工具)将其转换为RGB565A8的格式,而且在程序中,应该在读入后手动为其新建lv.image_header_t,包括图像的宽和高以及格式,在lv.image_dsc_t中添加图片大小、类型,以及刚刚新建的lv.image_header_t,示例见上代码。

存在问题

  1. 目前将平台连接到Thonny之后,可以正常的运行程序,但第2次运行程序的时候就就会卡住,只能将平台断开重连,不知道是不是固件编译的原因,还是其他原因,暂时不明确。
  2. 目前程序使用过程中可能会报内存错误MemoryError: memory allocation failed, allocating 6723 bytes,感觉可能是显示的内容比较多了,里面有一张图片,如果把图片去掉的话就基本上是可以稳定运行,不会报错,后期可以考虑对图片进行再次压缩。
  3. 在菜单的menu_page中,控件无法实现定位,尝试了bar和image控件均不行。

未来计划

  1. 目前的移植还只是一个初步尝试,还有许多控件没有进行学习,后面将进一步进行学习,并将学习所得进行分享。
  2. 希望可以实现主图滑动,点击进入子页面的效果,但是好像没有找到现成的控件,一个示例是使用多个页面进行跳转,后续再进行尝试。

参考资料

  1. Welcome to the documentation of LVGL! — LVGL documentation
  2. lvgl/lv_micropython: Micropython bindings to LVGL for Embedded devices, Unix and JavaScript
  3. lvgl/lv_binding_micropython: LVGL binding for MicroPython
  4. 【正点原子】手把手教你学LVGL图形界面编程
  5. LittleVGL (LVGL)干货入门教程二之LVGL的输入设备(indev)API对接。_lvgl indev-CSDN博客


附件下载
firmware.zip
编译好带LVGL的固件
MyCode_lvgl.zip
源代码
团队介绍
汕头大学 姚罗然
团队成员
136ytr
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号