← Índice del curso

Comunicación entre Dispositivos · Tema 10

Protocolos de texto sobre UART

La UART solo mueve bytes; qué significan lo decide un protocolo. Los módulos de comunicación y los receptores GPS hablan en texto legible: comandos AT que se pueden teclear a mano y sentencias NMEA que se leen en cualquier monitor. Este tema enseña a hablar y a entender esos protocolos, a comprobar que llegaron sanos y a frenar al emisor cuando el receptor no da abasto.

01 · Punto de partida

Hablar en texto

En el tema anterior vimos cómo recibir bytes sin perderlos. Ahora toca darles sentido. Hay dos grandes estilos: los protocolos binarios, compactos pero ilegibles, y los protocolos de texto, donde cada mensaje es una línea de caracteres imprimibles. Los de texto cuestan más bytes, pero tienen una ventaja enorme para quien programa: se pueden probar a mano con el monitor serie, se leen en una captura sin decodificar nada y un error salta a la vista.

Por eso casi todos los módulos que se conectan por UART hablan en texto: los módems y módulos celulares, Bluetooth y Wi-Fi se configuran con comandos AT, y los receptores GPS emiten sentencias NMEA una vez por segundo. Si aprendés a manejar esos dos, aprendés el patrón de decenas de dispositivos más.

02 · La forma común

Anatomía de un protocolo de texto

Aunque cada protocolo tiene sus particularidades, casi todos los de texto comparten una estructura:

  • Un mensaje por línea. El fin de línea marca dónde termina el mensaje; es el delimitador que los protocolos binarios resuelven de otra manera (tema 41).
  • Campos separados. Con una coma (NMEA), un espacio, dos puntos o un signo igual (AT).
  • Estilo pregunta–respuesta, en el que cada comando recibe una respuesta, o emisión espontánea, en la que el dispositivo transmite solo (el GPS).
  • Mensajes no solicitados. Algunos módulos emiten avisos en cualquier momento (un SMS recibido, una conexión que se cae); el programa debe saber reconocerlos y no confundirlos con la respuesta a su comando.

El detalle que más problemas causa es el terminador de línea, porque hay tres convenciones y cada dispositivo usa la suya:

Terminadores de línea habituales.
TerminadorBytesDónde se usa
CR (retorno de carro, \r)0x0DMuchos módems y módulos AT esperan un CR al final de cada comando; Macintosh antiguos
LF (nueva línea, \n)0x0AUnix, Linux; muchos protocolos propios
CR + LF (\r\n)0x0D 0x0ANMEA, HTTP, respuestas de módulos AT, Windows

La regla de oro: al enviar, usá exactamente lo que pide el dispositivo; al recibir, tolerá cualquiera. El lector de líneas del tema 9 ignora el CR y termina en el LF, y por eso admite los tres casos. Y como Serial Monitor de Arduino puede agregar «nueva línea», «retorno de carro» o ambos, la causa de muchos módulos que «no responden» es simplemente un terminador equivocado.

03 · Módems y módulos

Comandos AT

El conjunto de comandos AT nació en los años ochenta para controlar módems telefónicos (el «Hayes») y se convirtió en el estándar de hecho para configurar módulos de comunicación: celulares (SIM800, SIM7600), Bluetooth (HC-05), Wi-Fi (ESP8266 con firmware AT) y LoRa, entre otros. Cada comando empieza con las letras AT (de attention), sigue con el nombre y termina con el terminador de línea.

Formas habituales de un comando AT.
FormaSignificadoEjemplo
ATPrueba de vida: ¿estás ahí?AT → OK
AT+CMDEjecutar una acciónAT+RST
AT+CMD?Consultar el valor actualAT+CPIN? → +CPIN: READY
AT+CMD=valorEstablecer un valorAT+NAME=Sensor1
AT+CMD=?Consultar los valores admitidosAT+CWMODE=?

Las respuestas siguen su propia convención. Cuando el comando termina bien, el módulo devuelve una línea OK; si falla, ERROR o una variante con el motivo (+CME ERROR: 10). Antes del OK puede haber líneas de datos, que empiezan con el nombre del comando. Una conversación típica de un módulo celular, con los terminadores representados:

Intercambio típico con un módulo celular.
EnviásRecibísQué significa
AT\r\n\r\nOK\r\nEl módulo responde
AT+CPIN?\r\n\r\n+CPIN: READY\r\n\r\nOK\r\nLa tarjeta SIM está lista
AT+CSQ\r\n\r\n+CSQ: 18,0\r\n\r\nOK\r\nCalidad de señal 18 → −113 + 2 × 18 = −77 dBm
AT+FOO\r\n\r\nERROR\r\nComando desconocido

En el valor de AT+CSQ, la escala va de 0 (−113 dBm o menos) a 31 (−51 dBm o más), con 99 para «desconocido». La fórmula para el rango intermedio es dBm = −113 + 2 × n.

Reglas prácticas

  • El eco. Muchos módulos repiten lo que reciben (ATE0 lo desactiva). Si el eco está activo, el primer renglón de la respuesta es tu propio comando: el programa debe saltearlo.
  • Los tiempos. Una respuesta puede tardar milisegundos (AT) o decenas de segundos (conectarse a una red con AT+CWJAP). Cada comando necesita su propio timeout, que se toma de la hoja de datos.
  • La velocidad. Algunos módulos fijan su velocidad (el HC-05 en modo AT usa 38 400 baudios) y otros la detectan solos (el SIM800 con autobaud, como vimos en el tema 8): hay que enviar AT varias veces hasta que contesten OK.
  • Los avisos espontáneos (URC). Líneas como RING o +CMTI: "SM",1 pueden aparecer en cualquier momento, incluso entre un comando y su respuesta. Un buen manejador de AT reconoce el OK o el ERROR que corresponde y trata el resto como datos o avisos.
  • Un comando a la vez. No se envía el siguiente hasta recibir OK, ERROR o agotar el tiempo.

04 · El idioma del GPS

NMEA 0183: el idioma del GPS

Los receptores GPS más comunes (u-blox, los módulos NEO-6M y similares) salen de fábrica enviando por la UART, a 9600 baudios y una vez por segundo, un grupo de sentencias NMEA 0183, un estándar de texto de la industria naval. Cada sentencia es una línea con una estructura rígida:

Estructura de una sentencia NMEA La sentencia $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 se divide en: el signo de dólar que indica el inicio, el identificador de dos letras GP del sistema, el tipo de sentencia GGA, los campos de datos separados por comas, el asterisco y el checksum de dos dígitos hexadecimales 47, y el fin de línea CR LF. $GPGGA *47CRLF 123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,, inicio sistema(GP = GPS) tipo(posición) campos de datos separados por comas (pueden estar vacíos) fin dedatos checksum2 hex fin delínea checksum = XOR de todos los caracteres entre el «$» y el «*» (sin incluirlos) Una sentencia GGA (datos de posición)

El checksum es el XOR bit a bit de todos los caracteres que hay entre el $ y el *, escrito con dos dígitos hexadecimales. Es un chequeo simple, parecido a la paridad pero por mensaje: detecta casi cualquier alteración de un carácter, y lo calcula el receptor para comparar con el que llegó. Una sentencia con checksum incorrecto se descarta.

Los tipos de sentencia más usados son:

Sentencias NMEA que se encuentran habitualmente en un receptor GPS.
TipoQué informa
GGADatos de posición: hora, latitud, longitud, calidad de la señal, satélites, altitud
RMCMínimo recomendado: hora, estado, latitud, longitud, velocidad en nudos, rumbo, fecha
GSVSatélites a la vista (varias sentencias por ciclo)
GSASatélites activos y dilución de la precisión
GLLLatitud y longitud con la hora

Una sentencia GGA real tiene estos campos, numerados desde cero (el campo 0 es el tipo):

Campos de la sentencia GGA del ejemplo.
NºCampoValor del ejemploInterpretación
1Hora UTC12351912:35:19 UTC
2–3Latitud4807.038,N48° 07,038′ Norte = 48,1173°
4–5Longitud01131.000,E11° 31,000′ Este = 11,5167°
6Calidad de la solución10 = sin posición, 1 = GPS, 2 = GPS diferencial
7Satélites en uso088
8HDOP0.9Dilución horizontal de la precisión (menor es mejor)
9–10Altitud545.4,M545,4 metros sobre el nivel del mar
11–12Separación del geoide46.9,M46,9 metros

Las coordenadas no son grados decimales

