← Índice del curso

Comunicación entre Dispositivos · Tema 45 · Protocolos, paquetes y errores

Confirmaciones, reintentos y corrección

El tema anterior permite saber que una trama llegó dañada; este explica qué hacer con ese saber. El receptor que detecta un error puede callarse, y el emisor, que no oyó nada, tiene que adivinar qué pasó; o puede avisar, y el emisor, con esa información, reenvía. Eso es una confirmación (ACK) con un reintento, y alrededor de él aparecen todas las preguntas de un protocolo confiable. ¿Cuánto hay que esperar antes de dar la trama por perdida? ¿Cuántas veces se reintenta, y con qué pausa? ¿Qué pasa si lo perdido fue la confirmación y no la trama, de modo que el receptor recibe el mismo mensaje dos veces? ¿Cómo se manda más de una trama sin esperar a cada confirmación? ¿Y cuándo conviene no reenviar, sino agregar tanta redundancia que el receptor pueda corregir el error por sí mismo, como en un código de Hamming? El tema responde con cuentas: tiempos de un intercambio, probabilidades de entrega, números de secuencia y un enlace confiable completo que se prueba con pérdidas artificiales. Con él se completa el bloque que va del paquete (tema 41) a un canal confiable construido sobre uno que no lo es.

01 · Objetivo

Un enlace confiable sobre uno que no lo es

Un intercambio con pérdidas, espera y reintentos Diagrama de tiempo entre un emisor a la izquierda y un receptor a la derecha. El emisor envía el dato 0, que llega, y el receptor responde con la confirmación 0. El emisor envía el dato 1, que se pierde por el camino. Vence el temporizador y el emisor reenvía el dato 1, que llega; la confirmación 1 se pierde. Vence otra vez el temporizador y el emisor reenvía el dato 1 por segunda vez. El receptor reconoce el número de secuencia repetido, no entrega el mensaje de nuevo y vuelve a confirmar. La confirmación llega y el emisor pasa al dato 2. Una trama perdida y una confirmación perdida: el emisor reintenta y el receptor reconoce la copia. EmisorReceptor DATO #0 ACK #0 DATO #1 se pierde vence eltemporizador DATO #1 (reintento 1) ACK #1 se pierde la confirmación vence otra vez DATO #1 (reintento 2) #1 repetido:no se entrega ACK #1 confirmado: sigue con el #2 El emisor nunca ve la pérdida: solo el silencio. El receptor nunca ve la confirmación perdida: solo una copia.

La detección de errores del tema 44 convierte un problema invisible (el dato dañado que se acepta) en uno visible (la trama descartada). Pero descartar no resuelve: el mensaje se perdió, y el emisor, que ya lo mandó, no sabe nada. La solución más antigua y general se llama ARQ (automatic repeat request, petición automática de repetición): el receptor confirma lo que recibe bien; el emisor espera esa confirmación, y si no llega en cierto tiempo, reenvía. Con ese mecanismo simple, un canal que pierde el 20 % de las tramas se vuelve, para la aplicación, un canal que entrega todo, a costa de tiempo extra en los intentos perdidos.

El diagrama muestra el caso completo. Dos cosas pueden salir mal en un intercambio: la trama puede perderse, y la confirmación también. Desde el emisor son indistinguibles: en los dos casos, el silencio. El tratamiento es el mismo, reenviar, pero tiene consecuencias distintas en el receptor: si se perdió la trama, la copia es el mensaje original; si se perdió la confirmación, la copia es un duplicado de algo que el receptor ya procesó, y hay que reconocerlo para no actuar dos veces (encender un relé es inofensivo; sumar una moneda al contador, no). De esa asimetría nacen los números de secuencia. Quedan tres temas de diseño: cuánto esperar, cuántas veces insistir y cómo no confundir una copia con un mensaje nuevo.

02 · Avisar

ACK, NACK y silencio

Hay tres formas de que el receptor informe lo que pasó:

Las formas de confirmar.
FormaQué significaVentajasProblemasEjemplos del curso
ACK (confirmación positiva)«Recibí bien la trama número n»Simple. La falta de ACK cubre todas las pérdidas: no hace falta que el receptor note el problemaHay que esperar un tiempo para estar seguro de que no llegóBit de ACK en I²C (tema 17), ACK de TCP (tema 31), QoS 1 de MQTT (tema 32), ESP-NOW (tema 39)
NACK (confirmación negativa)«Recibí una trama pero estaba dañada» o «falta la n»Acelera la recuperación: el emisor reenvía de inmediato, sin esperar el tiempo de esperaSolo se puede enviar si el receptor se enteró de que algo falló. Una trama que no llegó nunca no genera NACK. Un NACK también puede perderseNACK de I²C (tema 17), excepciones de Modbus (tema 25), mensajes de error de protocolos de texto (tema 10)
Silencio (confirmación implícita)La respuesta es la confirmación: si el esclavo contestó, la consulta llegóSin tráfico extraSi no hay respuesta, no se sabe qué se perdió, la consulta o la respuestaConsulta y respuesta de Modbus (tema 25), sondeo del tema 42

El ACK es la base; el NACK es una optimización, y solo de las pérdidas que el receptor pueda ver: una trama con CRC incorrecto (el receptor sabe que algo llegó y estaba mal) o un hueco en los números de secuencia (llegó la 5 después de la 3: falta la 4). Una trama que se desvaneció sin dejar rastro solo se recupera con el temporizador del emisor. Por eso los protocolos serios usan ACK con temporizador como mecanismo de fondo y, a veces, NACK para acelerar.

También importa qué confirma el ACK. Puede ser la recepción («la trama llegó íntegra a mi búfer»), la aceptación («pasó la validación») o el procesamiento («ya hice lo que pedía»). Un ACK de recepción es rápido y no promete nada sobre la acción: si la orden era «cerrar la válvula» y el ACK llegó antes de que el actuador se moviera, un fallo posterior queda sin avisar. Cuando importa el resultado, la respuesta tiene que llevar el resultado (la lectura, el estado nuevo), no solo la confirmación de que el pedido se vio. En el paquete del tema 42, un nodo que atiende un pedido contesta con una lectura, que sirve de ACK y de resultado a la vez.

Y hay una regla heredada del tema 42: no se confirma una difusión. Si treinta nodos reciben un mensaje por difusión y los treinta contestan, las confirmaciones chocan. Para una difusión que importa, se hace una ronda de consultas individuales o se usa una confirmación con retardo al azar. Una confirmación es, al fin, un paquete como cualquier otro: lleva destino, origen y verificación, puede perderse y puede llegar dañada.

03 · Cuánto esperar, cuánto insistir

Tiempos de espera y reintentos

Cuando el emisor manda una trama, arranca un temporizador. Si la confirmación llega antes de que venza, el intercambio termina bien; si vence, reenvía y arranca otro. Elegir el tiempo es una apuesta entre dos errores:

  • Demasiado corto: vence antes de que la confirmación pueda llegar. El emisor reenvía una trama que sí llegó (reintento falso), el receptor recibe un duplicado y el enlace se llena de tráfico inútil. En el peor caso, la confirmación tardía de un intento se toma como la del siguiente.
  • Demasiado largo: cada pérdida detiene el enlace durante todo ese tiempo. Si se pierde el 10 % de los intercambios y el tiempo de espera es 20 veces el del intercambio, el 70 % del tiempo se pierde esperando.

