Vylepšené načítání USB teploměru Vernier Go!Temp modulem Arduino
Aneb nový návrat na staré místo činu.
Proč se po letech vracet ke Go!Temp?
Před několika lety jsme se zabývali možností načítání USB teploměru Vernier Go!Temp Modulem Arduino. Problém se nám tehdy podařilo vyřešit na principu úpravu kódu původně určenému pro načítání USB joysticku. Tehdejší řešení bylo funkční, ale upřímně řečeno to bylo takové řešení stylu: „Funguje to, prodáme to!“
Dnes bychom se chtěli k problematice načtení teploty z USB teploměru Vernier Go!Temp trochu vrátit a zkusit zde prezentovat nový program, který nyní vychází z několika poznatků o komunikaci USB HID, které jsme za ta léta nasbírali.
- Důležitá oprava z předchozích verzí (Ošetření záporných teplot):
- V předešlých článcích a verzích kódu byla v datové struktuře
rawTemppoužita beznaménková proměnnáuint16_t. Čidlo Vernier Go!Temp však v HID režimu vrací 16bitovou hodnotu se znaménkem (v jednotkách 1/128 °C). Beznaménkový datový typuint16_ttak při poklesu teploty pod 0 °C způsobil přetečení na nesmyslně velkou kladnou hodnotu. - V tomto (i předešlých článcích zpětně) je již chyba opravena změnou na znaménkový typ
int16_t rawTemp.
Nebudeme zde již prezentovat celé řešení zobrazovací jednotky, zaměříme se jen na načtení teploty z teploměru Go!Temp. Pochopitelně, pokud níže uvedený program doplníme obsluhou OLED displeje nebo jiné zobrazovací jednotky, získáme tak základ pro nový typ zobrazovací jednotky k tomuto USB teploměru. To již ale necháme na čtenáři – koneckonců potřebné informace k tomu může nalézt v předchozích článcích.
Pro potřeby tohoto článku používáme modul Arduino UNO doplněný rozšířením USB Shield, ale uvedený kód lze podobně jako předešlé kódy použít i na modulu Arduino Pro Mini s příslušným USB Shieldem.
V čem je nový přístup lepší?
Jak jsme naznačili v úvodu, původní kód byl z hlediska načítání USB zařízení jen určitou funkční úpravou programu pro USB joystick. Způsob komunikace Ardino–Go!Temp měl díky tomu několik nedostatků:
- Komunikace s USB shieldem byla zbytečně komplikovaná
- USB teploměr Vernier Go!Temp běžel v režimu, jako když je připojen k USB počítače, ale měřicí aplikace jej zatím nenalezla.
- Díky neúplné aktivaci teploměru nebyla odesílaná data korigována vnitřními kalibračními konstantami (naše domněnka)
- Program byl řešen systémem vzájemně vnořených podmínek s několika stavovými proměnnými, které suplovaly stavy dostupné z knihovny USB Shieldu.
Výše uvedené nedostatky jsme se pokusili nyní vyřešit následující úpravou:
- Zjednodušení struktury načítání dat a vytvoření jednodušší struktury HID parseru.
- Po připojení USB teploměru a jeho identifikaci dochází k odeslání inicializační sekvence, která převede teploměr Go!Temp do měřicího stavu (zelená barva stavové LED).
- Získaná data jsou nyní načítána v jednotkách, které činí 1/128 stupně Celsia (což je patrně důsledek inicializace teploměru, respektive přesnější korekce původních dat).
- Program je nyní řešen na principu stavového automatu (FSM), což je pro tyto programy mnohem vhodnější.
Nový zdrojový kód
USB a HID komunikace
Pro přístup k USB Shiledu se využívá objekt řídicího řadiče obvodu MAX3421E a pro komunikaci s USB teploměrem Vernier GoTemp, který komunikuje ve standardu HID, i objekt univerzálního HID obladače:
USB Usb; // Objekt řídicího řadiče MAX3421E
HIDUniversal Hid(&Usb); // Univerzální HID ovladač
Pro načítání dat z USB teploměru byla zavedena nová datová struktura:
struct GoTempData {
uint8_t ready; // 1. bajt: Stavový příznak (0x01 = nová platná data)
uint8_t sampleNum; // 2. bajt: Pořadové číslo naměřeného vzorku (0–255)
int16_t rawTemp; // 3. a 4. bajt: 16bitová RAW hodnota z ADC převodníku
};
Stále se jedná o trojici dat, kdy první bajt představuje stavový příznak, určující, zda jsou nová data platná (0x01). Druhý bajt udává pořadové číslo (0–255), které se dá využít pro identifikaci daného vzorku nebo jej lze využít ke kontrole, zda teploměr stále odesílá data. Poslední (16bitová) hodnota odpovídá výstupní hodnotě z AD převodníku teploměru. Jelikož čidlo Vernier Go!Temp v HID režimu vrací 16bitovou hodnotu se znaménkem v jednotkách 1/128, musí být tato proměnná deklarována int16_t. Jinak by teploty pod 0 byly ukládány jako vysoká kladná hodnota. Z hodnoty proměnné rawTemp budeme dále získávat
hodnotu teploty ve stupních Celsia a ukládat do proměnné temperatureC.
Pro předávání dat mezi HID parserem a hlavní smyčkou loop(), ve které běží hlavní část programu, využijeme proměnnou current, která bude deklarována právě jako výše uvedená struktura. Tato proměnná je nastaveno volatile, neboť do ní paralelně přistupuje HID parser, pracující na úrovni přerušení.
Pro vytěžení potřebných dat z HID paketů je v programu definována třída GoTemp:
class GoTemp
public:
virtual void Parse(
// Bezpečnostní kontrola délky paketu a platnosti ukazatele
if (buf == NULL || len < sizeof(
// Bajt buf[0] == 0x01 označuje platný datový paket s naměřenou teplotou
if (buf[0] == 0x01) {
current
current
// Složení 16bitového RAW čísla ze dvou 8bitových bajtů (Little-Endian)
current
// Přepočet RAW hodnoty na stupně Celsia (°C)
temperatureC = currentReading.rawTemp / 128.0;
}
}
};
Konkrétní instanci této třídy pro nastavení USB a HID komunikace pak představuje proměnná Parser:
GoTemp
Výchozí nastavení USB a HID komunikace najdeme v úvodní funkci setup(). Nejdřív jde o samotnou inicializaci USB Shieldu:
// Inicializace USB Host Shieldu
Serial.
if (
Serial.
while(1); // HW chyba shieldu -> zastavení programu
}
Pokud dojde k chybě, nemá cenu dále pokračovat, program tedy končí v nekonečné smyčce.
Druhým krokem je registrace HID parseru:
// Registrace HID parseru na rozhraní 0
if (!Hid.
Serial.
} else {
Serial.
}
Po úspěšném nastavení USB A HID komunikace již lze se dotazovat USB Shieldu na stavy USB rozhraní. K tomu dochází pravidelným voláním metody Usb, která běží asynchronně na pozadí a voláním HID parseru naplňuje globální pracovní proměnné: current a temperatureC.
Vyhodnocování stavů – stavový automat
Jednotlivé aktuální stavy programu jsou uložené ve výčtovém typu Connection:
enum ConnectionState {
STATE_
STATE_
STATE_
STATE_
STATE_
};
Program rozlišuje stav STATE_, kdy čeká na připojení USB zařízení. Je to výchozí stav celého programu, do kterého se vracíme, pokud není připojeno zařízení – například, pokud v hlavní smyčce program shledá, že tento stav není nastaven a zároveň není připojeno USB zařízení, vyhodnotí to jako odpojení USB zařízení.
Stav STATE_ nastává v okamžiku připojení USB zařízení. Je načtena dvojice údajů idVendor a idProduct, která charakterizuje výrobce a konkrétní USB zařízení. Pokud tato kombinace odpovídá firmě Vernier (buf.) a USB teploměru Go!Temp (buf), přechází program do stavu STATE_, jinak přechází do stavu STATE_.
Ve stavu STATE_ dojde k odeslání příkazu „SetReport“ (sekvence 0x1D, 0x80, 0x10…), který nastaví teploměr
Go!Temp do stavu měření – stavoví LED se rozsvítí zeleně).
uint8_t result = Hid.
Jestliže teploměr toto nastavení potvrdí (if (result == 0)) přechází teploměr do měřicího stavu STATE_, počká cca 0,5 s a pokusí se o odeslání příkazu „SetReport“ znova.
Stav STATE_ je již stavem, ve kterém jsou zachytávána data z HID parseru a vypisována na sériový výstup (do sériového monitoru). K výpisu dat (čísla vzorku, data z ADC a odpovídající teplotě) pochopitelně dochází jen tehdy, pokud jsou data platná (1 bajt paketu je nenulový)
if (currentReading.ready != 0) { … }
Poslední stav (nazvaný STATE_) je stavem ryze vyčkávacím, ve kterém program setrvává po zjištění jiného USB zařízení, než je USB teploměr Vernier Go!Temp. Program tak čeká, až bude toto zařízení odpojeno.
Následující obrázek ukazuje průběh stavů po startu programu, připojení USB flash paměti, která simuluje připojení jiného USB zařízení, než je teploměr Go!Temp. Následuje odpojení tohoto zařízení a připojení USB Teploměru Vernier Go!Temp. Následuje úspěšná identifikace, inicializace teploměru a následný proces měření.
Program samozřejmě musí testovat přítomnost USB zařízení i v průběhu měření, neboť nic nebrání náhlému odpojení USB teploměru Go!Temp, případně připojení opět jiného USB zařízení.
Stav připojení USB zařízení je testován v hlavní smyčce
uint8_t usbTask
// Test fyzického odpojení USB zařízení
if (
if (current
reset
}
return;
}
Dojde-li k odpojení, tedy metoda Usb. nevrací stav USB_, a stav programu není ve stavu vyčkávání na USB (STATE_), muselo dojít k odpojení. Program prostřednictvím funkce reset přechází do stavu STATE_.
Situaci náhlého vytažení teploměru při úspěšném měření (a připojení jiného USB zařízení) zachycuje následující obrázek.
Drobnou reminiscencí předešlých verzí programu je proměnná last, ve které se ukládá okamžik posledního vzorku. U tohoto času je kontrolováno, zda poslední přijatá hodnota teploty není starší než stanovený čas WATCHDOG_ (nastaveno na 2 s):
// Watchdog: Pokud po nastavenou dobu nepřijdou data, hlásíme odpojení
if (
reset
}
break;
Tato část kódu (společně s potřebnými proměnnými) je v programu vlastně zbytečná. Jedná se jen o jakousi pojistku, která hlídá provoz programu a komunikaci s USB teploměrem. Tato část kódu by měla zareagovat na situaci, kdy USB komunikace měla fungovat (tedy bude vracet stav USB_), ale data z nějakého důvodu nebudou z teploměru Go!Temp přicházet – například jakési „zaseknutí“ softwaru v teploměru. Mělo by v tomto případu dojít prostřednictvím
funkce reset k přechodu programu do stavu ConnectionState()STATE_ a jeho detekci a novému zaslání úvodního příkazu „SetReport“, což by jej mohlo resetovat.
Pravdou je, že jsme při zkušebním provozu a dalším testování zatím žádnou takovou situaci nezaznamenali. Je tedy možné, že tato část kódu je v programu zbytečná a je zde jen kvůli naší přílišné upjatosti a zbytečné snaze ošetřit chybu, ke které nedochází – nebo, jak říká docent Chocholoušek, dochází jedenkrát za deset let.
Výpis kódu programu:
/*
=============================================================================
PROGRAM: Ukázkový program pro USB teploměr Vernier Go!Temp
=============================================================================
Hardware:
- Arduino UNO (nebo kompatibilní deska)
- USB Host Shield v2.0 (čip MAX3421E)
- USB teploměr Vernier Go!Temp
=============================================================================
*/
#include // Knihovna pro USB Host Shield (MAX3421E)
#include // Třída pro univerzální HID zařízení
// -----------------------------------------------------------------------------
// GLOBÁLNÍ OBJEKTY A KONSTANTY
// -----------------------------------------------------------------------------
USB Usb; // Objekt řídicího řadiče MAX3421E
HIDUniversal Hid(&Usb); // Univerzální HID ovladač
// Identifikátory výrobce a zařízení Vernier Go!Temp
const uint16_t GOTEMP_VENDOR_ID = 2295; // 0x08F7 - Vernier Software & Technology
const uint16_t GOTEMP_PRODUCT_ID = 2; // 0x0002 - Go!Temp
// Inicializační příkaz pro spuštění čidla, interní korekce a rozsvícení zelené LED
const uint8_t startReport[8] = {0x1D, 0x80, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
// Časový limit (v ms) pro detekci výpadku přenosu dat z teploměru
const unsigned long WATCHDOG_TIMEOUT = 2000;
// -----------------------------------------------------------------------------
// DATOVÁ STRUKTURA PRO MAPOVÁNÍ USB HID REPORTU
// -----------------------------------------------------------------------------
struct GoTempData {
uint8_t ready; // 1. bajt: Stavový příznak (0x01 = nová platná data)
uint8_t sampleNum; // 2. bajt: Pořadové číslo naměřeného vzorku (0–255)
int16_t rawTemp; // 3. a 4. bajt: 16bitová RAW hodnota z ADC převodníku
};
// Sdílené globální proměnné pro předávání dat mezi parserem a smyčkou loop()
volatile GoTempData currentReading;
float temperatureC = 0.0;
// -----------------------------------------------------------------------------
// DEKODÉR USB HID PAKETŮ (REPORT PARSER)
// -----------------------------------------------------------------------------
class GoTempReportParser : public HIDReportParser {
public:
virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) override {
// Bezpečnostní kontrola délky paketu a platnosti ukazatele
if (buf == NULL || len < sizeof(GoTempData)) return;
// Bajt buf[0] == 0x01 označuje platný datový paket s naměřenou teplotou
if (buf[0] == 0x01) {
currentReading.ready = buf[0];
currentReading.sampleNum = buf[1];
// Složení 16bitového RAW čísla ze dvou 8bitových bajtů (Little-Endian)
currentReading.rawTemp = (int16_t)(buf[2] | ((int16_t)buf[3] << 8));
// Přepočet RAW hodnoty na stupně Celsia (°C)
temperatureC = currentReading.rawTemp / 128.0;
}
}
};
GoTempReportParser Parser; // Instance dekodéru
// -----------------------------------------------------------------------------
// STAVY A PROMĚNNÉ PRO ŘÍZENÍ SPOJENÍ
// -----------------------------------------------------------------------------
enum ConnectionState {
STATE_WAIT_FOR_DEVICE, // Čeká se na fyzické připojení USB zařízení
STATE_CHECK_DEVICE, // Zařízení je připojeno, provádí se ověření VID/PID
STATE_INIT_GOTEMP, // Zařízení je Go!Temp, posílá se inicializační příkaz
STATE_MEASURING, // Teploměr je spuštěn a probíhá měření
STATE_REJECTED_DEVICE // Připojeno jiné USB zařízení (čeká se na odpojení)
};
ConnectionState currentState = STATE_WAIT_FOR_DEVICE;
unsigned long lastSampleTime = 0;
// -----------------------------------------------------------------------------
// POMOCNÁ FUNKCE PRO RESETOVÁNÍ STAVU PŘI ODPOJENÍ ZAŘÍZENÍ
// -----------------------------------------------------------------------------
void resetConnectionState(const char* reason) {
if (currentState != STATE_WAIT_FOR_DEVICE) {
Serial.print(F("\n[USB] Odpojeno / Zmena stavu: "));
Serial.println(reason);
Serial.println(F("[USB] Cekam na pripojeni USB zarizeni..."));
}
currentState = STATE_WAIT_FOR_DEVICE;
currentReading.ready = 0;
}
// -----------------------------------------------------------------------------
// INICIALIZAČNÍ FUNKCE SETUP
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
while (!Serial); // Čekání na otevření sériového monitoru
Serial.println(F("=================================================="));
Serial.println(F(" Arduino UNO + USB Host Shield: Vernier Go!Temp"));
Serial.println(F("=================================================="));
// Inicializace USB Host Shieldu
Serial.println(F("[INIT] Inicializuji USB Host Shield..."));
if (Usb.Init() == -1) {
Serial.println(F("[CHYBA] USB Host Shield se nespustil!"));
while (1); // HW chyba shieldu -> zastavení programu
}
// Registrace HID parseru na rozhraní 0
if (!Hid.SetReportParser(0, &Parser)) {
Serial.println(F("[CHYBA] Nepodarilo se nastavit HID Report Parser."));
} else {
Serial.println(F("[INIT] USB Shield i HID parser pripraveny."));
}
Serial.println(F("[USB] Cekam na pripojeni USB zarizeni..."));
}
// -----------------------------------------------------------------------------
// HLAVNÍ SMYČKA LOOP
// -----------------------------------------------------------------------------
void loop() {
// Pravidelná obsluha USB sběrnice (vyvolá Parse() při příjmu dat)
Usb.Task();
uint8_t usbTaskState = Usb.getUsbTaskState();
// Test fyzického odpojení USB zařízení
if (usbTaskState != USB_STATE_RUNNING) {
if (currentState != STATE_WAIT_FOR_DEVICE) {
resetConnectionState("Fyzicke odpojeni USB kabelu.");
}
return;
}
// Stavový automat pro správu připojení a měření
switch (currentState) {
// Po fyzickém připojení přepneme do ověření typu zařízení
case STATE_WAIT_FOR_DEVICE:
currentState = STATE_CHECK_DEVICE;
break;
// Test připojení USB – kontrola, zda se jedná o Vernier Go!Temp
case STATE_CHECK_DEVICE: {
USB_DEVICE_DESCRIPTOR buf;
uint8_t rcode = Usb.getDevDescr(1, 0, sizeof(USB_DEVICE_DESCRIPTOR), (uint8_t *)&buf);
if (rcode == 0) {
Serial.print(F("[USB] Detekovano zarizeni -> Vendor ID: "));
Serial.print(buf.idVendor);
Serial.print(F(" (0x"));
Serial.print(buf.idVendor, HEX);
Serial.print(F("), Product ID: "));
Serial.print(buf.idProduct);
Serial.print(F(" (0x"));
Serial.print(buf.idProduct, HEX);
Serial.println(F(")"));
// Kontrola VID (2295 = Vernier) a PID (2 = Go!Temp)
if (buf.idVendor == GOTEMP_VENDOR_ID && buf.idProduct == GOTEMP_PRODUCT_ID) {
Serial.println(F("[USB] -> Nalezen spravny teplomer Vernier Go!Temp."));
currentState = STATE_INIT_GOTEMP;
} else {
Serial.println(F("[CHYBA] -> Pripojene USB zarizeni NENI teplomer Go!Temp!"));
Serial.println(F("[USB] Cekam na pripojeni spravneho zarizeni..."));
currentState = STATE_REJECTED_DEVICE;
}
}
break;
}
// Inicializace Go!Temp – odeslání příkazu (startReport)
case STATE_INIT_GOTEMP: {
Serial.println(F("[GOTEMP] Odesilam inicializacni prikaz (SetReport)..."));
// Odeslání reportu přes HID rozhraní
uint8_t result = Hid.SetReport(0, 0, 2, 0, 8, (uint8_t*)startReport);
if (result == 0) {
Serial.println(F("[GOTEMP] -> Prikaz odeslan, potvrzeno! Zelena LED sviti."));
Serial.println(F("[GOTEMP] Zahajuji mereni teploty...\n"));
lastSampleTime = millis();
currentState = STATE_MEASURING;
} else {
Serial.print(F("[CHYBA] Selhalo odeslani inicializacniho prikazu. Kod: "));
Serial.println(result);
delay(500); // Čekání před opakovaným pokusem
}
break;
}
// Probíhá aktivní měření a výpis na sériový port
case STATE_MEASURING: {
// Kontrola, zda parser zpracoval nový vzorek
if (currentReading.ready != 0) {
Serial.print(F("Vzorek #: "));
Serial.print(currentReading.sampleNum);
Serial.print(F(" | RAW: "));
Serial.print(currentReading.rawTemp);
Serial.print(F(" | Teplota: "));
Serial.print(temperatureC, 2);
Serial.println(F(" °C"));
// Vynulování příznaku a aktualizace časovače watchdogu
currentReading.ready = 0;
lastSampleTime = millis();
}
// Watchdog: Pokud po nastavenou dobu nepřijdou data, hlásíme odpojení
if (millis() - lastSampleTime > WATCHDOG_TIMEOUT) {
resetConnectionState("Vyprsel watchdog timeout (teplomer neposila data).");
}
break;
}
// Připojeno cizí USB zařízení – čekáme na jeho odpojení
case STATE_REJECTED_DEVICE:
// Neaktivní stav – čeká se, až uživatel odpojí cizí USB zařízení
break;
}
}
Závěr
Dnešním článkem jsme snad opustili dřívější mírně provizorní řešení odvozené z joystickového ovladače. Přechod k čitelnému stavovému automatu (FSM) a přímé HID komunikaci přinesl nejen stabilnější provoz, ale především korektní inicializaci teploměru Vernier Go!Temp a plně korigovaná data s přesností na 1/128 °C. Vznikl tak solidní základ, na kterém můžete ihned stavět vlastní projekty – kupříkladu autonomní meteostanici, bateriový datalogger s OLED displejem nebo jen neobyčejný laboratorní teploměr.
Plánujete-li rozšíření modulu Arduino s připojeným USB teploměrem Go!Temp o Ethernet, Wi-Fi či Bluetooth, myslete na to, že při použití standardních knihoven u klasického Arduina UNO (ATmega328P) rychle narazíte na limity paměti RAM. Na druhou stranu to může být skvělá příležitost pro bastlířskou výzvu. Spíš bychom ale doporučili zaměřit se na jiné moduly a vyzkoušet si tak portaci na výkonnější desky jako jsou Arduino Mega, ESP32 nebo RP2040 s USB Host rozhraním.
