Fyzikální kabinet FyzKAB

Displej pro Vernier Go!Temp pomocí modulu Arduino

V dnešním článku se vracíme k problematice spojení USB teploměru Vernier Go!Temp k modulu Arduino, kterou jsme tak trochu nakousli v článku Načtení Vernier Go!Temp modulem Arduino. V předchozím článku jsme si ukázali, jak načíst relevantní data z USB teploměru a jak z těchto čísel získat hodnotu teploty.

Vernier USB Go
Obr. č. 1 – připojení USB teploměru Vernier Go!Temp k modulu Arduino UNO s výstupem teploty na displeji

V dnešním článku bychom si chtěli především ukázat dvě další věci:

  1. Jak poznat, že je k modulu Arduino skutečně připojen USB teploměr Go!Temp, nikoliv jiné USB zařízení.
  2. Jak získanou hodnotu teploty prezentovat na připojeném displeji (LCD nebo OLED)

Identifikace připojeného USB zařízení

Vyjdeme z programového kódu, který jsme si uvedli v předchozím článku. Program byl navržen pro modul Arduino UNO s připojeným USB Host Shieldem a využíval knihovny USB_Host_Shield_2.0.

Doporučuji se k předchozímu článku vrátit a problematiku lehce zopakovat. Nyní si jen zrychleně vysvětlíme hlavní část programu.

V hlavní nekonečné smyčce Loop dochází k volání asynchronního požadavku funkce Task() třídy USB, jehož vykonání je testováno stavem GoTempEvent::mostRecentEvent.Ready (viz následující ukázka kódu)

void loop() {
     Usb.Task(); // probehne asynchronni pozadavek na USB
     if (GoTempEvent::mostRecentEvent.Ready != 0) { // USB cidlo se uz ozvalo, muzeme merit
     // nacteni hodnot z reportu USB
         Teplota = GoTempEvent::mostRecentEvent.Temp_RAW / SKLON - POSUN;
         Serial.print("cislo vzorku: ");
         Serial.println(GoTempEvent::mostRecentEvent.cisloVzorku);
         Serial.print("teplota: ");
         Serial.print(String(Teplota,1));
         Serial.println(" *C");
         GoTempEvent::mostRecentEvent.Ready = 0;
     }
}

Po dokončení asynchronního požadavku je načtena potřebná série dat, která jsou v proměnných cisloVzorku a Temp_RAW. Z hodnoty proměnné Temp_RAW je pak vypočtena hodnota teploty pomocí kalibračních konstant.

Za zmínku ještě stojí dvě globální proměnné Usb (třída USB) a Hid(&Usb) (třída HIDUniversal), které slouží k práci s USB, popřípadě HID protokolem připojeného zařízení. Pro náš další účel zejména využijeme funkce getUsbTaskState() a načtení čísla připojeného USB zařízení pomocí funkce Usb.getDevDescr().

První částí přidaného kódu do hlavní nekonečné smyčky loop bude test připojeného USB zařízení pomocí funkce Usb.getUsbTaskState(). Pokud zařízení ohlásí stav USB_STATE_RUNNING, můžeme přistoupit k načtení identifikačního čísla připojeného USB zařízení. To provedeme pomocí funkce Usb.getDevDescr, údaje načteme do proměnné buf (třída USB_DEVICE_DESCRIPTOR). Tato složená proměnná má několik atributů. Nás bude zajímat atribut idVendor, který obsahuje číslo výrobce a pak atribut idProduct, který obsahuje číslo zařízení daného výrobce.

Oč vůbec jde?

Každý typ zařízení, které se připojuje k USB, by měl mít svou jednoznačnou a nezaměnitelnou identifikaci. K tomuto účelu slouží dvě 16-bitová identifikační čísla – Vendor ID (VID) a Product ID (PID). VID slouží k identifikaci výrobce a přiděluje ho organizace USB-IF. PID pak slouží pro označování jednotlivých typů výrobků – nikoli jednotlivých kusů, na ty by šestnáctibitové číslo pro větší série nestačilo, ale daného typu. PID už si přiřazuje výrobce libovolně sám.

Bez unikátní kombinace VID&PID se lze obejít, pokud by zařízení bylo pouze experimentální, kde je jisté, že nemůže dojít ke kolizi dvou stejných VID&PID. Pokud, ale jde o výrobek profesionální, je vlastní kombinace VID&PID nutná. Takže kombinace těchto dvou čísel je pro identifikaci připojeného USB čidla přesně to „pravé ořechové“!

Nyní stačí jen tyto základní čísla USB zařízení zjistit. Kupříkladu stačí připojit čidlo zvoleného typu a nechat si proměnné idVendor a idProduct vypsat.

Nebo rovnou využít čísla z následující tabulky, která uvádí potřebná čísla pro některá USB produkty firmy Vernier.

Vernier
(Vendor)
číslo produktu
(Product)
USB zařízení
2295 0001 LabPro
0002 Go!Temp
0003 Go!Link
0004 Go!Motion

Tab. č. 1 – tabulka hodnot VID a PID některých USB čidel firmy Vernier

Jestliže se tedy připojené zařízení neohlásí dvojicí čísel 2295 a 2 (viz Tab. č. 1), nejde o USB teploměr Vernier Go!Temp, tedy jej vůbec nemá cenu načítat.

Část kódu, která bude testovat připojené USB zařízení, by mohla vypadat následujícím způsobem:

