在 Arduino 环境下为 掌控板3.0 移植 LVGL

在 Arduino 环境下为 掌控板3.0 移植 LVGL

W-Can1425 Lv3

说在前面

前一篇文章我向各位介绍了如何使用 PlatformIO 为 掌控板3.0 搭建 Arduino 开发环境,今天我们不妨更进一步,在这个环境上简单的移植一下 LVGL。

在很多文章和相关内容中我已经多次介绍了 LVGL,这是一个免费且开源的图形库,提供了创建嵌入式 GUI 所需的一切,包括易于使用的图形元素、美丽的视觉效果和低内存占用。

在掌控板3.0上,产品方使用了更高性能的 MCU,同时还有一块 320x172 的 TFT 彩屏,在官方的 MicroPython 环境中也默认打包了 LVGL 的 MicroPython 绑定,所以在我们的 Arduino 环境下使用 LVGL 是有必要的。

安装相关依赖库

首先你应该要新建一个 PIO 项目,如果你没有新建或者说你不知道我在说什么,建议你先去前一篇文章进行阅读与操作。

本次移植,我采用的方案是使用 TFT_eSPI 用作 LVGL 的支持驱动,首先进入 PIO Home,点击 PIO Home 侧边栏的 Libraries,搜索 TFT_eSPI,点击 Add to project,再在弹出的窗口中按提示选择将被添加进的项目,最后点击 Add 即可。

TFT_eSPI
弹出窗口

安装 LVGL 同理。

配置及测试 TFT_eSPI

TFT_eSPI 是一个著名的 Arduino TFT 驱动库,我们首先要正确配置它并使其可用,为下一步使用 LVGL 做好基础。

我们的目标是在掌控板屏幕 (10, 10) 的地方正确显示一行蓝色文字“Hello World”,屏幕使用白色填充,实机效果如图:

实机演示

我使用 User Setup 文件来进行配置。打开项目根目录下的 \.pio\libdeps\mpython_esp32s3_r8n16\TFT_eSPI\User_Setup.h,按照以下截图进行修改。

左侧为原始内容,右侧为修改后内容。

注释掉开启的 ILI9341 驱动,开启 ST7789 驱动

code line 45
code line 55

配置屏幕宽高(事实上仅需要使 TFT_WIDTH 为 172 即可)

code line 86

注释默认引脚配置,手动配置相关引脚,同时关闭颜色反转

code line 117
code line 170
code line 212

一些杂项配置

code line 363

需要注意的是,引脚部分我参考了官方 MicroPython 环境源码中的相关配置文件。

以及最后一个更改(line 377),我并不清楚 HSPI 是什么,但是不开启该特性将导致 Guru Meditation Error: Core 1 panic'ed (StoreProhibited). Exception was unhandled. 问题,开启了又会有 [ 989][E][esp32-hal-spi.c:215] spiAttachMISO(): HSPI Does not have default pins on ESP32S3! 报错,但此时可以正常点亮屏幕,我查询相关资料无果,希望读者能给出解答。

保存,进入 /scr/main.cpp 写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Arduino.h>
#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();

void setup() {
Serial.begin(115200);
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_WHITE);
tft.setTextColor(TFT_BLUE);
tft.setTextFont(4);
tft.drawString("Hello World", 10, 10);
}

void loop() {}

此时再编译并烧录,你将会看到如本章节开头的效果。

但是,大概率事实不是如此 /_ \

我有一个朋友之前在跟掌控板3.0写 framebuf 驱动的时候就遇到一个很匪夷所思的问题,就是屏幕方向的问题。具体问题我们不在这里叙述了,我们只看看他最后解决问题后探索出的一点事实:

屏幕方向示意图

这张图展示的是硬件上的方向,至于为什么使用官方 MicroPython 环境没问题,因为在软件上解决了。

在解决问题之前,不妨先看看当前的效果:

效果

其实主要的问题是显示的内容左右镜像了,那我们才说的方向问题哪去了?其实 tft.setRotation(1); 这一行代码已经解决了方向问题,我们注释掉这一行重新编译烧录再看看效果:

效果

可以看到内容的显示方式与上文的屏幕方向示意图基本一致。