La trampa más conocida de NMEA es el formato de las coordenadas: ddmm.mmmm para la latitud y dddmm.mmmm para la longitud, es decir, los grados enteros seguidos de los minutos con decimales, sin separador entre ellos. Para pasar a grados decimales:

grados = dd + mm.mmmm / 60  4807.038 N → 48 + 7,038 / 60 = 48,1173°  (Sur y Oeste son negativos)

Otros detalles: la velocidad de la sentencia RMC está en nudos (1 nudo = 1,852 km/h: 22,4 nudos son 41,5 km/h), la hora es UTC y la fecha viene como ddmmaa. Un GPS sin señal sigue emitiendo sentencias, pero con los campos de posición vacíos y la calidad en 0: hay que comprobarlo antes de usar los números.

05 · Convertir texto en datos

Analizar líneas sin tropezar

Con una línea completa en un arreglo (tema 9), el análisis tiene cuatro pasos: validar (largo, inicio, checksum), separar los campos, convertir los que se necesitan y verificar rangos. En cada uno hay trampas típicas:

Trampas habituales al analizar protocolos de texto.
PasoTrampaCómo evitarla
ValidarUsar datos de una línea con checksum incorrecto o cortadaVerificar el checksum y el largo antes de mirar los campos
Separarstrtok trata varias comas seguidas como un solo separador y saltea los campos vacíos: los índices se correnSeparar a mano o con strsep, conservando los campos vacíos
Convertiratoi y atof devuelven 0 con un campo vacío, que se confunde con un valor realComprobar que el campo no esté vacío antes de convertirlo
CoordenadasTomar 4807.038 como grados decimalesSeparar grados y minutos y convertir
PrecisiónUn float de un AVR tiene unos 7 dígitos: se pierden metros en la posiciónUsar enteros (minutos × 10 000) para precisión de metros
RangosAceptar valores imposibles por un error de transmisión que pasó el checksumVerificar rangos (latitud entre −90 y 90, satélites entre 0 y 24…)

El caso de los campos vacíos merece un ejemplo. Una sentencia GGA sin señal tiene 15 campos, pero la mayoría están vacíos: $GPGGA,123519,,,,,0,00,99.99,,,,,,*45. Un strtok con la coma como separador devuelve solo 5 elementos (GPGGA, 123519, 0, 00 y 99.99), y el programa que busca la calidad en la posición 6 leerá algo que no corresponde. El análisis por posición exige que cada coma cuente.

06 · Frenar al emisor

Control de flujo: RTS/CTS y XON/XOFF

En el tema 9 vimos que un buffer grande ayuda, pero que si el emisor es más rápido que el receptor siempre termina desbordándolo. La solución de fondo es que el receptor pueda pedirle al emisor que se detenga. Eso es el control de flujo. Hay dos variantes.

Por hardware: RTS/CTS

Se agregan dos cables. En el uso moderno, el receptor controla una línea RTS («listo para recibir») conectada a la entrada CTS del emisor («vía libre»). Mientras el receptor tiene lugar, mantiene RTS activa y el emisor transmite; cuando su buffer se acerca a llenarse, desactiva RTS y el emisor se detiene después del byte en curso. Cuando el buffer se vacía, la reactiva.

Conexión con control de flujo RTS/CTS Un emisor con pines TX y CTS y un receptor con pines RX y RTS. TX del emisor va a RX del receptor; el RTS del receptor va al CTS del emisor. Una nota explica que el receptor baja RTS cuando su buffer supera el umbral alto y lo sube cuando baja del umbral bajo. EmisorReceptor TXRX CTSRTS datos «vía libre» (RTS del receptor → CTS del emisor) El receptor desactiva RTS al pasar el umbral alto de su buffer y la reactiva al bajar del umbral bajo (la masa común también va conectada, y en las UART TTL la línea activa suele ser la de nivel bajo)

Hay que dejar un margen. Cuando el receptor baja RTS, todavía pueden llegar bytes: el que se estaba enviando y los que el emisor ya tenía en su FIFO de hardware (que puede ser de 16 bytes o más). Por eso el umbral alto no puede ser el 100 % del buffer: tiene que cumplir umbral alto + bytes en vuelo ≤ tamaño del buffer. Un umbral bajo bastante menor (por ejemplo, el 25 %) da histéresis y evita que la línea oscile.

Por software: XON/XOFF