El tiempo de un intercambio es la suma de lo que ocurre en cada tramo: la trama (bytes × 10 bits ÷ velocidad), el tiempo de proceso del receptor (validar, atender, preparar la respuesta) y la confirmación. Con una trama de 10 bytes a 9600 baudios (10,4 ms), una confirmación de 8 bytes (8,3 ms) y 5 ms de proceso, el viaje de ida y vuelta es de unos 24 ms (la herramienta 1). Una espera de 2 a 4 veces ese valor, 50 a 100 ms, deja margen para variaciones sin hacer lenta la recuperación. En un enlace cuyo tiempo varía mucho (Wi-Fi, redes con congestión), el valor fijo no sirve, y los protocolos estiman el tiempo medido: TCP lleva un promedio móvil del viaje de ida y vuelta y de su variación, y fija la espera en el promedio más cuatro veces la variación.

Insistir con criterio

Reintentar no puede ser indefinido, y no puede ser inmediato:

  • Un límite de reintentos. Si no hay respuesta después de R intentos, el emisor se rinde y avisa a la aplicación («el nodo 4 no contesta»). Un enlace que insiste para siempre bloquea al resto y oculta una falla que alguien debe atender. Tres a cinco reintentos es lo habitual.
  • Espera creciente (backoff exponencial): cada reintento espera el doble que el anterior (60, 120, 240, 480 ms…), hasta un tope. Si la causa es una interferencia que dura unos segundos (tema 43), reintentar enseguida cae dentro del mismo evento y falla igual; esperar cada vez más da tiempo a que pase. También evita que, cuando un nodo falla y muchos intentan hablarle, el bus se llene de insistencias.
  • Un poco de azar (jitter): sumarle a cada espera una fracción al azar. Si varios nodos fallaron a la vez, sin azar reintentarían a la vez y volverían a chocar. Es la misma idea que el retardo aleatorio del tema 42.
  • Distinguir las fallas: sin respuesta (temporizador), respuesta dañada (CRC), NACK, y excepción (respuesta que dice que el pedido es inválido). Las dos primeras se reintentan; las otras no, porque repetir un pedido inválido lo seguirá siendo.

Con un límite de 5 reintentos y una base de 60 ms que se duplica hasta un tope de 16 veces, la espera total hasta rendirse es de unos 2,8 s (60 + 120 + 240 + 480 + 960 + 960 ms, sin contar el azar): lo que hay que tener en cuenta para saber cuándo avisar de que un nodo está caído. Con esos números también se calcula cuánto cuesta el enlace con pérdidas: si cada intento sale bien con probabilidad ps, se necesitan en promedio 1/ps intentos, y un intento sale bien si llegan las dos, la trama y su confirmación: ps = (1 − ptrama)(1 − pconf). Con 10 % de pérdida en cada sentido, ps = 81 %: 1,23 intentos por mensaje, y la probabilidad de agotar 5 reintentos (6 intentos) es 0,196 ≈ 0,005 %. La herramienta 2 hace esa cuenta.

04 · No repetir lo ya hecho

Números de secuencia y duplicados

Un número de secuencia es un contador que el emisor pone en cada mensaje nuevo y que se repite en sus reintentos. Resuelve tres problemas a la vez:

  • Duplicados. Si se pierde la confirmación, el emisor reenvía y el receptor ve un mensaje con el mismo número que el último que aceptó. Lo reconoce como copia, no lo entrega otra vez, pero sí lo confirma, porque la confirmación anterior se perdió y el emisor sigue esperando. Descartar sin confirmar dejaría al emisor reintentando hasta rendirse.
  • Confirmaciones tardías. El ACK lleva el número de la trama que confirma. Si llega la confirmación del intento anterior después de un reintento, se la ignora o se la toma como confirmación del mensaje (que sí llegó): el número dice a cuál corresponde. Sin número, una confirmación vieja podría darse por la del mensaje siguiente.
  • Pérdidas y desorden. Si el receptor ve la trama 5 después de la 3, sabe que falta la 4: puede pedirla (NACK) o avisar a la aplicación. Si llega la 3 otra vez después de la 5, es vieja.

En el esquema más simple, el de un mensaje en vuelo a la vez (parar y esperar), alcanza con un solo bit de secuencia que alterna 0, 1, 0, 1…: el receptor recuerda el último bit aceptado, y una trama con el mismo bit es copia. Es el «protocolo de bit alternante», y funciona porque nunca hay dos mensajes distintos en vuelo. En la práctica se usa un contador de 8 o 16 bits, que además sirve para contar tramas perdidas (el medidor del tema 43) y para diagnosticar. El número va en el encabezado, y la suma o el CRC lo cubren.

La herramienta 4 simula el efecto con una pérdida del 20 % de tramas y del 20 % de confirmaciones, hasta 3 reintentos y 10 000 mensajes. Sin números de secuencia, la aplicación recibe 12 343 entregas de 9986 mensajes distintos: 2357 mensajes repetidos, uno de cada cuatro, cada vez que se perdió una confirmación. Con números de secuencia, las entregas son exactamente 9986, sin ninguna repetida. En los dos casos, el emisor se rinde en 166 mensajes (el 1,7 %), y en 152 de esos la trama sí había llegado: el receptor tiene el mensaje, el emisor cree que fracasó. Es un límite teórico (el «problema de los dos generales»): sobre un canal que pierde mensajes, ningún protocolo logra que ambos extremos estén seguros del mismo hecho. Se mitiga con reintentos y se asume con aplicaciones tolerantes, en la sección siguiente.

Dos advertencias de diseño. El contador tiene que ser mayor que lo que puede estar en vuelo, o se confunde una trama nueva con una vieja (un contador de un bit con dos tramas pendientes no sirve). Y el receptor tiene que sobrevivir a los reinicios con cuidado: si el emisor se reinicia y arranca la cuenta en 0, el receptor, que recuerda un número viejo, puede descartar mensajes nuevos por tomarlos como copias. Los protocolos agregan un intercambio de sincronización al comenzar (como el establecimiento de conexión de TCP) o una señal de que el emisor reinició.

05 · Aprovechar la espera

Más de una trama en el aire

En «parar y esperar», el enlace está ocioso mientras el emisor aguarda la confirmación. Con una trama de 10 bytes a 9600 baudios y 14 ms hasta recibir la confirmación, el enlace solo transporta datos el 43 % del tiempo (la herramienta 5); a velocidades altas o con latencias grandes (un enlace de radio, una red), es mucho peor. La mejora es mandar varias tramas seguidas sin esperar cada confirmación: una ventana deslizante de tamaño W deja hasta W tramas numeradas pendientes de confirmar. Cada confirmación que llega corre la ventana y libera lugar para otra trama.

El enlace queda ocupado cuando W × (tiempo de una trama) ≥ (tiempo de una trama + espera de la confirmación). Con las cifras anteriores, hace falta una ventana de 3. Ahora aparece una pregunta nueva: ¿qué se reenvía cuando una trama se pierde en medio de la ventana? Hay dos políticas:

Dos políticas de recuperación con ventana.
PolíticaCómo funcionaReceptorVentajaCostoNúmeros de secuencia
Retroceder N (go-back-N)Si la trama k se pierde, el emisor reenvía desde la k, aunque las siguientes hubieran llegadoAcepta solo en orden; descarta las que llegan adelantadasMuy simple: sin búfer de reordenamiento, confirmaciones acumulativas («recibí todo hasta la k»)Reenvía de más: una pérdida cuesta una ventana enteraW + 1 valores
Repetición selectivaEl emisor reenvía solo la perdidaGuarda las que llegan fuera de orden y las entrega en orden cuando completa el huecoAprovecha el enlace: solo se repite lo perdidoBúfer en el receptor y un temporizador (o NACK) por trama2W valores

