
简介本资源是一套面向嵌入式开发者与RTOS进阶学习者的LVGL图形库实战移植方案聚焦STM32F407硬件平台在RT-Thread操作系统环境下通过FSMC总线驱动TFT液晶屏完整集成LVGL V9最新版本。资源解决了GUI框架在资源受限MCU上与实时操作系统协同运行、显存管理、触摸交互及性能调优等典型工程难题适用于智能仪表、HMI人机界面、教学实验等场景。压缩包共2000个文件含949个C源码含LVGL核心渲染、RT-Thread驱动适配层、908个头文件定义接口与配置、50份Markdown文档含移植说明、配置指南与API速查、42个Python脚本用于字体生成、资源转换等自动化任务整体大小24.16MB。已有454人学习下载提供从底层FSMC时序配置、LVGL显示缓冲区分配、RT-Thread线程调度整合到示例UI控件演示的全链路可运行代码目录结构按模块分层清晰便于快速定位驱动、GUI、系统集成等关键子系统。1. 项目概述与核心价值最近在做一个基于STM32F407的工控HMI项目客户对界面的流畅度和美观度提出了新要求之前的裸机简单GUI的方案已经有点力不从心了。正好LVGL发布了V9版本带来了不少性能提升和新特性就决定把整个图形栈升级一下。我的硬件平台是正点原子的探索者开发板主控STM32F407ZGT6外接了一块4.3寸的RGB接口屏通过FSMC总线驱动。软件层面我选用了rt-thread这款国产实时操作系统它的组件生态和驱动框架能省不少事。这个“基于STM32F407rt-threadFSMC接口屏移植LVGL V9”的项目说白了就是在资源有限的MCU上搭建一个稳定、流畅且功能丰富的图形用户界面开发环境。如果你也在为STM32上的炫酷界面发愁或者想从LVGL V8升级到V9这篇从零到一的踩坑实录应该能帮到你。为什么是这套组合拳STM32F407自带FSMCFlexible Static Memory Controller可变静态存储控制器它能以接近内存访问的速度驱动8080并口屏解决刷屏的速度瓶颈。rt-thread提供了线程管理、信号量、消息队列等基础设施让LVGL的定时器、任务调度和事件处理有了依托比裸机轮询更优雅。LVGL V9相较于V8在内存管理、渲染管线、动画系统上都有优化对MCU更友好而且官方对rt-thread的支持也越来越完善。这次移植的核心目标就是让这三者协同工作在有限的RAM和Flash里跑起一个响应迅速、支持中文、能承载复杂交互的GUI应用。2. 硬件平台与软件环境搭建2.1 硬件选型与连接要点我的核心硬件是正点原子探索者开发板主芯片STM32F407ZGT6拥有192KB的RAM和1MB的Flash这对于运行RTOS和LVGL是基本够用的底线。屏幕是4.3寸、480x272分辨率的RGB接口屏驱动芯片通常是ILI9341或类似的通过16位8080并口与MCU通信。FSMC接口屏的连接是关键一旦接错轻则花屏重则无法初始化。FSMC引脚连接对照表以Bank1 sector4为例对应NE4片选STM32F407引脚FSMC信号线8080屏引脚作用说明PF12FSMC_A6RS (寄存器选择)命令/数据选择。0写命令1写数据。PD0FSMC_D2D2数据线低8位的一部分。PD1FSMC_D3D3数据线低8位的一部分。.........数据线D0-D15需一一对应连接。PD4FSMC_NOERD (读使能)读操作使能信号低有效。PD5FSMC_NWEWR (写使能)写操作使能信号低有效。PG12FSMC_NE4CS (片选)片选信号低有效。选择Bank1的sector4。注意1RSA6地址线的选择8080屏的RS引脚决定了当前操作的是命令寄存器还是数据寄存器。我们将其连接到FSMC的一根地址线上例如A6。这样当我们向不同的“内存地址”写数据时FSMC会自动控制A6RS的电平。例如定义#define LCD_CMD_ADDR (0x6C000000)和#define LCD_DATA_ADDR (0x6C000000 | (1 6))向后者写数据时A61即操作数据寄存器。注意2FSMC时序配置在stm32f4xx_hal_conf.h中使能FSMC后需要在CubeMX或手动代码中配置FSMC时序参数如地址建立时间(ADDSET)、数据建立时间(DATAST)。这些参数必须参考屏幕数据手册的时序图来设置太快会导致写入失败太慢会影响刷屏速度。对于ILI9341通常ADDSET设为1DATAST设为10单位是HCLK周期是一个比较保守且通用的起点。2.2 软件框架与包管理器准备我选择使用rt-thread的Env工具和包管理器这是管理项目依赖的神器。首先你需要安装好rt-thread的Env环境并确保pkgs --upgrade更新了软件包列表。创建RT-Thread项目可以在已有的BSPBoard Support Package板级支持包上修改比如bsp/stm32/stm32f407-atk-explorer。我用的是RT-Thread Studio它基于Eclipse图形化配置起来比较直观。开启FSMC驱动在RT-Thread Settings或menuconfig中找到“硬件驱动配置” - “启用SPI/UART等设备驱动” - “启用FSMC驱动”。确保FSMC的初始化代码会被生成并调用。添加LVGL软件包这是最关键的一步。在包管理器中搜索LVGL。rt-thread的包仓库里通常有LVGL v8.3.x的版本。但我们要V9所以需要手动指定。方法一推荐使用最新master分支在Env中进入项目目录执行pkgs --update。然后编辑packages/packages/packages/Kconfig文件路径可能略有不同找到LVGL的配置项将其下载链接指向LVGL官方的GitHub仓库V9分支例如https://github.com/lvgl/lvgl.git。方法二使用已发布的V9 tag如果包管理器提供了V9选项直接选择即可。如果没有就需要像方法一那样手动修改Kconfig指向特定tag如https://github.com/lvgl/lvgl.git#refs/tags/v9.0.0。配置LVGL参数选中LVGL包后进入其详细配置。这里有一堆参数需要根据你的硬件调整LV_COLOR_DEPTH: 设为16。我们的屏是RGB56516位色深刚刚好32位太浪费。LV_MEM_SIZE: 这是给LVGL动态分配的内存池大小。F407有192KB RAM要分给系统、堆栈、帧缓冲。我建议初始设置为(48 * 1024)即48KB。如果界面复杂可以加大但务必留足系统余量。LV_USE_FS_STDIO: 如果要从文件系统加载图片字体可以开启。LV_USE_LOG: 务必开启并设置好打印级别调试时它是你的眼睛。LV_TICK_CUSTOM: 必须开启因为我们要用RT-Thread的系统时钟来为LVGL提供心跳。开启后需要在代码中实现lv_tick_inc(ms)函数。LV_USE_OS: 选择RT-Thread。这样LVGL会创建自己的任务线程与你的应用线程分开。配置完成后在Env中执行scons --targetmdk5或IAR/VS生成工程所有依赖的源代码LVGL、相关驱动都会自动下载并加入项目非常方便。3. 驱动适配与核心移植步骤3.1 FSMC屏驱对接LVGL显示接口LVGL需要一个“显示驱动”回调函数来操作屏幕。我们的任务就是实现这个驱动并在其中调用FSMC的写函数。首先在RT-Thread的驱动框架下FSMC设备通常被注册为一个“块设备”或者我们直接使用HAL库函数。我倾向于封装一个简单的写函数直接操作我们定义好的内存映射地址。// lcd_fsmc.c #include “drv_fsmc.h” // RT-Thread提供的FSMC驱动头文件或自定义 // 假设通过FSMC Bank1 sector4驱动基地址0x6C000000 #define LCD_BASE_ADDR ((uint32_t)0x6C000000) #define LCD_CMD_ADDR (LCD_BASE_ADDR) #define LCD_DATA_ADDR (LCD_BASE_ADDR | (1 6)) // A61 for data static void lcd_write_cmd(uint16_t cmd) { *(__IO uint16_t *)LCD_CMD_ADDR cmd; } static void lcd_write_data(uint16_t data) { *(__IO uint16_t *)LCD_DATA_ADDR data; } // 更高效的连续写数据函数用于填充区域 static void lcd_write_data_multiple(uint16_t *data, uint32_t count) { volatile uint16_t *data_addr (__IO uint16_t *)LCD_DATA_ADDR; while(count--) { *data_addr *data; } }然后实现LVGL的flush_cb回调函数。这个函数会在LVGL渲染好一块区域area的图像后调用我们需要把这块图像数据color_map搬运到屏幕的对应位置。static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_map) { // 1. 设置屏幕的显示窗口行列起始和结束地址 lcd_set_window(area-x1, area-y1, area-x2, area-y2); // 2. 准备写GRAM lcd_write_cmd(0x2C); // ILI9341的写GRAM命令 // 3. 将LVGL渲染好的颜色缓冲区数据通过FSMC连续写入 uint32_t pixel_count (area-x2 - area-x1 1) * (area-y2 - area-y1 1); lcd_write_data_multiple((uint16_t*)color_map, pixel_count); // 4. 重要通知LVGL这块区域刷新已完成可以开始下一帧渲染 lv_disp_flush_ready(disp_drv); }最后在应用初始化时创建并配置LVGL的显示驱动对象void lv_port_disp_init(void) { // 初始化你的底层LCDFSMC、GPIO、复位等 lcd_init(); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res 480; disp_drv.ver_res 272; disp_drv.flush_cb disp_flush; // 设置刷新回调 disp_drv.buffer disp_buf; // 指向一个lv_disp_draw_buf_t缓冲区 // 注册驱动 lv_disp_t * disp lv_disp_drv_register(disp_drv); }实操心得双缓冲与局部刷新disp_drv.buffer可以配置为单缓冲或双缓冲。单缓冲时LVGL渲染和FSMC传输会争抢同一块内存容易导致撕裂。强烈建议使用双缓冲创建两个lv_color_t数组初始化lv_disp_draw_buf_t时传入它们。这样LVGL在一个缓冲区渲染时另一个缓冲区可以用于DMA传输如果支持或CPU搬运显著提升流畅度。另外确保你的lcd_set_window函数正确设置了屏幕的扫描窗口这是实现局部刷新的基础能避免全屏刷新带来的性能浪费。3.2 触摸屏与输入设备接口适配我的屏幕是电阻屏使用XPT2046芯片通过SPI接口连接。在RT-Thread中我使用PIN设备模拟SPI或者直接注册一个SPI设备驱动。首先实现触摸屏的读取函数获取原始的x, y坐标和按压状态。static bool touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data) { static int16_t last_x 0, last_y 0; uint8_t pressed 0; int16_t x 0, y 0; // 调用你的触摸屏驱动函数获取当前状态和坐标 tp_dev.scan(0); // 假设的扫描函数 pressed tp_dev.sta TP_PRES_DOWN; // 判断是否被按下 if(pressed) { x tp_dev.x[0]; y tp_dev.y[0]; last_x x; last_y y; } else { x last_x; y last_y; } >void lv_port_indev_init(void) { 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_drv_register(indev_drv); }踩坑记录坐标校准与滤波直接从触摸芯片读出的坐标通常不能直接使用需要进行两点或三点校准将其映射到屏幕像素坐标。LVGL本身不提供校准功能需要你在touchpad_read函数内部或之前完成映射。此外电阻屏容易有抖动建议加入简单的软件滤波比如取连续几次采样的中值能有效提升点击稳定性。3.3 RT-Thread系统时钟与LVGL心跳集成LVGL的动画、定时器都依赖于一个毫秒级的心跳tick。在RT-Thread中我们可以用系统时钟SysTick来提供。在LVGL的配置文件lv_conf.h中我们开启了LV_TICK_CUSTOM。现在需要实现一个函数每隔1毫秒被调用一次给LVGL“滴答”一下。最简单的方式是利用RT-Thread的定时器或直接在一个高优先级线程里延时// 创建一个1ms的软件定时器 static struct rt_timer lvgl_timer; static void lvgl_tick_timer_callback(void *parameter) { lv_tick_inc(1); // 告诉LVGL1ms过去了 } int lvgl_tick_init(void) { // 初始化一个周期为1ms的周期性定时器 rt_timer_init(lvgl_timer, “lv_tick”, lvgl_tick_timer_callback, RT_NULL, 1, // 1ms RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER); rt_timer_start(lvgl_timer); return 0; } INIT_APP_EXPORT(lvgl_tick_init); // 自动初始化更优雅的方式是挂钩到RT-Thread的SysTick钩子函数上但需要注意钩子函数的执行上下文通常在中断中不能做太复杂的操作lv_tick_inc是没问题的。注意事项心跳的准确性确保这个tick源的周期是稳定的1ms。如果使用线程sleep的方式RT-Thread线程调度的最小精度tick可能大于1ms如10ms这会导致LVGL动画卡顿或不均匀。强烈建议使用硬件定时器或RT-Thread的软定时器如上例来提供精确的1ms中断。4. LVGL V9关键配置与内存优化实战4.1 内存管理策略与缓冲区配置在MCU上玩LVGL内存是头号敌人。V9版本的内存管理有改进但配置不当依然会崩。1. 堆栈空间分配系统堆Heap在RT-Thread的board.h或链接脚本中确保堆空间足够。F407的RAM是192KB我通常分配100-120KB给堆剩下的留给全局变量、栈和LVGL的专用内存池。LVGL内存池LV_MEM_SIZE就是这块专用池的大小。它用于LVGL内部对象按钮、标签等的动态分配。48KB是一个安全的起点。你可以在lv_conf.h中修改也可以通过lv_mem_set_size()在运行时调整不推荐动态缩小。任务栈Thread Stack给LVGL的任务lv_task_handler所在线程分配足够的栈空间。在RT-Thread创建线程时我通常给lvgl_thread分配4KB以上的栈。太小会导致栈溢出现象可能是随机死机。2. 显示缓冲区Display Buffer配置这是影响性能的关键。在lv_port_disp_init中我们需要初始化lv_disp_draw_buf_t。// 定义两个缓冲区 static lv_color_t buf1[DISP_BUF_SIZE]; static lv_color_t buf2[DISP_BUF_SIZE]; static lv_disp_draw_buf_t disp_buf; void lv_port_disp_init(void) { // ... 其他初始化 lv_disp_draw_buf_init(disp_buf, buf1, buf2, DISP_BUF_SIZE); // ... 注册驱动 }DISP_BUF_SIZE怎么定它决定了LVGL一次渲染多少行像素。全屏缓冲区DISP_BUF_SIZE 屏幕宽度 * 屏幕高度。对于480x272就是130560个像素点每个点16位2字节需要约255KB内存F407总共才192KB RAM这显然不可能。所以MCU上基本不用全屏缓冲。部分缓冲区推荐DISP_BUF_SIZE 屏幕宽度 * 1010行。这样只需要480*10*2 ≈ 9.4KB内存。双缓冲也就不到19KB。LVGL会分块渲染屏幕。这个行数10是一个平衡点行数太少LVGL需要分更多次渲染一帧CPU负担重行数太多内存占用大。通常设置屏幕宽度的1/10到1/20。3. 渲染模式Render Mode选择在注册显示驱动时可以设置disp_drv.render_mode。LV_DISP_RENDER_MODE_PARTIAL部分渲染模式。只渲染屏幕上发生变化的区域效率最高。必须使用部分缓冲区。LV_DISP_RENDER_MODE_FULL全渲染模式。每次刷新都重绘整个屏幕需要全屏或大缓冲区。不推荐在MCU使用。LV_DISP_RENDER_MODE_DIRECT直接模式。不创建渲染缓冲区直接在flush_cb中绘制。对简单、静态界面可能有用但兼容性差。我们选择LV_DISP_RENDER_MODE_PARTIAL。4.2 字体与中文显示解决方案LVGL V9内置了ASCII字体但显示中文需要额外处理。方案一内置字体适用于固定文字使用LVGL官方提供的在线字体转换工具如LVGL Font Converter将你需要的特定中文字符如“确定”、“取消”、“温度”转换成C数组字体文件。优点是集成简单渲染快。缺点是字体固定无法动态显示所有汉字且字库太大会占用大量Flash。方案二文件系统字体推荐适用于动态文本如果你的项目有外部Flash或SD卡并挂载了文件系统如LittleFS这是最灵活的方案。在lv_conf.h中使能文件系统支持#define LV_USE_FS_STDIO 1并正确实现lv_fs_drv_t驱动对接RT-Thread的DFS。将.ttf或.bin格式的字体文件如思源黑体放入文件系统。在代码中动态创建字体lv_font_t * my_font; my_font lv_font_load(“S:/font/SourceHanSansSC-Medium.ttf”); // S: 是挂载点 lv_obj_set_style_text_font(label, my_font, 0);避坑指南字体缓存从文件系统加载字体特别是TTF在MCU上解析速度较慢。LVGL V9提供了字体缓存机制LV_FONT_FMT_TXT_LARGE等选项可以将常用字符的位图缓存到内存中极大提升第二次及以后渲染的速度。务必根据你的内存情况合理设置缓存大小。4.3 主题与样式优化LVGL V9的样式系统更加强大。对于MCU我们的原则是精简、复用。使用默认主题lv_theme_default_init是一个不错的起点它提供了基础的颜色和样式。创建样式对象并复用不要为每个按钮都创建新样式。为“主要按钮”、“次要按钮”、“标题文本”等创建几个全局的样式对象lv_style_t然后应用到多个对象上。static lv_style_t style_btn_main; lv_style_init(style_btn_main); lv_style_set_bg_color(style_btn_main, lv_palette_main(LV_PALETTE_BLUE)); lv_style_set_radius(style_btn_main, 5); lv_obj_t * btn1 lv_btn_create(lv_scr_act()); lv_obj_add_style(btn1, style_btn_main, 0);慎用阴影和渐变这些效果非常消耗CPU。在MCU上尽量使用纯色和简单的边框。图片优化使用工具如LVGL Image Converter将PNG/JPG转换成C数组或二进制文件并选择适当的颜色格式如RGB565。尽量使用索引色Indexed图片可以大幅减少存储空间。5. 应用架构设计与性能调优5.1 多线程与事件驱动设计在RT-Thread上合理的线程划分能让系统更稳定。LVGL主线程这个线程只做一件事——周期调用lv_task_handler()。它负责LVGL内部的所有事务渲染、动画、事件处理。我通常创建一个名为lvgl的线程优先级设为中等如10时间片给足。static void lvgl_thread_entry(void *parameter) { while(1) { lv_task_handler(); // 处理LVGL任务 rt_thread_mdelay(5); // 延时5ms相当于200Hz的刷新率控制 } } int lvgl_thread_init(void) { rt_thread_t thread rt_thread_create(“lvgl”, lvgl_thread_entry, RT_NULL, 2048, 10, 10); if(thread ! RT_NULL) rt_thread_startup(thread); return 0; } INIT_APP_EXPORT(lvgl_thread_init);应用逻辑线程你的业务逻辑比如读取传感器数据、处理网络通信等应该在另一个或几个独立的线程中运行。它们通过RT-Thread的IPC机制如消息队列、邮箱、信号量与LVGL线程通信。交互处理当触摸事件发生时LVGL在主线程内会触发对应控件的事件回调lv_obj_add_event_cb。在事件回调函数里不要执行耗时操作正确的做法是在回调里发送一个消息给应用逻辑线程让后者去处理复杂的业务然后通过lv_obj_set_xxx等线程安全的API或使用lv_async_call来更新UI。5.2 性能瓶颈分析与优化手段移植完后如果界面卡顿可以从以下几个方向排查1. 渲染性能Profile工具是神器 在lv_conf.h中开启LV_USE_PROFILER和LV_USE_PERF_MONITOR。然后在屏幕上会显示帧率FPS、渲染时间、CPU占用率等。如果FPS低于30就需要优化了。优化flush_cb这是最可能瓶颈。确保你的lcd_write_data_multiple函数是最高效的。检查是否使用了DMA如果没有CPU搬运大量像素数据会非常耗时。STM32F407的FSMC支持DMA可以尝试配置DMA2DChrom-ART加速器来搬运数据这是性能飞跃的关键。减少无效渲染区域确保LVGL的局部刷新正常工作。检查disp_flush中area参数它应该只是屏幕的一小部分而不是整个屏幕。如果总是全屏检查是否有控件设置了LV_OBJ_FLAG_HIDDEN但未真正隐藏或者样式变化导致了全局重绘。2. 内存瓶颈 使用lv_mem_monitor_t监控内存池使用情况。如果碎片严重或使用率长期高于80%考虑增大LV_MEM_SIZE或优化对象创建/删除逻辑避免频繁动态创建销毁使用对象池。3. CPU瓶颈简化界面减少控件数量特别是复杂的控件如图表、旋转图片。降低刷新率在lvgl_thread_entry中增加rt_thread_mdelay的值比如从5ms改为10ms100Hz降到50Hz可以显著降低CPU占用对于很多工业界面足够流畅。关闭动画如果不需要在lv_conf.h中设置LV_ANIM_DEF_TIME 0来禁用默认动画。5.3 稳定性与抗干扰设计工业环境下的HMI稳定是第一位的。看门狗Watchdog务必启用独立看门狗IWDG或窗口看门狗WWDG。在LVGL主线程和应用线程的关键循环中定期“喂狗”。如果某个线程卡死导致界面无响应看门狗能复位系统。异常处理在lv_task_handler外层可以加上__try/__catch如果编译器支持或判断关键函数返回值防止LVGL内部断言失败导致整个线程崩溃。电源管理如果设备是电池供电可以在无操作一段时间后调用lv_disp_set_rotation关闭背光或者让MCU进入低功耗模式LVGL有相应的睡眠/唤醒机制需要配合。6. 常见问题排查与调试技巧在实际操作中你肯定会遇到各种奇怪的问题。这里把我踩过的坑和解决方法列出来希望能帮你快速定位。问题1屏幕白屏或花屏检查FSMC时序这是最常见的原因。用逻辑分析仪或示波器抓取FSMC的写时序NOE, NWE, D[15:0], A[xx]与屏幕数据手册的时序图对比。重点看建立时间Setup和保持时间Hold是否满足。通常需要适当增加DATAST数据建立时间的配置值。检查初始化序列不同品牌的屏幕初始化命令序列可能有细微差别。确保你的lcd_init()函数发送的初始化命令和参数完全正确。最好能找到屏幕模组厂商提供的示例代码。检查电源和复位测量屏幕的VCC、背光电压是否稳定。确保复位引脚如果有的时序正确上电后进行了足够的延时再初始化。问题2触摸坐标不准或跳动校准必须进行触摸校准。实现一个校准界面记录屏幕四个角触摸时的ADC原始值然后用两点校准算法计算出转换矩阵。将校准参数保存到Flash如使用EasyFlash下次开机加载。滤波在touchpad_read函数中加入软件滤波。我常用的方法是连续采样5次去掉最大最小值取中间3次的平均值。SPI干扰如果触摸芯片通过SPI通信确保SPI的时钟线SCK和数据线MISO/MOSI远离噪声源或者在软件上适当降低SPI时钟频率。问题3LVGL任务运行一段时间后死机栈溢出这是RTOS下最常见的问题。增大LVGL线程的栈大小比如从2KB增加到4KB。可以在RT-Thread中开启栈溢出检测功能RT_USING_OVERFLOW_CHECK。内存耗尽使用lv_mem_monitor()打印内存信息看是否在频繁创建删除对象导致碎片化或者有内存泄漏创建了对象但没删除。中断冲突检查是否有高优先级中断如SysTick、定时器中断执行时间过长导致LVGL任务被长时间挂起。优化中断服务程序只做最必要的操作。问题4动画卡顿不流畅测量帧率开启性能监控看FPS是否稳定。如果波动大可能是其他高优先级任务抢占了CPU。检查缓冲区大小DISP_BUF_SIZE是否太小尝试将其增大到屏幕宽度*20或屏幕宽度*30观察流畅度变化。注意内存消耗。检查flush_cb耗时在disp_flush函数开始和结束处用GPIO翻转或定时器计时计算一次刷新的时间。如果时间接近或超过你的刷新周期如5ms那瓶颈就在写屏。考虑优化写屏函数或启用DMA。问题5中文显示为乱码或方框字体未加载确认字体文件路径正确且文件系统挂载成功。在加载字体后检查返回的lv_font_t指针是否非空。字符编码确保你的中文字符串源文件的编码是UTF-8。在Keil或IAR中项目属性里可以设置源文件编码。字体范围如果你使用内置字体确保转换工具包含了所有你需要的汉字字符。使用文件系统字体则无此问题。调试时善用LV_USE_LOG把日志级别调到LV_LOG_LEVEL_TRACELVGL会打印非常详细的内部运行信息对定位问题有奇效。可以把日志通过串口打印出来或者重定向到RT-Thread的ulog组件。本文还有配套的精品资源点击获取