RGB565 LVGL 示例说明
中文 | English
简介
本示例基于 LVGL 图形库,构建了一个适用于嵌入式设备的图形用户界面(GUI)示例项目,结合实际的显示驱动和输入驱动,展示了从底层驱动适配到上层界面构建的完整流程。通过该示例,开发者不仅可以学习如何在嵌入式环境中移植和配置 LVGL,还能掌握界面布局、事件响应、主题定制等关键技术点,从而为后续构建实际产品界面打下基础。
LVGL 简介
LVGL(Light and Versatile Graphics Library,轻量且多功能的图形库)是一个开源的嵌入式 GUI(图形用户界面)开发框架。它的设计初衷是为资源受限的嵌入式设备提供流畅、现代化的图形界面,因此在运行效率、内存占用和可移植性方面都做了大量优化。无论是在简单的低端 MCU 上,还是在功能更强大的 MPU 平台上,LVGL 都能够高效运行,并提供丰富的图形控件和交互体验。
主要特点
轻量级 LVGL 最大的优势在于轻量化,它对资源的需求非常低。在一个内存只有几十 KB 的微控制器上,LVGL 依然可以流畅运行。框架本身的内存开销较小,渲染算法经过精心优化,既可以保证较低的 CPU 使用率,又能在有限的硬件条件下呈现出较好的视觉效果。这使得 LVGL 特别适合那些对硬件资源敏感的应用场景,比如低功耗设备、可穿戴设备、家用电器控制面板等。
跨平台 LVGL 的跨平台特性非常突出,它能够运行在多种操作系统之上,包括 FreeRTOS、RT-Thread、Zephyr、Linux 等,也可以直接运行在裸机环境中。开发者只需要为 LVGL 提供底层的显示驱动和输入驱动接口,便可以快速完成移植,从而在不同的硬件平台之间复用同一套 UI 代码。这种灵活性大大降低了开发成本,使得 LVGL 成为嵌入式 GUI 的通用解决方案。
丰富的控件(Widgets) LVGL 内置了种类丰富的 GUI 控件,例如按钮、标签、滑条、进度条、复选框、列表、表格、图表等。这些控件几乎涵盖了常见人机交互界面的需求,开发者无需从零开始设计和实现组件,大大缩短了开发周期。同时,LVGL 还允许用户基于现有控件扩展新的组件,从而构建符合特定需求的界面。无论是简单的数字显示,还是复杂的图形控制界面,LVGL 都能胜任。
多样的渲染能力 在视觉呈现方面,LVGL 提供了丰富的渲染功能。它支持抗锯齿、透明度、渐变、阴影、边框、圆角等效果,可以让界面看起来更加美观和现代。除此之外,LVGL 还内置动画系统,支持多种缓动函数,能够实现平滑的控件移动、渐变过渡和动态效果。这些特性使得界面不仅功能完善,还能带来良好的用户体验。
输入设备支持 LVGL 支持多种输入设备类型,包括触摸屏、电容屏、鼠标、键盘和编码器,甚至可以实现多点触控交互。它提供了统一的输入接口层,开发者只需实现底层的驱动适配,就能轻松把输入事件传递到 LVGL 的事件系统中。这样一来,开发者可以更加专注于上层的界面逻辑,而不必花费过多时间在输入事件处理上。
国际化与多语言 LVGL 在国际化方面也有很好的支持。它使用 UTF-8 编码,可以处理几乎所有语言的字符集。同时,它支持双向文本渲染,可以正确显示如阿拉伯语、希伯来语等从右到左书写的语言。这使得 LVGL 能够应用在面向全球用户的产品中,不同国家和地区的用户都能通过本地化界面获得良好的体验。
可扩展性 LVGL 提供了灵活的主题和样式系统,开发者可以方便地定制控件的外观样式,以实现不同风格的 UI。通过更换主题,可以快速切换界面的整体视觉风格。此外,LVGL 还可以与第三方图形库、文件系统和图像解码器结合使用,从而扩展其功能。例如,可以使用文件系统来加载外部字体和图片,或者集成 JPEG/PNG 解码库来显示复杂图像。
应用场景
LVGL 在实际应用中有着非常广泛的覆盖范围。在消费电子领域,它常用于智能家居控制面板、家电显示屏、智能手表和健身设备等,这些产品往往需要在有限硬件资源下提供友好的用户交互界面。在工业控制领域,LVGL 被用于 HMI(人机交互界面)和各种仪器仪表,帮助用户直观地监控和操作设备。在汽车电子中,LVGL 可以驱动中控屏、副驾娱乐屏,甚至车载仪表盘。而在医疗设备方面,它则适合做小型显示界面,例如手持检测仪、便携式监护设备等。
生态与社区
LVGL 作为一个开源项目,采用 MIT License 开源协议,既适合个人学习,也可以在商业产品中免费使用。围绕 LVGL,已经形成了一个活跃的生态系统。官方提供了 SquareLine Studio 这类可视化设计工具,支持拖拽式设计界面并导出 LVGL 代码,极大地提升了开发效率。此外,LVGL Simulator 可以让开发者直接在 PC 上进行界面调试,不需要每次都烧录到目标硬件上。全球开发者社区十分活跃,贡献了大量开源控件、主题和移植案例,为新手和企业开发者提供了丰富的资源和支持。
硬件说明
运行本示例需要外接一块 RGB565 LCD 屏幕,开发板上 40pin RGB565 LCD 接口如下:

