Stiskněte "Enter" pro přeskočení obsahu

Přesnost časování pomocí Arduina

2

Před nedávnem jsem narazil na kámošových stránkách na jednoduchý časový spínač založený na Arduinu. Osobně mám takovéto věci radši, než miliontou „meteostanici“ připojenou na WiFi, termostat, který je chytrý asi jako kus bimetalu apod. Opravdu to někdo potřebuje? Tohle je alespoň k něčemu užitečné, i když se jedná pouze o jednoduchý časovač. V tom přesně je krása desek alá Arduino – lze si v momentě vytvořit něco, co ulehčí práci.

I když v tomto konkrétním případě je časování založeno na hlídání interního časovače millis(), napadlo mě zkusit alespoň přibližně změřit nepřesnost časování pomocí pouhého Arduina (chtěl jsem zkusit přesnost v reálné situaci, kdy program něco dělá). Samotné Arduino není příliš vhodné na měření dlouhých časových úseků, nebo použití jako hodiny. V těchto případech je vhodnější použít modul reálného času, který buď jednoduše spouští v přesně nastavených intervalech přerušení procesoru, které lze jednoduše počítat, nebo na nějaké sběrnici (často I2C) poskytuje přímo formátované informace o času. Hodiny založené pouze na počítání proměnné a prodlevě pomocí příkazu delay(ms) nebudou moc přesné – záleží samozřejmě na přesnosti použitého krystalu/rezonátoru.

Přesnost časování lze změřit více způsoby. Možné je připojit výstup Arduina k osciloskopu, nebo čítači a měřit periodu impulzů. Já jsem se rozhodl vytvořit jednoduchý program jak pro PC, tak pro Arduino – v mém případě čínský klon Arduino UNO s procesorem ATmega328. Pomocí těchto programů měřím rozdíl mezi skutečným časem z PC a časem z Arduina. Test probíhá v podstatě jako v reálném prostředí – testuje se dlouhodobá stabilita, včetně rychlosti programu, který je ovlivněný jednotlivými příkazy (každý příkaz může mít jinou délku).

Jenom taková zajímavost – na mojí desce UNO jsou diody RX a TX pro signalizaci sériové komunikace. Při odesílání dat však problikávala dioda RX, což by mělo znamenat Receive Data. Popisek je tedy opačně – je vztažen k počítači a ne přímo k procesoru.

Program v Arduinu čeká na synchronizaci s PC. Jakmile dojde k synchronizaci*, začne počítat v proměnné „počet sekund“. Každou sekundu je do počítače odeslána informace, kolik bylo napočítáno. Problém je v tom, že sekunda v Arduinu nemusí být přesně dlouhá 1 s. A to je přesně ten rozdíl, který chci sledovat.

Program v PC se nejprve připojí k desce pomocí sériového portu. Synchronizuje čas a začne naslouchat na portu. Když přijde datový paket (informace o napočítaném času z Arduina) je tento čas porovnán s interním časovačem počítače. Tento časovač by měl být přesnější než ten v Arduinu, takže bude brán jako referenční. Pokud by Arduino počítalo rychleji (1 sekunda by byla ve skutečnosti kratší) bude Arduino čas v PC předbíhat. Pokud bude naopak počítat pomaleji, bude čas PC předbíhat čas Arduina.
Program poté výsledek reprezentuje v jednoduchém grafu, kde lze přehledně vidět např. postupné zpožďování. Dále je možné změny počítadel přehledně sledovat v tabulce, kde je udán přesný čas (referenčního PC), změna a počet sekund od startu.

V jednotlivých grafech je pěkně vidět, jaký vliv na měření času má délka a náročnost kódu. A také, že pomocí různých technik lze docílit úplně jiné přesnosti.

* Synchronizace samotná taky nějakou chvíli trvá a tak do prvního měření může vnést nepřesnost. Je proto lepší porovnávat čas od první změny dále.

Popis aplikace pro PC

Aplikace pro PC slouží ke startu Arduina, přijímání dat z něho a jejich reprezentaci. V grafu lze zobrazit čas PC (počet celých sekund od startu komunikace s Arduinem), čas Arduina (číslo přijaté po sériové lince) a rozdíl těchto časů. V tabulce se poté zobrazuje v jaký čas (standardní čas a počet sekund) došlo ke startu a ke změně načítaných časů.

