Arduino porady i przykłady
Programy Rejestracja czasu pracy
Rig And Log
Zegar bilardowy
GET HTTPS4
-
Czytnik kart RFID w domofonie
Eeprom Reader Windows
Eeprom Reader Android
-
PDF Printer
PDF Cutter
RDP Klient
DIP Switch Konfigurator
ELFRO GeoFencing
-
ESP8266 FAN Controller
Arduino porady i przykłady
IO22D08 Library
-
Edytorek Zdjęć
Kill Process
LAN SCANNER
-
Backup serwera i plików
Większość osób zainteresowanych mikrokontrolerami na pewno słyszała o Arduino – platformie edukacyjnej zawierającej szereg płytek rozwojowych które w łatwy sposób można użyć we własnych projektach.
W sieci powstało szereg artykułów, przykładów, rozwiązań na ten temat.
W niniejszym dziale przedstawię kilka prostych aczkolwiek nieoczywistych porad które pomogą w rozpoczęciu programowania w Arduino:
1. Funkcja delay - czyli jak i na co czekamy
2. Obsługa przetwornika ADC - pomiar napięcia teoria i praktyka
1. Funkcja delay - czyli jak i na co czekamy
Często wykorzystywaną funkcją jest delay() - czyli poczekaj wybraną ilość milisekund.
Przykładowy szkic arduino mrugający wbudowaną diodą :
void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(1000); digitalWrite(LED_BUILTIN, LOW); delay(1000); }
Program ustawia wyjście kontrolera na 1 digitalWrite(LED_BUILTIN, HIGH); (dioda świeci) czeka sekundę - delay(1000) potem ustawia na 0 dioda gaśnie i czeka sekundę. Kod zawarty w funkcji loop wykonuje się w kółko więc po zakończeniu kodu wykona się on od nowa.
Program jest bardzo prosty problem powstaje wtedy gdy chcemy zrobić coś dodatkowo. W tym programie przez większość czasu mikrokontroler czeka. Funkcja delay blokuje także wykonywanie kodu niektórych bibliotek np AsyncWeb Serwer.
Należałoby zamienić taką funkcje na funkcje która nie blokuje wykonywania innych czynności. do tego posłużymy się funkcją millis() Zwraca ona ilość milisekund od uruchomienia mikrokontrolera.
Przykładowy nieblokujący kod:
void setup() { pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } unsigned long miliSekundy = 0; bool stanLedy = HIGH; void loop() { if (millis() - miliSekundy > 1000) { miliSekundy = millis(); stanLedy = !stanLedy; digitalWrite(LED_BUILTIN, stanLedy); }
//.... pozostały kod programu }
W tym kodzie sprawdzamy ile milisekund upłynęło od ostatniej zmiany i jak więcej niż 1000 to zmieniamy stan na odwrotny. Nie zatrzymuje to wykonywania kodu programu. Można w tym czasie wykonywać inne czynności.
Powstaje tutaj jednak dodatkowy problem często omijany w przykładach o którym chciałem wspomnieć. Funkcja millis() zwraca liczbę milisekund od czasu uruchomienia mikrokontrolera. Niby wszystko ok lecz w ardunino liczba unsigned long jest 32 bitowa czyli może zawierać liczby od 0 do 4 294 967 295. Po jakim czasie licznik się wyzeruje ? 4294967295 ÷ 1000(milisekund) ÷ 3600(sekund) ÷ 24(godziny) =49,7 dnia. Co potem? W niektórych przypadkach nastąpi błędne działanie funkcji. Przykładowo zmienna miliSekundy ma wartość bliską końca licznika np 4 294 967 290 i mamy poczekać 1000 milisekund więc wartość millis() powinna być większa od 4 294 968 290 niestety nigdy taka nie będzie bo się wyzeruje na liczbie 4294967295 i zacznie liczyć od nowa.
Jeżeli zastosujemy poprawną konstrukcję w zapytaniu tj if (millis() - miliSekundy > 1000) zamiast if (millis() > miliSekundy + 1000) to pomimo przepełnienia warunek zostanie poprawnie przeliczony.
Np. czasPoczątku=4294967290; czasBiezacy=1000; to czasBiezacy-czasPoczatku da wynik 1006 - czyli poprawny.
Zaś czasPoczątku=2000; czasBiezacy=1000; to czasBiezacy-czasPoczatku da wynik 4294966296 - czyli także poprawny ale jak widać teraz trzeba czekać 49 dni.
Gdy zastosujemy zły warunek dane będą niepoprawne i może on nigdy nie zostać spełniony.
Aby sprawdzić jaka jest różnica w działaniu możemy napisać prosty program testujący:
void setup() { Serial.begin(115200); delay(1000); unsigned long currentTime = 2000; unsigned long DELAY = 1000; unsigned long startTime = 4294957295; Serial.println(); Serial.print("startTime:"); Serial.println(startTime); Serial.print("currentTime"); Serial.println(currentTime); Serial.print("DELAY:"); Serial.println(DELAY); Serial.println("1. PRAWIDŁOWA WERSJA:"); Serial.print("currentTime-startTime>DELAY: "); Serial.print(currentTime - startTime); Serial.print(">"); Serial.print(DELAY); Serial.print(" ?"); Serial.println((currentTime - startTime > DELAY) ? "PRAWDA" : "FAŁSZ"); Serial.println("2. ZŁA WERSJA:"); Serial.print("currentTime>startTime+delay:"); Serial.print(currentTime); Serial.print(">"); Serial.print(startTime + DELAY); Serial.print(" ?"); Serial.println((currentTime > startTime + DELAY) ? "PRAWDA" : "FAŁSZ"); } void loop() {delay(1);}
Wynik możemy wyświetlić w okienku Monitora Portu szeregowego :
startTime:4294957295
currentTime:2000
DELAY:1000
1. PRAWIDŁOWA WERSJA:
currentTime-startTime>DELAY: 12001>1000 ?PRAWDA
2. ZŁA WERSJA:
currentTime>startTime+delay:2000>4294958295 ?FAŁSZ
2. Obsługa przetwornika ADC - pomiar napięcia teoria i praktyka
Często zachodzi potrzeba pomiaru wartości analogowych. np wartości prądu napięcia czy rezystancji np położenie potencjometru). Mikrokontrolery mają często wbudowany przetwornik ADC. Zamienia on wartość analogową napięcia na wartość cyfrową.
Mierząc napięcie i stosując odpowiednie podłączenie możemy tez zmierzyć prąd rezystancję a także wyliczyć wartości pośrednie temperaturę moc wilgotność itp. itd.
Zanim jednak zaczniemy mierzyć należy przyjrzeć się co i jak się da zmierzyć.
- Pierwsza ważna rzecz to rozdzielczość przetwornika przykładowo 8,10,12, 16 bitów. mówi nam na ile "porcji" dzielony jest cały zakres pomiarowy. np . 8 bitowy przetwornik może mieć 256 stanów od 0 do 255. 10 bitowy ma 1024 stany, 12-4096 a 16 bit 65535 Im więcej bitów tym większa dokładność pomiaru.
- Druga to zakres pomiarowy bezpośrednio związany z napięciem odniesienia czyli jakie maksymalne napięcie można podłączyć bezpośrednio do wejścia. NP 1.1V, 2V, 3.3V, 5V itp. W wielu mikrokontrolerach można je zmieniać w zależności od tego co jest potrzebne. Czasem mikrokontroler posiada kilka wewnętrznych źródeł odniesienia czasem można tez podłączyć zewnętrzne dokładniejsze źródło odniesienia o innym napięciu. Pamiętając, że nie może ono przekroczyć maksymalnego zakresu. Przykładowo wykorzystamy wewnętrzne źródło odniesienia w procesorze Atmega 328 (np Arduno Nano) o wartości 1,1V . Czasem napięciem odniesienia jest napięcie zasilania - należy unikać takiego przypadku ze względu na słabą stabilność takiego źródła. analogReference(INTERNAL); W tym procesorze przetwornik jest 10 bitowy więc mamy 1024 kroki dla napięcia od 0 do 1,1V co daje 0,00107421875V na jedną działkę przetwornika np różnica odczytana pomiędzy wartością 0 a 1 będzie mniej więcej taka.
- Trzecim ważnym parametrem są szumy przetwornika. Czyli jak zmienia się wartość mierzona np dla 12,34V (z dzielnikiem rezystancyjnym) wartość bezpośrednio odczytana na przetworniku waha się od 643 do 651
- I wiele innych czynników wpływających na pomiar jak np zmiany temperatur, stabilność w czasie itd.
Aby mierzyć większe napięcie np 12V należy zastosować dzielnik złożony z rezystorów np:
W przykładzie mierzone napięcie będzie dzieliło się na spadek na diodzie zabezpieczającej oraz na dwóch rezystancjach R1 i R2. Teoretycznie spadek na diodzie w pewnym zakresie napięć jest bardzo podobny więc można go tutaj pominąć. Zawsze można dodać spadek na diodzie do obliczeń.
Jeżeli na taki dzielnik podłączymy źródło zasilania o napięciu 17V podzieli się ono proporcjonalnie do rezystancji R1 i R2 i wyniesie 16V i 1V.
Znając wartości R1 i R2 oraz odczytując wartość z przetwornika możemy obliczyć mierzone napięcie :
#define V_REF 1.1 #define R1 16000 #define R2 1000 ... unsigned int adcValue = analogRead(analogInput); float napiecie = ((adcValue * V_REF) / 1024.0) / (R2 / (R1 + R2)); ...
Znając dokładną wartość V_REF oraz rezystorów w łatwy sposób obliczymy wartość napięcia.
Takie rozwiązanie ma szereg wad. Przede wszystkim musimy znać dokładną wartość rezystancji. Rezystory mają rozbieżność w produkcji czyli powielając układ każdy kolejny będzie mierzył napięcie trochę inaczej. Aby skalibrować taki układ trzeba dokładnie zmierzyć rzeczywista rezystancję oraz ją wpisać do programu. Aby zapewnić powtarzalność w produkcji konieczna jest kalibracja. Można to zrobić za pomocą potencjometru i skalibrować układ ręcznie. Lecz po pewnym czasie może się zmienić jego wartość od drgań, brudu czy innych czynników. Też nie po to mamy układ cyfrowy aby stosować elementy analogowe.
Pierwszym rozwiązaniem jest zamiana w kodzie rezystancje na napięcia. R1 i R2 będą proporcjonalne tak samo jak spadki napięć na nich wobec czego na zmontowanym układzie możemy zmierzyć napięcia na rezystorach i podstawić do tego wzoru. Wynik będzie identyczny.
Jednak mierzenie napięć na jakichś dyskretnych elementach jest trochę trudne. Najlepszym rozwiązaniem jest wyrzucenie tego szkolnego wzoru i podejście do tematu w inny sposób.
Wartość odczytana z przetwornika jest wprost proporcjonalna do napięcia. np wartości 714 odpowiada rzeczywiste napięcie 12,26V można z prostej proporcji wyliczyć wartość napięcia dla innych wartości bez rozpatrywania napięć w dzielniku.
Należy napisać odpowiednią procedurę która powie sterownikowi teraz masz 12,12V on sam odczyta wartość z przetwornika i ma już dwie wartości niezbędne do obliczeń napięcia tj wartości przetwornika i jakiemu napięciu ona odpowiada. Dane można zapisać w eepromie i taki układ jest już skalibrowany.
unsigned int V_RAW_ADC = 714; unsigned long V_VOLTS = 1226; unsigned int getVoltage() { return (unsigned int)((analogRead(A0)* V_VOLTS) / V_RAW_ADC) ;
}
Jak widać kod jest dużo prostszy. Zastosowałem tutaj prostą sztuczkę. Napięcia są liczbami zmiennoprzecinkowymi np 12.24. Dla łatwiejszego liczenia i obsługi (zapis odczyt zamian na string i z powrotem) zamieniamy je na int. 12,24 jest po prostu liczbą 1224 czyli 100* większą ale za to bez przecinków. Zawsze można z niej zrobić liczbę typu float dzieląc ją przez 100.0. Przykładowa kalibracja w aplikacji jest bardzo prosta i polega na wprowadzeniu jednej zmiennej bieżącego napięcia. (wartość przetwornika sterownik pobierze sam). Np:
Podczas pomiarów mierzonego napięcia występują wahania mierzonej wartości np:
12.65
12.64
12.67
itd.. jest to normalne wynika z szumów i niedokładności przetwornika. Można temu zapobiec zacznijmy jednak od początku.
Stosowane przetworniki ADC w tanich układach nie są liniowe i pomiar obarczony jest pewnym błędem. Dodatkowo przetwornik taki ma swój własny szum czyli dla takiego samego napięcia odczytana wartość się zmienia.
Wbudowane napięcie odniesienia tez nie jest super stabilne w funkcji czasu in temperatury. I tu tez uwaga przykładowo Arduino Nano może mieć napięcie odniesienia 1,1V i 5V - gdzie 1,1 jest z wewnętrznego źródła a 5 po prostu z zasilania. Przy napięciu odniesienia 1,1V będą bardziej widoczne szumy przetwornika zaś stabilność zasilania 5V jest też niekoniecznie duża. Dokładność zwiększymy np dodając lepsze napięcie odniesienia o dość dużej wartości jednak mniejszej równej napięciu zasilania (5V). Jednak przy 10 bitowym przetworniku nie możemy się spodziewać zbyt dużego skoku jakościowego.
Poniżej tabelka z dokładnością odczytu z zakresu 20V dla różnych przetworników przy przykładowym napięciu 12,31V i wahaniu +/-1 i +/-2 działki przetwornika.
Przetwornik | Zakres |
Minimalna różnica |
Rzeczywista wartość napięcia |
Wartość szumu +/-1 |
Wartość szumu+/-2 |
8 bit. | 0-255 | 20/255=~0,078V | 12,31V | 12,232-12,388 | 12.154-12,466 |
10 bit. | 0-1023 | 20/1023=~0,02V | 12,31V | 12,290-12,330 | 12,270-12,350 |
12 bit | 0-4095 | 20/4095=~0,005 | 12,31V | 12,305-12,315 | 12,300-12,320 |
16 bit | 0-65535 | 20/65535=~0,0003 | 12,31V | 12,3097-12,3103 | 12,3094-12,3106 |
Są to wartości teoretyczne nie uwzględniające wielu czynników np. większy zakres bitowy może powodować większe szumy przetwornika itp. Jest to tylko tabela obrazująca jak niedokładny jest to pomiar.
Warto się zastanowić czy na prawdę niezbędny jest nam pomiar 12,31V czy nie wystarczy informacja 12,3 ? I tak mamy pewną niedokładność wynikającą z wielu czynników.
Przy standardowym przetworniku 10 bit dla zakresu 20V mamy +/-0,02V(0,04V) niedokładności tylko przy zmianie wartości odczytanej z przetwornika o +/-1 (np 629, 630, 631). Wahania w takim przypadku będą od ok 12,29 do 12,33. uśredniając ta wartość do jednego miejsca po przecinku uzyskamy wartość 12,3 - co usunie nam w większości przypadków wahania pomiaru. Po prostu standardowym przetwornikiem 10 bit. nie da się dokładniej. Patrząc z tabelki idealny byłby 16 bitowy przetwornik ale tu też uwaga - dobrej jakości z małymi szumami.
Można próbować trochę odszumić cyfrowo odczytywane dane z przetwornika np. można pomiar uśrednić - odczytać np 100 wartości i wyciągnąć średnią czy medianę.
Rozwiązaniem na szumy przetwornika jest też zastosowanie np. biblioteki https://github.com/dxinteractive/ResponsiveAnalogRead. W uproszczeniu likwiduje ona cyfrowo szumy z przetwornika jednak wskazany pomiar może mieć trochę większy błąd.
Można też coś podobnego zrobić samodzielnie - ustawić wartość różnicy powyżej, której zmieni się odczytana wartość np. odczytujemy napięcie 12,31V i dopóki nie zmieni się o więcej niż 0,04V to nie przepisujemy nowych wartości. Dodatkowo można dodać warunek czasu - odśwież po 10 sekundach nawet jak różnica wartości jest mniejsza niż wspomniane 0,04V
Podczas pomiaru napięć z przetwornika należy tez zwrócić uwagę aby nie wykonywać ich w każdej wolnej chwili w funkcji loop gdy mamy uruchomione biblioteki wymagające nieblokowania jak np wspomniany ESP Async Web Server.
Poniżej kod przykładowego projektu uwzględniający powyższe uwagi:
// ANALLOG READ - (c)' 2021 ELFRO.pl Tomasz Fronczek #define MINIMAL_VALUE_CHANGE 4 // 4- oznacza zmianę minimum o 0.04V #define MINIMUM_VOLTAGE 20 // wartość poniżej której odczytywane napięcie traktowane jest jako 0V np. 20=0.20V #define FORCE_REFRESH_TIME 10000 // co ile milisekund odświeżać odczytywane napięcie pomimo braku zmian 0 - brak timera unsigned int rawADC = 0; // wartość odczytana z przetwornika int currentRawVoltage = 0; // wartość obliczona z rawADC // kalibracja unsigned int V_RAW_ADC = 630; // wartość z przetwornika unsigned long V_VOLTS = 1235; // dla której jest napięcie w voltach *100 unsigned int DIODE=70; // spadek napięcia na ewentualnej diodzie - tu 0,7V int currentVoltage = 0; // napięcie w voltach do dalszej obsługi przez użytkownika *100 np 1234= 12,34V; unsigned long forceVoltsUpdate = 0; // czas do aktualizacji unsigned long sec = 0; // czas co ile będzie sprawdzane napięcie // czy jest rożnica w napięciu ? - można było użyć funkcji abs lecz ta funkcja ma możliwość zmiany wartości np na float bool delta(int oldVal, int val, int result) { int v = val - oldVal; if (v < 0) v = -v; return v > result; } // sprawdź napięcie - opcjonalny parametr FORCE - przepisze wartość napięcia na wyliczoną niezależnie od algorytmu void checkVoltage(bool FORCE = false) { bool force = (millis() - forceVoltsUpdate >= FORCE_REFRESH_TIME ) || FORCE; if (force) forceVoltsUpdate = millis(); rawADC = analogRead(A0); if (V_VOLTS > DIODE) { currentRawVoltage = (int)((rawADC * (V_VOLTS - DIODE)) / V_RAW_ADC) + DIODE ; if (currentRawVoltage < MINIMUM_VOLTAGE + DIODE) currentRawVoltage = 0; } else { currentRawVoltage = (int)((rawADC * (V_VOLTS)) / V_RAW_ADC) ; if (currentRawVoltage < MINIMUM_VOLTAGE) currentRawVoltage = 0; } if (delta(currentVoltage, currentRawVoltage, MINIMAL_VALUE_CHANGE) || force) currentVoltage = currentRawVoltage; } void setup() { Serial.begin(115200); // jeżeli jest możliwość to trzeba skonfigurować napięcie odniesienia inne niż napięcie zasilania np : // analogReference(INTERNAL); } void loop() { if (millis() - sec > 1000) // pętla wykonywana co sekundę { sec = millis(); checkVoltage(); Serial.println("----------"); Serial.print("Dane z przetwornika: "); Serial.print(rawADC ); Serial.print("V obliczone: "); Serial.print(currentRawVoltage); Serial.print("(int) "); Serial.print(currentRawVoltage / 100.0); Serial.println("V"); Serial.print("V dla użytkownika : "); Serial.print(currentVoltage); Serial.print("(int) "); Serial.print(currentVoltage / 100.0); Serial.println("V !!!!"); Serial.println(); } // ..... pozostały kod aplikacji. }
Do odczytu temperatury często stosowany jest popularny układ DS18B20. Podłącza się go za pomocą magistrali OneWire.
Dokumentacja podaje że wg specyfikacji magistrali One Wire linia danych powinna być podciągnięta rezystorem 4,7 k. Próby wykazały że nie zawsze to działa dobrze i warto go trochę zmniejszyć. Dla 5V proponuję 3,6k a dla 3,3V 2,2k.
Magistrala one wire umożliwia podłączenie kilku termometrów. Aby odczytać dane z termometru należy ustawić jego rejestr do odczytu poczekać 750 ms i odczytać zgromadzone dane. Warto jest rozwiązać to w sposób nieblokujący. (patrz pkt1.)
Jeżeli mamy kilka termometrów warto jest zapamiętywać ich adresy. Po ponownym włączeniu mogą one zostać wykryte w innej kolejności dlatego warto sprawdzić z którego urządzenia o jakim adresie jest temperatura. Dodatkowo przy wymianie urządzenia dobrze jest aby program wykrył nowy termometr i zastąpił stary nieobecny.
Kolejną sprawą jest częsty pomiar temperatury. Jeżeli zrobimy to zbyt szybko (pomiar za pomiarem) pomiary mogą być zafałszowane. Termometr może się odrobinę rozgrzać od ciągłego mierzenia dlatego warto zastosować chwilę przerwy pomiędzy pomiarami.
Kompletny kod dla dwóch termometrów uwzględniający powyższe uwagi można pobrać stąd. Łatwo go przerobić do swoich potrzeb, zwiększyć ilość termometrów czy dodać obsługę po USB czy innej magistrali, dodać wyświetlacz itp.