TCP es una variante de ventana con confirmaciones acumulativas (y confirmaciones selectivas opcionales), y su ventana se ajusta sola según lo que informa el receptor y lo que la red tolera (tema 31). En un microcontrolador pequeño, con enlaces de 9600 a 115 200 baudios, de latencia corta, «parar y esperar» alcanza casi siempre; la ventana empieza a hacer falta con radios de varios milisegundos de latencia por tramo, y con tramas chicas a velocidades altas.

La ventana tiene además un uso que no es la velocidad: el control de flujo. Un receptor lento anuncia cuánto espacio tiene en su búfer (la «ventana» de TCP, o las líneas RTS/CTS del tema 10), y el emisor no manda más de lo que cabe. La misma pieza que da velocidad evita desbordar al que escucha.

06 · Qué se promete a la aplicación

Qué garantiza una entrega

Con confirmaciones y reintentos, el protocolo ofrece a la aplicación una de tres garantías, y la elección se decide por lo que cuesta un mensaje repetido o perdido:

Garantías de entrega.
GarantíaQué se aseguraCómo se lograCuándo sirveEjemplo
A lo sumo una vezNunca se repite; puede perderseUn envío, sin confirmación ni reintentoDatos que se repiten y se reemplazan: una temperatura cada segundoQoS 0 de MQTT, difusión, UDP
Al menos una vezSiempre llega; puede llegar repetidoConfirmación y reintentos, sin descartar copiasCuando repetir es inofensivo (idempotente)QoS 1 de MQTT
Exactamente una vezLlega, y solo una vezConfirmación, reintentos, números de secuencia y descarte de copias (y, con reinicios, un intercambio extra)Cuando repetir duplica el efecto: contar, cobrar, abrir una puerta temporalQoS 2 de MQTT, TCP dentro de una conexión

Una alternativa barata a «exactamente una vez» es diseñar los mensajes para que repetirlos no importe: idempotencia. «Poner el relé 3 en ON» es idempotente: se puede mandar diez veces y el resultado es el mismo. «Invertir el relé 3» no lo es: dos copias dejan el relé como estaba. «Sumar 1 al contador» no lo es; «fijar el contador en 57» sí. Un protocolo para un microcontrolador casi siempre se puede escribir con mensajes de estado en lugar de mensajes de cambio (el valor deseado, no la diferencia), y con eso las copias son inofensivas y alcanza con «al menos una vez», que es mucho más simple. Cuando no se puede, se agrega a la orden un identificador (el número de secuencia, o un identificador de transacción como el del tema 42) y el receptor recuerda los últimos que atendió.

Dos reglas más. Primero, en una consulta con respuesta, la respuesta es la confirmación (el silencio del tema 42 y de Modbus), y un reintento de la consulta es seguro si la consulta no tiene efectos (leer registros). Segundo, la aplicación debe decidir qué hace cuando se agotan los reintentos: reportar la falla, usar el último valor conocido con una marca de antigüedad, pasar a un estado seguro (una válvula, un motor), o reintentar más tarde. Un enlace confiable no elimina el fallo: lo convierte en un evento que se ve y se maneja.

07 · Reparar sin pedir de nuevo

Corregir en lugar de reenviar

Código de Hamming (7,4): qué cubre cada bit de paridad Siete posiciones numeradas del 1 al 7. Las posiciones 1, 2 y 4 son bits de paridad p1, p2 y p4; las posiciones 3, 5, 6 y 7 son datos d1, d2, d3 y d4. La fila de p1 cubre las posiciones 1, 3, 5 y 7; la de p2 cubre 2, 3, 6 y 7; la de p4 cubre 4, 5, 6 y 7. En el ejemplo, el bit de la posición 5 se invierte: fallan las paridades p1 y p4, y no p2. Leídas como un número binario, 1 0 1, dan 5: la posición del error. Cada bit de paridad cubre las posiciones cuyo número, en binario, tiene un 1 en su columna. p1p2d1p4d2d3d4 posición 1posición 2posición 3posición 4posición 5posición 6posición 7 bit invertido por el ruido p1p2p4 falla → 1bien → 0falla → 1 p4 p2 p1 = 1 0 1 = 5: el error está en la posición 5 El receptor invierte ese bit y recupera el dato sin pedir nada.

Reenviar necesita un canal de retorno, tiempo para esperar y que el mensaje se pueda repetir. A veces no se cumple alguna de las tres: una radio de sentido único (un sensor que solo transmite), un enlace con tanta latencia que cada ida y vuelta es carísima (una sonda lejana), una transmisión a muchos receptores (difusión), un dato que deja de servir si se retrasa (audio, video). En esos casos se usa la alternativa: corrección de errores hacia adelante (FEC, forward error correction), que agrega tanta redundancia que el receptor puede reparar el daño por sí mismo. La redundancia del tema 44 servía para detectar; esta, más abundante y mejor organizada, para localizar.

El caso más simple es el código de repetición: cada bit se manda tres veces y el receptor decide por mayoría. Corrige un error por grupo de tres, al precio de triplicar el tráfico (200 % de más). El código de Hamming hace lo mismo con mucha menos redundancia. En el Hamming (7,4), cada 4 bits de datos se agregan 3 bits de paridad y se mandan 7. Las posiciones 1, 2 y 4 (las potencias de 2) son de paridad; las 3, 5, 6 y 7, de datos. Cada bit de paridad cubre las posiciones cuyo número en binario tiene un 1 en esa columna: p1 cubre 1, 3, 5 y 7; p2, las 2, 3, 6 y 7; p4, las 4, 5, 6 y 7. Al recibir, se recalculan las tres paridades y se juntan como un número binario: ese número, el síndrome, es directamente la posición del bit errado, o 0 si no hay error. Basta invertir ese bit.

Por ejemplo, los datos 1011 se codifican como 0110011 (p1 = 0, p2 = 1, d1 = 1, p4 = 0, d2 = 0, d3 = 1, d4 = 1). Si el ruido invierte la posición 5, llega 0110111: fallan p1 y p4, no p2, el síndrome es 101 = 5, se invierte el bit 5 y quedan los datos 1011. La herramienta 3 lo hace con cualquier dato y cualquier daño. Las propiedades del código:

  • Distancia de Hamming 3: corrige cualquier error de un bit y detecta (sin poder corregir bien) uno de dos.
  • Redundancia de 3 bits cada 4: el 75 % de más. Los códigos de bloque más largos son más eficientes: (15,11) agrega 4 bits cada 11 (36 %), (31,26) agrega 5 cada 26 (19 %). Pero cada bloque corrige solo un error, y un bloque más largo acumula más probabilidad de tener dos.
  • Con dos errores en un bloque, el síndrome apunta a una posición que no es ninguna de las dos, y el «arreglo» agrega un tercer error: el dato queda mal sin que nadie lo note. Para evitarlo se agrega un bit de paridad global, el Hamming extendido (8,4): con dos errores, el síndrome indica un error pero la paridad global dice que hubo un número par, y el receptor sabe que no puede corregir (corrige uno, detecta dos: «SECDED»). Las memorias con ECC usan este esquema.
  • No cubre las ráfagas: una interferencia que daña varios bits seguidos afecta a más de un bit por bloque. La solución es el entrelazado: se mezclan los bits de varios bloques antes de transmitirlos, de modo que una ráfaga reparte un bit en cada bloque, y cada uno corrige el suyo.

