在嵌入式开发领域,图形用户界面(GUI)的实现一直是开发者面临的挑战之一。LVGL(Light and Versatile Graphics Library)作为一款轻量级开源图形库,凭借其丰富的控件库和高效的渲染性能,正成为STM32平台上的热门选择。本文将手把手带你完成LVGL V7.1.0在STM32F103ZET6芯片上的完整移植过程,最终实现一个可交互的触摸按钮。
本次移植基于正点原子战舰V3开发板,核心配置如下:
开发工具选择Keil MDK5,需特别注意:
makefile复制ARM Compiler版本:V6.14.1(推荐)
C语言标准:C99(必须)
优化选项:-O2(平衡性能与代码大小)
提示:ARM Compiler V6相比V5有显著的编译速度提升和bug修复,建议优先使用。若遇到兼容性问题,可尝试在工程选项的"C/C++"标签页添加
--c99编译参数。
合理的目录结构是项目可维护性的基础,建议按以下方式组织:
code复制Project/
├── Drivers/ # 硬件驱动层
├── GUI/
│ ├── lvgl/ # LVGL核心库(v7.1.0)
│ ├── lvgl_driver/ # 显示/触摸驱动适配
│ └── app/ # 应用代码
├── MDK-ARM/ # Keil工程文件
└── User/ # 用户代码
在Keil中创建对应的分组(Group):
首先从LVGL官方仓库获取v7.1.0版本源码,重点关注以下文件:
c复制#define LV_HOR_RES_MAX 480 // 水平分辨率
#define LV_VER_RES_MAX 800 // 垂直分辨率
#define LV_COLOR_DEPTH 16 // RGB565格式
#define LV_MEM_SIZE (30 * 1024) // 动态内存分配大小
#define LV_TICK_CUSTOM 1 // 使用自定义时钟源
c复制static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 使用DMA加速屏幕刷新 */
DMA_Fill_Color(area->x1, area->y1,
area->x2, area->y2,
(uint16_t*)color_p);
/* 必须调用以通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
c复制static bool touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint16_t last_x, last_y;
/* 读取触摸状态 */
Touch_Scan();
if(Touch_Status & 0x80) { // 触摸按下
last_x = Touch_X;
last_y = Touch_Y;
data->state = LV_INDEV_STATE_PR;
} else {
data->state = LV_INDEV_STATE_REL;
}
data->point.x = last_x;
data->point.y = last_y;
return false;
}
STM32F103的64KB RAM资源有限,推荐采用以下优化方案:
双缓冲机制:
c复制#define BUF_SIZE (LV_HOR_RES_MAX * 20) // 20行缓冲
static lv_color_t buf1[BUF_SIZE];
static lv_color_t buf2[BUF_SIZE];
lv_disp_buf_init(&disp_buf, buf1, buf2, BUF_SIZE);
外部SRAM利用:
c复制// 在链接脚本中指定内存区域
EXTERNAL_RAM (xrw) : ORIGIN = 0x68000000, LENGTH = 1M
LVGL内存监控:
c复制printf("Mem used: %d/%d\n",
lv_mem_get_used(),
LV_MEM_SIZE);
正确的初始化顺序对系统稳定性至关重要:
示例代码框架:
c复制void Hardware_Init(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FSMC_Init();
MX_DMA_Init();
LCD_Init();
Touch_Init();
/* LVGL初始化 */
lv_init();
lv_port_disp_init();
lv_port_indev_init();
}
LVGL依赖系统时钟进行动画和任务调度:
c复制// 使用TIM2提供1ms时基
void TIM2_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
lv_tick_inc(1); // LVGL心跳
}
}
注意:定时器中断优先级应设置为较低级别(如优先级6),避免影响其他关键中断。
下面实现一个带点击计数的按钮:
c复制static uint8_t btn_counter = 0;
static void btn_event_handler(lv_obj_t * btn, lv_event_t event)
{
if(event == LV_EVENT_CLICKED) {
btn_counter++;
lv_obj_t * label = lv_obj_get_child(btn, NULL);
lv_label_set_text_fmt(label, "Clicked: %d", btn_counter);
}
}
void Create_First_Button(void)
{
/* 创建按钮对象 */
lv_obj_t * btn = lv_btn_create(lv_scr_act(), NULL);
lv_obj_set_size(btn, 150, 60);
lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_event_cb(btn, btn_event_handler);
/* 添加标签 */
lv_obj_t * label = lv_label_create(btn, NULL);
lv_label_set_text(label, "Click Me!");
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);
}
LVGL支持灵活的样式定制:
c复制static lv_style_t btn_style;
static lv_style_t btn_style_pressed;
void Init_Button_Styles(void)
{
/* 默认状态样式 */
lv_style_init(&btn_style);
lv_style_set_bg_color(&btn_style, LV_STATE_DEFAULT, LV_COLOR_MAKE(0x33, 0x66, 0xFF));
lv_style_set_radius(&btn_style, LV_STATE_DEFAULT, 10);
/* 按下状态样式 */
lv_style_init(&btn_style_pressed);
lv_style_set_bg_color(&btn_style_pressed, LV_STATE_PRESSED, LV_COLOR_MAKE(0x11, 0x44, 0xDD));
lv_style_set_transform_width(&btn_style_pressed, LV_STATE_PRESSED, -5);
/* 应用样式 */
lv_obj_add_style(btn, LV_BTN_PART_MAIN, &btn_style);
lv_obj_add_style(btn, LV_BTN_PART_MAIN, &btn_style_pressed);
}
渲染优化:
c复制// 在lv_conf.h中启用
#define LV_USE_GPU 1 // 启用硬件加速
#define LV_USE_GPU_STM32_DMA2D 1 // 使用DMA2D
内存监控:
c复制void Check_Memory_Usage(void)
{
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
printf("Used: %d bytes (%.1f%%)\n",
mon.total_size - mon.free_size,
(float)(mon.total_size - mon.free_size) * 100 / mon.total_size);
}
帧率控制:
c复制// 在main循环中
while(1) {
lv_task_handler();
HAL_Delay(5); // 限制最高200FPS
}
移植完成后,建议运行LVGL的基准测试来评估性能:
c复制void Run_Benchmark(void)
{
lv_benchmark_create(lv_scr_act());
}