ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32+ST7789+LVGL实战:从底层驱动到界面优化全记录

ESP32+ST7789+LVGL实战:从底层驱动到界面优化全记录 把一块 ST7789 控制器的 TFT 屏接到 ESP32 上再用 LVGL 把界面跑起来听起来就像“接几根线、烧个示例、屏幕亮了”这么简单但真正从零开始走下去你会撞上配置地址、颜色顺序、缓冲区大小、flush 回调时机这些暗坑。这次我把整个流程完整走了一遍从选型、接线、底层驱动、到 LVGL 移植、性能调优、常见问题排查一路记下所有关键细节和踩坑记录整理成这篇实战笔记。如果你手头正好有 ESP32 开发板、一块 ST7789 屏幕想用 LVGL 做一个带界面的小项目那这篇内容基本可以直接照着抄。它既讲怎么把屏幕点亮也讲为什么这样接、为什么这样配、为什么用这些参数让你接下来做自己的 UI 项目时不至于到处试错。1. 项目方案选型与整体设计思路1.1 为什么是 ESP32 ST7789 LVGL 这个组合先说说这块组合的来历。ESP32 是乐鑫推出的双核 Wi-Fi 蓝牙 SoC价格便宜、生态成熟社区资料一搜一大把。做带屏幕的物联网设备时它既能当主控跑 UI又能联网、接传感器、做 OTA 升级一颗芯片能把很多事都干了。相比 STM32ESP32 的优势在于自带无线能力和更大的 RAM/Flash跑 LVGL 这种需要一定内存开销的图形库时不至于太紧张。ST7789 则是一颗非常常见的 TFT LCD 驱动芯片分辨率通常是 240x240、240x320 这类小尺寸性价比极高。市面上大量 1.3 寸、1.54 寸、1.8 寸、2.0 寸的 SPI 接口屏幕很多都内置这颗芯片。它的驱动逻辑清晰SPI 接口最多只要 4 根信号线就能点亮非常适合入门和快速原型验证。LVGLLight and Versatile Graphics Library是目前嵌入式领域最流行的开源图形库之一提供了按钮、滑块、图表、列表、动画等现成控件内存占用可以做到很小却能呈现比较专业的界面效果。用 LVGL 而不是自己写像素绘制逻辑原因很现实自己做 UI 绘制、控件布局、触摸事件处理工作量极大效果还未必好。LVGL 把这些高度封装好了开发者只需要专注业务逻辑和界面设计。这三样东西组合起来基本就是一套“低成本、高上限”的带屏设备参考方案ESP32 提供算力和连接能力ST7789 提供高性价比显示输出LVGL 提供高效 UI 开发框架。你完全可以把这套组合延伸成温湿度计、时钟、桌面小电视、设备控制面板等产品原型。1.2 方案对比为什么不用其他屏幕或方案在做选型的时候我也对比过其他屏幕方案比如 OLED、RGB 接口 TFT、触摸屏一体模组。0.96 寸 OLED 虽然也很流行但尺寸小、颜色单一通常是蓝或白做稍微复杂一点的界面非常吃力。RGB 接口的屏幕显示速度快但需要占用大量 GPIO还要额外的电平转换和时序配置对新手不友好。带触摸的 ST7789 模组比如 2.4 寸电阻触摸屏虽然可玩性更高但同时要处理触摸控制器通常是 XPT2046复杂度会上升一个台阶。我这次选择的是 1.8 寸 128x160 分辨率的 ST7789 模块。为什么不是网上最常见的 240x240因为手头正好有一块而且小分辨率跑 LVGL 更流畅优化压力小。重要的是驱动库和流程对所有 ST7789 屏幕是通用的只是分辨率参数和偏移量不同所以这篇笔记里的代码和思路换到其他尺寸屏上也成立。关键还是要先确认屏幕用的主控是什么。很多 1.8 寸屏其实用的是 ST7735而不是 ST7789两者的初始化和偏移设置不同写代码前必须确认清楚。确定方式最靠谱的是看卖家给的资料或者用 SPI 读寄存器 ID 来判断这一点后面会专门讲。1.3 从底层到 UI 的分层设计这个项目我按三条层级来拆解硬件层、驱动层、应用层。硬件层负责把 ESP32 和屏幕物理连接起来包括电源、SPI 引脚、背光控制、复位控制。硬件不稳后面全是白费所以接线和电平检查是第一步。驱动层做的事情是把“往内存里写像素”变成“屏幕上显示颜色”。ST7789 本身只是一块需要按命令操作的芯片它内部有 GRAM显存我们要通过 SPI 把像素数据一列一列、一行一行写进去。这一层的核心是命令序列正确、像素格式正确、窗口地址正确。应用层是 LVGL 所在的位置。LVGL 并不知道底下接的是什么屏幕它只管在内部缓冲区里画好一帧画面然后通过一个 flush 回调函数把数据交给驱动层。这个分层设计是 LVGL 能移植到任何屏上的根本原因。想清楚这三点后续调试就有一个清晰的定位思路屏幕不亮查硬件和驱动界面花屏查格式和缓冲区卡顿查性能配置。2. 硬件准备与环境搭建2.1 需要准备的材料清单动手之前先把材料备齐。首先是 ESP32 开发板我用的是经典的 ESP32 DevKitC V4WROOM-32 模组几十块钱稳定可靠。如果手头是 ESP32-S3 或 C3也能用但引脚定义和某些外设如 RTC GPIO 数量有差异接线时需要对照自己的板子调整。屏幕方面准备一块 ST7789 驱动的 SPI TFT 模块分辨率无所谓128x160、240x240 都可以但用法略有区别。我这次用 128x160。其他辅助材料包括面包板、公对公杜邦线至少 6 根、micro USB 数据线要确认是能传数据的线很多充电线只能供电不能传数这个坑特别常见、以及一个 USB-TTL 串口模块备用用于看日志或者给没有 USB 转串口的 ESP32 模块烧录。硬件清单如下项型号/规格数量用途主控ESP32 DevKitC V4 (WROOM-32)1主控、Wi-Fi、跑 LVGL屏幕1.8 寸 128x160 SPI TFTST77891显示输出供电Micro USB 线 5V/2A 适配器1给开发板供电连接面包板 杜邦线若干连接主控与屏幕注意有些 ST7789 屏幕模块的 VCC 引脚支持 5V 输入板上带稳压和电平转换有些只支持 3.3V。接线前最好看一下模块背面的丝印说明或者测一下 VCC 引脚对地电压。拿不准时建议都是从 3.3V 供电开始测避免烧屏。2.2 接线图SPI 四根线加电源控制线ST7789 走的是 SPI 协议但模块上一般还会引出 DC数据/命令选择、RST复位、BL背光等引脚。以我的手头这块 8 脚模块为例引脚定义为 VCC、GND、SCL、SDA、RES、DC、CS、BLK。接线方案如下ESP32 引脚屏幕引脚说明3V3VCC给屏幕供电GNDGND共地GPIO18SCLSCKSPI 时钟GPIO23SDAMOSISPI 数据屏幕只需要主机写所以不用 MISOGPIO4RESRST屏幕复位信号GPIO2DC数据/命令选择也叫 A0用来区分数据还是命令GPIO5CS片选拉低时屏幕才接受 SPI 数据GPIO21BLK背光背光控制也可直接接 3V3 常亮这里我用了 VSPI 默认引脚中部分有冲突的脚所以实际是手动分配的软件 SPI 引脚也就是可以用任意 GPIO 模拟 SPI。用 TFT_eSPI 这类库时引脚是在配置文件里指定的灵活性很大如果自己写驱动直接用 ESP32 的 SPI 外设再映射到这些 GPIO 上也没问题。关于 GPIO 选择的考虑避免使用只能做输入或带约束的引脚比如 GPIO34-39 只能输入以及可能影响启动流程的引脚GPIO0 低电平会导致下载模式GPIO12 是 MTDI 引脚上拉会影响 Flash 电压。我选的这些引脚都比较安全。接完先别急着通代码做两个简单检查用万用表确认 VCC 和 GND 没有短路再确认背光引脚电压能拉高。屏幕背光不亮大概率是 BLK 接错或没驱动。2.3 开发环境与库Arduino TFT_eSPI LVGL这次的环境我选择 Arduino IDE主要原因是对新手友好、生态成熟、示例代码多。对于这种偏验证和原型验证的项目Arduino 的“改改引脚就能跑”特性可以极大提高效率。如果你后续要做量产固件、深度定制 FreeRTOS 任务和低功耗再迁移到 ESP-IDF 也不迟。打开 Arduino IDE先配置 ESP32 开发板支持。在“文件 - 首选项 - 附加开发板管理器地址”中填入https://espressif.github.io/arduino-esp32/package_esp32_index.json然后在“工具 - 开发板 - 开发板管理器”里搜索 esp32安装最新版本的 esp32 by Espressif Systems。这一步会下载工具链时间较长耐心等待即可。库方面需要两个TFT_eSPI 和 LVGL。TFT_eSPI 是一个性能很高的 TFT 驱动库由 Bodmer 维护支持多种主控芯片包括 ST7789LVGL 就是图形库本身。TFT_eSPI 安装好之后要先改配置这是新手最容易卡住的一步。在 Arduino 的 libraries 目录里找到 TFT_eSPI 文件夹打开 User_Setup.h按照自己的屏幕和接线修改#define ST7789_DRIVER // 分辨率按自己的屏幕设置128x160 或 240x240 #define TFT_WIDTH 128 #define TFT_HEIGHT 160 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 2 #define TFT_RST 4 // 如果背面背光独立控制引脚可以定义一个 #define TFT_BL 21其中 ST7789_DRIVER 是告诉 TFT_eSPI 你的芯片型号TFT_WIDTH 和 TFT_HEIGHT 会在初始化时自动设置 GRAM 偏移很关键。LVGL 版本我建议用 8.x 系列比如 8.3.x网上资料最多API 也稳定。9.0 之后 API 变化比较大新手照着教程做容易踩坑除非你确定自己的库版本和教程版本一致。后面所有代码示例基于 LVGL 8.3。3. 底层屏幕驱动让 ST7789 真正亮起来3.1 ST7789 驱动的核心逻辑初始化序列点亮 ST7789 的第一步是给它发一串初始化命令。很多人直接复用网上示例发现屏幕显示文字颜色不对、方向不对、有偏移原因往往就是初始化序列不匹配自己的屏。最典型的例子是 ST7789V 和 ST7789VW 的差异、以及一部分屏模组厂商自定义的初始化参数。快速验证驱动是否正确的办法是先用 TFT_eSPI 的示例程序。在 Arduino IDE 中打开“文件 - 示例 - TFT_eSPI - 160x128 - TFT_Print_Test”如果你的屏幕能正常显示英文、数字和色块说明接线和驱动配置已经 OK。如果白屏或者花屏就需要排查初始化序列。TFT_eSPI 内部已经内置了大量的 ST7789 初始化序列默认适用于大多数标准模组。但在某些特定屏上尤其是转接板和二手屏还需要手动微调。如果你发现自己确实需要改初始化序列可以在 TFT_eSPI 库源码中的 ST7789_Init 函数里调整参数。常规的初始化流程是先拉低 RST 并延时完成硬件复位关闭休眠SLPOUT设置像素格式为 16bitCOLMOD: 0x3A 0x55根据屏幕分辨率设置 MADCTL数据方向设置显示窗口CASET/RASET打开显示DISPON。拿代码来说大概长这样void st7789_init() { // 复位 digitalWrite(RST_PIN, LOW); delay(50); digitalWrite(RST_PIN, HIGH); delay(120); // 关闭休眠 writeCommand(0x01); // SWRESET delay(150); writeCommand(0x11); // SLPOUT delay(200); // 像素格式 16bit writeCommand(0x3A); writeData(0x55); // MADCTL: 控制 RGB 顺序和扫描方向 writeCommand(0x36); writeData(0x00); // 默认方向RGB 顺序 // 开启显示 writeCommand(0x29); // DISPON delay(100); }命令序列中0x36MADCTL是最容易出问题的地方。它的高几位控制扫描方向上下左右翻转低两位控制 RGB/BGR 顺序。如果屏幕颜色显示的红色和蓝色反了就是这一位配错了。要解决颜色翻转在 TFT_eSPI 中可以直接在 User_Setup.h 中定义#define TFT_RGB_BGR来交换 R/B 通道具体按库版本看有的版本是TFT_BGR。实操心得我在调一块便宜的 1.8 寸屏时TFT_eSPI 默认的初始化序列白光屏。看了卖家提供的初始化脚本才发现它要求在 COLMOD 后额外设置一组电源控制命令0xC0、0xC5 等而这些寄存器值在不同模组之间差异巨大。所以当你从网上抄初始化序列时优先选同款屏幕、同卖家的资料而不是看图例相似就直接用。3.2 像素格式与 GRAM 窗口设置ST7789 的像素格式支持 12bit、16bit、18bit嵌入式开发中最常用的是 RGB565 即 16bit 格式。RGB565 的意思是红色占 5 位、绿色占 6 位、蓝色占 5 位总共 16 位正好对应 2 字节。LVGL 默认的颜色深度可以配置为 16bit这样显示缓冲区和屏幕 GRAM 可以直接一一对应CPU 开销最小。写像素前还必须先告诉屏幕“我要往哪块区域写”这是通过 CASET列地址设置和 RASET行地址设置两个命令实现的。比如 128x160 屏幕CASET 设置列范围 0~127RASET 设置行范围 0~159。值得注意的是ST7789 本身是 240x320 的全分辨率 GRAM只是模组通过偏移让 128x160 窗口显示到正确位置。所以小屏的 CASET/RASET 需要加偏移量比如 240x240 屏幕上很多模组的列偏移是 0行偏移是 01.8 寸 128x160 屏幕则常见偏移 0 和 0 或 2 和 1具体看驱动。TFT_eSPI 中这些偏移量已经通过内置配置处理好了。如果你自己写驱动就需要在 CASET 和 RASET 命令中把 X 和 Y 加上偏移值writeCommand(0x2A); // CASET writeData((x_start x_offset) 8); writeData((x_start x_offset) 0xFF); writeData((x_end x_offset) 8); writeData((x_end x_offset) 0xFF); writeCommand(0x2B); // RASET writeData((y_start y_offset) 8); writeData((y_start y_offset) 0xFF); writeData((y_end y_offset) 8); writeData((y_end y_offset) 0xFF); writeCommand(0x2C); // RAMWR之后连续写入的像素数据会被依次填进这个矩形区域。大多数 MCU 驱动问题画面整体偏移、只显示一半、边缘有条纹都出在偏移量设置不对。3.3 如何自写一个最简单的点亮测试在集成 LVGL 之前最好先确保底屏能正常操作。TFT_eSPI 已经封装好了 fillScreen、drawPixel 等方法但我还是建议你自己写一小段最原始的测试代码这样可以完全掌握数据链路是否正确。下面这段代码在初始化后把整个屏幕填充成红色再在某个位置画一个绿色方块#include SPI.h #define SCLK_PIN 18 #define MOSI_PIN 23 #define CS_PIN 5 #define DC_PIN 2 #define RST_PIN 4 void setup() { Serial.begin(115200); pinMode(RST_PIN, OUTPUT); pinMode(DC_PIN, OUTPUT); pinMode(CS_PIN, OUTPUT); SPI.begin(SCLK_PIN, -1, MOSI_PIN, CS_PIN); SPI.setFrequency(40000000); st7789_init(); fillColor(0xF800); // 红色 delay(500); fillRect(20, 20, 50, 50, 0x07E0); // 绿色方块 } void fillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { setWindow(x, y, x w - 1, y h - 1); digitalWrite(DC_PIN, HIGH); digitalWrite(CS_PIN, LOW); for (uint32_t i 0; i (uint32_t)w * h; i) { SPI.write16(color); } digitalWrite(CS_PIN, HIGH); }只要看到红色屏幕和绿色方块基本上底层链路就没问题了。如果只是白屏优先检查 RST 上拉和初始化时序如果颜色不对优先检查 BGR/RGB 顺序如果位置不对优先检查偏移量。4. LVGL 图形库移植与显示优化4.1 LVGL 的移植原理显示驱动、刷新机制、缓冲区LVGL 的架构像一个三层漏斗最顶层是用户写的 UI 代码中间是 LVGL 提供的对象和控件系统最底层是显示驱动和输入驱动。它通过lv_disp_drv_t结构体把“画一帧”的任务交给底层当内部渲染引擎需要输出像素时会调用我们注册的disp_flush回调把绘制好的缓冲区数据发送给屏幕。理解这个流程的关键在于“缓冲区”这个概念。LVGL 不会全屏一次性画完而是把屏幕分成多个小区域脏矩形只更新变化的部分。它需要至少一个缓冲区用来临时存放即将显示的数据这个缓冲区越大绘制效率越高但耗 RAM 也越多。常用的配置有两类单缓冲一个 bufferLVGL 画满一部分flush 一次再继续画下一部分。内存占用小但刷新率一般。双缓冲两个 bufferLVGL 在一个 buffer 里绘制的同时另一个 buffer 正被 DMA 或 SPI 发送交替进行可以显著提升刷新率。ESP32 有足够的 PSRAM取决于型号但 LVGL 8 的系统内存默认使用内部 RAM 速度更快。对于 240x240x2 字节的缓冲大约需要 115KB在 WROOM-32 的 520KB 内部 RAM 上完全可以承受但如果 PSRAM 不够或没有建议用较小的分块缓冲。4.2 在 ESP32 上配置 LVGL 的完整步骤我用的是 TFT_eSPI LVGL 8.3 这种组合关键是让 LVGL 的 flush 回调调用 TFT_eSPI 的 pushImage 方法。完整步骤如下第一步在 Arduino 库管理中安装 LVGL然后打开 lv_conf.h 进行配置。Arduino 环境下 LVGL 通常自带一个默认配置模板但你需要先复制lv_conf_template.h到 Arduino 库目录下并命名为lv_conf.h或者确认你的库版本已经自动生成了 lv_conf.h。关键配置项#define LV_COLOR_DEPTH 16 #define LV_COLOR_16_SWAP 1 #define LV_MEM_SIZE (32U * 1024U)其中LV_COLOR_16_SWAP 1表示交换 RGB565 的高低字节。ST7789 在 SPI 传输时通常要求先发高字节再发低字节如果不开这个交换颜色会乱或者花屏。第二步定义显示缓冲区static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[240 * 20]; static lv_color_t buf2[240 * 20]; // 双缓冲 lv_disp_draw_buf_init(draw_buf, buf1, buf2, 240 * 20);这里的宽度 240 可以用实际的屏幕宽度代替但高度 20 表示缓冲区能装 20 行像素。如果你的屏幕是 128x160缓冲区可以写成TFT_WIDTH * 20。第三步注册显示驱动static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res 128; disp_drv.ver_res 160; disp_drv.flush_cb my_disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv);第四步实现 flush 回调void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w area-x2 - area-x1 1; uint32_t h area-y2 - area-y1 1; tft.startWrite(); tft.setAddrWindow(area-x1, area-y1, w, h); tft.pushColors((uint16_t *)color_p-full, w * h, true); tft.endWrite(); lv_disp_flush_ready(disp); // 必须调用告诉 LVGL 这一块已经发送完成 }pushColors第三个参数设为 true 时TFT_eSPI 会在内部逐像素转换字节序正好配合 LV_COLOR_16_SWAP 的配置。这里还是建议保持配置一致性要么在 lv_conf.h 开 swap 并在 pushColors 里也做转换逻辑要么两边都不开避免双重交换导致颜色错乱。最后在 loop 函数中调用lv_timer_handler()并加个小 delayvoid loop() { lv_timer_handler(); delay(5); }这些做完板子通电后屏幕应该会显示 LVGL 默认的 demo如果没写 UI至少是黑屏。4.3 性能优化DMA、SPI 频率和缓冲区调优屏幕能显示和屏幕显示流畅是两件事。我实际测下来128x160 的小屏SPI 频率设成 40MHz单缓冲基本能肉眼可接受地刷新简单 UI如果要加动画、图表等动态内容就得做几项优化。第一项是 SPI 频率。TFT_eSPI 中在 User_Setup.h 里设置#define SPI_FREQUENCY 40000000示意图显示速度时40MHz 和 80MHz 差别明显但要注意部分屏幕模块尤其带长排线或面包板跳线的在高频下会因为信号完整性不足出现花屏。如果出现边缘重影或随机噪点降低频率到 20MHz 试试。第二项是 DMA 传输。TFT_eSPI 的pushImageDMA可以配合 ESP32 的 SPI DMA 引擎把像素数据从内存搬到外设这一过程交给硬件而不占用 CPU。这在你跑动画时尤其宝贵因为 CPU 可以同时绘制下一帧。tft.pushImageDMA(x, y, w, h, (uint16_t *)color_p, true);第三项是合理设置缓冲区大小。缓冲区越大LVGL 一次能处理的绘制区域就越大减少 flush 次数但内存占用也高。对于有 PSRAM 的 ESP32把缓冲区放进 PSRAM 可能依然可行但访问速度比内部 RAM 慢反而可能降低性能。我用的是两个 128x20 的缓冲区占大约 10KB 内存对 520KB RAM 来说负担很小。第四项是不要忽略lv_disp_flush_ready()的调用时机。在 DMA 实例下你要注册lv_disp_drv_t的flush_wait_cb确保上一次传输结束后再交还缓冲区给 LVGL。如果没有正确等待LVGL 可能在新数据写入时旧数据还没发完造成画面撕裂或紊乱。实操心得如果发现 LVGL 界面刷新有明显闪烁优先检查两个地方第一个是是否用了双缓冲单缓冲在绘制复杂控件时常见闪屏第二个是 flush 回调里是否有额外 delay 或阻塞操作。我在第一次跑 LVGL 示例时flush 里无意中加了一个无用的 delay(1)整个 UI 卡顿到肉眼可见去掉后立刻流畅了。这类性能问题优先怀疑阻塞和等待逻辑再去怀疑频率。4.4 简单 UI 编写与输入设备接入LVGL 的 UI 代码风格是“创建对象、设置属性、添加事件回调”。比如创建一个按钮并让它点击后改变标签文字代码大致如下static lv_obj_t *label; void button_event_handler(lv_event_t *e) { lv_label_set_text(label, Clicked); } void create_ui() { lv_obj_t *btn lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 80, 40); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(btn, button_event_handler, LV_EVENT_CLICKED, NULL); label lv_label_create(lv_scr_act()); lv_label_set_text(label, Hello LVGL); lv_obj_align(label, LV_ALIGN_CENTER, 0, -40); }如果你需要触摸或按键输入可以在lv_indev_drv_t中注册一个读指针输入设备。硬件上接一个按键到 GPIO然后用lv_indev上报坐标也可以模拟点击。完整的触摸方案比如 XPT2046需要额外的读取逻辑这里不展开但思路是在lv_indev_read_cb中返回是否按下以及坐标LVGL 自动帮你把点击事件分发到对应的控件上。5. 常见问题排查与实战避坑5.1 屏幕白屏、黑屏和花屏的定位思路屏幕出问题的时候第一件事是冷静确认到底属于哪一类情况。白屏说明背光应该是亮的否则你什么都看不见但没有图像数据通常意味着初始化失败、CS/DC 接错、SPI 根本没有数据过去。黑屏可能背光没亮或初始化时直接进入 sleep 模式。花屏则说明数据在传但格式、偏移、时序不对。我习惯按下面的顺序排查效率最高先检查供电和背光背光引脚是否有正确电平3.3V 是否稳定输出屏幕模块是否发烫。再检查接线对照引脚定义重新过一遍尤其是 DC 和 CS这两根线接错很常见而且引起的现象没有规律。然后用最小的测试程序不做 LVGL直接用 TFT_eSPI 的 fillScreen白色、红色、蓝色各刷一遍看基本绘制是否正常。最后才排查 SPI 细节协议时序、频率、初始化序列。如果 fillScreen 正常但 LVGL 不显示问题就聚焦在 LVGL 配置和 flush 回调里。5.2 花屏、颜色错乱与显示偏移的处理方法花屏通常有几种典型表现整屏显示雪花一样的乱点、图像左右或上下颠倒、颜色蓝红互换、显示区域偏到一边。雪花乱点大概率是数据时序问题。先用示波器看 SCL 和 SDA 的波形是否干净如果没有示波器最简单的方法是把 SPI 频率降到 10MHz 甚至 1MHz 试试如果正常了就说明是信号完整性问题跟飞线长度、面包板接触不良、供电不足都有关系。颜色蓝红互换就是在 MADCTL 的 BGR 位或 LV_COLOR_16_SWAP 配置上出了问题。ST7789 的 0x36 命令高三位中有一位控制 RGB/BGR 顺序LVGL 中还有一位控制高低字节交换。两者混在一起很容易绕晕我的建议是先固定一边比如让 MADCTL 保持默认只调整 LVGL 的 COLOR_16_SWAP 参数或者反过来每次只改一个变量观察效果不要同时改两个。显示偏移则要看 CASET/RASET 里是否正确加入了模组偏移。TFT_eSPI 对常见屏幕做了预设但如果你自己初始化需要去对照屏幕规格书。5.3 LVGL 界面刷新卡顿或撕裂的优化方向LVGL 跑起来后最常见的性能问题包括刷新率低、画面闪烁、动画卡顿、触摸响应迟钝。这些问题的根源通常是 CPU 在 SPI 传输上占用时间太长或者缓冲区配置太小导致频繁的小块刷新。优先考虑的优化手段是提高 SPI 频率和开启 DMA。对于 128x160 的屏幕40MHz SPI 双缓冲已经够用对于 240x240 的屏幕建议上 DMA 并考虑把 LVGL 刷新任务放到独立核心运行ESP32 支持双核但 Arduino core 默认可能就没有高效利用双核做 LVGL 刷新需要通过 FreeRTOS 任务分配才能榨干性能。另一个被很多人忽略的优化是脏矩形合并。LVGL 8 提供lv_disp_drv_t中的full_refresh开关以及LV_DPI_DEF的设置。如果屏幕很小且刷新负担小打开full_refresh让每次重绘都全量刷新反而能减少无效的局部刷新判断开销。但这不是绝对需要实测对比。5.4 问题排查速查表下面这个表是我在实际调试中整理出来的基本覆盖了这套组合最常见的坑现象可能原因排查/解决方案白屏初始化失败 / 背光不亮检查背光电源和初始化时序确认 SPI 引脚配置黑屏未发数据 / 屏幕 sleep检查 SLPOUT 命令是否执行尝试重新初始化花屏SPI 频率太高 / 信号干扰降频缩短杜邦线改为直接焊接颜色红蓝互换BGR 位错误 / 字节序错误调整 MADCTL 或 LV_COLOR_16_SWAP图像偏移/只显示一半GRAM 偏移量不对调整 CASET/RASET 的 offset 参数显示闪烁单缓冲 / flush 阻塞改双缓冲检查 flush 回调逻辑LVGL 卡顿SPI 频率低 / 缓冲区小提高频率增大缓冲区开启 DMA触摸无反应未注册输入设备 / 回调地址错误确认 lv_indev_drv_register检查返回坐标范围5.5 一些个人体会很深的注意事项最后聊几个文档上不会明确写、但我实际做项目时栽过跟头的地方。杜邦线是“能用”和“稳定”之间的最大变量。你用面包板杜邦线跑低帧率测试完全没问题但如果要做动画或者演示建议用短杜邦线甚至直接焊接。我在 40MHz 下用长杜邦线跑 LVGL 的示例屏幕边框明明出现灰条纹换成短导线后立刻消失。同时要小心 ESP32 开发板的 3.3V 输出电流有限。有些屏幕模组带背光时电流能到 80mA 以上再加上 ESP32 自身功耗直接由开发板 LDO 供电可能电压跌落。碰到“单独跑一个测试正常加 Wi-Fi 后屏幕闪烁”的现象先怀疑电源。给屏幕单独供电或接一个足够电流的 3.3V 稳压是一个常见解法。6. 进一步扩展从点亮屏幕到完整产品原型6.1 增加 OTA 升级能力做带屏物联网设备时固件升级是一个绕不开的刚需。ESP32 的 OTA 方案已经很成熟可以基于 Arduino 的 Update 库实现 HTTP OTA也可以把固件放在本地 HTTP 服务器或对象存储服务里。屏幕在这里能做一个很实用的角色显示升级进度和状态。在升级前调用 LVGL 画一个进度条和百分比标签在 OTA 过程中更新进度升级完成后重启并显示新固件版本用户体验会好很多。OTA 需要把固件分区表调整为至少两个 app 分区OTA 分区。在 Arduino 中可以新增一个自定义分区 CSV 文件然后选择对应的 partition scheme否则会报空间不足。 这一块踩坑之后深刻体会是一定要先规划好 Flash 空间不要等 UI 功能写完了再改分区。6.2 用代码生成工具加快 UI 设计LVGL 官方维护了一个图形化的 UI 设计工具可以拖拽控件、调整样式、设置动画然后一键生成 C 代码再导入到工程中编译。这一点非常推荐。对于复杂的界面手写控件布局不仅繁琐而且调节对齐和间距非常痛苦使用图形化编辑工具可以省掉大量重复劳动。需要注意的是不同版本生成代码的 API 有差异尽量让工具版本和 LVGL 库版本保持一致。工具生成的代码里可能引入了额外的文件夹和头文件需要按文档把这些文件一并加入工程。还有一点界面代码生成后业务逻辑比如按钮点击后干什么还是得自己写。生成器只负责铺界面不负责写功能。6.3 继续往下走的方向传感器接入、低功耗与 FreeRTOS 集成既然有了屏幕和 LVGL这个项目很容易延伸成真实产品原型。比如接一个温湿度传感器通过定时器刷新 UI或者接一个环境光传感器根据亮度切换主题甚至可以通过 MQTT 把自己接入物联网平台远程下发数据并实时显示在屏幕上。如果希望屏幕 UI 的核心逻辑能和业务逻辑并行运行比如边刷新 UI 边处理 Wi-Fi 回调就涉及 FreeRTOS 任务划分的问题了。LVGL 本身不是线程安全的所有 UI 操作最好都集中在一个任务中调用否则可能出现崩溃。ESP-IDF 环境下通常创建一个 LVGL 专用任务通过消息队列和业务任务通信Arduino 环境下如果用loop()跑lv_timer_handler()要避免在中断服务程序里直接操作 UI 对象。我的经验是简单项目只用单任务即可别过早复杂化等确实出现 UI 卡顿或业务延迟时再拆分任务。关于低功耗如果做电池供电设备就要考虑屏幕休眠、LVGL 定时器休眠、Wi-Fi 休眠等策略。屏幕进入 sleep 模式后仍然保持 GRAM 内容唤醒后不需要重新初始化可以直接显示这对省电很关键。LVGL 也提供了lv_disp_drv_t中的sleep_cb和wakeup_cb回调可以接入屏幕的休眠逻辑。这次从零到一跑通 ESP32 LVGL ST7789 的完整流程最大的收获是明白了“显示系统是一个分层协作的过程”硬件层保证数据能准确送达屏幕驱动层保证像素格式和窗口正确LVGL 负责把界面逻辑抽象成控件和事件。每一层的问题都有相对独立的排查思路只要不把问题混在一起看解决起来就不难。如果你正准备做带屏的嵌入式项目完全可以把这个组合作为起点先把一块小屏稳定跑起来再逐步加功能路径会很顺。
返回列表