FSP 配置
HyperRAM 配置
新建 r_ospi_b stack:

配置 r_ospi_b stack:



HyperRAM 引脚配置:

HyperRAM 所有相关引脚的驱动能力配置为 H,OM_1_SIO0~OM_1_SIO7 需要配置为输入上拉。

RGB LCD 配置
新建
r_glcdcstack:

配置中断回调和图形层1:

配置输出参数、CLUT、TCON和抖动。

配置 GLCDC 的引脚:


LCD 背光配置
新建
r_gptstack:

配置背光 PWM 输出:

D/AVE 2D 配置
新建
r_drwstack:

I2C0 配置
新建
r_iic_masterstack:

配置
r_iic_masterstack:

配置 I2C0 引脚:

触摸中断配置
新建
r_icustack:

配置 IRQ28:

配置 IRQ28 引脚:

RT-Thread Settings 配置
使能 RGB565 LCD、GT9147触摸驱动、使能 LVGL。

软件说明
LVGL 相关代码在 ./board/lvgl 中。
lv_port_disp.c
#include <lvgl.h>
#include <rtthread.h>
#include "ra8/lcd_config.h"
#include "hal_data.h"
static rt_sem_t _SemaphoreVsync = RT_NULL;
static uint8_t lvgl_init_flag = 0;
void DisplayVsyncCallback(display_callback_args_t *p_args)
{
rt_interrupt_enter();
if (DISPLAY_EVENT_LINE_DETECTION == p_args->event)
{
if (lvgl_init_flag != 0)
rt_sem_release(_SemaphoreVsync);
}
rt_interrupt_leave();
}
static void vsync_wait_cb(lv_display_t *display)
{
if (!lv_display_flush_is_last(display)) return;
// If Vsync semaphore has already been set, clear it then wait to avoid tearing
rt_sem_take(_SemaphoreVsync, RT_WAITING_FOREVER);
}
static void disp_flush(lv_display_t *display, const lv_area_t *area, uint8_t *px_map)
{
if (!lv_display_flush_is_last(display)) return;
#if (BSP_CFG_DCACHE_ENABLED)
int32_t size;
/* Invalidate cache - so the HW can access any data written by the CPU */
size = sizeof(fb_background[0]);
SCB_CleanInvalidateDCache_by_Addr(px_map, size);
#endif
R_GLCDC_BufferChange(&g_display0_ctrl,
(uint8_t *) px_map,
(display_frame_layer_t) 0);
}
void lv_port_disp_init(void)
{
static rt_device_t device;
/* LCD Device Init */
device = rt_device_find("lcd");
RT_ASSERT(device != RT_NULL);
_SemaphoreVsync = rt_sem_create("lvgl_sem", 1, RT_IPC_FLAG_PRIO);
if (RT_NULL == _SemaphoreVsync)
{
rt_kprintf("lvgl semaphore create failed\r\n");
RT_ASSERT(0);
}
/*------------------------------------
* Create a display and set a flush_cb
* -----------------------------------*/
lv_display_t *disp = lv_display_create(LV_HOR_RES_MAX, LV_VER_RES_MAX);
lv_display_set_flush_cb(disp, disp_flush);
lv_display_set_flush_wait_cb(disp, vsync_wait_cb);
lv_display_set_buffers(disp, &fb_background[0][0], &fb_background[1][0], sizeof(fb_background[0]), LV_DISPLAY_RENDER_MODE_DIRECT);
lvgl_init_flag = 1;
}
lv_port_indev.c
#include <lvgl.h>
#include <rtdevice.h>
#ifdef RT_USING_TOUCH
#include "gt9147.h"
#define GT9147_IRQ_PIN BSP_IO_PORT_00_PIN_07
#define GT9147_RST_PIN BSP_IO_PORT_04_PIN_12
#define DBG_TAG "lv_port_indev"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
#include "hal_data.h"
static rt_device_t touch_dev;
struct rt_touch_data *read_data;
static lv_indev_t *touch_indev;
#if LVGL_VERSION_MAJOR < 9
static void touchpad_read(lv_indev_drv_t *indev, lv_indev_data_t *data)
#else
static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data)
#endif
{
if (RT_NULL != rt_device_read(touch_dev, 0, read_data, 1))
{
if (read_data->event == RT_TOUCH_EVENT_NONE)
return;
data->point.x = LV_HOR_RES_MAX - read_data->x_coordinate;
data->point.y = LV_VER_RES_MAX - read_data->y_coordinate;
if (read_data->event == RT_TOUCH_EVENT_DOWN)
data->state = LV_INDEV_STATE_PR;
if (read_data->event == RT_TOUCH_EVENT_MOVE)
data->state = LV_INDEV_STATE_PR;
if (read_data->event == RT_TOUCH_EVENT_UP)
data->state = LV_INDEV_STATE_REL;
}
}
static rt_err_t gt9147_probe(rt_uint16_t x, rt_uint16_t y)
{
void *id;
touch_dev = rt_device_find("gt9147");
if (touch_dev == RT_NULL)
{
rt_kprintf("can't find device gt9147\n");
return -1;
}
if (rt_device_open(touch_dev, RT_DEVICE_FLAG_INT_RX) != RT_EOK)
{
rt_kprintf("open device failed!");
return -1;
}
id = rt_malloc(sizeof(struct rt_touch_info));
rt_device_control(touch_dev, RT_TOUCH_CTRL_GET_ID, id);
rt_uint8_t *read_id = (rt_uint8_t *)id;
rt_kprintf("id = GT%d%d%d \n", read_id[0] - '0', read_id[1] - '0', read_id[2] - '0');
rt_device_control(touch_dev, RT_TOUCH_CTRL_SET_X_RANGE, &x); /* if possible you can set your x y coordinate*/
rt_device_control(touch_dev, RT_TOUCH_CTRL_SET_Y_RANGE, &y);
rt_device_control(touch_dev, RT_TOUCH_CTRL_GET_INFO, id);
rt_kprintf("range_x = %d \n", (*(struct rt_touch_info *)id).range_x);
rt_kprintf("range_y = %d \n", (*(struct rt_touch_info *)id).range_y);
rt_kprintf("point_num = %d \n", (*(struct rt_touch_info *)id).point_num);
rt_free(id);
read_data = (struct rt_touch_data *)rt_calloc(1, sizeof(struct rt_touch_data));
if (!read_data)
{
return -RT_ENOMEM;
}
return RT_EOK;
}
rt_err_t rt_hw_gt9147_register(void)
{
struct rt_touch_config config;
rt_uint8_t rst;
rst = GT9147_RST_PIN;
config.dev_name = "i2c0";
config.irq_pin.pin = GT9147_IRQ_PIN;
config.irq_pin.mode = PIN_MODE_INPUT_PULLDOWN;
config.user_data = &rst;
if (rt_hw_gt9147_init("gt9147", &config) != RT_EOK)
{
rt_kprintf("touch device gt9147 init failed.\n");
return -RT_ERROR;
}
if (gt9147_probe(LV_HOR_RES_MAX, LV_VER_RES_MAX) != RT_EOK)
{
rt_kprintf("probe gt9147 failed.\n");
return -RT_ERROR;
}
return RT_EOK;
}
#endif
void lv_port_indev_init(void)
{
#ifdef RT_USING_TOUCH
#if LVGL_VERSION_MAJOR < 9
static lv_indev_drv_t indev_drv; /* Descriptor of a input device driver */
lv_indev_drv_init(&indev_drv); /* Basic initialization */
indev_drv.type = LV_INDEV_TYPE_POINTER; /* Touch pad is a pointer-like device */
indev_drv.read_cb = touchpad_read; /* Set your driver function */
/* Register the driver in LVGL and save the created input device object */
touch_indev = lv_indev_drv_register(&indev_drv);
#else
/*Register a touchpad input device*/
touch_indev = lv_indev_create();
lv_indev_set_type(touch_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(touch_indev, touchpad_read);
#endif
/* Register touch device */
rt_err_t res = rt_hw_gt9147_register();
RT_ASSERT(res == RT_EOK);
#endif
}
编译&下载
RT-Thread Studio:在RT-Thread Studio 的包管理器中下载 Titan Board 资源包,然后创建新工程,执行编译。
编译完成后,将开发板的 USB-DBG 接口与 PC 机连接,然后将固件下载至开发板。
运行效果
复位开发板后就能在 LCD 屏幕上看到 LVGL Demo 正在运行。