// Test pripojeneho USB
if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {      // uz bezi pripojene USB
     byte rcode;
     USB_DEVICE_DESCRIPTOR buf;
     rcode = Usb.getDevDescr( 1, 0, 0x12, ( char *)&buf );    // nacti jeho udaje
     if ((buf.idVendor != 2295)||(buf.idProduct != 2)) {      // ID vyrobce: Vernier -> 2295, ID cidla: Go!Temp -> 0002
         // je pripojene jine cidlo, tak se merit nebude
         Serial.println("Cidlo neni Go!Temp!");
     }
}

Nejdříve se testuje, zda je připojeno nějaké zařízení. Jestliže se zařízení ohlásí, je načtena identifikace do proměnné buf, za které se získá potřebná dvojice idVendor a idProduct. Pokud se alespoň jedno s čísel liší od dvojice definující USB teploměr Go!Temp, je vypsána hláška o tom, že se nejedná o požadovaný USB teploměr.

Nyní je již na konkrétním záměru programátora, jak tento test připojeného USB zařízení zakomponuje do svého programu. My si to asi ukážeme až v definitivní verzi dnešních programů.


Připojení displeje

Pokud jsme zvládli identifikaci čidla i načtení teploty z USB teploměru, můžeme se zaměřit na zobrazení výsledku na displeji. Ať již použijeme LCD displej, nebo OLED displej, v obou případech použijeme displeje připojené k modulu Arduino pomocí sběrnice I²C. Toto řešení nám poskytne připojení pouze pomocí dvou datových vodičů, což nám celkově zjednoduší hardwarovou část celého řešení.

LCD displej

Připojení dvouřádkového displeje LCD 1602 s připojeným převodníkem I²C je ve světě modulu Arduino poměrně standardní věc. I²C převodník je zpravidla přímo připájen na zadní straně LCD displeje a datové vodiče SDA a SCL jsou připojeny na modulu Arduino UNO na piny A4 a A5, které jsou vyhrazeny pro I²C komunikaci (viz Obr. 2 a Tab. č. 2).

Vernier USB Go
Obr. č. 2 – pohled na I²C displej a jeho připojení k modulu Arduino UNO.
I²C LCD 1602 modul Arduino UNO
Ucc +5 V
GND GND
SDA A4
SCL A5

Tab. č. 2 – tabulka propojení I²C LCD displeje a modulu Arduino UNO

Pro komunikaci s I²C LCD displejem se nejčastěji využívá knihovna LiquidCrystal_I2C, která využívá knihovnu Wire. Kromě přidání těchto knihoven do svého projektu je třeba v programu nastavit adresu použitého I²C převodníku. Tato adresa bývá obvykle 0x27, ale existují i převodníky s adresou např. 0x3F. Asi nejlepší je, připojit k modulu Arduino I²C displej a skutečnou adresu převodníku si zjistit pomocí programu I2C_Scanner, který je přímo jedním z ukázkových programů knihovny Wire (viz obr. 3).

Vernier USB Go
Obr. č. 3 – Nalezení programu I2C_scanner mezi ukázkovými programy knihovny Wire.

Výsledný program pro I²C LCD

Po získání adresy použitého I²C LCD displeje můžeme vytvořit kupříkladu následující program:

/*
   Nacitani USB teplomeru Vernier Go!Temp
   (c) N.P.C. 2023
  ---------------------------------------
  verze: BETA-4_LCD
*/

// kalibracni konstanty, Teplota = (hodnota z cidla) / SKLON - POSUN
#define SKLON 130.19
#define POSUN 0.899
//----------------------

// knihovny pro USB Host Shield
#include <usbhid.h>         // knihovna kvuli USB Shieldu
#include <hiduniversal.h>   // knihovna kvuli HID komunikaci s USB
// #include <SPI.h>         // pridani knihovny je nadbytecne, protoze si ji knuhovny pro USB HOST zavolaji samy

// knihovny kvuli sbernici I2C a I2C LCD displeji
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x3F,16,2);    // nastaveni adresy LCD na 0x3F pro zobrazení 16 znaku a 2 radku
// POZOR adresa LCD je casteji 0x27, tady je pouzit displej trochu s "exotickou" adresou!

// nasledujici cast GoTemp.h a GoTemp.cpp je prevzata z ovladace joysticku a upravena dle potreby
// (zdokomentovane jsou jen zmeni oproti originalu)
// ------ GoTemp.h --------------
struct GoTempData {        // struktura pro nacitani hodnot z USB
   uint8_t Ready;          // 1. Byte je stav 0/1 urcujici, zda je cidlo jiz ready
   uint8_t cisloVzorku;    // 2. Byte je poradove cislo vzorku (0-255)
   uint16_t Temp_RAW;      // 3. a 4. Byte jsou LO a HI hodnota odpovidajici teplote
// zbyle Byte jsou ignorovany (nejsou pro nas potreba)
};

class GoTempEvent {
   public:
     virtual void OnGoTempChanged(const GoTempData *evt);
     static GoTempData mostRecentEvent;
};

#define RPT_GOTEMP_LEN   sizeof(GoTempData)/sizeof(uint8_t)

class GoTempReportParser : public HIDReportParser {
   GoTempEvent *GoTempEvents;
   uint8_t oldTemp[RPT_GOTEMP_LEN];
   public:
     GoTempReportParser(GoTempEvent *evt);
     virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
};

// ------ GoTemp.cpp --------------
GoTempReportParser::GoTempReportParser(GoTempEvent *evt) :
   GoTempEvents(evt) {}

void GoTempReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
   bool match = true;
   // Checking if there are changes in report since the method was last called
     for (uint8_t i=0; i<RPT_GOTEMP_LEN; i++) {
       if( buf[i] != oldTemp[i] ) {
         match = false;
         break;
       }
     }
     // Calling Game Pad event handler
     if (!match && GoTempEvents) {
       GoTempEvents->OnGoTempChanged((const GoTempData*)buf);
       for (uint8_t i=0; i<RPT_GOTEMP_LEN; i++) oldTemp[i] = buf[i];
     }
}

GoTempData GoTempEvent::mostRecentEvent;    // (asi) potreba pro pristup z hlavniho programu (bez tohoto to nejde)

void GoTempEvent::OnGoTempChanged(const GoTempData *evt) {
   // ulozeni struktury evt, aby byla pristupna stylem: GoTempEvent::mostRecentEvent.JMENO (DULEZITE!)
   mostRecentEvent = *evt;
}

// ------ GoTemp.ino --------------
// hlavni cast programu
USB    Usb;
HIDUniversal    Hid(&Usb);
GoTempEvent    GoTempEvents;
GoTempReportParser    GoTemp(&GoTempEvents);

float Teplota = 999;    // teplota, dokud se nenacte poradna hodnota musi but stejna jako Teplota_old
float Teplota_old;      // predchozi teplota, pro kontrolu zmeny teploty

int cm;           // cislo mereni - to hlasi cidlo
int cm_old = -1;      // predchozi cislo mereni, slouzi pro sledovani zmeny cisla mereni (tj. zda je pripojen Teplomer)

int w;      // pocitadlo zobrazenych znaku pri cekani na cidlo
String LCD_vystup;      // pomocna promenna pro zaokrouhleni teploty na 1. des. misto
long int WatchDog;      // koncovy cas, po kterem (pokud se nebude měnit cm) je odpojen Teplomer
int wdInt = 2000;      // hodnota casu (2 sekundy), behem ktere se musi poradove cislo menit, jinak nekdo vytahl USB teplomer

void Uvod() {
   lcd.clear();      // smaze LCD
   lcd.setCursor(0,1);      // kurzor na 1 znak na 2 radku
   w = 0;      // pocitadlo znaku pro zobrazovani cekacich znaku
   Teplota_old = 999;      // vychozi je schvalne nesmysl, aby po nacteni Teploty doslo ihned k zapisu na LCD
}

void InitSetting() {
   w = 0;    // pocitadlo znaku pro zobrazovani cekacich znaku
   Teplota_old = 999;    // vychozi je schvalne nesmysl, aby po nacteni Teploty doslo ihned k zapisu na LCD
   GoTempEvent::mostRecentEvent.Ready = 0;    // nastavim priznak neaktivniho cidla, ono ji USB pak prepise
}

// =============================================== SETUP ===============================================
void setup() {
   lcd.init();      // initializace I2C LCD
   lcd.backlight();      // zapnuti podsviceni
   Uvod();
   delay(1500);

   if (Usb.Init() == -1) {
     // Serial.println("USB Shield se nespustil."); // nahozeni USB HOST Shield
     lcd.clear();      // smaze LCD
     lcd.setCursor(0,0);      // kurzor na 1 znak na 1 radku
     lcd.print("Chyba:USB Shield");    // vypise hlasku
     while(1);      // nekonecna smycka, ktera zastavi dalsi beh programu beh
   }

   delay(200);

   if (!Hid.SetReportParser(0, &GoTemp)) {    // nastaveni pro cteni USB ve stylu HID
     lcd.clear();      // smaze LCD
     lcd.setCursor(0,0);      // kurzor na 1 znak na 1 radku
     lcd.print("Chyba:HID parser");    // vypise hlasku
     while(1);      // nekonecna smycka, ktera zastavi dalsi beh programu beh
   }

   InitSetting();      // uvodni nastaveni
}

// =============================================== LOOP ===============================================
void loop() {
   Usb.Task();      // probehne asynchronni pozadavek na USB
   if (GoTempEvent::mostRecentEvent.Ready != 0) {    // USB cidlo se uz ozvalo, muzeme merit
     // nacteni hodnot z reportu USB
     // cm odpovida cislu vzorku
     cm = GoTempEvent::mostRecentEvent.cisloVzorku;
     // teplota je vypocitana z hodnot HI a LO pomoci kalibracnich konstant
     Teplota = GoTempEvent::mostRecentEvent.Temp_RAW / SKLON - POSUN;

     if (Teplota != Teplota_old) {      // zmenila se hodnota teploty, je teba ji zmenit na LCD
       LCD_vystup = String(Teplota,1);      // priprava teploty ve stavru s 1 des. mistem
       LCD_vystup.replace('.', ',');
       lcd.setCursor(1,0);     // nastavi kurzor 2 znak 1 radku
       lcd.print("Mereni teploty");    // vypise hlasku
       lcd.setCursor(0,1);      // nastavi kurzor 1 znak 2 radku
       lcd.print("  t = ");      // vypise "t = "
       lcd.print(LCD_vystup);      // teplotu
       lcd.print(" ");        // °C
       lcd.print((char)223);
       lcd.print("C   ");
       Teplota_old = Teplota;      // ulozeni teploty pro pristi zjisteni zmeny
     }

     if (cm != cm_old) {      // pokud se meni cislo mereni, cidlo bezi a nastavuje se WatchDog
       WatchDog = millis() + wdInt;      // stale se posouva hranice, do kdy musi prijit dalsi mereni
       cm_old = cm;      // ulozeni cisla mereni pro test zmeny
     } else {      // cislo mereni je stejne jako minule, je treba otestovat, jak dlouho to jiz trva
       if ( WatchDog < millis() ) {      // cislo mereni se nemeni dele nez je zdravo - nekdo vytahl teplomer z USB
         InitSetting();      // uvodni nastevni jako na zacatku - zacne uvodni hledani cidla
       }
     }

   } else {    // cidlo zatim vraci stav Ready = 0 - bud tam neni, nebo se jeste neinicializovalo

     // Test pripojeneho USB
     if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {    // uz bezi pripojene USB
       byte rcode;
       USB_DEVICE_DESCRIPTOR buf;
       rcode = Usb.getDevDescr( 1, 0, 0x12, ( char *)&buf );    // nacti jeho udaje
       if ((buf.idVendor != 2295)||(buf.idProduct != 2)) {    // ID vyrobce: Vernier -> 2295, ID cidla: Go!Temp -> 0002
         // je to pripojene jine cidlo, tak se merit nebude
         lcd.clear();      // smaze LCD
         lcd.setCursor(0,0);     // kurzor na 1 znak na 1 radku
         lcd.print("     CHYBA     " );    // vypise hlasku
         lcd.setCursor(0,1);
         lcd.print("USB neni Go!Temp");
         delay(2000);
         // GoTempEvent::mostRecentEvent.Ready = 0;
         InitSetting();
       }
     } else {    // ceka se, az se USB ozve
       lcd.setCursor(0,0);       // kurzor na 1 znak na 1 radku
       lcd.print("  Inicializace  ");    // vypise hlasku
       lcd.setCursor(0,1);      // vypsani pole cekacich znaku
       lcd.print("                ");    // smaze dolni radku
       if (w >= 16) { w = 0; }       // pokud se dolni radka zaplnila, jede se znova zleva
       lcd.setCursor(w,1);      // vypsani pole cekacich znaku
       lcd.print('.');
       w++;         // zapocita poradove cislo znaku
       delay(200);
     }
   }
}

