Najlepsze praktyki i wzorce protokołu komunikacyjnego

19

Za każdym razem, gdy projektuję protokół szeregowy do użycia między dwoma arduinami, czuję się trochę tak, jakbym wymyślał koło. Zastanawiam się, czy są jakieś najlepsze praktyki lub wzorce, które ludzie przestrzegają. To pytanie dotyczy mniej więcej samego kodu, ale więcej informacji na temat formatu wiadomości.

Na przykład, jeśli chciałbym powiedzieć arduino, aby flashowało, to pierwsza dioda LED 3 razy, mógłbym wysłać:

^L1,F3\n
  • „^”: uruchamia nowe polecenie
  • „L”: definiuje polecenie (L: celuje to polecenie w diodę LED)
  • „1”: kieruj na pierwszą diodę LED
  • ',': Separator wiersza poleceń, nowa wartość w tym komunikacie do naśladowania
  • „F”: polecenie podrzędne Flash
  • „3”: 3 razy (trzykrotnie błyśnij diodą LED)
  • „\ n”: Zakończ polecenie

Myśli? Jak zazwyczaj podchodzisz do pisania nowego protokołu szeregowego? Co jeśli chciałbym wysłać zapytanie z arduino 1 do arduino 2, a następnie otrzymać odpowiedź?

Jeremy Gillick
źródło

Odpowiedzi:

13

Istnieje wiele sposobów na napisanie protokołu szeregowego w zależności od potrzebnej funkcjonalności i wymaganej kontroli błędów.

Niektóre z typowych rzeczy, które widzisz w protokołach point-to-point:

Koniec wiadomości

Najprostsze protokoły ASCII mają po prostu koniec sekwencji znaków wiadomości, często \rlub \njako to, co jest drukowane po naciśnięciu klawisza Enter. Mogą być używane protokoły binarne 0x03lub inne wspólne bajty.

Początek wiadomości

Problem z samym końcem wiadomości polega na tym, że nie wiesz, jakie inne bajty zostały już odebrane podczas wysyłania wiadomości. Te bajty byłyby następnie poprzedzone komunikatem i spowodowałyby błędną interpretację. Na przykład, jeśli Arduino właśnie obudził się ze snu, może być trochę śmieci w buforze szeregowym. Aby obejść ten problem, musisz rozpocząć sekwencję wiadomości. W twoim przykładzie ^często w protokołach binarnych0x02

Sprawdzanie błędów

Jeśli komunikat może zostać uszkodzony, potrzebujemy kontroli błędów. Może to być suma kontrolna, błąd CRC lub coś innego.

Escape Characters

Może się zdarzyć, że suma kontrolna doda znak kontrolny, taki jak bajt „początek komunikatu” lub „koniec komunikatu”, lub wiadomość zawiera wartość równą znakowi kontrolnemu. Rozwiązaniem jest wprowadzenie postaci ucieczki. Znak zmiany znaczenia jest umieszczany przed zmodyfikowanym znakiem kontrolnym, tak aby rzeczywisty znak kontrolny nie był obecny. Np. Jeśli znakiem początkowym jest 0x02, za pomocą znaku ucieczki 0x10 możemy wysłać wartość 0x02 w komunikacie jako parę bajtów 0x10 0x12 (bajtowy znak kontrolny XOR)

Numer pakietu

Jeśli wiadomość jest uszkodzona, możemy poprosić o ponowne wysłanie wiadomości typu nack lub spróbować ponownie, ale jeśli wysłano wiele wiadomości, tylko ostatnia wiadomość może zostać wysłana ponownie. Zamiast tego pakietowi można nadać liczbę, która przewija się po określonej liczbie wiadomości. Na przykład, jeśli liczba ta wynosi 16, urządzenie nadawcze może przechowywać ostatnie 16 wysłanych wiadomości, a jeśli jakieś są uszkodzone, urządzenie odbierające może zażądać ponownego wysłania przy użyciu numeru pakietu.

Długość

Często w protokołach binarnych widzisz bajt długości, który informuje urządzenie odbierające, ile znaków jest w wiadomości. Dodaje to kolejny poziom sprawdzania błędów, tak jakby nie odebrano prawidłowej liczby bajtów, a następnie wystąpił błąd.

Specyficzne dla Arduino

Podczas opracowywania protokołu dla Arduino należy przede wszystkim zastanowić się, jak niezawodny jest kanał komunikacyjny. Jeśli wysyłasz za pośrednictwem większości bezprzewodowych mediów, XBee, WiFi itp., Wbudowane jest już sprawdzanie błędów i ponawianie prób, a zatem nie ma sensu umieszczać ich w protokole. Jeśli wysyłasz przez RS422 na kilka kilometrów, będzie to konieczne. To, co chciałbym uwzględnić, to początek i koniec znaków wiadomości, tak jak Ty. Moja typowa implementacja wygląda mniej więcej tak:

>messageType,data1,data2,…,dataN\n

Ograniczenie części danych przecinkiem pozwala na łatwą analizę, a wiadomość jest wysyłana za pomocą ASCII. Protokoły ASCII są świetne, ponieważ można wpisywać wiadomości na monitorze szeregowym.

Jeśli potrzebujesz protokołu binarnego, być może w celu zmniejszenia rozmiarów wiadomości, będziesz musiał zaimplementować funkcję zmiany znaczenia, jeśli bajt danych może być taki sam jak bajt kontrolny. Binarne znaki sterujące są lepsze w systemach, w których pożądane jest pełne spektrum sprawdzania błędów i ponownych prób. W razie potrzeby ładunek może nadal być ASCII.

geometrikal
źródło
Czy nie jest możliwe, że śmieci przed rzeczywistym początkiem kodu wiadomości mogą zawierać początek kodu kontroli wiadomości? Jak sobie z tym poradzisz?
CMCDragonkai
@CMCDragonkai Tak, jest to możliwość, szczególnie w przypadku kodów sterujących jednobajtowych. Jeśli jednak w trakcie analizowania komunikatu wystąpi początkowy kod sterujący, wiadomość zostanie odrzucona, a analiza zostanie uruchomiona ponownie.
geometrikal
9

Nie mam formalnej wiedzy specjalistycznej na temat protokołów szeregowych, ale korzystałem z nich kilka razy i mniej więcej ustaliłem ten schemat:

(Nagłówek pakietu) (bajt identyfikacyjny) (dane) (suma kontrolna fletcher16) (stopka pakietu)

Zazwyczaj robię nagłówek 2 bajty i bajt Stopka 1. Mój parser zrzuci wszystko, gdy zobaczy nowy nagłówek pakietu, i spróbuje parsować wiadomość, jeśli zobaczy stopkę. Jeśli suma kontrolna nie powiedzie się, komunikat nie zostanie porzucony, ale będzie kontynuowany dodawanie, dopóki nie zostanie znaleziony znak stopki i suma kontrolna się nie powiedzie. W ten sposób stopka musi mieć tylko jeden bajt, ponieważ kolizje nie zakłócają wiadomości.

Identyfikator jest dowolny, czasami z długością sekcji danych stanowiącą dolną część (4 bity). Można użyć drugiego bitu długości, ale zwykle nie przeszkadzam, ponieważ długość nie musi być znana z prawidłowego parsowania, więc widzenie odpowiedniej długości dla danego identyfikatora jest tylko większym potwierdzeniem, że komunikat był poprawny.

Suma kontrolna fletcher16 jest 2-bajtową sumą kontrolną o prawie takiej samej jakości jak CRC, ale jest znacznie łatwiejsza do wdrożenia. kilka szczegółów tutaj . Kod może być tak prosty:

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

Użyłem również systemu połączeń i reagowania na krytyczne wiadomości, w których komputer wysyła wiadomość co około 500 ms, dopóki nie otrzyma komunikatu OK z sumą kontrolną całej oryginalnej wiadomości jako danych (w tym oryginalnej sumy kontrolnej).

Ten schemat nie nadaje się oczywiście do wpisywania w terminalu, jak na przykład w twoim przykładzie. Twój protokół wydaje się całkiem dobry, ponieważ ogranicza się do ASCII i jestem pewien, że łatwiej jest w przypadku szybkiego projektu, który chcesz móc bezpośrednio czytać i wysyłać wiadomości. W przypadku większych projektów miło jest mieć gęstość protokołu binarnego i bezpieczeństwo sumy kontrolnej.

