多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32移植LVGL 8.0.2保姆级教程:从工程搭建到优化实战

STM32移植LVGL 8.0.2保姆级教程:从工程搭建到优化实战 1. 项目概述为什么要在STM32上折腾LVGL如果你正在用STM32做带屏幕的产品比如智能手表、工业HMI或者家用电器面板那你大概率绕不开一个选择用哪种GUI库是继续在LCD上画点画线还是用更高级的图形库我当年就是从自己写驱动、画UI走过来的深知其中的痛苦改个按钮位置要调半天坐标加个动画效果得写一堆状态机UI和业务逻辑搅在一起代码越写越乱。后来接触到LVGL感觉像是打开了新世界的大门。LVGLLight and Versatile Graphics Library是一个用C语言编写的开源图形库它提供了按钮、标签、滑块、图表等丰富的控件支持动画、主题、多语言甚至还有GPU加速的接口。最关键的是它资源占用相当友好在只有几十KB RAM的MCU上也能跑起来。版本8.0.2是一个相当成熟稳定的版本功能完善社区资料也多非常适合作为在STM32上的入门和商用选择。所以这个“保姆级教程”的目的就是帮你把LVGL 8.0.2这颗“心脏”成功地移植到你的STM32“身体”里。我会从最基础的工程准备开始一步步带你走过配置、适配、调试的全过程直到屏幕上出现第一个LVGL的“Hello World”。更重要的是我会分享我踩过的所有坑和对应的填坑方法并提供一份我亲自移植、测试通过的工程文件作为参考和起点。无论你是刚接触嵌入式GUI的初学者还是想为现有项目升级UI的开发者这篇教程都能让你少走弯路。2. 移植前的核心准备与工程框架搭建在动手写代码之前充分的准备工作能避免一半的麻烦。这一部分我们搞清楚需要什么以及如何搭建一个干净的基础工程。2.1 硬件与软件环境清单首先确认你的“战场”装备是否齐全。硬件部分主控芯片任意一款STM32系列MCU。本教程以常见的STM32F103C8T6蓝色药丸核心板为例因为它资源适中64KB Flash20KB RAM价格便宜读者持有量大。但原理适用于F1、F4、H7等全系列。显示屏一款支持SPI或8080/6800并行接口的TFT液晶屏。我使用的是1.44寸的ST7735S驱动芯片的SPI屏幕。请务必找到你屏幕的数据手册Datasheet和初始化代码序列。触摸屏可选如果你需要交互需要电阻式或电容式触摸屏。常见的是XPT2046电阻触摸芯片SPI接口。本教程会包含触摸移植部分。调试器ST-Link V2或J-Link等用于下载和调试。接线确保屏幕的SPI或FSMC引脚、背光控制引脚、触摸芯片引脚正确连接到STM32。软件部分开发环境Keil MDK-ARM我用的V5.36当然你也可以用IAR或者STM32CubeIDE但本文指令以Keil为主。STM32固件库/ HAL库为了方便我们直接使用STM32CubeMX生成基于HAL库的初始化代码。这能节省大量配置GPIO、时钟、外设的时间。LVGL源码从GitHub官方仓库https://github.com/lvgl/lvgl下载版本8.0.2的ZIP包或者使用Git克隆。注意不要用master分支确保是release/v8.0.2标签下的代码。文件系统可选如果你打算从SD卡加载图片、字体需要FATFS的中间件。2.2 使用STM32CubeMX创建基础工程这是构建“地基”的关键一步正确的初始化能避免后续很多底层驱动问题。选择芯片打开CubeMX选择你的芯片型号STM32F103C8T6。配置时钟树根据你的外部晶振如8MHz配置系统时钟到最高频率对于F103通常是72MHz。LVGL的动画流畅度与系统时钟直接相关。配置调试接口在System Core-SYS里将Debug改为Serial Wire否则可能无法使用ST-Link调试。配置显示接口对于SPI屏幕在Connectivity下启用一个SPI模式为Full-Duplex Master硬件NSS选Disable我们通常用软件控制片选。根据数据手册设置预分频器确保时钟如Baud Rate在屏幕规格范围内ST7735通常支持到几十MHz。然后配置对应的GPIOSCK、MOSI、RES复位、DC命令/数据、CS片选、BLK背光。对于8080并口屏幕通常使用FSMCF4/F7/H7或GPIO模拟。使用FSMC性能更好需要在Connectivity中配置FSMC选择LCD Interface并根据屏幕时序参数调整地址建立、数据建立等时间。配置触摸接口如果有时如果触摸芯片是XPT2046SPI需要再启用一个SPI或与屏幕共用但需注意片选隔离并配置IRQ和BUSY引脚如果有。配置FreeRTOS可选但推荐在Middleware中启用FREERTOS使用CMSIS_V2接口。LVGL推荐在操作系统的线程中运行其任务句柄lv_timer_handler这样不阻塞其他任务。我们将创建一个低优先级的任务专门用于LVGL。生成工程在Project Manager选项卡设置工程名、路径选择MDK-ARM作为Toolchain。在Code Generator里选择“Copy only necessary library files”以保持工程简洁并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files”。最后点击GENERATE CODE。注意CubeMX生成的gpio.c里会对所有用到的GPIO进行初始化。请务必检查屏幕和触摸的引脚初始化模式推挽输出、上拉输入等是否正确不正确的模式会导致通信失败。2.3 获取与整理LVGL源码解压下载的LVGL 8.0.2源码包你会发现里面有很多文件夹。我们不需要全部放进工程。核心必选文件将lvgl文件夹整个复制到你的Keil工程目录下例如与Core、Drivers文件夹同级。这个文件夹里包含src核心源码、examples示例、demos演示等。关键配置文件在lvgl目录下找到lv_conf_template.h将其复制一份到你的工程Core/Inc目录下并重命名为lv_conf.h。这个文件是LVGL的“大脑”所有配置开关都在这里。端口文件在lvgl目录下的examples/porting文件夹里找到我们需要移植实现的几个关键文件模板lv_port_disp_template.c/.h- 显示接口lv_port_indev_template.c/.h- 输入设备触摸、按键接口lv_port_fs_template.c/.h- 文件系统接口可选 同样将它们复制到你的工程目录比如新建一个LVGL_Port文件夹并移除文件名中的_template例如lv_port_disp.c。至此你的工程目录结构应该大致如下My_LVGL_Project/ ├── Core/ │ ├── Inc/ │ │ ├── lv_conf.h -- LVGL主配置文件 │ │ └── ... │ └── Src/ │ └── ... ├── Drivers/ ├── LVGL/ │ ├── src/ -- LVGL核心源码 │ ├── examples/ │ └── ... ├── LVGL_Port/ │ ├── lv_port_disp.c │ ├── lv_port_indev.c │ └── ... └── MDK-ARM/ -- Keil工程文件3. LVGL核心配置与显示接口移植详解这是移植最核心的一步我们要告诉LVGL如何驱动你的屏幕进行绘制。3.1 深度定制lv_conf.h配置文件打开lv_conf.h开头的#if 0需要改为#if 1来启用本文件。然后开始关键配置颜色深度LV_COLOR_DEPTH设置为16或32。对于大多数TFT屏16位色深RGB565足以满足需求且节省内存。如果你的屏幕是RGB888且内存充足可以设32。#define LV_COLOR_DEPTH 16堆内存大小LV_MEM_SIZE是LVGL内部动态分配内存的大小。对于中等复杂度的界面建议设置为32KB ~ 64KB (即 32 * 1024UL)。如果开了很多缓冲或控件需要加大。#define LV_MEM_SIZE (48 * 1024UL)Tick来源LV_TICK_CUSTOM需要启用并实现lv_tick_get()函数。我们将用SysTick中断来提供1ms的心跳。#define LV_TICK_CUSTOM 1 #if LV_TICK_CUSTOM #define LV_TICK_CUSTOM_INCLUDE main.h // 提供HAL_GetTick()声明 #define LV_TICK_CUSTOM_SYS_TIME_EXPR (HAL_GetTick()) #endif显示缓冲区LV_USE_DISP_BUFFER是性能关键。有三种模式单缓冲LV_DISP_BUF_FULL只有一个缓冲区绘制完成直接刷屏期间屏幕会撕裂。双缓冲LV_DISP_BUF_FULL两个缓冲区一个用于LVGL绘制一个用于DMA传输到屏幕无撕裂但耗内存。部分缓冲LV_DISP_BUF_PARTIAL只分配一小块缓冲区如屏幕的1/10高度LVGL分块绘制DMA分块传输。这是资源紧张MCU的推荐方案能极大降低RAM需求。#define LV_USE_DISP_BUFFER_PARTIAL 1 #define LV_DISP_DEF_REFR_PERIOD 30 // 默认刷新周期30ms #define LV_INDEV_DEF_READ_PERIOD 30 // 默认输入设备读取周期30ms启用功能根据需求打开控件和功能如标签、按钮、滑块、图表、动画、文件系统等。初期可以保持默认后续按需开启。#define LV_USE_LABEL 1 #define LV_USE_BTN 1 #define LV_USE_ANIMATION 13.2 实现显示驱动接口 lv_port_disp.c现在打开lv_port_disp.c进行实质性移植。初始化函数在disp_init(void)函数里你需要完成硬件初始化。这部分代码通常就是你屏幕厂家提供的LCD_Init()函数。确保SPI/FSMC、GPIO、背光都已正确配置。static void disp_init(void) { // 你实际的屏幕初始化代码 LCD_Init(); // 这个函数需要你根据屏幕驱动芯片编写或已有 // 例如设置屏幕旋转方向、清屏等 LCD_SetRotation(0); LCD_Clear(0x0000); }刷新回调函数这是最关键的disp_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_map)。area定义了屏幕上需要更新的矩形区域 (x1, y1, x2, y2)。color_map是一个数组存储了该区域内每个像素点的颜色值格式由LV_COLOR_DEPTH决定。你的任务将color_map中的数据写入到屏幕的area区域。对于SPI屏幕通常需要设置光标地址LCD_SetWindow然后连续发送颜色数据LCD_WriteData_Color。static void disp_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_map) { // 1. 设置屏幕的显示窗口为 area 区域 LCD_SetWindow(area-x1, area-y1, area-x2, area-y2); // 2. 准备发送数据例如拉低片选写命令等 LCD_Write_Cmd(0x2C); // 假设0x2C是内存写命令 // 3. 将color_map中的数据连续发送到屏幕 uint32_t size lv_area_get_width(area) * lv_area_get_height(area); LCD_Write_Data_Color((uint16_t*)color_map, size); // 你的SPI发送函数 // 4. 重要告诉LVGL刷新已完成 lv_disp_flush_ready(drv); }实操心得disp_flush函数会被LVGL频繁调用。为了提升性能务必使用DMA进行数据传输避免在SPI发送循环中阻塞CPU。对于SPI可以配置为DMA模式在disp_flush中启动DMA传输然后在DMA传输完成的中断回调里调用lv_disp_flush_ready(drv)。这是流畅与否的关键。注册显示驱动在lv_port_disp_init(void)函数中分配一个显示缓冲并初始化驱动。void lv_port_disp_init(void) { // 1. 初始化硬件 disp_init(); // 2. 分配显示缓冲区部分缓冲示例 static lv_disp_buf_t disp_buf; static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 缓冲区屏幕宽度 * 10行像素 lv_disp_buf_init(disp_buf, buf, NULL, LV_HOR_RES_MAX * 10); // 3. 注册驱动 lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.buffer disp_buf; disp_drv.flush_cb disp_flush; disp_drv.hor_res 160; // 你的屏幕实际宽度 disp_drv.ver_res 128; // 你的屏幕实际高度 // disp_drv.full_refresh 1; // 如果需要全屏刷新则启用 lv_disp_drv_register(disp_drv); }4. 输入设备触摸与操作系统适配一个没有交互的GUI是残缺的。接下来我们让屏幕能响应你的触摸。4.1 实现触摸驱动接口 lv_port_indev.c打开lv_port_indev.c我们以电阻触摸XPT2046为例。读取触摸数据核心是实现touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)函数。你需要编写一个TP_Read()函数来读取触摸芯片的ADC值并转换为屏幕坐标(x, y)。同时需要检测触摸状态是按下(LV_INDEV_STATE_PR)还是释放(LV_INDEV_STATE_REL)。static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data) { static int16_t last_x 0, last_y 0; uint8_t tp_state 0; int16_t x 0, y 0; // 你的触摸读取函数返回状态和坐标 tp_state TP_Read(x, y); if(tp_state 1) { >void lv_port_indev_init(void) { // 1. 初始化触摸硬件 TP_Init(); // 你的触摸初始化函数 // 2. 注册输入设备驱动 static lv_indev_drv_t indev_drv; lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; // 指针设备触摸、鼠标 indev_drv.read_cb touchpad_read; lv_indev_t * my_indev lv_indev_drv_register(indev_drv); // 3. 可选将输入设备与特定显示关联 lv_disp_t * my_disp lv_disp_get_default(); lv_indev_set_display(my_indev, my_disp); }4.2 在FreeRTOS中集成LVGL任务句柄如果你在CubeMX中启用了FreeRTOS那么让LVGL在其中运行是最佳实践。创建LVGL任务在main.c的StartDefaultTask或你创建的任务中初始化LVGL和端口然后创建一个低优先级的任务来周期性调用lv_timer_handler()。void StartDefaultTask(void *argument) { // 硬件初始化 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); // ... 其他外设初始化 // LVGL初始化 lv_init(); lv_port_disp_init(); lv_port_indev_init(); // 创建LVGL任务 xTaskCreate(lvgl_task, LVGL Task, 512, NULL, 1, NULL); // 优先级较低 // 创建你的应用任务 xTaskCreate(app_task, APP Task, 512, NULL, 2, NULL); vTaskStartScheduler(); while (1) {} } void lvgl_task(void *pvParameters) { TickType_t last_wake_time xTaskGetTickCount(); const TickType_t period pdMS_TO_TICKS(5); // 5ms执行一次即200Hz for(;;) { lv_task_handler(); // 在v8.0.2中定时器处理已集成到lv_task_handler() vTaskDelayUntil(last_wake_time, period); } }提供系统Tick在stm32f1xx_it.c的SysTick中断服务函数中调用lv_tick_inc(1)为LVGL提供时间基准。void SysTick_Handler(void) { HAL_IncTick(); lv_tick_inc(1); // 增加1毫秒计数 }实操心得lv_timer_handler()在v8中已合并的执行周期建议在1-10ms。太慢会导致UI响应迟钝动画卡顿太快会浪费CPU资源。5ms是一个不错的平衡点。确保这个任务的堆栈空间足够512字可能只是起点复杂界面需要更多。5. 工程集成、编译与第一个UI测试现在我们把所有模块像拼图一样组合起来并点亮屏幕。5.1 在Keil工程中添加文件与头文件路径添加文件分组在Keil的Project窗口中创建几个组例如LVGL_Core添加lvgl/src下的所有.c文件。注意lvgl/src下的子文件夹如drawwidgetsmisc等里的.c文件也要按结构添加进去。一个技巧是使用*.c通配符添加整个文件夹。LVGL_Port添加你修改好的lv_port_disp.clv_port_indev.c。LVGL_App存放你后续的应用程序文件。添加头文件路径在Options for Target-C/C-Include Paths中添加以下路径../Core/Inc(包含lv_conf.h)../LVGL(LVGL根目录)../LVGL/src(LVGL核心头文件)../LVGL_Port(你的端口文件目录)../Drivers/STM32F1xx_HAL_Driver/Inc等HAL库路径。5.2 编写主函数与第一个界面在main.c或你的应用任务中创建第一个简单的界面。void app_task(void *pvParameters) { // 等待LVGL初始化完成可以加个延时或信号量同步 vTaskDelay(pdMS_TO_TICKS(100)); // 1. 创建一个基础对象屏幕 lv_obj_t * scr lv_scr_act(); // 2. 创建一个按钮 lv_obj_t * btn lv_btn_create(scr); lv_obj_set_size(btn, 100, 50); // 宽100像素高50像素 lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); // 居中显示 // 3. 给按钮加个标签 lv_obj_t * label lv_label_create(btn); lv_label_set_text(label, Click Me!); lv_obj_center(label); // 4. 给按钮添加一个点击事件 lv_obj_add_event_cb(btn, btn_event_handler, LV_EVENT_CLICKED, NULL); for(;;) { // 你的其他应用逻辑 vTaskDelay(pdMS_TO_TICKS(100)); } } static void btn_event_handler(lv_event_t * e) { lv_event_code_t code lv_event_get_code(e); if(code LV_EVENT_CLICKED) { // 改变按钮标签文字 lv_obj_t * btn lv_event_get_target(e); lv_obj_t * label lv_obj_get_child(btn, 0); lv_label_set_text(label, Clicked!); } }5.3 编译、下载与调试编译点击Build。你可能会遇到大量错误通常是头文件找不到、函数未声明。请仔细检查头文件路径是否添加完整以及lv_conf.h是否被正确包含#if 1。解决常见编译错误undefined symbol lv_xxx通常是相应的源文件.c没有添加到工程中。去lvgl/src下找到对应的文件添加。lv_conf.hnot found确保路径正确且文件在Core/Inc里。HAL库相关错误确保CubeMX生成的HAL库文件已正确添加到工程。下载与调试连接ST-Link下载程序到芯片。打开串口助手如果你有打印调试信息。使用Keil的调试功能单步执行查看LVGL初始化、刷屏函数是否被调用。6. 高级优化、问题排查与资源管理当第一个按钮显示在屏幕上并能响应点击时恭喜你移植成功了90%。剩下的10%是优化和打磨这对产品化至关重要。6.1 性能优化技巧使用DMA进行刷屏这是提升流畅度的最有效手段。将disp_flush函数中的SPI发送循环改为启动DMA传输。在SPI的DMA传输完成中断HAL_SPI_TxCpltCallback中调用lv_disp_flush_ready(drv)。// 在disp_flush中 HAL_SPI_Transmit_DMA(hspi1, (uint8_t*)color_map, size * 2); // 16位色所以size*2 // 不要在这里调用 lv_disp_flush_ready // 在SPI发送完成回调函数中 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi-Instance SPI1.Instance) { lv_disp_flush_ready(disp_drv); // 通知LVGL本块刷新完成 } }调整缓冲区策略部分缓冲如果内存紧张坚持使用部分缓冲。可以尝试调整缓冲区行数。行数越多一次传输数据量越大DMA效率可能更高但内存占用也大。需要平衡。全屏双缓冲如果内存充足比如STM32F429有256KB RAM使用全屏双缓冲可以获得最流畅的体验完全无撕裂。优化LVGL配置在lv_conf.h中关闭你不需要的控件、特效和功能以节省Flash和RAM。调整LV_DISP_DEF_REFR_PERIOD刷新周期和LV_INDEV_DEF_READ_PERIOD输入读取周期。在保证流畅的前提下尽量调大降低CPU负载。使用LV_USE_PERF_MONITOR和LV_USE_MEM_MONITOR来在屏幕上实时显示帧率和内存使用情况帮助定位性能瓶颈。6.2 典型问题排查实录以下是我在多次移植中遇到的“坑”及其解决方案问题现象可能原因排查步骤与解决方案屏幕白屏或花屏1. 屏幕初始化序列错误。2. 刷屏函数disp_flush中设置窗口或发送数据命令错误。3. 颜色格式不匹配RGB565 vs RGB888。1. 用逻辑分析仪或示波器抓取SPI时序与数据手册对比初始化命令和数据。2. 在disp_flush开头用HAL_GPIO_WritePin点亮一个LED确认函数被调用。3. 尝试发送固定的纯色如红色0xF800测试刷屏通路。4. 检查lv_conf.h中LV_COLOR_DEPTH与屏幕驱动芯片要求的格式是否一致。触摸坐标完全不准1. 触摸芯片初始化或读取函数有误。2. 坐标未校准。3. X, Y轴方向反了或镜像了。1. 读取原始ADC值通过串口打印出来看按压时是否有变化。2. 实现一个简单的校准程序在屏幕四个角显示十字让用户点击计算校准系数。3. 在touchpad_read中尝试交换x和y或对坐标进行x 屏幕宽度 - x等变换。UI非常卡顿动画掉帧1.lv_timer_handler()调用周期太长。2. 刷屏未使用DMACPU被SPI阻塞。3. 缓冲区太小导致分块太多开销大。4. 系统时钟频率太低。1. 在lvgl_task中减少vTaskDelay的时间比如从10ms改为5ms。2.必须启用DMA。3. 在内存允许范围内增大部分缓冲区的大小。4. 检查MCU主频是否配置到最高。点击按钮等控件无反应1. 触摸坐标正确但未与显示驱动关联。2. 控件本身未启用点击事件。3. 输入设备读取周期LV_INDEV_DEF_READ_PERIOD设置过长。1. 检查lv_port_indev_init中是否调用了lv_indev_set_display将输入设备与显示关联。2. 确保按钮等控件使用了lv_obj_add_event_cb添加了事件回调。3. 在lv_conf.h中减小LV_INDEV_DEF_READ_PERIOD的值。编译后RAM或Flash溢出1. 启用了太多LVGL功能。2. 堆栈设置太小。3. 缓冲区开得太大。1. 在lv_conf.h中关闭不用的控件和功能如LV_USE_CANVAS,LV_USE_CHART。2. 在Keil的Target选项和FreeRTOS的任务创建中增加堆栈大小。3. 改用部分缓冲并减少缓冲区行数。使用LV_MEM_SIZE监控内存使用。6.3 字体、图片与文件系统集成添加自定义字体LVGL支持从C数组或外部文件加载字体。使用官方工具LVGL Font Converter在线转换.ttf字体为C文件。将生成的.c文件加入工程并在代码中声明并使用。LV_FONT_DECLARE(my_font_20); // 声明字体 lv_obj_set_style_text_font(label, my_font_20, 0); // 应用字体显示图片同样使用LVGL Image Converter工具将.png或.jpg图片转换为C数组或二进制文件。如果使用文件系统可以保存为.bin文件通过lv_img_set_src(img, “S:/path/to/img.bin”)加载。集成文件系统FATFS如果通过CubeMX启用了FATFS和SD卡需要在lv_port_fs.c中实现open,read,close等接口的映射将LVGL的文件操作指向FATFS。这样LVGL就能从SD卡读取字体、图片和主题文件极大节省MCU的Flash空间。移植完成后你的工程就成为了一个强大的GUI应用平台。你可以基于此利用LVGL丰富的控件和样式系统设计出美观、交互流畅的用户界面。这份移植好的工程文件不仅是一个起点更是一个经过验证的稳定基础你可以将其应用到不同的STM32项目中去。
返回列表