01 · Punto de partida
Dos ritmos que no coinciden
La UART trabaja al ritmo de la línea: a 115 200 baudios entrega un byte cada 86,8 µs, aunque tu programa esté haciendo otra cosa. El programa, por su parte, trabaja al ritmo de su loop(): lee sensores, actualiza una pantalla, espera 100 ms… y vuelve a mirar el puerto cuando le toca. En el tema 8 vimos qué pasa cuando esos dos ritmos chocan: el hardware solo retiene un par de bytes y, si el programa no los saca a tiempo, se produce un desbordamiento y se pierden datos.
Es el problema clásico del productor y el consumidor. El productor (la línea, a través de la UART) genera datos a una velocidad que no controlás; el consumidor (tu código) los procesa cuando puede. Para desacoplarlos se usa una cola intermedia: un buffer. La UART, apoyada en una interrupción, deposita cada byte en el buffer en cuanto llega; el programa los retira cuando le conviene. Mientras el buffer no se llene, no se pierde nada.
02 · Tres formas de atender el puerto
Consulta, interrupciones y DMA
| Estrategia | Cómo funciona | Ventajas | Problemas |
|---|---|---|---|
| Consulta (polling) | El programa revisa la bandera «RX completo» cada tanto y lee el dato | Simple, sin interrupciones | Si se demora más que unos bytes, hay desbordamiento; ocupa la CPU en revisar |
| Interrupción por byte | Cada byte recibido dispara una rutina que lo guarda en un buffer | No se pierden datos mientras haya lugar; el programa lee cuando quiere | Una interrupción por byte (a 1 Mbaud, una cada 10 µs); hay que escribirla con cuidado |
| FIFO de hardware | La UART acumula varios bytes (por ejemplo 128 en el ESP32) antes de avisar | Menos interrupciones; tolera retrasos mayores | La FIFO también es finita; sigue necesitando un buffer de software |
| DMA | Un controlador de memoria copia los bytes del periférico a un buffer sin intervención de la CPU | Altas velocidades con casi cero carga de CPU | Más configuración; hay que saber cuándo termina un mensaje de longitud variable (por ejemplo, con la interrupción de línea inactiva) |
En Arduino y en la mayoría de las plataformas, la combinación estándar es interrupción más buffer circular: el objeto Serial ya lo trae resuelto. Entender cómo está hecho permite dimensionarlo, evitar sus trampas y, si hace falta, construir uno propio.
03 · La estructura central
El buffer circular
Un buffer circular (ring buffer) es un arreglo de tamaño fijo N usado como una cola: los datos entran por un extremo y salen por el otro, en el mismo orden. Se manejan con dos índices:
- head (cabeza): posición donde se escribirá el próximo byte. Solo lo modifica quien produce (la interrupción).
- tail (cola): posición del byte más antiguo, el próximo a leer. Solo lo modifica quien consume (el programa).
Cuando un índice llega al final del arreglo, vuelve a cero: de ahí lo de «circular». Así el espacio se reutiliza sin mover datos.
Las reglas son sencillas:
- Vacío:
head == tail. - Lleno: el siguiente de
headseríatail, es decir,(head + 1) mod N == tail. Se sacrifica una posición, así que un buffer de N posiciones guarda como máximo N − 1 bytes (por eso el buffer de 64 de Arduino contiene 63). - Escribir: si no está lleno,
buf[head] = dato; head = (head + 1) mod N. Si está lleno, el dato nuevo se pierde (o se pisa el más viejo, según el diseño). - Leer: si no está vacío,
dato = buf[tail]; tail = (tail + 1) mod N.
Con N potencia de dos (16, 64, 256), el «mod N» se reduce a una máscara de bits (& (N − 1)), que es mucho más rápida en un microcontrolador chico. Y como cada índice lo cambia un solo actor, la estructura funciona sin bloqueos entre la interrupción y el programa, siempre que los índices sean de un byte (en un AVR, la lectura y escritura de un byte es atómica).
Cuánto tiene que medir
El tamaño del buffer lo fija el peor retardo con que el programa puede volver a leer. Durante ese tiempo llegan bytes sin parar:
A 115 200 baudios en 8N1 llegan 11 520 bytes por segundo. Si el programa puede tardar 20 ms en volver a leer (por ejemplo, mientras refresca una pantalla), llegan 230 bytes: un buffer de 64 se desborda y hace falta uno de 256. A 9600 baudios, con la misma pausa, llegan 960 bytes por segundo × 0,02 ≈ 19 bytes y alcanza un buffer de 32. La consecuencia práctica: la velocidad y el tiempo de bloqueo se compran uno con el otro. Los buffers por defecto son 64 bytes en un Arduino Uno y 256 en un ESP32 (ampliable con Serial.setRxBufferSize() antes de begin).
04 · El otro sentido
Transmitir con buffer
La transmisión funciona como la recepción, al revés. Cuando llamás a Serial.print, la biblioteca no espera a que los bytes salgan por el cable: los copia a un buffer de transmisión y vuelve enseguida. Una interrupción («registro de datos vacío») saca de ese buffer un byte cada vez que la UART está libre. Así, un println de 20 bytes a 9600 baudios, que en la línea dura 21 ms, devuelve el control en unos microsegundos.
Pero el buffer es finito. Si el programa escribe más bytes de los que caben, write se queda esperando a que la UART libere lugar, y el loop() queda frenado al ritmo del puerto. Esto es lo que ocurre al imprimir mucho texto a baja velocidad. Una cuenta aproximada:
Para 100 bytes con un buffer de 64 a 9600 baudios (1,04 ms por byte), el programa queda bloqueado cerca de 36 × 1,04 ≈ 37,5 ms; a 115 200 baudios, 3,1 ms. Una lectura de sensores que necesita tiempos precisos puede arruinarse por una impresión de depuración. Por eso conviene:
- Imprimir poco y breve dentro de los lazos críticos, o subir la velocidad.
- Preguntar antes:
Serial.availableForWrite()devuelve cuántos bytes caben sin bloquear. - Usar
Serial.flush()solo cuando haga falta (por ejemplo, para soltar un bus RS-485): espera hasta que sale el último bit.
El mismo principio vale a la inversa: la salida más rápida que el enlace puede sostener es la velocidad de la línea. Un programa que genera datos más rápido que eso termina siempre limitado por el puerto o pierde datos, según cómo se diseñe.
05 · Sin quedarse esperando
Leer sin quedarse esperando
Con el buffer, la lectura se vuelve una pregunta rápida: «¿hay algo?». Las funciones básicas de Serial no bloquean:
Serial.available(): cuántos bytes hay en el buffer de recepción.Serial.read(): toma un byte (o devuelve −1 si no hay).Serial.peek(): mira el próximo byte sin sacarlo.
En cambio, un grupo de funciones muy usadas sí bloquean, porque esperan hasta recibir lo que piden o hasta que vence un tiempo límite (1000 ms por omisión, ajustable con Serial.setTimeout()):
| Función | Qué espera | Riesgo |
|---|---|---|
readStringUntil('\n') | Un fin de línea o el tiempo límite | Hasta 1 s detenido si la línea no llega completa; además usa String (memoria dinámica) |
readBytes(buf, n) y readBytesUntil | Que lleguen n bytes o un carácter terminador | Bloquea si el mensaje es más corto de lo esperado |
parseInt() y parseFloat() | Que aparezca y termine un número | Bloquea hasta el tiempo límite si llegan menos caracteres |
find() y findUntil() | Una cadena concreta en el flujo | Idem |
Para un programa que debe seguir atendiendo otras tareas, el patrón correcto es acumular carácter a carácter: en cada vuelta del loop() se retiran del buffer los bytes que haya, se arma el mensaje en un arreglo fijo y solo cuando aparece el terminador se procesa. Es una pequeña máquina de estados; no se detiene nunca. Un ejemplo completo está en la sección «En el microcontrolador».
06 · Texto o binario
Dar formato a los datos
Los bytes del buffer no significan nada por sí mismos: el programa debe decidir cómo representar los valores. Hay dos grandes opciones.
Texto
Los números viajan como caracteres: «25.4», «LED=1». Se puede leer en cualquier monitor serie y depurar a ojo, y los terminadores de línea delimitan el mensaje. A cambio, ocupa más bytes y exige convertir (imprimir y analizar) en cada extremo.
Binario
Los valores viajan en su representación nativa: un entero de 16 bits ocupa dos bytes, un float, cuatro. Es compacto y rápido de procesar, pero ilegible en un monitor y exige acordar el orden de los bytes y la estructura exacta (tema 46).
| Representación | Bytes | Tiempo en la línea | Comentario |
|---|---|---|---|
Texto con fin de línea: 25.4\r\n | 6 | 6,25 ms | Legible; el receptor busca el \n |
| Entero escalado ×10: 254 en 16 bits | 2 | 2,08 ms | Compacto; hay que acordar la escala (décimas) y el orden de bytes |
float de 32 bits | 4 | 4,17 ms | Conserva decimales sin escala; ambos extremos deben usar el mismo formato de coma flotante |
La regla general: texto mientras estás desarrollando y para comandos de baja frecuencia; binario cuando importan el ancho de banda y la velocidad de procesamiento. Además hay trampas propias de los microcontroladores pequeños:
snprintfen un AVR no imprime números con coma (%fdevuelve un signo de interrogación). Se usadtostrf(valor, ancho, decimales, destino)o se envía el entero escalado.- La clase
Stringreserva y libera memoria dinámica, y en 2 KB de RAM puede fragmentarla hasta que el programa falle de forma misteriosa. Para mensajes se prefieren arregloscharde tamaño fijo. Serial.print(65)envía «65»;Serial.write(65), el byte 0x41. Mezclarlos produce mensajes ilegibles.- Enviar una estructura entera con
Serial.write((uint8_t*)&datos, sizeof(datos))es cómodo, pero depende del alineamiento y del orden de bytes de cada plataforma. Es una fuente típica de errores al comunicar un AVR con un ESP32.
07 · Interrupciones
Reglas de una rutina de interrupción
La rutina de servicio (ISR) de la UART se ejecuta cuando llega un byte, interrumpiendo lo que el programa esté haciendo. Para que el sistema funcione hay que respetar unas pocas reglas:
- Que sea corta. Mientras se ejecuta, otras interrupciones pueden quedar en espera. A 115 200 baudios, la ISR de recepción debe terminar mucho antes de los 86,8 µs entre bytes. Lo mínimo: leer el dato, guardarlo y salir.
- Leer siempre el registro de datos. Si la ISR no lee el byte, la bandera de recepción sigue activa y la interrupción se dispara sin parar. Por eso el ejemplo lo lee antes de comprobar si hay lugar.
- Marcar como
volatiletoda variable que se comparte con el programa. Sin esa palabra, el compilador puede conservar el valor en un registro y el programa nunca vería el cambio hecho por la interrupción. - No llamar a funciones lentas ni bloqueantes: ni
delay, niSerial.print, ni asignación de memoria. Unprintdentro de una ISR, con el buffer de transmisión lleno, espera a una interrupción que no puede ejecutarse porque está dentro de otra: se cuelga el programa. - Cuidado con los datos de más de un byte. En un AVR de 8 bits, leer un
intcompartido son dos instrucciones; si la interrupción lo cambia entre ambas, el valor queda mezclado. El programa debe desactivar las interrupciones un instante (noInterrupts()) para copiarlo. - Avisar, no trabajar. Lo habitual es que la ISR levante una bandera o encole el dato, y que el procesamiento real lo haga el
loop().
08 · Con el microcontrolador
Un buffer propio y un lector de líneas
El primer programa implementa la recepción completa en un ATmega328P, sin la biblioteca Serial: una rutina de interrupción que llena un buffer circular de 64 bytes, y dos funciones para consultarlo. Lleva además un contador de bytes perdidos, para saber si el buffer alcanzó alguna vez.
#define TAM 64 // potencia de dos: el módulo se hace con una máscara
volatile uint8_t buf[TAM];
volatile uint8_t head = 0; // lo modifica solo la ISR
volatile uint8_t tail = 0; // lo modifica solo el programa
volatile uint8_t perdidos = 0; // bytes descartados por buffer lleno
ISR(USART_RX_vect) {
uint8_t dato = UDR0; // SIEMPRE leer: así se limpia la bandera de la interrupción
uint8_t siguiente = (head + 1) & (TAM - 1);
if (siguiente == tail) {
perdidos++; // lleno: este byte se pierde
} else {
buf[head] = dato;
head = siguiente;
}
}
uint8_t disponibles() { return (head - tail) & (TAM - 1); }
int leerByte() { // -1 si no hay datos, igual que Serial.read()
if (head == tail) return -1;
uint8_t dato = buf[tail];
tail = (tail + 1) & (TAM - 1);
return dato;
}
void uartInicio() {
UBRR0 = 103; // 9600 baudios con 16 MHz
UCSR0B = (1 << RXEN0) | (1 << TXEN0) | (1 << RXCIE0); // receptor, transmisor e interrupción de recepción
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8N1
sei(); // habilitar interrupciones globales
}
El segundo programa usa la biblioteca Serial y lee líneas de comandos sin bloquear nunca. Retira del buffer los bytes que haya, arma la línea en un arreglo de tamaño fijo y, cuando aparece el fin de línea, interpreta el comando. El resto del programa sigue corriendo en cada vuelta:
const byte MAX_LINEA = 32;
char linea[MAX_LINEA];
byte largo = 0;
bool leerLinea() { // true cuando hay una línea completa en 'linea'
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\r') continue; // ignorar el retorno de carro
if (c == '\n') {
linea[largo] = '\0'; // terminar la cadena
largo = 0;
return true;
}
if (largo < MAX_LINEA - 1) linea[largo++] = c; // si es demasiado larga, se descarta el exceso
}
return false;
}
void procesar(const char *texto) {
if (strncmp(texto, "T=", 2) == 0) {
float t = atof(texto + 2); // "T=25.4" -> 25.4
Serial.print("Temperatura recibida: ");
Serial.println(t, 1);
} else if (strcmp(texto, "LED=1") == 0) {
digitalWrite(13, HIGH);
} else if (strcmp(texto, "LED=0") == 0) {
digitalWrite(13, LOW);
} else {
Serial.println("Comando desconocido");
}
}
void setup() {
Serial.begin(115200);
pinMode(13, OUTPUT);
}
void loop() {
if (leerLinea()) procesar(linea); // atiende comandos cuando llegan…
// …y el resto del programa sigue corriendo sin esperar (sensores, pantalla, control)
}
Observá lo que evita este diseño: no hay readStringUntil con su espera de hasta un segundo, no hay String que fragmente la memoria y una línea mal formada, o demasiado larga, no desordena el programa: se corta el exceso y se sigue. Para un protocolo real, el procesamiento agrega validación y límites (tema 41).
09 · Analogía con programación
Simular el buffer y sus pérdidas
Antes de elegir el tamaño de un buffer, conviene simularlo. El modelo de Python reproduce el problema del productor y el consumidor: llega un byte cada bits / baud segundos y el programa vacía el buffer cada periodo_ms milisegundos. Si el buffer está lleno cuando llega un byte, se pierde. La función devuelve cuántos bytes llegaron, cuántos se perdieron y la ocupación máxima alcanzada.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
import math
def simular(baud, tam, periodo_ms, bits=10, segundos=1.0):
"""Productor (la línea) y consumidor (el programa, que vacía el buffer cada 'periodo_ms')."""
llegados = perdidos = ocupados = maximo = 0
k = 1 # número de la próxima lectura del programa
i = 1 # número del byte que llega
while i * bits / baud <= segundos:
t = i * bits / baud
while k * periodo_ms / 1000 <= t: # el programa vacía el buffer
ocupados = 0
k += 1
llegados += 1
if ocupados < tam:
ocupados += 1
maximo = max(maximo, ocupados)
else:
perdidos += 1 # buffer lleno: el byte se pierde
i += 1
return llegados, perdidos, maximo
def tamano_minimo(baud, periodo_ms, bits=10):
return math.ceil(baud / bits * periodo_ms / 1000) + 1
print("115 200 baudios, 8N1: bytes perdidos en 1 segundo")
print("periodo mínimo | 2 bytes (solo hardware) 64 bytes 256 bytes")
for periodo in (2, 5, 10, 20):
celdas = []
for tam in (2, 64, 256):
_, perdidos, _ = simular(115200, tam, periodo)
celdas.append(f"{perdidos:6d}")
print(f"{periodo:4d} ms {tamano_minimo(115200, periodo):5d} | " + " ".join(celdas))
llegados, perdidos, maximo = simular(9600, 64, 100)
print()
print(f"9600 baudios, 64 bytes, lectura cada 100 ms: llegan {llegados}, se pierden {perdidos}, máximo {maximo}")
print("tamaño mínimo:", tamano_minimo(9600, 100))
Resultado: a 115 200 baudios, con solo los 2 bytes del hardware, el programa pierde la inmensa mayoría de los datos (10 519 de 11 520 con lecturas cada 2 ms). Con 64 bytes no se pierde nada con lecturas cada 2 ms y cada 5 ms (la ocupación máxima es 24 y 58), pero a 10 ms ya se pierden 5119 bytes por segundo; con 256 bytes se aguanta hasta 20 ms (ocupación máxima 231). A 9600 baudios, con 64 bytes y lecturas cada 100 ms, llegan 960 bytes y se pierden 319 porque el mínimo sería 97. Probá otros valores y comprobá que el tamaño mínimo calculado siempre alcanza.
El modelo supone que el programa vacía todo el buffer en cada lectura y que los bytes llegan a ritmo constante. En un sistema real, el retardo máximo puede ser mayor que el promedio, y es el máximo el que dimensiona el buffer.
10 · Exploración interactiva
Buffers y bloqueos
El laboratorio tiene dos herramientas. La primera simula un segundo de recepción continua: elegís la velocidad, el tamaño del buffer y cada cuánto vuelve a leer el programa, y ves cuántos bytes llegan, cuántos se pierden y qué ocupación máxima alcanzó el buffer. La segunda calcula cuánto bloquea una impresión larga el programa.
1. Recepción continua durante un segundo
Elegí velocidad, buffer y periodo de lectura.
2. ¿Cuánto bloquea una impresión?
Cambiá la cantidad de bytes o el buffer.
La primera herramienta usa el mismo modelo que el programa de Python (llegada a ritmo constante y vaciado completo del buffer en cada lectura). La barra muestra la ocupación máxima frente al tamaño del buffer; se pone en rojo cuando hay pérdidas. La segunda es una aproximación: bloqueo ≈ (bytes − buffer) × tiempo por byte.
Recorrido guiado
- Con los valores iniciales (115 200 baudios, 64 bytes, lectura cada 5 ms) llegan 11 520 bytes por segundo, la ocupación máxima es 58 y no se pierde nada. El mínimo recomendado es 59: el buffer alcanza con poco margen.
- Cambiá la lectura a 10 ms: el mínimo sube a 117 y el buffer de 64 se desborda; se pierden 5119 bytes. Con un lector más lento, el mismo buffer ya no sirve.
- Subí el buffer a 256 bytes: sin pérdidas, con ocupación máxima 116. Con lectura cada 20 ms todavía no se pierde nada, pero la ocupación llega a 231 de 256 (veredicto «justo»); a 50 ms vuelven las pérdidas.
- Elegí el buffer de 2 bytes (solo el hardware del AVR) y lectura cada 2 ms: se pierden 10 519 bytes, el 91 % de lo que llega. Es el desbordamiento del tema 8.
- Bajá la velocidad a 9600 baudios con buffer de 64 y lectura cada 50 ms: llegan 960 bytes y la ocupación máxima es 48, sin pérdidas. A baja velocidad, el mismo buffer tolera retardos mucho mayores. Con 100 ms ya se pierden 319.
- Probá 230 400 baudios con lectura cada 5 ms: hay que subir el buffer hasta 128 bytes o más para no perder.
- En la segunda herramienta, con 100 bytes, buffer de 64 y 9600 baudios, el programa queda bloqueado unos 37,5 ms. A 115 200 baudios, 3,1 ms. Con 60 bytes no se bloquea en absoluto.
- Con 1000 bytes y buffer de 256 a 115 200: el bloqueo es de unos 64 ms. Imprimir mucho siempre termina limitado por el ritmo del puerto.
Desafío: dimensionar para una pantalla lenta
Tu programa refresca una pantalla que tarda 40 ms en cada actualización, durante los cuales no lee el puerto. Los datos llegan a 57 600 baudios. ¿Qué tamaño de buffer necesitás? A 57 600 baudios llegan 5760 bytes por segundo; en 40 ms llegan 230 bytes (230,4 exactamente), así que hace falta un buffer de al menos 232: en la práctica, 256. Verificalo en la herramienta 1 con 57 600 baudios: leyendo cada 20 ms la ocupación máxima es 116 y con 50 ms sube a 288, que ya no cabe en 256. Otra salida: partir la actualización de la pantalla en trozos de pocos milisegundos para volver a leer más seguido.
11 · Método de trabajo
Cómo diseñar la capa serie
- Calculá el flujo. Bytes por segundo en cada sentido y si llegan en ráfagas (con la fórmula del tema 4).
- Medí o estimá el peor retardo del programa entre lecturas: pantallas, escrituras en memoria, esperas, otras comunicaciones.
- Dimensioná el buffer de recepción con la fórmula: bytes por segundo × retardo máximo + 1, más un margen, y redondeá a una potencia de dos.
- Evitá el bloqueo en la transmisión. Verificá con
availableForWrite()o limitá el largo de los mensajes; mové las impresiones largas fuera de los lazos críticos. - No uses funciones bloqueantes (
readStringUntil,parseInt) cuando el programa tenga otras tareas; acumulá los caracteres en un arreglo fijo. - Definí terminadores y límites de mensaje, y qué hacer con una línea demasiado larga o incompleta.
- Contá los errores. Mantené contadores de bytes perdidos, de errores de trama y de líneas descartadas: son la mejor señal de que algo se degradó.
- Elegí texto o binario según el ancho de banda y la facilidad de depuración, y documentá el formato exacto.
Cuando el flujo supera lo que un buffer de interrupciones puede sostener (altas velocidades o ráfagas largas), las soluciones son el control de flujo (RTS/CTS y XON/XOFF, tema 10), una FIFO de hardware mayor o el DMA con detección de línea inactiva para delimitar mensajes de longitud variable.
12 · Comprensión
Confusiones frecuentes
«Serial.print espera a que el mensaje salga por el cable»
Solo copia los bytes al buffer de transmisión y vuelve. Espera únicamente si el buffer se llena. Para esperar a que salga el último bit hay que usar Serial.flush().
«Con una velocidad alta, un buffer pequeño alcanza porque los datos pasan rápido»
Es al revés: a mayor velocidad llegan más bytes por unidad de tiempo, y el buffer debe cubrir el mismo retardo del programa con más datos. A 115 200 baudios, 20 ms de pausa acumulan 230 bytes.
«Un buffer más grande resuelve cualquier pérdida»
Un buffer mayor tolera retardos más largos, pero si el flujo promedio es mayor que lo que el programa procesa, termina llenándose igual. Entonces hace falta más velocidad de procesamiento, menos datos o control de flujo.
«Puedo imprimir dentro de una interrupción para depurar»
Es peligroso: Serial.print puede esperar a que se libere el buffer de transmisión, y eso depende de una interrupción que no se ejecuta mientras se está dentro de otra. El programa se cuelga. Dentro de la ISR se levanta una bandera, y se imprime fuera de ella.
«readStringUntil es lo más cómodo, así que es lo mejor»
Es cómodo pero bloquea hasta un segundo si el mensaje no llega completo, y usa String, que fragmenta la memoria en los microcontroladores chicos. Un lector con arreglo fijo es apenas más largo y no detiene el programa.
«Enviar la estructura entera en binario funciona igual en todas las placas»
No necesariamente: el tamaño de los tipos, el alineamiento de los campos y el orden de los bytes cambian entre un AVR y un ESP32. Los mensajes binarios deben definirse campo por campo, con un tamaño y un orden explícitos.
13 · Práctica guiada
Predecí antes de comprobar
1. Dimensionar un buffer
Un programa recibe datos a 115 200 baudios en 8N1 y puede tardar hasta 15 ms en volver a leer el puerto. ¿Cuántos bytes llegan en ese lapso y qué tamaño de buffer conviene?
Ver solución
11 520 bytes por segundo × 0,015 s = 172,8, es decir 173 bytes; el mínimo es 174 (173 + 1). El valor práctico es 256, la potencia de dos siguiente, que además deja margen. Un buffer de 128 se desbordaría.
2. Índices del buffer circular
Un buffer circular de 8 posiciones tiene head = 6 y tail = 2. ¿Cuántos bytes hay guardados y cuántos lugares libres? ¿Qué pasa si llegan 4 bytes más sin que el programa lea?
Ver solución
Ocupados = (6 − 2) mod 8 = 4. Libres = 8 − 1 − 4 = 3. Los tres primeros bytes nuevos se guardan en las posiciones 6, 7 y 0, y head pasa a 1. Ahora (head + 1) mod 8 = 2 = tail: el buffer está lleno, y el cuarto byte se pierde.
3. Una impresión larga
El programa ejecuta Serial.println con un texto de 100 bytes a 9600 baudios. El buffer de transmisión es de 64 bytes y está vacío. ¿Cuánto tiempo queda bloqueado?
Ver solución
Caben unos 64 bytes; los 36 restantes tienen que esperar a que la UART los libere. Cada byte tarda 10 / 9600 ≈ 1,04 ms, así que el bloqueo es de ≈ 36 × 1,04 ≈ 37,5 ms. A 115 200 baudios sería de 3,1 ms. Todo el mensaje tarda en la línea 100 × 1,04 ≈ 104 ms.
4. Texto o binario
Un sensor envía 50 lecturas por segundo, cada una un entero de 0 a 1023. Compará el tráfico enviándolas como texto con fin de línea (hasta 4 dígitos más CR y LF) y como entero de 2 bytes. ¿Cuál es la utilización a 9600 baudios en 8N1?
Ver solución
Texto: hasta 6 bytes por lectura → 50 × 6 × 10 = 3000 bps → 31,3 %. Binario: 2 bytes → 50 × 2 × 10 = 1000 bps → 10,4 %. El binario usa un tercio del ancho de banda, a costa de perder la legibilidad.
5. Un lector que se cuelga
Un programa lee comandos con Serial.readStringUntil('\n') y a la vez controla un motor que necesita una actualización cada 10 ms. Alguien envía un comando sin fin de línea. ¿Qué pasa y cómo se arregla?
Ver solución
La función espera hasta el tiempo límite (1000 ms por omisión) antes de rendirse: el motor queda sin actualizar durante un segundo. Se arregla con un lector que acumule caracteres en un arreglo fijo y solo procese cuando aparece el fin de línea (como leerLinea()), sin esperar nunca, o al menos bajando el tiempo límite con Serial.setTimeout().
6. Qué está mal en esta interrupción
Una ISR de recepción hace contador++ (con int contador, sin volatile), llama a Serial.println("llegó un byte") y no lee el registro de datos. Nombrá tres problemas.
Ver solución
(1) No lee el registro de datos: la bandera de recepción sigue activa y la interrupción se dispara sin parar; además se pierden los datos. (2) Llama a Serial.println dentro de la ISR: puede quedar esperando una interrupción de transmisión que no se ejecuta, y se cuelga; además es lenta. (3) contador sin volatile y de 16 bits: el programa puede no ver los cambios, y leer un int en un AVR no es atómico (puede leer un valor mezclado). Se corrige con volatile y noInterrupts() al copiarlo.
14 · Síntesis
Ideas para recordar
- La UART produce bytes a su ritmo y el programa los consume al suyo: un buffer los desacopla.
- La recepción por consulta pierde datos si el programa se demora; la combinación estándar es interrupción por byte más buffer circular, con FIFO de hardware o DMA para velocidades altas.
- El buffer circular usa dos índices (head lo mueve la ISR, tail lo mueve el programa); está vacío si head == tail y lleno si (head + 1) mod N == tail, con capacidad N − 1.
- Tamaño mínimo ≈ (velocidad / 10) × peor retardo sin leer + 1; velocidad alta o pausas largas exigen buffers grandes.
- La transmisión también usa un buffer: print vuelve enseguida, pero bloquea si el mensaje excede el buffer (≈ (bytes − buffer) × tiempo por byte).
- Funciones como readStringUntil, parseInt y readBytes bloquean hasta un segundo; para no detener el programa se acumulan caracteres en un arreglo fijo hasta el terminador.
- Texto es legible y fácil de depurar; binario es compacto pero exige acordar tamaño, orden de bytes y estructura. En un AVR, snprintf no admite %f.
- Una ISR debe ser corta, leer siempre el dato, usar variables volatile, no llamar a funciones bloqueantes y proteger los datos de varios bytes.
¿Listo para el tema 10?
Antes de avanzar, calculá el buffer necesario para recibir a 57 600 baudios con pausas de 30 ms, escribí a mano cómo se avanza head en un buffer de 16 posiciones y explicá por qué Serial.print dentro de una ISR es peligroso. Si podés justificar las tres respuestas, tenés dominada la capa serie del microcontrolador.
En el próximo tema estudiamos los protocolos de texto sobre UART: los comandos AT, las sentencias NMEA de un GPS y el control de flujo por RTS/CTS, que resuelve lo que los buffers no pueden.