Processing — oscyloskop (starcie nr 3)

Oscyloskop

Kontynuujemy tworzenie oscyloskopu w processingu. Tym razem chcemy dodać (proste) skalowanie wykresu oraz obsługę klawiatury. Na koniec słów kilka o optymalizacji kodu – uczmy się tworzyć mądre oprogramowanie. Uruchamiamy Arduino ze skeczem z poprzednich zajęć i koncentrujemy się na processingu.

Skalowanie

Stosujemy funkcję liniową y=ax+b aby dopasować otrzymywane liczby z Arduino (nazwijmy je x-ami) do wysokości okienka graficznego Processinga (nazwijmy je y-kami). Arduino „nadaje” nam x z przedziału 0..1023, a okienko z naszym wykresem ma height pikseli wysokości, czyli y jest z przedziału 0..height. Rysujemy prostokąty dla każdej otrzymanej liczby – choć dla jasności kodu powróćmy na chwilę do rysowania tych prostokątów od góry ekranu. 

Funkcja rect(x0,y0,szer,wys) – przypomnienie

Przypominam, że używamy funkcji rect(x0,y0, szer, wys) do rysowania prostokąta. Para liczb x0,y0 to współrzędne lewego górnego rogu, natomiast szer,wys to odpowiednio szerokość i wysokość prostokąta. Ostatnia para liczb liczona jest względem x0,y0 więc współrzędne przeciwległego rogu prostokąta (prawy dolny róg) wynosi (x0+szer, y0+wys). Warto zaznaczyć, że liczby szer,wys mogą być ujemne, co wykorzystamy za chwilę.

Wracamy do skalowania

