拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ESP32-S3+ST7789+CST328+LVGL:从零搭建带触摸的人机界面

简介面向ESP32S3与CST328/ST7789彩色屏幕开发者这套LVGLESP-IDF例程包提供了从硬件初始化到图形界面渲染的完整参考实现。资源共1342个文件涵盖C源码、头文件、Python脚本、Markdown说明文档及字体、位图、GIF等素材压缩后约26.45MB目录按源码、示例、文档和资源分开便于按模块查阅。目前已有1448人学习适合正在做物联网人机界面或嵌入式GUI项目的工程师快速上手。通过这套工程可以掌握SPI接口配置、LVGL组件移植、触摸校准以及音乐播放器、控件展示等多套demo的代码组织方式还能直接复用内置的位图、字体和波形数据显著缩短显示类产品开发周期。对于希望基于ESP-IDF调试LVGL的开发者来说这是很实用的入门与二次开发蓝本。 最近把手上一个老项目的显示部分整体升级了一遍主控换成 ESP32-S3屏幕用最常见的 ST7789 方案 TFT触摸 IC 配上 CST328UI 框架走 LVGL整个工程跑在 ESP-IDF 上。从环境搭建、底层驱动到 LVGL 界面跑通中间踩了不少坑也沉淀出一套可以直接复用的例程。这篇文章把整个链路从头到尾拆开讲适合正准备在 ESP32-S3 上做带触摸人机界面的朋友参考。这套组合在物联网产品和快速原型里非常常见ESP32-S3 本身双核 240MHz带 WiFi/BLE跑 LVGL 足够流畅ST7789 是屏幕驱动里出货量最大的一颗 SPI 接口芯片资料多、驱动成熟CST328 是电容触摸控制芯片走 I2C支持多点上报配 LVGL 的 indev 接口非常合适。下面从方案选型开始到最终运行调试完整记录一遍。1. 项目概述与方案选型1.1 为什么是 ESP32-S3 ST7789 CST328选 ESP32-S3 不是因为参数堆得高而是它的外设和内存对 LVGL 实在友好。LVGL 在 MCU 上最吃两样东西RAM 和刷新带宽。ESP32-S3 有 512KB SRAM跑 320×240 的 RGB565 界面双缓冲加 UI 内存开销一般 100KB 以内剩余空间足够跑业务逻辑。SPI 外设带 DMA40MHz 时钟下刷新一屏 320×240 基本十几毫秒级别UI 交互完全跟手。加上 WiFi/BLE一个屏就能撑起一个完整的联网控制面板这是很多裸外设方案不具备的。屏幕选 ST7789 也是从成本和资料两方面考虑。ST7789 支持 240×240、240×320 这类常见尺寸RGB565 输出够用初始化序列简单稳定。CST328 作为触摸 ICI2C 接口只需要 SDA、SCL、INT 三根线中点上报LVGL 的lv_indev读取起来很自然。这套组合的性价比和可替代性都很好后续换屏、换触摸 IC驱动改动集中在少数几个文件里。1.2 LVGL 与 ESP-IDF 的组合优势LVGL 版本选择上我最后锁定了 8.3 系列。LVGL 9 在 2024 年后已经成熟但 8.3 的资料、组件、GUI Guider 导出兼容性都更稳尤其对新手出问题搜得到答案。LVGL 9 用 Kconfig 方式集成到 ESP-IDF8.3 更习惯用lv_conf.h对我来说 8.3 是“少折腾能出活”的选择。ESP-IDF 这边我推荐 5.3 或 5.4。5.x 的esp_lcd组件已经把 ST7789 这类 SPI 屏幕封装好了初始化、DMA 刷新、旋转都有现成接口不用自己抠时序。LVGL 的显示刷新回调和触摸读取回调正好对接esp_lcd和 I2C 驱动代码结构非常清晰。这个组合的好处是底层显示交给esp_lcd管理LVGL 只负责 UI 绘制和事件响应不需要关心 SPI 寄存器细节。提示如果你用 Ubuntu 24.04装 ESP-IDF 优先选 v5.3 以上。v5.2 及以下版本的 Python 插件和 3.12 兼容性有点问题编译时会莫名其妙报错排查起来很浪费时间。2. 环境准备与工程骨架2.1 创建工程结构先按最常规的方式创建 IDF 工程。个人习惯用 VS Code ESP-IDF 插件插件装好之后会引导你安装工具链和 SDK。安装完成后建议先把 IDF 路径确认清楚。VS Code 里idf.py不在时最常见的报错是the path for esp-idf is not valid: /tools/idf.py not found这个问题的本质是插件配置的 IDF 路径指到了错误位置要填 IDF 的根目录也就是里面确实存在tools/idf.py的那一层。工程结构我建议这样组织your_project/ ├── main/ │ ├── CMakeLists.txt │ ├── idf_component.yml │ ├── lv_conf.h │ ├── app_main.c │ ├── lcd_display.c │ └── touch_cst328.c ├── partitions.csv └── sdkconfig.defaultsLVGL 我走的是组件管理器方式在main/idf_component.yml里声明依赖不用手动复制大段源码进工程dependencies: lvgl/lvgl: version: ^8.3.0然后在main/CMakeLists.txt里加一行REQUIRES lvgl编译时组件管理器会从仓库拉取并管理 LVGL。这样升级版本、换分支都方便工程目录也干净。2.2 VS Code 高频环境报错排查第一个高频报错是failed to set target esp32s3: non zero exit code 2。我一开始以为是芯片选错了后来发现最可能是工程里的sdkconfig和当前 target 不一致或者插件安装在 Windows 下路径没配对。解决方法是先备份sdkconfig删除build目录和sdkconfig然后在命令面板执行 “ESP-IDF: Set Espressif Device Target”重新选择esp32s3重新构建。如果是在类 Unix 系统下还要注意当前用户是否在dialout组里否则后面idf.py monitor会打不开串口。第二个高频问题是编译中途报esp_log_color.h找不到。这类报错通常和 IDF 路径有关很多是路径里包含中文、空格或者插件缓存了旧的 CMake 配置。处理方式是清理build目录重新加载 CMake确保路径干净。强调一下ESP-IDF 的安装路径最好不要带中文和空格这是老生常谈但永远有人踩。注意不要在普通终端里手动敲idf.py命令除非你已经执行过 IDF 的export.sh。VS Code 里所有构建操作都通过插件的任务执行插件会自动初始化环境变量省去很多环境问题。3. 硬件连接与底层驱动解析3.1 引脚分配参考ST7789 用 SPI 接口CST328 用 I2C 接口接线非常简单。下面是 I 用的一块 240×320 屏幕模组对应的引脚分配不同开发板引脚位置有差异按自己板子的丝印调整信号GPIO说明LCD_SCKGPIO12SPI 时钟LCD_MOSIGPIO11SPI 数据LCD_CSGPIO10片选低有效LCD_DCGPIO9数据/命令选择LCD_RSTGPIO8屏幕复位LCD_BLGPIO7背光控制TOUCH_SDAGPIO4I2C 数据TOUCH_SCLGPIO5I2C 时钟TOUCH_INTGPIO6触摸中断SPI 默认接在 SPI2 主机上ESP-IDF 的esp_lcd组件会自己管理这个外设。I2C 用 I2C_NUM_0速率 400kHz触摸屏对实时性要求不高这个速度足够。屏幕背光可以用一个 GPIO 直接控制如果需要亮度调节改成 LEDC PWM 输出就行。3.2 ST7789 显示驱动的关键点用esp_lcd驱动 ST7789 最大的好处是省心。初始化时把esp_lcd_panel_io_spi_config_t和esp_lcd_panel_dev_config_t配好然后调用一句esp_lcd_new_panel_st7789就能拿到面板句柄。关键参数有三个pclk_hz设 40MHzcolor_space设ESP_LCD_COLOR_SPACE_RGBbits_per_pixel设 16。RGB565 是 LVGL 默认色彩深度两边必须对齐否则颜色错乱。ST7789 初始化之后很多模组需要做一次esp_lcd_panel_invert_color(panel, true)否则颜色会变成负片效果。这个不是芯片决定死的而是取决于模组偏光片所以不同商家屏幕表现不一样。旋转方向用esp_lcd_panel_swap_xy和esp_lcd_panel_mirror调整。我这次横屏 320×240 显示就是先swap_xy再按实际效果改 mirror 参数一点一点试出来的。3.3 CST328 触摸驱动的关键点CST328 是 I2C 从设备常见 7 位地址是 0x15在总线上写地址为 0x2A读地址为 0x2B。不同模组厂家可能支持不同地址拿到板子后先确认硬件手册地址错了读出来全是 0xFF。读取触摸点的核心寄存器很简单0x01触摸点数低 4 位有效0x02第一点 X 坐标低字节0x03第一点 X 坐标高字节低 4 位有效0x04第一点 Y 坐标低字节0x05第一点 Y 坐标高字节低 4 位有效一次 I2C 读操作把 0x01 到 0x05 共 5 个字节读回来判断点数大于 0 就开始处理坐标。CST328 支持多点上报但 LVGL 默认只需要一个点所以每次取第一点的数据就够了。如果以后要手势识别再扩展读多点数据。4. LVGL 接入与界面实践4.1 初始化 LVGL 显示与输入设备LVGL 初始化分三步初始化显示驱动、初始化输入设备驱动、启动 tick 时钟。显示驱动里最重要的是 flush 回调LVGL 每次绘制完一块区域就调用一次回调让底层把像素推到屏幕。用esp_lcd实现极简单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); }这里需要注意esp_lcd_panel_draw_bitmap的参数是“结束坐标 1”的形式和 LVGL 的闭区间不一样写错会导致边缘花屏。flush 回调执行完必须调用lv_disp_flush_ready(drv)否则 LVGL 会一直等待整个 UI 卡死。分配绘制缓冲区时建议用heap_caps_malloc带MALLOC_CAP_DMA标志因为 SPI DMA 在部分配置下对内存有要求普通堆内存可能触发 cache 不连续问题。缓冲区大小我用横向 40 行的双缓冲static lv_color_t *buf1 heap_caps_malloc(320 * 40 * 2, MALLOC_CAP_DMA); static lv_color_t *buf2 heap_caps_malloc(320 * 40 * 2, MALLOC_CAP_DMA); lv_disp_draw_buf_init(draw_buf, buf1, buf2, 320 * 40);触摸输入设备驱动填read_cb回调即可。LVGL 会周期性调用这个回调读取当前触摸状态static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint8_t reg 0x01; uint8_t buf[5]; esp_err_t ret i2c_master_write_read_device(I2C_NUM_0, (0x15 1), reg, 1, buf, 5, 50 / portTICK_PERIOD_MS); if (ret ESP_OK (buf[0] 0x0F)) { uint16_t x ((buf[2] 0x0F) 8) | buf[1]; uint16_t y ((buf[4] 0x0F) 8) | buf[3]; >lv_font_conv --font NotoSansCJK-Regular.ttc --size 16 --bpp 4 \ --range 0x20-0x7E,0x4E00-0x9FA5 \ --format c --output font_cn.c上面这条命令仅供演示全字库两万多汉字生成后体积很大。生产项目里更合理的方式是用 GUI Guider 配套的字体子集化或者手动列出界面需要的汉字比如--range 0x20-0x7E,0xE6B8-0xE6B8这种按字符点选的写法。生成的 C 文件放进工程后注册方式就是普通 LVGL 字体指针extern lv_font_t font_cn; lv_obj_set_style_text_font(label, font_cn, 0);模态化弹窗效果也很常用。LVGL 8.3 里lv_disp_get_layer_top可以拿到顶层图层在这个图层上创建一个全屏遮罩再往遮罩上叠弹窗控件就能做出禁用下层点击的模态化界面static void create_modal(lv_obj_t *parent) { lv_obj_t * top lv_disp_get_layer_top(lv_disp_get_default()); lv_obj_t * mask lv_obj_create(top); lv_obj_set_size(mask, LV_HOR_RES, LV_VER_RES); lv_obj_set_style_bg_color(mask, lv_color_black(), 0); lv_obj_set_style_bg_opa(mask, LV_OPA_50, 0); lv_obj_set_style_border_width(mask, 0, 0); lv_obj_add_flag(mask, LV_OBJ_FLAG_CLICKABLE); }注意一定给遮罩容器加上LV_OBJ_FLAG_CLICKABLE否则点击空白区域会穿透到下层控件模态效果直接失效。这个细节在官方文档里写得很含蓄实测踩过一次坑。界面里的业务控件我顺手做了个 PWM 控制滑块很典型一个 Slider 控件调节占空比事件回调里写 LEDC 即可。电机转速控制、灯亮度调节都是这套逻辑LVGL 的lv_slider_get_value拿到 0 到 100 的数值映射到对应占空比后写入 LEDC 通道界面和业务就分开了。5. 实际运行中的问题与调试经验5.1 显示类问题速查问题现象排查方向解决办法白屏背光亮但无内容SPI 引脚、DC 脚、初始化时序确认init后调用了disp_on_off用逻辑分析仪抓 SPI 是否有数据颜色偏色或负片偏光片特性对 ST7789 执行esp_lcd_panel_invert_color切换试一次花屏或边缘错位坐标映射错误、缓冲大小错误检查draw_bitmap的 x21/y21检查 draw_buf 大小是否超过一行横竖屏方向不对MADCTL 设置用swap_xy加mirror组合试 4 种方向找到正确组合花屏问题最容易误导人。我一开始怀疑是 SPI 速率太高降到 10MHz 一样花屏最后发现是esp_lcd_panel_draw_bitmap传参写错了结束坐标没加 1。所以在动硬件参数之前先确认软件坐标边界处理。5.2 触摸类问题速查问题现象排查方向解决办法触摸完全没反应I2C 地址、中断脚、寄存器偏移先用i2cdetect确认设备地址读 0x01 看点数是否变化坐标方向错乱屏幕旋转后未映射在read_cb里写坐标变换四个角实测确认映射关系触摸偶发失灵INT 脚配置、I2C 速率确认 INT 是否上拉I2C 速率降到 100kHz 试试按下一个点UI 里跳动很大数据高低位拼接错误检查高字节是否多取了位CST328 高 4 位在低 4 位触摸坐标变换是最需要耐心的部分。CST328 读取到的坐标是相对触摸传感器的物理坐标而屏幕经过swap_xy和mirror旋转后物理坐标和逻辑坐标可能完全反过来。我的办法是先做一个调试页面把原始触摸坐标直接显示在屏上然后点击屏幕四个角根据对应关系写出映射公式。5.3 编译与运行常见报错编译阶段除了前面提到的 target 报错还有一类很常见LVGL 显示不正常但编译编译和烧录都成功串口日志又没明显错误。这种情况先看异常打印如果频繁出现LVGL memory allocate failed就是LV_MEM_SIZE太小。我默认设置成 96KB大部分 320×240 界面够用但如果控件多、图片多要开到 128KB。开启LV_USE_PERF_MONITOR和日志后刷新率、内存占用都能直接在屏上看是排查 UI 性能的第一手段。如果界面切换时明显卡顿优先检查是不是在动画里做了频繁创建和删除对象。LVGL 的动态内存碎片问题在长时间运行后会出现我的经验是尽量复用对象用lv_obj_clean清理内容而不是反复lv_obj_create和删除。5.4 日常调试的一条心得做这类带屏项目我强烈建议在开发板上预留一个日志输出串口脚平时不接屏幕也能看到 LVGL 日志和系统状态。LVGL 的日志按lv_log输出错误、警告、信息三级可以控制开发期全开发布版本只留错误。有了日志很多“看起来像硬件”的问题最后都会发现是软件状态问题。比如我调试触摸时开始怀疑 I2C 有问题后来打日志发现是read_cb里状态没更新LVGL 一直用旧坐标不是硬件问题。6. 后续扩展思路这套 ST7789 CST328 LVGL ESP-IDF 的基础例程跑通之后能延伸的方向很多。我最近在把触摸手势接入界面比如左右滑动切换页面、长按弹出确认框这些都是 LVGL 内置事件可以覆盖的比传统按键交互自然得多。另一个方向是用 NVS 保存界面配置和配网状态加一个按钮一键清除配网信息并重启设备这在物联网产品里几乎是刚需LVGL 界面里做起来也简单。如果界面复杂度继续上升可以考虑用 GUI Guider 做可视化布局。它的导出代码在 ESP-IDF 上基本可以直接编译不过底层底层驱动需要自己适配正好用这套例程里的lcd_display.c和touch_cst328.c对接。最后分享一个小技巧LVGL 8.3 的lv_conf.h里的宏很多不要一下子就全开。只开需要的功能比如用到滑块再开LV_USE_SLIDER用到中文再开字体。全开会导致编译慢、内存大对于实际交付来说没必要。按需裁剪配置等到项目后期再统一评估哪些功能值得打开这才是长期维护的正常节奏。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门