Sin cables extra, el receptor envía por la línea de retorno un carácter de control: XOFF (0x13) para pedir que el emisor pare y XON (0x11) para que reanude. Es simple, pero tiene dos defectos: no sirve para datos binarios (porque 0x11 y 0x13 pueden aparecer como datos) y la orden viaja con la misma demora que cualquier byte, lo que obliga a un margen mayor.

Comparación de las estrategias de control de flujo.
EstrategiaCablesDatos binariosReacciónCuándo usarla
NingunaTX, RX, GNDSí—Flujos pequeños o receptor siempre más rápido
XON/XOFFTX, RX, GNDNo (reserva dos códigos)Lenta (viaja como un byte más)Texto entre equipos sin cables adicionales
RTS/CTSTX, RX, GND, RTS, CTSSíRápida (una señal eléctrica)Velocidades altas, módulos Wi-Fi y Bluetooth, transferencias grandes

Los ESP32 y muchos STM32 implementan RTS/CTS por hardware: la UART sola vigila el nivel del buffer y mueve la línea. En un AVR sin esa función se hace por software, activando un pin desde la interrupción de recepción, como se muestra en el código del tema.

07 · Con el microcontrolador

Un manejador de comandos AT y un lector de NMEA

El primer programa envía un comando AT a un módulo conectado a Serial1 (dejando Serial para depurar) y espera OK, ERROR o el vencimiento del tiempo. Si se le indica un prefijo (por ejemplo +CSQ:), guarda la línea de datos que lo lleva. Descarta lo que haya antes, así que el eco y los avisos viejos no confunden:

// Devuelve true si el módulo respondió OK. Si 'prefijo' no es NULL, copia a 'dato' la línea que empieza con él.
bool comandoAT(const char *cmd, unsigned long timeoutMs, const char *prefijo = NULL,
               char *dato = NULL, size_t tam = 0) {
    while (Serial1.available()) Serial1.read();         // descartar avisos y restos anteriores
    Serial1.print(cmd);
    Serial1.print("\r\n");                              // la mayoría de los módulos piden CR LF

    char linea[64];
    byte n = 0;
    unsigned long t0 = millis();
    while (millis() - t0 < timeoutMs) {
        while (Serial1.available()) {
            char c = Serial1.read();
            if (c == '\r') continue;
            if (c != '\n') {
                if (n < sizeof(linea) - 1) linea[n++] = c;
                continue;
            }
            linea[n] = '\0';                            // línea completa
            n = 0;
            if (strcmp(linea, "OK") == 0) return true;
            if (strncmp(linea, "ERROR", 5) == 0 || strncmp(linea, "+CME ERROR", 10) == 0) return false;
            if (prefijo && dato && strncmp(linea, prefijo, strlen(prefijo)) == 0) {
                strncpy(dato, linea, tam - 1);
                dato[tam - 1] = '\0';
            }                                           // el eco y otras líneas se ignoran
        }
    }
    return false;                                       // se agotó el tiempo
}

void setup() {
    Serial.begin(115200);
    Serial1.begin(9600);
    for (byte i = 0; i < 5 && !comandoAT("AT", 500); i++) {}   // autobaud: insistir hasta que conteste
    char resp[32];
    if (comandoAT("AT+CSQ", 1000, "+CSQ:", resp, sizeof(resp))) {
        int n = atoi(resp + 6);                         // "+CSQ: 18,0" -> 18
        Serial.print("Señal: ");
        Serial.print(-113 + 2 * n);
        Serial.println(" dBm");
    }
}

void loop() {}

Esta versión bloquea mientras espera, y es adecuada para la inicialización. Con un programa que debe atender otras tareas, la misma lógica se convierte en una máquina de estados que se avanza en cada vuelta de loop() (estado «esperando respuesta» con su hora de inicio), como en el tema 41.

El segundo programa valida una sentencia NMEA, separa los campos conservando los vacíos y convierte la posición a grados:

// 's' = "$GPGGA,...*47" ya sin CR ni LF
bool nmeaValida(const char *s) {
    if (s[0] != '$') return false;
    uint8_t suma = 0;
    const char *p = s + 1;
    while (*p && *p != '*') suma ^= *p++;               // XOR de todo lo que está entre '$' y '*'
    if (*p != '*') return false;
    return suma == (uint8_t)strtol(p + 1, NULL, 16);    // comparar con los dos dígitos hexadecimales
}