针对屏幕显示镜像的问题,我想到的两种解决方案如下:

  • 在配置文件中寻找是否有显示镜像相关的配置,如有,进行配置
  • setup() 函数内直接调用 TFT_eSPI 实例的 writecommand 方法,向屏幕发送 0x80 命令(在 TFT_eSPI 里它有个上层封装,即 TFT_MAD_MY
  • 直接修改 TFT_eSPIsetRotation(uint8_t r) 方法(修改内容就是加一行向屏幕发送命令,与上一种方法大同小异)

显然,(至少是我)会优先选择更简单,更具有可移植性,具有更小侵入性的方案一,但是我足足找了半个小时属实是没找到这样一行。没办法,只好选方案二或者方案三了。

后两者其实大同小异,本质都是向屏幕发送特定的命令使显示内容镜像。但是前者有个不太优雅的问题,阅读 setRotation(uint8_t r) 方法的实现可知,其的每一次操作均会覆盖原有命令,这意味着生产环境中我们1每使用一次该方法就要再写一行代码重新发送镜像命令,这哪里能忍,可复用性实在是太低了 (゚Д゚*)ノ

所以我最后还是在 更低侵入性 和 更高可复用性 之间选择了后者。

说回正题,setRotation(uint8_t r) 方法的实现在 /.pio/libdeps/mpython_esp32s3_r8n16/TFT_eSPI/TFT_Drivers/ST7789_Rotation.h

哦我还要提一嘴,setRotation(uint8_t r) 方法其实是个二次封装,各位一看实现就知道了,不用多说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/***************************************************************************************
** Function name: setRotation
** Description: rotate the screen orientation m = 0-3 or 4-7 for BMP drawing
***************************************************************************************/
void TFT_eSPI::setRotation(uint8_t m)
{

begin_tft_write();

// This loads the driver specific rotation code <<<<<<<<<<<<<<<<<<<<< ADD NEW DRIVERS TO THE LIST HERE <<<<<<<<<<<<<<<<<<<<<<<
#if defined (ILI9341_DRIVER) || defined(ILI9341_2_DRIVER) || defined (ILI9342_DRIVER)
#include "TFT_Drivers/ILI9341_Rotation.h"

#elif defined (ST7735_DRIVER)
#include "TFT_Drivers/ST7735_Rotation.h"

#elif defined (ILI9163_DRIVER)
#include "TFT_Drivers/ILI9163_Rotation.h"

#elif defined (S6D02A1_DRIVER)
#include "TFT_Drivers/S6D02A1_Rotation.h"

#elif defined (ST7796_DRIVER)
#include "TFT_Drivers/ST7796_Rotation.h"

#elif defined (ILI9486_DRIVER)
#include "TFT_Drivers/ILI9486_Rotation.h"

#elif defined (ILI9481_DRIVER)
#include "TFT_Drivers/ILI9481_Rotation.h"

#elif defined (ILI9488_DRIVER)
#include "TFT_Drivers/ILI9488_Rotation.h"

#elif defined (HX8357D_DRIVER)
#include "TFT_Drivers/HX8357D_Rotation.h"

#elif defined (ST7789_DRIVER)
#include "TFT_Drivers/ST7789_Rotation.h"

... // 太长了浪费篇幅,后面我就删了 ¯\_(ツ)_/¯

#endif

delayMicroseconds(10);

end_tft_write();

addr_row = 0xFFFF;
addr_col = 0xFFFF;

// Reset the viewport to the whole screen
resetViewport();
}

好的,那我们怎么修改 ST7789 对应的内容呢,看一下我的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
  // This is the command sequence that rotates the ST7789 driver coordinate frame

writecommand(TFT_MADCTL);
rotation = m % 4;
switch (rotation) {
case 0: // Portrait
#ifdef CGRAM_OFFSET
if (_init_width == 135)
{
colstart = 52;
rowstart = 40;
}
else if(_init_height == 280)
{
colstart = 0;
rowstart = 20;
}
else if(_init_width == 172)
{
colstart = 34;
rowstart = 0;
}
else if(_init_width == 170)
{
colstart = 35;
rowstart = 0;
}
else
{
colstart = 0;
rowstart = 0;
}
#endif
writedata(TFT_MAD_MX | TFT_MAD_COLOR_ORDER);

_width = _init_width;
_height = _init_height;
break;

case 1: // Landscape (Portrait + 90)
#ifdef CGRAM_OFFSET
if (_init_width == 135)
{
colstart = 40;
rowstart = 53;
}
else if(_init_height == 280)
{
colstart = 20;
rowstart = 0;
}
else if(_init_width == 172)
{
colstart = 0;
rowstart = 34;
}
else if(_init_width == 170)
{
colstart = 0;
rowstart = 35;
}
else
{
colstart = 0;
rowstart = 0;
}
#endif
writedata(TFT_MAD_MY | TFT_MAD_MX | TFT_MAD_MV | TFT_MAD_COLOR_ORDER);

_width = _init_height;
_height = _init_width;
break;

case 2: // Inverter portrait
#ifdef CGRAM_OFFSET
if (_init_width == 135)
{
colstart = 53;
rowstart = 40;
}
else if(_init_height == 280)
{
colstart = 0;
rowstart = 20;
}
else if(_init_width == 172)
{
colstart = 34;
rowstart = 0;
}
else if(_init_width == 170)
{
colstart = 35;
rowstart = 0;
}
else
{
colstart = 0;
rowstart = 80;
}
#endif
writedata(TFT_MAD_MY | TFT_MAD_COLOR_ORDER);

_width = _init_width;
_height = _init_height;
break;
case 3: // Inverted landscape
#ifdef CGRAM_OFFSET
if (_init_width == 135)
{
colstart = 40;
rowstart = 52;
}
else if(_init_height == 280)
{
colstart = 20;
rowstart = 0;
}
else if(_init_width == 172)
{
colstart = 0;
rowstart = 34;
}
else if(_init_width == 170)
{
colstart = 0;
rowstart = 35;
}
else
{
colstart = 80;
rowstart = 0;
}
#endif
writedata(TFT_MAD_MV | TFT_MAD_COLOR_ORDER);

_width = _init_height;
_height = _init_width;
break;
}

其实我就是把每一种匹配模式下 writedata 方法的传参更改了,改动的地方不多,但确实挺烧脑的,不信你可以试试自己改改,虽然不要一会就能改完,但最开始那种⌈左右脑互博⌋的体验正是那张示意图 猎奇 的体现。

配置及测试 LVGL

好的,准备工作这才结束,现在我们来配置 LVGL 并进行测试。我个人认为这一部分比前面的步骤要简单些,第一是该解决的显示问题我们基本已经解决了,驱动层没有问题了,第二是 LVGL 的相关资料通用性高一些,我可以直接抄袭借鉴 ¯_(ツ)_/¯

首先我们要找到 /.pio/libdeps/mpython_esp32s3_r8n16/lvgl/lv_conf_template.h,这是 LVGL 的配置文件示例,我们将其复制一份放到 /.pio/libdeps/mpython_esp32s3_r8n16/,并将其更名为 lv_conf.h

然后进行如下必要的修改:

code line 15
code line 1250

说实话,我也不知道 line 1250 这个地方是否需要修改,因为好像不改照样用的了(?)

然后还有很多可自定义的可选项,这个看你自己了

接下来我们前往 /.pio/libdeps/mpython_esp32s3_r8n16/lvgl/examples/arduino/LVGL_Arduino/LVGL_Arduino.ino,复制里面的内容,把这些代码扔到 /scr/main.cpp 里面。

PIO 下的 Arduino 不能省略任何文件包含命令(这条针对的就是 Arduino.h),你要做的第一件事就是要在其中补全它:

1
#include <Arduino.h>

然后在 line 20 附近,修改代码:

1
2
3
#define TFT_HOR_RES   320
#define TFT_VER_RES 172
#define TFT_ROTATION LV_DISPLAY_ROTATION_90

说个题外话,事实上这个 LV_DISPLAY_ROTATION_90 的值就是 1(二次封装,不过更语义化了),并且下文的 lv_display_set_rotation(disp, TFT_ROTATION); 这个函数本身其实就是 setRotation(uint8_t r) 方法的二次封装(顺便还把面向对象砍了,因为 LVGL 用的是纯 C 的写法)

编译烧录,LVGL 已经在 掌控板3.0 跟 Arduino 打招呼了 (‾◡◝)

Hello Arduino, I’m LVGL

succeed

  • 标题: 在 Arduino 环境下为 掌控板3.0 移植 LVGL
  • 作者: W-Can1425
  • 创建于 : 2025-08-31 13:34:40
  • 更新于 : 2025-08-31 17:49:24
  • 链接: https://can1425.flowecho.org/2025/08/31/20250831/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
在 Arduino 环境下为 掌控板3.0 移植 LVGL