Los códigos prácticos de radio y almacenamiento son más potentes: Reed-Solomon (corrige bytes enteros, usado en CD, códigos QR y comunicaciones espaciales), los convolucionales y los LDPC (en Wi-Fi y 5G). LoRa agrega, en cada paquete, una tasa de codificación de 4/5 a 4/8, basada en una idea parecida a Hamming (tema 39). Todos hacen lo mismo que el Hamming, con otra matemática: redundancia ordenada para localizar el error.

08 · Una decisión de diseño

Reenviar o corregir

ARQ frente a corrección hacia adelante.
CriterioARQ (confirmar y reenviar)FEC (corregir)
Necesita canal de retornoSíNo
Costo sin erroresCasi nulo (una confirmación pequeña)Fijo y siempre: la redundancia se paga haya o no haya errores
Costo con erroresUna retransmisión y una espera por cada pérdidaCasi nulo si el daño es corregible
LatenciaVariable: crece con cada reintentoConstante y predecible
Si el daño supera lo corregibleSe detecta y se reenvía: siempre se llegaSe pierde, o peor, se corrige mal
Funciona con difusiónMal: cada receptor tendría que confirmarSí, cada receptor se repara solo
EjemplosModbus, TCP, MQTT QoS 1 y 2, la mayoría de los buses de campoCD y QR, comunicaciones espaciales, televisión digital, memorias ECC

Casi todo sistema serio los combina. Un enlace de radio usa un código corrector para reparar los errores más frecuentes (uno o dos bits por bloque), un CRC para detectar los que el código no pudo reparar y reintentos para recuperar lo perdido: el esquema se llama ARQ híbrido (HARQ). El corrector baja la tasa de pérdida de tramas de 10 % a 1 %; el reintento maneja el 1 % restante. Para elegir: si hay canal de retorno y los errores son raros, se usa ARQ, que cuesta poco. Si la latencia de ida y vuelta es grande o no hay retorno, FEC. Si los errores son frecuentes pero acotados, la combinación. Y en un bus industrial con RS-485 y Modbus, lo normal es CRC más sondeo con reintentos: los errores son raros y el costo de la espera es chico.

09 · Con el microcontrolador

Un enlace con confirmaciones y reintentos

El programa siguiente implementa «parar y esperar» entre dos placas, con el paquete del tema 42 y el CRC-16 del tema 44. El emisor manda un dato cada 500 ms con un número de secuencia y espera el ACK; si vence el temporizador, reintenta con espera creciente y un poco de azar, hasta MAX_REINTENTOS, y después se rinde. El receptor confirma todas las tramas, incluso las copias, y entrega a la aplicación solo las nuevas. Para probar sin un cable ruidoso, cada nodo descarta al recibir un porcentaje de las tramas válidas (PERDIDA_PCT), lo que simula pérdidas tanto de datos como de confirmaciones. Se carga en dos placas con TX y RX cruzados (o un bus RS-485), cambiando SOY_EMISOR.

// ---------- Configuración ----------
const bool     SOY_EMISOR = true;                              // true en la placa que envía; false en la que recibe
const uint8_t  MI_DIR = SOY_EMISOR ? 1 : 2;
const uint8_t  OTRA_DIR = SOY_EMISOR ? 2 : 1;
const uint8_t  PERDIDA_PCT = 20;                               // % de tramas válidas que este nodo "pierde" al recibir
const uint8_t  MAX_REINTENTOS = 5;
const uint32_t ESPERA_BASE_MS = 60;                            // unas 2,5 veces el viaje de ida y vuelta a 9600 baudios
const uint32_t PERIODO_MS = 500;                               // un mensaje nuevo cada 500 ms
const uint32_t INFORME_MS = 10000;

const uint8_t  SOF = 0xA5;
const uint8_t  MAX_LEN = 35;
const uint8_t  TIPO_DATO = 0x40, TIPO_ACK = 0x41;
const uint32_t TIEMPO_BYTE_MS = 10;

// ---------- CRC-16/Modbus (tema 44) ----------
uint16_t crc16(const uint8_t *d, size_t n) {
    uint16_t crc = 0xFFFF;
    while (n--) {
        crc ^= *d++;
        for (uint8_t i = 0; i < 8; i++) crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
    }
    return crc;
}

void enviar(uint8_t dst, uint8_t tipo, const uint8_t *datos, uint8_t n) {
    uint8_t p[MAX_LEN + 4];
    p[0] = SOF;
    p[1] = n + 3;
    p[2] = dst;
    p[3] = MI_DIR;
    p[4] = tipo;
    memcpy(p + 5, datos, n);
    uint16_t c = crc16(p + 1, n + 4);
    p[5 + n] = c & 0xFF;
    p[6 + n] = c >> 8;
    Serial1.write(p, n + 7);
    Serial1.flush();
}

// ---------- Emisor: parar y esperar ----------
uint8_t  seq = 0, intento = 0;
bool     esperando = false;
uint16_t valor = 0;
unsigned long venceEn = 0, enviadoEn = 0, ultimoMensaje = 0;
uint32_t confirmados = 0, reintentos = 0, rendidos = 0, sumaViaje = 0;

uint32_t espera(uint8_t n) {                                   // 60, 120, 240, 480, 960 ms... con tope y un poco de azar
    return (ESPERA_BASE_MS << (n < 4 ? n : 4)) + random(ESPERA_BASE_MS / 4);
}

void mandarDato() {
    uint8_t cuerpo[3] = { seq, (uint8_t)(valor & 0xFF), (uint8_t)(valor >> 8) };
    enviar(OTRA_DIR, TIPO_DATO, cuerpo, sizeof(cuerpo));
    venceEn = millis() + espera(intento);
}

// ---------- Receptor: confirmar todo, entregar lo nuevo ----------
uint8_t  ultimaSeq = 0;
bool     hayUltima = false;
uint32_t nuevos = 0, duplicados = 0, descartadosSim = 0;

void alPaquete(uint8_t dst, uint8_t src, uint8_t tipo, const uint8_t *d, uint8_t n) {
    if (dst != MI_DIR) return;
    if (random(100) < PERDIDA_PCT) { descartadosSim++; return; }   // el canal "perdió" esta trama
    if (!SOY_EMISOR && tipo == TIPO_DATO && n >= 3) {
        uint8_t s = d[0];
        if (!hayUltima || s != ultimaSeq) {                    // número nuevo: se entrega a la aplicación
            nuevos++;
            ultimaSeq = s;
            hayUltima = true;
            Serial.printf("mensaje %u: valor %u\n", s, d[1] | (d[2] << 8));
        } else duplicados++;                                   // misma secuencia: es una copia, no se entrega otra vez
        enviar(src, TIPO_ACK, &s, 1);                          // pero se confirma igual: la confirmación anterior pudo perderse
    } else if (SOY_EMISOR && tipo == TIPO_ACK && esperando && n >= 1 && d[0] == seq) {
        esperando = false;                                     // la confirmación es del mensaje en curso
        confirmados++;
        sumaViaje += millis() - enviadoEn;
        seq++;
    }                                                          // una confirmación con otro número es vieja: se ignora
}

