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