Popis programu

V principu se stále jedná o základní program pro načítání USB teploměru Vernier Go!Temp pomocí modulu Arduino, který jsme si vysvětlili již v předešlém článku. Výrazná změna proběhla jen v oblasti hlavní části programu, kde byla přidána část pro detekci a identifikaci USB zařízení, kterou jsme si vysvětlili za začátku tohoto článku. Tato část je do programu přidána jako možnost else k případu, kdy probíhá normální načítání teploty. Je jasné, že detekce USB zařízení má význam jen v okamžiku, když se čidlo ještě neozvalo a neposkytlo výstupní data, tedy proměnná GoTempEvent::mostRecentEvent.Ready je stále 0.

připojení USB flash
Obr. č. 4 – při připojení jiného USB zařízení, než je teploměr Go!Temp, načítání teploty nezačne.

Zároveň je při čekání na odezvu USB zařízení na LCD displeji zobrazena na 1. řádce hláška „Inicializace“ a 2. řádku postupně probíhá tečka dokazující činnost modulu Arduino. Jakmile se připojený USB modul ozve ( GoTempEvent::mostRecentEvent.Ready je stále 0, ale již vrací stav USB_STATE_RUNNING) je potvrzena jeho identifikace pomocí čísla výrobce (VID) a čidla (PID). Dojde i k platnosti dotazu pomocí funkce task(), o chvíli později tedy i k náležité asynchronní odpovědi. Tím se aktivuje původní část hlavní podmínky, která zobrazuje teplotu na displeji.

Aby nedocházelo k nehezkému blikání zobrazené hodnoty, kdy je displej neustále mazán a znova vykreslován, dochází k přepisu displeje pouze, pokud se hodnota teploty oproti předchozí změní. K tomuto srovnání slouží proměnné Teplota a Teplota_old, ve kterých je aktuální teplota, resp. teplota z předešlého měření. K přepisu displeje dojde jen, pokud se tyto hodnoty liší.

Program je ještě doplněn o jednu další funkci, o detekci odpojení teploměru. Pokud se v průběhu měření USB teploměr vytáhne z USB konektoru USB Host Shieldu, je třeba to nějak zjistit. Jinak by se čekalo na odezvu, která by ale nikdy nepřišla. Pro tuto detekci se využívá měnící se číslo vzorku, které USB teploměr v rámci svých návratových dat poskytuje. Číslo vzorku je uloženo do proměnné cm. Jakmile se hodnota proměnné cm po dobu delší, než je stanoveno konstantou WatchDog, nemění, znamená to, že čidlo je odpojeno (nebo z nějakého jiného důvodu neodpovídá). V činnosti tohoto programu to znamená, že pokud se po dobu 2 vteřin nenačte nový vzorek, dojde k vyvolání počátečního stavu programu, kdy je vyhledáváno USB čidlo na USB portu. Po opětovném připojení USB vše běží jako při prvním spuštění – nejdříve dojde k detekci USB, pak se získá jeho VID a PID, zároveň je vyvolána funkce task(), která po chvíli vrátí data, která se převedou na teplotu… atd.

Vernier Go!Temp a LCD displej
Obr. č. 5 – Zobrazení teploty z USB teploměru Go!Temp na LCD displeji.

Strukturu výše uvedeného programu asi nejlépe osvětlí následující vývojový diagram (obr. 6):

vývojový diagram
Obr. č. 6 – vývojový diagram programu načteni USB teploměru a výpisy na displej.

OLED displej

OLED displej používá tzv. organické diody (Organic Light-Emitting Diode), od kterých je odvozena i jeho zkratka. OLED je vyspělejší a v mnoha ohledech lepší technologie, než jsou klasické LCD. OLED displeje mají krom mnoha jiných výhod, jako je kupříkladu lepší podání černé, i nižší spotřebu. Tmavá místa na OLED displeji totiž spotřebovávají méně energie, a ta černá dokonce nespotřebovávají energii žádnou.