BrettAM
źródło
Ponieważ „[twój] parser zrzuci wszystko, gdy zobaczy nowy nagłówek pakietu”. Zastanawiam się, czy nie spowoduje to problemów, jeśli przypadkiem nagłówek zostanie napotkany w danych?
humanityANDpeace
@humanityANDpeace Powodem upuszczenia jest to, że kiedy pakiet zostanie odcięty, nigdy nie będzie poprawnie parsowany, więc kiedy zdecydujesz o jego śmieciach i przejdziesz dalej? Z mojego doświadczenia wynika, że ​​najłatwiejszym rozwiązaniem jest upuszczenie złego pakietu, gdy tylko pojawi się następny nagłówek. Używam 16-bitowego nagłówka bez problemu, ale możesz go wydłużyć, jeśli pewność jest ważniejsza niż pasmo.
BrettAM,
Więc to, co określasz jako nagłówek, jest w pewnym sensie magiczną kombinacją 16-bitową. tj. 010101001 10101010, prawda? Zgadzam się, że trafienie zmienia się tylko o 1/256 * 256, ale wyłącza także używanie tego 16-bitowego pliku w twoich danych, w przeciwnym razie zostanie źle zinterpretowany jako nagłówek i odrzucisz wiadomość, prawda?
humanityANDpeace
@humanityANDpeace Wiem, że to rok później, ale musisz wprowadzić sekwencję ucieczki. Przed wysłaniem serwer sprawdza ładunek pod kątem wszelkich specjalnych bajtów, a następnie ucieka z innym bajtem specjalnym. Po stronie klienta musisz ponownie złożyć oryginalny ładunek. Oznacza to, że nie można wysyłać pakietów o stałej długości i komplikuje implementację. Do wyboru jest wiele standardów protokołów szeregowych. Oto bardzo dobra lektura na ten temat .
RubberDuck,
1

Jeśli jesteś w zgodzie ze standardami, możesz rzucić okiem na kodowanie ASN.1 / BER TLV. ASN.1 to język używany do opisywania struktur danych, stworzony specjalnie do komunikacji. BER jest metodą TLV kodowania danych ustrukturyzowanych za pomocą ASN.1. Problem polega na tym, że kodowanie ASN.1 może być co najwyżej trudne. Stworzenie pełnoprawnego kompilatora ASN.1 to projekt sam w sobie (i szczególnie trudny, pomyśl miesiące ).


Prawdopodobnie lepiej zachować tylko strukturę TLV. TLV zasadniczo składa się z trzech elementów: znacznika, długości i pola wartości. Znacznik określa typ danych (ciąg tekstowy, ciąg oktetów, liczba całkowita itp.) Oraz długość długości wartości .

W BER T oznacza również, czy wartość jest zbiorem samych struktur TLV (skonstruowany węzeł) czy bezpośrednio wartością (prymitywny węzeł). W ten sposób możesz utworzyć drzewo w formacie binarnym, podobnie jak XML (ale bez narzutu XML).

Przykład:

TT LL VV
02 01 FF

jest liczbą całkowitą (tag 02) o długości wartości 1 (długość 01) i wartości -1 (wartość FF). W ASN.1 / BER liczby całkowite są znakami dużych liczb endianowych, ale oczywiście można użyć własnego formatu.

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

jest sekwencją (listą) o długości 7 zawierającą dwie liczby całkowite, jedną o wartości -1 i jedną o wartości 255. Dwa kodowania liczb całkowitych razem tworzą wartość sekwencji.

Możesz po prostu wrzucić to również do dekodera online, czy to nie miłe?


Możesz również użyć nieokreślonej długości w BER, co pozwoli ci na przesyłanie strumieniowe danych. W takim przypadku musisz poprawnie przeanalizować drzewo. Uważam to za zaawansowany temat, na przykład musisz wiedzieć o szerokości i głębokości pierwszego parsowania.


Korzystanie ze schematu TLV pozwala zasadniczo wymyślić dowolną strukturę danych i zakodować ją. ASN.1 idzie o wiele dalej, dając unikalne identyfikatory (OID), możliwości wyboru (podobnie jak związki C), obejmuje inne struktury ASN.1 itp. Itp., Ale może to być przesada w projekcie. Prawdopodobnie najbardziej znanymi dziś strukturami zdefiniowanymi w ASN.1 są certyfikaty używane przez przeglądarkę.

Maarten Bodewes
źródło
0

Jeśli nie, znasz podstawy. Twoje polecenia mogą być tworzone i odczytywane zarówno przez ludzi, jak i maszyny, co jest dużym plusem. Możesz dodać sumę kontrolną, aby wykryć źle sformułowane polecenie lub polecenie uszkodzone podczas transportu, szczególnie jeśli Twój kanał zawiera długi kabel lub łącze radiowe.

Jeśli potrzebujesz siły przemysłowej (Twoje urządzenie nie może powodować obrażeń lub śmierci ani pozwolić, aby ktoś doznał obrażeń lub śmierci; potrzebujesz wysokich prędkości danych, odzyskiwania błędów, wykrywania brakujących pakietów itp.), Skorzystaj z niektórych standardowych protokołów i praktyk projektowych.

JRobert
źródło