
简介这是一份基于ESP-IDF框架、面向ESP32S3微控制器的LVGL图形界面例程用于驱动CST328触摸芯片与ST7789 TFT显示屏适合希望快速构建彩色显示屏交互界面的嵌入式开发者与物联网应用工程师。资源包内共1342个文件包括497个C源文件、256个头文件、150个Python脚本、139个Markdown文档以及字体、图片、构建配置等辅助文件压缩后整体为26.45MB目录结构清晰。已有1448人学习/浏览例程完整覆盖了从SPI接口配置、显示屏初始化到加载按钮、滑块、图表等LVGL控件的全过程。资源中附带的音乐播放器、控件演示等UI示例及配套图片、字体素材可帮助开发者理解LVGL的移植和界面设计思路。开发者可直接参考或移植到实际项目大幅降低彩色触摸界面开发门槛缩短产品原型的验证周期。 最近在折腾一个基于ESP32-S3的物联网项目功能比较复杂需要一块带触摸的显示屏来做人机交互。屏幕选了ST7789驱动的TFT液晶触摸部分用的是CST328电容触摸芯片整体图形界面用LVGL来做。这里把从硬件接线、ESP-IDF环境搭建到ST7789和CST328底层驱动再到LVGL最终跑起来的完整过程分享出来。这不是一个简单的“抄代码”教程而是把我实际遇到的那些坑和排查思路都写出来希望对正在这个组合上花时间的开发者有点帮助。1. 为什么选这个硬件组合屏幕、触摸、芯片的定位1.1 几个关键部件的分工ESP32-S3是一颗带Wi-Fi和BLE的MCU主频最高能跑到240MHz内存和Flash配置也够用最关键的是它内置了硬件SPI和I2C外设驱动LCD和触摸屏不需要软件模拟时序这对LVGL这类需要高帧率渲染的场景很重要。ST7789是一个很常见的TFT显示屏驱动IC最大分辨率通常是240x320像素支持SPI接口初始化寄存器表完整兼容性很好市面上能找到大量现成的屏模组。CST328则是电容式触摸控制芯片通讯方式是I2C可以上报最多两点触摸坐标用在3.5寸左右的屏幕上刚刚好。LVGL则是一个开源嵌入式图形库自带控件、动画和对象管理像是做界面用的“积木盒”。这套组合的核心优势在于ESP32-S3的性能足够带动LVGL的渲染ST7789的SPI接口速度不低触摸部分用I2C读取也不占用太多资源三者配合可以做到页面切换流畅、触控跟手。1.2 适合用来做什么场景如果你需要在设备上展示数据、设置参数或者做成一个小型人机交互面板这套组合是非常合适的。比如我之前在做的智能家居中控需要显示传感器数值、开关状态还要能触摸切换页面。用LVGL做界面可比自己在屏幕上画裸像素省太多事了。另一个典型场景就是开发板的学习套件因为整个链路从显示到触摸到UI都很完整能学到不少东西。2. 动手前的硬件接线和引脚分配2.1 ST7789屏幕的SPI接线ST7789屏幕模组通常引出了SCLK时钟、MOSI主出从入、CS片选、DC数据/命令选择、RST复位、BLK背光这几个引脚。有的模组把MISO也引出来了但在只写不读的纯显示场景下可以不用接。我实际用的接线方式如下ESP32-S3引脚基于开发板丝印你完全可以按需调整SCLK - GPIO12MOSI - GPIO11CS - GPIO10DC - GPIO9RST - GPIO8BLK - GPIO7这里有个细节要注意ST7789的SPI最高时钟频率可以到几十MHz但刚开始调试时建议先降到10~20MHz等初始化正常后再把频率提上去不然很容易出现“花屏但主控没报错”的情况。2.2 CST328触摸的I2C接线CST328是通过I2C通信的通常引出SDA、SCL、INT中断、RST复位四个引脚。INT引脚在检测到触摸时会拉低主控可以通过读取这个引脚来减少轮询次数。接线参考SDA - GPIO4SCL - GPIO5INT - GPIO6RST - GPIO14通常可以随便接一个GPIO或者干脆接主控的复位值得注意的是CST328在模组上一般已经内置了上拉电阻外接I2C总线上就不需要额外再并联上拉了反而可能导致电平匹配问题。3. ESP-IDF开发环境准备版本选择和工程初始化3.1 版本选择和建议ESP-IDF现在迭代很快热词里有人提到“C:/esp_544”这类路径说明v5.4.4这个版本在开发者中很常用。我用的是v5.4.4整体稳定对ESP32-S3的支持很完善。如果你是完全从零开始推荐按官方文档装好飞书文档里说的依赖软件包Windows用户可以用Espressif的离线安装器Linux用户则需要手动克隆仓库并安装工具链。装好之后一定要记得执行install.sh和export.shWindows是install.bat和export.bat来设置环境变量。3.2 创建第一个工程并设置目标芯片初始化一个空工程很简单用idf.py create-project就可以。接下来最重要的一步就是设置目标芯片idf.py set-target esp32s3这个命令会重写项目里的sdkconfig文件同时会把工具链切换到ESP32-S3对应的版本。很多人在这一步会踩到“failed to set target esp32s3: non zero exit code 2”这个错误。根据我的经验这类报错常见的原因有三个环境变量没有正确加载导致idf.py找不到对应的工具。当前用户对项目目录没有写权限。Python包冲突特别是系统里同时装了多套Python时。排查方式很简单先重新打开终端确保ESP-IDF的环境变量已经生效然后试试idf.py --version能不能正常输出。如果不行再检查IDF_PATH是否指向了正确的ESP-IDF目录最后才考虑Python环境问题。4. 点亮屏幕ST7789底层驱动的完整实现4.1 初始化SPI接口ST7789走的是SPI总线在ESP-IDF里配置起来非常直接。下面是初始化SPI主机接口的代码示例#include driver/spi_master.h #define LCD_SCLK_PIN 12 #define LCD_MOSI_PIN 11 #define LCD_CS_PIN 10 #define LCD_DC_PIN 9 #define LCD_RST_PIN 8 #define LCD_BL_PIN 7 spi_device_handle_t lcd_spi; void lcd_spi_init(void) { spi_bus_config_t buscfg { .sclk_io_num LCD_SCLK_PIN, .mosi_io_num LCD_MOSI_PIN, .miso_io_num -1, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 320 * 240 * 2 }; spi_device_interface_config_t devcfg { .clock_speed_hz 20 * 1000 * 1000, // 先跑20MHz .mode 0, // ST7789一般为SPI Mode 0 .spics_io_num LCD_CS_PIN, .queue_size 7, .flags SPI_DEVICE_HALFDUPLEX }; spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO); spi_bus_add_device(SPI2_HOST, devcfg, lcd_spi); }要注意的是SPI_DEVICE_HALFDUPLEX这个标志ST7789这类屏幕只需要主控向屏幕写数据不需要读回半双工模式能有效降低总线负载。4.2 ST7789初始化命令序列ST7789的初始化要严格按照数据手册里面的命令时序来写核心是关闭睡眠、设置像素格式、设置分辨率、打开显示这几个步骤。下面是我整理的最简初始化序列void lcd_cmd(uint8_t cmd, const uint8_t *data, size_t len) { // 先拉低DC脚表示写命令 // 然后发送cmd本身 // 如果len0再拉高DC脚表示写数据 } void lcd_init(void) { // 硬件复位 gpio_set_level(LCD_RST_PIN, 0); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(LCD_RST_PIN, 1); vTaskDelay(pdMS_TO_TICKS(120)); lcd_cmd(0x11, NULL, 0); // 退出睡眠模式 vTaskDelay(pdMS_TO_TICKS(20)); lcd_cmd(0x3A, (uint8_t[]){0x55}, 1); // 16位色深 RGB565 lcd_cmd(0x36, (uint8_t[]){0x00}, 1); // 扫描方向根据你的模组调整 lcd_cmd(0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0x3F}, 4); // 列地址0~319 lcd_cmd(0x2B, (uint8_t[]){0x00, 0x00, 0x00, 0xEF}, 4); // 行地址0~239 lcd_cmd(0x29, NULL, 0); // 打开显示 lcd_cmd(0x2C, NULL, 0); // 开始写显存 }这里面的0x36寄存器里存着扫描方向屏幕模组的FPC排线不同这个值需要按实际显示的方向调整。如果发现显示内容是旋转或者镜像的改这个值就可以了。4.3 测试显示填充整屏颜色初始化跑通之后写一个简单的填色函数来验证屏幕是否工作正常void lcd_fill_color(uint16_t color) { uint8_t *buf malloc(320 * 240 * 2); memset(buf, (color 8) 0xFF, 320 * 240); for (size_t i 0; i 320 * 240; i) { buf[i * 2] (color 8) 0xFF; buf[i * 2 1] color 0xFF; } spi_device_transmit(lcd_spi, t); free(buf); }如果屏幕全屏显示成同一颜色说明初始化序列和SPI通信都正常了。如果出现横条纹或颜色不对多半是RGB565字节顺序问题也就是0x36寄存器里的BGR位没设置对。5. 触摸输入CST328驱动与坐标处理5.1 I2C初始化CST328的I2C地址一般是0x15(7bit) 或者0x2A(8bit)不同模组可能不同最好看下屏幕模组背后的丝印或者厂家提供的规格书。ESP-IDF代码里初始化I2C主机接口#include driver/i2c.h #define I2C_PORT I2C_NUM_0 #define TOUCH_SDA 4 #define TOUCH_SCL 5 void touch_i2c_init(void) { i2c_config_t conf { .mode I2C_MODE_MASTER, .sda_io_num TOUCH_SDA, .scl_io_num TOUCH_SCL, .sda_pullup_en GPIO_PULLUP_ENABLE, .scl_pullup_en GPIO_PULLUP_ENABLE, .master.clk_speed 400 * 1000 }; i2c_param_config(I2C_PORT, conf); i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0); }关于触摸芯片是否需要软复位我实际测下来如果拉到外部复位引脚每次上电后等50毫秒再走I2C读取是最稳定的不然偶尔会出现首次读取失败的情况。5.2 读取触摸坐标CST328内部维护一个触摸数据缓冲区主控读取该区域就能拿到坐标信息。以单点触摸为例核心思路是先读状态寄存器判断是否有手指按下然后读X和Y坐标寄存器。典型寄存器布局如下不同版本可能有差异需要参考数据手册寄存器0x00有效触摸点数寄存器0x02~0x05第一个触摸点的X/Y坐标高低字节寄存器0x06~0x09第二个触摸点的X/Y坐标如果有多点读取流程可以用i2c_master_write_read_device一次性把整块数据读出来然后解析。我封装成这样一个函数typedef struct { uint16_t x; uint16_t y; bool touched; } touch_point_t; touch_point_t touch_read(void) { uint8_t buf[8]; i2c_master_write_read_device(I2C_PORT, CST328_ADDR, (uint8_t[]){0x00}, 1, buf, 8, 100 / portTICK_PERIOD_MS); touch_point_t pt {0}; if (buf[0] 0x0F) { pt.touched true; pt.x (buf[2] 0x0F) 8 | buf[3]; pt.y (buf[4] 0x0F) 8 | buf[5]; } return pt; }注意坐标值不是直接用很多屏模组的触摸原点和屏幕原点不在一个角需要做一次坐标映射。5.3 触摸坐标到屏幕坐标的映射CST328输出的原始坐标范围通常是0~409512位精度或者0~204711位但我们的屏幕分辨率只有320x240所以必须做缩放和方向校正。比如我的模组触摸坐标X方向跟屏幕X方向相反Y方向一致那么映射公式是uint16_t screen_x 320 * pt.x / 4096; uint16_t screen_y 240 * pt.y / 4096;如果要镜像就改为screen_x 320 - 320 * pt.x / 4096;。这一步不做对的话LVGL界面点按钮会“点不准”明明点的是左上角实际触发的是右上角。6. LVGL接入让显示和触摸变成一套完整UI6.1 在ESP-IDF中引入LVGL组件LVGL在ESP-IDF里有两种集成方式一种是使用ESP-IDF的组件管理器拉取一种是把LVGL的源码直接放到components目录下。我推荐用组件管理器直接在项目根目录执行idf.py add-component lvgl它会自动下载并配置好LVGL还能识别已安装的其他组件。如果是手动拷贝源码记得在CMakeLists.txt里面把LV_TICK_CUSTOM配置为1让LVGL自己用系统时钟做心跳。6.2 注册显示驱动到LVGLLVGL要求提供一个“把某个区域的像素写到屏幕显存”的回调函数我们需要把ST7789的写显存逻辑填进去。核心代码如下static void lvgl_lcd_flush_cb(lv_disp_drv_t *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; lcd_set_window(area-x1, area-y1, area-x2, area-y2); // 把color_p中的数据通过SPI直接写入显存 lcd_write_data((uint8_t *)color_p, w * h * sizeof(lv_color_t)); lv_disp_flush_ready(drv); }这里有个关键点lv_color_t默认在宏LV_COLOR_DEPTH为16时是RGB565格式刚好可以跟ST7789的RGB565直接对应不需要做像素格式转换。6.3 注册触摸设备到LVGLLVGL的输入设备回调需要返回当前触摸坐标和按压状态接上我们之前的touch_read函数static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { touch_point_t pt touch_read(); >void app_main(void) { lcd_spi_init(); lcd_init(); touch_i2c_init(); lv_init(); lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.flush_cb lvgl_lcd_flush_cb; disp_drv.hor_res 320; disp_drv.ver_res 240; lv_disp_drv_register(disp_drv); lv_indev_drv_t indev_drv; lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; indev_drv.read_cb lvgl_touch_cb; lv_indev_drv_register(indev_drv); lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, Hello ESP32-S3 LVGL); lv_obj_center(label); while (1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }如果屏幕能显示“Hello ESP32-S3 LVGL”并且触摸屏幕时能感知到按压事件那硬件链路和软件链路就都通了后面就可以开始堆界面了。7. 常见错误与调试心得分享7.1 “failed to set target esp32s3”到底怎么排查这个报错在热词列表里反复出现说明踩的人不在少数。我是这样定位的先用idf.py --version确认工具链环境正常再用echo $IDF_PATH确认环境变量指向正确。如果这两步都通过问题往往是Python虚拟环境或者build目录里的缓存冲突。有一个很“土”但有效的办法把整个工程目录里的build文件夹删掉然后重新set-target。删了之后再执行idf.py fullclean idf.py set-target esp32s3基本能解决一大半环境问题。除此之外Windows用户用PowerShell执行export.ps1之前如果没开执行策略也会导致环境变量没生效这种情况下可以先看脚本输出有没有报错。7.2 显示花屏或颜色不对的处理ST7789花屏的原因不外乎四个SPI时钟频率太高、RGB565字节序没对齐、扫描方向错误、初始化序列漏步骤。我调试时习惯用20MHz时钟如果花屏就往下调到10MHz。颜色不对优先看0x36寄存器里的BGR位显示内容整体“镜像”也是这个寄存器里MSB位决定的。花屏还有一种被忽略的情况是背光的PWM频率太低屏幕看起来在闪烁检查BLK引脚的GPIO输出模式是不是设置成PWM通道了。7.3 触摸不灵敏或漂移的调整触摸不灵敏大多不是芯片问题而是I2C频率过高。如果你把master.clk_speed设置成1MHzCST328可能来不及应答。稳妥做法是先跑400kHz确认功能正常后再优化速度。漂移问题则几乎都是坐标映射没做对。很多屏模组的触摸原点在左上角但坐标方向可能跟屏幕扫描方向不同中间还会隔着一层玻璃厚度产生偏移。建议屏模组驱动起来后用LVGL做一个“触摸测试”界面把读取到的原始X、Y值直接打在屏幕上再跟手指实际位置对比这样大约几分钟就能摸清映射规律。7.4 性能优化的一点经验LVGL在ESP32-S3上跑最大的瓶颈往往在SPI总线传输上。建议打开ESP-IDF的SPI DMA功能也就是在初始化spi_bus_initialize时传入SPI_DMA_CH_AUTO这样传输大块数据时不用占用CPU时间。另外把ST7789的SPI时钟稳定提升到40MHz屏幕刷新率会明显改善。LVGL侧的LV_MEM_SIZE也建议调大到32KB以上否则复杂的页面布局可能会出现莫名其妙的卡死。8. 一个完整可用的工程结构参考最后晒一下我的实际工程目录结构方便你对着搭建my_lvgl_demo/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ ├── main.c // 入口驱动和LVGL初始化 │ ├── lcd.h // ST7789相关 │ ├── lcd.c │ ├── touch.h // CST328相关 │ └── touch.c ├── components/ │ └── lvgl/ // 组件管理器拉取或手动放置 └── sdkconfig主工程的CMakeLists.txt只需要保留最基础的cmake_minimum_required和include即可组件依赖交给idf.py add-component去管理这样最省心。另外如果你和我一样习惯在VSCode里开发装好Espressif的插件后可以直接在IDE里点按钮烧录和监视串口针对idf.py命令的各种回调错误也能在终端面板里看到更清晰的日志。第一次烧录前建议用idf.py -p COMx flash monitor分开执行这样能避免组合命令在出现异常时互相干扰日志也更容易定位。这个项目的完整链路其实不复杂但每一段都可能出现独立的问题。只要先把屏幕点亮、再把触摸读通、最后接上LVGL三个步骤分开做任何一个环节出问题都能快速定位。我个人最深的体会就是调试触摸坐标映射时别着急多点几次把打印出来的坐标跟自己手指的位置对应起来一次就能搞定。接下来你可以试着加两个按钮、做个页面切换体会一下LVGL的控件布局慢慢就会越做越顺手。本文还有配套的精品资源点击获取