Chceme-li vytvořit zařízení, které doplní USB teploměr o zobrazovací displej, jistě se bude jednat o zařízení přenosné, tedy na baterii. Proč tedy nezkusit naše zařízení osadit úspornějším displejem, tím tedy zvýšit živostnost použité baterie. Navíc OLED displej prostě vypadá COOL! 😊

pripojeni OLED k Arduino
Obr. č. 7 – připojení OLED displeje k modulu Arduino.
I²C SSD1306 modul Arduino UNO
Ucc +5 V
GND GND
SDA A4
SCL A5

Tab. č. 3 – tabulka propojení I²C OLED displeje a modulu Arduino UNO

Pro naše účely jsme použili displej SSD1306 (o velikosti 0.96" a rozlišením 128×64 pixelů) opět s rozhraním I²C. Aby byl výsledný efekt aspoň trochu zajímavý, použili jsme displej, který je dvoubarevný – tedy celý je modrý, jen horní část (proužek) je žlutý. Pokud horní žlutou část využijeme jako určitý stavový řádek a naopak na velké modré části displeje budeme zobrazovat hlavní informaci, mohl by být výsledný efekt skutečně „cool“.

Pro práci s tímto displejem se obecně doporučuje knihovna Adafruit_SSD1306. Knihovna disponuje možností přímého výpisu textu na zadané souřadnice. Knihovna je doplněna o mnoho příkladů, takže po chvilce lze na displeji vysázet téměř libovolný text. Jediným problémem asi může být snad jen znak stupňů. Ale i to lze v nejhorším vyřešit vykreslením malé kružnice.

Proč ne knihovna Adafruit_SSD1306

Důvod, proč však tuto knihovnu dále využívat nebudeme, se skrývá v možnosti zobrazení velkého písma. Standardní font pro výpis textu na tento relativně malý displej je dost malý. Rozhodně chceme informaci o teplotě zobrazit trochu větší. Knihovna sice umí nastavit až osm stupňů zvětšení, ale text je pak poměrně zubatý. Knihovna dále umožňuje měnit použitý font, takže lze program doplnit o definici většího hezkého písma. Ale pak nastává další problém – nedostatek místa v paměti modulu Arduino.

Kombinace poměrně rozsáhlé knihovny pro USB Host Shield, knihovny SSD1306 a definice nějakého většího fontu je na velikost paměti modulu Arduino UNO již trochu moc. Pravda, jistě se lze pustit do určité redukce a optimalizace celého kódu, ale osobní zkušenost s projekty, které jdou do paměti modulu Arduino tak říkajíc „nadoraz“ není zrovna nejlepší.

Takže je jasné, že pokud v tuto chvíli existuje plán B, který nás s minimem kompromisů dostane někam pod hranici 80% zaplnění paměti, zvolíme si spíše tuto cestu.

Sázíme na „babičku“ U8glib

Schůdné řešení s problémem nedostatku paměti nakonec nabídla knihovna U8glib (pozor: neplést s knihovnou U8g2!), která je sice již staršího data, ale její velikost je menší. Taktéž lze do ní načíst znakovou sadu, která obsahuje jen symboly číslic, tím lze ušetřit další místo v paměti. Programování OLED displeje je v pojetí této knihovny sice trochu zvláštní, ale zde především ctíme letité heslo: „Účel světí prostředky“.

Když jsme zmínili trochu podivném ovládání OLED displeje, tak si hned ukážeme základní strukturu jeho ovládání.

Po deklaraci objektu daného displeje:

U8GLIB_SSD1306_128X64 oled(U8G_I2C_OPT_NONE);      // inicializace OLED displeje

se příkazy pro výpis údajů na OLED zapisují do struktury, která začíná funkcí .firstPage() následovanou strukturou do…while. Podmínkou ukončení je funkce .nextPage(). Následující řádky ukazují, jak vypsat na OLED displej obligátní „Hello World!“

#include "U8glib.h"

U8GLIB_SSD1306_128X64 oled(U8G_I2C_OPT_NONE)

void setup(void) {
   oled.setFont(u8g_font_unifont);    // nastaveni fontu
   // oled.setRot180();    // pokud bychom potrebovali otocit rozbrazovani o 180
}

void loop(void) {
   oled.firstPage();
   do {
     oled.setFont(u8g_font_unifont);
     oled.drawStr( 0, 22, "Hello World!");
   } while( oled.nextPage() );

   delay(500);
}

V sekci setup vidíme, že i v této knihovně lze měnit font příkazem setFont(), zde je nastaven základní font. Jak již bylo zmíněno, kromě celkové menší velikosti knihovna nabízí i možnost použití redukovaných znakových sad – například obsahující jen znaky číslic (velikost 1 kB oproti celé sadě 8 kB). To se nám v případě potřeby zobrazování jen číselné hodnoty teploty docela hodí.

Druhou možností, jak zobrazovat velká čísla s minimálními paměťovými nároky je příkaz .setScale2x2(), který nastaví dvojnásobnou velikost. Jen je třeba dodat, že tím se nastavuje dvojnásobná velikost všeho, tedy rozlišení displeje se tím sníží na polovinu. To je třeba brát v úvahu i při zadávání souřadnic v tomto režimu. Návrat ke standardnímu rozlišení a velikosti je pomocí příkazu .undoScale(). Pochopitelně v režimu dvojnásobné velikosti se setkáváme se stejným problémem, jako byl při nastavení většího písma u předešlé knihovny, tedy dochází k určité „zubatosti“ písma.

Následující obrázek č. 8 ukazuje porovnání obou možností, jak vytvořit velký text na OLED displeji.

srovnaní výstupů na OLED
Obr. č. 8 – výpis teploty pomocí setScale2x2 (vlevo) a využitím změny fontu setFont (vpravo).

Jak asi obrázek 8 ukazuje, řešení s použitím nastavení vhodné znakové sady je mnohem efektnější. Přesto ani metoda dvojité velikosti nezůstane stranou! Jak bylo výše uvedeno, lze využít redukované znakové sady – tedy jen symbolů pro číslice. Tím se výrazně ušetří paměť modulu Arduino, ale na druhou stranu ztratíme možnost psát zvětšené texty – například symbol °C nebo nějakou chybovou hlášku. A tady asi pomůže právě zvětšení textu pomocí dvojité velikosti. Koneckonců podívejte se na obrázku 7 na symbol C. V obou případech je vypsán právě pomocí dvojité velikosti, akorát v případě napravo to trochu zanikne díky krásně hladkému textu číselné hodnoty a jemně vykreslené kružnice značky stupňů.

Následující program ukazuje výsledné řešení zobrazovací jednotky k USB teploměru Vernier Go!Temp pomocí OLED displeje. Celý program opět vychází ze základního vývojového diagramu (viz obr. 6). Program je pochopitelně doplněn o několik drobností, které jsou nutné pro výpis na OLED – například umístění číselné hodnoty na střed (to se u LCD řešit nemuselo). Také se trochu změnila čekací animace v okamžiku hledání USB čidla. A v neposlední řadě byl program doplněn o úvodní logo při zapnutí. Zkrátka člověk se musí občas umět i trochu bavit! 😊

/*
   Nacitani USB teplomeru Vernier Go!Temp
   (c) N.P.C. 2023
 ---------------------------------------
   verze: BETA-3_OLED
*/

#define VERZE "0.30"

// kalibracni konstanty, Teplota = (hodnota z cidla) / SKLON - POSUN
#define SKLON 130.19
#define POSUN 0.899
//----------------------

// knihovny pro USB Host Shield
#include <usbhid.h>    // knihovna kvuli USB Shieldu
#include <hiduniversal.h>    // knihovna kvuli HID komunikaci s USB
// #include <SPI.h>    // pridani knihovny je nadbytecne, protoze si ji knuhovny pro USB HOST zavolaji samy

#include "U8glib.h"    // knihovna kvuli OLED displeji

U8GLIB_SSD1306_128X64 oled(U8G_I2C_OPT_NONE);     // inicializace OLED displeje

// nasledujici cast GoTemp.h a GoTemp.cpp je prevzata z ovladace joysticku a upravena dle potreby
// (zdokomentovane jsou jen zmeni oproti originalu)
// ------ GoTemp.h --------------
struct GoTempData {        // struktura pro nacitani hodnot z USB
   uint8_t Ready;          // 1. Byte je stav 0/1 urcujici, zda je cidlo jiz ready
   uint8_t cisloVzorku;    // 2. Byte je poradove cislo vzorku (0-255)
   uint16_t Temp_RAW;      // 3. a 4. Byte jsou LO a HI hodnota odpovidajici teplote
// zbyle Byte jsou ignorovany (nejsou pro nas potreba)
};

class GoTempEvent {
   public:
     virtual void OnGoTempChanged(const GoTempData *evt);
     static GoTempData mostRecentEvent;
};

#define RPT_GOTEMP_LEN   sizeof(GoTempData)/sizeof(uint8_t)

class GoTempReportParser : public HIDReportParser {
   GoTempEvent *GoTempEvents;
   uint8_t oldTemp[RPT_GOTEMP_LEN];
   public:
     GoTempReportParser(GoTempEvent *evt);
     virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
};

// ------ GoTemp.cpp --------------
GoTempReportParser::GoTempReportParser(GoTempEvent *evt) :
   GoTempEvents(evt) {}

void GoTempReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
   bool match = true;
   // Checking if there are changes in report since the method was last called
     for (uint8_t i=0; i<RPT_GOTEMP_LEN; i++) {
       if( buf[i] != oldTemp[i] ) {
         match = false;
         break;
       }
     }
     // Calling Game Pad event handler
     if (!match && GoTempEvents) {
       GoTempEvents->OnGoTempChanged((const GoTempData*)buf);
       for (uint8_t i=0; i<RPT_GOTEMP_LEN; i++) oldTemp[i] = buf[i];
     }
}

