ESP32-TTGO: Postavme si hodinky
- UPOZORNĚNÍ:
- Od určité verze knihovny
TFT_eSPIse na modulu ESP32 TTGO T-Display začal zobrazovat pouze černý displej (viz různé diskuze a nářky bastlířů na internetu). Upravená knihovna TFT_eSPI od autora Xinyuan-LilyGO to dočasně opravila, ale dnes nefunguje pod jádrem Arduino ESP32 verze 3. - V tomto aktualizovaném článku si mimo jiného ukážeme, jak nastavit současnou verzi knihovny TFT_eSPI (ver.2.5.43) na jádru Arduino ESP32 ver. 3, aby vše opět fungovalo.
Asi se shodneme, že učení a hromadění všemožných informací o modulu ESP32 už bylo dost. Pojďme si dnes prostě „jen tak“ postavit nějakou blbost pro radost. Z našeho testování interního čidla nám tu zbyla vývojová deska ESP32 TTGO T-Display, která je z přední strany osazena barevným TFT displejem o rozlišení 135×240 pixelů), ze zadní strany je konektor pro připojení (a nabíjení) 3,7 V Li-Po/Li-Pol akumulátoru. Deska je dále doplněna dvojicí tlačítek, která jsou připojena k pinům GPIO 0 a GPIO35. Obrázek č. 1 ukazuje celkové rozložení pinů vývojové desky TTGO.
Obrázek 1 – Rozložení pinů vývojové desky ESP32 TTGO T-Display.
Je jasné, že naše zapojení musí něco zobrazovat na displeji (nejlépe barevně), mělo by to být přenosné zařízení napájené baterií a ovládat se dá pomocí dvojice tlačítek. Popravdě řečeno, první co nás napadlo, byla herní konzole pro Tetris! 😁 Nakonec však vyhrála jednodušší varianta, tedy hodinky. Budeme chtít zobrazit čas jak na analogovém ciferníku, tak zároveň v podobě digitální. Levé a pravé tlačítko nám bude sloužit pro nastavování hodin a minut.
A protože už přece něco umíme, pojďme to celé naprogramovat pomocí přerušení!
Návrh programu
Základem celého programu musí být algoritmus, který se nám bude starat o časování a pochopitelně o vykreslování potřebných údajů na displej. Jak už jsme řekli, chceme využít přerušení. Všem, kterým je používání přerušení zatím zakryto mlhou nevědomosti, doporučujeme prostudovat článek: ESP32: Přerušení – konfigurace a použití.
Časovač
Pro obsluhu času můžeme zvolit několik různých cest. My si ukážeme dvě základní, a to: přerušení vestavěným časovačem a využití knihovny Ticker.
Softwarové přerušení časovače
Nejdříve si ukážeme, jak řešit „krokování“ hodin softwarovým přerušením, které bude vyvoláváno časovačem. Prvním krokem je tedy nastavení časovače s vhodnou časovací frekvencí. Dále budeme muset nastavit hodnotu intervalu („alarmu“) pro spouštění přerušení, aby k němu docházelo 1× za sekundu. To nám na změnu počtu sekund, popřípadě minut a hodin stačí. Nakonec je pochopitelně třeba vyřešit samotnou servisní rutinu přerušení.
Verze jádra Arduino ESP32 ver. 3
V další části článku věnované přerušení budeme popisovat situaci, kdy máme v prostředí Arduino IDE nainstalované jádro Arduino ESP32 verze 3.
Je třeba říci, že Arduino core ESP32 v3.x není jen nějaký „malý update“, neboť se kupříkladu změnilo:
- staré ESP-IDF 4.x se nahradilo novým ESP-IDF 5.x
- je zde jiná implementace SPI
- změny v transakcích SPI
- změny v API přerušení časovače
- změny v DMA
- …
S přechodem na ESP32 Arduino Core 3.x bohužel došlo k několika vážným změnám, které pohřbily zpětnou kompatibilitu řady kódů – některé funkce, používané v předešlých verzích, byly odstraněny, jiné funkce mají jinou syntaxi nebo byly přejmenovány.
Nastavení časovače pomocí přerušení časovače (v jádře Arduino ESP32 v.3) budeme řešit takto:
timer = timerBegin(1000000); // nastaveni casovace (frekvence)
timerAttachInterrupt(timer, &onTimer); // nastaveni preruseni (casovac, ISR)
timerAlarm(timer, 1000000, true, 0); // kdy spustit preruseni (casovac, 1s, opakovat ANO, do nekonecna)
kde:
timernastavuje časovači pracovní frekvenci – zde 1 MHz.Begin( 1000000) timerpřipojuje zvolenou ISR (zde rutinu přerušeníAttach Interrupt( timer, &onTimer) onTimer) k časovači.timerkonfiguruje volání ISR procedury – první parametrAlarm( timer, 1000000, true, 0) timerodkazuje na časovač, který vyvolává přerušení; druhý parametr1000000stanovuje počet tiknutí, po kterých se spustí přerušení; třetí parametrtruevolí periodický alarm s počtem opakování, které udává poslední parametr (0znamená nekonečně krát).
Celkové nastavení časovače, jeho přerušení a „alarmu“ tedy uděláme následujícím způsobem:
hw_timer_t *timer = NULL; // objekt casovace
volatile bool NastalCas = false; // indikator, zda doslo k zavolani preruseni
// --- Obsluzna rutina IRQ --
void IRAM_ATTR onTimer() {
NastalCas = true; // zprava pro hlavni smycky, ze ma zacit kreslit na displej
}
void setup(void) {
timer = timerBegin(1000000); // nastaveni casovace (frekvence)
timerAttachInterrupt(timer, &onTimer); // nastaveni preruseni (casovac, ISR)
timerAlarm(timer, 1000000, true, 0); // kdy spustit preruseni (casovac, 1s, opakovat ANO, do nekonecna)
… atd …
Na začátku vytvoříme globální objekt časovače timer. V proceduře setup časovač dříve popsaným způsobem nastavíme na frekvenci čítání 1 MHz a následné spouštění servisní rutiny 1× za sekundu. K nastavenému časovači je připojena servisní rutina pojmenovaná onTimer, která na první pohled vlastně „nedělá nic pořádného“. V této rutině přerušení nastavujeme jen globální logickou proměnnou příznaku, který hlavní smyčku upozorní, že bude třeba překreslit displej. Jak jistě víme, servisní rutina musí proběhnout relativně rychle a vykreslování ciferníku na displej k těmto činnostem zrovna moc nepatří, takže to v ISR nemá co dělat!
Mluvíme-li o přerušení, všimněme si i „kouzelného“ slovíčka volatile v deklaraci proměnné NastalCas. Jen připomínáme, že tento kvalifikátor typu, který se přidává k deklaraci proměnné, říká kompilátoru, že hodnota dané proměnné se může kdykoli změnit, aniž by to bylo explicitně vidět v kódu (např. změna hardwarem, přerušením, jiným vláknem apod.).
Knihovna Ticker
Zajímavou alternativou k řešení problémů časovače v různých verzích jádra Arduino ESP32 je knihovna Ticker. Tato knihovna nám poskytuje elegantní způsob, jak obejít všechny drobné rozdíly mezi jádry Arduino ESP32 2 a 3 a zjednodušit práci s periodickými časovači. Funguje jako softwarový časovač, takže nemusíme řešit hardwarové registrace ani API, které se mezi jádry změnilo. Lze ji tedy použít na starých i nových projektech.
Při použití knihovny Ticker postupujeme dost podobně předešlému postupu. Vytvoříme objekt typu Ticker a nastavíme, jak často se má spouštět nějaká funkce. Ticker se sám postará o periodické volání zvolené funkce na pozadí (Není potřeba nastavovat pracovní frekvenci časovače a hodnoty alarmu manuálně).
Výhody knihovny Ticker
- Kompatibilní s jádrem Arduino ESP32 ver. 2 i 3 – není potřeba měnit kód programu při přechodu.
- Jednoduché API – žádné ISR, předděličky, čísla časovačů, frekvence časovače, hodnota alarmu…
- Bezpečné – běží na pozadí softwarově, takže netřeba se bát zmatku v hardwarových registrech.
- Snadné zastavení nebo restart – např.
timerokamžitě zastaví časovač..detach()
Je ale poctivé říci, že jsou zde i určité nevýhody. Softwarový ticker není tak přesný jako hardwarový časovač – drobné odchylky v řádu mikrosekund až milisekund se mohou objevit při velké zátěži ESP32. Ale pro většinu aplikací (LED blikání, ovládání relé, naše vykreslování vteřinové ručičky) je to naprosto dostačující.
Vyřešení časovače pro náš projekt pomocí knihovny Ticker uděláme následujícím způsobem:
#include <Ticker.h>
Ticker ticker; // Timer místo hw_timer
volatile bool NastalCas = false; // indikator, zda doslo k zavolani preruseni
// --- Obsluzna rutina tickeru --
void onTimer() { // Timer ISR (Ticker volá každou sekundu)
NastalCas = true;
}
void setup(void) {
// Ticker místo hw_timer
ticker.attach(1.0, onTimer); // 1 sekunda
… atd …
Tlačítka
Když jsme se tak pěkně „rozdováděli“ se softwarovým přerušením generovaným časovačem, můžeme se pustit do obsluhy tlačítek – pochopitelně pomocí přerušení hardwarového. Opět si trochu připomeneme, že je třeba vytvořit poměrně krátké (rychlé) servisní rutiny přerušení a na ně nasměrovat hardwarové události GPIO pinů. V našem případě půjde o stisknutí tlačítek, která jsou spojená s GPIO piny 0 a 35. Oba piny tedy nastavíme jako vstupy, a to nejlépe jako vstupy držené na úrovni log. 1 pomocí interních (volitelných) pull-up rezistorů. Problematiku nastavení GPIO pinů jsme řešili v článku: ESP32 – Jak používat GPIO piny?
Opět se podíváme na kousek kódu, na kterém si vysvětlíme, klíčové informace:
#define RightBUT 35
#define LeftBUT 0
bool initial = true;
void IRAM_ATTR ISR_HOD() {
if (!initial) {
hh = hh + 1;
if (hh > 23) hh = 0;
initial = true;
}
}
void IRAM_ATTR ISR_MIN() {
if (!initial) {
mm = mm + 1;
if (mm > 59) mm = 0;
// ss = 0; //pokud chceme při zmene minuty nastavit vterinovku na 0
initial = true;
}
}
void setup(void) {
pinMode(RightBUT, INPUT_PULLUP);
pinMode(LeftBUT, INPUT_PULLUP);
attachInterrupt(LeftBUT, ISR_HOD, FALLING);
attachInterrupt(RightBUT, ISR_MIN, FALLING);
… atd …
Počáteční definice pro překladač nastavuje číslo GPIO 35 pinu pro pravé tlačítko (RightBUT), kterým budeme zvyšovat hodnotu počtu minut (posouvání minutové rafičky) a GPIO 0 pinu levého tlačítka (LeftBUT) pro zvyšování počtu hodin (posouvání hodinové rafičky). Logická proměnná initial bude dále v hlavní smyčce určovat, zda má být překreslen displej – poprvé to je hned po spuštění programu, dále vždy, jakmile proběhne změna v některé ze servisních rutin přerušení.
Druhý úkol proměnné initial si ještě ukážeme dále v servisních rutinách přerušení.
Servisní rutiny hardwarového přerušení ISR_HOD a ISR_MIN reagují na sestupnou hranu (nastaveno v proceduře setup) a jedna zvyšuje globální proměnnou hh, která je počtem hodin, druhá zvyšuje počet minut. Servisní rutiny ještě obsahují podmínku hlídající tyto hodnoty, aby nebyly vyšší než 23 hodin nebo 59 minut.
A nyní se ještě jednou vraťme k proměnné initial. Další její funkce se ukazuje v servisní proměnné, kde ovlivňuje přičítání počtu hodin nebo minut. Jinými slovy, pokud není proměnná initial rovna hodnotě false, nebude se zvyšování minut ani hodin provádět. Proč?
Zkusíme si tento trik vysvětlit trochu obšírněji. Co se děje, když stiskneme tlačítko. Teoreticky by se měla na příslušném GPIO pinu změnit hodnota z log. 1 na log. 0 (nebo chcete-li z hodnoty HIGH na hodnotu LOW). Prakticky tomu však není! Mechanický kontakt, který propojí GPIO pin proti zemi ve skutečnosti může drobně zapružit, takže chvilku skutečně GPIO pin se zemí spojí, pak rozpojí a pak zase spojí. Pro běžného člověka je to mžik, který ani nepostřehne. Ne však modul ESP32, jehož kód probíhá velikou rychlostí. Takže při prvním kontaktu proběhne první přerušení, po „dopružení“ kontaktu druhé. Rázem toto mechanické zachvění generovalo dvě stisknutí. A kdyby občas jen dvě! Tento jev je poměrně znám a mechanické vypínače se proti němu musí ošetřit. Hardwarově se to v elektronice řeší kupříkladu zapojením logických hradel (tzv. flip-flop obvod), nebo aspoň pomocí RC členu. Softwarově v kódu programu se to dá ošetřit zareagováním na první sepnutí a pak určitou prodlevou (zpožděním), kdy jsou další zákmity ignorovány. Ale ta představa, jak do servisních rutin přerušení dáváme nějaké čekací zpoždění, není asi moc ideální! Musíme to tedy udělat jinak! Při reakci na první zákmit (sepnutí) spínače se spustí přerušení a normálně proběhne, ale zároveň nastavíme proměnnou initial na hodnotu true. Takže při druhém, třetím… zákmitu již hodnota proměnné initial nedovolí další přičtení počtu hodin nebo minut. Teď je však potřeba zajisti, aby po nějakém čase bylo stisknutí tlačítka „odemknuto“. K tomu využijeme fakt, že proměnná initial také ovládá překreslování displeje. Po návratu ze servisní rutiny přerušení dojde k překreslení displeje a na konci toho se opět nastaví proměnná initial na hodnotu false, která dovolí provedení inkrementace počtu hodin a minut v těle servisní procedury (to uvidíme v hlavní smyčce loop). Paradoxně pomalé překreslování displeje nám tady poslouží jako „zdržovačka“, která překlene zakmitání spínače. Toto zakmitání sice několikrát spustí rutinu přerušení, ale v dalších zákmitech se v rutině nic neprovádí (není splněna podmínka pro proměnnou initial).
TFT displej
Vývojový kit ESP32 TTGO T-Display má na sobě zabudovaný TFT displej, který budeme v prostředí Arduino IDE ovládat prostřednictvím rozšiřující knihovny TFT_eSPI.h.
Funkce knihovny TFT_eSPI (API)
Zde je seznam nejběžnějších funkcí, které nabízí knihovna TFT_eSPI. Vzhledem k absenci oficiální dokumentace byly funkce extrahovány přímo ze zdrojového kódu knihovny.
width() – šířka obrazovky v pixelech
height() – výška obrazovky v pixelech
setRotation(m) – otočí obsah obrazovky. Orientace je číslo od 0 do 3 a 4 až 7 pro bitmapu
getRotation() – vrací orientaci obrazovky
invertDisplay(i) – invertovat barvy displeje (i = true invertovat, i = false normální)
fillScreen(color) – vybarví pozadí obrazovky. Funkce ve skutečnosti nakreslí celý obdélník zabírající celou plochu obrazovky
setCursor(x, y) – umístí kurzor.
getCursorX– vrátí souřadnici x kurzoru
getCursorY – vrátí souřadnici y kurzoru
setPivot(x, y) – bod otáčení obrazovky
getPivotX() – souřadnice x otočného bodu
getPivotY() – x souřadnice otočného bodu
readPixel(x, y) – přečte barvu pixelu ve formátu 565
drawCircle(x0, y0, r, barva) – nakreslí obrys kruhu
fillCircle(x0, y0, r, barva) – nakreslí vyplněný kruh
drawEllipse(x0, y0, rx, ry, barva) – nakreslí obrys elipsy
fillEllipse(x0, y0, rx, ry, barva) – nakreslí vyplněnou elipsu
drawRect(x, y, w, h, barva) – nakreslí obrys obdélníku
fillRect(x, y, w, h, barva) – nakreslí vyplněný obdélník
drawRoundRect(x, y, w, h, r, barva) – obrys obdélníku se zaoblenou hranou. Všechny úhly mají stejný poloměr
fillRoundRect(x, y, w, h, r, barva) – stejně jako předchozí, ale objekt je vyplněný
drawTriangle(x0, y0, x1, y1, x2, y2, barva) – obrys trojúhelníku. (je třeba zadat souřadnice tří vrcholů)
fillTriangle(x0, y0, x1, y1, x2, y2, barva) – stejně, ale s vyplněným trojúhelníkem
drawPixel(x, y, barva) – nakreslit barevný pixel
drawLine(x0, y0 , x1, y1 , barva) – nakreslí čáru mezi dvěma libovolnými body
drawFastVLine(x, y, h, barva) – svislá čára délky h
drawFastHLline(x, y, w, barva) – vodorovná čára délky w
drawBitmap(x, y, bitmapa, w, h, barva) – zobrazí obrázek uložený v poli bitmapa
drawXBitmap (x, y, bitmapa, w, h, color) – vykreslí monochromatický obrázek uložený v poli. (Můžete například převést obrázek JPG nebo PNG pomocí GIMPu do formátu XBM a načíst pole bodů.)
fontsLoaded() – vrací 16bitovou kódovanou hodnotu označující načtená písma
fontHeight(h) – výška písma
setFreeFont(f) – Nastaví písmo GFX, které se má použít
setTextSize(s) – nastavuje velikost textu. Musí být mezi 1 a 7 (max.)
setTextColor(barva) – přiřadí barvu textu (barva v šestnáctkové soustavě)
setTextColor(barva, pozadí) – stejně jako předchozí, ale také definuje barvu pozadí. Upřednostňuje se pro text, který se často mění (hodiny, souřadnice, poloha…), díky tomu nedochází k překreslování celé obrazovky při každé aktualizaci.
textWidth(string, font) – vrací šířku řetězce v daném písmu v pixelech
drawChar(x, y, znak, barva, pozadí, velikost) – zobrazí (vykreslí) znak na zadané pozici.
setTextDatum(d) – textový odkaz. Použijte jednu z níže uvedených konstant
Dostupné pozice:
- TL_DATUM 0 // vlevo nahoře (výchozí)
- TC_DATUM 1 // Horní střed
- TR_DATUM 2 // Vpravo nahoře
- ML_DATUM 3 // Levý záložník
- CL_DATUM 3 // Uprostřed vlevo, jak je uvedeno výše
- MC_DATUM 4 // Centrální centrum
- CC_DATUM 4 // Střed na střed, jak je uvedeno výše
- MR_DATUM 5 // Pravý záložník
- CR_DATUM 5 // Uprostřed vpravo, jak je uvedeno výše
- BL_DATUM 6 // Vlevo dole
- BC_DATUM 7 // Uprostřed dole
- BR_DATUM 8 // Vpravo dole
- L_BASELINE 9 // Základní řádek levého znaku (řádek, na kterém by seděl znak "A")
- C_BASELINE 10 // Základní řádek centrálního znaku
- R_BASELINE 11 // Základní řádek pravého znaku
setTextPadding(x_width) – vnitřní okraj (ekvivalent CSS stylu)
getTextPadding() – vrací aktuální výplň
drawString(string, x, y) – nakreslí řetězec (typ String)
drawCentreString(string, x, y, font) – nakreslí řetězec (typ String) se středem na pozici x, y
color8to16(color) – převod 8bitové barvy na 16bitovou hodnotu barvy 565
color16to24(color565) – převod 16bitové barvy na 24bitovou hodnotu barvy 888
color24to16(color888) – převést 24bitovou barvu na 16bitovou hodnotu barvy 565
decodeUTF8(znak) – převede znak ASCII do formátu UTF-8
decodeUTF8(buf, index, zbývající) – dekódování znakové vyrovnávací paměti v rozšířeném formátu ASCII v UTF-8
Seznam výše použitých proměnných:
- m, s – celé číslo
- barva, pozadí–barva v hexadecimálním formátu
- x, y, x0, y0, x1, y1– souřadnice (v pixelech)
- r, rx, ry– oblast (v pixelech)
- w – šířka (v pixelech)
- h– výška (v pixelech)
- i – logická hodnota (false/true)
- string – textový řetězec
Pochopitelně nebudeme všechny tyto funkce a metody používat, ale můžeme si je zde vypsat, abychom se k nim mohli vrátit jindy. Třeba, až nás právě naprogramované hodinky přestanou bavit a budeme chtít naprogramovat tu herní konzolu na Tetris. 😉
Případným zájemcům o poměrně velkou sadu ukázkových kódů k ovládání TFT displeje knihovnou TFT_eSPI doporučujeme následující ukázkové kódy přímo od autora knihovny TFT_eSPI: https://
Při vykreslování ciferníku analogových hodin, budeme postupovat následujícím způsobem. Po připojení potřebné knihovny TFT_eSPI (popř. její pomocné SPI) deklarujeme objekt knihovny TFT_eSPI, přes který budeme přistupovat k ovládání TFT displeje. Zároveň definujeme odstín šedé barvy, který budeme později používat a který není mezi předdefinovanými barvami – viz následující seznam předdefinovaných barevných konstant v knihovně TFT_eSPI.
Předdefinované barvy v knihovně TFT_eSPI:
- TFT_BLACK – 0x0000
- TFT_NAVY – 0x000F
- TFT_DARKGREEN – 0x03E0
- TFT_DARKCYAN – 0x03EF
- TFT_MAROON – 0x7800
- TFT_PURPLE – 0x780F
- TFT_OLIVE – 0x7BE0
- TFT_LIGHTGREY – 0xC618
- TFT_DARKGREY – 0x7BEF
- TFT_BLUE – 0x001F
- TFT_GREEN – 0x07E0
- TFT_CYAN – 0x07FF
- TFT_RED – 0xF800
- TFT_MAGENTA – 0xF81F
- TFT_YELLOW – 0xFFE0
- TFT_WHITE – 0xFFFF
- TFT_ORANGE – 0xFDA0
- TFT_GREENYELLOW – 0xB7E0
- TFT_PINK – 0xFC9F
#include <TFT_eSPI.h>
#include <SPI.h>
TFT_eSPI tft = TFT_eSPI(135, 240);
#define TFT_GREY 0xBDF7
V proceduře setup() pak inicializujeme ovládání displeje, nastavíme orientaci a vyplníme obrazovku černě. Další kroky se již týkají vykreslení ciferníku, který se vykreslí nyní a jen jednou. Pohyblivé prvky vyřešíme tak, aby nám tuto „masku“ nijak nepřemalovávaly.
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
… atd …
V rámci vykreslení ciferníku je vymalováno orámování, malé značky minut a větší značky hodin a největší značky (řešeno dvojicí malých posunutých kružnic) pro číslice 3, 6, 9 a 12. Nesmíme také opomenout na vykreslení našeho loga, tím je vše statické hotovo.
V hlavní smyčce loop() je testován příznaková proměnná NastalCas, jejíž hodnotu true nastavuje servisní rutina přerušení časovače (již bylo zmíněno). Jakmile se tedy tato proměnná objeví jako true, je třeba nastavit tuto proměnnou zpět na false, a především zvýšit počet sekund v proměnné ss. Pochopitelně je třeba ošetřit přetečení hodnoty 60 pro počet sekund a minut a hodnoty 24 pro počet hodin.
if (NastalCas) {
NastalCas = false;
ss++; // zvys počet sekund
if (ss == 60) {
ss = 0;
mm++; // po 60 sec, pridej minutu
if (mm > 59) {
mm = 0;
hh++; // po 60 min, pridej hodinu
if (hh > 23) {
hh = 0;
}
}
}
… atd …
}
Dále se v kódu (stále však v těle splněné) podmínky pro NastalCas se z hodnot sekund, minut a hodin spočítají souřadnice x a y pro rafičky.
// Predpoctete polohu rucicek na souradnice x a y (pro rychlou aktualizaci obrazovky)
sdeg = ss * 6; // 0-59 -> 0-354
mdeg = mm * 6 + sdeg * 0.01666667; // 0-59 -> 0-360 - includes seconds
hdeg = hh * 30 + mdeg * 0.0833333; // 0-11 -> 0-360 - includes minutes and seconds
hx = cos((hdeg - 90) * 0.0174532925);
hy = sin((hdeg - 90) * 0.0174532925);
mx = cos((mdeg - 90) * 0.0174532925);
my = sin((mdeg - 90) * 0.0174532925);
sx = cos((sdeg - 90) * 0.0174532925);
sy = sin((sdeg - 90) * 0.0174532925);
… atd …
Běžně se překresluje jen vteřinová ručička. Pokud však je hodnota sekund rovna 0, začíná nová minuta, a tedy jsou překresleny všechny ručičky. Druhou částí podmínky pro překreslení všech rafiček je logická proměnná initial. Pokud si vzpomínáme, tak ta je nastavena na hodnotu true na začátku programu, aby se rafičky vykreslily okamžitě po spuštění. Ještě jednou je však nastavena na true, a to v případě stisknutí kteréhokoliv „seřizovacího“ tlačítka, aby se ručičky přemalovaly okamžitě po ručním přestavění času.
if (ss == 0 || initial) {
initial = false;
// Kazdou minutu mazte pozice hodinove a minutove rucicky
tft.drawLine(ohx, ohy, 65, 65, TFT_BLACK);
ohx = hx * 33 + 65;
ohy = hy * 33 + 65;
tft.drawLine(omx, omy, 65, 65, TFT_BLACK);
omx = mx * 44 + 65;
omy = my * 44 + 65;
tft.drawCentreString(to2dig(hh)+":"+to2dig(mm), 64, 180, 6);
}
Protože nám dole na displeji zbylo ještě trochu místa, přidáme tam čas v digitální podobě. I ten budeme přepisovat jen v případě, že se změní počet minut.
Pomocné procedury
Kromě servisních rutin přerušení, úvodní procedury setup() a hlavní smyčky loop() jsou ještě v programu další dvě pomocné funkce.
První pojmenovaná conv2d slouží pro získání hodnoty hodin, minut a vteřin z konstanty __TIME__, kterou vkládá při kompilaci kompilér prostředí Arduino IDE a která tyto údaje obsahuje. Vlastně jde o takový drobný trik. Modul ESP32 nemá vnitřní hodiny reálného času, které by běžely při odpojení napájení. Nastavením času při kompilaci kódu na hodnotu z počítače (prostředí Arduino IDE), tak zaručí, že po kompilaci ukazují hodiny správný čas. Toto nastavení času dle času kompiléru ukazuje následující řádka (včetně použití funkce conv2d):
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6);
Na druhou stranu, tato metoda zadání času není úplně ideální, pokud modul ESP32 vypneme a opět zapneme, nastaví se čas na okamžik kompilace kódu a nikoliv na aktuální čas.
Druhou pomocnou funkcí je funkce to2dig, která převádí číslo na řetězec dvouznakového formátu, tedy čísla menší než 9 budou vpředu doplněna nulou. Tuto funkci
využíváme pro tisk digitální podoby času. Pokud by se někomu nelíbila nula na začátku počtu hodin, může tuto proceduru modifikovat tak, aby u hodin přidávala na začátek mezeru.
Kód programu
Pokud již rozumíme principu kódu našich hodinek, můžeme se podívat na celý program. V úvodu jsme pořád něco řešili okolo verzí jádra Arduino ESP32 a mluvili jsme o tom, že se tyto dvě jádra liší v nastavování přerušení, takže zde uvádíme verzi s knihovnou Ticker, která pracuje v obou případech. Její části kódu jsou barevně zvýrazněné, aby je případný zájemce mohl vyměnit za část kódu pracující na principu hardwarového časovače a jeho softwarového přerušení.
/* hodiny na OLED displeji TTGO - verze s Ticker */
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Ticker.h>
TFT_eSPI tft = TFT_eSPI(135, 240);
#define TFT_GREY 0xBDF7
#define RightBUT 35
#define LeftBUT 0
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 64, osy = 64, omx = 64, omy = 64, ohx = 64, ohy = 64;
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
volatile bool initial = true;
volatile bool NastalCas = false; // Indikátor pro loop
Ticker ticker; // Timer místo hw_timer
// Timer ISR (Ticker volá na každou sekundu)
void onTimer() {
NastalCas = true;
}
// Funkce převodu čísla na 2-místný řetězec
String to2dig(int cislo) {
if (cislo <= 9) return "0" + String(cislo);
else return String(cislo);
}
// Funkce převodu z __TIME__
static uint8_t conv2d(const char* p) {
uint8_t v = 0;
if ('0' <= *p && *p <= '9') v = *p - '0';
return 10 * v + *++p - '0';
}
// Inicializace času z kompilace
uint8_t hh = conv2d(__TIME__);
uint8_t mm = conv2d(__TIME__ + 3);
uint8_t ss = conv2d(__TIME__ + 6);
//----------------- IRQ --------------
void IRAM_ATTR ISR_HOD() {
if (!initial) {
hh = hh + 1;
if (hh > 23) hh = 0;
initial = true;
}
}
void IRAM_ATTR ISR_MIN() {
if (!initial) {
mm = mm + 1;
if (mm > 59) mm = 0;
// ss = 0; //pokud chceme při zmene minuty nastavit vterinovku na 0
initial = true;
}
}
// funkce SETUP se spusti jednou pri stisknuti tlacitka reset nebo pri zapnuti desky.
void setup(void) {
pinMode(RightBUT, INPUT_PULLUP);
pinMode(LeftBUT, INPUT_PULLUP);
attachInterrupt(LeftBUT, ISR_HOD, FALLING);
attachInterrupt(RightBUT, ISR_MIN, FALLING);
// Ticker místo hw_timer
ticker.attach(1.0, onTimer); // 1 sekunda
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
// Draw clock face
tft.fillCircle(64, 64, 61, TFT_BLUE);
tft.fillCircle(64, 64, 57, TFT_BLACK);
// Draw 12 lines
for (int i = 0; i < 360; i += 30) {
sx = cos((i - 90) * 0.0174532925);
sy = sin((i - 90) * 0.0174532925);
x0 = sx * 57 + 64;
yy0 = sy * 57 + 64;
x1 = sx * 50 + 64;
yy1 = sy * 50 + 64;
tft.drawLine(x0, yy0, x1, yy1, TFT_BLUE);
}
// Draw 60 dots
for (int i = 0; i < 360; i += 6) {
sx = cos((i - 90) * 0.0174532925);
sy = sin((i - 90) * 0.0174532925);
x0 = sx * 53 + 64;
yy0 = sy * 53 + 64;
if (i == 0 || i == 180) { // velka tecka pro 12 a 6
tft.fillCircle(x0, yy0, 1, TFT_CYAN);
tft.fillCircle(x0 + 1, yy0, 1, TFT_CYAN);
}
if (i == 90 || i == 270) { // velka tecka pro 3 a 9
tft.fillCircle(x0, yy0, 1, TFT_CYAN);
tft.fillCircle(x0 + 1, yy0, 1, TFT_CYAN);
}
}
tft.fillCircle(65, 65, 3, TFT_RED);
tft.drawCentreString("DIYerLAB", 65, 140, 4);
tft.setTextColor(TFT_RED, TFT_BLACK); // Pridani cerne barvy pozadi automaticky vymaze predchozi text
tft.setTextSize(1);
}
// funkce LOOP bezi stale dokola.
void loop() {
if (NastalCas) {
NastalCas = false;
ss++; // zvys počet sekund
if (ss == 60) {
ss = 0;
mm++; // po 60 sec, pridej minutu
if (mm > 59) {
mm = 0;
hh++; // po 60 min, pridej hodinu
if (hh > 23) {
hh = 0;
}
}
}
// Predpoctete polohu rucicek na souradnice x a y (pro rychlou aktualizaci obrazovky)
sdeg = ss * 6; // 0-59 -> 0-354
mdeg = mm * 6 + sdeg * 0.01666667; // 0-59 -> 0-360 - includes seconds
hdeg = hh * 30 + mdeg * 0.0833333; // 0-11 -> 0-360 - includes minutes and seconds
hx = cos((hdeg - 90) * 0.0174532925);
hy = sin((hdeg - 90) * 0.0174532925);
mx = cos((mdeg - 90) * 0.0174532925);
my = sin((mdeg - 90) * 0.0174532925);
sx = cos((sdeg - 90) * 0.0174532925);
sy = sin((sdeg - 90) * 0.0174532925);
if (ss == 0 || initial) {
// Kazdou minutu mazte pozice hodinove a minutove rucicky
tft.drawLine(ohx, ohy, 65, 65, TFT_BLACK);
ohx = hx * 33 + 65;
ohy = hy * 33 + 65;
tft.drawLine(omx, omy, 65, 65, TFT_BLACK);
omx = mx * 44 + 65;
omy = my * 44 + 65;
tft.drawCentreString(to2dig(hh)+":"+to2dig(mm), 64, 180, 6);
initial = false;
}
// Prekresleni nove pozice (hod a min rucicky nejsou vymazany, aby se zabranilo blikani
tft.drawLine(osx, osy, 65, 65, TFT_BLACK);
tft.drawLine(ohx, ohy, 65, 65, TFT_WHITE);
tft.drawLine(omx, omy, 65, 65, TFT_WHITE);
osx = sx * 47 + 65;
osy = sy * 47 + 65;
tft.drawLine(osx, osy, 65, 65, TFT_RED);
tft.fillCircle(65, 65, 3, TFT_RED);
}
}
Test kódu
Kód překopírujeme do prostředí Arduino IDE. Asi si nyní uvědomujeme, že oproti předešlým experimentům se standardním modulem ESP32 nyní používáme trochu speciální modul ESP32. POZOR na to! Pokud jsme doteď používali „standardní“ vývojovou desku s ESP32, měli jsme zřejmě nastavenou desku typu „ESP32 DEV Module“. Nyní však používáme desku „ESP32 TTGO T-Display“, ta však ve výběru desek není! Naštěstí to nevadí. Můžeme nastavení „ESP32 DEV Module“ použít i pro TTGO T-Display!
Po úspěšné kompilaci kódu a jeho nahrání do modulu TTGO bychom měli na displeji modulu získat obrázek hodin, jako je znázorněno na následujícím obrázku č. 2.
Obrázek 2 – Modul ESP32 TTGO T-Display použitý jako jednoduché hodiny (stav roku 2021).
To však platilo před lety! (asi v roce 2021) V současné době (rok 2026) se „při pouhém“ odeslání kódu do modulu ESP32 dočkáme pouze černého displeje!
Problém byl dost aktivně diskutován v různých bastlířských fórech a poměrně časté řešení bylo použití upravené knihovny TFT_eSPI (ver. 2.2.20) a zpětný přechod na starší jádro Arduino ESP32.
Od té doby nějaká doba uplynula a lze předpokládat, že autor knihovny TFT_eSPI již tento problém v následujících verzích odstranil. Ale přesto při pouhém vložení knihovny do projektu, jak tomu bezproblémově bývalo dříve, nyní stále skončíme nezdarem.
Pod kapotou knihovny TFT_eSPI (ver. 2.5.43)
Na správné řešení nás kupodivu přivedla AI, kterou jsme zaúkolovali, aby porovnala starou (fungující) verzi s novou (stále vzdorující) verzí s ohledem na rozdílné vlastnosti jader Arduino ESP32 v.2 a v.3. Kromě korektního srovnání základních vlastností aktuálních nastavení jen jako mimochodem tato „Pýthie 21. století“ přihodila i informaci, na kterou se nikdo vlastně neptal, ale byla klíčová.
No, někdy mám dojem, že kouzlo celé AI spočívá v tom, že tam ve skutečnosti mají v kleci zavřeného nějakého geniálního brigádníka a on odpovídá na ty naše slavné prompty. 😉
Pro řešení se musíme dostat ke zdrojovému kódu knihovny TFT_eSPI. Prostředí Arduino IDE standardně ukládá své knihovny (nainstalované zpravidla přes Správce knihoven) do složky: C:\. V této složce najdeme složku TFT_eSPI, ve které se zaměříme na soubory User_ a User_.
Následující obrázky 3 a 4 ukazují, jaký zásah musíme v těchto souborech provést, abychom rozsvítili displej na modulu ESP32 TTGO T-Display.
Obrázek 3 – Nastavení ovladače displeje ST 7789, kterým je osazen modul ESP32 TTGO T-Display.
Prvním úkolem je nastavení správného ovladače použitého displeje v souboru User_. Jak vidíme na obrázku č. 3, je zde defaultně nastaven displej ILI9341. Tuto řádku musíme zakomentovat a naopak odkomentovat řádku s ovladačem displeje ST 7789, který je osazen na modulu ESP32 TTGO T-Display. Tím knihovna TFT_eSPI dostane správné informace o rozlišení displeje, rozsahu jeho paměti a formátu uložených informací (např. informace o barvách apod.).
Obrázek 4 – Nastavení použitého typu modulu ESP32 v souboru User_.
Máme-li v souboru User_ zvolený správný typ displeje, přesuneme se do souboru User_, kde zvolíme typ modulu ESP32, který bude displej řídit. Jak vidíme na obrázku č. 4, je třeba opět zakomentovat defaultně zvolený modul a odkomentovat položku, která pro knihovnu TFT_eSPI nastaví správné rozložení pinů propojující displej a modul ESP32. Používáme-li modul ESP32 TTGO T-Display, zvolíme položku Setup25_.
A máme hotovo! Ale bylo to drama… 😎
Nyní máme knihovnu TFT_eSPI nastavenou pro modul ESP32 TTGO T-Display a program bude fungovat při přeložení v prostředí Arduino IDE s nainstalovaným jádrem Arduino ESP32 verze 3.x. Výsledek nám ukazuje obrázek č. 5.
Obrázek 5 – Hodiny na modulu ESP32 TTGO T-Display, kompilováno v Arduino ESP32 Core 3 (stav roku 2026).
Na obrázcích č. 2 a 5 vidíme modul napájený prostřednictvím USB kabelu (mimochodem tento modul je oproti starším kitům s modulem ESP32 osazen USB konektorem typu C). Napájení USB kabelem není moc praktické pro nějaké přenosné použití. 😕 Ale v úvodu jsme si říkali, že na rubové straně modulu TTGO je konektor pro Li-Po/Li-Pol baterii – viz obrázek č. 6. Můžeme tedy připojit Li-Po/Li-Pol akumulátor a rázem máme mobilní hodiny, se kterými můžeme vyrazit do světa. Pravda design zatím není nic moc, ale možná právě tato minimalistická syrovost bude to, co ostatní „bastlíře“ nadchne!
Obrázek 6 – Modul TTGO T-Display disponuje konektorem pro připojení Li-Po/Li-Pol baterie.
Závěr:
V dnešním článku jsme se snad trochu odpoutali od suché teorie, ve které jsme stále jen ukazovali nové vlastnosti modulu ESP32. Představili jsme si poměrně zajímavý kit, který je oproti klasickým deskám s modulem ESP32 osazený TFT displejem, dvěma programovatelnými tlačítky, konektorem USB-C a konektorem pro externí akumulátor. Je pravda, že díky tomu, že jsme si museli zejména ovládání displeje trochu představit, nějaké té teorii jsme se přeci jen nevyhnuli, ale tak už to bývá – jakmile začneme pracovat na konkrétním projektu začínají se objevovat nové věci a problémy, které je třeba nastudovat a vyřešit. Ale jedině tak, se člověk něco nového naučí. Už sám Démokritos říkal, že vzdělání má sice hořké kořeny, ale sladké ovoce. Tak ať Vám to ovoce jménem: „užitečné znalosti o modulu ESP32 TTGO T-Display“ opravdu chutná a slouží ku zdraví!