3.2 Grafické funkcie ST7789
Princíp práce knižnice st7789
Na rozdiel od niektorých iných typov displeja, kreslenie na farebný LCD displej s čipom ST7789 cez náš objekt sa realizuje priamo do jeho obrazovej pamäte a nie cez frame buffer (virtuálny displej) v operačnej pamäti MCU.
Hlavným dôvodom je obmedzené množstvo operačnej pamäte - klasický ESP32, ktorý je aj v pôvodnom M5StickC Plus, má k dispozícii len 520 KiB pamäte, no väčšinu z nej používa interpret MicroPython a pre program zostáva len približne 140 KiB. Displej s rozlíšením 135 × 240 px má 32 400 px, čo pri 16-bitových farbách predstavuje 63 KiB obrazových dát.
Funkcie pre kreslenie
Objekt ST7789 teda nevyužíva štandardné „frame buffer“ funkcie pre kreslenie, ale poskytuje vlastné:
- fill({farba}) - vyplní celú plochu zadanou farbou;
- pixel({x}, {y}, {farba}) - nastaví pixel na zadanú farbu;
- hline({x}, {y}, {dĺžka}, {farba}) - vodorovná (horizontálna) čiara z bodu;
- vline({x}, {y}, {dĺžka}, {farba}) - zvislá (vertikálna) čiara z bodu;
- line({x1}, {y1}, {x2}, {y2}, {farba}) - čiara z bodu 1 do bodu 2, pomalšie ako hline a vline;
- rect / fill_rect({x}, {y}, {šírka}, {výška}, {farba}) - obdĺžnik na danej pozícii s danými rozmermi;
- circle / fill_circle({x}, {y}, {polomer}, {farba}) - kružnica / kruh so stredom na danej pozícii s daným polomerom;
- polygon / fill_polygon(({body}), {ref_x}, {ref_y}, {farba}) - uzavretý polygón (n-uholník):
- jeho body sú v uvedenej postupnosti 2-prvkových tuplí ((x1, y1), (x2, y2), (x3, y3), …),
- súradnice týchto bodov sú relatívne voči referenčnému bodu,
- je možné pridať ešte trojicu parametrov {otoč_uhol}, {otoč_x}, {otoč_y} - vtedy bude polygón otočený zadaným uhlom (v radiánoch) okolo zadaného bodu otočenia (ten je relatívny voči referenčnému bodu).
Bod (0, 0) je v ľavom hornom rohu. Rešpektuje orientáciu displeja.
Prečo je funkcia line() pomalšia ako hline() a vline()?
Čiary sa v skutočnosti musia kresliť po jednotlivých bodoch. Kreslenie vodorovnej a zvislej čiary je jednoduché - stačí obyčajný cyklus, ktorý jednu zo súradníc vždy len zvýši o 1. No univerzálne kreslenie čiary z bodu A do bodu B vyžaduje trochu zložitejšie výpočty a tie spôsobia spomalenie.
Nemohla by funkcia line() sama zistiť, že čiara je vodorovná alebo zvislá a použiť jednoduchší algoritmus?
Mohla by a aj to takto robí. Reálny rozdiel rýchlosti je preto malý - spôsobuje ho len úvodné vetvenie. Pokiaľ je však čiara šikmá, kreslenie je výrazne pomalšie.
Definovanie farieb
Všetky uvedené funkcie očakávajú farbu vo forme 16-bitového čísla (RGB v režime 5/6/5 bitov). Pre zjednodušenie zadávania farby je v knižnici st7789 k dispozícii pomocná funkcia:
- color565({r}, {g}, {b}) - jednotlivé zložky farby sú v rozsahu bajtu (0 až 255).
Tip: Pre pohodlnú voľbu farby môžeme využiť web HTML Color Codes.
Vedeli by sme si takúto funkciu napísať aj sami? Dostala by tri 8-bitové čísla a vrátila by jedno správne 16-bitové.
Odpoveď s riešením
Samozrejme, toto je pre programátora jednoriadková „brnkačka“.
- R a B zložku potrebujeme zredukovať o 3 bity (z 8 na 3), G zložku len o 2 bity (z 8 na 6), v čom nám pomôže bitový posun vpravo.
- Potom každú zložku posunieme tam, kam patrí: R pôjde o 11 (5 + 6) bitov vľavo, G o 5 bitov vľavo a B zostane.
- Výsledky potom stačí bitovo sčítať (bitový OR) a je to.
def color565(r, g, b): return r>>3 << 11 | g>>2 << 5 | b>>3
Jednoduché ukážky
Vyššie uvedené funkcie nám poskytujú dobrý základ pre kreslenie rôznych útvarov a scén. Nečakajme však extrémnu rýchlosť, MCU nie je GPU. 🙂
Ukážka funkcie fill()
Môžeme skúsiť na M5Stick postupne „rozsvietiť“ displej na žlto (prechádzať z čiernej do žltej po malých krokoch) a potom striedať zobrazovanie farieb s výpisom FPS:
import M5Stick
from machine import Signal, Pin
from time import sleep, ticks_us, ticks_diff
from st7789 import color565
print(M5Stick.spi)
displej = M5Stick.lcd
displej.rotation(0)
displej.init()
# nový žltý M5StickC Plus2 má priame podsvietenie, init() ho aj zapne
# starý oranžový M5StickC Plus má podsvietenie cez PMU, musíme zapnúť
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_on()
# trochu bitovej aritmetiky - plynule rozsvieti do žltej
for c in range(32):
displej.fill(c << 6 | c << 11)
tlačidlo = Signal(37, Pin.IN, invert = True)
farby = (color565(64, 64, 255), color565(192, 192, 0))
# jednoduchý benchmark - strieda farby cez celý displej
while not tlačidlo():
t1 = ticks_us()
for f in farby:
displej.fill(f)
t = ticks_diff(ticks_us(), t1)
print("{:.2f} fps ".format(1_000_000 / t * len(farby)), end = "\r")
sleep(1)
# uspíme čip
displej.sleep_mode(True)
# a vypneme aj podsvietenie
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_off()
else: displej.off()
Kreslenie čiar
Môžeme skúsiť zaplniť celý displej čiarami - porovnáme rýchlosť hline(), vline() a line():
import M5Stick
from st7789 import color565
from time import sleep, ticks_ms, ticks_diff
print(M5Stick.spi)
lcd = M5Stick.lcd
lcd.rotation(0)
lcd.init()
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_on()
t1 = ticks_ms()
for i in range(lcd.height()):
lcd.hline(0, i, lcd.width(), color565(100, 200, 100))
t = ticks_diff(ticks_ms(), t1)
print(f"hline: {t} ms")
sleep(1)
t1 = ticks_ms()
for i in range(lcd.height()):
lcd.line(0, i, lcd.width() - 1, i, color565(200, 100, 200))
t = ticks_diff(ticks_ms(), t1)
print(f"line: {t} ms")
sleep(1)
t1 = ticks_ms()
for i in range(lcd.width()):
lcd.vline(i, 0, lcd.height(), color565(255, 200, 100))
t = ticks_diff(ticks_ms(), t1)
print(f"vline: {t} ms")
sleep(1)
t1 = ticks_ms()
for i in range(lcd.width()):
lcd.line(i, 0, i, lcd.height() - 1, color565(100, 200, 255))
t = ticks_diff(ticks_ms(), t1)
print(f"line: {t} ms")
sleep(1)
lcd.sleep_mode(True)
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_off()
else: lcd.off()
Alebo si len tak šrafovať a kresliť obrazce v cykle:
import M5Stick
from st7789 import color565
from time import sleep
print(M5Stick.spi)
lcd = M5Stick.lcd
lcd.rotation(0)
lcd.init()
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_on()
w, h = lcd.width()-1, lcd.height()-1
f1, f2 = color565(10, 10, 150), color565(255, 200, 100)
kratka = min(w, h)
for i in range(8, kratka, 9):
lcd.line(0, i, i, 0, f1)
lcd.line(0, h-i, i, h, f1)
lcd.line(w-i, 0, w, i, f1)
lcd.line(w-i, h, w, h-i, f1)
sleep(1)
for i in range(8, kratka, 9):
lcd.line(0, i, w-i, 0, f2)
lcd.line(0, h-i, w-i, h, f2)
lcd.line(i, 0, w, i, f2)
lcd.line(i, h, w, h-i, f2)
sleep(10)
lcd.sleep_mode(True)
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_off()
else: lcd.off()
Kreslenie útvarov
Vyskúšajme si kreslenie obdĺžnikov a kružníc:
import M5Stick
from st7789 import color565
from time import sleep
print(M5Stick.spi)
lcd = M5Stick.lcd
lcd.init()
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_on()
w, h = lcd.width(), lcd.height()
k2 = min(w, h)//2
for i in range(0, k2, 5):
lcd.rect(i, i, w-i-i, h-i-i, color565(0, 0, 255*i//k2))
sleep(1)
for i in range(1, k2, 5):
lcd.circle(w//2, h//2, i, color565(255*i//k2, 255*i//k2, 0))
sleep(10)
lcd.sleep_mode(True)
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_off()
else: lcd.off()
Kreslenie polygónov
Trochu zložitejší program nám vykreslí hviezdu a bude ju otáčať, potom vykreslí aj rôzne ďalšie útvary, využijúc rotáciu:
import M5Stick
from st7789 import color565
from time import sleep, ticks_us, ticks_diff
from math import pi
from machine import freq
def Hviezda4(a, o = 5):
b = round(a / o)
return((0, -a), (b, -b), (a, 0), (b, b), (0, a), (-b, b), (-a, 0), (-b, -b))
farba_pozadie = color565(0, 0, 160)
farba_hviezda = color565(241, 196, 15)
freq(240_000_000)
print(freq() / 1_000_000, "MHz,", M5Stick.spi)
lcd = M5Stick.lcd
lcd.rotation(3)
lcd.init()
lcd.fill(farba_pozadie)
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_on()
# polygón
hviezda = Hviezda4(50)
lcd.fill_polygon(hviezda, 120, 67, farba_hviezda)
sleep(1)
# otáčanie okolo stredu polygónu s premazaním (animácia)
uhol = 0.0
while uhol < 2 * pi:
t1 = ticks_us()
uhol += 0.1
lcd.fill_rect(70, 17, 101, 101, farba_pozadie)
lcd.fill_polygon(hviezda, 120, 67, farba_hviezda, uhol, 0, 0)
t = ticks_diff(ticks_us(), t1)
print("{:.1f} fps ".format(1_000_000 / t), end = "\r")
print()
sleep(1)
# otáčanie okolo stredu polygónu
lcd.fill_polygon(hviezda, 120, 67, farba_hviezda, pi / 4, 0, 0)
sleep(1)
lcd.fill_polygon(hviezda, 120, 67, farba_hviezda, pi / 8, 0, 0)
lcd.fill_polygon(hviezda, 120, 67, farba_hviezda, 3 * pi / 8, 0, 0)
sleep(2)
# otáčanie okolo iného bodu
počet = 7
polomer = 15
hviezda = Hviezda4(polomer)
lcd.fill(farba_pozadie)
for i in range(počet):
uhol = i * 2 * pi / počet
lcd.fill_polygon(hviezda, 120, polomer, farba_hviezda, uhol, 0, 67 - polomer)
sleep(1)
# otáčanie okolo iného bodu s premazaním (animácia)
k = 2 * pi / počet
otoč = 0.0
while otoč < 2 * pi:
t1 = ticks_us()
otoč += 0.1
for i in range(počet):
uhol = i * k + otoč
lcd.fill_polygon(hviezda, 120, polomer, farba_pozadie, uhol - 0.1, 0, 67 - polomer)
lcd.fill_polygon(hviezda, 120, polomer, farba_hviezda, uhol, 0, 67 - polomer)
t = ticks_diff(ticks_us(), t1)
print("{:.1f} fps ".format(1_000_000 / t), end = "\r")
print()
sleep(1)
# otáčanie okolo iného bodu
lcd.fill(farba_pozadie)
počet = 12
polomer = 22
o = 16
while o >= 3:
hviezda = Hviezda4(polomer, o)
for i in range(počet):
uhol = i * 2 * pi / počet
lcd.fill_polygon(hviezda, 120, polomer, farba_hviezda, uhol, 0, 67 - polomer)
o = round(o / 2 + 1)
sleep(1)
sleep(1)
if hasattr(M5Stick, "pmu"): M5Stick.pmu.lcd_off()
else: lcd.off()
lcd.sleep_mode(True)
Zobrazenie obrázku
Objekt ST7789 poskytuje aj praktické funkcie pre zobrazenie obrázku zo súboru:
- jpg({súbor}, {x}, {y}, {režim}) - zobrazí JPEG obrázok zo súboru (nesmie byť v progresívnom režime) na zadaných súradniciach,
- parameter {režim} nie je povinný, no pokiaľ ho nastavíme na hodnotu 1, použije sa pamäťovo úsporné (ale pomalé) spracovanie;
- png({súbor}, {x}, {y}, {maskovanie}) - zobrazí PNG obrázok zo súboru na zadaných súradniciach,
- parameter {maskovanie} nie je povinný, no pokiaľ ho nastavíme na hodnotu True, potom sa plne transparentné body nevykreslia (čiastočná transparentnosť však nie je podporovaná).
Okrem toho máme k dispozícii dve pokročilé funkcie pre prácu s bitmapovým obrazom, ktoré môžeme využiť pri čiastočnom prekresľovaní obrazu, či pri animáciách:
- blit_buffer({údaje}, {x}, {y}, {šírka}, {výška}) - obraz z pamäte (bajtové pole) s uvedenými rozmermi skopíruje do obrazovej pamäte na zadané súradnice;
- jpg_decode({súbor}, {x}, {y}, {šírka}, {výška}) - z JPEG obrázku načíta do pamäte zvolenú časť, výstupom je 3-prvková tupla s údajmi a rozmermi (údaje, šírka, výška).