资讯详情

资讯详情

ESP32S3 LVGL ST7789 xpt2046 屏驱环境搭建与避坑指南

简介面向嵌入式与物联网开发者的ESP32S3图形界面驱动示例聚焦在IDF 5.2.2环境下将LVGL图形库与ST7789显示驱动、xpt2046电阻触摸控制器打通解决小尺寸LCD屏点亮、界面渲染与触摸坐标采集的入门与联调问题适合已具备一定ESP-IDF基础、希望快速搭建带触摸GUI原型的工程师参考。资源包共10个文件约36KB以C源码与头文件承载LVGL初始化、显示与触摸对接逻辑辅以CMakeLists构建脚本、sdkconfig及defaults配置、idf_component.yml与依赖锁文件便于直接导入工程复现。已有761人学习下载。示例完整呈现显示刷新与触摸输入的协同流程读者可据此验证屏幕显示效果与触摸响应理解组件依赖与工程配置组织方式并以此为起点扩展控件与交互节省从零适配驱动的时间。1. 一块屏点不亮多半不是屏的问题上周帮人收拾一块 2.8 寸 240×320 的 SPI 屏主控 ESP32S3驱动 ST7789触摸芯片 xpt2046图形库用 LVGL框架是 IDF 5.2.2。对方描述得很干脆代码编译过了烧进去白屏串口没报错。这种“编译通过、运行白屏”的场景在 ESP32S3 LVGL ST7789 xpt2046 这套组合里出现频率极高因为四层东西叠在一起——芯片引脚、SPI 总线、驱动初始化序列、LVGL 的显示缓冲任何一层错位都表现为同一块白屏。装这套环境的人通常是做小尺寸 HMI 的开发者想先把屏跑通再往上堆界面。这篇记录按我实际装机的顺序走先把版本和引脚定死再装 IDF、拉 LVGL、配 ST7789 与 xpt2046最后给一套能自证的验证方法。装不好不会炸但会让你在“是不是屏坏了”上浪费一整晚。2. 环境与版本先定死IDF 5.2.2 与 LVGL 的搭配取舍2.1 为什么锁 IDF 5.2.2 而不是随手装最新ESP32S3 的 SPI 主机驱动在 IDF 5.x 系列里改过接口spi_master那套老 API 在 5.x 里被标记为兼容层新代码走的是esp_lcd的 panel IO 接口。IDF 5.2.2 属于 5.2 分支的补丁版本esp_lcd的esp_lcd_panel_io_spi_config_t结构体字段和 5.1 相比没有破坏性变动社区里针对 ST7789 的现成初始化序列大多按这个版本写。装 5.3 或 5.4 也能跑但esp_lcd内部对 SPI 传输队列的处理有调整个别老例程会出现刷屏撕裂。我一般建议新项目锁 5.2.2已有工程别为了追新去动 IDF 版本升级 IDF 带来的排查成本远高于它给的那点收益。安装走官方安装器最省事Linux 和 macOS 用下面这套# 下载 IDF 5.2.2 安装脚本并执行-b 指定分支标签 mkdir -p ~/esp cd ~/esp git clone -b v5.2.2 --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh esp32s3 # 只装 esp32s3 的 toolchain省磁盘 . ./export.sh # 每次开新终端都要 source 一次-b v5.2.2是关键不加就拉到 master版本号对不上后面全是玄学。install.sh esp32s3只装 S3 的工具链比全量装少几个 G。export.sh必须用.或source执行直接./export.sh起子 shell环境变量出不来idf.py会提示找不到。Windows 下用官方 IDF Installer 选 5.2.2装完在开始菜单里开 “ESP-IDF 5.2.2 PowerShell”别用普通 PowerShell否则 PATH 里没有工具链。装完先验版本这一步别跳idf.py --version # 期望输出包含 v5.2.22.2 LVGL 版本与组件引入方式LVGL 在 IDF 里有两种引入法一是把 lvgl 源码整个拷进components/二是用 IDF 组件管理器从注册表拉。前者可控后者省事。我倾向后者因为版本号写在idf_component.yml里换机器复现时不会出现“我这能跑你那不能跑”。在工程根目录建main/idf_component.ymldependencies: idf: 5.2 lvgl/lvgl: ~8.3.11 # 锁 8.3 小版本9.x 的 API 变动大~8.3.11表示允许 8.3.x 的补丁升级但不跨到 8.4 或 9.x。LVGL 9 把lv_disp_drv_t换成了lv_display_t显示注册那套代码全得重写网上大量 ST7789 例程还是 8.x 写法混用必翻车。第一次idf.py build时组件管理器会自动下载 lvgl 到managed_components/国内网络慢的话在menuconfig里把组件镜像源换成国内镜像或者手动git clone到components/lvgl并删掉 yml 里那行依赖。2.3 引脚与硬件参数对照表接线错是白屏第一大来源先把这张表填死再动代码。下面是我这块板子的实际接法你的板子按原理图改但 SPI 的 MOSI/SCLK 必须落在 S3 的 SPI2 或 SPI3 可用 IO 上信号ESP32S3 引脚说明SCLKGPIO12SPI 时钟走 SPI2_HOSTMOSIGPIO11数据输出CSGPIO10屏片选低有效DCGPIO9命令/数据切换RSTGPIO8复位可接 EN 但建议独立 IOBLGPIO7背光PWM 可调TOUCH_CSGPIO6xpt2046 片选TOUCH_IRQGPIO5触摸中断可轮询不用背光单独给 IO 是为了能调亮度直接接 3.3V 也能亮但夜里刺眼。xpt2046 和 ST7789 共用 SPI 总线、各自独立 CS这是标准做法别给触摸单独开一条 SPIS3 的 SPI 外设数量够但没必要浪费。3. 装 ST7789 驱动esp_lcd 面板注册与初始化序列3.1 用 esp_lcd 还是自己写 SPI 时序IDF 5.2.2 里esp_lcd已经内置了 ST7789 的 panel 驱动路径在components/esp_lcd/src/esp_lcd_panel_st7789.c通过CONFIG_LCD_ENABLE_ST7789打开。自己写 SPI 时序也能跑但要处理命令/数据切换的 DC 电平、复位时序、初始化命令表工作量不小且容易在某个延时上翻车。常见做法是直接用esp_lcd_new_panel_st7789把初始化命令表交给驱动自己只管总线和 IO 配置。menuconfig里确认一下idf.py menuconfig # Component config - LCD drivers - 勾选 ST7789不勾这个链接时会报undefined reference to esp_lcd_new_panel_st7789这是新手最常见的第一个坑。3.2 SPI 总线与 panel IO 的配置代码下面这段是总线初始化放在app_main最前面#include driver/spi_master.h #include esp_lcd_panel_io.h #include esp_lcd_panel_vendor.h #include esp_lcd_panel_ops.h #define LCD_HOST SPI2_HOST #define PIN_SCLK 12 #define PIN_MOSI 11 #define PIN_CS 10 #define PIN_DC 9 #define PIN_RST 8 #define LCD_H_RES 240 #define LCD_V_RES 320 spi_bus_config_t bus_cfg { .sclk_io_num PIN_SCLK, .mosi_io_num PIN_MOSI, .miso_io_num -1, // 屏不读数据MISO 不接 .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz LCD_H_RES * 80 * sizeof(uint16_t), // 单次最大传输 }; ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, bus_cfg, SPI_DMA_CH_AUTO));max_transfer_sz决定单次 DMA 能搬多少字节设小了刷屏会分很多次传帧率上不去设大了占内存。按屏宽 × 80 行 × 2 字节算240×80×2 38400 字节够用且不浪费。SPI_DMA_CH_AUTO让驱动自己挑 DMA 通道S3 上一般没问题。接着配 panel IOesp_lcd_panel_io_handle_t io_handle NULL; esp_lcd_panel_io_spi_config_t io_cfg { .dc_gpio_num PIN_DC, .cs_gpio_num PIN_CS, .pclk_hz 40 * 1000 * 1000, // 40MHzST7789 上限约 62.5MHz .lcd_cmd_bits 8, .lcd_param_bits 8, .spi_mode 0, .trans_queue_depth 10, }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, io_cfg, io_handle));pclk_hz是取舍点40MHz 稳定拉到 60MHz 部分模块会花屏跟排线长度和屏的批次有关。trans_queue_depth是异步传输队列深度配合 LVGL 的 flush 回调做非阻塞刷屏10 是个保守值内存紧张可以降到 4。spi_mode 0对应 CPOL0、CPHA0ST7789 基本都吃这个模式接错模式表现为花屏或全黑。3.3 面板初始化与方向、反色、镜像esp_lcd_panel_handle_t panel_handle NULL; esp_lcd_panel_dev_config_t panel_cfg { .reset_gpio_num PIN_RST, .rgb_ele_order LCD_RGB_ELEMENT_ORDER_RGB, .bits_per_pixel 16, }; ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, panel_cfg, panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); // 多数 ST7789 需要反色 ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, 0, 0)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));invert_color(true)是血泪经验ST7789 出厂默认颜色反转不调这个白底变黑底、红变青。不同批次模块可能相反如果反色后颜色还是不对把它改成false再试。set_gap用于 240×320 屏在 240×240 驱动上的偏移纯 240×320 屏填 0,0。方向用esp_lcd_panel_swap_xy和esp_lcd_panel_mirror调横屏就 swap 加 mirror 组合别去改初始化命令表里的 MADCTL驱动已经封装好了。4. 接 xpt2046 触摸与 LVGL 显示注册4.1 xpt2046 的 SPI 读时序与校准xpt2046 是电阻屏控制器走 SPI 读 12 位 ADC 值命令字节里带通道选择。它和 ST7789 共用总线但读操作要单独发命令不能复用 panel IO 的写句柄。常见做法是给触摸单独加一个 SPI 设备spi_device_interface_config_t touch_cfg { .clock_speed_hz 2 * 1000 * 1000, // xpt2046 上限约 2MHz .mode 0, .spics_io_num PIN_TOUCH_CS, .queue_size 3, }; spi_device_handle_t touch_spi; ESP_ERROR_CHECK(spi_bus_add_device(LCD_HOST, touch_cfg, touch_spi)); // 读 X 通道0xD0读 Y 通道0x90返回值高 12 位有效 uint16_t xpt2046_read(uint8_t cmd) { uint8_t tx[3] {cmd, 0x00, 0x00}; uint8_t rx[3] {0}; spi_transaction_t t { .length 24, .tx_buffer tx, .rx_buffer rx, }; spi_device_transmit(touch_spi, t); return ((rx[1] 8) | rx[2]) 3; // 取 12 位 }clock_speed_hz必须压到 2MHz 左右xpt2046 的 SPI 时序比 ST7789 慢得多用 40MHz 读出来全是 0 或 4095。 3是因为返回值是 12 位左对齐在 16 位里不移位算出来的坐标会跳。原始 ADC 值范围大概 2003900需要做两点校准映射到屏幕坐标校准系数存 NVS别每次开机重校。4.2 LVGL 显示缓冲与 flush 回调LVGL 8.3 的显示注册分三步建 buffer、填lv_disp_drv_t、注册 flush 回调。buffer 大小决定刷屏方式全屏 buffer 占 240×320×2 150KBS3 的 PSRAM 够但内部 RAM 紧张我一般用 1/10 屏static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[LCD_H_RES * 40]; // 40 行缓冲 lv_disp_draw_buf_init(draw_buf, buf1, NULL, LCD_H_RES * 40); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res LCD_H_RES; disp_drv.ver_res LCD_V_RES; disp_drv.flush_cb lcd_flush_cb; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv);flush 回调里把 LVGL 的像素区丢给esp_lcd_panel_draw_bitmapstatic void lcd_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { esp_lcd_panel_draw_bitmap(panel_handle, area-x1, area-y1, area-x2 1, area-y2 1, color_map); lv_disp_flush_ready(drv); // 同步模式传完立即通知 }lv_disp_flush_ready必须在传输完成后调用同步模式下draw_bitmap返回即传完直接调没问题如果开了异步传输要等on_color_trans_done回调里再调否则 LVGL 会提前复用 buffer画面撕裂。area-x2 1这个加一是 LVGL 坐标含端点、esp_lcd 坐标不含端点的差异漏了会少刷一列像素表现为右边一条竖线不更新。4.3 触摸输入设备注册触摸读到的坐标要注册成 LVGL 输入设备read_cb里返回按下状态和坐标static void touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t raw_x xpt2046_read(0xD0); uint16_t raw_y xpt2046_read(0x90); bool pressed (raw_x 200 raw_y 200); // 简单阈值判按下 if (pressed) { style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →