動(dòng)SPI屏全攻略:從ST7789到ILI9488)
簡(jiǎn)介本資源是一套專(zhuān)為ESP32-S3平臺(tái)適配LVGL v9圖形庫(kù)的SPI LCD驅(qū)動(dòng)代碼集合面向嵌入式開(kāi)發(fā)工程師、物聯(lián)網(wǎng)項(xiàng)目開(kāi)發(fā)者及LVGL初學(xué)者解決多型號(hào)屏幕在ESP-IDF環(huán)境下快速移植與穩(wěn)定顯示的核心痛點(diǎn)。壓縮包共13個(gè)文件29KB涵蓋核心驅(qū)動(dòng)源碼lcd_lvgl.c、硬件抽象層頭文件config.h、構(gòu)建配置CMakeLists.txt、sdkconfig、idf_component.yml、開(kāi)發(fā)環(huán)境支持.vscode配置、項(xiàng)目說(shuō)明README.md及分區(qū)表partitions.csv等關(guān)鍵組件結(jié)構(gòu)清晰、開(kāi)箱即用。已有950人學(xué)習(xí)下載代碼已實(shí)測(cè)兼容ST7789、ST7735、ST7796、ILI9341和ILI9488五類(lèi)主流SPI屏覆蓋135×240至480×320等多種分辨率顯著降低LVGL在ESP32-S3上的顯示適配門(mén)檻助力用戶(hù)聚焦UI設(shè)計(jì)與業(yè)務(wù)邏輯開(kāi)發(fā)。 ESP32S3 配 LVGL9再把 SPI 接口的 ST7789、ST7735、ST7796、ILI9341、ILI9488 這些常見(jiàn)屏全跑起來(lái)這事我前后折騰了挺長(zhǎng)時(shí)間踩過(guò)的坑最后都變成了經(jīng)驗(yàn)。如果你正在做小尺寸 HMI、桌面擺件、溫濕度計(jì)、甚至開(kāi)源手表之類(lèi)的東西這篇文章基本能把你從“不知道從哪下手”帶到“一次點(diǎn)亮并流暢跑動(dòng)畫(huà)”的階段。先說(shuō)結(jié)論ESP32S3 的 SPIn 外設(shè)足夠強(qiáng)LVGL9 的接口比 LVGL8 清晰不少真正會(huì)卡人的地方反而不是 LVGL 本身而是屏幕 IC 的初始化序列、偏移量設(shè)置、以及 DMA 傳輸?shù)耐椒绞?。這篇文章不會(huì)只貼一份 ST7789 的代碼就完事我會(huì)把常見(jiàn)屏的差異、LVGL9 的 display 驅(qū)動(dòng)結(jié)構(gòu)、SPI 參數(shù)選擇、以及花屏和偏移的排查方法全部拆開(kāi)講。1. 整體設(shè)計(jì)思路為什么這個(gè)組合值得寫(xiě)1.1 ESP32S3 在 GUI 項(xiàng)目里的定位ESP32S3 跟早期的 ESP32 比最大的變化是增加了向量指令和 SIMD 支持雖然主頻還是 240MHz但在跑 LVGL 這種 2D 圖形庫(kù)時(shí)軟件渲染的功耗和流暢度都會(huì)好一些。關(guān)鍵是這顆芯片的 SPIn 控制器支持 DMA最大傳輸數(shù)據(jù)量可以做到按描述符走完一整塊顯存數(shù)據(jù)不需要 CPU 一點(diǎn)點(diǎn)往外擠。這個(gè)是決定你能不能跑“高分屏動(dòng)畫(huà)”的核心。很多人會(huì)問(wèn)那為什么不用 ESP32-S3 直接接 RGB 并口屏因?yàn)?RGB 屏需要的引腳太多而且需要 PSRAM 配合幀緩沖成本也比 SPI 屏高一截。SPI 屏雖然帶寬有限但勝在引腳少6 根線(xiàn)搞定、驅(qū)動(dòng) IC 成熟、資料多而且 LVGL9 本身對(duì)低帶寬的優(yōu)化做得不錯(cuò)緩存策略合理的話(huà)日常刷新 320x240 的屏沒(méi)有任何壓力。1.2 為什么選擇 LVGL9 而不是繼續(xù)用 LVGL8LVGL9 改了不少底層接口最大的變化是把顯示驅(qū)動(dòng)從lv_disp_drv_t重構(gòu)為lv_display_t注冊(cè)方式、buffer 處理、flush 回調(diào)的觸發(fā)邏輯都有調(diào)整。網(wǎng)上大量教程還停留在 LVGL8 甚至 LVGL7直接抄代碼在 LVGL9 下面沒(méi)法編譯。這篇文章所有的代碼都基于 LVGL9.x你拿最新的 release 過(guò)來(lái)可以直接用。還有一點(diǎn)就是 LVGL9 對(duì) partial refresh局部刷新的處理更聰明了它不再要求每次全屏 flush而是會(huì)計(jì)算 dirty area配合 ESP32S3 的 DMA實(shí)際跑起來(lái)幀率比 LVGL8 有明顯提升。所以如果你是新項(xiàng)目別猶豫直接用 LVGL9。2. LVGL9 的顯示驅(qū)動(dòng)接口變化2.1 LVGL8 到 LVGL9 哪些舊代碼不能抄LVGL8 時(shí)代典型的初始化代碼長(zhǎng)這樣static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[240 * 10]; lv_disp_draw_buf_init(draw_buf, buf, NULL, 240 * 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.flush_cb my_flush_cb; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv);到了 LVGL9這段代碼里的lv_disp_drv_t已經(jīng)被拆掉統(tǒng)一走lv_display_tstatic lv_display_t *disp; disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, my_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);注意 LVGL9 里 buffer 是用字節(jié)數(shù)傳入的不是用像素?cái)?shù)。而且有兩個(gè) buffer 的話(huà)就是雙緩沖LVGL9 會(huì)在后臺(tái)渲染一個(gè) buffer 的同時(shí)把另一個(gè) buffer 通過(guò) flush_cb 送出去。這個(gè)機(jī)制是流暢度的關(guān)鍵。2.2 flush_cb 到底該怎么寫(xiě)LVGL9 的 flush_cb 簽名是void my_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);它比 LVGL8 更直接第三個(gè)參數(shù)直接給你渲染好的像素?cái)?shù)據(jù)指針px_map就是你要通過(guò) SPI 發(fā)給屏幕的內(nèi)容。你需要做的是把px_map里的數(shù)據(jù)切分成 16 位的 RGB565 數(shù)據(jù)通過(guò) SPI 發(fā)出去發(fā)送完成后調(diào)用lv_display_flush_ready(disp)告訴 LVGL 可以繼續(xù)渲染下一幀。這里最容易犯錯(cuò)的是LVGL9 默認(rèn)的像素格式是 RGB565但很多屏幕尤其是國(guó)產(chǎn)屏內(nèi)部實(shí)際是按 BGR 方式排列的你需要額外配置LV_COLOR_16_SWAP或者自己轉(zhuǎn)換字節(jié)順序否則顏色會(huì)明顯偏色。px_map是像素?cái)?shù)據(jù)不是完整的“要發(fā)送的 frame buffer”它只是 dirty area 的那部分。你拿到area結(jié)構(gòu)體后要自己計(jì)算出顯存地址偏移然后把 area 的坐標(biāo)換算成屏幕的起始 column 和 row通過(guò) SPI 寫(xiě)命令設(shè)置 window再發(fā)送像素?cái)?shù)據(jù)。這一步是整個(gè)驅(qū)動(dòng)里最容易寫(xiě)錯(cuò)的地方。3. 常見(jiàn) LCD 驅(qū)動(dòng) IC 適配記錄3.1 ST7789現(xiàn)在用得最多的“街屏”ST7789 是當(dāng)前 1.3 寸到 2.0 寸小屏里最常見(jiàn)的驅(qū)動(dòng) IC分辨率全覆蓋240x240、240x320、320x480 甚至部分圓屏。它支持 RGB565 和 RGB666 兩種格式通常我們走 RGB565。ST7789 的一個(gè)特點(diǎn)是它沒(méi)有單獨(dú)的VCOM引腳初始化序列較短而且很多模組已經(jīng)把 madctl內(nèi)存訪問(wèn)控制的翻轉(zhuǎn)電路做在 PCB 上了所以你拿到屏之后可能需要在代碼里根據(jù)實(shí)際顯示方向調(diào)整MADCTL寄存器的值。初始化序列里有一個(gè)坑ST7789 在 240x320 和 240x240 屏上的 row/column 偏移不一樣。大部分 240x240 的模組其實(shí)都是 240x320 的玻璃只是為了做方形顯示而已驅(qū)動(dòng) IC 內(nèi)部仍然有 320 行的尋址空間所以你需要把 row offset 設(shè)置成 80。也就是說(shuō)當(dāng)你要刷新第 0 行的時(shí)候?qū)嶋H上要寫(xiě)命令到 row 80 開(kāi)始的位置。這個(gè)偏移量不確定時(shí)可以查模組資料或者通過(guò)反復(fù)改 x/y offset 看屏幕顯示情況來(lái)確定。3.2 ST7735小尺寸花屏重災(zāi)區(qū)ST7735 常見(jiàn)于 1.8 寸 128x160 和 1.44 寸 128x128是 ST7789 的“老前輩”。ST7735 的初始化序列更麻煩因?yàn)樗卸鄠€(gè)版本A/B/C不同版本的 gamma 曲線(xiàn)和偏壓配置不完全一樣。如果初始化不對(duì)最常見(jiàn)的現(xiàn)象是白屏或者顯示重影。ST7735 的另一個(gè)坑是它的 MADCTL 默認(rèn)掃描方向和很多 128x160 模組的實(shí)際接線(xiàn)不匹配你經(jīng)常需要在代碼里反轉(zhuǎn) RGB 順序MADCTL中的 BGR 位才能得到正確的顏色如果顏色反了把MADCTL的 BGR 位從 0 改成 1 或者反過(guò)來(lái)即可。ST7735 的偏移量128x160 屏通常沒(méi)有偏移問(wèn)題但 128x128 方屏需要把 row offset 設(shè)成 2也就是數(shù)據(jù)區(qū)域從第 2 行開(kāi)始。如果沒(méi)有設(shè)置這個(gè)偏移圖形會(huì)整體向下偏兩行像一條細(xì)線(xiàn)出現(xiàn)在頂部。3.3 ST7796 和 ILI9341大屏兼容性ST7796 和 ILI9341 都常見(jiàn)于 2.4 寸和 2.8 寸的 320x240 屏這兩顆 IC 的初始化序列有很多相似之處網(wǎng)上也經(jīng)常有人用同一套初始化序列去兼容兩種情況但嚴(yán)格來(lái)說(shuō)還是不一樣。ILI9341 是最經(jīng)典的 SPI LCD 驅(qū)動(dòng) IC幾乎所有單片機(jī)教程都會(huì)提到它。它的初始化序列很長(zhǎng)但兼容性很好如果你不確定自己手里的屏是哪顆 IC先用 ILI9341 的序列試著點(diǎn)亮成功的概率很高。ST7796 跟 ILI9341 默認(rèn)的 gamma 不同ST7796 的色彩更鮮艷一些但如果你把 ILI9341 的 gamma 設(shè)置到了 ST7796 上屏幕可能會(huì)發(fā)白或者發(fā)灰。我實(shí)測(cè)過(guò)如果你的屏是 ST7796 而用了 ILI9341 的初始化序列部分屏能顯示但對(duì)比度明顯差一截建議還是找對(duì)應(yīng) IC 的初始化序列來(lái)跑。3.4 ILI9488RGB666 的坑ILI9488 常見(jiàn)于 3.5 寸 480x320 屏有一個(gè)大坑它默認(rèn)是 RGB666 格式不是 RGB565。如果你直接把 LVGL 的 RGB565 buffer 發(fā)給它顯示顏色會(huì)明顯不對(duì)幾乎是扭曲的。解決辦法有兩種初始化序列里切換到 RGB565 模式如果 IC 支持具體操作是在COLMOD3Ah 命令里設(shè)置 16 位色深。如果你的屏模塊不支持 RGB565 模式就得在 flush_cb 里把 RGB565 轉(zhuǎn)換成 RGB666。但這樣不僅費(fèi) CPU還會(huì)降低幀率。所以我一般不建議在 ESP32S3 上用 ILI9488除非你確實(shí)需要大屏且沒(méi)有替代方案。下面是這幾顆 IC 的直觀對(duì)比表方便你快速定位自己手上的屏。IC 型號(hào)常見(jiàn)分辨率色深支持常見(jiàn)偏移問(wèn)題初始化復(fù)雜度實(shí)測(cè)推薦度ST7789240x240 / 240x320RGB565 / RGB666240x240 需 row offset 80低高ST7735128x160 / 128x128RGB565128x128 需 row offset 2中中ST7796320x240RGB565基本無(wú)低高ILI9341320x240RGB565基本無(wú)中高ILI9488480x320RGB666 默認(rèn)色深轉(zhuǎn)換麻煩高低4. 實(shí)操完整驅(qū)動(dòng)配置與初始化4.1 引腳分配與接線(xiàn)ESP32S3 的 SPIn 外設(shè)很靈活GPIO 矩陣允許你幾乎把 SPI 信號(hào)映射到任意引腳。但為了 DMA 性能和信號(hào)穩(wěn)定我個(gè)人建議把 SCLK、MOSI 固定到一組相鄰引腳上MISO 不用接LCD 輸出我們一般不讀。我常用的引腳分配是這樣信號(hào)GPIO說(shuō)明SCLKGPIO12SPI 時(shí)鐘MOSIGPIO11SPI 數(shù)據(jù)輸出CSGPIO10片選DCGPIO9數(shù)據(jù)/命令選擇RSTGPIO8復(fù)位BLGPIO45背光控制每個(gè)人手里的板子不一樣引腳完全可以根據(jù)自己的布局換。注意 DC 引腳很關(guān)鍵LCD 通過(guò) DC 來(lái)區(qū)分命令和數(shù)據(jù)如果你的 DC 接錯(cuò)屏幕能亮但顯示內(nèi)容會(huì)是一堆亂碼。RST 可以用 GPIO 也可以用 ESP32S3 的上電自動(dòng)復(fù)位電路但手動(dòng)控制更可靠。SPI LCD 常用 7 根線(xiàn)包含 GND 和 VCC有些模組是 6 針省掉了 RST 或 BL這就意味著 RST 要靠寄存器軟復(fù)位、BL 要一直拉高在代碼里要注意別去操作不存在的引腳。4.2 SPI 初始化配置使用 ESP-IDF 的spi_bus_initialize來(lái)初始化 SPI 主機(jī)ESP32S3 有 SPI2 和 SPI3 兩個(gè)可用主機(jī)LCD 接到 SPI2 比較常見(jiàn)spi_bus_config_t bus_cfg { .sclk_io_num 12, .mosi_io_num 11, .miso_io_num -1, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 240 * 320 * 2 8, }; spi_bus_initialize(SPI2_HOST, bus_cfg, SPI_DMA_CH_AUTO);重點(diǎn)在max_transfer_sz這個(gè)值決定了 DMA 能一次傳輸?shù)淖畲笞止?jié)數(shù)。如果你要直接傳輸整個(gè) 320x240 的 RGB565 frame153600 字節(jié)這里必須留夠空間。我一般會(huì)設(shè)成屏幕寬 * 屏幕高 * 2 8留一點(diǎn)余量。SPI_DMA_CH_AUTO讓驅(qū)動(dòng)自動(dòng)分配 DMA 通道ESP32S3 上可用 DMA 通道比老 ESP32 多這個(gè)建議別手動(dòng)指定讓系統(tǒng)自動(dòng)管理。4.3 LCD 初始化序列與偏移量設(shè)置先把 GPIO 和 SPI 總線(xiàn)準(zhǔn)備好然后對(duì) LCD 進(jìn)行復(fù)位和初始化。下面是一份標(biāo)準(zhǔn) ST7789 初始化序列的核心部分適用于 240x240 或 240x320 屏// 復(fù)位 gpio_set_level(RST_PIN, 0); vTaskDelay(pdMS_TO_TICKS(20)); gpio_set_level(RST_PIN, 1); vTaskDelay(pdMS_TO_TICKS(120)); // 初始化命令 lcd_cmd(0x01); // SWRESET vTaskDelay(pdMS_TO_TICKS(20)); lcd_cmd(0x11); // SLPOUT vTaskDelay(pdMS_TO_TICKS(120)); lcd_cmd(0x3A); // COLMOD lcd_data(0x05); // 16-bit color // 設(shè)置偏移 uint16_t x_offset 0; uint16_t y_offset 80; // 240x240 屏常見(jiàn)偏移 lcd_cmd(0x2A); // CASET lcd_data(x_offset 8); lcd_data(x_offset 0xFF); lcd_data((x_offset width - 1) 8); lcd_data((x_offset width - 1) 0xFF); lcd_cmd(0x2B); // RASET lcd_data(y_offset 8); lcd_data(y_offset 0xFF); lcd_data((y_offset height - 1) 8); lcd_data((y_offset height - 1) 0xFF); lcd_cmd(0x21); // INVON lcd_cmd(0x11); vTaskDelay(pdMS_TO_TICKS(20)); lcd_cmd(0x29); // DISPON這里最重要的就是CASET和RASET也就是 column address set 和 row address set。LVGL 告訴你需要刷新窗口的左上角和右下角坐標(biāo)你要把這兩個(gè)坐標(biāo)加上偏移量后換算成屏幕內(nèi)部顯存地址。如果偏移量設(shè)置有誤顯示的內(nèi)容會(huì)整體偏移輕則邊緣出現(xiàn)黑邊重則內(nèi)容錯(cuò)位或被截?cái)唷?.4 LVGL9 display 注冊(cè)代碼初始化完 LCD 之后把 display 注冊(cè)到 LVGL9 里static lv_color_t buf1[240 * 10]; static lv_color_t buf2[240 * 10]; lv_display_t *disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, lcd_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);注意兩個(gè) buffer 各是240*10像素?fù)Q算成 RGB565 就是 4800 字節(jié)。這個(gè) buffer 大小是我實(shí)測(cè)下來(lái)性能和內(nèi)存占用的平衡點(diǎn)太小會(huì)導(dǎo)致 LVGL 頻繁刷新太大又浪費(fèi)內(nèi)存。如果你開(kāi)了 PSRAM 且不在意內(nèi)存可以把 buffer 擴(kuò)大到一行像素的 2~4 倍幀率會(huì)更好。flush 回調(diào)里需要根據(jù) LVGL 傳入的 area 來(lái)控制 LCD 的 window。假設(shè)我們的屏是 240x320LVGL 的坐標(biāo)原點(diǎn)在左上角物理屏的坐標(biāo)原點(diǎn)也在左上角void lcd_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint16_t x1 area-x1; uint16_t x2 area-x2; uint16_t y1 area-y1; uint16_t y2 area-y2; uint16_t w x2 - x1 1; uint16_t h y2 - y1 1; // 設(shè)置 window lcd_cmd(0x2A); // CASET lcd_data((x1 x_offset) 8); lcd_data((x1 x_offset) 0xFF); lcd_data((x2 x_offset) 8); lcd_data((x2 x_offset) 0xFF); lcd_cmd(0x2B); // RASET lcd_data((y1 y_offset) 8); lcd_data((y1 y_offset) 0xFF); lcd_data((y2 y_offset) 8); lcd_data((y2 y_offset) 0xFF); lcd_cmd(0x2C); // RAMWR // 發(fā)送像素?cái)?shù)據(jù) spi_transaction_t t { .length w * h * 2 * 8, .tx_buffer px_map, }; spi_device_polling_transmit(spi_dev, t); lv_display_flush_ready(disp); }spi_device_polling_transmit是阻塞式發(fā)送簡(jiǎn)單但會(huì)浪費(fèi) CPU。更好的方式是使用spi_device_queue_trans配合傳輸完成中斷在中斷回調(diào)里調(diào)用lv_display_flush_ready。不過(guò)這套異步邏輯對(duì)初學(xué)者不太友好我建議先把阻塞式跑通再改成 DMA 中斷。5. 性能優(yōu)化DMA、雙緩沖、幀率調(diào)整5.1 SPI DMA 為什么是剛需如果不使用 DMA每次發(fā)送像素?cái)?shù)據(jù)都要 CPU 把數(shù)據(jù)搬到 SPI 外設(shè)的 FIFO320x240 一幀 153600 字節(jié)按每字節(jié) 8 個(gè) SPI 時(shí)鐘算純 SPI 傳輸就需要約 240 萬(wàn)次外設(shè)寫(xiě)入操作。即使 SPI 跑到 80MHzCPU 也會(huì)被 SPI 中斷占滿(mǎn)LVGL 的渲染時(shí)間被嚴(yán)重?cái)D壓。ESP32S3 的 SPI 主機(jī)支持 DMA 方式發(fā)送你需要用spi_device_queue_trans接口或者在初始化 SPI 設(shè)備時(shí)把spi_device_interface_config_t里的.flags設(shè)置上SPI_DEVICE_HALFDUPLEX和SPI_DEVICE_NO_DUMMY。發(fā)送大塊 buffer 時(shí)驅(qū)動(dòng)會(huì)自動(dòng)拆分成 DMA 描述符傳輸過(guò)程中 CPU 可以繼續(xù)做下一幀渲染。我用 80MHz SPI 時(shí)鐘 DMA 的方式跑 240x320 的 ST7789LVGL9 的動(dòng)畫(huà)幀率能到 50fps 左右簡(jiǎn)單場(chǎng)景日常 UI 操作也不卡。如果 SPI 時(shí)鐘降為 40MHz幀率會(huì)掉到一半感官上就明顯“肉”了。5.2 雙緩沖與 LVGL9 的渲染模式LVGL9 的雙緩沖機(jī)制比 LVGL8 要順暢原因在于它的渲染任務(wù)和 flush 任務(wù)可以并行。你在lv_display_set_buffers里傳入兩個(gè) buffer 后LVGL 會(huì)在 buffer 1 上渲染渲染完成觸發(fā) flush_cb然后立刻開(kāi)始在 buffer 2 上渲染下一幀DMA 傳輸 buffer 1 的同時(shí)CPU 在渲染 buffer 2等于把傳輸時(shí)間和渲染時(shí)間重疊起來(lái)了。如果你的內(nèi)存緊張可以退化成單緩沖但代價(jià)是每次渲染完必須等 SPI 發(fā)送完GPU 和 IO 完全串行流暢度會(huì)明顯下降。所以我建議哪怕是犧牲一點(diǎn) buffer 大小也要保留雙緩沖。5.3 SPI 時(shí)鐘頻率與傳輸優(yōu)化ESP32S3 的 SPI 時(shí)鐘在驅(qū)動(dòng) LCD 時(shí)通??梢耘艿?60~80MHz但不是所有屏都能穩(wěn)定跑 80MHz。如果你的屏模組走線(xiàn)很長(zhǎng)或者使用了杜邦線(xiàn)連接跑 80MHz 大概率會(huì)出現(xiàn)隨機(jī)花屏或條紋。這時(shí)候不要懷疑代碼先把 SPI 時(shí)鐘降到 40MHz 試試90% 的“花屏”都能好。另外SPI 設(shè)備初始化時(shí)有一個(gè)clock_source參數(shù)。ESP32S3 上可以選擇SPI_CLK_SRC_DEFAULT或SPI_CLK_SRC_XTAL。我實(shí)測(cè)發(fā)現(xiàn)用外部晶振作為 SPI 時(shí)鐘源在高溫環(huán)境下更穩(wěn)定。不過(guò)這個(gè)差異很小一般不需要專(zhuān)門(mén)改。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 花屏 / 顯示錯(cuò)位花屏是一大類(lèi)問(wèn)題我把它拆成幾個(gè)具體現(xiàn)象現(xiàn)象大概率原因排查方法全屏噪點(diǎn) / 雪花SPI 時(shí)序不穩(wěn)或頻率太高降低 SPI 時(shí)鐘到 20MHz 測(cè)試顯示內(nèi)容偏移 80 行ST7789 240x240 屏 offset 未設(shè)置加 row offset 80局部區(qū)域內(nèi)容錯(cuò)位window 設(shè)置不正確檢查 CASET/RASET 是否使用了正確的 x_offset / y_offset頂部有一條彩色線(xiàn)偏移量少了 1~2 像素調(diào)整 row/column offset 的細(xì)微值刷新時(shí)撕裂單緩沖 高速刷新改用雙緩沖花屏排查有一個(gè)通用技巧先用純色填充命令來(lái)測(cè)試 LCD 本身是否正常。如果你調(diào)用lcd_cmd(0x2C)后直接發(fā)送 2 字節(jié)的紅色像素?cái)?shù)據(jù)屏幕能正常顯示紅色說(shuō)明 LCD 和 SPI 底層沒(méi)問(wèn)題問(wèn)題出在 LVGL 的坐標(biāo)或 buffer 管理上。6.2 顏色不對(duì)顏色不對(duì)的原因主要有兩個(gè)RGB 順序不對(duì)如果你的紅色顯示成藍(lán)色、綠色顯示成紫色多半是 RGB 與 BGR 順序反了。在初始化序列里嘗試把 MADCTL36h中的 BGR 位翻轉(zhuǎn)或者設(shè)置LV_COLOR_16_SWAP 1取決于你的屏和 ESP32S3 的字節(jié)序。RGB565 與 RGB666 混用ST7789 默認(rèn) RGB565 沒(méi)問(wèn)題但 ILI9488 初始狀態(tài)往往是 RGB666如果不顯式切換顏色會(huì)整體發(fā)灰需要手動(dòng)設(shè)置 COLMOD 寄存器。另外ESP32S3 是小端 CPU當(dāng)你把 RGB565 顏色存入內(nèi)存時(shí)字節(jié)序是反的。LVGL9 默認(rèn)會(huì)處理這個(gè)但如果你直接輸出了裸 buffer可能就要手動(dòng) swap 一下高低字節(jié)。這個(gè)看具體編譯選項(xiàng)不需要死記先跑起來(lái)再根據(jù)現(xiàn)象調(diào)。6.3 亮度、閃爍背光控制建議用 PWM 引腳不要直接 GPIO 拉高拉低。一級(jí)線(xiàn)性調(diào)光時(shí)ESP32S3 的 LEDC 外設(shè)就能勝任。接線(xiàn)時(shí)把 BL 接到 LEDC 輸出引腳即可。閃爍問(wèn)題則大概率不是背光的問(wèn)題而是 LVGL 的刷新頻率過(guò)低或 buffer 太小。比如 240x320 的屏只給了 240x4 的 bufferLVGL 就得把屏幕切成幾十個(gè)局部區(qū)域刷新人眼看起來(lái)就像一直在閃。這時(shí)候加大 buffer、關(guān)閉 LVGL 的LV_DISPLAY_RENDER_MODE_DRAW_UNIT或者調(diào)整刷新周期都會(huì)有效果。6.4 初始化不成功白屏白屏通常說(shuō)明 LCD 已經(jīng)上電、背光也亮了但沒(méi)有收到有效的初始化序列或命令參數(shù)不對(duì)。先檢查 RST 和 DC 引腳是否連接正確然后用邏輯分析儀抓 SPI 信號(hào)確認(rèn) MOSI 上有沒(méi)有數(shù)據(jù)、SCLK 有沒(méi)有時(shí)鐘。如果手頭沒(méi)有邏輯分析儀就寫(xiě)一個(gè)最簡(jiǎn)單的循環(huán)反復(fù)發(fā)送0x2C命令和全紅像素?cái)?shù)據(jù)。如果屏幕有反應(yīng)但顏色不對(duì)說(shuō)明 SPI 通信正常只是初始化序列或者 RGB 配置有問(wèn)題。這個(gè)循環(huán)排查法能快速把問(wèn)題從“通信”和“時(shí)序配置”里分離出來(lái)。7. 我的實(shí)際配置參考與最后提醒最后給一套我用著比較順手的模板配置你可以直接照著抄一部分。屏幕是 1.86 寸 240x320 ST7789SPI 時(shí)鐘 80MHzLVGL9.1ESP-IDF 5.2static spi_device_interface_config_t dev_cfg { .clock_speed_hz SPI_MAIN_FREQ, // 80MHz .mode 0, .spics_io_num PIN_CS, .queue_size 7, .flags SPI_DEVICE_HALFDUPLEX, .pre_cb NULL, };static lv_display_t *disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, lcd_flush_cb); lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_0);其他屏的適配其實(shí)就是替換初始化序列和調(diào)整 x_offset / y_offsetflush_cb 里的邏輯完全通用。你只需要知道手里屏的 IC 型號(hào)和玻璃分辨率就能把整套驅(qū)動(dòng)遷移過(guò)去。我個(gè)人在實(shí)際操作中的體會(huì)是SPI LCD 驅(qū)動(dòng)的難點(diǎn)從來(lái)不在 LVGL 本身而在于你對(duì)“屏幕內(nèi)部顯存尋址”的理解。只要把 CASET / RASET 偏移量、RGB 順序、以及 SPI 傳輸?shù)耐椒绞脚靼譙PI LCD 就是一個(gè)數(shù)據(jù)搬運(yùn)工的角色。說(shuō)白了你是在用一根 SPI 線(xiàn)往屏幕顯存里不斷的寫(xiě)LVGL9 負(fù)責(zé)告訴你“該往哪里寫(xiě)、寫(xiě)什么顏色”你負(fù)責(zé)用最省 CPU 的方式把數(shù)據(jù)送出去。剩下的就是調(diào)參和磨代碼的過(guò)程了。本文還有配套的精品資源點(diǎn)擊獲取