// ---------- Receptor de tramas (el del tema 42, con CRC-16) ----------
class Decodificador {
  public:
    typedef void (*AlRecibir)(uint8_t dst, uint8_t src, uint8_t tipo, const uint8_t *datos, uint8_t n);
    uint32_t errores = 0;
    Decodificador(AlRecibir cb) : alPaq(cb) {}
    void alimentar(uint8_t b) {
        ultimoByte = millis();
        switch (estado) {
            case ESPERA_SOF: if (b == SOF) estado = ESPERA_LEN; break;
            case ESPERA_LEN:
                if (b >= 3 && b <= MAX_LEN) { buf[0] = b; n = 1; faltan = b + 2; estado = CUERPO; }   // el largo, más 2 bytes de CRC
                else { errores++; estado = ESPERA_SOF; alimentar(b); }
                break;
            case CUERPO:
                buf[n++] = b;
                if (--faltan == 0) {
                    estado = ESPERA_SOF;
                    if (crc16(buf, n) == 0) alPaq(buf[1], buf[2], buf[3], buf + 4, n - 6);
                    else {
                        errores++;
                        uint8_t copia[MAX_LEN + 3];
                        uint8_t m = n;
                        memcpy(copia, buf, m);
                        n = 0;
                        for (uint8_t i = 0; i < m; i++) alimentar(copia[i]);
                    }
                }
                break;
        }
    }
    void vigilarTiempo() {
        if (estado != ESPERA_SOF && millis() - ultimoByte > TIEMPO_BYTE_MS) { errores++; estado = ESPERA_SOF; n = 0; }
    }
  private:
    enum Estado { ESPERA_SOF, ESPERA_LEN, CUERPO } estado = ESPERA_SOF;
    AlRecibir alPaq;
    uint8_t buf[MAX_LEN + 3];
    uint8_t n = 0, faltan = 0;
    unsigned long ultimoByte = 0;
};

Decodificador receptor(alPaquete);
unsigned long ultimoInforme = 0;

void setup() {
    Serial.begin(115200);
    Serial1.begin(9600, SERIAL_8N1, 16, 17);
}

void loop() {
    while (Serial1.available()) receptor.alimentar(Serial1.read());
    receptor.vigilarTiempo();

    if (SOY_EMISOR) {
        if (!esperando && millis() - ultimoMensaje >= PERIODO_MS) {          // mensaje nuevo
            ultimoMensaje = millis();
            valor = random(1000, 3000);
            intento = 0;
            enviadoEn = millis();
            mandarDato();
            esperando = true;
        } else if (esperando && (long)(millis() - venceEn) >= 0) {       // venció el temporizador
            if (intento < MAX_REINTENTOS) { intento++; reintentos++; mandarDato(); }
            else { esperando = false; rendidos++; Serial.printf("el mensaje %u no se pudo confirmar\n", seq); seq++; }
        }
    }

    if (millis() - ultimoInforme >= INFORME_MS) {
        ultimoInforme = millis();
        if (SOY_EMISOR) Serial.printf("confirmados %lu · reintentos %lu · rendidos %lu · viaje medio %.1f ms\n", confirmados, reintentos, rendidos, confirmados ? (double)sumaViaje / confirmados : 0.0);
        else Serial.printf("nuevos %lu · duplicados %lu · descartados por la simulación %lu · errores de trama %lu\n", nuevos, duplicados, descartadosSim, receptor.errores);
    }
}

Qué hay que notar:

  • El ACK se manda siempre, también ante una copia (enviar(src, TIPO_ACK, &s, 1) está fuera del if). Es el punto que más se olvida: si se descarta la copia sin confirmar, el emisor, que no recibió la confirmación anterior, reintenta hasta rendirse, aunque el receptor tenga el dato.
  • La entrega depende del número, no del tipo de trama: s != ultimaSeq distingue lo nuevo de la copia. Con 0 % de pérdida, duplicados queda en 0; con 20 % aparecen cuando se pierde un ACK. Si se quita la comparación, nuevos cuenta los duplicados: es el experimento de la herramienta 4.
  • El emisor ignora los ACK de otro número (d[0] == seq): una confirmación tardía de un intento anterior no cierra el mensaje en curso.
  • La espera crece y tiene azar: ESPERA_BASE_MS << n con tope en 16 veces, más hasta un cuarto de la base. La comparación (long)(millis() − venceEn) >= 0 funciona aun cuando millis() da la vuelta a los 49 días.
  • La pérdida simulada afecta ambos sentidos: cada nodo descarta el 20 % de lo que recibe. La probabilidad de que un intento salga bien es 0,8 × 0,8 = 64 %; en promedio, 1,56 intentos por mensaje, y con 5 reintentos el emisor se rinde en el 0,2 % (0,366). Los números del monitor serie se acercan a eso con unos cientos de mensajes. Con PERDIDA_PCT = 0 el viaje medio es de unos 25 ms.
  • El emisor bloquea solo dentro del mensaje en curso: mientras espera, el loop sigue leyendo el puerto, y el programa puede hacer otras tareas. No usa delay.
  • Lo que no hace: una ventana (manda de a uno), recuperación ante reinicios (si el emisor reinicia en 0 y el receptor recuerda el 7, descartará hasta que el número sea distinto) ni mensajes con orden estricto entre varios emisores. Es el esquema mínimo, y alcanza para un sondeo con sensores.
  • En un Arduino sin printf en Serial, se reemplaza por Serial.print; sin segundo puerto serie, se usa SoftwareSerial.

10 · Analogía con programación

El enlace confiable en Python

El código de Hamming, las probabilidades de entrega, la simulación de reintentos con y sin números de secuencia, la eficiencia de la ventana y la espera creciente se prueban sin hardware. El simulador usa el mismo generador de números aleatorios que la página (mulberry32) y el mismo orden de sorteos, por eso sus cantidades coinciden con las de la herramienta 4.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

MASCARA = 0xFFFFFFFF


def mulberry32(semilla):
    a = semilla & MASCARA
    def siguiente():
        nonlocal a
        a = (a + 0x6D2B79F5) & MASCARA
        t = ((a ^ (a >> 15)) * (a | 1)) & MASCARA
        t ^= (t + (((t ^ (t >> 7)) * (t | 61)) & MASCARA)) & MASCARA
        return ((t ^ (t >> 14)) & MASCARA) / 4294967296
    return siguiente


def hamming_codificar(d):
    """Hamming (7,4): posiciones 1 a 7 = p1 p2 d1 p4 d2 d3 d4."""
    d1, d2, d3, d4 = d
    return [d1 ^ d2 ^ d4, d1 ^ d3 ^ d4, d1, d2 ^ d3 ^ d4, d2, d3, d4]


def hamming_decodificar(r):
    sindrome = 0
    for pos, bit in enumerate(r, start=1):
        if bit:
            sindrome ^= pos                                   # el XOR de las posiciones con un 1 es la posición del error
    r = r[:]
    if sindrome:
        r[sindrome - 1] ^= 1
    return sindrome, [r[2], r[4], r[5], r[6]]


def prob_entrega(pf, pa, reintentos, espera, viaje):
    """Probabilidades de un mensaje con reintentos; pf y pa son las pérdidas de trama y de confirmación."""
    total = reintentos + 1
    ps = (1 - pf) * (1 - pa)                                  # un intento sale bien si llegan la trama y su confirmación
    fallo = (1 - ps) ** total
    intentos = (1 - fallo) / ps
    duplicados = (1 - pf) * intentos - (1 - pf ** total)      # copias que le llegan al receptor, menos la primera
    tiempo = (intentos - (1 - fallo)) * espera + (1 - fallo) * viaje
    return ps, intentos, fallo, duplicados, tiempo


