Oscyloskop
Kontynuujemy poznawanie processinga. Podążamy w kierunku stworzenia aplikacji, która będzie wyświetlać napięcia z portów Arduino UNO (A0, A1..A5) – czyli taki softwarowy oscyloskop. Na poprzednich zajęciach przekonaliśmy się, że wysyłanie liczb z poziomu Arduino za pomocą Serial.println(analogRead(A0)) kiepski pomysł, bo
- każda cyfra w wysyłanej liczbie to 1 bajt, np. liczba „1017” to aż 5 bajtów (4 cyfry + znacznik końca linii)
- jest kłopot z odczytem liczb przesyłanych jako napisy, gdyż trzeba je odczytywać jako napis (tablica znaków) a następnie zamienić napis na liczbę – skoplikowane i powolne. Bez takiej procedury odczytu liczby będą błędnie wczytywane i dodatkowo będą pojawiać się nadmiarowe liczby (śmieci) odpowiadające znacznikowi końca linii.
Dlatego też musimy zmienić sposób wysyłania liczb z Arduino – bez udziału Serial.println.
Własny protokół
Będziemy wykorzystywać metodę Serial.write(dana) – która wysyła pojedynczy bajt przez komunikację szeregową. Nie jest wysyłany żaden znacznik końca linii, a na dodatek liczba z zakresu 0..255 wymaga wysłania tylko jednego bajta – duża oszczędność w porównaniu do Serial.println(). Z tego samego powodu liczba z przedziału 0..65535 będzie wymagać wysłania tylko 2 bajtów. Oszczędność na transmisji (a więc na szybkości!) widoczny jest od razu.
Jest jednak pewien problem – skoro wysyłamy tylko jeden bajt, bo jak wysłać liczbę typu int (czyli 2 bajty)? Przecież wyjścia analogowe w Arduino UNO są 10-cio bitowe, czyli generują liczby z zakresu 0..1023 (2 bajty = int). Rozwiązujemy to wysyłając te dwa bajty budujące liczbę typu int oddzielnie – najpierw bajt starszy, potem młodszy (za chwilę będzie to wyjaśnione). Brzmi dość zawile, ale tak nie jest – proszę przyjrzeć się poniższemu programikowi:
void setup(){
Serial.begin(9600);
}
void loop(){
int dana = analogRead(A0);
Serial.write( 0xff);
Serial.write( (dana>>8) & 0xff);
Serial.write( dana & 0xff);
}
W tym programie wysyłamy liczbę odczytaną z portu A0 Arduino jako 3 bajty – dwa bajty to ten odczyt- liczba int, ale przed nim wysyłam „flagę” informującą, że podaję właśnie liczbę typu int. Ta flaga jest potrzebna w sytuacjach, gdy z jakiegoś powodu rozsynchronizuje się nam transmisja i wówczas nie będziemy w stanie określić, który bajt jest pierwszym, a który drugim budującym liczbę typu int. Dzięki istnieniu flagi (u mnie liczby 0xff czyli 255 dziesiętnie) zawsze będziemy wiedzieć prawidłową kolejność.
Wartość flagi nie ma znaczenia, ja wygrałem 255 – ale każdy może wpisać swoją ulubioną liczbę z przedziału 0..255 (oczywiście, gdy zmieni wartość flagi to przy odczycie liczby należy to uwzględnić).
W kodowaniu liczby posługuję się pojęciami „starszego” i „młodszego” bajtu – w końcu liczba typu int to 2 bajty (=16 bitów), więc pierwszy z nich (pierwsze 8 bitów) niech się nazywa „starszy”, a drugi (kolejne 8 bitów) – „młodszy”. Kodowanie polega na:
- przesunięcie liczby o 8 bitów w prawo, w ten sposób mamy nową liczbę (w zapisie bitowym) jako 8-zer i 8 bitów z pierwotnej liczby, czyli tylko ten „starszy” bajt właśnie
- iloczyn logiczny z bajtem o wartości 255 (szesnastkowo 0xff) zwróci nam właśnie ten starszy bajt – który wysyłamy przez Serial.write jako pierwszy
- iloczyn logiczny danej liczby int bez wykonywania przesunięcia z bajtem 255 (szesnastkowo 0xff) „wycina” nam starszy bajt, pozostawiając jedynie młodszy bajt – który wysyłamy drugim poleceniem Serial.write
W ten sposób wysyłamy liczby odczytywane z portu A0 – przy czym teraz do ich oglądania nie możemy użyć Monitora portu szeregowego z Arduino IDE – gdyż zobaczymy tylko „krzaczki” (nawet bez końca linii, no chyba, że przypadkowo trafi się liczba 13 – który jest znakiem Entera). Jak więc odczytywać takie liczby? Trzeba wykonać operację odwrotną, czyli:
- czekać na nadejście 3 bajtów do portu szeregowego,
- sprawdzić, czy pierwszy z nich to nasza flaga – u nas 0xff,
- jeśli nie, to wracam do punktu punktu 1,
- jeśli jednak tak, to wczytujemy kolejny bajt, przesuwamy go w lewo o 8 bitów, robimy sumę logiczną ze wczytanym następnym bajtem,
- wracamy do 1. i odczytujemy kolejną liczbę.
Skomplikowane? może tak, może nie… zależy dla kogo 😉 Czy nasz protokół jest dobry? Tyle o ile… ma co prawda informację o początku liczby (to plus), ale nie ma jakiejś sumy kontrolnej, sprawdzającej poprawność wysłanej daje (to minus). Ale jest prosty i szybki (przesuwanie litów i liczenie iloczynu/sumy bitowej to „pestka”, nawet dla Arduino) – to kolejny plus, który przeważa o jego wykorzystaniu. Pamiętajmy, że zależy nam na wykreślaniu liczb z otrzymywanych z przetwornika analogowo-cyfrowego Arduino, który pracuje z częstością 10k – nadaje więc 10 000 liczb na sekundę. Sprawdziłem, że operacje kodowania liczb w naszym protokole skutkują nadawaniem około 9000 liczb na każdą sekundę, czyli całkiem dobrze.
Podsumowując: nadajemy flagę (0xff), potem starszy bajt, a potem młodszy. Ktoś może nie znać naszego protokołu i odczytywać (dobrą) flagę, a potem 2 bajty ALE myśleć, że to młodszy i starszy bajt – w odwrotnej kolejności niż my to zrobiliśmy. Więc zbudowane przez niego liczba int nie będzie tą liczbą, którą my wysłaliśmy. Chcąc uniknąć nieporozumień należałoby zaznajomić taką osobę z naszym protokołem – i teraz już rozumiemy, skąd się bierze konieczność standaryzacji w informatyce (i nie tylko).
Arduino + potencjometr
Aby mieć w pełni kontrolę nad tym, co robimy, proponuję zacząć od wysyłania liczb z portu analogowego Arduino z wykorzystaniem potencjometru – temat wielokrotnie przez nas wałkowany. Tym razem jednak nie używamy płytki stykowej i przewodów, a podłączymy potencjometr bezpośrednio do portów analogowych Arduino, o tak:
W ten sposób tracimy 2 porty analogowe (zamieniając ich funkcję na porty cyfrowe), a 3-ci wykorzystujemy do odczyty napięcia. Pozbywamy się konieczności stosowania płytki stykowej – coś za coś. W tym przypadku chodzi mi o maksymalne uproszczenie kwestii po stronie Arduino, a skupienie się na processingu. Dodam tylko, że na pinie A0 muszę włączyć napięcie 5V,a na pinie A4 napięcie 0V z wykorzystaniem funkcji pinMode(OUTPUT) i digitalWrite().
Skech łączący w sobie wysyłanie danych w naszym protokole ze wspomnianym „sprytnym” (ale kosztownym!) podłączeniu potencjometru:
#define OUT A2
#define POWER A0
#define GND A4
void setup() {
Serial.begin(9600);
pinMode(POWER, OUTPUT);
pinMode(GND, OUTPUT);
digitalWrite(GND, 0);
digitalWrite(POWER, 1);
}
void loop() {
int val=analogRead(OUT);
// Serial.println( val);
Serial.write( 0xff);
Serial.write( (val>>8) & 0xff);
Serial.write( val & 0xff);
}
Oczywiście w przyszłości zastąpimy potencjometr jakimiś czujkami – np. pola magnetycznego, natomiast na chwilę obecną bawimy się potencjometrem (daje to nam pełną kontrolę nad kreślonym wykresem).
Processing – funkcja odczytu
Dodajemy do naszego projektu (z poprzednich zajęć) funkcję odczytu danych – uwzględniających nasz protokół
int getValue(){
int value = -1;
while (port.available() >= 3){
if (port.read() == 0xff)
value = (port.read() <<8) | (port.read());
}
return value;
}
Nazwałem ją getValue() i jak widać realizuje wspomniany wyżej sposób odczytywania liczby typu int. Najpierw czekamy na (przynajmniej) 3 bajty w buforze, potem odczytuje pierwszy porównując do flagi, a następnie tworzę liczbę typu int ze starszego i młodszego bajtu.
Wyświetlamy słupki (dane)
W głównej funkcji processinga – funkcji draw() – dodajemy rysowanie prostokąta o wysokości odpowiadającej otrzymanej liczbie z portu szeregowego. Funkcja rysująca prostokąt to rect(x0, y0, szer, wys) – gdzie para (x0,y0) to lewy-dolny wierzchołek rysowanego prostokąta, o wysokości wys i szerkości szer. Cały kod poniżej:
int xPos=0;
void draw(){
int w=getValue();
println(w);
rect(xPos, height, 10, height-w);
xPos=xPos+10;
if (xPos>=width){
xPos=0;
background(0);
}
}
Rysowanie jest bardzo prymitywne – dla każdej otrzymanej i odczytanej liczby rysuję prostokąt od dołu ekrany – czyli współrzędna lewego-dolnego rogu naszego prostkoąta to (xPos,hight). Dlaczego y-kowa współrzędna wynosi height, czyli wysokość okienka? Dzieje się tak dlatego, że układ współrzędnych w grafice processinga jest inny niż ten z lekcji matematyki/fizyki ze szkoły – o ile współrzędne x-owe rosną w prawo, to współrzędne y-kowe rosną z góry na dół, przy czym punkt (0,0) jest lewym, górnym rogiem okienka. Szerokość prostokąta ustalam na 10 punktów, a wysokość – odpowiednio do wczytanej liczby (u mnie nazwanej w). Dlatego wysokość ta wynosi height – w. Po narysowaniu zwiększam zmienną xPos w programie o 10 pikseli, by kolejny prostokąt rysował się obok tego właśnie narysowanego. Kolejna linia kodu sprawdza, czy oby nie „wyszedłem” z wartością xPos poza szerokość okienka (sprawdzam, czy xPos>width) i jeśli tak jest, to zeruję zmienną xPos i czyszczę ekran graficzny. Poniżej wynik działania programu:
Kręcimy potencjometrem w lewo i prawo i obserujemy wykres na ekranie komputera – w sposób kontrolowany (potencjometr) wysyłamy dane do preocessinga. Widzimy, że dane większe niż 800 wylatują poza ekran – tym zajmiemy się na kolejnych zajęciach.
Ulepszenia:
Kilka rzeczy od razu przychodzi do głowy:
- kolor słupków – może niech zależy od rysowanej wartości? w ten sposób będziemy mieć kolorowe wykresiki, a któż nie lubi kolorów 😉
- może amiast prostokątów rysować linie? Jak wiadomo – do rysowania linii w processingu potrzebujemy współrzędnych początka (x0,y0) i końca (x1,y2). Dlatego aby to zrealizwać, musimy pamiętać wartość poprzedniej danej, a po narysowaniu linii zamienić tą wartość na aktualną.
Czego się dziś nauczyłeś
- nowa funkcja w Arduino: Serial.write()
- dlaczego stosujemy Serial.write() a nie Serial.println()
- tworzenie (prostego) protokołu
- liczba int = 2 bajty, bajt „starszy” i „młodszy”, operacje na bitach
- processing: rysowanie prostokątów
(ciągle) pozostaje kilka kwestii do rozwiązania:
poprawne wczytywanie liczb (z Arduino wysyłamy 0..1023, a processing „widzi” tylko jakieś małe liczby, <30?)
- skalowanie wykresu – czyli chcemy mieć tak, by liczby z zakresu 0..1023 zawsze mieściły się na ekranie, a nie „wyskakiwały” poza aktualną wysokość ekranu
- ładniejsza grafika
Zapraszam na kolejne zajęcia!