// Reemplaza cada coma por '\0' y guarda dónde empieza cada campo. A diferencia de strtok, conserva los vacíos.
byte separar(char *s, char *campo[], byte max) {
    byte n = 0;
    campo[n++] = s;
    for (; *s && n < max; s++) {
        if (*s == ',') { *s = '\0'; campo[n++] = s + 1; }
    }
    return n;
}

float grados(const char *campo, char hemisferio) {      // "4807.038", 'N' -> 48.1173
    float v = atof(campo);
    int g = (int)(v / 100);                             // los grados son lo que está antes de los minutos
    float res = g + (v - g * 100) / 60.0;
    return (hemisferio == 'S' || hemisferio == 'W') ? -res : res;
}

void procesarGGA(char *linea) {
    if (!nmeaValida(linea)) return;                     // checksum incorrecto: descartar
    char *estrella = strchr(linea, '*');
    *estrella = '\0';                                   // dejar solo el cuerpo para separar
    char *c[16];
    byte n = separar(linea, c, 16);
    if (n < 10 || c[6][0] == '0' || c[2][0] == '\0') return;   // sin posición válida: no usar
    float lat = grados(c[2], c[3][0]);
    float lon = grados(c[4], c[5][0]);
    Serial.print(lat, 4);  Serial.print(", ");  Serial.println(lon, 4);
}

Dos comentarios finales. Primero, como el float de un AVR tiene unos siete dígitos significativos, la latitud 48,1173° llega con una resolución de unos 10 m; para más precisión conviene guardar los minutos como entero. Segundo, para implementar RTS/CTS por software en un AVR, la rutina de recepción del tema 9 debe, después de guardar cada byte, comparar la ocupación con el umbral alto y bajar un pin de salida (digitalWrite(PIN_RTS, HIGH) en un esquema de lógica activa baja), y la función de lectura debe volver a subirlo al bajar del umbral bajo.

08 · Analogía con programación

Validar, separar y convertir en Python

El mismo análisis en Python cabe en pocas líneas, porque split(",") conserva los campos vacíos. El programa calcula el checksum, valida cuatro sentencias (una de ellas con un dígito alterado y otra sin señal), convierte la posición, transforma nudos en km/h y traduce la calidad de señal de un módulo AT a dBm.

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

def checksum_nmea(frase):
    cuerpo = frase[1:frase.index("*")]          # lo que está entre el '$' y el '*'
    suma = 0
    for c in cuerpo:
        suma ^= ord(c)
    return f"{suma:02X}"


def es_valida(frase):
    if not frase.startswith("$") or "*" not in frase:
        return False
    return checksum_nmea(frase) == frase.split("*")[1].strip().upper()


def grados(campo, hemisferio):
    valor = float(campo)
    g = int(valor // 100)                       # los grados son lo que está antes de los minutos
    decimal = g + (valor - g * 100) / 60
    return -decimal if hemisferio in "SW" else decimal


def leer_gga(frase):
    campos = frase.split("*")[0].split(",")     # split conserva los campos vacíos
    if campos[6] == "0":
        return "sin posición (calidad = 0)"
    lat = grados(campos[2], campos[3])
    lon = grados(campos[4], campos[5])
    return f"lat {lat:.4f}, lon {lon:.4f}, altitud {campos[9]} m, {int(campos[7])} satélites"


def dbm_de_csq(n):
    return None if n == 99 else -113 + 2 * n


frases = [
    "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
    "$GPGGA,123519,4807.039,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",    # un dígito alterado
    "$GPGGA,123519,,,,,0,00,99.99,,,,,,*45",                                  # sin señal
    "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A",
]
for f in frases:
    valida = es_valida(f)
    print(f[:30] + "...", "checksum calculado", checksum_nmea(f), "->", "válida" if valida else "RECHAZADA")
    if valida and f[1:6] == "GPGGA":
        print("    ", leer_gga(f))

campos = frases[3].split("*")[0].split(",")
nudos = float(campos[7])
print(f"velocidad: {nudos} nudos = {nudos * 1.852:.1f} km/h")

for n in (18, 5, 99):
    print(f"AT+CSQ: {n} -> {dbm_de_csq(n)} dBm")

Resultado: la primera sentencia tiene checksum 47 y es válida: latitud 48,1173°, longitud 11,5167°, altitud 545,4 m y 8 satélites. En la segunda, el dígito alterado cambia el XOR a 46 y la sentencia se rechaza. La tercera es válida (checksum 45) pero no tiene posición, y la cuarta (RMC, checksum 6A) da 22,4 nudos = 41,5 km/h. La señal AT+CSQ de 18 son −77 dBm; la de 5, −103 dBm; y 99 significa «desconocido». Alterá un carácter de la primera frase y mirá qué checksum calcula.

Una sentencia puede tener un checksum correcto y datos absurdos (un error doble que se compensa en el XOR, o un GPS defectuoso). Siempre conviene comprobar rangos antes de usar los valores.

09 · Exploración interactiva

NMEA y control de flujo

El laboratorio tiene dos herramientas. La primera es un analizador de sentencias NMEA: elegí una de muestra o escribí la tuya, y verás el checksum calculado, el recibido, la validez, los campos con su significado y la posición en grados decimales. La segunda simula una transferencia de 2000 bytes hacia un receptor lento, con y sin control de flujo.

1. Analizador de sentencias NMEA

Tipo de sentencia—
Checksum calculado—
Checksum recibido—
Veredicto—
Latitud y longitud—
Dato adicional—
Campos de la sentencia (los vacíos se conservan). Los nombres corresponden a los tipos GGA y RMC.
NºSignificadoValor

Elegí una sentencia o editá el texto.

2. Control de flujo con un receptor lento (2000 bytes)

Bytes perdidos—
Bytes entregados al programa—
Ocupación máxima del buffer—
Pausas pedidas por el receptor—
Tiempo hasta terminar—
Veredicto—

Elegí los parámetros de la transferencia.

En la primera herramienta, el «dato adicional» muestra la altitud (GGA) o la velocidad en km/h (RMC). En la segunda, el emisor manda sin pausa un byte por cada tiempo de byte de la línea; el receptor consume a su ritmo, desactiva RTS al llegar al umbral alto y la reactiva al bajar al 25 % del buffer. Los «bytes en vuelo» son los que todavía llegan después de la orden (el que se estaba enviando y los de la FIFO del emisor).

Recorrido guiado

  1. Con la sentencia GGA de muestra, el checksum calculado y el recibido son 47: es válida. La latitud es 48,1173° y la longitud 11,5167°; la altitud, 545,4 m. Fijate en la tabla que los campos 11 y 12 (separación del geoide) y los dos últimos, vacíos, mantienen su lugar.
  2. Pasá a RMC: el checksum es 6A, la velocidad 22,4 nudos (41,5 km/h) y la fecha 230394.
  3. Elegí GGA con un dígito alterado: el XOR calculado es 46 y el recibido 47: la sentencia se rechaza. Un solo carácter cambiado alcanza para detectarlo.
  4. Con esa misma sentencia, presioná Corregir el checksum: se reescribe el 47 por el 46 y pasa a ser válida, aunque la posición sea otra. Un checksum correcto no significa que los datos sean los originales: solo que son consistentes consigo mismos.
  5. Probá GGA sin señal: el checksum es válido, pero la calidad vale 0 y los campos de posición están vacíos. El analizador lo señala; un programa debe comprobarlo antes de usar la latitud.
  6. Editá la sentencia a mano: cambiá un carácter del cuerpo y mirá cómo cambia el checksum calculado; borrá el asterisco y verás que no es una sentencia NMEA.
  7. En la segunda herramienta, con los valores iniciales (115 200 baudios, receptor de 2000 bytes/s, buffer de 64, RTS/CTS al 75 % y 2 bytes en vuelo) no se pierde ningún byte; el máximo es 50 y la transferencia tarda 1 s, el tiempo que necesita el receptor.
  8. Cambiá a Ninguno: el emisor termina en 0,2 s pero el receptor solo alcanza a guardar 411 bytes; se pierden 1589 (el 79 %). El control de flujo cambió velocidad por integridad.
  9. Volvé a RTS/CTS y subí los bytes en vuelo a 32: el umbral del 75 % (48 bytes) más 32 en vuelo supera los 64 del buffer y se pierden 280 bytes. Bajá el umbral a 50 % y vuelve a funcionar.
Desafío: calcular el umbral máximo

Con un buffer de 128 bytes, 16 bytes en vuelo y la regla umbral + bytes en vuelo ≤ buffer, ¿cuál es el umbral alto máximo? 128 − 16 = 112 bytes, es decir, el 87,5 %. En la práctica se deja margen para la reacción del programa, y por eso se usa el 75 %. Con un FIFO de 64 bytes en el emisor y un buffer de 64 no hay umbral posible: el control de flujo no alcanza si el emisor ya tiene en cola más de lo que cabe en el receptor. Verificá el primer caso eligiendo 256 bytes de buffer y 32 en vuelo.

10 · Método de trabajo

Cómo integrar un módulo serie

  1. Leé la hoja de datos del módulo: velocidad inicial, formato, terminador de línea, niveles de tensión y lista de comandos o sentencias.
  2. Probá a mano antes de programar. Conectá el módulo a la computadora con un adaptador USB–serie y hablale con un terminal. Si no entendés la conversación a mano, el código no te va a salvar.
  3. Usá el terminador exacto al enviar y tolerá cualquiera al recibir.
  4. Esperá siempre con un tiempo límite. Cada comando tiene su plazo; un módulo colgado no puede congelar el programa.
  5. Descartá lo viejo antes de enviar un comando (restos del eco, avisos anteriores) y ordená el manejo de los avisos espontáneos.
  6. Validá antes de usar: checksum, largo, inicio de línea y rangos de los valores.
  7. Separá campos conservando los vacíos y comprobá que cada uno exista antes de convertirlo.
  8. Dimensioná buffers y flujo. A 9600 baudios un GPS genera unos 500 bytes por segundo; con velocidades altas o módulos que envían ráfagas (Wi-Fi, celular), usá buffers grandes o RTS/CTS.
  9. Registrá los fallos: contadores de sentencias descartadas, de comandos sin respuesta y de buffers desbordados, para poder diagnosticar el enlace en campo.

Una última idea: un protocolo de texto es ideal para empezar, y el mismo diseño (línea, campos, checksum, comando y respuesta) se traslada a un protocolo propio. Eso es lo que haremos en el tema 41.

11 · Comprensión

Confusiones frecuentes

«El módulo no responde a los comandos AT, así que está roto»

Lo más frecuente es un terminador de línea equivocado (falta CR o LF), una velocidad distinta o TX y RX sin cruzar. Probalo con un terminal que permita elegir el fin de línea, y empezá por 9600 y 115 200 baudios.

«Si el checksum coincide, los datos son correctos»

Significa que son consistentes con su checksum: puede pasar una alteración que se compense en el XOR, o un emisor defectuoso. Un receptor prudente también verifica rangos y coherencia (por ejemplo, la calidad de posición y los satélites).

«4807.038 es la latitud en grados»

Son 48 grados y 7,038 minutos. Hay que dividir los minutos por 60: 48 + 7,038 / 60 = 48,1173°. Si se usa el número tal cual, la posición queda a decenas de kilómetros de la real.

«strtok sirve para separar cualquier línea con comas»

Salta los campos vacíos y corrompe los índices de formatos como NMEA, donde la posición de cada campo es parte del significado. Además modifica la cadena y no es reentrante. Es mejor separar a mano.

«El control de flujo evita todas las pérdidas»

Solo si se respeta el margen: umbral alto más bytes en vuelo no puede superar el tamaño del buffer. Con un umbral demasiado alto o un emisor con una FIFO grande, el flujo se detiene demasiado tarde y los datos igual se pierden.

«XON/XOFF sirve para cualquier dato»

Reserva dos códigos de control, así que no sirve para binarios, donde 0x11 y 0x13 aparecen como datos. Para transferencias binarias hace falta RTS/CTS o un protocolo propio con confirmaciones.

12 · Práctica guiada

Predecí antes de comprobar

1. Costo de un comando AT

Enviás AT+CSQ con terminador CR LF a 9600 baudios en 8N1. ¿Cuántos bytes son y cuánto tarda en salir? ¿Y la respuesta \r\n+CSQ: 18,0\r\n\r\nOK\r\n?

Ver solución

El comando tiene 6 caracteres más 2 del terminador: 8 bytes → 8 × 10 / 9600 ≈ 8,3 ms. La respuesta: 2 + 10 («+CSQ: 18,0») + 2 + 2 + 2 + 2 = 20 bytes → ≈ 20,8 ms. Entre ambas, el módulo tarda además su propio tiempo de procesamiento.

2. Calidad de señal

Un módulo responde +CSQ: 5,0, otro +CSQ: 25,0 y un tercero +CSQ: 99,99. Pasá cada valor a dBm.

Ver solución

dBm = −113 + 2 × n. Con 5: −103 dBm (señal débil). Con 25: −63 dBm (buena). El 99 significa desconocido: no hay valor de señal disponible y no debe convertirse con la fórmula.

3. Checksum a mano

Calculá el checksum NMEA de la sentencia de cuerpo ABC, es decir $ABC*??. Los códigos ASCII son A = 0x41, B = 0x42 y C = 0x43.

Ver solución

0x41 XOR 0x42 = 0x03; 0x03 XOR 0x43 = 0x40. La sentencia completa es $ABC*40. Cambiando cualquier letra el resultado cambia, y por eso se detecta la alteración de un carácter.

4. Coordenadas

Una sentencia trae la latitud 3748.000,S y la longitud 14457.000,E. Pasalas a grados decimales.

Ver solución

Latitud: 37 + 48,000 / 60 = 37,8°, y como es Sur, −37,8°. Longitud: 144 + 57,000 / 60 = 144,95°, Este: +144,95°. (La longitud usa tres dígitos de grados, 144, y la latitud solo dos.)

5. Campos vacíos

La sentencia sin señal $GPGGA,123519,,,,,0,00,99.99,,,,,,*45 se separa por comas. ¿Cuántos campos hay en realidad y cuántos devolvería strtok? ¿Qué problema trae?

Ver solución

Hay 15 campos (14 comas), la mayoría vacíos. strtok devuelve solo 5 (GPGGA, 123519, 0, 00 y 99.99) porque trata las comas seguidas como un solo separador. El programa que lee la calidad de señal en la posición 6 encontraría algo equivocado, y cualquier campo posterior (como la altitud, en la posición 9) estaría desplazado. Por eso se separa a mano.

6. Umbral de control de flujo

Un receptor tiene un buffer de 128 bytes, necesita 3 bytes de reacción y el emisor tiene una FIFO de 16 bytes ya cargada. ¿Cuál es el umbral alto máximo para no perder datos? ¿Qué umbral práctico elegirías?

Ver solución

Bytes en vuelo = 3 + 16 = 19 (más el byte que se está transmitiendo: 20). Umbral máximo ≈ 128 − 20 = 108 bytes. En la práctica se elige un valor con más margen, como el 75 % (96 bytes), y se reactiva RTS al 25 % (32 bytes) para tener histéresis.

13 · Síntesis

Ideas para recordar

  • Los protocolos de texto usan una línea por mensaje, campos separados y un terminador; se prueban a mano y se depuran a simple vista, a cambio de más bytes.
  • El terminador (CR, LF o CR LF) es una causa frecuente de fallas: enviá el que pide el dispositivo y tolerá cualquiera al recibir.
  • Los comandos AT tienen las formas AT+CMD, ?, =valor y =?; la respuesta termina en OK o ERROR, y hay que usar un tiempo límite por comando y descartar el eco y los avisos espontáneos.
  • Una sentencia NMEA es $tipo,campos*checksum; el checksum es el XOR de todo lo que hay entre el $ y el *, y las sentencias con checksum incorrecto se descartan.
  • Las coordenadas NMEA son ddmm.mmmm: grados enteros más minutos; se convierten con grados = dd + mm.mmmm / 60, y Sur y Oeste son negativos.
  • Al separar campos hay que conservar los vacíos (nunca strtok) y comprobar que existan antes de convertirlos; además se verifican los rangos.
  • El control de flujo hace que el receptor detenga al emisor: RTS/CTS por hardware (rápido, sirve para binarios) o XON/XOFF por software (sin cables, solo texto).
  • Con RTS/CTS debe cumplirse umbral alto + bytes en vuelo ≤ tamaño del buffer; el umbral bajo da histéresis.

¿Listo para el tema 11?

Antes de avanzar, calculá a mano el checksum de una sentencia de tu invención, convertí la longitud 01131.000,E a grados decimales y explicá por qué un umbral de RTS demasiado alto puede perder datos. Si podés justificar las tres respuestas, manejás los protocolos de texto sobre UART.

En el próximo tema armamos un taller con dos placas conversando por UART: un protocolo de comandos y respuestas, tiempos de espera y comunicación entre microcontroladores.