Protocolo de comunicación Mejores prácticas y patrones

19

Cada vez que diseño un protocolo en serie para ser usado entre dos arduinos, siento que estoy reinventando una rueda. Me pregunto si hay mejores prácticas o patrones que la gente siga. Esta pregunta es menos sobre el código real, pero más sobre el formato de los mensajes.

Por ejemplo, si quisiera decirle a un arduino que parpadee su primer LED 3 veces podría enviar:

^L1,F3\n
  • '^': inicia un nuevo comando
  • 'L': define el comando, (L: dirige este comando a un LED)
  • '1': Apunte al primer LED
  • ',': Separador de línea de comando, nuevo valor en este mensaje a seguir
  • 'F': subcomando Flash
  • '3': 3 veces (parpadea el LED tres veces)
  • '\ n': finaliza el comando

Pensamientos? ¿Cómo sueles escribir un nuevo protocolo serial? ¿Qué pasa si quisiera enviar una consulta desde arduino 1 a arduino 2 y luego recibir una respuesta?

Jeremy Gillick
fuente

Respuestas:

13

Hay muchas formas de escribir un protocolo en serie dependiendo de la funcionalidad que desee y de la cantidad de errores que necesite.

Algunas de las cosas comunes que ve en los protocolos punto a punto son:

Fin del mensaje

Los protocolos ASCII más simples solo tienen un final de secuencia de caracteres del mensaje, a menudo \ro \nya que esto es lo que se imprime cuando se pulsa la tecla Intro. Los protocolos binarios pueden usar 0x03o algún otro byte común.

Inicio del mensaje

El problema con solo tener el final del mensaje es que no sabe qué otros bytes ya se han recibido cuando envía su mensaje. Estos bytes se prefijarán al mensaje y harán que se interprete incorrectamente. Por ejemplo, si el Arduino acaba de despertarse del sueño, puede haber algo de basura en el búfer en serie. Para evitar esto, tiene una secuencia de inicio de mensaje. En su ejemplo ^, en protocolos binarios a menudo0x02

Comprobación de errores

Si el mensaje puede corromperse, necesitamos alguna comprobación de errores. Esto podría ser una suma de verificación o un error de CRC u otra cosa.

Personajes de escape

Podría ser que la suma de verificación se agrega a un carácter de control, como el byte 'inicio del mensaje' o 'fin del mensaje', o el mensaje contiene un valor igual a un carácter de control. La solución es introducir un personaje de escape. El carácter de escape se coloca antes de un carácter de control modificado para que el carácter de control real no esté presente. Por ejemplo, si un carácter inicial es 0x02, utilizando el carácter de escape 0x10 podemos enviar el valor 0x02 en el mensaje como el par de bytes 0x10 0x12 (carácter de control XOR de bytes)

Número de paquete

Si un mensaje está dañado, podríamos solicitar un reenvío con un mensaje oculto o reintentar, pero si se han enviado varios mensajes, solo se puede reenviar el último mensaje. En cambio, el paquete puede recibir un número que se transfiere después de un cierto número de mensajes. Por ejemplo, si este número es 16, el dispositivo transmisor puede almacenar los últimos 16 mensajes enviados y, si alguno se dañó, el dispositivo receptor puede solicitar un reenvío utilizando el número de paquete.

Longitud

A menudo, en los protocolos binarios, se ve un byte de longitud que le dice al dispositivo receptor cuántos caracteres hay en el mensaje. Esto agrega otro nivel de comprobación de errores, ya que si no se recibió el número correcto de bytes, hubo un error.

Arduino específico

Al proponer un protocolo para Arduino, la primera consideración es qué tan confiable es el canal de comunicaciones. Si está enviando a través de la mayoría de los medios inalámbricos, XBee, WiFi, etc., ya hay una verificación de errores y reintentos incorporados y, por lo tanto, no tiene sentido incluirlos en su protocolo. Si envía RS422 durante un par de kilómetros, será necesario. Las cosas que incluiría son el comienzo del mensaje y el final de los caracteres del mensaje, como lo ha hecho. Mi implementación típica se parece a:

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

La delimitación de las partes de datos con una coma permite un análisis fácil, y el mensaje se envía mediante ASCII. Los protocolos ASCII son excelentes porque puede escribir mensajes en el monitor en serie.

Si desea un protocolo binario, tal vez para acortar el tamaño de los mensajes, tendrá que implementar el escape si un byte de datos puede ser lo mismo que un byte de control. Los caracteres de control binario son mejores para sistemas donde se desea el espectro completo de verificación de errores y reintentos. La carga útil aún puede ser ASCII si se desea.

Geometrikal
fuente
¿No es posible que la basura antes del inicio real del código del mensaje pueda contener un inicio del código de control del mensaje? ¿Cómo lidiarías con esto?
CMCDragonkai
@CMCDragonkai Sí, esta es una posibilidad, especialmente para los códigos de control de un solo byte. Sin embargo, si encuentra un código de control de inicio a la mitad del análisis de un mensaje, el mensaje se descarta y el análisis se reinicia.
geometrikal
9

No tengo ninguna experiencia formal en protocolos seriales, pero los he usado varias veces, y más o menos me decidí por este esquema:

(Encabezado de paquete) (Byte de ID) (datos) (suma de verificación fletcher16) (Pie de página de paquete)

Normalmente hago el encabezado de 2 bytes y el pie de página de 1 byte. Mi analizador volcará todo cuando vea un nuevo encabezado de paquete e intentará analizar el mensaje si ve un pie de página. Si la suma de verificación falla, no eliminará el mensaje, sino que continuará agregando hasta que se encuentre el carácter de pie de página y una suma de verificación tenga éxito. De esa forma, el pie de página solo debe ser de un byte, ya que las colisiones no interrumpen el mensaje.

