Kontroler do pojazdu — tekstowy protokół

Pojazd sterowany – nRF24L01

Pan Przemek dalej kombinuje z komunikacją radiową na bazie układu nRF24. Jako nową zabawkę dostał JoyShield-a do Arduino, o takiego:

Jest to bardzo ciekawy układ, bo nie dość, że ma joystick oraz 7 przycisków (4 duże, kolorowe, jeden w joysticku, oraz 2 małe – mikrostyki), to ma jeszcze adaptery na radiówkę nRF24 (i inne też, ale tego nie tykamy). Wszystko złożone w „kanapkę” może pełnić funkcję kontrolera – po przyczepieniu bateryjki 9V (np. gumką recepturką).

Tekstowy protokół — kodowanie

Bazujemy na protokole tekstowym – shield odczytuje położenie joysticka, oraz 5 przycisków (chwilowo nie obsługujemy wszystkich). Dane wysyłane są przez nRF24 jako tekst, a poszczególne pola oddzielone są średnikiem. Jedna paczka danych wygląda więc tak:

507;512;0;1;1;0;0;

gdzie pierwsza liczba określa położenie joy-a na osi X, druga na osi Y, a kolejne zera i jedynki to stan logiczny 5-ciu przycisków. Bardzo proste w utworzeniu tego napisu — dzięki klasie String i jego licznie przeciążonych konstruktorach, oraz operatorowi „dodawania”.

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
 
RF24 radio(9, 10);//CE, CS
 
uint8_t rxAddr[6] = "grzyb";
 
void setup(){
  Serial.begin(9600);
  Serial.print("nRF24 INIT=");

  bool ok=radio.begin();
  Serial.println(ok);
  radio.setRetries(15, 15);

  radio.openWritingPipe(rxAddr);  
  radio.stopListening();

  //dla modulu JoyShield
  pinMode(2, INPUT);
  pinMode(3, INPUT);  
  pinMode(4, INPUT);  
  pinMode(5, INPUT);  
  pinMode(6, INPUT); 
}
 
char bufor[32];
String napis;
 
void loop(){
  //tworzymy napis wedlug naszego protokolu 
  napis = String(analogRead(A0)) + ";" + String(analogRead(A1)) + ";" + String(digitalRead(2)) + ";" + String(digitalRead(3)) + ";" + String(digitalRead(4)) + ";" + String(digitalRead(5)) + ";"  + String(digitalRead(6)) + ";";

  //przygotowujemy bufor -- tablice z napisem...
  napis.toCharArray(bufor, 32);

  //wazne! wysylamy bufor a nie napis!
  radio.write(&bufor, sizeof(bufor));
}

Ponieważ radio wysyła dane w postaci tablicy, nie możemy wysyłać obiektu napis. Dlatego korzystamy z metody toCharArray() klasy String i przekopiowujemy zawartość napisu do bufora – tablicy. W „eter” wysyłamy tablicę bufor.

Tekstowy protokół — odczyt

No właśnie, prostota użycia napisów pociąga za sobą „problem” odczytywania takich danych. W grę wchodzą bardzo przydatne metody klasy String:

  • indexOf(napis) — zwracająca pozycję napisu w danym stringu (u nas napis to średnik, którym oddzielaliśmy liczby)
  • length() — zwracająca długość stringu
  • remove(od, do) — ucinająca napis od pozycji od do pozycji do
  • substring(od, do) — zwracająca podciąg w danym napisie, od pozycji od do pozycji do
  • toInt(napis) — zamienia napis na liczbę całkowitą (int)

Poniżej program dekodujący nasz protokół – czyli zamieniający napis na

int x,y;
byte b1,b2,b3,b4,b5;

Napis wczytany z klawiatury, przez komunikację szeregową – dzięki temu możemy testować nasz program na różne sposoby (o przeniesieniu tego kodu na radio — będzie za tydzień).

void setup() {
  //dane wprowadzamy z klawiatury przez Serial
  Serial.begin(9600);
}

String tekst;
String ciag;
char znak = ';';//separator pola
int x,y; //wspolrzedne x,y
byte b1,b2,b3,b4,b5;//stan 5 przyciskow
int k,l;//pomocnicze

unsigned long int t1,t2;