V okně níže jde krásně vidět změna času, pokud se příkaz delay(1000) nahradí příkazem delay(900). Program místo 1 sekundy čeká 900 ms – Arduino se předchází. Každou sekundu je o 100 ms rychlejší = za 9 sekund napočítá 10. Lze to vidět v tabulce. Kromě prvního časového údaje, který je ovlivněn synchronizací se čas mění každých 9 sekund.

S takto jednoduchým programem a na takto krátkém čase a s hrubým rozlišením (přece jenom, jedna sekunda je na procesor dost času) se žádná malá změna nepřesnosti neprojeví. Je tedy nutné Arduino nastavit a měřit delší čas.

Popis programu

Program pro Arduino – velmi jednoduchý časovač s příkazem delay(1000)

Jak jsem již psal, program může být velice jednoduchý. Nejprve je definována proměnná, ve které se bude počítat počet sekund. Poté je odstartována komunikace po sériové lince a čekací smyčka na příchozí komunikaci z PC. Dokud nepřijde startovací znak z počítače, program se motá ve smyčce while.

Po odstartování program skočí do smyčky loop(), kde nejdřív odešle naměřený čas, inkrementuje proměnnou a čeká nastavený čas. I ideálním případě (pokud by odesílání dat a inkrementace proměnné nezabírala žádný čas) by se čas řídil pouze příkazem delay().

V takto jednoduchém programu, kde je minimum příkazů (inkrementace proměnné, odesílání na sériovou linku) se program zpožďuje pouze minimálně. Samozřejmě, nějaké zpomalení nastane, protože i pár příkazů zabere nějaký ten strojový čas.

unsigned long timeStamp;
void setup() {
Serial.begin(9600);
while(Serial.available() == 0){
//wait for synchronisation
//it must be last command
}
}
void loop() {
Serial.print(timeStamp); //send time
timeStamp++; //add time
delay(1000); //delay 1s
}

Z grafu je evidentní, že ke změnám stavu nedocházelo skokově, ale několikrát došlo k překlápění. Je to dáno pomalou změnou času. První zpoždění nastalo mezi 7818. a 8108. sekundou. To je po cca 132 minutách, 43 sekundách. První záznam se však kvůli startu časování nedá brát jako směrodatný. Druhá změna nastala mezi 20666. a 20980. sekundou. To je po cca 12860 sekundách, což dělá 214 minut, 20 sekund. Samozřejmě, toto je malý vzorek, ze kterého nelze vycházet zcela přesně. Delší měření by bylo lepší; toto měření probíhalo téměř 6 hodin.

Čas Arduina při zpoždění pomocí delay(1000);

Program pro Arduino – časovač s příkazem delay(1000) a obsluha LCD

Výše uvedený příklad je samozřejmě v podstatě k ničemu. To že umí odesílat pouze informaci na sériový port je poněkud samoúčelné. Ve skutečnosti nekoná žádnou práci. Program by mohl např. zobrazovat na displeji čas od spuštění ve formátu hh:mm:ss. To už je něco, co by se dalo třeba k něčemu využít. S jednou podmínkou a výstupem by to mohl být např. odpočet, nebo časovač, kde po nastaveném čase dojde třeba k sepnutí relé…
Zde se odešlou informace na sériový port, převede se počet sekund na hodiny, minuty a sekundy a tato data se vykreslí na LCD 2×16 znaků. Aby byla prováděná operace o trošku delší vykresluje se do prvního řádku stále dokola stejný text. Je to zbytečné, ale řekněme, že by to mohlo sloužit místo dalších řídících struktur programu. Po vykreslení je samozřejmě obligátní čekání jednu sekundu.

