简介:面向 ESP32-S3 嵌入式开发者的 LVGL 图形界面例程包,基于 ESP-IDF 框架驱动 CST328 触摸与 ST7789 显示屏,适合需要在 TFT 彩屏上快速实现交互界面的物联网项目开发者。压缩包共 1342 个文件,大小 26.45MB,主要包含 c/h 源码、Python 辅助脚本、Markdown 说明文档以及 PNG 图片资源,覆盖 LVGL 各类原生 demo 与驱动配置示例。已有 1465 人学习下载,内容组织清晰,便于按模块检索。开发者可快速掌握 ESP32S3 与 LVGL 的整合方法,获得屏幕初始化、触摸校准、控件布局等核心代码,并直接复用其中音乐播放器、控件展示等示例,显著缩短人机界面开发周期。
1. ESP32S3驱动CST328与ST7789:一块带触摸的小屏如何跑起LVGL
手里一块屏幕标注触控IC是CST328、显示IC是ST7789,结果商家给的驱动只适配STM32,这种情况在开发圈里太常见了。我把这套硬件接到ESP32S3上,基于ESP-IDF框架把LVGL完整跑通,触摸、显示、UI三端衔接全部能编译运行。这篇笔记会拆开讲硬件连接、两颗驱动IC的初始化与读取逻辑、LVGL的移植点,再记录几个实测必踩的坑。适合手里有同款屏、想在ESP32S3上快速验证LVGL的开发者,也适合第一次把电容触摸屏接进ESP-IDF工程的人——踩坑记录能帮你少折腾一个晚上。
2. 系统架构与硬件连接:一块屏背后的协议链路
CST328与ST7789各管一摊活,它们和主控之间走的是不同总线。搞清这一步,后面代码看多少都有底。
2.1 引脚分配与硬件接线
ST7789是SPI接口的TFT驱动IC,CST328是I2C接口的电容触摸IC。一块成品屏幕模组通常把这两颗IC集成在同一块FPC上,对外引出两组引脚。接线前先确认屏厂给的原理图,不同批次引脚顺序可能不一样。
| 屏幕信号 | 对接IC | ESP32S3 GPIO | 说明 |
|---|---|---|---|
| SCK / SCL | ST7789 / CST328 | GPIO12(共用) | 两路总线的时钟 |
| MOSI / SDA | ST7789 / CST328 | GPIO11(共用) | 数据线 |
| CS | ST7789 | GPIO10 | 片选,低有效 |
| DC | ST7789 | GPIO9 | 数据/命令切换 |
| RST | ST7789 | GPIO8 | 复位 |
| BL | 背光 | GPIO7(或直连3.3V) | PWM可调亮度 |
| I2C_INT | CST328 | GPIO6 | 触摸中断,可选 |
注意CST328的I2C地址有两种常见值:0x15或0x01,具体由屏厂在模组内部通过地址引脚或寄存器配置决定。调试时可以先用I2C扫描工具探测地址,再写死到驱动里。这块屏出厂固件把触摸坐标原点放在左下角,和ST7789的默认扫描方向相反,后面LVGL里要做坐标变换,这部分在第四章展开。
2.2 协议链路:为什么触摸走I2C、显示走SPI
一条SPI总线跑满可以到几十Mbps,适合推送大量像素数据;I2C虽然慢,但触摸报文一次只有几个字节,实时性要求高的是上报频率而不是带宽。于是显示数据走SPI、触摸状态走I2C成为这类低成本模组的常见组合。
我一般把这两路独立看:显示链路是从LVGL的缓冲区到ST7789的GRAM,触摸链路是从CST328的寄存器到LVGL的indev驱动。两条链路在LVGL内部汇合,互不干扰。理解这个分层之后,调试的时候就能快速判断问题是出在屏幕刷新、触摸读取还是坐标映射层,不会一股脑怀疑硬件。
2.3 ESP-IDF工程目录与构建流程
这套例程用ESP-IDF的组件化结构组织,LVGL以组件形式引入。目录长这样:
├── main │ ├── CMakeLists.txt │ └── app_main.c ├── components │ ├── cst328 │ │ ├── include/cst328.h │ │ ├── cst328.c │ │ └── CMakeLists.txt │ ├── st7789 │ │ ├── include/st7789.h │ │ ├── st7789.c │ │ └── CMakeLists.txt │ └── lvgl │ └── 官方 LVGL 组件 ├── sdkconfig.defaults └── CMakeLists.txtESP-IDF构建系统会自动把main和components下的每个子目录当作待编译组件,前提是每个目录都有CMakeLists.txt并声明了源文件和头文件路径。构建命令没什么特殊的:
idf.py set-target esp32s3 idf.py menuconfig # 配置 SPI / I2C 引脚与频率 idf.py build idf.py flash -p /dev/ttyUSB0 monitorsdkconfig.defaults里预设了LVGL需要开启的宏,比如CONFIG_LV_COLOR_DEPTH_16=y和CONFIG_LV_MEM_CUSTOM=y。前者对应ST7789的RGB565,后者让LVGL使用ESP-IDF的堆管理器,避免内存碎片问题。LVGL组件通过IDF组件管理器拉取,版本锁定在8.3.x,这个版本与ESP-IDF的esp_lcd接口配合最稳定。
3. 核心驱动拆解:从初始化序列到坐标上报
这一章把两条驱动的关键函数逐段讲透。代码不算长,但每一行都对应着头疼过的时序问题。
3.1 ST7789:一条SPI初始化序列与显存刷新
ST7789的初始化说白了就是按顺序写寄存器,顺序错了或者时序不对,屏就是白的或者花的。例程里把初始化命令做成一个表,逐条发送:
// st7789_init_cmds: 命令, 参数个数, 参数... static const uint8_t init_cmds[] = { 0x01, 0, // SWRESET 软复位 0x11, 0, // SLPOUT 退出睡眠模式 0x36, 1, 0x00, // MADCTL 扫描方向, 0x00 按屏厂丝印默认 0x3A, 1, 0x05, // COLMOD 像素格式, 0x05 = 16位 RGB565 0x20, 0, // INVOFF 关闭颜色反转 0x13, 0, // NORON 正常显示模式 0x29, 0, // DISPON 打开显示 }; void st7789_init(void) { st7789_send_cmd(0x01, NULL, 0); vTaskDelay(pdMS_TO_TICKS(120)); // 软复位后必须等待, 否则后续命令丢失 for (int i = 0; i < sizeof(init_cmds); i += (init_cmds[i+1] + 2)) { st7789_send_cmd(init_cmds[i], &init_cmds[i+2], init_cmds[i+1]); vTaskDelay(pdMS_TO_TICKS(5)); } }st7789_send_cmd内部会先拉低DC引脚表示命令,再通过SPI发送;发送像素数据时拉高DC。每个命令之间的延时是ST7789数据手册要求的,尤其是软复位后那120毫秒,省略的话屏幕上大概率只有一片白。如果想要画面翻转,直接改MADCTL参数就行,比每次都去动XY坐标划算。
显存刷新的核心是开窗函数。LVGL每次只刷新一小块区域,驱动就要把GRAM的写指针移到那块区域:
void st7789_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t data[4]; data[0] = x0 >> 8; data[1] = x0 & 0xFF; data[2] = x1 >> 8; data[3] = x1 & 0xFF; st7789_send_cmd(0x2A, data, 4); // CASET 列地址 data[0] = y0 >> 8; data[1] = y0 & 0xFF; data[2] = y1 >> 8; data[3] = y1 & 0xFF; st7789_send_cmd(0x2B, data, 4); // PASET 行地址 st7789_send_cmd(0x2C, NULL, 0); // RAMWR 开始写显存 }开窗之后连续写入RGB565像素即可。注意分辨率:ST7789支持240x320,也有的屏是240x280,CASET和PASET的参数必须和实际分辨率对齐,否则画面会偏移甚至滚动。这个函数会被LVGL的flush回调频繁调用,所以性能很关键,所有等待都放在SPI硬件层处理,CPU不要在这条路径上做像素级操作。
3.2 CST328:读触摸状态、滤掉噪声、上报坐标
CST328的数据读取非常简单:往I2C总线上写一个寄存器地址,然后连续读6个字节。第一个字节是触摸状态和点数,后四个字节是两个触摸点的坐标:
#define CST328_I2C_ADDR 0x15 #define CST328_REG_TOUCH 0x01 bool cst328_read_point(int16_t *x, int16_t *y, uint8_t *points) { uint8_t buf[5] = {0}; // 先写寄存器地址, 再读 5 字节: 状态 + 点1 X + 点1 Y + 点2 X + 点2 Y i2c_write_read(CST328_I2C_ADDR, CST328_REG_TOUCH, buf, 5); *points = buf[0] & 0x0F; // 低四位是有效触摸点数 if (*points == 0) return false; *x = ((buf[1] & 0x0F) << 8) | buf[2]; // X: 高4位 + 低8位 *y = ((buf[3] & 0x0F) << 8) | buf[4]; // Y: 高4位 + 低8位 return true; }坐标是12位的,范围在0到4095之间,而屏幕分辨率只有240x320。这个比例映射放在LVGL的indev回调里做最简单。还有一点:CST328上报的坐标含有噪声,手指离开屏幕的瞬间经常出现一个跳变的点。例程里的策略是连续读到两次有效坐标且位置变化小于某个阈值才更新屏幕上的位置,相当于一个软件防抖。
3.3 LVGL桥接层:把两个驱动接进事件循环
LVGL本身不知道硬件长什么样,它只认两个回调,一个是显示刷新,一个是触摸输入。把ST7789和CST328接进LVGL,就是实现这两个回调:
void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { st7789_set_window(area->x1, area->y1, area->x2, area->y2); uint32_t size = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1); st7789_send_pixels((uint16_t *)color_p, size); lv_disp_flush_ready(drv); // 通知 LVGL 可以继续渲染 } void lvgl_touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { int16_t x, y; uint8_t points; if (cst328_read_point(&x, &y, &points)) { // CST328 原点在左下角, 屏幕显示原点在左上角, 做 Y 翻转 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />