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