def simular_arq(pf, pa, reintentos, n, con_seq, semilla):
    azar = mulberry32(semilla)
    entregas = recibidos = fallidos = sin_conf = intentos = 0
    for _ in range(n):
        copias, confirmado = 0, False
        for _ in range(reintentos + 1):
            if confirmado:
                break
            intentos += 1
            if azar() < pf:
                continue                                      # la trama se pierde
            copias += 1
            if azar() >= pa:
                confirmado = True                             # la confirmación llega
        recibidos += copias > 0
        entregas += min(copias, 1) if con_seq else copias    # con número de secuencia se descartan las copias
        if not confirmado:
            fallidos += 1
            sin_conf += copias > 0
    return entregas, recibidos, fallidos, sin_conf, intentos / n


def eficiencia_ventana(bytes_trama, baud, espera_ms, ventana):
    tf = bytes_trama * 10 / baud * 1000
    return min(1.0, ventana * tf / (tf + espera_ms))


def esperas(base, reintentos, tope=4):
    return [base * 2 ** min(i, tope) for i in range(reintentos + 1)]


datos = [1, 0, 1, 1]
codigo = hamming_codificar(datos)
print("Hamming de 1011:", "".join(map(str, codigo)))
for pos in range(1, 8):
    r = codigo[:]
    r[pos - 1] ^= 1
    sindrome, recuperado = hamming_decodificar(r)
    print(f"bit {pos} invertido: síndrome {sindrome}, datos {''.join(map(str, recuperado))}")
r = codigo[:]
r[1] ^= 1
r[4] ^= 1
sindrome, recuperado = hamming_decodificar(r)
print(f"bits 2 y 5 invertidos: síndrome {sindrome}, datos {''.join(map(str, recuperado))} (corrige mal)")

ps, intentos, fallo, dup, tiempo = prob_entrega(0.10, 0.10, 5, 60, 24)
print(f"10 % y 10 %, 5 reintentos: éxito por intento {ps:.2f}, intentos {intentos:.4f}, fallo {fallo:.2e}, duplicados {dup:.3f}, tiempo {tiempo:.1f} ms")

for con_seq in (False, True):
    e, r, f, u, i = simular_arq(0.2, 0.2, 3, 10000, con_seq, 1)
    print(f"{'con' if con_seq else 'sin'} números de secuencia: {e} entregas de {r} mensajes distintos ({e - r} repetidas), {f} sin confirmar ({u} sí habían llegado), {i:.3f} intentos por mensaje")

for w in (1, 2, 3, 4):
    print(f"ventana {w}: {eficiencia_ventana(10, 9600, 14, w):.3f} del enlace")
tiempos = esperas(60, 5)
print("esperas:", tiempos, "hasta rendirse:", sum(tiempos), "ms")

Resultado: los datos 1011 se codifican como 0110011, y al invertir cada una de las siete posiciones el síndrome es esa posición (1, 2, 3, 4, 5, 6, 7) y se recuperan siempre los datos 1011. Con dos bits invertidos (2 y 5), el síndrome es 7 y el «arreglo» entrega 1110: corrige mal. Con 10 % de pérdida en cada sentido y 5 reintentos, el éxito por intento es 0,81, con 1,2345 intentos por mensaje, un fallo de 4,70 × 10−5, 0,111 duplicados por mensaje y 38,1 ms de tiempo medio. En la simulación de 10 000 mensajes con 20 % y 20 % y hasta 3 reintentos: sin números de secuencia, 12 343 entregas de 9986 mensajes distintos (2357 repetidas), 166 sin confirmar (152 de ellos sí habían llegado) y 1,545 intentos por mensaje; con números, 9986 entregas, ninguna repetida, y los mismos 166 sin confirmar. La ventana de 1, 2, 3 y 4 tramas ocupa el 0,427, 0,853, 1,000 y 1,000 del enlace. Las esperas son 60, 120, 240, 480, 960 y 960 ms: 2820 ms hasta rendirse.

Dos lecciones del modelo. Primero, el efecto del número de secuencia no es sutil: el 24 % de los mensajes se duplican sin él, con las mismas pérdidas, y las mismas 166 fallas, que ningún reintento arregla, se ven igual con y sin él. Segundo, el código de Hamming cabe en diez líneas, y su mecanismo (el XOR de las posiciones con un uno es la posición del error) es la clase de idea que justifica estudiar la redundancia ordenada: no solo detecta, apunta.

11 · Exploración interactiva

Laboratorio: tiempos, entrega, Hamming, reintentos y ventana

El laboratorio tiene cinco herramientas. La primera calcula los tiempos de un intercambio con confirmación y evalúa el tiempo de espera. La segunda calcula las probabilidades de entrega con tramas y confirmaciones perdidas. La tercera codifica y decodifica con el código de Hamming. La cuarta simula miles de mensajes con reintentos, con y sin números de secuencia. La quinta calcula la eficiencia de una ventana deslizante.

1. Tiempos de un intercambio con confirmación

Trama—
Confirmación—
Viaje de ida y vuelta—
Ocupación del enlace—
Caudal—
Veredicto de la espera—

Elegí el enlace y la espera.

2. Probabilidad de entrega con pérdidas

Intento exitoso—
Intentos por mensaje—
Se agotan los reintentos—
Copias de más—
Tiempo medio—
Veredicto—

Elegí las pérdidas y los reintentos.

3. Código de Hamming

Palabra de código—
Recibida—
Síndrome—
Diagnóstico—
Datos recuperados—
Veredicto—

Elegí cuatro bits y el daño.

4. Simulación de un enlace con reintentos

Entregas a la aplicación—
Entregas repetidas—
Emisor sin confirmación—
De esos, llegaron igual—
Intentos por mensaje—
Lectura—

Elegí las pérdidas y los reintentos.

5. Ventana deslizante

Tiempos—
Parar y esperar—
Con ventana—
Ventana necesaria—
Números de secuencia—
Veredicto—

Elegí el enlace y la ventana.

En la herramienta 1, el viaje de ida y vuelta es el de la trama, el proceso del receptor y la confirmación, con 10 bits por byte (8N1). En la 2, un intento sale bien si llegan la trama y su confirmación; las «copias de más» son las que recibe el receptor además de la primera. En la 3, las posiciones van de 1 a 7 (o a 8 en el extendido, donde la 8 es la paridad global). En la 4, el azar usa el generador de la sección 10: cada intento sortea primero la pérdida de la trama y, si llegó, la de la confirmación. En la 5, la repetición selectiva descuenta del caudal la fracción perdida.

