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ódigo | Hex | Alt. | Significado |
---|---|---|---|
NUL | 0 | \0 | Null |
SOH | 1 | Start of Heading | |
STX | 2 | Start of Text | |
ETX | 3 | End of Text | |
EOT | 4 | End of Transmission | |
ENQ | 5 | Enquiry | |
ACK | 6 | Acknowledge | |
BEL | 7 | \a | Bell |
BS | 8 | \b | Backspace |
HT | 9 | \t | Horizontal Tabulation |
LF | 0A | \n | Line Feed |
VT | 0B | \v | Vertical Tabulation |
FF | 0C | \f | Form Feed |
CR | 0D | \r | Carriage Return |
SO | 0E | Shift Out | |
SI | 0F | Shift In | |
DLE | 10 | Data Link Escape | |
DC1 | 11 | Device Control One (XON) | |
DC2 | 12 | Device Control Two | |
DC3 | 13 | Device Control Three (XOFF) | |
DC4 | 14 | Device Control Four | |
NAK | 15 | Negative Acknowledge | |
SYN | 16 | Synchronous Idle | |
ETB | 17 | End of Transmission Block | |
CAN | 18 | Cancel | |
EM | 19 | End of medium | |
SUB | 1A | Substitute | |
ESC | 1B | Escape | |
FS | 1C | File Separator | |
GS | 1D | Group Separator | |
RS | 1E | Record Separator | |
US | 1F | Unit Separator | |
SP | 20 | Space | |
DEL | 7F | Delete |
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.