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.
V dnešním článku bychom si chtěli především ukázat dvě další věci:
- Jak poznat, že je k modulu Arduino skutečně připojen USB teploměr Go!Temp, nikoliv jiné USB zařízení.
- 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_
.
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::
(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.
.
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.
. Pokud zařízení ohlásí stav USB_STATE_
, můžeme přistoupit k načtení identifikačního čísla připojeného USB zařízení. To provedeme pomocí funkce Usb.
, údaje načteme do proměnné buf
(třída USB_DEVICE_
). 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).
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).
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::
je stále 0.
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::
je stále 0, ale již vrací stav USB_STATE_
) 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.
Strukturu výše uvedeného programu asi nejlépe osvětlí následující vývojový diagram (obr. 6):
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! 😊
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_
. 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_
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.
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.
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ě…“