Recorrido guiado

  1. En la herramienta 1, con 9600 baudios, trama de 10 bytes, confirmación de 8, 5 ms de proceso y 60 ms de espera: trama de 10,4 ms, confirmación de 8,33 ms, viaje de ida y vuelta de 23,8 ms, ocupación del 43,9 % y 42 intercambios por segundo (126 bytes útiles por segundo con 3 de datos); la espera es 2,5 veces el viaje: razonable. Con 20 ms: menor que el viaje, en rojo. Con 600 ms: «espera muy larga».
  2. En la herramienta 2, con 10 % y 10 % y 5 reintentos: 81 % de éxito por intento, 1,235 intentos por mensaje, fallo de menos de 0,01 %, 0,111 copias de más y 38,1 ms de tiempo medio (24,0 ms si sale a la primera). Con 0 % y 0 %: 1 intento y 24 ms. Con 40 % y 40 %, 0 reintentos: se pierde casi el 64 %, en rojo.
  3. En la herramienta 3, con los datos 1011 y la posición 5: palabra 0 1 1 0 0 1 1, el bit 5 recibido invertido, síndrome 5 (101), «corrige el bit 5» y datos 1011 recuperados sin reenviar. Con la posición 2 5: síndrome 7 y datos 1110: «corrigió mal», en rojo. Con el código extendido y 2 5: «dos errores detectados: no corrige». Con la posición 8 en el extendido: corrige la paridad global. Con datos 0110 y la posición 4 (un bit de paridad): síndrome 4 y datos intactos.
  4. En la herramienta 4, con 20 %, 20 %, 3 reintentos, 10 000 mensajes, sin números de secuencia y semilla 1: 12 343 entregas de 9986 mensajes distintos, 2357 repetidas (en rojo), 166 sin confirmación (1,66 %), 152 de ellos llegaron igual y 1,545 intentos. Cambiando a sí: 9986 entregas, ninguna repetida. Con 0 reintentos: el 36,5 % sin confirmación y 2004 mensajes perdidos de verdad. Con 5 % y 5 % y 5 reintentos: todo entregado y confirmado, 1,107 intentos.
  5. En la herramienta 5, con 9600 baudios, 10 bytes, 14 ms de espera, ventana 4 y 5 % de pérdidas: tramas de 10,4 ms, parar y esperar usa el 42,7 % del enlace, con ventana 4 el 100 % (95 % útil con las pérdidas), hace falta una ventana de 3. Con ventana 1: 42,7 %, «la ventana es chica» (en rojo). Con 2: 85,3 %. Con 115 200 baudios, tramas de 64 bytes y 20 ms: ocupación de 21,7 % y ventana necesaria de 5, con 3 bits de secuencia para retroceder N y 4 para repetición selectiva.
Desafío: el sondeo de veinte nodos por radio con pérdidas

Un maestro sondea 20 sensores por una radio de 9600 baudios. La consulta mide 8 bytes y la respuesta 12; el sensor tarda 6 ms en responder. Se pierde el 15 % de las tramas en cada sentido. Se busca leer cada nodo en menos de 3 s con una probabilidad del 99,9 %. Con las herramientas y lo visto: (1) El intercambio (herramienta 1): 8,3 ms de consulta, 12,5 ms de respuesta y 6 ms de proceso: unos 27 ms. Sin pérdidas, 20 nodos son 0,54 s. (2) Con pérdidas (herramienta 2): un intento sale bien con 0,85 × 0,85 = 72 %, 1,38 intentos por nodo si se reintentara sin límite; pero cada intento perdido cuesta la espera, no el viaje. Con una espera de 70 ms (2,6 veces el viaje) y 3 reintentos (4 intentos), la probabilidad de fallar es 0,284 = 0,6 %: el 99,4 % no alcanza el 99,9 % que se busca. Con 5 reintentos: 0,286 = 0,05 %: el 99,95 % sí. (3) El tiempo: cada nodo cuesta en promedio los 27 ms del intento que sale bien más 0,38 intentos fallidos de 70 ms, unos 54 ms; 20 nodos son 1,1 s, y caben en los 3 s con margen. Un nodo mudo, en cambio, agota 70 + 140 + 280 + 560 + 1120 + 1120 ≈ 3,3 s con la espera creciente: por eso, al marcarlo como caído, se le da un solo intento en los ciclos siguientes. (4) Números de secuencia: la consulta es una lectura, que no cambia nada, por lo que un duplicado es inofensivo, pero la respuesta lleva el número de la consulta para que una respuesta tardía de un reintento anterior no se tome por la del nodo siguiente. (5) Un nodo caído se marca después de 5 reintentos, se lo salta unos ciclos y se reintenta más tarde, para no demorar al resto. (6) Una orden (por ejemplo, un cambio de umbral) sí necesita ser idempotente («umbral = 25», no «subir 1») para tolerar la repetición con «al menos una vez». Es el contenido de los temas 41 a 46 aplicado a un mismo problema.

12 · Comprensión

Confusiones frecuentes

«Si no recibo el ACK, el mensaje no llegó»

No se sabe. Puede haberse perdido la trama o la confirmación. En el segundo caso el receptor tiene el dato y el emisor lo reenvía. Por eso hacen falta los números de secuencia: para que el receptor reconozca la copia y no la procese dos veces.

«Si el receptor ve una copia, tiene que ignorarla del todo»

Hay que no entregarla a la aplicación, pero sí confirmarla. Si no la confirma, el emisor, que no recibió la confirmación anterior, sigue reintentando hasta rendirse, y cree que falló algo que ya se había logrado.

«Reintentar enseguida es lo más rápido, y entonces lo mejor»

Si la falla es una interferencia que dura un rato, el reintento inmediato falla igual y llena el enlace. La espera creciente, con un poco de azar, le da tiempo a pasar y evita que varios nodos choquen al reintentar al mismo tiempo.

«Cuanto más largo el tiempo de espera, más seguro»

Más largo evita reintentos falsos, pero paraliza el enlace en cada pérdida. Con 10 % de pérdidas y una espera de 20 veces el viaje, el enlace pierde la mayor parte de su tiempo esperando. La espera se elige por encima del viaje medido y se hace crecer solo en los reintentos.

«Con confirmaciones, el sistema nunca pierde un mensaje»

Si se agotan los reintentos, el mensaje se pierde, y el emisor tiene que decidir qué hacer. Además, el emisor puede creer que falló algo que sí llegó (en la simulación, 152 de 166). Un enlace confiable convierte los fallos en eventos visibles, no los elimina.

«Un ACK significa que la orden se ejecutó»

Casi siempre significa que la trama se recibió (a veces, que se aceptó). Si el resultado importa, la respuesta tiene que informarlo. Un ACK antes de mover una válvula no dice nada de la válvula.

«Con reintentos, mandar el mismo comando varias veces es inofensivo»

Solo si el comando es idempotente. «Encender» da lo mismo repetido; «invertir» o «sumar uno» no. Se diseñan los mensajes como estados deseados, o se agrega un número que el receptor recuerda.

«El código de Hamming siempre corrige»

Corrige un error por bloque. Con dos, corrige mal y el dato queda mal sin aviso (salvo la versión extendida, que lo detecta). Y no cubre ráfagas sin entrelazado. Casi siempre se combina con un CRC que valida el resultado.

«Corregir es mejor que reenviar»

Depende. La corrección cuesta redundancia en todas las tramas, haya errores o no. Si los errores son raros y hay canal de retorno, reenviar cuesta mucho menos. La corrección gana cuando no hay retorno, cuando la latencia de ida y vuelta es grande o cuando se transmite a muchos.

13 · Práctica guiada

Predecí antes de comprobar

1. Tiempos

A 9600 baudios, una trama de 12 bytes, una confirmación de 6 y 4 ms de proceso. ¿Cuánto dura el intercambio, cuánto del tiempo trabaja el enlace y qué espera pondrías?

Ver solución

Trama: 12 × 10 / 9600 = 12,5 ms. Confirmación: 6,25 ms. Intercambio: 12,5 + 4 + 6,25 = 22,75 ms. El enlace lleva la trama 12,5 / 22,75 = 54,9 % del tiempo. Una espera de 2 a 4 veces el viaje: 45 a 90 ms; por ejemplo, 60 ms.

2. Probabilidad de fallo

Se pierde el 20 % de las tramas y ninguna confirmación. Con 2 reintentos, ¿cuál es la probabilidad de que un mensaje no se entregue y cuántos intentos cuesta en promedio?

