简介面向嵌入式软件开发者的完整示例工程基于乐鑫 ESP32-S3 微控制器驱动 CST328 触摸控制芯片与 ST7789 彩色显示屏配合官方 ESP-IDF 开发框架和 LVGL 开源图形库实现从屏幕点亮、触摸响应到复杂界面展示的完整流程。适合正在学习嵌入式图形界面开发、需要快速完成显示方案验证的开发者。压缩包内共有 1342 个文件主要包括 C 语言源文件、C 头文件、Python 辅助脚本、Markdown 说明文档、PNG 与 GIF 图片素材、字体资源、配置文件等压缩后大小约 26.45MB目录组织清楚便于按模块查阅。目前该资源已有 1465 人学习下载。例程具体展示了 SPI 外设初始化、LVGL 环境配置、触摸驱动适配、控件布局与事件处理等关键环节并附带多组演示画面和可复用素材能够帮助用户理解嵌入式 GUI 底层工作原理快速迁移至智能仪器仪表、自助终端、智能家居面板等实际项目有效降低重复移植成本。1. ESP32-S3 驱动 CST328 与 ST7789LVGL 从模拟器到真机的第一道坎用 ESP32-S3 驱动 CST328 触摸和 ST7789 屏幕再把 LVGL 跑起来这组合看着简单真上手全是坑。lvgl esp32 在模拟器里零成本到了真机SPI 时序、I2C 寄存器、缓冲策略全变成拦路虎黑屏、坐标乱跳、刷新撕裂会轮着教你做人。这套 ESP32S3 驱动 CST328、ST7789 的例程解决的就是「模拟器能跑、真机点不亮」的断层ST7789 走 SPI 出画面CST328 走 I2C 回触摸坐标再用 LVGL 的 flush 和 read 两个回调合成完整的触摸 UI。适合手里有一块 240x240 或 240x320 屏、想用 ESP-IDF 正经做界面的开发者也适合刚焊好第一块屏的人照着抄。2. 硬件与工程结构CST328 的 I2C、ST7789 的 SPI 与 ESP-IDF 组件骨架2.1 接口速览一块触摸屏模组里的两条总线先别急着写代码把模组拆开看总线关系。市面上常见的带触摸的 ST7789 模组内部是两颗独立的 die一颗 ST7789 驱动 TFT 面板一颗 CST328 负责电容触摸两者共用供电但对外引出的是完全独立的两组总线。显示走 SPI触摸走 I2C互不干扰这在电路设计上是好事两路可以各自调试显示挂了不影响触摸自检。为什么显示不走并口8080 并口要占十几根 GPIOESP32-S3 引脚虽然不算少但一块产品板的 GPIO 往往还要挂传感器、按键、串口调试SPI 四根线是最经济的方案。触摸用 I2C 同理两根线搞定而且 CST328 这类触摸 IC 对 I2C 的支持非常成熟寄存器简单、时序宽容度也好。CST328 默认 7 位 I2C 地址常见为 0x15具体以模组原理图为准有些屏厂会通过外部引脚电平把地址改成 0x2A。它支持多点触摸但 LVGL 是单指针 UI 框架绝大多数例程只取第一点读一次 7 个字节就能拿到坐标和按压状态。寄存器布局各家手册措辞不同但核心套路一致0x00 是状态字节低 4 位是触点数量后面跟着第一点的 X/Y 坐标。还有一个细节CST328 带了手势寄存器能上报上滑、下滑之类的方向事件LVGL 用不上因为 LVGL 自带手势识别。我的建议是别让 LVGL 和触摸 IC 两边同时做手势判断否则一个滑动会触发两次响应例程里只读坐标不做手势层的重复劳动。ST7789 是 240x320 的 TFT 驱动 IC这套例程用到的分辨率是 240x240 或 240x320 中的一个区域。它支持 3 线/4 线 SPI4 线就是标准的 CS、SCLK、MOSI 加一根 DC 数据/命令切换线外加复位和背光。颜色深度用 16 位 RGB565 时一帧数据是宽乘高乘 2 字节240x240 全屏要 115200 字节这个量直接决定了后续缓冲区怎么安排。供电要留意两颗 IC 都是 3.3V 逻辑但不少模组的背光走独立引脚有高电平点亮也有低电平点亮接反不烧东西只是背光不亮容易被误判成屏幕坏了。触摸的 SDA/SCL 还必须有上拉电阻很多模组内部没做不补上拉就等着间歇性失灵。信号示例 GPIO说明LCD SCLKGPIO12SPI 时钟建议先 40MHz 起步LCD MOSIGPIO11SPI 数据ST7789 只写不读MISO 可空着LCD CSGPIO10片选低有效LCD DCGPIO9数据/命令切换1 数据 0 命令LCD RSTGPIO8复位低有效LCD BLGPIO7背光可接 PWM触摸 SDAGPIO4I2C 数据触摸 SCLGPIO5I2C 时钟触摸 INTGPIO6触摸中断可选低功耗唤醒用2.2 工程骨架ESP-IDF 5.x 的组件目录与板级配置文件这套例程基于 ESP-IDF 5.x工程结构长这样project/ ├── CMakeLists.txt ├── sdkconfig ├── main/ │ ├── CMakeLists.txt │ ├── idf_component.yml │ ├── board_cfg.h # 引脚和分辨率定义改板子只动这一处 │ ├── st7789_lcd.c # 显示初始化与 LVGL flush │ ├── cst328_touch.c # 触摸读取与 LVGL read │ └── main.c # 应用入口关键在idf_component.yml用 ESP-IDF 的组件管理器拉 LVGL不需要手动往工程里塞源码dependencies: lvgl/lvgl: ^8.3.0版本决策要提前做LVGL 8 和 9 的 API 差得很大flush 回调签名、draw_buf 初始化方式都变了。例程按 8.3 写因为 8.x 资料最多、踩坑案例最全屏厂发布的参考驱动基本围着 8.x 做。如果你新开项目想用 9.x回调这块大概率要重写不建议边学边迁移。LVGL 作为托管组件装好后色深、内存池大小等配置项会出现在menuconfig里不用手改lv_conf.h。默认内存池对复杂界面偏小我一般调到 16KB 以上不然控件一多就开始拒绝分配内存现象是 UI 画到一半缺块串口报lv_mem相关错误。板级配置单独放头文件的好处是换屏时只改一处#define PANEL_WIDTH 240 #define PANEL_HEIGHT 240 #define PANEL_SWAP_XY true #define CST328_I2C_ADDR 0x15 #define PIN_LCD_SCLK 12 #define PIN_LCD_MOSI 11 #define PIN_LCD_CS 10 #define PIN_LCD_DC 9 #define PIN_LCD_RST 8 #define PIN_LCD_BL 7 #define PIN_TOUCH_SDA 4 #define PIN_TOUCH_SCL 5 #define PIN_TOUCH_INT 6建议在board_cfg.h里加一个编译期检查分辨率超过屏幕物理尺寸直接报错防止换屏时只改了宽高、忘了改触摸变换公式这种隐性 bug 排查起来特别费时。2.3 驱动路线怎么选esp_lcd 官方组件 vs 裸寄存器驱动在 ESP-IDF 5.x 下驱动 ST7789有两条路。第一条用官方esp_lcd组件。它内置esp_lcd_new_panel_st7789()初始化、交换坐标、镜像翻转都是现成接口LVGL 官方移植组件esp_lvgl_port也基于它封装代码量最少排错容易。缺点是把 ST7789 的命令序列封了一层屏厂魔改过的初始化代码不好塞进去个别冷门屏会不兼容。第二条是自己写 SPI 初始化序列照着手册把 Power_On、SLPOUT、COLMOD、DISPON 一条条发出去再直接对接 LVGL 的 flush 回调。优点是完全可控任何屏都能救回来缺点是要处理 DMA、字节序、同步工作量比第一条大一个量级。对比项esp_lcd 路线裸寄存器路线代码量少约 80 行核心多300 行起ST7789 兼容性覆盖主流模组全覆盖触摸接入仍需自己写 I2C 读同样要写排错难度低接口封装清晰高时序全靠自己抓LVGL 对接成本有现成组件手写 flush 和同步我的建议很直接除非手里是一块大厂魔改初始化时序的特殊屏否则一律走 esp_lcd 路线。CST328 没有官方驱动触摸部分两边工作量差不多触摸读取必须自己写但没必要为了证明技术能力把显示也裸写一遍。esp_lvgl_port组件内部把 SPI 异步传输和 LVGL 缓冲回收做了同步封装相当于把后面要讲的时序竞争提前处理好了不想操心 flush 细节就用它。3. 动手实现SPI 初始化、CST328 读取与 LVGL 双回调对接3.1 初始化 SPI 总线和 ST7789pclk、位宽与扫描方向显示链路第一步是初始化 SPI 总线。SPI 总线上可能同时挂 SD 卡或其他外设这里单独给 LCD 分配一个 host避免 DMA 队列互相干扰#include driver/spi_master.h #include esp_lcd_panel_io.h #include esp_lcd_panel_ops.h #include esp_lcd_panel_vendor.h #include board_cfg.h // 1. 初始化 SPI 总线自动分配 DMA 通道 spi_bus_config_t bus_cfg { .sclk_io_num PIN_LCD_SCLK, .mosi_io_num PIN_LCD_MOSI, .miso_io_num -1, // ST7789 不回数据 .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz PANEL_WIDTH * PANEL_HEIGHT * 2 8, }; spi_bus_initialize(LCD_SPI_HOST, bus_cfg, SPI_DMA_CH_AUTO); // 2. 挂载 SPI 面板 IOpclk 决定刷新上限 esp_lcd_panel_io_spi_config_t io_cfg { .dc_gpio_num PIN_LCD_DC, .cs_gpio_num PIN_LCD_CS, .pclk_hz 40 * 1000 * 1000, .lcd_cmd_bits 8, .lcd_param_bits 8, .spi_mode 0, .trans_queue_depth 20, }; esp_lcd_panel_io_handle_t io_handle NULL; esp_lcd_new_panel_io_spi(LCD_SPI_HOST, io_cfg, io_handle);.max_transfer_sz必须大于一次最大帧传输的长度否则 DMA 描述符会拒绝超过 4KB 的传输报错信息还特别隐晦。.pclk_hz就是 SPI 时钟频率40MHz 是起步值跑顺了可以往上加到 60 甚至 80MHz前提是排线别太长杜邦线就别想了排线或 PCB 走线才有戏。.spi_mode 0对应 ST7789 手册的 CPOL0、CPHA0绝大多数模组都吃这个模式反应慢就卡在这里。.trans_queue_depth决定能排队多少笔 SPI 事务深度 20 对 LVGL 整块刷屏够用太小会频繁返回 BUSY然后 flush 回调里全是超时日志。接着创建 ST7789 面板并执行初始化esp_lcd_panel_dev_config_t panel_cfg { .reset_gpio_num PIN_LCD_RST, .rgb_ele_order LCD_RGB_ELEMENT_ORDER_RGB, .bits_per_pixel 16, // RGB565 }; esp_lcd_panel_handle_t panel NULL; esp_lcd_new_panel_st7789(io_handle, panel_cfg, panel); // 3. 复位后等 120ms有些屏对这条时序特别敏感 esp_lcd_panel_reset(panel); vTaskDelay(pdMS_TO_TICKS(120)); // 4. 初始化后处理颜色方向和显示开关 esp_lcd_panel_init(panel); esp_lcd_panel_invert_color(panel, true); // RGB565 屏基本都要开 esp_lcd_panel_disp_on_off(panel, true); // 显式开显示别省.bits_per_pixel 16必须跟 LVGL 的色深严格一致否则颜色整体错位。.rgb_ele_order选 RGB 还是 BGR 取决于屏厂接反了红蓝互换一眼能看出来就翻回来。invert_color是 ST7789 家族著名的坑很多模组的 TFT 面板像素极性和驱动 IC 默认相反不开反色时白变黑、红变青而且不同批次的模组差异都可能有。ESP-IDF 里先设true颜色不对就改false二选一必中。3.2 读取 CST328 触摸数据寄存器地址、坐标字节序与按下判定触摸侧用 ESP-IDF 5.2 之后的i2c_master驱动API 比老的driver/i2c.h干净错误处理更细#include driver/i2c_master.h static i2c_master_dev_handle_t touch_dev; void cst328_init(void) { i2c_master_bus_config_t bus_cfg { .i2c_port I2C_NUM_0, .sda_io_num PIN_TOUCH_SDA, .scl_io_num PIN_TOUCH_SCL, .clk_source I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt 7, .flags.enable_internal_pullup true, }; i2c_master_bus_handle_t bus_handle NULL; i2c_new_master_bus(bus_cfg, bus_handle); i2c_device_config_t dev_cfg { .dev_addr_length I2C_ADDR_BIT_LEN_7, .device_address CST328_I2C_ADDR, // 0x15以模块原理图为准 .scl_speed_hz 400 * 1000, }; i2c_master_bus_add_device(bus_handle, dev_cfg, touch_dev); }.scl_speed_hz 400 * 1000是 I2C 快速模式CST328 手册一般都支持别用 100kHz 低速模式触摸采样是轮询的总线越快延迟越低。.glitch_ignore_cnt 7是毛刺过滤调试阶段波形一团糟可以先调大确认问题之后再把值收回来。地址这块如果 0x15 读不到 ACK用逻辑分析仪抓 SDA 在地址阶段的波形能直接看到七位地址是多少比盲猜省时间。读坐标的函数要处理两件事I2C 传输失败和坐标越界。写成返回 bool让上层决定要不要沿用旧坐标bool cst328_read_point(uint16_t *x, uint16_t *y, bool *pressed) { uint8_t reg 0x00; // 状态寄存器坐标数据跟随其后 uint8_t buf[7] {0}; esp_err_t err i2c_master_transmit_receive(touch_dev, reg, 1, buf, 7, 100); if (err ! ESP_OK) { return false; // 上层沿用上一次坐标避免 UI 抖动 } uint8_t touch_count buf[0] 0x0F; // 低 4 位是触点数量 *pressed (touch_count 0); if (*pressed) { *x ((uint16_t)buf[1] 8) | buf[2]; *y ((uint16_t)buf[3] 8) | buf[4]; // 坐标高位偶尔会混入状态位按分辨率钳位 if (*x PANEL_WIDTH) *x PANEL_WIDTH - 1; if (*y PANEL_HEIGHT) *y PANEL_HEIGHT - 1; } return true; }CST328 的坐标系原点默认在触摸传感器左上角X 向右、Y 向下和屏幕扫描方向一致大多数情况不用镜像。但有些屏厂把触摸排线换了个方向焊原点就跑到右下角去了这种情况不要改驱动在 LVGL 的 read 回调里做映射就行。坐标高字节是重灾区有些手册定义 X 是 12 位高 4 位另有含义直接把buf[1]全当坐标高位读出来的数会跳上千钳位逻辑能兜底但治标不治本正确做法是先把位域确认清楚。3.3 注册 LVGL 回调flush_cb 和 read_cb 的对接细节LVGL 对接的核心是两个回调外加一颗心跳。LVGL 跟裸跑了几年 HAL 库的人最大的认知差就是它需要你定期喂 tick否则动画、长按、双击判定全部失灵现象是界面画出来了按键也能点但滑动起来一顿一顿。先初始化 LVGL 本体、喂 tick、注册回调#include lvgl.h static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_part1[PANEL_WIDTH * 40]; static lv_color_t buf_part2[PANEL_WIDTH * 40]; // 双缓冲 static void lvgl_tick_task(void *arg) { while (1) { lv_tick_inc(10); // 每 10ms 喂一次心跳 vTaskDelay(pdMS_TO_TICKS(10)); } } void lvgl_port_init(void) { lv_init(); xTaskCreate(lvgl_tick_task, lv_tick, 2048, NULL, 5, NULL); lv_disp_draw_buf_init(draw_buf, buf_part1, buf_part2, PANEL_WIDTH * 40); lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res PANEL_WIDTH; disp_drv.ver_res PANEL_HEIGHT; disp_drv.flush_cb lvgl_flush_cb; disp_drv.draw_buf draw_buf; 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); }缓冲大小 240x40 算下来每块 19200 字节两块不到 40KBESP32-S3 内部 SRAM 放得下。缓冲高度越大一帧分块次数越少、CPU 开销越低但 SRAM 占用越高。40 行是性价比不错的起步想更顺就提到 80看编译后的内存余量决定。主循环里还得让 LVGL 干活void app_main(void) { // ... SPI 和 I2C 初始化 ... lvgl_port_init(); while (1) { lv_timer_handler(); // 处理动画和重绘 vTaskDelay(pdMS_TO_TICKS(5)); } }lv_timer_handler()调用间隔太大会让动画掉帧5ms 是比较通用的值不调它UI 基本就是一幅静态图。flush 回调做三件事把区域送到屏幕、处理好边界、通知 LVGL 缓冲可回收static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { // esp_lcd 的坐标是排他性的右边界LVGL 给的是包含性的x2/y2 必须 1 esp_lcd_panel_draw_bitmap(panel, area-x1, area-y1, area-x2 1, area-y2 1, (void *)color_p); lv_disp_flush_ready(drv); }x2 1这个细节漏了屏幕右边缘和下边缘会丢一行像素怎么查都像是屏幕坏了。lv_disp_flush_ready是告诉 LVGL「这块缓冲我用完了」叫得越早 LVGL 越早画下一块。但注意esp_lcd_panel_draw_bitmap是异步的DMA 可能还没读完缓冲。简单例程里直接用上面的即时版本双缓冲给了 DMA 足够时间追上一帧的绘制如果看到偶发花屏或条纹把 flush 改成等 SPI 完成信号量再flush_ready这是esp_lvgl_port的内部做法。read 回调把触摸数据填进lv_indev_data_t每次被 LVGL 输入定时器调用static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { static uint16_t last_x 0, last_y 0; static bool last_pressed false; uint16_t x last_x, y last_y; bool pressed last_pressed; if (cst328_read_point(x, y, pressed)) { last_x x; last_y y; last_pressed pressed; } >esp_lcd_panel_swap_xy(panel, PANEL_SWAP_XY); // 交换 X/Y 扫描方向 esp_lcd_panel_mirror(panel, false, false); // 按需镜像 esp_lcd_panel_set_gap(panel, 0, 0); // 裁边用320 屏显示 240 区域才需要swap_xy交换的是屏幕扫描方向mirror的两个布尔分别控制 X 轴和 Y 轴镜像。这几个开关一点亮显示侧就转了但触摸 IC 的坐标是硬件层面的它不知道你转了屏于是出现「画面横过来了触摸还按竖的算」的错位。解决办法是在 read 回调里做数学变换逻辑和屏幕变换保持一致// 假设屏幕做了 swap_xy 且没有镜像 if (PANEL_SWAP_XY) { uint16_t tmp x; x y; y PANEL_WIDTH - 1 - tmp; // 用旋转后的宽度算 }这行变换是我实测时最常调的地方。调试顺序必须固定先把原始坐标直接打印到串口手指点四角确认触摸 IC 的原始坐标系方向再打开显示旋转对照四角出现的位置决定 Y 要不要镜像、X 要不要镜像最后才把变换写死进 read 回调。顺序反了会绕晕因为屏幕和触摸的错误会互相叠加你改了一个方向另一个方向又错位了。4. 避坑与排查黑屏、坐标错乱、卡顿与触摸失灵的 5 个实战记录4.1 黑屏但背光亮先查复位时序和 DISPON现象上电后背光正常亮屏幕全黑串口日志显示 LVGL 任务在跑但屏幕上什么都画不出来。原因八成是 ST7789 没真正完成初始化。常见三类reset_gpio_num没配对或复用了其他功能引脚复位后延时太短IC 没就绪就接着发命令esp_lcd_panel_init()内部发了 SLPOUT 但没等够手册要求的 120msDISPON 自然没生效。还有一部分模组需要显式调用esp_lcd_panel_disp_on_off(panel, true)默认可能是关的。解决先核对reset_gpio_num别拿复用引脚顶上。第二步在esp_lcd_panel_reset()后加vTaskDelay(120ms)把复位到初始化的间隔拉到最稳妥区间。第三步把disp_on_off(panel, true)加上放在invert_color之后。还黑就用逻辑分析仪抓 CS、SCLK、DC 三条线确认命令确实发出去、极性对。记住背光亮不等于屏幕活着它只说明背光 LED 供电正常。4.2 触摸坐标错乱先打印原始坐标再谈对齐现象点屏幕左上角UI 响应在右下角或者点一下坐标数值疯狂跳变甚至出现几万的大数。原因数值跳变多半是寄存器位域取错了。CST328 的高字节里混了状态位或触点编号没做掩码直接当坐标高位就会出现几千上万的坐标。UI 视觉位置和触摸位置整体错位则九成是屏幕做了swap_xy/mirror之后触摸变换没同步更新。解决临时把读到的x、y、pressed直接ESP_LOGI打印去掉所有钳位和变换纯裸数据。手指点四个角对照打印值与物理位置确认原始坐标系然后再定变换公式。四角对上了再开 UI。最高优先级原则裸数据是基准一切变换都从裸数据出发别上来就猜公式。4.3 刷新卡顿与撕裂pclk、缓冲区和 DMA 三方拉扯现象滑动列表时一顿一顿快速滚动时屏幕中间有水平分割线像是两帧拼在一起。原因卡顿核心是单缓冲区——LVGL 画完一块区域前不能画下一块SPI 传输期间 CPU 有大段空转。撕裂是 flush 回调里lv_disp_flush_ready叫得太早LVGL 已经把新内容写进 DMA 还没读走的缓冲。另一点是 pclk 只有 20MHz 时240x240x2 字节的整帧传输要十几毫秒流畅度必然受影响。解决先上双缓冲改动最小收益最直接。再把pclk_hz从 40MHz 试到 60MHz画面没噪点就继续上有噪点就退一档——这跟走线质量和屏的极限有关实测出来才算数。撕裂就改 flush 等待 SPI 完成信号量把.trans_queue_depth设成 1 让传输严格串行牺牲一点吞吐换画面完整。最后是压缩 LVGL 脏矩形面积局部刷新永远比全屏快UI 上少用大面积的重绘调用。4.4 触摸偶尔失灵I2C 上拉电阻、INT 引脚和传输失败处理现象刚上电触摸一切正常跑几分钟后突然没反应或者按一个按钮要按好几次才触发还有的伴随坐标偶尔跳变。原因I2C 总线没有外部上拉或上拉电阻太大电平建立时间不够随着温升或走线耦合传输开始偶发失败。另一个常见原因是模块的 INT 引脚悬空CST328 靠它做触摸唤醒和事件同步悬空时 IC 行为不一定可预期。再一个是驱动里没处理 I2C 的 NACK 和超时返回值一次失败后总线状态残留后续所有传输都失败。解决PCB 上给 SDA/SCL 各加一颗 4.7kΩ 上拉到 3.3V这是最靠谱的做法别只依赖 ESP32-S3 的内部弱上拉。INT 引脚接到任意 GPIO哪怕代码暂时用不上也得给它一个确定电平。读取函数严格检查每次i2c_master_transmit_receive返回值失败时先把总线句柄复位再继续别让总线死在半途。最后可以在触摸 IC 供电上加一个断 10ms 再上电的软件复位流程作为最后的后悔药。4.5 花屏与颜色翻车INVON、COLMOD 和 LVGL 色深的三角关系现象屏幕能点亮但颜色完全不对——白色变黑色、红色变青色或者整屏出现不太规则的彩色噪点。原因颜色反相基本是 ST7789 的 INVON 没开或开错不同屏厂 TFT 面板极性相反这个开关二选一总能调对。彩色噪点和错位是像素格式不匹配屏的 COLMOD 设成了 18 位但 LVGL 打包的是 16 位 RGB565两边数据宽度对不上画面就花。解决先确认esp_lcd_panel_invert_color(panel, true)已调用颜色不对就改成false这是排查第一优先。再检查esp_lcd_panel_dev_config_t里bits_per_pixel 16同时确认 LVGL 配置里色深为 16 位在menuconfig里搜LV_COLOR_DEPTH就能改。最后对照屏厂的初始化序列看有没有自定义像素格式命令有就以屏厂手册为准覆写。5. 验证与进阶帧率测量、缓冲布局与低功耗收尾5.1 帧率怎么测perf 监视器和串口时间戳双管齐下LVGL 自带帧率测量menuconfig里打开LV_USE_PERF_MONITOR屏幕角落会常驻一个显示 FPS 和 CPU 占用的小角标这是最直观的验证手段。但这个数字是 LVGL 画图帧率不是屏幕实际刷新率。要核实屏侧吞吐我在 flush 回调里用esp_timer_get_time()掐耗时static int64_t flush_ms_sum; static uint32_t flush_count; static int64_t last_report_ms; static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { int64_t t0 esp_timer_get_time(); esp_lcd_panel_draw_bitmap(panel, area-x1, area-y1, area-x2 1, area-y2 1, (void *)color_p); flush_ms_sum (esp_timer_get_time() - t0) / 1000; flush_count; if (esp_timer_get_time() - last_report_ms 1000000) { ESP_LOGI(LCD, avg flush %lld us, count %lu, (long long)(flush_ms_sum * 1000UL / flush_count), (unsigned long)flush_count); flush_ms_sum 0; flush_count 0; last_report_ms esp_timer_get_time(); } lv_disp_flush_ready(drv); }两边的数字对不上时问题多半在 DMA 队列或 pclk 上回到 4.3 的思路去查。5.2 缓冲布局SRAM、PSRAM 与分块高度怎么选240x240 全屏缓冲需要 115KBESP32-S3 内部 SRAM 能挤出来但大屏就得考虑 PSRAM。三种方案的取舍缓冲方案占用性能适用场景全屏单缓冲 SRAM115KB高240x240 且内存余量大分块双缓冲 SRAM约 40KB中高多数 240x240 项目首选全屏双缓冲 PSRAM230KB中320x480 以上大屏分块高度从 40 起步UI 复杂就提到 60超过 80 收益递减。缓冲放 PSRAM 时注意地址对齐和缓存一致性读到异常闪烁就先把 DMA 缓存关掉再看。5.3 背光 PWM 与触摸唤醒低功耗的最后一道工序背光用 LEDC 输出 PWM把静态常亮改成可调亮度省下的电流比任何软件优化都实在。配合 INT 引脚做触摸唤醒屏幕超时后关背光把 CST328 的 INT 配成唤醒源手指点屏幕就能拉起。整套组合能把功耗压到待机毫安级前提是触摸 IC 的待机模式配置正确不同型号差异大以手里那颗的寄存器手册为准。我自己的习惯是每次拿到新屏先花半天把「复位 → ST7789 纯色显示 → CST328 裸坐标打印 → LVGL 最小触摸 Demo」这条最短链路走通再谈 UI 优化。跳过任何一步后面的排查成本都会翻倍。这套例程把整条链路打包好了你照着跑通再改业务能少走我当年绕的那些弯路。希望帮到你。本文还有配套的精品资源点击获取