Processing — oscyloskop (starcie nr 2)

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:

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:

  1. czekać na nadejście 3 bajtów do portu szeregowego,
  2. sprawdzić, czy pierwszy z nich to nasza flaga – u nas 0xff,
  3. jeśli nie, to wracam do punktu punktu 1,
  4. jeśli jednak tak, to wczytujemy kolejny bajt, przesuwamy go w lewo o 8 bitów, robimy sumę logiczną ze wczytanym następnym bajtem,
  5. 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:

5

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:

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ół

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:

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:

oscy2

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!

Ten wpis został opublikowany w kategorii FiBot i oznaczony tagami , , , , , , , , . Dodaj zakładkę do bezpośredniego odnośnika.