void loop() {
 if(Serial.available()>0){
    tekst=Serial.readString();
    Serial.println(tekst);

    t1=micros();
    k=tekst.indexOf(znak);
    ciag=tekst.substring(0,k);
    x=ciag.toInt();
    l=k+1;
    
    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    y=ciag.toInt();  
    l=k+1;
    
    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    b1=ciag.toInt();
    l=k+1;

    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    b2=ciag.toInt();  
    l=k+1;

    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    b3=ciag.toInt();  
    l=k+1;

    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    b4=ciag.toInt();  
    l=k+1;

    k=tekst.indexOf(znak,l);
    ciag=tekst.substring(l,k);
    b5=ciag.toInt();  
    l=k+1;
    
    t2=micros();
    
    Serial.print("Pozycja X: ");
    Serial.println(x);
    Serial.print("Pozycja Y: ");
    Serial.println(y);
    Serial.print("PRZYCISK 1: ");
    Serial.println(b1);
    Serial.print("PRZYCISK 2: ");
    Serial.println(b2);
    Serial.print("PRZYCISK 3: ");
    Serial.println(b3);
    Serial.print("PRZYCISK 4: ");
    Serial.println(b4);
    Serial.print("PRZYCISK 5: ");
    Serial.println(b5);
    Serial.print("czas dekodowaia [mikrosekundy]= ");
    Serial.println(t2-t1);
  }//if
}//loop

Tekstowy protokół — szybkość dekodowania

Wielokrotne użycie powyższych funkcji powoduje, że odkodowanie napisu 507;512;0;1;1;0;0; i zamiana go na

int x, y;
byte b1,b2,b3,b4,b5;

zajmuje dla Arduino UNO od 260 do 550 mikrosekund (czyli ~0.5ms). A dlaczego nie jeden, równy czas? Bo w zależności od postaci dekodowanego napisu mikrokotrolerek musi więcej lub mniej pracować. Mniej „męczy” się w przypadku ciągu 1;1;1;0;0;0;0; a więcej w przypadku 1012;1017;0;0;0;1;1; Dobra, zrozumiałe. A czy to duży czas? Dla człowieka to nic, dla elektroniki sporo… Czy to nam wystarczy – czy nie spowoduje opóźnień (tzw „lagów”) w sterowaniu pojazdem? O tym przekonamy się po zastosowaniu tego protokołu do pojazdu (za tydzień).

Tekstowy protokół: modyfikacje — suma kontrolna

Można pomyśleć o rozbudowie naszego protokołu na dodatkowe „pole”, będące sumą kontrolą. Jeden z pomysłów to wysumowanie wszystkich danych (w końcu to liczby całkowite) i zapisanie tej sumy jako ostatni, dodatkowy element. Dla naszego przypadku wyglądałoby to tak: 507;512;0;1;1;0;0;1021; Po stronie odbiornika dekodujemy napis, liczymy sumę x+y+b1+b2+b3+b4+b5 i porównujemy z ostatnią wartością – nazwijmy ją sumak. A Jeśli nie ma równości… odrzucamy (ignorujemy) paczkę danych i czekamy na kolejną.

Tekstowy protokół: modyfikacje — protokół binarny

Lepiej by było stworzyć podobny protokół ale binarny, czyli nie bawić się w zapis liczb w postaci stringów, dodatkowo oddzielać je przecinkami tylko wysyłać x,y jako integer, a przyciski b1,b2,b3,b4 i b5 jako byte (nie 5 bajtów, a jeden – wszak 1 bajt = 8 bitów, mamy więc nadam zapas). Zyskujemy na tym mniejsze porcje danych – wszystkie informacje z JoyShielda to tylko 5 bajtów, a nie 14 (najlepszy przypadek) czy nawet 20 bajtów (najgorszy przypadek). No i nie ma zabawy w dekodowanie danych z wykorzystaniem metod klasy String… Ale to może przy innej okazji 😉

(c) K.G.

joyshield + stringi + nRF24

Dziś nowy gadget:

