動ILI9486 SPI屏填充矩形出現(xiàn)隨機像素的排查與解決)
最近在做一塊 STM32F767ZI 開發(fā)板的外設擴展外接 Waveshare 4.0 寸 LCD ShieldSKU13587主控是 ILI9486跑 SPI 接口。剛開始一切正常初始化能刷背景色點個像素、畫條直線都沒毛病。等我開始調(diào)用 fillRect 填充一個大矩形的時候屏幕上突然出現(xiàn)零星的隨機像素點位置每次刷新都不一樣顏色也不固定就像屏幕上撒了一把細鹽。第一反應是排線松了、屏壞了但重新壓緊排線、換了一塊屏幕后問題照舊。后來把整個顯示鏈路從硬件到驅(qū)動代碼重新過了一遍才確認這根本不是屏的問題而是 SPI 傳輸細節(jié)里幾個非常容易忽略的坑疊在了一起。這篇記錄的就是完整的排查過程和最終修法遇到類似“大區(qū)域填充出現(xiàn)隨機像素”的朋友可以直接照著排。1. 故障現(xiàn)象還原填充矩形時出現(xiàn)的“雪花點”到底長什么樣1.1 最開始的復現(xiàn)步驟我的環(huán)境是這樣的STM32CubeMX 生成工程HAL 庫SPI1 做主模式查詢方式發(fā)送F767 主頻 216MHzSPI 時鐘最初配置在 27MHz。屏幕初始化序列用的從 Waveshare 官方例程改過來的 ILI9486 初始化代碼顏色格式選 RGB565。復現(xiàn)步驟如下全屏填充黑色0x0000調(diào)用 fillRect(50, 50, 250, 250) 畫一個紅色矩形期望結果是邊緣清晰、內(nèi)部純色的矩形實際結果是矩形內(nèi)部出現(xiàn)大約三四十個隨機像素點。這些像素點有一個共同特點位置不固定。同一段代碼重復執(zhí)行每次出現(xiàn)的位置都不一樣但橫坐標和縱坐標也不會偏到矩形外面去。矩形面積越小出現(xiàn)概率越低全屏填充時數(shù)量最多肉眼很明顯。小矩形比如 16x16 的填充有時候完全看不出問題這可能也是很多人最初沒當回事的原因。1.2 為什么偏偏是“填充矩形”暴露出問題這個問題最迷惑人的地方在于畫點、畫線都正常憑什么填充矩形就出亂子關鍵在于數(shù)據(jù)量。畫一個點SPI 上只需要發(fā)送幾條命令加兩三個字節(jié)數(shù)據(jù)整段時間不到幾十微秒傳輸間隙極短就算 SPI 配置有輕微偏差也很難積累出肉眼可見的錯誤。畫線稍微長一點但每條線最多也就是幾百個點數(shù)據(jù)量依然有限。填充矩形是完全不同的場景。一個 200x200 的矩形RGB565 格式下就是 200x200x2 80000 字節(jié)如果是 320x480 全屏一次要往 SPI 灌 307200 字節(jié)。這么長的連續(xù)數(shù)據(jù)流任何一字節(jié)的解釋錯誤、任何一次時鐘采樣點偏差、任何一次片選信號異常都會被放大成可見像素錯誤。SPI 協(xié)議本身是同步串行傳輸發(fā)送端和接收端只要有一個 bit 錯位后續(xù)所有數(shù)據(jù)全部錯位直到下一次命令序列重新同步。所以當時我就意識到屏幕和初始化大概率沒壞問題出在“長數(shù)據(jù)流”的傳輸機制上也就是 SPI 外設配置、片選控制、以及底層填充函數(shù)的實現(xiàn)方式。2. 從硬件開始排除確認 SPI 接口與片選控制2.1 ILI9486 的接口模式比你想的更復雜很多人把 ILI9486 當成一塊“SPI 屏”直接用 SPI 外設去驅(qū)動。實際上 ILI9486 內(nèi)部支持多種接口模式包括 I8080 并行接口、3 線 SPI、4 線 SPI型號引腳 IM[3:0] 決定當前用哪種。Waveshare 這種 Shield 板卡硬件上通常有模式選擇電阻出廠可能是 I8080 并口模式也可能是 SPI 模式必須看這塊板子的原理圖或者絲印說明確認。我當時最初犯過一個粗心錯誤以為 Shield 插上就能用結果白屏了半小時后來發(fā)現(xiàn)是 DC 引腳的復用沒配好。ILI9486 在 4 線 SPI 模式下DCX 引腳必須由主機 GPIO 控制用來區(qū)分當前傳輸?shù)氖敲钸€是數(shù)據(jù)。如果 DCX 沒接或者被固定拉高/拉低芯片會把所有字節(jié)都當成同一種類型命令和數(shù)據(jù)全混在一起。畫點和畫線時由于命令較短偶然能“碰對”但填充矩形時命令和數(shù)據(jù)字節(jié)數(shù)量不對等必然錯亂。另外SKU13587 這類 Shield 上還帶 SD 卡槽SD 卡也是走 SPI 的。如果 SD 卡和 LCD 共用一條 SPI 總線必須各自獨立 CS。很多代碼在初始化階段會先探測 SD 卡占住 SPI 總線一段時間如果 CS 控制沒做好SD 卡的通信垃圾數(shù)據(jù)會被 LCD 誤收屏幕上也會出現(xiàn)隨機點。2.2 每根線都值得重查一遍在排查這個問題時我把接線重新整理了一遍這里給出一份參考接線表不一定適用于所有開發(fā)板但作為 STM32F767ZI SPI1 的典型接法沒問題信號STM32 引腳說明CLKPA5 / SPI1_SCK接到屏幕 SCL線盡量短MOSIPA7 / SPI1_MOSI接到屏幕 SDI/SDAMISO可不接ILI9486 的 SDO 只用于讀顯存不用可以不連CS任意 GPIO例如 PB0軟件控制后續(xù)會重點講DC任意 GPIO例如 PB1命令/數(shù)據(jù)選擇RST任意 GPIO例如 PB2復位引腳這里特別提醒一點MISO 如果沒用到不要在主機的 SPI 配置里強制開啟讀功能。STM32F767 的 SPI 外設如果是全雙工模式發(fā)送每個字節(jié)的同時都會在 MISO 上采樣。MISO 懸空時采到的電平是隨機的雖然大多不會影響輸出數(shù)據(jù)但如果你開啟了 SPI 接收中斷或者 DMA 雙緩沖這些隨機垃圾數(shù)據(jù)會被 CPU/DMA 處理反而干擾主流程。對只寫屏的場景建議直接用 SPI 發(fā)送專用配置或者把 MISO 引腳在 GPIO 初始化時配置為普通輸入并下拉。還有一個硬件層面的檢查點供電。4 寸 TFT 背光電流很容易到幾十毫安如果從開發(fā)板的 3.3V 排針直接取電當背光占空比變化或屏幕刷新大塊面積時電源紋波會增大進而干擾 SPI 電平閾值。排障時最好用穩(wěn)壓源或單獨的 LDO 給屏幕單獨供電排除電源因素。2.3 硬件片選和軟件片選隨機像素能否消失的分水嶺說到 SPI 片選很多人會直接想到 STM32 的 NSS 引腳。NSS 可以作為硬件片選自動控制但這個“自動控制”在主機模式下并不總符合預期。特別是當你使用 HAL 的HAL_SPI_Transmit時如果你把 NSS 配成了硬件模式外設會在某些幀邊界翻轉(zhuǎn) NSS或在多字節(jié)傳輸過程中產(chǎn)生額外的電平變化。對于 ILI9486 這種對命令序列完整性有要求的芯片CS 在命令中間被拉高是災難性的。記住一個結論驅(qū)動 ILI9486 這類需要連續(xù)多字節(jié)傳輸?shù)?SPI 屏時推薦使用 GPIO 軟件片選不要使用硬件 NSS。軟件片選的核心原則是一個完整事務內(nèi)CS 保持低電平不動事務結束后才拉高。比如發(fā)送“設置窗口命令 0x2A 四個參數(shù)”CS 必須在這五個字節(jié)全部發(fā)送完畢后再釋放。下面給一個參考實現(xiàn)void ili9486_write_command(uint8_t cmd) { CS_LOW(); DC_LOW(); // 命令 spi_write_byte(cmd); CS_HIGH(); } void ili9486_write_data(uint8_t data) { CS_LOW(); DC_HIGH(); // 數(shù)據(jù) spi_write_byte(data); CS_HIGH(); }看起來更細的拆分就是每個字節(jié)一個事務。這樣在畫點時沒有問題但大填充時一定要演變成行緩沖模式不要讓 CS 在像素之間反復翻轉(zhuǎn)。后面第 4 章會專門講 fillRect 的實現(xiàn)方式。3. SPI 參數(shù)與 ILI9486 時序的匹配時鐘頻率和采樣邊沿3.1 檢查 CPOL/CPHA別讓采樣邊沿卡在懸崖上SPI 協(xié)議有四種模式由時鐘極性 CPOL 和時鐘相位 CPHA 決定。ILI9486 的 4 線 SPI 通常工作在 Mode 0也就是 CPOL0、CPHA0SCK 空閑低電平數(shù)據(jù)在上升沿采樣。如果你的初始化配置成 Mode 1、Mode 2 甚至 Mode 3屏幕可能也能亮因為芯片時序容忍度有一定余量低速時尤其不明顯。但問題恰恰出在“低速時正常高速時隨機錯”上。當 SPI 時鐘拉到 20MHz 以上SCK 的建立時間和保持時間余量都很小如果 CPOL/CPHA 和芯片要求不一致采樣點可能落在數(shù)據(jù)線翻轉(zhuǎn)的附近。這時候數(shù)據(jù)線上的毛刺、走線串擾、電平上升沿不陡都會導致某一 bit 被采錯。一個 bit 采錯反映到屏幕上就是一個顏色錯誤的隨機像素點。在 CubeMX 里檢查 SPI 配置時重點確認這幾個參數(shù)Clock Polarity (CPOL)LowClock Phase (CPHA)1 EdgeData Size8 bitFirst BitMSB First如果排查過程中不確定可以先按 Mode 0 跑最低頻確認現(xiàn)象有沒有變化。如果 Mode 0 低頻穩(wěn)定再逐步升頻。3.2 分頻降頻是百試百靈的定位手段SPI 時鐘頻率不是越高越好尤其當你用杜邦線連接屏幕時。F767 的 SPI1 掛在 APB2 上PCLK2 理論最高 108MHzSPI 外設分頻最小 2 就是 54MHz。但實際上 ILI9486 的 SPI 時鐘上限通常在 20MHz 左右Waveshare 官方 Arduino 例程一般也用 15~20MHz。超過這個范圍就算協(xié)議沒錯信號完整性也會出問題。信號完整性出問題時具體表現(xiàn)就是長線傳輸下出現(xiàn)偶發(fā)錯位。我最初用 27MHz隨機像素很多降到 13.5MHz隨機像素明顯變少但仍有再配合其他修復后回到 16MHz 才徹底穩(wěn)定。這個過程說明兩點時鐘頻率是誘發(fā)因素但不是根因。給你一個實用的分頻對照表方便你在 CubeMX 里做估算。假設 PCLK2 108MHzPrescalerSPI 時鐘254 MHz427 MHz813.5 MHz166.75 MHz323.375 MHz排障時建議從最低頻率開始比如 6.75MHz確認屏幕顯示一切正常后再往上升頻。如果 6.75MHz 也有隨機像素那就基本可以排除頻率因素。3.3 數(shù)據(jù)位序和像素格式的隱性影響SPI 字節(jié)傳輸還有一個容易被忽略的選項MSB first 還是 LSB first。STM32 的 SPI 外設默認 MSB firstILI9486 的數(shù)據(jù)手冊也是按 MSB 先傳來定義的。如果不小心把 SPI 配置成了 LSB first整個數(shù)據(jù)流按位翻轉(zhuǎn)圖像會左右鏡像或顏色通道錯亂表現(xiàn)上也可能是滿屏噪點。所以要在 CubeMX 里把First Bit設為 MSB First。更關鍵的還是像素格式。ILI9486 本身是 18 位色深也就是 RGB666但 SPI 模式下可以通過 0x3A 寄存器設置輸入數(shù)據(jù)格式0x5516 位/像素RGB5650x6618 位/像素RGB6660x113 位/像素RGB111一般不用于正常顯示驅(qū)動庫和初始化寄存器必須匹配。比如初始化時把 0x3A 設成了 0x66但 fillRect 按 RGB565 每像素 2 字節(jié)發(fā)送那么從第二個像素開始數(shù)據(jù)就錯位了芯片會把 RGB 分量拆錯屏幕上的表現(xiàn)就是密密麻麻的彩色噪點有時會被誤認為“隨機像素”。我在最后檢查初始化序列時確認 0x3A 0x55才排除這個因素。4. ILI9486 初始化序列與顯存窗口故障最可能的藏身之處4.1 關鍵初始化參數(shù)怎么調(diào)ILI9486 的初始化序列可以從 Waveshare 官方代碼或任何成熟驅(qū)動庫抄但別從 ILI9341 的代碼直接改芯片型號就完事。這兩顆芯片寄存器差異很大比如 ILI9486 的顯示分辨率是 320x480而 ILI9341 是 240x320窗口邊界和像素格式的設置完全不同。我見過不少“初始化后花屏”的案例查到最后都是因為用了別的芯片的初始化序列。這里列出幾個關鍵寄存器和它們的作用方便排查時對照寄存器作用常用值0x3A像素格式設置0x55 RGB5650x66 RGB6660x36顯存訪問控制MADCTL控制掃描方向、RGB/BGR 順序0xC0電源控制 1跟隨官方初始化0xB0顯示模式/幀率等跟隨官方初始化0x11退出睡眠模式初始化最后需要0x29打開顯示初始化完成打開顯示特別注意 0x36 MADCTL。如果你的窗口設置函數(shù)和 MADCTL 的掃描方向不匹配填充矩形時內(nèi)容方向會偏但不至于出現(xiàn)隨機點。不過 RGB/BGR 位如果搞反顏色通道會錯亂在某些顏色過渡區(qū)域出現(xiàn)類似噪點的紋理。排障時可以先固定 MADCTL 為 0x00 或 0x48測試純色矩形是否正常。4.2 窗口設置命令被截斷地址指針錯亂的源頭ILI9486 的顯存寫入流程是先用 0x2A 設置列地址范圍CASET再用 0x2B 設置行地址范圍PASET最后用 0x2C 連續(xù)寫入像素數(shù)據(jù)。任何一條命令后面的參數(shù)如果沒收到完整芯片內(nèi)部的窗口地址就會指向一個錯誤位置。錯誤窗口范圍內(nèi)的數(shù)據(jù)會被寫入顯存但不一定是當前你要填充的矩形區(qū)域。這些寫到錯誤區(qū)域的數(shù)據(jù)最終會在屏幕上表現(xiàn)為顯示內(nèi)容混亂如果只是一小部分越界看起來就是零星的異常像素。窗口命令的格式如下0x2A: [x0_high, x0_low, x1_high, x1_low] // CASET 0x2B: [y0_high, y0_low, y1_high, y1_low] // PASET 0x2C: [pixel data...] // RAMWR所有參數(shù)都必須在一個 CS 低電平區(qū)間內(nèi)連續(xù)發(fā)送。如果發(fā)送中間 CS 被拉高或者 SCK 意外停擺ILI9486 只能收到部分參數(shù)后續(xù)數(shù)據(jù)就會落到錯誤地址。這也解釋了為什么“CS 拆包”會導致隨機像素窗口地址設錯了數(shù)據(jù)還在往顯存里寫但位置不對。自查方法用邏輯分析儀抓 CS 和 MOSI放大 0x2A 后面一段確認 4 個地址字節(jié)是否連續(xù)、CS 是否一直維持低電平。一旦發(fā)現(xiàn)高脈沖插在參數(shù)中間直接定位。4.3 fillRect 實現(xiàn)方式對比逐像素發(fā)送是原罪接下來看 fillRect 本身。很多從畫點函數(shù)擴展來的矩形填充代碼長這樣void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { set_window(x0, y0, x0 w - 1, y0 h - 1); for (uint32_t i 0; i w * h; i) { CS_LOW(); DC_HIGH(); spi_write_byte(color 8); spi_write_byte(color 0xFF); CS_HIGH(); } }這代碼從功能上沒錯畫小矩形時看起來也正常。但每次只寫一個像素就把 CS 拉高意味著每兩個像素之間 CS 都會產(chǎn)生一個高脈沖。對于 ILI9486 來說每個高脈沖都意味著當前 SPI 事務結束芯片狀態(tài)機回到空閑或命令接收狀態(tài)。當這個操作發(fā)生在 RAMWR 數(shù)據(jù)流中間時芯片內(nèi)部可能會認為數(shù)據(jù)寫入結束或者顯存地址指針被重置。最終寫進去的數(shù)據(jù)不連續(xù)顯存里就出現(xiàn)了空洞。正確做法是把窗口設置和像素數(shù)據(jù)分成兩個大事務窗口設置一次性發(fā)完像素數(shù)據(jù)按行緩沖連續(xù)發(fā)送。推薦這樣實現(xiàn)static uint8_t line_buffer[480 * 2]; // 行緩沖按屏幕寬度算 void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint32_t row_bytes w * 2; // 填滿行緩沖 for (uint16_t i 0; i w; i) { line_buffer[i * 2] color 8; line_buffer[i * 2 1] color 0xFF; } set_window(x0, y0, x0 w - 1, y0 h - 1); CS_LOW(); DC_HIGH(); // 數(shù)據(jù)模式 for (uint16_t row 0; row h; row) { spi_write_buffer(line_buffer, row_bytes); } CS_HIGH(); }這個版本里