Ver solución

Hay 3 intentos y cada uno falla con 0,2: fallo = 0,23 = 0,8 %. Intentos esperados = (1 − 0,008) / 0,8 = 1,24. Sin confirmaciones perdidas no hay duplicados: cada trama recibida se confirma y termina.

3. Confirmaciones perdidas

Ninguna trama se pierde, pero el 30 % de las confirmaciones sí. Sin números de secuencia y sin límite de reintentos, ¿cuántas copias de más recibe el receptor por mensaje?

Ver solución

El emisor repite hasta recibir una confirmación: intentos esperados = 1 / 0,7 = 1,43, y cada intento llega al receptor. Copias de más: 1,43 − 1 = 0,43 por mensaje: el 43 % de los mensajes se entregan dos o más veces. Con números de secuencia, ninguno.

4. Espera creciente

Base de 50 ms, doble en cada reintento con tope de 16 veces la base, 5 reintentos. ¿Cuánto se espera en cada intento y cuánto hasta rendirse?

Ver solución

Esperas: 50, 100, 200, 400, 800 y 800 ms (el tope de 16 × 50 = 800 ms). Total: 2350 ms (sin el azar). Es el tiempo mínimo antes de declarar caído a un nodo; se tiene en cuenta para no bloquear el sondeo de los demás.

5. Ventana

A 115 200 baudios, tramas de 64 bytes y 20 ms hasta recibir la confirmación. ¿Qué parte del enlace usa «parar y esperar»? ¿Qué ventana hace falta y cuántos números de secuencia?

Ver solución

Trama: 640 / 115 200 = 5,56 ms; ciclo: 25,56 ms. Parar y esperar: 5,56 / 25,56 = 21,7 %. Ventana: 25,56 / 5,56 = 4,6, o sea 5. Retroceder N necesita W + 1 = 6 números (3 bits); repetición selectiva, 2W = 10 (4 bits).

6. Hamming a mano

Codificá los datos 0110 con Hamming (7,4). Si llega 1101110, ¿cuál es el síndrome y qué datos se recuperan?

Ver solución

d1 = 0, d2 = 1, d3 = 1, d4 = 0: p1 = d1 ⊕ d2 ⊕ d4 = 1, p2 = d1 ⊕ d3 ⊕ d4 = 1, p4 = d2 ⊕ d3 ⊕ d4 = 0. La palabra es 1100110. Llega 1101110: difiere en la posición 4. Los unos están en las posiciones 1, 2, 4, 5 y 6: 1 ⊕ 2 ⊕ 4 ⊕ 5 ⊕ 6 = 4. Se invierte el bit 4 (un bit de paridad) y los datos son 0110, intactos.

7. Dos errores

La misma palabra 1100110 llega con las posiciones 3 y 6 invertidas. ¿Qué hace el decodificador (7,4)? ¿Y el extendido (8,4)?

Ver solución

Llega 1110100; las posiciones con un uno son 1, 2, 3 y 5: 1 ⊕ 2 ⊕ 3 ⊕ 5 = 5. El (7,4) «corrige» la posición 5, que estaba bien, y entrega 1000 en lugar de 0110: tres bits mal, sin aviso. El (8,4) ve síndrome 5 con paridad global par: concluye que hubo dos errores, no corrige y avisa, y el emisor puede reenviar.

8. Reenviar o corregir

Un sensor a pilas transmite una vez por minuto por radio sin recibir nada (ahorra energía). La pérdida es del 5 % de los paquetes. ¿Qué estrategia usarías?

Ver solución

No hay canal de retorno: no se pueden usar confirmaciones. Opciones: repetir el envío dos o tres veces con pausas al azar (con 5 % de pérdida, la probabilidad de perder las tres copias es 0,053 ≈ 0,0125 %), con número de secuencia para que el receptor descarte copias; o agregar corrección (un código tipo Hamming con entrelazado) si los errores son de pocos bits; o ambas. Como el dato se actualiza cada minuto, perder uno no es grave, y la repetición es la más simple.

14 · Síntesis

Ideas para recordar

  • Detectar no basta: el mensaje se perdió. ARQ lo recupera: el receptor confirma lo que recibe bien, el emisor espera y reenvía si no llega la confirmación. Un canal que pierde mucho se vuelve confiable a costa de tiempo en los intentos perdidos.
  • El ACK es la base; el NACK acelera la recuperación pero solo cubre lo que el receptor pudo notar (CRC incorrecto, hueco de secuencia); el silencio implícito (la respuesta es la confirmación) es el de Modbus. Un ACK suele confirmar la recepción, no la ejecución. No se confirman las difusiones.
  • El tiempo de espera supera el viaje de ida y vuelta (trama + proceso + confirmación), unas 2 a 4 veces. Si es corto, hay reintentos falsos; si es largo, cada pérdida paraliza el enlace. Los reintentos tienen un límite, una espera creciente y un poco de azar. Un intento sale bien con probabilidad (1 − ptrama)(1 − pconf).
  • Si se pierde la confirmación, el receptor recibe una copia. El número de secuencia permite reconocerla: no se entrega de nuevo, pero se confirma igual. Sin él, con 20 % de pérdida en cada sentido, uno de cada cuatro mensajes se duplica. Aun así, queda un límite: tras agotar los reintentos, el emisor no sabe si el mensaje llegó.
  • «Parar y esperar» deja el enlace ocioso durante la espera. Una ventana deslizante de W tramas lo llena cuando W · Ttrama alcanza el ciclo. Retroceder N es simple y reenvía de más (W + 1 números); la repetición selectiva reenvía solo lo perdido y pide búfer (2W números). La ventana también controla el flujo.
  • Las garantías de entrega son a lo sumo una vez, al menos una vez y exactamente una vez. Una alternativa barata a la última es el diseño idempotente: mensajes de estado deseado («relé = ON»), no de cambio («invertir»), que se pueden repetir sin efecto. Y la aplicación debe decidir qué hacer cuando se agotan los reintentos.
  • La corrección hacia adelante repara sin reenviar, con redundancia en cada trama. El código de Hamming (7,4) agrega 3 bits cada 4: el síndrome, el XOR de las posiciones con un uno, es la posición del error. Corrige uno, y con dos corrige mal salvo en la versión extendida (8,4), que lo detecta. Contra ráfagas, entrelazado.
  • ARQ cuesta poco sin errores y necesita retorno; FEC cuesta siempre y sirve sin retorno, con latencia fija y para difusión. Lo habitual es combinar: corrección para lo frecuente, CRC para validar, reintentos para el resto. Es lo que hace confiable un enlace imperfecto, sobre el cable bien hecho del tema 43.

¿Listo para el siguiente tema?

Antes de avanzar, calculá el tiempo de un intercambio a 19 200 baudios con una trama de 10 bytes, una confirmación de 8 y 4 ms de proceso, y elegí una espera; explicá por qué un receptor debe confirmar también una copia que descarta; y decí en qué casos reenviarías y en cuáles agregarías corrección. Si podés justificar las tres respuestas, entendés cómo se hace confiable un enlace.

En el próximo tema, formatos de datos, se estudia qué va dentro de la carga útil: binario o texto, el orden de los bytes (little-endian y big-endian), estructuras, JSON y CBOR, y cómo hacer que dos equipos distintos se entiendan sin ambigüedad ni problemas de compatibilidad. El enlace ya entrega los mensajes íntegros y una sola vez; falta acordar qué significan.