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