Fyzikální kabinet FyzKAB

ESP32-TTGO: Postavme si hodinky

UPOZORNĚNÍ
Následující článek byl již upraven pro jádro Arduino ESP32 verze 3.x.
ESP-IDF

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 OLED, která je z přední strany osazena barevným OLED displejem o rozlišení 135×240 pixelů), ze zadní strany je konektor pro připojení (a nabíjení) 3,7 V LiPo(l) 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.

ESP32 TTGO - pinout
Obrázek 1 – Rozložení pinů vývojové desky ESP32 TTGO OLED.

Je tedy 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č

Obsluhu času budeme řešit softwarovým přerušením, vyvolávaným časovačem. Prvním krokem je tedy nastavení časovače s vhodnou časovací frekvencí. Dále musíme nastavit hodnotu intervalu („alarmu“) 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 třeba vyřešit samotnou servisní rutinu přerušení.

Nastavení časovače, jeho přerušení a „alarmu“ uděláme následujícím způsobem:

bool NastalCas = false;   // indikator, zda doslo k zavolani preruseni

hw_timer_t *timer = NULL;   // objekt casovace

// --- 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, nekonecnekrat)

… atd …

Na začátku vytvoříme globální objekt časovače timer. Nastavili jsme pro něj frekvenci čítání 1 MHz. K nastavenému časovači je připojena servisní rutina pojmenovaná onTime. Nakonec je nastavena hodnota alarmu na 1 000 000 mikrosekund (1 sec) a je nastaveno, aby se pak časovač opět (automaticky) znovu aktivovat (počet opakování nekonečněkrát).

V servisní rutině přerušení nastavíme jen globální logickou proměnnou příznaku, který hlavní smyčky 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ří.

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ů. Problematika 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 IST_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 hodnotu, 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&icute; tlačítka „odemknuto“. K tomu vyž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).

OLED displej

Vývojový kit ESP32 s TTGO OLED má na sobě zabudovaný OLED 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í OLED displeje knihovnou TFT_eSPI doporučujeme následující ukázkové kódy přímo od autora knihovny TFT_eSPI: https://platformio.org/lib/show/1559/TFT_eSPI/examples

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í OLED 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>    // Graphics and font library for ST7735 driver chip
#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ávali.

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ěnou 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 an souřadnice 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žíté 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 přeložení 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

Pokud již rozumíme principu kódu našich hodinek, můžeme se podívat na celý kód.

/* hodiny na OLED displeji TTGO - verze s prerusenimi*/

#include <TFT_eSPI.h>    // Graphics and font library for ST7735 driver chip
#include <SPI.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;
bool NastalCas = false;    //indikator, zda doslo k zavolani preruseni
bool initial = true;

hw_timer_t * timer = NULL;    // objekt casovace

static uint8_t conv2d(const char* p) {
   uint8_t v = 0;
   if ('0' <= *p && *p <= '9') v = *p - '0';
   return 10 * v + *++p - '0';
}

uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6);    // Get H, M, S from compile time

String to2dig(int cislo) {
   if (cislo <= 9) return "0" + String(cislo);
   else return String(cislo);
}

//----------------- 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;
   }
}

void IRAM_ATTR onTimer() {
   NastalCas = true;    // zprava pro hlavni smycky, ze ma zacit kreslit na displej
}

// 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);

   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, nekonecnekrat)

   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("FyzKAB", 70, 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 an souřadnice 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, ve kterém musíme nastavit správnou vývojovou desku. 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 s OLED displejem, musíme tedy v seznamu desek typu ESP32 vyhledat desku typy TTGO-OLED (např. TTGO-LoRa32-OLED). Po úspéšném přeložení 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.

ESP32 TTGO - hodiny
Obrázek 2 – Modul TTGO s OLED displejem použitý jako jednoduché hodiny.

Na obrázku č. 2 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 LiPo(l) baterii – viz obrázek č. 3. Můžeme tedy připojit LiPo(l) 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á surovost bude to, co ostatní „bastlíře“ nadchne!

ESP32 TTGO - baterie
Obrázek 3 – Modul TTGO s OLED disponuje konektorem pro připojení LiPo(l) 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ý OLED 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“ opravdu chutná a slouží ku zdraví!

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!