)
簡介面向嵌入式與物聯(lián)網開發(fā)者的 ESP32S3LVGL 觸摸屏驅動資源圍繞 1.69 英寸電容觸摸屏提供完整圖形界面實現(xiàn)方案。顯示側采用 ST7789 控制器通過 SPI 接口傳輸圖像數據觸摸側采用 CST816 芯片支持多點觸控并通過 I2C/SPI 與主控通信。內容重點解決兩塊控制器的初始化與驅動適配包括屏幕初始化時序、SPI 時鐘配置、觸摸中斷處理、坐標上報格式等關鍵細節(jié)同時將 CST816 的觸摸數據接入 LVGL 觸摸輸入層注冊回調函數完成點擊、滑動等用戶交互涵蓋 LVGL 8 的配置方式和移植要點適合正在做小型彩屏人機界面或物聯(lián)網可視化項目的中級嵌入式開發(fā)者參考也可作為快速搭建觸摸界面的起步模板。壓縮包共 971 個文件約 17.39MB以 C/H 源碼、Python 腳本、Markdown/RST 文檔、PNG/GIF 圖片、FNT 字體及 JSON 配置為主既有源碼示例也有說明文檔與構建配置便于對照實驗和二次開發(fā)。目前已有 6160 人學習使用結合實際硬件調整參數后即可在此基礎上擴展頁面控件、動畫效果與業(yè)務邏輯。1. ESP32-S3 LVGL 驅動 1.69 寸電容觸摸屏先打通顯示和觸摸兩條鏈路「ESP32-S3 驅動 1.69 寸電容觸摸屏跑 LVGL」聽起來是件很常規(guī)的事真正動手時卻最容易在兩個地方翻車屏能點亮但觸摸沒反應或者觸摸能用但坐標和顯示完全對不上。我見過不少人被「只需四根線」的說法誤導結果卡在 SPI 和 I2C 兩套驅動上一卡就是兩三天。本文就是順著這條落地路徑走一遍先分立驗證顯示和觸摸再交給 LVGL 統(tǒng)一調度最后處理坐標對齊和性能。適合手里已經有一塊 1.69 寸電容觸摸屏想用 ESP32-S3 做出可交互界面的開發(fā)者已經用過 LVGL 的人可以直接跳到坐標對齊那一章。這塊屏雖然小但當顯示刷新、觸摸采樣和 LVGL 的刷新機制對齊后跑一個三五百行的 UI 完全夠用。2. 屏和觸摸是兩條獨立鏈路讀懂 1.69 寸電容觸摸屏的硬件組成2.1 常見驅動組合與引腳識別ST7789V CST816S1.69 寸電容觸摸屏是一個「顯示 觸摸」的合屏模組。顯示這邊絕大多數用的是 ST7789V 這類 SPI 接口的彩色驅動 IC觸摸這邊常見的是 CST816S走 I2C 接口。這個組合的好處是 ESP32-S3 只需要一組 SPI 加一組 I2C總共七八個引腳就能同時覆蓋顯示和觸摸如果換成了電阻觸摸屏還要額外占用兩個 ADC 通道反而更麻煩。我一般在拿到模塊后先不看絲印直接把排針標號抄下來分成兩組一組是 SPI 信號分別是 CLK、MOSI、CS、DC、RST再帶上 BL 背光另一組是觸摸信號常見的是 T_SDA、T_SCL、T_INT有的板子還會給 T_RST。這組信號中T_RST 不是必須的因為很多觸摸控制器的復位可以和顯示復位共用或者模塊內部已經做了 RC 復位電路。不過我會建議你單獨引一根出來排錯的時候很有用后面第 5 章的避坑記錄里會看到原因。下面這個表是我常用的接線約定具體 GPIO 號根據開發(fā)板的引出情況自行調整信號建議 GPIO說明LCD_SCLKGPIO12SPI 時鐘LCD_MOSIGPIO11主機輸出屏端輸入LCD_CSGPIO10片選低有效LCD_DCGPIO9數據/命令選擇LCD_RSTGPIO8復位低有效LCD_BLGPIO7背光高電平點亮TOUCH_SDAGPIO4I2C 數據TOUCH_SCLGPIO5I2C 時鐘TOUCH_INTGPIO6觸摸中斷可不上拉有兩點要提前確認第一1.69 寸這類小屏的背光電流通常不大直接由 GPIO 驅動沒問題但如果你遇到背光閃爍就在 BL 到 GND 之間加一個 100nF 電容或者用三極管驅動第二I2C 的 SDA/SCL 必須在模塊端有 4.7kΩ 左右的上拉電阻。很多觸摸模塊出廠時把這組電阻也焊上了但有些廉價款會省略這時候需要在外部再加兩個上拉電阻否則觸摸會時好時壞。先把這個硬件前提理清楚后面所有軟件問題都不會指向接線錯誤。提示在跑任何軟件之前用萬用表量一次 VCC 對 GND 的阻值以及 RST 引腳對上電瞬間的復位波形。這樣能省下一整天排查玄學問題的時間。2.2 點亮 LCDSPI 初始化、復位時序和最小命令序列我常用的方式是基于 ESP-IDF 的 SPI 主機接口先把總線初始化出來再注冊一個設備給屏幕。下面是最小代碼關鍵參數都寫了注釋#include driver/spi_master.h #define PIN_LCD_SCLK GPIO_NUM_12 #define PIN_LCD_MOSI GPIO_NUM_11 #define PIN_LCD_CS GPIO_NUM_10 #define PIN_LCD_DC GPIO_NUM_9 #define PIN_LCD_RST GPIO_NUM_8 #define PIN_LCD_BL GPIO_NUM_7 #define LCD_H_RES 240 #define LCD_V_RES 280 spi_device_handle_t spi_lcd; static void lcd_spi_init(void) { spi_bus_config_t bus_cfg { .sclk_io_num PIN_LCD_SCLK, .mosi_io_num PIN_LCD_MOSI, .miso_io_num -1, // 寫屏為主不讀數據就用不到 MISO .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz LCD_H_RES * LCD_V_RES * 2, }; esp_err_t ret spi_bus_initialize(SPI2_HOST, bus_cfg, SPI_DMA_CH_AUTO); ESP_ERROR_CHECK(ret); spi_device_interface_config_t dev_cfg { .clock_speed_hz 40 * 1000 * 1000, // 小屏跑 40MHz 很穩(wěn) .mode 0, // ST7789V 用 mode 0 或 mode 3 .spics_io_num PIN_LCD_CS, .queue_size 7, }; ret spi_bus_add_device(SPI2_HOST, dev_cfg, spi_lcd); ESP_ERROR_CHECK(ret); }這里有兩處要說明。一是max_transfer_sz我直接給了整幀的字節(jié)數240*280*2這樣后續(xù)一幀一次性 DMA 發(fā)送也不會被限制如果你內存緊張可以改成240*280*2/8甚至更小但每次 flush 的數據塊就不能超過這個值。二是mode 0ST7789V 支持 mode 0 和 mode 3都表現(xiàn)為能點亮但在高速 SPI 下 mode 0 更常見如果你發(fā)現(xiàn)畫面有規(guī)律錯位可以試 mode 3這屬于換著試的范疇。點亮屏幕還需要一套初始化命令。我不建議把某家供應商的幾十條初始化序列原封不動貼進來最關鍵是以下幾條static void lcd_write_cmd(uint8_t cmd) { gpio_set_level(PIN_LCD_DC, 0); spi_transaction_t t { .length 8, .tx_buffer cmd }; spi_device_polling_transmit(spi_lcd, t); } static void lcd_write_data(const uint8_t *data, size_t len) { gpio_set_level(PIN_LCD_DC, 1); spi_transaction_t t { .length len * 8, .tx_buffer data }; spi_device_polling_transmit(spi_lcd, t); } static void lcd_init_sequence(void) { gpio_set_level(PIN_LCD_RST, 0); vTaskDelay(pdMS_TO_TICKS(20)); // 復位低電平保持時間 gpio_set_level(PIN_LCD_RST, 1); vTaskDelay(pdMS_TO_TICKS(120)); // 退出復位要等穩(wěn)定 lcd_write_cmd(0x11); // Sleep Out vTaskDelay(pdMS_TO_TICKS(120)); uint8_t madctl 0x00; // 默認掃描方向先讓畫面正常 lcd_write_cmd(0x36); lcd_write_data(madctl, 1); uint8_t pixfmt 0x55; // RGB56516位色 lcd_write_cmd(0x3A); lcd_write_data(pixfmt, 1); lcd_write_cmd(0x29); // Display ON vTaskDelay(pdMS_TO_TICKS(20)); gpio_set_level(PIN_LCD_BL, 1); }在初始化里0x36是內存訪問控制它會同時影響 RGB 順序、行/列掃描方向和顯示刷新方向這個寄存器是你后面做坐標對齊的老朋友0x3A設置像素格式為0x55意思是 16 位 RGB565。0x11退出睡眠后必須等待 120ms 左右否則后續(xù)命令可能被忽略表現(xiàn)就是背光亮但屏幕一直是白屏。這一整套跑完后如果屏有背光但沒有任何顏色變化試著在0x29后面加讀返回狀態(tài)大多數時候是 SPI 引腳映射寫錯或者 DC 高低電平反了。2.3 讓觸摸獨立工作I2C 讀 CST816S 坐標并打印驗證觸摸這邊不需要任何初始化序列上電后用 I2C 掃描到0x15這個地址就算基本通了。CST816S 的觸摸坐標寄存器在不同批次上存在差異所以我習慣一次性讀 6 個字節(jié)打印出來看#include driver/i2c.h #define TOUCH_ADDR 0x15 void touch_i2c_init(void) { i2c_config_t conf { .mode I2C_MODE_MASTER, .sda_io_num GPIO_NUM_4, .scl_io_num GPIO_NUM_5, .sda_pullup_en GPIO_PULLUP_ENABLE, .scl_pullup_en GPIO_PULLUP_ENABLE, .master.clk_speed 400000, // 觸摸 IC 允許 400KHz }; i2c_param_config(I2C_NUM_0, conf); i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); } bool touch_read_raw(uint8_t raw[6]) { i2c_cmd_handle_t cmd i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (TOUCH_ADDR 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, 0x00, true); // 從寄存器 0x00 開始連續(xù)讀 i2c_master_start(cmd); i2c_master_write_byte(cmd, (TOUCH_ADDR 1) | I2C_MASTER_READ, true); i2c_master_read(cmd, raw, 6, I2C_MASTER_LAST_NACK); i2c_master_stop(cmd); esp_err_t ret i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); return ret ESP_OK; }讀到raw[0]到raw[5]后先不要急著解析。在開發(fā)板下載口接一組串口日志手指按住觸摸屏的不同位置把 6 個字節(jié)全部打印出來。按下與抬起之間至少會有一個字節(jié)出現(xiàn)連續(xù)變化常見布局是raw[1]的 bit0 表示觸摸狀態(tài)raw[2]是高 4 位坐標raw[3]是低 8 位raw[4]和raw[5]同理。有的模塊會把坐標整體右移一位這就是為什么直接抄網上的寄存器表不一定好使。打印兩三次后你就知道自己的板子用哪種映射。我一般會在這一步先驗證一下坐標范圍。用指尖從左上角滑到右下角記錄數值如果最大值在 240 和 280 附近說明坐標是屏幕物理分辨率如果跑滿 4095說明它工作在低分辨率模式需要檢查觸摸 IC 上電后的輸出配置。CST816S 的坐標位數可以通過寄存器配置但絕大多數模塊出廠已經設好不用額外寫。I2C 連不上時優(yōu)先檢查觸摸 IC 的復位引腳很多模塊的觸摸復位會被顯示復位一起拉低導致上電后觸摸 IC 還沒從復位中釋放I2C 掃描就會失敗。這一步獨立跑通后再進 LVGL你會少掉一半的排錯煩惱。接下來我們把這兩條鏈路交給 LVGL 來調度。3. LVGL 移植顯示 driver 和觸摸 driver 的接入方式決定一切3.1 lv_conf.h 里的關鍵開關LV_COLOR_DEPTH、字體和 tick 設置LVGL 本身不是驅動庫它只是定義了一套顯示驅動接口和刷新流程。所以第一步不是寫代碼而是把lv_conf.h里幾個關鍵開關打開。這里我默認用 LVGL v8.3.x接口穩(wěn)定網上資料也多如果你用的是 v9接口名會變我將在 3.4 里給出區(qū)別表底層思路完全一致。我常用的一組最小配置如下#define LV_COLOR_DEPTH 16 // 匹配 ST7789V 的 RGB565 #define LV_USE_LOG 1 #define LV_LOG_PRINTF 1 #define LV_TICK_CUSTOM 0 // 使用外部 tick 源 #define LV_USE_ASSERT_NULL 1 #define LV_FONT_MONTSERRAT_16 1 #define LV_FONT_MONTSERRAT_24 1 #define LV_USE_PERF_MONITOR 1LV_COLOR_DEPTH必須和屏幕像素格式一致這一項設錯會直接導致顏色錯亂。LV_USE_PERF_MONITOR是我強烈建議打開的它會在屏幕上畫出幀率統(tǒng)計后期定位性能問題非常有用量產前再關掉。LV_FONT_MONTSERRAT_24對應 1.69 寸屏上的可讀字號如果沒有啟用你調用lv_label_set_text時界面會只顯示空白。LV_TICK_CUSTOM的原則是如果系統(tǒng)里已經有 1ms 的定時器就通過lv_tick_inc(1)喂給 LVGL如果沒有才打開LV_TICK_CUSTOM。ESP-IDF 環(huán)境下我推薦用esp_timer創(chuàng)建一個 1ms 周期回調實際過程是這樣的static void lv_tick_cb(void *arg) { lv_tick_inc(1); } void lvgl_tick_init(void) { esp_timer_handle_t tick_timer; esp_timer_create_args_t tick_args { .callback lv_tick_cb, .arg NULL, .name lv_tick, }; esp_timer_create(tick_args, tick_timer); esp_timer_start_periodic(tick_timer, 1000); // 每 1ms 觸發(fā)一次 }注意 1ms 的周期是 LVGL 動畫和輸入事件的基礎心跳如果周期偏大動畫卡頓、觸摸響應延遲會一并出現(xiàn)。esp_timer 的回調上下文不能做耗時操作這里只是累加一個計數完全沒問題。3.2 注冊顯示設備flush_cb 要遵守的規(guī)則LVGL 把屏幕抽象成一個lv_disp_drv_t。注冊顯示設備時只需要告訴它屏幕的邏輯分辨率、繪制緩沖區(qū)和刷新回調。下面是 v8 的注冊方式static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[240 * 280 / 10]; // 1/10 屏的繪制緩沖區(qū) void lvgl_display_init(void) { lv_disp_draw_buf_init(draw_buf, buf, NULL, 240 * 280 / 10); lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res 240; disp_drv.ver_res 280; disp_drv.flush_cb st7789_flush; disp_drv.draw_buf draw_buf; disp_drv.full_refresh false; // 允許局部刷新 lv_disp_drv_register(disp_drv); }緩沖區(qū)大小是個權衡。1/10 屏大概是 240*28 個像素在內存占用和刷新速度之間比較均衡。如果你的 UI 里有大面積動態(tài)圖像可以改成 1/4 屏甚至使用雙緩沖lv_disp_draw_buf_init(draw_buf, buf1, buf2, SIZE)但 ESP32-S3 整體 RAM 有限整屏雙緩沖會吃 134KB 內存除非你外掛 PSRAM否則我不建議。full_refreshfalse讓 LVGL 只更新臟區(qū)域這對小屏相當重要能減少無謂的 SPI 傳輸。st7789_flush是真正的關鍵時刻。LVGL 會調用這個回調告訴你要更新的矩形區(qū)域area和緩沖區(qū)color_p你需要把這片區(qū)域的像素搬到屏幕上并在完成后通知 LVGL。示例代碼static void st7789_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w lv_area_get_width(area); uint32_t h lv_area_get_height(area); st7789_set_window(area-x1, area-y1, area-x2, area-y2); st7789_push_pixels((uint8_t *)color_p, w * h * 2); lv_disp_flush_ready(drv); }st7789_set_window向屏幕寫入列地址0x2A和行地址0x2B然后寫 RAM0x2C。st7789_push_pixels可以直接復用前面 SPI 設備的spi_device_polling_transmit把color_p的原始內存作為tx_buffer發(fā)出去。這里需要注意的是color_p的字節(jié)序問題。LVGL 內部根據LV_COLOR_DEPTH16來存儲顏色在 ESP32-S3 上就是一個 16 位小端整數而 ST7789V 的 RGB565 也希望收到先低位后高位的字節(jié)流。大多數情況下這兩個字節(jié)序天然一致直接發(fā)即可如果出現(xiàn)紅色和藍色互換不要急著改像素格式而是先去排查初始化里的0x36RGB 位。不能忘記的是lv_disp_flush_ready(drv)。LVGL 依靠這個函數確認緩沖區(qū)已被釋放。如果你用了 DMA 異步發(fā)送必須在發(fā)送完成中斷或回調里調用如果漏掉它繪制流程會卡死表現(xiàn)為畫面只有第一幀或持續(xù)卡頓。3.3 注冊輸入設備read_cb 的寫法與調用頻率觸摸屏在 LVGL 里注冊成LV_INDEV_TYPE_POINTER輸入設備。LVGL 會周期性調用read_cb因此在回調里盡量只做緩存讀和狀態(tài)轉換不要把耗時的 I2C 多次讀取放在這里。示例static void touchpad_read(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, y; bool pressed; if (touch_read_point(x, y, pressed)) { last_x x; last_y y; last_pressed pressed; // 在這里可以加入旋轉映射函數 map_touch_coords(...) } >#define TOUCH_MAX_X 240 #define TOUCH_MAX_Y 280 void map_touch_coords(uint16_t tx, uint16_t ty, uint16_t *dx, uint16_t *dy, uint8_t rot) { switch (rot) { case 0: // 0 度原樣 *dx tx; *dy ty; break; case 1: // 90 度交換 X/Y修正新 X *dx ty; *dy TOUCH_MAX_X - 1 - tx; break; case 2: // 180 度 *dx TOUCH_MAX_X - 1 - tx; *dy TOUCH_MAX_Y - 1 - ty; break; case 3: // 270 度 *dx TOUCH_MAX_Y - 1 - ty; *dy tx; break; default: break; } }這個映射的正確性依賴于TOUCH_MAX_X和TOUCH_MAX_Y的原始值。比如 1.69 寸屏物理分辨率是 240寬x 280高如果觸摸 IC 輸出的 X 范圍是 240Y 范圍是 280那上面公式成立但如果你之前為了不裁剪畫面把 LVGL 設置成了 280x240 橫屏這里的TOUCH_MAX_X和TOUCH_MAX_Y還是橫屏時用的 240 和 280映射就會出錯。所以第一步還是在 2.3 里的打印結果里確認觸摸最大坐標。把rot從 0 到 3 依次試一遍總有一個方向是對的這不是土辦法而是因為觸摸 IC 和 LCD 之間的裝配誤差本來就是批次性的先枚舉再固化是更可靠的落地路徑。4.3 三招驗證觸摸映射打印坐標、十字線和四角標定試映射前不要直接跑業(yè)務界面先用三個手段確認。第一在 LVGL 里放一個標簽實時顯示觸摸坐標同時用串口打印原始坐標和映射后的坐標手指從左上角劃到右下角觀察兩個坐標的關系。第二畫一個全屏十字線橫豎各一條點擊交叉點附近看點擊高亮是否跟隨手指。第三四角標定依次點擊屏幕四個角對比坐標是否分別接近(0,0)、(W-1,0)、(0,H-1)、(W-1,H-1)。如果四個角的誤差不大但整體有偏移可能不是方向問題而是屏幕區(qū)域有邊框。很多 1.69 寸模組本身帶有一個約 2~3 像素的觸摸邊距觸摸原點并不在 LCD 顯示區(qū)域的真正角落。此時可以在映射函數里加上偏移量#define TOUCH_OFFSET_X 2 #define TOUCH_OFFSET_Y 2 // 在每路選擇映射前先做裁剪 if (tx TOUCH_OFFSET_X) tx 0; else tx - TOUCH_OFFSET_X; if (ty TOUCH_OFFSET_Y) ty 0; else ty - TOUCH_OFFSET_Y;這一步做完后再進 LVGL demo。如果只是方向不平上面rot試一次就搞定如果需要修正偏移那這已經是可用性達標的狀態(tài)。至于更嚴格的九點校準在小尺寸電容屏上沒必要因為線性度通常很好反而是校準表會占用大量內存。四角驗證足夠。5. 避坑清單ESP32-S3 接 1.69 寸電容觸摸屏的五個高頻排錯點5.1 屏亮但畫面花屏或白屏現(xiàn)象背光正常但整屏顯示亂碼或者一直白屏沒有任何內容。 原因最常見有三類SPI 的 mode 設錯導致采樣沿不對max_transfer_sz太小一次傳輸被截斷初始化命令沒有按順序執(zhí)行尤其0x11后的延時不夠。 解決先把 SPI 速度降到 26MHzmode 固定 0 和 3 各試一次max_transfer_sz給到一幀大小如果內存不夠就給到 1/4 幀并確認 flush 的傳輸長度不超過它把0x11后的延時加到 150ms。白屏還有個容易被忽略的地方就是 DC 引腳接錯命令和數據全被當成命令處理表現(xiàn)是屏幕一直顯示一塊固定顏色但沒圖形。5.2 I2C 掃描不到觸摸地址現(xiàn)象用 I2C scanner 掃描一路都掃不到0x15觸摸完全無響應。 原因排線接線錯位、I2C 上拉電阻缺失、觸摸 IC 的復位沒有釋放或者觸摸 IC 被拉進了睡眠模式。 解決先用萬用表量 T_SDA/T_SCL 到 VCC 的電壓正常應接近 3.3V 高電平再確認模塊的觸摸復位引腳 T_RST 沒有被誤拉低可以在初始化時把 T_RST 拉高并保持。某些模塊的觸摸復位和 LCD 復位共用引腳如果 LCD 初始化最后把復位引腳一直拉低觸摸 IC 就永遠處于復位態(tài)這在排針編號不清晰時非常隱蔽。解決方法是初始化結束后把 RST 全部拉高再掃描 I2C。5.3 觸摸能讀但坐標完全反了現(xiàn)象手指點在左上角光標出現(xiàn)在右下角或者左右鏡像、上下顛倒。 原因觸摸 IC 和 LCD 被屏幕接線方向定義成了不同的原點LVGL 側又開啟了不同角度的旋轉。 解決先固定一個顯示掃描方向比如通過0x36讓畫面在正向安裝時正常然后在read_cb里用 4.2 的map_touch_coords枚舉四種rot。不要同時改0x36和映射函數兩處一起改會很難判斷到底差在哪。確認方案后把映射固化再裁剪TOUCH_MAX_X/Y到屏幕實際范圍。5.4 顏色不對紅色和藍色互換現(xiàn)象顯示燒錄圖片時膚色全偏成綠色或者條形圖里紅藍對調。 原因SPI 發(fā)送的是 LVGL 生成的小端 16 位顏色而屏端按大端 RGB565 處理或者0x36里的 BGR 位被設成了相反值。 解決優(yōu)先改0x36的 RGB 順序位而不是去改lv_color_t的存儲方式。記住LV_COLOR_DEPTH16只是確定每個像素兩個字節(jié)沒有規(guī)定這兩個字節(jié)的發(fā)送順序。如果你用 DMA 發(fā)送時發(fā)現(xiàn)每次總是低字節(jié)先出去而屏希望高字節(jié)先到就需要在st7789_push_pixels里做一次字節(jié)交換或者設置MADCTL的 BGR 位。對 ST7789V 來說正確設置0x36后就不需要額外交換。5.5 LVGL 偶爾卡死或復位現(xiàn)象UI 運行幾分鐘后ESP_ERROR_CHECK報 SPI timeout或者看門狗復位debug 里看到 LVGL 斷言在 flush 回調。 原因flush 回調里使用的是阻塞spi_device_polling_transmit當 LVGL 刷新頻率高時 SPI 傳輸容易排隊超時也可能繪制緩沖區(qū)太大超過了默認任務棧。 解決第一把 flush 里的 SPI 發(fā)送改成spi_device_transmit它使用中斷不會占用調用線程第二確認任務棧至少給到4096字節(jié)把 LVGL 的繪制和刷新放在獨立任務里不要讓它在低??臻g里跑第三如果還卡把繪制緩沖區(qū)從 1/4 屏降到 1/10 屏降低每幀傳輸量。這幾個參數互相影響改一個不夠就三個一起調。6. 進階驗證刷新幀率、減小 LVGL 內存占用與后續(xù)優(yōu)化到這一步屏幕已經能正常顯示、觸摸也能點準了但這個項目還不算真正立住了。我通常會在交付前做兩件事先用性能數據驗證整體流暢度再根據數據決定要不要繼續(xù)壓縮內存。LVGL v8 自帶的LV_USE_PERF_MONITOR會在屏幕右上角顯示一個刷新率數值這是最直接的辦法。如果你的 UI 只有幾個頁面切換數值一般能達到 30 FPS 左右如果低于 15 FPS就要考慮繪制緩沖區(qū)大小或 SPI 頻率了。如果你想自己統(tǒng)計也可以開一個定時器統(tǒng)計每秒調用lv_task_handler的次數static uint32_t task_cnt 0; static void count_task(void *arg) { task_cnt; } static void fps_timer_cb(lv_timer_t *t) { static uint32_t last_cnt 0; uint32_t now_cnt task_cnt; ESP_LOGI(perf, fps: % PRIu32, now_cnt - last_cnt); last_cnt now_cnt; }這只是個統(tǒng)計手段重點是后續(xù)優(yōu)化方向。如果 CPU 占用率不高但幀率低問題多半在 SPI 傳輸和 LVGL 的單緩沖等待如果 CPU 占用率接近 100%問題在繪制開銷。對 1.69 寸屏我常用的三個優(yōu)化手段依次是繪制緩沖區(qū)改為雙緩沖約 1/4 屏讓 LVGL 在繪制下一幀時上一幀還能并行傳輸SPI 開啟 40MHz 以上并啟用 DMA如果 UI 全都是扁平色塊和文字把LV_COLOR_DEPTH從 16 降為 8 位調色板能顯著減內存但代價是不能顯示透圖和色彩豐富的圖片。圖形庫里還要記得關掉陰影、透明度和動畫特效這些在 1.69 寸小屏上效果有限卻會直接拖慢刷新。我自己的習慣是把這套顯示驅動封裝成獨立模塊st7789_init、touch_init、lvgl_init三個函數彼此解耦這樣換另一塊 1.69 寸屏時只需要改驅動參數。前期這些驅動層面被驗證好了后面所有 LVGL 界面開發(fā)才不會反復遷就硬件層的意外。希望這套來自采坑經驗的接法能幫到你。本文還有配套的精品資源點擊獲取