czyli joy-shield. Co to ten „shield”? To rozszerzenie, z różnymi modułami, które nadrukowane są na płytkę PCB i nie wymagają płytki stykowej i masy przewodów. Taką płytkę „wtyka” się w piny Arduino, tworząc „kanapkę”. Ten shield ma „gierkowe” przeznaczenie – choć my go wykorzystamy do sterowania pojazdem, ale są też inne (do internetu przez kabel eRJotkę, przez sieć bezprzewodową, z kartą SD do zapisu danych, z obsługą silników i inne). Warto łapać okazję na allegro lub podobnych, bo ja swój shield kupiłem za 25 zł! A co on ma w sobie?

  • Joystick (dwie osie + przycisk)
  • cztery przyciski z kolorowymi klawiszami
  • dwa dodatkowe przyciski microswich
  • złącze dla modułów transmisji radiowych:
    — nRF24xx
    —  xbee
    — APC200
  • stabilizator napięcie 3.3V (do zasilania układów radiowych)

Oczywiście moduł (=shield) przeznaczony do współpracy z płytkami Arduino UNO, MEGA, LEONARDO itp. Warto zwrócić uwagę na jakość wykonania – mój ma fajne opisy w łatwo dostępnych miejscach.

Obsługa joysticka – shielda

Okazuje się banalnie prosta. Czytamy położenie dwóch osi (x i y) – czyli porty analogowe A0 i A1 (cóż, ten shield je sobie „rezerwuje” i nie mamy ich możliwosci wykorzystywania). Wszystko ładnie opisane na płytce. Podobnie z przyciskami.

void setup() {
Serial.begin(9600);
}
int x,y;
void loop() {
  x=analogRead(A0);
  y=analogRead(A1);
  Serial.print(x);
  Serial.print("  ");
  Serial.print(y);
  if (digitalRead(5)==1)
    Serial.print(" niebieski");
  if (digitalRead(4)==1)
    Serial.print(" bialy");
  if (digitalRead(3)==1)
    Serial.print(" czerwony");
//i tak dalej
  Serial.println();
  delay(100);
}

Zwracam uwagę na „sprytne” granie metodą print oraz println wraz ze spacjami tak, aby wszystko mieściło się w jednej linii i nie było „pozlepiane”.

A odczytywanie przycisków – skoro używam funkcji digitalRead(pin) to dlaczego nie ma wcześniej pinMode(pin, INPUT)? Otóż domyślnie piny ustawione są w trybie INPUT, dlatego właśnie nic nie dopisywałem. Jesto to warte zapamiętania, co powino nie być trudne – w końcu dla wejść analogowych nie ustawialiśmy w tryb INPUT, czyli z wejściami cyfrowymi jest tak samo.

Stringi = napisy

Temat rzeka… trzeba zacząć, czym są tablice w C. Zauwazyłem, że godnym polecenia jest artykuł na oficjalnej stronie Arduino. Ja dołączam „zrzut ekranu” z zajęć.

Jak zmienić nasz poprzedni kod, aby działał na napisach?

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

String napis;
void loop() {
  napis=String(analogRead(A0));
  napis = napis + "  ";
  napis = napis + String(analogRead(A1));
  if (digitalRead(5)==1)
    napis = napis + String(" niebieski");
  if (digitalRead(4)==1)
    napis += String(" bialy");
  if (digitalRead(3)==1)
    napis += String(" czerwony");
  //i tak dalej
  Serial.println(napis);
  delay(100);
}

Oczywiście nic tutaj nie zyskaliśmy, a nawet gorzej – obsługa klasy String w Arduino jest zasobożerna – czyli mocno powiększa nasz kod, ale tym się jeszcze nie przejmujemy. Na uwagę zasługuje łatwe dodawanie (łączenie) ze sobą napisów – realizuje to zwykły operator + (plus)

Joyshield + nRF24L01+

Łączymy poznane dziś rzeczy z poprzednimi zajęciami i nadajemy to, co robimy na naszym kontrolerze. Właśnie dlatego użyliśmy wersji Stringowej kodu do joysticka, bo radyjko nadawało właśnie napisy – a nawet napisy z maksymalną długością 32 znaków (bajtów). Poniżej kod:

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>

RF24 radio(9, 10);//CE, CS

uint8_t rxAddr[6] = "grzyb";

void setup(){
  Serial.begin(9600);
  Serial.print("nRF24 INIT=");
  bool ok=radio.begin();
  Serial.println(ok);
  printf_begin;
  radio.printDetails();
  radio.setRetries(15, 15);
  radio.openWritingPipe(rxAddr);
  
  radio.stopListening();
}

String napis;

