01 · Objetivo
Qué vamos a construir
Hasta ahora estudiamos cada pieza de la UART por separado: la trama, la velocidad, los buffers, los protocolos de texto. En este taller las juntamos. Vamos a conectar dos placas y hacer que conversen:
- La placa A (controlador, la que manda) pregunta cada segundo la temperatura de la placa B (nodo remoto, que tiene un sensor LM35 y un LED).
- Según el valor recibido, A le ordena a B que encienda o apague el LED (por ejemplo, encender si hace más de 28 °C).
- Si B no responde, A lo detecta, reintenta y avisa del fallo sin quedarse bloqueada.
Es un diálogo maestro–esclavo, como los que vimos en el tema 6, montado sobre un enlace punto a punto full duplex. La tarea parece sencilla, y justamente por eso es la mejor para descubrir los detalles que hacen que un enlace funcione de verdad: el tiempo de espera, qué hacer con una respuesta que no llega y por qué un comando puede ejecutarse dos veces.
02 · Materiales y cableado
Materiales y cableado
Hace falta lo siguiente (todo el código funciona con cualquier combinación de placas; los cambios se indican):
- Dos placas con UART: por ejemplo un Arduino Mega o un ESP32 (que tienen
Serial1) y otra placa cualquiera. Con dos Arduino Uno se usaSoftwareSerialpara el enlace, a 9600 baudios. - Un LM35 (o un potenciómetro como sensor simulado) y un LED con su resistencia en la placa B.
- Cables, y si una placa funciona a 5 V y la otra a 3,3 V, dos resistencias de 1 kΩ y 2 kΩ para el divisor del tema 5.
Dos detalles del cableado. Primero, la masa va conectada siempre: sin ella los niveles no tienen referencia. Segundo, la línea de B hacia A queda con poco margen (3,3 V contra un umbral de 3,0 V, como vimos en el tema 5): funciona, pero si ves errores, un traductor de nivel es la solución limpia. Con las dos placas a la misma tensión se omite el divisor.
Antes de escribir el programa, probá el cableado: con el programa de eco del tema 7 en cada placa, escribí un carácter desde el monitor serie de una y verificá que llega a la otra, en ambos sentidos. Es la prueba más barata y evita buscar errores de software que no existen.
03 · Reglas del diálogo
Diseño del protocolo
Siguiendo lo aprendido en el tema 10, el protocolo es de texto, una línea por mensaje, y se puede probar a mano con un terminal. Las reglas:
- Cada mensaje es una línea ASCII que termina en
\n. El receptor ignora el\r, y las líneas vacías. - Largo máximo de 31 caracteres. Una línea más larga se rechaza.
- Los comandos van en mayúsculas, con sus argumentos separados por espacios.
- Solo A inicia la conversación. B contesta una línea por cada línea que recibe, y nunca habla por su cuenta.
- La respuesta empieza con
OK(y un valor opcional) o conERR(y un código numérico).
| Comando | Respuesta correcta | Qué hace |
|---|---|---|
PING | OK PONG | Comprueba que el nodo está vivo |
GET T | OK 24.0 | Lee la temperatura (°C, un decimal) |
GET LED | OK 1 o OK 0 | Informa el estado del LED |
SET LED 1 / SET LED 0 | OK | Enciende o apaga el LED |
| Respuesta | Significado | Ejemplo que lo provoca |
|---|---|---|
ERR 1 | Comando desconocido | HOLA, get t (minúsculas) |
ERR 2 | Argumento inválido | SET LED 7 |
ERR 3 | Línea demasiado larga | Más de 31 caracteres |
Un diálogo típico, con los dos sentidos de la conversación:
| A → B | B → A | Comentario |
|---|---|---|
PING | OK PONG | El enlace funciona |
GET T | OK 29.3 | Hace calor: A decide encender el LED |
SET LED 1 | OK | Orden ejecutada |
SET LED 7 | ERR 2 | Argumento inválido: B responde, no se reintenta |
Hay una decisión de diseño importante: los errores se responden, no se callan. Si B ignorara un comando inválido, A no podría distinguir «el comando está mal» de «el mensaje se perdió», y reintentaría en vano. Con una respuesta ERR, A sabe que el enlace funciona y que el problema está en el comando.
04 · Cálculo
El tiempo de espera
Cuando A envía un comando, tiene que esperar la respuesta durante un tiempo máximo, el timeout. Si es demasiado corto, A se rinde antes de que la respuesta pueda llegar; si es demasiado largo, un mensaje perdido deja al programa esperando de más. Se calcula sumando todo lo que ocurre entre el envío y la llegada de la respuesta:
Con 9600 baudios en 8N1 (1,04 ms por byte), y suponiendo 5 ms de proceso en B (leer el sensor, formatear la respuesta):
| Comando | Petición | Respuesta | Ida y vuelta | Timeout con 50 % de margen |
|---|---|---|---|---|
GET T | GET T\n: 6 bytes = 6,25 ms | OK 24.0\n: 8 bytes = 8,33 ms | 19,6 ms | ≈ 29 ms → 30 ms |
SET LED 1 | 10 bytes = 10,4 ms | OK\n: 3 bytes = 3,13 ms | 18,5 ms (con 5 ms de proceso) | ≈ 28 ms → 30 ms |
En la práctica se redondea hacia arriba, hacia un valor holgado como 50 ms. El criterio importante: nunca menos que el tiempo de ida y vuelta (con un timeout de 10 ms, A reintentaría siempre y sin motivo, aunque el enlace fuera perfecto), y sin exagerar, porque el peor caso de una consulta sin respuesta es (reintentos + 1) × timeout.
Dos factores que alargan la respuesta sin que se note: el procesamiento de B puede ser mayor de lo previsto (una lectura analógica promediada, una escritura en memoria, una interrupción larga) y la velocidad de la línea baja con facilidad el rendimiento (a 115 200 baudios los tiempos de línea se dividen por 12). Conviene medir el tiempo de ida y vuelta real con el analizador y dejar un margen sobre el máximo observado.
05 · Cuando algo se pierde
Reintentos y comandos idempotentes
En un cable corto los mensajes casi nunca se pierden, pero en un entorno con ruido, un contacto flojo o un receptor ocupado sí. La respuesta estándar es reintentar: si no llega nada antes del timeout, A reenvía el comando. La probabilidad de éxito mejora mucho. Si cada mensaje (petición o respuesta) se pierde con probabilidad p, una transacción necesita que lleguen los dos:
| Pérdida por mensaje | 1 intento | 3 intentos (2 reintentos) | 6 intentos (5 reintentos) |
|---|---|---|---|
| 1 % | 98,0 % | ≈ 100 % | ≈ 100 % |
| 5 % | 90,3 % | 99,9 % | ≈ 100 % |
| 20 % | 64,0 % | 95,3 % | 99,8 % |
| 40 % | 36,0 % | 73,8 % | 93,1 % |
Pero los reintentos esconden una trampa, y es la más importante del taller. Cuando A no recibe respuesta, no sabe si se perdió la petición (el comando no se ejecutó) o la respuesta (el comando sí se ejecutó). Si reenvía, el comando puede ejecutarse dos veces.
Comandos idempotentes
Repetirlos no cambia el resultado: SET LED 1, GET T, GET LED. Si se ejecutan dos veces, el LED sigue encendido. Son seguros de reintentar.
Comandos no idempotentes
Cada ejecución cambia el estado: TOGGLE (invertir el LED), INC CONTADOR, «avanzar un paso». Si el comando se ejecutó y se perdió la respuesta, el reintento lo ejecuta de nuevo y el estado queda al revés de lo que A cree.
La solución más simple es diseñar los comandos como idempotentes: «poné el LED en 1» en lugar de «invertí el LED»; «fijá la posición en 90°» en lugar de «avanzá 10°». Cuando no se puede, se agrega un número de secuencia a cada mensaje: B recuerda el último número procesado y su respuesta, y si llega un reintento con el mismo número, reenvía la respuesta guardada sin ejecutar el comando de nuevo. (Es lo que haremos en la unidad de protocolos propios, tema 45.)
Y una regla sobre qué reintentar: se reintenta solo ante falta de respuesta. Una respuesta ERR es una respuesta: el enlace funciona y el comando está mal, así que reenviar lo mismo daría el mismo error.
06 · Sin quedarse esperando
El maestro como máquina de estados
La función de espera del código completo es bloqueante: A se queda dentro de pedir() hasta que llega la respuesta o vence el tiempo. Para el taller es lo más claro, pero un programa real que además lee otros sensores o actualiza una pantalla no puede detenerse hasta 150 ms en cada consulta. La alternativa es una pequeña máquina de estados que avanza un paso en cada vuelta de loop():
En cada vuelta, el estado ESPERANDO hace dos preguntas: ¿llegó una línea completa? (si sí, procesar y pasar a LIBRE) y ¿venció el tiempo? (si sí, reenviar mientras queden intentos, o pasar a FALLO). Nada espera: el resto del programa sigue corriendo entre una pregunta y otra. El esqueleto de código está en la sección siguiente.
07 · El otro lado
El nodo remoto
El programa de B es más simple, porque su trabajo es reactivo: leer líneas, interpretarlas y contestar. Sus reglas de diseño:
- Un lector de líneas sin bloqueo (el del tema 9), con un arreglo de tamaño fijo y una bandera para las líneas demasiado largas.
- Una respuesta por cada línea, inmediata y siempre del tipo
OKoERR. - Nunca hablar por iniciativa propia. Si más adelante B necesita avisar de un evento, se agrega un mensaje con otro prefijo (por ejemplo
EVT) para que A lo distinga de una respuesta. - Validar todo. Cada argumento se comprueba antes de actuar sobre el hardware:
SET LED 7no debe encender nada. - Trabajar rápido. Cuanto menos tarde B en responder, más corto puede ser el timeout de A. Una lectura del sensor es rápida; una espera de 100 ms con
delayno lo es.
08 · Con el microcontrolador
El código completo
Primero el nodo B. Para dejar el puerto principal libre para depuración, el enlace se llama ENLACE: con Serial1 en una placa que lo tenga, o con SoftwareSerial en un Uno (se indica en el comentario).
// ===== PLACA B (nodo remoto): LM35 en A0 y LED en el pin 13 =====
#define ENLACE Serial1 // en un Uno: SoftwareSerial enlace(2, 3); #define ENLACE enlace
const byte PIN_LED = 13;
const byte PIN_SENSOR = A0;
const byte MAX_LINEA = 32;
char linea[MAX_LINEA];
byte largo = 0;
bool desbordada = false; // la línea superó el largo máximo
bool ledEncendido = false;
bool leerLinea() {
while (ENLACE.available() > 0) {
char c = ENLACE.read();
if (c == '\r') continue;
if (c == '\n') {
if (largo == 0 && !desbordada) continue; // línea vacía (el LF de un CR LF): se ignora
linea[largo] = '\0';
largo = 0;
return true;
}
if (largo < MAX_LINEA - 1) linea[largo++] = c;
else desbordada = true; // demasiado larga: se descarta el exceso
}
return false;
}
void responder(const char *texto) {
ENLACE.print(texto);
ENLACE.print('\n');
}
void procesar(const char *l) {
if (desbordada) { desbordada = false; responder("ERR 3"); return; }
if (strcmp(l, "PING") == 0) {
responder("OK PONG");
} else if (strcmp(l, "GET T") == 0) {
float t = analogRead(PIN_SENSOR) * 5.0 / 1023.0 * 100.0; // LM35: 10 mV por °C
char texto[16] = "OK ";
dtostrf(t, 1, 1, texto + 3); // "OK 24.0"
responder(texto);
} else if (strcmp(l, "GET LED") == 0) {
responder(ledEncendido ? "OK 1" : "OK 0");
} else if (strncmp(l, "SET LED ", 8) == 0) {
char v = l[8];
if ((v == '0' || v == '1') && l[9] == '\0') {
ledEncendido = (v == '1');
digitalWrite(PIN_LED, ledEncendido);
responder("OK");
} else {
responder("ERR 2");
}
} else {
responder("ERR 1");
}
}
void setup() {
pinMode(PIN_LED, OUTPUT);
ENLACE.begin(9600);
}
void loop() {
if (leerLinea()) procesar(linea);
}
Ahora el controlador A. La función pedir envía el comando, espera con un tiempo límite y reintenta. Devuelve tres resultados distintos, porque no es lo mismo una respuesta OK, una respuesta ERR (que no se reintenta) y la falta de respuesta:
// ===== PLACA A (controlador) =====
#define ENLACE Serial1
enum Resultado { RESP_OK, RESP_ERR, SIN_RESPUESTA };
Resultado pedir(const char *cmd, char *resp, size_t tam, unsigned long timeoutMs, byte reintentos) {
for (byte intento = 0; intento <= reintentos; intento++) {
while (ENLACE.available()) ENLACE.read(); // descartar respuestas tardías de intentos anteriores
ENLACE.print(cmd);
ENLACE.print('\n');
byte n = 0;
unsigned long t0 = millis();
while (millis() - t0 < timeoutMs) {
if (!ENLACE.available()) continue;
char c = ENLACE.read();
if (c == '\r') continue;
if (c == '\n') {
resp[n] = '\0';
// ERR también es una respuesta: el enlace funciona, no tiene sentido reintentar
return (strncmp(resp, "OK", 2) == 0) ? RESP_OK : RESP_ERR;
}
if (n < tam - 1) resp[n++] = c;
}
// venció el tiempo: el siguiente intento reenvía el comando
}
resp[0] = '\0';
return SIN_RESPUESTA;
}
void setup() {
Serial.begin(115200);
ENLACE.begin(9600);
}
void loop() {
char resp[24];
Resultado r = pedir("GET T", resp, sizeof(resp), 50, 2); // 50 ms de timeout, hasta 3 intentos
if (r == RESP_OK) {
float t = atof(resp + 3); // "OK 24.0" -> 24.0
Serial.print("Temperatura remota: ");
Serial.println(t, 1);
pedir(t > 28.0 ? "SET LED 1" : "SET LED 0", resp, sizeof(resp), 50, 2);
} else if (r == SIN_RESPUESTA) {
Serial.println("Sin respuesta del nodo B");
} else {
Serial.print("Error del nodo: ");
Serial.println(resp);
}
delay(1000);
}
Observá tres detalles. El while (ENLACE.available()) ENLACE.read() inicial elimina respuestas atrasadas de un intento anterior, que de otro modo se leerían como respuesta del comando siguiente. El umbral de 28 °C no tiene histéresis, así que cerca de ese valor el LED parpadearía (el tema 42 del curso de Sensores y Actuadores lo trata). Y el comando SET LED se repite sin riesgo porque es idempotente.
Para un programa que no puede detenerse, el mismo diálogo se escribe como la máquina de estados del apartado 06. Este es su esqueleto, que reutiliza el leerLinea() del nodo B:
enum Estado { LIBRE, ESPERANDO };
Estado estado = LIBRE;
const char *pendiente;
unsigned long tEnvio;
byte intentos;
const unsigned long TIMEOUT_MS = 50;
const byte MAX_INTENTOS = 3;
void transmitir() {
ENLACE.print(pendiente);
ENLACE.print('\n');
tEnvio = millis();
intentos++;
estado = ESPERANDO;
}
void enviar(const char *cmd) { // solo si estado == LIBRE
pendiente = cmd;
intentos = 0;
transmitir();
}
void tick() { // se llama en cada vuelta de loop(); nunca espera
if (estado != ESPERANDO) return;
if (leerLinea()) {
procesarRespuesta(linea); // OK o ERR: la transacción terminó
estado = LIBRE;
} else if (millis() - tEnvio > TIMEOUT_MS) {
if (intentos < MAX_INTENTOS) transmitir(); // retransmitir
else { enlaceCaido(); estado = LIBRE; } // FALLO: informar y liberar
}
}
09 · Poner a andar el enlace
Plan de pruebas
Un enlace nuevo se prueba por etapas, de lo más simple a lo más completo. Cada etapa descarta un grupo de causas.
| Etapa | Cómo se hace | Resultado esperado | Si falla, sospechá de… |
|---|---|---|---|
| 1. Loopback | Unir TX y RX de una misma placa y enviar un carácter | Vuelve el mismo carácter | El programa o el periférico de esa placa |
| 2. Un sentido por vez | Enviar de A a B y mirar con un terminal conectado a B; luego al revés | Llegan los caracteres en cada sentido | TX/RX sin cruzar, masa, niveles |
| 3. Nodo B a mano | Conectar B a la PC con un adaptador USB–serie y escribir los comandos con un terminal (fin de línea «nueva línea») | PING → OK PONG; GET T → OK 24.0 | Código de B, velocidad, terminador |
| 4. Errores | Enviar HOLA, SET LED 7, get t y una línea de 40 caracteres | ERR 1, ERR 2, ERR 1, ERR 3 | Validación en B |
| 5. Diálogo completo | Conectar A y B y ver el monitor serie de A | Temperatura cada segundo y el LED de B que responde | Timeout, formato de la respuesta, atof |
| 6. Pérdida | Desconectar un instante el cable B → A | «Sin respuesta» tras (reintentos + 1) × timeout = 150 ms; al reconectar, se recupera sola | Manejo de reintentos y de restos en el buffer |
| 7. Medición | Con un analizador lógico, medir el tiempo de ida y vuelta de GET T | Cerca de los 19,6 ms calculados | Un proceso más lento en B |
Con un analizador lógico (o un adaptador USB–serie conectado en paralelo a la línea de B a A, en modo de solo escucha) se ve exactamente lo que cada placa envía, con la hora de cada mensaje. Ese registro resuelve el 90 % de los misterios: un comando que nunca sale, una respuesta con un carácter de más, un terminador equivocado.
10 · Analogía con programación
Cuántos reintentos hacen falta
Antes de elegir la cantidad de reintentos conviene calcularla y comprobarla con una simulación. El programa usa la fórmula del apartado 05 y la contrasta con una simulación de 20 000 transacciones. Para que los resultados sean reproducibles (y los mismos en cualquier máquina) se usa un generador congruencial lineal propio en lugar de random. Además cuenta las ejecuciones de más: veces en que el comando llegó a B y se ejecutó, pero la transacción se repitió porque la respuesta se perdió.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
class Azar:
"""Generador congruencial lineal de 32 bits: la misma secuencia en cualquier máquina."""
def __init__(self, semilla):
self.x = semilla
def siguiente(self):
self.x = (1664525 * self.x + 1013904223) % 2**32
return self.x / 2**32
def prob_exito(p, intentos):
un_intento = (1 - p) ** 2 # la petición Y la respuesta deben llegar
return 1 - (1 - un_intento) ** intentos
def timeout_recomendado(t_peticion, t_proceso, t_respuesta, margen=0.5):
return (t_peticion + t_proceso + t_respuesta) * (1 + margen)
def simular(p, intentos, n=20000, semilla=1):
azar = Azar(semilla)
exitos = de_mas = 0
for _ in range(n):
ejecuciones = 0
ok = False
for _ in range(intentos):
llega = azar.siguiente() > p # ¿llegó la petición?
if llega:
ejecuciones += 1 # B ejecutó el comando
if llega and azar.siguiente() > p: # ¿llegó la respuesta?
ok = True
break
exitos += ok
de_mas += max(0, ejecuciones - 1) # ejecuciones repetidas del mismo comando
return exitos / n, de_mas / n
print("pérdida intentos teórico simulado ejecuciones de más por transacción")
for p in (0.01, 0.05, 0.2):
for intentos in (1, 3):
exito, de_mas = simular(p, intentos)
print(f"{p:6.0%} {intentos:6d} {prob_exito(p, intentos):7.2%} {exito:7.2%} {de_mas:8.3f}")
t = timeout_recomendado(6.25, 5, 8.33)
print()
print(f"GET T a 9600 baudios, 5 ms de proceso: ida y vuelta {6.25 + 5 + 8.33:.1f} ms, timeout con 50 % de margen {t:.1f} ms")
Resultado: con 1 % de pérdida un solo intento da 98,0 %; con 5 %, 90,3 % (simulado 89,8 %) y con 3 intentos sube a 99,9 %; con 20 %, 64,0 % con un intento y 95,3 % con tres. Los valores simulados siguen de cerca los teóricos. La última columna muestra el costo oculto: con 20 % de pérdida y 3 intentos, una de cada cinco transacciones ejecuta el comando más de una vez (0,207 ejecuciones de más por transacción). Con comandos idempotentes es inofensivo; con un TOGGLE, el estado de B termina desfasado. El timeout recomendado para GET T (ida y vuelta de 19,6 ms con 5 ms de proceso) es 29,4 ms.
El modelo supone que las pérdidas son independientes. En un cable con ráfagas de ruido, los mensajes se pierden en grupos, y los reintentos inmediatos fallan juntos: conviene esperar un poco entre uno y otro (espera creciente).
11 · Exploración interactiva
Enlace con pérdidas y nodo virtual
El laboratorio tiene dos herramientas. La primera simula 200 transacciones entre A y B por un enlace con mensajes que se pierden: elegís el comando, la velocidad, la tasa de pérdida, el procesamiento de B, el timeout y los reintentos, y ves cuántas transacciones se completan, cuánto tardan y qué pasa con el LED cuando el comando no es idempotente. La segunda es un nodo B virtual: le escribís comandos y responde exactamente como el programa de la placa.
1. 200 transacciones por un enlace con pérdidas
Elegí los parámetros y presioná «Ejecutar 200 transacciones».
2. Nodo B virtual
Escribí un comando o usá los botones.
En la primera herramienta, el estado real del LED depende de cuántas veces se ejecutó el comando TOGGLE en B, mientras que A solo cuenta las transacciones que terminó con éxito. Cada pulsación de «Ejecutar» continúa la secuencia pseudoaleatoria (con el botón «Reiniciar» se repiten los mismos resultados). La segunda herramienta aplica las reglas del protocolo: mayúsculas, argumento válido y largo máximo de 31 caracteres; las líneas vacías se ignoran.
Recorrido guiado
- Con los valores iniciales (GET T, 9600 baudios, 5 % de pérdida, 5 ms de proceso, timeout de 50 ms y 2 reintentos) el tiempo de ida y vuelta es de 19,6 ms y el éxito teórico, 99,9 %. Ejecutá las 200 transacciones: casi todas se completan, algunas necesitan un segundo intento.
- Poné la pérdida en 0 %: todas se completan al primer intento, el tiempo medio es el de ida y vuelta y no hay ejecuciones de más.
- Subí la pérdida a 40 % y bajá los reintentos a 0: el éxito teórico es 36 % y la simulación lo confirma. Con 5 reintentos la teoría da 93,1 % (la simulación se mueve alrededor de ese valor), pero el tiempo medio sube porque cada intento fallido cuesta un timeout completo.
- Con pérdida en 0 %, bajá el timeout a 10 ms: como el tiempo de ida y vuelta es de 19,6 ms, todas las transacciones fallan aunque el enlace sea perfecto, y B ejecuta el comando en cada intento. Subí a 20 ms y todo vuelve a funcionar, apenas.
- Con 30 ms de procesamiento en B y 20 ms de timeout ocurre lo mismo: el timeout hay que calcularlo con el tiempo de proceso real, no solo con el de línea.
- Elegí TOGGLE con 20 % de pérdida y 2 reintentos. Ejecutá: aparecen ejecuciones de más y el indicador del LED muestra que A y B discrepan en más de la mitad de las 200 transacciones, aunque el estado final pueda coincidir por casualidad. El veredicto es «estado inconsistente».
- Cambiá a SET LED 1 con los mismos parámetros: las ejecuciones de más siguen ocurriendo, pero el LED nunca se desfasa (0 de 200), porque repetir «poné el LED en 1» deja el mismo resultado. El veredicto sigue señalando que alrededor del 5 % de las transacciones no se completan, pero eso es otro problema.
- En el nodo virtual, enviá
PING,GET TySET LED 1, y mirá cómo cambia el LED. Mové la temperatura y repetíGET T: el valor tiene un decimal y refleja la cuantización del ADC (24 °C se lee 23,9 °C). - Probá los errores:
SET LED 7respondeERR 2,get trespondeERR 1y la línea larga respondeERR 3. En los tres casos B contesta, y A sabe que no debe reintentar.
Desafío: el reintento más corto posible
Con GET T, 115 200 baudios y 5 ms de proceso en B, ¿cuánto vale el tiempo de ida y vuelta y qué timeout elegirías? La línea ocupa 6 + 8 = 14 bytes × 10 / 115 200 ≈ 1,2 ms, así que el ida y vuelta es de ≈ 6,2 ms: casi todo es procesamiento. Un timeout de 10 ms ya alcanza, a diferencia de los 9600 baudios. Comprobalo en la herramienta 1: el tiempo medio por transacción baja de 19,6 ms a 6,2 ms.
12 · Qué viene después
Ampliaciones y buenas prácticas
El taller deja un enlace funcional y se puede extender en varias direcciones, cada una anticipo de un tema posterior:
- Checksum o CRC por mensaje para detectar líneas corrompidas que lleguen completas (temas 43 a 45). Una respuesta
OK 24.0alterada aOK 94.0pasaría inadvertida sin él. - Números de secuencia para poder usar comandos no idempotentes y para descartar respuestas atrasadas.
- Un latido (heartbeat): A envía un
PINGperiódico y, tras varios sin respuesta, declara el enlace caído y pasa a un modo seguro, por ejemplo apagando una carga. - Eventos espontáneos: permitir que B envíe líneas
EVT ...cuando ocurra algo importante, con un prefijo que A distinga de las respuestas. - Más nodos: pasar a un bus RS-485 con direcciones, para tener varios nodos B (temas 24 y 25), reutilizando casi todo el diseño.
- Espera creciente entre reintentos si el ruido llega en ráfagas, y un contador de fallos para diagnóstico.
Y tres hábitos que valen para cualquier enlace: probar por etapas, diseñar para la falla (qué hace cada lado si el otro desaparece) y medir los tiempos en lugar de suponerlos.
13 · Comprensión
Confusiones frecuentes
«Si no hay respuesta, el comando no se ejecutó»
No se sabe. Puede haberse perdido la petición (no se ejecutó) o la respuesta (sí se ejecutó). Por eso los comandos que se reintentan deben ser idempotentes, o llevar un número de secuencia.
«Un timeout largo es más seguro»
Evita falsos vencimientos, pero cada mensaje perdido deja al programa esperando todo ese tiempo, y el peor caso se multiplica por los reintentos. El timeout se calcula con el tiempo de ida y vuelta más un margen razonable.
«Un ERR hay que reintentarlo, porque algo salió mal»
Una respuesta ERR indica que el enlace funciona y que el comando no es válido. Reenviar lo mismo dará el mismo error. Se reintenta solo cuando no hay respuesta.
«Si el esclavo no entiende un comando, lo mejor es ignorarlo»
Ignorarlo es indistinguible de un mensaje perdido, y el maestro reintentaría inútilmente y declararía caído un enlace que funciona. Siempre se responde, aunque sea con un código de error.
«delay en el esclavo no importa, porque el maestro espera»
Todo el tiempo que B pierde se suma al tiempo de ida y vuelta, y si supera el timeout de A provoca reintentos y ejecuciones duplicadas. B debe contestar lo más rápido posible.
«Si cada placa anda por separado, el enlace va a andar»
El enlace agrega sus propias fallas: cables cruzados mal, masa sin unir, niveles de tensión, velocidades o terminadores distintos. Por eso se prueba por etapas y se mira la línea con un analizador.
14 · Práctica guiada
Predecí antes de comprobar
1. Calcular el timeout
El comando SET LED 1\n (10 bytes) recibe la respuesta OK\n (3 bytes) a 9600 baudios en 8N1. B tarda 2 ms en procesarlo. Calculá el tiempo de ida y vuelta y un timeout con 50 % de margen.
Ver solución
Petición: 100 bits / 9600 ≈ 10,4 ms. Respuesta: 30 bits / 9600 ≈ 3,1 ms. Total = 10,4 + 2 + 3,1 ≈ 15,5 ms. Con 50 % de margen: ≈ 23 ms, que se redondea a 25 ms (o 30 ms para tener más holgura).
2. Probabilidad con reintentos
Cada mensaje se pierde con probabilidad del 10 %. ¿Cuál es la probabilidad de completar una transacción con un solo intento? ¿Y con 2 reintentos (3 intentos)?
Ver solución
Un intento: 0,9² = 81 %. Fallar los tres intentos: (1 − 0,81)³ = 0,19³ ≈ 0,0069; el éxito es 1 − 0,0069 ≈ 99,3 %.
3. Idempotencia
Decidí cuáles de estos comandos son idempotentes: SET LED 1, TOGGLE, GET T, INC CONTADOR, SET PWM 128. Para los que no lo son, proponé cómo hacerlos seguros ante reintentos.
Ver solución
Idempotentes: SET LED 1, GET T y SET PWM 128 (fijan un valor absoluto o solo leen). No idempotentes: TOGGLE e INC CONTADOR. Se pueden reemplazar por versiones con valor absoluto (SET LED 0/1, SET CONTADOR 42), o numerar los mensajes para que B reconozca un reintento y reenvíe la respuesta guardada sin ejecutarlo otra vez.
4. Qué responde B
¿Qué responde el nodo B a: (a) SET LED 7; (b) get t; (c) una línea de 50 caracteres; (d) una línea vacía; (e) SET LED (con un espacio al final y nada más)?
Ver solución
(a) ERR 2: argumento inválido. (b) ERR 1: los comandos distinguen mayúsculas y minúsculas. (c) ERR 3: línea demasiado larga. (d) Nada: las líneas vacías se ignoran. (e) ERR 2: el comando es válido pero falta el argumento.
5. Una respuesta atrasada
A usa un timeout de 15 ms, pero B tarda 19 ms en contestar GET T. A reintenta y la respuesta del primer intento llega durante el segundo. ¿Qué ocurre y cómo lo evita el código de pedir()?
Ver solución
A se rinde antes de que llegue la primera respuesta, reenvía el comando y recibe después la respuesta del primer intento, que toma por la del segundo; la del segundo queda en el buffer y se leerá como respuesta de la próxima consulta: el diálogo queda desfasado. El código de pedir() descarta al inicio de cada intento lo que haya en el buffer, pero el remedio real es calcular bien el timeout (≥ 19,6 ms) y, para mayor seguridad, numerar los mensajes.
6. Detectar un enlace caído
A envía un PING por segundo con timeout de 50 ms y 2 reintentos, y declara el enlace caído tras 3 transacciones fallidas seguidas. ¿Cuánto tarda en detectarlo, como máximo, desde que B se desconecta?
Ver solución
Cada transacción fallida consume 3 × 50 = 150 ms. La primera falla en el instante siguiente a la desconexión (hasta 1 s de espera hasta el próximo ping), y las otras dos se suman a un ritmo de un ciclo por segundo: ≈ 1 s + 150 ms + 1 s + 150 ms + 1 s + 150 ms ≈ 3,5 s en el peor caso. Se puede acortar enviando los pings más seguido o bajando el número de fallos que se exigen, a costa de falsas alarmas.
15 · Síntesis
Ideas para recordar
- Un enlace entre dos placas se construye por capas: cableado cruzado con masa común y niveles compatibles, un protocolo de líneas de texto, tiempos de espera calculados y manejo de pérdidas.
- El protocolo maestro–esclavo de texto usa una línea por mensaje, comandos en mayúsculas, respuestas
OKoERR n, y el esclavo nunca habla sin que se le pregunte. - Los errores se responden, no se callan: ignorar un comando inválido es indistinguible de un mensaje perdido.
- El timeout se calcula como el tiempo de ida y vuelta (petición + proceso + respuesta) más un margen; demasiado corto provoca reintentos sin motivo y demasiado largo estira el peor caso.
- Los reintentos mejoran la probabilidad de éxito (1 − (1 − (1 − p)²)n) y solo se aplican ante falta de respuesta, nunca ante un
ERR. - Ante un reintento no se sabe si el comando se ejecutó: los comandos idempotentes (fijar un valor) son seguros; los no idempotentes (invertir, incrementar) necesitan números de secuencia.
- El maestro sin bloqueo es una máquina de estados (libre, esperando, fallo) que avanza un paso por vuelta del programa.
- Un enlace nuevo se prueba por etapas (loopback, un sentido, nodo a mano, errores, diálogo, pérdida, medición) y se observa con un analizador lógico.
¿Listo para el siguiente bloque?
Antes de avanzar, calculá el timeout de un comando de 12 bytes con respuesta de 20 bytes a 19 200 baudios y 4 ms de proceso, explicá por qué TOGGLE es peligroso con reintentos y decidí cuántos reintentos usarías con una pérdida del 10 % por mensaje para superar el 99,9 % de éxito. Si podés justificar las tres respuestas, tenés el bloque de UART completo.
En el próximo tema empezamos el bloque de SPI, el bus rápido: cuatro hilos, un reloj del maestro y registros de desplazamiento.