
简介这一示例以乐鑫ESP32S3芯片和IDF5.2.2框架为基础整合LVGL图形库、ST7789屏幕驱动与xpt2046触摸控制器为物联网开发者提供一套可直接运行的LCD显示与触摸交互参考适合正在评估带屏产品可行性或刚接触嵌入式GUI开发的工程师。压缩包共10个文件以C源码、头文件、sdkconfig系列配置和依赖清单为主同时包含说明性文本能够完整支撑工程构建与代码走读整体体积仅36KB结构精简便于快速部署。已有755人学习/下载从侧面验证了该示例在ESP32S3显示方案中的实用性。示例覆盖从屏幕初始化、SPI通信、电阻触摸校准到LVGL控件响应分发的完整链路并列出了组件依赖、默认配置与编译入口开发者可直接移植驱动代码在真实硬件上验证显示效果与触摸反馈大幅缩短带屏物联网设备的原型搭建时间。1. 为什么 ESP32S3 IDF5.2.2 LVGL 这套 LCD Demo 值得照抄一遍做嵌入式 GUI 最怕的不是画界面而是画完界面屏幕根本不亮、触摸点不准、刷新像幻灯片。ESP32S3 IDF5.2.2 LVGL ST7789 xpt2046 这套组合正好是低成本彩屏交互设备里最常被点名的搭配IDF 5.2.2 自带了 ST7789 面板驱动LVGL 负责画界面xpt2046 负责把人手点的位置变成坐标。四个部分每一块都有替代品但它们拼起来能覆盖大多数温控面板、仪表盘、小型 HMI 的屏幕需求。这篇笔记按我实际搭 Demo 的顺序来讲先把接线和引脚规划讲清楚再落到组件配置和点亮代码最后把触摸校准和那些容易出现“翻车现场”的坑一次性说完。2. 硬件接线ST7789 和 xpt2046 的引脚规划2.1 ST7789 的供电、背光与 SPI 接线屏幕模组的供电不复杂VDDA 和 VDDI 都接 3.3VGND 接系统地。真正容易翻车的是背光很多模块的背光 LED 串了限流电阻但电流仍有 2040mA如果直接从 ESP32-S3 的 3.3V LDO 取电再叠加屏幕逻辑电流电压会被拉低表现就是背光发暗、SPI 波形变形。我一般会给屏幕单独一路 3.3V或者至少保证主 3.3V 的输出能力在 500mA 以上。SPI 总线选择 SPI2_HOST 也就是常说的 FSPI信号线分配如下表。信号功能本方案接 GPIOSCLKSPI 时钟GPIO12MOSI主机输出GPIO11MISO主机输入触摸用GPIO13CS屏幕片选GPIO10DC数据/命令选择GPIO9RST复位低有效GPIO8BL背光控制GPIO7MOSI 和 SCLK 会被 LCD 和触摸芯片共用所以这两根线要尽量短、尽量等长。用杜邦线飞线做原型时优先把 SCLK 和 MOSI 单独走不要贴着背光电源线缠在一起否则跑 40MHz 时钟时容易收到干扰屏幕出现横纹。2.2 xpt2046 触摸引脚怎么接xpt2046 是四线电阻触摸屏控制器它本身不输出“手指位置”而是输出两个 ADC 通道的原始值需要软件换算后面第 5 章会细说。它的引脚并不多但接线规律性强T_CLK 复用屏幕的 SCLKT_DIN 复用屏幕的 MOSIT_DO 必须接到总线的 MISO 上T_CS 单独接一个 GPIO。信号功能本方案接 GPIOT_CLKSPI 时钟GPIO12 与屏幕共用T_DIN主机输出GPIO11 与屏幕共用T_DO从机输出GPIO13 即 MISOT_CS触摸片选GPIO6T_IRQ触摸中断可选GPIO5为什么 T_CS 不能和屏幕 CS 共用因为两个设备挂在同一条 SPI 总线上片选低电平决定当前谁和主机通信。如果共用屏幕和触摸会同时响应读回的数据全是乱的。T_IRQ 可选如果做忙碌轮询式读取不接也能用接了之后可以配置成下降沿唤醒减少无触摸时的 SPI 开销。2.3 打样前检查接线四处接线最容易出问题的是四个点。第一屏幕和触摸的电源地必须连到同一个地不要出现两个板卡各自的地线否则 SPI 高电平参考不一致触摸 ADC 读数会漂。第二RST 不要省略某些低成本模块内部复位电路不可靠必须由 GPIO 控制至少确保低电平保持 10ms。第三背光 BL 不要直接接 3.3V 就以为完了如果后续要做息屏省电BL 引脚应当接 GPIO 或 PWM 输出我习惯上把 BL 接到可软件拉低的 GPIO7。第四T_DO 这根 MISO 线非常容易被忽略很多人只接屏幕不接触摸时没引出 MISO到后面加触摸发现总线颜色不对查出是 MISO 悬空。3. 搭建 IDF5.2.2 工程LVGL 组件与最小点亮3.1 用 idf.py 建工程并装 LVGL 组件IDF 5.2.2 用组件管理器把 LVGL 集成做得比较顺手不需要再手动 git clone 第三方库。常见的做法是先建工程再添加 LVGL 和触摸组件。idf.py create-project lvgl_st7789_demo cd lvgl_st7789_demo idf.py add-dependency lvgl/lvgl idf.py add-dependency espressif/esp_lcd_touch_xpt2046第一次执行 add-dependency 时 IDE 或命令行会自动生成idf_component.yml。LVGL 组件会解析到兼容的 8.3.x 版本并自动把 LVGL 配置项接入到 IDF 的 menuconfig 里。触摸组件提供 xpt2046 的 SPI 初始化和坐标读取封装比自己写寄存器省事。这里要特别提醒IDF 5.2.2 本身已经带了 ST7789 面板驱动也就是esp_lcd_new_panel_st7789()这个函数。不要再额外从组件市场添加一颗 ST7789 驱动否则两个驱动符号重名编译直接报错。如果遇到重复定义错误先检查是不是组件清单里多写了显示驱动。3.2 menuconfig 把 LVGL 和面板参数先锁死依赖装好后打开 menuconfigidf.py menuconfig进入 Component config → LVGL configuration 后第一件要改的是 Color depth 设置为 16bit对应 RGB565这是 ST7789 模块最常用的格式。同时把默认显示分辨率 Dimension of display 设置为 horizontal resolution 240、vertical resolution 240。如果你的屏幕是 320x240把这两个数值改成宽 320、高 240。LVGL 在 IDF 中的动态内存建议打开 LV_MEM_CUSTOM让它在 ESP32-S3 的堆上分配而不是用静态数组。静态数组在内存紧张时容易初始化失败表现为 LVGL 跑起来后控件画到一半卡住。SPI 时钟频率不需要在 menuconfig 里设这部分写在代码里。我一般先按 20MHz 起步点亮成功后再提高到 40MHz避免一步到位把信号完整性引入的偶发花屏误判成驱动问题。3.3 最小点亮代码让屏幕先出 LVGL 默认主题最小点亮代码分三段初始化 SPI 总线创建 ST7789 面板创建 LVGL 显示驱动。核心如下#include esp_lcd_panel_io.h #include esp_lcd_panel_ops.h #include esp_lcd_panel_st7789.h #include lvgl.h #define LCD_SCLK 12 #define LCD_MOSI 11 #define LCD_MISO 13 #define LCD_CS 10 #define LCD_DC 9 #define LCD_RST 8 #define LCD_BL 7 #define LCD_H_RES 240 #define LCD_V_RES 240 static lv_disp_draw_buf_t disp_buf; static lv_color_t buf1[LCD_H_RES * 40]; static void lcd_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, area-y2, color_p); lv_disp_flush_ready(drv); } void app_main(void) { spi_bus_config_t bus_cfg { .sclk_io_num LCD_SCLK, .mosi_io_num LCD_MOSI, .miso_io_num LCD_MISO, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz LCD_H_RES * 40 * 2, }; spi_bus_initialize(SPI2_HOST, bus_cfg, SPI_DMA_CH_AUTO); esp_lcd_panel_io_spi_config_t io_cfg { .cs_gpio_num LCD_CS, .dc_gpio_num LCD_DC, .spi_mode 0, .pclk_hz 40 * 1000 * 1000, .trans_queue_depth 10, .lcd_cmd_bits 8, .lcd_param_bits 8, }; esp_lcd_panel_io_handle_t io_handle NULL; esp_lcd_new_panel_io_spi(SPI2_HOST, io_cfg, io_handle); esp_lcd_panel_dev_config_t panel_cfg { .reset_gpio_num LCD_RST, .rgb_ele_order LCD_RGB_ELEMENT_ORDER_RGB, .bits_per_pixel 16, }; esp_lcd_panel_handle_t panel NULL; esp_lcd_new_panel_st7789(io_handle, panel_cfg, panel); esp_lcd_panel_reset(panel); esp_lcd_panel_init(panel); esp_lcd_panel_disp_on_off(panel, true); lv_init(); lv_disp_draw_buf_init(disp_buf, buf1, NULL, LCD_H_RES * 40); 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 disp_buf; disp_drv.user_data panel; lv_disp_drv_register(disp_drv); while (1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }这段代码值得注意的参数有两个。第一个是spi_mode 0即 CPOL0、CPHA0ST7789 和 xpt2046 都支持模式 0一旦设置成模式 2 或模式 3屏幕可能还能出图但触摸必乱。第二个是max_transfer_sz它必须不小于一次 flush 的最大传输字节数这里 LVGL 缓冲区是 240x40 像素的 RGB565所以给240 * 40 * 2如果 buffer 改大这项要同步改。4. ST7789 显示参数调优时钟、色彩、偏移与背光4.1 SPI 时钟与刷屏率调到多少不玄学显示时钟受两个因素约束ESP32-S3 的 SPI 外设上限和 ST7789 模块的信号质量。对 2.8 英寸到 1.54 英寸的常见模组来说40MHz 是相对稳妥的上限20MHz 是调试期的起步值。不要一上来就跑 80MHz杜邦线或软排线条件下很容易花屏而且花屏位置随机一会儿左上角一会儿右下角。刷新率可以简单算240x240x2 字节等于 115.2KB40MHz SPI 理论传输一帧约 2.9ms但实际有 SPI 总线调度开销、LVGL 渲染开销和屏幕内部 RAM 刷新时间整帧刷新率很难超过 30fps。日常 UI 局部刷新占多数所以 LVGL 动画只要不整屏重绘流畅度足够。还有一种常见误用是 SPI 时钟追求高却把 LVGL 缓冲区设得很小。缓冲区只有几行时LVGL 每画一个控件就要触发多次 SPI 传输高频在这条总线上切换命令和数据效率反而低。缓冲行数建议至少为屏幕高度的八分之一到四分之一240 高度的屏我一般用 40 行起步。4.2 RGB565、BGR 与屏幕偏移量ST7789 本身是 18 位色R、G、B 各 6 位但绝大多数开发板模组以 RGB565 方式接入LVGL 侧把颜色深度配成 16bit驱动侧bits_per_pixel 16两者对齐即可。真正容易踩的是颜色顺序。同样的代码有的屏显示暖色调有的显示偏蓝。这是因为模块的排线把 RGB 三组数据线交换了。遇到这个问题不需要改代码逻辑只需把初始化里的rgb_ele_order从LCD_RGB_ELEMENT_ORDER_RGB换成LCD_RGB_ELEMENT_ORDER_BGR。判断方法很简单画一个纯红色块如果屏幕上显示成蓝色说明当前屏是 BGR 接线。偏移量是另一个隐藏参数。同样喊 240x240 分辨率部分模组内部 RAM 起点不在 (0,0)图像会整体偏移表现为左边或上边出现一条固定宽度的色带或图像残缺。IDF 的 ST7789 面板驱动支持把偏移量写进 panel 配置里常见做法是先初始化再用esp_lcd_panel_set_gap()设置 x 和 y 的起始 gap例如esp_lcd_panel_set_gap(panel, 0, 0);如果发现画面整体右移了 20 像素就把 x gap 设成 20 或 -20具体正负取决于模组品种。这个参数对买裸屏、没有厂家初始化代码的人尤其重要。4.3 背光、复位时序与上电灰边背光那根线看似简单但直接接电源和接 GPIO 的区别很大。直接接电源的屏软件不能关背光待机功耗压不下来接到 GPIO7 之后至少能在app_main里掉电前拉低。如果要做无级调光改用 LEDC 输出 PWM频率选 10kHz 左右太低会看到背光频闪太高可能超出 LED 驱动电路响应能力。复位时序里最常见的“血泪经验”是顺序问题。上电后 ST7789 内部上电复位不可靠所以esp_lcd_panel_reset()要放在初始化之前并且确保复位引脚不是悬空。有些模组 RST 引脚内部有上拉电阻很多开发者直接这根线不接结果每次插电白屏概率 50%必须手动按复位键才好原因就是上电瞬间 GPIO 状态不定。初始化的顺序建议为先 SPI 总线、再面板 reset、再 panel init、再开背光。如果先把背光亮起来再初始化屏幕会先显示一段灰边或白屏观感很差个别模组还会因为背光电流拉扯导致 SPI 时序短暂不稳定。5. 触摸接入与避坑排查从点到不点到终于跟手5.1 读取 xpt2046 的原始坐标寄存器与时序xpt2046 通过 SPI 发送一个控制字节选择读取通道。常用两个命令读 X 坐标发送0xD0读 Y 坐标发送0x90。这两个命令都是 12 位 ADC 模式输出 04095 的原始值。常规流程是拉低 T_CS发送控制字节再连续读两个字节作为结果然后拉高 T_CS。需要注意的是读取到的 16 位数据里有 4 位是无效位有效的是 12 位 ADC 结果。很多人第一次读回坐标发现数值比实际小很多就是把右移位数写错了。正确的是把两个字节拼成 16 位后右移 4 位而不是右移 8 位。如果直接用引脚操作实现读坐标代码大概是这样的思路uint16_t xpt2046_read_x(void) { uint16_t value xpt2046_raw_read(0xD0); return value; } uint16_t xpt2046_raw_read(uint8_t cmd) { uint8_t buf[2] { cmd, 0x00 }; uint8_t result[2]; // 拉低 T_CS通过 SPI2 发送 buf同时读回 result spi_transaction_t t { .length 16, .tx_buffer buf, .rx_buffer result, }; spi_device_polling_transmit(tp_spi_handle, t); return ((result[0] 8) | result[1]) 4; }实际工程里不必亲手实现这么底层的读取IDF 组件市场里的esp_lcd_touch_xpt2046已经把这层封装好了它会周期性地采样并给出屏幕上的坐标。但理解原始寄存器格式很有用后续遇到坐标整体偏移或方向反了你能直接判断是采样问题还是换算问题而不是盲目调参。5.2 校准映射与 LVGL indev 回调LVGL 的输入设备通过一个read_cb回调读取触摸数据。回调里要做两件事填充坐标point和触摸状态state。坐标关键在把 xpt2046 的 04095 原始值映射到屏幕分辨率并处理方向旋转。static void touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t touch_x 0, touch_y 0; uint8_t touch_count 0; esp_lcd_touch_handle_t tp (esp_lcd_touch_handle_t) drv-user_data; esp_lcd_touch_get_coordinates(tp, touch_x, touch_y, NULL, NULL, 1); uint16_t lcd_x (touch_x * LCD_H_RES) / 4096; uint16_t lcd_y (touch_y * LCD_V_RES) / 4096; >#define TOUCH_SWAP_XY 1 #define TOUCH_INV_X 0 #define TOUCH_INV_Y 1 #if TOUCH_SWAP_XY uint16_t tmp lcd_x; lcd_x lcd_y; lcd_y tmp; #endif #if TOUCH_INV_X lcd_x LCD_H_RES - 1 - lcd_x; #endif #if TOUCH_INV_Y lcd_y LCD_V_RES - 1 - lcd_y; #endif校准参数不写在代码里写死会更好。我习惯把旋转标志、翻转标志、以及 X/Y 的最小最大标定值存到 NVS 里量产同一套固件适配不同安装方向的屏幕时只需要在调试界面改一次参数。5.3 四个典型避坑排查记录第一个坑屏幕画面旋转了 90 度触摸也跟着偏。现象是点击左边按钮右侧菜单被触发。原因只改esp_lcd_panel_set_rotation改了显示触摸回调没同步交换 X/Y。解决方法是把旋转和触摸翻转统一放进同一个 display_config 结构体任何变更都从同一个头文件读取。第二个坑背光一开触摸坐标漂移几百个单位。现象是屏幕显示稳定但手指按同一个位置读回的坐标值上下浮动很大尤其在点击屏幕边缘时。原因是背光电流和触摸四线模拟前端共用电源平面按压力度大或背光 PWM 占空比变化时参考电压波动。解决方法是给触摸的参考电压加一个电容滤波并在读坐标时做连续多次采样去掉最大最小值后取平均值治标也治本。第三个坑读回坐标全是最大值或附近值。现象是屏幕能正常点亮但触摸数据始终接近 4095。原因是 SPI 时钟模式配成了 2 或 3或者 T_DO 没有真正接入 MISO误用了屏幕的 MOSI 回读。解决方法是确认spi_mode 0同时用万用表或杜邦线确认 T_DO 确实接到 GPIO13很多飞线板这里接错。第四个坑第一次点击有效后面就不动了。现象是上电后第一次触摸能触发按钮按第二下毫无反应。原因是 LVGL 的 indev 在读到LV_INDEV_STATE_PR后没有持续上报坐标或者触摸中断引脚持续低电平导致状态机卡住。解决方法是确保回调每次以最新坐标填充style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />