動實戰(zhàn):FSMC配置與60fps刷新優(yōu)化)
1. 為什么我要折騰ST7789V的8080并口模式市面上關于ST7789V的教程十篇里有八篇講的是SPI驅(qū)動剩下兩篇講的是RGB接口。8080并口也就是Intel 8080時序的MCU接口的完整教程少得可憐能跑通的初始化代碼更是鳳毛麟角。我最近做一個工業(yè)手持終端的項目主控用的是STM32F103RCT6屏幕選了一塊1.3寸240x240的ST7789V模組客戶要求刷新率必須穩(wěn)在60fps以上SPI那點帶寬根本喂不飽只能上8080并口。這個選擇不是拍腦袋決定的。SPI模式下哪怕你把時鐘拉到STM32F103的極限36MHz算上命令開銷和像素填充的間隙實際有效像素吞吐也就2MB/s出頭。240x240x16bit一幀就是115KB理論極限也就17fps左右實際跑下來12fps都費勁。而8080并口用8位數(shù)據(jù)總線配合STM32的FSMC靈活靜態(tài)存儲控制器寫一個像素只需要一個總線周期72MHz的HCLK下理論吞吐能到72MB/s實際穩(wěn)定在30MB/s以上毫無壓力60fps輕松拿捏。所以這篇東西是寫給那些被SPI刷新率折磨過、想上并口又找不到完整參考的兄弟。我會把ST7789V的8080時序、FSMC配置、初始化代碼、踩過的坑全部攤開講。代碼基于STM32 HAL庫但時序原理是通用的你換GD32、換國產(chǎn)M4核芯片邏輯一模一樣。2. 8080并口和SPI到底差在哪為什么快這么多2.1 從物理連線看本質(zhì)區(qū)別SPI是串行一根數(shù)據(jù)線一根時鐘線一個時鐘周期傳1個bit。8080并口是并行8根數(shù)據(jù)線D0-D7加一根寫信號WR、一根讀信號RD、一根數(shù)據(jù)/命令選擇線DC一個寫周期傳8個bit。同樣是72MHz的總線時鐘SPI一個周期傳1bit8080一個周期傳8bit理論帶寬就是8倍差距。但實際差距比8倍還大。SPI每傳一個字節(jié)你得手動拉低片選、發(fā)數(shù)據(jù)、等標志位、拉高片選軟件開銷巨大。8080并口配合FSMC硬件控制器你只需要往一個固定地址寫數(shù)據(jù)硬件自動完成時序生成CPU該干嘛干嘛DMA還能接管效率完全不是一個量級。2.2 ST7789V的8080時序長什么樣ST7789V支持8080的18位、16位、9位、8位總線模式我們最常用的是8位模式。一次完整的寫操作分兩步先發(fā)命令再發(fā)數(shù)據(jù)。命令和數(shù)據(jù)的區(qū)分靠DC線——DC拉低是命令DC拉高是數(shù)據(jù)。寫一個字節(jié)的時序是這樣的CS拉低如果用了片選DC設置好電平數(shù)據(jù)放到D0-D7上WR拉低再拉高一個上升沿完成采樣。讀操作類似用RD信號。ST7789V的寫周期最小時間要求是66ns也就是說WR的高低電平各維持33ns以上就行72MHz的FSMC完全滿足。這里有個關鍵點ST7789V的8080接口不需要讀操作也能工作因為顯示RAM的讀寫你基本用不到讀。所以RD線可以懸空不接省一根線。但如果你想讀屏幕ID或者讀顯存做截圖那就得接上。2.3 FSMC是怎么把并口時序硬件化的STM32的FSMC可以把外部存儲器映射到內(nèi)部地址空間。你把ST7789V當成一個SRAM來對待配置好時序參數(shù)然后往特定地址寫數(shù)據(jù)FSMC自動生成CS、WR、DC這些信號。具體來說我們把A0地址線接到ST7789V的DC引腳上那么訪問地址0x60000000時A0為0命令訪問0x60000002時A0為1數(shù)據(jù)。這樣代碼里寫*(volatile uint16_t*)0x60000000 cmd就是發(fā)命令寫*(volatile uint16_t*)0x60000002 data就是發(fā)數(shù)據(jù)干凈利落。FSMC的時序參數(shù)需要根據(jù)ST7789V的數(shù)據(jù)手冊來算。ADDSET地址建立時間、DATAST數(shù)據(jù)保持時間、ADDHOLD地址保持時間這幾個參數(shù)我后面會給出具體計算過程。3. 硬件連線與FSMC配置的完整方案3.1 引腳分配表先上接線表這塊搞錯了后面全白搭。我用的是STM32F103RCT6的FSMC Bank1 NOR/PSRAM1區(qū)域?qū)狽E1片選。ST7789V引腳STM32引腳功能說明D0-D7PD14,PD15,PD0,PD1,PE7,PE8,PE9,PE10FSMC數(shù)據(jù)總線D0-D7WRPD5FSMC寫使能RDPD4FSMC讀使能可不接DCPD11地址線A16區(qū)分命令/數(shù)據(jù)CSPD7FSMC片選NE1RESETPC6普通GPIO復位引腳BLKPC7背光控制PWM可調(diào)這里有個坑FSMC的數(shù)據(jù)線不是隨便選的必須用FSMC對應的引腳。STM32F103的FSMC_D0-D7分布在PD和PE口上具體哪個引腳對應哪根數(shù)據(jù)線得查芯片的復用功能表。我上面列的是F103RCT6的對應關系換芯片一定要重新查。3.2 FSMC時序參數(shù)計算ST7789V的數(shù)據(jù)手冊里8080接口的寫時序要求如下地址建立時間tAST最小0ns地址保持時間tAHT最小0ns寫脈沖寬度tWP最小66ns寫周期tWC最小66ns??雌饋硪蠛艿偷獸SMC的時序參數(shù)是以HCLK周期為單位的得換算。假設HCLK72MHz一個周期約13.9ns。FSMC寫時序的公式是ADDSET地址建立時間 tAST / HCLK周期 - 1DATAST數(shù)據(jù)保持時間 tWP / HCLK周期 - 1tAST最小0ns那ADDSET取0就行。tWP最小66ns66/13.9≈4.75取5DATAST5-14。但實際調(diào)試時我發(fā)現(xiàn)取ADDSET1、DATAST5更穩(wěn)因為PCB走線有延遲留點余量沒壞處。最終配置ADDSET1DATAST5ADDHOLD0CLKDivision1。這套參數(shù)在72MHz下實測穩(wěn)定示波器抓WR信號高電平約70ns低電平約70ns完全滿足ST7789V的要求。3.3 FSMC初始化代碼static void MX_FSMC_Init(void) { FSMC_NORSRAM_TimingTypeDef Timing {0}; FSMC_NORSRAM_InitTypeDef Init {0}; /* 時序配置 */ Timing.AddressSetupTime 1; Timing.AddressHoldTime 0; Timing.DataSetupTime 5; Timing.BusTurnAroundDuration 0; Timing.CLKDivision 1; Timing.DataLatency 0; Timing.AccessMode FSMC_ACCESS_MODE_A; /* 初始化結(jié)構(gòu)體 */ Init.NSBank FSMC_NORSRAM_BANK1; Init.DataAddressMux FSMC_DATA_ADDRESS_MUX_DISABLE; Init.MemoryType FSMC_MEMORY_TYPE_SRAM; Init.MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_8; Init.BurstAccessMode FSMC_BURST_ACCESS_MODE_DISABLE; Init.WaitSignalPolarity FSMC_WAIT_SIGNAL_POLARITY_LOW; Init.WrapMode FSMC_WRAP_MODE_DISABLE; Init.WaitSignalActive FSMC_WAIT_TIMING_BEFORE_WS; Init.WriteOperation FSMC_WRITE_OPERATION_ENABLE; Init.WaitSignal FSMC_WAIT_SIGNAL_DISABLE; Init.ExtendedMode FSMC_EXTENDED_MODE_DISABLE; Init.AsynchronousWait FSMC_ASYNCHRONOUS_WAIT_DISABLE; Init.WriteBurst FSMC_WRITE_BURST_DISABLE; HAL_FSMC_NORSRAM_Init(FSMC_NORSRAM_DEVICE, Init); HAL_FSMC_NORSRAM_Timing_Init(FSMC_NORSRAM_DEVICE, Timing, FSMC_NORSRAM_BANK1); HAL_FSMC_NORSRAM_Extended_Timing_Init(FSMC_NORSRAM_EXTENDED_DEVICE, Timing, FSMC_NORSRAM_BANK1, FSMC_EXTENDED_MODE_DISABLE); __HAL_RCC_FSMC_CLK_ENABLE(); }這段代碼里MemoryDataWidth設成8位因為ST7789V的8080接口是8位數(shù)據(jù)總線。如果你用的是16位模式改成FSMC_NORSRAM_MEM_BUS_WIDTH_16但ST7789V的16位模式需要額外配置一般用8位就夠了。4. ST7789V初始化代碼逐行拆解4.1 復位與基礎命令序列初始化代碼不是隨便抄一份就行的每個命令背后都有原因。我按順序講。#define ST7789_CMD 0x60000000 #define ST7789_DATA 0x60000002 static void ST7789_WriteCmd(uint8_t cmd) { *(volatile uint8_t*)ST7789_CMD cmd; } static void ST7789_WriteData(uint8_t data) { *(volatile uint8_t*)ST7789_DATA data; } static void ST7789_WriteData16(uint16_t data) { *(volatile uint8_t*)ST7789_DATA data 8; *(volatile uint8_t*)ST7789_DATA data 0xFF; }注意這里用的是volatile uint8_t*因為FSMC的地址是8位寬寫16位數(shù)據(jù)得分兩次。如果你配置成16位總線可以直接寫uint16_t但ST7789V的16位模式需要設置IM[3:0]引腳硬件上得改不推薦。復位序列static void ST7789_Reset(void) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(120); }復位拉低至少10us我給了10ms拉高后等120ms讓內(nèi)部穩(wěn)壓器穩(wěn)定。這個120ms是ST7789V數(shù)據(jù)手冊要求的不能省。4.2 關鍵初始化命令詳解static void ST7789_Init(void) { ST7789_Reset(); /* 1. 軟件復位 */ ST7789_WriteCmd(0x01); HAL_Delay(120); /* 2. 睡眠退出 */ ST7789_WriteCmd(0x11); HAL_Delay(120); /* 3. 像素格式16位RGB565 */ ST7789_WriteCmd(0x3A); ST7789_WriteData(0x55); /* 4. 內(nèi)存數(shù)據(jù)訪問控制 */ ST7789_WriteCmd(0x36); ST7789_WriteData(0x00); /* 5. porch設置 */ ST7789_WriteCmd(0xB2); ST7789_WriteData(0x0C); ST7789_WriteData(0x0C); ST7789_WriteData(0x00); ST7789_WriteData(0x33); ST7789_WriteData(0x33); /* 6. 柵極控制 */ ST7789_WriteCmd(0xB7); ST7789_WriteData(0x35); /* 7. VCOM設置 */ ST7789_WriteCmd(0xBB); ST7789_WriteData(0x19); /* 8. LCM控制 */ ST7789_WriteCmd(0xC0); ST7789_WriteData(0x2C); /* 9. VDV和VRH使能 */ ST7789_WriteCmd(0xC2); ST7789_WriteData(0x01); /* 10. VRH設置 */ ST7789_WriteCmd(0xC3); ST7789_WriteData(0x12); /* 11. VDV設置 */ ST7789_WriteCmd(0xC4); ST7789_WriteData(0x20); /* 12. 幀率控制60Hz */ ST7789_WriteCmd(0xC6); ST7789_WriteData(0x0F); /* 13. 電源控制 */ ST7789_WriteCmd(0xD0); ST7789_WriteData(0xA4); ST7789_WriteData(0xA1); /* 14. 正電壓伽馬 */ ST7789_WriteCmd(0xE0); ST7789_WriteData(0xD0); ST7789_WriteData(0x04); ST7789_WriteData(0x0D); ST7789_WriteData(0x11); ST7789_WriteData(0x13); ST7789_WriteData(0x2B); ST7789_WriteData(0x3F); ST7789_WriteData(0x54); ST7789_WriteData(0x4C); ST7789_WriteData(0x18); ST7789_WriteData(0x0D); ST7789_WriteData(0x0B); ST7789_WriteData(0x1F); ST7789_WriteData(0x23); /* 15. 負電壓伽馬 */ ST7789_WriteCmd(0xE1); ST7789_WriteData(0xD0); ST7789_WriteData(0x04); ST7789_WriteData(0x0C); ST7789_WriteData(0x11); ST7789_WriteData(0x13); ST7789_WriteData(0x2C); ST7789_WriteData(0x3F); ST7789_WriteData(0x44); ST7789_WriteData(0x51); ST7789_WriteData(0x2F); ST7789_WriteData(0x1F); ST7789_WriteData(0x1F); ST7789_WriteData(0x20); ST7789_WriteData(0x23); /* 16. 顯示反轉(zhuǎn)開啟 */ ST7789_WriteCmd(0x21); /* 17. 顯示開啟 */ ST7789_WriteCmd(0x29); }逐條解釋幾個關鍵命令0x3A像素格式0x55表示16位RGB565。這個必須設對設錯了顏色會花。RGB565是5位紅、6位綠、5位藍一個像素兩個字節(jié)。0x36內(nèi)存訪問控制這個命令控制顯示方向。0x00是正常方向0x60是橫屏旋轉(zhuǎn)0xC0是180度旋轉(zhuǎn)0xA0是270度旋轉(zhuǎn)。具體值取決于你的屏幕安裝方向。我一開始設成0x00結(jié)果屏幕內(nèi)容是倒的改成0xC0才對。0xB2Porch設置這個命令設置前后沿影響顯示區(qū)域的起始位置。0x0C 0x0C 0x00 0x33 0x33是常見配置但不同廠家的屏幕模組可能不一樣。如果顯示區(qū)域偏移了調(diào)這個。0xC6幀率控制0x0F對應60Hz左右。這個值越小幀率越高但太高了屏幕會閃。我試過0x01幀率上去了但畫面有輕微抖動0x0F最穩(wěn)。0xE0和0xE1伽馬校正這兩組14個字節(jié)的伽馬值決定了色彩表現(xiàn)。不同批次的屏幕伽馬值可能不同如果顏色偏得厲害可以找廠家要對應的伽馬表。我用的這組是通用值實測色彩還算正。4.3 顯示窗口設置與像素填充初始化完了還得設置顯示窗口不然你不知道往哪寫數(shù)據(jù)。static void ST7789_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ST7789_WriteCmd(0x2A); /* 列地址設置 */ ST7789_WriteData16(x0); ST7789_WriteData16(x1); ST7789_WriteCmd(0x2B); /* 行地址設置 */ ST7789_WriteData16(y0); ST7789_WriteData16(y1); ST7789_WriteCmd(0x2C); /* 寫顯存 */ }設置完窗口后連續(xù)寫數(shù)據(jù)就會自動填充。240x240的屏幕全屏填充就是57600個像素每個像素兩個字節(jié)總共115200次寫操作。用FSMC的話每次寫就是一個總線周期72MHz下大約1.6ms就能填滿一屏60fps綽綽有余。如果要更快可以用DMA。FSMC支持DMA請求配置好DMA通道后CPU只需要設置好源地址和長度DMA自動把數(shù)據(jù)搬到FSMC的數(shù)據(jù)地址。我實測DMA填充全屏只要0.8msCPU占用幾乎為零。5. 實際調(diào)試中踩過的坑和排查方法5.1 屏幕完全不亮這是最常見的問題原因通常有三個復位沒做好、背光沒開、FSMC時序不對。先查復位。用萬用表量RESET引腳上電后應該是高電平。如果一直是低檢查GPIO配置。我遇到過因為GPIO時鐘沒使能導致復位引腳一直拉低的情況查了半天才發(fā)現(xiàn)是__HAL_RCC_GPIOC_CLK_ENABLE()忘了加。再查背光。BLK引腳如果是高電平有效直接給3.3V。如果是PWM調(diào)光確認PWM輸出正常。我有個項目BLK接的是低電平有效結(jié)果一直給高電平屏幕黑得跟關機一樣。最后查FSMC時序。用示波器抓WR引腳看有沒有波形。如果沒有檢查FSMC時鐘使能了沒、片選配置對不對。如果有波形但屏幕還是不亮把DATAST調(diào)大試試可能是時序太快了。5.2 屏幕亮但花屏花屏一般是數(shù)據(jù)線接錯了或者像素格式不對。先確認D0-D7的接線順序FSMC的數(shù)據(jù)線順序是固定的不能隨便換。我試過把D0和D1接反結(jié)果顏色完全亂套。再確認0x3A命令的值。RGB565是0x55RGB666是0x66設錯了顏色會偏。還有0x36命令的方向設置設錯了畫面會旋轉(zhuǎn)或鏡像。如果花屏是規(guī)律性的條紋那可能是FSMC的ADDSET或DATAST太小數(shù)據(jù)沒穩(wěn)住就被采樣了。把DATAST從4調(diào)到6試試一般能解決。5.3 刷新率上不去SPI模式下刷新率低是正常的但8080并口還低就不應該了。先確認FSMC的時鐘源HCLK是不是72MHz。如果系統(tǒng)時鐘配錯了FSMC也跟著慢。再確認是不是用了DMA。不用DMA的話CPU每次寫數(shù)據(jù)都要等FSMC的等待周期效率會打折。用DMA后CPU只需要配置一次剩下的交給硬件。還有一個容易忽略的點FSMC的寫操作模式。AccessMode設成FSMC_ACCESS_MODE_A是SRAM模式速度最快。如果設成B模式會多一個等待周期速度慢一截。5.4 常見問題速查表現(xiàn)象可能原因排查方法解決方案完全不亮復位異常量RESET引腳電平檢查GPIO時鐘和配置完全不亮背光未開量BLK引腳給正確電平或PWM完全不亮FSMC無輸出示波器抓WR檢查FSMC時鐘和片選花屏數(shù)據(jù)線接錯核對D0-D7順序按FSMC引腳表重接花屏像素格式錯檢查0x3A值RGB565用0x55花屏時序太快調(diào)大DATAST從4調(diào)到6或8顏色偏伽馬值不對對比廠家伽馬表替換0xE0/0xE1數(shù)據(jù)刷新率低未用DMA檢查DMA配置配置FSMC DMA請求刷新率低時鐘源錯量HCLK頻率重新配置系統(tǒng)時鐘顯示偏移Porch設置錯檢查0xB2值按屏幕模組調(diào)整6. 性能優(yōu)化與進階玩法6.1 DMA雙緩沖實現(xiàn)無撕裂刷新單緩沖DMA刷新有個問題DMA正在寫顯存的時候如果你更新了緩沖區(qū)數(shù)據(jù)畫面上會出現(xiàn)撕裂。解決辦法是雙緩沖——準備兩個緩沖區(qū)DMA寫A的時候CPU填BDMA寫B(tài)的時候CPU填A交替進行。實現(xiàn)上用DMA的傳輸完成中斷來切換緩沖區(qū)。中斷里重新配置DMA的源地址指向另一個緩沖區(qū)然后啟動下一次傳輸。這樣CPU和DMA并行工作刷新率能再提升30%左右。6.2 局部刷新減少數(shù)據(jù)量全屏刷新115KB數(shù)據(jù)如果只改了一小塊區(qū)域沒必要全刷。ST7789V支持設置局部窗口只刷新變化區(qū)域。比如一個時鐘界面只有秒數(shù)在變那就只刷新秒數(shù)所在的矩形區(qū)域數(shù)據(jù)量從115KB降到幾KBCPU占用大幅下降。局部刷新的關鍵是維護一個臟矩形列表。每次繪圖操作時把受影響的區(qū)域合并到臟矩形里刷新時只刷臟矩形。這個邏輯在GUI庫里有現(xiàn)成實現(xiàn)自己寫也不復雜。6.3 硬件旋轉(zhuǎn)與鏡像的取舍ST7789V的0x36命令支持硬件旋轉(zhuǎn)但硬件旋轉(zhuǎn)有個限制旋轉(zhuǎn)后顯存的寫入順序會變DMA配置得跟著改。如果項目里需要動態(tài)旋轉(zhuǎn)屏幕建議用軟件旋轉(zhuǎn)——在緩沖區(qū)里把像素坐標變換好再寫雖然多花點CPU但邏輯簡單不容易出錯。硬件鏡像也是類似0x36的bit7控制水平鏡像bit6控制垂直鏡像。鏡像后文字會反所以一般只在特殊場景用。6.4 背光PWM調(diào)光的頻率選擇背光用PWM調(diào)光時頻率不能太低否則人眼能感覺到閃爍。一般建議1kHz以上。但頻率太高了MOS管的開關損耗會增加。我一般用2kHz占空比0-100%可調(diào)實測無閃爍MOS管也不燙。PWM的分辨率也要考慮。如果PWM時鐘是72MHz分頻后2kHz那ARR寄存器可以設到36000分辨率足夠細膩。如果ARR太小低亮度時會出現(xiàn)跳變。7. 移植到其他平臺要注意什么這套代碼雖然基于STM32 HAL庫但核心邏輯是通用的。移植到GD32的話FSMC的寄存器操作略有不同但時序參數(shù)的計算方法一樣。移植到國產(chǎn)M4核芯片如果它有類似的靜態(tài)存儲控制器配置思路完全一致。關鍵是要理解8080并口的本質(zhì)它就是一組并行數(shù)據(jù)線加幾個控制信號時序要求寫在ST7789V的數(shù)據(jù)手冊里。不管用什么芯片只要能把時序生成出來屏幕就能亮。我試過用GPIO模擬8080時序在STM32F103上跑刷新率只有5fps因為GPIO翻轉(zhuǎn)太慢了。所以能用硬件控制器就用硬件控制器別跟自己過不去。最后分享一個調(diào)試技巧如果屏幕不亮又找不到原因先寫一個最簡單的測試——只發(fā)0x11睡眠退出和0x29顯示開啟然后全屏填紅色。如果紅色能出來說明基礎時序沒問題再逐步加命令。如果紅色出不來那就是硬件連接或FSMC配置的問題跟初始化命令無關。這個二分法能幫你快速定位問題范圍。