#include <LiquidCrystal.h> 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
unsigned long timeStamp;
int hour, minute, second;
void setup() {
Serial.begin(9600);
Setings(); // seting up
while(Serial.available() == 0){
//wait for synchronisation
//it must be last command
}
}
void loop() {
Serial.print(timeStamp); //send time time
Stamp++; //add time
DoSomething(); //optional programm's delay
delay(1000); //delay 1s
}
void DoSomething(){
hour = timeStamp / 3600;
minute = timeStamp / 60 % 60;
second = timeStamp % 60;
lcd.setCursor(0, 0);
lcd.print("NACTENY CAS:    ");
lcd.setCursor(0, 1);
//hodiny
if (hour < 10) {
lcd.print("0");
}
lcd.print(hour);
lcd.print(":");
//minuty
if (minute < 10) {
lcd.print("0");
}
lcd.print(minute);
lcd.print(":");
//sekundy
if (second < 10) {
lcd.print("0");
}
lcd.print(second);
}
void Setings(){
lcd.begin(16, 2);
lcd.setCursor(0, 0);
lcd.print("CEKA NA START...");
}

Na výsledném grafu je už zřetelné zpomalování měřeného času Arduinem za časem počítače. Podle tabulky a grafu je zpoždění jednu sekundu za každých cca 130 sekund. Protože se provádí stále stejná operace, je zpoždění téměř pravidelné. Šlo by tedy kompenzovat prostým snížením hodnoty delay() o 7,7 ms (1 s / 130 s = 0,00769 s). Pokud by docházelo k větvení programu podle nějakých vnějších vlivů (vstupy, vnitřní proměnné atd.), nelze tímto způsobem kompenzaci přesně provést.

Čas Arduina při vykreslování na LCD

Po změně hodnoty čekání z 1000 na 992 ms vypadá graf stability času jako na obrázku níže. Při stejné době měření (cca 30 minut) je evidentní zpřesnění časovače. Čas se oproti počítači nerozešel ani o sekundu.

Čas Arduina při vykreslování na LCD s provedenou kompenzací příkazu delay()

Program pro Arduino – časovač pomocí millis() a obsluha LCD

Již v článku Kusy kódu k Arduinu jsem v části Čekání bez příkazu delay(); Multitasking na Arduinu zmínil nevýhody příkazu delay(). Pokud je použit tento příkaz neprovádí se po nastavenou dobu žádné příkazy.
V kontextu tohoto článku by se dala zmínit další nevýhoda a to sčítání doby provádění programu a doby čekání. Délka smyčky programu uvedeného výše se skládá z provádění programu (zde 7,7 ms) a čekání (1000 ms). Doba provádění programu však může být pokaždé jiná. Nelze tedy jednoduše vždy spočítat čas, který je třeba kompenzovat.
Použitím odlišné konstrukce programu lze zcela eliminovat použití příkazu delay(). Místo něho lze měřit čas pomocí příkazu millis(), porovnávat naměřený čas s nastaveným časem a ve vhodný okamžik provést nějakou operaci – třeba inkrementaci proměnné a vykreslování na displej. Na rozdíl od příkladů výše se čas provádění operace nepřičítá k nastavenému zpoždění, ale z nastavené doby cyklu (třeba 1 s) si provádění operací „ukousne“ požadovaný čas. Zbytek času je program víceméně nečinný – pouze se kontroluje, jestli se má znovu provést operace. Tímto stylem lze provést jakýsi „pseudomultitasking“.

#include <LiquidCrystal.h> 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
unsigned long timeStamp;
unsigned long lastTime;
unsigned long presentTime;
int hour, minute, second;
void setup() {
Serial.begin(9600);
Setings(); // seting up
while(Serial.available() == 0){
//wait for synchronisation
//it must be last command
}
}
void loop() {
presentTime = millis(); //uložení aktuálního času do proměnné
if (presentTime - lastTime >= 1000) {
//pokud je čas od poslední změny větší, nebo roven 1000 ms, proveď příkazy
lastTime = presentTime; //ulož čas provedení
Serial.print(timeStamp); //send time
timeStamp++; //add time
DoSomething(); //optional programm's delay
}
}
void DoSomething(){
hour = timeStamp / 3600;
minute = timeStamp / 60 % 60;
second = timeStamp % 60;
lcd.setCursor(0, 0);
lcd.print("NACTENY CAS: ");
lcd.setCursor(0, 1);
//hodiny
if (hour < 10) {
lcd.print("0");
}
lcd.print(hour);
lcd.print(":");
//minuty
if (minute < 10) {
lcd.print("0");
}
lcd.print(minute);
lcd.print(":");
//sekundy
if (second < 10) {
lcd.print("0");
}
lcd.print(second);
}
void Setings(){
lcd.begin(16, 2);
lcd.setCursor(0, 0);
lcd.print("CEKA NA START...");
}