GoTempData GoTempEvent::mostRecentEvent;    // (asi) potreba pro pristup z hlavniho programu (bez tohoto to nejde)

void GoTempEvent::OnGoTempChanged(const GoTempData *evt) {
   // ulozeni struktury evt, aby byla pristupna stylem: GoTempEvent::mostRecentEvent.JMENO (DULEZITE!)
   mostRecentEvent = *evt;
}

// ------ GoTemp.ino --------------
// hlavni cast programu
USB    Usb;
HIDUniversal    Hid(&Usb);
GoTempEvent    GoTempEvents;
GoTempReportParser    GoTemp(&GoTempEvents);

float Teplota = 999;    // teplota, dokud se nenacte poradna hodnota musi but stejna jako Teplota_old
float Teplota_old;      // predchozi teplota, pro kontrolu zmeny teploty

int cm;           // cislo mereni - to hlasi cidlo
int cm_old = -1;      // predchozi cislo mereni, slouzi pro sledovani zmeny cisla mereni (tj. zda je pripojen Teplomer)

int w;      // pocitadlo zobrazenych znaku pri cekani na cidlo
String OLED_vystup;      // pomocna promenna pro zaokrouhleni teploty na 1. des. misto
String hlaska[] = {"Detekce USB ...","USB nenalezeno!"};    // pokud se nedetekuje cidlo, ceka se a stridaji se dve hlasky
char znak[] = {'.',' '};    // znaky pro vykreslovani znaku pri cekani
char CekaciTecky[9];      // znakove pole pro zobrazeni tecek pri cekani
int ocasek, odraz;        // promenne pro zarovnani teploty na stred radky
int ukazatel;        // promena, ktera ukazuje do predchozich poli, co se ma zobrazovat
long int WatchDog;      // koncovy cas, po kterem (pokud se nebude měnit cm) je odpojen Teplomer
int wdInt = 2000;        // hodnota casu (2 sekundy), behem ktere se musi poradove cislo menit, jinak nekdo vytahl USB teplomer
bool logo = true;        // promenna pro zobrazeni uvodniho loga (jen prvne)

