動TFT LCD 3.5寸屏:從SPI時序到排錯全解析)
簡介這是一套基于STM32的3.5寸TFT LCD分辨率320×240驅(qū)動程序與演示例程主要面向使用GLCD庫進(jìn)行嵌入式顯示開發(fā)的工程師、學(xué)生及電子愛好者。例程基于MDK4.0及以上版本工程文件路徑明確下載調(diào)試工具為ULINK使用時將配套LCD模塊插至板子CN4接口燒錄HEX后即可看到屏幕演示畫面適合快速驗證顯示功能并深入學(xué)習(xí)GLCD初始化、繪圖與刷屏流程。資源包共219個文件大小約4.19MB包含c/h源文件、o/s編譯中間文件、uvproj工程文件、hex燒錄文件、map/sct鏈接腳本以及txt說明等目錄結(jié)構(gòu)完整便于定位與分析。目前已有2150人瀏覽學(xué)習(xí)對于剛接觸STM32液晶驅(qū)動或需要參考官方例程排查問題的開發(fā)者這是一份可直接借鑒的實用資料。 做嵌入式開發(fā)的朋友手頭最不缺的大概就是各種屏幕了。而TFT LCD 3.5寸屏絕對算得上出鏡率最高的一款尺寸不大不小分辨率從480x320到800x480都有既能做手持儀表又能做桌面小盒子的人機交互界面。不過屏幕買回來只是第一步真正讓人頭大的是驅(qū)動——尤其是你手里同時握著STM32開發(fā)板和一塊裸屏想讓它跑起來的時候。我這些年幫不少人調(diào)過這類屏也見過太多人卡在同一道坎上例程下載了引腳也對上了編譯也沒報錯但屏幕就是死活不亮。所以今天干脆把這套東西完整梳理一遍基于STM32平臺把TFT LCD 3.5寸屏的驅(qū)動思路、代碼結(jié)構(gòu)和排錯方法全講透順便把一些常規(guī)文檔里不會寫的經(jīng)驗也抖出來。1. 先說清楚驅(qū)動這塊屏到底難在哪1.1 市面上3.5寸屏的控制器差異很大很多初學(xué)者容易忽略一個關(guān)鍵事實TFT LCD 3.5寸只是一個物理尺寸規(guī)格它背后的驅(qū)動IC并不是統(tǒng)一的。同一家淘寶店賣的3.5寸屏可能這批次用的是ILI9488下批次就是ST7796再換一家可能又是HX8357D。而這些IC的初始化寄存器序列是不同的寫錯型號直接白屏或者花屏。判斷控制器型號通常有幾種途徑看商家商品詳情頁、看屏幕背面絲印、或者直接問客服。最穩(wěn)妥的辦法是買屏的時候讓商家同時提供對應(yīng)控制器的例程。如果實在拿不到就靠后面提到的初始化序列試探法來判斷。反正我自己踩坑的經(jīng)驗是手里這批320x480的屏用ILI9488驅(qū)動最多其次是ST7796兩者命令集大部分兼容但細(xì)節(jié)上有差異。1.2 為什么例程必須自己動手移植網(wǎng)上的例程千千萬但幾乎沒有一個能直接燒進(jìn)去就用。原因很簡單——引腳分配不一樣。同一個SPI接口有人把CS接PA4有人接PB12有人用硬件SPI有人用GPIO模擬SPI。再加上不同的STM32型號、不同的HAL庫版本直接拿來編譯不出錯才稀奇。所以驅(qū)動屏這件事的核心能力不是背代碼而是能看懂時序能對照數(shù)據(jù)手冊改引腳能把一段MCU型號A的代碼搬到MCU型號B上。說白了你得掌握移植的方法而不是復(fù)制粘貼的結(jié)果。這也是為什么我強烈建議你在做任何屏幕項目前先把SPI通信的底層摸一遍哪怕只是翻翻參考手冊里的時序圖后面調(diào)屏能少走一半彎路。2. 驅(qū)動方案選型與硬件連接2.1 到底用SPI還是并口3.5寸屏的分辨率如果是320x480驅(qū)動IC大多支持SPI、8位并口、16位并口甚至RGB接口。很多人上來就糾結(jié)SPI那么慢刷個全屏要半天是不是必須上并口我的建議是先別急根據(jù)應(yīng)用場景來取舍。以ILI9488為例如果是標(biāo)準(zhǔn)SPI 4線模式理論上時鐘跑到40MHz時可以勉強刷個短視頻但實際上因為SPI傳輸還要穿插命令字節(jié)、CS片選切換這些開銷全屏填充的速度也就十幾幀每秒。如果只是顯示靜態(tài)界面、儀表盤、菜單這個速度完全夠用但如果要做動畫、刷視頻、跑GUI特效SPI確實力不從心那種場景建議上FSMC或RGB接口。FSMC并口方式的優(yōu)勢在于MCU把屏幕映射成一段內(nèi)存地址往這個地址寫數(shù)據(jù)就跟往內(nèi)存寫一樣刷屏速度能快一個數(shù)量級。缺點是需要占用大量引腳板子連線也麻煩。從穩(wěn)妥和通用角度看我先以SPI接口方案為例展開這也是網(wǎng)上絕大多數(shù)例程采用的方式。2.2 引腳分配和硬件連接以最常見的STM32F103C8T6藍(lán)色Pill板為例我一般這樣分配引腳SCK時鐘→ PA5SPI1_SCKMOSI主出從入→ PA7SPI1_MOSICS片選→ PA4軟件控制DC數(shù)據(jù)/命令選擇→ PA6軟件控制RST復(fù)位→ PA3軟件控制BLK背光→ PA2軟件控制或直接接3.3V這里有個細(xì)節(jié)很多屏幕模塊的DC引腳也叫RS或A0指的都是同一根線接線前看一眼絲印確認(rèn)。另外3.5寸屏的背光LED電流不小盡量不要直接從STM32的GPIO口取電標(biāo)準(zhǔn)做法是GPIO控制一個MOS管或三極管再由外部電源給背光供電。如果你買的模塊已經(jīng)帶了背光驅(qū)動電路那BLK引腳可以直接接GPIO但判斷不了的時候先量一下模塊原理圖別硬猜。我自己供電的習(xí)慣是屏幕的VCC接5V或3.3V取決于模塊說明邏輯電平統(tǒng)一3.3V千萬別用5V去懟GPIO。STM32的引腳基本都是FT5V容忍引腳但為了穩(wěn)妥還是按3.3V邏輯來。連線這塊看著簡單恰恰是翻車重災(zāi)區(qū)。3. 核心代碼拆解從初始化到點亮一個像素3.1 STM32硬件SPI配置與底層函數(shù)如果只是點亮屏幕其實模擬SPI的代碼更通用、更好理解隨便換引腳都行。但如果追求刷新速度建議還是用硬件SPI。以STM32的HAL庫為例初始化代碼可以這樣寫SPI_HandleTypeDef hspi1; void SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }這里有幾個點需要展開講一下。時鐘極性和相位是SPI最容易出錯的地方?,F(xiàn)在常見的TFT LCD驅(qū)動IC比如ILI9488、ST7789、ST7796官方例程里用的基本都是模式0CPOL0CPHA0即空閑時鐘為低電平第一個邊沿采樣數(shù)據(jù)。如果你用的是模式1、2、3初始化屏幕大概率不出圖像或者干脆白屏。有個笨辦法就是把四種模式都試一遍總有一種能點亮但最好還是看數(shù)據(jù)手冊的時序圖確認(rèn)。波特率預(yù)分頻器也值得說道。F103的SPI1掛在APB2總線上時鐘是72MHz預(yù)分頻4就是18MHz。理論上ILI9488支持到更高的SPI時鐘但實測下來杜邦線連接、沒做阻抗匹配的狀態(tài)下超過20MHz就容易出現(xiàn)花屏或色彩錯亂。所以我建議先用預(yù)分頻8甚至16把屏幕點亮穩(wěn)定之后再試著提高頻率。跑通了功能再優(yōu)化速度這個順序永遠(yuǎn)不要顛倒。3.2 寫命令和寫數(shù)據(jù)的核心函數(shù)屏幕驅(qū)動的本質(zhì)就是兩件事發(fā)送命令、發(fā)送數(shù)據(jù)。命令和數(shù)據(jù)通過DC引腳來區(qū)分DC拉低時發(fā)送的是命令DC拉高時發(fā)送的是數(shù)據(jù)。這個函數(shù)寫好了后面所有操作都建立在這兩個函數(shù)之上void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 10); LCD_CS_HIGH(); } void LCD_WriteData(uint8_t data) { LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(hspi1, data, 1, 10); LCD_CS_HIGH(); }還有一個高頻操作是連續(xù)發(fā)送數(shù)據(jù)比如填充一片顏色區(qū)域。如果每個字節(jié)都調(diào)用一次HAL_SPI_Transmit效率會低得離譜。更好的做法是直接把整個數(shù)據(jù)緩沖區(qū)一次性發(fā)出去void LCD_WriteDataBuffer(uint8_t *buf, uint32_t len) { LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(hspi1, buf, len, 100); LCD_CS_HIGH(); }這里補充一個經(jīng)驗CS引腳在整個傳輸過程中應(yīng)該保持低電平不要在每個字節(jié)之間都拉高再拉低。很多初學(xué)者寫循環(huán)每次發(fā)送一個字節(jié)就切換一次CS屏幕雖然能點亮但速度會慢很多而且某些嚴(yán)格的屏還會出現(xiàn)兼容性問題。正確的做法是一次完整操作比如一次畫矩形填充期間CS一直拉低操作結(jié)束再拉高。3.3 初始化序列屏幕的靈魂屏幕點亮的關(guān)鍵全在初始化序列。這串寄存器配置代碼才真正體現(xiàn)“例程”的價值。網(wǎng)上流傳的初始化代碼五花八門有的能亮有的不能亮根源就在于初始化序列是否完整、順序是否正確。以ILI9488為例一小段最基本的初始化流程簡化版大致是這樣LCD_WriteCmd(0x01); // 軟件復(fù)位 HAL_Delay(120); LCD_WriteCmd(0x11); // 退出睡眠模式 HAL_Delay(120); LCD_WriteCmd(0x36); // 內(nèi)存訪問控制掃描方向 LCD_WriteData(0x48); // 根據(jù)屏幕接線方向調(diào)整 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x66); // 18位顏色RGB666 LCD_WriteCmd(0x29); // 打開顯示這里最讓人迷惑的是0x36寄存器也就是MADCTL。它控制顯示掃描方向值不同會導(dǎo)致圖像上下顛倒、左右鏡像甚至顏色分量錯位。0x00是默認(rèn)左上角到右下角0x80是行方向反轉(zhuǎn)0x40是列方向反轉(zhuǎn)0x20是行列交換。具體選哪個值完全取決于你的屏幕排線方向和安裝方向改一改這個值就能解決90%的顯示方向問題。還有一個容易踩坑的點是0x3A像素格式設(shè)置。ILI9488是RGB666的屏0x66表示18位顏色但很多人在代碼里用的是RGB565的0x55格式然后發(fā)送16位顏色數(shù)據(jù)。這樣也能顯示但色彩深度不同最高位會被截斷顯示效果偏色或者有噪點。我一般在3.5寸屏上直接寫0x66然后發(fā)送數(shù)據(jù)時每個像素用3個字節(jié)R、G、B各6位這樣色彩最準(zhǔn)確。完整的初始化序列遠(yuǎn)比上面這段復(fù)雜會包含電源設(shè)置、伽馬校正、顯示時序調(diào)整等。這些寄存器值和順序直接抄官方例程或靠譜商家提供的即可沒必要每個都去深究——但你要明白一點初始化序列不是隨便抄抄就完事它跟屏幕型號強相關(guān)換一塊屏就得換一套序列。3.4 畫點、矩形填充和顯存計算有了初始化接下來就是最基礎(chǔ)的畫點了。TFT LCD的操作思路是先設(shè)置顯示窗口即要操作的像素區(qū)域然后往窗口內(nèi)連續(xù)寫像素數(shù)據(jù)。以ILI9488為例設(shè)置窗口的代碼如下void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址設(shè)置 LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // 行地址設(shè)置 LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // 寫內(nèi)存開始 }然后填充顏色如果是RGB666的18位色每個像素是3字節(jié)void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t r, uint8_t g, uint8_t b) { uint32_t len (uint32_t)(x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); LCD_CS_LOW(); LCD_DC_HIGH(); for (uint32_t i 0; i len; i) { uint8_t color[3] {r, g, b}; HAL_SPI_Transmit(hspi1, color, 3, 10); } LCD_CS_HIGH(); }這里必須提醒一個隱蔽的問題如果一次性填充大量像素用上面這種逐像素發(fā)送3字節(jié)的方式速度極慢。刷個320x480的全屏可能要好幾秒鐘。優(yōu)化方案是構(gòu)建一個足夠大的緩沖區(qū)把顏色數(shù)據(jù)一次性塞進(jìn)去再通過DMA或大塊傳輸發(fā)送。我之前算過一筆賬320x480的分辨率如果每個像素按RGB565的2字節(jié)計算全屏數(shù)據(jù)量是320×480×2307200字節(jié)也就是300KB。而STM32F103C8T6的SRAM只有20KB根本塞不下全屏緩沖區(qū)。所以對于小內(nèi)存MCU要么分塊填充比如每次刷一行或幾行要么改用3字節(jié)發(fā)送加速優(yōu)化沒有別的捷徑。4. 實操中常見的坑與排查技巧4.1 白屏問題先查硬件再查初始化白屏是TFT LCD調(diào)試中最常見的問題幾乎每個人都會遇到一次。我按排查順序整理了一張清單現(xiàn)象可能原因排查方法白屏且屏幕無任何反應(yīng)背光沒亮或背光電路故障確認(rèn)BLK引腳電平用萬用表量背光LED供電白屏但有背光RST復(fù)位信號不對確認(rèn)RST引腳初始化后拉高且初始化序列前有足夠延時白屏但MCU程序在跑SPI模式不對確認(rèn)CPOL/CPHA是否設(shè)成模式0白屏且第一次上電偶爾壞電源供電不足3.5寸屏瞬間電流較大用示波器看VCC是否跌落按這個順序排查基本能覆蓋絕大多數(shù)白屏情況。尤其是供電問題很多人換一塊屏幕就白屏結(jié)果發(fā)現(xiàn)是電源模塊輸出電流不夠屏幕一啟動背光就把電壓拉垮了。4.2 花屏和偏色多和時序與格式有關(guān)花屏的表現(xiàn)形式很多有的是滿屏彩色噪點有的是圖像錯位重影有的是顏色明顯偏紫或偏綠。噪點類花屏八成是SPI時鐘太快或者線路干擾。杜邦線一長信號的反射和串?dāng)_就明顯了我實測過超過20cm的杜邦線在SPI時鐘18MHz時就開始出現(xiàn)間歇性花屏。解決辦法降低SPI分頻系數(shù)或者縮短導(dǎo)線最好用排線連接。如果非要高速就得考慮PCB板設(shè)計了。圖像錯位重影多半是像素格式設(shè)置和實際發(fā)送的數(shù)據(jù)寬度不匹配。比如IC是RGB666你設(shè)置0x55用16位色又用18位打包發(fā)送數(shù)據(jù)就會錯位。解決方法是統(tǒng)一格式要么寄存器選0x66每像素發(fā)3字節(jié)要么寄存器選0x55每像素發(fā)2字節(jié)然后自己轉(zhuǎn)換。顏色偏綠偏紫一般是RGB的字節(jié)順序反了。ILI9488的顯示數(shù)據(jù)寫入順序是R、G、B但有些派生的ST7796屏可能實際是B、G、R。遇到這種情況最簡單的處理辦法是修改0x36寄存器的BGR位bit 3或者直接在顏色轉(zhuǎn)換時交換R和B分量。這兩個方案我都試過改寄存器更省事不用動上層代碼。4.3 顯示方向不對一個寄存器搞定屏幕裝進(jìn)外殼后發(fā)現(xiàn)上下顛倒或者左右鏡像是特別常見的事。很多人遇到這個問題就開始改畫點函數(shù)其實完全沒必要?;氐角懊嫣岬降腗ADCTL寄存器0x36它有一個完整的映射表bit7 MYbit6 MXbit5 MV效果000默認(rèn)方向100上下翻轉(zhuǎn)010左右翻轉(zhuǎn)110旋轉(zhuǎn)180度001X/Y軸互換確定安裝方向后根據(jù)這個表格挑一個組合值就行不用改任何其它代碼。這個經(jīng)驗在處理各種屏幕安裝角度時特別管用。4.4 關(guān)于觸摸功能別和顯示驅(qū)動混在一起很多3.5寸屏模塊是帶觸摸的常見的是XPT2046電阻觸摸芯片通過SPI接口獨立通信。一個特別容易犯的錯是把觸摸芯片的片選和屏幕驅(qū)動的片選接到同一個引腳上。這兩個設(shè)備雖然都叫SPI但物理上是兩個獨立的從機片選必須分開。如果共用一個CS觸摸和顯示的數(shù)據(jù)會互相串?dāng)_屏幕閃、觸摸亂跳都是這么來的。觸摸部分的驅(qū)動思路和顯示完全不同它不需要初始化序列只要不斷讀取坐標(biāo)數(shù)據(jù)并做濾波處理。所以無論代碼結(jié)構(gòu)還是調(diào)試思路都應(yīng)該把觸摸驅(qū)動和顯示驅(qū)動分開各自獨立成文件。5. 移植例程的通用方法論5.1 拿到例程先做什么每次從網(wǎng)上Down一個LCD例程我的習(xí)慣是先不動代碼先做三個動作打開原理圖確認(rèn)引腳、打開底層驅(qū)動函數(shù)確認(rèn)SPI模式、打開初始化序列確認(rèn)屏幕控制器型號。這三個信息對齊了再談編譯和燒錄。移植的時候最省事的方法是分層。把代碼分成三層最底層是硬件抽象SPI傳輸、GPIO控制中間層是屏幕驅(qū)動的命令封裝寫命令、寫數(shù)據(jù)、設(shè)置窗口最上層才是畫點、畫線、顯示圖片、顯示文字這些應(yīng)用函數(shù)。移植到新平臺時只需要改最底層中間層和上層基本不用動。很多人移植失敗就是所有代碼揉成一團改起來牽一發(fā)動全身。5.2 如何快速判斷屏幕的控制IC型號有些商家不提供型號或者屏幕是二手拆機件完全沒資料。這時候可以靠初始化序列試探法網(wǎng)上找ILI9488、ST7796、HX8357D等幾份常見初始化代碼逐個試。不用全屏顯示只需要初始化完填充一個純色能正確顯示純色的就是對應(yīng)型號。這個方法聽起來土但非常有效。我?guī)团笥颜{(diào)試的時候就碰到過一張屏幕印著ILI9341的絲印實際內(nèi)部卻是ST7789V——老庫存屏在這個圈子里并不罕見。所以絲印只能參考點亮才是硬道理。5.3 從點亮到項目落地的一點建議最后再聊點實際的。如果你只是調(diào)試屏幕跑個例程看到彩條或者圖片就收工那其實還差得遠(yuǎn)。真正把一個屏幕驅(qū)動用到項目里至少要做到三件事第一把屏幕驅(qū)動封裝成模塊跟業(yè)務(wù)邏輯解耦第二建立一套簡單的字體顯示和圖標(biāo)顯示機制別每次都臨時畫像素第三考慮刷新機制靜態(tài)界面和動態(tài)數(shù)據(jù)用不同的刷新策略避免整屏刷新浪費CPU。我個人更推薦的做法是一開始就用LittlevGLLVGL這類開源圖形庫。LVGL底層有現(xiàn)成的顯示驅(qū)動接口把flush函數(shù)對接好上面就能跑控件、跑動畫開發(fā)效率比裸寫畫點函數(shù)高太多。但LVGL對RAM有一定要求F103C8這種只有20KB RAM的芯片跑起來很吃力至少得F407或F429起。如果是F103又不想上GUI那就老老實實裸機驅(qū)動把基礎(chǔ)畫圖和字體顯示寫扎實做幾個固定界面的項目也完全夠用。說實話TFT LCD驅(qū)動算不上什么高深技術(shù)但它是一個特別典型的“會者不難難者不會”的坑。你花一下午把它調(diào)通了后面再遇到任何SPI接口的屏幕哪怕是2.4寸、4.3寸換一套初始化序列就能秒上手。反過來如果每次都是復(fù)制粘貼改個引腳就收工下次遇到問題依然兩眼一抹黑。我把這套東西完整寫下來就是想讓你少走一點彎路。第一步先確認(rèn)控制器型號第二步核對SPI模式第三步模塊化移植第四步再談優(yōu)化——按這個順序來屏幕點亮只是時間問題。如果你在調(diào)的時候卡住了歡迎把具體現(xiàn)象和控制器型號記下來對照上面的排查表一項項過多半能找到答案。本文還有配套的精品資源點擊獲取