最近在做一块 1.54 寸 240x240 的 ST7789 彩屏小项目把 LVGL 9.1 成功跑到了 STM32F103C8T6 上。折腾过程中踩了不少坑网上能找到的 LVGL 9.x 移植教程又大多停留在 8.x接口和配置差别不小。这篇笔记就把完整流程捋一遍从 Keil 工程搭建、LVGL 9.1 源码整合、lv_conf.h 关键配置到 ST7789 的 SPI 驱动和 flush 回调再到常见黑屏、花屏的排查方法一步一步说清楚。适合手里有 F103 最小系统板、想低成本玩上 GUI 的朋友也适合想把老项目 UI 升级成 LVGL 的嵌入式开发者。1. 项目方案与整体设计1.1 为什么是这个组合F103C8T6、ST7789、LVGL 9.1STM32F103C8T6 是我特别爱用的入门级 MCU72MHz 主频、64KB Flash、20KB SRAM价格便宜、资料多、开发工具链成熟。很多人第一反应是20KB 内存跑 LVGL 够吗我最初也怀疑过。后来实测发现LVGL 9.1 对资源的要求比想象中低关键在于配置别堆太满渲染区域别开全屏缓冲区跑个表盘、菜单、简单图表完全没问题。ST7789 是当前小尺寸彩屏里性价比最高的驱动 IC 之一常见于 0.96 寸、1.3 寸、1.54 寸等模块分辨率一般是 240x240 或 240x320SPI 接口只需要占用 4~6 根 GPIO非常适合 F103 这种引脚不算多的芯片。市面上很多屏虽然外壳印着1.54 IPS实际驱动就是 ST7789选模块时看清楚描述就行。LVGL 9.1 是 2024 年下半年发布的大版本相比 8.x 在内核、显示接口、控件体系上都有调整。既然要新学就没必要守着 8.3 的老接口不放虽然网上 8.x 教程多但 9.x 才是未来的主线。9.1 在动画、渲染性能、控件样式上都做了不少优化跑在 F103 上只要能控制好刷新区域和内存分配体验并不差。这套组合的定位很清晰花几十块钱在巴掌大的屏幕上看数据、点按钮、翻页面做一个能展示的小型 GUI 设备。无论做桌面温湿度计、锂电池监测、还是纯粹的 UI 学习板都非常合适。1.2 用 LVGL 9.1 还是 8.3版本差异先看明白如果你是从 8.3 的经验直接上手 9.1最痛苦的是接口变了。我把它当成两个不完全一样的库来处理。具体变化包括几个方面显示设备创建从lv_disp_drv_register变成了lv_display_create刷新回调名从disp_drv.flush_cb变成lv_display_set_flush_cb。绘制缓冲区相关函数也完全不同9.x 用的是lv_display_set_buffers而 8.x 是lv_disp_draw_buf_init。结构上LVGL 9 虽然保留了lv_disp_前缀的部分兼容宏但底层调度逻辑已经重写建议直接用新 API。内存管理也改了。8.x 习惯配LV_MEM_SIZE9.x 换成了LV_MEM_POOL_SIZE。如果你打开lv_conf.h发现找不到LV_MEM_SIZE别慌搜索LV_MEM_POOL_SIZE就行。内部动态内存、日志、缓存刷新的实现都有变化。动画和样式方面9.x 把更多绘制工作放到了执行时动态分配上所以对内存池大小的要求会比 8.x 略高一些。但反过来9.x 对脏矩形区域的裁剪更激进局部刷新时实际刷屏的数据量反而可能更小。也就是说跑小界面时 9.1 不一定会更吃内存。所以我的建议很简单新项目直接上 9.1。如果手里有很成熟的 8.3 工程那也没必要折腾迁移能用就继续用。1.3 内存账本20KB RAM 到底够不够跑STM32F103C8T6 的 SRAM 是 20KB用之前先算笔账。显示缓冲区是最大的开销。屏幕分辨率 240x240RGB565 一个像素 2 字节整屏需要 115200 字节也就是约 112.5KB这远远超出了 RAM 的上限。所以必须采用局部缓冲区方案。LVGL 9.1 在lv_display_set_buffers里可以通过LV_DISPLAY_RENDER_MODE_PARTIAL让图形库只使用一块很小的渲染目标刷新时自动分成多个区域写入屏幕。我实测下来F103 上开 240x20 的缓冲区240202 9600 字节约 9.4KB是比较舒服的一个点渲染效率不错剩余内存依然有 10KB 左右给 LVGL 对象、动画和用户变量用。内存池方面LVGL 内部对象、控件、样式、事件等都需要从内存池分配。我在这个项目里设置了 8KB 的LV_MEM_POOL_SIZE。跑一个包含标签、进度条、按钮、图表的页面打开全屏动画内存池使用率大概维持在 60%~70%。如果你同时放很多图片资源内存池建议加到 10KB但这时候要特别注意总 RAM 余量。从 Keil 的Map文件里可以看到LVGL 9.1 自带驱动、字体和控件在 Flash 上占了大约 30~40KB加上 STM32 标准外设库和用户代码64KB Flash 还能剩 10KB 以上。前提是别把字体全量使能——这点后面专门说。2. 环境搭建与Keil工程目录规划2.1 安装Keil MDK与芯片支持包Keil MDK 是 ARM 平台最常用的 Windows IDE下载和安装都很直接。需要注意几点第一安装路径别带空格和中文比如默认的C:\Keil_v5就很好有些人为了省事装到桌面后面编译器容易出诡异问题。第二装完 MDK 后必须安装 F1 系列芯片支持包在 Pack Installer 里搜索STM32F1xx_DFP安装 2.x 以上的版本。没有支持包就没有STM32F103C8这个器件型号新建工程时压根选不到芯片。关于许可证我建议优先考虑 MDK 的社区版或评估模式个人学习完全够用别一上来就搜什么注册机。Keil 现在的授权检查很严格乱装破解版还可能出现编译到一半弹错误的问题反而浪费时间。芯片包、编译器、调试器最好都用对应版本配套省得后面对半天。2.2 新建工程把LVGL源码塞进去新建工程时选STM32F103C8型号在Manage Run-Time Environment里勾选CMSIS CORE和Device Startup这样启动文件和系统初始化文件就自动进来了。外设库方面用户可以根据习惯二选一标准外设库SPL或 HAL 库。我这次用的是标准库因为工程比较小标准库代码直白、编译快也没有多少抽象层次。LVGL 9.1 的源码可以从 GitHub 仓库下载release/v9.1分支的 zip 包。解压后整个src目录都可以参与编译。在 Keil 里建一个lvgl的 Group把src目录下的.c文件全部添加进去。具体做法是在 Project 窗口右键选择Add Existing Files找到lvgl\src目录全选.c文件加入。因为 LVGL 的源码是按子目录分类core、draw、widgets、drivers 等Keil 支持直接添加整个目录里的文件。需要注意9.x 源码里自带了一个lv_conf_template.h模板在lvgl根目录下。但要真正启用配置需要把lv_conf.h从lvgl目录复制到你自己的项目根目录下或放到某个 include 路径里。然后打开lv_conf.h把文件开头#if 0改成#if 1否则整个配置文件都不会生效会出现LV_COLOR_DEPTH找不到之类的编译错误。Include 路径至少要添加三个项目根目录放lv_conf.h、lvgl根目录、lvgl\src。如果你还用了LV_CONF_INCLUDE_SIMPLE系统会在头文件搜索路径里找lv_conf.h这种情况下尤其要确认项目根目录在 include 路径里。2.3 接线与引脚映射ST7789到底接在哪ST7789 液晶模块大部分是 SPI 接口常见的引脚有VCC、GND、CS、RES、DC、SDA、SCK、BLK这 8 个。CS是片选DC是数据/命令选择脚RES是复位SDA是 MOSISCK是时钟BLK是背光控制。具体接哪几个 GPIO可以灵活配置我这次用的是 SPI1 的一组默认引脚屏幕引脚STM32F103C8T6 引脚说明VCC3.3V电源GNDGND地CSPA4片选软件控制RESPA2复位软件控制DCPA3数据/命令选择SDAPA7SPI1_MOSISCKPA5SPI1_SCKBLK3.3V 或 GPIO背光直接接高电平最省事我这里没用 MISO因为 ST7789 只写不读四线 SPI 的发送方向就够了。如果用的是硬件 SPI1PA5 和 PA7 是复用功能需要把对应引脚配置为GPIO_Mode_AF_PP。CS、DC、RES 这三个脚用普通推挽输出就行。引脚不是死的。如果你手里模块正好占了 PA2/PA3 这些引脚完全可以换到 PB0/PB1、PA8/PA9 这类脚上只要代码里保持一一对应即可。硬件 SPI1 默认引脚是固定的不想改硬件引脚就固定用 PA5/PA7如果非要用其他引脚又不想软件模拟 SPI就得换到 SPI2PB13/PB15或者自己写 GPIO 翻转模拟时序。2.4 编译器设置C99和优化等级LVGL 9.1 的源码大量使用了 C99 特性Keil MDK 自带的 AC5 编译器默认只按 C90 标准直接编译会报出一大堆unrecognized declaration之类的错误。解决办法是在Options for Target C/C页面的Misc Controls里填入--c99。如果你用的 AC6armclang默认标准已经是 C11一般不用额外设置。另外宏定义也很关键。标准库工程需要定义USE_STDPERIPH_DRIVERHAL 库工程需要定义USE_HAL_DRIVER,STM32F103xB。如果没有这个宏外设寄存器地址和外设库的头文件都对应不上编译能过但运行起来引脚配置会乱套。优化等级的设置也值得注意。调试阶段我建议先选-O0否则看变量值、单步调试都会很折磨人LVGL 内部结构体在优化后经常被寄存器缓存Watch 窗口里看到的值是假的。等整个界面能正常显示再切到-O1或-O2实测 F103 跑 -O2 时渲染速度能提升 20% 左右但出现 bug 后难定位我会先保留 -O1 作为稳定版本。3. LVGL 9.1移植配置详解3.1 lv_conf.h先打开这个开关LVGL 的几乎所有行为都由lv_conf.h控制这个文件既是配置文件也是和源码交互的桥梁。第一步就是确认#if 1确实生效了。然后是LV_CONF_INCLUDE_SIMPLE建议设置为 1。这样源码内部#include lv_conf.h时系统会直接搜索 include 路径不需要写很长的相对路径。LV_COLOR_DEPTH在 9.x 里同样存在默认是 32这样在 F103 上内存开销会翻好几倍必须改成 16。对应 ST7789 的 RGB565 格式LV_COLOR_16_SWAP这个宏要看具体屏幕模块大多数 ST7789 模块的 SPI 字节序是高位在前但有一部分国产模块要求字内字节交换。这个配置不对屏幕显示的颜色会偏得离谱具体表现是红色和蓝色对调、绿色发紫很典型。字体配置是 Flash 消耗大户。默认模板里开了大量字体包括 12/14/16/20/24/32 号以及中文相关的字体如果全部使能Flash 很容易超过 64KB。我的建议是只保留LV_FONT_DEFAULT对应的一个字体比如 14 号再酌情开启LV_FONT_MONTSERRAT_16和数字符号字体。中文字体优先用自己生成的子集或者干脆先用 LVGL 内置的 Montserrat 字体只在需要时引入中文字库。3.2 分辨率、颜色深度与内存池配置在lv_conf.h里有几个宏是 F103 这块板上必须调整的LV_HOR_RES_MAX和LV_VER_RES_MAX在 9.x 中仍然保留分别设为 240 和 240。虽然显示驱动创建时也会指定分辨率但这两个宏负责编译期优化比如裁剪算法的静态数组分配必须和实际屏幕一致。LV_MEM_POOL_SIZE是 LVGL 内部动态内存池大小。我在这个项目的最终配置是#define LV_MEM_CUSTOM 0 #define LV_MEM_POOL_SIZE (8 * 1024)LV_MEM_CUSTOM为 0 表示使用 LVGL 自己管理的内存池不依赖 C 库的malloc这在裸机项目里最可靠。如果你改成 1会用标准库的malloc/free那就需要调大 Keil 工程里的 Heap 区域。我用 8KB 内存池跑一个包含按钮、仪表盘、标签、列表的界面内存池占用率约 70%剩余空间够放临时对象。LV_DEF_REFR_PERIOD是刷新周期默认 33ms对应 30fps 的刷新频率。F103 渲染 240x240 区域时实际帧率到不了 30fps所以把刷新周期调到 20ms50fps没有意义反而增加 CPU 负担。建议保留默认值 33ms 或放宽到 40ms画面看起来更稳定。3.3 初始化流程lv_init 与显示设备LVGL 9.1 初始化代码很短但顺序不能乱。在我的工程里lv_init加显示设备初始化的核心流程如下static lv_display_t *disp; void lvgl_port_init(void) { lv_init(); ST7789_Init(); // 先初始化液晶屏 disp lv_display_create(240, 240); lv_display_set_flush_cb(disp, lvgl_flush_cb); lv_display_set_buffers(disp, buf_1, NULL, sizeof(buf_1), LV_DISPLAY_RENDER_MODE_PARTIAL); // 设置颜色格式需要和 lv_conf.h 里 LV_COLOR_DEPTH 对应 lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565); }这里我用的是单缓冲方式buf_2传NULL。LVGL 9.x 官方推荐双缓冲因为双缓冲可以让渲染和刷屏重叠减少撕裂。但 F103 只有 20KB RAM如果开双缓冲每个缓冲 240x20 就需要 19KB 左右再加上代码的栈和 LVGL 内存池实在太紧张。所以我选择了单缓冲 DMA实测下来画面轻微闪烁可以接受稳定性反而更好。LV_DISPLAY_RENDER_MODE_PARTIAL指定了局部渲染模式这样 LVGL 在渲染时只会把脏矩形区域分块输出到我的缓冲区里不会尝试一次性生成整帧画面。3.4 心跳时基让LVGL动起来LVGL 是一个事件驱动加周期刷新的 GUI 库它的动画、长按、双击、时间刷新都依赖一个毫秒级时基。这个时基必须由用户提供。最简单的做法是在 SysTick 中断里调用lv_tick_inc(1)void SysTick_Handler(void) { systick_count; lv_tick_inc(1); }注意如果你用了 STM32 HAL 库SysTick 通常已经被 HAL 的HAL_GetTick占用了。这时候再用 SysTick 调 LVGL 会有冲突。建议用一个通用定时器比如 TIM2配置成 1ms 中断在定时器中断里调lv_tick_inc(1)。我这次因为用的是标准库SysTick 没被占用直接就用它了。然后主循环里这样写while (1) { lv_timer_handler(); delay_ms(5); }lv_timer_handler负责处理事件、动画、刷新脏矩形以及调用显示驱动的 flush 回调。10ms 内至少调用一次否则动画会显得卡顿。我这里的delay_ms(5)是给其他任务一个时间片如果项目只有 UI可以把这个延时去掉直接让lv_timer_handler全速跑。4. ST7789驱动编写从SPI到画面4.1 驱动初始化序列为什么这些寄存器顺序不能乱ST7789 是一个很成熟的 LCD 驱动 IC网上能找到大量初始化代码但千万不要随意从网上复制一把就完事有些序列针对不同厂家玻璃面板做了调整直接套用可能屏幕亮度极低或者画面泛白。这里给一份我实测在多家 240x240 模块上都稳定的序列void ST7789_Init(void) { ST7789_Reset(); ST7789_WriteCmd(0x01); delay_ms(150); // 软复位 ST7789_WriteCmd(0x11); delay_ms(120); // 退出睡眠 ST7789_WriteCmd(0x36); ST7789_WriteData(0x00); // MADCTL旋转方向 ST7789_WriteCmd(0x3A); ST7789_WriteData(0x05); // 16位像素 RGB565 ST7789_WriteCmd(0xB2); ST7789_WriteData(0x0C); ST7789_WriteData(0x0C); ST7789_WriteData(0x00); ST7789_WriteData(0x33); ST7789_WriteData(0x33); // 电压、伽马相关寄存器 ST7789_WriteCmd(0xB7); ST7789_WriteData(0x35); ST7789_WriteCmd(0xBB); ST7789_WriteData(0x3F); ST7789_WriteCmd(0xC0); ST7789_WriteData(0x0C); ST7789_WriteCmd(0xC2); ST7789_WriteData(0x01); ST7789_WriteCmd(0xC3); ST7789_WriteData(0x13); ST7789_WriteCmd(0xC4); ST7789_WriteData(0x20); ST7789_WriteCmd(0xC6); ST7789_WriteData(0x0F); ST7789_WriteCmd(0xD0); ST7789_WriteData(0xA4); ST7789_WriteData(0xA1); ST7789_WriteCmd(0xE8); ST7789_WriteData(0x03); ST7789_WriteCmd(0xF0); ST7789_WriteData(0x00); ST7789_WriteCmd(0x21); delay_ms(20); // 显示反转 ST7789_WriteCmd(0x29); delay_ms(20); // 开显示 }0x36的 MADCTL 寄存器的值很关键它控制扫描方向和 RGB/BGR 顺序。0x00表示从左上角开始扫描RGB 顺序。如果画面方向不对或者显示在镜像位置调整这个值就行常见值包括0x00、0x60、0xA0、0xC0等分别对应旋转 0°、90°、180°、270° 的部分情况。0x3A设置为0x05表示 16 位色深RGB565。有些模块支持 18 位色深但 LVGL 里配置成 16 位渲染效率更高SPI 传输数据量也更小。这里的0x05不能写成0x55虽然两者都标称 16 位但不同厂家的寄存器定义不完全一样网上到处抄的0x55其实来自 ILI9341 是可信的用在 ST7789 上会导致颜色量化为 RGB666然后出现严重偏色。我实测过ST7789 的 COLMOD 寄存器写0x55会让画面明显发灰。4.2 像素填充函数flush_cb是显示性能的关键LVGL 渲染完一帧内容后会调用显示设备上注册的 flush 回调把内存缓冲区里的像素数据发送给屏幕。这个回调就是整个显示性能的核心。我实现的 flush 函数是这样的void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w lv_area_get_width(area); uint32_t h lv_area_get_height(area); ST7789_SetWindow(area-x1, area-y1, area-x2, area-y2); ST7789_WriteDataBulk(px_map, w * h * 2); lv_display_flush_ready(disp); }ST7789_SetWindow通过设置列地址和行地址告诉屏幕接下来填充的数据要写到哪个区域。只有把窗口设置正确LVGL 的局部刷新才能生效否则会出现第一帧正常后面越刷越花的问题。窗口设置的实现如下void ST7789_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { ST7789_WriteCmd(0x2A); ST7789_WriteData(x1 8); ST7789_WriteData(x1 0xFF); ST7789_WriteData(x2 8); ST7789_WriteData(x2 0xFF); ST7789_WriteCmd(0x2B); ST7789_WriteData(y1 8); ST7789_WriteData(y1 0xFF); ST7789_WriteData(y2 8); ST7789_WriteData(y2 0xFF); ST7789_WriteCmd(0x2C); }ST7789_WriteDataBulk是批量发送函数。在标准库下我直接用 SPI 发送接口连续发送一段数据。注意LVGL 和屏幕交互时不需要在 flush 函数里加大量延时。传输完成后一定要立刻调用lv_display_flush_ready(disp)告诉 LVGL 这块缓冲区已经用完了可以继续渲染下一块。如果忘了调用界面会卡死或只刷新一部分这也是新手最容易踩的坑。4.3 加不加DMA性能与稳定性的取舍在 F103 上把像素数据从缓冲区搬到 SPI 外设有两种方式CPU 阻塞发送和 DMA 后台搬运。CPU 阻塞发送的代码简单但一次 flush 可能需要几毫秒到十几毫秒期间 CPU 不能干别的。DMA 方式在发送期间可以回过去继续跑 LVGL但同步逻辑要处理清楚。我用 DMA 时的 flush 代码加入了一个传输完成标志volatile uint8_t dma_tx_done 1; void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { ST7789_SetWindow(area-x1, area-y1, area-x2, area-y2); // 等待上一次 DMA 传输结束 while (dma_tx_done 0); dma_tx_done 0; DMA1_Channel3_ClearFlag(DMA1_FLAG_TC3); DMA1_Channel3_Config((uint32_t)px_map, (uint32_t)SPI1-DR, w * h * 2); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_TX, ENABLE); } void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_TX, DISABLE); dma_tx_done 1; lv_display_flush_ready(disp_global); } }关键是lv_display_flush_ready必须在 DMA 真正传输完成后调用否则 LVGL 会认为缓冲区已经空闲直接往里面写新的像素数据但 DMA 可能还在读旧数据最终出现画面撕裂或者花屏。如果项目预算和引脚足够我建议优先用 SPI 的 DMA 方式。整个 F103 在刷局部小窗口时CPU 占用能低很多特别是做动画的时候画面会更跟手。缺点是多了一些中断回调的细节需要在初始化时打开 DMA1 通道 3 的中断。5. 从黑屏到流畅显示常见问题排查与优化5.1 黑屏和花屏先查这些我在调试过程中遇到的第一个问题是屏幕完全黑屏。黑屏最大的排查点在于硬件初始化链路。先看 RES 引脚很多 ST7789 模块的复位脚需要外部下拉电阻如果你的模块没有自带上拉代码里复位时序又不对上电后屏幕会一直死在复位状态。确保代码里有拉低 20ms 再拉高的过程并且拉高后延时至少 120ms 再发初始化命令。然后是背光脚 BLK。有些模块的背光默认由 3.3V 供电接上就亮有些模块的 BLK 必须由 GPIO 输出高电平控制。如果屏幕只是背光亮但没画面问题多半在 SPI 通信或初始化命令上。用逻辑分析仪拉一下 MOSI 和 CS 脚确认命令有没有发出去是最快的定位方式。花屏的情况就更复杂一些但最常见的原因有三类窗口设置不对、像素字节序反了、SPI 速率太高。窗口设置不对时第一帧往往正常第二帧开始出现整块颜色错位。字节序反了会导致颜色完全不是预期颜色比如红色显示成蓝色。SPI 速率太高时画面会出现细横条纹或随机色块这种情况把 SPI 分频系数调大比如从 4 分频改成 8 分频症状通常就消失了。5.2 屏幕颜色偏色或“像素错位”的处理颜色偏蓝或偏黄或者红色和蓝色对调要从几个方向看。首先确认LV_COLOR_DEPTH是 16且 ST7789 的 COLMOD 是 16 位模式。如果两边都是 16 位但颜色还是不对那就是 RGB 顺序的问题。ST7789 默认可能是 RGB 顺序但很多模块硬件上换成 BGR此时要在 MADCTL 的第 3 位置 1#define ST7789_MADCTL_BGR 0x08把0x00改成0x08通常就能解决红蓝对调问题。如果颜色发灰、对比度低多半是 COLMOD 写成了 18 位模式或者像素格式设置成 RGB666建议仔细核对初始化序列。还有一种像素错位看起来像画面被切成了好几块每块内容都重复这种通常是行地址和列地址的窗口设置超出屏幕范围或者 flush 回调里的area坐标没有正确传递给 ST7789。LVGL 9.1 的 flush 回调里area是绝对坐标不是相对坐标直接传给屏幕驱动即可不要自行加减偏移除非屏幕面板本身有偏移。5.3 Keil编译链接报错的几种经典案例移植过程中最容易遇到的编译错误是打开lv_conf.h后依然报LV_COLOR_DEPTH is not defined。这种问题基本都是配置没有生效检查两个位置第一lv_conf.h开头的#if 0是不是改成了#if 1第二include 路径里能不能搜到这个文件。Keil 在Options for Target C/C Include Paths里手动加路径后一定要点确定有时候只加了工程视图路径不会生效。另一个高频错误是variable lv_disp_drv_t has initializer but incomplete type或者unknown type name lv_disp_drv_t。这通常是 LVGL 版本不对。8.3 和 9.1 的头文件结构不同网上教 9.x 的教程如果用 8.x 的习惯写就会报这种错误。我从一开始就锁定 9.1 的 API不混合用。链接时报undefined symbol __aeabi_assert也很常见原因在于 LVGL 开启了断言和日志但工程里没有对应的支持。把LV_USE_ASSERT_STYLE、LV_USE_LOG等调试宏打开后LVGL 可能会引用一些额外的辅助函数需要在lv_conf.h里关闭或者确保lv_assert相关宏配置正确。对于 F103 这种小资源目标正式版本建议把断言全部关掉能省不少代码空间。5.4 实测帧率与进一步优化建议我在 240x240 的 ST7789 上SPI 时钟 18MHzCPU 主频 72MHz不开 DMA 时一个包含动画的界面整体流畅度大概在 15fps 左右开了 DMA 后简单界面的帧率能到 25fps 左右。如果刷新频繁的区域很小LVGL 的局部刷新机制会让帧率看起来更高。进一步优化可以尝试三个方向第一把 SPI 时钟从 18MHz 提到 36MHz但要注意部分 ST7789 模块布线质量一般可能需要降低第二用双缓冲替换单缓冲画面会更平滑但内存要算好第三尽量不整屏刷新使用lv_obj_invalidate只标记需要更新的控件。我的经验是F103 上最影响流畅度的不是 CPU 算不过来而是 SPI 吞吐量所以 DMA 带来的收益最明显。界面设计上也别太贪心F103 跑 LVGL 9.1 虽然可行但控件不要叠加太多阴影、圆角、半透明效果这些视觉效果在 F103 上的渲染开销很大。把字体、边长、颜色、背景图控制在小范围体验会好很多。最后再分享一个小技巧如果屏幕显示方向不对除了改 MADCTL还可以在lv_display_set_resolution里把分辨率交换一下比如把 240x240 改成 240x240 看不出差别但如果是 240x320 的 ST7789 模块横竖屏切换就靠这两个数值和 MADCTL 配合。调试方向问题的时候先用一个全屏纯色块刷一遍比跑 LVGL 官方 demo 更容易定位是屏幕方向问题还是 LVGL 坐标问题。