01 · Objetivo
Qué es un protocolo
Un protocolo es el conjunto de reglas con las que dos extremos se entienden: qué se envía, en qué orden, con qué forma y qué se hace ante un error. Los protocolos del curso lo muestran en todos los niveles: el UART del tema 3 define cómo viaja un byte (bit de inicio, bits de datos, parada), Modbus (tema 26) define cómo se arma una consulta a un esclavo, MQTT (tema 32) cómo se publica un valor, y el servidor GATT de BLE (tema 38) cómo se organizan los datos. Por encima de la capa física, cada uno responde las mismas preguntas, y por eso se puede aprender a diseñarlos:
| Pregunta | Decisión | Tema |
|---|---|---|
| ¿Dónde empieza y termina un mensaje? | Encuadre: delimitadores, longitud o silencio | Este tema |
| ¿Qué se hace con el delimitador dentro de los datos? | Transparencia: relleno de bytes, COBS | Este tema |
| ¿Qué dice cada mensaje? | Tipo y campos | Este tema y el 46 |
| ¿Quién habla con quién? | Direcciones, difusión | 42 |
| ¿Se dañó algo en el camino? | Paridad, suma, CRC | 43 y 44 |
| ¿Llegó? ¿Hay que repetir? | ACK, reintentos, secuencia | 45 |
| ¿Quién puede enviar esto? | Autenticación, cifrado | 49 |
El criterio de diseño de todo el tema es uno solo: el receptor tiene que poder entrar en cualquier momento, equivocarse sin consecuencias y volver a sincronizarse por sí solo. Un enlace real pierde bytes, recibe ruido y arranca a mitad de un mensaje porque se encendió un equipo antes que el otro. Un protocolo que solo anda con el flujo perfecto no es un protocolo: es una demostración.
02 · El problema del flujo
Dónde empieza y dónde termina un mensaje
Un puerto serie, un socket TCP o una característica BLE entregan un flujo de bytes: sin fronteras. El receptor puede recibir un mensaje en tres pedazos, dos mensajes pegados o la mitad de uno, según la velocidad, los búferes y la suerte. Para entender qué le llegó, necesita encuadrar (framing): saber dónde empieza y dónde termina cada mensaje. Hay cuatro métodos básicos, que se combinan:
| Método | Cómo funciona | Ventajas | Problemas | Ejemplos |
|---|---|---|---|---|
| Delimitador de fin | Un byte reservado cierra el mensaje (un salto de línea) | Simple, legible, resincroniza en cada fin | El delimitador no puede aparecer en los datos | Comandos AT, NMEA, órdenes de texto (temas 11 y 37) |
| Inicio y fin | Un byte marca el comienzo y otro el final | Descarta basura previa; el inicio es un punto de resincronización | Los dos bytes no pueden aparecer en los datos | STX y ETX de protocolos antiguos, tramas HDLC y SLIP |
| Longitud primero | El primer campo dice cuántos bytes siguen | Admite cualquier byte en los datos; sin esperas | Un largo dañado desordena todo lo que sigue: hace falta una verificación | MQTT, TLS, la mayoría de los protocolos binarios |
| Silencio | Una pausa mayor que cierto tiempo separa mensajes | Sin bytes especiales en los datos | Depende del tiempo: un retraso del emisor parte un mensaje, y no sirve con búferes que mezclan datos | Modbus RTU (3,5 caracteres) |
Ninguno alcanza solo en un enlace con errores, y lo habitual es combinar: un delimitador de inicio para tener un punto de resincronización, una longitud para saber cuándo termina sin prohibir ningún valor en los datos, y una suma de verificación para saber que el encuadre fue el correcto. Es el diseño de este tema. La longitud sola es frágil (si se corrompe, el receptor espera de más y se come las tramas siguientes), y el delimitador solo no admite datos binarios; juntos, cada uno cubre la debilidad del otro.
Un ejemplo que ya se vio: el lector RDM6300 (tema 39) usa 0x02 y 0x03 como inicio y fin y manda los datos como caracteres ASCII hexadecimales, de modo que ni el inicio ni el fin pueden aparecer en ellos: es la forma más simple de evitar el problema de la sección siguiente, al costo de duplicar el tamaño de los datos.
03 · Datos arbitrarios
Cuando el delimitador aparece en los datos
Si los datos son binarios, cualquier valor de byte puede aparecer, incluido el delimitador. El receptor lo tomaría por un fin de mensaje. Hay tres soluciones, y conviene conocerlas aunque este tema use la tercera:
- Mandar los datos como texto (hexadecimal o base64): el delimitador queda reservado, y el tamaño crece (el doble con hexadecimal, un tercio más con base64).
- Relleno de bytes (byte stuffing): se reemplaza cada aparición del delimitador dentro de los datos por una secuencia de dos bytes que empieza con un escape. Es el método de SLIP (RFC 1055): el fin es
0xC0, el escape es0xDB, y dentro de los datos0xC0pasa a0xDB 0xDCy0xDBpasa a0xDB 0xDD. El receptor deshace la sustitución. Con datos al azar, cada byte tiene una probabilidad de 2/256 de ser reemplazado: el costo medio es menor al 1 %, pero en el peor caso (todos bytes0xC0) los datos se duplican, y el tamaño máximo de la trama deja de ser fijo. HDLC y PPP hacen lo mismo con0x7Ey0x7D. - COBS (Consistent Overhead Byte Stuffing): reescribe los datos para que no contengan ningún cero, y el cero queda libre como delimitador. Cada bloque de datos sin ceros lleva delante un byte con la distancia hasta el próximo cero, de modo que cada cero se reemplaza por un número. El costo está acotado: a lo sumo un byte por cada 254 de datos (más el delimitador), así que el tamaño máximo es predecible, y la implementación es corta y rápida. Es una elección habitual en enlaces serie entre microcontroladores.
| Codificación | Bytes enviados | Tamaño |
|---|---|---|
| Sin transparencia | 01 C0 DB 00 00 05 | 6 (con un C0 que parecería un fin) |
| SLIP | 01 DB DC DB DD 00 00 05 C0 | 9 (con el C0 final) |
| COBS | 04 01 C0 DB 01 02 05 00 | 8 (con el 00 final) |
| Hexadecimal en texto | 01C0DB000005\n | 13 |
Pero la trama de este tema no necesita ninguna de las tres. Como lleva la longitud en el segundo byte, el receptor sabe cuántos bytes quedan sin mirar su valor: un 0xA5 dentro de los datos no confunde, porque ya se sabe que son datos. El delimitador de inicio sirve solo para encontrar una trama después de un error, no para cerrarla. Es una diferencia importante: cuando el protocolo cierra con un delimitador (SLIP, COBS, texto), los datos tienen que ser transparentes; cuando cierra con la longitud, no, y el costo es depender de que ese campo sea correcto, que se verifica con la suma. El relleno de bytes y COBS son la elección cuando se quiere que el receptor se resincronice en cada delimitador sin necesitar suma, o cuando el protocolo encapsula paquetes de longitud variable.
04 · El formato
La estructura de una trama
El diseño de la trama del tema es mínimo y alcanza para casi todo:
| Campo | Tamaño | Contenido | Para qué |
|---|---|---|---|
| SOF (start of frame) | 1 byte | Siempre 0xA5 | Punto de resincronización: el receptor ignora todo hasta verlo |
| LEN | 1 byte | De 1 a 32: cuenta el tipo y los datos | Dice cuántos bytes siguen; el valor 0 o mayor que 32 es imposible y se rechaza |
| TIPO | 1 byte | Qué mensaje es | El receptor decide cómo interpretar los datos |
| DATOS | 0 a 31 bytes | Campos del mensaje, con tamaños y orden fijos | La información |
| SUMA | 1 byte | Complemento a dos de la suma de LEN, TIPO y datos | Detectar tramas dañadas o falsas |
Cuatro decisiones de fondo, que se repiten en cualquier protocolo:
- El largo tiene un máximo. Un receptor con un búfer de 34 bytes no puede aceptar un largo de 200: el límite protege la memoria, y también acota el tiempo que una trama falsa puede tener al receptor esperando (sección 6). El máximo de 32 da tramas de hasta 35 bytes: unos 36 ms a 9600 baudios.
- La suma cubre todo menos el inicio. LEN, TIPO y datos están protegidos; el SOF no hace falta, porque si está dañado la trama simplemente no se encuentra. Un largo dañado produce una suma incorrecta con muy alta probabilidad, y se descarta.
- La suma es un mínimo. Un byte de suma detecta el 99,6 % de los errores al azar (255 de cada 256), pero no detecta dos errores que se compensan ni el intercambio de dos bytes. Se la reemplaza por un CRC en el tema 44, sin tocar nada más: el campo cambia de contenido, no de lugar.
- El tipo es un número, no un texto. Un byte da 256 mensajes distintos, y el receptor decide con un
switch. Se reserva un rango para el uso experimental y se documenta una tabla de tipos.
| Tipo | Nombre | Datos | Ejemplo |
|---|---|---|---|
0x01 | LECTURA | Nodo (uint8), temperatura (int16, centésimas de grado), humedad (uint16, centésimas de por ciento): 5 bytes, little-endian | A5 06 01 01 2E 09 7C 15 30 |
0x10 | LATIDO | Ninguno | A5 01 10 EF |
| Otro | Desconocido | — | Se ignora sin error (sección 7) |
Con 4 bytes de estructura, la trama tiene un costo fijo: un mensaje con 2 bytes de datos ocupa 6 y el 67 % es estructura; uno de 31 bytes ocupa 35 y la estructura es el 11 %. Por eso conviene agrupar datos en una trama en lugar de mandar un mensaje por cada valor: la herramienta 1 muestra el costo en bytes y en tiempo. El tiempo de un byte en 8N1 es 10 bits: a 9600 baudios, 1,04 ms.
05 · El receptor
El receptor como máquina de estados
Los bytes llegan de a uno y en cualquier momento. La forma natural de recibirlos es una máquina de estados alimentada byte a byte: una función alimentar(b) que, según el estado en que está, decide qué hacer con el byte y a qué estado pasar. No bloquea (no hay while ni delay esperando datos), lo que permite atender otras tareas en el mismo programa, y es la versión del tema 11 («el maestro como máquina de estados») aplicada a la recepción de una trama. Tiene tres estados:
- ESPERA_SOF. Todo byte que no sea
0xA5se descarta (y se cuenta). Es el estado en el que arranca el receptor y al que vuelve ante cualquier problema: es lo que da la resincronización. - ESPERA_LEN. Llegó un posible inicio. El byte siguiente es el largo: si está entre 1 y 32, se guarda y se pasa a CUERPO; si no, el inicio era falso.
- CUERPO. Se acumulan bytes (tipo, datos y suma) hasta completar LEN + 1. Al terminar se suma todo: si da cero, la trama es válida y se entrega; si no, era falsa o llegó dañada.
El detalle que distingue un receptor robusto de uno que funciona en la demostración es qué se hace al fallar. Dos casos:
- Largo imposible. Si en ESPERA_LEN llega un 0 o un valor mayor que 32, el
0xA5anterior era un dato cualquiera. Se vuelve a ESPERA_SOF, pero el byte recién llegado se vuelve a procesar en ese estado, porque puede ser el0xA5de una trama verdadera (como enA5 A5 03 01 11 22 C9: el primer A5 es basura y el segundo es un inicio válido). - Suma incorrecta. Un
0xA5falso (parte de unos datos, o de una trama dañada) pudo hacer que el receptor consumiera bytes de una trama verdadera que venía atrás. Para no perderla, después de un error no se descarta todo el bloque sino solo el falso SOF: los bytes que se habían acumulado se vuelven a alimentar, uno por uno, desde ESPERA_SOF, y si entre ellos hay una trama válida, se encuentra. Es la misma regla de la herramienta 2.
En código, la corrección tiene pocas líneas y vale mucho: una trama dañada cuesta solo esa trama, y no la siguiente. La herramienta 2 deja ver el comportamiento con un flujo con basura, una trama dañada, una válida y una cortada al final.
06 · Defensa
Cuando algo sale mal
Un protocolo se juzga por lo que hace con lo que no esperaba. La lista de lo que puede pasar en un enlace real, y la defensa de cada caso en este diseño:
| Falla | Qué ve el receptor | Defensa |
|---|---|---|
| Arranque a mitad de una trama | Bytes de datos sin inicio | ESPERA_SOF los descarta hasta el próximo 0xA5 |
| Basura o ruido entre tramas | Bytes sueltos, a veces un falso 0xA5 | Largo imposible o suma incorrecta: se descarta y se vuelve a buscar |
| Un bit cambiado | Un byte distinto | La suma no da cero: se descarta la trama (y se cuenta el error) |
| Un byte perdido | Una trama más corta que lo anunciado: consume bytes de la siguiente | La suma falla; el re-procesamiento recupera la siguiente trama |
| Una trama cortada para siempre (el emisor se apagó) | Silencio con el estado en CUERPO | El tiempo de espera entre bytes vuelve a ESPERA_SOF |
| Un largo falso enorme | El receptor espera cientos de bytes | El máximo de 32 lo rechaza antes de reservar nada |
| Un tipo desconocido | Una trama válida con un tipo que no se conoce | Se ignora (y se cuenta): quizás es de una versión más nueva |
| Un desborde del búfer del puerto | Bytes que nunca llegaron al programa | Leer el puerto con frecuencia; a veces hay un indicador de desborde (tema 43) |
El tiempo de espera entre bytes
Sin ningún tiempo de espera, una trama cortada deja al receptor en CUERPO esperando bytes que nunca llegan; cuando por fin llegan (de la siguiente trama), se mezclan. El remedio es un tiempo de espera entre bytes: si en cualquier estado distinto de ESPERA_SOF pasa más tiempo del esperado sin recibir nada, se descarta lo acumulado y se vuelve a ESPERA_SOF. Debe ser claramente mayor que el tiempo de un byte (para tolerar las demoras del emisor y las interrupciones del receptor) y mucho menor que el período entre mensajes. A 9600 baudios, un byte dura 1,04 ms; con diez tiempos de byte (unos 10 ms) se tolera cualquier demora razonable. A 115 200 baudios, diez bytes son 87 µs: demasiado fino para el sistema operativo o el microcontrolador, y se pone un mínimo de 2 ms. La herramienta 5 calcula estos valores para una velocidad dada.
Este tiempo se mide entre bytes, no sobre la trama completa. Una trama de 35 bytes a 1200 baudios tarda 292 ms, y no es un error: lo que sería un error es un silencio de más de unas decenas de milisegundos a mitad de la trama. Modbus RTU usa el silencio como delimitador (3,5 caracteres para cerrar la trama, 1,5 entre bytes como máximo), y es el ejemplo de un protocolo donde el tiempo forma parte del formato; por eso sus implementaciones son tan sensibles al comportamiento del puerto.
Contar los errores
El receptor debe llevar cuentas: tramas válidas, errores de suma, errores de largo, bytes descartados, tipos desconocidos. Son baratas (un contador por categoría) y son la única forma de saber cómo se porta el enlace: un cable mal conectado, una velocidad ligeramente distinta o un emisor que se resetea dejan huellas distintas en los contadores. Un sistema real los publica periódicamente, igual que el taller del tema 40 hacía con las pérdidas.
07 · Con el tiempo
Diseñar para que dure
Un protocolo se escribe una vez y se usa durante años, con equipos de distintas épocas hablando entre sí. Los errores de diseño de esta etapa son los más caros, porque no se arreglan con una actualización del programa: hay que cambiar todos los equipos a la vez, o convivir con la herencia. Ocho reglas, aprendidas a golpes en protocolos reales:
- Poner una versión desde el primer día. Un byte de versión en cada mensaje (o en un mensaje de apertura) permite que el receptor sepa cómo leer lo que llega. Es el byte que más se lamenta no haber puesto.
- Reservar valores. Dejar sin usar rangos de tipos y bits de banderas, con la regla «el emisor los pone en cero, el receptor los ignora». Se pueden asignar después sin romper nada.
- El receptor ignora lo que no entiende. Un tipo desconocido se descarta (y se cuenta, no se trata como error fatal), y los bytes de más al final de los datos se ignoran. Con esa regla se puede agregar un campo al final de un mensaje sin romper a los equipos viejos, que leen los campos que conocen. Si en cambio se exige que el largo sea exactamente el esperado, cualquier extensión rompe todo.
- Tamaños y orden fijos. Enteros de 8, 16 o 32 bits, con el orden de bytes definido (en este curso, little-endian) y sin dejarlo a lo que haga el compilador; signo y unidad escritos en la documentación (centésimas de grado, milivolts), sin decimales de coma flotante. Un
floatde una placa no es el de otra, y una estructura de C sinpackedpuede tener relleno distinto. - Un valor para «sin dato». Un valor reservado (como
0x8000en una temperatura) para decir «el sensor falló». Un nodo mudo es más difícil de diagnosticar que uno que avisa su falla. - Mensajes autodescriptivos y de una sola cosa. Una lectura es una trama, un comando es otra. No mezclar órdenes y datos en un mismo mensaje con banderas que cambian cómo se lee el resto.
- Documentar. Una tabla con campos, tamaños, orden, unidades, rango válido y significado de los tipos, y un ejemplo de trama completa con sus bytes. Es el producto: el código se puede reescribir, el formato no.
- Probar con casos adversos. Probar el receptor con tramas dañadas, cortadas, repetidas, con largos imposibles y con basura al azar. Es lo que se hace en la herramienta 2 y en los ejercicios.
Cómo agregar un campo
Un caso típico: la LECTURA tiene nodo, temperatura y humedad (5 bytes), y un año después se quiere agregar la presión (2 bytes más). Con la regla 3, el emisor nuevo manda 7 bytes de datos, el receptor viejo lee los 5 que conoce y descarta los 2 de más: funciona sin tocar los equipos viejos. El receptor nuevo, al recibir un mensaje viejo (5 bytes), sabe por el largo que no trae la presión y la deja «sin dato». Si el campo tuviera que ir en el medio, o si cambiara el significado de uno existente, no hay forma compatible: se crea un tipo nuevo (0x02, LECTURA_V2) o se sube la versión, y ambos formatos conviven un tiempo.
Tipo, longitud y valor
La generalización de la regla 3 es el esquema TLV (tipo-longitud-valor): los datos son una secuencia de campos, cada uno con un byte de tipo, uno de longitud y los bytes del valor. El receptor lee los que conoce y salta los que no, usando la longitud. Es lo que usan los paquetes de publicidad de BLE (las estructuras [largo][tipo][contenido] del tema 38), los certificados X.509, las opciones de TCP y IP y los protocolos de red de muchos fabricantes. Cuesta dos bytes por campo (el doble de estructura que un formato fijo) y a cambio permite que el protocolo crezca sin límites. Para una lectura fija de pocos bytes, un formato fijo es más compacto; para un mensaje de configuración con muchos parámetros opcionales, el TLV es la elección.
Texto o binario
| Aspecto | Texto (N=1 T=23.50 H=55.00\n) | Binario (A5 06 01 01 2E 09 7C 15 30) |
|---|---|---|
| Tamaño del mismo mensaje | 20 bytes | 9 bytes |
| Tiempo a 9600 baudios | 20,8 ms | 9,4 ms |
| Depuración | Se lee en un terminal | Hace falta un decodificador o un volcado hexadecimal |
| Conversión de números | Cadena a número y al revés (más código y más tiempo) | Copia directa de bytes |
| Error de un carácter | Puede seguir pareciendo válido (28,50 por 23,50) | La suma lo detecta |
| Compatibilidad entre plataformas | Alta (ASCII) | Hay que fijar tamaño, signo y orden de bytes |
| Cuándo conviene | Pocos mensajes, órdenes de operador, depuración | Muchos datos, enlaces lentos, batería, más de un fabricante |
No hay una respuesta única y es común empezar con texto (se depura en minutos con un terminal) y pasar a binario cuando el volumen o la batería lo piden. Lo importante es que el cambio no afecte al resto de la arquitectura: si el encuadre, los tipos y la validación están separados del formato de los datos, se cambia solo la parte de los datos.
08 · Lo que todavía falta
Lo que todavía falta
La trama de este tema resuelve el encuadre y detecta la mayoría de los daños, pero no es un protocolo completo. Las faltas más importantes son el tema de los cinco siguientes:
| Falta | Consecuencia | Dónde se resuelve |
|---|---|---|
| No hay dirección | Sirve para un enlace de dos extremos; en un bus con varios dispositivos todos reciben todo | Tema 42: direcciones, difusión, encabezado |
| La suma es débil | No detecta dos errores que se compensan ni el cambio de orden de dos bytes | Temas 43 y 44: causas físicas y CRC-8 o CRC-16 |
| No hay confirmación | El emisor no sabe si llegó; una trama perdida se pierde | Tema 45: ACK, reintentos, números de secuencia |
| Los datos son bytes en bruto | El orden, el signo y la compatibilidad entre plataformas quedan a cargo del programador | Tema 46: formatos de datos |
| No hay autenticación ni cifrado | Cualquiera puede escuchar o inyectar tramas con la suma correcta (la suma no es una firma) | Tema 49: seguridad |
En el tema 47 se juntan todas las piezas en un taller: una trama con dirección, CRC, números de secuencia, confirmaciones y reintentos entre dos placas, con una pérdida simulada. Lo de este tema es el esqueleto sobre el que se agregan las demás.
09 · Con el microcontrolador
El emisor y el receptor
El programa siguiente se carga en dos placas (un ESP32, o un Arduino con un segundo puerto serie) conectadas con el TX de una al RX de la otra y una masa común (niveles de 3,3 V en las dos, tema 4). Una se compila como emisor y la otra como receptor, con la constante ROL_EMISOR. El emisor envía una LECTURA cada 2 s y un LATIDO cada 5 s; el receptor las decodifica con la máquina de estados y cuenta lo que pasa. Con INYECTAR_ERRORES el emisor daña una de cada diez tramas, para ver cómo se resincroniza el receptor.
// ---------- Configuración ----------
#define ROL_EMISOR 1 // 1 en la placa que envía, 0 en la que recibe
#define INYECTAR_ERRORES 1 // el emisor daña una de cada diez tramas
const uint8_t SOF = 0xA5;
const uint8_t MAX_LEN = 32; // tipo más hasta 31 bytes de datos
const uint8_t TIPO_LECTURA = 0x01;
const uint8_t TIPO_LATIDO = 0x10;
const uint32_t TIEMPO_BYTE_MS = 10; // silencio máximo entre bytes de una misma trama (a 9600 baudios)
// ---------- Armado de una trama ----------
size_t armarTrama(uint8_t tipo, const uint8_t *datos, uint8_t n, uint8_t *salida) {
salida[0] = SOF;
salida[1] = n + 1; // el largo cuenta el tipo y los datos
salida[2] = tipo;
memcpy(salida + 3, datos, n);
uint8_t suma = 0;
for (uint8_t i = 1; i < 3 + n; i++) suma += salida[i];
salida[3 + n] = (uint8_t)(0x100 - suma); // complemento a dos: la suma de todo da cero
return n + 4;
}
// ---------- Recepción como máquina de estados ----------
class Decodificador {
public:
typedef void (*AlRecibir)(uint8_t tipo, const uint8_t *datos, uint8_t n);
uint32_t errores = 0, descartados = 0, validas = 0;
Decodificador(AlRecibir cb) : alTrama(cb) {}
void alimentar(uint8_t b) {
ultimoByte = millis();
switch (estado) {
case ESPERA_SOF:
if (b == SOF) estado = ESPERA_LEN; else descartados++;
break;
case ESPERA_LEN:
if (b >= 1 && b <= MAX_LEN) { buf[0] = b; n = 1; faltan = b + 1; estado = CUERPO; }
else { errores++; descartados++; estado = ESPERA_SOF; alimentar(b); } // el byte puede ser el verdadero SOF
break;
case CUERPO:
buf[n++] = b;
if (--faltan == 0) {
estado = ESPERA_SOF;
uint8_t suma = 0;
for (uint8_t i = 0; i < n; i++) suma += buf[i];
if (suma == 0) { validas++; alTrama(buf[1], buf + 2, n - 3); }
else { // el SOF era falso: se vuelven a mirar los bytes que siguieron
errores++; descartados++;
uint8_t copia[MAX_LEN + 2];
uint8_t m = n;
memcpy(copia, buf, m);
n = 0;
for (uint8_t i = 0; i < m; i++) alimentar(copia[i]);
}
}
break;
}
}
void vigilarTiempo() { // se llama en cada vuelta de loop()
if (estado != ESPERA_SOF && millis() - ultimoByte > TIEMPO_BYTE_MS) {
errores++;
estado = ESPERA_SOF; // trama cortada: descartar y buscar otro comienzo
n = 0;
}
}
private:
enum Estado { ESPERA_SOF, ESPERA_LEN, CUERPO } estado = ESPERA_SOF;
AlRecibir alTrama;
uint8_t buf[MAX_LEN + 2]; // LEN, TIPO, datos y SUMA
uint8_t n = 0, faltan = 0;
unsigned long ultimoByte = 0;
};
// ---------- Aplicación ----------
void alRecibir(uint8_t tipo, const uint8_t *d, uint8_t n) {
if (tipo == TIPO_LECTURA && n >= 5) { // se leen los campos que se conocen; si hay más, se ignoran
int16_t t = (int16_t)(d[1] | (d[2] << 8));
uint16_t h = d[3] | (d[4] << 8);
Serial.printf("LECTURA nodo %u: %.2f °C, %.2f %%\n", d[0], t / 100.0, h / 100.0);
} else if (tipo == TIPO_LATIDO) {
Serial.println("LATIDO");
} else {
Serial.printf("Tipo desconocido 0x%02X: se ignora\n", tipo);
}
}
Decodificador receptor(alRecibir);
void setup() {
Serial.begin(115200);
Serial1.begin(9600, SERIAL_8N1, 16, 17); // RX en el pin 16 y TX en el 17 (ESP32)
}
void enviar(uint8_t tipo, const uint8_t *datos, uint8_t n) {
uint8_t trama[MAX_LEN + 3];
size_t largo = armarTrama(tipo, datos, n, trama);
#if INYECTAR_ERRORES
if (random(10) == 0) trama[random(largo)] ^= 0x04; // un bit cambiado en un byte al azar
#endif
Serial1.write(trama, largo);
}
void loop() {
#if ROL_EMISOR
static unsigned long ultimaLectura = 0, ultimoLatido = 0;
if (millis() - ultimaLectura >= 2000) {
ultimaLectura = millis();
int16_t t = 2350 + random(-50, 50); // 23,50 ± 0,50 °C
uint16_t h = 5500;
uint8_t d[5] = { 1, (uint8_t)(t & 0xFF), (uint8_t)((uint16_t)t >> 8), (uint8_t)(h & 0xFF), (uint8_t)(h >> 8) };
enviar(TIPO_LECTURA, d, sizeof(d));
}
if (millis() - ultimoLatido >= 5000) {
ultimoLatido = millis();
enviar(TIPO_LATIDO, nullptr, 0);
}
#else
while (Serial1.available()) receptor.alimentar(Serial1.read());
receptor.vigilarTiempo();
static unsigned long ultimoInforme = 0;
if (millis() - ultimoInforme >= 10000) {
ultimoInforme = millis();
Serial.printf("válidas %lu · errores %lu · bytes descartados %lu\n", receptor.validas, receptor.errores, receptor.descartados);
}
#endif
}
Qué hay que notar:
- La recepción no bloquea.
loop()vacía el puerto conwhile (available())y entrega cada byte aalimentar(): puede atender otras cosas entre medio (el emisor y el receptor comparten la estructura del programa de los talleres de los temas 11 y 33). - Los dos errores de la máquina de estados se manejan con el mismo recurso: volver a procesar los bytes. En el largo imposible se reprocesa un byte; en la suma incorrecta, hasta 34. La recursión tiene profundidad acotada (cada nivel procesa menos bytes que el anterior), y la copia local evita que el reprocesamiento pise el búfer.
- El tiempo de espera se vigila afuera de
alimentar(), convigilarTiempo(), porque la función solo corre cuando llega un byte y el problema es justamente que no llega ninguno. El tiempo de 10 ms corresponde a 9600 baudios (herramienta 5); con otra velocidad se recalcula. - El receptor lee los campos que conoce (
n >= 5) y no exige un largo exacto: una versión futura con más campos no lo rompe (regla 3). Un tipo desconocido se informa pero no detiene nada. - Los números se arman byte a byte, en little-endian, sin copiar la memoria de una estructura: así no dependen de cómo almacena los datos cada placa.
- La inyección de errores es una herramienta de prueba valiosa: con
INYECTAR_ERRORESen 1 el receptor muestra en el informe unos pocos errores y bytes descartados por cada decena de tramas, y nunca se queda trabado. Si se queda mudo después de un error, la resincronización tiene una falla. - En un Arduino sin
printfenSerial(AVR), se reemplaza porSerial.print; y en una placa sin segundo puerto, se puede usarSoftwareSeriala 9600 baudios.
10 · Analogía con programación
El protocolo en Python
El armado de la trama, el receptor como máquina de estados, el relleno de bytes, COBS y las cuentas de tiempo se prueban sin hardware. El modelo siguiente reproduce las reglas del programa y de la página, y es la forma de diseñar el protocolo: se escribe el decodificador, se lo prueba con flujos dañados hasta que se porta bien y recién entonces se lo lleva al microcontrolador.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
SOF = 0xA5
MAX_LEN = 32 # tipo más datos: hasta 31 bytes de datos
ESPERA_SOF, ESPERA_LEN, CUERPO = range(3)
def suma_de_verificacion(cuerpo):
return (0x100 - sum(cuerpo)) & 0xFF # la suma de todo, módulo 256, vale cero
def armar_trama(tipo, datos=b""):
cuerpo = bytes([len(datos) + 1, tipo]) + datos # el largo cuenta el tipo y los datos
return bytes([SOF]) + cuerpo + bytes([suma_de_verificacion(cuerpo)])
class Decodificador:
def __init__(self):
self.estado = ESPERA_SOF
self.buf = []
self.faltan = 0
self.errores = 0
self.descartados = 0
def alimentar(self, b):
"""Procesa un byte y devuelve la lista de tramas válidas que completó."""
tramas = []
if self.estado == ESPERA_SOF:
if b == SOF:
self.estado = ESPERA_LEN
else:
self.descartados += 1
elif self.estado == ESPERA_LEN:
if 1 <= b <= MAX_LEN:
self.buf, self.faltan, self.estado = [b], b + 1, CUERPO
else: # largo imposible: el SOF era falso
self.errores += 1
self.descartados += 1
self.estado = ESPERA_SOF
tramas += self.alimentar(b) # este byte puede ser el verdadero SOF
else:
self.buf.append(b)
self.faltan -= 1
if self.faltan == 0:
self.estado = ESPERA_SOF
resto, self.buf = self.buf, []
if sum(resto) % 256 == 0:
tramas.append((resto[1], bytes(resto[2:-1])))
else: # suma incorrecta: el SOF era falso, volver a mirar lo que sigue
self.errores += 1
self.descartados += 1
for x in resto:
tramas += self.alimentar(x)
return tramas
def slip_codificar(datos):
salida = bytearray()
for b in datos:
salida += {0xC0: b"\xDB\xDC", 0xDB: b"\xDB\xDD"}.get(b, bytes([b]))
return bytes(salida) + b"\xC0"
def cobs_codificar(datos):
salida, indice, codigo = bytearray([0]), 0, 1
for b in datos:
if b == 0:
salida[indice], indice, codigo = codigo, len(salida), 1
salida.append(0)
else:
salida.append(b)
codigo += 1
if codigo == 0xFF:
salida[indice], indice, codigo = codigo, len(salida), 1
salida.append(0)
salida[indice] = codigo
return bytes(salida)
def cobs_decodificar(datos):
salida, i = bytearray(), 0
while i < len(datos):
codigo = datos[i]
salida += datos[i + 1:i + codigo]
i += codigo
if codigo < 0xFF and i < len(datos):
salida.append(0)
return bytes(salida)
def tiempo_ms(n_bytes, baudios, bits=10):
return n_bytes * bits / baudios * 1000
lectura = armar_trama(0x01, bytes.fromhex("2E097C15"))
print("trama:", lectura.hex(" ").upper(), len(lectura), "bytes")
print("vacía:", armar_trama(0x10).hex(" ").upper())
print("ejercicio 1:", armar_trama(0x01, bytes.fromhex("112233")).hex(" ").upper())
flujo = bytes.fromhex("00 FF A5 05 01 2E 09 7C 15 32 A5 05 01 2E 09 7C 15 33 A5 01 10 EF 55 A5 05 01 2E")
d = Decodificador()
tramas = [t for b in flujo for t in d.alimentar(b)]
print("tramas:", [(hex(t), datos.hex(" ")) for t, datos in tramas], "errores:", d.errores, "descartados:", d.descartados)
d = Decodificador()
print("falso SOF:", [(hex(t), datos.hex(" ")) for b in bytes.fromhex("A5 A5 03 01 11 22 C9") for t, datos in d.alimentar(b)], "errores:", d.errores, "descartados:", d.descartados)
datos = bytes.fromhex("01 C0 DB 00 00 05")
print("SLIP:", slip_codificar(datos).hex(" ").upper(), len(slip_codificar(datos)), "bytes")
print("COBS:", cobs_codificar(datos).hex(" ").upper(), len(cobs_codificar(datos)) + 1, "bytes con el delimitador")
print("COBS recupera:", cobs_decodificar(cobs_codificar(datos)) == datos)
print("ejercicio SLIP:", slip_codificar(bytes.fromhex("C0 01 DB")).hex(" ").upper(), "COBS:", cobs_codificar(bytes.fromhex("11 00 22")).hex(" ").upper())
texto = "N=1 T=23.50 H=55.00\n"
binaria = armar_trama(0x01, bytes([1]) + bytes.fromhex("2E097C15"))
print("texto:", len(texto), "bytes,", round(tiempo_ms(len(texto), 9600), 1), "ms; binario:", len(binaria), "bytes,", round(tiempo_ms(len(binaria), 9600), 2), "ms")
for n in (2, 31):
t = armar_trama(0x01, bytes(n))
print(f"{n} bytes de datos: trama de {len(t)} bytes, {tiempo_ms(len(t), 9600):.2f} ms, {n * 8 / tiempo_ms(len(t), 9600) * 1000:.0f} bit/s útiles")
print("timeout a 1200 baudios:", round(10 * tiempo_ms(1, 1200), 1), "ms")
print("tipo 02:", armar_trama(0x02, bytes.fromhex("1020")).hex(" ").upper())
Resultado: la trama de una lectura (2E 09 7C 15) es A5 05 01 2E 09 7C 15 32, de 8 bytes; un latido vacío es A5 01 10 EF; y los datos 11 22 33 del tipo 1 dan A5 04 01 11 22 33 95. El flujo 00 FF A5 05 01 2E 09 7C 15 32 A5 05 01 2E 09 7C 15 33 A5 01 10 EF 55 A5 05 01 2E (basura, una lectura válida, otra con la suma dañada, un latido, un byte suelto y una trama cortada) entrega la lectura 01: 2e 09 7c 15 y el latido 10, con 1 error y 11 bytes descartados; la trama cortada del final queda esperando. Con un falso inicio delante, A5 A5 03 01 11 22 C9 entrega igual la trama 01: 11 22, con 1 error y 1 byte descartado: el segundo A5 se reprocesó. Los datos 01 C0 DB 00 00 05 con SLIP son 01 DB DC DB DD 00 00 05 C0 (9 bytes) y con COBS 04 01 C0 DB 01 02 05 más el cero final (8 bytes); COBS recupera los datos originales. Los datos C0 01 DB con SLIP dan DB DC 01 DB DD C0 y 11 00 22 con COBS, 02 11 02 22. El mismo mensaje en texto ocupa 20 bytes y 20,8 ms a 9600 baudios, y en binario 9 bytes y 9,38 ms. Con 2 bytes de datos, la trama mide 6 bytes, tarda 6,25 ms y entrega 2560 bit/s útiles; con 31 bytes mide 35, tarda 36,46 ms y entrega 6802 bit/s. A 1200 baudios, diez tiempos de byte son 83,3 ms. La trama A5 03 02 10 20 CB es el tipo 2 con datos 10 20.
Dos lecciones del modelo. Primero, el decodificador es más corto que cualquier explicación de cómo funciona: una clase de unas treinta líneas con tres estados. Segundo, la forma de probar un protocolo es con un generador de flujos hostiles: si el decodificador sobrevive a miles de flujos con basura, bytes perdidos y tramas cortadas, sin quedarse trabado y entregando las tramas buenas que haya, está listo.
11 · Exploración interactiva
Laboratorio: trama, receptor, relleno, texto y binario, tiempos
El laboratorio tiene cinco herramientas. La primera arma una trama con su largo y su suma. La segunda decodifica un flujo con basura y errores como lo haría el receptor. La tercera aplica el relleno de bytes (SLIP) y COBS. La cuarta compara el mismo mensaje en texto y en binario. La quinta calcula los tiempos de un enlace serie.
1. Armar una trama
Escribí el tipo y los datos.
2. Decodificar un flujo con errores
Escribí un flujo de bytes.
3. Relleno de bytes (SLIP) y COBS
Escribí unos datos.
4. Texto frente a binario
Elegí una lectura.
5. Tiempos del enlace serie
Elegí la velocidad.
En la herramienta 1, el tiempo supone 8N1 (10 bits por byte) y la suma es el complemento a dos de la suma de LEN, TIPO y datos. En la 2, un 0xA5 que resulta falso se descarta y la búsqueda sigue desde el byte siguiente. En la 3, SLIP agrega el 0xC0 final y COBS el 0x00 final, y los dos se cuentan con su delimitador. En la 4, el texto es N=… T=… H=… con un salto de línea. En la 5, la espera entre bytes es de 10 caracteres con un mínimo de 2 ms.
Recorrido guiado
- En la herramienta 1, con el tipo 01 y los datos 2E097C15 a 9600 baudios:
A5 05 01 2E 09 7C 15 32, 8 bytes, LEN = 5, suma 0x32, la estructura es el 50 % de la trama y tarda 8,33 ms. Con datos vacíos:A5 01 01 FE, 4 bytes y la estructura es el 100 % (en rojo). Con 31 bytes de A5 y 115 200 baudios: 35 bytes, la estructura es el 11,4 % y tarda 3,04 ms; los A5 de los datos no causan problemas porque la longitud manda. - En la herramienta 2, con el flujo inicial: 2 tramas válidas (la lectura
2E 09 7C 15y el latido), 1 con suma incorrecta (la lectura dañada), 11 bytes descartados (los dos del comienzo, los ocho de la trama dañada y el 55 suelto) y 4 bytes de una trama cortada al final. Con una sola trama buena: «flujo limpio». ConA5 A5 03 01 11 22 C9: la trama se encuentra igual, con 1 error y 1 byte descartado, porque el segundo A5 se reprocesa. Con01 02 03: ninguna trama. - En la herramienta 3, con 01 C0 DB 00 00 05: SLIP
01 DB DC DB DD 00 00 05 C0(9 bytes, +50 %) y COBS04 01 C0 DB 01 02 05 00(8 bytes, +33 %). Con 41 42 43: SLIP agrega solo el final (4 bytes) y COBS, 5. Con C0 C0 C0: SLIP duplica y más (7 bytes, +133 %) y COBS tiene 5. Con 00 00 00 00: SLIP 5 bytes y COBS 6. - En la herramienta 4, con 23,5 °C, 55 %, nodo 1 y 9600 baudios: el texto
N=1 T=23.50 H=55.00\ntiene 20 bytes y tarda 20,8 ms; la tramaA5 06 01 01 2E 09 7C 15 30, 9 bytes y 9,38 ms: el texto ocupa 2,2 veces más. Con −5,25 °C y 7 %: la trama conF3 FDmantiene los 9 bytes, y el texto sigue en 20. - En la herramienta 5, con 9600 baudios, 10 bits y 35 bytes: 1,04 ms por byte, 36,5 ms la trama más larga, un silencio de 3,65 ms, una espera de 10,4 ms y 120 tramas pequeñas por segundo. Con 115200: 87 µs por byte, 3,04 ms, silencio de 1,75 ms y espera de 2 ms (el mínimo). Con 1200 baudios y 100 bytes: 8,33 ms por byte, 833 ms la trama máxima (en rojo) y espera de 83,3 ms.
Desafío: el sensor que mandaba basura
Un sensor remoto manda cada segundo una LECTURA al maestro por un cable largo, y el maestro informa de vez en cuando «tipo desconocido 0xA5» y pierde alguna lectura. Con las herramientas 2 y 5 y lo visto: (1) El tipo 0xA5 indica que el receptor interpretó como tipo un byte que era un SOF: se perdió un byte de la trama anterior (por ruido o por una interrupción), el receptor quedó desfasado y tomó el SOF de la siguiente como un dato de la primera. En un diseño sin reprocesamiento se perdían dos tramas: con el reprocesamiento de este tema, solo se pierde la dañada (herramienta 2: la trama siguiente se recupera). (2) La causa se mide con las cuentas: muchos errores de suma con pocos de largo apuntan a ruido; descartados sin errores, a un emisor que reinicia o a un arranque desfasado; errores solo a ciertas horas, a una interferencia (tema 43). (3) El cable largo a 9600 baudios aguanta bien; con 115 200 empeora (tema 5): se baja la velocidad o se usa RS-485 (tema 24). (4) Sin dirección ni confirmación, el maestro no puede pedir la lectura perdida: la próxima llegará en un segundo. Si no se tolera, hace falta un ACK con reintento y un número de secuencia (tema 45). (5) El tipo «desconocido» no debe bloquear nada: se informa y se sigue, como hace el programa de la sección 9.
12 · Comprensión
Confusiones frecuentes
«Con un salto de línea al final de cada mensaje alcanza»
Solo si los datos nunca contienen un salto de línea y el enlace nunca pierde un byte. Con datos binarios (un 0x0A es un valor tan válido como cualquier otro) hace falta relleno de bytes o una longitud, y para saber que el mensaje llegó entero, una suma.
«Si la trama lleva la longitud, no hace falta un byte de inicio»
Hace falta para resincronizarse: sin un inicio reconocible, un receptor que arranca a mitad de una trama o que perdió un byte no tiene forma de saber dónde empieza la siguiente, y toma datos por largos. El inicio no cierra la trama (la cierra el largo): ubica el comienzo.
«Un 0xA5 dentro de los datos va a confundir al receptor»
No, porque con la longitud el receptor sabe cuántos bytes más son datos y no los examina. Puede confundirlo fuera de una trama (en basura), y ahí la suma lo desenmascara. El relleno de bytes solo hace falta cuando el delimitador es lo que cierra el mensaje.
«Cuando falla la suma, se descarta todo lo recibido y se espera la próxima trama»
Se pierde una trama buena que estaba escondida adentro: un falso inicio puede haber consumido los primeros bytes de la verdadera. Lo correcto es descartar solo el falso SOF y volver a procesar los bytes que siguieron. Con unas pocas líneas se evita perder la trama siguiente.
«El receptor puede esperar con un while a que llegue el resto de la trama»
Si el resto nunca llega (el emisor se apagó), el programa se cuelga para siempre, y mientras espera no atiende nada más. Se recibe byte a byte, con una máquina de estados y un tiempo de espera entre bytes.
«La suma de verificación garantiza que los datos son correctos»
La suma de un byte deja pasar 1 de cada 256 errores al azar y no detecta errores que se compensan ni intercambios de bytes. Detecta muchos daños, no todos. Y no autentica: cualquiera puede calcular la suma de una trama falsa. Para lo primero, un CRC (tema 44); para lo segundo, una firma o un código de autenticación (tema 49).
«El texto es siempre mejor porque se puede leer»
Es mejor para depurar y para pocos mensajes. Cuesta el doble de bytes o más, necesita conversiones y no detecta un carácter cambiado. En un enlace lento, con batería o con muchos datos, el binario es la elección, con un volcado hexadecimal para depurar.
«Mi protocolo funciona, lo probé y anda»
Lo probaste con el flujo perfecto. Un protocolo se prueba con lo que sale mal: arranques a mitad de una trama, bytes perdidos, ruido, tramas cortadas, largos imposibles y tipos desconocidos. Si el receptor se recupera de todo eso solo, funciona.
«Después agregamos la versión»
Cuando haya equipos instalados sin versión, no hay forma de distinguirlos de los nuevos: cualquier cambio rompe a unos o a otros. Es un byte, y es el que más se lamenta no haber puesto en el primer día.
13 · Práctica guiada
Predecí antes de comprobar
1. Armar una trama
Armá la trama del tipo 01 con los datos 11 22 33. ¿Cuánto vale LEN y cuánto la suma?
Ver solución
LEN = 1 + 3 = 4. Suma de LEN, TIPO y datos: 04 + 01 + 11 + 22 + 33 = 0x6B. La suma de verificación es el complemento a dos: 0x100 − 0x6B = 0x95. La trama: A5 04 01 11 22 33 95 (7 bytes). Comprobación: 0x6B + 0x95 = 0x100, que módulo 256 es cero.
2. ¿Es válida?
El receptor ve A5 03 02 10 20 CB. ¿Es una trama válida? ¿Y si el quinto byte fuera 21?
Ver solución
LEN = 3: siguen el tipo (02) y dos bytes de datos (10 y 20), y después la suma. 03 + 02 + 10 + 20 + CB = 3 + 2 + 16 + 32 + 203 = 256 → cero: válida, del tipo 2 con datos 10 20. Con 21 en lugar de 20 la suma da 257, o sea 1: inválida, se descarta y se cuenta un error.
3. Relleno de bytes
Codificá con SLIP los datos C0 01 DB y con COBS los datos 11 00 22. ¿Cuántos bytes ocupa cada uno, con su delimitador?
Ver solución
SLIP: C0 pasa a DB DC, 01 queda, DB pasa a DB DD y se agrega el C0 final: DB DC 01 DB DD C0, 6 bytes (el doble de los datos). COBS: 11 es el primer bloque, hasta el cero, con el código 2; después 22, con el código 2: 02 11 02 22 y el 00 final: 5 bytes.
4. Resincronización
El receptor ve A5 A5 03 01 11 22 C9. ¿Qué entrega, cuántos errores cuenta y cuántos bytes descarta?
Ver solución
El primer A5 pasa a ESPERA_LEN; el byte siguiente, A5 = 165, es un largo imposible (máximo 32): se cuenta un error, se descarta el primer A5 y se vuelve a procesar el segundo A5 en ESPERA_SOF, donde es un inicio. Sigue 03 01 11 22 C9: LEN = 3, tipo 01, datos 11 22 y suma C9 (03 + 01 + 11 + 22 + C9 = 3 + 1 + 17 + 34 + 201 = 256). Entrega una trama válida (tipo 1, datos 11 22) con 1 error y 1 byte descartado. Sin el reprocesamiento, el segundo A5 se habría perdido junto con la trama.
5. Costo de la estructura
A 9600 baudios, ¿cuánto tarda una trama con 2 bytes de datos y cuántos bits de datos útiles por segundo entrega si se envían sin pausa? ¿Y con 31 bytes de datos?
Ver solución
2 bytes de datos: 6 bytes, 60 bits, 6,25 ms. Se entregan 16 bits de datos en 6,25 ms: 2560 bit/s, el 27 % de los 9600. Con 31 bytes de datos: 35 bytes, 350 bits, 36,46 ms, con 248 bits de datos: 6802 bit/s, el 71 %. Por eso conviene agrupar los datos en tramas más grandes cuando el tiempo importa.
6. Tiempo de espera
A 1200 baudios, ¿cuánto dura un byte y qué tiempo de espera entre bytes usarías (10 tiempos de byte)? ¿Y a 115 200?
Ver solución
A 1200 baudios, un byte (10 bits) dura 8,33 ms: diez bytes son 83,3 ms. A 115 200, un byte dura 87 µs y diez bytes serían 0,87 ms: demasiado fino para el microcontrolador (las interrupciones y el sistema operativo lo exceden), y se impone un mínimo de 2 ms.
7. Agregar un campo
La LECTURA tiene 5 bytes de datos (nodo, temperatura, humedad) y quieren agregar la presión (2 bytes) sin tocar los 200 equipos ya instalados. ¿Qué regla lo permite y cómo se hace?
Ver solución
Lo permite la regla de que el receptor lee los campos que conoce e ignora los bytes de más. El emisor nuevo manda 7 bytes de datos con la presión al final; los receptores viejos (n >= 5) leen los cinco primeros y descartan los dos últimos. El receptor nuevo, al recibir una trama de 5 bytes, sabe por el largo que no trae presión y la deja «sin dato». Si el receptor viejo exigiera n == 5, cualquier extensión lo rompería, y habría que crear un tipo nuevo o subir la versión.
14 · Síntesis
Ideas para recordar
- Un enlace transporta un flujo de bytes sin fronteras. Un protocolo responde en orden: dónde empieza y termina un mensaje, qué hacer con el delimitador en los datos, qué dice cada mensaje, y qué hace el receptor ante un error. El criterio es que el receptor entre en cualquier momento, se equivoque sin consecuencias y se resincronice solo.
- Hay cuatro métodos de encuadre: un delimitador de fin, inicio y fin, longitud primero y silencio. Ninguno alcanza solo en un enlace con errores: se combinan un inicio reconocible (para resincronizarse), una longitud (para no prohibir valores en los datos) y una suma (para saber que el encuadre fue correcto).
- Cuando un delimitador cierra el mensaje, los datos binarios deben ser transparentes: el relleno de bytes (SLIP) reemplaza el delimitador y el escape por pares de bytes (en el peor caso duplica los datos), y COBS reescribe los datos sin ceros con un costo de un byte cada 254. Con la longitud en la trama, no hace falta ninguno.
- La trama del tema es [SOF 0xA5][LEN][TIPO][datos 0 a 31 bytes][SUMA]. LEN cuenta el tipo y los datos, tiene un máximo de 32 (protege la memoria y acota la espera), y SUMA es el complemento a dos de la suma de LEN, TIPO y datos (la suma de todo, módulo 256, da cero). Tiene 4 bytes de estructura: se agrupan datos para que el costo no pese.
- El receptor es una máquina de estados alimentada byte a byte (ESPERA_SOF, ESPERA_LEN, CUERPO) que no bloquea. Ante un largo imposible, reprocesa ese byte; ante una suma incorrecta, descarta solo el falso inicio y reprocesa los bytes que siguieron: una trama dañada cuesta solo esa trama.
- Se agrega un tiempo de espera entre bytes (unos 10 tiempos de byte, con un mínimo de 2 ms a velocidades altas) que descarta una trama cortada, y se llevan contadores de tramas válidas, errores y bytes descartados: son la única forma de saber cómo se porta el enlace.
- Para que el protocolo dure: una versión desde el primer día, valores reservados, el receptor que ignora lo que no entiende (tipos desconocidos y bytes de más al final), tamaños y orden de bytes fijos, un valor para «sin dato» y una tabla documentada. TLV da extensibilidad total a costo de dos bytes por campo.
- Texto o binario: el texto se depura a simple vista pero ocupa el doble o más y no detecta un carácter cambiado; el binario es compacto y verificable pero necesita un decodificador. A la trama le faltan todavía direcciones (tema 42), un CRC (44), confirmaciones y reintentos (45), formatos más completos (46) y seguridad (49).
¿Listo para el siguiente tema?
Antes de avanzar, calculá la trama del tipo 2 con datos 10 20 y explicá por qué un 0xA5 dentro de los datos no confunde al receptor; describí qué hace el receptor cuando en ESPERA_LEN llega un largo de 200 y por qué se vuelve a procesar ese byte; y decidí cómo agregarías un campo a un mensaje que ya usan equipos instalados. Si podés justificar las tres respuestas, entendés cómo se diseña un protocolo propio.
En el próximo tema, direccionamiento y paquetes, la trama gana un encabezado con dirección de origen y de destino, y se estudian las difusiones, el multidifusión, los identificadores y la separación entre encabezado, carga útil y cola: lo que hace falta para que muchos dispositivos compartan un mismo medio.