Práticas recomendadas e padrões do protocolo de comunicação

19

Toda vez que projeto um protocolo serial para ser usado entre dois arduinos, sinto como se estivesse reinventando uma roda. Gostaria de saber se existem práticas recomendadas ou padrões que as pessoas seguem. Esta pergunta é menos sobre o código real, mas mais sobre o formato das mensagens.

Por exemplo, se eu quisesse dizer a um arduino para piscar, seria o primeiro LED três vezes que eu poderia enviar:

^L1,F3\n
  • '^': inicia um novo comando
  • 'L': define o comando (L: direciona este comando para um LED)
  • '1': direcione o primeiro LED
  • ',': Separador de linha de comando, novo valor nesta mensagem a seguir
  • 'F': subcomando Flash
  • '3': 3 vezes (pisque o LED três vezes)
  • '\ n': finalize o comando

Pensamentos? Como você costuma escrever um novo protocolo serial? E se eu quisesse enviar uma consulta do arduino 1 para o arduino 2 e depois receber uma resposta?

Jeremy Gillick
fonte

Respostas:

13

Existem várias maneiras de escrever um protocolo serial, dependendo da funcionalidade desejada e da verificação de erros necessária.

Algumas das coisas comuns que você vê nos protocolos ponto a ponto são:

Fim da mensagem

Os protocolos ASCII mais simples têm apenas uma sequência de caracteres no final da mensagem, geralmente \rou \né isso que é impresso quando a tecla Enter é pressionada. Protocolos binários podem usar 0x03ou algum outro byte comum.

Início da mensagem

O problema de ter apenas o final da mensagem é que você não sabe quais outros bytes já foram recebidos ao enviar sua mensagem. Esses bytes seriam prefixados para a mensagem e causariam uma interpretação incorreta. Por exemplo, se o Arduino acabou de acordar, pode haver algum lixo no buffer serial. Para contornar isso, você inicia uma sequência de mensagens. No seu exemplo ^, em protocolos binários frequentemente0x02

Verificação de erros

Se a mensagem puder ser corrompida, precisamos de uma verificação de erro. Pode ser uma soma de verificação ou um erro de CRC ou outra coisa.

Escape Characters

Pode ser que a soma de verificação seja adicionada a um caractere de controle, como o byte 'start of message' ou 'end of message', ou a mensagem contenha um valor igual a um caractere de controle. A solução é introduzir um caractere de escape. O caractere de escape é colocado antes de um caractere de controle modificado para que o caractere de controle real não esteja presente. Por exemplo, se um caractere inicial for 0x02, usando o caractere de escape 0x10, podemos enviar o valor 0x02 na mensagem como o par de bytes 0x10 0x12 (caractere de controle XOR do byte)

Número do pacote

Se uma mensagem estiver corrompida, poderemos solicitar um reenvio com uma mensagem nack ou tentar novamente, mas se várias mensagens forem enviadas, apenas a última mensagem poderá ser reenviada. Em vez disso, o pacote pode receber um número que rola após um certo número de mensagens. Por exemplo, se esse número for 16, o dispositivo transmissor poderá armazenar as últimas 16 mensagens enviadas e, se alguma estiver corrompida, o dispositivo receptor poderá solicitar um reenvio usando o número do pacote.

comprimento

Frequentemente, em protocolos binários, você vê um byte de comprimento que informa ao dispositivo receptor quantos caracteres há na mensagem. Isso adiciona outro nível de verificação de erro, como se o número correto de bytes não tivesse sido recebido, e ocorreu um erro.

Específico para Arduino

Ao criar um protocolo para o Arduino, a primeira consideração é a confiabilidade do canal de comunicação. Se você estiver enviando pela maioria dos meios sem fio, XBee, WiFi, etc, já existe uma verificação de erros e novas tentativas e, portanto, não faz sentido colocá-las em seu protocolo. Se você enviar o RS422 por alguns quilômetros, será necessário. As coisas que eu incluiria são o início e o final dos caracteres da mensagem, como você fez. Minha implementação típica se parece com:

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

A delimitação das partes dos dados com uma vírgula permite uma análise fácil e a mensagem é enviada usando ASCII. Os protocolos ASCII são ótimos porque você pode digitar mensagens no monitor serial.

Se você deseja um protocolo binário, talvez para diminuir o tamanho das mensagens, será necessário implementar escape se um byte de dados puder ser o mesmo que um byte de controle. Os caracteres de controle binário são melhores para sistemas em que o espectro completo de verificação de erros e novas tentativas é desejado. A carga útil ainda pode ser ASCII, se desejado.

geometrikal
fonte
Não é possível que o lixo antes do código real de início da mensagem contenha um código de controle de início? Como você lidaria com isso?
CMCDragonkai
@CMCDragonkai Sim, essa é uma possibilidade, especialmente para códigos de controle de byte único. No entanto, se você encontrar um código de controle de início no meio da análise de uma mensagem, a mensagem será descartada e a análise será reiniciada.
geometrikal
9

Eu não tenho nenhum conhecimento formal em protocolos seriais, mas eu os usei algumas vezes e mais ou menos resolvi esse esquema:

(Cabeçalho do pacote) (byte ID) (dados) (soma de verificação do fletcher16) (Rodapé do pacote)

Eu costumo fazer o cabeçalho 2 bytes e o rodapé 1 byte. Meu analisador despeja tudo quando vê um novo cabeçalho de pacote e tenta analisar a mensagem se encontrar um rodapé. Se a soma de verificação falhar, ela não descartará a mensagem, mas continuará adicionando até que o caractere de rodapé seja encontrado e uma soma de verificação seja bem-sucedida. Dessa forma, o rodapé precisa ter apenas um byte, pois as colisões não atrapalham a mensagem.

