動(dòng)與刷新提速指南)
簡介面向嵌入式開發(fā)者的ESP8266與MicroPython聯(lián)合開發(fā)資源包圍繞ST7735驅(qū)動(dòng)芯片的TFT屏幕展開核心目標(biāo)是解決模擬SPI方式刷新過慢、直接切換硬件SPI后速度依舊不理想的問題。作者從畫點(diǎn)、鋪色、顯示字符串等基礎(chǔ)繪圖函數(shù)入手逐項(xiàng)重寫優(yōu)化底層數(shù)據(jù)寫入與傳輸方式讓屏幕刷新速度獲得明顯提升。資源中特別提醒MicroPython環(huán)境下ESP8266設(shè)有兩個(gè)硬件SPI其中一個(gè)被Flash占用無法供用戶使用因此實(shí)際可用的硬件SPI只有一個(gè)若改用軟件SPI則需注意spi.write函數(shù)必須傳入bytes類型且SPI相位和極性要依據(jù)芯片數(shù)據(jù)手冊時(shí)序圖正確配置。壓縮包為zip格式大小僅4KB共3個(gè)py文件涵蓋LCD屏幕驅(qū)動(dòng)、功能加載與主程序入口代碼精簡、結(jié)構(gòu)清晰便于移植到其他工程。目前已有535人瀏覽學(xué)習(xí)適合正在調(diào)試TFT顯示、希望提高屏幕刷新效率的嵌入式開發(fā)愛好者參考。 如果你在ESP8266上用MicroPython做過TFT屏幕顯示大概率經(jīng)歷過這樣一個(gè)場景屏幕能亮但全屏刷個(gè)圖就像在慢放一行一行往下爬肉眼能清楚看到掃描過程。稍微復(fù)雜一點(diǎn)的界面刷一次等得人心煩。這個(gè)問題多半不是代碼邏輯有毛病而是數(shù)據(jù)傳輸通路選錯(cuò)了。把單片機(jī)引腳當(dāng)成人肉SPI時(shí)鐘用Python一句一句去翻轉(zhuǎn)GPIO速度當(dāng)然上不去。想讓ESP8266配合ST7735這類彩屏跑出比較體面的刷新效果正確做法是走硬件SPI——讓芯片內(nèi)部的移位寄存器幫你去發(fā)時(shí)鐘和數(shù)據(jù)CPU只需要把數(shù)據(jù)塞給外設(shè)就行速度完全不在一個(gè)量級。這篇內(nèi)容我按自己實(shí)際調(diào)這塊屏幕的經(jīng)驗(yàn)來寫從硬件SPI和軟件SPI的差異講起把接線、固件準(zhǔn)備、驅(qū)動(dòng)文件、初始化參數(shù)和各類坑一次說完。適合剛?cè)胧諩SP8266開發(fā)、準(zhǔn)備給項(xiàng)目配一塊小巧TFT屏的朋友參考也適合已經(jīng)在用軟件SPI刷屏、覺得卡頓想換硬件SPI提速的開發(fā)者。1. 為什么非要用硬件SPI軟件GPIO刷屏是真的難受1.1 從一次全屏刷新說起先算一筆賬。ST7735常見的分辨率是128x160顏色深度一般是16bit也就是每個(gè)像素占用2字節(jié)。一屏完整數(shù)據(jù)量是128x160x2 40960字節(jié)約40KB。如果是軟件SPI每發(fā)送一個(gè)字節(jié)流程是拉低時(shí)鐘、在MOSI上設(shè)置電平、拉高時(shí)鐘、再重復(fù)8次。在MicroPython的解釋環(huán)境下每次GPIO翻轉(zhuǎn)差不多要1~3us加上循環(huán)判斷、函數(shù)調(diào)用開銷發(fā)一個(gè)字節(jié)的耗時(shí)能走到30~50us甚至更慢。按50us算40KB就是40960x50us約2秒。就算優(yōu)化一點(diǎn)也差不多要1秒以上。這個(gè)速度意味著你寫個(gè)簡單的動(dòng)畫刷新一次至少一秒界面必然肉眼可見地卡頓。硬件SPI就不一樣了。時(shí)鐘信號由外設(shè)電路產(chǎn)生MicroPython只需要把數(shù)據(jù)塊交給SPI外設(shè)它會(huì)自動(dòng)按設(shè)定的波特率同步輸出。以20MHz為例理論上發(fā)完40KB只需要16.4ms是軟件方案一個(gè)量級以上的提升。實(shí)際項(xiàng)目里因?yàn)檫€要發(fā)命令、切換DC電平、處理繪圖開銷全屏刷新在幾十毫秒級別是能達(dá)到的。我的板子上實(shí)測20MHz硬件SPI下執(zhí)行一次整屏填充大約在40~80ms跟軟件的“秒級”完全是兩種體驗(yàn)。1.2 ESP8266硬件SPI的引腳和一些認(rèn)知誤區(qū)ESP8266在MicroPython環(huán)境下的用戶可操作硬件SPI只有一個(gè)就是machine.SPI(1)引腳是固定的三根SCK在GPIO14、MOSI在GPIO13、MISO在GPIO12。和某些單片機(jī)不一樣ESP8266的硬件SPI引腳不能隨意映射想在代碼里寫“SCKGPIO5”這種操作在硬件SPI下行不通。這里有個(gè)容易誤會(huì)的點(diǎn)很多人一聽“硬件SPI”怕引腳太死板下意識(shí)還是去用軟件SPI。實(shí)際上ST7735這塊屏本身只需要SPI的寫方向讀取操作基本用不上所以MISO那個(gè)腳不接任何設(shè)備也無所謂。但MicroPython構(gòu)造硬件SPI對象時(shí)有些固件版本還是要求你指定miso引腳那就把GPIO12填進(jìn)去讓這個(gè)腳懸空或做普通IO處理即可不影響寫屏幕。還有一個(gè)誤區(qū)是覺得硬件SPI一定比軟件SPI難配置。梳理清楚了就知道兩者在MicroPython里的API差別不大硬件SPI只是額外限制了一組固定引腳換來的是幾十倍的性能提升。用屏幕這種數(shù)據(jù)吞吐量大的設(shè)備選硬件SPI是必須的而不是可選項(xiàng)。2. ST7735模塊接線與供電細(xì)節(jié)2.1 引腳對照與選腳避坑市面上的ST7735模塊雖然賣家不同但接口基本是統(tǒng)一的常見引腳有VCC、GND、CS、RESET、A0有些標(biāo)DC、SDAMOSI、SCK、LED背光。對照ESP8266接線可以這樣規(guī)劃ST7735模塊引腳ESP8266引腳說明VCC3.3V模塊供電不要接5VGNDGND共地SCKGPIO14硬件SPI時(shí)鐘SDAGPIO13硬件SPI MOSICSGPIO5片選普通GPIO即可A0/DCGPIO4數(shù)據(jù)/命令選擇普通GPIORESETGPIO16復(fù)位普通GPIO即可LED3.3V背光可常亮或GPIO控制選GPIO的時(shí)候有講究。ESP8266啟動(dòng)時(shí)會(huì)檢查幾個(gè)特殊引腳的狀態(tài)GPIO0、GPIO2上電必須為高GPIO15上電必須為低否則會(huì)進(jìn)入錯(cuò)誤的啟動(dòng)模式。如果把這些特殊引腳用來接外部模塊并且模塊在上電瞬間把電平拉反就有可能導(dǎo)致板子啟動(dòng)異常。穩(wěn)妥起見CS、DC、RST這類信號引腳盡量挑GPIO4、GPIO5、GPIO16這種沒有額外啟動(dòng)約束的腳位。我自己喜歡用GPIO5做CS、GPIO4做DC、GPIO16做RST跑了好幾個(gè)項(xiàng)目都很穩(wěn)定。MISO那根線再次強(qiáng)調(diào)SPI構(gòu)造時(shí)需要指定GPIO12但硬件上可以不連。如果你買的模塊引出了MISO留著懸空或者接到GPIO12都行代碼里只寫屏不讀屏完全無所謂。2.2 供電與干擾問題ST7735模塊上的VCC絲印雖然是5V兼容的樣子但很多小模塊板載穩(wěn)壓和電平轉(zhuǎn)換并不完善習(xí)慣上還是按3.3V供電最穩(wěn)。背光LED如果直接接3.3V模塊自帶限流電阻常亮沒問題如果想讓背光可以軟件控制就接一個(gè)GPIO但要注意GPIO驅(qū)動(dòng)能力有限最好通過一個(gè)三極管或MOS管去控制背光的開關(guān)。供電這塊經(jīng)常有人忽略卻最容易出花屏和閃屏的毛病。ESP8266在開啟WiFi后電流尖峰很猛瞬間可能沖到幾百毫安如果和TFT屏共用一顆小功率LDO很容易在WiFi收發(fā)瞬間把3.3V電壓拉低屏幕就會(huì)隨機(jī)花屏甚至重啟。我的習(xí)慣是在電源輸入端并聯(lián)一顆100uF到470uF的電解電容再在屏的VCC腳附近加一顆0.1uF陶瓷電容。別小看這兩顆電容能把很多“莫名其妙花屏”的問題擋在發(fā)生之前。3. 燒錄固件并準(zhǔn)備驅(qū)動(dòng)文件3.1 ESP8266的MicroPython固件燒錄如果板子還是出廠自帶的AT固件第一步是燒錄MicroPython。去MicroPython官網(wǎng)下載ESP8266對應(yīng)的固件bin文件注意選擇匹配你板子Flash容量的版本常規(guī)NodeMCU一般是4MB。燒錄工具用esptool先完整擦除Flash再寫入固件esptool.py --port COM3 erase_flash esptool.py --port COM3 --baud 460800 write_flash -fm dio -fs 4MB 0x0 esp8266-20220618-v1.19.1.binWindows的串口是COMxLinux下一般是/dev/ttyUSB0改成自己實(shí)際看到的端口即可。先擦除再寫入這個(gè)順序別跳如果舊固件殘留MicroPython啟動(dòng)后文件系統(tǒng)可能掛載失敗出現(xiàn)莫名其妙的重啟循環(huán)。燒錄完可以用PuTTY或Thonny連接REPL看到提示符說明固件已經(jīng)正常跑起來了。3.2 上傳ST7735驅(qū)動(dòng)和main.pyMicroPython本身沒有內(nèi)置ST7735驅(qū)動(dòng)需要把驅(qū)動(dòng)文件放到設(shè)備上。社區(qū)里常見的做法是下載一份現(xiàn)成的st7735.py放到ESP8266的文件系統(tǒng)然后在main.py里import使用。上傳工具有很多最省事的是Thonny圖形界面里直接右鍵上傳文件對新手非常友好。也可以用ampy走命令行ampy --port COM3 put st7735.py ampy --port COM3 put main.py拿到驅(qū)動(dòng)文件后別急著運(yùn)行先打開源碼掃一眼構(gòu)造函數(shù)。ST7735驅(qū)動(dòng)的開源版本很多接口細(xì)節(jié)有差異有的版本初始化時(shí)直接在驅(qū)動(dòng)內(nèi)部創(chuàng)建SPI對象有的版本要求你從外面?zhèn)饕粋€(gè)SPI實(shí)例進(jìn)來。標(biāo)題這里強(qiáng)調(diào)“使用硬件SPI”所以我們要用的是后一種方式外部創(chuàng)建machine.SPI(1)再把SPI對象傳給顯示設(shè)備類。如果下載的驅(qū)動(dòng)是內(nèi)部創(chuàng)建SPI的寫法需要微調(diào)一下把驅(qū)動(dòng)里創(chuàng)建SPI那段代碼替換為接收外部傳入的SPI參數(shù)。很多人在這一步卡住其實(shí)把源碼打開對比幾行就看明白了。4. 核心代碼與參數(shù)調(diào)優(yōu)4.1 初始化硬件SPI和顯示屏下面這段是典型的外部傳入SPI實(shí)例的寫法細(xì)節(jié)可能因驅(qū)動(dòng)版本略有不同但思路一致from machine import Pin, SPI import st7735 spi SPI(1, baudrate20000000, polarity0, phase0, bits8, firstbitSPI.MSB, sckPin(14), mosiPin(13), misoPin(12)) cs Pin(5, Pin.OUT) dc Pin(4, Pin.OUT) rst Pin(16, Pin.OUT) cs.value(1) tft st7735.ST7735(spi, cs, dc, rst, rotation0, offset(0, 0), bgrTrue, invertTrue) tft.init() tft.fill(0xFFFF)baudrate20000000就是20MHz。先別一上來就追求極限這個(gè)速率能保證大部分模塊穩(wěn)定工作。等整屏顯示正常了再試著改到40MHz如果出現(xiàn)花屏、條紋就降回去。網(wǎng)上有一種說法是ST7735數(shù)據(jù)手冊里寫的最大寫周期約15MHz20MHz已經(jīng)算超頻實(shí)際很多模塊配合杜邦線短距離連接也能跑但不要因此忽略線材質(zhì)量。杜邦線太長、接觸不良都會(huì)影響高速SPI的波形。代碼中polarity0, phase0對應(yīng)SPI Mode 0ST7735一般工作在Mode 0。firstbitSPI.MSB表示高位先出這是SPI設(shè)備的常規(guī)要求。如果你的固件不支持firstbit這個(gè)參數(shù)去掉即可不影響默認(rèn)行為。cs、dc、rst三個(gè)引腳初始化成輸出后記得把CS先拉高避免初始化前SPI總線上有其他干擾信號被屏幕誤解析成命令。rst在init()內(nèi)部會(huì)自己拉低再拉高完成復(fù)位外部不需要額外處理。4.2 幾個(gè)最影響顯示效果的參數(shù)驅(qū)動(dòng)初始化里最折磨人的三個(gè)參數(shù)是rotation、bgr、invert。這三者配置不對屏幕能亮但顯示效果千奇百怪。rotation控制旋轉(zhuǎn)方向取值范圍通常是0到3對應(yīng)0度、90度、180度、270度。改了旋轉(zhuǎn)方向后如果屏幕顯示區(qū)域偏移了還需要配合offset參數(shù)調(diào)整起點(diǎn)位置。很多模塊不是標(biāo)準(zhǔn)的128x160全屏可視區(qū)域比如有些80x160或128x128的屏安裝方向不同坐標(biāo)偏移也不同。出現(xiàn)顯示內(nèi)容不在屏幕中心的時(shí)候就是offset該派上用場的時(shí)候。bgr控制RGB還是BGR顏色順序。這個(gè)參數(shù)錯(cuò)了最典型的表現(xiàn)是紅色和藍(lán)色互換。比如你畫一個(gè)紅色矩形屏幕顯示出來是藍(lán)色。遇到這種狀況把bgr從True改成False或者反過來問題立刻解決。invert是顏色反相。有的ST7735面板需要開啟反色否則顯示效果像相片底片一樣整個(gè)世界都是反色的。這個(gè)參數(shù)沒有絕對標(biāo)準(zhǔn)跟具體面板的初始化序列有關(guān)最好的方法就是寫一個(gè)純白填充和純紅填充的測試程序多試幾次True/False選顯示正常的那組。最后還有一個(gè)隱性參數(shù)是MADCTL也就是ST7735寄存器0x36控制掃描方向、RGB/BGR順序和行列交換。rotation和bgr從根本上說都是在幫你設(shè)置這個(gè)寄存器。有些驅(qū)動(dòng)把這兩個(gè)參數(shù)封裝好了而有的驅(qū)動(dòng)需要你直接改源碼里的MADCTL值原理明白后看到相關(guān)代碼就不會(huì)懵。4.3 刷新性能優(yōu)化思路顯示大面積圖形時(shí)繪制和刷屏要配合好。ST7735控制芯片內(nèi)部自帶GRAM我們可以一次性把所有像素?cái)?shù)據(jù)推給屏幕讓控制器自己管理刷新。問題在ESP8266的內(nèi)存很緊張128x160x2的完整幀緩沖需要40KB而MicroPython環(huán)境能穩(wěn)定使用的大塊連續(xù)內(nèi)存沒那么多直接分配很容易MemoryError。實(shí)際項(xiàng)目里我一般用“行緩沖分塊刷新”的方式不需要一整個(gè)40KB的buffer先繪制一小塊區(qū)域到內(nèi)存然后立刻通過SPI刷出去再繪制下一塊。比如用bytearray(128*2)先準(zhǔn)備一行數(shù)據(jù)繪制完一行就推一行。這樣做內(nèi)存占用小效果上也幾乎看不出差別。如果是顯示靜態(tài)界面可以算好一個(gè)區(qū)域用fill_rect配合局部刷新比整屏重繪高效得多。如果確實(shí)需要整屏緩沖那么在程序啟動(dòng)早期、文件系統(tǒng)加載完但還沒開啟WiFi時(shí)分配成功率會(huì)高一些。分配之前先執(zhí)行g(shù)c.collect()整理堆內(nèi)存減少碎片。分配完就不要再頻繁創(chuàng)建大對象否則后期內(nèi)存碎片會(huì)越來越嚴(yán)重。5. 踩坑實(shí)錄與排查清單5.1 常見問題速查表把這段時(shí)間用ST7735碰到的典型問題整理成了一張表排查的時(shí)候?qū)φ罩茨苁『芏鄷r(shí)間現(xiàn)象大概率原因解決辦法全屏白屏復(fù)位時(shí)序不對、SPI速率太高、供電不足檢查RST引腳接線降低baudrate電源并電容全屏花屏、橫條紋電源干擾或SPI速度過高加強(qiáng)濾波電容逐步降低SPI速率測試顏色紅藍(lán)互換bgr參數(shù)不對切換bgr的True/False顯示像底片反色invert參數(shù)不對切換invert的True/False內(nèi)容偏移/顯示不全offset或rotation不對修改offset坐標(biāo)測試不同rotation背光不亮但屏幕有顯示背光引腳沒接對LED引腳接3.3V或確認(rèn)GPIO控制電平上電后板子反復(fù)重啟Flash固件沒擦干凈重新完整erase_flash再燒錄刷新時(shí)WiFi偶爾掉線SPI中斷與WiFi爭搶資源降低SPI速率減少整屏刷新頻率5.2 幾個(gè)容易被忽略的坑第一個(gè)坑是RST復(fù)位等待時(shí)間。有些驅(qū)動(dòng)初始化序列里Sleep Out之后只等待幾十毫秒個(gè)別ST7735面板需要更長時(shí)間才能穩(wěn)定。我的做法是在init()里的0x11命令之后主動(dòng)加一個(gè)time.sleep_ms(200)寧可多等一下也不要讓屏幕帶著半睡狀態(tài)就開始收數(shù)據(jù)能明顯減少白屏概率。第二個(gè)坑是GPIO初始狀態(tài)。CS、DC、RST在初始化SPI之前就應(yīng)該明確設(shè)置成輸出并給到正確電平。尤其CS如果不小心懸空或默認(rèn)拉低上電瞬間芯片可能把GPIO上的噪聲當(dāng)成片選信號導(dǎo)致屏幕吃進(jìn)一堆垃圾命令。養(yǎng)成習(xí)慣cs Pin(5, Pin.OUT); cs.value(1)初始化屏幕之前CS保持高需要通信時(shí)才拉低。第三個(gè)坑是SPI速率并非越高越好。我測試過40MHz下刷新速度確實(shí)更快但MicroPython在推大塊數(shù)據(jù)時(shí)會(huì)長時(shí)間占用CPU和SPI外設(shè)此時(shí)如果WiFi正在收發(fā)數(shù)據(jù)兩邊可能互相拖累表現(xiàn)為屏幕撕幀、WiFi偶發(fā)斷連。很多場景下20MHz的穩(wěn)定性比40MHz的極限速度更重要。如果你做的是傳感器數(shù)據(jù)展示這種低頻刷新場景20MHz完全夠用。第四個(gè)坑是繪圖坐標(biāo)和屏幕物理方向的組合。常見的小屏幕安裝方向五花八門有的屏接口在左邊有的在右邊有的排線朝下。rotation參數(shù)調(diào)好后務(wù)必用英文和中文混合文本、矩形框、實(shí)心圓各畫一遍確認(rèn)上下左右沒有鏡像或偏移再寫正式界面代碼。一次調(diào)好后面省心很多。我自己在實(shí)際項(xiàng)目中還習(xí)慣把顯示刷新和業(yè)務(wù)邏輯分離WiFi聯(lián)網(wǎng)、傳感器采集走一個(gè)流程屏幕刷新走另一個(gè)流程用標(biāo)志位控制避免兩者在同一時(shí)間片里搶CPU。這樣即使SPI偶發(fā)耗時(shí)較長也不會(huì)導(dǎo)致整個(gè)系統(tǒng)卡死。這個(gè)思路在ESP8266這種資源受限的板子上尤其重要屏幕只是顯示終端不能讓它拖垮主邏輯。本文還有配套的精品資源點(diǎn)擊獲取