Best practice e schemi del protocollo di comunicazione


19

Ogni volta che progetto un protocollo seriale da utilizzare tra due arduinos, mi sento un po 'come se stessi reinventando una ruota. Mi chiedo se ci siano buone pratiche o modelli che la gente segue. Questa domanda riguarda meno il codice attuale, ma più il formato dei messaggi.

Ad esempio, se volessi dire a un arduino di lampeggiare è il primo LED 3 volte che potrei inviare:

^L1,F3\n
  • '^': avvia un nuovo comando
  • 'L': definisce il comando, (L: indirizza questo comando a un LED)
  • '1': scegli come target il primo LED
  • ',': Separatore della riga di comando, nuovo valore da seguire in questo messaggio
  • 'F': comando secondario Flash
  • '3': 3 volte (lampeggia il LED tre volte)
  • '\ n': termina il comando

Pensieri? Come approcci di solito la scrittura di un nuovo protocollo seriale? E se volessi inviare una query da Arduino 1 ad Arduino 2 e quindi ricevere una risposta?

Risposte:


13

Esistono molti modi per scrivere un protocollo seriale in base alla funzionalità che potresti desiderare e alla quantità di errore che ti serve per il controllo.

Alcune delle cose comuni che vedi nei protocolli punto a punto sono:

Fine del messaggio

I protocolli ASCII più semplici hanno solo una fine della sequenza di caratteri del messaggio, spesso \ro \npoiché questo è ciò che viene stampato quando viene premuto il tasto Invio. I protocolli binari potrebbero usare 0x03o qualche altro byte comune.

Inizio del messaggio

Il problema di avere solo la fine del messaggio è che non sai quali altri byte sono già stati ricevuti quando invii il tuo messaggio. Questi byte verrebbero quindi preceduti dal messaggio e interpretati in modo errato. Ad esempio, se Arduino si è appena svegliato dalla sospensione, potrebbero esserci dei rifiuti nel buffer seriale. Per ovviare a questo hai un inizio della sequenza di messaggi. Nel tuo esempio ^, nei protocolli binari spesso0x02

Controllo errori

Se il messaggio può essere danneggiato, abbiamo bisogno di un controllo degli errori. Questo potrebbe essere un checksum o un errore CRC o qualcos'altro.

Personaggi di fuga

È possibile che il checksum si aggiunga a un carattere di controllo, ad esempio il byte "inizio del messaggio" o "fine del messaggio", oppure che il messaggio contenga un valore uguale a un carattere di controllo. La soluzione è introdurre un carattere di fuga. Il carattere di escape viene posto prima di un carattere di controllo modificato in modo che il carattere di controllo effettivo non sia presente. Ad esempio, se un carattere iniziale è 0x02, utilizzando il carattere di escape 0x10 possiamo inviare il valore 0x02 nel messaggio come coppia di byte 0x10 0x12 (carattere di controllo XOR byte)

Numero pacchetto

Se un messaggio è danneggiato, è possibile richiedere un nuovo invio con un messaggio nack o riprovare, ma se sono stati inviati più messaggi, è possibile inviare nuovamente solo l'ultimo messaggio. Invece al pacchetto può essere assegnato un numero che passa dopo un certo numero di messaggi. Ad esempio, se questo numero è 16, il dispositivo di trasmissione può memorizzare gli ultimi 16 messaggi inviati e, se presenti, il dispositivo di ricezione può richiedere un nuovo invio utilizzando il numero di pacchetto.

Lunghezza

Spesso nei protocolli binari viene visualizzato un byte di lunghezza che indica al dispositivo ricevente quanti caratteri ci sono nel messaggio. Ciò aggiunge un altro livello di controllo degli errori come se non fosse ricevuto il numero corretto di byte, quindi si è verificato un errore.

Specifico per Arduino

Quando si tratta di un protocollo per Arduino, la prima considerazione è l'affidabilità del canale di comunicazione. Se si sta inviando tramite la maggior parte dei supporti wireless, XBee, WiFi, ecc., Sono già presenti controlli e tentativi di errore incorporati e quindi non ha senso inserirli nel protocollo. Se stai inviando tramite RS422 per un paio di chilometri, sarà necessario. Le cose che vorrei includere sono l'inizio del messaggio e la fine dei caratteri del messaggio, come hai fatto tu. La mia implementazione tipica è simile a:

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

La delimitazione delle parti di dati con una virgola consente un'analisi semplice e il messaggio viene inviato utilizzando ASCII. I protocolli ASCII sono fantastici perché è possibile digitare messaggi nel monitor seriale.

Se vuoi un protocollo binario, forse per abbreviare le dimensioni del messaggio, dovrai implementare l'escaping se un byte di dati può essere uguale a un byte di controllo. I caratteri di controllo binario sono migliori per i sistemi in cui si desidera l'intero spettro di controlli e tentativi di errore. Il payload può comunque essere ASCII se lo si desidera.


Non è possibile che la spazzatura prima del vero inizio del codice del messaggio possa contenere un inizio del codice di controllo del messaggio? Come lo faresti?
CMCDragonkai,

@CMCDragonkai Sì, questa è una possibilità, specialmente per i codici di controllo a byte singolo. Tuttavia, se si incontra un codice di controllo di avvio a metà dell'analisi di un messaggio, il messaggio viene scartato e l'analisi si riavvia.
geometrikal,

9

Non ho alcuna competenza formale sui protocolli seriali, ma li ho usati parecchie volte e più o meno stabilito su questo schema:

(Intestazione pacchetto) (ID byte) (dati) (checksum fletcher16) (Piè di pagina pacchetto)

Di solito faccio l'intestazione 2 byte e il piè di pagina 1 byte. Il mio parser scaricherà tutto quando vedrà una nuova intestazione del pacchetto e tenterà di analizzare il messaggio se vede un piè di pagina. Se il checksum fallisce, non abbandonerà il messaggio, ma continuerà ad aggiungerlo fino a quando non viene trovato il carattere piè di pagina e un checksum ha esito positivo. In questo modo il piè di pagina deve essere solo un byte poiché le collisioni non interrompono il messaggio.

L'ID è arbitrario, a volte con la lunghezza della sezione di dati che è il nibble inferiore (4 bit). È possibile utilizzare un secondo bit di lunghezza, ma di solito non mi preoccupo poiché non è necessario conoscere la lunghezza per analizzare correttamente, quindi vedere la lunghezza giusta per un determinato ID è solo una conferma del fatto che il messaggio era corretto.

Il checksum fletcher16 è un checksum a 2 byte con quasi la stessa qualità di CRC ma è molto più facile da implementare. alcuni dettagli qui . Il codice può essere semplice come questo:

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

Ho anche usato un sistema di chiamata e risposta per i messaggi critici, in cui il PC invierà un messaggio ogni 500 ms circa fino a quando non riceve un messaggio OK con un checksum dell'intero messaggio originale come dati (incluso il checksum originale).

Questo schema, ovviamente, non è adatto per essere digitato in un terminale come sarebbe il tuo esempio. Il tuo protocollo sembra abbastanza buono per essere limitato ad ASCII e sono sicuro che è più facile per un progetto veloce che vuoi essere in grado di leggere direttamente e inviare messaggi. Per progetti più grandi è bello avere la densità di un protocollo binario e la sicurezza di un checksum.


Dal momento che "[tuo] parser scaricherà tutto quando vedrà una nuova intestazione del pacchetto" Mi chiedo se questo non crea problemi se per caso l'intestazione viene rilevata all'interno dei dati?
umanità e

@humanityANDpeace Il motivo per cui è stato eliminato è che quando un pacchetto viene tagliato non verrà mai analizzato correttamente, quindi quando deciderai la sua spazzatura e proseguirai? La soluzione più semplice e, nella mia esperienza, abbastanza buona è quella di eliminare un pacchetto errato non appena arriva la prossima intestazione. Sto usando un'intestazione a 16 bit senza problemi, ma potresti allungarla se la certezza è più importante che larghezza di banda.
BrettAM,

