LVGL移植
写在前面
如果您对LVGL十分感兴趣,我非常建议自己尝试一下移植LVGL,本文将以通俗易懂的方式,向大家介绍LVGL移植的过程,希望读者能在本文的辅助下,独自完成LVGL的移植。 在本文的最后部分会向大家介绍有关性能优化的细节。
事实上,LVGL的每个大版本,driver部分的接口都有较大的变化,所以我们选择了两个目前最新的release版本讲述移植过程,分别是 v8.4
和 v9
。
你可以在 lvgl
工程源码的 examples/porting
文件夹下找到当前版本的移植模板文件
准备工作
如果你想要在你的显示模组上移植LVGL,最先应该做的是将其点亮。 比如实现一个描点函数。
为了验证屏幕处于可用状态,我们通常会在HAL层实现一些基本功能,比如复位、初始化、通常分为以下几步:
1. 复位显示模组
一般来说,大部分显示面板都提供了 RESET 的复位引脚,且低有效。 通常这是在向显示模组写入初始化序列的前一步。
#define dm_gpio_set_value(p,v) gpio_put(p, v)
#define mdelay(v) sleep_ms(v)
static int ili9488_reset(struct ili9488_priv *priv)
{
dm_gpio_set_value(priv->gpio.reset, 1);
mdelay(10);
dm_gpio_set_value(priv->gpio.reset, 0);
mdelay(10);
dm_gpio_set_value(priv->gpio.reset, 1);
mdelay(10);
return 0;
}
2. 向显示模组写入初始化序列
所谓初始化序列,其实就是命令 + 数据的组合,举个例子:
write_reg(priv, 0x3A, 0x55);
代表发送 触发像素格式设置命令
+ 设置像素格式为RGB565
这一系列设置命令完成了对显示驱动IC的初步设置。
static int ili9488_init_display(struct ili9488_priv *priv)
{
pr_debug("%s, writing initial sequence...\n", __func__);
ili9488_reset(priv);
// dm_gpio_set_value(&priv->gpio.rd, 1);
// mdelay(150);
// Positive Gamma Control
write_reg(priv, 0xE0, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F);
// Negative Gamma Control
write_reg(priv, 0xE1, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F);
write_reg(priv, 0xC0, 0x17, 0x15); // Power Control 1
write_reg(priv, 0xC1, 0x41); // Power Control 2
write_reg(priv, 0xC5, 0x00, 0x12, 0x80); // VCOM Control
// write_reg(priv, 0x36, 0x28); // Memory Access Control
write_reg(priv, 0x3A, 0x55); // Pixel Interface Format RGB565 8080 16-bit
write_reg(priv, 0xB0, 0x00); // Interface Mode Control
// Frame Rate Control
// write_reg(priv, 0xB1, 0xD0, 0x11); // 60Hz
write_reg(priv, 0xB1, 0xD0, 0x14); // 90Hz
write_reg(priv, 0xB4, 0x02); // Display Inversion Control
write_reg(priv, 0xB6, 0x02, 0x02, 0x3B); // Display Function Control
write_reg(priv, 0xB7, 0xC6); // Entry Mode Set
write_reg(priv, 0xF7, 0xA9, 0x51, 0x2C, 0x82); // Adjust Control 3
write_reg(priv, 0x11); // Exit Sleep
mdelay(60);
write_reg(priv, 0x29); // Display on
return 0;
}
3. 设置绘制区域
一些显示驱动IC,为了节省MCU的内存,将显示buffer集成到了显示驱动IC中。 这样做的 另一个优势是不强制要求MCU侧具备强大的显示控制器,MCU在讲数据写入到显示驱动IC的SRAM 时,驱动IC会自行将SRAM中的数据刷新到显示面板上。
所以像这种驱动IC,基本都支持局部刷新,我们只需要将显示缓冲区中需要更新的部分写入到SRAM中即可,
这类驱动IC一般都提供了一个设置绘制区域的命令,例如 0x2A (设置列地址)
0x2B (设置行地址)
,
在发送完设置命令之后,还需要发送具体参数,请参考数据手册中的要求。
下面这个接口展示了通过 xs
, ys
, xe
, ye
四个参数,设置一个从 (xs, ys) 到 (xe, ye) 的矩形刷新区域。
static int ili9488_set_addr_win(struct ili9488_priv *priv, int xs, int ys, int xe,
int ye)
{
/* set column adddress */
write_reg(priv, 0x2A, xs >> 8, xs, xe >> 8, xe);
/* set row address */
write_reg(priv, 0x2B, ys >> 8, ys, ye >> 8, ye);
/* write start */
write_reg(priv, 0x2C);
return 0;
}
0x2C
命令用于通知驱动IC将要写入SRAM。
4. 发送颜色数据
局部刷新
发送颜色数据一般跟在 0x2C
命令之后,如果你选择局部数据更新,则还应该在这之前设置绘制区域 0x2A
, 0x2B
。
如下所示的是一个窗口绘制函数,接受一个矩形区域的参数和一个给定长度的像素数据buffer
static void ili9488_video_sync(struct ili9488_priv *priv, int xs, int ys, int xe, int ye, void *vmem16, size_t len)
{
// pr_debug("video sync: xs=%d, ys=%d, xe=%d, ye=%d, len=%d\n", xs, ys, xe, ye, len);
priv->tftops->set_addr_win(priv, xs, ys, xe, ye);
write_buf_rs(priv, vmem16, len * 2, 1);
}
在本工程中,write_buf_rs
是一个宏定义,会根据另一个宏DISP_OVER_PIO
来决定是否通过PIO
外设模拟I8080协议发送数据
/* rs=0 means writing register, rs=1 means writing data */
#if DISP_OVER_PIO
#define write_buf_rs(p, b, l, r) i80_write_buf_rs(b, l, r)
#else
#define write_buf_rs(p, b, l, r) fbtft_write_gpio16_wr_rs(p, b, l, r)
#endif
下面是两个函数的原型,执行的逻辑是先设置RS引脚,再发送给定buffer中的数据到I8080端口。
static void fbtft_write_gpio16_wr_rs(struct ili9488_priv *priv, void *buf, size_t len, bool rs)
void i80_write_buf_rs(void *buf, size_t len, bool rs);
全屏刷新
如果你需要全屏刷新,则必须先分配一个全屏的buffer,在本工程中,一块全屏buffer的大小为 307200
字节,
这超出了RP2040上的 256KB SRAM 限制,不过在RP2350上,有512KB SRAM,可以使用全屏刷新。
在发送完初始化序列后,设置一次绘制区域为整屏大小,然后发送命令0x2C
,下面是一个示例
priv->tftops->set_addr_win(priv, 0, 0,
priv->display->xres - 1,
priv->display->yres - 1);
然后就可以循环发送全屏buffer,驱动IC中的行列指针会根据发送的数据自动移动的复位。
全屏刷新一般用于内部有 LCD 控制器的 MCU 或 MPU,例如在 F1C200s 上,可以将 TCON 设置成I8080模式, 在初始化TCON之前,先通过GPIO模拟I8080端口,初始化显示屏,然后再初始化 TCON, BE, FE 等,根据framebuffer 中的数据输出I8080时序,就可以实现 LCD 控制器全屏刷新的效果。
LVGL 8.4
显示驱动
可以参考pico_dm_qd3503728_noos/porting/lv_port_disp_template.c
1. 分配 LVGL 显示 buffer
/* 你可以在这里调用 LCD 的初始化函数 */
disp_init();
#ifndef MY_DISP_BUF_SIZE
#warning '"MY_DISP_BUF_SIZE" is not defined, defaulting to (HOR_RES * VER_RES / 2)'
#define MY_DISP_BUF_SIZE (MY_DISP_HOR_RES * MY_DISP_VER_RES / 2)
#endif
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_BUF_SIZE];
/* 调用lvgl接口初始化显示buffer */
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_BUF_SIZE);
2. 分配、注册 LVGL 显示驱动
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/* 你的屏幕的宽高分辨率 */
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
/* 这个是需要你实现的显示 buffer 刷新的回调函数 */
disp_drv.flush_cb = ili9488_flush;
/* 将第一步分配的 LVGL 显示 buffer 注册到显示驱动中 */
disp_drv.draw_buf = &draw_buf_dsc_1;
/* 注册显示驱动 */
lv_disp_drv_register(&disp_drv);
下面是 ili9488_flush
相关函数的实现
static inline void ili9488_write_cmd(uint16_t cmd)
{
write_buf_rs(&g_priv, &cmd, sizeof(cmd), 0);
}
#define write_cmd ili9488_write_cmd
static inline void ili9488_write_data(uint16_t data)
{
write_buf_rs(&g_priv, &data, sizeof(data), 1);
}
#define write_data ili9488_write_data
#include "lvgl/lvgl.h"
void ili9488_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
#if 1
write_cmd(0x2A);
write_data(area->x1 >> 8);
write_data(area->x1);
write_data(area->x2 >> 8);
write_data(area->x2);
/* set row address */
write_cmd(0x2B);
write_data(area->y1 >> 8);
write_data(area->y1);
write_data(area->y2 >> 8);
write_data(area->y2);
/* write start */
write_cmd(0x2C);
write_buf_rs(&g_priv, (void *)color_p, lv_area_get_size(area) * 2, 1);
#else
struct ili9488_priv *priv = &g_priv;
priv->tftops->set_addr_win(priv, area->x1, area->y1, area->x2, area->y2);
write_buf_rs(priv, (void *)px_map, lv_area_get_size(area) * 2, 1);
#endif
lv_disp_flush_ready(disp_drv);
}
输入驱动
可以参考pico_dm_qd3503728_noos/porting/lv_port_indev_template.c
1. 分配、注册 LVGL Touchpad 驱动
static lv_indev_drv_t indev_drv;
/*------------------
* Touchpad
* -----------------*/
/* 你可以在这里初始化你的触摸屏 */
touchpad_init();
/* 初始化 LVGL 触摸屏驱动 */
lv_indev_drv_init(&indev_drv);
/* 设置驱动类型为点类型 */
indev_drv.type = LV_INDEV_TYPE_POINTER;
/* 设置触摸屏驱动的读取回调函数,这个需要你自己实现 */
indev_drv.read_cb = touchpad_read;
/* 注册触摸屏驱动 */
indev_touchpad = lv_indev_drv_register(&indev_drv);
下面是 touchpad_read
相关函数的实现
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
/*Save the pressed coordinates and the state*/
if(touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
// printf("touchpad is pressed, x: %d, y: %d\n", last_x, last_y);
data->state = LV_INDEV_STATE_PR;
}
else {
data->state = LV_INDEV_STATE_REL;
}
/*Set the last pressed coordinates*/
data->point.x = last_x;
data->point.y = last_y;
}
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
return ft6236_is_pressed();
// return false;
}
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
/*Your code comes here*/
(*x) = ft6236_read_x();
(*y) = ft6236_read_y();
}
Tick 驱动
v8.4 版本的 LVGL 允许在 lv_conf.h 中设置 tick 回调函数
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
#define LV_TICK_CUSTOM_INCLUDE "pico/time.h" /*Header for the system time function*/
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (time_us_32() / 1000LL) /*Expression evaluating to current system time in ms*/
/*If using lvgl as ESP32 component*/
// #define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
#endif /*LV_TICK_CUSTOM*/
time_us_32()
是 Pico SDK 中的一个函数,用于获取系统启动后经过的时间,单位为微秒,因为 LVGL 的tick以毫秒为单位,所以需要除1000
LVGL 9
显示驱动
void lv_port_disp_init(void)
{
disp_init();
/*------------------------------------
* 创建一个 display 对象,并注册刷新回调函数
* -----------------------------------*/
disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
lv_display_set_flush_cb(disp, ili9488_flush);
/* 如果没有定义 MY_DISP_BUF_SIZE,则默认设置为屏幕分辨率的 1/8 */
#ifndef MY_DISP_BUF_SIZE
#define MY_DISP_BUF_SIZE (MY_DISP_HOR_RES * MY_DISP_VER_RES / 8)
#endif
/* 设置 display 对象的显示 buffer, 默认使用 1 个 buffer,刷新方式为局部刷新 */
static lv_color_t buf_1_1[MY_DISP_BUF_SIZE];
lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
}
static void disp_init(void)
{
/*You code here*/
ili9488_driver_init();
}
输入驱动
lvgl v9 版本的输入驱动相比 v8.4 版本没有太大的变化,只不过原来在 lv_conf.h 中 设置输入事件的读取周期,改为了定时器。
void lv_port_indev_init(void)
{
/*Initialize your touchpad if you have*/
touchpad_init();
/* 分配、注册一个 pointer 类型的输入设备 */
indev_touchpad = lv_indev_create();
lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev_touchpad, touchpad_read);
/* 创建一个 lvgl 定时器,用于周期性的调用 touchpad_read */
lv_timer_t *indev_timer = lv_indev_get_read_timer(indev_touchpad);
lv_timer_set_period(indev_timer, 16);
}
static void touchpad_init(void)
{
/*Your code comes here*/
ft6236_driver_init();
}
static void touchpad_read(lv_indev_t * indev_drv, lv_indev_data_t * data)
{
static int32_t last_x = 0;
static int32_t last_y = 0;
/*Save the pressed coordinates and the state*/
if(touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->state = LV_INDEV_STATE_PR;
}
else {
data->state = LV_INDEV_STATE_REL;
}
/*Set the last pressed coordinates*/
data->point.x = last_x;
data->point.y = last_y;
}
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
return ft6236_is_pressed();
}
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(int32_t * x, int32_t * y)
{
/*Your code comes here*/
(*x) = ft6236_read_x();
(*y) = ft6236_read_y();
}
Tick 驱动
在 lvgl 的 v9 版本中,你需要调用一个接口来注册 tick 的回调函数
static uint32_t __time_critical_func(my_tick_get_cb)(void)
{
return time_us_32() / 1000;
}
lv_tick_set_cb(my_tick_get_cb);
性能优化
待添加