void loop(){
  napis = String("Heniek "); 
  napis += String(analogRead(A0));
  napis += " ";
  napis += String(analogRead(A1));
  if (digitalRead(5)==1)
    napis += String(" niebieski");
  if (digitalRead(4)==1)
    napis += String(" bialy");
  if (digitalRead(3)==1) 
    napis += String(" czerwony");
  //i tak dalej<br>
  radio.write(napis.c_str(), napis.length());
  Serial.println(napis);
  delay(100);
}

Na uwagę zasługuje linia nr 37, gdzie musiałem użyć nowej metody klasy String o nazwie c_str(), której zadaniem jest stworzenie napisu w stylu języka C (nieobiektowego). Wymuszone jest to przez metodę write() klasy radio. Dodatkowo, długość napisu w klasie String uzyskujemy za pomocą metody length() – a nie sizeof(napis), gdyż to by zwróciło tylko rozmiar obiektu (a nie danych wewnątrz tego obiektu).

Z kolei w linii 26 rozpocząłem nasz napis imieniem (fikcyjnym) uczestnika koła Fi-BOT tak, aby uruchomiony przeze mnie program odbiornika nRF mógł łatwo zidentyfikować kto mi nadaje (Tosiek, Heniek czy Ziuta — także fikcyjne imiona).

Co dalej?

Wydawać by się mogło, że trzeba teraz tak przygotowany kod uruchomić do sterowania pojazdem. Nieprawda. Było by to baaaardzo kłopotliwe. Problem polega na tym, że trzeba odczytać napis w odbiorniku nRF umieszczonym w pojeździe, a potem odczytać aktualne wychylenie osi X i Y joya, i przyciski. Ale to trudne! Nawet przy pomocy funkcji toInt() zamieniającej napis na liczbę całkowitą – bo najpier należało by podzielić nasz napis na części – może przecinkami, jakoś tak:

123,512,czerwony,zielony

Czyli wówczas przecinek (kropka, myślnik – cokolwiek) by nam oddzielał jedną informację od drugiej, co by było do wykorzystania w połączeniu z metodą indexOf() z klasy String. Cała procedura mogła by wyglądać jakoś tak:

  • odczytujemy napis w pojeździe, przesłany przez joyshield (z przecinkami)
  • skupiamy się na pierwszej części napisu, aż do pierwszego przecinka – wiemy, że ma to być pozycja na osi X – funkcja StringSubstring() i/lub wspomniana indexOf()
  • zamieniamy ten krótki podciąg (od zera do 4 znaków) na liczbę całkowitą – metoda toInt() – i mamy już oś X
  • kasujemy/wyrzucamy z napisu pierwszą część do przcinka, przechodzimy do pozycji na osi Y
  • robimy podobnie, a potem
  • jeśli napis nie jest jeszcze pusty (bo ciągle kasowaliśmy informację o osi X i Y), to znaczy, że mamy jakieś przyciski
  • jeśli jest przecinek, to znaczy, że są nawet dwa (lub więcej) wciśniętych przycisków
  • tworzymy pierwszy podciąg aż do pierwszego przecinka, porównujemy czy jest to „zielony”, „czerwony” itd…
  • po odczytaniu przycisku kasujemy z napisu tą informację
  • jeśli napis niepusty, to są kolejne przyciski…

Proszę zwrócić uwagę, jakie to zagmatfane! Dlatego na kolejnych zajęciach będziemy wysyłać z nadajnika nie napisy (Stringi), ale bajty z informacjami. Dwa bajty wystarczą do podania osi X, kolejne dwa do osi Y, a jeszcze jeden – tylko jeden – do podania stanu wszystkich 7 przycisków na naszym shieldzie. Stworzymy własny protokół danych – podobny do tego, jakiego używaliśmy przy omawianiu oscyloskopu. Dlatego zachęcam do ponownego wczytania się w tamten wpis i opis protokołu danych. Właśnie to nas czeka na następnych zajęciach! Ekscytujące, nieprawdaż? 😉

Przypominam o spotkaniu w ten piątek 19-maja o 14:15 w sprawie XV Festiwalu, a kolejne zajęcia we wtorek 23 maja o godz. 16:15. Nie wiem, czy będziemy mieć siły na 22 więc od razu uprzedzam, że w poniedziałek 22 maja, podczas imprezy Festiwalowej, ustalimy termin kolejnego spotkania Fi-BOTa. Zapraszam!