動(dòng)ILI9341:2.8寸TFT LCD從點(diǎn)不亮到刷屏實(shí)戰(zhàn))
簡介本資源面向使用STM32F103C8T6最小系統(tǒng)板驅(qū)動(dòng)2.8寸TFT液晶的嵌入式開發(fā)者與學(xué)習(xí)者針對48腳芯片不具備FSMC功能這一限制采用GPIO模擬方式實(shí)現(xiàn)ILI9341的16位并口顯示A端口0至15位作為數(shù)據(jù)總線適合入門液晶驅(qū)動(dòng)、練習(xí)時(shí)序控制與模塊化硬件搭建的初中級工程師參考。壓縮包共182個(gè)文件約3.16MB包含33個(gè)c源文件與37個(gè)h頭文件構(gòu)成驅(qū)動(dòng)與STM32標(biāo)準(zhǔn)外設(shè)庫主體另有41個(gè)匯編啟動(dòng)文件、22個(gè)o編譯產(chǎn)物、14個(gè)d依賴文件及uvproj、uvopt等Keil工程配置還附帶hex、axf、map等可直接燒錄與調(diào)試的產(chǎn)物目錄結(jié)構(gòu)完整便于二次開發(fā)。目前已有5092人學(xué)習(xí)下載讀者可獲取一套可運(yùn)行的ILI9341驅(qū)動(dòng)代碼與工程模板理解模擬并口刷屏的時(shí)序?qū)崿F(xiàn)并在此基礎(chǔ)上擴(kuò)展圖形顯示與觸屏功能觸屏輸入暫未實(shí)現(xiàn)。1. 一塊 2.8 寸屏點(diǎn)不亮八成不是屏的問題STM32F103C8T6 驅(qū)動(dòng) ILI9341 的 2.8 寸 TFT LCD是嵌入式入門里最經(jīng)典也最容易翻車的一組搭配。很多人拿到一塊標(biāo)著 2.8 寸、240×320 分辨率、帶 XPT2046 觸摸的模塊焊上排針、接好杜邦線、燒進(jìn)例程屏幕要么純白、要么花屏、要么干脆一點(diǎn)反應(yīng)沒有。第一反應(yīng)往往是“屏壞了”或者“買到了假貨”但實(shí)際排查下來絕大多數(shù)問題出在供電、復(fù)位時(shí)序、FSMC 配置或者初始化命令序列上屏本身反而是最不容易壞的那一環(huán)。這篇筆記面向的是手上已經(jīng)有這塊最小系統(tǒng)板和這塊屏、想把圖形刷出來的開發(fā)者。我會(huì)把從硬件連線、并口時(shí)序、ILI9341 初始化、到刷屏和局部刷新驗(yàn)證的完整路徑講清楚參數(shù)怎么設(shè)、坑在哪、失敗時(shí)先看什么都落到可復(fù)現(xiàn)的步驟上??赐昴銘?yīng)該能自己判斷是接線錯(cuò)了、時(shí)序不對還是初始化命令少發(fā)了一條。2. 先搞清楚 ILI9341 到底怎么被“驅(qū)動(dòng)”起來2.1 8080 并口和 SPI 的選型差別ILI9341 這顆驅(qū)動(dòng) IC 支持三種主機(jī)接口8080 并口、SPI 三線/四線、以及 RGB 接口。2.8 寸模塊上最常見的是 8080 并口和 SPI 兩種引出方式。選哪種直接決定你后面代碼的寫法和你 MCU 的引腳預(yù)算。8080 并口分 8 位和 16 位兩種數(shù)據(jù)寬度。16 位并口一次傳一個(gè)像素的高低位刷一屏 240×320 只需要 76800 次寫操作速度上明顯占優(yōu)適合做動(dòng)畫或頻繁刷新。代價(jià)是它要占用 16 根數(shù)據(jù)線加若干控制線對 STM32F103C8T6 這種 48 腳封裝來說引腳會(huì)非常緊張通常得靠 FSMC可變靜態(tài)存儲控制器來扛否則軟件模擬并口時(shí)序會(huì)慢到懷疑人生。SPI 接口只需要 4 根線SCK、MOSI、CS、DC再加一根 RST 和 BLK引腳壓力小得多接線也簡單。代價(jià)是速度SPI 一次只能傳一位刷一屏要傳 240×320×16 個(gè)時(shí)鐘位即使跑到 18MHz整屏刷新也在幾十毫秒量級做靜態(tài)界面夠用做流暢動(dòng)畫就吃力。我一般的判斷是如果只是顯示文字、圖標(biāo)、簡單界面SPI 足夠接線省心如果要做波形、視頻幀、頻繁局部刷新優(yōu)先上 FSMC 并口。下面兩條路徑我都會(huì)給到關(guān)鍵配置你可以按自己的板子選。2.2 供電和背光最容易被忽略的第一道坎ILI9341 模塊的邏輯供電是 3.3V但背光 LED 那一路經(jīng)常是獨(dú)立的。很多模塊把背光 LED 的限流電阻做在板上直接接 3.3V 就能亮也有模塊把背光引腳單獨(dú)引出需要你自己串電阻或者用三極管控制。如果你接的是 5V 邏輯而模塊沒有電平轉(zhuǎn)換長期跑下來邏輯口容易受損表現(xiàn)就是時(shí)好時(shí)壞。背光不亮和屏幕不顯示是兩回事。背光不亮你看到的是全黑背光亮但沒數(shù)據(jù)你看到的是白屏或者灰屏。排查時(shí)先把這兩個(gè)現(xiàn)象分開能省掉一半時(shí)間。提示上電前用萬用表量一下模塊 VCC 和 GND 之間有沒有短路再量背光引腳對地電阻確認(rèn)限流電阻存在。這一步花兩分鐘能避免燒掉整塊屏。2.3 用 SPI 跑通最小顯示接線與初始化先給一條 SPI 路徑的最小可跑通方案。以 STM32F103C8T6 的 SPI1 為例引腳分配如下信號STM32 引腳說明SCKPA5SPI1 時(shí)鐘MOSIPA7SPI1 數(shù)據(jù)輸出CSPA4片選低有效DCPA3數(shù)據(jù)/命令選擇RSTPA2復(fù)位低有效BLKPB0背光控制可接 3.3V 常亮初始化 SPI 的代碼大致如下用的是標(biāo)準(zhǔn)外設(shè)庫風(fēng)格HAL 庫把寄存器操作換成對應(yīng)函數(shù)即可// SPI1 初始化主機(jī)模式8 位數(shù)據(jù)模式 0 void LCD_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // SCK PA5 和 MOSI PA7 復(fù)用推挽 gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // CS PA4、DC PA3、RST PA2 普通推挽輸出 gpio.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_3 | GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, gpio); SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; // 只發(fā)不收 SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 模式 0 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 72M/418M SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }這段代碼里幾個(gè)參數(shù)值得說清楚。SPI_BaudRatePrescaler_4把 72MHz 的 PCLK2 分頻到 18MHz這是 ILI9341 在 3.3V 下比較穩(wěn)妥的上限再往上跑有些批次會(huì)丟數(shù)據(jù)。SPI_CPOL_Low和SPI_CPHA_1Edge組合成模式 0是 ILI9341 的默認(rèn)采樣方式如果你接的是模式 3 的屏這里要改成SPI_CPOL_High加SPI_CPHA_2Edge。SPI_Direction_1Line_Tx表示只發(fā)送不接收省掉 MISO 引腳因?yàn)閷懫粮静恍枰x回?cái)?shù)據(jù)。寫命令和寫數(shù)據(jù)靠 DC 引腳區(qū)分封裝成兩個(gè)函數(shù)// 寫一個(gè)命令字節(jié) void LCD_WriteCmd(uint8_t cmd) { LCD_DC_CLR(); // DC 拉低表示命令 LCD_CS_CLR(); SPI_WriteByte(cmd); LCD_CS_SET(); } // 寫一個(gè)數(shù)據(jù)字節(jié) void LCD_WriteData(uint8_t data) { LCD_DC_SET(); // DC 拉高表示數(shù)據(jù) LCD_CS_CLR(); SPI_WriteByte(data); LCD_CS_SET(); }SPI_WriteByte內(nèi)部就是等 TXE 標(biāo)志、寫 DR、等 BSY 清零這三步。DC 的拉高拉低必須在 CS 有效之前完成否則命令和數(shù)據(jù)會(huì)被屏當(dāng)成同一種東西表現(xiàn)就是初始化全亂。2.4 ILI9341 初始化命令序列的關(guān)鍵幾條初始化序列網(wǎng)上一搜一大把但真正決定能不能點(diǎn)亮的是前幾條。下面這段是精簡后的核心序列省略了伽馬校正等不影響點(diǎn)亮的項(xiàng)void LCD_Init(void) { LCD_RST_CLR(); Delay_ms(20); LCD_RST_SET(); Delay_ms(120); // 復(fù)位后必須等夠 120ms LCD_WriteCmd(0x01); // 軟復(fù)位 Delay_ms(120); LCD_WriteCmd(0x11); // 退出睡眠 Delay_ms(120); LCD_WriteCmd(0x36); // 內(nèi)存訪問控制 LCD_WriteData(0x48); // 橫屏BGR 順序 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x55); // 16 位 RGB565 LCD_WriteCmd(0x29); // 開顯示 }0x11退出睡眠后那 120ms 不能省很多白屏就是這里等得不夠。0x36的 0x48 決定掃描方向和 RGB/BGR 順序如果你發(fā)現(xiàn)紅色顯示成藍(lán)色改這一位就行。0x3A的 0x55 是 16 位色寫 0x66 是 18 位一般用 16 位省帶寬。0x29開顯示必須放在最后前面配置沒完成就開顯示屏?xí)i在異常狀態(tài)。3. 用 FSMC 并口把刷新速度拉起來3.1 FSMC 映射到 ILI9341 的地址規(guī)劃STM32F103C8T6 的 FSMC 支持 NOR/PSRAM 模式可以把 ILI9341 當(dāng)成一個(gè) 16 位寬的 SRAM 來訪問。關(guān)鍵是把 RS也就是 DC引腳接到某根地址線上這樣寫命令和寫數(shù)據(jù)就變成訪問兩個(gè)不同地址不用再手動(dòng)拉 DC。常見接法是把 RS 接到 FSMC_A16那么寫命令地址 基地址 0寫數(shù)據(jù)地址 基地址 (1 16) × 2因?yàn)?FSMC 是 16 位寬地址線每加 1 對應(yīng) 2 字節(jié)所以 A16 對應(yīng)偏移是 0x20000?;刂愤x0x60000000NE1 片選于是命令地址0x60000000數(shù)據(jù)地址0x60020000。#define LCD_BASE ((uint32_t)0x60000000) #define LCD_CMD_ADDR (LCD_BASE) #define LCD_DATA_ADDR (LCD_BASE 0x20000) #define LCD_WR_CMD(cmd) (*(__IO uint16_t *)LCD_CMD_ADDR (cmd)) #define LCD_WR_DATA(dat) (*(__IO uint16_t *)LCD_DATA_ADDR (dat))這樣寫屏就是一條賦值語句編譯器直接生成 FSMC 總線寫速度比軟件模擬快一個(gè)數(shù)量級。3.2 FSMC 時(shí)序參數(shù)怎么算FSMC 的讀寫時(shí)序由 BTCR 寄存器控制核心是三個(gè)參數(shù)地址建立時(shí)間 ADDSET、地址保持時(shí)間 ADDHLD、數(shù)據(jù)建立時(shí)間 DATAST。ILI9341 的 8080 時(shí)序要求是寫脈沖寬度最小 66ns讀周期最小 355ns讀 ID 時(shí)。以 72MHz 的 HCLK 為例一個(gè)時(shí)鐘周期約 13.9ns。寫時(shí)序取ADDSET 1約 14nsDATAST 5約 70ns滿足 66ns 最小寫脈沖讀時(shí)序要慢得多取ADDSET 15DATAST 60配置代碼// FSMC NOR/SRAM 模式配置NE1 片選 FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 1; timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 5; timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_CLKDivision 0; timing.FSMC_DataLatency 0; timing.FSMC_AccessMode FSMC_AccessMode_B; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM1; fsmc.FSMC_DataWidth FSMC_DataWidth_16b; fsmc.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; fsmc.FSMC_ReadWriteTimingStruct timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);FSMC_AccessMode_B表示 SRAM 模式讀寫用同一套時(shí)序。如果你要讀 ID 或者讀顯存得開FSMC_ExtendedMode_Enable給讀操作單獨(dú)配一套更慢的時(shí)序否則讀回來的數(shù)據(jù)是錯(cuò)的。3.3 刷一屏純色驗(yàn)證并口通路并口配好后用刷純色做第一次驗(yàn)證void LCD_FillColor(uint16_t color) { uint32_t i; LCD_SetWindow(0, 0, 239, 319); // 設(shè)置全屏窗口 LCD_WR_CMD(0x2C); // 寫顯存命令 for (i 0; i 240 * 320; i) { LCD_WR_DATA(color); } }0x2C是寫顯存命令發(fā)完之后連續(xù)寫數(shù)據(jù)就會(huì)自動(dòng)按窗口遞增地址。LCD_SetWindow內(nèi)部發(fā)的是0x2A列地址和0x2B頁地址各帶四個(gè)參數(shù)起始高、起始低、結(jié)束高、結(jié)束低。窗口設(shè)錯(cuò)顏色會(huì)只刷一部分或者刷到屏幕外。如果刷出來是純色但顏色不對先查0x36的 BGR 位如果是條紋或者雪花查 FSMC 時(shí)序和接線如果是全白回到初始化序列重點(diǎn)看0x11后的延時(shí)和0x29的位置。4. 避坑與排查點(diǎn)不亮?xí)r按這個(gè)順序查4.1 現(xiàn)象背光亮但屏幕全白原因通常是初始化序列沒跑完或者復(fù)位時(shí)序不對。ILI9341 上電后內(nèi)部狀態(tài)機(jī)需要完整的復(fù)位脈沖RST 低電平至少要維持 10us很多代碼只拉低一兩個(gè)指令周期就拉高屏根本沒復(fù)位。解決把 RST 拉低后延時(shí) 20ms 再拉高拉高后再等 120ms 再發(fā)命令。這個(gè)延時(shí)寧長勿短屏不會(huì)因?yàn)槟愣嗟榷鴫摹?.2 現(xiàn)象SPI 模式下花屏、顏色錯(cuò)位原因多半是 SPI 模式選錯(cuò)。ILI9341 默認(rèn)模式 0但有些模塊因?yàn)樽呔€或者上拉電阻實(shí)際工作在模式 3。表現(xiàn)是數(shù)據(jù)錯(cuò)位、顏色塊亂跳。解決把 CPOL 和 CPHA 換成模式 3 試一次。如果換完正常說明模塊的采樣邊沿和你原來的配置反了。另外檢查 SPI 分頻18MHz 跑不穩(wěn)就降到 9MHz 驗(yàn)證確認(rèn)是速度問題還是模式問題。4.3 現(xiàn)象FSMC 并口刷屏有隨機(jī)噪點(diǎn)原因是 FSMC 寫時(shí)序太快數(shù)據(jù)還沒穩(wěn)定就被屏采樣。尤其是杜邦線連接、線長超過 10cm 的時(shí)候信號完整性會(huì)明顯變差。解決把 DATAST 從 5 加到 8 或 10ADDSET 從 1 加到 2。如果噪點(diǎn)消失說明就是時(shí)序余量不夠。長期方案是縮短接線、加地線隔離或者把屏直接插在轉(zhuǎn)接板上。4.4 現(xiàn)象顯示內(nèi)容整體偏移或鏡像原因是0x36內(nèi)存訪問控制的掃描方向位設(shè)錯(cuò)。這一字節(jié)的 bit7 控制 MY、bit6 控制 MX、bit5 控制 MV、bit3 控制 BGR。橫屏和豎屏切換就是改這幾位。解決豎屏常用 0x48 或 0x08橫屏常用 0x28 或 0xE8具體取決于你的排線方向和想要的坐標(biāo)系。改完重新刷一屏看原點(diǎn)在哪個(gè)角再?zèng)Q定要不要翻轉(zhuǎn)。4.5 現(xiàn)象局部刷新時(shí)舊內(nèi)容殘留原因是沒設(shè)窗口就直接寫數(shù)據(jù)或者窗口設(shè)了但沒重新發(fā)0x2C。ILI9341 的顯存地址在每次0x2C后從窗口起點(diǎn)開始如果你連續(xù)寫兩次局部區(qū)域但中間沒重設(shè)窗口第二次會(huì)從上次結(jié)束的位置繼續(xù)。解決每次局部刷新前都完整走一遍0x2A、0x2B、0x2C不要圖省事復(fù)用窗口。這個(gè)開銷很小但能避免大部分殘影問題。5. 把刷新率再壓一壓局部刷新和 DMA 的配合前面刷一屏純色SPI 路徑大概要幾十毫秒FSMC 路徑能壓到幾毫秒。但實(shí)際界面里你不需要每次都刷全屏狀態(tài)欄、按鈕、數(shù)值這些區(qū)域是分開變的。把全屏刷新改成局部刷新是提升響應(yīng)最直接的手段。局部刷新的核心是只對變化區(qū)域設(shè)窗口。比如一個(gè) 80×24 的數(shù)值顯示區(qū)每次只刷這 1920 個(gè)像素SPI 下也就一兩毫秒。做法是把要刷的區(qū)域坐標(biāo)傳進(jìn)LCD_SetWindow然后只寫這塊區(qū)域的數(shù)據(jù)。// 只刷新 (x, y) 起、寬 w 高 h 的區(qū)域 void LCD_UpdateArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *buf) { uint32_t i, n (uint32_t)w * h; LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_WR_CMD(0x2C); for (i 0; i n; i) { LCD_WR_DATA(buf[i]); } }buf是你在內(nèi)存里維護(hù)的一塊像素緩沖界面邏輯只改這塊緩沖改完調(diào)一次LCD_UpdateArea推過去。這樣界面邏輯和刷屏解耦也方便后面接 DMA。FSMC 路徑下如果 MCU 支持 DMA 到 FSMC 的通道可以把buf直接交給 DMACPU 去處理下一幀邏輯。STM32F103C8T6 的 DMA 通道和 FSMC 的配合需要查具體通道映射不是所有通道都能觸發(fā) FSMC 寫。一個(gè)穩(wěn)妥的驗(yàn)證方法是先用 CPU 輪詢寫測出單區(qū)域耗時(shí)再換 DMA 對比看提升是否值得。最后說一個(gè)我自己的習(xí)慣每次改完初始化序列或者時(shí)序參數(shù)先刷三屏純色——紅、綠、藍(lán)再刷一屏棋盤格。純色驗(yàn)證顏色通道和窗口棋盤格驗(yàn)證地址遞增和邊界。這四屏過了再往上疊文字和圖形出問題也能快速定位是底層還是上層。這個(gè)習(xí)慣幫我省過很多次“以為是屏壞了”的返工。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取