Quindi ciò a cui ti riferisci come intestazione è in qualche modo una combinazione di Magic a 16 bit. cioè 010101001 10101010, giusto? Sono d'accordo che è solo 1/256 * 256 a colpire, ma disabilita anche l'utilizzo di questo 16 bit all'interno dei tuoi dati, altrimenti viene interpretato erroneamente come un'intestazione e scarti il ​​messaggio, giusto?
umanità e

@humanityANDpeace So che è un anno dopo, ma è necessario introdurre una sequenza di escape. Prima di inviare, il server controlla il payload per eventuali byte speciali, quindi li elimina con un altro byte speciale. Lato client, è necessario rimettere insieme il payload originale. Ciò significa che non è possibile inviare pacchetti a lunghezza fissa e complica l'implementazione. Ci sono molti standard di protocollo seriale tra cui scegliere che tutti affrontano questo. Ecco un'ottima lettura sull'argomento .
RubberDuck,

1

Se ti piacciono gli standard, puoi dare un'occhiata alla codifica TLV ASN.1 / BER. ASN.1 è un linguaggio utilizzato per descrivere le strutture di dati, creato appositamente per la comunicazione. BER è un metodo TLV per codificare i dati strutturati usando ASN.1. Il problema è che la codifica ASN.1 potrebbe essere al massimo complicata. La creazione di un compilatore ASN.1 a tutti gli effetti è un progetto in sé (e uno particolarmente complicato, pensa mesi ).


Probabilmente è meglio mantenere solo la struttura TLV. TLV è sostanzialmente composto da tre elementi: un tag, una lunghezza e un campo valore. Il tag definisce il tipo di dati (stringa di testo, stringa di ottetti, numero intero, ecc.) E la lunghezza della lunghezza del valore .

In BER la T indica anche se il valore è un insieme di strutture TLV stesse (un nodo costruito) o direttamente un valore (un nodo primitivo). In questo modo è possibile creare un albero in binario, in modo molto simile a XML (ma senza l'overhead XML).

Esempio:

TT LL VV
02 01 FF

è un numero intero (tag 02) con una lunghezza del valore 1 (lunghezza 01) e valore -1 (valore FF). In ASN.1 / BER gli interi sono segni di grandi numeri endian, ma puoi ovviamente usare il tuo formato.

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

è una sequenza (un elenco) con lunghezza 7 contenente due numeri interi, uno con valore -1 e uno con valore 255. Le due codifiche di numeri interi costituiscono il valore della sequenza.

Puoi semplicemente lanciarlo anche in un decodificatore online, non è così carino?


Puoi anche usare una lunghezza indefinita in BER che ti permetterà di trasmettere i dati. In tal caso, però, devi analizzare correttamente l'albero. Lo considererei un argomento avanzato, è necessario conoscere prima l'ampiezza e il primo approfondimento, per uno.


L'utilizzo di uno schema TLV consente sostanzialmente di pensare a qualsiasi tipo di struttura di dati e codificarlo. ASN.1 va molto oltre, dandoti identificatori univoci (OID), scelte (molto simili a C-unions), include altre strutture ASN.1 ecc. Ecc., Ma ciò potrebbe essere eccessivo per il tuo progetto. Probabilmente le strutture meglio definite ASN.1 oggi sono i certificati utilizzati dal tuo browser.


0

In caso contrario, hai le basi coperte. I tuoi comandi possono essere creati e letti da umani e macchine, il che è un grande vantaggio. È possibile aggiungere un checksum per rilevare un comando mal formato o uno danneggiato durante il trasporto, soprattutto se il canale include un cavo lungo o un collegamento radio.

Se hai bisogno di forza industriale (il tuo dispositivo non deve causare o permettere a qualcuno di farsi male o morire; hai bisogno di alte velocità di dati, recupero di guasti, rilevamento di pacchetti mancanti ecc.) Allora osserva alcuni dei protocolli standard e delle pratiche di progettazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.