Výsledek měření tohoto kódu můžete vidět na obrázku níže. Je patrné, že se Arduino oproti skutečnému času předbíhá. Z toho plyne, že krystal, který řídí rychlost procesoru má vyšší frekvenci než jmenovitou (většinou 16 MHz). Pokud pomineme první změnu, která nastala za 3455 sekund (57 min, 35 sec) a byla ovlivněna startem jsou další změny po 6632 (110 min, 32 sec), po 6726 (112 min, 6 sec) a po 6786 (113 min, 6 sec) sekundách. Delší čas měření by odchylku zpřesnil, ale i tak je vidět, že se Arduino předbíhá průměrně o 1 s za  111 minut a 54 sekund. Pokud by byla odchylka stále stejná, šla by podobně jako u příkazu delay() kompenzovat vhodnou úpravou kódu.
Na obrázku lze také vidět „zákmity“ kdy se čas neměnil skokově, ale odchylka času mezi Arduinem a PC několikrát oscilovala. To je zapříčiněno pomalou změnou času z Arduina od toho referenčního.

Čas Arduina při vykreslování na LCD s časováním pomocí millis()

Jak je patrné z uvedených příkladů, je lepší svěřit přesný čas specializovaným obvodům, jako jsou DS3231, nebo podobný. Zmiňovaný obvod DS3231, který je často používaný jako modul reálného času pro Arduino má datovou sběrnici (I2C) pro nastavení a vyčtení času a „alarmový“ výstup pro externí přerušení. Obvod má vlastní oscilátor, takže jeho přesnost je dle datasheetu ±2 ppm, což je nepřesnost cca 1 sekunda za 6 dnů. Další výhodou je bateriové zálohován, takže pro udržení správného času nemusí být Arduino připojeno celou dobu k napájení. No, mám tu hodiny s tímto obvodem už docela dlouho a stále ukazují správný čas.
Další informace je možné nalézt buď v diskuzi pod článkem, nebo můžete navštívit Arduino Fórum, kde je o článku založeno vlákno.

  1. Milan Milan

    Nápad je to určitě zajímavý, ale výsledek se nedá zevšeobecnit.

    Veškeré časování v Arduinu je odvozeno od základní frekvence krystalu.

    Důležité je, že každý krystal má svou toleranci.

    Frekvence krystalu navíc závisí i na teplotě.
    Například hodně přesné generátory mají ty krystaly vyhřívané na konstantní teplotu.
    Nebo přesné RTC obvody mají vnitřní teploměr a změřenou teplotu používají pro dolaďování frekvence pomocí vnitřního pole přídavných kondenzátorů (DS3232)

    A dokonce se frekvence krystalu trochu mění i s časem (krystaly stárnou).

    Takže výsledky, které jsi naměřil jsou sice užitečné a můžeš je využít pro přesné doladění tvého časovače, ale jiné Arduino bude mít ty výsledky odlišné – a možná i výrazně.

    Určitě by bylo zajímavé porovnat například originální Arduino a čínský klon. Tam bych očekával opravdu velké rozdíly (ale možná že Číňani překvapí).

  2. Peťan: Jak jsem psal, jde pouze o přibližné měření. Prostě Arduino, tak jak je. Spíše jde o použití v „reálné“ aplikaci. Jak říkáš, určitě záleží na konkrétní desce. Chtěl jsem hlavně ukázat, to zpoždění samotným kódem a pak čekáním s příkazem delay().
    Mám originální Arduino Nano, mohl bych to zkusit změřit i na něm.
  3. Zdarec, oprav si pls. odkaz na začátku na ty mý časovače, nově na: https://www.arze.cz/casovy_spinac_2_12h.html
    Dík a měj se 😉

    Peťan: Zdar, odkaz jsem upravil 😉 BTW, docela mi na té stránce chybí nějaký výraznější tlačítko na návrat na domovskou stránku. To tlačítko „Home“ je jakože dobrý, ale až dole. Lepší by bylo rovnou nějaký menu, nebo pruh trvale připnutej nahoře na stránce.

Napsat komentář: Milan Zrušit odpověď na komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *