ARTICLE DETAIL

资讯详情

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

ESP32驱动SPI TFT屏:TFT_eSPI与LVGL移植实战

ESP32驱动SPI TFT屏:TFT_eSPI与LVGL移植实战 1. 项目缘起与整体方案设计1.1 为什么选择 ESP32 TFT_eSPI LVGL 这套组合手头有一块 2.4 寸 ILI9341 的 SPI TFT 屏一直想拿 ESP32 做点带界面的小项目。最开始图省事直接用 TFT_eSPI 裸画画几个按钮、几行文字还行一旦界面复杂起来——比如要做多页面切换、滑动列表、带状态反馈的控件——代码就变成了一团乱麻。每个控件的坐标、刷新逻辑、触摸响应全得自己算改一个布局要动十几处数字维护成本极高。后来转向 LVGL本质上是把“画界面”这件事从“手工作坊”升级成“有设计规范的流水线”。LVGL 负责控件的创建、布局、事件分发和局部刷新TFT_eSPI 负责最底层的 SPI 数据传输和屏幕初始化。两者分工明确LVGL 是大脑TFT_eSPI 是手脚。ESP32 双核 240MHz 主频、520KB SRAM、支持 DMA 和 SPI 高速时钟跑这套组合绰绰有余。这套方案适合谁适合已经能用 Arduino 框架点亮 TFT 屏、但被裸画界面折磨过的开发者也适合从 STM32 转过来、想快速在 ESP32 上跑起 LVGL 的朋友。如果你连 SPI 屏幕都没点亮过建议先把 TFT_eSPI 的示例跑通再来看这篇。1.2 整体架构与数据流拆解整个系统的数据流可以这样理解LVGL 内部维护一棵“控件树”每个控件有自己的属性位置、大小、颜色、文字。当某个控件状态变化时LVGL 把它标记为“脏区域”然后在lv_timer_handler()被调用时把这些脏区域渲染到一块内存缓冲区里。TFT_eSPI 则通过pushImage()或pushColors()把缓冲区数据推送到屏幕。关键角色有三个LVGL 的显示驱动disp_drv负责告诉 LVGL“缓冲区在哪、多大、怎么刷”。LVGL 的输入驱动indev_drv负责把触摸坐标喂给 LVGL。TFT_eSPI 的配置通过User_Setup.h定义引脚、驱动型号、SPI 频率。这三者之间的衔接点就是本文要讲的核心——flush_cb 回调函数。LVGL 渲染完一块区域后会调用这个回调我们在回调里调用 TFT_eSPI 的推送函数把像素数据送到屏幕。理解了这个回调整套移植就通了。1.3 开发环境选型PlatformIO 而非 Arduino IDE我最终选了 PlatformIO VS Code 的组合。原因很直接LVGL 和 TFT_eSPI 都是活跃更新的库版本迭代快Arduino IDE 的库管理在依赖冲突时很头疼。PlatformIO 用platformio.ini声明依赖版本每个项目独立环境不会出现“这个项目能编译、换个项目就报错”的情况。platformio.ini的核心配置如下[env:esp32dev] platform espressif32 board esp32dev framework arduino monitor_speed 115200 upload_speed 921600 lib_deps bodmer/TFT_eSPI^2.5.43 lvgl/lvgl^8.3.11 build_flags -DLV_CONF_INCLUDE_SIMPLE -DLV_LVGL_H_INCLUDE_SIMPLE这里锁定了 LVGL 8.3.x 版本。为什么不直接上 LVGL 9因为 9.x 的 API 变动较大很多网上教程还是 8.x 的写法新手混用容易踩坑。8.3 是 8.x 系列最稳定的版本资料最全。提示upload_speed设到 921600 能显著加快烧录速度但前提是你的 USB 转串口芯片支持。CH340 一般稳在 460800CP2102 可以上 921600。如果烧录失败先降回 115200 排查。2. 核心细节解析与实操要点2.1 TFT_eSPI 的 User_Setup.h 配置详解TFT_eSPI 不像其他库那样在代码里传引脚而是通过一个头文件User_Setup.h在编译期确定硬件参数。这个文件在.pio/libdeps/esp32dev/TFT_eSPI/目录下。直接改库文件有个问题库更新后配置会被覆盖。更稳妥的做法是在项目include/目录下建一个User_Setup.h然后在platformio.ini里加build_flags -DUSER_SETUP_LOADED1 -include include/User_Setup.h我的 ILI9341 屏幕配置如下#define ILI9341_DRIVER #define TFT_MISO 19 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST 4 #define TFT_BL 32 #define LOAD_GLCD #define LOAD_FONT2 #define LOAD_FONT4 #define LOAD_GFXFF #define SMOOTH_FONT #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 20000000 #define SPI_TOUCH_FREQUENCY 2500000几个关键点解释一下。SPI_FREQUENCY设到 40MHz 是 ILI9341 的常见上限实测杜邦线短接时稳定线长了要降到 27MHz。TFT_BL是背光引脚如果你的模块背光常亮可以不接。LOAD_GFXFF加载了 FreeFont 支持LVGL 用不到但 TFT_eSPI 自己的文字显示需要。注意ESP32 的 VSPI 默认引脚就是 18/19/23用这些引脚不需要额外配置。如果改用 HSPI需要在 User_Setup 里定义USE_HSPI_PORT。2.2 LVGL 显示缓冲区的大小计算与分配策略LVGL 的渲染缓冲区大小直接决定刷新效率和内存占用。缓冲区是“一行或多行像素”的临时存放区LVGL 渲染完一部分就通过 flush_cb 推走。缓冲区大小的计算公式缓冲区字节数 水平分辨率 × 行数 × 每像素字节数以 320×240 的 ILI9341 为例RGB565 每像素 2 字节。如果分配 1/10 屏幕大小的缓冲区320 × 24 × 2 15360 字节 ≈ 15KBESP32 有 520KB SRAM分配 15KB 完全没问题。我实际用了 40 行static lv_color_t buf1[320 * 40];这样约 25KB刷新时撕裂感明显减少。如果你用 ESP32-S3 带 PSRAM可以分配全屏缓冲区320×240×2 150KB实现无撕裂刷新。LVGL 支持双缓冲buf1和buf2交替使用一个在渲染时另一个在推送。但双缓冲会占用双倍内存且需要disp_drv.full_refresh 0配合。对于 SPI 屏幕单缓冲 40 行已经够用因为 SPI 传输本身就是瓶颈。2.3 flush_cb 回调函数的实现细节这是整个移植的“咽喉”。LVGL 渲染完一块区域后调用它参数是lv_disp_drv_t *disp_drv和const lv_area_t *area。void my_disp_flush(lv_disp_drv_t *disp_drv, 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_drv); }startWrite()和endWrite()之间 CS 拉低避免每次推送都切换片选。setAddrWindow设置写入区域pushColors批量写入。最后一个参数true表示交换字节序——LVGL 的lv_color_t在 RGB565 下是低字节在前而 ILI9341 期望高字节在前所以必须交换。踩坑记录如果忘了lv_disp_flush_ready()LVGL 会一直等这个回调完成界面直接卡死。这个函数必须调用且要在数据推送完成后调用。2.4 触摸输入的对接方式触摸屏分电阻和电容两种。电阻屏用 XPT2046 芯片走 SPI电容屏用 FT6236 或 GT911走 I2C。我手上是电阻屏用 TFT_eSPI 自带的触摸功能。void my_touch_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) { uint16_t touchX, touchY; bool touched tft.getTouch(touchX, touchY); if (!touched) { >#include Arduino.h #include TFT_eSPI.h #include lvgl.h TFT_eSPI tft TFT_eSPI(); static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[320 * 40]; static lv_disp_drv_t disp_drv; static lv_indev_drv_t indev_drv; void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); lv_init(); lv_disp_draw_buf_init(draw_buf, buf, NULL, 320 * 40); lv_disp_drv_init(disp_drv); disp_drv.hor_res 320; disp_drv.ver_res 240; disp_drv.flush_cb my_disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; indev_drv.read_cb my_touch_read; lv_indev_drv_register(indev_drv); // 创建界面 lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, Hello ESP32 LVGL); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } void loop() { lv_timer_handler(); delay(5); }第四步编译烧录。如果屏幕白屏或花屏先检查User_Setup.h的驱动型号和引脚。3.2 第一个 LVGL 界面的创建与验证上面代码跑通后屏幕中央会出现一行文字。但这只是验证链路通了。接下来做一个稍微完整的界面一个按钮、一个标签、一个滑块。lv_obj_t *btn lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 100, 50); lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 20, 20); lv_obj_t *btn_label lv_label_create(btn); lv_label_set_text(btn_label, Click); lv_obj_center(btn_label); lv_obj_t *slider lv_slider_create(lv_scr_act()); lv_obj_set_width(slider, 200); lv_obj_align(slider, LV_ALIGN_BOTTOM_MID, 0, -30); lv_slider_set_range(slider, 0, 100); lv_slider_set_value(slider, 50, LV_ANIM_OFF);按钮和滑块都是 LVGL 内置控件自带按压和拖动效果。如果触摸没反应先确认my_touch_read里的坐标是否正确。可以在回调里加Serial.printf打印坐标看触摸时有没有数值变化。3.3 性能优化SPI 频率与 DMA 的取舍默认 40MHz SPI 下刷一屏 320×240 的 16 位色数据需要320 × 240 × 2 字节 153600 字节 153600 × 8 bit / 40MHz ≈ 30.7 ms也就是说全屏刷新约 30ms理论帧率 32fps。但 LVGL 通常只刷局部区域实际体验流畅。如果觉得不够快可以尝试提高 SPI 频率到 80MHz部分 ILI9341 模块支持但线材要求高使用 ESP32 的 DMA 通道让 SPI 传输不占用 CPU减小缓冲区行数降低单次传输量TFT_eSPI 在 ESP32 上默认使用 DMA如果USE_DMA未禁用。DMA 的好处是pushColors调用后 CPU 可以继续处理其他任务但 LVGL 的 flush_cb 是同步等待的所以实际收益有限。真正能提升的是把lv_timer_handler()放在独立任务里用 FreeRTOS 双核跑。3.4 FreeRTOS 双核任务划分实践ESP32 双核的优势在这里体现。把 LVGL 的定时器处理放在 Core 1其他逻辑放在 Core 0。void lvgl_task(void *pvParameter) { while (1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } } void setup() { // ... 初始化代码 ... xTaskCreatePinnedToCore(lvgl_task, LVGL, 4096, NULL, 2, NULL, 1); } void loop() { // 其他任务比如传感器读取 delay(100); }xTaskCreatePinnedToCore的最后一个参数 1 表示绑定到 Core 1。LVGL 不是线程安全的所有 LVGL API 调用必须在同一个任务里。如果其他任务要更新界面用lv_async_call或消息队列传递。注意LVGL 8.x 默认LV_COLOR_DEPTH是 16对应 RGB565。如果屏幕是 RGB888需要改lv_conf.h并调整 flush_cb 的字节序处理。4. 常见问题与排查技巧实录4.1 屏幕白屏、花屏、颜色反转的排查路径白屏最常见的原因是User_Setup.h里驱动型号选错。ILI9341 和 ST7789 的初始化序列不同选错就是白屏。确认模块背面丝印的驱动型号或者问卖家要资料。花屏通常是 SPI 频率过高或接线过长。先把SPI_FREQUENCY降到 10MHz 试试如果正常了再逐步往上加。杜邦线超过 10cm 时40MHz 基本不稳。颜色反转红蓝互换是字节序问题。在User_Setup.h里加#define TFT_RGB_ORDER TFT_BGR或TFT_RGB两个都试一下。现象可能原因解决方法白屏驱动型号错误确认模块型号改 User_Setup花屏SPI 频率过高降到 10MHz 测试颜色反转字节序错误改 TFT_RGB_ORDER触摸无反应触摸 CS 未定义检查 TOUCH_CS 引脚界面卡死未调用 flush_ready在 flush_cb 末尾调用4.2 LVGL 内存不足与 lv_conf.h 调优LVGL 默认配置比较保守跑复杂界面时可能报内存分配失败。关键配置项#define LV_MEM_SIZE (48U * 1024U) #define LV_MEM_CUSTOM 0 #define LV_DISP_DEF_REFR_PERIOD 30 #define LV_INDEV_DEF_READ_PERIOD 30LV_MEM_SIZE是 LVGL 内部堆大小默认 48KB。如果界面控件多加到 64KB 或 96KB。ESP32 有 520KB SRAM不用太抠。LV_DISP_DEF_REFR_PERIOD是刷新周期单位毫秒。30ms 对应约 33fps够用。设太小会增加 CPU 占用设太大界面会卡顿。踩坑记录lv_conf.h默认在库目录下直接改会被库更新覆盖。正确做法是在项目include/下建lv_conf.h并在platformio.ini里加-DLV_CONF_INCLUDE_SIMPLE。4.3 触摸坐标偏移与校准的实操方法电阻屏的触摸坐标和屏幕像素坐标不是线性对应的需要校准。TFT_eSPI 提供了校准函数uint16_t calData[5]; tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15);运行后屏幕四角会出现校准点依次点击。校准完把calData的值打印出来硬编码到代码里uint16_t calData[5] { 275, 3620, 264, 3532, 1 }; tft.setTouch(calData);如果触摸方向反了左右或上下颠倒在my_touch_read里做坐标变换data-point.x 320 - touchX;>static lv_color_t buf[320 * 240]; lv_disp_draw_buf_init(draw_buf, buf, NULL, 320 * 240); disp_drv.full_refresh 1;full_refresh 1表示每次都刷全屏配合全屏缓冲区撕裂完全消失。代价是内存占用 150KB且每次刷新都要传全屏数据帧率会降到 30fps 左右。如果内存不够用双缓冲 40 行static lv_color_t buf1[320 * 40]; static lv_color_t buf2[320 * 40]; lv_disp_draw_buf_init(draw_buf, buf1, buf2, 320 * 40);双缓冲下 LVGL 渲染 buf1 时DMA 推送 buf2交替进行。撕裂感会明显减轻但不会完全消失。4.6 常见问题速查表问题排查方向快速验证编译报错找不到 lvgl.hlib_deps 未添加检查 platformio.ini屏幕不亮背光引脚未接万用表测 BL 电压触摸坐标不准未校准跑 calibrateTouch界面刷新慢SPI 频率低提高到 40MHz内存分配失败LV_MEM_SIZE 小加到 64KB烧录失败upload_speed 高降到 115200串口无输出monitor_speed 不匹配改 115200按钮点击无反应触摸未注册检查 indev_drv5. 进阶扩展与个人经验分享5.1 用 SquareLine Studio 加速界面开发手写 LVGL 代码创建界面效率不高尤其是复杂布局。SquareLine Studio 是 LVGL 官方推荐的拖拽式界面编辑器导出 C 代码直接集成到工程里。它生成的代码结构清晰控件命名规范比自己手写更易维护。导出时选择 LVGL 8.3 版本生成的ui.c和ui.h放到src/目录。在setup()里调用ui_init()即可。注意 SquareLine 生成的代码默认使用lv_scr_act()作为父对象如果有多屏幕需求需要手动调整。5.2 中文字体裁剪与图标字体的制作LVGL 内置字体不支持中文需要自己生成。用 LVGL 官方的字体转换工具选择需要的汉字范围生成.c文件。常用汉字 3500 个生成的字体文件约 1-2MBESP32 的 Flash 放得下。图标字体可以用 FontAwesome 或 Material IconsLVGL 内置了部分图标符号直接用LV_SYMBOL_XXX宏调用。比如LV_SYMBOL_WIFI、LV_SYMBOL_BATTERY_FULL。提示字体文件放在src/目录下在lv_conf.h里声明LV_FONT_CUSTOM_DECLARE然后在代码里用lv_obj_set_style_text_font设置。5.3 从 SPI 屏升级到 RGB 屏的注意事项SPI 屏的瓶颈在传输带宽320×240 分辨率下 40MHz SPI 勉强够用。如果上 480×320 或更高分辨率SPI 就吃力了需要换 RGB 接口屏。ESP32-S3 支持 RGB LCD 接口可以直接驱动 800×480 的屏幕帧率能到 60fps。但 RGB 屏占用大量 GPIO且需要 PSRAM 做帧缓冲。从 SPI 迁移到 RGBLVGL 的 flush_cb 逻辑完全不同——RGB 屏是连续扫描不需要手动推送只需要把帧缓冲地址告诉 LCD 控制器。TFT_eSPI 不支持 RGB 接口需要换 ESP32 官方的esp_lcd组件。5.4 个人实操体会与后续扩展方向这套组合我前后调了大概一周踩的坑主要集中在配置文件和字节序上。最深的体会是先把 TFT_eSPI 单独跑通再叠 LVGL。很多人一上来就整合出了问题分不清是屏幕驱动的问题还是 LVGL 的问题。分步验证能省大量时间。后续可以扩展的方向接入 WiFi 显示天气、用蓝牙接收手机通知、加 SD 卡做图片轮播。ESP32 的无线能力配合 LVGL 的界面能做出很实用的小设备。如果要做低功耗记得用esp_sleep系列函数LVGL 在休眠前要暂停定时器。最后分享一个小技巧调试 LVGL 界面时打开LV_USE_PERF_MONITOR和LV_USE_MEM_MONITOR屏幕右上角会显示帧率和内存占用对优化很有帮助。这两个宏在lv_conf.h里开启正式发布时关掉即可。
返回列表