O ID é arbitrário, às vezes com o comprimento da seção de dados sendo a mordidela inferior (4 bits). Um segundo bit de comprimento poderia ser usado, mas normalmente não me incomodo, pois o comprimento não precisa ser conhecido para analisar corretamente, portanto, ver o comprimento certo para um determinado ID é apenas mais uma confirmação de que a mensagem estava correta.

A soma de verificação fletcher16 é uma soma de verificação de 2 bytes com quase a mesma qualidade que a CRC, mas é muito mais fácil de implementar. alguns detalhes aqui . O código pode ser tão simples como este:

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

Também usei um sistema de chamada e respone para mensagens críticas, onde o PC envia uma mensagem a cada 500 ms ou mais, até que receba uma mensagem OK com uma soma de verificação de toda a mensagem original como dados (incluindo a soma de verificação original).

Obviamente, esse esquema não é adequado para ser digitado em um terminal como seria o seu exemplo. Seu protocolo parece muito bom por estar limitado ao ASCII e, com certeza, é mais fácil para um projeto rápido que você deseja ler e enviar mensagens diretamente. Para projetos maiores, é bom ter a densidade de um protocolo binário e a segurança de uma soma de verificação.

BrettAM
fonte
Como "[seu] analisador irá despejar tudo quando vir um novo cabeçalho de pacote" Será que isso não cria problemas se, por acaso, o cabeçalho for encontrado dentro dos dados?
humanityANDpeace
@humanityANDpeace O motivo para a retirada é que, quando um pacote é cortado, ele nunca é analisado corretamente; então, quando você decide o lixo e segue em frente? A solução mais fácil e, na minha experiência, boa o suficiente, é descartar um pacote inválido assim que o próximo cabeçalho chegar. Estou usando um cabeçalho de 16 bits sem problemas, mas você pode prolongá-lo se a certeza for mais importante do que isso. largura de banda.
BrettAM
Então, o que você chama de cabeçalho é uma combinação mágica de 16 bits. ou seja, 010101001 10101010, certo? Concordo que é apenas 1/256 * 256 alterações a serem atingidas, mas também desabilita o uso desses 16 bits nos seus dados; caso contrário, ele é mal interpretado como um cabeçalho e você descarta a mensagem, certo?
humanityANDpeace
@humanityANDpeace Eu sei que é um ano depois, mas você precisa introduzir uma sequência de escape. Antes do envio, o servidor verifica a carga útil em busca de bytes especiais e os escapa com outro byte especial. No lado do cliente, é necessário reunir novamente a carga útil original. Isso significa que você não pode enviar pacotes de tamanho fixo e complica a implementação. Existem muitos padrões de protocolo serial para escolher, todos os quais abordam isso. Aqui está uma leitura muito boa sobre o tópico .
precisa saber é o seguinte
1

Se você gosta de padrões, pode dar uma olhada na codificação ASN.1 / BER TLV. ASN.1 é uma linguagem usada para descrever estruturas de dados, feitas especificamente para comunicação. O BER é um método TLV de codificação dos dados estruturados usando ASN.1. O problema é que a codificação ASN.1 pode ser complicada na melhor das hipóteses. Criar um compilador ASN.1 completo é um projeto em si (e particularmente complicado, pense meses ).


Provavelmente é melhor manter apenas a estrutura TLV. O TLV consiste basicamente em três elementos: um tag, um comprimento e um campo de valor. A tag define o tipo dos dados (sequência de texto, sequência de octetos, número inteiro etc.) e o comprimento o comprimento do valor .

No BER, o T também indica se o valor é um conjunto de estruturas TLV (um nó construído) ou diretamente um valor (um nó primitivo). Dessa forma, você pode criar uma árvore em binário, muito parecido com XML (mas sem a sobrecarga de XML).

Exemplo:

TT LL VV
02 01 FF

é um número inteiro (tag 02) com um comprimento do valor de 1 (comprimento 01) e do valor -1 (valor FF). No ASN.1 / BER, os números inteiros são números endian grandes, mas é claro que você pode usar seu próprio formato.

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

é uma sequência (uma lista) com o comprimento 7 que contém dois números inteiros, um com o valor -1 e outro com o valor 255. As duas codificações inteiras juntas formam o valor da sequência.

Você pode simplesmente jogar isso em um decodificador on-line também, não é legal?


Você também pode usar comprimento indefinido no BER, o que permitirá transmitir dados. Nesse caso, você precisa analisar sua árvore corretamente. Eu consideraria um tópico avançado, você precisa saber sobre a amplitude e a profundidade da primeira análise, por exemplo.


O uso de um esquema TLV basicamente permite que você pense em qualquer tipo de estrutura de dados e a codifique. O ASN.1 vai muito além disso, fornecendo identificadores exclusivos (OIDs), opções (muito como uniões C), inclui outras estruturas do ASN.1 etc. etc., mas isso pode ser um exagero para o seu projeto. Provavelmente, as estruturas definidas pela ASN.1 mais conhecidas atualmente são os certificados usados ​​pelo seu navegador.

Maarten Bodewes
fonte
0

Caso contrário, você tem o básico coberto. Seus comandos podem ser criados e lidos por humanos e máquinas, o que é uma grande vantagem. Você pode adicionar uma soma de verificação para detectar um comando mal formado ou um danificado em trânsito, especialmente se o seu canal incluir um cabo longo ou um link de rádio.

Se você precisar de força industrial (seu dispositivo não deve causar ou permitir que alguém se machuque ou morra; você precisa de altas taxas de dados, recuperação de falhas, detecção de pacotes ausentes etc.), consulte alguns dos protocolos e práticas de design padrão.

JRobert
fonte