Po przypomnieniu o funkcji rect wracamy do rysowania przeskalowanych prostokątów – żądamy więc, aby spełnione było:

  1. dla wczytanego x=zero mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=1023 (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Powyższe warunki dla skalowania liniowego y=ax+b przyjmują matematyczną postać (nie znamy jeszcze współczynników skalowania ab):

  1. 0=a*0+b
  2. height=a*1023+b

Musimy rozwiązać układ równań 1. i 2. wyznaczając ab. Nie jest to trudne, gdyż momentalnie otrzymujemy:

b=0 oraz a=heigh/1023.

Dlatego ulepszona funkcja rysująca prostokąty (od góry ekranu) wygląda następująco:

float y;
void draw(){
 int x=getValue(); 
 print("z Arduino x=",x);
 y=height/1023.0*x;
 rect(xPos, 0, 10, y);
 println(" y=", y);
 
 xPos=xPos+10;
 if (xPos>=width){
   xPos=0;
   background(0);
 }
}

Na uwagę zasługują:

  • utworzenie pomocniczej zmiennej y przechowującej wartość przeskalowanego x-a
  • wyliczanie y-ka wykorzystuje dzielenie liczb rzeczywistych – zwróć uwagę na liczbę 1023.0 pisaną z kropeczką. Bez takiego postępowania otrzymywalibyśmy zero, jeden lub dwa (liczby całkowite), gdyż dzielenie wartości height (liczba typu int) przez 1023 (bez kropeczki – też liczba typu int!) pomija część ułamkową. 
  • uzbrajam kod w wypisywanie na ekran różnych pomocniczych informacji, tutal: wczytanego „gołego” x-a, oraz przeskalowanego y-ka (aby wszystko dobrze kontrolować).

Mamy już działające skalowanie, dzięki czemu liczby nie „wsykoczą” poza ekran okienka!

Sukces! dlatego lekko to ulepszymy rysując prostokąty od dołu ekranu:

float y;
void draw(){
 int x=getValue(); 
 print("z Arduino x=",x);
 y=height/1023.0*x;
 rect(xPos, height, 10, -y);
 println(" y=", y);
 
 xPos=xPos+10;
 if (xPos>=width){
   xPos=0;
   background(0);
 }
}

Cała zmiana polega na „zaczepieniu” lewego-górnego rogu prostokąta na dole okienka graficznego – współrzędna (xPos, height), nadanie mu szerokości =10 pikseli a wysokości… właśnie -y (minus y). Nie powinno to zaskakiwać w świetle przypomnienia o funkcji rect (przeczytaj o tym wyżej), bo wówczas współrzędna prawego-dolnego rogu prostokąta wynosi właśnie (xPos+10, height-y). Otrzymujemy:

Możemy być już z siebie dumni 😉

Obsługa klawiatury 

Dodajemy nową funkcjonalność do naszego programu — chcemy, aby klawisz spacja zatrzymywał rysowanie wykresu. Arduino ciagle działa i nie przestaje nadawać liczb na port szeregowy, tym samym wszystkie liczby nie są odbierane przez processing i je „tracimy” – można pomyśleć o stworzeniu bufora na te liczby, ale dla prostoty my tak nie będziemy robić.

Funkcja keyReleased() 

Obsługuje klawiaturę w processingu – wystarczy, że w jej ciało wpiszemy obsługę konkretnych klawiszy, a wysztko będzie działać automatycznie – za to właśnie kochamy processing! Zacznijmy od prostego przykładu:

void keyReleased() {
  if (key == ' ') 
    println("SPACJA!")
}

Dodanie powyższej funkcji do naszego programu skutkuje tym, że przy każdym naciśnięciu spacji w konsoli tekstowej processinga pojawi się napis „SPACJA!”. Fajnie! i jeszcze raz podkreślę – jak prosto.

A co to jest tutaj zmienna key? Otóż jest to jedna z wielu zmiennych, których nie trzeba deklarować w processingu, a która przechowuje kod ostatnio wciśniętego klawisza na klawiaturze.

Jak więc uruchamiać i zatrzymywać tworzenie wykresu wciskając spację? W tym celu dodajemy pomocniczą zmienną stop i robimy coś takiego:

boolean stop=false;
void keyReleased() {
  if (key == ' ') {
    stop=!stop;
    if (stop)
      println("PAUZA");
    else
      println("GO!");
  }//if spacja
}

Objaśnienia:

  • zmianna typu boolean przyjmuje dwie wartości – true lub false (inaczej mówiąc tak i nie, zero i jeden…) i informuje nas, czy zatrzymujemy wykres czy nie,
  • w funkcji keyReleased() sprawdzamy, czy wciśnięto spację czy nie, a jeśli tak to zmieniamy wartość zmiennej stop na przeciwną (instrukcja stop=!stop). Następnie wypisujemy odpowiedni komunikat.

Aby nie tylko wypisywany był odpowiedni komunikat ALE i faktycznie wykres przestawał się rysować, należy zmodyfikować funkcję draw, odpowiedzialną za rysowanie wykresu, w odpowiedni sposób (uwzględniający istnienie zmiennej stop):

boolean stop=false;
float y;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    rect(xPos, height, 10, -y);
    println(" y=", y);
 
    xPos=xPos+10;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Jak widać zmiana polega na dodaniu ważnego if-a, który sprawdza, czy mam rysować wykres czy nie. Jeśli stop=false to negacja tego warunku jest prawdą, więc  wykonują się instrukcje z linii 5-14. Jeśli zaś stop=true (czyli po pierwszym wciśnięciu spacji) warunek z linii 4 nie jest spełniony (negacja prawdy to nieprawda!) i omijamy całą procedurę pobrania x-a z portu szeregowego, przeskalowanie, wypisywanie pomocniczych rzeczy w konsoli i kreślenie prostokątów (linie 5-14) i lądujemy w linii 16 (koniec if-a). Ale to już ostatnia linia funkcji draw więc… zgodnie z zasadami processinga funkcja draw wykonuje się ponownie. Przypomnę, że Arduino nie zaprzestaje nadawania liczb (nie sterujemy Arduino, tylko processingiem) więc dane te są tracone.

Rysowanie linii (zamiast prostokątów)

Zmieniamy rysowanie prostokątów na rysowanie linii. Ma to swoje uzasadnienie – w przyszłości kreślić będziemy nie dane z jednego portu analogowego Arduino, ale z dwóch, trzech… Prostokąty w takim przypadku pokrywałyby się i wykresy nie byłby czytelny. Dlatego kreślimy linię.

line(x1,y1, x2, y2) — pierwsza para liczb to współrzędne początku linii, druga para liczb – współrzędne końca linii. U nas linia oddalona jest o 10 pikseli w x-ach (czyli x2=x1+10) ale współrzędne y linii zależą od danych z Arduino: y1 to poprzednia dana, y2 to dana aktualnie otrzymana (oczywiście już po przeskalowaniu funkcją liniową). Skoro musimy pamiętać poprzednią daną to wprowadzamy pomocniczą zmienną do jej przechowywania poprzedniY i modyfikujemy kod w następujący sposób:

boolean stop=false;
float y, poprzedniY;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    line(xPos-10, poprzedniY, xPos, y);
    poprzedniY = y;
    println(" y=", y);
 
    xPos=xPos+10;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Zwracam uwagę na:

  • dodanie pomocniczej zmiennej poprzedniY (linia 2)
  • rysowanie linii uwzględniające poprzedni pomiar (linia 8)
  • zamiana aktualnego y-ka na poprzedniY, ale już PO narysowaniu linii (linia 9)
  • ustaliłem grubość linii na 10 pikseli funkcją strokeWeight() – zrobiłem to na początku programu w funkcji setup()
  • ustaliłem kolor linii na czerwony funkcją stroke() – także w funkcji setup()


Przy pierwszym uruchomieniu programu wartość zmiennej poprzedniY jest nieustalona co nie jest dobrze, ale nam to nie przeszkadza – to tylko 1 pomiar (po nim wszystko już dobrze działa). Dlatego tym niuansikiem nie będziemy się dalej zajmować.

Ulepszenie skalowania (klawisze z/Z jak zoom)

Jak na razie wykorzystujemy potencjometr jako źródło danych do wyświetlania, więc u nas Arduino wysyła liczby z pełnego zakresu 0..1023 (odpowiadające napięciom z przedziału 0..5V). Ale przecież zależy nam na podłączaniu różnych czujek do Arduino i tworzenia wykresów, więc nie zawsze tak będzie. Może się bowiem okazać, że czujka „produkuje” napięcia z zakresu 0..1V i wówczas… bardzo duża część naszego wykresu nie jest wykorzystywana! Co więcej, jeśli w takim przypadku napięcia oscylują, to my tracimy te informacje – wykres jest za mało czytelny, bo „schowany” na dole ekranu a więc i te oscylacje są malutkie, nieczytelne. Przykład:

Jak temu zaradzić? Bardzo prosto – wprowadzamy dodatkową zmienną określającą maksymalną liczbę (zmienna ML) zamiast dotychczasowej liczby 1023 dla jakiej odbywa się skalowanie wysokości ekranu. Dzięki zmianie ML-a będziemy mogli „zajrzeć w głąb” wykresu, czyli ograniczyć zakres wykresu – powstanie „zoom” 😉

Kontrolę nad zmienną ML odbywa się z klawiatury za pomocą klawiszy 'z’ (małe zet) oraz 'Z’ (duże zet), w tan prosty sposób, że odpowiednio zmniejszamy/zwiększamy wartość ML dwa razy. Kod funkcji keyReleased pod zmianach:

float ML=1024;
boolean stop=false;
void keyReleased() {
  if (key == ' ') {
    stop=!stop;
    if (stop)
      println("PAUZA");
    else
      println("GO!");
  }//if spacja
  if (key == 'z') ML=ML/2;
  if (key == 'Z') ML=ML*2;
}

Dodatkowo należy zmienić skalowanie wartości y w funkcji draw() na następujące: 

y=(float)height/ML*w;

co wynika z nowych żądań dotyczących skalowania:

  1. dla wczytanego x=zero mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=ML (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Warto też zmodyfikować wyświetlanie pomocniczych informacji w okienku terminala na uwzględniające wypisywanie aktualnej wartości ML-a, czyli:

    println(" y=", y, " ML=", ML);

Mamy teraz prostą możliwość zawężania widoku wykresu do wartości ML, którą kontrolujemy klawiszami z/Z. Oczywiście warto zadbać o to, aby zmniejszając wartość ML nie spaść na zero (co się może zdarzyć), więc należy dodać kolejnego if-a.

Większa kontrola nad skalowaniem wykresu

Warto też wprowadzić inne klawisze kontrolujące wawrtość ML-a, gdyż dzielenie/mnożenie przez 2 jest dobre tylko na początek zabawy. Inne klawisze mogą zmieniać ML np. o 100 – dbając także o to, aby nie uzyskać w ten sposób liczb ujemnych).

Dodatkowo można też rozważyć sytuację, że otrzymywane napięcie nie będzie z przedziału 0..1V a np. 0.5..0.7V — wówczas wykres może być ponownie nieczytelny, bo my kontrolujemy tylko maksymalną liczbę, a nie minimalną (u nas ona zawsze wynosi zero!). Można więc wprowadzić kolejną liczbę pomocniczą mL – minimalna liczba i ją zmieniać z klawiatury… Wówczas układ równań przy skalowaniu musi być zbudowany z żądań:

  1. dla wczytanego x=mL mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=ML (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Tę modyfikację pozostawiam jako zadanie domowe 😉

Ulepszenie rysowania (klawisze x/X do historii wykresu)

Aktulanie nasze okienko ma szerokość 800 pikseli, a linie/prostokąty są rysowane co 10 pikseli. Oznacza to, że na jednym pełnym ekranie zmieścimy maksymalnie 80 punktów nadesłanych z Arduino. W ten sposób powstaje historia pomiarów o długości 80 pomiarów. Jeśli jednak zmienimy szerokość linii/prostokątów z 10 pikseli na 5, to ta historia wynosi już 160 pomiarów. Jeśli zmienimy szerokość na 2, to mamy już historię 400 punktów. Wiadomo, do czego zmierzam? Należy wprowadzić zmienną kontrolującą tą szerokość, nazwijmy ją skokX (o tyle punktów „skaczemy” w x-ach rysując linię/prostokąt) i wprowadźmy możliwość jej zmiany za pomocą przycisków x/X na klawiaturze.

int skokX=10;
boolean stop=false;
float y, poprzedniY;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    line(xPos-skokX, poprzedniY, xPos, y);
    poprzedniY = y;
    println(" y=", y);
 
    xPos=xPos+skokX;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Zmiany są niewielkie:

  • wprowadziliśmy zmienną skokX z wartością początkową 10 (linia 1), oraz
  • zastąpiliśmy liczbę 10 zmienną skokX w dwóch miejscach – w rysowaniu (linia 9) oraz w miejscu zwiększania pozycji xPos (linia 13). 

Należy też zmodyfikować funkcję obsługi klawiatury o reakcję na wciskanie klawiszy x/X – powiedzmy odejmujemy/dodajemy wartość 1 do aktualnej wartości skokX. Należy jednak uwzględnić sytuację, żeby nie zmniejszyć tej zmiennej do zera (lub poniżej).

void keyReleased(){
  if (key == ' '){
    stop=!stop;
    if (stop)
      print("PAUZA");
    else
      print("WZNĂ“W");
    };
  if (key == 'z') ML=ML/2;
  if (key == 'Z') ML=ML*2;
  if (key == 'x') skokX-=1;  
  if (key == 'X') skokX+=1;
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Wynik działania programu może wyglądać tak (widzimy dłuuuugą historię pomiarów):

Optymalizacja

Czyli jak „kodować”, aby nasz program działał szybciej. Nawet jeśli teraz działa szybko, to i tak warto uczyć się optymalizacji. Jeśli tego nie będziemy robić, to będziemy marnować zasoby komputera (pamięć, CPU) na niepotrzebne instrukcje… Zaczynamy!

Przypatrzmy się funkcji keyReleassed() – mamy tam szereg if-ów. Jest raczej oczywiste, że muszą tam one być, ale nie w tej postaci. Bo jeśli wciśnięto klawisz Z to wykonujemy odpowiednią akcję, ale po co sprawdzać dalej, czy wciśnięto klawisz x albo X? przecież już obsłużyliśmy wciśnięcie przycisku Z. Dlatego wypada to zrobić z szeregiem if-elsów, jakoś tak:

void keyReleased(){
  if (key == 'z') ML=ML/2;
  else
    if (key == 'Z') ML=ML*2;
    else
      if (key == 'x') skokX-=1;  
      else
        if (key == 'X') skokX+=1;
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Teraz jest zdecydowanie lepiej! Wyobraźmy sobie, że jedno porównanie kosztuje 1$ (w komputerze walutą jest ułamek sekundy, jakie CPU musi spędzić na wykonaniu danej operacji – ale ja będę posługiwać się dolarami, bo mocno przemawia to do naszej wyobraźni). Widać więc, że jeśli wybrano klawisz 'z’ to zapłacimy za wykonanie funkcji tylko 1$, a nie 5$ jak w pierwotnym przykładzie – ogromna oszczędność!

Wygodnie posługiwać się instrukcją switch-case, zamiast dużej liczby par if-elsów, w ten oto sposób:

void keyReleased(){
 switch (key){
    case ' ': 
    stop=!stop;
    if (stop)
      print("PAUZA");
    else
      print("GO!");
    break;
    
    case 'z': ML=ML/2;break;
    case 'Z': ML=ML*2;break;
    case 'x': skokX-=1;break;
    case 'X': skokX+=1;break;
  }//switch
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Od razu zauważamy też, że nie ma co wykonywać sprawdzenia if (skokX<1) w każdym wywołaniu funkcji keyReleased(), gdyż to sprawdzenie dotyczy tylko przypadku zmiany zmiennej skokX – za pomocą klawisza 'x’. Dlatego to dodatkowe sprawdzenie przenosimy w odpowiednie miejsce:

   case 'x': skokX-=1; if (skokX < 1) skok=1; break;

i kasujemy z końca funkcji keyReleased(). Prosta sprawa, a poprawiamy wydajność naszego programu.

Kolejną optymalizacją jest samo skalowanie liczb: w funkcji draw() za każdym razem z odebranego x-a robimy odpowiedni (przeskalowany) y-k, co wymaga operacji dzielenia i mnożenia. Dużo – za dużo. To dzielenie wynika z wyliczenia współczynnika a dla prostej, który przecież zmienia się tylko wówczas, gdy zmieniamy zoom (klawisze z/Z). Jeśli nic nie zmieniamy, to po co wykonujemy dzielenie? Dzielenie jest drogie, więc warto tego unikać. Dlatego wprowadzę zmienną przechowującą wartość tego współczynnika (pamięć jest tania w porównaniu do dzielenia!), nazwę go wspA i będę go wyliczać tylko wówczas, gdy trzeba (klawisze z/Z).

float wspA;
void keyReleased(){
 switch (key){
 case ' ': 
   stop=!stop;
   if (stop)
     print("PAUZA");
   else
    print("GO!");
   break;
 
   case 'z': ML=ML/2;wspA=(float)height/ML;break;
   case 'Z': ML=ML*2;wspA=(float)height/ML;break;
   case 'x': skokX-=1;if (skokX < 1) skokX=1;break;
   case 'X': skokX+=1;break;
 }//swicth
}

oczywiście należy także zmodyfikować funkcję rysującą – draw()

void draw(){
 if (!stop){
   int x=getValue(); 
   print(x);
   y=wspA*w;
   line(xPos-skokX, poprzedniY, xPos, y);
   poprzedniY = y;
   println(" ", y, " ML=",ML, " skokX=", skokX);
 
   xPos=xPos+skokX;
   if (xPos >= width){
     xPos=0;
     background(0);
   }
 }//if stop
}

i widzimy, że wyznaczenie y-ka polega teraz już na wykonaniu tylko jednej operacji mnożenia – bez dzielenia. Sprytne, prawda? (uwaga: należy jeszcze dodać wyliczenie współczynnika wspA w funkcji setup(), aby wartość ta była ustalona na początku programu).

Będzie to tym bardziej efektywne, gdy nasze skalowanie będzie pełne i uwzględni nie tylko maksymalną wartość ML ale i minimalną mL (o czym pisałem gdzieś wyżej). W takim przypadku pojawi się niezerowy współczynnik b prostej, który wymaga kolejnych operacji mnożenia/dzielenia – dlatego trzeba wprowadzić kolejną pomocniczą zmienną, powiedzmy wspB i postępować podobnie.

Czego się dziś nauczyłeś

  • skalowanie
  • processing: rysowanie prostokątów
  • skalowanie 
  • obsługa klawiatury
  • skalowanie
  • instrukcje switch-case
  • optymalizacja
  • czy wspomniałem o skalowaniu? 😉

(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
  • skalowanie minimalnej liczby (więcej skalowań)
  • przesuwanie wykresu góra/dół (więcej skalowań)
  • obsługa myszki i wyświetlanie dowolnych wartości z historii wykresu
  • pisanie napisów w oknie processinga (np. liczb z wartościami na wykresach)
  • pisanie skali na wykresie (np. mL i ML z brzegu okienka)
  • podpis osi x – można użyć czasu do opisu tej osi, będzie widać kiedy odebrano konkretną daną
  • ładniejsza grafika

Zapraszam na kolejne zajęcia!

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:

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:

  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:

#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:

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!

Processing — wizualizacja danych z Arduino

processing.org

Świetny program do nauki programowania – gdyż w bardzo prosty sposób wyświetla grafikę. IDE jest proste, intuicyjne, bardzo podobne do Arduino IDE (w rzeczy samej – to też produkt MIT). Wiele osób, w tym także my, będziemy używać processing do graficznego przedstawienia tego, co „mówi” do nas Arduino (czyli Serial.print z Arduino trafi do processinga, który wyświetli stosowną grafikę). Warto wspomnieć o kilka zalet tego softwaru:

  • wieloplatfromowy (Linux, Mac, Windows)
  • darmowy (w tym także do celów komercyjnych)
  • wyświetla grafikę 2D, ale także 3D
  • obsługuje OpenGL
  • ogromna liczba dodatkowych bibliotek, rozszerzających możliwości
  • dużo manuali online (a także książek).

Podstawy

Język programowania to C++ (Java, JavaScript, Ruby, Python) więc nie powinniśmy się czuć się nieswojo. Przyjrzyjmy się szkieletowi typowego programu („skeczu”)

void setup(){
  size(800,600);
  background(0);
}

void draw(){
  ellipse(10,10, 60, 100);
  fill(12, 23, 34);
  rectangle(50,50, 100, 200);
}

Widzimy dwie funkcje – setup() oraz draw() – podobnie, jak programując w Arduino IDE. To podobieństwo nie jest przypadkowe, bo jak już wspomniałem projekt powstał przez ludzi z MIT. Dlatego też znaczenie funkcji setup() jest takie samo jak w Arduino – funkcja wykona się tylko jeden raz, na początku programu. Kolejna funkcja – draw() – będzie się wykonywana „w kółko”, czyli po zakończeniu wykona się ponownie – czyli tak, jak funkcja loop() w Arduino IDE (co prawda w przypadku processinga można zmienić takie zachowanie, ale nie wnikajmy w takie szczegóły).

Po zapoznaniu się z podstawowymi prymitywami graficznymi processinga (prostokąt, linia, elipsa) przeszliśmy do części głównej dzisiejszych zajęć – połączenia Arduino z grafiką.

Wysyłamy liczby z Arduino

W przyszłości  będziemy wyświetlać odczyty z podłączonych czujek (np. czujki pola magnetycznego SS49e, o czym było na poprzednich zajęciach), ale teraz upraszczamy maksymalnie i wykorzystujemy Arduino do nadawania pseudolosowych liczb całkowitych. Uruchamiamy Arduino IDE i wpisujemy poniższy skech

void setup(){
  Serial.begin(9600);
}

void draw(){
  Serial.println(random(1024));
  delay(200);
}

Odczyt danych z portu COM w processingu

import processing.serial.*;
Serial moj_port;

void setup(){
 println("lista dostepnych portow COMM: ");
 println(Serial.list());
 println("probuje port: ", Serial.list()[0]);
 moj_port = new Serial(this, Serial.list()[0], 9600);
}

int dane;
void draw(){
  if (moj_port.available() > 0){
    dane = moj_port.read();
    println("wczytalem: ", dane);
  }//if
}

Ważne w powyższym programie jest wybórk konkretnego portu szeregowego, czyli linijka 

moj_port = new Serial(this, Serial.list()[0], 9600);

gdzie ja wybrałem pierwszy element (indeks zerowy tablic, czyli [0]) z listy dostępnych portów (Serial.list()) – ale może się zdarzyć, że u Was nie będzie to pierwszy, a jakiś inny element – dlatego wypisałem na ekranie całą listę i port, z którym próbuję się połączyć. Proszę przyjrzeć się tym komunikatom, jeśli coś nie idzie tak jak powinno.

Oczywiście programik nic nie wyświetla graficznego, tylko odczytuje dane (i to błędnie! o czym wspomniałem na końcu zajęć – problem ten rozwiążemy później) i wypisuje je na ekranie. Przechodzimy do grafiki (prostej).

Kreślimy wykres

Lekko modyfikujemy programik. 

import processing.serial.*;
Serial moj_port;

int xPos;

void setup(){
 println("lista dostepnych portow COMM: ", Serial.list());
 println("probuje port: ", Serial.list()[0]);
 moj_port = new Serial(this, Serial.list()[0], 9600);
}

int dane;
void draw(){
  if (moj_port.available() > 0){
    dane = moj_port.read();
    println("wczytalem: ", dane);

    rect(xPos, height/2, 10, dane);
    xPos+=10;
    if (xPos >= width) {
      xPos = 0;
      background(0);
    }
  }//available
}

Bardzo prymitywna grafika, ale niech to wystarczy na dzisiaj. 

procc0

Pozostaje kilka kwestii do rozwiązania:

  • poprawne wczytywanie liczb (z Arduino wysyłamy 0..1023, a processing „widzi” tylko jakieś małe liczby, <30?)
  • ładniejsza grafika.

Zapraszam na kolejne zajęcia!

Podstawy: czujka pola magnetycznego SS49E

Analogowa czujka pola magnetycznego SS49E

 

ss49eSS49E to bardzo fajny układ do mierzenia wartości pola magnetycznego w zakresie -1500..0..+1500 Gs. Warto zwrócić uwagę, że czujka ta mierzy zarówno „plusy” jak i „minusy”, czyli jest bibolarna – w odróżnieniu od układów unipolarnych (które reagują jedynie na konkretną polaryzację magnesu, czyli tylko biegun N lub tylko biegun S). Wartość zmierzonego pola tłumaczona jest na napięcie na pinie OUT z zakresu od 0.86V do 4.21V w sposób liniowy. Tę liniowość wykorzystamy za chwilę.

Analogowa czy cyfrowa?

Jest dużo podobnych układów. Inne czujki (szczególnie te unipolarne) są nazywane switczami (ang. switche) i są cyfrowe. Unipolarność wyjaśniłem wyżej, natomiast cyfrowość oznacza tu, że układy te reagują jedynie na jakąś konkretną wartość pola magnetycznego (wartość progową). I tak w przypadku przekroczenia tego progu czujka zwraca napięcie wysokie, a jeśli próg nie został przekroczony – napięcie niskie. Świetnie to się sprawdza w wielu sytuacjach (inteligentne domy – okno zamknięte/otwarte, lub do mierzenia obrotów wszelakiego rodzaju kół). Przykładem takiej czujki jest TLE4905L. Jest też z reguły o połowę tańsza. Ale nie o niej dziś będzie (wspomnianą pobawimy się na innych zajęciach) – my mamy czujkę analogową. Jak więc jej używać?

Pinologia (aka. podłączenie)

Jak zawsze należy pobrań z internetu notę katalogową (szukamy frazy SS49E datasheet lub podobnie) i przeglądamy całość. Nie musimy wszystkiego rozumieć, zwracamy uwagę na opis ogólny układu i sposób podłączenia. Poniżej zamieszczam wycinek z tego dokumentu:

ss49e-pins

i już wiemy, która nóżka tego układu co oznacza. Dalej doczytujemy, że napięcie zasilające (Vdd) musi być z przedziału 4-6V, czyli Arduino UNO jak najbardziej się nada. Przy podłączeniu dbamy o odpowiednie ustawienie układu!

Odczyt wartości pola

Specyfikacja podaje, że SS49E zwraca wartość 0.86V gdy pole wyosi -1500 Gs, oraz 4.21V gdy pole wynosi +1500 Gs. Jak więc obliczać co wskazuje czujka? Musimy podłączyć układ do zasilania (nóżki 1 i 2) a także nóżkę 3 (nazwaną OUT) do woltomierza (miernika uniwersalnego) lub do Arduino i portu analogowego (np. A0). Oczywiście zachęcam do rozpoczęcia zabawy z miernikiem i magnesem, aby sprawdzić jak to działa. Po tej czynności wracamy do Arduino i podłączamy nóżkę OUT do portu analogowego A0 w Arduino. Potrzebujemy przelicznika.

ss49e
(układ SS49E + Arduino UNO + magnes + płytka stykowa).

Czujka liniowa

Oznacza to, że wartości napięcia z pinu OUT odpowiadają wartościom pola w sposób liniowy, czyli jak funkcja y=a*x+b. Traktujemy więc x jako napięcie z pinu OUT, a y jako wskazywane pole. Dlatego zapisujemy układ równań na nieznane jeszcze współczynniki prostej a i b. Są to:

-1500 = a*0.86 + b    (1)
oraz
1500 = a*4.21 + b.     (2)

Te dwa równania należy rozwiązać wyznaczając współczynniki prostej a i b a następnie dla każdego odczytanego napięcia z pinu OUT (oznaczmy to x) obliczać natężenie pola według przepisy

y=a*x+b,

gdzie y to właśnie wartość pola.

float a=....;//policz samodzielnie, rozwiązując (1) i (2)
float b=....;//policz samodzielnie
void setup(){
  Serial.begin(9600);
}

void loop(){
  int odczyt=analogRead(A0);
  float x=odczyt*4.56/1023;
  Serial.print("napięcie OUT [V] = ");
  Serial.print(x);
  Serial.print(" ,pole magnetyczne [Gs] = ");
  Serial.println(a*x+b);
}

Uwaga

Kluczowe jest odpowiednie odczytanie napięcia z nóżki OUT układu SS49E – bardzo przydatny okaże się woltomierz (multimetr w trybie woltomierza). Zwróć uwagę na program powyżej, gdzie moje Arduino jest już lekko uszkodzone i zamiast książkowych 5V otrzymuję jedynie 4.56V z pinu oznaczonego 5V – dlatego moje przeliczenie odczyt <—> x jest w taki, a nie inny sposób.

Rozdzielczość czujnika

Ze specyfikacji czujnika wynika, że na 1 Gs przypada napięcie 0.001116666 V, czyli 1.116 mV (obliczone jako iloraz 4.21-0.86 — caly zakres napięcia — i liczby 3000 — cały zakres pola, od -1500 do 1500). Natomiast rozdzielczość wejścia analogowego to 4.457 mV na 1 jednostkę w funkcji analogRead (obliczone jako iloraz 4.56 i 1023). Oznacza to, że nie jesteśmy w stanie mierzyć Aruinem tak dokładnie, jak by się chciało, ale tylko z dokładnością plus/minus 4 Gs. Dochodzą do tego jeszcze fluktuacje samego przetwornika analogowo-cyfrowego w Arduino – przyznacie, że widzicie czasami skoki odczytów o plus/minus 1? może nawet 2? Dlatego zmierzona w ten sposób wartość pola to raczej plus/minus 8 Gs. 

Ziemskie pole magnetyczne

Na powierzchni Ziemi, bez magnesów i innych zewnętrznych pól powinniśmy otrzymać około 0.5 Gs — z naszą dokładnością każdy wynik <10 Gs jest OK.

Zabawa magnesem

Jak najbardziej! Próbujemy odczyty z obu biegunów magnesu, sprawdzamy zależność od odległości od czujki. W przypadku mocnych magnesów (np. neodymowych) uważamy, aby ich nie zbliżać do mikrokonktrolera Atmega na płytce Arduino — możemy go uszkodzić!

Okazuje się, że w zależności od napięcia zasilania czujka podaj zero pola magnetycznego różnymi wartościami napięcia z pinu OUT — opisane jest to w specyfikacji. Wrócimy do tego zagadnienia później.

 

Podstawy: Programowanie strukturalne cz.2 + czujka pola magnetycznego

Kontynuujemy – wyświetlacz 7-segmentowy

Tym razem dopięliśmy swego i wyświetliliśmy wszystkie cyferki 😉 Przy okazji użyliśmy tablic – ba, nawet tablic dwuwymiarowych! Ostro… Znalezione obrazy dla zapytania wyswietlacz 7 segmentowy

Na dodatek stworzyliśmy funkcję z argumentem – jaką cyferkę chcemy wyświetlić.

void cyfra(int nr){
//kod do wyświetlania...
}

void loop(){
  for (int i=0; i<=9; i++){
    cyfra(i);
    delay(500);
  }
}

Czujka pola – SS49E

Poznaliśmy też czujkę pola, działającą w zakresie -1500..+1500 G. Podłączyliśmy ją do woltomierza a Pan Kamil nawet do Arduino. Za tydzień każdy z nas podłączy sobie ten układ 😉

Podstawy: (7-seg.) + programowanie strukturalne cz.1

Podstawy – wyświetlacz 7-segmentowy

Nic nas nie goni, więc możemy poznawać tajniki programowania bez pośpiechu. Dlatego wróciliśmy do zagadnienia LED-ów, ale tym razem bazowaliśmy na całym układzie połączonych ze sobą LED-ów w „kostkę” – dzięki temu można wyświetlać cyfry. Znalezione obrazy dla zapytania wyswietlacz 7 segmentowy

Zabawę z tym układem rozpoczęliśmy od zestawu: płytka stykowa, bateryjka + opornik. Do zrozumienia co jest w środku tej kostki przydatny może być taki oto rysunek:

Znalezione obrazy dla zapytania wyswietlacz 7 segmentowy schemat

który właściwie pokazuje dwie wersje tego układu: ze wspólną katodą i anodą.

Podstawy – programowanie strukturalne

Po sprawdzeniu układu bateryjką na płytce stykowej przeszliśmy do programowania – na Arduino. Właściwie na… wirtualnym Arduino. Utworzyliśmy funkcje pomocnicze wyświetlające cyferki 0,1 i 2, oraz funkcję kasującą „ekran” wyświetlacza. Tworzenie takich funkcji – małych cegiełek, które możemy wielokrotnie wykorzystać – to właśnie programowanie strukturalne. 

//zmienne z informacja, gdzie podlaczylismy kazda z nozek kostki z cyferka
int ledA = 5;
int ledB = 4;
...

void setup(){
  pinMode(ledA, OUTPUT);
  pinMode(ledB, OUTPUT);
  ...
}

void nic(){
  kod do zerowania tego, co na wyświetlaczu
}

void jeden(){
  "rysujemy" jedynke
}

void dwa(){
  "rysujemy" dwojke
}

void loop(){
  nic();
  delay(500);
  jeden();
  delay(500);
  dwa();
  delay(500);
}

Zapraszam za tydzień!

Podstawy: dzielnik napięć, PWM, fotorezystor = inteligentne oświetlenie

Podstawy: dzielnik napięć

Obrazki z tablicy… 

Najpierw bawiliśmy się multimetrem i dzielnikiem:

fibot2016-11-22-note-19-36-1

a potem podłączyliśmy fotorezystor i próbowaliśmy go odczytywać z poziomu Arduino.

Pomysł Pana Pawła (PPP) aby najpierw zmierzyć multimetrem prąd płynący w obwodzie, a następnie znając podane napięcie i stosując prawo Ohma otrzymywać wartość rezystancji na fotooporniku – był dobry, ale wymagał ówczesnego użycia amperomierza (z multimetru). Mi bardziej chodziło o wykorzystanie fotorezystora w ten sposób, aby uzyskać informację czy go zasłaniamy czy nie, czy jest dużo światła zastanego (w pomieszczeniu) czy jest ciemno. Dlatego nie koniecznie interesuje mnie sama wartość oporu, a raczej jej zmiany. Dlatego po sprawdzeniu działania PPP i przyznaniu mu racji (a raczej Ohmowi), zaproponowałem zastosowanie dzielnika napięć i mierzenia napięcia w standardowy spodówb

fibot2016-11-22-note-19-36-2

void setup(){
  Serial.begin(9600);
}

void loop(){
  Serial.println(analogRead(A0));  
  delay(100);
}

W zależności od kolejności oporników (stałego R i zmiennego fotorezystora) otrzymywaliśmy liczby rosnące lub malejące zasłaniając fotorezystor ręką. Dodatkowo można było użyć latarki z telefonu komórkowego i symulować mocne oświetlenie.

PWM

Aby zrobić inteligentne oświetlenie potrzebowaliśmy sposobu na kontrolowanie jasności LED-a. Poznaliśmy Pulse Width Modulation i piny cyfrowe Arduino z „falką” (wolę: tyldą).

fibot2016-11-22-note-19-36-3

Inteligentne oświetlenie

To nic innego jak połączenie dwóch poznanych schematów:

  • mierzymy napięcie na fotorezystora przez wejście analogowe, a następnie
  • ustawiamy jasność LED-a sterując wypełnieniem PWM.

Jedyny problem to kwestia zamiany odczytywanych wartości z portu A0 (fotorezystora) na wartości akceptowane przez piny PWM (przypominam: 0..255). W tym celu wróciliśmy do gimnazjum i zastosowaliśmy funkcję liniową.

Dla przykładu: Pani Emanuela zastosowała stały opornik R o takiej wartości, że na porcie A0 odczytywała wartości 130 gdy fotorezystor był zasłonięty palcem, oraz 500 gdy był oświetlany światłem zastanym. Wartości pomiędzy przedziałem 130..500 odpowiadały częściowemu zasłonięciu ręką fotorezystora. Trzeba to teraz zamienić na liczby 0..255 aby sterować LED-em przez PWM (bo PWM akceptuje właśnie takie liczby, a nie 130..500). Dlatego stosujemy liniowe skalowanie (y=ax+b, współczynniki ab na razie nie znane), gdzie wartość 130 ma odpowiadać maksymalnemu świeceniu LED-a, czyli 100% wypełnieniu PWM-a (wartość 255), natomiast gdy odczytujemy 500 (jest jasno) to LED ma się nie świecić (wypełnienie 0). Trzeba skonstruować układ równań i wyznaczyć a oraz b a następnie przeliczać wskazania z portu A0 (traktując je jako x w równaniu prostej, a otrzymany y to właśnie wartość przekazana do PWM-a). Poniższy rysunek wyjaśniał ten opis:

fibot2016-11-22-note-19-36-4

Z kolei Pan Paweł użył innego rezystora (oraz innej kolejności ustawienia oporników) i miał następujący schemat do rozważenia 

fibot2016-11-22-note-19-36-5

W obu przypadkach trzeba było rozwiązać otrzymany układ równań na kartce a następnie wpisać liczby (wyliczone współczynniki a b) to takiego prostego programiku:

#define lampka 9
float a=0.689189, b = 344.595;

void setup(){
  Serial.begin(9600);
  pinMode(lampka, OUTPUT);
}

void loop(){
  int fotorezystor = analogRead(A0);  
  Serial.print("fotorezystor=");
  Serial.print(fotorezystor);  
  Serial.print("-->");  
  int pwm = a*fotorezystor+b:
  Serial.print(" PWM=");  
  Serial.println(pwm);
  analogWrite(lampka, pwm);  
  delay(100);  
}

Wrócimy do tego programu na kolejnym spotkaniu, bo trzeba tu o paru kwestiach wspomnieć. Ale już teraz zachęcam do zabawy z powyższym programikiem na wirtualnym Arduino (jest tam też wirtualny fotorezystor). Proszę też zastanowić się nad następującymi kwestiami:

  • dlaczego Pan Paweł miał prostą o współczynniki kierunkowym a>0, a Pani Ema a<0 ?
  • kto zastosował lepszą wartość opornika stałego – Pani Ema czy Pan Paweł, a może to nieistotne? 

Zapraszam za tydzień!

 

Podstawy – wejście analogowe + potencjometr

Podstawy – komunikacja szeregowa i obiekt Serial.

Z racji sporej liczby nowych ludzi (nie tylko studentów), głodnych wiedzy i żądnych przygód (zdjęć nie publikuję – tak, jak się umawialiśmy) rozpoczęliśmy od przypomnienia podstaw… Na warsztat trafiło pojęcie zmiennej. Aby to pojęcie „namacalnie” zobrazować przygotowałem programik, w którym poziom życia bohatera (np. Wiedźmina) reprezentowane przez zmienną energia ciągle malał (np. bohater ranny = krwawi). Aktualna wartość życia był wypisywany na ekranie monitora (via obiekt Serial i metoda print/println).

byte energia=77;

void setup(){
  Serial.begin(9600);
  energia=17;
}

void loop(){
  Serial.print("Energia= ");  
  Serial.println(energia);  
  delay(1000);
  energia= energia-1;  
}

Powyższy programik posłużył także do omówienia pojęć bit i bajt oraz wielkości informacji, jaką można zapisać wiadomości za pomocą n-bitów. Zapiski z tablicy w trakcie zajęć (pokolorowałem już po zajęciach):
fibot2016-11-15-note-19-03-1

gdzie przypominam, ze RAM na moim super-obrazku przedstawia całą pamięć operacyjną płytki Arduino UNO (dlatego komórki pamięci – bajty – są ponumerowane od 1..2048, bo UNO ma 2kB pamięci), gdzie mikrokontroler przechowuje właśnie zmienne. W szczególności w naszym programie zaznaczyłem miejsce w pamięci, gdzie zadeklarowaliśmy zmienną energia. Na tym rysunku (modyfikowanym w trakcie zajęć – pamiętacie?) zmienna ta zajmuje 2 komórki (bajty) i odpowiada to już sytuacji innej niż z pierwszego listingu programu: mianowicie int energia=77; Zmienna typu int to 16 bitów, czyli 2^16 różnych informacji (kolor zielony na pokolorwanej tablicy, natomiast kolor żółty – to bity). Pierwotnie była to zmienna byte, czyli 8 bitów i 256 dopuszczalnych wartości (kolor niebieski). Ta pierwsza wersja programu była bardzo treściwa, gdyż pokazywała sytuację co się dzieje z wartością zmiennej, gdy przekraczamy jej dopuszczalny zakres: w naszym przypadku zmniejszaliśmy wartość zmiennej energia co 1 sekundę (delay(1000)) no i gdy mieliśmy już „na liczniku” 0 (zero) to wcale nie pojawiło się -1 (minus jeden) tylko… 255! a potem juz 254… 253… itd. Warte to jest zapamiętania (no i oczywiście w drugą stronę – gdybyśmy zwiększali naszą zmiennę z wartości 255 o jedne to… wiesz, co będzie? jeśli nie, proponuję sprawdzić!).

Potencjometr nastawny.

potencjometr-osiowy-liniowy-5kNa spotkaniu poznawaliśmy potencjometr nastawny i jego podłączenie/obsługę przez Arduino. Ale najpierw zabawy z bateryjka i multimetrem – ćwiczenia obowiązkowe. Dodatkowo przypomniałem co to jest dzielnik napięć i jak „to się je”, a tym samym (mam nadzieję) zrozumieliśmy działanie potencjometru nastawnego (w naszym przypaku liniowego 10k).potencometr

Powyższy schemat tłumaczy działanie potencjometru… Warty zapamiętania jest też taki rysunek:

który pokazuje co się dzieje gdy mierzymy napięcie, lub raczej (prawidłowo) różnicę napięć (potencjałów). Podłaczamy do pinu nr 1 (kolor czerwony na rysunku, numeracja odnosi się do schematu potencjometru z poprzedniego obrazka) „minus” z bateryjki, a do pinu 3 „plus” z bateryjki (niech to będzie 4x bateria AAA – czyli właśnie 6V). Gdy ustawiemy potencjometr w takiej pozycji, aby zmierzone napięcie na pinie nr 2 wynosiło 4V to w zależności od tego, jak mierzymy (=jak podłączamy sondy multimetru) możemy otrzymać też wynik 2V. Chodzi oczywiście o poziom odniesienia (sonda czerwona jest cały czas w „środkowej nóżce”, czyli pinie nr 2, natomiast sonda czarna – poziom odniesienia właśnie – może być w pinie 1 lub 3). Jeśli naszym poziomem będzie GND (=0V, pin 1) to faktycznie otrzymamy 4V, ale gdy mierzymy napięcie pomiędzy „szczytem góry” a naszą pozycją (sonda czarna „na szczycie”, czyli pinie 3) to oczywiście otrzymamy 2V. Wszystko jasne?

Po zabawach z multimetrem (i LED-em podłączonym do potencjometru) przyszedł czas na podłączenie do Arduino i wpisanie nowego kodu programu:

#define IN A0

void setup(){
  Serial.begin(9600);
}

void loop(){
  int war = analogRead(IN);  
  Serial.print(war);  
  Serial.print("-->");  
  Serial.print(war*5.0/1024);  
  Serial.println("V");  
  delay(100);  
}

Ten program (dla odróżnienia się od poprzedniego) wprowadza etykiety nazw (za pomocą dyrektywy preprocesora #define) i nie posługuje się zmienną w tym celu. Zmienna pojawia się dopiero w funkcji loop(). Przypominam, że „zysk” ze stosowania etykiet nazw jest taki, że nie zajmują one pamięci RAM komputera… (ale są i minusy).

„Danie główne” dzisiejszego spotkania to piny analogowe Arduino, i aby je zrozumieć posłużyłem się takim rysuneczkiem:

fibot2016-11-15-note-19-03-3

gdzie pokazuję jakiś przebieg napięcia w czasie (krzywa czarna na wykresie V(t), minimalne napięcie 0, zaznaczone jest też poziom 5V), który teraz możemy odczytywać z rozdzielczością 7-miu poziomów (od 0..6). Mamy więc odczyty jako napięcia jako liczby całkowite 0,1,2,..,6 które odpowiadają napięciom 0V, 0.8333V, 1.666V, …, 5V (przedziały napięcia to właśnie dV=5/6V). Przy takiej rozdzielczości nie ma możliwości odróżnić napięć 0.2V, 0.6V czy 0.8V, gdyż te odczyty trafiają do jednego „worka” (tu: zero). Dopiero poziom 0.84V zmienia wartość mojego odczytu (tu: jedynka).

W przpadku Arduino mamy nie 7 dostępnych poziomów, a 1024 (gdyż jest tam przetwornik analogowo cyfrowy 10-cio bitowy, czyli 2^10=1024). Stąd też i dokładność pomiarów dużo lepsza niż na moim rysuneczku. 

Co najciekawsze, wykonaliśmy kalibrację odczytów z analogowego portu Arduino – posłużyliśmy się multimetrem. Okazało się bowiem, że bez tego często pomiary były baaaardzo nietrafione (tj. dużo się różniły wskazania woltomierza od wskazań Arduino). Przyczyną były doś „spracowane” płytki Arduino…

Bardzo ważna była też informacja o dzieleniu liczb: przypominam, że 5/1024 jest zawsze 0 (zero), natomiast 5.0/1024 już nie.

Zapraszam za tydzień!

 

Encoder do silników DC – pracujemy…

Panu Kamilowi udało się zbudować układ, który w podłączeniu z processingiem produkuje takie obrazki:zad4 zad3 zad2 zad1

Widzimy tu dwie krzywe, ale to właśnie ta biała pochodzi z omawianego encodera. Teraz „trzeba tylko” opracować algorytmiczną metodę zliczania pików w danym okresie czasowym, a następnie przełożyć to na faktyczne obroty koła… Prace w toku…

Przy okazji: ta żółta krzywa pochodzi od czujki pola magnetycznego – dzięki niej w późniejszym czasie można będzie przełożyć zliczone piki (białe) na faktycznie wykonane obroty (żółte dwa piki odpowiadają jednemu pełnemu obrotowi).

Plik z przykładowymi danymi – do wstepnej obróbki (gnuplot, arkusz kalkulacyjny Calc…).