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

文章详情

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

ESP32+ST7789+LVGL嵌入式显示实战:从裸屏到流畅UI

ESP32+ST7789+LVGL嵌入式显示实战:从裸屏到流畅UI 开头做嵌入式这几年最常被朋友问起的问题之一就是我想给ESP32加块屏幕到底怎么搞说实话ESP32搭配ST7789驱动的小尺寸IPS屏再挂上LVGL图形库几乎是我见过性价比最高、也最适合快速出效果的显示方案了。这块组合在智能家居面板、桌面小摆件、运动手表原型、环境监测站里随处可见网上资料虽然多但大多零散要么只讲点亮屏幕要么只讲LVGL的API很少有文章把从拿到一块裸屏到跑出流畅界面的完整链路串起来讲透。这篇文章就是我基于实际项目整理的完整实战记录涵盖硬件接线、ST7789底层驱动、SPI与DMA加速、LVGL移植、界面开发、内存优化和常见坑点排障。我自己用的是ESP-IDF v5.x环境但文章中会把关键差异点标注出来你用Arduino IDE或者PlatformIO也能跟着走通。无论你是刚接触单片机的学生还是想快速给产品加显示功能的工程师这篇文章的目标只有一句话让你少走弯路用最短时间把界面跑起来。1. 项目全景与方案选型1.1 为什么是ESP32、ST7789和LVGL的组合先聊聊这仨为什么能凑到一块。ESP32是我个人非常喜欢的主控双核240MHz的Xtal内置520KB SRAM这个内存规模在微控制器里算相当宽裕了。驱动一块240x240分辨率的屏幕如果用16位色深一帧完整画面需要240x240x2字节算下来差不多112.5KB。ESP32完全可以装下两块这样的帧缓冲这为LVGL的双缓冲刷新模式提供了硬件基础。而且ESP32的SPI外设带DMA可以做到屏幕上屏不占CPU这在显示复杂界面时非常关键。ST7789是目前小尺寸彩屏的主流驱动芯片常见于1.3寸、1.54寸、2.0寸的IPS屏模组分辨率从240x240到240x320都有。它支持SPI接口最少只要4根信号线加电源就能点亮硬件连接极其简单对飞线和手工焊接都很友好。同时ST7789具有1.8V到3.3V的IO电平容忍范围可以直接和ESP32的3.3V GPIO对接不需要电平转换电路。LVGLLight and Versatile Graphics Library则是目前嵌入式图形库的事实标准。它的优势在于控件丰富、动画流畅、支持中文字体而且对资源的要求很灵活裁剪配置做好之后ROM占用可以控制在几十KBRAM占用也很可控。更关键的是LVGL是纯C写的移植接口就几个回调函数不挑底层平台。这个组合本质上解决的是一个经典矛盾想快速做出漂亮界面又不想在产品成本、开发周期、硬件体积上付出太大代价。ESP32负责带得动ST7789负责显示好LVGL负责画得快三者各司其职几乎没有短板。1.2 显示方案的三种驱动思路对比在实际动手之前我建议先搞清楚一个方向性问题你的显示路径走哪条。第一种裸机直接推帧缓冲。这种方案就是自己准备好一整块显示数据然后通过SPI整帧推给ST7789不经过任何图形库。优点是实现极简适合开机logo、静态画面、数字仪表等简单场景。缺点是所有绘图逻辑都要自己写画个圆、显示个汉字都是体力活后续维护成本很高。第二种单缓冲加局部刷新。先移植LVGL但只开辟一块较小的缓冲区比如240x20像素LVGL每渲染好一小块区域就通过DMA发送到屏幕发完再渲染下一块。这种方案RAM占用小适合内存紧张的MCU。缺点是屏幕刷新过程有明显的撕裂或扫描感尤其是显示滚动列表或动画时视觉上会有轻微闪烁。第三种双缓冲加DMA后台传输。这是体验最好的方案。LVGL在后台缓冲区渲染前台缓冲区通过DMA发送到屏幕两块交替使用渲染和传输可以并行。对于240x240的屏幕双缓冲需要约120KB RAMESP32刚好够用。我实测下来这种配置下LVGL的60fps动画可以跑得比较顺畅拖动列表也不会出现撕裂。我的建议是如果你用的是ESP32且屏幕分辨率不超过320x320直接上双缓冲这是最优体验和开发效率的平衡点。如果内存确实紧张再退回单缓冲模式LVGL的配置改动只是一个宏定义的事。2. 硬件连接与环境准备2.1 屏幕接口与ESP32接线市面上ST7789屏幕模组引脚定义大同小异常见的就8个引脚VCC、GND、SCL、SDA、RES、DC、CS、BLK。有些模组还会把BLK标注为BL或者LED。接线之前一定先看屏幕背面丝印别直接照着网上的引脚顺序套我见过不少因为同一家店的模组批次不同引脚顺序就变了而烧板子的案例。下面这张表是我在项目中实际使用的接线对应关系屏幕引脚ESP32引脚说明VCC3V3部分模组也兼容5V但建议3.3VGNDGND共地必须接不能省SCLGPIO18SPI时钟SDAGPIO23SPI数据主出从入RESGPIO4复位低电平有效DCGPIO2数据/命令选择CSGPIO5片选低电平有效BLKGPIO22背光控制也可以直接接3V3常亮ESP32的SPI引脚不是固定的GPIO18和GPIO23是我惯用的默认SPI引脚实际你可以根据自己PCB布局改到任何支持输出的引脚上。需要特别提醒的是背光引脚的处理。BLK直接接3V3虽然能常亮但如果你有低功耗需求或者想在界面空闲时灭屏最好把BLK接到一个独立的GPIO上通过PWM控制亮度。我调试初期为了省事直接接了3V3结果每次想测息屏逻辑都要拔线后来才用GPIO22接管。另外注意BLK引脚如果悬空部分模组会默认关闭背光屏幕点不亮还以为是驱动代码的问题特别坑。电源方面也要多说一句屏幕模组虽然标称3.3V但瞬时电流在点亮全白画面时可能到几十毫安如果用开发板上的AM1117稳压器供电在WiFi开启和高亮度背光同时工作时可能会造成压降导致屏幕闪烁或重启。稳妥的做法是外接一个独立的3.3V LDO给屏幕供电或者至少保证供电电流足够。2.2 开发环境搭建ESP-IDF还是Arduino做这个项目之前开发环境选择会直接影响后面所有代码的写法。我强烈推荐使用ESP-IDF v5.x原因有三一是ESP-IDF的SPI驱动esp_lcd和spi_master对ST7789这类屏幕驱动芯片有非常完善的现成支持底层DMA调度、中断处理都做好了二是官方的组件仓库可以直接拉取LVGL版本管理省心三是IDF带FreeRTOSLVGL跑在独立任务里非常自然后面扩展OTA、蓝牙、传感器采集都方便。当然如果你对Arduino更熟悉也没问题。Arduino环境下有TFT_eSPI这个库配合LVGL的Arduino绑定也很流畅。TFT_eSPI的好处是配置简单改一下User_Setup.h文件就能驱动ST7789但我个人感觉它在复杂场景下对DMA和中断的掌控力还是不如IDF的esp_lcd。安装ESP-IDF的过程我就不展开了官方文档按步骤来就行。需要留意的点是如果你用VS Code搭配Espressif插件安装的时候选完整版避免后面拉取组件时缺工具链。另外如果网络下载慢记得配置代理源这个在实际开发中能省很多时间。工程结构上我习惯用如下目录组织项目project/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ ├── main.c │ ├── lvgl_demo/ │ │ ├── ui.c │ │ └── ui.h │ └── hardware/ │ ├── st7789_driver.c │ └── st7789_driver.h └── dependencies.lock2.3 LVGL组件的获取方式ESP-IDF v5.x下获取LVGL有两条路一条是用组件管理器Component Manager在工程根目录的main/idf_component.yml里声明依赖另一条是把LVGL源码直接以git子模块形式放进components目录。我推荐用组件管理器因为LVGL官方在ESP-IDF注册中心维护了一个lvgl/lvgl组件包直接支持通过esp_idf_components拉取。在main目录下创建idf_component.yml写入以下内容dependencies: idf: 5.0 lvgl/lvgl: ^8.3.0需要注意的版本匹配问题LVGL 8.x和LVGL 9.x的API变化不小8.x的lv_disp_drv_t在9.x中被改名为lv_display_t如果你参考的是老教程一定要注意版本差异。我目前生产项目还在用8.3因为生态最成熟、资料最多等9.x的坑踩得差不多了再迁移。如果你在Arduino环境下开发更简单在库管理器里直接搜lvgl安装版本同样建议选8.x。3. ST7789驱动核心细节3.1 ST7789芯片特性与初始化时序ST7789这颗驱动IC内部集成了240x320像素的GRAM显存它本身不存储完整图像帧你通过SPI把像素数据写入它的GRAM屏幕控制器就会持续刷新显示。所以从主控的角度看你的工作只有两件事初始化屏幕寄存器、往GRAM里灌像素数据。初始化可以简单理解为让屏幕进入一个已知的正常工作状态。下面是ST7789初始化过程中最关键的几个寄存器和操作序列软复位Software Reset, 0x01让芯片回到上电默认状态之后要等待至少5ms。退出休眠Sleep Out, 0x11屏幕从低功耗模式唤醒等待至少120ms让内部DC-DC稳压器稳定。设置像素格式Color Format, 0x3A将数据格式设为RGB565即16位色深命令参数是0x05表示65K色。设置扫描方向和坐标偏移0x36, 0x37这两个寄存器决定图像是正对屏幕还是旋转了90度/镜像以及原点偏移量不同模组的封装差异主要就在这里。打开显示Display On, 0x29让屏幕真正开始显示GRAM里的内容。下面是我在实际项目中整理出来的一段精简初始化序列你可以直接照着用static const uint8_t st7789_init_cmds[] { 0x01, 0, // Software Reset 0x11, 0, // Sleep Out 0x3A, 1, 0x05, // RGB565 color format 0x36, 1, 0x00, // MADCTL: scan direction 0x37, 2, 0x00, 0x00, // Vertical scroll start 0xB2, 5, 0x0C, 0x0C, 0x00, 0x33, 0x33, // Porch setting 0xB7, 1, 0x35, // Gate Control 0xBB, 1, 0x19, // VCOM setting 0xC0, 1, 0x2C, // LCM Control 0xC2, 1, 0x01, // VDV and VRH command enable 0xC3, 1, 0x12, // VRH setting 0xC4, 1, 0x20, // VDV setting 0xC6, 1, 0x0F, // Frame rate control 0xD0, 2, 0xA4, 0xA1, // Power control 0xE0, 14, 0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F,0x23, // Gamma 0xE1, 14, 0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20,0x23, // Gamma- 0x21, 0, // Display Inversion On 0x29, 0, // Display On };这段序列里的Gamma设置部分可能因屏幕批次而异如果你发现显示偏色严重优先检查Gamma参数和模块厂商提供的参考值。另外要注意速度问题发送初始化命令时节奏不能太快关键延时不能省比如Sleep Out后必须等待至少120ms否则屏幕可能无法正常唤醒表现为白屏或者闪屏。很多人会在这里犯一个错误想当然地以为屏幕驱动芯片都是相同的初始化序列直接从别人代码里抄过来。不同厂家、不同批次的屏幕初始化参数可能不同最保险的是向模组供应商要初始化代码或者对照datasheet逐个寄存器核对。3.2 SPI通信参数与DMA加速ST7789通过SPI接收数据时钟极性CPOL和相位CPHA建议用SPI模式0CPOL0, CPHA0。ESP32的SPI主机支持的最高时钟频率远高于屏幕本身的上限实际使用中ST7789的SPI时钟上限通常在60MHz左右但我不建议一上来就用最高频。我在调试时习惯从10MHz起步确认屏幕能正常显示后再逐步提升频率直到出现花屏再降回安全值。这个花屏降频的临界点会有个体差异和你的飞线长度、PCB布线质量都有关系。我最终稳定使用的SCLK是40MHz配合DMA传输整帧240x240x2字节数据大概耗时2.4ms这个速度足够LVGL流畅刷屏了。在ESP-IDF下启用DMA很简单配置spi_bus_config_t时把.quadwp_io_msb留空然后在spi_device_interface_config_t中配置.flags SPI_DEVICE_HALFDUPLEX发送大块数据时使用spi_device_transmit配合spi_transaction_t.tx_buffer。值得注意的一点是ESP32的DMA对发送缓冲区有对齐要求。如果你用的是动态分配的内存比如heap_caps_malloc一定要用MALLOC_CAP_DMA标志来分配否则DMA传输可能出现随机性的数据错乱。我第一次移植时就忽略了这一点用默认的malloc分配帧缓冲结果屏幕偶尔出现横条纹排查了大半天才定位到是缓冲区不对齐。3.3 坐标窗口、偏置与屏幕方向的坑ST7789内部GRAM的实际大小是240x320而你买的屏幕可能是240x240这意味着芯片有很多显存地址超出了物理屏幕范围。当你往超出屏幕范围的内存地址写入数据时数据不会显示出来但也不会报错界面会出现内容偏移或者只显示左上角的情况。解决这个问题靠两个寄存器配合**CASET0x2A**设定列地址范围**RASET0x2B**设定行地址范围也就是所谓的窗口window。每次往GRAM写数据前都要先设定窗口然后持续写入数据数据量超过窗口宽度时会自动换行。对于240x240的屏幕最常用的做法是把窗口设置在GRAM的某个偏移位置上使屏幕正好显示GRAM的一个子区域。常见的偏移组合是水平偏移0、垂直偏移80或者水平偏移52、垂直偏移40具体取哪个值取决于模组出厂时封装的玻璃走线方向。最快的方法是自己试先用全屏填充命令画一个纯色背景然后调整偏移参数看屏幕是否被完整均匀覆盖。屏幕方向控制则是通过MADCTL寄存器0x36实现的。MADCTL的bit5到bit7分别控制行/列方向交换和刷新方向常见的方向配置值有0x00竖屏、0x6090度旋转、0xC0180度旋转、0xA0270度旋转。注意方向改变后窗口偏移参数也需要对应调整否则会出现图像颠倒但位置不对的问题。我在做不同的项目时经常需要适配不同安装方向的屏幕这块踩过好多次坑建议你在驱动层封装一个函数专门用来设置方向方便后续复用。4. LVGL移植与适配4.1 LVGL移植的整体思路LVGL的移植官方文档写得很清楚本质上是实现三个底层接口显示屏驱动回调flush_cb接收LVGL渲染好的像素数据通过SPI写入屏幕。触摸/输入设备驱动回调read_cb如果接的是触摸屏通过I2C或SPI读取触摸坐标并传给LVGL。时钟心跳tickLVGL内部所有动画、定时器都依赖内部的系统时钟节拍。弄清楚这三个接口之后LVGL移植这件事就变得非常简单了难点反而在于你对底层的屏幕驱动是否足够熟悉。所以我在第3节花了大篇幅讲ST7789因为如果屏幕底层不稳定就算LVGL移植成功显示效果也一定一团糟。在ESP-IDF环境下LVGL官方还提供了一个叫esp_lcd的抽象层它把SPI接口和屏幕驱动芯片进一步封装成了标准的esp_lcd_panel_io和esp_lcd_panel接口。用这套接口的好处是代码更简洁而且官方对ST7789的支持是开箱即用的你只需要传入SPI总线和GPIO配置就能得到一个已经初始化好的面板对象。但我个人建议如果你是初学者还是先自己写一遍裸驱动的流程再考虑用esp_lcd抽象。自己写一遍能让你真正理解DMA怎么工作、窗口怎么设定、底层数据流怎么走将来遇到问题时不会抓瞎。4.2 lv_conf.h关键配置项详解LVGL通过一个头文件lv_conf.h来裁剪功能这个文件可以在LVGL源码目录下找到模板拷贝出来后根据需求修改。以下是我认为影响最大的几个配置项#define LV_COLOR_DEPTH 16颜色深度必须和屏幕驱动的RGB565格式对应如果你这里定义了32位色深显示会直接崩溃或者颜色全乱这是移植初期最常见的错误。#define LV_MEM_CUSTOM 1LVGL内部的动态内存分配器默认使用自带的lv_mem实现你也可以设为1后让它直接调用系统的malloc和free。在ESP-IDF下我建议设为1因为IDF的堆管理器本身就带内存统计功能方便你监控剩余内存。#define LV_DISP_DEF_REFR_PERIOD 30这个宏表示LVGL的刷新周期单位是毫秒。默认30ms意味着约33fps的刷新率如果你觉得界面不够流畅可以改成16甚至10但要注意刷新太快会显著增加CPU占用。我的经验是30ms在大多数场景下够用追求动画丝滑可以改成15ms同时确保你的SPI传输速度跟得上。#define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_WARN打开日志输出调试时能看到LVGL内部的警告和错误发布时关掉可以省一点Flash和CPU资源。内存管理方面LVGL在初始化时会从系统堆里取一块内存作为自己的缓冲区池。这个大小在lv_init之后由lv_mem_init决定实际上取决于LV_MEM_SIZE宏。240x240屏幕建议至少给LVGL分配48KB内存池才能比较从容地创建多个控件和动画。4.3 和ST7789驱动对接flush_cb实现细节flush_cb是LVGL移植的核心LVGL每渲染好一块局部画面就会调用这个回调把数据送出去。我的实现大概长这样static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { esp_lcd_panel_handle_t panel (esp_lcd_panel_handle_t)drv-user_data; esp_lcd_panel_draw_bitmap(panel, area-x1, area-y1, area-x2 1, area-y2 1, color_p); lv_disp_flush_ready(drv); }要注意lv_disp_flush_ready的调用时机会影响性能。如果你在SPI数据发送完成后再调用flush_ready那么发送期间LVGL会等待浪费了时间正确做法是在启动DMA传输后立即返回并调用flush_readyLVGL就会立刻开始渲染下一块画面而DMA在后台默默发送实现了渲染和传输的流水线并行。不过这样做有一个隐患如果缓冲区还在被DMA读取LVGL就开始往里写新数据会冲掉还没发送完的内容造成画面错乱。所以实际实现里需要为DMA准备两块缓冲区交替使用或者等DMA完成后才释放缓冲区这又回到前面讲的双缓冲问题。以下是我在项目中最终采用的方案分配两块缓冲区一个用于LVGL渲染另一个用于SPI DMA发送。当LVGL完成渲染并调用flush_cb后交换两块缓冲区的角色同时启动DMA发送新数据。这样在DMA发送期间LVGL在另一块缓冲区渲染新的画面效率最大化。4.4 FreeRTOS下的LVGL任务架构ESP-IDF自带的FreeRTOS让你可以很自然地把LVGL放进一个独立任务里运行。我的任务结构大致如下void lvgl_task(void *arg) { lv_init(); // ... 初始化显示屏驱动和输入设备 while (1) { lv_timer_handler(); // LVGL定时器处理处理输入事件、更新动画、调度渲染 vTaskDelay(pdMS_TO_TICKS(5)); } }lv_timer_handler是LVGL的心脏它负责处理所有事件、刷新过期动画、触发渲染每次调用返回后需要小延时一下避免把CPU占满。上面代码里的5ms是经验值这样CPU使用率不会太高动画也足够流畅。由于LVGL不是线程安全的所有涉及UI的操作都要在lvgl_task的上下文里执行。如果你在其他任务里直接调用LVGL的API修改控件会出现随机崩溃或显示错乱。正确的做法是通过队列或事件组把消息投递给lvgl_task由它在主循环里统一处理。这个架构问题一定要在一开始就设计好不然后面加功能时到处都要改。5. 界面开发与资源优化5.1 第一个LVGL界面屏幕、标签与按钮驱动层打通之后LVGL界面开发其实是一件很愉快的事情。先看一段最简单的界面代码void create_demo_ui(void) { lv_obj_t *scr lv_scr_act(); // 获取当前活动屏幕 // 创建一个标签 lv_obj_t *label lv_label_create(scr); lv_label_set_text(label, Hello LVGL); lv_obj_set_style_text_font(label, lv_font_montserrat_28, 0); lv_obj_center(label); // 创建一个按钮 lv_obj_t *btn lv_btn_create(scr); lv_obj_set_size(btn, 120, 50); lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -20); lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); }这段代码的运行效果是屏幕上出现一行居中的Hello LVGL文字底部一个按钮点击按钮后触发btn_event_cb回调函数。LVGL的布局思路和前端框架很像控件树screen - container - label/btn样式系统style和自动布局flex、grid。如果你以前搞过CSS上手会非常快。在实际开发中我强烈建议用SquareLine Studio或者LVGL官方网页版UI生成器来拖拽搭建界面原型再导出C代码集成到工程里。对于复杂UI手写C代码逐像素对齐太耗费时间可视化的设计工具能把开发效率提升好几倍。但最终代码审查和性能调优还是得能看懂导出的代码所以手写基础控件的能力也不能丢。5.2 中文字库与图片显示的处理LVGL默认字体只包含ASCII字符直接lv_label_set_text写中文只能看到一个个方块。要显示中文需要把中文字库转成LVGL认识的格式。我用的流程是先在电脑上用字库生成软件LVGL官方有在线字体转换器也可以在本地跑lv_font_conv命令行工具选择需要的字体文件和字符范围然后生成一个C源文件放进工程里编译。中文字体文件会很大一个常用字约3500个汉字的16号字体在16位色深下可能占用100KB到200KB的Flash所以在选字时务必按需选择字符范围只包含界面里真正用到的字。我习惯把界面上的静态文案汇总到一张表里用这些字生成一个精简字库可以把体积压缩到几KB级别。图片显示也类似LVGL原生不支持PNG/JPG解码虽然8.x提供了可选的图片解码插件最常见的方式是先用工具把图片转换成C数组格式可以是RGB565或者带透明通道的ARGB8888。如果你只需要简单图标推荐用lv_img_conv工具生成RGB565格式体积小、显示快不需要额外解码器。图片转数组在Flash占用上最夸张。一张240x240的RGB565图片就要115KB放两张图Flash就见底了。所以做产品界面时尽量多用线条、矢量图形和纯色少用大图这是嵌入式UI设计和网页设计一个很重要的区别。5.3 内存与渲染性能调优界面跑起来只是第一步跑得流畅才是体验的关键。在实际使用中我总结出几个影响LVGL性能的关键因素。首先是缓冲区大小和刷新模式。前面推荐的双缓冲方案能显著提升流畅度但它对RAM的要求也摆在那里。如果内存不够退而求其次可以采用局部刷新加单缓冲代价是滚动时会有闪烁。LVGL 8.x还支持一种叫直接渲染direct mode的模式允许LVGL渲染到一块大缓冲区后整帧直接输出适合有充足RAM的场景。其次是渲染效果开关。LVGL 8.x里有几个很吃资源的效果阴影shadow、模糊blur、渐变gradient。这三个特性会显著增加CPU渲染耗时对嵌入式设备来说是性能杀手。我在性能敏感的界面上会全局禁用这些效果或者只在高端屏上开启在240x240的小屏幕上说实话这些花哨效果的意义不大反而拖慢帧率。再就是局部刷新的天然优势。LVGL本身只对被修改的区域重新渲染不刷新整帧。这意味着界面如果只是更新一个数字那么实际传输到屏幕的数据量很小SPI带宽压力也就小了。但前提是屏幕驱动支持任意窗口渲染ST7789是支持的所以要确保你的驱动没有把窗口强制设为整个屏幕。最后提一下CPU负载的观察方法。我的做法是在lvgl_task里周期性地用esp_timer_get_time()统计两帧渲染间隔并将其与理论值做对比。如果间隔明显超过预期说明渲染耗时过长可以逐步关掉特效、调低刷新率来定位瓶颈。6. 常见问题与排查实录6.1 白屏、花屏、颜色不对这三个问题是屏幕驱动入门时遇到最多的下面的排查顺序是我反复验证过的建议按顺序检查白屏最常见的原因是初始化时序问题。重点检查Sleep Out后的120ms延时是否足够、复位引脚是否有足够宽的复位脉冲、SPI模式是否匹配ST7789用Mode 0、CS和DC引脚是否接反。如果初始化流程没问题再用示波器看一眼SCL上是否有正常的时钟波形没有波形的话基本就是SPI走的是错误的引脚或者SPI根本没有被初始化。花屏大概率是SPI时钟太快或者接线太长导致信号质量差。先把时钟频率降到10MHz附近如果问题消失就是信号完整性问题。宽松点说飞线超过10cm时40MHz时钟就可能出现数据错乱尤其是没有共地或者地线很细的情况下。颜色不对则优先检查颜色深度配置。ST7789需要设置为RGB565格式如果你代码里设置的是RGB888或者RGB666颜色会明显偏色或变成奇怪的条纹。另一类颜色问题出现在LCD和OLED屏上有些LCD屏需要开启反色显示Display Inversion On即命令0x21否则画面呈现负片效果我之前调试一块新模组时就被这个坑了好几个小时。6.2 LVGL画面闪烁、撕裂、刷新慢LVGL界面闪烁一般有两种可能。第一种是帧率太低人眼在30fps以下能明显感觉到闪烁和卡顿。排查方法很简单在lv_timer_handler里加一个计数器打印到串口看看一秒实际调用多少次。如果每秒调用次数只有10次左右说明渲染跟不上需要减少界面复杂度或提高SPI时钟。第二种是缓冲区刷新策略不当。如果你用的是单缓冲加局部刷新屏幕不同区域刷新时间不一致会产生明显的扫描线效果。这时候最好的解决办法是升级到双缓冲模式。这里有个容易混淆的点LVGL的lv_disp_buf_init函数里的两个缓冲区参数如果传入两块独立的缓冲区LVGL会自动选择双缓冲模式很多教程只传一块或者传了同一块数组等于还是单缓冲刷新模式没切换过来。刷新慢还有一种可能是因为你在flush_cb里同步等待DMA传输完成才返回这等于把渲染和传输串行化了。优化做法是前面提过的双缓冲流水线或者在发送完成后用中断回调的方式调用lv_disp_flush_ready不要让CPU空等。6.3 触摸屏没有反应或点击位置偏移如果你用的是带触摸功能的屏幕模组触摸不工作时先确认两件事触摸芯片是什么型号走的是什么接口。市面上ST7789屏幕模组常见的触摸芯片有CST816S、FT6236、GT911等大多数走I2C接口少数走SPI。驱动的第一步是在I2C总线上扫描设备的I2C地址确认芯片和主控通信正常。触摸坐标偏移则一般是坐标系没有和屏幕方向对齐。LVGL里可以通过lv_indev_drv_t的read_cb函数手动校正把读取到的触摸坐标做旋转或镜像变换后再传给LVGLbool touch_read_cb(lv_indev_drv_t *indev, lv_indev_data_t *data) { uint16_t x, y; touch_read(x, y); // 根据屏幕方向进行坐标变换 >
返回列表