void NormalFont() {
   oled.setFont(u8g_font_unifontr);    // nastaveni znakove sady
   oled.setFontPosTop();      // nastaveni vychozich souradnic textu
}

void BigFont() {
   oled.setFont(u8g_font_fur25n);    // nastaveni velke znakove sady
   oled.setFontPosTop();     // nastaveni vychozich souradnic textu
}

void Error(String Hlaska) {
   oled.firstPage();        // zacatek naplnovani bufferu pro OLED
   do {
     oled.setScale2x2();      // 2x zvetseni textu (i souradnic)
     oled.drawStr(9, 11, "Chyba!");      // vypis hodnoty teploty
     oled.undoScale();      // nastaveni puvodniho velikosti a rozliseni
     oled.drawStr(0, 0, Hlaska.c_str());      // vypis hlasky do nadpisu
   } while (oled.nextPage());
}

void InitSetting() {
   memset(CekaciTecky, ' ', 8);    // pole CekaciTecky jsou same mezery prazdne;
   ukazatel = 0;        // ukazatele do pole nabidky nastaven na vychozi
   w = 0;        // pocitadlo znaku pro zobrazovani cekacich znaku
   Teplota_old = 999;      // vychozi je schvalne nesmysl, aby po nacteni Teploty doslo ihned k zapisu na LCD
   GoTempEvent::mostRecentEvent.Ready = 0;    // nastavim priznak neaktivniho cidla, ono ji USB pak prepise
}

// =============================================== SETUP ===============================================
void setup() {
   NormalFont();
   // vypsani uvodniho loga
   oled.firstPage();        // zacatek naplnovani bufferu pro OLED
   do {
     oled.setScale2x2();      // 2x zvetseni textu (i souradnic)
     oled.drawStr(10, 9, "N.P.C.");      // logo NPC
     oled.undoScale();      // nastaveni puvodniho velikosti a rozliseni
     oled.drawStr(0, 52, "TINKERER PROJECT");      // logo maly radek
     oled.drawFrame(0, 16, 128, 33);      // ramecek
     oled.drawStr(0, 0, "Firmware");      // vypis nadpisu verze
     oled.drawStr(67, 0, "-");
     oled.drawStr(78, 0, "v.");
     oled.drawStr(96, 0, VERZE);
   } while (oled.nextPage());
   delay(1500);

   if (Usb.Init() == -1) {
     Error("USB Shield:");
     while(1);      // nekonecna smycka, ktera zastavi dalsi beh programu beh
   }

   delay(200);

   if (!Hid.SetReportParser(0, &GoTemp)) {    // nastaveni pro cteni USB ve stylu HID
     Error("HID parser:");
     while(1);      // nekonecna smycka, ktera zastavi dalsi beh programu beh
   }

   InitSetting();      // uvodni nastaveni
}

