
簡介面向ESP32S3與CST328/ST7789彩色屏幕開發(fā)者這套LVGLESP-IDF例程包提供了從硬件初始化到圖形界面渲染的完整參考實現(xiàn)。資源共1342個文件涵蓋C源碼、頭文件、Python腳本、Markdown說明文檔及字體、位圖、GIF等素材壓縮后約26.45MB目錄按源碼、示例、文檔和資源分開便于按模塊查閱。目前已有1448人學習適合正在做物聯(lián)網(wǎng)人機界面或嵌入式GUI項目的工程師快速上手。通過這套工程可以掌握SPI接口配置、LVGL組件移植、觸摸校準以及音樂播放器、控件展示等多套demo的代碼組織方式還能直接復(fù)用內(nèi)置的位圖、字體和波形數(shù)據(jù)顯著縮短顯示類產(chǎn)品開發(fā)周期。對于希望基于ESP-IDF調(diào)試LVGL的開發(fā)者來說這是很實用的入門與二次開發(fā)藍本。 最近把手上一個老項目的顯示部分整體升級了一遍主控換成 ESP32-S3屏幕用最常見的 ST7789 方案 TFT觸摸 IC 配上 CST328UI 框架走 LVGL整個工程跑在 ESP-IDF 上。從環(huán)境搭建、底層驅(qū)動到 LVGL 界面跑通中間踩了不少坑也沉淀出一套可以直接復(fù)用的例程。這篇文章把整個鏈路從頭到尾拆開講適合正準備在 ESP32-S3 上做帶觸摸人機界面的朋友參考。這套組合在物聯(lián)網(wǎng)產(chǎn)品和快速原型里非常常見ESP32-S3 本身雙核 240MHz帶 WiFi/BLE跑 LVGL 足夠流暢ST7789 是屏幕驅(qū)動里出貨量最大的一顆 SPI 接口芯片資料多、驅(qū)動成熟CST328 是電容觸摸控制芯片走 I2C支持多點上報配 LVGL 的 indev 接口非常合適。下面從方案選型開始到最終運行調(diào)試完整記錄一遍。1. 項目概述與方案選型1.1 為什么是 ESP32-S3 ST7789 CST328選 ESP32-S3 不是因為參數(shù)堆得高而是它的外設(shè)和內(nèi)存對 LVGL 實在友好。LVGL 在 MCU 上最吃兩樣?xùn)|西RAM 和刷新帶寬。ESP32-S3 有 512KB SRAM跑 320×240 的 RGB565 界面雙緩沖加 UI 內(nèi)存開銷一般 100KB 以內(nèi)剩余空間足夠跑業(yè)務(wù)邏輯。SPI 外設(shè)帶 DMA40MHz 時鐘下刷新一屏 320×240 基本十幾毫秒級別UI 交互完全跟手。加上 WiFi/BLE一個屏就能撐起一個完整的聯(lián)網(wǎng)控制面板這是很多裸外設(shè)方案不具備的。屏幕選 ST7789 也是從成本和資料兩方面考慮。ST7789 支持 240×240、240×320 這類常見尺寸RGB565 輸出夠用初始化序列簡單穩(wěn)定。CST328 作為觸摸 ICI2C 接口只需要 SDA、SCL、INT 三根線中點上報LVGL 的lv_indev讀取起來很自然。這套組合的性價比和可替代性都很好后續(xù)換屏、換觸摸 IC驅(qū)動改動集中在少數(shù)幾個文件里。1.2 LVGL 與 ESP-IDF 的組合優(yōu)勢LVGL 版本選擇上我最后鎖定了 8.3 系列。LVGL 9 在 2024 年后已經(jīng)成熟但 8.3 的資料、組件、GUI Guider 導(dǎo)出兼容性都更穩(wěn)尤其對新手出問題搜得到答案。LVGL 9 用 Kconfig 方式集成到 ESP-IDF8.3 更習慣用lv_conf.h對我來說 8.3 是“少折騰能出活”的選擇。ESP-IDF 這邊我推薦 5.3 或 5.4。5.x 的esp_lcd組件已經(jīng)把 ST7789 這類 SPI 屏幕封裝好了初始化、DMA 刷新、旋轉(zhuǎn)都有現(xiàn)成接口不用自己摳時序。LVGL 的顯示刷新回調(diào)和觸摸讀取回調(diào)正好對接esp_lcd和 I2C 驅(qū)動代碼結(jié)構(gòu)非常清晰。這個組合的好處是底層顯示交給esp_lcd管理LVGL 只負責 UI 繪制和事件響應(yīng)不需要關(guān)心 SPI 寄存器細節(jié)。提示如果你用 Ubuntu 24.04裝 ESP-IDF 優(yōu)先選 v5.3 以上。v5.2 及以下版本的 Python 插件和 3.12 兼容性有點問題編譯時會莫名其妙報錯排查起來很浪費時間。2. 環(huán)境準備與工程骨架2.1 創(chuàng)建工程結(jié)構(gòu)先按最常規(guī)的方式創(chuàng)建 IDF 工程。個人習慣用 VS Code ESP-IDF 插件插件裝好之后會引導(dǎo)你安裝工具鏈和 SDK。安裝完成后建議先把 IDF 路徑確認清楚。VS Code 里idf.py不在時最常見的報錯是the path for esp-idf is not valid: /tools/idf.py not found這個問題的本質(zhì)是插件配置的 IDF 路徑指到了錯誤位置要填 IDF 的根目錄也就是里面確實存在tools/idf.py的那一層。工程結(jié)構(gòu)我建議這樣組織your_project/ ├── main/ │ ├── CMakeLists.txt │ ├── idf_component.yml │ ├── lv_conf.h │ ├── app_main.c │ ├── lcd_display.c │ └── touch_cst328.c ├── partitions.csv └── sdkconfig.defaultsLVGL 我走的是組件管理器方式在main/idf_component.yml里聲明依賴不用手動復(fù)制大段源碼進工程dependencies: lvgl/lvgl: version: ^8.3.0然后在main/CMakeLists.txt里加一行REQUIRES lvgl編譯時組件管理器會從倉庫拉取并管理 LVGL。這樣升級版本、換分支都方便工程目錄也干凈。2.2 VS Code 高頻環(huán)境報錯排查第一個高頻報錯是failed to set target esp32s3: non zero exit code 2。我一開始以為是芯片選錯了后來發(fā)現(xiàn)最可能是工程里的sdkconfig和當前 target 不一致或者插件安裝在 Windows 下路徑?jīng)]配對。解決方法是先備份sdkconfig刪除build目錄和sdkconfig然后在命令面板執(zhí)行 “ESP-IDF: Set Espressif Device Target”重新選擇esp32s3重新構(gòu)建。如果是在類 Unix 系統(tǒng)下還要注意當前用戶是否在dialout組里否則后面idf.py monitor會打不開串口。第二個高頻問題是編譯中途報esp_log_color.h找不到。這類報錯通常和 IDF 路徑有關(guān)很多是路徑里包含中文、空格或者插件緩存了舊的 CMake 配置。處理方式是清理build目錄重新加載 CMake確保路徑干凈。強調(diào)一下ESP-IDF 的安裝路徑最好不要帶中文和空格這是老生常談但永遠有人踩。注意不要在普通終端里手動敲idf.py命令除非你已經(jīng)執(zhí)行過 IDF 的export.sh。VS Code 里所有構(gòu)建操作都通過插件的任務(wù)執(zhí)行插件會自動初始化環(huán)境變量省去很多環(huán)境問題。3. 硬件連接與底層驅(qū)動解析3.1 引腳分配參考ST7789 用 SPI 接口CST328 用 I2C 接口接線非常簡單。下面是 I 用的一塊 240×320 屏幕模組對應(yīng)的引腳分配不同開發(fā)板引腳位置有差異按自己板子的絲印調(diào)整信號GPIO說明LCD_SCKGPIO12SPI 時鐘LCD_MOSIGPIO11SPI 數(shù)據(jù)LCD_CSGPIO10片選低有效LCD_DCGPIO9數(shù)據(jù)/命令選擇LCD_RSTGPIO8屏幕復(fù)位LCD_BLGPIO7背光控制TOUCH_SDAGPIO4I2C 數(shù)據(jù)TOUCH_SCLGPIO5I2C 時鐘TOUCH_INTGPIO6觸摸中斷SPI 默認接在 SPI2 主機上ESP-IDF 的esp_lcd組件會自己管理這個外設(shè)。I2C 用 I2C_NUM_0速率 400kHz觸摸屏對實時性要求不高這個速度足夠。屏幕背光可以用一個 GPIO 直接控制如果需要亮度調(diào)節(jié)改成 LEDC PWM 輸出就行。3.2 ST7789 顯示驅(qū)動的關(guān)鍵點用esp_lcd驅(qū)動 ST7789 最大的好處是省心。初始化時把esp_lcd_panel_io_spi_config_t和esp_lcd_panel_dev_config_t配好然后調(diào)用一句esp_lcd_new_panel_st7789就能拿到面板句柄。關(guān)鍵參數(shù)有三個pclk_hz設(shè) 40MHzcolor_space設(shè)ESP_LCD_COLOR_SPACE_RGBbits_per_pixel設(shè) 16。RGB565 是 LVGL 默認色彩深度兩邊必須對齊否則顏色錯亂。ST7789 初始化之后很多模組需要做一次esp_lcd_panel_invert_color(panel, true)否則顏色會變成負片效果。這個不是芯片決定死的而是取決于模組偏光片所以不同商家屏幕表現(xiàn)不一樣。旋轉(zhuǎn)方向用esp_lcd_panel_swap_xy和esp_lcd_panel_mirror調(diào)整。我這次橫屏 320×240 顯示就是先swap_xy再按實際效果改 mirror 參數(shù)一點一點試出來的。3.3 CST328 觸摸驅(qū)動的關(guān)鍵點CST328 是 I2C 從設(shè)備常見 7 位地址是 0x15在總線上寫地址為 0x2A讀地址為 0x2B。不同模組廠家可能支持不同地址拿到板子后先確認硬件手冊地址錯了讀出來全是 0xFF。讀取觸摸點的核心寄存器很簡單0x01觸摸點數(shù)低 4 位有效0x02第一點 X 坐標低字節(jié)0x03第一點 X 坐標高字節(jié)低 4 位有效0x04第一點 Y 坐標低字節(jié)0x05第一點 Y 坐標高字節(jié)低 4 位有效一次 I2C 讀操作把 0x01 到 0x05 共 5 個字節(jié)讀回來判斷點數(shù)大于 0 就開始處理坐標。CST328 支持多點上報但 LVGL 默認只需要一個點所以每次取第一點的數(shù)據(jù)就夠了。如果以后要手勢識別再擴展讀多點數(shù)據(jù)。4. LVGL 接入與界面實踐4.1 初始化 LVGL 顯示與輸入設(shè)備LVGL 初始化分三步初始化顯示驅(qū)動、初始化輸入設(shè)備驅(qū)動、啟動 tick 時鐘。顯示驅(qū)動里最重要的是 flush 回調(diào)LVGL 每次繪制完一塊區(qū)域就調(diào)用一次回調(diào)讓底層把像素推到屏幕。用esp_lcd實現(xiàn)極簡單static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { esp_lcd_panel_handle_t panel (esp_lcd_panel_handle_t)drv-user_data; esp_lcd_panel_draw_bitmap(panel, area-x1, area-y1, area-x2 1, area-y2 1, color_p); lv_disp_flush_ready(drv); }這里需要注意esp_lcd_panel_draw_bitmap的參數(shù)是“結(jié)束坐標 1”的形式和 LVGL 的閉區(qū)間不一樣寫錯會導(dǎo)致邊緣花屏。flush 回調(diào)執(zhí)行完必須調(diào)用lv_disp_flush_ready(drv)否則 LVGL 會一直等待整個 UI 卡死。分配繪制緩沖區(qū)時建議用heap_caps_malloc帶MALLOC_CAP_DMA標志因為 SPI DMA 在部分配置下對內(nèi)存有要求普通堆內(nèi)存可能觸發(fā) cache 不連續(xù)問題。緩沖區(qū)大小我用橫向 40 行的雙緩沖static lv_color_t *buf1 heap_caps_malloc(320 * 40 * 2, MALLOC_CAP_DMA); static lv_color_t *buf2 heap_caps_malloc(320 * 40 * 2, MALLOC_CAP_DMA); lv_disp_draw_buf_init(draw_buf, buf1, buf2, 320 * 40);觸摸輸入設(shè)備驅(qū)動填read_cb回調(diào)即可。LVGL 會周期性調(diào)用這個回調(diào)讀取當前觸摸狀態(tài)static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint8_t reg 0x01; uint8_t buf[5]; esp_err_t ret i2c_master_write_read_device(I2C_NUM_0, (0x15 1), reg, 1, buf, 5, 50 / portTICK_PERIOD_MS); if (ret ESP_OK (buf[0] 0x0F)) { uint16_t x ((buf[2] 0x0F) 8) | buf[1]; uint16_t y ((buf[4] 0x0F) 8) | buf[3]; >lv_font_conv --font NotoSansCJK-Regular.ttc --size 16 --bpp 4 \ --range 0x20-0x7E,0x4E00-0x9FA5 \ --format c --output font_cn.c上面這條命令僅供演示全字庫兩萬多漢字生成后體積很大。生產(chǎn)項目里更合理的方式是用 GUI Guider 配套的字體子集化或者手動列出界面需要的漢字比如--range 0x20-0x7E,0xE6B8-0xE6B8這種按字符點選的寫法。生成的 C 文件放進工程后注冊方式就是普通 LVGL 字體指針extern lv_font_t font_cn; lv_obj_set_style_text_font(label, font_cn, 0);模態(tài)化彈窗效果也很常用。LVGL 8.3 里lv_disp_get_layer_top可以拿到頂層圖層在這個圖層上創(chuàng)建一個全屏遮罩再往遮罩上疊彈窗控件就能做出禁用下層點擊的模態(tài)化界面static void create_modal(lv_obj_t *parent) { lv_obj_t * top lv_disp_get_layer_top(lv_disp_get_default()); lv_obj_t * mask lv_obj_create(top); lv_obj_set_size(mask, LV_HOR_RES, LV_VER_RES); lv_obj_set_style_bg_color(mask, lv_color_black(), 0); lv_obj_set_style_bg_opa(mask, LV_OPA_50, 0); lv_obj_set_style_border_width(mask, 0, 0); lv_obj_add_flag(mask, LV_OBJ_FLAG_CLICKABLE); }注意一定給遮罩容器加上LV_OBJ_FLAG_CLICKABLE否則點擊空白區(qū)域會穿透到下層控件模態(tài)效果直接失效。這個細節(jié)在官方文檔里寫得很含蓄實測踩過一次坑。界面里的業(yè)務(wù)控件我順手做了個 PWM 控制滑塊很典型一個 Slider 控件調(diào)節(jié)占空比事件回調(diào)里寫 LEDC 即可。電機轉(zhuǎn)速控制、燈亮度調(diào)節(jié)都是這套邏輯LVGL 的lv_slider_get_value拿到 0 到 100 的數(shù)值映射到對應(yīng)占空比后寫入 LEDC 通道界面和業(yè)務(wù)就分開了。5. 實際運行中的問題與調(diào)試經(jīng)驗5.1 顯示類問題速查問題現(xiàn)象排查方向解決辦法白屏背光亮但無內(nèi)容SPI 引腳、DC 腳、初始化時序確認init后調(diào)用了disp_on_off用邏輯分析儀抓 SPI 是否有數(shù)據(jù)顏色偏色或負片偏光片特性對 ST7789 執(zhí)行esp_lcd_panel_invert_color切換試一次花屏或邊緣錯位坐標映射錯誤、緩沖大小錯誤檢查draw_bitmap的 x21/y21檢查 draw_buf 大小是否超過一行橫豎屏方向不對MADCTL 設(shè)置用swap_xy加mirror組合試 4 種方向找到正確組合花屏問題最容易誤導(dǎo)人。我一開始懷疑是 SPI 速率太高降到 10MHz 一樣花屏最后發(fā)現(xiàn)是esp_lcd_panel_draw_bitmap傳參寫錯了結(jié)束坐標沒加 1。所以在動硬件參數(shù)之前先確認軟件坐標邊界處理。5.2 觸摸類問題速查問題現(xiàn)象排查方向解決辦法觸摸完全沒反應(yīng)I2C 地址、中斷腳、寄存器偏移先用i2cdetect確認設(shè)備地址讀 0x01 看點數(shù)是否變化坐標方向錯亂屏幕旋轉(zhuǎn)后未映射在read_cb里寫坐標變換四個角實測確認映射關(guān)系觸摸偶發(fā)失靈INT 腳配置、I2C 速率確認 INT 是否上拉I2C 速率降到 100kHz 試試按下一個點UI 里跳動很大數(shù)據(jù)高低位拼接錯誤檢查高字節(jié)是否多取了位CST328 高 4 位在低 4 位觸摸坐標變換是最需要耐心的部分。CST328 讀取到的坐標是相對觸摸傳感器的物理坐標而屏幕經(jīng)過swap_xy和mirror旋轉(zhuǎn)后物理坐標和邏輯坐標可能完全反過來。我的辦法是先做一個調(diào)試頁面把原始觸摸坐標直接顯示在屏上然后點擊屏幕四個角根據(jù)對應(yīng)關(guān)系寫出映射公式。5.3 編譯與運行常見報錯編譯階段除了前面提到的 target 報錯還有一類很常見LVGL 顯示不正常但編譯編譯和燒錄都成功串口日志又沒明顯錯誤。這種情況先看異常打印如果頻繁出現(xiàn)LVGL memory allocate failed就是LV_MEM_SIZE太小。我默認設(shè)置成 96KB大部分 320×240 界面夠用但如果控件多、圖片多要開到 128KB。開啟LV_USE_PERF_MONITOR和日志后刷新率、內(nèi)存占用都能直接在屏上看是排查 UI 性能的第一手段。如果界面切換時明顯卡頓優(yōu)先檢查是不是在動畫里做了頻繁創(chuàng)建和刪除對象。LVGL 的動態(tài)內(nèi)存碎片問題在長時間運行后會出現(xiàn)我的經(jīng)驗是盡量復(fù)用對象用lv_obj_clean清理內(nèi)容而不是反復(fù)lv_obj_create和刪除。5.4 日常調(diào)試的一條心得做這類帶屏項目我強烈建議在開發(fā)板上預(yù)留一個日志輸出串口腳平時不接屏幕也能看到 LVGL 日志和系統(tǒng)狀態(tài)。LVGL 的日志按lv_log輸出錯誤、警告、信息三級可以控制開發(fā)期全開發(fā)布版本只留錯誤。有了日志很多“看起來像硬件”的問題最后都會發(fā)現(xiàn)是軟件狀態(tài)問題。比如我調(diào)試觸摸時開始懷疑 I2C 有問題后來打日志發(fā)現(xiàn)是read_cb里狀態(tài)沒更新LVGL 一直用舊坐標不是硬件問題。6. 后續(xù)擴展思路這套 ST7789 CST328 LVGL ESP-IDF 的基礎(chǔ)例程跑通之后能延伸的方向很多。我最近在把觸摸手勢接入界面比如左右滑動切換頁面、長按彈出確認框這些都是 LVGL 內(nèi)置事件可以覆蓋的比傳統(tǒng)按鍵交互自然得多。另一個方向是用 NVS 保存界面配置和配網(wǎng)狀態(tài)加一個按鈕一鍵清除配網(wǎng)信息并重啟設(shè)備這在物聯(lián)網(wǎng)產(chǎn)品里幾乎是剛需LVGL 界面里做起來也簡單。如果界面復(fù)雜度繼續(xù)上升可以考慮用 GUI Guider 做可視化布局。它的導(dǎo)出代碼在 ESP-IDF 上基本可以直接編譯不過底層底層驅(qū)動需要自己適配正好用這套例程里的lcd_display.c和touch_cst328.c對接。最后分享一個小技巧LVGL 8.3 的lv_conf.h里的宏很多不要一下子就全開。只開需要的功能比如用到滑塊再開LV_USE_SLIDER用到中文再開字體。全開會導(dǎo)致編譯慢、內(nèi)存大對于實際交付來說沒必要。按需裁剪配置等到項目后期再統(tǒng)一評估哪些功能值得打開這才是長期維護的正常節(jié)奏。本文還有配套的精品資源點擊獲取