简介:面向ESP32-S3物联网开发者的LVGL电容触摸屏驱动资源包,基于ST7789显示控制器与CST816触摸芯片,给出完整的适配与示例代码,可帮助中高级嵌入式开发者快速搭建1.69寸屏的图形交互界面。压缩包共971个文件,大小17.39MB,以C源文件、头文件为主,包含LVGL官方示例工程,另有Python脚本、Markdown文档、Kconfig配置、PNG/JPG图片素材及TTF字体文件,覆盖源码、配置、文档与图像资源;其中C示例演示了widgets、music等经典界面,可直接编译观察显示与触摸联动效果。已有6074人学习下载,适合正在调试LVGL移植、SPI或I2C驱动及自定义触摸回调的开发者。除基础驱动外,还可从配置文件与示例中学习DMA或中断方式读取CST816坐标、将触摸事件注册为LVGL输入设备,以及适配ST7789初始化参数等关键细节;按模块划分的目录有助于在真实工程中快速定位代码,为物联网项目加入流畅的图形控制界面提供可参考实现。 手里拿到一块1.69寸的电容触摸屏,身边正好有块ESP32-S3开发板,想用LVGL把界面跑起来——这是很多刚接触嵌入式GUI的人都会走一遍的路。拆开看核心就三件事:把屏幕的SPI驱动搞定,把触摸芯片的I2C数据读出来,再把LVGL的显示缓冲区和输入设备接上。这套方案跑通以后,我可以在屏幕上滑动控件、切换页面、做仪表盘,所有渲染都在本地完成,不依赖外部系统。这篇把完整的驱动思路、关键代码和踩坑点都整理出来,单片机上想上触摸界面的朋友可以直接参考。
1. 项目概述与硬件选型
1.1 这套方案到底在做什么
ESP32-S3 本身不带屏幕控制器,LVGL 是纯软件图形库,所以中间需要一个“显示驱动层”把 LVGL 绘制好的像素点搬运到屏幕,同时把触摸芯片的坐标喂回给 LVGL。显示链路是:LVGL 生成 RGB565 像素数据 → 通过 SPI 写入屏幕控制芯片 → 屏幕显示。触摸链路是:手指按下 → 电容触摸芯片通过 I2C 上报坐标 → LVGL 的输入设备驱动读取坐标 → UI 响应。
这个方案解决的核心痛点是“怎样用一套可复用的代码,让 LVGL 原生的控件、动画和事件机制跑到一块便宜的 SPI 屏幕 + I2C 触摸屏上”。相比用成品串口屏,这套做法的灵活性高很多,成本也低,适合产品原型、工控面板、桌面小摆件这类场景。
1.2 关键元件选型与备坑提示
1.69寸这个规格市面上常见的组合是 ST7789V 屏幕控制芯片 + CST816T 触摸芯片,分辨率 240×280,SPI 接口,触摸走 I2C。也有少数模组用 GC9A01 圆形屏或 CST816S,买的时候一定问清楚主控型号和触摸芯片型号,丝印也要核对一下,不同批次引脚定义可能不一样。
ESP32-S3 选型上注意两个点。第一,S3 的 GPIO 数量多且大部分可以映射到 SPI/I2C,但 GPIO35、GPIO36、GPIO37 这类通常只能做输入,不能做输出,接屏幕信号线会直接翻车。第二,部分开发板把 USB 串口芯片接在固定引脚上,如果和你的屏幕信号冲突,就需要换引脚或者换板子。我自己的习惯是先把原理图找出来,确认要用的引脚没有被板载外设占用,再动手接线。
LVGL 版本也要提前定。资料最全、教程最多的是 8.3.x,API 稳定,网上大部分例程都是这个版本;9.x 改动比较大,显示驱动和输入设备的注册方式变了,新手不建议一上来就用新版。后面所有代码和配置我都按 8.3 来写。
2. 开发环境搭建与工程骨架
2.1 环境选型:ESP-IDF 还是 Arduino
这里我推荐用 PlatformIO + Arduino 框架作为起步,不是因为它比 ESP-IDF 专业,而是因为跑 LVGL 这类偏验证性质的工程时,Arduino 框架的构建链路短、报错直观、库管理方便。ESP-IDF 功能更底层,适合要精细控制内存、做产品量产的项目,但第一次调试屏幕和触摸时,它会把很多变量引入进来,增加排查成本。
如果后续项目要量产,再把底层移植到 ESP-IDF 也不迟,LVGL 的显示驱动和输入设备驱动是跨框架的,核心代码基本不用改,只需要替换 SPI 和 I2C 的操作接口。
2.2 platformio.ini 配置解析
一个能跑的 PlatformIO 工程配置大概是这样的:
[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 build_flags = -DLV_CONF_INCLUDE_SIMPLE lib_deps = lvgl/lvgl@^8.3.11这里有个关键的LV_CONF_INCLUDE_SIMPLE。LVGL 库本身不自带lv_conf.h,需要你把配置文件放在项目的include目录里,然后在编译时定义为简单引用方式,LVGL 内部才能找到它。如果你把lv_conf.h放到了别的位置,编译时会报一个类似找不到 lv_conf.h 的错误。
board字段也可以直接用esp32-s3-devkitc-1,如果你的板子不在列表里,就手动指定。S3 的 USB 串口驱动(CP2102 或 CH340)没装好的话,Windows 下会找不到 COM 口,先处理驱动再继续。
2.3 环境相关的常见报错
很多人在新建工程后第一步就卡住,报错信息类似failed to set target esp32s3: non zero exit code 2。这个错误大概率是平台工具链版本太老,或者 Python 依赖损坏。处理方法是升级 platform:
pio pkg update -p espressif32然后清掉缓存重新编译。如果是 Arduino IDE 环境,则优先检查开发板管理器里的 ESP32 包版本是否支持 S3,尽量升级到 2.0.x 以上。这类问题看着吓人,其实和环境本身的关系最大,代码还没开始写,先深呼吸,把工具链理顺。
3. 屏幕与触摸底层驱动
3.1 屏幕初始化与像素格式
ST7789V 这颗芯片上电后不会自动进入可显示状态,需要按顺序发送初始化命令,比如睡眠退出、像素格式设置、显示开。常用初始化序列里最核心的几条是:
spi_write_cmd(0x01); // SWRESET delay(50); spi_write_cmd(0x11); // SLPOUT delay(100); spi_write_cmd(0x3A); // COLMOD spi_write_data(0x05); // 16bit RGB565 spi_write_cmd(0x20); // INVOFF spi_write_cmd(0x21); // INVON, 配合模块默认偏色情况调整 spi_write_cmd(0x13); // NORON spi_write_cmd(0x29); // DISPON0x3A设置为0x05表示 RGB565,LVGL 颜色深度 16bit 时正好对应。颜色反色的处理建议直接在初始化里用INVON/INVOFF切换,而不是在 LVGL 层做,省资源也直观。不同模组默认反色情况不一样,有的要0x20有的要0x21,以实际显示为准。
SPI 速率方面,ST7789V 官方标称可以跑到很高,但实际要看杜邦线长度和供电稳定性。调试阶段控制在 20MHz-40MHz 比较稳,跑通后再往上拉。SPI 模式一般是 Mode0,如果屏幕花屏且颜色错乱,试试 Mode2 或者检查接线长度。
3.2 触摸芯片 CST816T 的读取
CST816T 是 I2C 接口的电容触摸芯片,7-bit 地址通常是0x15。它有几组寄存器存触摸数据,最常用的读取方法是读连续寄存器:
uint8_t reg[6]; i2c_read_bytes(0x15, 0x01, reg, 6); uint8_t gesture = reg[0]; // 手势 uint8_t fingers = reg[1]; // 触控点数 uint16_t x = ((reg[2] & 0x0F) << 8) | reg[3]; uint16_t y = ((reg[4] & 0x0F) << 8) | reg[5];X 和 Y 都是 12bit 数据,所以高字节只取低 4 位。手指数为 0 的时候表示没有触摸,这个状态一定要正确地传给 LVGL,否则界面会卡在按下状态。
有些模组的触摸芯片需要复位引脚拉一下才能正常 I2C 通信,建议初始化时把触摸复位脚拉低 10ms 再拉高,等 50ms 再读取。I2C 速度用 400kHz 一般没问题,100kHz 也能跑,只是响应略慢。
3.3 坐标映射与方向校正
触摸芯片上报的坐标方向和 LVGL 的屏幕方向不一定一致,这是新手最容易懵的地方。我的排查方法是先在代码里画一个十字光标,实际点击屏幕四个角,看触摸坐标落在哪里,然后总结出映射关系。
假设屏幕竖屏是 240×280,LVGL 分辨率也是 240×280,触摸原始坐标可能正好对应。但如果 LVGL 设置了屏幕旋转,比如LV_DISP_ROT_90,那就需要交换 X/Y,并对其中一个轴取反,常见的映射转换如下:
switch (rotation) { case LV_DISP_ROT_90: out_x = raw_y; out_y = 240 - 1 - raw_x; break; case LV_DISP_ROT_180: out_x = 240 - 1 - raw_x; out_y = 280 - 1 - raw_y; break; default: out_x = raw_x; out_y = raw_y; break; }这里的取反要用当前分辨率减 1 再减坐标,直接用 240 减会差一个像素,虽然肉眼几乎看不出来,但有些强迫症会介意到底对齐没有。其实从触摸芯片读到坐标后,你还可以加一点软件校准,比如把最大最小值映射到屏幕分辨率,不过大部分正点原子、微雪、中景园的模组出厂一致性都还行,省掉这一步问题也不大。
4. LVGL 接入与界面跑通
4.1 lv_conf.h 关键配置
lv_conf.h是整个 LVGL 行为的开关。最基础的几个配置,建议直接抄:
#define LV_COLOR_DEPTH 16 #define LV_MEM_SIZE (64U * 1024U) #define LV_TICK_CUSTOM 0 #define LV_USE_LOG 1颜色深度和屏幕的 RGB565 保持一致。LV_MEM_SIZE是 LVGL 内部动态内存池大小,给 64KB 跑简单界面够用,如果要加载大量图片资源或复杂控件树,可以加大到 80KB 或更多。日志功能调试时打开,定到LV_LOG_LEVEL_WARN就好,全开会导致刷屏。
tick 这一项,如果用 Arduino 的millis()自己喂心跳,就把LV_TICK_CUSTOM设为 0;如果你想让 LVGL 内部自动获取时间,可以设为 1 并配置自定义时间函数。我习惯手动喂,逻辑直白可控。
4.2 显示 flush 与输入设备注册
LVGL 绘制过程是:先把需要更新的区域渲染到 draw buffer,然后调用 flush 回调把这段 buffer 的数据推到屏幕。flush 回调的写法核心就几步,设置写地址窗口、发送像素数据、调用lv_disp_flush_ready:
void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); lcd_set_address_window(area->x1, area->y1, area->x2, area->y2); lcd_push_pixels((uint16_t *)color_p, w * h); lv_disp_flush_ready(drv); }lv_disp_flush_ready必须在数据发送完成后调用,LVGL 靠这个信号判断当前 buffer 是否可复用。如果在 DMA 模式下,一定要在传输完成中断里调用,不能在发起后立刻调用。这是最常见的崩溃点之一。
输入设备注册类似,需要实现一个read_cb:
void touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { static int16_t last_x, last_y; uint16_t x, y; uint8_t f = touch_read(&x, &y); if (f > 0) { last_x = x; last_y = y; >void loop() { lv_timer_handler(); delay(5); }但如果你的工程里有网络、传感器等其他任务,把 LVGL 放进一个独立 FreeRTOS 任务更好。创建一个 4096 字节栈的任务,在循环里调lv_timer_handler(),并把其它任务对 UI 的修改通过队列或互斥量同步过来。我习惯在 S3 双核上把 LVGL 任务绑到一个核心,其它业务放另一个核心,互不干扰。
5. 踩坑记录与排查速查表
5.1 编译与链接阶段的问题
最常见的一个错误就是所有文件都在,但编译时报 can't findlv_conf.h。这时先检查lv_conf.h是否在 include 目录,以及是否开启了LV_CONF_INCLUDE_SIMPLE。另一个错误是内存不足:LVGL 初始化时报lv_memallocation failed,多半是LV_MEM_SIZE配置太小,调到 80KB 以上一般能解决。
S3 板子如果用 Arduino 框架,偶尔会有 flash 分区不够或工具链版本问题,导致下载失败。遇到这种先换 USB 线,再换串口驱动,最后才怀疑代码。我至少有一次是因为数据线只能充电不能传数据,卡了大半天。
5.2 显示与触摸阶段的问题
白屏先看屏幕初始化有没有执行到 DISPON,再看 SPI 有没有数据。花屏优先查 SPI 模式、接线和供电,如果是动态花屏,可能和刷新时 SPI 与其他外设冲突有关。颜色不正直接查像素格式和 INVON/INVOFF。显示方向不对就改lv_disp_drv_t里的旋钮,不要自己改坐标。
触摸完全没反应,先确认 I2C 地址对不对,很多模组 touch 芯片挂在引脚上需要先复位。触摸经常飘或者点不准,优先做坐标校正而非换芯片。LVGL 控件按住后触发不了 click,检查是不是触摸状态一直保持 PRESSED,常见原因是手指抬起时 FIFO 没有清空或触摸读取频率太低。
5.3 排查速查表
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
| 完全不亮 | 背光引脚没使能 | 给 BL 脚高电平 |
| 白屏 | 初始化序列没走完 | 检查 DISPON、复位时序 |
| 花屏 | SPI Mode/速率不对 | 改成 Mode0,速率降到 20MHz |
| 颜色偏色 | 像素格式或 INVON 反了 | 确认 COLMOD=0x05,切换 0x20/0x21 |
| 触摸无响应 | I2C 地址或复位不对 | 确认 0x15,拉一次触摸复位 |
| 触摸点反向 | 坐标系未映射 | 按前面公式转换 |
| UI卡死 | LVGL内存不足 | 调大 LV_MEM_SIZE |
6. 性能优化与个人体会
6.1 双缓冲与 DMA
如果只是点亮屏幕,单缓冲就够了。但 LVGL 刷新区域比较密集时,单缓冲会导致撕裂感和闪烁。我试过给 240×280 的屏配置两个 20 行高的行缓冲,LVGL 渲染完一块,DMA 发一块,画面明显顺滑很多。代码上就是初始化两个 buffer 传给lv_disp_draw_buf_init,然后将 SPI 发送函数改成 DMA 模式。
S3 自带缓存一致性处理,用内部 SRAM 做 DMA buffer 最稳。如果要从 PSRAM 做 buffer,需要先确认工程里打开了 PSRAM 并正确配置 cache,否则 DMA 传输可能出现随机花屏。性能上不必过度追求,简单 UI 能做到几十帧就够流畅了,触摸操作主要是响应延迟,也不是帧率问题。
6.2 触摸滑动的体验调优
LVGL 里LV_INDEV_DEF_DRAG_THROW之类的参数会影响滑动手感,默认值偏保守。如果觉得列表滑动不够跟手,可以在lv_indev_drv_t里调整采样间隔和消抖时间,但注意采样太频繁会占用 CPU。我在实际项目里把read_cb的调用频率保持在 20ms 左右,既不会有明显延迟,也不会因为坐标跳变引起误触。
还有一点,CST816T 支持手势寄存器(滑动方向、双击等),这些手势数据可以读出来直接映射成 LVGL 的键盘事件或自定义事件,实现“屏幕下滑返回上一页”这类操作,不需要用像素坐标去算手势,省很多事。
6.3 从点亮到产品化的小建议
按我自己的经验,这块屏跑 LVGL 非常适合做本地控制面板和桌面信息屏,S3 的 WiFi/BLE 能力可以顺带把数据源接进来,比如温湿度、MQTT 消息、设备状态。硬件上建议把屏幕排线固定住,避免触摸 I2C 线在运行中被干扰,尤其是 S3 双核高负载运行时,触摸偶尔飘一下大概率是线的问题不是代码问题。
最后分享一个我很喜欢的小技巧:调试触摸对齐时,在 LVGL 里建一个隐藏页面,放一个跟随lv_indev坐标移动的十字光标,启动后点屏幕四个角,直接把坐标映射关系打印出来。这样比一遍遍猜方向高效得多,也是排查坐标问题最直观的工具。
本文还有配套的精品资源,点击获取