LVGL 的绑定
此仓库是 lv_micropython 的子模块。 请 fork lv_micropython 以快速开始使用 LVGL MicroPython 绑定。
另请参阅 Micropython + LittlevGL 博客文章。(LittlevGL 是 LVGL 的前身。) 有关高级功能,请参阅 纯 MicroPython 显示驱动程序 博客文章。 如有问题和讨论,请使用论坛:https://forum.lvgl.io/c/micropython
MicroPython
LVGL 的 MicroPython 绑定提供了一个自动生成的 MicroPython 模块,其中包含允许用户访问大部分 LVGL 库的类和函数。
该模块由脚本 gen_mpy.py
自动生成。
此脚本读取、预处理并解析 LVGL 头文件,然后生成一个 C 文件 lv_mpy.c
,该文件定义了从 MicroPython 访问 LVGL 的 MicroPython 模块(API)。
Micopython 的构建脚本(Makefile 或 CMake)应自动运行 gen_mpy.py
以生成和编译 lv_mpy.c
。
- 如果您想查看生成的
lv_mpy.c
的示例,请查看lv_mpy_example.c
。注意,它唯一导出(非静态)的符号是mp_module_lvgl
,应在 MicroPython 中将其注册为一个模块。 - lv_binding_micropython 通常作为 lv_micropython 的 git 子模块使用,后者构建 MicroPython + LVGL + lvgl-bindings,但也可用于 MicroPython 的其他分支。
值得注意的是,Micropython 绑定模块(lv_mpy.c
)依赖于 LVGL 配置。
LVGL 通过 lv_conf.h
进行配置,可以启用或禁用不同的对象和功能。LVGL 绑定仅为启用的对象和功能生成。更改 lv_conf.h
需要重新运行 gen_mpy.py
,因此最好在构建脚本中自动运行它,就像 lv_micropython 所做的那样。
内存管理
当 LVGL 作为 MicroPython 库构建时,它被配置为使用 MicroPython 内存分配函数分配内存,并利用 MicroPython 的垃圾回收("gc")。 这意味着为 LVGL 使用分配的结构不需要显式释放,gc 会处理这个问题。 为了正确工作,LVGL 被配置为使用 gc 和 MicroPython 的内存分配函数,并将所有 LVGL "根"全局变量注册到 MicroPython 的 gc 中。
从用户的角度来看,可以创建结构,当不再引用它们时,gc 会自动收集它们。
然而,LVGL 屏幕对象(没有父对象的 lv.obj
)会自动分配给默认显示,因此即使不再显式引用,也不会被 gc 收集。
当您想释放一个屏幕及其所有后代以便 gc 可以收集它们的内存时,请确保在不再需要它时调用 screen.delete()
。
请确保保留对显示驱动程序和输入驱动程序的引用,以防止它们被收集。
并发性
LVGL 的这个 MicroPython 绑定实现假设 MicroPython 和 LVGL 在单个线程上同一线程上运行(或者在没有多线程的情况下运行)。 没有使用同步手段(锁、互斥锁)。 然而,对 LVGL 的异步调用仍然会定期进行,用于屏幕刷新和其他 LVGL 任务,如动画。
这是通过使用内部 MicroPython 调度器(必须启用)来实现的,通过调用 mp_sched_schedule
。
当屏幕需要刷新时,会调用 mp_sched_schedule
。LVGL 期望函数 lv_task_handler
被定期调用(参见 lvgl/README.md#porting)。这通常在显示设备驱动程序中处理。
这里是一个示例,通过 mp_sched_schedule
调用 lv_task_handler
来刷新 LVGL。mp_lv_task_handler
被安排在与 MicroPython 运行的相同线程上运行,它同时调用 lv_task_handler
处理 LVGL 任务,以及 monitor_sdl_refr_core
刷新显示和处理鼠标事件。
在使用 REPL(交互式控制台)时,在等待用户输入时也可能发生异步事件。在这个示例中,我们在等待按键时只是定期调用 mp_handle_pending
。mp_handle_pending
负责调度通过 mp_sched_schedule
注册的异步事件。
结构类和全局变量
LVGL绑定脚本解析LVGL头文件并提供API来访问LVGL的类(如btn
)和结构体(如color_t
)。所有结构体和类都可以在lvgl micropython模块下使用。
lvgl类包含:
- 函数(如
set_x
) - 与该类相关的枚举(如
btn
的STATE
)
lvgl结构体只包含可读写的属性。例如:
c = lvgl.color_t()
c.ch.red = 0xff
结构体也可以通过字典初始化。例如,上面的示例可以这样写:
c = lvgl.color_t({'ch': {'red' : 0xff}})
所有lvgl全局变量(函数、枚举、类型)都可以在lvgl模块下使用。例如,lvgl.SYMBOL
是符号字符串的"枚举",lvgl.anim_create
将创建动画等。
回调函数
在C中,回调是一个函数指针。 在MicroPython中,我们还需要为每个回调注册一个MicroPython可调用对象。 因此,在MicroPython绑定中,我们需要为每个回调同时注册一个函数指针和一个MicroPython对象。
因此,我们定义了一个回调约定,期望lvgl头文件以特定方式定义。按照约定声明的回调将允许绑定在注册回调时在函数指针旁边注册一个MicroPython对象,并在调用回调时访问该对象。
当注册或调用回调时,MicroPython可调用对象会自动保存在提供的user_data
变量中。
回调约定假设以下情况:
- 有一个结构体包含一个名为
void * user_data
的字段。 - 该结构体的指针作为回调注册函数的第一个参数提供。
- 该结构体的指针作为回调本身的第一个参数提供。
另一种选择是回调函数指针只是结构体的一个字段,在这种情况下,我们期望同一个结构体也包含user_data
字段。
还有一种选择是:
- 一个名为
void * user_data
的参数作为最后一个参数提供给注册函数。 - 回调本身接收
void *
作为最后一个参数
在这种情况下,用户应该提供None
或一个字典作为注册函数的user_data
参数。
回调将在最后一个参数中接收一个可以转换为字典的Blob。
(参见下面的async_call
示例)
只要遵循上述约定,lvgl MicroPython绑定脚本就会在设置和使用回调时自动设置和使用user_data
。
从用户的角度来看,任何Python可调用对象(如Python常规函数、类函数、lambda等)都可以用作lvgl回调。例如:
lvgl.anim_set_custom_exec_cb(anim, lambda anim, val, obj=obj: obj.set_y(val))
在这个例子中,为动画anim
注册了一个执行回调,它将动画obj
的y坐标。
lvgl API函数也可以直接用作回调,所以上面的例子也可以这样写:
lv.anim_set_exec_cb(anim, obj, obj.set_y)
不遵循回调约定的lvgl回调不能与micropython可调用对象一起使用。关于调整lvgl回调以符合约定的讨论:https://github.com/lvgl/lvgl/issues/1036
用户不能直接使用 user_data
字段,因为它在内部用于保存指向MicroPython对象的指针。
显示和输入驱动
LVGL可以配置使用不同的显示和不同的输入设备。更多信息可在LVGL文档中找到。
注册驱动本质上是调用注册函数(例如disp_drv_register
)并传递一个函数指针作为参数(实际上是一个包含函数指针的结构体)。函数指针用于访问实际的显示/输入设备。
在使用MicroPython实现LVGL显示或输入驱动时,有3种选择:
- 实现纯Python驱动。这是实现驱动最简单的方式,但性能可能较差。
- 实现纯C驱动。
- 实现混合驱动,其中关键部分(如
flush
函数)用C实现,非关键部分(如初始化显示)用Python实现。
纯/混合驱动的一个例子是ili9XXX.py。
驱动注册最终应该在MicroPython脚本中执行,对于纯/混合驱动可以在驱动代码本身中执行,对于C驱动可以在用户代码中执行(例如,在SDL驱动的情况下)。在Python而不是C中注册驱动很重要,这样可以让用户在不构建项目和更改C文件的情况下轻松选择和替换驱动。
在创建显示或输入LVGL驱动时,确保让用户可以在运行时配置所有参数,如SPI引脚、频率等。 最终,用户希望只构建一次固件,并在不重新构建C项目的情况下使用同一驱动的不同配置。 这与标准LVGL C驱动不同,通常使用宏来配置参数,并要求用户在任何配置更改时重新构建。
示例:
# 初始化ILI9341显示
from ili9XXX import ili9341
self.disp = ili9341(dc=32, cs=33, power=-1, backlight=-1)
# 注册xpt2046触摸驱动
from xpt2046 import xpt2046
self.touch = xpt2046()
示例:
# 初始化
import lvgl as lv
lv.init()
from lv_utils import event_loop
WIDTH = 480
HEIGHT = 320
event_loop = event_loop()
disp_drv = lv.sdl_window_create(WIDTH, HEIGHT)
mouse = lv.sdl_mouse_create()
keyboard = lv.sdl_keyboard_create()
keyboard.set_group(self.group)
在这个例子中,我们使用LVGL内置的LVGL驱动。
目前Micropyton支持的驱动有
- LVGL内置驱动程序使用Unix/Linux SDL(显示、鼠标、键盘)和帧缓冲(
/dev/fb0
) - ESP32的ILI9341驱动
- ESP32的XPT2046驱动
- ESP32的FT6X36(电容触摸IC)驱动
- ESP32的原始电阻触摸驱动(ADC直接连接到屏幕,无触摸IC)
驱动代码位于/driver
目录下。
也可以通过提供回调函数(disp_drv.flush_cb
、indev_drv.read_cb
等)用纯MicroPython实现驱动。
目前支持的ILI9341、FT6X36和XPT2046都是纯MicroPython驱动。
驱动程序在哪里?
LVGL C驱动和MicroPython驱动(C或Python)是相互独立的。 主要原因是配置方式不同:
- C驱动通常使用C宏进行配置(使用哪些引脚、频率等) 任何配置更改都需要重新构建固件,这是可以理解的,因为应用程序的任何更改都需要重新构建固件。
- 在MicroPython中,驱动程序要么随MicroPython固件一起构建(如果是C驱动),要么根本不构建(如果是纯Python驱动)。在运行时用户初始化驱动并进行配置。如果用户切换SPI引脚或其他配置,无需重新构建固件,只需更改Python脚本并在运行时重新初始化驱动即可。
因此MicroPython驱动的位置是https://github.com/lvgl/lv_binding_micropython/tree/master/driver,与https://github.com/lvgl/lv_drivers无关。
事件循环
LVGL需要一个事件循环来重绘屏幕、处理用户输入等。 默认事件循环在lv_utils.py中实现,使用MicroPython定时器来调度LVGL调用。 如果需要,它还支持在uasyncio中运行事件循环。 如果事件循环尚未运行,某些驱动程序会自动启动它。要为这些驱动程序配置事件循环,只需在注册驱动程序之前初始化事件循环。 LVGL原生驱动(如SDL驱动)不会启动事件循环。你必须显式启动事件循环,否则屏幕不会刷新。
事件循环可以这样启动:
from lv_utils import event_loop
event_loop = event_loop()
你可以通过提供参数来配置它,详见lv_utils.py。
向项目添加MicroPython绑定
"MicroPython + lvgl + 绑定"的示例项目是lv_mpy
。
以下是向现有MicroPython项目添加lvgl的步骤。(这些示例来自lv_mpy
):
- 在
lib
下添加lv_bindings
作为子模块。 - 在
lib
中添加lv_conf.h
。 - 编辑Makefile以自动运行
gen_mpy.py
并构建其产物。示例。 - 将lvgl模块和显示/输入驱动程序注册为MicroPython的内置模块。示例。
- 将lvgl根添加到gc根。示例。
通过设置几个lv_conf.h已移至lv_binding_micropython git模块。LV_MEM_CUSTOM_*
和LV_GC_*
宏配置lvgl使用垃圾回收示例- 确保在
partitions.csv
中正确配置分区,并为LVGL模块留出足够空间。 - 我忘记了什么吗?请告诉我。
gen_mpy.py语法
用法: gen_mpy.py [-h] [-I <包含路径>] [-D <宏名称>]
[-E <预处理文件>] [-M <模块名称字符串>]
[-MP <前缀字符串>] [-MD <元数据文件名>]
输入 [输入 ...]
位置参数:
输入
可选参数:
-h, --help 显示此帮助消息并退出
-I <包含路径>, --include <包含路径>
预处理器包含路径
-D <宏名称>, --define <宏名称>
定义预处理器宏
-E <预处理文件>, --external-preprocessing <预处理文件>
防止预处理。假设输入文件已预处理
-M <模块名称字符串>, --module_name <模块名称字符串>
模块名称
-MP <前缀字符串>, --module_prefix <前缀字符串>
模块前缀,每个函数名都以此开头
-MD <元数据文件名>, --metadata <元数据文件名>
可选文件,用于发出元数据(内省)
示例:
python gen_mpy.py -MD lv_mpy_example.json -M lvgl -MP lv -I../../berkeley-db-1.xx/PORT/include -I../../lv_binding_micropython -I. -I../.. -Ibuild -I../../mp-readline -I ../../lv_binding_micropython/pycparser/utils/fake_libc_include ../../lv_binding_micropython/lvgl/lvgl.h
绑定其他C库
lvgl绑定脚本可用于将其他C库绑定到MicroPython。 我曾将它用于lodepng和ESP-IDF的部分内容。 更多详情请阅读这篇博客文章。
MicroPython绑定使用方法
一个简单的例子:advanced_demo.py
。
更多示例可以在/examples
文件夹下找到。
导入和初始化LVGL
import lvgl as lv
lv.init()
注册显示和输入驱动
from lv_utils import event_loop
WIDTH = 480
HEIGHT = 320
event_loop = event_loop()
disp_drv = lv.sdl_window_create(WIDTH, HEIGHT)
mouse = lv.sdl_mouse_create()
keyboard = lv.sdl_keyboard_create()
keyboard.set_group(self.group)
在这个例子中,LVGL原生SDL显示和输入驱动在MicroPython的unix端口上注册。
这里是另一个针对ESP32 ILI9341 + XPT2046驱动的示例:
import lvgl as lv
# 导入ILI9341驱动并初始化
from ili9XXX import ili9341
disp = ili9341()
# 导入XPT2046驱动并初始化
from xpt2046 import xpt2046
touch = xpt2046()
默认情况下,ILI9341和XPT2046都在同一个SPI总线上初始化,使用以下参数:
- ILI9341:
miso=5, mosi=18, clk=19, cs=13, dc=12, rst=4, power=14, backlight=15, spihost=esp.HSPI_HOST, mhz=40, factor=4, hybrid=True
- XPT2046:
cs=25, spihost=esp.HSPI_HOST, mhz=5, max_cmds=16, cal_x0 = 3783, cal_y0 = 3948, cal_x1 = 242, cal_y1 = 423, transpose = True, samples = 3
你可以在ili9341/xpt2046构造函数中更改这些参数。 如果需要,你也可以通过提供miso/mosi/clk参数将它们初始化在不同的SPI总线上。将它们设置为-1以使用现有(已初始化)的spihost总线。
这里是另一个例子,这次导入并初始化M5Stack Core2设备的显示和触摸驱动。该设备使用I2C总线上的FT6336芯片读取电容触摸屏,并使用ili9342显示控制器,与ili9341相比有一些信号是反转的:
from ili9XXX import ili9341
disp = ili9341(mosi=23, miso=38, clk=18, dc=15, cs=5, invert=True, rot=0x10)
from ft6x36 import ft6x36
touch = ft6x36(sda=21, scl=22, width=320, height=280)
驱动init
参数
通过为驱动的init方法提供width
、height
、start_x
、start_y
、colormode
、invert
和rot
参数,可以支持多种不同的显示模块。
显示尺寸
width
和height
参数应设置为显示器在使用方向上的宽度和高度。显示器可能有一个比可见显示区域更大的内部帧缓冲区。start_x
和start_y
参数用于指示相对于内部帧缓冲区开始的位置,可见像素从哪里开始。
颜色处理
colormode
和invert
参数控制显示器如何处理颜色。
显示方向
rot
参数用于设置显示器的MADCTL寄存器。MADCTL寄存器控制像素写入帧缓冲区的顺序。这设置了显示器的方向或旋转。
有关MADCTL寄存器以及如何确定显示器的colormode
和rot
参数的更多信息,请参阅examples/madctl目录中的README.md文件。
st7789驱动类
默认情况下,st7789驱动使用以下与TTGO T-Display兼容的参数初始化:
st7789(
miso=-1, mosi=19, clk=18, cs=5, dc=16, rst=23, power=-1, backlight=4,
backlight_on=1, power_on=0, spihost=esp.HSPI_HOST, mhz=40, factor=4, hybrid=True,
width=320, height=240, start_x=0, start_y=0, colormode=COLOR_MODE_BGR, rot=PORTRAIT,
invert=True, double_buffer=True, half_duplex=True, asynchronous=False, initialize=True)
参数 | 描述 |
---|---|
miso | 显示器SPI数据输入引脚,如果不使用则设为-1,因为许多st7789显示器没有这个引脚 |
mosi | 显示器SPI数据输出引脚(必需) |
clk | SPI时钟引脚(必需) |
cs | 显示器片选引脚 |
dc | 显示器数据/命令选择引脚(必需) |
rst | 显示器复位引脚 |
power | 显示器电源开启引脚,如果不使用则设为-1 |
power_on | 电源开启引脚值 |
backlight | 显示器背光控制引脚 |
backlight_on | 背光开启引脚值 |
spihost | ESP SPI端口 |
mhz | SPI波特率(MHz) |
factor | 帧缓冲区缩小因子 |
hybrid | 布尔值,True使用C刷新例程,False使用纯Python驱动 |
width | 显示器宽度 |
height | 显示器高度 |
colormode | 显示器颜色模式 |
rot | 显示方向,可选PORTRAIT、LANDSCAPE、INVERSE_PORTRAIT、INVERSE_LANDSCAPE或与颜色模式进行OR运算的原始MADCTL值 |
invert | 显示器颜色反转设置 |
double_buffer | 布尔值,True使用双缓冲,False使用单缓冲(节省内存) |
half_duplex | 布尔值,True使用半双工SPI通信 |
asynchronous | 布尔值,True使用异步例程 |
initialize | 布尔值,True初始化显示器 |
TTGO T-Display st7789配置示例
import lvgl as lv
from ili9XXX import st7789
disp = st7789(width=135, height=240, rot=st7789.LANDSCAPE)
TTGO TWatch-2020 st7789配置示例
import lvgl as lv
from ili9XXX import st7789
import axp202c
# 初始化电源管理器,设置背光
axp = axp202c.PMU()
axp.enablePower(axp202c.AXP202_LDO2)
axp.setLDO2Voltage(2800)
# 初始化显示器
disp = st7789(
mosi=19, clk=18, cs=5, dc=27, rst=-1, backlight=12, power=-1,
width=240, height=240, rot=st7789.INVERSE_PORTRAIT, factor=4)
st7735驱动类
默认情况下,st7735驱动使用以下参数初始化。参数描述与st7789相同。
st7735(
miso=-1, mosi=19, clk=18, cs=13, dc=12, rst=4, power=-1, backlight=15, backlight_on=1, power_on=0,
spihost=esp.HSPI_HOST, mhz=40, factor=4, hybrid=True, width=128, height=160, start_x=0, start_y=0,
colormode=COLOR_MODE_RGB, rot=PORTRAIT, invert=False, double_buffer=True, half_duplex=True,
asynchronous=False, initialize=True):
ST7735 128x128配置示例
from ili9XXX import st7735, MADCTL_MX, MADCTL_MY
disp = st7735(
mhz=3, mosi=18, clk=19, cs=13, dc=12, rst=4, power=-1, backlight=15, backlight_on=1,
width=128, height=128, start_x=2, start_y=1, rot=PORTRAIT)
ST7735 128x160配置示例
from ili9XXX import st7735, COLOR_MODE_RGB, MADCTL_MX, MADCTL_MY
disp = st7735(
mhz=3, mosi=18, clk=19, cs=13, dc=12, rst=4, backlight=15, backlight_on=1,
width=128, height=160, rot=PORTRAIT)
创建带有按钮和标签的屏幕
scr = lv.obj()
btn = lv.button(scr)
btn.align(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("按钮")
# 加载屏幕
lv.scr_load(scr)
创建结构体实例
symbolstyle = lv.style_t(lv.style_plain)
symbolstyle将是一个lv_style_t
实例,初始化为与lv_style_plain
相同的值
设置结构体中的字段
symbolstyle.text.color = lv.color_hex(0xffffff)
symbolstyle.text.color将被初始化为lv_color_hex
返回的颜色结构体
使用字典设置嵌套结构体
symbolstyle.text.color = {"red":0xff, "green":0xff, "blue":0xff}
创建对象实例
self.tabview = lv.tabview(lv.scr_act())
对象构造函数的第一个参数是父对象,第二个是从哪个元素复制此元素。 这两个参数都是可选的。
调用对象方法
self.symbol.align(self, lv.ALIGN.CENTER,0,0)
在此示例中,lv.ALIGN
是一个枚举,lv.ALIGN.CENTER
是一个枚举成员(整数值)。
使用回调
for btn, name in [(self.btn1, '播放'), (self.btn2, '暂停')]:
btn.set_event_cb(lambda obj=None, event=-1, name=name: self.label.set_text('%s %s' % (name, get_member_name(lv.EVENT, event))))
使用带有user_data
参数的回调:
def cb(user_data):
print(user_data.cast()['value'])
lv.async_call(cb, {'value':42})
列出可用的函数/成员/常量等
print('\n'.join(dir(lvgl)))
print('\n'.join(dir(lvgl.btn)))
...