arduino-caracteres-control-puerto-serie

Caracteres de control en el puerto serie en Arduino

Continuamos profundizando en el uso del puerto serie avanzado en procesadores como Arduino. En esta entrada vamos a ver como añadir delimitadores de trama y caracteres de control a nuestros sistemas de transmisión para dotarlo de mayor robustez.

Anteriormente hemos visto cómo enviar bytes por puerto serie como una forma conveniente y “profesional” de realizar la comunicación. En la entrada anterior vimos que, frecuentemente, emplearemos una o varias estructuras definiendo un mensaje que queremos enviar o recibir.

Ahora queremos ampliar la trama (los bytes que enviamos en la comunicación) rodeando los bytes de datos con una serie de elementos que aumenten la “calidad” de la comunicación. Un ejemplo es añadir una función de checksum para comprobar la integridad de los datos, algo que veremos en la próxima entrada.

Otro ejemplo, que es el que vamos a ver en esta entrada, es añadir delimitadores de trama. Es decir, una determinada “señal” o marca que identifica el inicio y final de la comunicación. Ya que estamos, también nos gustaría poder añadir ciertos caracteres de control, que tengan un significado especial.

Como de costumbre todo esto ya está inventado y se denominan, precisamente, caracteres de control. De hecho, los estamos usando con frecuencia desde la primera entrada de comunicación cada vez que usamos ‘\n’ (salto de línea) o ‘\r’ (retorno de carro).

Aquí tenemos una lista de algunos de los caracteres de control disponibles con su valor hexadecimal y con su significado.

CódigoHexAlt.Significado
NUL0\0Null
SOH1Start of Heading
STX2Start of Text
ETX3End of Text
EOT4End of Transmission
ENQ5Enquiry
ACK6Acknowledge
BEL7\aBell
BS8\bBackspace
HT9\tHorizontal Tabulation
LF0A\nLine Feed
VT0B\vVertical Tabulation
FF0C\fForm Feed
CR0D\rCarriage Return
SO0EShift Out
SI0FShift In
DLE10Data Link Escape
DC111Device Control One (XON)
DC212Device Control Two
DC313Device Control Three (XOFF)
DC414Device Control Four
NAK15Negative Acknowledge
SYN16Synchronous Idle
ETB17End of Transmission Block
CAN18Cancel
EM19End of medium
SUB1ASubstitute
ESC1BEscape
FS1CFile Separator
GS1DGroup Separator
RS1ERecord Separator
US1FUnit Separator
SP20Space
DEL7FDelete

En particular, vemos que los caracteres de control aceptados para inicio y final de trama son, respectivamente, 0x02 (STX) y 0x03 (ETX). Por supuesto no estamos obligados a emplear estos caracteres. De hecho, en ocasiones veréis en códigos de Internet usar ‘H’ (Header) como principio de un encabezado. No hay ninguna regla que impida usarlo, pero, siendo que existen los caracteres de control, es lógico (y más higiénico) emplear el estándar.

El funcionamiento es sencillo. Al empezar el envío de una trama empezaremos mandando el carácter STX, y al final ETX. Estamos aumentando el tamaño de la trama en dos bytes, a costa de una mejor calidad de la comunicación. El incremento relativo del tamaño de la trama es menor cuanto mayor sea la cantidad de datos que estamos enviando.

Aquí tenemos un ejemplo de envío de un array de datos con delimitadores de trama.

const char STX = '\x002';
const char ETX = '\x003';

const int data[] = {0, 50, 100, 150, 200, 250};
const size_t dataLength = sizeof(data) / sizeof(data[0]);
const int bytesLength = dataLength * sizeof(data[0]);

void setup()
{
  Serial.begin(9600);
  Serial.write(STX);
  Serial.write((byte*)&data, dataLength);
  Serial.write(ETX);
}

void loop() 
{
}

Mientras que un ejemplo de receptor sería el siguiente,

const char STX = '\x002';
const char ETX = '\x003';

const int dataLength = 3;
size_t data[dataLength];
const int bytesLength = dataLength * sizeof(data[0]);

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

void loop()
{
  if (Serial.available() >= bytesLength)
  {
    if (Serial.read() == STX)
    {
      Serial.readBytes((byte*)&data, bytesLength);

      if (Serial.read() == ETX)
      {
        //performAction();
      }
    }
  }
}

Sin embargo, los caracteres de control no son más que bytes. ¿Cómo de seguro son estos delimitadores? Es decir, ¿es posible que lo confundamos con un byte de datos que contenga 0x02 o 0x3? ¿Es posible que, aun perdiendo bytes, interpretemos equivocadamente uno byte de datos como un delimitador?

Pues efectivamente, así es, ningún sistema es totalmente robusto. Añadir delimitadores de trama mejora el sistema, pero no hace que sea infalible. De hecho, ni siquiera estamos comprobando que la integridad de los datos, solo intentamos comprobar si mantenemos un cierto grado de “sincronización”.

Para que los delimitadores fallen tiene coincidir que, tras perder varios o unos bytes, el byte recibido en la posición en la que debería estar el limitador tenga el mismo valor. Si estamos trabajando en un ambiente con muchos fallos, no va ser suficiente para filtrar todos los defectos.

Puede parecer poco probable pero, en realidad, la posibilidad de interpretar incorrectamente un código de control es de 1/256. Sin embargo, la probabilidad combinada de interpretar mal simultáneamente el inicio y final del mensaje es de 1/65.536.

Sin embargo, la ventaja real es que aporta una cierta capacidad de “resincronización”. En un entorno “normal”, ante una puntual pérdida de paquetes, el sistema puede detectar el fallo y, eventualmente, recuperar la sincronización.

Por supuesto, podemos mejorar mucho el proceso de transmisión, añadiendo un timeout, o un checksum. Veremos todo esto en las próximas entradas.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github. github-full