La ID es arbitraria, a veces con la longitud de la sección de datos siendo el mordisco inferior (4 bits). Se podría usar un segundo bit de longitud, pero normalmente no me molesto, ya que no es necesario conocer la longitud para analizar correctamente, por lo que ver la longitud correcta para una ID dada es solo más confirmación de que el mensaje era correcto.

La suma de verificación fletcher16 es una suma de verificación de 2 bytes con casi la misma calidad que CRC pero es mucho más fácil de implementar. Algunos detalles aquí . El código puede ser tan simple como esto:

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

También utilicé un sistema de llamada y respuesta para mensajes críticos, donde la PC enviará un mensaje cada 500 ms aproximadamente hasta que reciba un mensaje OK con una suma de verificación del mensaje original completo como datos (incluida la suma de verificación original).

Este esquema, por supuesto, no es adecuado para ser escrito en una terminal como lo sería su ejemplo. Su protocolo parece bastante bueno por estar limitado a ASCII y estoy seguro de que es más fácil para un proyecto rápido que desea poder leer y enviar mensajes directamente. Para proyectos más grandes, es bueno tener la densidad de un protocolo binario y la seguridad de una suma de verificación.

BrettAM
fuente
Dado que "[su] analizador volcará todo cuando vea un nuevo encabezado de paquete" Me pregunto si esto no crea problemas si por casualidad se encuentra el encabezado dentro de los datos.
humanityANDpeace
@humanityANDpeace La razón para descartarlo es que cuando se corta un paquete, nunca se analizará correctamente, entonces, ¿cuándo decide su basura y continúa? La solución más fácil, y en mi experiencia lo suficientemente buena, es descartar un paquete defectuoso tan pronto como llegue el siguiente encabezado. He estado usando un encabezado de 16 bits sin problemas, pero podría hacerlo más largo si la certeza es más importante que banda ancha.
BrettAM
Entonces, a lo que se refiere como encabezado es algo así como una combinación Magic de 16 bits. es decir, 010101001 10101010, ¿verdad? Estoy de acuerdo en que solo es 1/256 * 256 cambio para golpear, pero también deshabilita el uso de este 16 bits dentro de sus datos, de lo contrario, se malinterpreta como un encabezado y descarta el mensaje, ¿verdad?
humanityANDpeace
@humanityANDpeace Sé que es un año después, pero debes introducir una secuencia de escape. Antes de enviar, el servidor verifica la carga útil en busca de bytes especiales, luego los escapa con otro byte especial. Del lado del cliente, debe volver a armar la carga original. Esto significa que no puede enviar paquetes de longitud fija y complica la implementación. Hay muchos estándares de protocolos en serie para elegir que abordan todo esto. Aquí hay una muy buena lectura sobre el tema .
RubberDuck
1

Si te gustan los estándares, entonces puedes echar un vistazo a la codificación ASN.1 / BER TLV. ASN.1 es un lenguaje utilizado para describir estructuras de datos, creado específicamente para la comunicación. BER es un método TLV para codificar los datos estructurados utilizando ASN.1. El problema es que la codificación ASN.1 puede ser difícil en el mejor de los casos. La creación de un compilador ASN.1 completo es un proyecto en sí mismo (y particularmente complicado, piense meses ).


Probablemente sea mejor mantener solo la estructura TLV. El TLV consiste básicamente en tres elementos: una etiqueta, una longitud y un campo de valor. La etiqueta define el tipo de datos (cadena de texto, cadena de octeto, entero, etc.) y la longitud, la longitud del valor .

En BER, la T también indica si el valor es un conjunto de estructuras TLV (un nodo construido) o directamente un valor (un nodo primitivo). De esa manera puede crear un árbol en binario, muy parecido a XML (pero sin la sobrecarga de XML).

Ejemplo:

TT LL VV
02 01 FF

es un entero (etiqueta 02) con una longitud del valor de 1 (longitud 01) y el valor -1 (valor FF). En ASN.1 / BER, los enteros son signos de grandes números endianos, pero, por supuesto, puede usar su propio formato.

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

es una secuencia (una lista) con longitud 7 que contiene dos enteros, uno con valor -1 y otro con valor 255. Las dos codificaciones de enteros forman el valor de la secuencia.

También puede simplemente lanzar esto a un decodificador en línea, ¿no es agradable?


También puede usar una longitud indefinida en BER que le permitirá transmitir datos. Sin embargo, en ese caso debes analizar tu árbol correctamente. Consideraría que es un tema avanzado, necesita saber sobre la amplitud primero y el análisis profundo primero, por ejemplo.


El uso de un esquema TLV básicamente le permite pensar en cualquier tipo de estructura de datos y codificarlo. ASN.1 va mucho más allá que eso, al proporcionarle identificadores únicos (OID), opciones (muy parecidas a uniones en C), incluye otras estructuras ASN.1, etc., etc., pero eso puede ser excesivo para su proyecto. Probablemente, las estructuras definidas por ASN.1 más conocidas hoy en día son los certificados utilizados por su navegador.

Maarten Bodewes
fuente
0

Si no, tienes lo básico cubierto. Sus comandos pueden ser creados y leídos por humanos y máquinas, lo cual es una gran ventaja. Puede agregar una suma de verificación para detectar un comando mal formado o uno dañado en tránsito, especialmente si su canal incluye un cable largo o un enlace de radio.

Si necesita fuerza industrial (su dispositivo no debe causar o permitir que alguien salga lastimado o muera; necesita altas velocidades de datos, recuperación de fallas, detección de paquetes faltantes, etc.), busque algunos de los protocolos estándar y prácticas de diseño.

JRobert
fuente