// =============================================== LOOP ===============================================
void loop() {
   Usb.Task();      // probehne asynchronni pozadavek na USB
   if (GoTempEvent::mostRecentEvent.Ready != 0) {    // USB cidlo se uz ozvalo, muzeme merit
     // nacteni hodnot z reportu USB
     logo = false;      // uz to zacalo merit, takze logo pryc a uz nikdy vice
     // cm odpovida cislu vzorku
     cm = GoTempEvent::mostRecentEvent.cisloVzorku;
     // teplota je vypocitana z hodnot HI a LO pomoci kalibracnich konstant
     Teplota = GoTempEvent::mostRecentEvent.Temp_RAW / SKLON - POSUN;

     if (Teplota != Teplota_old) {      // zmenila se hodnota teploty, je teba ji zmenit na LCD
       OLED_vystup = String(Teplota,1);      // priprava teploty ve stavru s 1 des. mistem
       OLED_vystup.replace('.', ',');
       oled.firstPage();      // zacatek naplnovani bufferu pro OLED
       do {
         BigFont();      // nastaveni velke znakove sady
         // vypocet odraz a konecnik pro zarovnani na stred a pridani znacky C
         odraz = 15*OLED_vystup.length();      // odhad velikost textu
         ocasek = odraz;      // zakladni souradnice pro znacku C
         odraz = odraz + 38;      // pridani mista pro C do delky textu
         odraz = 128 - odraz;      // okraje kolem textu
         odraz = trunc( odraz / 2);      // jeden okraj textu
         ocasek = ocasek + odraz + 16;      // pripocitani odrazu k umisteni C
         ocasek = trunc( ocasek / 2);      // prepocet na 2x vetsi Scale
         // velke cislice
         oled.drawStr(odraz, 24, OLED_vystup.c_str());    // vypis hodnoty teploty
         // znacka stupnu C
         NormalFont();      // nastaveni normalni znakove sady
         oled.setScale2x2();      // 2x zvetseni textu (i souradnic)
         oled.drawStr(ocasek + 3, 13, "C");    // vypis znacky C
         oled.undoScale();      // nastaveni puvodniho velikosti a rozliseni
         oled.drawCircle(2*ocasek, 32, 2);      // vnitrni kolecko pro znak stupne
         oled.drawCircle(2*ocasek, 32, 3);      // vnejsi kolecko pro znak stupne
         // maly nadpis
         oled.drawStr(10, 0, "T E P L O T A");      // vypis nadpisu
       } while (oled.nextPage());      // vypis bufferu na OLED

       Teplota_old = Teplota;       // ulozeni teploty pro pristi zjisteni zmeny
     }

     if (cm != cm_old) {      // pokud se meni cislo mereni, cidlo bezi a nastavuje se WatchDog
       WatchDog = millis() + wdInt;      // stale se posouva hranice, do kdy musi prijit dalsi mereni
       cm_old = cm;      // ulozeni cisla mereni pro test zmeny
     } else {      // cislo mereni je stejne jako minule, je treba otestovat, jak dlouho to jiz trva
       if ( WatchDog < millis() ) {    // cislo mereni se nemeni dele nez je zdravo - nekdo vytahl teplomer z USB
         // GoTempEvent::mostRecentEvent.Ready = 0;    // nastavim tedy priznak neaktivniho cidla a spustim cekacku jako na zacatku
         InitSetting();      // uvodni nastevni jako na zacatku - zacne uvodni hledani cidla
       }
    }

   } else {    // cidlo zatim vraci stav Ready = 0 - bud tam neni, nebo se jeste neinicializovalo
     // Test pripojeneho USB
     if( Usb.getUsbTaskState() == USB_STATE_RUNNING ) {    // uz bezi pripojene USB
       byte rcode;
       USB_DEVICE_DESCRIPTOR buf;
       rcode = Usb.getDevDescr( 1, 0, 0x12, ( char *)&buf );    // nacti jeho udaje
       if ((buf.idVendor != 2295)||(buf.idProduct != 2)) {    // ID vyrobce: Vernier -> 2295, ID cidla: Go!Temp -> 0002
         // je to pripojene jine cidlo, tak se merit nebude
         oled.firstPage();      // zacatek naplnovani bufferu pro OLED
         do {
           oled.setScale2x2();      // 2x zvetseni textu (i souradnic)
           oled.drawStr(4, 11, "Go!Temp");      // vypis hlasky
           oled.undoScale();    // nastaveni puvodniho velikosti a rozliseni
           oled.drawStr(20, 0, "Na USB neni");     // vypis nadpisu
           oled.drawLine(105, 1, 106, 0);     // carka na i
         } while (oled.nextPage());
         delay(1000);
         // GoTempEvent::mostRecentEvent.Ready = 0;
         InitSetting();
       }
     } else {    // ceka se, az se USB ozve
       CekaciTecky[w]= znak[ukazatel];
       w++;        // zapocita poradove cislo znaku
       if (w >= 9) {      // pokud se dolni radka zaplnila, zmeni se znak a jeden se znova zleva
         ukazatel = -ukazatel + 1;      // zmena ukazatele do pole nabidky
         w = 0;        // znova pocitame znaku
         logo = false;      // uvodni logo pryc, vypis hlasku problemu
       }

       if (!logo) {      // pokud se na zacatku nezorazuje logo, tak pis tecky
         oled.firstPage();      // zacatek naplnovani bufferu pro OLED
         do {
           oled.setScale2x2();    // 2x zvetseni textu (i souradnic)
           oled.drawStr(0, 10, CekaciTecky);    // vypsani pole cekacich znaku
           oled.undoScale();      // nastaveni puvodniho velikosti a rozliseni
           oled.drawStr(4, 0, hlaska[ukazatel].c_str());    // vypsani stridajici se hlasky pri cekani na cidlo
         } while (oled.nextPage());    // vypis bufferu na OLED
       }

       delay(80);        // cekacka, aby zobrazovani tecek nebyl takovy fofr

     }
   }

}

Výsledné řešení zobrazuje obrázek č. 9. V horní části je vidět výpis čekacích teček a výpis chybové hlášky pomocí dvojité velikosti textu, dole pak vykreslení teploty při měření pomocí. Tato volba je, myslím, slušným kompromisem mezi kavlitou výstupu a zabrané paměti použitého modulu Arduino UNO.

připojení OLED k USB teploměru Vernier Go!Temp
Obr. č. 9 – Výsledné řešení připojení OLED k USB teploměru Vernier Go!Temp.

Obrázek č. 9 nám nejen ukazuje výsledné řešení, ale dokonce nám i naznačuje to, čemu bychom se rádi věnovali v dalším článku, tedy bateriovému napájení a tím pádem i případnému načítání a zobrazení stavu baterie.

A abychom ten náš vývoj zobrazovací jednotky k USB teploměru Vernier Go!Temp opět posunuli trochu blíže k reálně použitelnému zařízení, mohli bychom propříště opustit modul Arduino UNO a přenést náš program do některé z miniaturních podob této platformy.

A jak uvidíme, zase to nebude tak úplně jednoduché, jak by se zprvu zdálo!

Teď ale končím tyto články obvyklou (už téměř pravidelnou) frází: „Ale o tom zase někdy příště…“

Autor článku: Miroslav Panoš

Článek je součástí následující série článků:
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!